Goal: Make Dark Sakura (MSHvSF char-id $2A) an actually playable fighter in MvC1's $2E secret slot -- normals, specials, super jump, her own animation on every frame -- not just a loaded sprite set.
Constraints: No source code. Base ROM never modified (all work lives in roms/modified/ build artifacts). Her animation tree, framedata, and 9,000+ tiles were imported -- but her recognizer code was not, and cannot be (different program ROM). Custom 64MB FBNeo core required for her tiles, which breaks Fightcade netplay -- offline only.
Approach: ARCH-1 "donor chassis": force-remap her in-fight char-id from $00 to $12 (real Ryu) at the state-machine dispatch layer, so Ryu's input detector, animator, and handler spine run her -- while a gated composer hook forces HER imported animation tree (DS_TREE at $3D0700). A 19-row (group,sub) translation table bridges the two namespaces in the specials band. A hand-assembled m68k driver replaces Ryu's special-move phase machines with hers.
Result: Attack commits (b0=$28) went from 0 in 8,400 probe frames to 117; all six normals compose her native records (live-verified). Tatsu arc rises 80px against a 78-83px native spec. v19 (today): super-jump flag found at $BA(a6) -- a one-word fix -- probe shows 368px rise on her own records and launcher super-jump-cancel; 81 missing crouch-HK tiles recovered (9,439 -> 9,520 verified).
Proof / Validation: Headless FBNeo Lua probes with scripted inputs, plus live hands-on passes as the final oracle. Every hand-assembled thunk is capstone branch-asserted at build time. Honest caveat: the v19 super jump is probe-verified only -- it is deployed but awaiting a hands-on pass.
Repo / Artifacts: CHARACTER_SELECT/analysis/dark_sakura/ (plan.md, attack_recognizer_diagnosis.md, design_ground_truth.md, ds_specdrv_asm.py, ds_specdrv_stage1.bin), CHARACTER_SELECT/scripts/build_dark_sakura.py (build chain), headless probe harnesses (ds_atk2e.lua, ds_movemap_verify.lua, ds_jumpv.lua)
The other road got walked, and it won. The native port -- $52 = $2E kept end to end, no donor -- shipped on July 17 (One ROM, Two Truths), and the native lineage has since fully superseded the donor chassis: both secret characters (Dark Sakura $2E and Shuma-Gorath $30) now fight on their own relocated MSHvSF code and animation trees, hardened by 17 live-gated fixes, with their names on both the select screen and the lifebar. The donor-era critical path below (authentic tatsu cells, Shououken, Haru Ichiban, Hunter chains) was overtaken by that pivot -- those items never shipped on this lineage and are not claimed to have. Read this post as the historical record of the first playable Dark Sakura, and of what a donor architecture can and cannot buy you.
This is the payoff post for the $2E slot. The binary forensics that found the slot, the state-machine injection techniques, and the 64MB GFX expansion that broke the sprite wall all fund this: a full cross-game character port, executed and fighting. The earlier posts' "Status update" callouts said the pivot was "table porting plus tile bank remapping." That turned out to be the easy half. This post is about the other half: making the ported data fight.
The Pivot: Why the $2E Slot Got a New Tenant
The Armored Spider-Man / Iron Spider line was retired in early July. It was always a palette-and-flag variant of Spider-Man -- real forensics, real injection work, but ultimately a recolor with an armor bit. Meanwhile, two facts had landed that made something much more ambitious possible:
- MSHvSF and MvC1 are same-generation engines. Dark Sakura's framedata is byte-identical between the two games -- all 759 mode-2 animation blocks, pixel-proven. Her data doesn't need conversion; it needs relocation.
- The custom 64MB core broke the 32MB sprite wall. Her tile set (9,439 tiles at the time, now 9,520 -- more on that below) seats into the upper GFX region with zero remapping.
So the slot's identity changed: $2E now selects Dark Sakura, ported whole from MSHvSF where she is char-id $2A. The Iron Spider machinery -- the secret-code handler, the committed-slot guards, the select-screen wiring -- was all reusable.
By the start of this week she loaded, animated, walked, jumped, and took hits, all on her own imported records.
She could not attack.
The Diagnosis: A Recognizer That Never Fires
An 8,400-frame instrumented capture (atk2e.log) with scripted six-button, QCF, DP, crouch, and jump inputs settled it:
| Signal | Expected | Observed |
|---|---|---|
b0 state byte reaches $28 (attack) | On every button press | Never (grep -c ',28,' = 0) |
b7 (commit priority) | Nonzero on attack | $00 every frame |
a7 (move id) | Nonzero on attack | $00 every frame |
b0 values seen | Full state set | {00 02 04 06 08 0A 1E 24 36} -- movement, hit, jump, crouch only |
Her attack recognizer was 100% dead. The root cause is an architecture lesson: the port had imported her data (tree, framedata, tiles) but not her recognizer code. Her in-fight char-id $52 was $00, and the engine's $52-keyed detector table at $18712 resolves entry [$00] to $02848A:
02848A: moveq #0,d0
rtsA stub. Char $00 is the dummy slot. Every button press in that 8,400-frame log walked into two instructions and returned. This is the same class of finding as the original $2E forensics -- not broken code, absent code -- except this time I was the one who left it absent.
The Failed Approach: Reanimate In Place
The first fix attempt kept $52=$00 and tried to hook the animator dispatch to give her attack animations directly -- reanimate her in place. It failed twice, in instructive ways:
- The first hook pointed the dispatch at
$068E5A. That is Shadow Lady's animator -- not a shoto, wrong move structure -- and it crashed. - The deeper refutation came from static analysis:
b0=$28(the attack state) is committed only by the shared input processor ($0181C8->$018542), which is reached from theb0state-handler spine -- never from the animator dispatch. Ryu's animator at$044290contains zeromove.b #$28,$b0instructions. No animator hook, however clever, can make a character attack. The approach was structurally dead, not merely buggy.
The conclusion: attacking is not an animation. It's a decision, and the decision-maker is selected by $52. If her decision-maker doesn't exist in this ROM, borrow one that does.
ARCH-1: The Donor Chassis
The architecture that shipped -- and instantly fixed "she can't attack" -- is a force-remap at the state-machine dispatch layer. Two existing thunks (the remap thunk and the crash-guard thunk) each gained one instruction:
move.w #$12,$52(a6) ; in-fight char-id := REAL RYUHer committed select-id stays $2E (that's her identity, palette, portrait, tile bank). But every frame, the state machine dispatches her as $12 -- Ryu. That means Ryu's entire behavioral spine runs her, consistently:
| Layer | Table | Entry for $12 |
|---|---|---|
| Input detector | $18712[$52] | $0490C0 (Ryu's button/motion decoder) |
| Animator dispatch | $18DF0[$52] | $044290 |
b0=$28 attack handler | $E6B8[$52] | $0463C0 |
And one hook keeps her hers: the composer at $135E6 -- the single point where every (group, sub) animation request resolves to a record pointer -- is intercepted by a gated thunk (keyed on her $276 identity flag) that forces the tree base to DS_TREE at $3D0700: her imported animation tree, feeding her 9,520 tiles in the 64MB core's upper GFX region.
One character's brain, another character's body, stitched at exactly two seams: the char-id word and the tree base pointer.
The first probe on the ARCH-1 build told the story in one number: b0=$28 fired 117 times where the previous build managed zero. All six normals composed native groups into her tree, records resolved and advanced, she recovered to idle, and there were zero crashes across the full run. The live pass confirmed it -- normals come out, crouch is stable, hit recovery works. Here is the donor build running live:
Two long-standing user-facing bugs (a down-input that randomly triggered super jumps, and an infinite roll loop after eating a combo) vanished without a dedicated fix: they were both artifacts of the dead char-$00 recovery path, and Ryu's native recovery spine ($16FBA -> $1797A) simply doesn't have them.
DS_MOVEMAP: 19 Rows Between Two Namespaces
The donor chassis raised an obvious question: when Ryu's handler emits an animation request, does it mean the same thing in her tree?
For normals, the answer turned out to be yes -- completely. Her tree uses Ryu's exact namespace (group = stance + punch/kick class, sub = strength x2). An earlier analysis claiming an 18-cell normals mismatch was a logging artifact; there is no normals translation, ever, and building one would have broken working attacks. This is worth stating plainly because I nearly built it.
The real mismatch is confined to the specials band (groups $2A/$2C/$2E), where the two characters' directories genuinely differ in size and order -- Ryu emits 14 subs into a directory where she has 4, which is not just wrong animation but a read-past-the-directory crash surface.
DS_MOVEMAP at $23C820 is the bridge: a 19-row (group,sub) -> (group,sub) table plus 0x74 bytes of code, hooked at the composer $135E6 ahead of the tree-override thunk (the two hooks chain; the displaced instruction pair replays first with raw registers so the engine's compose-once guards still match). It translates registers only, gated on the fighter bounds check, the $276 identity flag, and group membership in the specials band. Highlights:
- F+MK -> her flower kick record (
$3D185C) - Ryu's fireball animation emissions -> her fireball entry chain (
$3D4B8C, all three strengths) - Air kicks -> her air records (j.MK
$3D1ECC, j.HK$3D1F70-- the latter was previously a garbage pose read 8 bytes past her directory) - Overrun clamps close the crash surface:
(2A, sub >= $06)->(2A, 0)and(2E, sub >= $24)->(2E, 0)
Build-time tree-sanity asserts (her directory words vs. the native dump) stand guard against the table drifting from reality.
DS_SPECDRV: A Hand-Assembled Special-Move Driver
Translation makes the specials look right. It does not make them move right -- Ryu's tatsu physics on her animation is still Ryu's tatsu. Authentic special moves need her phase machines, and those don't exist in this ROM either.
DS_SPECDRV at $23E000 is a 708-byte hand-assembled m68k driver (assembler in ds_specdrv_asm.py, blob sha-asserted by the build, zero absolute self-references so it's relocatable). It mounts on the b0=$2A special-move handler entry at $47008:
- A
$276-gated dispatcher indexes a pc-relative table by the detector's move index (E6), routing owned moves to owned handlers; everything else falls through to the stock path, byte-preserved. - Each handler is a phase machine driving pure MvC primitives: compose a record, set Y-velocity/gravity/X-velocity directly, count ticks, advance phase, exit through the engine's own clean-recovery routine.
- Her records carry events as parameter words (
$FFxx-class latches read into$3C(a6)) -- the same mechanism the engine uses natively -- which the driver and stock code consume for phase termination, projectile spawn, and voice triggers. Her DP hit-sparks are driven by per-record attack-ids$33-$38(per strength), Ryu-faithful.
The first shipped handler is her tatsu (Shunpuu Kyaku): launch Yvel -$62000, gravity $B000, per-strength kick/hold/re-kick duration rows. The headless gate test measured an 80px rise against a 78-83px native capture spec, with a mid-move hit-yank resolving to clean hitstun and stock characters' tatsus byte-normal through the mounted dispatcher. The driver is live; the cells it composes are not yet her authentic tatsu cells -- see the catalog below.
The Wrong-Premise Catalog
Every port of this depth accumulates confidently-held wrong beliefs. These are the four that cost the most, documented because the debugging shape -- premise, symptom, disproof -- is the transferable part.
| # | Premise | Reality | Symptom it caused |
|---|---|---|---|
| 1 | "The super-jump guard hook is inert under the donor" | Its hook point $27B10 is the shared super-jump phase machine -- spine-reached for every character | She could not jump vertically at all; only dash-then-jump worked |
| 2 | "$EB is the super-jump flag" | Ryu's $E8/$EB callbacks are plain rts -- that flag can never be set. The real flag is $BA(a6) | Her super-jump physics data sat parked and dormant |
| 3 | "The (1C,xx) band is her tatsu" | (1C,xx) is her hit-reaction/juggle band; real tatsu lives at (2C,$20/$22/$24) with per-strength physics | The tatsu driver composed her getting-hit poses, in a spin |
| 4 | "Walk cells are at tree+0x42/+0x46" | Those are sub[8]/sub[C] -- cells the engine never reads for walking. The composer's actual pointer math reads sub[4] (+0x3E fwd) and sub[6] (+0x40 back) | Forward walk played the guard-walk animation; back walk slid on an idle pose |
Premise 1 deserves the full story. The guard hook was written months ago to heal a crash: under the old char-$00 donor, a super-jump command entered a dispatch path with no handler and died, so the hook rewrote b0=$0A back to a neutral jump. Under ARCH-1 that crash is impossible -- Ryu has a real super-jump handler -- but the hook was still installed, and a review had labeled it "inert." It was not. Every real-world vertical jump (up after crouching or blocking = down-then-up = a correctly parsed super-jump command) was being healed into a grounded, zero-velocity state that landed instantly. The physics probe showed clean-tap jumps launching perfectly while the user's jumps died, which localized it in one session. The fix was deletion.
The lesson generalized: every guard written for the old donor is a suspect until breakpoint-proven live or dead under the new one. "Inert" claims need a breakpoint hit count per site, not an argument.
Premise 2 is the reason today's build exists, so it gets its own section.
v19: A One-Word Fix and an 81-Tile Hunt
Once premise 2 fell -- the super-jump flag is $BA(a6), not $EB -- the fix was a single word in the jump thunk: test the byte that actually gets set. That's the whole patch. Deployed today as v19.
The headless probe on v19 shows the full super-jump pipeline alive on her own records:
- Down-up input fires the state chain
b0$0A->$0E->$1A(prep -> launch -> airborne) $BAreads 1 at launch; launch Y-velocity is -17.0 px/frame- Measured rise: 368px -- genuine super-jump height, on her records
- The launcher (c.HP) super-jump-cancels on hit, which unlocks her air combos -- the launcher -> air-chain flow that her design docs specify and that had been dead end-to-end
v19 is deployed and every number above comes from the headless probe. It has not had a hands-on pass yet. Under this project's rules that means it is not "working" -- it is promising. The probe cannot feel input timing under real play, and premise 1 in the catalog above is a standing reminder that jump bugs specifically have survived probes that only tested clean scripted taps. This paragraph gets rewritten only after a live pass.
v19 also closed a data gap: her crouching HK had been rendering with holes because 81 tiles were missing from the imported set. The hunt traced the gap to branches of her tree that were unreachable under the old donor (nothing ever composed crouch-HK, so nothing ever exposed the missing art) and became reachable the moment Ryu's machine started emitting the full move set. The tiles were extracted, decoded, and merged: the verified inventory went from 9,439 to 9,520 tiles. The count in the earlier expansion post should be read with this correction.
Verification Discipline
The methodology, stated once because everything above depends on it:
- Headless probes first. FBNeo Lua harnesses navigate the secret code, script inputs, and log state bytes, record pointers, and physics per frame. Probes answer "does the mechanism fire" with numbers.
- A human as the live oracle. Probes cannot see "looks like her move" or feel input reliability. Nothing is called working until a hands-on pass says so -- and this post marks exactly which claims have had one.
- Capstone branch-asserts on every thunk. Each hand-assembled patch is disassembled at build time and its branch targets verified before it ships. Hook sites carry expected-bytes asserts so an engine byte that moved fails the build instead of corrupting a fight.
- The base ROM is never modified. All work composes into
roms/modified/artifacts from a pristine source; every patch is expect-asserted and reversible by rebuild. - The trade-off stays stated: her tiles live above 32MB, so she requires the custom 64MB core -- which breaks Fightcade netplay. This is an offline character until that story changes.
Implementation Status
| Component | Status | Evidence |
|---|---|---|
| Data port (tree, framedata, tiles) | Complete | 759/759 mode-2 blocks byte-identical; 9,520 tiles verified in upper GFX |
| ARCH-1 donor chassis | Complete, live-verified | b0=$28 0 -> 117; six normals on her records; user pass confirmed |
| DS_MOVEMAP (19 rows) | Complete, live-verified | F+MK flower kick, fireball chain, air kicks; crash surface clamped |
| DS_SPECDRV driver + mount | Live | Mount transparent for stock characters; tatsu arc 80px vs 78-83px spec |
| Authentic tatsu cells | In progress | Driver composes provisional cells; real band is (2C,$20/$22/$24) |
v19 super jump ($BA flag) | Probe-verified, awaiting live pass | $0A->$0E->$1A, 368px rise, launcher SJ-cancel |
| Shououken (running DP) run-phase | Planned | Phase spec drafted; native capture in hand |
| Haru Ichiban (QCB+KK super) | Planned | Phase table captured from native game |
| Midare Zakura (QCF+PP super) | Planned | Needs its own native capture |
| Six-button "Hunter" chain system | Planned | Target = her chain-permission row in the move-id matrix family |
| Push block | Trace in flight | MSHvSF trigger = 3P while blocking; MvC-side trigger under trace |
Critical path: v19 live pass -> authentic tatsu cells -> Shououken run-phase -> Haru Ichiban -> Hunter chains.
Known Limitations
- v19 super jump has no hands-on pass yet -- probe numbers only; explicitly not claimed as working.
- Specials are hers in shape, not yet fully in cell -- the tatsu driver runs her arc physics but composes provisional cells until the
(2C,$20/$22/$24)rewrite lands. - Supers are not wired -- Haru Ichiban's phase table is captured but not built; Midare Zakura needs capture; the Shinkuu beam needs detector work (Ryu has no QCB+PP detector).
- Chains and cancels are still Ryu's -- her six-button Hunter chain (ground and air, buttons skippable) is a documented port target, not yet shipped.
- Offline only -- the 64MB core is a custom build; Fightcade netplay requires the stock core.
- No ROMs are distributed -- this is a documentation of technique against legally sourced dumps; the build chain patches a ROM you already own.
How to Validate
# Build the modified ROM (base ROM untouched; artifact lands in roms/modified/)
cd CHARACTER_SELECT
python3 scripts/build_dark_sakura.py
# Build fails loudly if any hook site's expected bytes or any thunk's
# capstone branch-assert doesn't match -- that's the point.
# Headless probes (FBNeo Lua harnesses; scripted secret-code nav + inputs):
# ds_atk2e.lua does b0 reach $28? do normals compose into DS_TREE ($3D0xxx)?
# ds_movemap_verify.lua specials-band translation: F+MK -> $3D185C, QCF+P -> $3D4B8C
# ds_jumpv.lua jump physics: down-up -> b0 $0A/$0E/$1A, $BA=1, rise in px
# Live pass: select her ($2E secret code), play.
# The probe proves mechanisms; only hands prove the character.Open Research: Two More Ports on the Bench
The donor-chassis result reframes what "porting a character" means: if the target engine's spine can run the behavior and the data can be relocated, the character doesn't need to originate in a sibling game -- or in an arcade game at all. Two feasibility threads are now active. Both are open research: analysis started, no conclusions yet, and nothing below is a result.
MUGEN Super Mario -> MvC. Two candidate source builds are under analysis: the ShinRyoga & NeOaNkH classic (2006) and ZoneSeven's CvS-styled build (2026). The interesting property is that MUGEN characters are plaintext: .air files are animation scripts, .cns files are state-machine definitions. There is no binary forensics step -- the whole problem is a compiler: translate declarative animation/state source into CPS-2 tree records, framedata blocks, and (for behavior) chassis-driver phase machines. Whether the semantics gap is bridgeable at acceptable fidelity is exactly the open question.
SF3 Third Strike Ken -> MvC. The opposite profile: no plaintext anywhere, but three unusually strong inputs -- CPS-3 SIMM graphics whose decode is a known quantity, two decades of community frame data for 3S Ken (startup/active/recovery for every move, documented to the frame), and a Ryu chassis in MvC that is behaviorally adjacent. The open questions are the CPS-3 -> CPS-2 tile/format distance and whether authored framedata (built from community tables rather than extracted from a sibling engine) can hit the same fidelity bar the byte-identical Dark Sakura port set.
If either lands, it gets its own post with its own evidence strip. If either dies, the post-mortem goes in the wrong-premise catalog tradition instead.
References
- Binary Forensics: Recovering an Unreleased Character -- the
$2Eslot discovery this work inherits - State-Machine Injection -- the dispatch-layer techniques the remap thunks build on
- CPS-2 Maximum ROM Expansion -- the 64MB core whose upper GFX region holds her 9,520 tiles
CHARACTER_SELECT/analysis/dark_sakura/attack_recognizer_diagnosis.md-- the dead-recognizer diagnosis and ARCH-1/ARCH-2 adjudicationCHARACTER_SELECT/analysis/dark_sakura/design_ground_truth.md-- period-accurate movelist and chain-system spec from 1998 arcade FAQsCHARACTER_SELECT/analysis/dark_sakura/ds_specdrv_asm.py-- the DS_SPECDRV assembler and blob- Detector/animator/handler dispatch tables:
$18712,$18DF0,$E6B8(all keyed by in-fight char-id$52) - Composer at
$135E6; DS_TREE at$3D0700; DS_MOVEMAP at$23C820; DS_SPECDRV at$23E000 - 68000 Programmer's Reference Manual (Motorola)