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 ;
}
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;
}
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"
Code: Select all
G00 (os=0x00000000) = "$cond"
G01 (os=0x00000006) = "%bla"