case studyUpdated Sep 4, 202648 min read$30

MvC Gave You Two Colors. I Gave Every Button Its Own.

Six buttons, 21 selectable fighters, 126 captured choices. The downloadable palette patch follows your pick from select to fight; v2 fixes remain in v8 testing.

All versions · Every public patch

cps2marvel-vs-capcomxmvsfmshvsfmvc2
CPS-2 Reverse EngineeringPart 37 of 43
Browse all writing
On this page
Version two: secret characters, Ryu's forms, and a five-level meter

The palette system has a second version in the combined v8 research build. It is still in testing, including the five-level meter; it is not a released standalone download. The v1 palette installer remains available. The v2 work addresses two things this page originally described as scope limits and one thing that was simply wrong.

The six EX/secret ids no longer reuse their parent's banks literally — each is anchored to its own stock identity and its other five banks are graded to distinct themes. Ryu's Ken and Akuma forms get six blocks each, which is what stops Ken wearing Akuma's palette on index 2 and Akuma rendering solid white on index 2; both were the six-color reader walking off the end of a hardcoded two-block table at $1CEC44 and $1CED04. The v8 Hyper-meter patch raises its cap to five levels in the statically verified build, with full-match testing still pending — that has its own write-up.

Still open: Mega Man's hyper palette rows pick their bank with the stock boolean, so Beat Plane and friends do not match the body on MP through HK. The fix is written and currently disabled, because the first build carrying it rendered him black.

Six colors are also included in the current DHC release

This page remains the standalone v1 release history and complete palette tutorial. The current DHC download includes the same six physical-button color system alongside Super-to-Super, Special-to-Super, the 0–999 display, Magnetic Shockwave, Gold Mega Optic, and native-Duo isolation. The standalone v1 download remains available for players who want only the palette feature.

Marvel vs. Capcom now gives every vanilla-selectable fighter six color choices: LP, MP, HP, LK, MK, and HK. The match body, the large character-select figure, and the selected-character animation all follow the same button. The body colors come from Marvel vs. Capcom 2, and the CPS-2 ROM does not expand by one byte.

Ryu's matched live MAME body shown side by side in the six installed palettes, labeled LP, MP, HP, LK, MK, and HK, followed by three aligned half-overlay comparisons
THE RESULT FIRST // Ryu in six live MAME body captures. LP, MP, HP, LK, MK, and HK each select a different installed MvC2 palette.
How many colors the six attack buttons actually select
  • Stock X-Men vs. Street Fighter2colors

    LP/MP/HP share one index; LK/MK/HK share the other

  • Stock Marvel Super Heroes vs. Street Fighter2colors

    the same punch-versus-kick collapse

  • Stock Marvel vs. Capcom2colors

    six physical inputs already exist, but the selector keeps only 0 or 1

  • MvC2 selectable donor banks6colors

    LP, HP, A1, LK, HK, and A2 become the six MvC1 cabinet choices

  • MvC1 with this patch6colors

    the stored index remains distinct through select and gameplay

The older CPS-2 games did not secretly expose six base colors. This patch authors that behavior using MvC2's six selectable banks.

what changes on screen

One button reaches three patched surfaces; two families stay stock

Blue means the MvC2 color choice is bridged into MvC1. Green means Capcom's original artwork or effect path is deliberately untouched.

nativeverifiedbridgeddonorpending
routeLPMPHPLKMKHK
match fighterbridgedbridgedbridgedbridgedbridgedbridged
selected animationbridgedbridgedbridgedbridgedbridgedbridged
large select figurebridgedbridgedbridgedbridgedbridgedbridged
small roster cellnativenativenativenativenativenative
HUD + effect palettesnativenativenativenativenativenative
Scope is visible instead of buried: the three large character presentations are six-way; small cells, HUD portraits, and separately loaded effects remain stock.

How six colors became true everywhere

6 STOPS · 2 CORRECTED · 2 RETRACTIONS · 1 OPEN
  1. LP  MP  HP  →  0
    LK  MK  HK  →  1

    Stock selector stores at $00B15C, $00BC62, and $00C464

    01 / 6prior art1996–1998 CPS-2 baseline

    Stock code keeps only punch or kick

    2 indicessix buttons collapse to 0 / 1

    Fresh MAME runs and the shared 68000 selector agree: LP, MP, and HP store 0; LK, MK, and HK store 1.

    COVERAGE 0% OF THE ANIMATION TREE

    Read this section
  2. Ryu shown in six live body palettes

    Six live Ryu MAME body captures, 48/48 RGB words each

    02 / 6correctedfirst integrated body build

    The body reader accepts indices 0–5

    6 body colorsselect presentation still incomplete

    Ryu reached six exact $60-spaced body blocks, but that did not prove the character-select artwork used the same index.

    COVERAGE 33% OF THE ANIMATION TREE

    Retracted here
    Six body colors meant the whole feature was finished
    Read this section
  3. Six palettes for every large character-select figure

    All 126 physical selections copied the expected 48-word source

    03 / 6measuredsecond loader

    The large select figure gets six sources

    48 / 48large-figure words match

    A separate hook routes the existing large select figure through a 21-by-6 palette arena while preserving Capcom's original artwork.

    COVERAGE 67% OF THE ANIMATION TREE

    Read this section
  4. War Machine HP on character select and in the live match

    MAME palette-RAM trace at the selected-animation loader $07AF6C

    04 / 6correctedselected-animation bug

    War Machine exposes a third collapse

    1–5 → bank 1stock animation loader behavior

    HP looked like MP during confirmation and became correct only in the match. The transition proved another loader was reducing every nonzero index to one.

    COVERAGE 67% OF THE ANIMATION TREE

    Retracted here
    A correct large figure and body proved the selected animation
    Read this section
  5. All 21 selectable fighters across six body palettes

    21 fighters × six physical buttons; 32/32 + 48/48 + 48/48 exact uploads

    05 / 6measuredv1 acceptance

    All three loaders agree

    126 / 126fighter-button selections pass

    Every vanilla fighter was selected with every attack button. Animation, large figure, and body each matched the requested source before the cut was accepted.

    COVERAGE 100% OF THE ANIMATION TREE

    Read this section
  6. palette write  →  ?
    identity change →  ?
    capture both + intervening frame

    N41 report; not yet reproduced or assigned to this patch

    06 / 6opencredited follow-up

    One frame-order question stays open

    trace pendingpalette write versus sprite change

    N41 reported that some select transitions may update palette before character identity for one frame. The article labels that question instead of converting it into a claim.

    COVERAGE 100% OF THE ANIMATION TREE

    Read this section
  • BODYnot yet true
  • FIGUREnot yet true
  • ANIMATIONnot yet true
Scroll the build history. Amber notches are claims that later evidence corrected; the three lamps show when each independent loader became measured.These are measured build stages, not a time axis.

Watch all 126 selections

All 126 MVC2-to-MVC1 six-color selections — 21 fighters × LP/MP/HP/LK/MK/HKYouTube ↗

The reel keeps every physical selection, the complete selected-character animation, and a live body result. Its evidence rail reports animation 32/32, large figure 48/48, and body 48/48 before each cut. The center roster-grid cells remain stock.

Choose your depth

Just play: the patch is at the top. See the result: continue through the roster and the 126-selection video. Reverse it: jump to the Ghidra, MAME, assembly, and encrypted-hex walkthrough.

Will this work on vanilla MvC1? Yes—and it does not add two characters

The installer starts from the exact, unmodified 21-member Euro parent mvsc.zip. That is the vanilla game: every member is checked by case-sensitive name, byte length, and SHA-256 before one patch byte is applied. Repacked clean ZIPs are accepted even when their member order differs.

Dark Sakura ($2E) and Shuma-Gorath ($30) are not selectable characters in that parent. The release prepares six palette blocks and directory rows for those IDs because the custom character project uses them, but vanilla never asks the selector to load either row. Dormant palette data cannot make a character appear, execute moves, or gain graphics.

So the result has two clean boundaries:

  • On a vanilla parent, all 21 stock-selectable fighter IDs receive six body colors, six matching confirmed-selection animation colors, and six matching large select-side figures. The center roster-grid cells remain stock.
  • On a separate custom build that already exposes $2E or $30, those IDs have compatible six-color body rows ready. This patch alone does not install either custom character or invent select artwork for it.
MvC fighter IDs consumed by the palette directory
12 · 16 vanilla main-form fighters345AB$000000$000032
  • 1
    null / non-fighter row
    $000000$000002 · 2 B
  • 2
    16 vanilla main-form fighters
    $000002$000022 · 32 B
  • 3
    Onslaught boss row remains stock
    $000022$000024 · 2 B
  • 4
    5 vanilla EX-form fighters
    $000024$00002E · 10 B
  • 5
    Dark Sakura + Shuma custom IDs
    $00002E$000032 · 4 B
    prepared palette rows; unreachable from the vanilla select screen
  • A
    Dark Sakura
    $00002E · marker
  • B
    Shuma-Gorath
    $000030 · marker
One directory, four beats // why custom-ready rows do not stop a vanilla install
what vanilla can select

The untouched game reaches IDs $02 through $20 for its main forms and $24 through $2C for its five EX forms. The $22 boss row keeps its stock pointer.

what the patch widens

Those 21 selectable IDs now resolve six $60-byte body blocks instead of two, and both the confirmed-selection animation and existing large select figure follow the same six-way choice. No select-grid entry, moveset, graphics tile, or executable character is added.

what the two extra rows mean

$2E and $30 point at prepared colors only. A custom build that already supplies Dark Sakura or Shuma can consume them; a vanilla build never reaches them.

why the installer accepts vanilla

Input verification is against the clean parent, not against a custom roster. The extra palette bytes are written into verified $FF space after that clean-parent proof succeeds.

Stock MvC, XvSF, and MSHvSF really had two colors

I began convinced that Marvel Super Heroes vs. Street Fighter and X-Men vs. Street Fighter gave every attack button its own color. I was wrong. The useful test is not “do these screenshots look different?” Color perception, animation timing, and duplicate-character rules can all muddy that. The decisive test is the value at the stock selector's store instruction.

For MSHvSF, that store is $00B15C. For XvSF, it is $00BC62. I ran each of the six physical MAME buttons in a fresh process and logged d1 plus the byte after the store retired:

stock arcade programLPMPHPLKMKHK
MSHvSF (mshvsfu)000111
XvSF (xmvsfjr1)000111

Every run passed. MP is the punch color. MK is the kick color. Neither is a third or fifth selector value in these stock CPS-2 binaries.

That result does not tell me which memory produced the original expectation. A home port, a hidden recolor entered with a button-and-direction combination, and a different Capcom game remain separate hypotheses. I am not converting any of those into a fact without another trace.

Run MSHvSF yourself before you believe me

With your own MAME ROM path:

mame mshvsfu -rompath "C:\path\to\your\roms" -window -skip_gameinfo

Pick one high-contrast fighter six times. Confirm with exactly one of LP, MP, HP, LK, MK, or HK, reset with F3 between runs, and avoid a duplicate-character test. Pause the same in-fight pose with P; use F12 for a frame capture.

To inspect the decisive byte instead of judging a costume, launch with -debug, set bp 00b15c for MSHvSF or bp 00bc62 for XvSF, confirm with one attack button, then single-step the move.b. D1 and the destination byte are 0 for LP/MP/HP and 1 for LK/MK/HK. Reset with F3 and start a fresh process for each button so a duplicate choice or stale team state cannot answer the question for you.

One Capcom routine shipped in three games

Here is MvC's stock selector. XvSF and MSHvSF use the same instruction sequence; only the held-input offsets and fighter-object offsets differ.

; Marvel vs. Capcom, decrypted 68000 @ $00C426
00C426  342E 0024      move.w  $24(a6),d2        ; P1 held-button word
00C42A  4A2C 0002      tst.b   $2(a4)
00C42E  6704           beq.s   $00C434
00C430  342E 0026      move.w  $26(a6),d2        ; P2 held-button word
00C434  7200           moveq   #0,d1
00C436  0242 0700      andi.w  #$0700,d2         ; all three kicks, one test
00C43A  6702           beq.s   $00C43E
00C43C  7201           moveq   #1,d1             ; never 2, 3, 4, or 5
00C43E  6008           bra.s   $00C448
00C464  1141 0277      move.b  d1,$277(a0)       ; store fighter color byte

The common fourteen-byte core is the receipt:

gamecore addressstored bytecommon bytes
MvC$00C42A$277(a0)4a2c0002 6704 342e0026 7200 02420700 6702 7201
XvSF$00BC28$217(a0)4a2c0002 6704 342e0016 7200 02420700 6702 7201
MSHvSF$00B122$257(a0)4a2c0002 6704 342e0016 7200 02420700 6702 7201

One moveq #1,d1. That is the entire two-color decision.

What those hex patterns mean

This is the part I would want if I inherited the patch six months from now—and it is the part N41's questions consistently push toward: do not publish a range of changed bytes without decoding the state they carry.

hex / value68000 meaningwhy it matters
0242 0700andi.w #$0700,d2keep only bits 8, 9, and 10: LK/MK/HK as one kick family
7201moveq #1,d1collapse any kick to index 1
1141 0277move.b d1,$277(a0)store the chosen index in the fighter object
4a2e 0277tst.b $277(a6)stock loader asks only “zero or nonzero?”
41e8 0060lea $60(a0),a0nonzero means advance one 96-byte color block
0281 f000f000andi.l #$F000F000,d1preserve the high nibble of two palette-RAM words before OR-ing RGB
$0770binary 0000 0111 0111 0000all six attack bits: three punches plus three kicks

The live MvC input sweep names every bit:

MvC1 buttonMAME fieldheld-word maskpatched index
LPP1 Button 1$00100
MPP1 Button 2$00201
HPP1 Button 3$00402
LKP1 Button 4$01003
MKP1 Button 5$02004
HKP1 Button 6$04005

Those masks OR to $0770, exactly matching MvC's own presence check at $014494. The game was already carrying six distinct inputs. Only the color routine threw four of the distinctions away.

Follow the byte from the button to palette RAM

How it works
Drawing the diagram…

The important distinction is storage versus interpretation. $277 is already one byte, so values 0–5 fit without new RAM. The stock consumer is what makes the byte boolean:

The confirmed-slot latch is now named too: $FF40C4 = P1 point, $FF40C5 = P1 bench, $FF40C6 = P2 point, and $FF40C7 = P2 bench. They come from select objects $FF3000/$FF3800/$FF3400/$FF3C00, respectively. That is why one-byte indices survive without allocating new state.

The directory record, decoded

The table at $1C7268 is not executable code. It is one big-endian 32-bit pointer for each even character ID: entry = $1C7268 + (id / 2) × 4. For IDs $02..$20, the compact arena row is (id/2)-1; after Onslaught's untouched $22 row, IDs $24..$30 use (id/2)-2. The pointed block is $184000 + compactRow × $240, and one button inside that block is base + colorIndex × $60. Ryu (id=$12) therefore reads directory entry $1C728C, which changes from $1C87C4 to $185200; HP/index 2 reaches $1852C0.

; MvC fighter-body loader @ $00A4BC
00A4BC  4A2E 0277      tst.b   $277(a6)
00A4C0  6704           beq.s   .base
00A4C2  41E8 0060      lea     $60(a0),a0
.base

The patched reader bounds the value to 0–5, multiplies it by $60, and adds that offset to the fighter's directory pointer. Values above 5 fall back to 0. That makes an out-of-range byte a safe first color rather than a read into unrelated data.

Register names matter here. At $00A4B0, $2C8(a6) loads a0, the ROM source pointer previously resolved through $001C7268. $288(a6) loads a1, the live palette-RAM destination; $28C(a6) loads a2, the second/mirror destination. Calling $2C8 the destination would reverse the data flow.

The arXiv method we actually used

The 2026 SoK on AI-augmented binary reversing describes an iterative four-mode workflow: triage, static analysis, dynamic analysis, and interventional testing. Here that means: search three games for a common byte signature; decode the selector and its consumers in Ghidra; observe D1, $277, source pointers, and palette RAM in MAME; then deliberately carry values 2–5 through paths the stock game never produces. Parallel agents helped rank searches and automate repeated runs, but an agent's interpretation is not an evidence class. The paper supplies the workflow vocabulary. The listings, raw bytes, register stops, RAM dumps, and rendered frames below prove this ROM.

Nine body readers widen; the effect reader does not

The fixed +$60 idiom appears ten times in this palette cluster. Nine sites read fighter-body blocks and now scale by the 0–5 index:

$00A4C2  $00A5B8  $00A634  $00A688  $00A6CA
$00A7B8  $00A834  $00A888  $00A8C6

Their six-byte hooks begin at $00A4BC, $00A5B2, $00A62E, $00A682, $00A6C2, $00A7B2, $00A82E, $00A882, $00A8C0. The tenth site, $00A920, selects an assist/effect palette table that still contains only two colors, so the release leaves it stock. Widening it would turn index 5 into a read beyond that table.

This is why “replace every +$60” is not a patch strategy. The index-5 propagation run recorded 36 executions across the widened body-reader family; the effect path is a deliberate boundary, not an overlooked hook.

The patch turns the stored bit into a six-way index

The release payload lives at $3F4000 in verified $FF space inside stock mvc.10. A six-byte encrypted hook at $00C434 redirects into this plaintext routine:

; v1 payload @ $3F4000
        moveq   #0,d1
        btst    #4,d2           ; $0010 LP
        bne.s   .done
        moveq   #1,d1
        btst    #5,d2           ; $0020 MP
        bne.s   .done
        moveq   #2,d1
        btst    #6,d2           ; $0040 HP
        bne.s   .done
        moveq   #3,d1
        btst    #8,d2           ; $0100 LK
        bne.s   .done
        moveq   #4,d1
        btst    #9,d2           ; $0200 MK
        bne.s   .done
        moveq   #5,d1
        btst    #10,d2          ; $0400 HK
        bne.s   .done
        moveq   #0,d1           ; no attack button
.done  jmp     $00C448

This also defines simultaneous-button behavior: the lowest-numbered physical button wins—LP before MP before HP before LK before MK before HK. With no attack button held, the routine defaults to index 0/LP, matching the stock punch-side default.

Six live selector runs passed: Button 1 stored 0, Button 2 stored 1, through Button 6 storing 5 in the original $277 field. The integrated release then repeated all six runs on Ryu: each index selected the expected $60-spaced ROM block, produced a unique live-palette hash, and matched all 48/48 lower RGB444 words with zero mismatches.

The normal duplicate-choice branch is $00C440, not $00C46A. It advances the already-used index modulo six. A live two-player receipt closes the path: P1 chose Button 6/index 5, P2 chose the same fighter, $3F4038 received d2=5, and P2 retired index 0. A second live receipt carries index 5 through $FF40C4, fighter setup, and 36 widened body-reader hits. $00C46A is a separate duplicate normalizer used by automatic preset/progression team synthesis (plus one zero-caller legacy/debug route); it is not the normal player mirror-button path and remains stock.

Put the bodies side by side: all 21 selectable fighters, all six buttons

Palette swatches are useful to a hex editor. A player wants to see the costume. The showcase therefore uses the 126 harness-verified live MAME body snapshots, not an offline reconstruction of a multipart CPS-2 sprite. For each fighter, the renderer finds pixels that respond across the six matched frames, restores the adjacent shared dark outline, removes the unchanged stage, and floor-aligns the six intact cutouts. LP, MP, HP, LK, MK, and HK are printed under the bodies; the MvC2 source-bank label sits above each column.

All 21 vanilla-selectable Marvel vs. Capcom fighters extracted from verified live MAME body frames across six columns labeled LP, MP, HP, LK, MK, and HK
THE PLAYABLE ROSTER // 21 vanilla-selectable IDs × six live body results. The silhouettes come from the same MAME evidence used by the release receipt—never from hand-recolored or partially assembled tiles.

Open the six-body roster at full resolution.

This is the full vanilla-selectable set: sixteen main fighters and five stock EX forms. Onslaught is a boss with one stock body color, not a six-button select choice. Dark Sakura and Shuma-Gorath are not in the vanilla character select, so presenting them in this playable-roster wall would repeat the exact confusion the installer section resolves.

Every rendered row produced six different pixel hashes. That is a static visual audit of 126 body-palette applications, not a claim that I played 126 matches. Ryu remains the live MAME body integration control; the roster wall uses the same screenshot-validated CPS-2 tile decoder, hardware pen 15 as transparency, each character's group-0/slot-0 idle record, and the generated body blocks whose SHA-256 values are in the manifest.

The second sheet makes smaller color changes easier to inspect. Each extracted body is floor- and center-aligned with its partner. The left half uses the punch palette, the right half uses the same-strength kick palette, and a narrow center band linearly blends both RGBA cutouts. The unchanged stage is absent, so it cannot masquerade as palette evidence at the seam.

All 21 selectable fighters shown in three half-overlay comparisons per row: LP against LK, MP against MK, and HP against HK, with a transparent blend through each center seam
HALF-OVERLAY VIEW // LP|LK, MP|MK, and HP|HK on the same pixels. Left and right halves stay opaque; the center band blends the two palettes so small ramp changes remain visible.

Open the half-overlay roster at full resolution.

The audit records the live-snapshot census hash, source crop, foreground-mask method, palette-block hash, animation record, sprite dimensions, and six-way uniqueness for each fighter. No pose was generated, traced, or hand-recolored; the downloadable patch is independent of these presentation images.

The first patch fixed the body and missed the large select figure

The bug report was correct. Six body uploads did not prove six colors everywhere. MvC draws the large character-select figure with a separate 48-word loader. My first build widened the fighter-body readers while explicitly leaving that UI path stock, so the game remembered indices 0–5 but the preview still reduced them to its P/K pair.

The stock source setup begins here:

; large character-select figure @ $0B7690
0B7690  207C 001D 2A24   movea.l #$001D2A24,a0
0B7696  D1C0             adda.w  d0,a0
; ... copy 24 longwords / 48 palette words to $914180

Replacing body colors could never fix that instruction. The corrected release patches the encrypted mvce.04a bytes at member offset $037690 from f2f1ee8bca79 to 872b24e0e8cb; after CPS-2 decryption, that is a six-byte jmp $003F40F0. The wrapper identifies P1/P2 and point/bench, reads the corresponding fighter object's existing $277 index, validates 0–5, and computes:

preview source = $3F5000 + compactVanillaFighter × $240 + colorIndex × $60

For Ryu, compact row 8 starts at $3F6200. Six fresh MAME processes reached $3F6200, $3F6260, $3F62C0, $3F6320, $3F6380, $3F63E0. Then the final capture harness repeated that contract for all 21 fighters × all six buttons. Each clean process navigates with ordinary directions, enters Capcom's original arcade unlock code when the target is hidden, and confirms with the labeled physical button. It never writes a fighter ID or color index. All 126 runs matched the confirmed animation's selected body bank 32/32, copied the large figure to $914180 and matched it 48/48, advanced into the match, matched the body block at $914000 48/48, and produced six distinct selected-animation uploads plus six palette-responsive large-select-figure/body foregrounds per fighter.

A third loader made the selected animation show the wrong color

The next bug report was more specific than “the colors look close”: War Machine selected with HP animated in MP colors, then entered the match in the correct HP color. Captain America showed the same pattern. That transition proves two loaders disagree; it cannot be repaired by re-encoding the video or making the large figure more saturated.

clip · loops
THE TRANSITION // physical HP confirmation, complete selected animation, then the live body. The same navy/red index survives the cut.

The short clip keeps the transition that a still image loses. The large figure and selected animation appear first; the live fighter body follows. Matching only the last frame would have missed the exact bug that exposed this third consumer.

The live palette-RAM trace found the third consumer at $07AF36. It resolves the same fighter directory as the match body and reads the already-committed point/bench color byte—but Capcom's stock code reduces that byte to zero versus nonzero:

; confirmed character-select animation
07AF42  302E 0052       move.w  $52(a6),d0
07AF48  207C 001C 7268  movea.l #$001C7268,a0
07AF4E  2070 0000       movea.l (a0,d0.w),a0
; a1 now points at the committed slot color byte
07AF6C  4A11            tst.b   (a1)
07AF6E  6704            beq.s   $07AF74
07AF70  D0FC 0060       adda.w  #$60,a0

That is the exact visual failure: index 0 uses bank 0; indices 1, 2, 3, 4, and 5 all add one $60 block and animate with bank 1. The match later reaches the already-widened body reader and uses the true index, so its color “suddenly” becomes correct.

The new wrapper preserves the stock animation, destination logic, and priority nibbles. It changes only source selection:

; payload @ $3F42D0, entered from encrypted hook $07AF6C
        moveq   #0,d0
        move.b  (a1),d0
        cmpi.w  #5,d0
        bls.s   .bounded
        moveq   #0,d0
.bounded:
        mulu.w  #$60,d0
        adda.l  d0,a0
        jmp     $07AF74

The stock routine then copies 32 lower-RGB words into one of $914040/$914080/$9140C0/$914100. The census accepts a select frame only after those 32 words equal the requested body bank and the separate 48-word large select-side figure upload passes. War Machine HP now animates navy/red instead of green; Captain America's MP, HP, LK, MK, and HK secondary ramps no longer collapse to MP before the fight.

War Machine HP: one button, three independent loadersPhysical Button 3 → stored index 2 → navy/red on select and in the matchCONFIRMED IN GAME
War Machine selected with HP: the top live MAME frame shows the navy and red large character-select figure and selected animation, while the bottom frame shows the same navy and red palette on his live match body
selected animation32 / 32 RGB words
large select figure48 / 48 RGB words
live match body48 / 48 RGB words
ROM source$1840C0 for animation + body
figure source$3F50C0

The center roster cell remains stock and is not part of these PASS labels. The card pairs the visible result with the exact machine-state checks that accepted it.

MAME run 003 · War Machine · HP · patched v1 acceptance

This paired frame is the visual version of the debugger chain. At select time, index 2 chooses large-figure source $3F50C0 and selected-animation/body-bank source $1840C0. When the match becomes gameplay-ready, $1840C0 is still the body source. Before the third hook existed, the top animation used bank 1 while the bottom body used bank 2—the exact “wrong on select, right in game” report that exposed the missing consumer.

The 21 vanilla-selectable Marvel vs. Capcom large character-select figure palettes, with six distinct preview palettes arranged under LP, MP, HP, LK, MK, and HK
THE SECOND LOADER // 21 existing select figures × six choices. Stock P/K artwork anchors pass through the body semantics authored for the same fighter; stable warm ramps protect skin and hair while costume pens inherit each indexed MvC2 target hue.

These preview words cannot be literal copies of the fighter-body words: a portrait pen and a sprite-body pen do not necessarily color the same semantic feature. The final builder first matches each portrait pen through the stock body palette authored for that fighter. Costume pens then take the matched MvC2 pen's hue and chroma plus the portrait-to-body brightness difference. That target-hue result contributes 3/4 and a ridge-regularized affine fit contributes 1/4, followed by a restrained 5/4 chroma gain. This specifically prevents a blue target ramp becoming olive merely because the stock portrait was less blue than the stock body. Warm skin/hair pens may only use warm body pens whose RGB range stays stable across all six banks; those pens keep indexed channel motion and receive no chroma gain.

There is also a measured escape hatch, not a hand-tuned character exception. If two generated palettes differ in fewer than 12 visible words or 30 total RGB nibbles, the later costume reruns through the stronger unconstrained transfer. Strider MK is the one case that crosses that floor. Every channel still clamps to 0–15, and pen 15 remains the figure's original transparent/tag value.

That distinction matters: the body arena is literal MvC2 RGB data; the select arena is a deterministic, manifested transfer onto Capcom's different artwork. Calling both literal would make the screenshots simpler and the provenance wrong.

The full reel below is also an adversarial test of that transfer. The final gate hashes only palette-responsive pixels inside the large-select-figure ROI, then enforces minimum changed-area and RGB-distance floors. The HUD-free body gate evaluates each pair's own changed pixels, so a different intro pose cannot dilute—or impersonate—a palette change. That second surface caught Mega Man restoring the stock kick palette after the common six-color loader; three owner-aware intro-row guards now preserve his real animation while keeping LP/MP/HP/LK/MK/HK distinct.

Why a correct color can disappear midway through an animation

An animation record chooses poses and tiles. A palette-animation writer is separate: it can replace one 16-word color row while those poses continue. That distinction explains the “right for a frame, then wrong” class of bugs.

Mega Man first passes through the common three-row body loader and receives the correct six-way block. During his intro, three later stock workers repaint the rows from scripts that know only punch and kick:

stock writerrow it can replacev1 hook targetv1 source
$064F0C0$3F4258$1861C0 + index×$60 + rowOffset
$07D0C01$3F4294$1861C0 + index×$60 + rowOffset
$064EAA2$3F421C$1861C0 + index×$60 + rowOffset

The wrappers do not skip his animation or freeze a palette. They keep the stock row timing and destination, bound $277 to 0–5, and change only the row source.

Shadow Lady exposed a second trap. $92FC00 is a shared slot-local dynamic palette address reached by both Shadow Lady and Mega Man; the address alone does not identify its owner. The final resolver checks fighter ID before substituting either Mega Man's six-bank base $1861C0 or Shadow Lady's $186AC0. A separate guard at $06987A skips Shadow Lady's two-color row-0 overwrite while every other caller follows stock. Treating “pointer equals $92FC00” as “this must be Shadow Lady” painted Mega Man from the wrong fighter's arena—an intermediate build the settled-frame body comparison caught.

Animation, palette, and proof are three different things

The selected-character animation still executes its original pose sequence. Mega Man's match intro still executes all three original row-update moments. The patch changes the palette source chosen at those moments. That is why the reel retains motion and waits for a later gameplay-ready body frame: a correct first upload cannot hide a subsequent stock writer.

MvC2 already stores the six donor banks we need

The current Dreamcast definitions in PalMod expose the selectable banks in storage order:

LP, LK, HP, HK, A1, A2

The PalMod author's MvC2 color-editing guide identifies those as the first six character-select choices. Each bank starts with eight rows of sixteen little-endian ARGB4444 words in the character's S2 data. MvC1's body loader needs the first three rows.

That is six defined selectable base palettes per character, not six choices for P1 plus another exposed six for P2. Neither PalMod's selectable-bank map nor the game guide defines colors 7–12; unrelated effect rows are not a mirror-only palette set. A same-character collision can permit, reuse, or redirect among those six choices, but this extraction does not pretend to have live-traced MvC2's exact same-button fallback. This MvC1 patch uses an explicit modulo-six rule instead: a collision advances to the next existing bank.

MvC1 has a six-attack-button cabinet while MvC2 labels the same physical positions as four attacks and two assists. The release mapping follows cabinet position:

MvC1 targetMvC2 donor bank
LPLP
MPHP
HPA1
LKLK
MKHK
HKA2

So the visible rows are MvC2 LP / HP / A1 across MvC1's punch buttons and MvC2 LK / HK / A2 across its kick buttons.

Contact sheet of 23 configured Marvel vs. Capcom fighter IDs, each with six three-row MvC2-derived palette blocks arranged under LP, MP, HP, LK, MK, and HK
THE WHOLE ARENA // 23 configured IDs × six selections × three body rows. Asterisks mark named base-form proxies; Dark Sakura and Shuma are custom-ready rows, not characters added to vanilla.

Yes—the MvC2 colors can be assigned to different buttons

Nothing in the RGB data knows which physical button selected it. The selector returns a target slot 0–5; the builder decides which MvC2 source bank occupies each slot.

PalMod storage order is [LP,LK,HP,HK,A1,A2]. The release's six-entry lookup is:

{
  "target_order": ["LP", "MP", "HP", "LK", "MK", "HK"],
  "mvc2_storage_order": ["LP", "LK", "HP", "HK", "A1", "A2"],
  "cabinet_lut": [0, 2, 4, 1, 3, 5]
}

Changing that lookup can apply any one-to-one permutation without changing the 68000 selector. For example, swapping entries 2 and 4 would exchange which physical button gets A1 and HK. The builder could technically repeat a source bank too, although the release deliberately uses all six exactly once. This is a build-time assignment, not a new in-game configuration menu.

The RGB conversion is exact nibble copying, not quantization

Ryu's six MvC2 donor palette banks shown as three rows of sixteen RGB444 pens
1 // DATA — six exact three-row RGB444 blocks from MvC2
Ryu rendered in MAME with all six installed body palettes
2 // PIXELS — the same six blocks after MvC1's stock body uploader consumes them

Early planning assumed Dreamcast colors might be RGB565 and need downscaling. The bytes disproved that too. MvC2 stores ARGB4444. MvC1 expects 0RGB, also four bits per channel. Conversion is simply:

source ARGB4444:  AAAA RRRR GGGG BBBB
target 0RGB4444:  0000 RRRR GGGG BBBB
                  ^^^^ discard alpha; copy RGB nibbles exactly

The measured pen bridge is equally specific:

MvC1 rows 0..2, pens 0..14  <-  MvC2 rows 0..2, pens 1..15
MvC1 rows 0..2, pen 15      <-  preserved target row tag

Why the one-pen shift? MvC2 uses source pen 0 as transparent; MvC1 reserves destination pen 15 for its row tag/transparent hardware pen. Across the selected donor data, source pen 0 has alpha 0 and visible pens 1–15 have alpha F.

This bridge has a strong control: for 14 of the 16 stock returning fighters, MvC2 LP matches all 45 visible RGB words in MvC1 after mapping. Ryu and Chun-Li are the two named exceptions. I keep those differences visible in the audit instead of guessing why they differ or silently normalizing them.

Rebuild one fighter's donor blocks by hand

Use your own Dreamcast extraction and open Ryu's PL00_DAT.BIN. These are the six source ranges; each row is $20 bytes and the three listed rows form the $60 bytes MvC1 needs:

MvC1 buttonMvC2 bankthree source-row offsetsexpected CPU-order output SHA-256
LPLP$6E440/$6E460/$6E48075ed3088c037783a68319efda6206da96fce48c7ede6594e4ed2e8fdbe027323
MPHP$6E640/$6E660/$6E680bc7bbb2410be87e7b247e27b85df7028d5dbe8ab6352b12e72fa72fac57b8bdf
HPA1$6E840/$6E860/$6E8801c43faa7d0bcf21c862c710647b1e30fce06cec9be4d0f13788eb10d0a5c167c
LKLK$6E540/$6E560/$6E5800b0901da492e8dfd8771758c68233c73509521912381b8fdd19097a27990302f
MKHK$6E740/$6E760/$6E780f0d2fb0dee07db6e4634e923979e920314a46729621753f8cd8ecb4cd45f9dfe
HKA2$6E940/$6E960/$6E98093235f97998357cfcaa9d2e49cb069933329cbbf6f5a682a63208947305c2b3c

For example, xxd -g 2 -e -s 0x6e440 -l 0x60 PL00_DAT.BIN exposes the three LP rows as little-endian 16-bit words. For each row, discard source pen 0, move source pens 1–15 into MvC1 destination pens 0–14, clear the alpha nibble, and retain the corresponding MvC1 row's pen 15 tag. Concatenating the six CPU-order blocks in the table produces Ryu's $240-byte group with SHA-256 9efc9098dff6846549d5b68f40ba77dc14156fe04843c3d8d38cbbd2c11c65dd. The group begins at CPU $185200; add index × $60 to reach any button. Reverse each byte pair only when writing it into the word-swapped mvc.06a member.

Every target has a donor, but six targets are proxies

Seventeen targets have a direct MvC2 counterpart:

direct stock returnersdirect custom target
War Machine, Captain America, Hulk, Wolverine, Gambit, Venom, Spider-Man, Roll, Ryu, Captain Commando, Chun-Li, Jin Saotome, Zangief, Strider Hiryu, Morrigan, Mega ManShuma-Gorath

Shuma is direct by identity, but its custom MvC1 target sits outside the valid vanilla 192-byte fighter blocks, so its target validation remains separate.

Six MvC1 variants do not exist as matching MvC2 identities. Their manifest rows say exactly which base fighter supplies the literal RGB words:

MvC1 targetMvC2 sourceprovenance label
Red VenomVenomderived-proxy
Orange HulkHulkderived-proxy
Gold War MachineWar Machinederived-proxy
Shadow LadyChun-Liderived-proxy
Lilith-mode MorriganMorriganderived-proxy
Dark SakuraSakuraderived-proxy

Those are not presented as official MvC2 Red Venom or Shadow Lady palettes. They are literal MvC2 base-form donor banks applied to named MvC1 variants. There are no unlabelled generated fillers and no missing roster rows.

No ROM expansion is arithmetic plus an enforced invariant

One body color is $60 bytes. Six colors are $240 bytes per fighter. For 23 targets:

23 fighters × 6 colors × $60 bytes = 13,248 bytes = $33C0

The palette member mvc.06a already contains a 16,774-byte $FF run from $183E7A to $188000 (exclusive). Aligning the arena to $184000 leaves exactly 16,384 bytes. A 13,248-byte arena ends at $1873C0, leaving 3,136 bytes in the same stock member.

The corrected release uses two existing caves inside mvc.10: a 744-byte code payload at CPU $3F4000 and the large-select-figure arena at $3F5000:

21 vanilla fighters × 6 previews × $60 bytes = 12,096 bytes = $2F40
$3F5000 + $2F40 = $3F7F40

Both fit inside the stock 512 KiB member. No new member is added, no member gets longer, and no address beyond the stock program map is introduced.

mvc.06a // stock 512 KiB member
4 · stock data after caveA$180000$200000
  • 1
    stock data before aligned cave
    $180000$184000 · 16 KB
  • 2
    13,248-byte six-color arena
    $184000$1873C0 · 12.9 KB
    23 × 6 × $60; every byte had a stock $FF preimage
  • 3
    3,136 aligned cave bytes remain
    $1873C0$188000 · 3.1 KB
  • 4
    stock data after cave
    $188000$200000 · 480 KB
  • A
    first widened fighter-directory row
    $1C726C · marker
    directory entries point back into the arena
The no-expansion receipt // a fixed-size member before and after
start with the member boundary

mvc.06a is $80000 bytes before the patch and $80000 bytes after it. The picture's endpoints do not move.

spend only proved-empty bytes

The arena consumes $33C0 bytes from an aligned $4000-byte part of a measured $FF run. $0C40 bytes remain untouched after it.

route data; do not grow it

The fighter directory at $1C726C is rewritten so each configured ID's base pointer lands on its six contiguous blocks. That changes pointers and contents, not member length.

recheck the archive

Acceptance opens the result again: 21 members, original member names, every member's original byte length, exactly four changed member hashes, and romExpansion=false.

No expansion does not mean no changes

The patched members will have different CRCs and SHA-256 hashes. “No ROM expansion” means every archive member keeps its original byte length. The builder checks that invariant and records romExpansion: false; it does not pretend the output is a stock ROM.

The final acceptance repeated the size proof after integrating the 13,248-byte body arena, 12,096-byte large-select-figure arena, and all 23 body-directory pointers: 21 members entered, 21 same-size members left, and only mvc.06a, mvc.10, mvce.03a, and mvce.04a changed. Six live Ryu body runs prove the body path. The 126-run select census proves the figure arena is not merely writable: every fighter/button pair reads the expected address and uploads all 48 words without a mismatch.

The automated census does not trust a free-floating PASS line. Its receipt is bound to the exact patched-ROM hash, the exact run definition, raw source/destination observations, all 126 select/body frame pairs, foreground-separation metrics, and final video hash. Changing any input makes the old evidence stale. The manual procedure below reaches the same four decisive stops without requiring that automation.

Reproduce the patch in Ghidra, MAME, and a hex editor

This section is intentionally redundant with the story above. It turns the findings into a clean-room checklist another reverser can repeat with legally obtained MvC1 and MvC2 media. You can audit the shipped patch without rebuilding the derived artwork, or continue farther and reconstruct Ryu's donor blocks using the offsets already published.

Terms, once

A color index is the value 0–5 chosen by a button. A palette row is sixteen 16-bit words. A fighter body color is three rows, so one palette block is 3 × 16 × 2 = 96 = $60 bytes. An arena is verified unused space inside an existing ROM member. A directory is the pointer table that tells a loader where a fighter's palette block begins.

The six questions that keep the hex honest

For each visible change, ask: Which physical input bit changed? Which byte stored the choice? Which routine consumed it? Which ROM address became the source? Which palette-RAM range received it? Which pixels changed on screen? The steps below keep all six answers connected.

1. Lock the exact clean parent before naming an address

The reference input is the dat-official 21-member Euro parent mvsc.zip, whole-archive SHA-256:

6f63627cc37c554f74e8bf07b21730fa7f85511c7d5d07449850be98dde91da8

These are the four members the release eventually changes:

membersizeclean SHA-256
mvce.03a$8000048128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757
mvce.04a$8000071ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67
mvc.06a$80000132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b
mvc.10$80000e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a
A nearby revision is not close enough

If one member hash differs, stop. Do not search for a similar-looking six-byte sequence and paste a jump over it. A wrong parent may boot and still fail only when a specific fighter, slot, or intro writer reaches the overwritten instruction. The installer refuses that ambiguity before its first write.

2. Build the same address space in Ghidra

  1. Preserve the raw ZIP and hashes above.
  2. Word-unswap mvce.03a and mvce.04a, place them at CPU $000000 and $080000, and apply the CPS-2 Euro-parent key schedule from MAME's cps2crypt.cpp using the mvsc.key member from your own archive.
  3. The clean decrypted one-megabyte opcode image must hash to c3e8fdff391d8666f231b6bde175b110b99e2e2da531eeeaea3189e38ec8910a. Its first two longwords are initial SP $00FF245C and initial PC $000000E2.
  4. Import that image as Motorola 68000, big-endian, 32-bit address space, image base $000000.
  5. Add the word-unswapped data members as memory blocks: mvc.06a at CPU $180000 and mvc.10 at CPU $380000.
  6. Label these landmarks before following any cross-reference:
labelCPU addressclean observation
select_color$00C426loads P1/P2 held word, reduces it to D1=0/1
color_store$00C464move.b d1,$277(a0)
body_reader$00A4B0source in A0, destination in A1, boolean at $00A4BC
selected_animation$07AF36resolves directory, boolean at $07AF6C
large_select_figure$0B7690loads stock side-figure source $1D2A24+d0
fighter_palette_directory$1C7268one longword pointer per even fighter ID
six_color_code$3F4000clean $FF; patched 744-byte payload
six_color_body_arena$184000clean $FF; patched 23 × 6 × $60
six_color_select_arena$3F5000clean $FF; patched 21 × 6 × $60

Ghidra and MAME can appear to disagree in the encrypted low megabyte. Ghidra's decrypted image and MAME's disassembly show the opcode-fetch view. A hex editor shows the encrypted bytes physically stored in .03a/.04a. Data tables and the high program members are word-swapped on disk but are not CPS-2 opcode-encrypted.

3. Follow the stock byte before looking at the patch

In Ghidra, start at $00C426, not in the new cave:

  1. Rename $24(a6) and $26(a6) as the P1/P2 held-button words.
  2. At $00C436, decode 0242 0700 as andi.w #$0700,d2.
  3. At $00C43C, note that 7201 is the only nonzero assignment: stock can emit only 0 or 1.
  4. Follow $00C464 to the fighter object's $277 color byte.
  5. Follow $277 to $00A4BC. The tst.b plus one lea $60(a0),a0 proves the match body also interprets every nonzero value as bank 1.
  6. Follow the directory load at $07AF48 to $07AF6C. That second tst.b proves the selected animation repeats the collapse independently.
  7. Inspect $0B7690. Its absolute source $1D2A24 proves the large side figure is a third data path, not another call to the body uploader.

This order matters. If you begin at $3F4000, the payload looks plausible but you have not proved why each hook exists or which consumer was intentionally left stock.

4. Make MAME print the runtime chain

Run the patched output from its own ROM directory:

mame mvsc -rompath "C:\path\to\your\patched-roms;C:\path\to\mame-roms" -debug -window -skip_gameinfo

With the main 68000 selected in the debugger, these action breakpoints log the four stages and immediately continue:

bp 00c468,,{ printf "SELECT d2=%04X index=%X stored=%X\n",d2,d1,b@(a0+277); g }
bp 00a4c6,,{ printf "BODY id=%X index=%X src=%08X dst=%08X\n",w@(a6+52),b@(a6+277),a0,a1; g }
bp 07af74,,{ printf "ANIM index=%X src=%08X\n",b@a1,a0; g }
bp 0b7698,,{ printf "SIDE id=%X src=%08X\n",w@(a6+52),a0; g }
If four log lines are not enough, save every executed instruction

Set a start and stop breakpoint around the stock selector, run to the first one, then turn on a bounded trace with the live data and address registers prefixed to every line:

bp 00c426
bp 00c468
g

After $00C426 stops:

trace six-color-select.tr,,noloop,{tracelog "D0=%08X D1=%08X D2=%08X A0=%08X A1=%08X A6=%08X ",d0,d1,d2,a0,a1,a6}
g

At $00C468, enter trace off before continuing. The resulting six-color-select.tr is the instruction-by-instruction proof between input load and stored byte. Repeat with boundaries $00A4B0..$00A4C6 for the body reader, $07AF36..$07AF74 for the selected animation, and $0B7690..$0B7698 for the large figure. Use traceover with the same arguments if you intentionally want to skip subroutine bodies; use trace when the question is which callee changed D1, A0, or A1.

Choose Ryu with one attack button, reset with F3, and repeat. The expected source addresses are:

buttonstored indexbody + selected-animation sourcelarge-figure source
LP0$185200$3F6200
MP1$185260$3F6260
HP2$1852C0$3F62C0
LK3$185320$3F6320
MK4$185380$3F6380
HK5$1853E0$3F63E0

The values advance by exactly $60; no visual judgement is involved. For raw writes, set one watchpoint at a time because the ranges overlap across UI phases:

wp 914000,60,w
wp 914040,40,w
wp 914180,60,w

Those are the point-fighter body, two-row selected-animation, and large-side-figure destinations used by the control run. After the relevant writer stops, MAME can save each range directly:

save body-pal.bin,914000,60
save select-animation-pal.bin,914040,40
save large-figure-pal.bin,914180,60

For body and animation comparisons, ignore each word's high nibble: stock code preserves live priority/brightness there and copies the lower RGB nibbles from ROM. The release gate compares exactly those 48 body words, 32 animation words, and 48 side-figure words.

5. Translate CPU addresses into raw member offsets

Use this map before opening a hex editor:

CPU $000000..$07FFFF -> mvce.03a + CPU address
CPU $080000..$0FFFFF -> mvce.04a + (CPU address - $080000)
CPU $180000..$1FFFFF -> mvc.06a  + (CPU address - $180000)
CPU $380000..$3FFFFF -> mvc.10   + (CPU address - $380000)

These four primary hooks show the distinction between meaningful decrypted 68000 and encrypted bytes on disk:

byte patch $00C434 · mvce.03a6 bytes · 5 changed
beforeEBD9765671FF
afterF1276DB0719F
six-button decode. Decrypted, the patched six bytes are JMP $3F4000.
byte patch $00A4BC · mvce.03a6 bytes · 6 changed
before60753F547B95
after3961C328B0F0
first body reader. Decrypted, this becomes JMP $3F404C.
byte patch $07AF6C · mvce.03a6 bytes · 6 changed
before6EE505C4A592
after911551A5DA56
selected-animation reader. Decrypted, this becomes JMP $3F42D0.
byte patch $037690 · mvce.04a6 bytes · 6 changed
beforeF2F1EE8BCA79
after872B24E0E8CB
CPU $0B7690, the large select-side figure. Decrypted, this becomes JMP $3F40F0.

The complete 17-hook raw ledger is below. Every entry is exactly six bytes long; every patched low-ROM entry decrypts to an absolute JMP to the listed payload target.

purposeraw member + offsetpayload targetclean raw bytespatched raw bytes
six-button decodemvce.03a + $00C434$3F4000ebd9765671fff1276db0719f
duplicate modulo sixmvce.03a + $00C440$3F40380ef18564e99da15762f7a171
body reader A6mvce.03a + $00A4BC$3F404C60753f547b953961c328b0f0
body reader P1 slot Amvce.03a + $00A5B2$3F405A67fb5daccaac70bdd1f958d5
body reader P1 slot Bmvce.03a + $00A62E$3F4068514b658c75c6c5b063fea521
body reader P1 copy Amvce.03a + $00A682$3F40A2df942762045e2613d3509bef
body reader P1 copy Bmvce.03a + $00A6C2$3F40B02f02ba9ab95f2288c6c8f005
body reader P2 slot Amvce.03a + $00A7B2$3F407662d1c2bb0f78a80bb5e8d32d
body reader P2 slot Bmvce.03a + $00A82E$3F4084d396708885bd2b1720145f7c
body reader P2 copy Amvce.03a + $00A882$3F40BE4f581520a74f4f5ebcf01dae
body reader P2 copy Bmvce.03a + $00A8C0$3F40CC70048b4ed4ed388920215904
Mega Man intro row 2mvce.03a + $064EAA$3F421C0ed5e63cd579f4fc120c584f
Mega Man intro row 0mvce.03a + $064F0C$3F4258f97c1685e04ffc0e73756bd1
Shadow Lady row guardmvce.03a + $06987A$3F41A22207ec1c56c040e7f7b19308
selected animationmvce.03a + $07AF6C$3F42D06ee505c4a592911551a5da56
Mega Man intro row 1mvce.03a + $07D0C0$3F4294864efdb0a015363e5c1d0c1c
large select-side figuremvce.04a + $037690$3F40F0f2f1ee8bca79872b24e0e8cb

The data edits remain legible in a raw member view, but each 16-bit word is byte-swapped:

raw member locationclean first bytespatched first bytesmeaning
mvc.06a + $004000ffffffffffffffffffffffffffffffff0000ff0fab0b77084506340423031101start of War Machine's body arena
mvc.06a + $04728C1c00c48718000052Ryu directory pointer, CPU $1C87C4 → $185200
mvc.10 + $074000ffffffffffffffffffffffffffffffff0072020804002a660172020805002266first 16 on-disk bytes of the 744-byte code payload
mvc.10 + $075000ffffffffffffffffffffffffffffffffe00fee0edd0dcc0caa0a990988087707first 16 on-disk bytes of the select-figure arena

Do not paste 4E F9 ... from Ghidra into .03a/.04a: those members require the encrypted after-bytes in the ledger. Do not paste CPU-order data into mvc.06a or mvc.10 without swapping each byte pair.

6. Assemble every line of the 744-byte payload

The patch-only bundle includes the complete six_color.s—selector, duplicate policy, all nine body readers, Shadow Lady ownership guard, all three Mega Man intro-row guards, large-side-figure reader, and selected-animation reader. It is not a partial listing.

m68k-elf-as -m68000 -o six_color.o six_color.s
m68k-elf-ld -Ttext=0x3F4000 -e selector_decode -o six_color.elf six_color.o
m68k-elf-objcopy -O binary -j .text six_color.elf six_color.bin
m68k-elf-nm -n six_color.elf

Expected results:

source SHA-256  8cbcdb5755ce60f19849d6ffd39fbee163be347ae33d02486fd9ca3e24212fb8
binary size     744 bytes
binary SHA-256  a58ce8b820d5bf486f560f02c02f454ba0985b74d69bd769035199576a0da7f3

The symbols that must not drift are selector_decode=$3F4000, duplicate_next_color=$3F4038, the nine body entries from $3F404C through $3F40CC, select_sideart_six_color=$3F40F0, shadow_palette_guard=$3F41A2, the Mega Man guards at $3F421C/$3F4258/$3F4294, and select_animation_six_color=$3F42D0. Word-swap the assembled binary before placing it at mvc.10 + $074000.

7. Close the audit with deterministic member hashes

After applying all hooks, arenas, and directory entries, exactly four member hashes change:

memberclean SHA-256patched SHA-256
mvce.03a48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757d69f8b5c05d745d899377cddbb9ccd4b39af698ec8cf565db6cc634a7a97de2b
mvce.04a71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67d7aa82828984f939b09569c79d62d77b65a05f1af9f7a1a2c8179f17ff13b37d
mvc.06a132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354bbe22d6cf23d84a12e3520e6c344dd974141c7b730aa58a1295ffa6df1eddcc0a
mvc.10e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172af21f3a8881734f844350e9063b0ef8164cdd6edbafd7fb68c37b08d0b7980969

The other seventeen members must remain byte-identical. Container order can differ, so member hashes—not a casually repacked outer ZIP hash—are the portable result.

How to read a failed checkpoint
  • Clean member mismatch: wrong region, wrong revision, merged set, or an earlier mod.
  • Decrypted-megabyte mismatch: wrong key, decrypt range, concatenation order, or word swap.
  • D1 reaches 0–5 but A0 only chooses base/base+$60: an unpatched consumer remains.
  • Source pointer is correct but the saved RAM words differ: check slot, phase, and the live high nibble before blaming the ROM block.
  • A fifth member changes or any member grows: the no-expansion build invariant failed.

Each failure narrows the problem. None should be “fixed” by weakening a hash check.

How a 118 KiB CMD changes the whole roster

The installer is small because it is a delta and verifier, not a game image. It contains 21 clean member identities and 21 exact patch records. Those records write 26,286 bytes total:

744 bytes is therefore the code payload, not the entire color data. It can do so much because MvC already owns the input word, $277 state byte, palette upload loops, sprite art, animation state machines, and palette RAM. The patch changes which $60-byte source block those systems read. It does not add six copies of every sprite or a new renderer.

The CMD is a Batch/PowerShell polyglot:

  1. The small Batch header finds the marker inside its own file and launches the embedded PowerShell.
  2. PowerShell opens the supplied ZIP in memory and requires exactly the 21 known members.
  3. It checks each member's case-sensitive name, length, and SHA-256.
  4. Before every copy, it checks the exact local preimage again. A failed preimage means nothing has been written.
  5. It copies same-length after-bytes over those 21 spans; no insertion operation exists.
  6. It hashes every resulting member against the four patched identities above.
  7. It writes a temporary ZIP, reopens it, rechecks member order, sizes, and hashes, then renames it and emits a receipt. The source ZIP is never opened for writing.

Most of the CMD's text is hex for the large before/after spans. The large preimages are verified $FF caves; the after-images are compact RGB444 tables. That is why one roughly 118 KiB text file can safely transform a multi-megabyte archive without containing or downloading a complete Capcom ROM.

The supply-chain rule comes from the same place as the technical one. The Reproducible Builds paper distinguishes trusting source from trusting the executable artifact. Here, the practical bridge is the clean member identity, exact preimage, deterministic after-image, patched member hash, and install receipt. The paper motivates that chain; the published values let another person test it.

Release record

The standalone v1 patch is linked at the top and remains available as the historical single-feature release. Players who want this feature together with the combined gameplay work can use DHC v5. These are the sealed acceptance records for technical readers auditing the standalone download:

itemSHA-256 / value
clean mvsc.zip input6f63627cc37c554f74e8bf07b21730fa7f85511c7d5d07449850be98dde91da8
13,248-byte body arena833d74c203d7f2e90eb34e210e51a3b0623d38733b83135732a4dab44b6900e2
12,096-byte large-select-figure arena79879820f9bce737e5ad678ca3f0c4575b03a4c10e69317fed3ecbc54d77852c
744-byte payloada58ce8b820d5bf486f560f02c02f454ba0985b74d69bd769035199576a0da7f3
complete six_color.s source8cbcdb5755ce60f19849d6ffd39fbee163be347ae33d02486fd9ca3e24212fb8
Windows installer CMDe1e407dc5b61788bf6e8be75a3dd0c4c23a0a992d143d72cae4648dccb6f50cd
Public patch-only bundlea54f822f188740d5c828ab48c6d4375ac8cc13f93b8c8676e8a8a6189d61a01b
126-selection 1080p60 master7803da179519609ec1a5f57b91a12b7337699f8d1c2a892cb98cfa669dc520b4
Published evidence reelYouTube OrfUnVK_Ezk
ROM expansionfalse

The release applies reproducibility at the member level: fixed clean member contents produce deterministic patched member contents. The outer ZIP hash is pinned only for the canonical input archive; the member hashes are the portable contract.

What remains stock and what remains open

N41 reported a one-frame select-screen palette/character ordering mismatch. I have not captured the two writes and the frame between them, so this post does not call it reproduced, fixed, or caused by this patch. It remains an explicitly credited trace to run.

The body-palette, confirmed-animation, large select-side-figure, and installer features are complete. The remaining work is follow-up, not a hidden release gate:

  1. The confirmed-selection animation and large select-side figure are six-way. Small select cells, VS portraits, HUD headshots, nameplates, win art, and separately loaded effect palettes stay stock.
  2. The automatic/demo/preset $00C46A path retains its stock binary duplicate policy; ordinary human two-player duplicate selection is live-verified modulo six.
  3. The release has static body verification for all 23 configured IDs, static preview verification for 21 vanilla IDs, and 126 physical-button runs with exact live uploads for the animated selection, large select-side figure, and match body. It does not claim 264 visual playthroughs.
  4. Vanilla PalMod definitions still point at the old two-bank blocks, so they do not edit the relocated body data. A future DEF addendum can expose the new $184000 arena from the same six-bank provenance table; the ROM patch itself does not require PalMod.
  5. N41's frame-order report remains the next timing trace: capture both writes and the intervening frame before assigning a cause.

The method here is iterative on purpose: static analysis finds the candidate path, dynamic observation names the real inputs, and controlled intervention tests values the stock game never emits. The arXiv reverse-engineering survey below gives that workflow a useful name; the ROM listings and receipts—not the paper—prove these addresses.

References

I deliberately do not cite image-quantization or learned palette-transfer papers here. The body pipeline copies RGB nibbles exactly. The preview pipeline is the explicit stock-body semantic target-hue/affine algorithm documented above—not a learned model or an LCh method—and its stable-warm assignments, fallback decisions, outputs, and clipping counts are manifested.

Written by Daniel Plas Rivera · 10,539 words · $30

ShareXLinkedIn