# Six-color engineering notes: historical archive

Preserved from the article before the October 2, 2026 interactive revamp. Build status, addresses and open questions below belong to that historical version. The current workshop explains the separate standalone, combined six-color and nine-color layouts.

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](/blog/five-hyper-levels-in-marvel-vs-capcom).

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.

This page remains the standalone v1 release history and complete palette
tutorial. The [current DHC download](/downloads/dhc)
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](/blog/six-color-mvc/ryu-six-palette-fighters.png)

_THE RESULT FIRST // Ryu in six live MAME body captures. LP, MP, HP, LK, MK, and HK each select a different installed MvC2 palette._

**The player version**

- **21** — selectable fighters (the complete vanilla roster, including five stock EX forms)
- **6** — button choices (LP / MP / HP / LK / MK / HK)
- **3** — matching surfaces (large select figure, selected animation, and live match body)
- **0 B** — ROM expansion (every original member keeps its original size)

**How many colors the six attack buttons actually select**

- **2 colors** — Stock X-Men vs. Street Fighter (LP/MP/HP share one index; LK/MK/HK share the other)
- **2 colors** — Stock Marvel Super Heroes vs. Street Fighter (the same punch-versus-kick collapse)
- **2 colors** — Stock Marvel vs. Capcom (six physical inputs already exist, but the selector keeps only 0 or 1)
- **6 colors** — MvC2 selectable donor banks (LP, HP, A1, LK, HK, and A2 become the six MvC1 cabinet choices)
- **6 colors** — MvC1 with this patch (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._

**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.

| route | LP | MP | HP | LK | MK | HK |
|---|---|---|---|---|---|---|
| match fighter | bridged | bridged | bridged | bridged | bridged | bridged |
| selected animation | bridged | bridged | bridged | bridged | bridged | bridged |
| large select figure | bridged | bridged | bridged | bridged | bridged | bridged |
| small roster cell | native | native | native | native | native | native |
| HUD + effect palettes | native | native | native | native | native | native |

_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

| # | stop | era | status | measurement | what it measures | section |
|---|---|---|---|---|---|---|
| 1 | Stock code keeps only punch or kick | 1996–1998 CPS-2 baseline | prior-art | 2 indices | six buttons collapse to 0 / 1 | [Stock code keeps only punch or kick](#stock-mvc-xvsf-and-mshvsf-really-had-two-colors) |
| 2 | The body reader accepts indices 0–5 | first integrated body build | corrected | 6 body colors | select presentation still incomplete | [The body reader accepts indices 0–5](#the-patch-turns-the-stored-bit-into-a-six-way-index) |
| 3 | The large select figure gets six sources | second loader | measured | 48 / 48 | large-figure words match | [The large select figure gets six sources](#the-first-patch-fixed-the-body-and-missed-the-large-select-figure) |
| 4 | War Machine exposes a third collapse | selected-animation bug | corrected | 1–5 → bank 1 | stock animation loader behavior | [War Machine exposes a third collapse](#a-third-loader-made-the-selected-animation-show-the-wrong-color) |
| 5 | All three loaders agree | v1 acceptance | measured | 126 / 126 | fighter-button selections pass | [All three loaders agree](#watch-all-126-selections) |
| 6 | One frame-order question stays open | credited follow-up | open | trace pending | palette write versus sprite change | [One frame-order question stays open](#what-remains-stock-and-what-remains-open) |

**01 — Stock code keeps only punch or kick**

_1996–1998 CPS-2 baseline · status: prior-art_

**2 indices** — six 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 after this stop: 0% of the animation tree.

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

Measured output: `LP  MP  HP  →  0`; `LK  MK  HK  →  1`

[Read this section](#stock-mvc-xvsf-and-mshvsf-really-had-two-colors)

**02 — The body reader accepts indices 0–5**

_first integrated body build · status: corrected_

**6 body colors** — select 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 after this stop: 33% of the animation tree.

> **Retracted here:** Six body colors meant the whole feature was finished

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

![Ryu shown in six live body palettes](/blog/six-color-mvc/ryu-six-palette-fighters.png)

Lamps: BODY -> on

[Read this section](#the-patch-turns-the-stored-bit-into-a-six-way-index)

**03 — The large select figure gets six sources**

_second loader · status: measured_

**48 / 48** — large-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 after this stop: 67% of the animation tree.

Evidence: All 126 physical selections copied the expected 48-word source

![Six palettes for every large character-select figure](/blog/six-color-mvc/select-sideart-six-color-contact-sheet.png)

Lamps: FIGURE -> on

[Read this section](#the-first-patch-fixed-the-body-and-missed-the-large-select-figure)

**04 — War Machine exposes a third collapse**

_selected-animation bug · status: corrected_

**1–5 → bank 1** — stock 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 after this stop: 67% of the animation tree.

> **Retracted here:** A correct large figure and body proved the selected animation

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

![War Machine HP on character select and in the live match](/blog/six-color-mvc/war-machine-hp-select-to-body.png)

Lamps: ANIMATION -> claimed

[Read this section](#a-third-loader-made-the-selected-animation-show-the-wrong-color)

**05 — All three loaders agree**

_v1 acceptance · status: measured_

**126 / 126** — fighter-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 after this stop: 100% of the animation tree.

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

![All 21 selectable fighters across six body palettes](/blog/six-color-mvc/roster-six-palette-fighters.png)

Lamps: ANIMATION -> on

[Read this section](#watch-all-126-selections)

**06 — One frame-order question stays open**

_credited follow-up · status: open_

**trace pending** — palette 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 after this stop: 100% of the animation tree.

Evidence: N41 report; not yet reproduced or assigned to this patch

Measured output: `palette write  →  ?`; `identity change →  ?`; `capture both + intervening frame`

[Read this section](#what-remains-stock-and-what-remains-open)

**What each lamp measures**

- **BODY** — live match body follows index 0–5 (stop 2: on)
- **FIGURE** — large select figure follows index 0–5 (stop 3: on)
- **ANIMATION** — selected animation follows index 0–5 (stop 4: claimed, stop 5: on)

_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

[Video: All 126 MVC2-to-MVC1 six-color selections — 21 fighters × LP/MP/HP/LK/MK/HK](https://www.youtube.com/watch?v=OrfUnVK_Ezk)

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.

**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](#reproduce-the-patch-in-ghidra-mame-and-a-hex-editor).

**The technical result in one inspection card** · status: Shipped v1 — live-verified on the clean 21-member Euro parent

- **Goal:** Make LP, MP, HP, LK, MK, and HK choose six distinct body palettes, then carry the same 0–5 choice through the large select figure and selected animation.
- **Method:** Replace the stock punch-or-kick boolean with a 0–5 index, scale body pointers by index × $60, and widen the two independent select-screen consumers.
- **Result:** All 21 vanilla-selectable fighters receive six literal MvC2 body banks and six matching derived large-figure palettes. Dark Sakura and Shuma-Gorath are not added to vanilla MvC1.
- **Proof:** A 126-run MAME census physically selects every fighter with every attack button. Each accepted run matches 32/32 selected-animation words, 48/48 large-figure words, and 48/48 body words.
- **Bounds:** Small roster cells, VS portraits, HUD headshots, nameplates, win art, and separately loaded effects remain stock. N41's reported one-frame ordering question remains open.

## 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.

**Address map — MvC fighter IDs consumed by the palette directory** · span $000000–$000032

| # | region | range | size | tone | note |
|---|---|---|---|---|---|
| 1 | null / non-fighter row | `$000000–$000002` | 2 B | muted |  |
| 2 | 16 vanilla main-form fighters | `$000002–$000022` | 32 B | accent |  |
| 3 | Onslaught boss row remains stock | `$000022–$000024` | 2 B | muted |  |
| 4 | 5 vanilla EX-form fighters | `$000024–$00002E` | 10 B | accent |  |
| 5 | Dark Sakura + Shuma custom IDs | `$00002E–$000032` | 4 B | hot | prepared palette rows; unreachable from the vanilla select screen |
| A | Dark Sakura | `$00002E` | marker | hot |  |
| B | Shuma-Gorath | `$000030` | marker | hot |  |

_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 program | LP | MP | HP | LK | MK | HK |
|---|---:|---:|---:|---:|---:|---:|
| MSHvSF (`mshvsfu`) | 0 | 0 | 0 | 1 | 1 | 1 |
| XvSF (`xmvsfjr1`) | 0 | 0 | 0 | 1 | 1 | 1 |

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:

```powershell
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.

```asm
; 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:

| game | core address | stored byte | common 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 / value | 68000 meaning | why it matters |
|---|---|---|
| `0242 0700` | `andi.w #$0700,d2` | keep only bits 8, 9, and 10: LK/MK/HK as one kick family |
| `7201` | `moveq #1,d1` | collapse any kick to index 1 |
| `1141 0277` | `move.b d1,$277(a0)` | store the chosen index in the fighter object |
| `4a2e 0277` | `tst.b $277(a6)` | stock loader asks only “zero or nonzero?” |
| `41e8 0060` | `lea $60(a0),a0` | nonzero means advance one 96-byte color block |
| `0281 f000f000` | `andi.l #$F000F000,d1` | preserve the high nibble of two palette-RAM words before OR-ing RGB |
| `$0770` | binary `0000 0111 0111 0000` | all six attack bits: three punches plus three kicks |

The live MvC input sweep names every bit:

| MvC1 button | MAME field | held-word mask | patched index |
|---|---|---:|---:|
| LP | P1 Button 1 | `$0010` | 0 |
| MP | P1 Button 2 | `$0020` | 1 |
| HP | P1 Button 3 | `$0040` | 2 |
| LK | P1 Button 4 | `$0100` | 3 |
| MK | P1 Button 5 | `$0200` | 4 |
| HK | P1 Button 6 | `$0400` | 5 |

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

```mermaid
flowchart LR
    input["held input word<br/>$24(a6) / $26(a6)"] --> decode["selector<br/>stock 0/1 · patch 0..5"]
    decode --> object["fighter color byte<br/>$277(a0)"]
    object --> preview["large select-figure hook<br/>$0B7690"]
    preview --> previewArena["large-figure arena<br/>$3F5000 + fighter × $240 + index × $60"]
    previewArena --> previewRam["select palette RAM<br/>$914180"]
    object --> latch["confirmed-slot bytes<br/>$FF40C4..$FF40C7"]
    latch --> selectAnim["selected-animation hook<br/>$07AF6C · index × $60"]
    selectAnim --> animRam["two-row animation palette RAM<br/>32 exact RGB words"]
    latch --> setup["fighter setup<br/>restores $277(a6)"]
    setup --> directory["fighter directory<br/>$001C7268"]
    directory --> scale["base + index × $60"]
    scale --> rows["3 rows × 16 words"]
    rows --> ram["body palette RAM slot<br/>$914000 + slot × $60"]
```

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 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`.

```asm
; 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 2026 [SoK on AI-augmented binary reversing](https://arxiv.org/abs/2606.17398)
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:

```text
$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:

```asm
; 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](/blog/six-color-mvc/roster-six-palette-fighters.png)

_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.](/blog/six-color-mvc/roster-six-palette-fighters.png)

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](/blog/six-color-mvc/roster-half-overlay-comparisons.png)

_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.](/blog/six-color-mvc/roster-half-overlay-comparisons.png)

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:

```asm
; 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:

```text
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.

[THE TRANSITION // physical HP confirmation, complete selected animation, then the live body. The same navy/red index survives the cut.](/blog/clips/mvsc-six-color-war-machine-hp.mp4)

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:

```asm
; 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:

```asm
; 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 loaders** — Physical Button 3 → stored index 2 → navy/red on select and in the match · status: CONFIRMED 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](/blog/six-color-mvc/war-machine-hp-select-to-body.png)

| field | value |
|---|---|
| selected animation | 32 / 32 RGB words |
| large select figure | 48 / 48 RGB words |
| live match body | 48 / 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._

Evidence: 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](/blog/six-color-mvc/select-sideart-six-color-contact-sheet.png)

_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 writer | row it can replace | v1 hook target | v1 source |
|---:|---:|---:|---|
| `$064F0C` | 0 | `$3F4258` | `$1861C0 + index×$60 + rowOffset` |
| `$07D0C0` | 1 | `$3F4294` | `$1861C0 + index×$60 + rowOffset` |
| `$064EAA` | 2 | `$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.

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](https://github.com/Preppy/PalMod) expose
the selectable banks in storage order:

```text
LP, LK, HP, HK, A1, A2
```

The PalMod author's [MvC2 color-editing guide](https://zachd.com/mvc2/colors/tools/ComboMasher/Combomasher%20Guide/customizecharacters.html)
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 target | MvC2 donor bank |
|---|---|
| LP | LP |
| MP | HP |
| HP | A1 |
| LK | LK |
| MK | HK |
| HK | A2 |

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](/blog/six-color-mvc/mvc2-six-color-contact-sheet.png)

_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:

```json
{
  "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](/blog/six-color-mvc/mvc2-six-color-ryu-strip.png)
_1 // DATA — six exact three-row RGB444 blocks from MvC2_

![Ryu rendered in MAME with all six installed body palettes](/blog/six-color-mvc/ryu-six-palette-fighters.png)
_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:

```text
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:

```text
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 button | MvC2 bank | three source-row offsets | expected CPU-order output SHA-256 |
|---|---|---|---|
| LP | LP | `$6E440/$6E460/$6E480` | `75ed3088c037783a68319efda6206da96fce48c7ede6594e4ed2e8fdbe027323` |
| MP | HP | `$6E640/$6E660/$6E680` | `bc7bbb2410be87e7b247e27b85df7028d5dbe8ab6352b12e72fa72fac57b8bdf` |
| HP | A1 | `$6E840/$6E860/$6E880` | `1c43faa7d0bcf21c862c710647b1e30fce06cec9be4d0f13788eb10d0a5c167c` |
| LK | LK | `$6E540/$6E560/$6E580` | `0b0901da492e8dfd8771758c68233c73509521912381b8fdd19097a27990302f` |
| MK | HK | `$6E740/$6E760/$6E780` | `f0d2fb0dee07db6e4634e923979e920314a46729621753f8cd8ecb4cd45f9dfe` |
| HK | A2 | `$6E940/$6E960/$6E980` | `93235f97998357cfcaa9d2e49cb069933329cbbf6f5a682a63208947305c2b3c` |

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 returners | direct 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 Man | Shuma-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 target | MvC2 source | provenance label |
|---|---|---|
| Red Venom | Venom | `derived-proxy` |
| Orange Hulk | Hulk | `derived-proxy` |
| Gold War Machine | War Machine | `derived-proxy` |
| Shadow Lady | Chun-Li | `derived-proxy` |
| Lilith-mode Morrigan | Morrigan | `derived-proxy` |
| Dark Sakura | Sakura | `derived-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:

```text
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`:

```text
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.

**Address map — mvc.06a // stock 512 KiB member** · span $180000–$200000

| # | region | range | size | tone | note |
|---|---|---|---|---|---|
| 1 | stock data before aligned cave | `$180000–$184000` | 16 KB | muted |  |
| 2 | 13,248-byte six-color arena | `$184000–$1873C0` | 12.9 KB | hot | 23 × 6 × $60; every byte had a stock $FF preimage |
| 3 | 3,136 aligned cave bytes remain | `$1873C0–$188000` | 3.1 KB | accent |  |
| 4 | stock data after cave | `$188000–$200000` | 480 KB | muted |  |
| A | first widened fighter-directory row | `$1C726C` | marker | accent | 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`.

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.

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.

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:

```text
6f63627cc37c554f74e8bf07b21730fa7f85511c7d5d07449850be98dde91da8
```

These are the four members the release eventually changes:

| member | size | clean SHA-256 |
|---|---:|---|
| `mvce.03a` | `$80000` | `48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757` |
| `mvce.04a` | `$80000` | `71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67` |
| `mvc.06a` | `$80000` | `132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b` |
| `mvc.10` | `$80000` | `e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a` |

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:

| label | CPU address | clean observation |
|---|---:|---|
| `select_color` | `$00C426` | loads P1/P2 held word, reduces it to `D1=0/1` |
| `color_store` | `$00C464` | `move.b d1,$277(a0)` |
| `body_reader` | `$00A4B0` | source in `A0`, destination in `A1`, boolean at `$00A4BC` |
| `selected_animation` | `$07AF36` | resolves directory, boolean at `$07AF6C` |
| `large_select_figure` | `$0B7690` | loads stock side-figure source `$1D2A24+d0` |
| `fighter_palette_directory` | `$1C7268` | one longword pointer per even fighter ID |
| `six_color_code` | `$3F4000` | clean `$FF`; patched 744-byte payload |
| `six_color_body_arena` | `$184000` | clean `$FF`; patched 23 × 6 × `$60` |
| `six_color_select_arena` | `$3F5000` | clean `$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:

```powershell
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:

```text
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 }
```

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:

```text
bp 00c426
bp 00c468
g
```

After `$00C426` stops:

```text
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:

| button | stored index | body + selected-animation source | large-figure source |
|---|---:|---:|---:|
| LP | 0 | `$185200` | `$3F6200` |
| MP | 1 | `$185260` | `$3F6260` |
| HP | 2 | `$1852C0` | `$3F62C0` |
| LK | 3 | `$185320` | `$3F6320` |
| MK | 4 | `$185380` | `$3F6380` |
| HK | 5 | `$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:

```text
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:

```text
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:

```text
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.03a · 6 bytes, 5 changed

```text
before  EB D9 76 56 71 FF
after   F1 27 6D B0 71 9F
```

_six-button decode. Decrypted, the patched six bytes are JMP $3F4000._

**Byte patch** `$00A4BC` · mvce.03a · 6 bytes, 6 changed

```text
before  60 75 3F 54 7B 95
after   39 61 C3 28 B0 F0
```

_first body reader. Decrypted, this becomes JMP $3F404C._

**Byte patch** `$07AF6C` · mvce.03a · 6 bytes, 6 changed

```text
before  6E E5 05 C4 A5 92
after   91 15 51 A5 DA 56
```

_selected-animation reader. Decrypted, this becomes JMP $3F42D0._

**Byte patch** `$037690` · mvce.04a · 6 bytes, 6 changed

```text
before  F2 F1 EE 8B CA 79
after   87 2B 24 E0 E8 CB
```

_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.

| purpose | raw member + offset | payload target | clean raw bytes | patched raw bytes |
|---|---|---:|---|---|
| six-button decode | `mvce.03a + $00C434` | `$3F4000` | `ebd9765671ff` | `f1276db0719f` |
| duplicate modulo six | `mvce.03a + $00C440` | `$3F4038` | `0ef18564e99d` | `a15762f7a171` |
| body reader A6 | `mvce.03a + $00A4BC` | `$3F404C` | `60753f547b95` | `3961c328b0f0` |
| body reader P1 slot A | `mvce.03a + $00A5B2` | `$3F405A` | `67fb5daccaac` | `70bdd1f958d5` |
| body reader P1 slot B | `mvce.03a + $00A62E` | `$3F4068` | `514b658c75c6` | `c5b063fea521` |
| body reader P1 copy A | `mvce.03a + $00A682` | `$3F40A2` | `df942762045e` | `2613d3509bef` |
| body reader P1 copy B | `mvce.03a + $00A6C2` | `$3F40B0` | `2f02ba9ab95f` | `2288c6c8f005` |
| body reader P2 slot A | `mvce.03a + $00A7B2` | `$3F4076` | `62d1c2bb0f78` | `a80bb5e8d32d` |
| body reader P2 slot B | `mvce.03a + $00A82E` | `$3F4084` | `d396708885bd` | `2b1720145f7c` |
| body reader P2 copy A | `mvce.03a + $00A882` | `$3F40BE` | `4f581520a74f` | `4f5ebcf01dae` |
| body reader P2 copy B | `mvce.03a + $00A8C0` | `$3F40CC` | `70048b4ed4ed` | `388920215904` |
| Mega Man intro row 2 | `mvce.03a + $064EAA` | `$3F421C` | `0ed5e63cd579` | `f4fc120c584f` |
| Mega Man intro row 0 | `mvce.03a + $064F0C` | `$3F4258` | `f97c1685e04f` | `fc0e73756bd1` |
| Shadow Lady row guard | `mvce.03a + $06987A` | `$3F41A2` | `2207ec1c56c0` | `40e7f7b19308` |
| selected animation | `mvce.03a + $07AF6C` | `$3F42D0` | `6ee505c4a592` | `911551a5da56` |
| Mega Man intro row 1 | `mvce.03a + $07D0C0` | `$3F4294` | `864efdb0a015` | `363e5c1d0c1c` |
| large select-side figure | `mvce.04a + $037690` | `$3F40F0` | `f2f1ee8bca79` | `872b24e0e8cb` |

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

| raw member location | clean first bytes | patched first bytes | meaning |
|---|---|---|---|
| `mvc.06a + $004000` | `ffffffffffffffffffffffffffffffff` | `0000ff0fab0b77084506340423031101` | start of War Machine's body arena |
| `mvc.06a + $04728C` | `1c00c487` | `18000052` | Ryu directory pointer, CPU `$1C87C4 → $185200` |
| `mvc.10 + $074000` | `ffffffffffffffffffffffffffffffff` | `0072020804002a660172020805002266` | first 16 on-disk bytes of the 744-byte code payload |
| `mvc.10 + $075000` | `ffffffffffffffffffffffffffffffff` | `e00fee0edd0dcc0caa0a990988087707` | first 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`](/downloads/mvsc-six-color/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.

```bash
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:

```text
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:

| member | clean SHA-256 | patched SHA-256 |
|---|---|---|
| `mvce.03a` | `48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757` | `d69f8b5c05d745d899377cddbb9ccd4b39af698ec8cf565db6cc634a7a97de2b` |
| `mvce.04a` | `71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67` | `d7aa82828984f939b09569c79d62d77b65a05f1af9f7a1a2c8179f17ff13b37d` |
| `mvc.06a` | `132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b` | `be22d6cf23d84a12e3520e6c344dd974141c7b730aa58a1295ffa6df1eddcc0a` |
| `mvc.10` | `e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a` | `f21f3a8881734f844350e9063b0ef8164cdd6edbafd7fb68c37b08d0b7980969` |

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.

- 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:

**What is actually inside the visual change**

- **744 B** — new 68000 code (selector, readers, guards, and rejoins—the complete source ships beside the CMD)
- **25,344 B** — palette data (13,248 body bytes plus 12,096 derived large-figure bytes)
- **198 B** — routing edits (17 six-byte encrypted hooks plus 96 bytes of fighter-directory pointers)
- **4** — changed ROM members (all four remain exactly $80000 bytes; the other 17 are copied unchanged)

`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](https://arxiv.org/abs/2104.06020) 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](/downloads/mvsc-dhc/MVSC-DHC-v5.cmd).
These are the sealed acceptance records for technical readers auditing the
standalone download:

| item | SHA-256 / value |
|---|---|
| clean `mvsc.zip` input | `6f63627cc37c554f74e8bf07b21730fa7f85511c7d5d07449850be98dde91da8` |
| 13,248-byte body arena | `833d74c203d7f2e90eb34e210e51a3b0623d38733b83135732a4dab44b6900e2` |
| 12,096-byte large-select-figure arena | `79879820f9bce737e5ad678ca3f0c4575b03a4c10e69317fed3ecbc54d77852c` |
| 744-byte payload | `a58ce8b820d5bf486f560f02c02f454ba0985b74d69bd769035199576a0da7f3` |
| complete `six_color.s` source | `8cbcdb5755ce60f19849d6ffd39fbee163be347ae33d02486fd9ca3e24212fb8` |
| Windows installer CMD | `e1e407dc5b61788bf6e8be75a3dd0c4c23a0a992d143d72cae4648dccb6f50cd` |
| Public patch-only bundle | `a54f822f188740d5c828ab48c6d4375ac8cc13f93b8c8676e8a8a6189d61a01b` |
| 126-selection 1080p60 master | `7803da179519609ec1a5f57b91a12b7337699f8d1c2a892cb98cfa669dc520b4` |
| Published evidence reel | [YouTube `OrfUnVK_Ezk`](https://youtu.be/OrfUnVK_Ezk) |
| ROM expansion | `false` |

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

- [PalMod source repository](https://github.com/Preppy/PalMod) — the current MvC2
  Dreamcast bank definitions and labels used as the offset oracle.
- [PalMod author's MvC2 character-color guide](https://zachd.com/mvc2/colors/tools/ComboMasher/Combomasher%20Guide/customizecharacters.html)
  — primary documentation that the first six choices are LP, LK, HP, HK, A1, and A2.
- [Reproducible Builds: Increasing the Integrity of Software Supply Chains
  (arXiv:2104.06020)](https://arxiv.org/abs/2104.06020) — context for fixed inputs,
  deterministic artifacts, and published hashes.
- [SoK: AI-Augmented Binary Reversing
  (arXiv:2606.17398)](https://arxiv.org/abs/2606.17398) — context for iterating among
  static analysis, dynamic observation, and controlled intervention.

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.