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/__init__.py +25 -0
- vtskit/errors.py +8 -0
- vtskit/ifo.py +113 -0
- vtskit/lang.py +648 -0
- vtskit/menu.py +171 -0
- vtskit/nav.py +256 -0
- vtskit/ps.py +484 -0
- vtskit/spu.py +262 -0
- vtskit/subtitles.py +126 -0
- vtskit/titleset.py +967 -0
- vtskit/vm.py +482 -0
- vtskit/vmg.py +159 -0
- vtskit-0.1.0.dist-info/METADATA +205 -0
- vtskit-0.1.0.dist-info/RECORD +18 -0
- vtskit-0.1.0.dist-info/WHEEL +5 -0
- vtskit-0.1.0.dist-info/licenses/COPYING +674 -0
- vtskit-0.1.0.dist-info/licenses/COPYING.LESSER +165 -0
- vtskit-0.1.0.dist-info/top_level.txt +1 -0
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)
|