case study15 min read$28

MvC Kept Making Me Delete a Fighter. I Added Another Column.

The 1998 select screen now renders 20 visible boxes. Five new spaces, no erased stock routes—and the names, portraits, and fighters that still have to fill them.

cps2reverse-engineeringmarvel-vs-capcomcharacter-selectrom-expansion
CPS-2 Reverse EngineeringPart 30 of 43
Browse all writing
On this page

I have been looking at the same Marvel vs. Capcom character-select screen since 1998. Fifteen faces. Three columns. Five rows. Every time I build another fighter, the game asks the same bad question: who are you willing to erase?

I am tired of answering it.

I do not want Zero to exist only because Mega Man disappeared. I do not want Magneto to live behind a route that still looks like somebody else's slot. I do not want a new character to require a long secret sequence that a player has to memorize, look up, or accidentally break while trying to reach it. A new fighter should be allowed to be new: its own ID, its own portrait, its own box, and one obvious directional input from the screen everyone already knows.

Real FBNeo capture of Marvel vs. Capcom rendering four visible portrait columns and five rows after a new column was added to the left
REAL EMULATOR CAPTURE // four visible columns × five rows; the new left column mirrors the former left edge for this renderer proof

That is the missing frame: not a prompt image, not a composition, and not the hidden-character route counted as a visible column. It is the running game drawing four columns by five rows on screen. The isolated layer below makes the count even clearer.

Real FBNeo Scroll 1 layer capture showing exactly four portrait columns and five rows
REAL SCROLL 1 CAPTURE // 20 rendered boxes, with the side art, title and background disabled for layer isolation
Visible count versus logical count

The screen is 4 visible columns × 5 rows. The old hidden-character route is not part of that visible count. When the hidden route is included in a system diagram, the model is 5 columns × 5 rows: one hidden logical column plus four rendered columns. Roll remains a separate side branch. The two counts describe different things and are labeled separately throughout this post.

Every game screenshot in this post is now an emulator capture. The supplied generated reference image was useful for communicating the desired composition, but it is not published here as proof. Diagrams are explicitly captioned as maps.

The replacement trap

The character work already on this site shows why another slot matters. Zero was built through Mega Man's destination. Magneto has been reachable in the select flow while the grid still presents the stock arrangement. Dark Sakura and Shuma-Gorath can own side art, names and battle identity while the persistent mug surface remains a different subsystem.

Zero standing on the left side of Marvel vs. Capcom's character select while the destination remains Mega Man's slot
Replacement pressure // a new fighter arrives through Mega Man's address
Magneto highlighted in a live Marvel vs. Capcom character-select capture while the stock three-by-five portrait grid remains
Secret-route pressure // Magneto is reachable, but the visible grid has no new home for him
Before-and-after Marvel vs. Capcom select captures showing Dark Sakura side art and name replacing the prior Iron Spider presentation
Identity layer // Sakura earns her side art and wordmark; the 15-cell mug surface is unchanged
Before-and-after Marvel vs. Capcom select captures showing Shuma-Gorath side art and name while the stock grid remains
Identity layer // Shuma is native in the port, but still needs a distinct visible seat

Those are valuable milestones. They also expose the architectural debt. A fighter can be present in the engine while borrowing a roster address, a mug, a name path, a hidden unlock path, or all four. The fourth column is not decoration. It is the UI layer that lets the character roster finally match the work happening underneath it.

The player-facing idea is intentionally simple: stand on one of the five characters at the current left edge, tap LEFT once, and arrive at a new fighter. No wiki. No twenty-direction ritual. No guessing whether a hidden flag is active.

First, make room without deleting anyone

The menu cannot point at new characters if the ROM has nowhere stable to hold their programs, graphics and audio. That is why the first deliverable looks boring: it takes the exact encrypted Euro 980123 parent and creates an expanded—but otherwise untouched—base.

ROM expansion map showing 21 vanilla members preserved and eleven new members providing 32 MiB graphics, 8 MiB QSound and 1 MiB 68000 program capacity
FOUNDATION MAP // the installer adds capacity beside the original set instead of editing a stock member in place

The output adds:

new memberscapacityrole
mvc.h13mmvc.h20m8 × 4 MiBupper graphics region for imported character art
mvc.h11m, mvc.h12m2 × 4 MiBupper QSound sample region
mvc.exp1 MiBflat 68000 expansion window at $500000–$5FFFFF

That is 41 MiB of neutral new capacity and zero changed bytes across the original 21 members. The custom core contract is pinned to branch cps2-sprite-expansion, commit c025d0ecfb9187a6962dc94ac3c240a8ae258fc9. Stock MAME and ordinary Fightcade do not understand these added members; this is a deliberate custom-core project, not a disguised stock-compatible patch.

68000 program space used by the expansion design
1 · stock encrypted program23$000000$600000
  • 1
    stock encrypted program
    $000000$400000 · 4 MB
    vanilla program remains the authority
  • 2
    unmapped gap
    $400000$500000 · 1 MB
    not treated as free ROM
  • 3
    mvc.exp
    $500000$600000 · 1 MB
    1 MiB expansion arena in the pinned core
The new window is explicit. It is not another claim that padding inside a ZIP magically makes bytes addressable.

The live gate opens the expanded archive in the pinned core, confirms all 32 members report OK, reads $FF at five points spanning the new program window, reaches the character-select screen, chooses a team, and enters a fight. Storage is not inferred from file size; the emulator has to fetch it.

Reconstruct the old menu before changing it

The stock select screen is not one columns = 3 constant. It is several cooperating tables:

raw-program offsetstock sizeresponsibility
$0E2C0A21 XY pairs / 84 bytes15 normal cells, Roll, and five secret positions
$0E2C5E23 words / 46 bytescharacter ID → XY token
$0E2C8C23 × 4 words / 184 bytesbase UP, DOWN, LEFT, RIGHT navigation
$0E2D44$0E3194seven more 184-byte tablessix individual unlock states plus the master state

The first proof was deliberately conservative: generate the stock blobs from a human-readable model and compare every byte with the primary raw program. If the generator could not reproduce the old screen exactly, it had no business authoring a new one. It passed for the XY table, the ID map, and all eight navigation variants.

The five visible Y rows are $0040, $0060, $0080, $00A0, $00C0. The three existing X columns are $00D0, $0100, $0130. The measured fourth-column position is $00A0, one 48-unit pitch before the current left edge. The live proof at the top renders that exact additional pitch.

Technical map comparing Marvel vs. Capcom's stock three-column by five-row roster with a measured four-column by five-row roster and a new left column
TECHNICAL GRID MAP // five coordinates are prepended visually; this is a diagram, while the opening figures are real emulator captures

The new visible IDs are $30, $32, $34, $36, and $38. Existing $2E work is reserved for Iron Spider compatibility rather than being silently reused. The names NEW A through NEW E are temporary seats, not claims about which five ports will occupy them.

Table delta

The new table model grows only where the fourth column needs it

Generated binary sizes; proposed values are not yet injected into the encrypted program.

  1. visible XY cells+5 / +33.3%
    stock screen154×5 model20
  2. XY table bytes+20 / +23.8%
    21 coordinate pairs8426 coordinate pairs104
  3. ID-map bytes+12 / +26.1%
    through id $2C46through id $3858
  4. bytes per navigation state+48 / +26.1%
    23 IDs18429 IDs232
The expansion keeps the six hidden coordinates and adds five visible ones. Eight navigation states grow together.

Add a column before the old ones—not on top of them

Appending a fourth column on the right would collide with Roll's stock secret route and the visual space already used by the screen composition. The useful opening is on the left. More importantly, the left insertion has a compatibility property the secret-code audit can prove.

The base rule for each row is:

new cell LEFT   -> itself          old left-edge LEFT  -> new cell
new cell RIGHT  -> old left-edge   new cell UP/DOWN    -> new column neighbor

So the top row becomes $30 ↔ $16 ↔ $12 ↔ $1A, the second becomes $32 ↔ $1E ↔ $14 ↔ $20, and so on. Top and bottom new cells clamp vertically just as the stock edge cells do. Every other stock link remains unchanged.

This detail matters because cursor movement and selection are different proofs. A box can draw at the right coordinate while committing the wrong fighter ID. The planned one-cell engine test must therefore show the new mug, accept P1 and P2 cursor movement, commit the new ID, survive team selection, and reach gameplay before the remaining four seats are enabled.

Preserve every old secret door

MvC does not recognize its six secret codes as abstract arrows. The program stores the character-ID sequence visited by each code. That was the compatibility break hiding behind the simple grid edit: change navigation carelessly and the same physical input string walks through different IDs, so the matcher never unlocks its flag.

The raw matcher arrays were decoded directly from the program. Then each sequence was replayed through every navigation state before and after the proposed insertion: six codes × eight tables × two models = 96 replays.

Five-column by five-row logical map showing one hidden route column, one new visible column and the three stock visible columns, with Roll as a side branch
LOGICAL 5×5 MAP // the screen renders columns two through five; the first column represents the existing hidden routes and is not counted in the visible 4×5

The six stock unlock edges stay exactly where players expect them:

unlockstock edge changed when active
RollMega Man RIGHT → Roll
Red VenomChun-Li UP → Red Venom
Orange HulkRyu UP → Orange Hulk
Gold War MachineZangief UP → Gold War Machine
Shadow LadyGambit DOWN → Shadow Lady
LilithWar Machine DOWN → Lilith

The base table, six individual-secret tables, and the all-secrets table all receive the same new left-column routing. Patching only the default table would create exactly the kind of “works until a secret flag is active” bug this project is trying to eliminate.

Marvel vs. Capcom secret test menu with hit editor, catch editor, scroll blocks and diagnostics
THE OTHER MENU // debug access made the character ports possible, but player access should be one visible box and one direction—not a research procedure

Finding the real portrait renderer

The repository contains an older, confident story: fifteen explicit calls near $005ECE–$005F06 looked like one per portrait, and a splice at $024AB4 tried to append a sixteenth OBJ entry. Runtime testing did not validate that story. The splice never produced the intended cell, later ran during gameplay, and emitted garbage tiles and numbers. The build script now leaves it off.

A synchronized OBJ capture made the contradiction sharper. The active object list contained the cursor, side art, title and UI objects—but no regular fifteen-mug pattern. The decisive test was to turn the CPS layers off one at a time in the running game. Sprites left the side presentation. Scroll 2 left the Earth. Scroll 3 left the star field. Scroll 1 left the complete portrait grid and nothing else.

Running Marvel vs. Capcom screen with a real fourth visible portrait column added on the left
All layers // the live game renders 4 × 5
Isolated Scroll 1 layer containing four columns and five rows of portraits
Scroll 1 only // the 20-box surface isolated

The stock box boundaries are 48 pixels wide and 32 pixels high. In Scroll 1 video RAM, the old left portrait column occupies six tile columns across twenty-two tile rows, including its border. The new destination is the six tile columns immediately before it. Copying that measured surface produced the two live captures above: 20 actual boxes, not an image laid over the game.

For this proof, the emulator instrumentation copies the old left edge into the new destination, which is why Chun-Li, Morrigan, Strider, Captain America and Gambit appear twice. That is deliberate: it proves geometry and rendering without pretending five character portraits are finished. The production patch still has to move this copy into a ROM-resident writer, connect five new IDs, and replace the duplicated art with each fighter's real mug.

A player installer, not a folder of patch instructions

The installer follows the hardened pattern used by the other MvC releases. The player supplies their own clean mvsc.zip. The command validates the archive SHA-256, exact case-sensitive member names, member count, sizes and per-member hashes. It rejects duplicates, extras, mislabeled sets, wrong regions, modified parents and existing expanded sets.

Vanilla-rooted installer transaction
Drawing the diagram…

The Python authority and the no-dependency Windows CMD produce decompressed members that are byte-identical and identically ordered. Their ZIP container hashes differ because their deflate implementations differ; the verifier compares the payload that the emulator actually loads. The installer never edits or overwrites the clean input and contains no Capcom ROM data.

Current Windows installer SHA-256:

4bd7b0d32ca105e0a30b9708c049352f458c92b9a3aca30af4a7eacf12fe923e

The scorecard keeps the ending honest

The fighter-port scorecards elsewhere on this site taught a useful lesson: “present,” “modeled,” and “verified” are different states. Character expansion needs the same discipline.

Character expansion scorecard separating verified foundation, layer isolation and live four-column rendering from pending ROM writer, selection and regression work
IMPLEMENTATION SCORECARD // Scroll 1 and the live 4×5 renderer proof are green; ROM-resident drawing and playable new seats stay gray until their own gates pass

Release boundary

What each expansion layer owns today

Verified means a named artifact and replay or runtime gate exists. Modeled means generated bytes exist but are not in the live ROM.

nativeverifiedbridgeddonorpending
routedata modelwritten ROMlive runtimeplayer release
neutral ROM capacityverifiedverifiedverifiedverified
stock select preservationverifiedverifiedverifiedverified
4×5 XY + ID + navigationverifiedpendingrelocation and reader repoints openpendingpending
six secret-code routesverified96 model replayspendingexpanded tables not injectedpendingpending
Scroll 1 portrait surfaceverifiedbridgedverifiedlayer-isolated live capturepending
four visible columnsverifiedpendinginstrumented, not ROM-residentverified20 live rendered boxespending
five new selection commitsbridgedpendingpendingpending
five playable fighterspendingpendingpendingpending
The v1 player installer releases the green foundation only. It does not convert a modeled row into a finished character-select feature.

How this moves forward

The next milestones are deliberately narrow:

  1. Move the proven Scroll 1 copy from emulator instrumentation into a guarded ROM-resident writer.
  2. Relocate the expanded XY, ID and all eight navigation tables into a safe arena.
  3. Repoint every reader and verify the old fifteen cells remain pixel-identical.
  4. Enable $30 only, draw its real left-column mug, move P1 and P2 onto it, and commit the ID.
  5. Replay the six secret codes, Roll, the all-secret state, continue, attract and team-select paths.
  6. Run at least ten minutes of gameplay after selection before enabling $32$38.

After that, the character ports stop competing for chairs. Magneto, Dark Sakura, Shuma-Gorath, Zero, Cable, Ken—or whichever five pass their own behavior gates—can be linked to visible boxes without rewriting the stock fifteen. The menu expansion is the seating plan; it does not waive the inspection for the people sitting in it.

The real screen at the end of this pass

This is where the work stands in the running game: twenty visible boxes, four columns, five rows, and the added column physically before the original fifteen. It is shown again here because the article should end on the evidence it actually produced—not on generated target art.

Real running-game capture of Marvel vs. Capcom with four visible portrait columns and five rows
END-OF-PASS LIVE CAPTURE // 4 visible columns × 5 rows; duplicated left-edge mugs mark the renderer proof, not finished fighter assignments

We have been looking at the old screen for twenty-eight years. Capcom never came back to give it five more doors. That does not mean those doors cannot exist.

The point is not to erase 1998. It is to preserve every character and every secret route from 1998, then make room beside them for everything we wished had happened next. Twenty-something years later, the ROM has room, the real menu can render the extra column, and the installer lets another person reproduce the foundation without becoming a reverse engineer first.

Next, those five new doors need their own names, portraits, IDs and fighters.

Written by Daniel Plas Rivera · 3,286 words · $28

ShareXLinkedIn