For education purposes only · no game files included · bring your own THUG2
Chapter 1
thug-macos · branch thug2 · 43aced5

Hostile 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.

diffTHUG/Code/Gel/Scripting/script.cpp · 43aced5

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:

diffTHUG/Code/Gel/Scripting/parse.cpp · 43aced5

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.

diffTHUG/Wn32/Code/Plat/Gfx/nx/chars.cpp · 43aced5

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.

diffTHUG/Wn32/Code/Plat/Gfx/p_nxsector.cpp · 43aced5

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.

FlagMeans
0x100Rotation keys have 4-byte headers
0x2000The animation brings its own table of rotations
0x8000Rotation components are stored as their high byte only
0x100001-byte components index that table's 4th column
0x20000Translations 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.