-
Notifications
You must be signed in to change notification settings - Fork 0
SwanSong Playtesting and Debugging
SwanSong is Story Forge's primary compiled-game progression target. Mesen and Mednafen remain useful independent emulators, but neither substitutes for a complete SwanSong route pass.
For a game under games/<slug>/, the normal command is:
python3 scripts/ship_wscvn_game.py <slug>The required order is:
- rebuild and audit the current project and assets;
- discover every reachable graph route and play each one in SwanSong;
- package the fresh ROM and evidence;
- verify the release ZIP against the live workspace.
--route all is the default. The route planner evaluates initial flag values,
choice conditions, choice flag operations, branch conditions, investigations,
hotspots, default exits, and endings. Instantaneous branch and chapter
nodes remain in the graph plan but are not expected as observable runtime
frames.
The playthrough report records discovered and tested route counts. Packaging fails unless those counts match.
The top-level Signal slice uses explicit paths because it predates the
games/<slug>/ layout:
python3 scripts/playtest_wscvn_swansong.py \
--name signal-before-dawn-slice \
--project projects/signal-before-dawn-slice.wscvn.json \
--rom runtime-local/signal-before-dawn-slice.wsc \
--evidence-root assets/signal-before-dawn-slice/swansong-playthrough \
--report assets/signal-before-dawn-slice/swansong-playthrough-report.json \
--route allBuild Signal immediately before this command. A ROM created before mailbox schema 2/save schema 5 cannot provide current SwanSong evidence.
By default the test runner uses:
/Applications/SwanSong.app/Contents/Frameworks/libSwanAresEngine.dylib
The runner preflights that dylib before moving old route captures. It negotiates a bounded set of public ABI versions around Story Forge's current version and records the selected ABI, attempted versions, app version, dylib hash, backend, and build ID. This allows a compatible SwanSong ABI bump without mislabeling a harness startup problem as a game bug.
For an exact diagnostic override only:
SWANSONG_ENGINE_ABI=7 python3 scripts/playtest_wscvn_swansong.py \
<slug> --route 1If no compatible session can be created, the runner writes an explicit failed playthrough report over any prior green report. Never use old route evidence after an ABI or session-creation failure.
Bind a local or release-candidate build explicitly when needed:
python3 scripts/ship_wscvn_game.py <slug> \
--swansong-dylib /path/to/SwanSong.app/Contents/Frameworks/libSwanAresEngine.dylibThe equivalent environment override is SWANSONG_ENGINE_DYLIB. Reports bind
the exact dynamic-library hash, app version/build, engine build ID, ROM hash,
and backend name.
An installed app and a newly built app can share a version number while
containing different payloads. Check the actual bundle; do not infer freshness
from CFBundleShortVersionString alone.
The Story Forge runtime exposes a read-only WVNDBG1 mailbox in internal RAM.
Schema 2 reports:
- current phase, node, text block, choice index, and transition count;
- observed keys, new keys, and accepted-action count;
- investigation cursor coordinates;
- Auto and Skip Read state;
- text-speed mode;
- music and SFX volume levels;
- whether the current node has been read.
This mailbox is test instrumentation, not a substitute for input. The runner still sends real WonderSwan button masks through SwanSong's public engine ABI and verifies that confirm actions were accepted.
The project converter uses a stable topological order so explicit jumps remain
forward in the ROM. Mailbox node values are indices in that compiled order,
not positions in the source JSON list. The playtest runner mirrors the
converter before mapping indices back to node IDs, and
scripts/selftest_wscvn_swansong_node_order.py locks that behavior. A source
expansion that moves a branch subtree must never make a scene look like an
unplanned choice in the report.
For exhaustive coverage of finished-length games, the runner first uses the
real Options UI to create a hash-recorded cartridge-RAM seed with Text Speed
set to Instant. It stages that seed for route enumeration only; branching,
buttons, audio, transitions, save states, and captures still execute in the
compiled ROM. The independent restart-persistence test begins from factory
defaults and advances through every paginated {pause} block after loading
before it claims the game progressed beyond the saved node.
For a complete --route all run it also quarantines exact old
route-N-{ending,audio,stall}.{png,wav} artifacts before recapturing them.
They move into a timestamped reports/runtime-stale/ directory; the report
records both the quarantine root and moved names. The runner never deletes
route evidence in place, and obsolete routes cannot survive a graph rewrite as
current release evidence.
scripts/playtest_wscvn_swansong.py writes:
- the planned and observed node sequence;
- every requested input and observed key mask;
- accepted-action counters and a progress trace;
- a native ending capture;
- a native SwanSong audio sample with RMS, peak, clipping, and finite-sample checks;
- every declared fade's native presented-raster luminance profile, including frame count, distinct levels, black hold, scene-swap spike analysis, and proof that fade-in recovers above the black basin;
- a stall snapshot and runtime state if progress stops;
- exact ROM and SwanSong engine facts.
Separate graph routes may legitimately converge on one final scene. Distinct final scene nodes may not converge on the same evidence: readiness rejects a matching terminal page plus visible state, and SwanSong rejects identical ending captures. Long-form scene expanders therefore keep branch-specific payoff text on the final page instead of finishing every ending with shared cadence prose.
transition: fade is only project intent. The compiled runtime must traverse
all 15 RGB444 brightness levels, disable both display layers while scene VRAM
and palettes change, restore the runtime's known SCR1|SCR2 enable state,
restore black before reenabling them, hold black for at least two presented
frames, and then fade in. Never restore from display-register readback;
SwanSong caught that value leaving the game black. This prevents both the
one-frame full-bright target flash and a black-screen fade-in.
The route runner samples SwanSong's native raster while the debug mailbox is in node-entry phase. A fade profile fails when it is shorter than 24 presented frames, exposes fewer than six observable luma levels, lacks two dark frames, contains a bright spike inside the black scene-swap basin, or ends without recovering above that basin. The source guard still requires all 15 hardware levels even when dark artwork produces fewer visually distinct whole-frame averages.
Run the synthetic regression guard directly with:
python3 scripts/selftest_wscvn_transition_continuity.pyThe first route also captures a native save state, advances the engine, restores the state, and requires an exact raster replay.
After route coverage, the runner uses the real in-game menus to:
- enable Auto and Skip Read;
- change text speed;
- lower music and SFX volume;
- save into slot 1;
- read cartridge persistence through SwanSong's ABI;
- destroy the engine;
- create a fresh engine and stage the persisted bytes;
- verify settings restoration;
- load the saved node and continue beyond it.
The runtime save schema is version 5. It stores player preferences and read history. Schema-4 save data is intentionally rejected rather than interpreted with the wrong layout.
The runtime now includes:
- Auto advance;
- Skip Read;
- Story, Slow, Normal, Fast, and Instant text speeds;
- Mute, 25%, 50%, 75%, and 100% music levels;
- Mute, 25%, 50%, 75%, and 100% SFX levels;
- a visible
A/X>,AUTO, orSKIPcontinue indicator.
Preferences and read history persist in cartridge RAM.
The SwanSong source repository builds and bundles SwanSongRouteRunner under:
SwanSong.app/Contents/Helpers/SwanSongRouteRunner
Verify the whole bundle, including the helper:
CONFIGURATION=debug Scripts/build-app.sh
Scripts/check-app-bundle.sh .build/app/SwanSong.appFor a real focus and keyboard-input regression test:
Scripts/check-player-input.sh /absolute/path/to/authorized-homebrew.wscThe script launches an isolated SwanSong instance, injects a physical X key, and requires the input log to show keyboard-active effective A input plus a changed native raster fingerprint. macOS Accessibility permission is required for the terminal or Codex host that injects the event. Exit 77 means the gate was skipped and is not a pass.
Automated debug logs use schema swan-song-input-frame-log-v2; each frame may
include gameRasterSHA256. SWAN_SONG_ENABLE_DEBUG_TOOLS=1,
SWAN_SONG_DEBUG_LOG_PATH, and SWAN_SONG_STOP_AT_FRAME support isolated UI
automation without enabling debug tools for ordinary users.
Before calling a release current:
python3 scripts/audit_wscvn_releases.py
python3 scripts/status_story_forge.py \
--no-write \
--check-index CURRENT_RELEASES.md
python3 scripts/check_build_wonderswan_vn_skill.py --require-installed-match
git diff --checkAlso verify that new scripts are present in the Story Forge or SwanSong source
repository, not only inside an installed skill, generated runtime-local/
copy, application bundle, or temporary directory. The canonical runtime change
is the repository patch under runtime-patches/; generated runtime copies are
not source of truth.
The Signal ship transaction may internally run the inventory and status tools
with --allow-pending-signal-ship. That narrow recovery mode ignores only the
previous ship report's ok/errors fields while still checking all release ZIP
paths, sizes, and hashes. It exists so a failed prior ship attempt can replace
its own stale report. After the ship report is green, always run the strict
commands above without that flag; the final doctor and CURRENT_RELEASES.md
must represent strict, non-transactional status.
The Signal ship transaction does not rebuild the finished-length game fleet.
First ship each game with ship_wscvn_game.py, or run
doctor_story_forge.py --build-games; then ship Signal. Signal performs a
read-only repository doctor/status pass against those current game releases.
Keeping the transactions independent avoids a long sequential fleet rebuild
inside a reference-slice release and makes failures attributable to the title
that produced them.