Sensible World of Soccer 96/97 · DOS · ITALIAN.EXE · English version

Manuale tecnico SWOS ITA Mod

Documentazione per chi vuole capire, ampliare o portare su altri eseguibili le patch del mod: Serie C1 e C2, coppe sudamericane (Libertadores, Supercopa, CONMEBOL), Coppa Intercontinentale in carriera, rose reali 1996-97. Tutto quello che segue è stato ricavato dal disassemblato e verificato con build e partite di prova.

Stato al 30/09/2026 (sessioni 1–14) Build exe cb6c817f… TEAM.020 ded1b97b… Exe originale 60cdb6b1…

Premessa

Tanto tempo fa - anno 2004 - in una galassia lontana lontana, dove Internet stava appena separandosi dal suono del modem, l'intelligenza artificiale era un argomento da romanzi di fantascienza e i videogiochi si vendevano in eleganti scatole di cartone nei negozi, il sottoscritto - insieme ad Ali Erdinc Koroglu, Jan Wickberg, Martin Binet, Zlatko Karakas, Ross Mayhew e Steve Smith - fondò lo Swos Working Group (web.archive.org) in onore del nostro videogioco preferito di sempre: l'edizione 1996-1997 di Sensible World of Soccer. Eravamo animati da un sogno: rendere ancora più bello quel gioco aggiungendo alcune competizioni la cui mancanza avvertivamo particolarmente amara. La serie C italiana (divisa in C1 e C2), in cui giocavano atleti che sarebbero poi diventati campioni a livello internazionale come Simone Inzaghi e Gianluca Zambrotta, e le coppe sudamericane, su tutte la mitica Copa Libertadores di cui, in un'epoca nella quale il calcio in tv era ancora una novità e non così abbondante, sentivamo il fascino delle cose esotiche e lontane. Ci sarebbe piaciuto giocare queste competizioni particolarmente in modalità carriera, quella preferita in assoluto dai giocatori di Swos: anche perché, nella modalità stagione e competizioni predefinite, eravamo già riusciti ad aggiungerle in una nuova edizione di Swos che si sarebbe dovuta chiamare Swos 2024. Il lavoro fu fatto con il reverse engineering e l'hex editing, ricordo l'emozione nello scoprire le routine di aggiunta delle competizioni ma anche l'amara frustrazione di non riuscire a farle funzionare in modalità carriera, che puntualmente si bloccava. Oggi, anno di grazia 2026, ho voluto chiudere il conto con questa storia personale che durante questi due decenni non mi aveva mai abbandonato, che era il rimpianto di essere arrivati vicino a un sogno e di non esserci mai riusciti. Del resto, mi dicevo per tentare di consolarmi, io alla fine sono diventato uno sviluppatore web e mobile, quello del reverse engineering su un codice x86 del 1996 era stato già un miracolo e per lo più nessuno dei tantissimi modder moderni era riuscito a compiere l'impresa.

Oggi, grazie alla AI, che ha cambiato per sempre il concetto stesso di programmazione, l'impresa è compiuta: potete ora scaricare la patch che aggiunge, in tutte le lingue del gioco, la serie C1 e C2 al campionato italiano, con le rose originali dell'epoca, ma soprattutto aggiunge per il Sud America la Coppa Libertadores, la Conmebol e la Supercopa, con le regole di ammissione di allora. E, come ciliegina sulla torta, non poteva mancare la Coppa Intercontinentale. Quello che mi interessa, però, non è solo il risultato, ma anche la spiegazione di come attraverso la AI ci si è arrivati. Quindi qui pubblichiamo, in inglese e in italiano, la documentazione completa del metodo seguito per creare le patch, che mi ha finalmente svelato cosa non avevamo capito nel lontano 2004 e spero possa ispirare altri sviluppatori e amanti del videogioco più bello del mondo a espanderne ulteriormente la modalità carriera. Un grazie immenso al lavoro di Zlatko Karakas, di cui dopo tanti anni di assenza ho trovato il fondamentale lavoro di disassemblaggio dell'exe originale (github.com).

Questa patch è dedicata alla memoria di Steve Smith, straordinario compilatore di rose aggiornate per Swos, che ci ha lasciati troppo presto. E a tutti quelli che sperano di vedere, prima o poi, realizzato un sogno che può essere lungo anche 22 anni…

— Davide Lorigliola

1Panoramica

Il mod non tocca mai i file originali. Uno script Python legge orig/ITALIAN.EXE e orig/DATA/TEAM.020, applica tutte le modifiche in modo deterministico e scrive i file installati in c/SWOS/. Ogni indirizzo che serve viene cercato nell'eseguibile per firma (sequenze di byte uniche) e verificato con assert: se una firma non si trova o non è unica, il build si ferma invece di produrre un exe sbagliato.

AreaCosa faDoveStato
Italia a 4 divisioniSerie A 18, B 20, C1 18, C2 18; 4 promozioni e 4 retrocessioni fra livelli adiacenti; risolto il blocco di fine stagione del 1997patch.pyprovato
Squadre C1/C236 club reali 1996-97 con rose, allenatori e forza calibratac1c2.py, c1c2_rosters.pyprovato
Coppe sudamericaneLibertadores (gironi + eliminazione), Supercopa, CONMEBOL: menu, vista mondo, carriera, qualificate dalle classifiche, regole della detentricesacups.pyprovato
Doppia partecipazioneClub sudamericani: Supercopa nello slot 1 (quello della coppa nazionale, libero in Sudamerica) + coppa continentale nello slot 3sacups.pyprovato
PersistenzaListe delle coppe sudamericane e finaliste dell'Intercontinentale salvate con la carrierasacups.py, salists.pyprovato
IntercontinentaleFinale secca a dicembre fra vincitrice Coppa Campioni e vincitrice Libertadores; finaliste aggiornate a fine stagione; slot 2 oppure slot 4sacups.pyslot 2, fine stagione slot 4

2Ambiente e build

Cartelle

~/swos-mod/
  orig/            ITALIAN.EXE, ENGLISH.EXE, DATA/TEAM.020, swos9697ita.iso   (MAI modificare)
  c/SWOS/          gioco installato per DOSBox-X: ITALIAN.EXE, DATA/TEAM.020, salvataggi *.CAR
  tools/           tutto il codice del mod (Python + sorgenti nasm dentro sacups.py)
  ref/swos.asm     disassemblato IDA completo (etichette = VA dell'exe INGLESE), da github.com/zlatkok/swos-port
  ref/symbols.txt, ref/docs/SWOS/*.txt   formati di file e note (teams.txt, career.txt, ...)
  notes/STATUS.md  registro dettagliato di ogni sessione (fonte primaria)
  notes/addrmap.json  mappa di delta ENGLISH → ITALIAN per oggetto
  docs/            questo manuale (src/ = sorgenti nelle due lingue, tools/mkdocs.py genera le pagine)

Comandi

cd ~/swos-mod/tools && python3 patch.py      # scrive c/SWOS/ITALIAN.EXE e c/SWOS/DATA/TEAM.020
python3 tools/salists.py c/SWOS/SALVA.CAR     # decodifica le liste SA / Intercontinentale di un salvataggio
ndisasm -b32 -o 0xA1000 file.bin              # disassemblatore (Homebrew), nasm per assemblare

Dipendenze: Python 3, nasm e ndisasm nel PATH. Il gioco gira in DOSBox-X con il CD montato dall'ISO originale. Il build è deterministico: stessi input, stesso md5 (il generatore casuale ha seme fisso 1997).

Ordine del build in patch.py:

  1. patch_teams: ricostruisce TEAM.020 (Serie A e B originali + 36 club C1/C2) e restituisce la tabella di rimappatura degli indici.
  2. patch_exe: aggiunge una pagina da 4 KB all'oggetto 1 (add_object_page), prepara il pool di stringhe, rimappa i club italiani nelle liste delle coppe europee, applica la patch Italia (patch_italy), poi tutto sacups.patch (coppe, tabelle, carriera, fine stagione), imposta vsize e flag dell'oggetto 1, salva ricostruendo le tabelle dei fixup.

3Regole d'oro

Ognuna di queste regole viene da un bug reale incontrato durante lo sviluppo.

Puntatori fra oggetti solo tramite fixup. L'oggetto 1 (codice) e l'oggetto 2 (dati) non vengono caricati alla stessa distanza che hanno nel file. Ogni indirizzo assoluto scritto nel codice o nei dati deve avere un fixup LE che punti all'oggetto e all'offset giusti. Mai memorizzare un offset relativo all'oggetto 2 che punti a dati dell'oggetto 1.
Le stringhe dei nomi stanno nell'oggetto 2. I nomi di leghe e coppe sono offset relativi a STR_BASE = obj2+0x16F8 (non "aChairmanScenes" come dice il disassemblato inglese). Un nome messo nell'oggetto 1 appare come spazzatura. Per i nomi nuovi c'è strpool.py, che oggi è praticamente pieno.
Rispettare i checksum del gioco. A fine stagione cseg_9487A verifica che la somma dei 2000 byte di leaguesTableCopy sia 0 modulo 256; se non lo è, il gioco si blocca (int 3; jmp $). Qualsiasi dato parcheggiato in tabelle del gioco deve portarsi dietro un byte di bilanciamento.
Niente istruzioni di stringa (movs, rep). Il registro ES non è garantito uguale a DS nei punti in cui entriamo. Tutte le copie nel codice aggiunto sono cicli byte per byte attraverso DS.
I "registri" D0–D7 e A0–A6 sono variabili in memoria. Il gioco è una traduzione automatica di codice 68000 (Amiga). Le routine ricevono parametri in queste variabili (D0 = obj2+0x315B7 …). I registri x86 non vengono preservati fra chiamate del gioco.
Cercare per firma, mai indirizzi fissi. Tutte le posizioni sono trovate con pattern di byte e confermate con assert. Questo rende possibile il port sugli altri eseguibili (inglese, francese, tedesco) e protegge da errori silenziosi.
Mai eliminare squadre che si bloccano. Una squadra o un salvataggio che causa un blocco va conservato e analizzato: è la prova del bug.

4Formato LE e fixup

ITALIAN.EXE è un eseguibile DOS/4GW in formato LE (Linear Executable) con due oggetti principali:

OggettoContenutoBase di linkNote
obj1codice0x10000vsize originale 0xA09C9 → 0xA1000 (sessione 2) → 0xA2000 (pagina aggiunta). Reso scrivibile (flag 0x2) perché il codice di fine stagione riscrive le liste delle coppe che stanno nella nuova pagina.
obj2dati0xC0000tutte le variabili del gioco, i buffer di carriera, le tabelle delle competizioni, le stringhe

Gli indirizzi nel disassemblato (cseg_8B2D3, dseg_D8CAA) sono VA dell'exe inglese. Per l'italiano: offset nell'oggetto = VA − base, poi si applica il delta di notes/addrmap.json (11 tratti per obj1, 16 per obj2). In pratica, per ogni routine ci si fida della firma trovata nell'exe italiano; la mappa serve solo per orientarsi.

Fixup

Ogni indirizzo assoluto nel file ha un record di fixup interno (oggetto sorgente, offset nella pagina, oggetto destinazione, offset destinazione). Quando si sovrascrive un'istruzione che conteneva un indirizzo, il suo fixup va rimosso (p.remove), altrimenti il loader corrompe i byte nuovi. Quando si aggiunge un indirizzo, va aggiunto il fixup (p.add_ptr). lepatch.save ricostruisce la tabella dei fixup; se cresce oltre lo spazio disponibile sposta in avanti le pagine dati (finora di 0x800 byte) e aggiorna gli offset dell'header.

Pagina aggiunta

add_object_page(data, 1) aggiunge una pagina fisica da 4 KB in fondo al file, la inserisce nella mappa delle pagine subito dopo quelle dell'oggetto 1, aggiunge una voce vuota nella tabella delle pagine dei fixup, sposta di +4/+8 gli offset delle tabelle dell'header e incrementa page_idx dell'oggetto 2. Tutto il codice e i dati delle coppe sudamericane vivono in questa pagina, da obj1+0xA1000. Oggi è occupata fino a circa obj1+0xA1932 (circa 1,7 KB liberi). Lo spazio lasciato libero nella vecchia cavità 0xA0A18–0xA1000 (circa 1,5 KB) è di nuovo disponibile.

5Gli strumenti

le.py
Parser LE in sola lettura: oggetti, mappa pagina → file, obj_bytes(n), fixups() che restituisce (obj sorgente, offset, tipo, obj destinazione, offset destinazione, …). Usato per tutte le verifiche.
lepatch.py
Classe LEPatch: put/get per oggetto e offset, target(obj, off) legge dove punta un fixup, add_ptr, remove, retarget, set_vsize, add_flags, save (ricostruzione fixup + spostamento pagine dati). Funzione add_object_page. Andata e ritorno senza modifiche = file identico.
nasmcave.py
Assembla sorgenti nasm (scritti come stringhe dentro sacups.py) a un indirizzo della cavità. I simboli arrivano come {nome: (oggetto, offset)}. Il sorgente viene assemblato tre volte: normale, con obj1 spostato di 0x01000000 e con obj2 spostato. Ogni dword che cambia è un riferimento assoluto e riceve il fixup verso l'oggetto giusto; i salti relativi dentro obj1 non cambiano e non ne hanno bisogno. labels() legge il file map di nasm per gli indirizzi delle etichette.
strpool.py
Spazio per nuove stringhe nell'oggetto 2: le stringhe mai referenziate .COPPA AFRICANA CLUB****** e COPPA SUDAMERICANA*****, più il riempimento con asterischi della seconda COPPA EUFA e di ERRORE DISCO (il testo resta intatto). Contiene COPA LIBERTADORES, SUPERCOPA, COPA CONMEBOL, SERIE C1, SERIE C2 e un nome accorciato per COPPA INTERCONTINENTALE. Pieno: resta 1 byte.
sacups.py
Il cuore del mod: strutture delle coppe sudamericane e dell'Intercontinentale, tabelle continentali, sorgenti nasm CAREER_ASM (inizio stagione e caricamento) e QUALIFY_ASM (fine stagione), ricerca di tutte le firme e installazione degli agganci.
patch.py
Punto d'ingresso del build: Italia (base dei numeri globali, struttura a 4 divisioni), TEAM.020, rimappatura dei club italiani nelle coppe europee.
c1c2.py / c1c2_rosters.py
Club di C1/C2 (maglie, divisione), rose reali 1996-97 con allenatori, obiettivi di forza, costruzione dei record squadra (sezione 8).
salists.py
Legge un .CAR e stampa il blocco salvato: formato (SA o S2), somma di controllo, liste Libertadores/Supercopa/CONMEBOL, coppia dell'Intercontinentale.
addrmap.py
Genera notes/addrmap.json confrontando ENGLISH.EXE e ITALIAN.EXE a finestre mascherate (i byte dei fixup esclusi).
asmbytes.py
Estrae i byte db di un'etichetta dati da ref/swos.asm, utile per cercare le stesse tabelle nell'exe italiano.
mkdocs.py
Genera docs/index.html (inglese) e docs/it.html (italiano) per GitHub Pages dai sorgenti in docs/src/.

6Mappa della memoria (exe italiano)

Offset relativi all'oggetto. Tra parentesi il nome nel disassemblato inglese. Tutti trovati per firma nel build.

Oggetto 2: dati

OffsetNomeSignificato
0x16F8STR_BASEbase degli offset dei nomi di leghe e coppe
0x71C8dseg_C707Elista delle competizioni internazionali (45 voci) → spostata in obj1
0x8032italyTablestruttura lega Italia originale (42 B) → sostituita da una in obj1
0x8C66competitionsTable256 puntatori per nazione/continente
0x9122southAmericaTabletabella del Sudamerica → spostata in obj1
0x96D6careerFileBufferinizio del salvataggio carriera (offset 0 del .CAR)
0x16E20selTeamNumbersquadra del giocatore (byte basso = file, alto = indice)
0x16E26dseg_D6CDCdivisione del giocatore
0x17FB2someLeaguesTable2000 byte, uno per numero globale di squadra (promozioni/retrocessioni); offset 0xE8DC nel .CAR
0x18DF4dseg_D8CAApuntatori ai contest dei 5 slot (+4 per slot, sezione 11)
0x18E08dseg_D8CBEtipo di coppa europea del giocatore (trofeo, reputazione, premio)
0x18E0Adseg_D8CC0tabella posizioni dei play-off (slot 4)
0x18E54 / 0x199A6 / 0x1A22C / 0x19DE9buffer slot 1 / 2 / 3 / 40x443 byte ciascuno
0x1F640competitionFileBufferbuffer slot 0 (il campionato)
0x1F67Ddseg_DF533regole sostituzioni del campionato (3 word: 5, 2, 8)
0x20A79dseg_E092Fquale coppa è nello slot 3 (0 CC, 1 CWC, 2 UEFA; il mod aggiunge 3 Lib, 4 Sup, 5 CON; −1 nessuna)
0x315B7 …D0 D1 D2 … D7 A0 … A4pseudo-registri: D0 0x315B7, D1 0x315BB, D2 0x315BF, D7 0x315D3, A0 0x315D7, A1 0x315DB, A2 0x315DF, A3 0x315E3, A4 0x315E7
0x4EFF3DIY_competitionStartcontest "di lavoro": le routine copiano qui uno slot, lo elaborano e lo ricopiano
0x5953CleaguesTableCopycopia di someLeaguesTable usata a fine stagione (checksum!)
0xC058Bdseg_18070Fvincitore della Coppa Campioni (detentore); il mod lo usa anche per la Libertadores

Oggetto 1: codice (routine del gioco usate dal mod)

OffsetRoutineRuolo
0x7B127cseg_8B2D3carica un contest in uno slot (D0 = slot, A0 = contest, D1, D2)
0x7D4B5cseg_8D661il club del giocatore è nella lista di questa coppa?
0x7D917cseg_8DAC3slot 4: play-off della divisione, con squadre segnaposto
0x7EB02cseg_8ECAEfine campionato: squadre vere nei play-off
0x82BA9cseg_92D55fine Coppa Campioni: vincitore → dseg_18070F
0x2915FGetCurrentSeasonPointerrecord della stagione (nomi delle competizioni a +1Ah, +1Eh…)
VA 0x91345assert 80 squadrequello che bloccava il gioco nel 1997
0xA09D0(nuovo)struttura lega Italia a 4 divisioni (70 B)
0xA1000–0xA1932(nuovo)pagina aggiunta: coppe SA, Intercontinentale, tabelle, codice carriera e fine stagione

7Italia a 4 divisioni

Il blocco di fine stagione del 1997

Ogni squadra ha un numero globale (0–1999) usato come indice in someLeaguesTable. L'Italia partiva da 424 con solo 51 numeri: aggiungere squadre sconfinava nella Lettonia (475). Le squadre in più collidevano, e a fine stagione l'assert cseg_91428 (80 squadre nelle liste europee, contatori 16/32/32) finiva in int 3; jmp $. Nel 1997 il mod originale inseriva byte fra la struttura dell'Italia e quella della Lettonia: circa 50 strutture di leghe raggiunte da puntatori fissi venivano lette sfasate e il gioco si bloccava.

La soluzione: la base dell'Italia passa a 1850 (numeri 1794–1999 liberi) nella tabella teamsCountryNumbers, e la nuova struttura della lega va nella cavità di obj1, con il fixup di italyTable reindirizzato (e allargato da 16 a 32 bit).

Struttura di una lega

header 10 byte: [num, type, country, start, end, nameOff, 0, 0, 0, nDiv]
02 03 35
per divisione 6 byte: [squadre, promosse dirette, squadre play-off promozione, retrocesse, play-off retrocessione, offset dati play-off]
00
per divisione 2 dword: offset dei nomi (da STR_BASE)

Il mod usa DIVISIONS in patch.py: A (18, 0, 0, 4), B (20, 4, 0, 4), C1 (18, 4, 0, 4), C2 (18, 4, 0, 0). L'Inghilterra ha già 4 divisioni con lo stesso formato: il motore gestisce 4 livelli senza modifiche al codice.

Il byte "offset dati play-off" (+12h dal record di divisione, visto dal codice) è quello che decide se lo slot 4 ospita i play-off. Nel mod è sempre 0: nessuna divisione italiana ha play-off.

8Squadre e rose C1/C2

TEAM.020

Il file inizia con un word big-endian (numero squadre) seguito da record da 684 byte. Nel record: +1 ordinale, +2 numero globale (big-endian), +5 nome (17 B), +25 divisione (0 A, 1 B, 2 C1, 3 C2; 4 era "non-league"), +26 prima maglia (5 B), +31 seconda maglia, +36 allenatore (23 B), +60 ordine dei 16 giocatori in formazione, +76 16 giocatori da 38 byte.

Byte giocatoreContenuto
+0nazionalità (18 Italia, 13 Germania, 68 Argentina, 109 Gabon…)
+2numero di maglia
+3nome (23 B, terminato da zero: massimo 22 caratteri)
+26bit 5–7 ruolo (0 G, 1 RB, 2 LB, 3 D, 4 RW, 5 LW, 6 M, 7 A), bit 3–4 carnagione (00 chiara, 10 scura, 01 rossa)
+28…+317 abilità a nibble: passaggio, tiro, colpo di testa, contrasto, controllo, velocità, finalizzazione. Il bit 3 del nibble segna un'abilità principale; il valore reale è v & 7
+32prezzo (indice 0–49: 25K … 15M+). Il portiere non ha abilità: il prezzo è la sua bravura

Il file finale ha 74 squadre (A 18, B 20, C1 18, C2 18), ordinate alfabeticamente, e rimane sotto il limite di circa 93 squadre per nazione (teamFileBuffer da 64014 B). Poiché l'ordine cambia rispetto all'originale, remap_italian_cup_teams corregge gli indici dei club italiani nelle liste delle coppe europee (per nome).

Come si costruisce un club (c1c2.real_team)

  1. Rosa reale dalla pagina it.wikipedia "<club> 1996-1997" (sezione rosa), divisa in P/D/C/A. I titolari noti vanno per primi; gli altri seguono l'ordine della pagina.
  2. Si sceglie come modello uno dei 13 club non-league originali con un mix di ruoli compatibile (G/D/M/A contati sulle 16 posizioni), a caso fra i migliori.
  3. Ogni posizione del modello riceve il prossimo nome del ruolo corrispondente; se il ruolo è esaurito si prende da un ruolo vicino. Per i club che esistevano già, un ruolo mancante (per esempio il secondo portiere) si riempie con un giocatore originale SWOS di quel club.
  4. Abilità e prezzi del modello si spostano di passi interi verso un obiettivo di prezzo medio ricavato dalla classifica reale (1 passo di abilità = 2 unità di prezzo, limite −3…+2). C1: 15.5–19; C2: 12–15; la Serie B originale ha media 17.9.
  5. I nomi già presenti nel file (trasferimenti a metà stagione) vanno in fondo alla lista del ruolo, per evitare doppioni. Nomi oltre 22 caratteri: iniziale del nome (M. DAL COMPARE).
Per cambiare una rosa basta modificare c1c2_rosters.py (l'ordine decide chi entra nei 16) e rilanciare il build. Per cambiare la forza di un club: TARGET.

9Struttura delle competizioni

[0]  id contest          [1] tipo: 1 eliminazione diretta, 2 gironi + eliminazione
[2]  nazione (FF = qualsiasi)
[3],[4] mesi: 8 × mese contato da gennaio del primo anno della stagione (≥ 12 = anno dopo)
[5]  nomi a +5+[5]: due dword, offset da STR_BASE (stringhe nell'oggetto 2!)
[7]  lista squadre a +7+[7]: coppie di byte (numero file TEAM, indice)
[8]  1 = lista fissa
tipo 1: [10] numero squadre, [14..] un byte per turno (0x94 andata e ritorno, 0x14 finale secca), poi 00
tipo 2: [15] numero squadre, gironi da 4
IdCompetizioneFormatoMesi
0x6CCOPA LIBERTADORESclone della Coppa Campioni: 16 squadre, 4 gironi da 4, poi eliminazionecome le coppe europee (settembre–maggio)
0x6DSUPERCOPA16 squadre, andata e ritorno, finale secca (clone della Coppa delle Coppe)idem
0x6ECOPA CONMEBOLidemidem
0x6FCOPPA INTERCONTINENTALEtipo 1, 2 squadre, un turno 0x14 (finale secca); default Juventus – River Plate (Tokyo, novembre 1996)0x58–0x60: dicembre, finestra fino a gennaio

Id liberi: 0x70–0x7B (le leghe usano 0x24–0x6B, 0x7C le personalizzate, 0x7D–0xB1 le coppe). Le squadre sudamericane vengono da TEAM.043 (ARG), 045 (BOL), 046 (BRA), 048 (CHI), 049 (COL), 050 (ECU), 064 (PAR), 065 (PER), 071 (URU), 077 (VEN).

10Tabelle continentali e vista mondo

ViewWorldMenu → ChooseCompetitionMenu → SelectTeamsFinalMenu: ogni puntatore nella voce del continente di competitionsTable diventa un pulsante. Le coppe europee non hanno un trattamento speciale, quindi basta aggiungere puntatori.

Scegliendo una competizione, cseg_3AA17 controlla se è uno dei 4 slot del giocatore (stato reale). Altrimenti cseg_3ABB2 la rigenera al volo: seme casuale = id del contest << 8 | stagione, poi simula fino alla data corrente. Le competizioni in cui il giocatore non c'è non hanno stato persistente: per mostrarle basta che esistano struttura e lista squadre.

11Carriera: i 5 slot

In carriera ogni squadra ha 5 slot di competizione. Per ogni slot il gioco tiene un puntatore al contest (dseg_D8CAA + 4 × slot, 0 = vuoto) e un buffer da 0x443 byte con la competizione in corso. Il calendario scorre gli slot e gioca le partite della settimana.

SlotPuntatoreBufferUso originaleUso nel mod
00x18DF40x1F640campionatoinvariato
10x18DF80x18E54coppa nazionaleSupercopa per i club sudamericani (nessuna coppa nazionale in Sudamerica)
20x18DFC0x199A6coppa di lega, oppure amichevoli e tournéeIntercontinentale se lo slot è libero (Italia, tutti i paesi sudamericani)
30x18E000x1A22Ccoppa europeaanche Libertadores / Supercopa / CONMEBOL
40x18E040x19DE9play-off della divisioneIntercontinentale per i paesi con coppa di lega, se la divisione non ha play-off

Intestazione del buffer

+0 tipo (word), +27h puntatore al nome, +2Bh numero dello slot (word), +2Dh id del contest, +3Dh/+3Fh/+41h regole sostituzioni (5 in panchina, 2 cambi, 8).

cseg_8B2D3 (carica un contest in uno slot)

Un sesto slot è stato valutato e scartato: i puntatori degli slot sono seguiti subito da altre variabili, ci sono circa 250 riferimenti in circa 100 routine, e cambierebbe il formato del salvataggio.

12Inizio stagione

ContinueCareerNextSeason (e InitCareer) chiamano InitializeNewSeason: slot 0, 1, 2 dalla tabella della nazione, poi la catena delle coppe europee (cseg_8D661 con D7 = 0, 1, 2 su Coppa Campioni, Coppe, UEFA), infine call cseg_8DAC3 (slot 4, play-off).

flowchart TD
  A[InitializeNewSeason: slot 0, 1, 2] --> B{Club in una coppa europea?}
  B -- sì --> F[euro_found: int_step, poi E092F = D7]
  B -- no --> I[init_sa]
  I --> I1{Blocco salvato 'S2' o 'SA'?}
  I1 -- no, carriera nuova --> I2[DEFAULTS in liste + INTLIST, lists_out]
  I1 -- sì --> I3
  I2 --> I3{Slot 1 vuoto e club nella Supercopa?}
  I3 -- sì --> I4[Supercopa nello slot 1]
  I3 -- no --> I5
  I4 --> I5[int_step: Intercontinentale nello slot 2 se libero e club finalista]
  I5 --> I6[Slot 3: Libertadores, Supercopa se non già in slot 1, CONMEBOL; D8CBE = D7 - 3]
  F --> D[call cseg_8DAC3 dirottata su int4_step]
  I6 --> D
  D --> D1[cseg_8DAC3: play-off in slot 4 se la divisione li ha]
  D1 --> D2{Slot 4 vuoto, club finalista, Intercontinentale non in slot 2?}
  D2 -- sì --> D3[Intercontinentale nello slot 4 con lo scambio dei buffer]
  D2 -- no --> Z[fine]
  D3 --> Z

Intercontinentale nello slot 4: lo scambio dei buffer

Caricare la coppa direttamente nello slot 4 darebbe una partita "tua squadra contro tua squadra" senza date. int4_step invece:

  1. chiama il vero cseg_8DAC3; se lo slot 4 è occupato (play-off) si ferma;
  2. azzera l'id del buffer 4 (così un vecchio buffer non viene ripristinato al caricamento);
  3. copia il buffer dello slot 2 (coppa di lega) nel buffer 4;
  4. chiama cseg_8B2D3 con slot 2: l'Intercontinentale viene costruita completa, con le finaliste e le date;
  5. scambia i due buffer da 0x443 byte: Intercontinentale nello slot 4, coppa di lega di nuovo intatta nello slot 2;
  6. +2Bh = 4, regole sostituzioni dal campionato (come fa il gioco per i play-off), ripristina il puntatore dello slot 2, D8CBA = INT.

L'aggancio playoffs all'ingresso di cseg_8ECAE esce subito quando lo slot 4 contiene l'Intercontinentale: altrimenti a fine campionato il gioco la ricostruirebbe leggendo una tabella dei play-off inesistente.

13Caricamento di un salvataggio

ProcessCareerFile ricostruisce i puntatori degli slot dalla tabella della nazione (slot 0–2), dal record della divisione (slot 4) e da E092F (slot 3). I buffer degli slot sono nel salvataggio, i puntatori no: il mod li ripristina guardando l'id nei buffer.

14Fine stagione

cseg_91428: combinazione delle liste, per la nazione del giocatore e poi per ogni nazione europea simula o legge la classifica (cseg_9153F), promozioni, coppe; cseg_92D55 / 9307A / 9339D chiudono Coppa Campioni, Coppe e UEFA; cseg_93974 aggiunge le qualificate; assert finale 80 squadre (16/32/32); cseg_92BBF riammette i detentori.

Il mod dirotta la chiamata a cseg_92D55 (obj1+0x812C4) su sa_qualify:

  1. [A4] = Libertadores e call cseg_92D55 + 10 (salta la prima istruzione che imposterebbe la Coppa Campioni): chiude la Libertadores (giocata dal giocatore o simulata con cseg_3ADAE) e scrive la vincitrice in dseg_18070F → LIBWIN.
  2. Per ogni nazione sudamericana: simula la massima divisione con cseg_915ED (la stessa simulazione della vista mondo, così le classifiche coincidono). Per la divisione del giocatore legge invece lo slot 0. Dal DIY: +6Dh offset delle righe in ordine di classifica (righe da 12h byte), +12Dh numero squadra per riga.
  3. Scrive le qualificate: Libertadores = campioni dei 10 paesi + seconde di ARG, BRA, URU, PAR, COL, CHI (4 gironi misti, LIB_Q); CONMEBOL = piazzamenti successivi (CON_Q, 8 coppie). Supercopa: lista fissa di ex campioni.
  4. Regole della detentrice (reali 1997): se LIBWIN non è già in Libertadores prende il posto della seconda del suo paese (altrimenti CHI2), e la squadra esclusa prende il suo posto in CONMEBOL; se non è in Supercopa, ne prende l'ultimo posto.
  5. call cseg_92D55 vero: chiude la Coppa Campioni. Entrambi i rami finiscono in cseg_92F1C, che scrive sempre il vincitore in dseg_18070F.
  6. INTLIST[0] = [dseg_18070F], INTLIST[1] = LIBWIN (ciascuno saltato se 0FFFFh), poi lists_out e ret.
flowchart LR
  S[cseg_91428 fine stagione] --> Q[sa_qualify]
  Q --> L[Fine Libertadores: LIBWIN]
  L --> C[Classifiche SA → liste Lib e CON]
  C --> H[Regole detentrice]
  H --> E[cseg_92D55 vero: vincitrice CC]
  E --> N[INTLIST = CC, LIBWIN]
  N --> O[lists_out: blocco salvato + checksum]
  O --> R[ritorno a cseg_91428]

15Persistenza nel salvataggio

Il salvataggio contiene someLeaguesTable (2000 byte, uno per numero globale). I numeri 1730–1849 non appartengono a nessuna squadra: il mod ci parcheggia i suoi dati dal numero 1740 (obj2+0x1867E), con una copia speculare in leaguesTableCopy (obj2+0x59C08), perché a fine stagione il gioco copia una tabella sull'altra (cseg_8CC0A e cseg_8CC4E). InitCareer azzera la tabella: una tabella a zero significa carriera nuova.

OffsetFormato S2 (attuale)Formato SA (sessione 12)
+0marcatore 'S2' (word 0x3253)marcatore 'SA' (0x4153)
+2Libertadores: 16 × (file, indice)idem
+34Supercopaidem
+66CONMEBOLidem
+98Intercontinentale: 2 × (file, indice)byte di bilanciamento
+102byte di bilanciamento—

Il byte di bilanciamento vale −(somma dei byte precedenti del blocco) mod 256, così la somma dell'intero blocco è 0 e quella della tabella resta 0 per il controllo di cseg_9487A. Lettura: 'S2' tutto dal salvataggio; 'SA' le tre liste più la coppia di default Juventus – River; nessun marcatore → valori del 1997 (DEFAULTS, 100 byte). Il primo fine stagione di una carriera 'SA' la riscrive in 'S2'.

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

16Trofei e reputazione

I bit dei trofei si costruiscono in cseg_39392 dai flag D6C64 (Coppa Campioni, bit 2, reputazione +100), D6C68 (UEFA, bit 3, +50) e D6C66 (Coppe, bit 4, +50). cseg_3D7CB sceglie il flag da dseg_D8CBE, cseg_5DEB9 il premio. Il mod imposta D8CBE = D7 − 3 quando entra una coppa sudamericana:

Il riepilogo della stagione (ManagementRecordMenu) mostra già il nome corretto ("COPA LIBERTADORES - WINNERS"). Nomi e bit dedicati (i bit 10–15 sono liberi) richiederebbero nuove stringhe, ma il pool è pieno.

17Tabella degli agganci

Ogni aggancio sovrascrive istruzioni originali con un jmp o cambia il bersaglio di una call; i fixup delle istruzioni sovrascritte vengono rimossi.

Sito (obj1)ByteOriginaleDiventa
0x7D30711mov [E092F],-1; jmp done (nessuna coppa europea)jmp init_sa
0x7D31212mov ax,[D7]; mov [E092F],ax (coppa europea trovata)jmp euro_found
0x7D31Erel32call cseg_8DAC3call int4_step
0x237C110cmp [E092F],-1; jz out (ProcessCareerFile)jmp load_slot1
0x237FD12cmp [E092F],2; jz map; jmp outjmp load_slot3
0x7EB0213cmp dword [D8CBA],0; jz out (cseg_8ECAE)jmp playoffs
0x812C4rel32call cseg_92D55 (fine stagione)call sa_qualify

Modifiche ai dati: base Italia nella tabella dei numeri globali; fixup di italyTable reindirizzato alla nuova struttura; tabelle Sudamerica, Europa e lista internazionale reindirizzate in obj1; nomi nuovi nel pool di stringhe; indici dei club italiani nelle liste europee; vsize e flag dell'oggetto 1.

Gli indirizzi delle etichette nella pagina aggiunta (init_sa, sa_qualify…) cambiano quando il codice cresce: l'output di patch.py ne stampa i principali, e nasmcave.labels() li restituisce tutti.

18Verifica senza giocare

Prima di consegnare una build per le prove, questi controlli vengono fatti ogni volta:

  1. Il build passa tutti gli assert (firme uniche, bersagli attesi).
  2. Rilettura con le.py: le tabelle dei fixup si analizzano senza errori.
  3. ndisasm del codice generato e di ogni sito di aggancio: ogni salto va all'etichetta giusta.
  4. Ogni operando assoluto ha un fixup verso l'oggetto e l'offset corretti (lo script annota ogni riga del disassemblato con i fixup).
  5. Confronto con la build precedente: i byte cambiano solo nei punti voluti; i fixup cambiati fuori dalla pagina aggiunta devono essere solo quelli attesi.
  6. Per i dati salvati: emulazione in Python della somma di controllo su una copia di un salvataggio vero.

Se un controllo fallisce e non si risolve, si rimette la build precedente (ITALIAN_S12.EXE è l'ultima provata a fondo).

19Limiti e questioni aperte

20Storico e prossimi passi

SessioneContenutoProva
1–2Strumenti LE; causa del blocco del 1997; Italia a 4 divisioni con base 1850passata
5Club reali C1/C2 1996-97passata
9Coppe sudamericane nei menu e nella vista mondo; pool di stringhepassata
10Club sudamericani nelle coppe in carriera; qualificate dalle classifiche; detentricepassata
11Supercopa nello slot 1; pagina aggiunta all'oggetto 1passata
12Persistenza delle liste con checksum; scambio CONMEBOL; trofei; Intercontinentale nei menu e nello slot 2persistenza, slot 2, CONMEBOL trofei
13Finaliste dell'Intercontinentale a fine stagione; formato S2; Intercontinentale nello slot 4fine stagione, S2 slot 4
14Rose reali 1996-97 e forza calibrata per tutti i 36 club di C1/C2passata

Roadmap

  1. Prove delle sessioni 12–14 e correzioni.
  2. Port su ENGLISH/FRENCH/GERMAN.EXE dello stesso CD. Da rendere generici: STR_BASE, gli offset degli pseudo-registri scritti nelle espressioni regolari di sacups.py, le stringhe sorgente del pool (testi italiani), i nomi visualizzati, gli indirizzi degli slot (tutti trovati per firma: da ricontrollare uno per uno).
  3. Patcher in un unico file HTML/JS che distribuisce solo differenze: l'utente sceglie i propri file originali, il patcher controlla gli md5, applica le differenze e scarica i file modificati; README in italiano e inglese; per disinstallare si tengono gli originali.
  4. Dopo la 1.0: panchina da 5 per le squadre inglesi; prova dell'Intercontinentale nello slot 4; C1 su due gironi, dopo una sessione di sola analisi.

Crediti: disassemblato e documentazione dei formati da swos-port (zlatkok). Mod: Davide Lorigliola, con Claude.