Goal: Determine whether character ID 0x2E in MvC1's ROM represents planned but cut content, and document the complete secret character system architecture.
Constraints: No source code. Hardware-encrypted program code (mitigated via Phoenix Edition decrypted ROM). No developer documentation. Must distinguish real references from instruction-encoding false positives.
Approach: Systematic binary scan for ID 0x2E byte patterns, cross-referenced with animation marker counts, conditional branch analysis, and comparison against known secret character implementations.
Result: 85 confirmed code references for ID 0x2E. 4 conditional load points where MOVE.B #$2E, D0 is guarded by BEQ branches. 3/4 share code paths with Shadow Lady (0x2A). False positives for IDs 0x30 and 0x32 identified and filtered (opcode 0x303C = MOVE.W #imm).
Proof / Validation: ULTRA SETTINGS in-game diagnostic verifies: all 4 secret load functions contain valid code at documented addresses. Character roster scan confirms 16 normal IDs, 5 secret IDs, ID 0x2E present in ROM but not in roster.
Artifacts: A reproducible build pipeline that rebuilds the patched ROM set from a pristine base, one decompiled C translation per recovered secret-character load function, and a saved static-analysis database carrying the cross-references and labels behind the 85-reference count.
The forensic findings below stand as documented. The revival plan for the $2E slot has since moved three times: the Armored Spider-Man / Iron Spider line was retired (an addition attempt -- no stock character was ever removed), the slot pivoted to a Dark Sakura port from MSHvSF (char-id $2A in that game) shipped July 11 on a donor-chassis architecture ($2E force-remapped to real Ryu at state-machine dispatch, her imported 9,520-tile animation tree forced from the custom 64MB core's upper GFX region) -- and then the donor chassis itself was superseded. As of late July, both secret slots run as native ports: Dark Sakura ($2E) and Shuma-Gorath ($30), their real MSHvSF code blocks relocated into the ROM, fighting on their own animation trees with no Ryu remap, hardened by a cumulative chain of live-gated fixes, names on both the select screen and the lifebar. Full write-ups: the donor chassis (history) and the native port (current).
The Question
MvC1 ships with 5 secret characters (Red Venom, Orange Hulk, Gold War Machine, Shadow Lady, Lilith). Each is a palette-swapped version of a normal character, accessible via a directional input code. The ID space is wider than the roster: IDs are even numbers from 0x02 to 0x3E, and several are never used. Wider is not the same as provisioned -- see the correction below. Is there evidence that Capcom planned a 6th?
Discovery: 85 Code References for ID 0x2E
Scanning the decrypted program ROM for byte patterns matching each unused character ID produced dramatically different results:
| ID | Code References | Animation Markers | Interpretation |
|---|---|---|---|
| 0x2E | 85 | Multiple | Strong evidence of planned character |
| 0x30 | 80 | 0 | FALSE POSITIVE -- opcode 0x303C = MOVE.W #imm,D0 |
| 0x32 | 81 | 0 | FALSE POSITIVE -- same opcode pattern |
| 0x34-0x3E | 0 | 0 | Nothing behind these IDs |
Filtering False Positives
IDs 0x30 and 0x32 initially appeared promising with 80+ references each. Deeper analysis revealed these are the Motorola 68000 instruction MOVE.W #imm, D0, which encodes as opcode 0x303C. A binary search for 0x30 or 0x32 as a two-byte value naturally matches these instruction opcodes.
packet-beta
title Why a byte scan for 0x30 finds 80 hits -- 68000 MOVE.W imm to D0
0-7: "0x30 -- first opcode byte, also a candidate character id"
8-15: "0x3C -- addressing mode, immediate"
16-31: "the 16-bit immediate operand"The distinguishing factor: animation marker count. Real character IDs have corresponding animation data entries. IDs 0x30 and 0x32 have zero animation markers -- nothing at all behind them. ID 0x2E has multiple animation markers, confirming real character data.
Read that last column literally -- "nothing behind these IDs," and nothing more. The ports that came later established that the engine has no reserved room for a 24th fighter: every per-character table has exactly 23 entries, ids 0x00-0x2C, with zero slack between tables. 0x2E has no entry of its own and borrows 0x0E's. Capcom left nothing half-wired to finish -- putting a character on one of these IDs means giving the engine a value it was never built to hold, which is precisely why the resulting reads run off the end of every table.
The whole method is a two-stage funnel: a raw byte scan produces comparable reference counts for all three candidates, and only the animation-marker cross-check separates a real character from an instruction-encoding artifact.
flowchart TD
scan["Scan decrypted program ROM<br/>for each unused ID byte"] --> a["ID 2E<br/>85 byte matches"]
scan --> b["ID 30<br/>80 byte matches"]
scan --> c["ID 32<br/>81 byte matches"]
a --> m{"Animation markers<br/>present for this ID"}
b --> m
c --> m
m -->|"2E multiple markers"| real["Real character data<br/>planned but cut content"]
m -->|"30 and 32 zero markers"| fp["False positive<br/>opcode 303C is MOVE.W imm D0"]
style real fill:#112d20,stroke:#419865,color:#eaf2ef
style fp fill:#351b1b,stroke:#b56565,color:#f6dadaThe Secret Character System
Existing Secret Characters
| Character | ID | Base Character | Base ID | Input Code Length |
|---|---|---|---|---|
| Red Venom | 0x24 | Venom | 0x0C | 14 inputs |
| Orange Hulk | 0x26 | Hulk | 0x06 | 14 inputs |
| Gold War Machine | 0x28 | War Machine | 0x02 | 15 inputs |
| Shadow Lady | 0x2A | Chun-Li | 0x16 | 16 inputs |
| Lilith | 0x2C | Morrigan | 0x1E | 17 inputs |
| Armored Spider-Man | 0x2E | Spider-Man | 0x0E | Not implemented |
The ID pattern is clear: secret characters use IDs base_id + 0x18 (Red Venom: 0x0C + 0x18 = 0x24). Following this pattern, a Spider-Man variant would be 0x0E + 0x20 = 0x2E. The actual ID 0x2E fits.
Three Table Formats
The first architectural surprise: secret characters are NOT stored in a single unified table. ROM forensics revealed three completely different data structures:
Type A: Structured Entries (16 bytes) -- Found at $014D6E, Shadow Lady only.
014D60: 40 A0 00 40 00 00 49 00 00 00 06 00 2B 00 16 2A
^^ ^^
Chun-Li(0x16) → Shadow Lady(0x2A)| Offset | Value | Purpose |
|---|---|---|
| 14 | 0x16 | Base character ID (Chun-Li) |
| 15 | 0x2A | Secret character ID (Shadow Lady) |
The base/secret pair lives in the last two bytes of the fixed 16-byte record -- everything before it is entry metadata the mapping does not depend on:
packet-beta
title Type A structured entry at 014D60 16 bytes
0-13: "entry header 14 bytes"
14: "base id 16 Chun Li"
15: "secret id 2A Shadow Lady"Type B: Sequence Format (2-byte pairs) -- Found at $04A3F2 (Lilith) and $03EDD4 (Gold War Machine).
04A3F2: 1E 2C ...
^^ ^^
Morrigan(0x1E) → Lilith(0x2C)Type C: Packed Format (2-byte pairs) -- Found at $024D3A (Orange Hulk) and $07249E (Red Venom).
024D3A: 06 26 ...
^^ ^^
Hulk(0x06) → Orange Hulk(0x26)Why Three Formats?
This is evidence of iterative development. Capcom added secret characters at different stages, each time integrating with whichever subsystem was most convenient:
| Character | Table Type | ROM Section | Likely Dev Stage |
|---|---|---|---|
| Shadow Lady | Type A | Character select code | Select screen implementation |
| Lilith | Type B | Animation/graphics data | Animation system work |
| Gold War Machine | Type B | Animation/graphics data | Same batch as Lilith |
| Orange Hulk | Type C | Palette/graphics data | Palette system work |
| Red Venom | Type C | Late ROM section | Added late in development |
flowchart TD
subgraph A["Type A -- structured, 16 bytes"]
a1["$014D6E<br/>Chun-Li 0x16 to Shadow Lady 0x2A"]
end
subgraph B["Type B -- sequence, 2-byte pairs"]
b1["$04A3F2<br/>Morrigan 0x1E to Lilith 0x2C"]
b2["$03EDD4<br/>War Machine 0x02 to Gold WM 0x28"]
end
subgraph C["Type C -- packed, 2-byte pairs"]
c1["$024D3A<br/>Hulk 0x06 to Orange Hulk 0x26"]
c2["$07249E<br/>Venom 0x0C to Red Venom 0x24"]
end
A --> sec["Character select code<br/>select-screen implementation"]
B --> anim["Animation and graphics data<br/>animation-system work"]
C --> pal["Palette and graphics data,<br/>plus a late ROM section"]
sec --> iter["Three formats = iterative development:<br/>each character integrated with whichever<br/>subsystem was convenient at the time"]
anim --> iter
pal --> iter
iter --> imp["Adding a sixth secret character<br/>means entries in ALL THREE"]Implication: Adding Armored Spider-Man requires entries in ALL THREE table types.
The Smoking Gun: 4 Conditional Load Points
The most compelling evidence: the ROM contains 4 locations where ID 0x2E is conditionally loaded via MOVE.B #$2E, D0, each guarded by a BEQ (branch if equal) that prevents execution in the retail game:
| Load Point | Address | Near Shadow Lady Refs? | Pattern |
|---|---|---|---|
| Point 1 | $06DC76 | Yes ($6DC4D, $6DC5D) | Alternate path from Shadow Lady |
| Point 2 | $096B86 | No | Independent check |
| Point 3 | $0A8A9E | Yes ($A8A77, $A8A85) | Alternate path from Shadow Lady |
| Point 4 | $0BAD8E | Yes ($BAD79, $BAD85) | Alternate path from Shadow Lady |
3 out of 4 load points have Shadow Lady references nearby. The code pattern is:
IF (some_condition)
load Shadow Lady (0x2A)
ELSE
load ID 0x2E ← disabled via BEQDisassembly: Load Point 1
06DC40: DC.W $002A ; Shadow Lady ID
06DC4C: DC.W $002A ; Shadow Lady ID again
06DC72: BEQ.S $06DC78 ; Branch if equal → SKIP 0x2E
06DC74: BEQ.S $06DC7E ; Another branch
06DC76: MOVE.B #$2E, D0 ; ← LOAD ID 0x2E (never reached)The BEQ instructions check some game state variable -- possibly a debug flag or unlock condition that was zeroed before shipping. Because this condition is always false in the retail game, execution always skips the MOVE.B #$2E, D0.
This is not dead code -- it's disabled functionality.
Decompiled Secret Load Functions
To understand how to wire ID 0x2E, we decompiled all 4 existing secret character load functions:
| Function | Address | Size | Key Pattern |
|---|---|---|---|
load_red_venom | $022B2E | 66 bytes | move.w #$24, D0 + bsr.w $22CF2 (shared subroutine) |
load_orange_hulk | $05D044 | 60 bytes | move.w #$24, D0 + property writes to player structure |
load_gold_war_machine | $07EF90 | 12 bytes | move.w #$24, D0 + movep.l D0, $3141(A0) |
load_shadow_lady | $0A26C8 | 20 bytes | move.b #$24, D0 + flag setup via ori.b |
The general character-load routine they sit alongside, character_load_main, is at $0FF9B4 (98 bytes).
All functions follow the same pattern: set a value in D0, then write character properties to the player structure via indirect addressing through A0 or A6. Gold War Machine is the simplest at 12 bytes -- our ID 0x2E load function can follow this minimal pattern.
Gold War Machine (12 bytes -- simplest)
07EF90: move.w #$24, d0 ; Set character value
07EF94: movep.l d0, $3141(a0) ; Write to player data structure
07EF98: tst.b $c(a6) ; Test condition byteRed Venom (66 bytes -- most complex)
022B2E: move.w #$24, d0 ; Set character value
022B32: addi.b #$74, d2 ; Adjust parameter
022B36: bsr.w $22cf2 ; Call shared subroutine
...
022B42: ori.w #$14, (a4) ; Set flags (armor/properties)The Position Check Problem
After adding data entries for Wolverine → Spider-Man in all three table types, the secret box still didn't appear. The assembly code that decides whether to show a secret box has hardcoded position checks:
; Pseudocode of position check logic
check_for_secret:
CMP.B #$0D, D0 ; Position 13 (Chun-Li)?
BEQ show_shadow_lady
CMP.B #$01, D0 ; Position 1 (Morrigan)?
BEQ show_lilith
CMP.B #$06, D0 ; Position 6 (Hulk)?
BEQ show_orange_hulk
CMP.B #$0C, D0 ; Position 12 (War Machine)?
BEQ show_gold_wm
CMP.B #$0B, D0 ; Position 11 (Venom)?
BEQ show_red_venom
; Position 5 (Wolverine) is NOT in the list!flowchart TD
cur["Cursor lands on a portrait"] --> chk{"Hardcoded position check"}
chk -->|"13 Chun-Li"| sl["show Shadow Lady"]
chk -->|"1 Morrigan"| li["show Lilith"]
chk -->|"6 Hulk"| oh["show Orange Hulk"]
chk -->|"12 War Machine"| gw["show Gold War Machine"]
chk -->|"11 Venom"| rv["show Red Venom"]
chk -->|"5 Wolverine"| none["NOT IN THE LIST<br/>no secret box, ever"]
none --> data["The Type A/B/C data entries<br/>exist and are correct --<br/>the code simply never reaches them"]
data --> fix["Fix: patch the check list<br/>in expanded ROM space $0F1000+"]The data tables are read by the code, but the code never reaches them for Wolverine's position. The fix requires patching the assembly to add a position 5 check in expanded ROM space ($0F1000+).
Current status: This is the remaining blocker. The Ghidra decompilation and Capstone disassembly tools are being used for static analysis to locate the exact check routine address.
Input Encoding System
All existing secret codes use directional-only inputs starting from Zangief's portrait:
| Direction | Byte Value |
|---|---|
| Up | 0x08 |
| Down | 0x02 |
| Left | 0x04 |
| Right | 0x06 |
Those bytes accumulate in a circular buffer holding the last N inputs, and the detector is a single accumulate-or-reset loop: entries expire after roughly 60 frames (one second) of inactivity, and any input that breaks the pattern resets the state. The buffer's own address was never located -- it is one of the few things in this system that stayed TBD. The full catalog of the six shipped codes, with byte patterns, is in the input-code post.
Proposed Armored Spider-Man Code
Starting position: Spider-Man (breaks the Zangief convention -- thematically appropriate).
Sequence (10 inputs): Down, Down, Left, Left, Up, Up, Right, Right, Down, Down
Byte pattern: 02 02 04 04 08 08 06 06 02 02
Design rationale:
- Shorter than existing codes (10 vs 14-17)
- Forms a "box" pattern (easy to remember)
- Starts on Spider-Man's portrait (logical for a Spider-Man variant)
- Unique -- no conflicts with existing 6 secret codes
Character Data Architecture
From decompilation, the complete character data layout:
Character Data Table: $065CCA (32 bytes per entry)
Player Runtime: $FF3000 (P1), $FF3100 (P2)
Player Structure Offsets:
+$10 word Player state (animation state machine)
+$20 word X position
+$24 word Y position
+$53 byte CHARACTER ID ← this is what we write
+$54 long Character data pointer
+$60 word Health
Unlock Flags (base $FF8000):
+$3C Red Venom
+$40 Orange Hulk
+$44 Gold War Machine
+$48 Shadow Lady
+$4C Lilith
Cursor Position:
$FF2000 P1 grid position
$FF2002 P2 grid positionThe surrounding machine, confirmed by decompilation:
=== CPS-2 memory map (68000 view) ===
$000000-$0FFFFF Program ROM (1MB used, 4MB mapped)
$400000-$40000F CPS-B registers (layer control, priority, palette)
$660000-$66BFFF Video RAM (48KB, scroll layers)
$700000-$701FFF Object RAM bank 1 (8KB, 1024 sprites)
$708000-$709FFF Object RAM bank 2 (8KB, double-buffered)
$800000-$800007 Input ports (P1/P2 joystick + buttons)
$900000-$92FFFF Palette / GFX RAM (192KB)
$FF0000-$FFFFFF Work RAM (64KB)
=== Other confirmed ROM tables ===
$065CCA Character data table (32 bytes/entry)
$AC7A Character roster table
$00BD00 Animation / sprite data (~512 bytes)
$BB04C Stage tablePalette Locations (in mvc.06a)
Spider-Man has 4 palettes (32 bytes each, 16 colors, RGB444):
| Palette | Offset | Purpose |
|---|---|---|
| P button | $48642 | Main punch color scheme |
| K button | $486A2 | Main kick color scheme |
| Web (P) | $48682 | Web attack palette |
| Web (K) | $486E2 | Web attack alt palette |
Armored Spider-Man Palette Design
Silver/gunmetal "Iron Spider" color scheme:
| Index | Spider-Man | Armored | RGB444 |
|---|---|---|---|
| 0 | Transparent | Transparent | $0000 |
| 1 | Red (main) | Silver | $0999 |
| 2 | Red (light) | Light silver | $0BBB |
| 3 | Red (dark) | Dark silver | $0666 |
| 4 | Blue (main) | Gunmetal | $0444 |
| 5 | Blue (light) | Light gunmetal | $0777 |
| 6 | Blue (dark) | Dark gunmetal | $0333 |
| 7 | Black (outline) | Black | $0000 |
| 8 | White (eyes) | Yellow (visor) | $0FF0 |
| 9 | Skin | Skin (unchanged) | $0DA8 |
| 10 | Web (gray) | Chrome | $0DDD |
| 11 | -- | Mid-gray | $0555 |
| 12 | -- | Dark accent | $0222 |
| 13 | -- | Medium gray | $0888 |
| 14 | -- | Light gray | $0AAA |
| 15 | -- | White | $0FFF |
Palette format is RGB444 big-endian (0x0RGB), 2 bytes per color, 32 bytes per 16-color palette -- not the RGB555 that CPS-2 palettes are often assumed to use. All 16 entries are listed because a palette swap is a whole-palette write: leaving the tail undefined leaves whatever the base character had in the unwritten slots.
Implementation Status
| Component | Status | Evidence |
|---|---|---|
| ID 0x2E analysis (85 refs) | Complete | Binary scan, animation marker cross-reference |
| False positive filtering | Complete | IDs 0x30/0x32 confirmed as MOVE.W opcode artifacts |
| Secret table forensics | Complete | 3 table types documented with hex dumps and structures |
| Load function decompilation | Complete | All 4 existing functions decompiled to C |
| Palette locations | Complete | P/K/Web palettes verified at $48642/$486A2/$48682/$486E2 |
| ROM expansion (96MB GFX free) | Complete | ULTRA SETTINGS confirms 128MB GFX, 16MB Audio |
| In-game verification | Complete | 22-line diagnostic screen in F2 Test Menu |
| Position check discovery | Blocker | Ghidra + Capstone static analysis in progress |
| Assembly position patch | Planned | Add position 5 to hardcoded check list |
| Load function wiring | Planned | Model after Gold WM's 12-byte pattern |
| Armored palette injection | Planned | RGB444 values ready |
| Integration testing | Planned | MAME with modified ROM |
Critical path: Position check discovery → assembly patch → palette injection → load function → testing.
Known Limitations
- Position check address not yet found -- the exact ROM address of the hardcoded position comparison list is needed to patch it
- Input code state machine not implemented -- the box will initially activate on a simple Down press, not the full directional sequence
- No sprite modifications -- initial version is palette-only; actual armor tile modifications require GFX ROM editing across all 8 interleaved chips
- CPS-2 encryption -- all patches target the Phoenix Edition (decrypted
mvscud); targeting encrypted retail ROMs would require re-encryption
How to Validate
# 1. Build the patched ROM set (one command, from a pristine base)
# 2. Run in MAME
mame mvscud -window
# 3. Verify via F2 Test Menu
# Press F2 → select "9: ULTRA SETTINGS"
# Check: FN REDVENOM: OK, FN ORNGHULK: OK, FN GOLDW.M.: OK, FN SHDWLADY: OK
# Check: ID 2E: NO (target -- will become YES when wired)
# 4. Verify character ID scan
# Check: NORMAL IDS: 16, SECRET IDS: 05References
- MvC1 character select disassembly (MAME debugger + Ghidra)
- Decompiled C for each secret-character load function (Gold War Machine, Red Venom, Orange Hulk, Shadow Lady)
- Character table at
$065CCA(32 bytes per entry) - Secret tables: Type A at
$014D6E, Type B at$04A3F2, Type C at$024D3A - Spider-Man palettes: P
$48642, K$486A2, Web P$48682, Web K$486E2 - 4 conditional load points:
$06DC76,$096B86,$0A8A9E,$0BAD8E - 68000 Programmer's Reference Manual (Motorola)
- CPS-2 sprite format: 16x16 tiles, 128 bytes each, 4bpp planar