#!/usr/bin/env python3 """Build TAS-style input movies for the sl0pboy emulator's `input play` replayer. A movie is a text file of ` [buttons...]` lines: hold the given buttons (a b start select up down left right; none/'-' = released) for that many frames. Blank lines and `# comments` are ignored. The emulator drives one line's worth of joypad state per emulated frame, so replay is deterministic: ./gbctl input play /abs/demo.gbmv reset # reset + replay from boot ./gbctl record start /abs/demo.gbv 2 # (optional) capture video ... ./gbctl record stop python3 tools/gbgif.py demo.gbv demo.gif --scale 3 Use as a library to synthesize demos: from gbmovie import Movie m = Movie() m.wait(40) # let fastboot settle m.hold("select", 8) # open the SL0P menu m.wait(16) m.tap("down", repeat=13) # cursor down to the last item m.hold("a", 6) # select it m.wait(600) # watch it run m.save("demo.gbmv") Run this file directly to emit the sample demo above to stdout. """ from __future__ import annotations def _btns(buttons) -> str: if buttons is None: return "" if isinstance(buttons, str): buttons = buttons.split() return " ".join(buttons) class Movie: def __init__(self): self._lines: list[str] = [] self._frames = 0 def hold(self, buttons, frames: int): """Hold `buttons` (str/list) for `frames` frames.""" frames = int(frames) if frames <= 0: return self b = _btns(buttons) self._lines.append(f"{frames} {b}".rstrip()) self._frames += frames return self def wait(self, frames: int): """Idle (no buttons) for `frames` frames.""" return self.hold(None, frames) def tap(self, buttons, frames: int = 4, gap: int = 6, repeat: int = 1): """Press `buttons` for `frames`, release for `gap`, `repeat` times. The gap matters: the menu's low-sensitivity polling needs the button to be released between presses to register a repeat.""" for _ in range(int(repeat)): self.hold(buttons, frames) if gap > 0: self.wait(gap) return self def comment(self, text: str): self._lines.append(f"# {text}") return self @property def total_frames(self) -> int: return self._frames def render(self) -> str: return "\n".join(self._lines) + "\n" def save(self, path: str): with open(path, "w") as f: f.write(self.render()) return path def _sample() -> Movie: m = Movie() m.comment("SL0P demo: open menu -> last item (SAVER) -> watch it wander") m.wait(340) # boot splash + fastboot -> overworld (~307f) m.hold("select", 8) m.wait(16) m.tap("down", repeat=13) m.hold("a", 6) m.wait(600) return m if __name__ == "__main__": import sys m = _sample() if len(sys.argv) > 1: m.save(sys.argv[1]) print(f"wrote {sys.argv[1]} ({m.total_frames} frames)") else: sys.stdout.write(m.render())