We wanted to understand an old Amiga tank game well enough to eventually recreate it in a browser. Getting the disk to boot was only the first hurdle. We then had to work out which emulator the debugger was controlling, recognise that the executable was compressed, and fix a protocol parser that threw away successful replies.
This is our beginner-friendly account of reverse engineering Conqueror on Windows, with results recorded on 3 October 2026. The useful milestone is concrete: we recovered a 300,416-byte unpacked game image that matches two live memory captures byte for byte. We have not finished decompiling the game, and reliable automatic breakpoint stops remain unresolved. This guide covers how we got this far, including the attempts that did not work.
1. Know what each piece does
| Term or tool | Plain-English meaning |
|---|---|
| ADF | A file containing an Amiga floppy-disk image. It can hold a boot block, directories and executable files. |
| WinUAE | The emulator running the Amiga hardware and software on Windows. |
| Kickstart / AROS ROM | Startup and operating-system code used by the emulated machine. Our working configuration uses WinUAE’s built-in AROS ROM. |
| MCP bridge | A tool server that lets an assistant request debugger operations. Having this server running does not mean it is connected to the emulator. |
| GDB remote protocol | The messages the bridge sends to the debugger-enabled emulator: read memory, inspect registers, step and set breakpoints. |
| Hunk executable | An Amiga executable container with code/data sections, memory requirements and potentially relocation information. |
| Decruncher | A small routine that expands compressed code or data before the game uses it. |
| Disassembly / decompilation | Disassembly turns bytes into CPU instructions. Decompilation attempts higher-level pseudocode. Neither restores the author’s original comments, names or project files. |
The practical chain is assistant → MCP bridge → GDB connection → debugger-enabled WinUAE → emulated Amiga. Check each connection separately. The bridge project documents its custom WinUAE build and setup.
2. Preserve the disk and question your first interpretation
Our ADF is 901,120 bytes. Its directory contains CONQUEROR, game, intro, logo and an s directory. The startup sequence prints a text file, then runs CONQUEROR:
s/type s/QUARTEX_RULES!
CONQUEROR
That matters: a file named game is not necessarily the first program the machine executes. Follow the boot and startup chain instead of choosing the most promising filename.
Our first visible screen was a crack/trainer screen with an unlimited-tanks option. A trainer is extra code that can change game behaviour. We treat this disk as a trainer variant, keep cheats off for baseline observations, and keep WHDLoad comparison files separately identified. A trainer screen is not proof that the retail title screen or gameplay works.
We also had to quarantine earlier analysis that described a vector space shooter. It did not agree with the tank game and reference material. Plotting arbitrary bytes can produce something that looks like a model; that does not establish an asset format. Require code references, a repeatable structure and an in-game match before trusting an extracted model.
- Keep the original disk image and comparison packs unchanged; use working copies for experiments.
- Record source paths, file sizes, SHA-256 hashes and any conversions. Close the emulator before hashing an image it has open, so the baseline is stable.
- Keep packed files, unpacked outputs and runtime captures in separate, clearly named files.
- Label observations as confirmed, inferred or untested. This prevents an early guess becoming the foundation of the remake.
3. Reuse the configuration that actually booted
We briefly wondered whether the problem was “the 6800 version”. The relevant Amiga CPU names here are 68000 and 68020. Our proven boot configuration used a 68020 CPU, AGA chipset and built-in AROS ROM. The important settings were:
cpu_type=68020
cpu_model=68020
cpu_compatible=false
chipset=aga
chipmem_size=2
fastmem_size=4
bogomem_size=0
kickstart_rom_file=:AROS
floppy0=C:\Amiga\Conqueror-working.adf
nr_floppies=1
ntsc=false
The disk path above is an example: replace it with your working copy. Memory values are WinUAE configuration fields; do not assume every number is a count of megabytes. These settings describe our successful boot environment, not a claim that the original game requires a 68020 or AGA.
The trap was that the visible, working emulator used Conqueror-AROS-68020.uae, while MCP launched its debugger instance using Conqueror.uae. The latter selected a different ROM and CPU compatibility setting. We backed it up and made its contents match the proven AROS configuration. ROM reads then showed AROS ROM, and subsequent register reads showed startup executing.
Lesson: compare the actual executable path and actual configuration path. Seeing a working WinUAE window does not establish which configuration another process will launch.
4. “MCP is running” has several meanings
In our session the standard WinUAE process and the MCP server were both running, but the status tool returned Not connected. Connecting launched a separate winuae-gdb.exe instance. The original game window was still there. It was easy to look at one while inspecting the other.
WINUAE_PATH=C:\Tools\WinUAE-GDB
WINUAE_CONFIG=C:\Amiga\Conqueror-AROS-68020.uae
WINUAE_GDB_PORT=2345
These are example environment values for the bridge. WINUAE_PATH points to the directory containing the debugger executable. Check your installed bridge’s documentation and source: our copy generated default.uae and supplied debugger flags when launching. Editing a different configuration file would have had no effect.
- Check server availability, then connection status, then registers and memory. Each proves a different layer.
- Use one GDB client at a time. A standalone test script and MCP can compete for the same connection.
- Understand lifecycle commands before using them. In our installed bridge, disconnect stopped its owned emulator, and reset restarted it.
- Allow for ROM startup. An unusual stack value immediately after reset alone does not prove a broken machine; inspect the ROM bytes and subsequent execution.
- Our short-lived command runner often launched a fresh emulator on the next invocation. Do not assume registers, RAM or breakpoints survive a script ending.
5. Inspect the disk before disassembling the engine
We used amitools’ xdftool in read-only mode. Run these from a working directory containing your disk copy:
xdftool -r .\Conqueror-working.adf list
xdftool -r .\Conqueror-working.adf boot show asm
Directory listing worked. The boot command reported a valid boot block and matching checksum, but assembly output failed with package 'machine68k' missing! please install with pip. The Hunk command-line tool also hit that dependency problem. We did not count partial output as a successful disassembly.
For this session we used amitools’ Python Hunk parser and an already-available Capstone disassembler. This let us inspect the executable sections without claiming the missing dependency had been repaired. Another small gotcha: the parser’s segment objects expose get_type(); our initial assumption that they had a .type field produced an AttributeError.
The distinction is important: a tool can successfully list a disk while its optional disassembly path fails. Diagnose the failing operation, rather than reinstalling the entire environment blindly.
6. The first code was an unpacker
The recovered game and intro files are Hunk executables, but their opening instructions implement decompression. Treating that packed body as game logic would produce misleading analysis. We also avoided naming a compressor solely because the loader looked familiar; no successful Ancient identification was established in this session.
Disassembling the loader gave us specific evidence. Both routines expand data at $21000, where the dollar sign means hexadecimal. They consume a packed length, an output length and an XOR checksum. The game’s successful-checksum path jumps to $57B8A; the intro’s jumps to $2B01C. These are addresses derived from instructions, not guesses from filenames.
| File | Packed payload | Unpacked output | Entry target | Validation achieved |
|---|---|---|---|---|
| game | 75,324 bytes | 300,416 bytes | $57B8A | Complete payload consumed; checksum zero; two full live RAM matches |
| intro | 229,804 bytes | 241,536 bytes | $2B01C | Complete payload consumed; checksum zero; runtime match still outstanding |
Those packed payload sizes exclude the executable wrapper. They are not the sizes of the whole files on disk.
We translated the observed unpacking routine into Python, checked every output boundary and required the loader’s checksum to pass. A deliberate checksum corruption was rejected. Then we compared the game output with two separately captured memory images. All 300,416 bytes matched in both comparisons. That runtime comparison is much stronger evidence than “the disassembly looks plausible”.
For a local comparison of two files you have already captured, this tiny script is enough:
from pathlib import Path
import hashlib
expected = Path("game-unpacked-021000.bin").read_bytes()
captured = Path("runtime-021000.bin").read_bytes()
assert len(expected) == len(captured) == 300416
assert expected == captured
print("Exact match:", hashlib.sha256(expected).hexdigest())
The filenames are illustrative. The comparison does not obtain a capture or decompress anything by itself. Our game output’s SHA-256 is 1ac478dbb6aacb384421453b0333607c5f0ea81f9fca23e2dd80da7dd8423e3e. This identifies our analysed variant; a different release or trainer may produce different bytes.
7. The breakpoint timeout was hiding an “OK”
We tried installing breakpoints at the loader’s jump targets. The commands timed out. The local bridge parser contained this broad test:
if (packetData.startsWith('O')) {
// Treat as console output, then skip it.
continue;
}
GDB console-output packets begin with O and carry hexadecimal-encoded text. But the success acknowledgement is OK — which also begins with O. The parser discarded it, leaving the command waiting for a reply it had already received.
Our local correction recognises O followed by complete hexadecimal byte pairs:
if (/^O(?:[0-9a-fA-F]{2})+$/.test(packetData)) {
// Existing console-output handling goes here.
continue;
}
We backed up the old files and corrected both the TypeScript source and the compiled JavaScript used by our test runner. In a normal build workflow, edit the source and rebuild. Crucially, restart the MCP process too: an already-running Node process retains its imported module. Updating a file on disk does not replace that code in memory.
A regression check fed the parser console output, then an OK packet split across two chunks. Console output was ignored and OK reached the waiting command. A fresh runner subsequently received acknowledgements for both breakpoint insertion and removal.
This is a finding in the installed bridge copy we inspected, not a promise that every upstream release has the same bug. Check your version before applying a patch.
8. An acknowledged breakpoint is not a breakpoint hit
This is the main unresolved gotcha. Fixing the discarded acknowledgement did not establish reliable breakpoint stops. Our timed runs at the game entry and selected ROM addresses ended by manual pause, without the expected asynchronous breakpoint stop.
We inspected the WinUAE fork’s source. Its normal continue path called deactivate_debugger(), while breakpoint checking was tied to tracing state. That provided a diagnostic lead. We tried the range-execution command to keep tracing active, but it did not demonstrate a working breakpoint stop either. We are not presenting that attempt as a fix.
There was also a command-format trap while testing ranges: the source parser searched for a comma after a minimum position, so a one-digit range start was unsuitable. Padding the start address corrected our test command’s format; it still did not prove breakpoint stopping. Separately, a single-step smoke test returned S05 with an unchanged program counter, leaving stop-response synchronisation as another question to investigate.
- “Set breakpoint returned OK” means the request was acknowledged.
- “We received a stop” needs a recorded reason and register state.
- “The program counter equals the target” must be tied to the intended execution event.
- “The unpacked bytes are present” proves memory contents, not that the game’s entry routine has executed.
9. Screenshots and disassembly had their own limitations
Our MCP screenshot command returned E01, and the monitor-based disassembly request also failed. A separate desktop-capture attempt failed to connect to its native helper. These failures meant visual verification remained outstanding in that debugging session; they did not invalidate successful memory reads.
The bridge’s README and the installed tool descriptions did not perfectly agree about disassembly capabilities. We therefore used the independently captured bytes with Capstone instead of assuming a listed tool would work. Verify the operation against your actual build, especially when combining a fork, a bridge and locally changed code.
10. Import the right bytes at the right address
Our unpacked output is a raw memory image, not a rebuilt Hunk executable. Its base is $21000 and its last byte is $6A57F. Importing it at zero would shift absolute references and make analysis confusing. For example, the entry at $57B8A is file offset $36B8A: subtract the base address.
For the next Ghidra stage, import the raw image with a big-endian Motorola 68000 language and that base address. Our observed loader instructions were decoded as 68000-family code; using a 68020 to boot the emulator does not by itself prove the game needs 68020-specific instructions. Adjust the analysis language only when the code provides evidence.
The Ghidra Amiga extension supports Amiga containers such as Hunk executables. That is useful for original files, but does not turn an arbitrary raw RAM dump into a Hunk. We prepared a small entry-label helper; we have not yet run it in Ghidra or completed higher-level decompilation.
At the verified game entry, the code stores two addresses, passes them to a routine with display-list pointers, then writes a list pointer and display settings to Amiga hardware registers. That supports an initial display-setup interpretation. We have deliberately left the other calls unnamed until tracing explains their roles. A plausible function name is still a hypothesis.
A troubleshooting checklist for your own first attempt
| Symptom | What to check next |
|---|---|
| Game visible, MCP says not connected | Separate server availability from the GDB connection. Check which WinUAE process is being controlled. |
| Debugger behaves differently from the working game | Compare executable, ROM, CPU, chipset, RAM and the configuration file actually loaded. |
| Assembly looks nonsensical | Check the container, section boundaries, compression, byte order and load address. |
| Breakpoint command times out | Inspect protocol replies. In our copy, a broad O-packet filter swallowed OK. |
| Patch seems to do nothing | Rebuild compiled output and restart the process that imports it. |
| Breakpoint acknowledged but never stops | Record stop reasons and PCs; investigate execution control separately from insertion. |
| Screenshot or monitor command returns E01 | Treat that capability as failed; use working read paths and keep visual verification outstanding. |
| A repeat script has forgotten its state | Check emulator process lifetime and whether the script started a fresh instance. |
Where we are now
We have a verified unpacked game image, a checksum-validated intro image, address-aware loader and entry disassembly, recorded hashes and a regression-tested acknowledgement-parser correction. We still need reliable breakpoint stops, a confirmed trainer-to-game transition, repeatable gameplay observations and annotations supported by runtime evidence.
The next useful step is to make stopping at a known instruction reliable, then trace the startup wrapper into the game. Only after controls, menus, rendering and rules have been measured will it make sense to build the browser version. For now, the important progress is that we can finally analyse bytes proven to be in the running Amiga’s memory.