Het decompileren van Fast Food Tycoon 2: De wereld tekenen

Ik heb de afgelopen dagen mijn favoriete computerspel gedecompileerd: Fast Food Tycoon 2. Er is een grote kans dat je nog nooit hebt gehoord van deze vreemde business simulator uit 2001. Maar toen ik in 2004 een kind was, koos ik het uit een rijtje afgeprijsde spellen in een winkelcentrum, en sindsdien is het een vast onderdeel van mijn psyche.

Om de gameplay kort te beschrijven: je bouwt een pizza-imperium in een levende, isometrische stad, waarbij je restaurants en magazijnen huurt, personeel aanneemt, pizza's ontwerpt en je toeleveringsketens beheert. Klanten beoordelen vervolgens je eten, service en reputatie. Rivaliserende bedrijven concurreren met je via advertenties, politieke invloed en georganiseerde misdaad.

Voor mij voelde het spel altijd vooruitstrevend, met een verrassende hoeveelheid details in de wereld.

Om het spel te decompileren, heb ik een agentic loop op mijn computer ingesteld met GPT-6.1 Sol Medium. We gebruikten Ghidra om de 4.046 functies van fastfood2.exe te decompileren tot ongeveer 9,5 MB aan C-achtige tekst (functieadressen, veldoffsets, constanten en aanroepen).

Deze ruwe code werd geladen in SQLite en voorzien van een script om gerelateerde functies te vinden zonder door enorme hoeveelheden tekst te hoeven lezen:

decomp_index.py grep    "0xe8\) = "  # zoek naar schrijfacties naar velden
decomp_index.py callers 0x4ed360     # zoek naar aanroepers
decomp_index.py string  "fnt"        # zoek naar referenties naar lettertypen

In Windows bestaat er een API om procesgeheugen te lezen, ReadProcessMemory, waardoor de agent het draaiende spel kon inspecteren en geheugenwaarden kon vergelijken met wat er op het scherm verschijnt. Zo kon bijvoorbeeld het geheugenadres voor contant geld, de camerapositie of het geselecteerde gebouw worden gevonden. Vervolgens werd de gedecompileerde code doorzocht naar de functies die deze waarden veranderden.

Om het spel te besturen, werd de Windows-berichtafhandelaar (message handler) gepatcht, zodat commando's konden worden verzonden om te draaien op de eigen UI-thread van het spel. De agent kon op knoppen klikken, de camera bewegen en de simulatie versnellen zonder het uitvoerbare bestand op de schijf te wijzigen. Een vroege doorbraak kwam door het gebruik van een crawler die door de interface van het spel klikte om de rest van de onbekende bedieningselementen te ontdekken.

Op dat punt stuurde ik de agent met mijn nieuwsgierigheid, en we gingen dieper graven.

De buitenwereld

Een stad is een schilderij plus een paar honderd sprites. Onder deze volledig gerenderde scène bevat het kaartbestand ber.tga straten en kavels (en de schaduw van de Brandenburger Tor), maar geen gebouwen, mensen of auto's.

Om de buitenwereld te tekenen, rendert FFT2 de scène in een buffer in het geheugen. Eerst bepaalt het welke schermgebieden opnieuw getekend moeten worden, zoals waar een persoon bewoog of wanneer de camerapositie is veranderd. Het herstelt de achtergrond in die gebieden en tekent vervolgens de scène eroverheen. Elke pixel bevat een 16-bit kleurwaarde en een aparte lighting-byte.

dirty_areas = find_areas_that_changed()
draw_background(dirty_areas)
for draw_pass in scene_draw_passes:
    for cell in diagonal_map_order:
        draw_parts_for_this_pass(draw_pass, cell, dirty_areas)

De renderer voert verschillende passes uit om te bepalen welke objecten andere objecten bedekken. De renderer bezoekt kaartcellen diagonaal (of, vermoed ik, isometrisch). Om verborgen mensen zichtbaar te houden (hun omtrek is zichtbaar door muren), markeert de renderer hun vorm met een tijdelijke pixelwaarde voordat er objecten bovenop worden getekend. Latere passes tekenen een gele omtrek rond verborgen lichaamsdelen en een witte omtrek rond verborgen gedachte-iconen, om vervolgens de zichtbare delen in te vullen.

Uiteindelijk combineert de renderer de kleur van elke pixel met de lighting-waarde en schrijft dit naar de uiteindelijke 16-bit display-buffer. Deze conversie past een herhalend dither-patroon toe om tussenliggende tinten te tekenen.

mark_shape_with_temporary_value()
draw_covering_objects()
outline_covered_parts(outline_colour)
fill_parts_where_temporary_value_remains()

Ik heb ook een grafische weergave gegenereerd om de efficiëntie van de renderer te tonen. Pixels die in de laatste 0,6 seconden zijn veranderd, worden gemarkeerd (auto's, wandelaars, zwaaiende boomtoppen, knipperende borden). Het grootste deel van het beeld verandert niet, omdat het dirty-region systeem beperkt wat er opnieuw getekend moet worden.

Verlichting

Het daglicht volgt een tabel met 25 uurlijkse helderheidswaarden: 70 's nachts, scherp stijgend rond zonsopgang naar 251 overdag, en vervolgens dalend bij zonsondergang.

Lampen reageren op dit lichtniveau. Hun drempelwaarden voor het gloeien variëren licht per object-ID, waardoor ze in kleine groepen inschakelen. Hun lichtvlekken blijven minimaal 160, zodat delen van het wegdek gedurende de hele nacht helder blijven.

light = interpolate_hourly_table(clock, brightness_setting)
for lamp in lamps:
    show_glow = light <= 160 + (lamp.id & 7)
    draw_lamp(lamp, glow=show_glow)
    draw_light_pool(lamp, light=max(light, 160))

De id & 7 zorgt voor acht mogelijke drempel-offsets (0-7). Met de standaard helderheid van FFT2 schakelen standaardlampen in rond 18:25–18:30 en uit rond 05:35–05:40. Andere objectfamilies gebruiken lagere drempels, waardoor hun gloed pas later verschijnt naarmate de stad donkerder wordt. Dit draagt bij aan een zeer bevredigend gevoel van de dag-nachtcyclus.

Elke pizzeria-fase heeft zijn eigen lichtmasker, opgeslagen in bijvoorbeeld hpizza1.m.spr. Elke zichtbare pixel in dit masker bevat een helderheidswaarde. De renderer vergelijkt die waarde met het licht dat al op dezelfde positie aanwezig is en behoudt de hoogste waarde. Hierdoor blijven de ramen en het uithangbord helder terwijl de rest van het gebouw donkerder wordt.

Een apart bestand, hpizza1.l.spr, levert de raamkleuren die 's nachts worden gebruikt. De renderer tekent deze kleuren en past vervolgens het lichtmasker toe (zoals eerder vermeld, kleur en licht blijven gescheiden tot de laatste stap wanneer de pixels op het scherm worden getekend).

Deze lichtcurve-waarden kunnen worden gewijzigd door de helderheidsinstelling, wat resulteert in:

light = base + (251 − base) * brightness

De personages

Alles wat beweegt is opgeslagen in een .ani-bestand, wat een sprite-sheet is met benoemde animatiesequenties. De meeste interne tekst die ik tegenkwam was in het Duits geschreven (de taal van de ontwikkelaars).

Omdat FFT2 een pixel-gebaseerd isometrisch spel uit 2001 is, is elke animatie vooraf getekend.

De "walker update"-logica vergelijkt de positie van de volgende node met de huidige en kiest de richtingsanimatie op basis van twee tekens:

  • (−,+) is 1
  • (0,+) is 2
  • (+,+) is 3, enzovoort.

De benoemde animatiesequentie slaat ook op hoe ver de sprite per frame beweegt. Bijvoorbeeld: de gehen4 van de serveerster beweegt elke frame (+1.25, 0), en gehen3 beweegt (+0.884, +0.442), enzovoort. De tick voegt deze vector bij elke frame-update toe, zodat voeten en grond synchroon bewegen.

Kaarten hebben vooraf gedefinieerde voetpad-nodes (voor wandelaars) en straat-nodes (voor voertuigen). De loopjongen is de snelste wandelaar met 27px per seconde, vergeleken met de toerist (16,2px/sec) en de beroemdheid (13,8px/sec).

In een filiaal

Het restaurantzicht gebruikt hetzelfde renderproces als de buitenwereld: platte afbeeldingen worden in een buffer getekend, waarbij kleur en licht apart worden opgeslagen. Binnenverlichting blijft echter constant op 248, ongeacht het uur (terwijl daglicht buiten 251 bereikt).

Deze weergave maakt gebruik van aparte, grotere artwork voor de personages en hanteert een speciale tekenvolgorde om mensen te plaatsen die op meubilair zitten. Wanneer de rugleuning van de stoel naar de camera is gericht, tekent de renderer eerst de hoofd afbeelding van de stoel, dan het personage, en vervolgens de rugleuning (of de "voorzijde" van de stoel vanuit het perspectief van de camera).

Er is een speciale sprite voor de pizza op tafel. Ik vergeef de animators dat iedereen hetzelfde eetpatroon heeft.

Verborgen details

Tijdens mijn onderzoek ben ik op veel eigenaardigheden gestuit. Hier zijn een paar favorieten:

Mensen behouden hun dagelijkse gewoonten. Wanneer burgers worden gecreëerd, krijgen ze een schema. Deze schema's worden niet elke dag gereset, dus een kantoormedewerker die is toegewezen aan een specifieke bioscoop voor de middag, blijft daar telkens terugkeren. Dit draagt waarschijnlijk bij aan het "echte gevoel" van de stad.

Deze schema's bevatten stereotypen:

  • Goths blijven thuis tot 09:00 uur.
  • Studenten hebben een kans van 40% op een pub-bezoek tussen middernacht en 03:00 uur.
  • Importanties (Big shots) hebben een kans van 70% op een operabezoek tussen 18:00 en 21:00 uur.

Het spel noemt zichzelf intern "Pizza3", zoals te zien in een configuratiebestand voor de stad: Init-File für Pizza3-Stadt.

En tot slot kon ik een mysterie uit mijn jeugd oplossen: hoe in hemelsnaam meet en scoort dit spel mijn vermogen om pizza's te maken? Nu ik deze kennis heb ontgrendeld, kunnen we dat bekijken.

Het spel verdeelt de pizza in 24 regio's om te scoren hoe gelijkmatig de toppings zijn verspreid, zoekt naar overeenkomstige stukken aan tegenovergestelde zijden en controleert welke ingrediënten bij elkaar passen (waarbij verschillende klantgroepen verschillende beoordelingen geven).

Er is zelfs een plagiaatcontrole (!) op basis van ingrediënten-gewichten. Het herschikken van de pizza van een concurrent (een strategie die ik zeker heb geprobeerd) fopt het spel dus niet. Een gekopieerd recept wordt gemarkeerd als "niet origineel" en mist +30 smaakpunten voor yuppies, goths en toeristen, en +5 voor importanties.

Al dit detail, en zeer weinig begeleiding in het spel zelf. Voor zover ik me herinner — aangezien ik het ben verloren tijdens een verhuizing — werd FFT2 geleverd met een handleiding van ongeveer 100 pagina's. Dit was in de tijd dat het normaal was dat spellen inherent uitdagend waren!

Een leuke noot: de afbeeldingen en video's die ik voor dit bericht heb gegenereerd, maakten gebruik van enkele van de gedecompileerde lettertypen van FFT2. Ook is bij sommige content nog "PC2" zichtbaar, omdat ik dit spel eigenlijk Pizza Connection 2 noem; dat is namelijk de Europese naam!