I began reverse-engineering the Amiga version of Sensible World of Soccer (SWOS) in August 2025. My goal is to produce a faithful, well-documented understanding of the original game's codebase and data formats so that bugs can be analyzed, features can be studied, and future ports or tooling can build on a solid foundation. Although the target is the Amiga build, this work stands on the shoulders of prior DOS research - most notably Zlatko Karakas (@zlatkok), whose IDA Pro database of the DOS version provided a treasure trove of function/variable names, comments, and structures that I carefully adapted to the Amiga binary.
The workflow blends IDA Pro for disassembly with Ghidra for decompilation. While IDA excels at organizing symbols and navigation, only Ghidra currently offers a practical m68k to C decompiler, so I import the IDA database into Ghidra using the community plugin. To make this round-trip smoother and the output more accurate, I modified that plugin to improve export fidelity and symbol transfer. From there, I rely on a suite of custom Python scripts to lift the overall quality of the decompilation: normalizing calling conventions, taming aggressive casts, teasing apart union-like data access, and annotating engine-specific patterns that recur throughout the code.
Automated decompilation can only take you so far. SWOS - like many performance-tuned 90s games - leans on tight register juggling, overlapping data layouts, and hand-written assembly tricks that don't cleanly map to high-level constructs. After a certain point, manual reconstruction becomes essential: refactoring control flow into meaningful functions, elevating magic numbers into enums and constants, and reconciling ambiguous pointer arithmetic with real engine abstractions.
Looking ahead, I plan to use AI-assisted cleanup to accelerate the most tedious parts of this process - e.g., proposing types for "undefined" temporaries, surfacing likely structure fields, and suggesting clearer function boundaries - while keeping a human in the loop for all correctness and style decisions. The aim isn't to auto-port the game, but to produce readable, reviewable C that reflects the original intent of the Amiga code, informed by DOS symbols where appropriate and verified against observed behavior.
PC version of SWOS was written as a conversion from Amiga. It can be deduced by analyzing the code, and there are even some left over strings such as `INSERT BLANK DISK IN DF0:`. There are 15 memory locations (dwords) that are used as registers. Their start is at 0x3146d. These dwords correspond to Amiga s MC68000 processor registers: data registers D0-D7 and address registers A0-A7
This is not a mistake (15 locations for 16 registers) because register A7 (stack pointer) is mapped directly to Intel's esp register. This means that PC version of SWOS is using so called static emulation, where whole program is translated into host machine language and then executed. Other type of emulation is dynamic emulation, where an interpreter is used that interprets original code run-time (this is the way most emulators operate).
Since SWOS is a commercial game, it was more feasibile to use the first approach. I think they did the following: first Amiga s source code (which I believe was mostly written in assembler, or some optimizing C compiler - somebody correct me if I'm wrong) was converted into C code, probably using some third party utility. Than they rewrited some heavily used routines completely in x86 assembler for speed, and rewrited non-portable routines (file handling, input handling, video) to get fully running x86 executable. Compiler used to put all this together was Watcom C - sw(o)s.exe contains whole Watcom run-time library, both 16 and 32-bit versions.
- @Playaveli
More info about the DOS version here
In the early 2000s, Zlatko Karakas and his cousin got hooked on Sensible World of Soccer (SWOS) but hit a snag: no two-player keyboard support. Fresh from learning machine code and taking a compiler course, Zlatko reverse-engineered the input routines and hacked in two-player keyboard play. Hex editing soon hit its limits, so in 2004 he created SWOS++ - a tiny loader patched into the game that brings in a separate SWOS++ binary at runtime (DLL-style). That worked well until DOS constraints capped progress, leading to swos-port, a cross-platform re-creation.
The workflow evolved from raw hex patches to NASM/MASM x86 assembly with advanced macros, then gradually to C, and ultimately C++ for growing systems like multiplayer. Zlatko's toolkit included IDA Pro, hex editors, NASM/MASM, Perl scripts, and piles of notes and diagrams. Exploration of SWOS formats also produced SWPE (SWOS Picture Editor). As knowledge accumulated, "static emulation" became feasible - mixing emulated chunks of the original with manually decompiled parts and new code.
Technically, swos-port starts from a full disassembly (one big .asm), converted to C++ with x86 and 68k registers modeled as variables and carefully controlled memory access to preserve original assumptions. Some code is removed or replaced, external variables are introduced, and the generated core is linked with hand-written input, graphics, and other systems. It's a long-running side project pursued in bursts (often between jobs), adding up to several years of effort. A key inspiration for making SWOS++ a proper add-on was Swedish Yankspankers' TA Demo Recorder for Total Annihilation, which showed how powerful reverse-engineered enhancements could be.
- Sensible World Of Soccer '96/'97
SENSIBLE WORLD OF SOCCER.
Copyright (C) 1994/1995 Sensible Software (6/11/1996).
Copyright (C) 1994/1995 Chris Chapman (6/11/1996)
SWOS VERSION 152.
(06/11/96 13.55) ASM 6/11/1996
- Coder: Chris Chapman (Chipper)
- Graphics:
- Jonathan Hare (Johnathan Hare, Jovial Jops)
- Stoo Cambridge (Stuart Cambridge)
- Audio:
- Richard Joseph
- Jonathan Hare (Johnathan Hare, Jovial Jops)
- Alex Joseph
- Graham King
- Jason Page
- Jacks Read
- Audio System
- https://www.exotica.org.uk/wiki/Richard_Joseph_(format)
- Richard Joseph (format) / Vectordean format
- RJP1
CODED BY ANDI SMITHERS, VECTORDEAN (C) MILLENNIUM 1992
The adf's used in this project are:
- Sensible World of Soccer 96-97 (1996)(Renegade)[cr HF](Disk 1 of 2).adf
- Sensible World of Soccer 96-97 (1996)(Renegade)[cr HF](Disk 2 of 2).adf
| Section Name | Start Address | Length | Permissions |
|---|---|---|---|
| CHIP_RAM | 0x000000 |
0x200000 |
rwx |
| CIAB | 0xBFD000 |
0x1000 |
rw |
| CIAA | 0xBFE000 |
0x1000 |
rw |
| AMIGA_CUSTOM | 0xDFF000 |
0x200 |
rw |
| KICK_ROM | 0xF80000 |
0x80000 |
rx |
This is a memory map for an Amiga system showing key address ranges and how they can be used:
- CHIP_RAM: Main shared RAM the CPU and custom chips (Agnus/Denise/Paula) can access - for code, data, and graphics/audio buffers.
- CIAB and CIAA (
0xBFE000-0xBFEFFF, 4 KB,rw): The two Complex Interface Adapters - memory-mapped I/O registers for timers, keyboard/joystick, serial/parallel, etc. - AMIGA_CUSTOM: Custom-chip register block (blitter, copper, bitplanes, audio, sprites, interrupts).
- KICK_ROM: The Kickstart ROM with the OS core; readable/executable but not writable.
Permissions indicate whether each region is readable (r), writable (w), and/or executable (x).
NOTE: If you don't have IDA Pro skip to Ghidra Setup and use the SWOS2.xml file that's already exported from IDA Pro.
Here are some useful shortcuts:
uundefineyEnter "C-type"gJump to label / addressdCycle data typenEdit name:Edit commentESCJump back to previous location (used ALOT)CTRL-FText searchCTRL-F1Local types- Local Types - View > Open subviews > Local Types (or Shift+F1)
- Place the cursor at the start (001796BC)
- Cycle using
d - Press * (asterisk) repeatedly to define the rest as an array:
- Or: Press Alt+F2 and type 256 to define exactly 256 elements
- Cycle using
- Edit > Other > Reset decompiler information, then selecting (only) All caches
- Codesegment data directives
db= define byte (1 B)dw= define word (2 B)dd= define dword (4 B)dq= define qword (8 B)
- Pointer sizes in 16-bit DOS:
Near(offset-only) = 2 BFar(segment:offset) = 4 B
find_undefined_filebacked.py scans your IDA database for contiguous ranges of file-backed bytes (i.e., from the original file, not BSS) that are still undefined (no code/data item) and prints each run as START_ADDRESS len=BYTES to the Output window; this makes it easy to spot "holes" you need to define (with dc.b/w/l, arrays, or blk.b) so your re-emitted assembly preserves the original binary's size, layout, and addresses for true 1:1 rebuilds.
Example output:
Undefined (unknown) file-backed data runs:
0x00140ca3 len=1
0x001448f2 len=2
0x00145360 len=2
0x00145364 len=38
0x001473c4 len=24
0x001473de len=10
0x001473ea len=16
...
0x00156ce2 len=1
0x00156ce6 len=1
0x00156cec len=1
0x00156cf0 len=1
0x00156cf4 len=1
0x00156cf8 len=1
0x00156cfd len=1
export_sparse_asm.py is an IDA 9-friendly "sparse assembly" exporter that writes a clean, readable .s file containing just the interesting heads (code/data items), labels, prototypes, and comments - without dumping every single byte. It iterates segments in address order (optionally skipping Amiga MMIO segments like AMIGA_CUSTOM, CIAA, CIAB), emits an ORG 0x... at each segment start and again only when there's a real hole of unknown bytes, and then walks heads to print one disassembly line per item with a fixed indent. For functions, it prints a labeled header, a prototype reconstructed from the function's tinfo (or a sensible fallback like void name(...)), and both regular and repeatable function comments. Inside functions it can also emit intra-function code labels (e.g., loc_...) and data labels so that cross-references stay meaningful.
A key feature is robust comment handling for IDA 9's wrapped/overflow comments. The script captures the base disassembly line (optionally keeping EOL comments), then collects indented/anterior and extra posterior/anterior comment blocks, and deduplicates any lines that merely repeat the EOL text - so you don't get the same comment echoed three times. It also pads EOL semicolons for neat formatting, and avoids ORG spam by detecting genuine unknown-data gaps rather than interleaved items. The result is a compact, navigable assembly listing that's ideal for code review, porting, or feeding into documentation - focused on structure (labels, prototypes, comments) rather than raw bytes.
export_sparse_asm.py is an IDAPython menu beautifier for the Amiga build of SWOS - a faithful port of Zlatko Karakaš's sws_menu_x86.idc, adapted to the Amiga menu data layout and IDA 9.x APIs. You run it with the cursor on a menu table and it non-destructively formats the raw menu blob into readable fields, without renaming any symbols. It systematically converts bytes/words/dwords into proper data items, applies offset rendering for function pointers (op_off), decimal/signed rendering for numeric fields (op_dec + sign fix for negative literals), and annotates everything with clear comments so the structure is self-documenting in IDA.
The script recognizes the menu header (four dwords: init/return/draw/default entry), then walks entries until the sentinel -999 (0xFB39) "end menu", handling start menu (-998), template mode (-997) and coordinate mode switches (0xFFFE=wide, 0xFFFF=byte). In template or entry blocks it parses the standard x,y,width,height coordinates (byte or word based on mode), and then decodes a stream of typed chunks:
1invisible entry;2navigation links (left/right/up/down);3custom background proc (pointer);4entry+frame color flags (pretty-printed to human color names);5/9/21sprite background/foreground/copy;7/15/16/13/14function hooks (foreground, before/after draw, on-select, input mask);8/10/11/12/22string/text forms (with string flags: color, font size, alignment; tables and multi-line pointers), and small two-byte "skip" args for17-20. It also prints a "control mask" explanation for input flags (fire/up/down/left/right variants). Throughout, it guards against accidental mis-signing (only toggles signed display when a negative literal is actually present), and it uses IDA's current types/comments where possible. Net effect: messy menu data becomes a clean, browsable structure with comments and correct renderings, tailored to Amiga's formats while preserving the original names.
The XML Exporter for IDA version 9 has been modified to achieve better export results.
- Copy xml_loader.py to
%PROGRAMFILES%/IDA Professional 9.0/loaders/ - Copy xml_exporter.py to
%PROGRAMFILES%/IDA Professional 9.0/plugins/ - Copy xml_importer.py to
%PROGRAMFILES%/IDA Professional 9.0/plugins/ - Copy idaxml.py to
%PROGRAMFILES%/IDA Professional 9.0/python/3/
This patch makes IDA's Ghidra-export XML far more "Ghidra-friendly," with cleaner type strings, real register-storage metadata, and correct array/size descriptions - so your downstream Ghidra import needs less guessing and fewer fix-ups. It adds a normalization layer (normalize_for_ghidra) that rewrites MS/IDA type spellings into the shorter names Ghidra expects (e.g., unsigned __int8 to uchar, strips struct/union/enum/class tags, removes calling-convention/attr tokens like __usercall, __cdecl, __ptr64, etc.), tidies whitespace, and preserves pointer spacing. Where available, it now prefers actual IDA tinfo declarations at addresses (get_decl_from_tinfo_at) for <DEFINED_DATA> so arrays and pointers (e.g., Type *[32]) are exported as a single, accurate declaration, and then normalizes those strings before writing them to XML.
Function export gains much richer calling-convention detail without over-asserting: it only marks CALLING_CONVENTION="__usercall" (and CUSTOM_STORAGE) if the IDA prototype text truly contains __usercall. It also lifts register-backed parameters and returns from IDA 9's func details and emits them as <REGISTER_VAR> elements and <RETURN_TYPE> with a concrete register name, including width-aware mapping on 68k data registers (D0W, D0B, etc.). If IDA says the function returns void, the exporter forces <RETURN_TYPE void/> so Ghidra won't infer a spurious D0 return. On the data/UDT side, export_members() is rewritten to properly handle multi-dimensional arrays (e.g., byte[10][35]) by walking nested array types, computing the true total size, and keeping the full bracket sequence in the XML. get_member_type() now prefers typed pointer declarations (e.g., TeamGeneralInfo *) and UDT names instead of generic flags, and all member/datatype strings pass through the same normalization to keep names consistent. Finally, a few new XML tokens (e.g., PRESERVED_REGISTERS, REGISTER_ELEM) are introduced for future use, and scattered writes now route through normalization - yielding exports that import cleanly into Ghidra with fewer post-processing scripts and much better fidelity for pointers, arrays, registers, and calling conventions.
You can view the full patch @ idaxml.patch
Main disassembly view in IDA Pro 9.0 showing the loaded SWOS Amiga binary.
IDA Pro 9.0 menu showing how to access the XML Exporter plugin.
When you run the plugin you should see the following output:
XML Exporter v5.0.2 : SDK 900 : Python : Sep 24 2025 18:40:45
-----------------------------------------------------------
Exporting XML <PROGRAM> document ....
Processing PROGRAM CPU time: 0.2380
Processing DATATYPES CPU time: 0.0946
Processing MEMORY_MAP CPU time: 5.0945
Processing CODE CPU time: 2.4486
Processing DATA CPU time: 4.0267
Processing COMMENTS CPU time: 8.8220
Processing SYMBOL_TABLE CPU time: 3.4779
Processing FUNCTIONS CPU time: 0.4861
Processing MARKUP CPU time: 1.2152
Total CPU time: 26.1107
--------------------------------------
PROGRAM 1
INFO_SOURCE 1
PROCESSOR 1
COMPILER 1
DATATYPES 1
STRUCTURE 26
MEMBER 375
REPEATABLE_CMT 379
UNION 3
ENUM 23
DISPLAY_SETTINGS 23
ENUM_ENTRY 532
MEMORY_MAP 1
MEMORY_SECTION 5
MEMORY_CONTENTS 1
CODE 1
CODE_BLOCK 252
DATA 1
DEFINED_DATA 23325
TYPEINFO_CMT 5732
COMMENTS 1
COMMENT 18209
SYMBOL_TABLE 1
SYMBOL 5920
FUNCTIONS 1
FUNCTION 1654
ADDRESS_RANGE 1731
RETURN_TYPE 1603
REGISTER_VAR 1310
STACK_FRAME 10
REGULAR_CMT 352
MARKUP 1
MEMORY_REFERENCE 2583
MANUAL_OPERAND 1
EQUATE_REFERENCE 772
--------------------------------------
Total XML Elements: 64833
Database exported to: C:\Projects\GitHub\swos-port-c\reverse\SWOS2\SWOS2.xml
The 68000.cspec file is a customized language file for Ghidra that marks all the 68000 registers as unaffected. Because SWOS is hand written assembly we should not assume registers will be clobbered in a uniform way such as those generated by a C compiler.
- Copy 68000.cspec to Ghidra's
Ghidra/Processors/68000/data/languages/folder.
Ghidra project window open with the SWOS2 project loaded.
Selecting the SWOS2 XML export file for import into Ghidra.
Ghidra import dialog.
Prompt asking whether to run auto-analysis on the imported program.
Opening the Script Manager from the Ghidra Window menu.
Custom Amiga-related scripts visible inside Ghidra's Script Manager. Select the amiga_sanitize.py script and click the Run Script button.
The script will prompt for the same SWOS2 XML file used on import.
amiga_sanitize.py> Running...
[IDA-XML->Ghidra] Functions updated: 1652, skipped: 0 (from SWOS2.xml)
[IDA-XML->Ghidra] DEFINED_DATA applied: 23027, skipped: 0.
amiga_sanitize.py> Finished!
Ghidra's analysis options showing selected analyzers and settings. Set the following:
- Analysis Options
- Call-Fixup Installer (Disabled)
- Data Reference
- Unicode String References (Disabled)
- Non-Returning Functions - Discovered (Disabled)
- Edit > Tool Options > Decompiler > Analysis > Respect readonly flags (Enabled)
After analysis is complete the decompiled code should refresh.
Select File > Export Program... and a dialog will appear. Select the C/C++ format. Click the Options... button.
Put a tick next to the Create Header File (.h) option. Click OK, OK to output the SWOS2.c and SWOS2.h files.
- Copy the scripts in ghidra_scripts to
%HOMEPATH%/ghidra_scripts
amiga_call_graph.py is a Ghidra script that exports a reachable call graph starting from the function currently under your cursor. It does a depth-first search over direct call sites (JSR/BSR/etc.), collecting every callee it can reach, optionally filtering out externals and thunks. The functions are then sorted by entry address and emitted to a single StartFunctionName.c file. The file begins with a banner and a clean list of C function prototypes synthesized from each function's return type, calling convention (if present), and parameters - so you get a quick, compilable-looking signature map of the slice of code you care about.
Optionally, it also decompiles each reached function with Ghidra's DecompInterface (Parameter ID enabled, "decompile" simplification) and appends the raw decompiler output after the prototypes, labeling each block with the function name and entry address. Crucially, it writes the file in binary mode and preserves the decompiler's line endings verbatim to avoid mangling formatting. There's a toggle to re-run auto-analysis on the collected bodies (if your Ghidra build exposes AutoAnalysisManager), as well as configurable flags for including thunks/externals and a per-function decompile timeout. The save dialog lets you pick either a directory (it auto-names the file) or a specific filename (it adds .c if missing). In short: this script is a one-click way to harvest a coherent, address-ordered "mini-module" of prototypes plus (optionally) decompiled C for all functions reachable from a chosen entry point - perfect for staging focused refactors, code reviews, or AI-assisted cleanups.
amiga_prototypes.py is a quick, one-shot exporter for function prototypes from your current Ghidra program. It walks every discovered function in address order (the binary's natural layout), formats a C-style prototype for each (return type, optional calling convention, name, and parameters), and writes them to a single .txt file you choose. Varargs are preserved (...), parameter names and types come from Ghidra's datatypes, and if anything is missing it falls back gracefully (e.g., to int or param). The result is a clean, linear list of signatures that's perfect for building headers, searching for API surfaces, or seeding decompiled modules with consistent declarations.
A couple of toggles let you trim noise: by default it skips externals (import stubs) and thunks (wrapper functions), but you can flip those flags if you want a complete picture. Under the hood it uses the FunctionManager's forward iterator to preserve address order, and a small helper normalizes type/parameter display names so the output stays readable. When it's done, it prints how many prototypes were written and where, giving you a simple, reproducible snapshot of the program's callable surface.
amiga_sanitize.py is a two-pass IDA-XML to Ghidra importer that turns metadata from an IDA export into precise, compiler-like function signatures and correctly-typed MMIO data in your Amiga SWOS Ghidra project. Pass 1 (FUNCTIONS) reads each <FUNCTION> from the XML and applies an explicit __usercall signature with custom register storage: it maps every <REGISTER_VAR> to an actual 68k register via Ghidra's VariableStorage, sets the declared return type (and return register if given), carries over comments, and then locks the signature so later analysis won't undo it. The script also "sanitizes" types: it resolves built-ins and pointers, falls back to unsigned int or void* when a type can't be found, and avoids illegal void parameters. It normalizes register names (d0.w, a1.l to d0, a1) and wraps all updates in transactions for safety.
Pass 2 (DEFINED_DATA / MMIO) walks <DEFINED_DATA> nodes to define hardware registers as volatile scalars at their exact addresses, attaching the label from TYPEINFO_CMT. It enforces Amiga-specific rules: CIA ranges (BFDxxx/BFExxx) are forced to 8-bit types (e.g., uchar) while Custom chip (DFFxxx) keeps the XML's width (typically ushort). The helper carefully reuses existing exact data when it matches, or surgically clears only the necessary byte range to create the right unit - avoiding unnecessary bulldozing of larger containers. Overall, the script upgrades a raw IDA XML dump into Ghidra with realistic calling conventions, register-accurate prototypes, and correctly-typed volatile MMIO, giving you cleaner decompilation and fewer false "unknown/undefined" artifacts downstream.
GameLoop.c is the outer, per-frame orchestrator: it advances global timers/state machines and gates when real play can proceed. In this code snippet, the work is centered in UpdateGameTimersAndCameraBreakMode(). It increments the in-game/stoppage counters, handles penalty progression, and drives game-state transitions (kick-off, half-time, full-time, extra time, showers/results screens). It also runs the whole "camera break" pipeline (the breakCameraMode 0 to 7/8 ladder) that freezes play, positions the ball, triggers replays/highlights, waits for players/keeper states to settle, then re-arms the match. Along the way it primes or suppresses inputs depending on menus/substitutions and special states (e.g., ST_KEEPER_HOLDS_BALL). In other words, GameLoop.c is the authority on when football can actually happen and what phase of the match we're in.
UpdatePlayers.c is the core simulation loop - the "everything happens here" routine that executes the football itself once the outer loop says go. You see it being driven from UpdateAndApplyTeamControls(), which interprets input for one team per frame (alternating teams across frames), computes directional intent and fire/quick-fire/normal-fire edges from the button counters, applies menu/substitution gating, and then calls UpdatePlayers(team) to realize movement, tackles/headers, animation table switches, ball interactions, and per-sprite state updates. That same UpdatePlayers(...) can also be hit from inside UpdateGameTimersAndCameraBreakMode() during panic/recovery paths (e.g., after a break sequence) to keep the simulation coherent. Practically, this means GameLoop.c controls the cadence and high-level state of the match, while UpdatePlayers.c performs the per-frame physics/AI/control application. They're tightly coupled: UpdateGameTimersAndCameraBreakMode() is invoked first to ensure the world is in a legal state, and UpdateAndApplyTeamControls() (which calls UpdatePlayers twice per frame across the two teams) applies the inputs and updates the on-pitch actors. This layering is why both files are pivotal: one keeps the match rules, timers, and presentation consistent; the other actually plays the football.
Install the Amiga Assembly VSCode Extension and open the project located in the reverse/Amiga/SWOS folder. The export_sparse_asm.py script generates the assembly file SWOS2.s which can be assembled to create a 1:1 binary of the original.
After you install the extension you'll need to copy the Kickstart v3.1 rev 40.68 (1993)(Commodore)(A1200).rom into the following locations:
- Windows
- WinUAE:
%HomeDrive%%HomePath%\.vscode\extensions\prb28.amiga-assembly-1.8.13\dist\bin\winuae\Kickstarts - FS-UAE:
%HomeDrive%%HomePath%\FS-UAE\Kickstarts
- WinUAE:
Unfortunately I can't get FS-UAE to work as I get a Exception "Stackframe Format Error" ($38) at $939A20 (ExpMem $83A20) occurred. but WinUAE does launch.
Set your VSCode debugger to WinUAE Debug.
Click the Start Debugging (F5) to launch WinUAE and you should see SWOS running.
- Pause/Resume emulation:
Pause/Break - Toggle fast-forward (warp):
End+Pause/Break(press again to go back) - Toggle fullscreen:
Ctrl+F12 - Quit:
Alt+F4 - Settings:
F12opens the WinUAE settings dialog
hcommand Show detailed help for command- UAE Console:
Use a
$prefix to execute commands in the emulator's built-in console debugger e.g:$v -3Enable visual debugger$?Show help - Expressions:
Expressions use JavaScript-like syntax and can include literals, symbols, registers and memory values.
They can be evaluated in the console as well as in command args, watch, conditional breakpoints and logpoints.
Type 'h expressions' for more details.
@(address[,size=4])Unsigned memory value@s(address[,size=4])Signed memory value
You can download Devpac 3.14 and assemble SWOS2.s from Workbench! It's not really worth the effort to cover this but it's pretty cool! Fortunately for us vasm supports assembling m68k with the -devpac parameter to produce the same binary. So we can get a 1:1 binary of the original using VSCode instead of using an Amiga or emulator.
The WHDLoad SWOS.Slave has the following patches:
- Fixes major long-standing bugs & crashes
- Fixes a crash when changing/loading tactics (issue #0006185).
- Reworks load/save logic so tactics can be properly saved and loaded again.
- Adds a workaround for random access faults when selecting teams (not fully systematic, but reduced).
- Removes several old low-level problems (memory bus errors, divide-by-zero, CACR code, snoop bugs on most versions).
- Improves performance and memory usage on real hardware
- Makes the game use fast RAM instead of only chip RAM, removing the infamous long-time slowdown on accelerated Amigas.
- Adjusts memory requirements to 1 MB Chip + ~2.5 MB other (Fast) RAM, and fixes the config so it runs properly on 68000-68060 systems.
- Cleans up file layout & saves
- Game now looks for data files in a
datadirectory (matching how the installer copies them). - User data (seasons, leagues, careers, highlights, tactics, DIY, etc.) is saved/loaded from a
SAVEdirectory. - Allows loading larger team files (
team.xxx) without having to RNC-pack them (with some remaining size limits).
- Game now looks for data files in a
- Quality-of-life & WHDLoad integration
- Full load from hard drive ("real files", no virtual disk images).
- Quit key added (Del / WHDLoad quit support, including on 68000 in most versions).
- Provides icons (RomIcon, NewIcon, GlowIcon) and standard WHDLoad tooltype hints (NOCACHE, PAL/NTSC, etc.).
- Known original-game bug that remains
- The old SWOS quirk is still there: if you try to watch a highlight without having played a match since loading, the game can hang. You still need to play at least a moment of a match before watching highlights.
The SWOS.Slave project contains the SWOSHD.s which will assemble to SWOS.Slave.
You will need to download the NDK3.2R4.lha and extract it using a tool like 7-Zip and create an environment variable called AMIGA_NDK that contains the directory to it (Eg. C:/Projects/Amiga/NDK3.2R4). Make sure you have the Amiga Assembly VSCode Extension installed as it uses its vasmm68k_mot as the assembler.
C:WHDLoad SLAVE=DH0:SWOS.Slave DATA=DH0:data PRELOAD PreloadSize=2051816
; Quits the emulator at the end of the program
C:UAEquit
SWOS.Slave is a WHDLoad slave is a tiny loader/patcher that makes an original game run cleanly from hard drive under WHDLoad (instead of from floppies). In practice, the slave:
- Declares runtime requirements (chip/fast memory, version, flags) via the WHDLoad slave header, and exposes a quit key.
- Loads and relocates the game binary (e.g.,
SWOS2plus its relocation table), placing code/data into the configured ExpMem/ChipMem. - Applies patches at startup (via a patch list) to fix compatibility/snoop issues, skip copy-protection, adjust blitter/timing, and route file I/O.
- Bridges file access (save/load) to WHDLoad's "resload" API so saves go to a folder (e.g.,
SAVE/) instead of disks. - Handles WHDLoad tooltypes like
PRELOAD,NOCACHE,NOVBRMOVE, etc., and cooperates with options likewhdload_quit. - Installs small hooks (e.g., keyboard exit) and cleanup paths, then jumps into the game's entry point.
The SWOS2 binary is a 1:1 match of the WHDLoad version.
The Hdf binary contains the following patches:
Patches:
- SWOS HDF boots directly into the game (no quit key etc.)
- Game fully loaded from hard drive
- Save files are saved to floppy drive
ssboot SWOS
The SWOS-HD version developed for AIAB (Amiga In A Box) by Galahad of Fairlight uses jst to boot a WHDLoad slave file.
Features are the same as the WHDLoad version.
jst swos.slave PRELOAD >nil:
reset
There are various versions of ASWOS in the SWOS dev team have produced. So I've created a bunch of patch files using diff to show what those changes are from the original TOSEC release version.
Currently the only change is to remove the size check and automatically launch swos_152 version.
- SWOS2.patch (24/25) this patch for the Amiga version of Sensible World of Soccer applies the SWOS 24/25 season database to the original SWOS 96/97 executable and data files. It updates player, team, and national squad data worldwide to reflect the 2024/25 season while intentionally preserving the original Amiga league structures and division layouts for most countries (including major European and several American leagues), even where these no longer match the real-world 24/25 formats. Structural changes and expanded league formats that exist in some PC-only edits are not included in this Amiga patch; only the underlying database entries (teams, squads, and related metadata) are refreshed. The patch also incorporates specific Amiga-targeted adjustments for selected regions (e.g., revised Oceania setups) and an updated national-team pool, but it does not alter core game code, executable logic, or gameplay systems beyond these data-level modifications
- SWOS2.patch (ASWOS) enables Player 2 on Port 0 (Mouse port) to be able to move around in the menus when game starts
- SWOS2.patch (Segra) early patched version with change from Yugoslavia to Serbia, cup changes to Euro Cup, Cup Winners, UEFA Cup and World Cup Qualifying Group data and Belarussia, English, Greek, Scottish, Slovakian, Slovenian, Bolivian, Brasilian and Chilean league data
- SWOS2.patch (HD) the patches contained are interesting but unknown as to what they do at this stage
- SWOS2.patch (WHDLoad) README.md
To get a full SWOS2.s of the patch you can copy the unpatched SWOS2.s into the folder and apply the patch like so:
$ git apply SWOS2.patchOnce you've created a full source patch of SWOS2.s you can now use VS Code to compare them and view the patches side by side with the original source.
- Right-click on the original
SWOS2.sfile and selectSelect for Compare
- Right-click on the newly created
SWOS2.sfile and selectCompare with Selected
- It will open a side by side view of both files with some navigation buttons.
- Click on the down arrow to view the various patches within the assembly.
The original Sensible World of Soccer Amiga release ships with two main game binaries:
- SWOS - the original executable, used on older configurations (for example, machines with Kickstart versions and CPUs below the A1200-style Kick 3.1 + 68020 setup).
- SWOS2 - the A1200-oriented executable, tuned for later machines and used when the SSBOOT loader detects a suitable configuration.
All of the work described here focuses on SWOS2, which is the A1200-targeted version of the game.
SWOS2.REL is a separate relocation table used by the original game's loader in conjunction with the flat, pre-relocated SWOS2 executable image. It does not contain code or data itself; instead, it encodes the offsets within the SWOS2 image that must be adjusted when the image is placed in memory.
At runtime, the loader:
- Loads the static SWOS2 binary into memory at some chosen base address.
- Reads
SWOS2.RELand walks its relocation entries. - For each relocation entry, it:
- Interprets the value as an offset into the SWOS2 image.
- Locates the word or long at that offset.
- Adds the final load base to that word/long to make it a valid runtime address.
Because SWOS2.REL enumerates all locations in the SWOS2 image that are treated as pointers, it effectively encodes where all absolute address references live. That makes it an extremely valuable source of information when reverse engineering and when reconstructing a fully relocatable hunk-based build later.
SWOS2.REL is a flat array of 32-bit big-endian offsets. There is no header, no counts, and no extra metadata - just N longwords, each describing where a relocatable pointer lives in the original SWOS2 image.
For example, the start of the file looks like this:
00 00 00 03 00 00 00 C2 00 00 00 C3 00 00 00 C7
00 00 00 D9 00 00 00 DF 00 00 00 E9 00 00 01 0D
...
Interpreted as big-endian 32-bit integers, this gives a sequence of offsets:
index raw bytes offset (big-endian)
----- -------------- -------------------
0 00 00 00 03 0x00000003
1 00 00 00 C2 0x000000C2
2 00 00 00 C3 0x000000C3
3 00 00 00 C7 0x000000C7
4 00 00 00 D9 0x000000D9
5 00 00 00 DF 0x000000DF
6 00 00 00 E9 0x000000E9
7 00 00 01 0D 0x0000010D
...
Given a known program base (in your IDA setup, PROGRAM_BASE = 0x00100000), each relocation entry is applied by:
- Computing the effective
a0after the game's relocation add:
a0_after_add = PROGRAM_BASE + offset
- Backing up one byte (because the 68k code does
tst.b -(a0)before reading the long):
ptr_ea = a0_after_add - 1
- Reading the 32-bit value at
ptr_ea, which is the relocatable pointer value. - Treating that value as the pre-relocation target address inside the SWOS image.
For example, for the very first entry:
- Offset bytes:
00 00 00 03→offset = 0x00000003 a0_after_add = 0x00100000 + 0x00000003 = 0x00100003ptr_ea = 0x00100003 - 1 = 0x00100002- The long at
0x00100002is0x001000C4, so the target isstartat0x001000C4.
This matches the first line of the relocation dump:
0000: off=0x00000003 ptr_ea=0x00100002 val=0x001000C4 -> target=0x001000C4 [C] start jmp start
Likewise, later entries map cleanly to other code and data symbols (interrupt handlers, globals, drive string tables, etc.), e.g.:
0015: off=0x00000233 ptr_ea=0x00100232 val=0x00100268 -> target=0x00100268 [D] kDriveListPtrs lea (kDriveListPtrs).l,a3
0016: off=0x00000269 ptr_ea=0x00100268 val=0x00100278 -> target=0x00100278 [D] aDf0 dc.l aDf0
...
This simple "array of big-endian offsets from the program base" structure is what makes SWOS2.REL such a rich source of information: by replaying the relocation logic against the IDA database, we can reconstruct exactly which words in the original static image were treated as addresses, and where they pointed.
export_sparse_asm.py is an IDAPython script for IDA 9.0 that runs on the SWOS2 database which matches the original static executable layout (1:1 bytes). It is specifically designed to consume the original SWOS2.REL file and use IDA's analysis to generate a relocation table text file.
It performs two key tasks:
-
Reads and parses
SWOS2.RELdirectly from disk
The script opens the originalSWOS2.REL, walks the relocation stream, and reconstructs the list of relocation entries (i.e. "these offsets in the original image contain relocatable addresses"). Each entry is treated as a big-endian 32-bit offset from the program base, matching the way the original game and WHDLoad slave apply relocations. -
Uses the IDA 9.0 API to correlate relocations with the loaded database and display its references in the disassembly Using the IDA 9.x API the script maps each relocation entry to an effective address (EA) in the IDA database and inspects the surrounding code/data and types. From this it generates a relocation dump (
SWOS2_reloc_dump.txt) summarising the relocation entries in a readable, EA-based form.
Below is a sample of the relocation dump produced by the script, showing how each relocation entry is resolved back into the SWOS2 IDB:
0000: off=0x00000003 ptr_ea=0x00100002 val=0x001000C4 -> target=0x001000C4 [C] start jmp start
0001: off=0x000000C2 ptr_ea=0x001000C1 val=0x38303721 [FILTERED BY HEURISTICS]
0002: off=0x000000C3 ptr_ea=0x001000C2 val=0x303721FC [FILTERED BY HEURISTICS]
0003: off=0x000000C7 ptr_ea=0x001000C6 val=0x001000D0 -> target=0x001000D0 [C] loc_1000D0 move.l #loc_1000D0,(DIVIDE_BY_ZERO).w
0004: off=0x000000D9 ptr_ea=0x001000D8 val=0x001023AC -> target=0x001023AC [C] InitHardwareAtFrameStart jsr InitHardwareAtFrameStart
0005: off=0x000000DF ptr_ea=0x001000DE val=0x001573C8 -> target=0x001573C8 [D] g_loaderBankBitmap lea (g_loaderBankBitmap).l,a0
0006: off=0x000000E9 ptr_ea=0x001000E8 val=0x0017FFF6 -> target=0x0017FFF6 [D] byte_17FFF6 cmpa.l #byte_17FFF6,a0
0007: off=0x0000010D ptr_ea=0x0010010C val=0x001573D0 -> target=0x001573D0 [D] g_loaderMemTop move.l d7,(g_loaderMemTop).l
0008: off=0x00000113 ptr_ea=0x00100112 val=0x001573C8 -> target=0x001573C8 [D] g_loaderBankBitmap move.l d6,(g_loaderBankBitmap).l
0009: off=0x00000119 ptr_ea=0x00100118 val=0x001573CC -> target=0x001573CC [D] dword_1573CC move.l d5,(dword_1573CC).l
0010: off=0x00000163 ptr_ea=0x00100162 val=0x00101B2C -> target=0x00101B2C [C] Int12KeyboardHandler move.l #Int12KeyboardHandler,(LEVEL_2_INTERRUPT_AUTOVECTOR).w
0011: off=0x0000016B ptr_ea=0x0010016A val=0x00101BCE -> target=0x00101BCE [C] Int13TimerHandler move.l #Int13TimerHandler,(LEVEL_3_INTERRUPT_AUTOVECTOR).w
0012: off=0x00000173 ptr_ea=0x00100172 val=0x00107446 -> target=0x00107446 [C] Level4InterruptHandler move.l #Level4InterruptHandler,(LEVEL_4_INTERRUPT_AUTOVECTOR).w
0013: off=0x0000017B ptr_ea=0x0010017A val=0x00103996 -> target=0x00103996 [C] SetupInterrupts jsr SetupInterrupts
0014: off=0x000001A9 ptr_ea=0x001001A8 val=0x0010E6FE -> target=0x0010E6FE [C] main jmp main
0015: off=0x00000233 ptr_ea=0x00100232 val=0x00100268 -> target=0x00100268 [D] kDriveListPtrs lea (kDriveListPtrs).l,a3
0016: off=0x00000269 ptr_ea=0x00100268 val=0x00100278 -> target=0x00100278 [D] aDf0 dc.l aDf0
0017: off=0x0000026D ptr_ea=0x0010026C val=0x0010027D -> target=0x0010027D [D] aDf1 dc.l aDf1
0018: off=0x00000271 ptr_ea=0x00100270 val=0x00100282 -> target=0x00100282 [D] aDf2 dc.l aDf2
0019: off=0x00000275 ptr_ea=0x00100274 val=0x00100287 -> target=0x00100287 [D] aDf3 dc.l aDf3
...
The next step is to take the sparse assembly produced by export_sparse_asm.py, tidy and structure it (adding meaningful labels, types, comments, and segment boundaries), and then rebuild SWOS2 as a true Amiga hunk executable.
In this new build:
- Code and data are placed into proper HUNK_CODE and HUNK_DATA segments.
- The assembler/linker generate relocation hunks instead of relying on an external flat relocation file.
- Absolute addresses are replaced by labels and relocatable expressions, which the linker can freely move.
Moving to a hunk-based SWOS2 gives several key benefits:
- Relocation flexibility - the code and data no longer have to live at exactly the same physical addresses as the original static build. The OS can load the program where it wants, and the relocation hunks will patch everything automatically.
- Room for growth - new code and data can be added (e.g. new gameplay logic, graphics structures, tables) without constantly fighting fixed addresses or overflowing the original layout.
- Better toolchain integration - once the game is a standard hunk executable, it can be linked with additional object files, libraries, and even mixed-language modules, just like any other Amiga program.
In the original setup, the static SWOS2 image is launched via WHDLoad with a SWOS.Slave file. That slave injects callbacks to handle disk access and decompression by calling WHDLoad's resload_* entry points (for example, for getting file sizes, loading files, optionally decrunching data, and saving/deleting files). From the game's point of view, many of these operations still assume floppy-style paths like DFx:.
In the new SWOS2 hunk-based build, the goal is to:
- Move file access from floppy-style devices to hard-disk-style devices, e.g. from
DF0:toDH0:(or other system volumes), so that SWOS2 behaves like a normal hard-disk-installed game. - Eliminate the dependency on WHDLoad's
resload_*functions by implementing our own file I/O stubs directly inside the SWOS2 hunk executable, written in 68k assembly and linked as part of the program. - Bind all file operations directly to AmigaDOS or equivalent system services, so launching the game is just running the SWOS2 hunk executable, without going through WHDLoad at all.
Once all call sites that previously relied on resload_GetFileSize, resload_LoadFile, resload_LoadFileDecrunch, resload_DeleteFile, and resload_SaveFile have been redirected to in-executable routines, the SWOS.Slave file and WHDLoad are no longer required for normal gameplay. SWOS2 becomes a fully standalone Amiga hunk executable ("jailbroken").
At that point, development and debugging get significantly easier:
- The Amiga Assembly can be used to assemble the SWOS2 source and attach its debugger directly to the running hunk executable.
- Breakpoints, single-stepping, and inspection of registers/memory can be done directly against the real game binary, without WHDLoad's wrapper in the way.
A clean, symbolised hunk-based SWOS2 executable also lays the groundwork for an incremental SWOS-to-C port.
With a proper hunk build and a modern toolchain, we can:
- Port one assembly subroutine at a time to C (or C++), link it into the same executable, and verify its behaviour against the original implementation.
- Keep the rest of the game in 68k assembly while gradually replacing pieces with higher-level code.
- Use VSCode's tooling, including the Amiga C/C++ Compile, Debug & Profile extension, which ships with vasm (the same assembler used in the Amiga Assembly extension) and a complete C/C++ toolchain, to build, debug, and profile mixed C/assembly builds.
This staged approach means we don't need a "big bang" rewrite. Instead, the C port can be evolved incrementally, with each converted routine tested in the real game environment.
The hunk-based SWOS2 executable is also the platform on which the SWOS 25/26 Amiga version will be built.
Planned goals for this future version include:
- Updated team data for the 25/26 season, including leagues, clubs, and player rosters aligned with the modern database used by the community.
- Additional graphics and presentation polish, such as refreshed kits, logos, menus, and potentially new in-game UI elements enabled by the more flexible memory layout.
- Gameplay and options enhancements, made possible by being able to insert new logic, tables, and state without the tight constraints of the original static binary layout.
Because the new SWOS2 hunk build removes the rigid address dependencies and external WHDLoad file handling, all of these changes can be implemented in a more maintainable, modular way. The same infrastructure that supports the 25/26 season update can also support follow-up patches and experimental features, making this a long-term, extensible foundation for SWOS on the Amiga.
swos-port-c is the long-term C port of the Amiga SWOS2 codebase, using the new SWOS2 Amiga Hunk executable as the reference for behaviour. The goal is to translate the original 68k gameplay and menu logic into clean, portable C and drive it with an SDL2 frontend (for video, input, audio, and timing) so the game can run on modern platforms (Windows, Linux, macOS, etc.). Once the C core reaches parity with the Amiga version, it will be integrated into the SWOS 2020 project, providing a single, maintainable engine for future seasons and feature updates.
When launching SWOS2 under WHDLoad, you cannot attach the Amiga Assembly VSCode extension's debugger directly to the running game. Instead, you rely on low-level techniques that let you observe control flow and state by manipulating hardware registers and deliberately triggering exceptions.
This section describes two practical techniques:
- Using the COLOR00 register to visualise execution paths.
- Using illegal / NOP opcodes to trigger WHDLoad's core dump mechanism and then analysing the dumps with tools like HxD and the assembler .lst listing.
The Amiga's first colour register, COLOR00, controls the background colour. By writing different values to this register at key points in the code, you can tell where the CPU is executing just by watching the screen.
Example (set background to bright red):
move.w #$F00,(COLOR00).l ; RedSome useful 12-bit RGB examples (OCS/ECS format: 0xRGB, 4 bits per channel):
move.w #$000,(COLOR00).l ; Black
move.w #$F00,(COLOR00).l ; Red
move.w #$0F0,(COLOR00).l ; Green
move.w #$00F,(COLOR00).l ; Blue
move.w #$FF0,(COLOR00).l ; Yellow
move.w #$FFF,(COLOR00).l ; WhiteTypical usage patterns:
- Entry/exit markers - Set
COLOR00to one colour on function entry and another on exit to see if/when a routine runs. - Branch tracing - Use different colours on each side of a conditional branch to see which path is taken.
- Critical sections - Mark "should never happen" code paths with a loud colour (e.g. bright red) to make failures visually obvious.
Because COLOR00 is a simple hardware write, it is a very low-overhead way to gain insight into control flow when no debugger is attached.
When running under WHDLoad, an illegal instruction will cause WHDLoad to interrupt the game and show an error dialog with the current PC and other state. You can leverage this behavior to capture full memory/register dumps at interesting points.
On 68000:
ILLEGALis encoded as$4AFC.NOPis encoded as$4E71.
A common pattern is:
- Replace an existing instruction with
ILLEGAL($4AFC) to trap execution when it reaches that point. - If you need to keep instruction count/size stable (for the static version with hard-coded offsets), use
NOP($4E71) to pad out or neutralise instructions without changing the byte count.
Important (static 1:1 version):
When patching the original static image you must keep the total byte count identical. Shifting code or data will break all hard-coded offsets used with the originalSWOS2.REL. That means any injected ILLEGAL/NOP sequence must be the same size as the code it replaces.
Once you've patched in an ILLEGAL instruction, run the game under WHDLoad. When execution hits that opcode, WHDLoad will display a message similar to:
Exception "Illegal Instruction" ($10) at $8760EC (ExpMem $800EC) occurred.
Besides illegal instruction traps, WHDLoad intercepts a lot of other problems and turns them into readable error dialogs. This effectively gives you "poor-man’s debugging" information: a description, a PC, sometimes a memory range, and often the name of the resload_ function involved.
Typical categories you’ll see include:
- DOS / file I/O errors - read/write/scan/delete failures on specific files.
- CPU exceptions - address/bus/illegal instruction, always with a PC.
- OS emulation /
resload_API errors - bad SR/SP, destroyed registers, invalidPLCMD_*patch commands, failed decrunch/delta/relocate. - Kickstart / data integrity issues - invalid Kickstart images, corrupt relocation/patch tables, CRC mismatches.
- Hardware misuse - blitter not waited for, blitter out of bounds, copperlist problems, incomplete MMU trees.
- Self-modifying code / SMC checks - modified opcodes detected, unsupported addressing modes for Snoop/S.
- CPU / chipset / video mode requirements - needs 68020+, AGA, PAL/NTSC, etc.
- Stack / interrupts / VBR / CACR problems - bad SP or modified VBR/CACR on entering
resload_*. - Misc config issues - XPK not cached, PreLoad/Examine errors, invalid QuitKey/controller combinations.
When you hit one of these dialogs, treat it like a mini crash report: note the PC, any mentioned address or range, and the error type. You can then cross-reference that information with your .lst file and disassembly, and-in combination with the WHDLoad dump files-narrow down where your code or data is going wrong even without a "real" debugger attached.
From this dialog:
-
Click "Make CoreDump".
-
WHDLoad will write a set of dump files into
DH0:C(or the configured WHDLoad directory):*.whdl_dump*.whdl_expmem*.whdl_memory*.whdl_register
The exact format is internal to WHDLoad, but for debugging SWOS2 you can treat them roughly as:
.whdl_register- A text file with a snapshot of all CPU registers (D0-D7, A0-A7, SR, PC, etc.) plus some WHDLoad state. This is your starting point for understanding what the CPU was doing when the ILLEGAL occurred..whdl_memory- A binary dump of main memory (chip/fast) as seen by the program at the time of the exception. Addresses like$0010xxxxcan usually be located directly in this file..whdl_expmem- A binary dump of the expansion memory area used by WHDLoad (ExpMem). If the error message mentionsExpMem $800EC, for example, you can subtract the expansion base (e.g.$80000) to get an offset inside this file..whdl_dump- A WHDLoad-specific consolidated dump intended mainly for WHDLoad authors; you typically don't need to touch this directly for day-to-day SWOS2 work.
HxD is a versatile hex editor that you can use to inspect the .whdl_memory and .whdl_expmem binaries:
-
Open
*.whdl_memoryor*.whdl_expmemin HxD. -
Use Search → Goto... (or Ctrl+G) and select "hexadecimal offset".
-
To locate an address reported by WHDLoad:
- If the exception shows
PC = $0010ABCD, you can usually use$0010ABCDdirectly as an offset in.whdl_memory(or subtract the base if the dump is offset). - If the exception shows
ExpMem $800EC, subtract the expansion base (e.g.$80000) to get the offset ($800EC - $80000 = $EC) and jump to that offset in.whdl_expmem.
- If the exception shows
This lets you see exactly what bytes were at the faulting address and around it at the moment of the crash.
When building the static version, run vasm with a listing option, for example:
vasmm68k_mot ... -L build/SWOS2.lst ...This generates a listing file (SWOS2.lst) that interleaves:
- Source lines
- Final addresses
- Assembled opcodes
Here's a small section from the SWOS2.lst file:
19256: ; ===== GameLoop =====
19257: ; prototype: void __usercall GameLoop()
19258: ; in:
19259: ; A5 -> left team in-game struct
19260: ; A6 -> right team in-game struct
19261: GameLoop:
01:0010E8D6 42790012F6FC 19262: clr.w (gameCanceled).l
01:0010E8DC 42790010F056 19263: clr.w (instantReplayFlag).l
01:0010E8E2 42790010F058 19264: clr.w (saveHighlightScene).l
01:0010E8E8 42790010F05A 19265: clr.w (fadeAndSaveHighlightScene).l
01:0010E8EE 42790010F05C 19266: clr.w (spaceReplayTimer).l
01:0010E8F4 323C00A8 19267: move.w #168,d1
01:0010E8F8 343C0139 19268: move.w #313,d2
01:0010E8FC 4A790012FF94 19269: tst.w (g_trainingGame).l
01:0010E902 661A 19270: bne.s pre_frame_init
01:0010E904 4EB90010F3FA 19271: jsr Rand
01:0010E90A 08000000 19272: btst #0,d0
01:0010E90E 6706 19273: beq.s set_small_scroll
01:0010E910 343C0250 19274: move.w #592,d2
01:0010E914 6004 19275: bra.s reset_base_scroll
...
This is invaluable when combined with WHDLoad's register snapshot:
-
Open
*.whdl_registerin a text editor. -
Note key values, especially:
- PC - the program counter at the time of the ILLEGAL.
- SR - the status register (condition codes, interrupt level).
- Any registers that look relevant (e.g. A0/A1 for pointers, D0/D1 for loop counters).
-
Determine whether the PC is in main memory or expansion memory:
- If it looks like a normal code address (e.g.
$0010xxxx), treat it as a main memory address. - If it matches an
ExpMemrange, adjust by the expansion base (as you did when using HxD).
- If it looks like a normal code address (e.g.
-
In
SWOS2.lst, search for the hex address of the PC.
The listing will show you:- The exact instruction that triggered the ILLEGAL.
- The surrounding instructions and labels (function name, local labels, etc.).
- The assembled bytes, which you can cross-check against
.whdl_memoryor.whdl_expmem.
By combining:
- The PC and register state from
.whdl_register, - The raw bytes from
.whdl_memory/.whdl_expmemvia HxD, and - The symbolic listing from
SWOS2.lst,
you can reconstruct what the engine was doing at the moment of failure and track down bugs even without an interactive debugger.
These techniques are essential while we're still running via WHDLoad and the static version, because we can't attach the Amiga Assembly VSCode debugger directly to the game process.
One of the long-term goals of converting SWOS2 into a stand-alone Amiga Hunk executable (with its own HDD file I/O routines instead of WHDLoad's resload_* helpers) is to eliminate the WHDLoad layer entirely. Once SWOS2 can be launched as a normal Hunk executable:
- We can run it under the Amiga Assembly VSCode extension's debugger,
- Set breakpoints and single-step directly in our source code,
- And make development and porting (e.g. to C) much easier.
Those debugger-based workflows are covered in the following sections once the custom HDD routines and stand-alone Hunk build are in place.
Once SWOS2 stopped using WHDLoad's resload_* helpers and started calling dos.library directly, a new class of problems appeared: illegal instructions and Gurus inside dos_Open / dos_Read.
The underlying reason is:
- The game replaces the OS level-2 autovector with its own keyboard interrupt (
Int2KeyboardHandler). dos.libraryitself relies on Exec's own level-2 handler (and uses privileged instructions likeORI #$2000,SR) while doing I/O.- If we call into DOS while our custom handler is still installed, the OS ends up running code that assumes a different interrupt setup than what we actually have, which can lead to illegal instructions or stack corruption.
The safest fix is to:
Temporarily give level-2 back to Exec/DOS for the duration of each disk I/O call, then restore the game's handler afterwards.
We already saved the original vector at startup:
; Save original Exec/OS autovectors
move.l LEVEL_1_INTERRUPT_AUTOVECTOR(a0),S_SavedInt1
move.l LEVEL_2_INTERRUPT_AUTOVECTOR(a0),S_SavedInt2
move.l LEVEL_3_INTERRUPT_AUTOVECTOR(a0),S_SavedInt3
move.l LEVEL_4_INTERRUPT_AUTOVECTOR(a0),S_SavedInt4
move.l LEVEL_5_INTERRUPT_AUTOVECTOR(a0),S_SavedInt5So the WHDLoad compatibility stubs in resload.s do this dance around every call that touches dos.library.
For example, the _LoadFile wrapper that Rob Northen's loader calls:
; --- _LoadFile ------------------------------------------------------------
; In: A0 = filename, A1 = destination buffer
; Out: global 'size' updated with length; caller registers restored
_LoadFile:
movem.l d0-d1/a0-a2,-(sp)
move.l a0,a2 ; save filename
; Exec owns level 2 while we do DOS I/O
move.l S_VBR(pc),a0
move.l S_SavedInt2,LEVEL_2_INTERRUPT_AUTOVECTOR(a0)
move.l a2,a0 ; restore filename for resload_LoadFile
jsr resload_LoadFile ; D0 = length, D1 = error
move.l S_VBR(pc),a0
move.l #Int2KeyboardHandler,LEVEL_2_INTERRUPT_AUTOVECTOR(a0)
; store returned length for rob_*
lea size(pc),a0
move.l d0,(a0)
movem.l (sp)+,d0-d1/a0-a2
rtsThe same pattern is used for the other WHDLoad-style entry points:
_GetFileSize→resload_GetFileSize_SaveFile→resload_SaveFile_Deletefile→resload_Deletefile_ListFiles→resload_ListFiles
Each of these:
- Saves the game's registers (via
movem.l). - Writes
oldInt2back intoLEVEL_2_INTERRUPT_AUTOVECTORto restore the original DOS/Exec handler. - Calls the new
resload_*implementation, which internally usesdos_Open,dos_Read,dos_Write, etc. - Re-installs
Int2KeyboardHandleras the level-2 autovector. - Restores registers and returns to the Rob Northen loader / game code.
This keeps the visible behaviour of all existing resload_* call sites identical (same prototypes, same size global), while avoiding illegal instructions inside the DOS I/O path.
Once SWOS2 runs as a normal Hunk executable with AmigaDOS file I/O, we also need a clean way to exit back to the shell / Workbench instead of just "falling off the end" or leaving interrupts and DMA enabled.
Some of this is covered in the Amiga 500: Bare-Metal Startup Code blog post.
There are three pieces to this:
The keyboard hook (KBHook) is installed as part of the custom level-2 handler. It already sees raw keycodes via CIA:
xdef KBHook
KBHook
move.b $bfec01,d0
move.w d0,-(a7)
not.b d0
ror.b #1,d0
cmp.b _keyexit(pc),d0
beq.b quit
move.w (a7)+,d0
rts
quit:
move.w (a7)+,d0
move.w #1,g_quitToDos
rts
_keyexit dc.b $46 ;ws_keyexit = Del
xdef g_quitToDos
g_quitToDos:
dc.w 0Instead of trying to tear everything down directly inside the interrupt handler (which risks stack mismatches and re-entrancy issues), the project uses a global flag (e.g. g_quitToDos) and checks it in the main loops.
A typical pattern in a polling loop looks like:
tst.w g_quitToDos
bne.s intro_continue ; or branch to a clean exit pathThis lets the game finish its inner loop, unwind to a safe point (usually main), and then call the final cleanup routine.
At the very top level, after main returns, the entry point simply calls a dedicated cleanup routine:
start:
...
jsr main
jsr ExitToDos
rtsExitToDos is responsible for putting the machine back into a state that AmigaDOS expects:
xdef ExitToDos
ExitToDos:
move.l S_VBR(pc),a0
move.l S_SavedInt1,LEVEL_1_INTERRUPT_AUTOVECTOR(a0)
move.l S_SavedInt2,LEVEL_2_INTERRUPT_AUTOVECTOR(a0)
move.l S_SavedInt3,LEVEL_3_INTERRUPT_AUTOVECTOR(a0)
move.l S_SavedInt4,LEVEL_4_INTERRUPT_AUTOVECTOR(a0)
move.l S_SavedInt5,LEVEL_5_INTERRUPT_AUTOVECTOR(a0)
; Wait for blitter idle again (paranoia)
btst #14-8,(DMACONR).l
.blt_idle_exit:
btst #14-8,(DMACONR).l
bne.s .blt_idle_exit
; Now safe to kill DMA and interrupts
;move.w #$01FF,(DMACON).l ; Disable all DMA channels
;move.w #$3FFF,(INTENA).l ; Disable all interrupts
; Restore original view via graphics.library
move.l S_GfxBase(pc),a6 ; Load gfxbase
move.l gfx_copinit(a6),COP1LC ; Restore coplist pointer 1
move.l gfx_LOFlist(a6),COP2LC ; Restore coplist pointer 2
clr.w COPJMP1 ; Make Copper use restored pointer
; Restore DMA / interrupts / ADKCON
move.w S_SavedDmaCon,(DMACON).l
move.w S_SavedIntEna,(INTENA).l
move.w S_SavedAdkCon,(ADKCON).l
move.l S_SavedView,a1
beq.s .no_view ; (paranoia)
jsr GfxLoadView(a6)
move.l S_GfxBase(pc),a6
jsr GfxWaitTOF(a6)
jsr GfxWaitTOF(a6)
.no_view:
tst.w S_FromWB
beq.s .done
; --- Hand control back to Workbench / CLI ---
move.l 4.w,a6 ; Exec base
tst.l S_WBMsg
beq.s .no_wbmsg
move.l S_WBMsg(pc),a1
jsr ExecReplyMsg(a6)
.no_wbmsg:
; Close graphics.library if we opened it
move.l S_GfxBase(pc),a1
beq.s .done
jsr ExecCloseLib(a6)
.done:
movea.l S_SavedSP,sp
moveq #0,d0
rtsKey points:
- All hardware vectors we touched (levels 1-5) are restored from the saved
oldIntXvalues taken at startup. - CIAs and custom-chip interrupts are disabled so DOS doesn't inherit stray interrupt sources.
- DMA is turned off and the background colour is reset, so the user is dropped back to a clean shell / Workbench screen.
Because the program was launched as a regular AmigaOS process (from Shell or via a script), a simple RTS from start is enough to return control to DOS. There's no need to call dos_Exit() explicitly (and in practice, doing so after heavy custom interrupt work can hang under some environments).
If running under UAE / FS-UAE, a tiny SWOS script in the same directory can wrap the executable and then immediately request that the emulator shuts down once the game returns:
CD SYS:SWOS
data/SWOS2
From the game's perspective, it just exits back to DOS via ExitToDos and returns from start. From the user's perspective, double-clicking the icon:
- launches SWOS2,
- the game runs in full-screen as a stand-alone executable,
- and when they hit Del (or otherwise exit the game), control returns to the script, which calls
UAEquitand closes the emulator.
Taken together, the interrupt hand-off around resload_* and the clean shutdown path in ExitToDos complete the journey from "WHDLoad-only slave" to a proper stand-alone Amiga Hunk executable that plays nicely with both AmigaDOS and modern debugging tools.
Now that SWOS2 runs as a stand-alone Hunk executable with normal AmigaDOS I/O, we can finally bring the Amiga Assembly VSCode debugger into the loop instead of flying blind through hex dumps and printf-style hacks. The game can be launched directly under the debugger (or via a script that the debugger controls), which means all of your assembled source files line up with the running code: you can set source-level breakpoints in SWOS2.s / resload.s, single-step through instructions, and immediately see the effect on registers and memory. Instead of guessing why UnpackMenu jumped into space or why LoaderIntroWaitForFireOrTimeout blew its stack, you can just stop inside those routines, inspect A0/A1/D0/D1, and watch each write to g_currentMenu or currentGameTick as it happens.
On top of that, the debugger gives you “modern IDE” style features mapped onto the Amiga: watch expressions for key globals (e.g. g_quitToDos, g_currentMenuPtr, size), memory views of chip/fast RAM to track graphics and buffers, and step-into/step-out control around your new resload_* implementations and Rob Northen loader stubs. You can set breakpoints on your custom interrupts (Int2KeyboardHandler, Int3TimerHandler) to see exactly when CIA events fire, or break on the return from a dos_Read/dos_Open wrapper to confirm file sizes and destinations. In practice, this turns SWOS2 from a black box that occasionally Guru-meditates into a fully inspectable process, making reverse-engineering, refactoring, and the eventual C port vastly more manageable.
Be sure to visit the Visual Studio Code Amiga Assembly Wiki for more details on how to use the debugging features.
Coming Soon!
-
ChatGPT
-
MCP Servers
- UAEUNP 0.8 list and extract Amiga based disk images and archives such as ADF.
- rnc_lib.exe will compress or decompress an RNC compressed file.
- SWOEditorS an editor for Sensible world of soccer
- AG-SWSEdt
Here's a collection of useful tools and utilities:
- 010 Editor - Commercial text and hex editor with powerful binary templates for inspecting and editing complex binary formats.
- HxD - Free, fast hex editor and disk editor for Windows, useful for inspecting raw files, memory, and disks.
- grepWin - Lightweight Windows search tool that adds grep-style regex search and replace to folders via a simple GUI and Explorer integration.
- Amiga Hardware Reference Manual 3rd Edition
- MarkeyJester's Motorola 68000 Beginner's Tutorial
- Reverse-engineering Amiga games
- An approach to reverse engineering for old amiga games...
- Visual Studio Code Amiga Assembly Wiki
- Amiga 500: Bare-Metal Startup Code
- Mastering Amiga Assembler
- Total AMIGA assembler
- Bare-Metal Amiga Programming: For OCS, ECS and AGA
- Motorola M68000 Family Programmer's Reference Manual
- Amiga Assembly
- Amiga C/C++ Compile, Debug & Profile
- Amiga C/C++ Compile, Debug & Profile (fork with libs support)
- benbaker76 - SWOS Amiga RE Project Lead/Developer
- Playaveli - SWOS 2020 Project Manager
- segra - SWOS Amiga RE Project Founder
- starwindz - SWOS 2020 PC Lead/Developer
- zlatkok - SWOS DOS RE Project Lead






























