ff9mapkit 1.0.0b3__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.
Files changed (193) hide show
  1. ff9mapkit/__init__.py +18 -0
  2. ff9mapkit/__main__.py +36 -0
  3. ff9mapkit/_animdb.py +2994 -0
  4. ff9mapkit/_animdb_all.py +14125 -0
  5. ff9mapkit/_fieldtable.py +1516 -0
  6. ff9mapkit/_fieldtext.py +845 -0
  7. ff9mapkit/_held_poses.py +44 -0
  8. ff9mapkit/_itemdb.py +65 -0
  9. ff9mapkit/_modeldb.py +725 -0
  10. ff9mapkit/_narrowmap_data.py +10 -0
  11. ff9mapkit/_npcparams.py +634 -0
  12. ff9mapkit/_regen_animdb.py +72 -0
  13. ff9mapkit/_regen_animdb_all.py +66 -0
  14. ff9mapkit/_regen_fieldtable.py +95 -0
  15. ff9mapkit/_regen_fieldtext.py +66 -0
  16. ff9mapkit/_regen_modeldb.py +67 -0
  17. ff9mapkit/_regen_npcparams.py +123 -0
  18. ff9mapkit/_regen_scenedb.py +57 -0
  19. ff9mapkit/_scenedb.py +869 -0
  20. ff9mapkit/abilities.py +225 -0
  21. ff9mapkit/animations.py +120 -0
  22. ff9mapkit/archetypes.py +218 -0
  23. ff9mapkit/areatitle.py +76 -0
  24. ff9mapkit/battle/__init__.py +21 -0
  25. ff9mapkit/battle/abilityfeatures.py +294 -0
  26. ff9mapkit/battle/actiondelta.py +441 -0
  27. ff9mapkit/battle/aiauthor.py +305 -0
  28. ff9mapkit/battle/ailint.py +140 -0
  29. ff9mapkit/battle/aipatch.py +175 -0
  30. ff9mapkit/battle/battleai.py +148 -0
  31. ff9mapkit/battle/battlecsv.py +390 -0
  32. ff9mapkit/battle/battlepatch.py +395 -0
  33. ff9mapkit/battle/build.py +558 -0
  34. ff9mapkit/battle/camera_codec.py +332 -0
  35. ff9mapkit/battle/camera_data.py +128 -0
  36. ff9mapkit/battle/characterdelta.py +789 -0
  37. ff9mapkit/battle/event_data.py +72 -0
  38. ff9mapkit/battle/extract.py +540 -0
  39. ff9mapkit/battle/fbx.py +223 -0
  40. ff9mapkit/battle/reskin.py +149 -0
  41. ff9mapkit/battle/scene_codec.py +314 -0
  42. ff9mapkit/battle/scene_data.py +369 -0
  43. ff9mapkit/battle/scenelint.py +125 -0
  44. ff9mapkit/battle/seqasm.py +131 -0
  45. ff9mapkit/battle/seqauthor.py +220 -0
  46. ff9mapkit/battle/seqcodec.py +300 -0
  47. ff9mapkit/battle/seqdis.py +106 -0
  48. ff9mapkit/battle/seqpatch.py +137 -0
  49. ff9mapkit/battle_bgm.py +133 -0
  50. ff9mapkit/binutils.py +60 -0
  51. ff9mapkit/build.py +5445 -0
  52. ff9mapkit/campaign.py +1276 -0
  53. ff9mapkit/catalog.py +316 -0
  54. ff9mapkit/chain.py +358 -0
  55. ff9mapkit/cli.py +3114 -0
  56. ff9mapkit/config.py +360 -0
  57. ff9mapkit/content/__init__.py +13 -0
  58. ff9mapkit/content/areatitle.py +36 -0
  59. ff9mapkit/content/ate.py +118 -0
  60. ff9mapkit/content/camera.py +123 -0
  61. ff9mapkit/content/chest.py +186 -0
  62. ff9mapkit/content/choice.py +163 -0
  63. ff9mapkit/content/conductor.py +217 -0
  64. ff9mapkit/content/cutscene.py +290 -0
  65. ff9mapkit/content/encounter.py +41 -0
  66. ff9mapkit/content/entry_settle.py +50 -0
  67. ff9mapkit/content/equipment.py +93 -0
  68. ff9mapkit/content/event.py +191 -0
  69. ff9mapkit/content/gateway.py +101 -0
  70. ff9mapkit/content/inventory.py +59 -0
  71. ff9mapkit/content/itemdata.py +644 -0
  72. ff9mapkit/content/itemtext.py +168 -0
  73. ff9mapkit/content/jump.py +114 -0
  74. ff9mapkit/content/ladder.py +633 -0
  75. ff9mapkit/content/movement.py +53 -0
  76. ff9mapkit/content/music.py +97 -0
  77. ff9mapkit/content/npc.py +348 -0
  78. ff9mapkit/content/object.py +340 -0
  79. ff9mapkit/content/onentry.py +135 -0
  80. ff9mapkit/content/party.py +111 -0
  81. ff9mapkit/content/pathfind.py +138 -0
  82. ff9mapkit/content/platform.py +314 -0
  83. ff9mapkit/content/player.py +168 -0
  84. ff9mapkit/content/prop.py +75 -0
  85. ff9mapkit/content/region.py +340 -0
  86. ff9mapkit/content/reinit.py +59 -0
  87. ff9mapkit/content/savepoint.py +90 -0
  88. ff9mapkit/content/shop.py +178 -0
  89. ff9mapkit/content/sps_trigger.py +66 -0
  90. ff9mapkit/content/startup.py +71 -0
  91. ff9mapkit/content/synthesis.py +106 -0
  92. ff9mapkit/content/text.py +183 -0
  93. ff9mapkit/content/textcarry.py +290 -0
  94. ff9mapkit/content/verbatim.py +86 -0
  95. ff9mapkit/content/walkmesh_hotfix.py +38 -0
  96. ff9mapkit/data/__init__.py +48 -0
  97. ff9mapkit/data/_regen_provenance.py +142 -0
  98. ff9mapkit/data/provenance/blank.es.patch +1 -0
  99. ff9mapkit/data/provenance/blank.fr.patch +1 -0
  100. ff9mapkit/data/provenance/blank.gr.patch +1 -0
  101. ff9mapkit/data/provenance/blank.it.patch +1 -0
  102. ff9mapkit/data/provenance/blank.jp.patch +1 -0
  103. ff9mapkit/data/provenance/blank.uk.patch +1 -0
  104. ff9mapkit/data/provenance/blank.us.patch +1 -0
  105. ff9mapkit/data/provenance/manifest.json +65 -0
  106. ff9mapkit/data/provenance/region_template.patch +1 -0
  107. ff9mapkit/data/reference_arcs.toml +89 -0
  108. ff9mapkit/data/region_catalog.toml +593 -0
  109. ff9mapkit/deploystack.py +358 -0
  110. ff9mapkit/dialogue.py +803 -0
  111. ff9mapkit/eb/__init__.py +12 -0
  112. ff9mapkit/eb/_exprtable.py +59 -0
  113. ff9mapkit/eb/_membertable.py +38 -0
  114. ff9mapkit/eb/_optables.py +537 -0
  115. ff9mapkit/eb/_regen_optables.py +76 -0
  116. ff9mapkit/eb/cmdasm.py +323 -0
  117. ff9mapkit/eb/disasm.py +332 -0
  118. ff9mapkit/eb/edit.py +439 -0
  119. ff9mapkit/eb/exprasm.py +158 -0
  120. ff9mapkit/eb/model.py +178 -0
  121. ff9mapkit/eb/opcodes.py +463 -0
  122. ff9mapkit/eblint.py +177 -0
  123. ff9mapkit/editor/__init__.py +20 -0
  124. ff9mapkit/editor/app.py +950 -0
  125. ff9mapkit/editor/battle_forms.py +240 -0
  126. ff9mapkit/editor/breadcrumb.py +89 -0
  127. ff9mapkit/editor/dialogs.py +116 -0
  128. ff9mapkit/editor/feedback.py +208 -0
  129. ff9mapkit/editor/forms.py +632 -0
  130. ff9mapkit/editor/graphview.py +350 -0
  131. ff9mapkit/editor/jobs.py +342 -0
  132. ff9mapkit/editor/model.py +243 -0
  133. ff9mapkit/editor/picker.py +120 -0
  134. ff9mapkit/editor/theme.py +212 -0
  135. ff9mapkit/eventscan.py +1441 -0
  136. ff9mapkit/extract.py +2279 -0
  137. ff9mapkit/flags.py +693 -0
  138. ff9mapkit/forkreport.py +1383 -0
  139. ff9mapkit/hub.py +477 -0
  140. ff9mapkit/idgated.py +101 -0
  141. ff9mapkit/infohub.py +580 -0
  142. ff9mapkit/items.py +63 -0
  143. ff9mapkit/itemstats.py +346 -0
  144. ff9mapkit/journey.py +1902 -0
  145. ff9mapkit/keyitems.py +93 -0
  146. ff9mapkit/logic_add.py +632 -0
  147. ff9mapkit/logic_edit.py +728 -0
  148. ff9mapkit/logic_map.py +526 -0
  149. ff9mapkit/pack.py +175 -0
  150. ff9mapkit/playerswap.py +231 -0
  151. ff9mapkit/prop_archetypes.py +228 -0
  152. ff9mapkit/provision.py +282 -0
  153. ff9mapkit/refarc.py +825 -0
  154. ff9mapkit/save.py +337 -0
  155. ff9mapkit/save_items.py +1673 -0
  156. ff9mapkit/scene/__init__.py +11 -0
  157. ff9mapkit/scene/arena.py +63 -0
  158. ff9mapkit/scene/bgart.py +140 -0
  159. ff9mapkit/scene/bgi.py +732 -0
  160. ff9mapkit/scene/bgs.py +174 -0
  161. ff9mapkit/scene/bgx.py +185 -0
  162. ff9mapkit/scene/cam.py +345 -0
  163. ff9mapkit/scene/guide.py +311 -0
  164. ff9mapkit/scene/paint.py +506 -0
  165. ff9mapkit/scene/placeholder.py +107 -0
  166. ff9mapkit/sjbinary.py +285 -0
  167. ff9mapkit/sps/__init__.py +17 -0
  168. ff9mapkit/sps/author.py +294 -0
  169. ff9mapkit/sps/catalog.py +88 -0
  170. ff9mapkit/sps/codec.py +264 -0
  171. ff9mapkit/sps/edit.py +184 -0
  172. ff9mapkit/sps/lint.py +58 -0
  173. ff9mapkit/sps/render.py +116 -0
  174. ff9mapkit/sps/templates.py +47 -0
  175. ff9mapkit/sps/texture.py +131 -0
  176. ff9mapkit/walkmesh_hotfixes.py +163 -0
  177. ff9mapkit/workspace/__init__.py +18 -0
  178. ff9mapkit/workspace/battledoc.py +985 -0
  179. ff9mapkit/workspace/builddoc.py +607 -0
  180. ff9mapkit/workspace/forms_qt.py +586 -0
  181. ff9mapkit/workspace/importdoc.py +665 -0
  182. ff9mapkit/workspace/mapview.py +131 -0
  183. ff9mapkit/workspace/palette.py +85 -0
  184. ff9mapkit/workspace/savedoc.py +664 -0
  185. ff9mapkit/workspace/shell.py +6907 -0
  186. ff9mapkit/workspace/style.py +105 -0
  187. ff9mapkit/workspace/tuningdialog.py +223 -0
  188. ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
  189. ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
  190. ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
  191. ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
  192. ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
  193. ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
ff9mapkit/eb/model.py ADDED
@@ -0,0 +1,178 @@
1
+ """Structured, byte-exact model of a FF9 field event script (``.eb``).
2
+
3
+ An :class:`EbScript` is a *parsed view* over the raw bytes. The raw bytes are always the
4
+ source of truth: parsing only derives structure for queries and locators, and every edit
5
+ (in :mod:`ff9mapkit.eb.edit`) splices the existing bytes rather than re-serializing from the
6
+ parse. So ``EbScript.from_bytes(x).to_bytes() == x`` holds for any valid input — the round
7
+ trip is the identity. (Verified across every shipped/room ``.eb`` in the Phase-1 tests.)
8
+
9
+ File layout (little-endian), reverse-engineered + confirmed against Memoria's EventEngine
10
+ and the project's existing tooling:
11
+
12
+ [0x00] 'EV' magic
13
+ [0x02] u8 unknown
14
+ [0x03] u8 entryCount number of entry-table slots
15
+ [0x04..0x2B] header (opaque; preserved verbatim)
16
+ [0x2C..0x7F] 84 bytes PSX field-name string (FF9 text encoding; per-language,
17
+ cosmetic/debug — the field is resolved by DictionaryPatch
18
+ + filename, not by this string)
19
+ [0x80] entry table entryCount * 8 bytes, each:
20
+ off:u16 (entry start, RELATIVE to 0x80)
21
+ sz :u16 (entry byte length; 0 = empty slot)
22
+ loc:u8 flags:u8 pad:u16
23
+ entry body (when sz > 0):
24
+ type:u8 funcCount:u8
25
+ funcCount * (tag:u16, fpos:u16) func table; fpos RELATIVE to entryStart+2
26
+ ... bytecode ...
27
+ => a function's code starts at entryStart + 2 + fpos and runs to the next func's
28
+ start (or the entry end). This "funcBasePos = entryStart + 2" convention is the one
29
+ subtlety that trips up naive parsers.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from dataclasses import dataclass
35
+
36
+ from ..binutils import u16
37
+ from . import disasm
38
+
39
+ MAGIC = b"EV"
40
+ ENTRY_TABLE_OFF = 0x80 # 128
41
+ ENTRY_SLOT_SIZE = 8
42
+ NAME_OFF = 0x2C # 44
43
+ NAME_LEN = 84
44
+ HEADER_LEN = ENTRY_TABLE_OFF # everything before the entry table (header + name)
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class Func:
49
+ """One function within an entry."""
50
+
51
+ index: int
52
+ tag: int
53
+ fpos: int # relative to entryStart + 2
54
+ abs_start: int # absolute byte offset of this function's code
55
+ abs_end: int # absolute byte offset where it ends (next func / entry end)
56
+
57
+ @property
58
+ def length(self) -> int:
59
+ return self.abs_end - self.abs_start
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Entry:
64
+ """One entry-table slot and (if non-empty) its parsed body."""
65
+
66
+ index: int
67
+ off: int # relative to 0x80
68
+ size: int
69
+ loc: int
70
+ flags: int
71
+ abs_start: int # 0x80 + off
72
+ abs_end: int # abs_start + size
73
+ type: int | None
74
+ func_count: int
75
+ funcs: tuple[Func, ...]
76
+
77
+ @property
78
+ def empty(self) -> bool:
79
+ return self.size == 0
80
+
81
+ def func_by_tag(self, tag: int) -> Func | None:
82
+ for f in self.funcs:
83
+ if f.tag == tag:
84
+ return f
85
+ return None
86
+
87
+
88
+ class EbScript:
89
+ """Parsed, byte-exact view of a ``.eb`` field script."""
90
+
91
+ def __init__(self, data: bytes):
92
+ self.data = bytes(data)
93
+ if self.data[:2] != MAGIC:
94
+ raise ValueError(f"not an .eb script (magic={self.data[:2]!r}, expected {MAGIC!r})")
95
+ self.entry_count = self.data[3]
96
+ self.entries: tuple[Entry, ...] = tuple(self._parse_entry(i) for i in range(self.entry_count))
97
+
98
+ # -- construction / serialization --
99
+ @classmethod
100
+ def from_bytes(cls, data: bytes) -> "EbScript":
101
+ return cls(data)
102
+
103
+ @classmethod
104
+ def from_file(cls, path) -> "EbScript":
105
+ with open(path, "rb") as fh:
106
+ return cls(fh.read())
107
+
108
+ def to_bytes(self) -> bytes:
109
+ return self.data
110
+
111
+ # -- parsing --
112
+ def _slot_off(self, i: int) -> int:
113
+ return ENTRY_TABLE_OFF + i * ENTRY_SLOT_SIZE
114
+
115
+ def _parse_entry(self, i: int) -> Entry:
116
+ d = self.data
117
+ so = self._slot_off(i)
118
+ off = u16(d, so)
119
+ size = u16(d, so + 2)
120
+ loc = d[so + 4]
121
+ flags = d[so + 5]
122
+ abs_start = ENTRY_TABLE_OFF + off
123
+ abs_end = abs_start + size
124
+ if size == 0:
125
+ return Entry(i, off, size, loc, flags, abs_start, abs_end, None, 0, ())
126
+ etype = d[abs_start]
127
+ fc = d[abs_start + 1]
128
+ fbase = abs_start + 2
129
+ raw_funcs = []
130
+ q = fbase
131
+ for _ in range(fc):
132
+ tag = u16(d, q)
133
+ fpos = u16(d, q + 2)
134
+ raw_funcs.append((tag, fpos))
135
+ q += 4
136
+ funcs = []
137
+ for fi, (tag, fpos) in enumerate(raw_funcs):
138
+ fstart = fbase + fpos
139
+ fend = (fbase + raw_funcs[fi + 1][1]) if fi + 1 < fc else abs_end
140
+ funcs.append(Func(fi, tag, fpos, fstart, fend))
141
+ return Entry(i, off, size, loc, flags, abs_start, abs_end, etype, fc, tuple(funcs))
142
+
143
+ # -- convenient accessors --
144
+ @property
145
+ def name_region(self) -> bytes:
146
+ """The 84-byte PSX field-name field (per-language; preserved across edits)."""
147
+ return self.data[NAME_OFF:NAME_OFF + NAME_LEN]
148
+
149
+ @property
150
+ def main(self) -> Entry:
151
+ """Entry 0 — the Main entry (Main_Init = its first function, tag 0)."""
152
+ return self.entries[0]
153
+
154
+ def entry(self, i: int) -> Entry:
155
+ return self.entries[i]
156
+
157
+ def free_slots(self) -> list[int]:
158
+ """Indices of empty entry-table slots, in order."""
159
+ return [e.index for e in self.entries if e.empty]
160
+
161
+ def first_free_slot(self) -> int:
162
+ """Index of the first empty entry-table slot. When the table is FULL, returns
163
+ :attr:`entry_count` (the index one past the last slot) -- the signal for
164
+ :func:`ff9mapkit.eb.edit.append_entry` to grow the table. Real fields run to ~30 entries, so a
165
+ content-dense field (e.g. an Ice Cavern screen with 6 jumps) legitimately needs more than the
166
+ blank template's 10 slots; growth is on-demand so fields that fit stay byte-identical."""
167
+ for e in self.entries:
168
+ if e.empty:
169
+ return e.index
170
+ return self.entry_count
171
+
172
+ def instrs(self, func: Func):
173
+ """Iterate decoded instructions of a function."""
174
+ yield from disasm.iter_code(self.data, func.abs_start, func.abs_end)
175
+
176
+ def __repr__(self) -> str:
177
+ used = sum(1 for e in self.entries if not e.empty)
178
+ return f"<EbScript {len(self.data)}B entries={self.entry_count} used={used}>"
@@ -0,0 +1,463 @@
1
+ """Encoders for the event-script opcodes the kit emits.
2
+
3
+ A single :func:`encode` builds the exact byte sequence for an opcode + immediate operands,
4
+ following the engine's reader rules:
5
+ * extended opcodes (>= 0x100) get a leading ``0xFF`` page byte,
6
+ * opcodes >= 0x10 that take operands carry a 1-byte ``argFlag`` bitmask (0 = all immediate),
7
+ * each immediate is little-endian, width per :func:`~ff9mapkit.eb.disasm.argsize`,
8
+ * a set ``argFlag`` bit means that operand is a pre-encoded expression-token blob (``bytes``).
9
+
10
+ The named helpers below cover everything the content injectors produce. Each was checked
11
+ against the exact byte strings the original tools emitted (e.g. ``run_sound_code(0, 9)`` ==
12
+ ``C5 00 00 00 09 00``; ``set_control_direction(-1, -1)`` == ``67 00 FF FF``).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from ._optables import OP_ARG_COUNT, OP_NAMES
18
+ from .disasm import argsize
19
+
20
+ _NAME_TO_OP = {v: k for k, v in OP_NAMES.items()}
21
+
22
+
23
+ def resolve(op) -> int:
24
+ """Accept an int opcode or a mnemonic string; return the int opcode."""
25
+ if isinstance(op, str):
26
+ if op not in _NAME_TO_OP:
27
+ raise KeyError(f"unknown opcode mnemonic {op!r}")
28
+ return _NAME_TO_OP[op]
29
+ return op
30
+
31
+
32
+ def _imm(v: int, size: int) -> bytes:
33
+ """Little-endian, two's-complement for negatives, masked to ``size`` bytes."""
34
+ if size <= 0:
35
+ return b""
36
+ return (v & ((1 << (8 * size)) - 1)).to_bytes(size, "little")
37
+
38
+
39
+ def encode(op, *args, arg_flags: int = 0) -> bytes:
40
+ """Encode one instruction. ``args`` are ints (immediates) or, for set arg_flags bits, bytes."""
41
+ op = resolve(op)
42
+ if op < len(OP_ARG_COUNT) and OP_ARG_COUNT[op] < 0:
43
+ raise ValueError(f"opcode 0x{op:02X} has a variable operand count; encode it explicitly")
44
+ head = bytes([0xFF, op & 0xFF]) if op >= 0x100 else bytes([op])
45
+ argc = OP_ARG_COUNT[op] if op < len(OP_ARG_COUNT) else 0
46
+ body = bytearray()
47
+ if op >= 0x10 and argc != 0:
48
+ body.append(arg_flags & 0xFF)
49
+ for i, a in enumerate(args):
50
+ if arg_flags & (1 << i):
51
+ body += bytes(a) # pre-encoded expression operand
52
+ else:
53
+ body += _imm(int(a), argsize(op, i))
54
+ return head + bytes(body)
55
+
56
+
57
+ # --- init/dispatch (entry activators) ---
58
+ def init_code(slot: int, arg: int = 0) -> bytes: # 0x07
59
+ return encode(0x07, slot, arg)
60
+
61
+
62
+ def init_region(slot: int, arg: int = 0) -> bytes: # 0x08
63
+ return encode(0x08, slot, arg)
64
+
65
+
66
+ def run_script_sync(level: int, uid: int, tag: int) -> bytes: # 0x14 (REQEW) argsize [1,1,1]
67
+ """RunScriptSync(level, uid, tag): run function ``tag`` on the object with this UID and WAIT for it
68
+ to return. The FF9 ladder idiom: a region calls ``RunScriptSync(2, 250, <climb_tag>)`` to run the
69
+ PLAYER's (UID 250) climb function in the player's own context (so its moves move the player),
70
+ synchronously. Decoded from Treno/Residence's real ladder."""
71
+ return encode(0x14, level, uid, tag)
72
+
73
+
74
+ def run_script(level: int, uid: int, tag: int) -> bytes: # 0x12 (REQSW) argsize [1,1,1]
75
+ """RunScript(level, uid, tag): run function ``tag`` on the object with this UID and CONTINUE (the callee
76
+ runs concurrently in its own context). Blocks only until the callee's entry script-level is free. The
77
+ multi-actor conductor's lever for an animated WALK on another actor (base ``Walk`` acts on gExec, so the
78
+ walk must run INSIDE the actor)."""
79
+ return encode(0x12, level, uid, tag)
80
+
81
+
82
+ def run_script_async(level: int, uid: int, tag: int) -> bytes: # 0x10 (REQ) argsize [1,1,1]
83
+ """RunScriptAsync(level, uid, tag): run function ``tag`` on the object with this UID, fire-and-return.
84
+ The conductor's parallel fan-out (start several actors at once)."""
85
+ return encode(0x10, level, uid, tag)
86
+
87
+
88
+ def run_shared_script(entry: int) -> bytes: # 0x43 (STARTSEQ) argsize [1]
89
+ """RunSharedScript(entry): spawn a shared coroutine running ``entry`` bound to the EXECUTING object
90
+ (uid = gExec.uid + cSeqOfs); only ONE shared script per object at a time."""
91
+ return encode(0x43, entry)
92
+
93
+
94
+ def wait_shared_script() -> bytes: # 0x44 (WAITSEQ) 0 args
95
+ """WaitSharedScript(): block until the shared script THIS object spawned (via RunSharedScript) ends.
96
+ NB this joins only the executing object's OWN shared script -- it is not a global async barrier."""
97
+ return encode(0x44)
98
+
99
+
100
+ # --- targeted "Ex" opcodes: drive an object BY UID (operand 0) -- a CONDUCTOR drives any actor from one
101
+ # function without a context switch. uid == the object's entry slot (sid); 250 = the control character.
102
+ # (See content/conductor.py + memory project-ff9-cutscene-multiactor; arg layout verified vs _optables.)
103
+ def window_sync_ex(uid: int, win: int, flags: int, text_id: int) -> bytes: # 0x95 argsize [1,1,1,2]
104
+ """WindowSyncEx(uid, win, flags, text_id): open a dialogue window attributed to the object ``uid`` (its
105
+ tail points at that actor), blocking until dismissed. The conductor's per-actor ``say``."""
106
+ return encode(0x95, uid, win, flags, text_id)
107
+
108
+
109
+ def turn_instant_ex(uid: int, angle: int) -> bytes: # 0x87 (TurnInstantEx) argsize [1,1]
110
+ """TurnInstantEx(uid, angle): face ``angle`` INSTANTLY on object ``uid`` (0=S,64=W,128=N,192=E)."""
111
+ return encode(0x87, uid, angle)
112
+
113
+
114
+ def timed_turn_ex(uid: int, angle: int, speed: int = 16) -> bytes: # 0xBB argsize [1,1,1]
115
+ """TimedTurnEx(uid, angle, speed): face ``angle`` animated on object ``uid`` (pair WaitTurnEx -- but
116
+ avoid WaitTurnEx on a player-cloned actor: its turn anim may not complete -> hang)."""
117
+ return encode(0xBB, uid, angle, speed)
118
+
119
+
120
+ def wait_turn_ex(uid: int) -> bytes: # 0xBC (WaitTurnEx) argsize [1]
121
+ """WaitTurnEx(uid): block until object ``uid`` finishes its TimedTurnEx. Hangs on a player clone."""
122
+ return encode(0xBC, uid)
123
+
124
+
125
+ def run_animation_ex(uid: int, anim: int) -> bytes: # 0xBD (RunAnimationEx) argsize [1,2]
126
+ """RunAnimationEx(uid, anim): play ``anim`` on object ``uid`` (async; pair a fixed Wait, not WaitAnimationEx)."""
127
+ return encode(0xBD, uid, anim)
128
+
129
+
130
+ def wait_animation_ex(uid: int) -> bytes: # 0xBE (WaitAnimationEx) argsize [1]
131
+ """WaitAnimationEx(uid): block until object ``uid``'s animation ends. Hangs on a player clone -> avoid."""
132
+ return encode(0xBE, uid)
133
+
134
+
135
+ def bubble(state: int) -> bytes: # 0x68 (BUBBLE) argsize [1]
136
+ """Bubble(state): show(1)/hide(0) the floating "!" action-available prompt over the player. A
137
+ ladder/sign region shows it on tread so the player knows to press the action button."""
138
+ return encode(0x68, state)
139
+
140
+
141
+ def ate(mode: int) -> bytes: # 0xD7 (AICON) argsize [1]
142
+ """ATE(mode): arm/hide the blinking on-field "Active Time Event / Press SELECT" prompt (engine
143
+ ``EIcon.SetAIcon`` -> ``sAIconMode``, ``EIcon.cs:416-454``). ``mode`` is a 3-bit FLAG WORD, not an
144
+ enum: ``>0`` = enable (``0`` = off/hide); ``&3 == 2`` = Gray banner (else Blue); ``&4`` = FORCE-show
145
+ (draw even without user control). ``ProcessAIcon`` draws only when ``sAIconMode>0 && (mode&4 ||
146
+ GetUserControl())``. The shipping game uses exactly 4 of the 8 combos: 1 = Blue/optional (free-roam
147
+ hub, player has control), 6 = Gray+force = the grey UNSKIPPABLE banner (forced cutscene), 0 = clear.
148
+ AVOID 5 (Blue+force, field 206's lone real site) -- it re-flashes the SELECT glyph during auto-play
149
+ (use 6 for a forced scene). Verified ``d7 00 05`` = ATE(5) vs field 206. See docs/ATE_SYSTEM.md."""
150
+ return encode(0xD7, mode)
151
+
152
+
153
+ def init_object(slot: int, arg: int = 0) -> bytes: # 0x09
154
+ return encode(0x09, slot, arg)
155
+
156
+
157
+ def menu(menu_id: int, sub_id: int = 0) -> bytes: # 0x75 (Menu) argsize [1,1]
158
+ """Menu(menu_id, sub_id): open a field menu via ``EventService.StartMenu`` -> ``FF9Menu_Command``.
159
+ The menu enum (``EventService.cs``): 1 = name, 2 = shop, **4 + sub_id 0 = the SAVE menu**
160
+ (``OpenSaveMenu`` -> ``SaveLoadUI.SerializeType.Save``), 5 = chocograph. ``menu(4, 0)`` is the
161
+ functional save point, verified byte-exact (``75 00 04 00``) against the real Dali save moogle
162
+ (field 122 entry 5 tag 3)."""
163
+ return encode(0x75, menu_id, sub_id)
164
+
165
+
166
+ # --- flow / misc ---
167
+ def wait(n: int) -> bytes: # 0x22
168
+ return encode(0x22, n)
169
+
170
+
171
+ RETURN = bytes([0x04]) # function return (level-0 return drives ExitBattleEnd)
172
+ NOP = bytes([0x00])
173
+ ENABLE_MOVE = bytes([0x2E]) # EnableMove (0 args) -- give the player control
174
+ DISABLE_MOVE = bytes([0x2D]) # DisableMove (0 args) -- lock control (cutscenes)
175
+ DEFINE_PLAYER_CHARACTER = bytes([0x2C])
176
+
177
+
178
+ # --- objects / models / animation ---
179
+ def set_model(model: int, animset: int) -> bytes: # 0x2F argsize [2,1]
180
+ return encode(0x2F, model, animset)
181
+
182
+
183
+ def create_object(x: int, z: int) -> bytes: # 0x1D argsize [2,2]
184
+ return encode(0x1D, x, z)
185
+
186
+
187
+ def set_stand_animation(anim: int) -> bytes: # 0x33 argsize [2]
188
+ return encode(0x33, anim)
189
+
190
+
191
+ def set_control_direction(x: int, y: int) -> bytes: # 0x67 (TWIST)
192
+ return encode(0x67, x, y)
193
+
194
+
195
+ # --- actor movement / animation / turning (cutscene "v2" steps) ---
196
+ # These all act on the EXECUTING object (gExec) -- so they're emitted into a specific NPC's own
197
+ # function (its Init choreography), where gExec == that NPC. Grounded in the engine's DoEventCode
198
+ # handlers + real cutscene scripts (e.g. Gargan/Kuja walk functions: SetWalkSpeed -> RunAnimation ->
199
+ # WaitAnimation -> InitWalk -> Walk).
200
+ def init_walk() -> bytes: # 0x25 (CLRDIST) 0 args
201
+ """InitWalk(): make the following Walk synchronous (the canonical idiom; Walk also self-blocks)."""
202
+ return encode(0x25)
203
+
204
+
205
+ def walk(x: int, z: int) -> bytes: # 0x23 (MOVE) argsize [2, 2]
206
+ """Walk(x, z): walk the executing actor to world (x, z); blocks (stay()) until it arrives."""
207
+ return encode(0x23, x, z)
208
+
209
+
210
+ def set_walk_speed(speed: int) -> bytes: # 0x26 (MSPEED) argsize [1]
211
+ """SetWalkSpeed(speed): set the actor's walk speed (units/frame; vanilla cutscenes use ~15)."""
212
+ return encode(0x26, speed)
213
+
214
+
215
+ def set_walk_turn_speed(speed: int) -> bytes: # 0x55 (MROT) argsize [1]
216
+ """SetWalkTurnSpeed(speed): how fast the actor rotates toward its target WHILE walking (omega;
217
+ default 16 ~= 11 deg/frame). Cranking it high (255 ~= 179 deg/frame) shrinks the turn-while-walk
218
+ arc to ~nothing, so a Walk to a point BEHIND the actor turns and goes straight instead of orbiting
219
+ it forever -- without the animated-turn path (TimedTurn/TurnTowardPosition) that can hang at 180."""
220
+ return encode(0x55, speed)
221
+
222
+
223
+ def move_instant_xzy(x: int, z: int, y: int = 0) -> bytes: # 0xA1 (POS3) argsize [2, 2, 2]
224
+ """MoveInstantXZY: teleport the actor to world (x, z) at height y -- no walk animation.
225
+
226
+ GOTCHA (verified from source): the engine reads ``destX=arg1; destZ=-arg2; destY=arg3`` then calls
227
+ ``SetActorPosition(po, destX, destZ, destY)`` = ``po.x=destX; po.y=destZ; po.z=destY``. So despite
228
+ the "XZY" name the bytecode args are (worldX, -worldY, worldZ): arg2 is the NEGATED height, arg3 is
229
+ the world depth Z (NOT arg2). So encode (x, -y, z). Use to place an actor before a walk-in."""
230
+ return encode(0xA1, x, -y, z)
231
+
232
+
233
+ def run_animation(anim: int) -> bytes: # 0x40 (ANIM) argsize [2]
234
+ """RunAnimation(anim): play an animation on the executing actor (async; pair WaitAnimation)."""
235
+ return encode(0x40, anim)
236
+
237
+
238
+ def wait_animation() -> bytes: # 0x41 (WAITANIM) 0 args
239
+ """WaitAnimation(): block until the executing actor's current animation has ended."""
240
+ return encode(0x41)
241
+
242
+
243
+ def stop_animation() -> bytes: # 0x42 (ENDANIM) 0 args
244
+ """StopAnimation(): stop the current animation -> resets to idle and CLEARS the anim flags
245
+ (afExec/afLower/afFreeze). Needed before a Walk: the engine only swaps idle->walk when moving if
246
+ those flags are clear (ProcessEvents), and a player-cloned NPC's idle can leave afExec set, so it
247
+ glides in the idle pose. StopAnimation first => the auto walk-anim swap fires."""
248
+ return encode(0x42)
249
+
250
+
251
+ def turn_instant(angle: int) -> bytes: # 0x36 (DIRE) argsize [1]
252
+ """TurnInstant(angle): face an angle instantly (0=south, 64=west, 128=north, 192=east)."""
253
+ return encode(0x36, angle)
254
+
255
+
256
+ def timed_turn(angle: int, speed: int = 16) -> bytes: # 0x56 (TURN) argsize [1, 1]
257
+ """TimedTurn(angle, speed): face an angle, animated (0=S,64=W,128=N,192=E; pair WaitTurn)."""
258
+ return encode(0x56, angle, speed)
259
+
260
+
261
+ def turn_toward_object(uid: int, speed: int = 16) -> bytes: # 0x51 (TURNA) argsize [1, 1]
262
+ """TurnTowardObject(uid, speed): turn to face an object by UID (250=player), animated; pair WaitTurn."""
263
+ return encode(0x51, uid, speed)
264
+
265
+
266
+ def turn_toward_position(x: int, z: int) -> bytes: # 0x9B (TURNTO) argsize [2, 2]
267
+ """TurnTowardPosition(x, z): turn the actor IN PLACE to face world (x, z), animated (uses the
268
+ actor's turn speed). No Z-negation (uses posZ directly, like Walk). Pair with WaitTurn. Emit this
269
+ before a Walk so the actor faces its destination first -- otherwise it ARCS toward a target behind
270
+ it (moves at full speed while turning only ~omega/frame) and orbits a nearby point forever."""
271
+ return encode(0x9B, x, z)
272
+
273
+
274
+ def wait_turn() -> bytes: # 0x50 (WAITTURN) 0 args
275
+ """WaitTurn(): block until the executing actor's (animated) turn has finished."""
276
+ return encode(0x50)
277
+
278
+
279
+ def set_pathing(active: int) -> bytes: # 0xA8 (BGI) argsize [1]
280
+ """SetPathing(active): enable(1)/disable(0) the actor's walkmesh collision. MoveInstantXZY
281
+ DISABLES it (so a teleport off the mesh doesn't snap back); call SetPathing(1) after to re-enable
282
+ it before walking (the real walk-in pattern: MoveInstantXZY -> SetPathing(1) -> ... -> Walk)."""
283
+ return encode(0xA8, active)
284
+
285
+
286
+ def setup_jump(x: int, z: int, y: int, steps: int = 6) -> bytes: # 0xE2 (SETVY3) argsize [2,2,2,1]
287
+ """SetupJump(x, z, y, steps): set the destination + duration for a following Jump. Same arg
288
+ convention as MoveInstantXZY -- (worldX, -worldY, worldZ) -- so encode (x, -y, z). `steps` is the
289
+ jump duration in frames (0 -> 8). `y` is the world HEIGHT (up = positive; a ladder top is y>0).
290
+ Pair with Jump(); the engine interpolates a parabolic arc from the actor's current pos to here."""
291
+ return encode(0xE2, x, -y, z, steps)
292
+
293
+
294
+ def jump() -> bytes: # 0xDC (JUMP3) 0 args
295
+ """Jump(): perform the jump set up by SetupJump -- synchronous (blocks `steps` frames) and moves
296
+ the actor along a parabolic arc to the SetupJump destination (incl. the height y)."""
297
+ return encode(0xDC)
298
+
299
+
300
+ def set_jump_animation(anim: int, a: int = 2, b: int = 6) -> bytes: # 0x94 (SETJUMP) argsize [2,1,1]
301
+ """SetJumpAnimation(anim, a, b): set the animation played during the next Jump arc (e.g. a ladder
302
+ mount/dismount climb-grab). Verified vs field 706's vine: ``94 00 BF29 02 06`` = (10687, 2, 6)."""
303
+ return encode(0x94, anim, a, b)
304
+
305
+
306
+ def run_jump_animation() -> bytes: # 0x9C (RUNJUMP) 0 args
307
+ """RunJumpAnimation(): play the animation set by SetJumpAnimation (paired with a Jump)."""
308
+ return encode(0x9C)
309
+
310
+
311
+ def run_land_animation() -> bytes: # 0x9D (RUNLAND) 0 args
312
+ """RunLandAnimation(): play the landing animation after a Jump arc."""
313
+ return encode(0x9D)
314
+
315
+
316
+ def set_animation_flags(a: int, b: int) -> bytes: # 0x3F (ANIMFLAG) argsize [1,1]
317
+ """SetAnimationFlags(a, b): configure the actor's animation blending. A ladder climb sets (1,0) at
318
+ mount and restores (0,0) on dismount (field 706). Verified: ``3F 00 01 00`` = (1, 0)."""
319
+ return encode(0x3F, a, b)
320
+
321
+
322
+ def set_animation_in_out(a: int, b: int) -> bytes: # 0x3D (ANIMINOUT) argsize [1,1]
323
+ """SetAnimationInOut(a, b): set the in/out frame window of the current animation. Verified vs
324
+ field 706: ``3D 00 00 00`` = (0, 0)."""
325
+ return encode(0x3D, a, b)
326
+
327
+
328
+ def add_character_attribute(flag: int) -> bytes: # 0xCC (ADDATTR) argsize [2]
329
+ """AddCharacterAttribute(flag): set a character attribute bit. Flag 4 = the LADDER flag -- tells
330
+ the engine the actor is on a ladder so it isn't snapped to the floor during a height climb."""
331
+ return encode(0xCC, flag)
332
+
333
+
334
+ def remove_character_attribute(flag: int) -> bytes: # 0xCD (DELATTR) argsize [2]
335
+ """RemoveCharacterAttribute(flag): clear a character attribute bit (e.g. 4 = ladder, on dismount)."""
336
+ return encode(0xCD, flag)
337
+
338
+
339
+ def disable_move() -> bytes: # 0x2D (UCOFF) 0 args
340
+ """DisableMove(): lock the player's movement control (cutscene start)."""
341
+ return DISABLE_MOVE
342
+
343
+
344
+ def enable_move() -> bytes: # 0x2E (UCON) 0 args
345
+ """EnableMove(): restore the player's movement control (cutscene end)."""
346
+ return ENABLE_MOVE
347
+
348
+
349
+ # --- text windows ---
350
+ def window_sync(win: int, flags: int, text_id: int) -> bytes: # 0x1F
351
+ return encode(0x1F, win, flags, text_id)
352
+
353
+
354
+ def window_async(win: int, flags: int, text_id: int) -> bytes: # 0x20
355
+ return encode(0x20, win, flags, text_id)
356
+
357
+
358
+ # --- audio ---
359
+ def run_sound_code(sound_code: int, sound_id: int) -> bytes: # 0xC5 argsize [2,2]
360
+ return encode(0xC5, sound_code, sound_id)
361
+
362
+
363
+ # --- visual ---
364
+ def fade_filter(a: int, b: int, c: int, d: int, e: int, f: int) -> bytes: # 0xEC 6x1
365
+ return encode(0xEC, a, b, c, d, e, f)
366
+
367
+
368
+ # --- battles ---
369
+ def set_random_battles(slot: int, b1: int, b2: int, b3: int, b4: int) -> bytes: # 0x3C [1,2,2,2,2]
370
+ return encode(0x3C, slot, b1, b2, b3, b4)
371
+
372
+
373
+ def set_random_battle_frequency(freq: int) -> bytes: # 0x57 [1]
374
+ return encode(0x57, freq)
375
+
376
+
377
+ # --- field camera (multi-camera) ---
378
+ def run_script_sync(script_level: int, uid: int, func_tag: int) -> bytes: # 0x14 (REQEW) [1,1,1]
379
+ """RunScriptSync(level, uid, tag): run object ``uid``'s function ``tag`` and WAIT until it returns
380
+ (the engine's REQEW). Targets by UID (GetObjUID). A director uses this to drive an NPC's
381
+ choreography function -- which then runs while the NPC is 'running' (so its animations advance,
382
+ unlike code spliced into the NPC's Init). ``level`` is the script level (real cutscenes use 2)."""
383
+ return encode(0x14, script_level, uid, func_tag)
384
+
385
+
386
+ def set_field_camera(cam_id: int) -> bytes: # 0x7E (SETCAM) [1]
387
+ """SetFieldCamera(cam_id): switch the active background camera (engine SetCurrentCameraIndex)."""
388
+ return encode(0x7E, cam_id)
389
+
390
+
391
+ def enable_dialog_choices(avail_mask: int, default: int = 0) -> bytes: # 0x7C (CHOOSEPARAM) [2,1]
392
+ """EnableDialogChoices(avail_mask, default): configure the NEXT choice window. ``avail_mask`` is the
393
+ availability bitmask (bit i = row i selectable, LSB-first; -1/0xFFFF = all on) -> ETb.sChooseMask;
394
+ ``default`` is the initially-highlighted row. The engine only APPLIES the mask if the choice text
395
+ carries a ``[PCHM]`` tag (``[PCHC]`` passes default/cancel but ignores the mask). Grounded in the
396
+ field-100 ATE menu: ``EnableDialogChoices( VAR_GenInt16_241 | 32768, 0 )``. See content.choice."""
397
+ return encode(0x7C, avail_mask & 0xFFFF, default)
398
+
399
+
400
+ def enable_dialog_choices_var(mask_expr: bytes, default: int = 0) -> bytes: # 0x7C, arg0 = expression
401
+ """EnableDialogChoices where the mask is a RUNTIME EXPRESSION (e.g. a scratch var built from story
402
+ flags) rather than a literal. ``mask_expr`` is a bare RPN token blob terminated by ``0x7F`` (see
403
+ ``region.var_expr``); the gArgFlag bit for arg0 is set so the engine evaluates it (getv->CalcExpr).
404
+ Real-field verified (Dali/Storage 407: ``7c 01 d9 21 7d 04 00 26 7f 00`` = ``EnableDialogChoices(VAR | 4, 0)``)."""
405
+ return encode(0x7C, mask_expr, default, arg_flags=0b01)
406
+
407
+
408
+ def terminate_entry(entry: int = 255) -> bytes: # 0x1C (KILL) [1]
409
+ """TerminateEntry(entry): stop an entry's code (255 = This). Used to deactivate a switch zone."""
410
+ return encode(0x1C, entry)
411
+
412
+
413
+ # --- field transitions (a ladder top that exits to another field / the world map) ---
414
+ # NOTE: there is deliberately NO preload_field() helper. FF9's PreloadField is opcode 0xFD (HINT),
415
+ # "ignored in the non-PSX versions" -- a no-op on Steam, so a Field() alone warps. Do NOT encode it as
416
+ # 0x2A: that opcode is **Battle**, and emitting it before a Field warp literally starts a battle using
417
+ # the field id as the battle-scene id (invalid id -> InitBattleScene null-ref crash; valid id -> a real
418
+ # battle). This bit us once; keep the warp to just Field().
419
+ def field(target: int) -> bytes: # 0x2B (MAPJUMP) argsize [2]
420
+ """Field(target): transition to field ``target`` (arriving via the entrance var D8:2, set just
421
+ before). Verified vs field 70's warp: ``2B 00 <id>``."""
422
+ return encode(0x2B, target)
423
+
424
+
425
+ def world_map(entry: int) -> bytes: # 0xB6 (WMAPJUMP) argsize [2]
426
+ """WorldMap(entry): transition to the world map at ``entry`` -- a world-exit vine's top boundary
427
+ (e.g. Gizamaluke's vine to the world map). Real fields branch the entry by story-progress; this
428
+ emits the simple single-target form."""
429
+ return encode(0xB6, entry)
430
+
431
+
432
+ # --- inventory (events / treasure) ---
433
+ def add_item(item_id: int, count: int = 1) -> bytes: # 0x48 (ITEM) argsize [2, 1]
434
+ """AddItem(item_id, count): add an item to the party inventory (real-chest opcode)."""
435
+ return encode(0x48, item_id, count)
436
+
437
+
438
+ def remove_item(item_id: int, count: int = 1) -> bytes: # 0x49 (ITEMDELETE) argsize [2, 1]
439
+ """RemoveItem(item_id, count): take ``count`` of an item from the party inventory (the symmetric
440
+ counterpart of :func:`add_item` -- a trade / quest-item consume). The engine removes up to what's
441
+ held, so removing more than carried is a safe clamp, not an error."""
442
+ return encode(0x49, item_id, count)
443
+
444
+
445
+ def set_text_variable(slot: int, value: int) -> bytes: # 0x66 (MESVALUE) argsize [1, 2]
446
+ """SetTextVariable(slot, value): set dialogue text-variable ``slot`` -> ``value`` (ETb.gMesValue).
447
+ A ``[ITEM=slot]`` tag in the next window renders that value's item name, ``[VAR=slot]`` its number.
448
+ The chest "Received [ITEM=0]!" pattern uses SetTextVariable(0, item) (real-field verified, field 407:
449
+ ``66 00 00 ec 00`` = SetTextVariable(0, 236))."""
450
+ return encode(0x66, slot, value)
451
+
452
+
453
+ def add_gil(amount: int) -> bytes: # 0xCE (GILADD) argsize [3]
454
+ """AddGil(amount): add gil to the party purse. ``amount`` is an UNSIGNED 24-bit value -- the engine
455
+ does ``party.gil += amount`` (caps at 9999999), so a negative here wraps to a huge add. To SUBTRACT
456
+ gil use :func:`remove_gil`."""
457
+ return encode(0xCE, amount)
458
+
459
+
460
+ def remove_gil(amount: int) -> bytes: # 0xCF (GILDELETE) argsize [3]
461
+ """RemoveGil(amount): subtract gil from the party purse (engine ``party.gil -= amount``, floored at
462
+ 0). ``amount`` is a POSITIVE 24-bit value."""
463
+ return encode(0xCF, amount)