thug2 · 43aced5Hostile Takeover
The THUG1 port could already play THUG2's levels, one at a time, through a side door. That should have
been enough. It wasn't. If the engine is the same, why not point it at THUG2's entire Data folder and see
what boots? Spoiler: the intro movies. Then about ninety missing functions, a font from the future and a skeleton
counting in threes.
"Same engine, same file extensions, same version numbers. How different can it be?"everyone, right before every port
1A second build, same engine
The THUG1 build had to keep working, so THUG2 became a build flag. build-thug2.sh configures CMake with
-DTHUG_THUG2=ON, writes the executable to Game2/ and clones THUG2's Data next to it (APFS clones,
so a whole second game costs zero bytes). In that mode CMake stops staging THUG1's scripts, languages and extras. Every
line of code that only makes sense for THUG2 goes inside #ifdef THUG_THUG2, so the THUG1 build never notices.
2Stop crashing on things that don't exist
THUG2's scripts call C functions that THUG1 never had: Create-A-Graphic, buddy lists, EyeToy (yes, EyeToy). Normally the script interpreter asserts on an unknown name, and the game stops. Ninety stops in, that gets old. So in THUG2 mode the scripts are lenient: a missing function is reported once and returns false, like a check that failed.
That one printf turned the whole job into a to-do list. Run the game, grep the log for THUG2 MISSING,
implement the top few, repeat. The ones that mattered went into a new thug2funcs.cpp (700 lines in this
commit alone), each checked against what THUG2.exe actually does.
Returning false, not true, was deliberate. A missing CheckEyeToyPresent should say
"no EyeToy", not "yes, and here's nothing".
3When the script wants to be the function
Some names THUG1 implemented in C++ (Grind, Land, Manual,
GetTriggerArray) THUG2 rewrote as scripts. Loading those scripts made the parser scream "defined twice".
In THUG2 mode, the script simply wins:
Remember GetTriggerArray. It comes back in chapter 5 with a crash in its hand.
4A font from the future
The first screen with text came up blank. THUG2's font files look exactly like THUG1's for 8 bytes, then slip in a version number and pad every character entry to 6 bytes instead of 4. The giveaway: THUG1's loader reads "number of characters: 1", which no real font has.
5The skeleton that counted in threes
The skater appeared, and immediately folded into a ball of limbs. THUG2's skinned meshes store, per vertex, which bones move it. THUG1's store bone numbers. THUG2's store vertex shader registers: each bone is a 4x3 matrix, three registers, so bone 5 is written down as 15. Divide by three, and the skater unfolds.
6Animations: five new flags, zero documentation
THUG2's .ska animations kept the container and changed what's inside. Each flag was found the same way:
decode an animation in Python, compare to what the skater should look like, stare at the bytes that don't fit, check
THUG2.exe's decoder, repeat.
| Flag | Means |
|---|---|
0x100 | Rotation keys have 4-byte headers |
0x2000 | The animation brings its own table of rotations |
0x8000 | Rotation components are stored as their high byte only |
0x10000 | 1-byte components index that table's 4th column |
0x20000 | Translations are relative to the skeleton's neutral pose |
Flag 0x20000 looked harmless here. It isn't. It sinks a skater into the floor in
chapter 5.
Where it landed
The game boots through every intro movie into THUG2's main menu, the Wheel of Looogie, with the skater spinning it. The submenus work: Story and Classic with difficulties, Create-A-Skater, Create-A-Graphic, Create-A-Goal, Game Options, high scores, cheats. Starting a story loads the Training level, where the skater stands in a perfect T-pose, arms out, like he's waiting for a hug from the whole engine. Fair enough.