The first KOEI SNES title reverse-engineered (Romance of the Three Kingdoms II, 1990/SNES), decoded independently of its NES twin and then compared. The payoff: KOEI’s strategy engine is a genuinely portable bytecode VM — same instruction set carried across two consoles — and the SNES release is a faithful port + facelift, not a mechanics rewrite. Reversing the VM from both sides of a port makes each reconstruction the other’s oracle.
Links: How KOEI’s NES AI & combat evolved (the five-decompiler NES
study this extends to the SNES line) · the exception — Nobunaga’s Ambition SNES compiled to native, no VM (the one KOEI SNES port that does not carry this portable VM) · SNES cartridge teardown (the
method entry point) · L’Empereur — the two-tier turn · DREAM goto-free
structuring (the decompiler-output experiment) ·
The Rosetta Stone Method (the meta-lesson: this corpus is a
hardware-learning stone).
Code: sibling repos koei-snes (shared SNES engine, the SNES sibling of koei-nes), snes-decompiler
(65C816/hardware substrate), rot3k2-snes-decompiler (the title).
Full reversal of the KOEI SNES engine + game: 512 named bytecode routines across 10 code modules
(root 146, comroot=AI 117, settei 82, kisetsu 75, excsub1-6 = 92 event scripts), 218 opcodes named,
69 syscalls mapped, ~238 strings of game text recovered, layered .src source, and a Mesen .mlb
(484 labels). Two decompiler-output experiments kept: a C lift (bytecode→pseudo-C) and a
DREAM-style structurer (65-75% of routines already goto-less from conditional structuring alone).
$00; a 22-byte-param-block syscall ABI.root resident at $7E:2000; a $7200 slot
time-shares comroot(AI)/settei/kisetsu; a $C300 slot holds excsub1-6.call.ind overlay invocation; state = shared $F800+
globals; code = overlays call resident root as a shared library (do_syscall/memcpy/…).cbmdata1/sdata are DATA resources (combat tables, sound), not code.A WRAM-global data-walk (naming the RAM variables the way the routine walk named the code —
data/root/globals.toml, auto-merged into every overlay) exposed the VM’s memory layout, and it
inverts the NES/6502 convention. On the NES the reset vector sits at the top ($FFFC) and the
zero page ($00xx) is the primary fast-variable store. The KOEI SNES VM flips it (a Z80-like
“grow up from the bottom”):
WRAM region (bank $7E) |
Role |
|---|---|
$2000–$6FFF (bottom) |
code — overlay modules relocated here, root resident at $2000, growing up |
$00xx (direct page) |
secondary scratch-register file, reused per routine (same address = grid-col in one routine, syscall-id in another) |
$C0xx / $F2xx–$F4xx (top) |
record tables — Gemfire $C0xx, ROTK2 packs them higher (province_table $F424, officer_table $F204) |
$F8xx–$FExx |
dedicated game state (cur_province_ptr, cur_sel, cursor_cell, hdma_write_ptr) |
$FFF7–$FFFF (very top) |
the VM’s 32-bit accumulator / working-register file — hotter than anything else (g_FFFF: 878 rd / 580 wr / 293 fns in ROTK2) |
The split is the point: a small set of dedicated semantic variables (record bases, turn/AI/battle
state — nameable, ~15–20) versus a reusable scratch-register file ($00xx + $FFFx) that must stay
g_XXXX because a single name would misdescribe it. Naming the dedicated ~15–20 resolved 500–1100
references per title and made the record accessors read as source (idx*36 + lord_table). It also
surfaced incidental structure — the HDMA table is built over the $FF00 battle-grid region, and the
battle-grid cell address is (row*10 + col) + battle_grid, low nibble = the occupying troop slot.
Cross-checked against the NES rot3k2-decompiler:
$E9 hostcall, $AC call, $DD/$EA call.ind, $CF
ret, $D5/$D9 switch, $D6 jmp, $D8 brz, $B7 32-bit prefix, $00-3F frame matrix all at the
same byte. Counts: NES 219 vs SNES 218.Why it holds: the bytecode targets a virtual 16-bit address space + stable syscall ABI; all
memory-architecture difference (banking vs overlays) is pushed into the native kernel and a couple of
dispatch syscalls. The bytecode is architecture-agnostic by design — which is exactly why the opcode
numbers line up. Dead code (unreached blocks) is the fingerprint of recompiling shared source for a
richer kernel (e.g. a manual num_to_ascii left in but bypassed by a display syscall).
“Compare, don’t derive” — decoded each SNES handler on its own, used the NES only as a cross-check.
Because both sides were reconstructed independently, the byte-for-byte opcode match and stride-25
province match are confirmations, and even caught a stale NES memory (the old “49×24” was NA1’s fief
schema, not ROTK2). The koei-snes toolchain is profile-driven and transferred to new modules with
zero changes — the compounding-through-reuse thesis, now demonstrated across an architecture boundary.
Next cross-gen twins: Gemfire (now fully decompiled — all 591 bytecode routines named across 11 overlay modules; see Gemfire (SNES) fully decompiled, which also caught a systematic offset-0 bug in this ROTK2-SNES walk) and Uncharted Waters (the genre swerve — trade/exploration, the real test of whether the VM is a general game engine, not strategy-specific). After: pure SNES titles — do they finally spend the kernel’s teeth (Mode 7, SPC700) once they’re not porting? The portable VM is the fixed baseline against which KOEI’s ambition (PTO landing, Operation Europe over-reaching) can be read directly.
reverse-engineering · snes · koei · game-design · decompilation