1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
#!/usr/bin/env python3
"""Build TAS-style input movies for the sl0pboy emulator's `input play` replayer.
A movie is a text file of `<frames> [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())
|