A native C11 + raylib foundation for a Demon's Crest fan game. The original game is used as the reference: level/object data and graphics are extracted from the player's own ROM, and the recompilation project is used as a frame-accurate oracle for movement and timing.
No ROM data is distributed with this project. The
assets/generated/andcaptures/directories are git-ignored. You must own Demon's Crest (USA) and generate them locally.
Full project state, recovered technical details, traps and next steps:
docs/PROJECT_STATE.md.
Full-campaign gap analysis and remaining work:
docs/FULL_CAMPAIGN_PLAN.md.
- 256×224 render target, integer-scaled, 60 Hz update.
- Extracted full graveyard starting room from ROM Area 1 (
screens 8, 9= bottom-rowslots 9, 10), 2048×256 px across the populated 8-screen row, with the stone pillar (cx=24,x=384..399,y=128..159) beneath the urn at(385, 111). - BG1 comes from the ROM block tables for all eight screens; BG2 is the captured 64×32 forest backdrop and loops horizontally as the camera scrolls.
- Slope surface heights decoded from the
$80:962Bjump table: Firebrand walks up/down the0x07..0x0ehill, stands aty=164onattr=0x09, and the camera follows across the full room with horizontal and vertical deadzones. - Player and item sprites extracted from OAM.
- Walk, jump, hover and fall driven by the authentic Bank
$80state machine in 8.8 fixed point, with slope-aware floor collision and viewport bounds.B + Upflight is a debug-only extra (DC_FLY_DEBUG=1). - Firebrand animation set extracted from
save0(ROM Area 3,screens 52, 53): idle, 6-pose walk cycle, jump rise, hover/fly, fall, landing, 17-frame Y fireball spit and 31-frame A spell. - Bank
$80($80:DB00..F310) player state machine and constants ported:- walk speed
$1020: flat ground keeps the0x0166cap (1.398 px/frame); the0x00B3first step and$80:8B85ramp belong to the$1070terrain path and are not wired to the new slope surface table yet, - jump initial velocity
$1022 = 0x059C(5.609 px/frame;0x02CEunderwater), with immediatevy = 0cut on B release while rising ($80:E227), - gravity
$1026 = 0x0038(0.21875 px/frame²;0x002Aunderwater), terminal fall0xFA00(-6.0 px/frame), - mid-air hover (
State 0x06at$80:E1A3/$80:E29F): pressing B in air zeroes$1022(vy = 0) and bypasses gravity; pressing B again returns to falling (State 0x04). ($85:C1D4was identified as the dropped heart bounce routine, not player jump.)
- walk speed
- Fireball shot implemented end to end:
- 7-slot projectile pool (
src/projectile.c) matching$1080..$1260, - spawn on the Y press frame at
floor(player.x) + 3,floor(player.y) - 8with velocity0x0300(3.0 px/frame), - palette-6 OAM stream baked as
fireball_0..8.png+fireball_anim.json, - despawn on solid walls or camera viewport exit, mirrored for left shots.
- 7-slot projectile pool (
- Object system (
src/object.c, 26 slots) driven by the Area 1$81:C874object list:- four destructible urns (
species $57) at the listed positions, - fireball hits shatter urns and drop a heart with the
$85:C180arc (4.0 px/frame pop-up, 0.25 gravity, decreasing bounces), - touching a heart increments HP up to max,
- species
$67ghouls run their ROM state machine ($83:AF74: perched volley, hop, chase), damage on contact with knockback and drop hearts when killed by fireballs.
- four destructible urns (
- BG3 HUD extracted from
save0: six-point health bar plus three crest slots at the top of the frame; the active spell slot is highlighted. - Fixed 60 Hz simulation with a frame-time accumulator; presentation refresh
is decoupled while
DC_RUN_FRAMESremains deterministic. - Player/fireball animation frames have pre-baked mirrored textures, so the renderer no longer uses negative source rectangles.
ctest/tools/run_regression.shruns deterministic demos and asserts physics, projectile, pickup, enemy and HP invariants.- Original procedural audio: a looping chiptune theme plus jump, land, shoot, break, pickup, hurt and enemy-hit SFX (no ROM audio data).
- Vertical-slice flow: title screen, pause overlay, and the cemetery's mid-boss
gate — the Hippogriff (species
$09atx=1920) blocks the way to Area 2 until he is killed. - Full-campaign foundation:
- native ROM CHR decompressor (
$80:C2AF/$80:C300) and$98:C000/$98:D000table decoders; - all 113 valid ROM areas extracted to
room_area<N>.jsonwith 2D multi-row/vertical grids, collision, spawns, transitions and object lists; - per-area BG atlases baked from ROM CHR/palettes and swapped per room;
- OAM metasprite decoder (
$8B:8000) with real ROM item/Crest and ghoul sprites; - BG2 HDMA scanline parallax from
$84:AF65for Area 1; - per-area BG2 backdrops from oracle frames:
tools/dc_extract/bg2_captures.jsonmaps an area to acapture_anim.pyframe; the extractor bakes that frame's BG2 tilemap plus a capture-based atlas (Area 1 and Area 3 are baked today). The guest generates BG2 tilemaps at runtime ($84:A057DMAs them from WRAM$7F), so this is the interim path until the generator is decoded; - multi-room transitions, boss gates and boss fights (Area 0 Somulo, Area 1 Hippogriff);
- six Gargoyle forms with Earth dash, Air flight, Water swim, Time/Ultimate defense, Fire-Crest elements, wall cling and headbutt;
- skeleton/bat/gargoyle enemies, moving platforms, crest/Life-Stone pickups, gold, hazards, water and breakable terrain;
- Crest/Item subscreen (
Tab), shop, password/options/game-over/ending screens and an overworld map with six landing nodes (still drawn as a baked texture through a rotatingCamera2D; the original Mode 7 plane is coming).
- native ROM CHR decompressor (
tools/audit_campaign.pyreports per-room structure (BG1/collision coverage, BG2 presence, spawn support, object/transition bounds) and classifies rooms as playable / partial / empty;--strictis suitable for CI.- Save/options persistence: campaign progression (area, cleared stages, HP,
Crests/forms, Talismans, Urns, Spells, gold) and the music volume are written
to
saves/dc_save.jsonon area clear, overworld entry and exit. When a save exists the title menu gains aCONTINUEentry (andDC_RESUME=1resumes for headless tests).DC_SAVE_PATH,DC_NO_SAVEandDC_SAVE_FORCEcontrol it; deterministicDC_RUN_FRAMESruns never touch the disk. - HUD health bar uses the extracted mask with dark/red tints (missing health is dark, current HP is red) instead of the previous flat white bar.
- Native password system: 10 letters carry stages, max HP, Crests/form, Urns,
Spells and Talismans plus an 8-bit checksum, with an interactive entry screen
(up/down letter, left/right move, Enter applies, invalid codes rejected).
DC_PASSWORD=<code>applies a code at boot andDC_PASSWORD_SELFTEST=1runs 2000 round-trips for CI. The retail algorithm is still unknown. - Per-species enemy sprites:
tools/oracle/capture_species.pyinjects a minimal object record so the guest initialises and draws a chosen species in the current room (no area travel), andtools/dc_extract/extract_species.pybakes the object's OAM cluster intoenemy_<species>_<n>.png+species_anim.json. 23 of the 25 species referenced by rooms now have their own art — 261 of 269 placed objects (97%) — most of it baked straight from the ROM: the species→ metasprite mapping only exists in memory, sotools/dc_extract/ match_group_from_oam.pyrecovers it by matching the guest's live OAM against$8B:8000frame signatures andbake_rom_species.pycomposes the result withcompose_rom_metasprite. The object draw puts the metasprite origin ~19 px above the object position; the baker measures that per species. The two remaining ids are not sprite objects:$01platform and$24talisman (drawn at the player). The$09Hippogriff sheet is baked pose by pose with the VRAM/CGRAM of the frame that drew each pose (bake_hippogriff.py): the guest streams OBJ CHR per animation frame, so one capture only holds the tiles of the pose on screen. - Windows build on the raylib Windows installer (
tools/build_windows.ps1,-DDC_RAYLIB_ROOT), verified with raylib 6.2 + MinGW GCC 16.2. - Campaign-specific debug hooks:
DC_AREA,DC_CAMPAIGN,DC_BOSS_DEFEATED,DC_BOSS_HP,DC_STATE,DC_NO_HDMA,DC_UNDERWATER. - Saved-state validation from
save0(normal graveyard gameplay):- walk speed measured 1.40 px/frame,
- default SNES bindings (
$80:DB00via$1E3A..$1E4C): B = Jump/Hover, Y = Fireball shot (spawns in$1080..$1260via$80:F222), A = Talisman/Headbutt (State 0x16/0x1A), X = Subscreen, - B tap is an ~11 px / 14-frame hop; holding B gives the full jump arc,
- 603-frame
DC_DEMO=6replay (validated before the full-room terrain change): max deltax=0.00 px,y=0.53 px.
- Debug collision overlay (
F1orDC_DEBUG_COLLISION=1), FPS (F3).
This is a playable campaign foundation, not a finished game. The next milestone is content fidelity: per-species OAM/AI, BG2/mid-stage CHR, text/cutscenes, exact password/save and ROM audio.
- All 113 areas load, and 65 of them now have a real BG2 backdrop baked from
an oracle capture. The mapping from save states to extracted rooms is solved
(
area = WRAM $008D / 2, verified three ways indocs/SAVE_TO_AREA.md); the remaining rooms have no verified save yet, so their BG2 stays transparent until one is captured (the layer is generated by the guest at runtime, not stored in the ROM block tables). - A handful of rooms (notably areas 43, 42, 30 and 29) still render mostly black: the BG1 extraction only fills the screens the reference capture covers, and their atlases are complete, so the fix is a capture-based BG1 rebake, not new art.
- BG2 parallax now uses the recovered
$84:AF65HDMA bands in Area 1; other areas still need their own scanline tables. - Six Gargoyle forms are implemented with movement/abilities and palette tints, but real per-form OAM sheets and animations are still needed.
- Per-species enemy AI is ported for the
$67ghoul only; skeletons, bats and gargoyles still run the generic patrol/chase until their$82:8300handlers are ported. Their sprites are baked per species now. Note that species$10and$81are NPC/prop species, not enemies. - The
$1070probe is wired (attr & 0xE0 == 0x80) and selects the0x00B3 → 0x0166ramp, but exact per-shape oracle comparison remains. - Hurt/death player animations are still missing. The
$67ghoul now runs its ROM handler ($83:AF74), but its$80F81Danimation streams are still mapped onto the baked poses and the pacing tables are not byte-for-byte (docs/GHOUL_AI.md). HUD icons remain approximations. - No audio from the original ROM yet; the current music/SFX are original procedural development placeholders.
- Text/dialogue/cutscenes and the exact retail password algorithm are stubs; the clear/password/ending screens are functional but minimal, and the password codec is a native checksummed format rather than the retail code. Native save persistence (above) is in place, but it is not the retail SRAM format.
- The HUD health bar is scaled from the extracted six-point bar and highlights the active Crest form; the exact retail talisman-slot icons are not done.
-
CMake 3.20+
-
A C11 compiler
-
Python 3 with Pillow
-
Demon's Crest (USA), SHA-256
18d40a807d5f88c5b6a1ad849eec7e0f189225d9a1586037c850f6680b5844de -
raylib is fetched automatically at configure time. Set
-DDC_RAYLIB_ROOTto use a local raylib install instead (required for the offline Windows build). On Fedora the desktop build also needs the X11/GL development headers:
sudo dnf install libXrandr-devel libXinerama-devel libXcursor-devel \
libXi-devel libX11-devel mesa-libGL-devel alsa-lib-develUse the recompilation project's trace build. The gameplay.script route reaches
the first playable area at frame 43659 and dumps the machine 600 frames later.
RECOMP_DIR=/path/to/DemonsCrestSNESRecomp \
ROM="/path/to/Demon's Crest (USA).sfc" \
./tools/capture_graveyard.shOr run it by hand:
cd /path/to/DemonsCrestSNESRecomp
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
SNESRECOMP_DUMP_DIR=/path/to/DemonsCrestNativePort/captures/graveyard \
./build-trace/DemonsCrestSNESRecomp "/path/to/Demon's Crest (USA).sfc" \
--script scripts/routes/gameplay.scriptCopy/rename the four files the extractor needs:
captures/graveyard/stage_600.vram.bin
captures/graveyard/stage_600.cgram.bin
captures/graveyard/stage_600.oam.bin
captures/graveyard/stage_600.regs.json
The dump command writes the framebuffer too; it is useful for visual
comparison but the extractor does not need it.
python3 tools/dc_extract/extract.py \
--rom "/path/to/Demon's Crest (USA).sfc" \
--dump captures/graveyard \
--prefix stage_600 \
--area 1 \
--screens 9,10 \
--full-room \
--out assets/generated--full-room builds every screen in the captured 8-screen row (2048×256 px
for Area 1) from the ROM block tables; omit it for the old two-screen
256×224 viewport slice.
Per-area BG2 backdrops come from an optional manifest,
tools/dc_extract/bg2_captures.json:
{"areas": {"3": {"dir": "captures/anim", "frame": 20614}}}Each entry points at a capture_anim.py frame (its .json + .vram.bin).
The extractor copies that frame's BG2 tilemap into the room document, keeps the
original BG2-vs-BG1 scroll offset, and bakes the area's BG atlases from the
capture's VRAM/CGRAM so streamed tiles are covered. Use --bg2-map <path> to
point at another manifest, or --bg2-map at an empty file to disable it.
Outputs:
room.json— full-row BG1 tilemap, looping BG2 backdrop, slope collision grid, spawn, camera/play bounds.room_area<N>.json— one 2D room document per valid ROM area (113 areas), with spawns, transitions, boss gate and object list.bg_area<N>_pal<P>.png— per-area BG atlases baked from ROM CHR/palettes (palettes used by that area only).bg_pal0..7.png— 4bpp BG tile atlases for the captured Area 1 frame.obj_pal0..7.png— 4bpp OBJ tile atlases.player_idle.png— Firebrand composed from OAM.item_00.png— the visible item composed from OAM.heart_00..01.png/enemy_00..01.png— real ROM metasprite item and ghoul.campaign.json— area list, Gargoyle palettes and Area 1 HDMA bands.manifest.json— ROM identity and capture provenance.
A full extractor run writes about 57 MB and takes roughly a minute. The generated directory is git-ignored and never distributed.
The trace build exposes a 6000-frame per-frame state ring plus a TCP debug
server. capture_anim.py starts it paused, resumes a copy of save0.sav, and
drives absolute frame targets with held buttons. It then saves OAM, CGRAM,
registers and VRAM for each frame.
python3 tools/oracle/capture_anim.py \
--recomp /path/to/DemonsCrestSNESRecomp \
--rom "/path/to/Demon's Crest (USA).sfc" \
--save /path/to/DemonsCrestSNESRecomp/saves/save0.sav \
--out captures/anim
python3 tools/dc_extract/extract_anim.py \
--dump captures/anim \
--out assets/generated \
--cleanThis writes player_idle_0.png, player_walk_0..5.png,
player_jump_0..2.png, player_hover_0.png, player_fall_0..1.png,
player_land_0..1.png, player_attack_0..2.png, player_spell_0..2.png,
fireball_0..8.png, hud_bar.png, hud_slots.png, player_anim.json and
fireball_anim.json. The real item/Crest and ghoul PNGs
(heart_00..01.png, enemy_00..01.png) are baked by extract.py from ROM
CHR/OAM.
The engine falls back to player_idle.png if the player JSON is absent, and
skips the HUD if hud_bar.png/hud_slots.png are absent.
python3 tools/oracle/capture_anim.py \
--recomp /path/to/DemonsCrestSNESRecomp \
--rom "/path/to/Demon's Crest (USA).sfc" \
--save /path/to/DemonsCrestSNESRecomp/saves/save0.sav \
--segments tools/oracle/segments_save0_physics.json \
--out captures/phys
python3 tools/oracle/export_capture_csv.py captures/phys -o /tmp/oracle_phys.csv
DC_DEMO=6 DC_RUN_FRAMES=603 DC_TRACE_CSV=/tmp/native_demo6.csv \
./build/demonscrest # xvfb-run -a when headless
python3 tools/oracle/compare.py /tmp/native_demo6.csv /tmp/oracle_phys.csv \
--oracle-start 0 --threshold 1.0Expected: 603 frames compared, max delta x=0.00 px, y=0.53 px.
Linux/macOS (raylib fetched by CMake):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/demonscrestWindows (MinGW + the raylib Windows installer, offline):
powershell -NoProfile -ExecutionPolicy Bypass -File tools/build_windows.ps1
# or point CMake at any raylib tree with a prebuilt libraylib.a:
cmake -S . -B build -G "MinGW Makefiles" -DDC_RAYLIB_ROOT=C:\raylib\raylib
cmake --build build
build\demonscrest.exe --assets assets\generated-DDC_RAYLIB_ROOT skips the FetchContent download and links the local
libraylib.a; the script defaults to C:\raylib\raylib and builds serially
because some Windows shells restrict the jobserver named pipe used by
make/ninja.
Use --assets <dir> to load assets from another directory.
assets/generated/ and captures/ are ROM-derived and git-ignored, so a fresh
clone needs one extraction pass (ROM required, no assets are redistributed):
# 1. Base extraction from the ROM + a gameplay capture set
python tools/dc_extract/extract.py --rom "<rom>" --dump captures\graveyard `
--prefix stage_600 --area 1 --screens 9,10 --full-room --out assets\generated
# 2. Optional: extra areas / shop / map from recomp save states
python tools/oracle/capture_save_frames.py --save 7 --out captures\save7 --frames 0,20
python tools/oracle/capture_save_frames.py --save 8 --out captures\save8 --frames 0,20
python tools/oracle/capture_save_frames.py --save 9 --out captures\save9 --frames 0,20
python tools/oracle/capture_save_frames.py --save 10 --out captures\save10 --frames 0,20
python tools/dc_extract/build_world_map.py --capture captures\save11 --frame 61
python tools/dc_extract/build_endings.py
# 3. Verify
python tools/run_regression.py --binary build\demonscrest --assets assets\generatedtools/dc_extract/bg2_captures.json lists which areas have captured backdrops
(2, 3, 48, 58, 74); drop new captures/saveN frames there to dress more rooms.
The port is playable from the title screen through the campaign loop to the endings. A full sweep before each release:
| Check | Command | Current result |
|---|---|---|
| Regression suite | python tools/run_regression.py --binary ... --assets ... |
PASS (demos 6/7/9/10/11/12/13, boss data/runtime, Hippogriff fight, boss clears, area subset, playthrough, endings, save/password) |
| All rooms load | python tools/check_areas.py --binary ... --assets ... |
113/113 ok |
| Boss stage clears | python tools/check_campaign.py --binary ... --assets ... |
11 boss entries clear (55 rooms have no boss) |
| Campaign flow | python tools/check_playthrough.py --binary ... --assets ... |
6/6 steps (title → clear → map → stage travel → boss clear → ending) |
Known fidelity gaps (tracked in docs/PROJECT_STATE.md): many enemies still run
a generic patrol/chase rather than their $82:8300 handler, most areas have no
captured BG2 backdrop, the overworld is a camera-rotated baked texture rather
than the original Mode 7 plane, and audio is a replacement soundtrack rather
than ROM BRR/DSP data.
| Action | Keyboard | Gamepad | Original SNES ($80:DB00) |
|---|---|---|---|
| Move | Arrows / WASD | D-pad | D-pad |
| Jump / hover | Z or Space | B (right face) | B ($1E3A) |
| Fly upward (debug only) | Z + Up | B + Up | Crest-dependent |
| Fireball shot | X or J | X / Y (west/top) | Y ($1E3C) |
| Talisman / Headbutt / Earth dash | C or K | A (south) | A ($1E3E) |
| Crest/Item subscreen | Tab or I | — | X ($1E4C) |
| Cycle Gargoyle form | Q / E | L / R | — |
| Start / confirm | Enter | Start | Start |
| Pause / resume | Escape | Back | Start |
| Collision overlay | F1 | — | — |
| FPS | F3 | — | — |
These environment variables exist for deterministic testing:
| Variable | Effect |
|---|---|
DC_RUN_FRAMES=N |
Run N updates then exit (used for screenshots) |
DC_SCREENSHOT=name.png |
Save the last presented frame (raylib writes it in the working directory) |
DC_TRACE_CSV=path |
Write per-frame player/camera state as CSV |
DC_DEMO=1 |
Walk right, jump, then fly (long demo) |
DC_DEMO=2 |
Walk right into the pillar, jump and land (short demo) |
DC_DEMO=3 |
Mirrors the physics probe timing |
DC_DEMO=4 |
Tap-jump vs hold-jump comparison |
DC_DEMO=5 |
Animation regression: walk, special, jump, fly |
DC_DEMO=6 |
Exact save0 Step 3 physics replay (603 frames) |
DC_DEMO=7 |
Fireball demo: right shots into the pillar, long left shot |
DC_DEMO=8 |
Full-room traversal: walk left, then hover right across pillars |
DC_DEMO=9 |
Urn → bouncing heart → heal regression |
DC_DEMO=10 |
Enemy chase/damage/drop regression (pair with DC_START_X) |
DC_DEMO=11 |
Crest-form cycle, Earth dash, Air flight, Aerial shots |
DC_DEMO=12 |
Campaign traversal and boss-gate flow |
DC_DEMO=13 |
Title → NEW GAME → Area 0 flow |
DC_START_X / DC_START_Y |
Override the player spawn for focused tests |
DC_TRACE_OBJECTS=path |
Write active object slots as CSV |
DC_TRACE_PROJECTILES=path |
Write per-frame active projectile slots as CSV |
DC_FLY_DEBUG=1 |
Enable the non-canonical B+Up vertical flight |
DC_TITLE=1 |
Force the title screen even with DC_RUN_FRAMES |
DC_NO_AUDIO=1 |
Disable audio device/music/SFX |
DC_AUDIO_DEBUG=1 |
Print audio device/music status |
DC_AUDIO_FORCE=1 |
Allow audio in deterministic DC_RUN_FRAMES runs |
DC_DEBUG_COLLISION=1 |
Enable the F1 collision overlay in headless runs |
DC_LAYER_MASK=1|2|3 |
Draw BG2 only / BG1 only / both |
DC_AREA=N |
Load room_areaN.json instead of room.json |
DC_CAMPAIGN=1 |
Enable campaign transitions/overworld progression |
DC_STATE=name |
Render a menu state (title, overworld, subscreen, shop, password, options, clear, gameover, ending) |
DC_BOSS_DEFEATED=1 |
Skip boss spawns for campaign flow tests |
DC_BOSS_HP=N |
Override boss HP |
DC_GOLD=N |
Starting gold for shop/subscreen testing |
DC_POTIONS=N / DC_SCROLLS=N |
Starting Urn/Spell counts |
DC_UNLOCK_ALL=1 |
Unlock all six Gargoyle forms |
DC_NO_HDMA=1 |
Disable the BG2 scanline parallax bands |
DC_UNDERWATER=1 |
Force underwater physics |
DC_WINDOW_INFO=1 |
Print window/render-target size and integer scale once per frame |
DC_SAVE_PATH=path |
Native save file (default saves/dc_save.json) |
DC_NO_SAVE=1 |
Disable save loading/writing |
DC_SAVE_FORCE=1 |
Allow saving inside deterministic DC_RUN_FRAMES runs |
DC_RESUME=1 |
Boot straight into the saved run instead of the title |
DC_PASSWORD=<code> |
Apply a native 10-letter password at boot |
DC_PASSWORD_SELFTEST=1 |
Run password codec round-trips and exit (CI) |
DC_NO_STORY=1 |
Skip the crest/item story beats |
DC_ENDING=bad|normal|good|true |
Preview an ending variant |
DC_TALK=<step> |
Preview the city NPC conversation page |
DC_STORY_TEST=<crest> / DC_STORY_PAGE=n |
Open a story page directly |
DC_FIRE_TEST=<1..4> |
Open a fire-crest piece page directly |
DC_LAYER_MASK=<bits> |
Draw only selected BG layers (1 = BG2, 2 = BG1) |
python tools/dc_extract/build_world_map.py ... |
Bake the Mode 7 world map from a capture |
DC_BG2_MERGE_FRAMES=<n> |
Frames merged when baking a captured BG2 atlas |
SNESRECOMP_LAYER_MASK |
On the recomp side, isolate BG/OBJ layers for comparison |
Example:
DC_DEMO=2 DC_RUN_FRAMES=120 DC_TRACE_CSV=/tmp/native.csv ./build/demonscrestAfter building and extracting assets:
./tools/run_regression.sh
# or
cd build && ctest --output-on-failureThe harness runs the deterministic demos through DC_RUN_FRAMES, writes trace
CSVs, and asserts known movement, fireball, pickup, enemy, HP, Crest-form and
campaign-transition invariants. It also boots every boss room (check_bosses),
the species $67 ghoul state machine and the Area 1 Hippogriff fight (spawn,
260-frame stone phase, kill and the gate to Area 2), plus the console's
28+64+28-frame room fade. Save/options persistence is checked twice: one run
autosaves (with DC_SAVE_FORCE), a second run resumes it and the trace must
report the same area, max HP and gold. The password codec is exercised twice:
the binary self-test, and a Python-generated code applied via DC_PASSWORD
must restore max HP and form. On Linux the runner re-executes itself under
xvfb-run when no display is set. It exits with code 77 (CTest skip) when the
binary or generated assets are missing.
tools/audit_campaign.py checks every generated room document without running
the game: world/collision/BG dimensions, BG1 and collision coverage, BG2
presence, spawn support, object bounds and transition/gate targets. Rooms are
classified as playable, partial or empty.
python3 tools/audit_campaign.py
python3 tools/audit_campaign.py --strict --json /tmp/campaign_audit.json--strict exits non-zero when a playable room breaks a structural invariant, so
it can gate CI alongside the regression suite.
src/ native engine
src/anim.c player_anim.json / fireball_anim.json loaders
src/projectile.c 7-slot fireball pool
src/object.c 26-slot urn/enemy/pickup pool
src/audio.c procedural music/SFX
tools/dc_extract/ extraction from ROM + dump (room and animation frames)
tools/dc_extract/bg2_captures.json area -> oracle frame map for BG2 backdrops
tools/dc_extract/extract_species.py per-species sprite baking from injected
object captures
tools/dc_extract/bake_hippogriff.py per-pose bake for species $09 (VRAM at
the frame that drew each pose)
tools/oracle/capture_species.py injected-object species capture
tools/audit_campaign.py per-room structural audit
tools/build_windows.ps1 offline MinGW build against local raylib
tools/capture_graveyard.sh recomp dump helper
tools/oracle/ WRAM/OAM trace parsers, probe routes, comparison and
debug-server animation capture
captures/ per-run recomp dumps (git-ignored)
assets/generated/ extracted assets (git-ignored)
docs/PLAN_FULL_PLAYABLE.md plan to make the original fully playable (milestones M1-M9)
docs/PROJECT_STATE.md full handoff: state, findings, traps, next steps
docs/FULL_CAMPAIGN_PLAN.md full-campaign remaining-work plan
docs/ROADMAP.md next milestones
The larger feasibility study is in the recompilation repository at
docs/NATIVE_PORT_PLAN.md.
The engine and tools are original code. The ROM, extracted graphics, maps, music and any other game data remain the property of Capcom. Do not commit or distribute the generated assets. For a public release, either require the player's own ROM at generation time or replace the original art/music with licensed or original assets.