feat(runtime): add an opt-in attract loop without the movies
Summary
Adds a Skip attract movies checkbox to the three launchers (RECOMP_ATTRACT_NO_MOVIES=1 for the runner, saved as attract_no_movies in launcher.json). With it on, a game left idle at the title alternates between the title screen and the real-time volleyball demo, and the pre-rendered movie no longer comes back between them. It is off by default, and with it off the runtime changes nothing.
- Loop: title screen, demo, title screen, demo. The original loop is title, demo, title, movie.
- Title hold: the title screen stays for 10 seconds instead of 30 before the demo starts. The 10 seconds include the game's own one second fade.
- Startup: the movie after the logos still plays once, and START skips it as before. It does not return while the game idles.
- Input: START during the demo returns to the title, and START at the title opens the main menu, as without the option.
Behavior, guest addresses and seams are in docs/attract-mode.md.
How it works
The game's attract sequence is a table of seven steps that ends in a jump back to the first title step. attract_sequence.c runs once per presented frame and changes the parameter of that jump, one byte, so the loop returns to the title step before the demo and never reaches the movie step again. It checks the seven step kinds and the jump's original target first; if the table is not the one it expects, it prints a line and leaves the sequence alone.
The movie step stays in the table because the boot path searches the table for it after the logos. A table without it hangs there, which is why the startup movie still plays.
For the shorter hold, the title step's update (0x0006B290) is wrapped through recomp_lookup_manual: at frame 540 the wrapper moves the step's frame counter to the frame where the game starts its fade, then calls the generated update. The game's own code does the fade and ends the step.
Seams: the swap adapter (per frame) and one manual lookup entry. The recipe is unchanged.
Verification
python -m unittest discover -s tools -p "test_*.py",tools/public_export.py verify --require-public-treeandverify-archive, and the runtimectestsuite pass on macOS (26 of 26, including the newattract_sequence_test.c: on synthetic guest memory, only the jump byte changes, a second pass changes nothing, a table holding another sequence is left alone along with the title's hold, and the hold moves the frame counter only between frame 540 and the fade). The tools tests ran against Capstone 5.0.7, not 5.0.9.- Scripted runs on macOS (Apple Silicon, Metal), following the sequence's step index and screenshots:
- Option on, no input for five minutes: logos, movie, title, demo, title, demo. No movie after the first.
- Option on, START during the startup movie: the title held for about 600 frames and the demo ran about 6800, twice over, with no movie frames.
- Option on: START during the demo returned to the title, and START at the title opened the main menu.
- Option off: the title held 1800 frames, then demo, title, movie, as before the change.
- The macOS and SDL launchers were built.
- Not checked: the unexpected-table path in a run of the game (it has a unit test only), the new launcher row on screen in any of the three launchers, sound (runs were muted), sessions longer than five minutes, and Windows and Linux. The PowerShell launcher change is untested. It has not been play-tested.
Draft until it has had a play test.
Touches the same launcher lines as #112 and #113; whichever lands later needs a small rebase.
Contribution checklist
- The change follows
AGENTS.mdandCONTRIBUTING.md. - No XBE, generated game C, retail instruction bytes, extracted assets or
filenames, saves, BIOS data, private paths, or run evidence are included. - Third-party material and its license are identified, if applicable.