Sensible World of Soccer 96/97 · DOS · ENGLISH.EXE · Versione italiana

SWOS ITA Mod Technical Manual

Documentation for anyone who wants to understand, extend or port the mod's patches: Serie C1 and C2, South American club cups (Libertadores, Supercopa, CONMEBOL), the Intercontinental Cup in career mode, real 1996-97 squads. Everything below was worked out from the disassembly and checked with builds and test games.

State as of 2026-09-30 (sessions 1–14) ENGLISH.EXE build d094c39d… TEAM.020 ded1b97b… Original ENGLISH.EXE aa21cd1c…

Foreword

A long time ago - in 2004 - in a galaxy far, far away, where the Internet was only just leaving behind the sound of the modem, artificial intelligence was the stuff of science fiction novels and video games were sold in elegant cardboard boxes in shops, yours truly - together with Ali Erdinc Koroglu, Jan Wickberg, Martin Binet, Zlatko Karakas, Ross Mayhew and Steve Smith - founded the Swos Working Group (web.archive.org) in honour of our all-time favourite video game: the 1996-1997 edition of Sensible World of Soccer. We were driven by a dream: to make that game even better by adding a few competitions whose absence we felt especially keenly. The Italian Serie C (split into C1 and C2), where players who would later become international stars, such as Simone Inzaghi and Gianluca Zambrotta, were playing, and the South American cups, above all the legendary Copa Libertadores, which, in an age when football on TV was still a novelty and far from plentiful, held for us the charm of exotic, faraway things. We would have loved to play these competitions above all in career mode, the SWOS players' absolute favourite: partly because in season mode and in the preset competitions we had already managed to add them, in a new edition of SWOS that was going to be called Swos 2024. The work was done by reverse engineering and hex editing; I remember the thrill of discovering the routines that add competitions, but also the bitter frustration of not being able to make them work in career mode, which would freeze every time. Today, in the year of grace 2026, I wanted to settle the score with this personal story that never left me over these two decades: the regret of having come close to a dream and never quite making it. After all, I told myself in an attempt at consolation, I ended up becoming a web and mobile developer; reverse engineering 1996 x86 code had already been a miracle, and hardly any of the many modern modders had managed the feat.

Today, thanks to AI, which has changed the very idea of programming forever, the feat is accomplished: you can now download the patch that adds, in every language of the game, Serie C1 and C2 to the Italian league, with the original squads of the time, and above all adds, for South America, the Copa Libertadores, the CONMEBOL and the Supercopa, with the entry rules of the day. And, as the icing on the cake, the Intercontinental Cup could not be missing. What matters to me, though, is not only the result but also the explanation of how we got there through AI. So here we publish, in English and Italian, the complete documentation of the method followed to create the patches, which finally revealed to me what we had not understood back in 2004, and which I hope will inspire other developers and lovers of the most beautiful video game in the world to expand its career mode even further. An immense thank you to the work of Zlatko Karakas, whose fundamental disassembly of the original executable I found after so many years of absence (github.com).

This patch is dedicated to the memory of Steve Smith, an extraordinary compiler of updated squads for SWOS, who left us far too soon. And to everyone who hopes to see, sooner or later, a dream come true, even one that has lasted 22 years…

— Davide Lorigliola

1Overview

The mod never touches the original files. A Python script reads orig/ITALIAN.EXE and orig/DATA/TEAM.020, applies every change deterministically and writes the installed files to c/SWOS/. Every address it needs is located in the executable by signature (unique byte sequences) and checked with assert: if a signature is missing or not unique, the build stops instead of producing a wrong exe.

AreaWhat it doesWhereStatus
Italy with 4 divisionsSerie A 18, B 20, C1 18, C2 18; 4 up and 4 down between adjacent levels; fixes the 1997 end-of-season freezepatch.pytested
C1/C2 teams36 real 1996-97 clubs with squads, coaches and calibrated strengthc1c2.py, c1c2_rosters.pytested
South American cupsLibertadores (groups + knockout), Supercopa, CONMEBOL: menus, world view, career, qualifiers from the standings, holder rulessacups.pytested
Double participationSouth American clubs: Supercopa in slot 1 (the national cup slot, free in South America) + continental cup in slot 3sacups.pytested
PersistenceSouth American cup lists and Intercontinental finalists saved with the careersacups.py, salists.pytested
IntercontinentalSingle-match final in December between the Champions Cup winner and the Libertadores winner; finalists updated at season end; slot 2 or slot 4sacups.pyslot 2, season end slot 4

2Environment and build

Folders

~/swos-mod/
  orig/            ITALIAN.EXE, ENGLISH.EXE, DATA/TEAM.020, swos9697ita.iso   (NEVER modify)
  c/SWOS/          game installed for DOSBox-X: ITALIAN.EXE, DATA/TEAM.020, *.CAR saves
  tools/           all the mod's code (Python + nasm sources inside sacups.py)
  ref/swos.asm     full IDA disassembly (labels = ENGLISH exe VAs), from github.com/zlatkok/swos-port
  ref/symbols.txt, ref/docs/SWOS/*.txt   file formats and notes (teams.txt, career.txt, ...)
  notes/STATUS.md  detailed log of every session (primary source)
  notes/addrmap.json  ENGLISH → ITALIAN delta map per object
  docs/            this manual (src/ = sources in both languages, tools/mkdocs.py builds the pages)

Commands

cd ~/swos-mod/tools && python3 patch.py      # writes c/SWOS/ITALIAN.EXE and c/SWOS/DATA/TEAM.020
python3 tools/salists.py c/SWOS/SAVE.CAR      # decodes a save's SA lists / Intercontinental pair
ndisasm -b32 -o 0xA1000 file.bin              # disassembler (Homebrew); nasm to assemble

Requirements: Python 3, nasm and ndisasm on the PATH. The game runs in DOSBox-X with the CD mounted from the original ISO. The build is deterministic: same inputs, same md5 (the random generator has a fixed seed, 1997).

Build order in patch.py:

  1. patch_teams: rebuilds TEAM.020 (original Serie A and B + 36 C1/C2 clubs) and returns the index remap table.
  2. patch_exe: adds a 4 KB page to object 1 (add_object_page), sets up the string pool, remaps Italian clubs in the European cup lists, applies the Italy patch (patch_italy), then all of sacups.patch (cups, tables, career, season end), sets object 1's vsize and flags, saves while rebuilding the fixup tables.

3Golden rules

Each of these rules comes from a real bug met during development.

Pointers across objects only through fixups. Object 1 (code) and object 2 (data) are not loaded at the same distance they have in the file. Every absolute address written into code or data needs an LE fixup pointing to the right object and offset. Never store an object-2-relative offset that points to object-1 data.
Name strings live in object 2. League and cup names are offsets relative to STR_BASE = obj2+0x16F8 (not "aChairmanScenes" as the English disassembly suggests). A name placed in object 1 shows up as garbage. New names go through strpool.py, which is almost full today.
Respect the game's checksums. At season end cseg_9487A checks that the 2000 bytes of leaguesTableCopy add up to 0 modulo 256; if not, the game freezes (int 3; jmp $). Any data parked in game tables must carry a balance byte.
No string instructions (movs, rep). ES is not guaranteed to equal DS where we enter. All copies in added code are byte loops through DS.
"Registers" D0–D7 and A0–A6 are memory variables. The game is a machine translation of 68000 (Amiga) code. Routines take parameters in these variables (D0 = obj2+0x3146D …). x86 registers are not preserved across game calls.
Search by signature, never fixed addresses. Every location is found with byte patterns and confirmed with assert. This makes porting to the other executables (English, French, German) possible and guards against silent errors.
Never drop teams that freeze. A team or save that causes a freeze must be kept and analysed: it is the proof of the bug.

4LE format and fixups

Each language executable (ENGLISH.EXE, ITALIAN.EXE, FRENCH.EXE, GERMAN.EXE) is a DOS/4GW program in LE (Linear Executable) format with two main objects. Values below are for ENGLISH.EXE:

ObjectContentLink baseNotes
obj1code0x10000original vsize 0xA0B79 → 0xA1000 (session 2) → 0xA2000 (added page). Made writable (flag 0x2) because the season-end code rewrites the cup lists stored in the new page.
obj2data0xC0000all game variables, career buffers, competition tables, strings

Addresses in the disassembly (cseg_8B2D3, dseg_D8CAA) are ENGLISH.EXE virtual addresses: object offset = VA − base. The other languages are shifted by small deltas (notes/addrmap.json maps ENGLISH → ITALIAN); the build never relies on them and finds every location by signature.

Fixups

Every absolute address in the file has an internal fixup record (source object, offset in page, target object, target offset). When an instruction holding an address is overwritten, its fixup must be removed (p.remove), or the loader corrupts the new bytes. When an address is added, the fixup must be added (p.add_ptr). lepatch.save rebuilds the fixup table; if it grows past the available room it moves the data pages forward (0x800 bytes so far) and updates the header offsets.

Added page

add_object_page(data, 1) appends a 4 KB physical page at the end of the file, inserts it into the page map right after object 1's pages, adds an empty entry to the fixup page table, shifts the header table offsets by +4/+8 and increments object 2's page_idx. All South American cup code and data live in this page, from obj1+0xA1000. Today it is used up to about obj1+0xA1932 (about 1.7 KB free). The rest of the old cave after the Italy league structure, 0xA0BC6–0xA1000 (about 1 KB), is still free.

5The tools

le.py
Read-only LE parser: objects, page → file map, obj_bytes(n), fixups() returning (source obj, offset, type, target obj, target offset, …). Used for all checks.
lepatch.py
LEPatch class: put/get by object and offset, target(obj, off) reads where a fixup points, add_ptr, remove, retarget, set_vsize, add_flags, save (fixup rebuild + data page move). add_object_page function. A round trip with no changes gives an identical file.
nasmcave.py
Assembles nasm sources (written as strings inside sacups.py) at a cave address. Symbols come as {name: (object, offset)}. The source is assembled three times: plain, with obj1 moved by 0x01000000 and with obj2 moved. Every dword that changes is an absolute reference and gets a fixup to the right object; relative jumps inside obj1 do not change and need none. labels() reads nasm's map file for label addresses.
strpool.py
Room for new strings in object 2: the never-referenced strings .COPPA AFRICANA CLUB****** and COPPA SUDAMERICANA*****, plus the asterisk padding of the second COPPA EUFA and of ERRORE DISCO (the text stays intact). Holds COPA LIBERTADORES, SUPERCOPA, COPA CONMEBOL, SERIE C1, SERIE C2 and a shortened name for COPPA INTERCONTINENTALE. Full: 1 byte left.
sacups.py
The heart of the mod: South American cup and Intercontinental structures, continental tables, nasm sources CAREER_ASM (season start and load) and QUALIFY_ASM (season end), all signature searches and hook installation.
patch.py
Build entry point: Italy (global number base, 4-division structure), TEAM.020, remap of Italian clubs in the European cups.
c1c2.py / c1c2_rosters.py
C1/C2 clubs (kits, division), real 1996-97 squads with coaches, strength targets, team record construction (section 8).
salists.py
Reads a .CAR and prints the saved block: format (SA or S2), checksum, Libertadores/Supercopa/CONMEBOL lists, Intercontinental pair.
addrmap.py
Builds notes/addrmap.json by comparing ENGLISH.EXE and ITALIAN.EXE in masked windows (fixup bytes excluded).
asmbytes.py
Extracts the db bytes of a data label from ref/swos.asm, handy to find the same tables in the other language executables.
mkdocs.py
Builds docs/index.html (English) and docs/it.html (Italian) for GitHub Pages from the sources in docs/src/.

6Memory map (ENGLISH.EXE)

Offsets relative to the object, in ENGLISH.EXE. In this executable every offset is exactly the disassembly address minus the object base (obj1 0x10000, obj2 0xC0000): dseg_D8CAA is obj2+0x18CAA. ITALIAN, FRENCH and GERMAN have the same layout at other offsets; the build finds all of them by signature (the Italian values are in the Italian manual).

Object 2: data

OffsetNameMeaning
0x16F8STR_BASEbase of league and cup name offsets
0x707Edseg_C707Elist of international contests (45 entries) → moved to obj1
0x7EE8italyTableoriginal Italy league structure (42 B) → replaced by one in obj1
0x8B1CcompetitionsTable256 pointers per nation/continent
0x8FD8southAmericaTableSouth America table → moved to obj1
0x958CcareerFileBufferstart of the career save (offset 0 of the .CAR)
0x16CD6selTeamNumberplayer's team (low byte = file, high = index)
0x16CDCdseg_D6CDCplayer's division
0x17E68someLeaguesTable2000 bytes, one per global team number (promotions/relegations); offset 0xE8DC in the .CAR
0x18CAAdseg_D8CAAcontest pointers of the 5 slots (+4 per slot, section 11)
0x18CBEdseg_D8CBEplayer's European cup kind (trophy, reputation, prize)
0x18CC0dseg_D8CC0play-off positions table (slot 4)
0x18D0A / 0x1985C / 0x1A0E2 / 0x19C9Fslot 1 / 2 / 3 / 4 buffers0x443 bytes each
0x1F4F6competitionFileBufferslot 0 buffer (the league)
0x1F533dseg_DF533league substitution rules (3 words: 5, 2, 8)
0x2092Fdseg_E092Fwhich cup is in slot 3 (0 CC, 1 CWC, 2 UEFA; the mod adds 3 Lib, 4 Sup, 5 CON; −1 none)
0x3146D …D0 D1 D2 … D7 A0 … A4pseudo-registers: D0 0x3146D, D1 0x31471, D2 0x31475, D7 0x31489, A0 0x3148D, A1 0x31491, A2 0x31495, A3 0x31499, A4 0x3149D
0x4EEA9DIY_competitionStart"working" contest: routines copy a slot here, process it and copy it back
0x593F2leaguesTableCopycopy of someLeaguesTable used at season end (checksum!)
0xC070Fdseg_18070FChampions Cup winner (holder); the mod also uses it for the Libertadores

Object 1: code (game routines used by the mod)

OffsetRoutineRole
0x7B2D3cseg_8B2D3loads a contest into a slot (D0 = slot, A0 = contest, D1, D2)
0x7D661cseg_8D661is the player's club in this cup's list?
0x7DAC3cseg_8DAC3slot 4: the division's play-offs, with placeholder teams
0x7ECAEcseg_8ECAEleague end: real teams into the play-offs
0x82D55cseg_92D55Champions Cup end: winner → dseg_18070F
0x2930BGetCurrentSeasonPointerseason record (competition names at +1Ah, +1Eh…)
VA 0x914F180-team assertthe one that froze the game in 1997
0xA0B80(new)Italy 4-division league structure (70 B)
0xA1000–0xA1932(new)added page: SA cups, Intercontinental, tables, career and season-end code

7Italy with 4 divisions

The 1997 season-end freeze

Every team has a global number (0–1999) used as an index into someLeaguesTable. Italy started at 424 with only 51 numbers: adding teams overflowed into Latvia (475). The extra teams collided, and at season end the cseg_91428 assert (80 teams in the European lists, counters 16/32/32) ended in int 3; jmp $. In 1997 the original mod inserted bytes between Italy's structure and Latvia's: about 50 league structures reached through fixed pointers were read out of alignment and the game froze.

The fix: Italy's base moves to 1850 (numbers 1794–1999 are free) in the teamsCountryNumbers table, and the new league structure goes into the obj1 cave, with the italyTable fixup retargeted (and widened from 16 to 32 bits).

League structure

10-byte header: [num, type, country, start, end, nameOff, 0, 0, 0, nDiv]
02 03 35
6 bytes per division: [teams, direct promotions, promotion play-off teams, relegated, relegation play-off, play-off data offset]
00
2 dwords per division: name offsets (from STR_BASE)

The mod uses DIVISIONS in patch.py: A (18, 0, 0, 4), B (20, 4, 0, 4), C1 (18, 4, 0, 4), C2 (18, 4, 0, 0). England already has 4 divisions in the same format: the engine handles 4 levels with no code changes.

The "play-off data offset" byte (+12h from the division record, as the code sees it) decides whether slot 4 hosts play-offs. In the mod it is always 0: no Italian division has play-offs.

8C1/C2 teams and squads

TEAM.020

The file starts with a big-endian word (team count) followed by 684-byte records. In the record: +1 ordinal, +2 global number (big-endian), +5 name (17 B), +25 division (0 A, 1 B, 2 C1, 3 C2; 4 was "non-league"), +26 first kit (5 B), +31 second kit, +36 coach (23 B), +60 order of the 16 players in the line-up, +76 16 players of 38 bytes.

Player byteContent
+0nationality (18 Italy, 13 Germany, 68 Argentina, 109 Gabon…)
+2shirt number
+3name (23 B, zero-terminated: 22 characters max)
+26bits 5–7 position (0 G, 1 RB, 2 LB, 3 D, 4 RW, 5 LW, 6 M, 7 A), bits 3–4 skin (00 light, 10 dark, 01 ginger)
+28…+317 nibble skills: passing, shooting, heading, tackling, ball control, speed, finishing. Bit 3 of the nibble marks a key skill; the real value is v & 7
+32price (index 0–49: 25K … 15M+). Goalkeepers have no skills: the price is their quality

The final file has 74 teams (A 18, B 20, C1 18, C2 18), sorted alphabetically, and stays under the limit of about 93 teams per nation (teamFileBuffer, 64014 B). Since the order differs from the original, remap_italian_cup_teams fixes the indexes of Italian clubs in the European cup lists (by name).

How a club is built (c1c2.real_team)

  1. Real squad from the it.wikipedia page "<club> 1996-1997" (squad section), split into P/D/C/A. Known regulars come first; the rest follow the page order.
  2. A template is chosen among the 13 original non-league clubs with a compatible role mix (G/D/M/A counted over the 16 positions), at random among the best fits.
  3. Each template position gets the next name of the matching role; if the role runs out, a neighbouring role lends one. For clubs that already existed, a missing role (for example the second goalkeeper) is filled with an original SWOS player of that club.
  4. The template's skills and prices move by whole steps towards an average price target taken from the real standings (1 skill step = 2 price units, clamp −3…+2). C1: 15.5–19; C2: 12–15; the original Serie B averages 17.9.
  5. Names already in the file (mid-season transfers) go last in their role list, to avoid duplicates. Names over 22 characters: first-name initial (M. DAL COMPARE).
To change a squad, edit c1c2_rosters.py (the order decides who makes the 16) and rerun the build. To change a club's strength: TARGET.

9Contest structure

[0]  contest id          [1] type: 1 knockout, 2 groups + knockout
[2]  nation (FF = any)
[3],[4] months: 8 × month counted from January of the season's first year (≥ 12 = next year)
[5]  names at +5+[5]: two dwords, offsets from STR_BASE (strings in object 2!)
[7]  team list at +7+[7]: byte pairs (TEAM file number, index)
[8]  1 = fixed list
type 1: [10] team count, [14..] one byte per round (0x94 two legs, 0x14 single-match final), then 00
type 2: [15] team count, groups of 4
IdCompetitionFormatMonths
0x6CCOPA LIBERTADORESChampions Cup clone: 16 teams, 4 groups of 4, then knockoutas the European cups (September–May)
0x6DSUPERCOPA16 teams, two legs, single-match final (Cup Winners' Cup clone)same
0x6ECOPA CONMEBOLsamesame
0x6FCOPPA INTERCONTINENTALEtype 1, 2 teams, one 0x14 round (single match); default Juventus – River Plate (Tokyo, November 1996)0x58–0x60: December, window to January

Free ids: 0x70–0x7B (leagues use 0x24–0x6B, 0x7C custom, 0x7D–0xB1 cups). South American teams come from TEAM.043 (ARG), 045 (BOL), 046 (BRA), 048 (CHI), 049 (COL), 050 (ECU), 064 (PAR), 065 (PER), 071 (URU), 077 (VEN).

10Continental tables and world view

ViewWorldMenu → ChooseCompetitionMenu → SelectTeamsFinalMenu: every pointer in the continent's competitionsTable entry becomes a button. The European cups get no special treatment, so adding pointers is enough.

When a competition is picked, cseg_3AA17 checks whether it is one of the player's 4 slots (real state). Otherwise cseg_3ABB2 regenerates it on the fly: random seed = contest id << 8 | season, then it simulates up to the current date. Competitions the player is not in have no persistent state: to show them, a structure and a team list are all that is needed.

11Career: the 5 slots

In career mode every team has 5 competition slots. For each slot the game keeps a contest pointer (dseg_D8CAA + 4 × slot, 0 = empty) and a 0x443-byte buffer with the running competition. The calendar walks the slots and plays the week's matches.

SlotPointerBufferOriginal useUse in the mod
00x18CAA0x1F4F6leagueunchanged
10x18CAE0x18D0Anational cupSupercopa for South American clubs (no national cup in South America)
20x18CB20x1985Cleague cup, or friendlies and toursIntercontinental when the slot is free (Italy, all South American countries)
30x18CB60x1A0E2European cupalso Libertadores / Supercopa / CONMEBOL
40x18CBA0x19C9Fdivision play-offsIntercontinental for countries with a league cup, when the division has no play-offs

Buffer header

+0 type (word), +27h name pointer, +2Bh slot number (word), +2Dh contest id, +3Dh/+3Fh/+41h substitution rules (5 on the bench, 2 changes, 8).

cseg_8B2D3 (load a contest into a slot)

A sixth slot was evaluated and dropped: the slot pointers are immediately followed by other variables, there are about 250 references in about 100 routines, and it would change the save format.

12Season start

ContinueCareerNextSeason (and InitCareer) call InitializeNewSeason: slots 0, 1, 2 from the nation's table, then the European cup chain (cseg_8D661 with D7 = 0, 1, 2 on Champions Cup, Cup Winners' Cup, UEFA), finally call cseg_8DAC3 (slot 4, play-offs).

flowchart TD
  A[InitializeNewSeason: slots 0, 1, 2] --> B{Club in a European cup?}
  B -- yes --> F[euro_found: int_step, then E092F = D7]
  B -- no --> I[init_sa]
  I --> I1{Saved block 'S2' or 'SA'?}
  I1 -- no, new career --> I2[DEFAULTS into lists + INTLIST, lists_out]
  I1 -- yes --> I3
  I2 --> I3{Slot 1 empty and club in the Supercopa?}
  I3 -- yes --> I4[Supercopa into slot 1]
  I3 -- no --> I5
  I4 --> I5[int_step: Intercontinental into slot 2 if free and club is a finalist]
  I5 --> I6[Slot 3: Libertadores, Supercopa unless already in slot 1, CONMEBOL; D8CBE = D7 - 3]
  F --> D[call cseg_8DAC3 redirected to int4_step]
  I6 --> D
  D --> D1[cseg_8DAC3: play-offs into slot 4 if the division has them]
  D1 --> D2{Slot 4 empty, club a finalist, Intercontinental not in slot 2?}
  D2 -- yes --> D3[Intercontinental into slot 4 via the buffer swap]
  D2 -- no --> Z[done]
  D3 --> Z

Intercontinental in slot 4: the buffer swap

Loading the cup straight into slot 4 would give a "your team against your team" match with no dates. int4_step instead:

  1. calls the real cseg_8DAC3; if slot 4 is taken (play-offs) it stops;
  2. clears buffer 4's id (so an old buffer is not restored on load);
  3. copies the slot 2 buffer (league cup) into buffer 4;
  4. calls cseg_8B2D3 with slot 2: the Intercontinental is built in full, with the finalists and the dates;
  5. swaps the two 0x443-byte buffers: Intercontinental in slot 4, league cup back intact in slot 2;
  6. +2Bh = 4, substitution rules from the league (as the game does for play-offs), restores the slot 2 pointer, D8CBA = INT.

The playoffs hook at the entry of cseg_8ECAE returns at once when slot 4 holds the Intercontinental: otherwise at the league's end the game would rebuild it from a play-off table that does not exist.

13Loading a save

ProcessCareerFile rebuilds the slot pointers from the nation's table (slots 0–2), the division record (slot 4) and E092F (slot 3). The slot buffers are in the save, the pointers are not: the mod restores them from the id stored in the buffers.

14Season end

cseg_91428: combines the lists; for the player's nation and then every European nation it simulates or reads the standings (cseg_9153F), promotions, cups; cseg_92D55 / 9307A / 9339D close the Champions Cup, Cup Winners' Cup and UEFA; cseg_93974 adds the qualifiers; final 80-team assert (16/32/32); cseg_92BBF re-admits the holders.

The mod redirects the call to cseg_92D55 (obj1+0x81470) to sa_qualify:

  1. [A4] = Libertadores and call cseg_92D55 + 10 (skips the first instruction, which would select the Champions Cup): closes the Libertadores (played by the player or simulated with cseg_3ADAE) and writes the winner into dseg_18070F → LIBWIN.
  2. For each South American nation: simulates the top division with cseg_915ED (the same simulation as the world view, so the standings match). For the player's own division it reads slot 0 instead. From the DIY: +6Dh row offsets in finishing order (rows of 12h bytes), +12Dh team number per row.
  3. Writes the qualifiers: Libertadores = champions of the 10 countries + runners-up of ARG, BRA, URU, PAR, COL, CHI (4 mixed groups, LIB_Q); CONMEBOL = the next places (CON_Q, 8 pairs). Supercopa: fixed list of former champions.
  4. Holder rules (real 1997): if LIBWIN is not already in the Libertadores it takes its country's runner-up place (else CHI2), and the displaced club takes its place in the CONMEBOL; if it is not in the Supercopa, it takes the last place.
  5. The real call cseg_92D55: closes the Champions Cup. Both branches end in cseg_92F1C, which always writes the winner to dseg_18070F.
  6. INTLIST[0] = [dseg_18070F], INTLIST[1] = LIBWIN (each skipped if 0FFFFh), then lists_out and ret.
flowchart LR
  S[cseg_91428 season end] --> Q[sa_qualify]
  Q --> L[Libertadores end: LIBWIN]
  L --> C[SA standings → Lib and CON lists]
  C --> H[Holder rules]
  H --> E[real cseg_92D55: CC winner]
  E --> N[INTLIST = CC, LIBWIN]
  N --> O[lists_out: saved block + checksum]
  O --> R[back to cseg_91428]

15Persistence in the save

The save holds someLeaguesTable (2000 bytes, one per global number). Numbers 1730–1849 belong to no team: the mod parks its data from number 1740 (obj2+0x18534), mirrored in leaguesTableCopy (obj2+0x59ABE), because at season end the game copies one table onto the other (cseg_8CC0A and cseg_8CC4E). InitCareer clears the table: an all-zero table means a new career.

OffsetS2 format (current)SA format (session 12)
+0marker 'S2' (word 0x3253)marker 'SA' (0x4153)
+2Libertadores: 16 × (file, index)same
+34Supercopasame
+66CONMEBOLsame
+98Intercontinental: 2 × (file, index)balance byte
+102balance byte—

The balance byte is −(sum of the block's preceding bytes) mod 256, so the whole block sums to 0 and the table stays at 0 for cseg_9487A's check. Reading: 'S2' everything from the save; 'SA' the three lists plus the default Juventus – River pair; no marker → 1997 values (DEFAULTS, 100 bytes). The first season end of an 'SA' career rewrites it as 'S2'.

# check in Python
blk = save[SAVED:SAVED + 103]
assert blk[:2] == b'S2' and sum(blk) % 256 == 0

16Trophies and reputation

Trophy bits are built in cseg_39392 from the flags D6C64 (Champions Cup, bit 2, reputation +100), D6C68 (UEFA, bit 3, +50) and D6C66 (Cup Winners' Cup, bit 4, +50). cseg_3D7CB picks the flag from dseg_D8CBE, cseg_5DEB9 the prize. The mod sets D8CBE = D7 − 3 when a South American cup is entered:

The season record (ManagementRecordMenu) already shows the right name ("COPA LIBERTADORES - WINNERS"). Dedicated names and bits (bits 10–15 are free) would need new strings, and the pool is full.

17Hook table

Each hook overwrites original instructions with a jmp or changes a call target; the fixups of the overwritten instructions are removed.

Site (obj1)BytesOriginalBecomes
0x7D4B311mov [E092F],-1; jmp done (no European cup)jmp init_sa
0x7D4BE12mov ax,[D7]; mov [E092F],ax (European cup found)jmp euro_found
0x7D4CArel32call cseg_8DAC3call int4_step
0x2396D10cmp [E092F],-1; jz out (ProcessCareerFile)jmp load_slot1
0x239A912cmp [E092F],2; jz map; jmp outjmp load_slot3
0x7ECAE13cmp dword [D8CBA],0; jz out (cseg_8ECAE)jmp playoffs
0x81470rel32call cseg_92D55 (season end)call sa_qualify

Data changes: Italy's base in the global number table; italyTable fixup retargeted to the new structure; South America, Europe and international list tables retargeted into obj1; new names in the string pool; indexes of Italian clubs in the European lists; object 1 vsize and flags.

Label addresses in the added page (init_sa, sa_qualify…) change when the code grows: patch.py prints the main ones, and nasmcave.labels() returns them all.

18Verifying without playing

Before handing out a build for testing, these checks are run every time:

  1. The build passes every assert (unique signatures, expected targets).
  2. Re-read with le.py: the fixup tables parse without errors.
  3. ndisasm of the generated code and of every hook site: every jump goes to the right label.
  4. Every absolute operand has a fixup to the correct object and offset (the script annotates each disassembly line with its fixups).
  5. Comparison with the previous build: bytes change only where intended; the only fixups changed outside the added page must be the expected ones.
  6. For saved data: Python emulation of the checksum on a copy of a real save.

If a check fails and cannot be fixed, the previous build is put back (ITALIAN_S12.EXE is the last one tested in depth).

19Limits and open issues

20History and next steps

SessionContentTest
1–2LE tools; cause of the 1997 freeze; Italy with 4 divisions, base 1850passed
5Real C1/C2 1996-97 clubspassed
9South American cups in menus and world view; string poolpassed
10South American clubs in the cups in career mode; qualifiers from the standings; holderpassed
11Supercopa in slot 1; page added to object 1passed
12List persistence with checksum; CONMEBOL swap; trophies; Intercontinental in menus and in slot 2persistence, slot 2, CONMEBOL trophies
13Intercontinental finalists at season end; S2 format; Intercontinental in slot 4season end, S2 slot 4
14Real 1996-97 squads and calibrated strength for all 36 C1/C2 clubspassed

Roadmap

  1. Tests of sessions 12–14 and fixes.
  2. Port to ENGLISH/FRENCH/GERMAN.EXE from the same CD. To be made generic: STR_BASE, the pseudo-register offsets written into sacups.py's regular expressions, the pool's source strings (Italian texts), displayed names, slot addresses (all found by signature: to be rechecked one by one).
  3. Patcher as a single HTML/JS file that ships only differences: users pick their own original files, the patcher checks the md5s, applies the differences and downloads the patched files; README in Italian and English; to uninstall, keep the originals.
  4. After 1.0: 5-man bench for English clubs; Intercontinental in slot 4 test; C1 with two groups, after an analysis-only session.

Credits: disassembly and file format documentation from swos-port (zlatkok). Mod: Davide Lorigliola, with Claude.