vtskit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vtskit/lang.py ADDED
@@ -0,0 +1,648 @@
1
+ """dvdauthor's command language, compiled to the DVD virtual machine's
2
+ commands (vtskit.vm).
3
+
4
+ dvdauthor describes a disc's navigation in a small language (its manual
5
+ documents it): `jump title 2;`, `g1 = 0; call menu entry root;`,
6
+ `if (g5 != 0) { button = g5; } else { button = 1024; }`. A tool that
7
+ already speaks it, as oncemore does, can keep its text and hand it here. A
8
+ `Disc` knows the disc's titles and menus (the numbers a jump resolves to
9
+ depend on where the command runs: a title's number inside its titleset or
10
+ on the whole disc, a menu's program chain number) and compiles each
11
+ command list for the domain it runs in: "fp" (the first-play chain),
12
+ "vmgm" (the video manager's menus), "vtsm" (a titleset's menus) or "vts"
13
+ (a titleset's titles).
14
+
15
+ What a form compiles to follows dvdauthor's output for the same text
16
+ (tests/test_lang.py reads the commands back from discs dvdauthor
17
+ authored):
18
+
19
+ * in a titleset's menus `jump title N` is JumpVTS_TT, `jump title N
20
+ chapter C` JumpVTS_PTT, `jump menu N` LinkPGCN; in the video manager's
21
+ menus and the first-play chain `jump title N` is JumpTT (the title's
22
+ number on the disc) and `jump menu N` JumpSS to that program chain;
23
+ `jump vmgm menu entry title` is JumpSS to the entry menu, `call menu
24
+ entry root` CallSS with the first cell to resume at;
25
+ * `audio = 1; subtitle = 65;` next to each other are one SetSTN; `g1 = 0;
26
+ jump menu 2;` is one command (vtskit.vm folds a Set and a Link), and
27
+ what follows an unconditional jump in a block is left out;
28
+ * a menu's button runs one command: a button whose text compiles to more
29
+ sets GPRM 15 to its number and links to the end of the program chain,
30
+ whose post commands start with a dispatcher (`g14 = g15; g15 = 0;` and a
31
+ Goto per such button) and end with each button's code;
32
+ * a jump into another titleset (dvdauthor's "jumppad") cannot be one
33
+ command: from the video manager a jump to a chapter sets GPRM 15 to a
34
+ code naming the title and chapter and jumps to the titleset's chapter
35
+ (PTT) menu, whose commands (`vts_pad`) decode it and JumpVTS_PTT; from a
36
+ titleset's menus or titles it goes through a program chain of the video
37
+ manager's menus (`vmgm_pads`) that does the same, or JumpTT for a whole
38
+ title (a menu jumps to that chain, a title calls it). GPRM 13 and 14 are
39
+ the pads' scratch, as dvdauthor keeps them; a disc using this language
40
+ leaves GPRM 13 to 15 alone.
41
+ """
42
+
43
+ import re
44
+ from dataclasses import dataclass, field
45
+
46
+ from . import vm
47
+ from .errors import VtsError
48
+
49
+ CODE_REG, SCRATCH, SCRATCH2 = 15, 14, 13
50
+ PAD_TARGETS = 30 # cross-titleset targets per video manager pad chain (128 commands)
51
+
52
+ _TOKEN = re.compile(r"\s*(?:(\d+)|([A-Za-z_]\w*)|(==|!=|<=|>=|\+=|-=|\*=|/=|%=|&=|\|=|\^=|[-+*/%&|^<>=(){};,]))")
53
+ _COMPARE = {"==": vm.EQ, "eq": vm.EQ, "!=": vm.NE, "ne": vm.NE, ">=": vm.GE, "ge": vm.GE,
54
+ ">": vm.GT, "gt": vm.GT, "<=": vm.LE, "le": vm.LE, "<": vm.LT, "lt": vm.LT}
55
+ _MIRROR = {vm.EQ: vm.EQ, vm.NE: vm.NE, vm.GE: vm.LE, vm.LE: vm.GE, vm.GT: vm.LT, vm.LT: vm.GT}
56
+ _ARITH = {"+": vm.ADD, "-": vm.SUB, "*": vm.MUL, "/": vm.DIV, "%": vm.MOD, "&": vm.AND,
57
+ "|": vm.OR, "^": vm.XOR}
58
+ _SYSTEM = {"audio": 1, "subtitle": 2, "angle": 3, "button": 8}
59
+ _LINKS = {("next", "pgc"): vm.NEXT_PGC, ("prev", "pgc"): vm.PREV_PGC, ("pgc", "top"): vm.TOP_PGC,
60
+ ("pgc", "tail"): vm.TAIL_PGC, ("up", "pgc"): vm.GOUP_PGC,
61
+ ("next", "program"): vm.NEXT_PG, ("prev", "program"): vm.PREV_PG,
62
+ ("program", "top"): vm.TOP_PG, ("next", "cell"): vm.NEXT_CELL,
63
+ ("prev", "cell"): vm.PREV_CELL, ("cell", "top"): vm.TOP_CELL}
64
+ _ENTRIES = {"title": vm.MENU_TITLE, "root": vm.MENU_ROOT, "subtitle": vm.MENU_SUBPICTURE,
65
+ "audio": vm.MENU_AUDIO, "angle": vm.MENU_ANGLE, "ptt": vm.MENU_PTT}
66
+
67
+
68
+ # --- the language -------------------------------------------------------------
69
+
70
+ def tokens(text):
71
+ out, pos = [], 0
72
+ text = text.strip()
73
+ while pos < len(text):
74
+ m = _TOKEN.match(text, pos)
75
+ if not m or m.end() == pos:
76
+ raise VtsError(f"cannot read the command at {text[pos:pos + 20]!r}")
77
+ pos = m.end()
78
+ if m.group(1):
79
+ out.append(int(m.group(1)))
80
+ else:
81
+ out.append(m.group(2) or m.group(3))
82
+ return out
83
+
84
+
85
+ @dataclass
86
+ class Reg:
87
+ code: int # a vm.Register
88
+
89
+
90
+ @dataclass
91
+ class Num:
92
+ value: int
93
+
94
+
95
+ @dataclass
96
+ class Random:
97
+ value: int
98
+
99
+
100
+ @dataclass
101
+ class Arith:
102
+ left: object
103
+ op: str
104
+ right: object
105
+
106
+
107
+ @dataclass
108
+ class Assign:
109
+ target: object # ("g", n) or a system register's name
110
+ op: str # "=", "+=", ...
111
+ value: object
112
+
113
+
114
+ @dataclass
115
+ class Compare:
116
+ left: object
117
+ op: int
118
+ right: object
119
+
120
+
121
+ @dataclass
122
+ class Junction:
123
+ kind: str # "and" / "or"
124
+ parts: list
125
+
126
+
127
+ @dataclass
128
+ class If:
129
+ condition: object
130
+ then: list
131
+ orelse: list
132
+
133
+
134
+ @dataclass
135
+ class Jump:
136
+ words: list # the words after "jump" or "call", numbers as ints
137
+
138
+
139
+ @dataclass
140
+ class Call:
141
+ words: list
142
+ resume: int
143
+
144
+
145
+ @dataclass
146
+ class Word:
147
+ name: str # "resume", "exit", "break"
148
+
149
+
150
+ class _Parser:
151
+ def __init__(self, text):
152
+ self.t = tokens(text)
153
+ self.i = 0
154
+
155
+ def peek(self, k=0):
156
+ return self.t[self.i + k] if self.i + k < len(self.t) else None
157
+
158
+ def take(self, want=None):
159
+ tok = self.peek()
160
+ if tok is None or (want is not None and tok != want):
161
+ raise VtsError(f"expected {want!r}, found {tok!r}" if want else "the command ends early")
162
+ self.i += 1
163
+ return tok
164
+
165
+ def program(self):
166
+ out = []
167
+ while self.peek() is not None:
168
+ out.append(self.statement())
169
+ return out
170
+
171
+ def block(self):
172
+ if self.peek() == "{":
173
+ self.take("{")
174
+ out = []
175
+ while self.peek() != "}":
176
+ out.append(self.statement())
177
+ self.take("}")
178
+ return out
179
+ return [self.statement()]
180
+
181
+ def statement(self):
182
+ tok = self.take()
183
+ if tok == "if":
184
+ self.take("(")
185
+ cond = self.condition()
186
+ self.take(")")
187
+ then = self.block()
188
+ orelse = []
189
+ if self.peek() == "else":
190
+ self.take("else")
191
+ orelse = self.block()
192
+ return If(cond, then, orelse)
193
+ if tok in ("jump", "call"):
194
+ words = []
195
+ while self.peek() not in (";", None):
196
+ words.append(self.take())
197
+ self.take(";")
198
+ if tok == "call":
199
+ resume = 1
200
+ if len(words) >= 2 and words[-2] == "resume":
201
+ resume, words = words[-1], words[:-2]
202
+ return Call(words, resume)
203
+ return Jump(words)
204
+ if tok in ("resume", "exit", "break"):
205
+ self.take(";")
206
+ return Word(tok)
207
+ if isinstance(tok, str) and (tok in _SYSTEM or re.fullmatch(r"g\d+", tok)):
208
+ target = ("g", int(tok[1:])) if tok[0] == "g" and tok not in _SYSTEM else tok
209
+ op = self.take()
210
+ if op not in ("=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^="):
211
+ raise VtsError(f"{op!r} is not an assignment")
212
+ value = self.expression()
213
+ self.take(";")
214
+ return Assign(target, op, value)
215
+ raise VtsError(f"cannot read the statement starting {tok!r}")
216
+
217
+ def condition(self):
218
+ parts = [self.conjunction()]
219
+ while self.peek() in ("or", "||"):
220
+ self.take()
221
+ parts.append(self.conjunction())
222
+ return parts[0] if len(parts) == 1 else Junction("or", parts)
223
+
224
+ def conjunction(self):
225
+ parts = [self.comparison()]
226
+ while self.peek() in ("and", "&&"):
227
+ self.take()
228
+ parts.append(self.comparison())
229
+ return parts[0] if len(parts) == 1 else Junction("and", parts)
230
+
231
+ def comparison(self):
232
+ if self.peek() == "(":
233
+ self.take("(")
234
+ c = self.condition()
235
+ self.take(")")
236
+ return c
237
+ left = self.expression()
238
+ op = self.take()
239
+ if op not in _COMPARE:
240
+ raise VtsError(f"{op!r} is not a comparison")
241
+ right = self.expression()
242
+ return Compare(left, _COMPARE[op], right)
243
+
244
+ def expression(self):
245
+ left = self.term()
246
+ while self.peek() in _ARITH:
247
+ op = self.take()
248
+ left = Arith(left, op, self.term())
249
+ return left
250
+
251
+ def term(self):
252
+ tok = self.take()
253
+ if isinstance(tok, int):
254
+ return Num(tok)
255
+ if tok == "random":
256
+ self.take("(")
257
+ n = self.take()
258
+ self.take(")")
259
+ return Random(n)
260
+ if tok in _SYSTEM:
261
+ return Reg(vm.SPRM(_SYSTEM[tok]))
262
+ m = re.fullmatch(r"([gs])(\d+)", tok or "")
263
+ if m:
264
+ return Reg(vm.GPRM(int(m.group(2))) if m.group(1) == "g" else vm.SPRM(int(m.group(2))))
265
+ raise VtsError(f"{tok!r} is not a value")
266
+
267
+
268
+ def parse(text):
269
+ """The statements of a command text."""
270
+ return _Parser(text).program()
271
+
272
+
273
+ # --- the disc ----------------------------------------------------------------
274
+
275
+ @dataclass
276
+ class Menus:
277
+ """A menu domain: how many program chains its own menus take, in
278
+ order, and the number of each entry menu ("title", "root", "ptt",
279
+ ...)."""
280
+ count: int = 0
281
+ entries: dict = field(default_factory=dict)
282
+
283
+
284
+ class Disc:
285
+ """The disc a command text is compiled for: `titles` {(titleset, title
286
+ number in it): title number on the disc}, `vmgm` the video manager's
287
+ Menus, `titlesets` {titleset: Menus}."""
288
+
289
+ def __init__(self, titles, vmgm=None, titlesets=None):
290
+ self.titles = dict(titles)
291
+ self.by_number = {n: key for key, n in self.titles.items()}
292
+ self.vmgm = vmgm or Menus()
293
+ self.titlesets = dict(titlesets or {})
294
+ self.vmgm_targets = [] # (titleset, title, chapter) reached through the VMGM pads
295
+ self.vts_targets = {} # titleset -> {(title, chapter)} decoded by its PTT pad
296
+
297
+ # -- resolving --------------------------------------------------------------
298
+ def _menus(self, vts):
299
+ if vts not in self.titlesets:
300
+ raise VtsError(f"titleset {vts} is not on the disc")
301
+ return self.titlesets[vts]
302
+
303
+ def _global(self, vts, ttn):
304
+ if (vts, ttn) not in self.titles:
305
+ raise VtsError(f"title {ttn} of titleset {vts} is not on the disc")
306
+ return self.titles[(vts, ttn)]
307
+
308
+ def _entry(self, menus, kind, where):
309
+ if kind not in _ENTRIES:
310
+ raise VtsError(f"{kind!r} is not a menu entry")
311
+ if kind not in menus.entries:
312
+ raise VtsError(f"{where} has no {kind} menu")
313
+ return menus.entries[kind]
314
+
315
+ def _vts_code(self, vts, ttn, pttn):
316
+ self.vts_targets.setdefault(vts, set()).add((ttn, pttn))
317
+ return 0x8000 | ttn << 8 | pttn
318
+
319
+ def _vmgm_pad(self, target):
320
+ """(code, PGC number) of the video manager pad that reaches `target`."""
321
+ if target not in self.vmgm_targets:
322
+ self.vmgm_targets.append(target)
323
+ k = self.vmgm_targets.index(target)
324
+ return k % PAD_TARGETS + 1, self.vmgm.count + 1 + k // PAD_TARGETS
325
+
326
+ def _to_title(self, vts, ttn, pttn, domain, here):
327
+ """Instructions jumping to chapter `pttn` (0: the start) of title
328
+ `ttn` of titleset `vts`, from `domain` (in titleset `here`)."""
329
+ if domain in ("vts", "vtsm"):
330
+ if vts == here:
331
+ return [vm.jump_vts_ptt(ttn, pttn) if pttn else vm.jump_vts_tt(ttn)]
332
+ # another titleset: through a pad chain of the video manager (a
333
+ # title cannot JumpSS: it calls the chain, which jumps on)
334
+ self._global(vts, ttn)
335
+ code, pgcn = self._vmgm_pad((vts, ttn, pttn))
336
+ go = vm.jump_ss_vmgm_pgc(pgcn) if domain == "vtsm" else vm.call_ss_vmgm_pgc(pgcn)
337
+ return [vm.set_gprm(CODE_REG, vm.MOV, code), go]
338
+ number = self._global(vts, ttn)
339
+ if not pttn:
340
+ return [vm.jump_tt(number)]
341
+ self._menus(vts)
342
+ return [vm.set_gprm(CODE_REG, vm.MOV, self._vts_code(vts, ttn, pttn)),
343
+ vm.jump_ss_vtsm(vts, 1, vm.MENU_PTT)]
344
+
345
+ def _jump(self, words, domain, here):
346
+ w = list(words)
347
+
348
+ def number(what):
349
+ if not w or not isinstance(w[0], int):
350
+ raise VtsError(f"jump {' '.join(map(str, words))}: a {what} number is missing")
351
+ return w.pop(0)
352
+
353
+ def chapter():
354
+ if w[:1] == ["chapter"]:
355
+ w.pop(0)
356
+ return number("chapter")
357
+ return 0
358
+
359
+ def title(vts):
360
+ ttn = number("title")
361
+ out = self._to_title(vts, ttn, chapter(), domain, here)
362
+ return out
363
+
364
+ if not w:
365
+ raise VtsError("jump what?")
366
+ head = w.pop(0)
367
+ if head == "title":
368
+ if domain in ("vmgm", "fp"):
369
+ n = number("title")
370
+ if n not in self.by_number:
371
+ raise VtsError(f"title {n} is not on the disc")
372
+ vts, ttn = self.by_number[n]
373
+ out = self._to_title(vts, ttn, chapter(), domain, here)
374
+ else:
375
+ out = title(here)
376
+ elif head == "titleset":
377
+ vts = number("titleset")
378
+ if w[:1] == ["title"]:
379
+ w.pop(0)
380
+ out = title(vts)
381
+ elif w[:1] == ["menu"]:
382
+ w.pop(0)
383
+ kind = vm.MENU_ROOT
384
+ if w[:1] == ["entry"]:
385
+ w.pop(0)
386
+ kind = _ENTRIES.get(w.pop(0) if w else None)
387
+ if kind is None:
388
+ raise VtsError("jump titleset N menu entry what?")
389
+ out = [vm.jump_ss_vtsm(vts, 1, kind)]
390
+ else:
391
+ raise VtsError("jump titleset N title K or menu")
392
+ elif head == "menu":
393
+ if domain == "vts":
394
+ raise VtsError("a title cannot jump to a menu: call it")
395
+ menus = self.vmgm if domain in ("vmgm", "fp") else self._menus(here)
396
+ if w[:1] == ["entry"]:
397
+ w.pop(0)
398
+ pgcn = self._entry(menus, w.pop(0) if w else None,
399
+ "the video manager" if domain in ("vmgm", "fp") else f"titleset {here}")
400
+ elif w and isinstance(w[0], int):
401
+ pgcn = w.pop(0)
402
+ else:
403
+ pgcn = self._entry(menus, "root" if domain == "vtsm" else "title", "the menu domain")
404
+ out = [vm.link_pgcn(pgcn) if domain == "vtsm" else vm.jump_ss_vmgm_pgc(pgcn)]
405
+ elif head == "vmgm":
406
+ if w[:1] != ["menu"]:
407
+ raise VtsError("jump vmgm menu ...")
408
+ w.pop(0)
409
+ if w[:1] == ["entry"]:
410
+ w.pop(0)
411
+ kind = _ENTRIES.get(w.pop(0) if w else None)
412
+ if kind is None:
413
+ raise VtsError("jump vmgm menu entry what?")
414
+ out = [vm.jump_ss_vmgm_menu(kind)]
415
+ elif w and isinstance(w[0], int):
416
+ out = [vm.jump_ss_vmgm_pgc(w.pop(0))]
417
+ else:
418
+ out = [vm.jump_ss_vmgm_menu(vm.MENU_TITLE)]
419
+ elif head == "fpc":
420
+ out = [vm.jump_ss_fp()]
421
+ elif head == "cell":
422
+ out = [vm.link_cn(number("cell"))]
423
+ elif head == "program":
424
+ out = [vm.link_pgn(number("program"))]
425
+ elif head == "chapter":
426
+ out = [vm.link_pttn(number("chapter"))]
427
+ elif (head, w[0] if w else None) in _LINKS:
428
+ out = [vm.link(_LINKS[(head, w.pop(0))])]
429
+ else:
430
+ raise VtsError(f"cannot jump to {' '.join(map(str, words))!r}")
431
+ if w:
432
+ raise VtsError(f"jump {' '.join(map(str, words))}: {' '.join(map(str, w))!r} left over")
433
+ return out
434
+
435
+ def _call(self, words, resume, domain, here):
436
+ w = list(words)
437
+ if domain not in ("vts",):
438
+ # (dvdauthor allows CallSS from titles; a menu "calls" nothing to resume)
439
+ raise VtsError("call is for a title's commands")
440
+ if w[:1] == ["vmgm"]:
441
+ w.pop(0)
442
+ if w[:1] != ["menu"]:
443
+ raise VtsError("call vmgm menu ...")
444
+ w.pop(0)
445
+ kind = vm.MENU_TITLE
446
+ if w[:1] == ["entry"]:
447
+ w.pop(0)
448
+ kind = _ENTRIES.get(w.pop(0) if w else None)
449
+ elif w and isinstance(w[0], int):
450
+ return [vm.call_ss_vmgm_pgc(w.pop(0), resume)]
451
+ if kind is None or w:
452
+ raise VtsError(f"cannot call {' '.join(map(str, words))!r}")
453
+ return [vm.call_ss_vmgm_menu(kind, resume)]
454
+ if w[:1] == ["menu"]:
455
+ w.pop(0)
456
+ kind = vm.MENU_ROOT
457
+ if w[:1] == ["entry"]:
458
+ w.pop(0)
459
+ kind = _ENTRIES.get(w.pop(0) if w else None)
460
+ if kind is None or w:
461
+ raise VtsError(f"cannot call {' '.join(map(str, words))!r}")
462
+ return [vm.call_ss_vtsm_menu(kind, resume)]
463
+ if w == ["fpc"]:
464
+ return [vm.call_ss_fp(resume)]
465
+ raise VtsError(f"cannot call {' '.join(map(str, words))!r}")
466
+
467
+ # -- compiling --------------------------------------------------------------
468
+ def _value(self, e, what):
469
+ if isinstance(e, Num):
470
+ return e.value
471
+ if isinstance(e, Reg):
472
+ return e.code
473
+ raise VtsError(f"{what}: a number or a register")
474
+
475
+ def _assign(self, a):
476
+ if a.target in _SYSTEM:
477
+ raise VtsError("system registers are set in _system")
478
+ n = a.target[1]
479
+ if a.op != "=":
480
+ return vm.set_gprm(n, _ARITH[a.op[0]], self._value(a.value, f"g{n} {a.op}"))
481
+ if isinstance(a.value, Random):
482
+ return vm.set_gprm(n, vm.RND, a.value.value)
483
+ if isinstance(a.value, Arith):
484
+ left, op, right = a.value.left, a.value.op, a.value.right
485
+ if not (isinstance(left, Reg) and left.code == vm.GPRM(n)):
486
+ raise VtsError(f"g{n} = {op}: only g{n} = g{n} {op} value is compiled (no scratch registers)")
487
+ return vm.set_gprm(n, _ARITH[op], self._value(right, f"g{n} = g{n} {op}"))
488
+ return vm.set_gprm(n, vm.MOV, self._value(a.value, f"g{n} ="))
489
+
490
+ def _system(self, assigns):
491
+ """One SetSTN for a run of audio / subtitle / angle assignments, or
492
+ a SetHL_BTNN."""
493
+ if assigns[0].target == "button":
494
+ a = assigns[0]
495
+ if a.op != "=":
496
+ raise VtsError("button = value")
497
+ return vm.set_hl_btnn(self._value(a.value, "button ="))
498
+ fields = {}
499
+ for a in assigns:
500
+ if a.op != "=":
501
+ raise VtsError(f"{a.target} = value")
502
+ fields[a.target] = self._value(a.value, f"{a.target} =")
503
+ return vm.set_stn(fields.get("audio"), fields.get("subtitle"), fields.get("angle"))
504
+
505
+ def _condition(self, c):
506
+ if isinstance(c, Junction):
507
+ raise VtsError("and / or inside a comparison")
508
+ left, right = c.left, c.right
509
+ op = c.op
510
+ if isinstance(left, Num) and isinstance(right, Reg):
511
+ left, right, op = right, left, _MIRROR[op]
512
+ if not isinstance(left, Reg):
513
+ raise VtsError("a comparison needs a register")
514
+ return vm.compare(left.code, op, self._value(right, "a comparison"))
515
+
516
+ def _statements(self, stmts, domain, here):
517
+ out = []
518
+ i = 0
519
+ while i < len(stmts):
520
+ s = stmts[i]
521
+ if isinstance(s, Assign) and s.target in _SYSTEM and s.target != "button":
522
+ run = []
523
+ while i < len(stmts) and isinstance(stmts[i], Assign) and \
524
+ stmts[i].target in ("audio", "subtitle", "angle") and \
525
+ stmts[i].target not in [r.target for r in run]:
526
+ run.append(stmts[i])
527
+ i += 1
528
+ out.append(self._system(run))
529
+ continue
530
+ i += 1
531
+ if isinstance(s, Assign):
532
+ out.append(self._system([s]) if s.target == "button" else self._assign(s))
533
+ elif isinstance(s, If):
534
+ then = self._statements(s.then, domain, here)
535
+ orelse = self._statements(s.orelse, domain, here)
536
+ c = s.condition
537
+ if isinstance(c, Junction) and c.kind == "or":
538
+ out.append(vm.Any([self._condition(p) for p in c.parts], then, orelse))
539
+ elif isinstance(c, Junction):
540
+ out.append(vm.If([self._condition(p) for p in c.parts], then, orelse))
541
+ else:
542
+ out.append(vm.If(self._condition(c), then, orelse))
543
+ elif isinstance(s, Jump):
544
+ out.extend(self._jump(s.words, domain, here))
545
+ elif isinstance(s, Call):
546
+ out.extend(self._call(s.words, s.resume, domain, here))
547
+ elif isinstance(s, Word):
548
+ out.append({"resume": vm.link(vm.RSM), "exit": vm.exit(), "break": vm.break_()}[s.name])
549
+ else:
550
+ raise VtsError(f"{s!r} is not a statement")
551
+ # what follows an unconditional transfer never runs: dvdauthor leaves it out
552
+ if out and vm._transfers(out[-1]):
553
+ break
554
+ return out
555
+
556
+ def _check_domain(self, domain):
557
+ if domain not in ("fp", "vmgm", "vtsm", "vts"):
558
+ raise VtsError(f"{domain!r} is not a domain (fp, vmgm, vtsm, vts)")
559
+
560
+ def instructions(self, text, domain, vts=0):
561
+ """The vm instructions of a command text."""
562
+ self._check_domain(domain)
563
+ if domain in ("vtsm", "vts") and vts not in self.titlesets:
564
+ raise VtsError(f"titleset {vts} is not on the disc")
565
+ return self._statements(parse(text), domain, vts)
566
+
567
+ def compile(self, text, domain, vts=0):
568
+ """The commands (8 bytes each) of a command text run in `domain`
569
+ (of titleset `vts` for "vtsm" and "vts")."""
570
+ return vm.assemble(self.instructions(text, domain, vts))
571
+
572
+ def menu(self, buttons, post, domain="vtsm", vts=0):
573
+ """A menu's buttons and post commands: ([one command a button],
574
+ post commands). A button whose text takes several commands runs
575
+ them from the post, through a dispatcher, as dvdauthor arranges."""
576
+ self._check_domain(domain)
577
+ if domain not in ("vtsm", "vmgm"):
578
+ raise VtsError("buttons belong to menus")
579
+ commands, deferred = [], []
580
+ for n, text in enumerate(buttons, 1):
581
+ items = self.instructions(text, domain, vts)
582
+ code = vm.assemble(items) if items else []
583
+ if len(code) == 1:
584
+ commands.append(code[0])
585
+ else:
586
+ deferred.append((n, items))
587
+ commands.append(vm.assemble([vm.set_gprm(CODE_REG, vm.MOV, n), vm.link(vm.TAIL_PGC)])[0])
588
+ post_items = self.instructions(post, domain, vts) if post else [vm.exit()]
589
+ if not deferred:
590
+ return commands, vm.assemble(post_items)
591
+ if not (post_items and vm._transfers(post_items[-1])):
592
+ raise VtsError("a menu with multi-command buttons needs post commands that leave the chain")
593
+ items = [vm.set_gprm(SCRATCH, vm.MOV, vm.GPRM(CODE_REG)), vm.set_gprm(CODE_REG, vm.MOV, 0)]
594
+ folded = None
595
+ if len(deferred) == 1 and len(post_items) == 1 and isinstance(post_items[0], vm.Instruction):
596
+ # one such button and a post of one command: the command runs
597
+ # unless the button was pressed (as dvdauthor folds it)
598
+ try:
599
+ folded = post_items[0].when(vm.compare(vm.GPRM(SCRATCH), vm.NE, deferred[0][0]))
600
+ except VtsError:
601
+ pass
602
+ if folded is not None:
603
+ items.append(folded)
604
+ else:
605
+ items += [vm.Goto(("button", n), vm.compare(vm.GPRM(SCRATCH), vm.EQ, n)) for n, _ in deferred]
606
+ items += post_items
607
+ for n, body in deferred:
608
+ items += [vm.Label(("button", n))] + body
609
+ return commands, vm.assemble(items)
610
+
611
+ # -- the pads ---------------------------------------------------------------
612
+ def vmgm_pads(self):
613
+ """The pre commands of each pad program chain the video manager's
614
+ menus need, after its own `vmgm.count` chains, in order."""
615
+ pads = []
616
+ for first in range(0, len(self.vmgm_targets), PAD_TARGETS):
617
+ items = [vm.set_gprm(SCRATCH, vm.MOV, vm.GPRM(CODE_REG)), vm.set_gprm(CODE_REG, vm.MOV, 0)]
618
+ for k, (vts, ttn, pttn) in enumerate(self.vmgm_targets[first:first + PAD_TARGETS], 1):
619
+ items.append(vm.set_gprm(SCRATCH2, vm.MOV, k))
620
+ cond = vm.compare(vm.GPRM(SCRATCH), vm.EQ, vm.GPRM(SCRATCH2))
621
+ if pttn:
622
+ items.append(vm.If(cond, [vm.set_gprm(CODE_REG, vm.MOV, self._vts_code(vts, ttn, pttn)),
623
+ vm.jump_ss_vtsm(vts, 1, vm.MENU_PTT)]))
624
+ else:
625
+ items.append(vm.If(cond, [vm.jump_tt(self._global(vts, ttn))]))
626
+ # nothing matched: the title menu, else title 1
627
+ if "title" in self.vmgm.entries:
628
+ items.append(vm.jump_ss_vmgm_pgc(self.vmgm.entries["title"]))
629
+ else:
630
+ items.append(vm.jump_tt(1))
631
+ pads.append(vm.assemble(items))
632
+ return pads
633
+
634
+ def vts_pad(self, vts):
635
+ """The pre commands of titleset `vts`'s chapter (PTT) menu chain,
636
+ decoding the code a pad jump left in GPRM 15, or None when nothing
637
+ jumps into the titleset that way. Ends at the root menu."""
638
+ targets = sorted(self.vts_targets.get(vts, ()))
639
+ if not targets:
640
+ return None
641
+ items = [vm.set_gprm(SCRATCH, vm.MOV, vm.GPRM(CODE_REG)), vm.set_gprm(CODE_REG, vm.MOV, 0)]
642
+ for ttn, pttn in targets:
643
+ items.append(vm.set_gprm(SCRATCH2, vm.MOV, 0x8000 | ttn << 8 | pttn))
644
+ items.append(vm.If(vm.compare(vm.GPRM(SCRATCH), vm.EQ, vm.GPRM(SCRATCH2)),
645
+ [vm.jump_vts_ptt(ttn, pttn)]))
646
+ menus = self._menus(vts)
647
+ items.append(vm.link_pgcn(menus.entries["root"]) if "root" in menus.entries else vm.exit())
648
+ return vm.assemble(items)