Skip to content
Index / $07

Binary Forensics: Recovering an Unreleased Character from a 1998 Fighting Game

Systematic binary analysis discovers 85 code references, 4 disabled load points, and 3 distinct table formats for character ID 0x2E -- evidence of an unreleased Armored Spider-Man in MvC1's CPS-2 ROM

Date
Feb 6, 2026 · upd Jul 4, 2026
Runtime
11 min · 2,356 words
Tags
binary-forensics, reverse-engineering, cps2
Slot
$07
Contents
tip // Evidence Strip

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.

Repo / Artifacts: CHARACTER_SELECT/scripts/patch_rom_check_complete.py (build pipeline), DECOMP_MVC/src/character/ (decompiled load functions), CHARACTER_SELECT/ghidra_projects/ (static analysis)

note // Status update (July 2026)

The forensic findings below stand as documented. The revival plan for the $2E slot has changed: the Armored Spider-Man / Iron Spider line was retired, and the slot's character work pivoted to a Dark Sakura port from MSHvSF (char-id $2A in that game) -- a same-generation port whose animation/frame tables carry over byte-identical to MvC1, so the work is table porting plus tile bank remapping rather than new art.

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 character ID space has room for more: IDs are even numbers from 0x02 to 0x3E, with several unused. 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:

IDCode ReferencesAnimation MarkersInterpretation
0x2E85MultipleStrong evidence of planned character
0x30800FALSE POSITIVE -- opcode 0x303C = MOVE.W #imm,D0
0x32810FALSE POSITIVE -- same opcode pattern
0x34-0x3E00Empty slots

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.

The distinguishing factor: animation marker count. Real character IDs have corresponding animation data entries. IDs 0x30 and 0x32 have zero animation markers -- they are completely empty slots. ID 0x2E has multiple animation markers, confirming real character data.

The Secret Character System

Existing Secret Characters

CharacterIDBase CharacterBase IDInput Code Length
Red Venom0x24Venom0x0C14 inputs
Orange Hulk0x26Hulk0x0614 inputs
Gold War Machine0x28War Machine0x0215 inputs
Shadow Lady0x2AChun-Li0x1616 inputs
Lilith0x2CMorrigan0x1E17 inputs
Armored Spider-Man0x2ESpider-Man0x0ENot 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)
OffsetValuePurpose
140x16Base character ID (Chun-Li)
150x2ASecret character ID (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:

CharacterTable TypeROM SectionLikely Dev Stage
Shadow LadyType ACharacter select codeSelect screen implementation
LilithType BAnimation/graphics dataAnimation system work
Gold War MachineType BAnimation/graphics dataSame batch as Lilith
Orange HulkType CPalette/graphics dataPalette system work
Red VenomType CLate ROM sectionAdded late in development

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 PointAddressNear Shadow Lady Refs?Pattern
Point 1$06DC76Yes ($6DC4D, $6DC5D)Alternate path from Shadow Lady
Point 2$096B86NoIndependent check
Point 3$0A8A9EYes ($A8A77, $A8A85)Alternate path from Shadow Lady
Point 4$0BAD8EYes ($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 BEQ

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

FunctionAddressSizeKey Pattern
load_red_venom$022B2E66 bytesmove.w #$24, D0 + bsr.w $22CF2 (shared subroutine)
load_orange_hulk$05D04460 bytesmove.w #$24, D0 + property writes to player structure
load_gold_war_machine$07EF9012 bytesmove.w #$24, D0 + movep.l D0, $3141(A0)
load_shadow_lady$0A26C820 bytesmove.b #$24, D0 + flag setup via ori.b

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 byte

Red 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!

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:

DirectionByte Value
Up0x08
Down0x02
Left0x04
Right0x06

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 position

Palette Locations (in mvc.06a)

Spider-Man has 4 palettes (32 bytes each, 16 colors, RGB444):

PaletteOffsetPurpose
P button$48642Main punch color scheme
K button$486A2Main kick color scheme
Web (P)$48682Web attack palette
Web (K)$486E2Web attack alt palette

Armored Spider-Man Palette Design

Silver/gunmetal "Iron Spider" color scheme:

IndexSpider-ManArmoredRGB444
0TransparentTransparent$0000
1Red (main)Silver$0999
2Red (light)Light silver$0BBB
3Red (dark)Dark silver$0666
4Blue (main)Gunmetal$0444
5Blue (light)Light gunmetal$0777
8White (eyes)Yellow (visor)$0FF0
10Web (gray)Chrome$0DDD

Implementation Status

ComponentStatusEvidence
ID 0x2E analysis (85 refs)CompleteBinary scan, animation marker cross-reference
False positive filteringCompleteIDs 0x30/0x32 confirmed as MOVE.W opcode artifacts
Secret table forensicsComplete3 table types documented with hex dumps and structures
Load function decompilationCompleteAll 4 existing functions decompiled to C
Palette locationsCompleteP/K/Web palettes verified at $48642/$486A2/$48682/$486E2
ROM expansion (96MB GFX free)CompleteULTRA SETTINGS confirms 128MB GFX, 16MB Audio
In-game verificationComplete22-line diagnostic screen in F2 Test Menu
Position check discoveryBlockerGhidra + Capstone static analysis in progress
Assembly position patchPlannedAdd position 5 to hardcoded check list
Load function wiringPlannedModel after Gold WM's 12-byte pattern
Armored palette injectionPlannedRGB444 values ready
Integration testingPlannedMAME with modified ROM

Critical path: Position check discovery → assembly patch → palette injection → load function → testing.

Known Limitations

  1. Position check address not yet found -- the exact ROM address of the hardcoded position comparison list is needed to patch it
  2. Input code state machine not implemented -- the box will initially activate on a simple Down press, not the full directional sequence
  3. No sprite modifications -- initial version is palette-only; actual armor tile modifications require GFX ROM editing across all 8 interleaved chips
  4. 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
cd CHARACTER_SELECT
python3 scripts/patch_rom_check_complete.py
 
# 2. Run in MAME
mame mvscud -rompath roms -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: 05

References

  • MvC1 character select disassembly (MAME debugger + Ghidra)
  • Decompiled load functions (load_gold_war_machine.c, load_red_venom.c, etc.)
  • 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
EOF · $07 · 2,356 words · Daniel Plas Rivera
Share[X][LinkedIn]