For education purposes only · no game files included · bring your own THUG2
Chapter 7
thug2 · ec938de · "Online back"

Lost Connection to GameSpy

GameSpy shut down in 2014, and out of the box THUG2's online mode has nothing to talk to. Its favourite line says it all: You have lost connection to GameSpy. Check your network cables and reconnect. The cables were fine. GameSpy was the one that left. The THUG1 port already had its own online service, with lobbies, homies and stats, and a client inside the engine that talks to it. THUG2 runs on that engine. So how hard could it be? (Every chapter of this story starts with that question. Every chapter answers it.)

"I have read the Terms of Use for THUG 2 Online Play at www.thug2online.com and I agree to abide by these Terms of Use."THUG2's own scripts, still asking, twenty years later

1A service for the sequel

The THUG1 port's service, thugonline, is one Go program behind nginx with PostgreSQL behind it. It does what GameSpy's half dozen servers used to: message of the day, profiles, lobbies with chat, the game list, homies, NAT traversal, ranked games and top lists. THUG2 speaks the same protocol (it's the same client code), so the plan was: copy it, rename everything, point THUG2 at the copy. Two services, one server, nothing allowed to collide.

THUG1THUG2
HTTPSonline.epicmaxim.netonline2.epicmaxim.net
NAT (UDP)nat.epicmaxim.net:27901nat2.epicmaxim.net:27902
Databasethug_onlinethug2_online
Profiles start at100000200000

The only thing that really had to change was the game's data. THUG1 ranks 12 levels, from New Jersey to School II. THUG2's Online Stats screen lists its own, and it says so in plain QScript:

excerptscripts/game/menu/onlinestats.qb (THUG2, decompiled with qb)

Fifteen levels. The service keeps them as checksums of those load scripts, the game sends the same checksum when it reports a match, and the rating scale grows from 2900 to 500 + 15 × 200 = 3500. Same numbers on both ends:

diffCode/Sk/GameNet/Online/stats.cpp · ec938de

Spot the survivor: 0x991b5359 is in both lists. It's load_SE. THUG1 called that level Hotter than Hell, THUG2 calls it Pro Skater. Same name in the scripts, different level, same checksum. The service doesn't mind: each game has its own database.

2Ready, then nothing

First run: Multiplayer, Online Play, pick a skater, THUG 2 Online, Ready. The menu disappeared, the skate park stayed, and nothing else ever happened. The log had one new line:

excerptthe game's log

THUG1 goes straight from Ready to the message of the day. THUG2 asks first whether GameSpy's services are up: GameSpy had an "availability check" by then, a little server that could say "we're down for maintenance" (or "we're down forever", which nobody wrote a dialog for). The missing function returns false, the IF skips everything, and the script ends quietly:

excerptscripts/game/net/net.qb (THUG2)

The service has no maintenance mode to report. If it's down, the message of the day fails, and THUG2 already has a dialog for that. So the answer is yes, always:

diffCode/Sk/GameNet/GameNet.cpp · ec938de
Message: Welcome to THUG2 Online! Local test. With the ESRB notice: Game Experience May Change During Online Play.
One return true later. "Game Experience May Change During Online Play." It sure did.

3Ask the oracle for the rest

One missing function is a bug. Several are a pattern. A small script listed every word THUG2's net scripts use as a command and crossed out the ones the port knows: a C function, a script, a global. A few hundred survivors, nearly all of them parameter names and text keys. Eight were real commands, all new in THUG2:

CommandWhere THUG2 calls itHere
ServicesAreAvailableReadyyes
CancelMotdDownloadCancel on "checking for the message of the day"fails the download, as a timeout would
CancelLobbyConnectBack on "Connecting to GameSpy..."drops the answer on its way
ResetConnectionTimerOK on the lost connection dialog (PS2 only)nothing to reset
IsAlreadyMyBuddya message from someonethe homie list
SendBuddyMessageSend message, Replynew endpoint
AnyBuddyMessagesLeftthe pager, the pause menuthe inbox
ShowNextBuddyMessagethe pagerthe inbox

The scripts say when each one is called, not what it hands back. For that, THUG2.exe. Its function table maps each name to an address, and ShowNextBuddyMessage turns out to be short and chatty: it puts four checksums into the calling script's parameters, one after the other.

excerptTHUG2.exe, ShowNextBuddyMessage (0x549dc0)

So the port's version does exactly that, oldest message first:

diffCode/Sk/GameNet/Online/buddy.cpp · ec938de

CancelMotdDownload in THUG2.exe sets one byte. The message-of-the-day loop checks that byte next to a cmpl $0x3a98: 15000 milliseconds. Fifteen seconds was how long 2004 waited for a text file.

4Beep beep: the pager

THUG1 had homies. THUG2 gave them a Motorola pager (product placement was a whole genre in 2004). The service had nowhere to send a message, so it got one more endpoint and one more event:

excerptthug2online: internal/hub/hub.go

On the game's side the event lands in a small inbox (twenty messages, oldest out first) and runs THUG2's own script flash_buddy_message_icon. The icon shows up next to the GameSpy logo, and View Messages opens the pager:

The first message didn't arrive. The game said homie message not sent: 405, and for a minute that looked like a deep protocol mystery. It was the local test server, still running the build from before the endpoint existed. Method Not Allowed, because the method didn't exist yet. Restarting it also tested the next screen for free: "You have lost connection to GameSpy", on cue, on both instances.

5Host a game, crash a game

Host Game, Boston, Ready. The level started loading and the game asserted:

excerptthe game's log

Network games load a lighter version of each level, BO_net. The file is right there in Data/levels/BO/, named BO_net.scn.xbx. The port's file lookup indexes everything under Data, lowercase, then strips .xbx from the name it's asked for, because the THUG1 port's loose files don't have it. THUG2's do. Asked for bo_net.scn, the index only knew bo_net.scn.xbx. Story mode never noticed, because its levels come out of .prx archives, not loose files.

diffWn32/Code/Plat/Sys/File/p_filesys.cpp · ec938de

6The two-minute ranked game

Start Game, Trick Attack, two minutes. The test hook held forward and mashed ollie and flip on both instances, which is roughly how I played in 2004 too.

Trick Attack in Boston, 1:17 left, ProSkater 2300, the other ProSkater 1994.
Two copies of the same custom skater, with the same name, going for the same fridge.

When the clock hit zero, the host reported the scores, the service checked that both players had confirmed they were in the game, and the database got its first THUG2 rows: level 0x2a179957 (load_BO), 2300 and 1994, ratings 210 and 196. Online Stats in the game, back in the menus:

Online Stats, Recent Best Scores: Boston, ProSkater 2,300 and ProSkater 1,994.
The first entry on the new boards. Rank 0 out of 10. Everybody starts somewhere.

7The login that ate itself

Personal Stats were still empty after that game. That's how retail works: they're read once, at login. Fine: Change GameSpy Login, same email, Login/Create… and "Please Wait..." forever. The log said session lost, which was true, and the whole problem.

Logging in again with the same profile makes the service end the old session, and the old session's events poll (always open, waiting for chat and homies) comes back with "closed". If that answer arrives before the login's, the game handles a lost session: forget the profile, bump the request counter, so answers to old requests get dropped. The login's answer arrives next, sees an old counter, and gets dropped. The new session drowned the old one, and the old one took it down with it.

diffCode/Sk/GameNet/Online/buddy.cpp · ec938de

Ending the old session ourselves moves the client to a new generation first, so the old poll's "closed" is ignored. Log in again, and Personal Stats say Boston, 2,300.

Online Stats, All Time Personal Stats: ProSkater, Boston 2,300.
One row. Our row.

8Bonus level: the address that wouldn't leave

Everything committed, build script run… and the game still called online.epicmaxim.net, THUG1's service. The source said online2. The binary said online. CMake said nothing, because CMake never says anything: the address was a cache variable, and every build folder made before this chapter remembered the old default. A changed default never beats a cached value. So the variable got a new name, and the old one gets thrown out on the next configure:

diffCMakeLists.txt · ec938de

Where it landed

THUG2's online menus, all of them retail, talk to online2.epicmaxim.net: profiles, the four regions and Beginners, chat, homies with their status, the pager, hosting and joining through NAT, ranked games on the 15 levels and the Online Stats boards. How to use it is in the Online play guide.

The lobby: ProSkater: hello from thug2.
The first words said in the new lobby: "hello from thug2". Nobody answered. Nobody else was there yet.

The screen still says "Powered by GameSpy". It isn't. It's powered by a Go program, a PostgreSQL database and a lot of goodwill. The logo stays anyway: some things you just don't paint over.