referenceUpdated Aug 30, 202618 min read$08

I Took Marvel vs. Capcom Apart. Here's What Was Hiding Inside.

Secret developer tools, unused character routes, stolen boss attacks, and fighters in games they never shipped in. The complete CPS-2 research trail, including the dead ends.

cps2reverse-engineeringseries-indexsystems-engineering
On this page

Start Here

This series documents my attempt to make a 1998 arcade game accept things it was never designed to hold: larger ROM regions, new diagnostics and audio, then two native fighters beyond the end of its 23-character tables. The through-line is not automation. It is the decisions made after a plausible explanation failed, and the live evidence required before a binary patch became a public claim.

Short human path (three posts):

  1. Dark Sakura: No Room for a 24th Character -- start with the donor wrong turn, the decision to go native, and the measured bugs that finally made the port work.
  2. Shuma-Gorath, Whole -- see whether the method survives a second character and a full relocated code block.
  3. The Editor Was Me -- then read how I directed fallible agents, retracted working code built on a false premise, and decided what counted as proof.

Full reference path: Continue into Reading Order for the complete chain from binary forensics through expansion, diagnostics, audio, native ports, stages, and feasibility studies. The deep sections preserve addresses, hashes, table layouts, failed hypotheses, and open defects for readers who need to reproduce or challenge the work.

Target system: CPS-2 arcade board (1998). Motorola 68000 CPU @ 11.8 MHz, Z80 sound CPU @ 8 MHz, QSound DSP, 8-way interleaved GFX ROMs, hardware-encrypted program code.

Where it runs: in an emulator, against a real modified CPS-2 ROM image. The expansion work (64MB GFX, 16MB QSound) needs a custom FBNeo core, so those builds would not boot on a stock CPS-2 board as-is, and they break Fightcade netplay.

Prerequisites: Comfort reading hex dumps, basic familiarity with assembly concepts, general understanding of memory-mapped I/O.

Two milestones from the back half of the series, as they actually render in the game:

Shuma-Gorath's Mystic Stare with six eyeballs in a hexagonal ring
Post 9 // Mystic Stare, six eyeballs in a hexagonal ring
MSHvSF's New York street tier rendering live
Post 10 // NYC street tier, live

Reading Order

The full posts are chained: each one depends on discoveries from the ones before it. For a chronological technical foundation, read 1 through 15; for the human project arc, use the three-post path above.

One structural note. The opening of the series covers a single subject -- MvC1's secret-character system, its table formats, and the unused id 0x2E -- and it was originally written across five overlapping posts. Two of them are now the canonical treatments: the hidden-character system (the fullest: three table formats, the position-check blocker, the interrupt hook that bypassed it) and binary forensics (the 0x2E evidence). Posts 1 and 3, plus two companion pieces, keep their own angle -- method, injection mechanics, the input-code catalog -- and point at those two for the deep dives instead of restating them.

How it works
Drawing the diagram…

1. Reverse Engineering a UI State Machine from Compiled Binary

What: The method post. How do you locate an entity system in a stripped binary? Search for data whose shape you can predict -- a base character ID sitting next to its secret ID -- and read what the spread of the hits tells you. Also the record of two things this post got wrong and later corrected.

Key discoveries: Five base→secret pairs at five widely separated addresses in three ROM sections (so: no unified table). Character data table at $065CCA (32 bytes/entry), player structures at $FF3000/$FF3100, unlock flags at $FF803C-$FF804C, cursor grid indices that are not the on-screen reading order.

Points to: the hidden-character system for the table formats and the position-check blocker.

Read →


2. Binary Forensics: Recovering an Unreleased Character  (canonical)

What: Using the entity system knowledge from Post 1, systematically scan for hidden content. Found 85 code references for character ID 0x2E (Armored Spider-Man), 4 conditional load points, and evidence that this character was planned but disabled before shipping. This is the canonical write-up of the 0x2E evidence, the CPS-2 memory map, and the palette work.

Key discoveries: BEQ-guarded load points at $06DC76, $096B86, $0A8A9E, $0BAD8E. False positive filtering for IDs 0x30/0x32 (opcode 0x303C). All 4 secret character load functions decompiled to C, plus character_load_main at $0FF9B4.

Read →

Companion: Finding Hidden Features in Compiled Code -- the reference sheet for this post: all six shipped secret codes with byte patterns, the complete character ID map 0x02-0x3E, and the detector's shape.


2b. Reverse-Engineering MvC1's Hidden-Character System  (canonical)

What: The deep dive Posts 1 and 3 both point at, and the fullest single treatment in the series opener. Three incompatible table formats for one conceptual operation, the hardcoded position-check list that makes data-only patching impossible, the 13-state select machine at $01DFA0 -- and the decision to stop fighting all of it and hook the VBlank interrupt instead.

Key techniques: Autovector 2 interrupt hook (116 bytes at $0F1FA0, original handler at $054A preserved via JMP tail call), 60-frame char-change safety window, input edge detection, MAME Lua prototype before ROM commit, Capstone-verified assembly.

Read →


3. Injecting New Behaviors into a Binary Without Source Code

What: The injection mechanics, isolated: the CPS-2 decrypt/patch/re-encrypt loop, finding somewhere to put new code, the input-sequence matcher as 68000 assembly, whole-palette stamping in RGB444 -- and the generalizable trade at the end of it (when the target is tightly coupled, stop modifying it and own a periodic entry point instead). (Status: the Armored Spider-Man plan was later retired -- the $2E slot pivoted to a Dark Sakura port from MSHvSF, now executed; see Post 7. The techniques remain valid, and are load-bearing in Post 8.)

Key techniques: cps2crypt round trip, injection into free ROM space at $0F1000+, cursor-position-guarded buffer comparison, proposed 10-input directional code.

Points to: the hidden-character system for the hook that replaced this plan.

Read →


4. CPS-2 Maximum Memory Model & Expansion

What: Expanding every ROM type to hardware theoretical limits. GFX ROM files from 32MB to 128MB, Audio from 8MB to 16MB, Program from 1MB toward 4MB. Built a fully automated Python pipeline. (Corrected July 2026: padding is file-level only -- the stock emulator masks GFX at 32MB and QSound at 8MB. The genuine cap-break, 64MB GFX + 16MB QSound, came via a custom FBNeo core; see the post's Editor's update.)

Key techniques: 8-way GFX ROM interleaving, transparent padding (0xFF for GFX, 0x00 for audio), GOLD marker verification, MAME compatibility.

Read →

The overclaim, for the record: an earlier and narrower write-up of this same pipeline claimed the padded space was addressable. It was not -- an independent signal (pixels from the upper GFX region) finally falsified it, and the correction is folded into the post above.


5. Runtime Verification via Test Menu Injection

What: Embedding a 22-line diagnostic screen into the game's F2 Test Menu using 68000 assembly. Proves every claim from the documentation by running verification inside the game itself.

Key techniques: Hardware-polled input, dual OBJ bank clearing, VRAM tile fix, sound test with live QSound programming, interrupt-disabled display.

Read →


6. Cross-Architecture Audio Injection

What: The most advanced post. Porting MvC2 Dreamcast soundtrack to CPS-2 by building a multi-bank streaming engine in Z80 machine code, with QSound DSP programming and a custom 68000 sound test interface.

Key techniques: Z80 dispatch table hook, ISR exit hook for bank-advance streaming, QSound register programming (int16_t constraints), 68000 sound command range extension.

Read →


7. The Same-Generation Port, Executed: Dark Sakura

What: The capstone: everything above put to work. Dark Sakura (MSHvSF char-id $2A) fights in MvC1's $2E slot via a donor-chassis architecture -- the slot is force-remapped to real Ryu ($12) at state-machine dispatch, so Ryu's input detector, animator, and handler spine run her, while a composer hook forces her imported 9,520-tile animation tree in the custom 64MB core's upper GFX region. Documents the failed reanimate-in-place approach, the wrong-premise catalog, and probe-vs-live verification discipline. (Status: the donor era is history -- superseded by the native ports of both secret characters; see Post 8 and the project status table below. The donor-era critical path -- authentic tatsu cells, supers, Hunter chains -- never shipped on this lineage.)

Key techniques: Dispatch-layer remap + crash thunks, DS_MOVEMAP (group, sub) translation table (19 rows at $23C820), DS_SPECDRV hand-assembled 68000 special-move driver at $23E000, Capstone branch-asserted thunks, headless FBNeo Lua probes with a live human oracle.

Read →


8. No Room for a 24th Character: The Native Port

What: The donor chassis retires. Dark Sakura runs native -- in-fight char-id $52 = $2E, her own dispatch entries, tiles, name, and dark-gi palette, in a stable live match (0 wild jumps, 0 non-boot resets (earlier audit in the chain) on full harness runs). Getting there meant instrumenting the emulator's 68000 core after three high-confidence static fixes moved the crash by zero frames -- and the root causes were structural: CPS-2 ROMs decrypt differently for opcode fetches vs. data reads (a relocated thunk reproducing a PC-relative table read absolutely reads ciphertext), a byte-cell that had to be table data and handler opcodes simultaneously, a sprite bank-word table missing the upper-32MB bit, and a palette fill that never consulted her source pointer. Closes with the Shuma-Gorath "compressed sprites" blocker dissolving -- an artifact of the same two-view encryption model corrupting our own analysis dump.

Key techniques: Emulator-core PC ring buffer + wild-jump detector (fault localization in one run), decrypt-view table copies in unencrypted arena space, displaced-instruction thunks for dual-use byte-cells, live A/B OBJ-RAM diffing (donor vs. native), per-frame gated palette stamping, chainable data-only identity post-processors.

Read →


9. Shuma-Gorath, Whole: Relocating a Character's Real Code

What: The native recipe, generalized to a second character in its strongest form. Where Dark Sakura's port leans on shared engine paths, Shuma-Gorath ($30) relocates an entire block of his real MSHvSF code ($0301DE..$032A3E) verbatim into arena space at $3FA060, re-entered through trampolines, so his logic runs as his own. His "compressed sprites" turned out to be our own decryption tool's garbage (one ROM, two truths again); his crashes trace to the shared past-end table defect; and his flagship Mystic Stare fires a visible, moving eyeball -- its object allocator found by tracing a live Ryu Hadouken, rerouted with three encrypted bytes, stock-safe (Ryu's fireball still fires) -- and spawns all six eyeballs in a hexagonal ring -- verified live on the build where the ring landed, with a target-relative orbit landed since. Bounded honestly: the eyeballs still carry no hitbox, and the ring has not been visually re-confirmed on the current tip. Includes live gameplay.

Key techniques: Verbatim code-block relocation + trampoline re-entry; the two-view (opcode vs. data) decryption trap that faked "compressed" data; live-trace object-allocator discovery; a 3-byte object-type dispatch reroute; independent adversarial byte re-derivation + screenshot re-gate.

Read →


10. Porting a Two-Tier Stage: MSHvSF's Collapsing NYC Street

What: MSHvSF's New York street -- the one where the ground gives way and the fight drops into the subway -- heads to MvC. Reverse engineering revealed the underground is not a second stage: it's the same stage id $06, scrolled +512 pixels into one tall tilemap, flipped by a police-van bounce counter. The signature police van renders and animates live in MvC, and the load hang that blocked the stage has since been fixed -- stage $0D loads live. What remains open is the art: a ~400KB scroll-art shortfall (74 tiles dropped), so the stage renders incomplete.

Key techniques: Tier-flip RE (bounce counter $FFFF30, gate $06A70A, scripted camera pan, 51/64 palette-page swap); a ~400KB scroll-art budget against MvC's 51KB largest free run, solved via GFX-space donation + the 64MB core; the GFX poison test ("all-FF is not sufficient to seat a tile").

Read →


11. What a Saturn Port Teaches You: MSHvSF's Byte-Identical Data

What: MSHvSF's 1998 Sega Saturn port turns out to be a source-level SH-2 recompile with mechanically-converted assets. The character data tables are byte-identical to the arcade ROM (Shuma's MC05 = 1,604-byte frame records, 100% match); sprites are a deterministic pen-map, saturn = (arcade + 1) & 15 away from arcade tiles (0 of 7,680 pixels differ on a decode test); the whole colorspace is one 128KB LUT on the disc. That makes the disc a plaintext second witness for the arcade RE the native ports depend on -- and lets the post cost, honestly, whether MvC content could go the other direction onto Saturn hardware.

Key techniques: 2-bit-code bit-packed sprite codec proven by 0-pixel-diff decode; PALET.TBL 12-bit → RGB555 LUT; streaming-slot layout (0x02486BC0 + k*0xA3320, 4MB RAM cart mandatory); the four-phase MvC → Saturn cost model (tooling through full game, 600-1500h+, phases 3-4 still research).

Read →


12. Magneto From XvSF: The Cost, the Two Trees, and 100% of HIT EDITER

What: The unified Magneto article. The July costing still stands: 15,304 tiles against 2,542 in the largest free stock bank, a striker seed that is shared FX, and an extraction "blocker" that was a corrupted dump. The eviction prediction does not: Magneto sits on Captain America $04, fight tree MAG_TREE $588000, editor/select still Cap $107FF0. HIT EDITER wrap is donor slot count. A disposable magtree dump printed the holes. Join v5 closed 186/186 names and slots. Scoped gameplay is a seven-segment reel, not full parity. V18 identity smoke passes Magneto and Storm P1/LP; that is two paths, not release.

Key techniques: Single-bank rule $E67C8; two-tree seating (DIR_ANIM vs MAG_TREE); GROUP_SKIP dest dumps; editor FLY at L1 28 not mapped 30; fail-closed named-row vs table.

The abandoned Hulk-eviction prediction later became Dual Rogue: public $06, secret $26, CLEAN $399816. Its failed pre-ledger candidate remains part of Post 15's history; the current public state is the exact-V18b 17/97 playtest ledger. XvSF 68K is not copied onto $02F6FC. The July costing URL redirects here and is not a second Magneto post.

Read →


13. Storm and Magneto's Select-Grid Icons, Made Permanent

What: A later, tightly scoped identity-layer result. A controlled read tap falsified the roster-pointer-table theory, located the stock 3×5 grid in the Scroll 1 tilemap, and led to a 2,736-byte patch across six ROM members. Storm now occupies Jin's grid cell and Magneto occupies Captain America's from ROM data itself: no Lua writes, no debugger pokes, and no changes outside the two cell interiors in the regression capture.

Boundary at publication: This completes two select-grid icons, not two fighters. The next post advances both characters through scoped gameplay receipts while preserving measured damage, identity, diagnostic and coverage gaps. This post itself proves only the identity layer.

Read the icon work as it landed inside the unified Magneto article →


14. Storm and Magneto Are Fighting—Scoped Gameplay, Receipts Attached

What: One composed ROM now supports both demonstrations. Storm moves, performs all six standing normals and completes Lightning Storm for ten life debits, 112 damage, native combo counter 10 and 35 simultaneous owned class-$28 objects. Her ten command masks are semantically derived from verified XMvSF replay frames 5090..5099; exactly three simultaneous-direction/SOCD rows change $5F8 (D+L+R) to $5E8 (D+R). Magneto completes 7/7 named segments: movement, sampled normals, Disruptor, Force Field, Flight, Tempest and Shockwave.

Boundary: This is gameplay progress, not full parity. Storm's HUD still says JIN; her harness positions the select cursor, writes one diagnostic core-enable and stabilizes P2 while leaving all P1 fighter fields untouched. Magneto's harness grants meter twice and stabilizes his defender; standing-HK, Shockwave and shared-audio mismatches remain. Throws, tags, Variable Cross, story/AI and complete presentation remain open.

Read and watch the reel on the unified Magneto article →


15. Dual Rogue: Helper to Hulk's Chair

What: XvSF Rogue on vanilla Euro mvsc, public Hulk $06 and secret Orange Hulk $26. Exact V18b owns the fixed evidence ledger at 17/97 accepted, one waived, and 79 blockers; parity remains null. Its bounded acceptances cover walk/body-push, stolen Hadouken, and Goodnight scopes, not the complete fighter. Private ROM V29 is not offered and is not a playable candidate. V28 establishes a byte-identical 8/52 ordinary-input opcode-read/prefetch baseline. Matrix M1 repeats it and adds only fixed-slot $CB50 inside two exact active-CATCH configurations, moving the strict observed subset to 9/52 while leaving 43 callers unknown. The four MP+MK lanes add no caller and do not prove assist activation. None of this proves instruction retirement, behavior, or gameplay, and every gameplay/release credit remains zero. The previous V18b package has been withdrawn while a new one-file builder completes validation. Full movement, normals, moves, supers, chains, combos, CATCH choreography, audio, presentation, team/stock regression, and extended manual play stay open: Dual Rogue download status.

Key techniques: Exclusive-only GFX probe (0xFF is not owned); palette-pair eviction; dest overlay 0; join v5 ETC at editor L1 26; CLEAN $399816 68K graft; restored lea $190(a6),a3 contract for $019710; identity-preserving $2A→$16 / $2C→$1E behavior aliases; Power Drain grab veneer $39D0CA; 96-cell CSS atlas packing; empty mode-4 EFFECT stub; inclusive DATA/OPCODE boundary reconstruction; read-only reset/TRAP-0 trace; ordinary-input CSS/scheduler/join read/prefetch and active-stack correlation.

Read → · Download status


Supporting Posts

The Reverse Engineer Was a Fleet of Claude Agents. The Editor Was Me.

What: The methodology post for the whole series: how I used parallel hypothesis and adversarial-refutation agents as tools, live-gated their output in the emulator, and documented four cases where the workflow caught a confident error.

Read →


The Harness That Says No

What: Two machines. Nested HIT EDITER dumps plus join v5 closed P01 names and slots for five fighters. The live damage scorer still fails closed without a source expected value. Magtree and fly38 editor zips are dump instruments, not ship ROMs.

Read →


Making Reverse Engineering Work Reproducible

What: Meta-post on methodology. How to structure RE work so both humans and AI can execute it. The "observe → instrument → validate → automate → publish" loop.

Read →


Platform Architecture: Next-Gen Emulator with Metal + ML

What: Architecture specification for a development platform that combines Apple Metal rendering, CoreML/PyTorch inference for AI opponents, and massively parallel headless training. The CPS-2 work validates why these tools matter.

Read →


Project Status (updated August 30, 2026)

Shuma-Gorath's select-screen cell: IRON SPIDER before, SHUMA after
Select screen // IRON SPIDER before, SHUMA after
ComponentStatus
ROM expansion (148MB file set)Complete at file level; addressable expansion (64MB GFX + 16MB QSound) via custom FBNeo core (breaks standard Fightcade netplay)
ULTRA SETTINGS diagnostic screenComplete
Audio injection (5 MvC2 tracks)Complete at ROM level (banks past 8MB playable only under custom core)
Multi-bank streaming engineComplete (Z80 hook + ISR streaming)
Character ID 0x2E forensicsComplete
Secret table forensics (3 types)Complete
Armored Spider-Man injectionRetired (it was an addition attempt; no stock character was ever removed) -- the $2E slot pivoted to the Dark Sakura port (MSHvSF, char-id $2A there) (Post 7)
Cross-game character importNative, both characters, deployed (custom 64MB core, offline only) -- Dark Sakura ($2E) and Shuma-Gorath ($30) run their own relocated MSHvSF code and animation trees, no Ryu remap; a cumulative chain of live-gated fixes (each verified with the visible effect + match-liveness + stock-control asserts) each stacking on the last; names on both screens (SHUMA / SAKURA on select, SHUMA-GORATH / SAKURA on the lifebar); Mystic Stare spawns all six eyeballs in a hexagonal ring (verified live when the ring landed), with a target-relative orbit landed since; Mystic Smash has its spikes and a live hitbox; Shuma has his own voice table; win/KO crash and dizzy softlock fixed (Post 8; Shuma's full story is Post 9). Donor chassis (Post 7) is the historical record
Native-port remaining workThe eyeballs still carry no hitbox (and freeze during hitfreeze), and the ring has not been visually re-confirmed on the current tip; residual Ryu physics clamps remain past the seated walk/jump/gravity rows (mini "RYU" tag under the portrait); Dark Sakura's SFX are still silenced, not replaced; supers (Devitalization / Chaos Dimension / Hyper Mystic Smash) are researched but their builds have not started; pre-fight walk; the moveset identity gate reads pass=11 FAIL=6
NYC two-tier stage portLoad hang fixed, art shortfall open -- police van renders + animates live in MvC; tier-flip fully RE'd; the load hang was root-caused and fixed, and stage $0D loads LIVE in the current lineage; the ~400KB underground art shortfall (74 dropped tiles) remains open, so the stage renders incomplete (Post 10)
Saturn MSHvSF port studyDocumented -- character data tables proven byte-identical to arcade; sprites a deterministic (pixel+1)&15 pen-map (0/7,680 pixel diff on decode); MvC → Saturn direction costed as research (Post 11)
Storm and Magneto imports (XvSF → MvC)Scoped gameplay milestone; HIT names/slots 100%; V18 identity smoke 2/2 P1 LP; full parity still open -- Magneto seated on Cap $04 with MAG_TREE $588000 for fight and Cap $107FF0 for select. Join v5 186/186. Gameplay reel 7/7 named segments on composed ROM md5 4279ea69bb585ad92dce98319082490b. Identity smoke is two paths, not release. Hyper Grav, throws, tags, V-Cross open. The July Hulk-eviction prediction is retracted (unified Magneto; harness)
Dual Rogue (XvSF → MvC, Hulk pair)Experimental V18b: 17/97 accepted, 79 blockers; not full parity. New one-file builder is in validation; download temporarily withheld. Private ROM V29 is not offered or playable. Matrix M1 broadens the strict opcode-read/prefetch subset from 8/52 to 9/52 through repeatable $CB50 observations in active CATCH; 43 callers remain unobserved, assists gain no new site, and all gameplay/release credit stays zero. Full movement, moves, supers, combos, CATCH choreography, audio, presentation, team/stock, and extended manual play remain open (Dual Rogue)
Stage soundtrack replacementPlanned

Written by Daniel Plas Rivera · 3,898 words · $08

ShareXLinkedIn