DAZZLE'S UPGRADE PACK

If you haven't already done so, upgrade your game by downloading Dazzle's all-in-one upgrade pack. It comes with everything you need for today's servers. Does your blue bar freeze when joining servers? Do you lag in games? Do you get an annoying siren in Phobik's Servers? This is what you need. CLICK HERE TO DOWNLOAD.

ThinkTanks Script Decompiler

Discuss anything.

Moderators: Admin, Moderator

Post Reply
ArtCrazy
Veteran Member
Veteran Member
Posts: 290
Joined: Fri Dec 29, 2006 10:14 am

ThinkTanks Script Decompiler

Post by ArtCrazy »

The DSO format is a very simple bytecode language, surprisingly easy to decode and decompile, especially since we have access to an open-source implementation of the compiler. The hard part was actually figuring out all the changes since ThinkTanks was released, since ThinkTanks uses DSO specification 21, over 10 years old, and a lot has changed since then. Another project called Untorque already exists to decompile the newer versions of DSO files, but it's way too recent to be compatible with ThinkTanks.

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.
User avatar
Cloud
Site Admin
Site Admin
Posts: 921
Joined: Thu Dec 28, 2006 7:38 pm
Location: Utah

Re: ThinkTanks Script Decompiler

Post by Cloud »

Nice work AC.

Man if only this could have happened many years ago. Hopefully those that are interested can play around with this.
//()*()*()*()*--[][][][]-^-^)(CLOUD)(^-^-[][][][]--()*()*()*()\\
User avatar
Dazzle
Evil Site Admin
Evil Site Admin
Posts: 1320
Joined: Fri Dec 29, 2006 6:59 am
Location: London, England
Contact:

Re: ThinkTanks Script Decompiler

Post by Dazzle »

Art,

I have downloaded you tool and will give it a go, like Cloud says shame we did not have this years ago but hey still worthwhile. Hope you are good and thing are going well :thumbup:
Post Reply