rpgsync 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.
- rpgsync/__init__.py +8 -0
- rpgsync/__main__.py +3 -0
- rpgsync/cli.py +1242 -0
- rpgsync/commands.py +920 -0
- rpgsync/compat.py +203 -0
- rpgsync/database.py +1025 -0
- rpgsync/db.py +1377 -0
- rpgsync/dbschema.py +1011 -0
- rpgsync/dsl.py +2616 -0
- rpgsync/dynparams.py +357 -0
- rpgsync/dynrpg.py +612 -0
- rpgsync/fmt.py +79 -0
- rpgsync/lcf.py +826 -0
- rpgsync/maniac.py +387 -0
- rpgsync/maptree.py +888 -0
- rpgsync/messages.py +113 -0
- rpgsync/named.py +1051 -0
- rpgsync/project.py +323 -0
- rpgsync/py.typed +0 -0
- rpgsync/pyexpr.py +1472 -0
- rpgsync/resources.py +455 -0
- rpgsync/scaffold.py +567 -0
- rpgsync/script.py +3532 -0
- rpgsync/scriptfiles.py +924 -0
- rpgsync/sync.py +1365 -0
- rpgsync/tables.py +114 -0
- rpgsync/venv.py +102 -0
- rpgsync-0.1.0.dist-info/METADATA +288 -0
- rpgsync-0.1.0.dist-info/RECORD +31 -0
- rpgsync-0.1.0.dist-info/WHEEL +4 -0
- rpgsync-0.1.0.dist-info/entry_points.txt +3 -0
rpgsync/commands.py
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
"""Event command catalog: binary <-> script call translation.
|
|
2
|
+
|
|
3
|
+
Every command can always be written in the *generic* form::
|
|
4
|
+
|
|
5
|
+
cmd("TintScreen", 100, 100, 100, 100, 20, 1)
|
|
6
|
+
cmd(12345, 1, 2, text="string parameter")
|
|
7
|
+
|
|
8
|
+
Common commands additionally have a *rich* form with readable arguments,
|
|
9
|
+
e.g. ``variable(1, "=", 9999999)``. The decompiler only emits a rich form
|
|
10
|
+
after checking that compiling it back yields the exact same command, so
|
|
11
|
+
round trips stay lossless even for data the rich form does not anticipate
|
|
12
|
+
(Maniac Patch extensions, odd parameter counts, ...).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
from collections.abc import Callable, Sequence
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from .lcf import Command, MoveCommand
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CompileError(Exception):
|
|
27
|
+
def __init__(self, msg: str, node=None):
|
|
28
|
+
self.lineno = getattr(node, "lineno", None)
|
|
29
|
+
super().__init__(msg)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------
|
|
33
|
+
# Command codes (from liblcf enums.csv) and generic names
|
|
34
|
+
# --------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
CODE_NAMES = {
|
|
37
|
+
10: "END",
|
|
38
|
+
1005: "CallCommonEvent",
|
|
39
|
+
1006: "ForceFlee",
|
|
40
|
+
1007: "EnableCombo",
|
|
41
|
+
1008: "ChangeClass",
|
|
42
|
+
1009: "ChangeBattleCommands",
|
|
43
|
+
5001: "OpenLoadMenu",
|
|
44
|
+
5002: "ExitGame",
|
|
45
|
+
5003: "ToggleAtbMode",
|
|
46
|
+
5004: "ToggleFullscreen",
|
|
47
|
+
5005: "OpenVideoOptions",
|
|
48
|
+
10110: "ShowMessage",
|
|
49
|
+
10120: "MessageOptions",
|
|
50
|
+
10130: "ChangeFaceGraphic",
|
|
51
|
+
10140: "ShowChoice",
|
|
52
|
+
10150: "InputNumber",
|
|
53
|
+
10210: "ControlSwitches",
|
|
54
|
+
10220: "ControlVars",
|
|
55
|
+
10230: "TimerOperation",
|
|
56
|
+
10310: "ChangeGold",
|
|
57
|
+
10320: "ChangeItems",
|
|
58
|
+
10330: "ChangePartyMembers",
|
|
59
|
+
10410: "ChangeExp",
|
|
60
|
+
10420: "ChangeLevel",
|
|
61
|
+
10430: "ChangeParameters",
|
|
62
|
+
10440: "ChangeSkills",
|
|
63
|
+
10450: "ChangeEquipment",
|
|
64
|
+
10460: "ChangeHP",
|
|
65
|
+
10470: "ChangeSP",
|
|
66
|
+
10480: "ChangeCondition",
|
|
67
|
+
10490: "FullHeal",
|
|
68
|
+
10500: "SimulatedAttack",
|
|
69
|
+
10610: "ChangeHeroName",
|
|
70
|
+
10620: "ChangeHeroTitle",
|
|
71
|
+
10630: "ChangeSpriteAssociation",
|
|
72
|
+
10640: "ChangeActorFace",
|
|
73
|
+
10650: "ChangeVehicleGraphic",
|
|
74
|
+
10660: "ChangeSystemBGM",
|
|
75
|
+
10670: "ChangeSystemSFX",
|
|
76
|
+
10680: "ChangeSystemGraphics",
|
|
77
|
+
10690: "ChangeScreenTransitions",
|
|
78
|
+
10710: "EnemyEncounter",
|
|
79
|
+
10720: "OpenShop",
|
|
80
|
+
10730: "ShowInn",
|
|
81
|
+
10740: "EnterHeroName",
|
|
82
|
+
10810: "Teleport",
|
|
83
|
+
10820: "MemorizeLocation",
|
|
84
|
+
10830: "RecallToLocation",
|
|
85
|
+
10840: "EnterExitVehicle",
|
|
86
|
+
10850: "SetVehicleLocation",
|
|
87
|
+
10860: "ChangeEventLocation",
|
|
88
|
+
10870: "TradeEventLocations",
|
|
89
|
+
10910: "StoreTerrainID",
|
|
90
|
+
10920: "StoreEventID",
|
|
91
|
+
11010: "EraseScreen",
|
|
92
|
+
11020: "ShowScreen",
|
|
93
|
+
11030: "TintScreen",
|
|
94
|
+
11040: "FlashScreen",
|
|
95
|
+
11050: "ShakeScreen",
|
|
96
|
+
11060: "PanScreen",
|
|
97
|
+
11070: "WeatherEffects",
|
|
98
|
+
11110: "ShowPicture",
|
|
99
|
+
11120: "MovePicture",
|
|
100
|
+
11130: "ErasePicture",
|
|
101
|
+
11210: "ShowBattleAnimation",
|
|
102
|
+
11310: "PlayerVisibility",
|
|
103
|
+
11320: "FlashSprite",
|
|
104
|
+
11330: "MoveEvent",
|
|
105
|
+
11340: "ProceedWithMovement",
|
|
106
|
+
11350: "HaltAllMovement",
|
|
107
|
+
11410: "Wait",
|
|
108
|
+
11510: "PlayBGM",
|
|
109
|
+
11520: "FadeOutBGM",
|
|
110
|
+
11530: "MemorizeBGM",
|
|
111
|
+
11540: "PlayMemorizedBGM",
|
|
112
|
+
11550: "PlaySound",
|
|
113
|
+
11560: "PlayMovie",
|
|
114
|
+
11610: "KeyInputProc",
|
|
115
|
+
11710: "ChangeMapTileset",
|
|
116
|
+
11720: "ChangePBG",
|
|
117
|
+
11740: "ChangeEncounterSteps",
|
|
118
|
+
11750: "TileSubstitution",
|
|
119
|
+
11810: "TeleportTargets",
|
|
120
|
+
11820: "ChangeTeleportAccess",
|
|
121
|
+
11830: "EscapeTarget",
|
|
122
|
+
11840: "ChangeEscapeAccess",
|
|
123
|
+
11910: "OpenSaveMenu",
|
|
124
|
+
11930: "ChangeSaveAccess",
|
|
125
|
+
11950: "OpenMainMenu",
|
|
126
|
+
11960: "ChangeMainMenuAccess",
|
|
127
|
+
12010: "ConditionalBranch",
|
|
128
|
+
12110: "Label",
|
|
129
|
+
12120: "JumpToLabel",
|
|
130
|
+
12210: "Loop",
|
|
131
|
+
12220: "BreakLoop",
|
|
132
|
+
12310: "EndEventProcessing",
|
|
133
|
+
12320: "EraseEvent",
|
|
134
|
+
12330: "CallEvent",
|
|
135
|
+
12410: "Comment",
|
|
136
|
+
12420: "GameOver",
|
|
137
|
+
12510: "ReturntoTitleScreen",
|
|
138
|
+
13110: "ChangeMonsterHP",
|
|
139
|
+
13120: "ChangeMonsterMP",
|
|
140
|
+
13130: "ChangeMonsterCondition",
|
|
141
|
+
13150: "ShowHiddenMonster",
|
|
142
|
+
13210: "ChangeBattleBG",
|
|
143
|
+
13260: "ShowBattleAnimation_B",
|
|
144
|
+
13310: "ConditionalBranch_B",
|
|
145
|
+
13410: "TerminateBattle",
|
|
146
|
+
20110: "ShowMessage_2",
|
|
147
|
+
20140: "ShowChoiceOption",
|
|
148
|
+
20141: "ShowChoiceEnd",
|
|
149
|
+
20710: "VictoryHandler",
|
|
150
|
+
20711: "EscapeHandler",
|
|
151
|
+
20712: "DefeatHandler",
|
|
152
|
+
20713: "EndBattle",
|
|
153
|
+
20720: "Transaction",
|
|
154
|
+
20721: "NoTransaction",
|
|
155
|
+
20722: "EndShop",
|
|
156
|
+
20730: "Stay",
|
|
157
|
+
20731: "NoStay",
|
|
158
|
+
20732: "EndInn",
|
|
159
|
+
22010: "ElseBranch",
|
|
160
|
+
22011: "EndBranch",
|
|
161
|
+
22210: "EndLoop",
|
|
162
|
+
22410: "Comment_2",
|
|
163
|
+
23310: "ElseBranch_B",
|
|
164
|
+
23311: "EndBranch_B",
|
|
165
|
+
}
|
|
166
|
+
NAME_CODES = {v: k for k, v in CODE_NAMES.items()}
|
|
167
|
+
|
|
168
|
+
END = 10
|
|
169
|
+
|
|
170
|
+
# Block structure. A "closer" command ends a group of blocks; it is implied
|
|
171
|
+
# in scripts and re-inserted when the group's last block ends.
|
|
172
|
+
# closer code -> (codes of blocks that belong to the group, codes that may
|
|
173
|
+
# *continue* an open group)
|
|
174
|
+
GROUPS = {
|
|
175
|
+
22011: ({12010, 22010}, {22010}), # if / else / end
|
|
176
|
+
23311: ({13310, 23310}, {23310}), # battle if / else / end
|
|
177
|
+
22210: ({12210}, set()), # loop / end loop
|
|
178
|
+
20141: ({20140}, {20140}), # choice options / end
|
|
179
|
+
20713: ({20710, 20711, 20712}, {20710, 20711, 20712}), # battle handlers
|
|
180
|
+
20722: ({20720, 20721}, {20720, 20721}), # shop handlers
|
|
181
|
+
20732: ({20730, 20731}, {20730, 20731}), # inn handlers
|
|
182
|
+
}
|
|
183
|
+
CLOSER_OF = {}
|
|
184
|
+
for _closer, (_members, _cont) in GROUPS.items():
|
|
185
|
+
for _m in _members:
|
|
186
|
+
CLOSER_OF[_m] = _closer
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --------------------------------------------------------------------------
|
|
190
|
+
# Script values
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Sym(BaseModel):
|
|
195
|
+
"""A bare name in a script, e.g. ``move_up``."""
|
|
196
|
+
|
|
197
|
+
name: str
|
|
198
|
+
node: Any = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Call(BaseModel):
|
|
202
|
+
"""A call in a script: ``wait(1)``, or a method call ``this.move(...)``
|
|
203
|
+
(then ``target`` is the evaluated object)."""
|
|
204
|
+
|
|
205
|
+
name: str
|
|
206
|
+
args: list[Any]
|
|
207
|
+
kwargs: dict[str, Any]
|
|
208
|
+
target: Any = None
|
|
209
|
+
node: Any = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Ctx(BaseModel):
|
|
213
|
+
"""Context shared by the encoders/decoders of one file."""
|
|
214
|
+
|
|
215
|
+
encoding: str = "cp1252"
|
|
216
|
+
engine: str = "2k3" # "2k" or "2k3"
|
|
217
|
+
names: dict[str, dict[int, str]] = Field(default_factory=dict) # db names for comments
|
|
218
|
+
comments: Any = None # CommentIndex of the script being compiled
|
|
219
|
+
match_header: bool = False # encoding/decoding the subject of a `match` (battle, shop, inn)
|
|
220
|
+
event_names: dict[int, str] = Field(default_factory=dict) # map event names for comments
|
|
221
|
+
handles: dict[str, dict[int, str]] = Field(default_factory=dict) # variables/switches id -> Python name
|
|
222
|
+
choice_sep: str | None = None # how the editor joined show_choices options (None: engine default)
|
|
223
|
+
dynparams: bool = False # DynPlugins/DynParams.dll: variables wherever RPG Maker takes a number
|
|
224
|
+
|
|
225
|
+
def enc(self, s: str, node=None) -> bytes:
|
|
226
|
+
try:
|
|
227
|
+
return s.encode(self.encoding, errors="surrogateescape")
|
|
228
|
+
except UnicodeEncodeError as e:
|
|
229
|
+
raise CompileError( # noqa: B904 - report the script position, not the codec error
|
|
230
|
+
"text %r cannot be stored in the game encoding %s (%s)" % (s, self.encoding, e.reason), node
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def dec(self, b: bytes) -> str:
|
|
234
|
+
s = b.decode(self.encoding, errors="surrogateescape")
|
|
235
|
+
if s.encode(self.encoding, errors="surrogateescape") == b:
|
|
236
|
+
return s
|
|
237
|
+
# Some code pages (e.g. cp932) map several byte sequences to the same
|
|
238
|
+
# character. Keep such characters as escaped bytes so they survive.
|
|
239
|
+
import codecs
|
|
240
|
+
|
|
241
|
+
decoder = codecs.getincrementaldecoder(self.encoding)(errors="surrogateescape")
|
|
242
|
+
out, start = [], 0
|
|
243
|
+
for i in range(len(b)):
|
|
244
|
+
chars = decoder.decode(b[i : i + 1])
|
|
245
|
+
if chars:
|
|
246
|
+
raw = b[start : i + 1]
|
|
247
|
+
if chars.encode(self.encoding, errors="surrogateescape") == raw:
|
|
248
|
+
out.append(chars)
|
|
249
|
+
else:
|
|
250
|
+
out.append(raw.decode("ascii", errors="surrogateescape"))
|
|
251
|
+
start = i + 1
|
|
252
|
+
if start < len(b):
|
|
253
|
+
out.append(b[start:].decode("ascii", errors="surrogateescape"))
|
|
254
|
+
return "".join(out)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# --------------------------------------------------------------------------
|
|
258
|
+
# Source rendering helpers
|
|
259
|
+
# --------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def pystr(s: str) -> str:
|
|
263
|
+
"""Render a Python string literal, preferring raw strings for \\ codes."""
|
|
264
|
+
printable = all((c.isprintable() and not 0xDC80 <= ord(c) <= 0xDCFF) for c in s)
|
|
265
|
+
if "\\" in s and printable and '"' not in s and not s.endswith("\\"):
|
|
266
|
+
return 'r"%s"' % s
|
|
267
|
+
out = ['"']
|
|
268
|
+
for c in s:
|
|
269
|
+
o = ord(c)
|
|
270
|
+
if c == "\\":
|
|
271
|
+
out.append("\\\\")
|
|
272
|
+
elif c == '"':
|
|
273
|
+
out.append('\\"')
|
|
274
|
+
elif c == "\n":
|
|
275
|
+
out.append("\\n")
|
|
276
|
+
elif c == "\t":
|
|
277
|
+
out.append("\\t")
|
|
278
|
+
elif 0xDC80 <= o <= 0xDCFF or not c.isprintable():
|
|
279
|
+
out.append("\\x%02x" % o if o < 0x100 else "\\u%04x" % o)
|
|
280
|
+
else:
|
|
281
|
+
out.append(c)
|
|
282
|
+
out.append('"')
|
|
283
|
+
return "".join(out)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def lit(v) -> str:
|
|
287
|
+
if isinstance(v, bool) or v is None:
|
|
288
|
+
return repr(v)
|
|
289
|
+
if isinstance(v, int):
|
|
290
|
+
return str(v)
|
|
291
|
+
if isinstance(v, str):
|
|
292
|
+
return pystr(v)
|
|
293
|
+
if isinstance(v, tuple):
|
|
294
|
+
return "(" + ", ".join(lit(x) for x in v) + ("," if len(v) == 1 else "") + ")"
|
|
295
|
+
if isinstance(v, list):
|
|
296
|
+
return "[" + ", ".join(lit(x) for x in v) + "]"
|
|
297
|
+
if isinstance(v, Raw):
|
|
298
|
+
return v.src
|
|
299
|
+
raise TypeError(v)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class Raw(BaseModel):
|
|
303
|
+
"""Pre-rendered source fragment."""
|
|
304
|
+
|
|
305
|
+
src: str
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def render_call(name: str, args: Sequence = (), kwargs: Sequence[tuple[str, Any]] = ()) -> str:
|
|
309
|
+
parts = [lit(a) for a in args] + ["%s=%s" % (k, lit(v)) for k, v in kwargs]
|
|
310
|
+
return "%s(%s)" % (name, ", ".join(parts))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# --------------------------------------------------------------------------
|
|
314
|
+
# Argument helpers for encoders
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class Args:
|
|
319
|
+
"""Positional/keyword argument access with good error messages."""
|
|
320
|
+
|
|
321
|
+
def __init__(self, call: Call, spec: Sequence[str]):
|
|
322
|
+
self.call = call
|
|
323
|
+
self.values: dict[str, Any] = {}
|
|
324
|
+
if len(call.args) > len(spec):
|
|
325
|
+
raise CompileError(
|
|
326
|
+
"%s() takes at most %d positional arguments (%s)" % (call.name, len(spec), ", ".join(spec)), call.node
|
|
327
|
+
)
|
|
328
|
+
for name, v in zip(spec, call.args):
|
|
329
|
+
self.values[name] = v
|
|
330
|
+
for k, v in call.kwargs.items():
|
|
331
|
+
if k not in spec:
|
|
332
|
+
raise CompileError(
|
|
333
|
+
"%s() got an unknown argument %r (expected: %s)" % (call.name, k, ", ".join(spec)), call.node
|
|
334
|
+
)
|
|
335
|
+
if k in self.values:
|
|
336
|
+
raise CompileError("%s() got %r twice" % (call.name, k), call.node)
|
|
337
|
+
self.values[k] = v
|
|
338
|
+
|
|
339
|
+
def get(self, name, default=None):
|
|
340
|
+
return self.values.get(name, default)
|
|
341
|
+
|
|
342
|
+
def req(self, name):
|
|
343
|
+
if name not in self.values:
|
|
344
|
+
raise CompileError("%s() is missing the argument %r" % (self.call.name, name), self.call.node)
|
|
345
|
+
return self.values[name]
|
|
346
|
+
|
|
347
|
+
def int(self, name, default=None) -> int:
|
|
348
|
+
v = self.values.get(name, default) if default is not None else self.req(name)
|
|
349
|
+
if isinstance(v, bool):
|
|
350
|
+
return int(v)
|
|
351
|
+
if not isinstance(v, int):
|
|
352
|
+
raise CompileError("%s(): %s must be an integer, got %r" % (self.call.name, name, v), self.call.node)
|
|
353
|
+
return v
|
|
354
|
+
|
|
355
|
+
def bool(self, name, default=False) -> bool:
|
|
356
|
+
v = self.values.get(name, default)
|
|
357
|
+
if not isinstance(v, (bool, int)):
|
|
358
|
+
raise CompileError("%s(): %s must be True/False" % (self.call.name, name), self.call.node)
|
|
359
|
+
return bool(v)
|
|
360
|
+
|
|
361
|
+
def str(self, name, default=None) -> str:
|
|
362
|
+
v = self.values.get(name, default) if default is not None else self.req(name)
|
|
363
|
+
if v is None:
|
|
364
|
+
return ""
|
|
365
|
+
if not isinstance(v, str):
|
|
366
|
+
raise CompileError("%s(): %s must be a string, got %r" % (self.call.name, name, v), self.call.node)
|
|
367
|
+
return v
|
|
368
|
+
|
|
369
|
+
def enum(self, name, table: Sequence[str], default=None) -> int:
|
|
370
|
+
v = self.values.get(name, default) if default is not None else self.req(name)
|
|
371
|
+
if isinstance(v, int) and not isinstance(v, bool):
|
|
372
|
+
return v
|
|
373
|
+
if isinstance(v, str) and v in table:
|
|
374
|
+
return list(table).index(v)
|
|
375
|
+
raise CompileError(
|
|
376
|
+
"%s(): %s must be one of %s, got %r" % (self.call.name, name, ", ".join(repr(t) for t in table if t), v),
|
|
377
|
+
self.call.node,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def enum_name(table: Sequence[str], v: int):
|
|
382
|
+
if 0 <= v < len(table) and table[v]:
|
|
383
|
+
return table[v]
|
|
384
|
+
return v
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def is_call(v, *names) -> bool:
|
|
388
|
+
return isinstance(v, Call) and v.name in names
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# --------------------------------------------------------------------------
|
|
392
|
+
# Shared enums
|
|
393
|
+
# --------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
DIRECTIONS = ["up", "right", "down", "left"]
|
|
396
|
+
TELEPORT_DIRECTIONS = ["retain", "up", "right", "down", "left"]
|
|
397
|
+
|
|
398
|
+
MOVES = [
|
|
399
|
+
"move_up",
|
|
400
|
+
"move_right",
|
|
401
|
+
"move_down",
|
|
402
|
+
"move_left",
|
|
403
|
+
"move_up_right",
|
|
404
|
+
"move_down_right",
|
|
405
|
+
"move_down_left",
|
|
406
|
+
"move_up_left",
|
|
407
|
+
"move_random",
|
|
408
|
+
"move_toward_player",
|
|
409
|
+
"move_away_from_player",
|
|
410
|
+
"move_forward",
|
|
411
|
+
"face_up",
|
|
412
|
+
"face_right",
|
|
413
|
+
"face_down",
|
|
414
|
+
"face_left",
|
|
415
|
+
"turn_right",
|
|
416
|
+
"turn_left",
|
|
417
|
+
"turn_around",
|
|
418
|
+
"turn_random",
|
|
419
|
+
"face_random",
|
|
420
|
+
"face_player",
|
|
421
|
+
"face_away_from_player",
|
|
422
|
+
"pause",
|
|
423
|
+
"jump_start",
|
|
424
|
+
"jump_end",
|
|
425
|
+
"lock_facing",
|
|
426
|
+
"unlock_facing",
|
|
427
|
+
"speed_up",
|
|
428
|
+
"speed_down",
|
|
429
|
+
"frequency_up",
|
|
430
|
+
"frequency_down",
|
|
431
|
+
"switch_on",
|
|
432
|
+
"switch_off",
|
|
433
|
+
"change_graphic",
|
|
434
|
+
"play_se",
|
|
435
|
+
"phasing_on",
|
|
436
|
+
"phasing_off",
|
|
437
|
+
"stop_animation",
|
|
438
|
+
"start_animation",
|
|
439
|
+
"transparency_up",
|
|
440
|
+
"transparency_down",
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# --------------------------------------------------------------------------
|
|
445
|
+
# Move routes
|
|
446
|
+
# --------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def moves_to_src(moves: list[MoveCommand], ctx: Ctx) -> list[Raw]:
|
|
450
|
+
"""Move steps as source; runs of the same simple step become ``step * n``."""
|
|
451
|
+
out: list[Raw] = []
|
|
452
|
+
i = 0
|
|
453
|
+
while i < len(moves):
|
|
454
|
+
m = moves[i]
|
|
455
|
+
j = i + 1
|
|
456
|
+
if m.code not in (32, 33, 34, 35) and 0 <= m.code < len(MOVES):
|
|
457
|
+
while j < len(moves) and moves[j].code == m.code:
|
|
458
|
+
j += 1
|
|
459
|
+
if j - i > 1:
|
|
460
|
+
out.append(Raw(src="%s * %d" % (MOVES[m.code], j - i)))
|
|
461
|
+
i = j
|
|
462
|
+
continue
|
|
463
|
+
out.extend(_move_src(m, ctx))
|
|
464
|
+
i += 1
|
|
465
|
+
return out
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _move_src(m: MoveCommand, ctx: Ctx) -> list[Raw]:
|
|
469
|
+
if m.code in (32, 33):
|
|
470
|
+
from .pyexpr import sw_src # switches by name: switch_on(switches.door_open)
|
|
471
|
+
|
|
472
|
+
return [Raw(src="%s(%s)" % (MOVES[m.code], sw_src(m.params[0])))]
|
|
473
|
+
if m.code == 34:
|
|
474
|
+
return [Raw(src=render_call("change_graphic", [ctx.dec(m.string), m.params[0]]))]
|
|
475
|
+
if m.code == 35:
|
|
476
|
+
kw = [(k, v) for k, v, d in zip(("volume", "tempo", "balance"), m.params, (100, 100, 50)) if v != d]
|
|
477
|
+
return [Raw(src=render_call("play_se", [ctx.dec(m.string)], kw))]
|
|
478
|
+
if 0 <= m.code < len(MOVES):
|
|
479
|
+
return [Raw(src=MOVES[m.code])]
|
|
480
|
+
return [Raw(src="move(%d)" % m.code)]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def moves_from_values(values: Sequence[Any], ctx: Ctx, node=None) -> list[MoveCommand]:
|
|
484
|
+
out = []
|
|
485
|
+
expanded = []
|
|
486
|
+
for v in values:
|
|
487
|
+
if type(v).__name__ == "Repeat":
|
|
488
|
+
expanded.extend([v.value] * v.count)
|
|
489
|
+
else:
|
|
490
|
+
expanded.append(v)
|
|
491
|
+
for v in expanded:
|
|
492
|
+
if isinstance(v, Sym):
|
|
493
|
+
if v.name not in MOVES or MOVES.index(v.name) in (32, 33, 34, 35):
|
|
494
|
+
raise CompileError("unknown move command %r" % v.name, v.node or node)
|
|
495
|
+
out.append(MoveCommand(code=MOVES.index(v.name)))
|
|
496
|
+
elif isinstance(v, Call):
|
|
497
|
+
if v.name in ("switch_on", "switch_off"):
|
|
498
|
+
a = Args(v, ["switch"])
|
|
499
|
+
switch = a.get("switch")
|
|
500
|
+
if getattr(switch, "coll", None) == "switches" and isinstance(switch.index, int):
|
|
501
|
+
switch = switch.index # switches[12] / switches.door_open
|
|
502
|
+
elif not isinstance(switch, int) or isinstance(switch, bool):
|
|
503
|
+
raise CompileError(
|
|
504
|
+
"%s(): switch must be switches[id], switches.name or an id" % v.name, v.node or node
|
|
505
|
+
)
|
|
506
|
+
out.append(MoveCommand(code=MOVES.index(v.name), params=[switch]))
|
|
507
|
+
elif v.name == "change_graphic":
|
|
508
|
+
a = Args(v, ["charset", "index"])
|
|
509
|
+
out.append(MoveCommand(code=34, string=ctx.enc(a.str("charset"), v.node), params=[a.int("index")]))
|
|
510
|
+
elif v.name == "play_se":
|
|
511
|
+
a = Args(v, ["name", "volume", "tempo", "balance"])
|
|
512
|
+
out.append(
|
|
513
|
+
MoveCommand(
|
|
514
|
+
code=35,
|
|
515
|
+
string=ctx.enc(a.str("name"), v.node),
|
|
516
|
+
params=[a.int("volume", 100), a.int("tempo", 100), a.int("balance", 50)],
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
elif v.name == "move":
|
|
520
|
+
a = Args(v, ["code"])
|
|
521
|
+
out.append(MoveCommand(code=a.int("code")))
|
|
522
|
+
elif v.name in MOVES:
|
|
523
|
+
out.append(MoveCommand(code=MOVES.index(v.name)))
|
|
524
|
+
else:
|
|
525
|
+
raise CompileError("unknown move command %s()" % v.name, v.node or node)
|
|
526
|
+
else:
|
|
527
|
+
raise CompileError("expected a move command, got %r" % (v,), node)
|
|
528
|
+
return out
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# --------------------------------------------------------------------------
|
|
532
|
+
# Rich command registry
|
|
533
|
+
# --------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
Decoder = Callable[[Command, Ctx], str | None]
|
|
536
|
+
Encoder = Callable[[Call, Ctx], Command]
|
|
537
|
+
|
|
538
|
+
RICH_DECODERS: dict[int, Decoder] = {}
|
|
539
|
+
RICH_ENCODERS: dict[str, Encoder] = {}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def rich(code: int, *names: str):
|
|
543
|
+
def deco(pair):
|
|
544
|
+
dec, enc = pair
|
|
545
|
+
if dec is not None:
|
|
546
|
+
RICH_DECODERS[code] = dec
|
|
547
|
+
for n in names:
|
|
548
|
+
RICH_ENCODERS[n] = enc
|
|
549
|
+
return pair
|
|
550
|
+
|
|
551
|
+
return deco
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def C(code, params=(), string=b"") -> Command:
|
|
555
|
+
return Command(code=code, string=string, params=list(params))
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# -- Messages -----------------------------------------------------------------
|
|
559
|
+
# text()/comment() span several commands and are handled by the script layer;
|
|
560
|
+
# see TEXT_CODES.
|
|
561
|
+
TEXT_CODES = {10110: ("text", 20110), 12410: ("comment", 22410)}
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _msg_options():
|
|
565
|
+
def dec(c, ctx):
|
|
566
|
+
if len(c.params) != 4 or c.string:
|
|
567
|
+
return None
|
|
568
|
+
kw = []
|
|
569
|
+
if c.params[0]:
|
|
570
|
+
kw.append(("transparent", bool(c.params[0]) if c.params[0] == 1 else c.params[0]))
|
|
571
|
+
kw.append(("position", enum_name(["top", "middle", "bottom"], c.params[1])))
|
|
572
|
+
if c.params[2]:
|
|
573
|
+
kw.append(("avoid_hero", bool(c.params[2]) if c.params[2] == 1 else c.params[2]))
|
|
574
|
+
if c.params[3]:
|
|
575
|
+
kw.append(("continue_events", bool(c.params[3]) if c.params[3] == 1 else c.params[3]))
|
|
576
|
+
return render_call("message_options", [], kw)
|
|
577
|
+
|
|
578
|
+
def enc(call, ctx):
|
|
579
|
+
a = Args(call, ["transparent", "position", "avoid_hero", "continue_events"])
|
|
580
|
+
return C(
|
|
581
|
+
10120,
|
|
582
|
+
[
|
|
583
|
+
a.int("transparent", 0),
|
|
584
|
+
a.enum("position", ["top", "middle", "bottom"], "bottom"),
|
|
585
|
+
a.int("avoid_hero", 0),
|
|
586
|
+
a.int("continue_events", 0),
|
|
587
|
+
],
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
return dec, enc
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
rich(10120, "message_options")(_msg_options())
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _face():
|
|
597
|
+
def dec(c, ctx):
|
|
598
|
+
if not c.string:
|
|
599
|
+
if not c.params:
|
|
600
|
+
return "face(None)"
|
|
601
|
+
if len(c.params) == 3:
|
|
602
|
+
flags = [bool(x) if x in (0, 1) else x for x in c.params[1:]]
|
|
603
|
+
return render_call("face", [None, c.params[0]] + flags)
|
|
604
|
+
return None
|
|
605
|
+
if len(c.params) != 3:
|
|
606
|
+
return None
|
|
607
|
+
kw = []
|
|
608
|
+
if c.params[1]:
|
|
609
|
+
kw.append(("right", bool(c.params[1]) if c.params[1] == 1 else c.params[1]))
|
|
610
|
+
if c.params[2]:
|
|
611
|
+
kw.append(("flip", bool(c.params[2]) if c.params[2] == 1 else c.params[2]))
|
|
612
|
+
return render_call("face", [ctx.dec(c.string), c.params[0]], kw)
|
|
613
|
+
|
|
614
|
+
def enc(call, ctx):
|
|
615
|
+
a = Args(call, ["faceset", "index", "right", "flip"])
|
|
616
|
+
if a.get("faceset") is None and len(a.values) <= 1:
|
|
617
|
+
return C(10130)
|
|
618
|
+
return C(10130, [a.int("index", 0), a.int("right", 0), a.int("flip", 0)], ctx.enc(a.str("faceset"), call.node))
|
|
619
|
+
|
|
620
|
+
return dec, enc
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
rich(10130, "face")(_face())
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# -- Map / movement --------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _teleport():
|
|
630
|
+
def dec(c, ctx):
|
|
631
|
+
n = len(c.params)
|
|
632
|
+
if n not in (3, 4):
|
|
633
|
+
return None
|
|
634
|
+
if (n == 3) != (ctx.engine == "2k"):
|
|
635
|
+
return None
|
|
636
|
+
kw = []
|
|
637
|
+
if n == 4 and c.params[3]:
|
|
638
|
+
kw.append(("direction", enum_name(TELEPORT_DIRECTIONS, c.params[3])))
|
|
639
|
+
from .pyexpr import map_src
|
|
640
|
+
|
|
641
|
+
return render_call("teleport", [Raw(src=map_src(c.params[0])), *c.params[1:3]], kw)
|
|
642
|
+
|
|
643
|
+
def enc(call, ctx):
|
|
644
|
+
a = Args(call, ["map", "x", "y", "direction"])
|
|
645
|
+
p = [a.int("map"), a.int("x"), a.int("y")]
|
|
646
|
+
if ctx.engine != "2k" or "direction" in a.values:
|
|
647
|
+
p.append(a.enum("direction", TELEPORT_DIRECTIONS, "retain"))
|
|
648
|
+
return C(10810, p)
|
|
649
|
+
|
|
650
|
+
return dec, enc
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
rich(10810, "teleport")(_teleport())
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# -- Flow ---------------------------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _wait():
|
|
660
|
+
def dec(c, ctx):
|
|
661
|
+
n = len(c.params)
|
|
662
|
+
if n not in (1, 2) or (n == 1) != (ctx.engine == "2k"):
|
|
663
|
+
return None
|
|
664
|
+
kw = []
|
|
665
|
+
if n == 2 and c.params[1]:
|
|
666
|
+
if c.params[1] != 1:
|
|
667
|
+
return None
|
|
668
|
+
kw.append(("key", True))
|
|
669
|
+
t = c.params[0] / 10
|
|
670
|
+
return render_call("wait", [Raw(src=repr(t))], kw)
|
|
671
|
+
|
|
672
|
+
def enc(call, ctx):
|
|
673
|
+
a = Args(call, ["seconds", "key"])
|
|
674
|
+
s = a.get("seconds", 0)
|
|
675
|
+
if not isinstance(s, (int, float)) or isinstance(s, bool):
|
|
676
|
+
raise CompileError("wait(): seconds must be a number", call.node)
|
|
677
|
+
tenths = round(s * 10)
|
|
678
|
+
if abs(tenths - s * 10) > 1e-6:
|
|
679
|
+
raise CompileError("wait(): RPG Maker waits in steps of 0.1 seconds", call.node)
|
|
680
|
+
p = [int(tenths)]
|
|
681
|
+
if ctx.engine != "2k" or a.bool("key"):
|
|
682
|
+
p.append(int(a.bool("key")))
|
|
683
|
+
return C(11410, p)
|
|
684
|
+
|
|
685
|
+
return dec, enc
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
rich(11410, "wait")(_wait())
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _simple(code, name, argnames=()):
|
|
692
|
+
def dec(c, ctx):
|
|
693
|
+
if len(c.params) != len(argnames) or c.string:
|
|
694
|
+
return None
|
|
695
|
+
return render_call(name, c.params)
|
|
696
|
+
|
|
697
|
+
def enc(call, ctx):
|
|
698
|
+
a = Args(call, list(argnames))
|
|
699
|
+
return C(code, [a.int(n) for n in argnames])
|
|
700
|
+
|
|
701
|
+
return dec, enc
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
for _code, _name, _args in [
|
|
705
|
+
(12110, "label", ("id",)),
|
|
706
|
+
(12120, "goto", ("id",)),
|
|
707
|
+
(12420, "game_over", ()),
|
|
708
|
+
(12510, "return_to_title", ()),
|
|
709
|
+
(11340, "wait_for_movement", ()),
|
|
710
|
+
(11350, "halt_all_movement", ()),
|
|
711
|
+
(11530, "memorize_bgm", ()),
|
|
712
|
+
(11540, "play_memorized_bgm", ()),
|
|
713
|
+
(11910, "open_save_menu", ()),
|
|
714
|
+
(11950, "open_main_menu", ()),
|
|
715
|
+
]:
|
|
716
|
+
rich(_code, _name)(_simple(_code, _name, _args))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _sound(code, name, has_fade):
|
|
720
|
+
keys = (["fade"] if has_fade else []) + ["volume", "tempo", "balance"]
|
|
721
|
+
defaults = ([0] if has_fade else []) + [100, 100, 50]
|
|
722
|
+
|
|
723
|
+
def dec(c, ctx):
|
|
724
|
+
if len(c.params) != len(keys):
|
|
725
|
+
return None
|
|
726
|
+
kw = [(k, v) for k, v, d in zip(keys, c.params, defaults) if v != d]
|
|
727
|
+
return render_call(name, [ctx.dec(c.string)], kw)
|
|
728
|
+
|
|
729
|
+
def enc(call, ctx):
|
|
730
|
+
a = Args(call, ["name"] + keys)
|
|
731
|
+
return C(code, [a.int(k, d) for k, d in zip(keys, defaults)], ctx.enc(a.str("name", ""), call.node))
|
|
732
|
+
|
|
733
|
+
return dec, enc
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
rich(11510, "play_bgm")(_sound(11510, "play_bgm", True))
|
|
737
|
+
rich(11550, "play_se")(_sound(11550, "play_se", False))
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
# --------------------------------------------------------------------------
|
|
741
|
+
# Generic form
|
|
742
|
+
# --------------------------------------------------------------------------
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def generic_src(c: Command, ctx: Ctx, with_indent: bool = False) -> str:
|
|
746
|
+
head = CODE_NAMES.get(c.code, c.code)
|
|
747
|
+
kw = []
|
|
748
|
+
if c.string:
|
|
749
|
+
kw.append(("text", ctx.dec(c.string)))
|
|
750
|
+
if with_indent:
|
|
751
|
+
kw.append(("indent", c.indent))
|
|
752
|
+
return render_call("cmd", [head] + list(c.params), kw)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def generic_encode(call: Call, ctx: Ctx) -> tuple[Command, int | None]:
|
|
756
|
+
"""-> (command, explicit indent or None)"""
|
|
757
|
+
if not call.args:
|
|
758
|
+
raise CompileError("cmd() needs a command code or name", call.node)
|
|
759
|
+
head = call.args[0]
|
|
760
|
+
if isinstance(head, str):
|
|
761
|
+
if head not in NAME_CODES:
|
|
762
|
+
raise CompileError("unknown command name %r" % head, call.node)
|
|
763
|
+
code = NAME_CODES[head]
|
|
764
|
+
elif isinstance(head, int) and not isinstance(head, bool):
|
|
765
|
+
code = head
|
|
766
|
+
else:
|
|
767
|
+
raise CompileError("cmd(): first argument must be a command code or name", call.node)
|
|
768
|
+
params = call.args[1:]
|
|
769
|
+
for p in params:
|
|
770
|
+
if not isinstance(p, int) or isinstance(p, bool):
|
|
771
|
+
raise CompileError("cmd(): parameters must be integers, got %r" % (p,), call.node)
|
|
772
|
+
for k in call.kwargs:
|
|
773
|
+
if k not in ("text", "indent"):
|
|
774
|
+
raise CompileError("cmd() got an unknown argument %r" % k, call.node)
|
|
775
|
+
text = call.kwargs.get("text", "")
|
|
776
|
+
if not isinstance(text, str):
|
|
777
|
+
raise CompileError("cmd(): text must be a string", call.node)
|
|
778
|
+
indent = call.kwargs.get("indent")
|
|
779
|
+
return Command(code=code, string=ctx.enc(text, call.node), params=list(params)), indent
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def decode_command(c: Command, ctx: Ctx) -> str:
|
|
783
|
+
"""Best script form for a single command (verified by caller)."""
|
|
784
|
+
dec = RICH_DECODERS.get(c.code)
|
|
785
|
+
if dec is not None:
|
|
786
|
+
try:
|
|
787
|
+
src = dec(c, ctx)
|
|
788
|
+
except (IndexError, ValueError):
|
|
789
|
+
src = None
|
|
790
|
+
if src is not None:
|
|
791
|
+
return src
|
|
792
|
+
return generic_src(c, ctx)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def encode_call(call: Call, ctx: Ctx) -> tuple[Command, int | None]:
|
|
796
|
+
if call.name == "cmd":
|
|
797
|
+
return generic_encode(call, ctx)
|
|
798
|
+
enc = RICH_ENCODERS.get(call.name)
|
|
799
|
+
if enc is None:
|
|
800
|
+
raise CompileError("unknown command %s()" % call.name, call.node)
|
|
801
|
+
return enc(call, ctx), None
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
# --------------------------------------------------------------------------
|
|
805
|
+
# Comments with database names (purely informative)
|
|
806
|
+
# --------------------------------------------------------------------------
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def hint(c: Command, ctx: Ctx) -> str | None:
|
|
810
|
+
def nm(kind, i):
|
|
811
|
+
if i in ctx.handles.get(kind, {}):
|
|
812
|
+
return None # written by name in the code already
|
|
813
|
+
n = ctx.names.get(kind, {}).get(i)
|
|
814
|
+
return "".join(ch for ch in n if ch.isprintable()) if n else None
|
|
815
|
+
|
|
816
|
+
p = c.params
|
|
817
|
+
try:
|
|
818
|
+
if c.code == 10220 and p[0] == 0:
|
|
819
|
+
return nm("variables", p[1])
|
|
820
|
+
if c.code == 10210 and p[0] == 0:
|
|
821
|
+
return nm("switches", p[1])
|
|
822
|
+
if c.code == 12010 and p[0] == 0:
|
|
823
|
+
return nm("switches", p[1])
|
|
824
|
+
if c.code == 12010 and p[0] == 1:
|
|
825
|
+
return nm("variables", p[1])
|
|
826
|
+
if c.code == 10320 and p[1] == 0:
|
|
827
|
+
return nm("items", p[2])
|
|
828
|
+
if c.code == 10330 and p[1] == 0:
|
|
829
|
+
return nm("actors", p[2])
|
|
830
|
+
if c.code == 12330 and p[0] == 0:
|
|
831
|
+
return nm("commonevents", p[1])
|
|
832
|
+
except IndexError:
|
|
833
|
+
return None
|
|
834
|
+
return None
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
# --------------------------------------------------------------------------
|
|
838
|
+
# `size = N` in database files
|
|
839
|
+
# --------------------------------------------------------------------------
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
class TableSize(BaseModel):
|
|
843
|
+
"""``size = N`` at the top of a database file: how many slots the table
|
|
844
|
+
has in the editor. Travels with the entries as a pseudo-entry of id 0,
|
|
845
|
+
so that it is merged like them."""
|
|
846
|
+
|
|
847
|
+
id: int = 0
|
|
848
|
+
size: int
|
|
849
|
+
lineno: int | None = None
|
|
850
|
+
|
|
851
|
+
def key(self) -> tuple:
|
|
852
|
+
return ("size", self.size)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
SIZE_NOTE = "slots in the editor; lower it to drop empty slots at the end"
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def config_src(size: int, indent: str = " ") -> list[str]:
|
|
859
|
+
"""The ``class Config:`` block of a database class."""
|
|
860
|
+
return [indent + "class Config:", indent + " size = %d # %s" % (size, SIZE_NOTE)]
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def size_stmt(stmt: ast.stmt) -> TableSize | None:
|
|
864
|
+
"""The TableSize of a ``class Config: size = N`` block (or of a plain
|
|
865
|
+
``size = N``, as older rpgsync versions wrote it), None for other statements."""
|
|
866
|
+
if isinstance(stmt, ast.ClassDef) and stmt.name == "Config":
|
|
867
|
+
found = None
|
|
868
|
+
for sub in stmt.body:
|
|
869
|
+
if isinstance(sub, ast.Pass) or (
|
|
870
|
+
isinstance(sub, ast.Expr) and isinstance(sub.value, ast.Constant) and isinstance(sub.value.value, str)
|
|
871
|
+
):
|
|
872
|
+
continue
|
|
873
|
+
size = size_stmt(sub) if not isinstance(sub, ast.ClassDef) else None
|
|
874
|
+
if size is None:
|
|
875
|
+
raise CompileError("class Config may only contain `size = N`", sub)
|
|
876
|
+
if found is not None:
|
|
877
|
+
raise CompileError("size is assigned twice", sub)
|
|
878
|
+
found = size
|
|
879
|
+
if found is None:
|
|
880
|
+
raise CompileError("class Config needs `size = N` (the number of slots)", stmt)
|
|
881
|
+
return found
|
|
882
|
+
if not (
|
|
883
|
+
isinstance(stmt, ast.Assign)
|
|
884
|
+
and len(stmt.targets) == 1
|
|
885
|
+
and isinstance(stmt.targets[0], ast.Name)
|
|
886
|
+
and stmt.targets[0].id == "size"
|
|
887
|
+
):
|
|
888
|
+
return None
|
|
889
|
+
v = stmt.value
|
|
890
|
+
if not (
|
|
891
|
+
isinstance(v, ast.Constant) and isinstance(v.value, int) and not isinstance(v.value, bool) and v.value >= 0
|
|
892
|
+
):
|
|
893
|
+
raise CompileError("size must be a number of slots, e.g. size = 100", stmt)
|
|
894
|
+
return TableSize(size=v.value, lineno=stmt.lineno)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def split_size(specs: list) -> tuple[list, int | None]:
|
|
898
|
+
"""(entries, size) from a spec list that may hold a TableSize."""
|
|
899
|
+
sizes = [s.size for s in specs if isinstance(s, TableSize)]
|
|
900
|
+
return [s for s in specs if not isinstance(s, TableSize)], (sizes[0] if sizes else None)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def table_length(current: int, size: int | None, ids: list[int]) -> int:
|
|
904
|
+
"""Number of slots after an update. Without `size` the table keeps its
|
|
905
|
+
length; ids past the end grow it. With `size`, lowering it cuts the
|
|
906
|
+
slots at the end (refused while entries still use them)."""
|
|
907
|
+
top = max(ids, default=0)
|
|
908
|
+
if size is None:
|
|
909
|
+
return max(current, top)
|
|
910
|
+
blocked = sorted(i for i in ids if size < i <= current)
|
|
911
|
+
if blocked:
|
|
912
|
+
shown = (
|
|
913
|
+
", ".join(map(str, blocked))
|
|
914
|
+
if len(blocked) <= 5
|
|
915
|
+
else "%d of them, %d to %d" % (len(blocked), blocked[0], blocked[-1])
|
|
916
|
+
)
|
|
917
|
+
raise ValueError(
|
|
918
|
+
"size = %d would delete entries still listed (%s): remove them first or raise size" % (size, shown)
|
|
919
|
+
)
|
|
920
|
+
return max(size, top)
|