That's where I come in. As a way to brush up my reverse engineering and C++ skills, which had fallen to disuse over the last few years, I spent about 10 hours over the last couple of days reverse engineering all implementation differences between ThinkTanks and the latest version of Torque 3D, and wrote a DSO parser and disassembler. Finally, I adapted the aforementioned Untorque, removing all the dependencies on the new Torque 3D engine (basing it instead on my DSO parser/disassembler), and then adapted it to the older ThinkTanks DSO format.
Download a Windows binary here
View the source code
I've tested it on a ton of my scripts, and they all seem to have decompiled correctly. I was also able to decompile tank.cs, for example. However, I cannot guarantee there won't be mistakes or problems with some combinations of DSO opcodes, and I have no idea if the Mac/Linux version of ThinkTanks uses the same DSO specification as Windows (I distinctly remember scripts needing to be recompiled for Mac, the cause is probably that the bytecode specification is slightly different).
Again: There will likely be issues and bugs that I haven't caught! Use at your own risk. (for example, backup all your DSO files first)
For example, here's a decompiled tank.cs.dso using this application (obtained by running "ThinkTanksScriptDecompiler.exe tank.cs.dso --decompile > tank.cs"):
- Code: Select all
exec("./tankFx.cs");
exec("./tankDb.cs");
exec("./tankAI.cs");
function TankData::create(%block)
{
if (%block $= "LightTank")
{
%obj = new Tank("")
{
.dataBlock = %block;
};
return %obj;
}
else
{
if (%block $= "MediumTank")
{
%obj = new Tank("")
{
.dataBlock = %block;
};
return %obj;
}
else
{
if (%block $= "HeavyTank")
{
%obj = new Tank("")
{
.dataBlock = %block;
};
return %obj;
}
}
}
return -1;
return ;
}
function TankData::onAdd(%this, %obj)
{
return ;
}
function TankData::onSPDestroyed(%db, %this, %killer)
{
if (%this.client == 0)
{
$Game::SPBots = $Game::SPBots - 1;
$Game::DeadBots = $Game::DeadBots + 1;
if ((isObject(%killer) && isObject(%killer.client)) && (%killer.client.lives > 0))
{
%pts = 0;
%startScore = strfrap(strswiz($Game::IdleText @ "client", 12), %killer.client.spscore);
if (strstr(%db.getName(), "Bronze") != -1)
{
%pts = $Game::BronzePoints;
%killer.client.bronzeKills = %killer.client.bronzeKills + 1;
}
else
{
if (strstr(%db.getName(), "Silver") != -1)
{
%pts = $Game::SilverPoints;
%killer.client.silverKills = %killer.client.silverKills + 1;
}
else
{
if (strstr(%db.getName(), "Gold") != -1)
{
%pts = $Game::GoldPoints;
%killer.client.goldKills = %killer.client.goldKills + 1;
}
else
{
if (strstr(%db.getName(), "Boss") != -1)
{
%pts = $Game::BossPoints;
%killer.client.bossTankKilled = %killer.client.bossTankKilled + 1;
}
}
}
}
%time = getRealTime();
if ((%time - %killer.client.lastKillTime) < $Game::QuickShotTime)
{
%killer.client.quickKill = %killer.client.quickKill + 1;
%pts = (%pts * 5) * %killer.client.quickKill;
commandToClient(%killer.client, 'BottomPrint', "x" @ 5 * %killer.client.quickKill SPC "Quick-shot bonus!", 2, 2);
alxPlay(SPQuick);
}
else
{
%killer.client.quickKill = 0;
}
%killer.client.lastKillTime = %time;
%newScore = %startScore + %pts;
%killer.client.spscore = strfrip(strswiz($Game::IdleText @ "client", 12), %newScore);
SPScoreGui.setScore(%newScore);
%freeLives = mFloor(%newScore / 10000) - mFloor(%startScore / 10000);
if (%freeLives > 0)
{
alxPlay(SPExtra);
%killer.client.lives = %killer.client.lives + %freeLives;
SPLivesGui.showLives(%killer.client.lives);
}
}
else
{
%newdb = "GoldHeavyTank";
$Game::DeadBots = $Game::DeadBots - 1;
$Game::SPTotalBots = $Game::SPTotalBots - 1;
spawnBotSP(%newdb);
}
}
else
{
%this.client.lives = %this.client.lives - 1;
%this.client.deaths = %this.client.deaths + 1;
SPLivesGui.showLives(%this.client.lives);
if (%this.client.lives > 0)
{
if ($Game::DemoMode)
{
%this.client.player = 0;
%this.client.spawnPlayer();
}
else
{
commandToClient(%this.client, 'CenterPrint', "Your brain has been separated from your tank, press SPACE.", 0, 2);
}
}
else
{
if ($Game::DemoMode)
{
%this.client.player = 0;
%this.client.spawnPlayer();
}
else
{
commandToClient(%this.client, 'CenterPrint', "G A M E O V E R", 0, 2);
%idx = $playerList::lastSelection;
%score = strfrap(strswiz($Game::IdleText @ "client", 12), %this.client.spscore);
if (%score > getSolohiscore(%idx))
{
%swiz = strswiz(%idx @ $playerList::playerName[%idx] @ $Game::IdleText, 12);
$playerList::hiscore[%idx] = strfrip(%swiz, %score) @ ;
if (!isDemo())
{
export("$playerList::*", "~/client/players.cs");
}
}
}
}
}
return ;
}
function TankData::onTargetDestroyed(%db, %this, %killer)
{
%isPlayerKilled = isObject(%this.client);
%isPlayerKiller = isObject(%killer.client);
if (%isPlayerKiller)
{
%killer.incScore(1, 1);
%killer.client.kills = %killer.client.kills + 1;
if ((%killer.client.kills % 10) == 0)
{
spawnTarget(TargetSaucer);
}
}
if (%isPlayerKilled)
{
commandToClient(%client, 'CenterPrint', "Your brain has been separated from your tank, press SPACE.", 0, 1);
}
else
{
if (!(%this.dataBlock.getName() $= "TargetSaucer"))
{
spawnTarget(%this.dataBlock);
}
}
return ;
}
function TankData::onDestroyed(%db, %this, %killer)
{
if ($Game::singlePlayer)
{
%db.onSPDestroyed(%this, %killer);
return ;
}
if ($Game::TargetRange)
{
%db.onTargetDestroyed(%this, %killer);
return ;
}
%client = %this.client;
%client.deaths = %client.deaths + 1;
if (isObject(%killer))
{
messageAll('MsgClientKilled', '%1 has been eliminated by %2!', %client.name, %killer.client.name);
if ($Game::MissionType $= "Deathmatch")
{
if ($Game::TeamGame)
{
if (%killer.client.team.getId() != %client.team.getId())
{
%killer.incScore(1, 1);
}
}
else
{
%killer.incScore(1, 1);
}
}
%killer.client.kills = %killer.client.kills + 1;
}
else
{
messageAll('MsgClientKilled', '%1 has been eliminated!', %client.name);
}
if ($Game::MissionType $= "Deathmatch")
{
if ($Game::TeamGame)
{
if (isObject(%killer) && (%killer.client.team.getId() == %client.team.getId()))
{
%this.incScore(-1, -1);
}
else
{
%this.incScore(-1, 0);
}
}
else
{
%this.incScore(-1, -1);
}
}
if (!isObject(%client.ai))
{
if ($Game::aiControlMode)
{
%client.player = 0;
%client.schedule(4000, "spawnPlayer");
}
else
{
commandToClient(%client, 'CenterPrint', "Your brain has been separated from your tank, press SPACE.", 0, 1);
}
}
else
{
spawnBotPlayer(%client);
}
return ;
}
function Tank::onRemove(%this)
{
return ;
}
function Tank::incScore(%this, %delta, %deltaTeam)
{
%client = %this.client;
if (-%delta > %client.score)
{
%client.cumScore = %client.cumScore - %client.score;
%client.score = 0;
}
else
{
%client.score = %client.score + %delta;
%client.cumScore = %client.cumScore + %delta;
}
if (isObject(%client.team))
{
if (-%deltaTeam > %client.team.score)
{
%client.team.cumScore = %client.team.cumScore - %client.team.score;
%client.team.score = 0;
}
else
{
%client.team.score = %client.team.score + %deltaTeam;
%client.team.cumScore = %client.team.cumScore + %deltaTeam;
}
}
messageAll('MsgClientScoreChanged', "", %client.score, %client.cumScore, %client);
if (isObject(%client.team))
{
messageAll('MsgTeamScoreChanged', "", %client.team.score, %client.team.cumScore, %client.team.getId());
}
return ;
}
Not sure how many people are even going to see this (that's why I'm posting in the general forum instead of the Modding one), but this could open quite a lot of doors for modding. The possibility I'm most interested in, however, is decompiling all .dso files, and porting the game over to a new open-source version of the Engine. It might be much more difficult to do than simply copy+pasting stuff, as there are ThinkTanks-specific engine implementations of stuff (GuiControl profiles, Tank datablocks, script callbacks, and some more things) which would need to be reverse engineered and re-implemented (and it'll be much more difficult than decompiling DSO files - most likely out of my league).
That's all from me for a few months, though, as I'm currently writing my Master's thesis! I suggest someone plays around with this, and sees what they can find. Specifically, I'm curious to know how much works if you decompile all ThinkTanks scripts and delete the DSO files (i.e., if the decompiler is working perfectly!) - if not the case, then there's probably some bugs for some very weird cases. In addition, assuming everything works, I'd be interested in knowing what happens if you decompile everything, and replace the ThinkTanks executable with one taken from the latest Torque 3D version. Most things should not work, but I wouldn't be surprised if the menus worked more or less correctly! (Which would be great news)
-----------------
Note for the technically curious, the DSO format is basically a list of strings, floats, and then Torque bytecode (machine code). It runs by being loaded into a Virtual Machine (with a stack-based architecture) that is part of the torque engine.
For example, this code
- Code: Select all
if ($cond)
{
%bla = 2;
}
is compiled into this list of opcodes (obtained by running "ThinkTanksScriptDecompiler file.cs.dso --disassemble > file.disasm", header and comments added manually):
- Code: Select all
ADDRESS : OPCODE_IN_HEX/OPCODE_IN_DEC HEX_DUMP : DISASSEMBLED VIEW
0x00000000 : 0x24/36 00000024 G00 : OP_SETCURVAR var=$cond // Set $cond as current variable
0x00000002 : 0x29/41 00000029 : OP_LOADVAR_FLT // Load current variable ($cond) as a float value to the stack
0x00000003 : 0x06/06 00000006 0000000B : OP_JMPIFFNOT ip=0x0000000B // Jump to address 0xB if the value on top of the stack ($cond) is 0
0x00000005 : 0x41/65 00000041 00000002 : OP_LOADIMMED_UINT val=2 // Load immediate "2" to the top of the stack
0x00000007 : 0x25/37 00000025 G01 : OP_SETCURVAR_CREATE var=%bla // Set current variable to %bla
0x00000009 : 0x2B/43 0000002B : OP_SAVEVAR_UINT // Save top of the stack ("2") to the current var (%bla)
0x0000000A : 0x40/64 00000040 : OP_UINT_TO_NONE // Pop top of the stack
0x0000001B : 0x0D/13 0000000D : OP_RETURN // Return, also used to indicate "end of file"
"G00" and "G01" in the HEX_DUMP section are simply names the disassembler gives to the string identifiers, and can be listed by using the "--strings" parameter as well:
- Code: Select all
G00 (os=0x00000000) = "$cond"
G01 (os=0x00000006) = "%bla"
All the application does, is to figure out what the combinations of opcodes meant in the original script. While a rather complex process, the opcodes map very closely with the scripts, so almost 1:1 compilation->decompilation is feasible. The fact that Untorque already existed saved me a ton of work on that front.