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/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)