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.
- ff9mapkit/__init__.py +18 -0
- ff9mapkit/__main__.py +36 -0
- ff9mapkit/_animdb.py +2994 -0
- ff9mapkit/_animdb_all.py +14125 -0
- ff9mapkit/_fieldtable.py +1516 -0
- ff9mapkit/_fieldtext.py +845 -0
- ff9mapkit/_held_poses.py +44 -0
- ff9mapkit/_itemdb.py +65 -0
- ff9mapkit/_modeldb.py +725 -0
- ff9mapkit/_narrowmap_data.py +10 -0
- ff9mapkit/_npcparams.py +634 -0
- ff9mapkit/_regen_animdb.py +72 -0
- ff9mapkit/_regen_animdb_all.py +66 -0
- ff9mapkit/_regen_fieldtable.py +95 -0
- ff9mapkit/_regen_fieldtext.py +66 -0
- ff9mapkit/_regen_modeldb.py +67 -0
- ff9mapkit/_regen_npcparams.py +123 -0
- ff9mapkit/_regen_scenedb.py +57 -0
- ff9mapkit/_scenedb.py +869 -0
- ff9mapkit/abilities.py +225 -0
- ff9mapkit/animations.py +120 -0
- ff9mapkit/archetypes.py +218 -0
- ff9mapkit/areatitle.py +76 -0
- ff9mapkit/battle/__init__.py +21 -0
- ff9mapkit/battle/abilityfeatures.py +294 -0
- ff9mapkit/battle/actiondelta.py +441 -0
- ff9mapkit/battle/aiauthor.py +305 -0
- ff9mapkit/battle/ailint.py +140 -0
- ff9mapkit/battle/aipatch.py +175 -0
- ff9mapkit/battle/battleai.py +148 -0
- ff9mapkit/battle/battlecsv.py +390 -0
- ff9mapkit/battle/battlepatch.py +395 -0
- ff9mapkit/battle/build.py +558 -0
- ff9mapkit/battle/camera_codec.py +332 -0
- ff9mapkit/battle/camera_data.py +128 -0
- ff9mapkit/battle/characterdelta.py +789 -0
- ff9mapkit/battle/event_data.py +72 -0
- ff9mapkit/battle/extract.py +540 -0
- ff9mapkit/battle/fbx.py +223 -0
- ff9mapkit/battle/reskin.py +149 -0
- ff9mapkit/battle/scene_codec.py +314 -0
- ff9mapkit/battle/scene_data.py +369 -0
- ff9mapkit/battle/scenelint.py +125 -0
- ff9mapkit/battle/seqasm.py +131 -0
- ff9mapkit/battle/seqauthor.py +220 -0
- ff9mapkit/battle/seqcodec.py +300 -0
- ff9mapkit/battle/seqdis.py +106 -0
- ff9mapkit/battle/seqpatch.py +137 -0
- ff9mapkit/battle_bgm.py +133 -0
- ff9mapkit/binutils.py +60 -0
- ff9mapkit/build.py +5445 -0
- ff9mapkit/campaign.py +1276 -0
- ff9mapkit/catalog.py +316 -0
- ff9mapkit/chain.py +358 -0
- ff9mapkit/cli.py +3114 -0
- ff9mapkit/config.py +360 -0
- ff9mapkit/content/__init__.py +13 -0
- ff9mapkit/content/areatitle.py +36 -0
- ff9mapkit/content/ate.py +118 -0
- ff9mapkit/content/camera.py +123 -0
- ff9mapkit/content/chest.py +186 -0
- ff9mapkit/content/choice.py +163 -0
- ff9mapkit/content/conductor.py +217 -0
- ff9mapkit/content/cutscene.py +290 -0
- ff9mapkit/content/encounter.py +41 -0
- ff9mapkit/content/entry_settle.py +50 -0
- ff9mapkit/content/equipment.py +93 -0
- ff9mapkit/content/event.py +191 -0
- ff9mapkit/content/gateway.py +101 -0
- ff9mapkit/content/inventory.py +59 -0
- ff9mapkit/content/itemdata.py +644 -0
- ff9mapkit/content/itemtext.py +168 -0
- ff9mapkit/content/jump.py +114 -0
- ff9mapkit/content/ladder.py +633 -0
- ff9mapkit/content/movement.py +53 -0
- ff9mapkit/content/music.py +97 -0
- ff9mapkit/content/npc.py +348 -0
- ff9mapkit/content/object.py +340 -0
- ff9mapkit/content/onentry.py +135 -0
- ff9mapkit/content/party.py +111 -0
- ff9mapkit/content/pathfind.py +138 -0
- ff9mapkit/content/platform.py +314 -0
- ff9mapkit/content/player.py +168 -0
- ff9mapkit/content/prop.py +75 -0
- ff9mapkit/content/region.py +340 -0
- ff9mapkit/content/reinit.py +59 -0
- ff9mapkit/content/savepoint.py +90 -0
- ff9mapkit/content/shop.py +178 -0
- ff9mapkit/content/sps_trigger.py +66 -0
- ff9mapkit/content/startup.py +71 -0
- ff9mapkit/content/synthesis.py +106 -0
- ff9mapkit/content/text.py +183 -0
- ff9mapkit/content/textcarry.py +290 -0
- ff9mapkit/content/verbatim.py +86 -0
- ff9mapkit/content/walkmesh_hotfix.py +38 -0
- ff9mapkit/data/__init__.py +48 -0
- ff9mapkit/data/_regen_provenance.py +142 -0
- ff9mapkit/data/provenance/blank.es.patch +1 -0
- ff9mapkit/data/provenance/blank.fr.patch +1 -0
- ff9mapkit/data/provenance/blank.gr.patch +1 -0
- ff9mapkit/data/provenance/blank.it.patch +1 -0
- ff9mapkit/data/provenance/blank.jp.patch +1 -0
- ff9mapkit/data/provenance/blank.uk.patch +1 -0
- ff9mapkit/data/provenance/blank.us.patch +1 -0
- ff9mapkit/data/provenance/manifest.json +65 -0
- ff9mapkit/data/provenance/region_template.patch +1 -0
- ff9mapkit/data/reference_arcs.toml +89 -0
- ff9mapkit/data/region_catalog.toml +593 -0
- ff9mapkit/deploystack.py +358 -0
- ff9mapkit/dialogue.py +803 -0
- ff9mapkit/eb/__init__.py +12 -0
- ff9mapkit/eb/_exprtable.py +59 -0
- ff9mapkit/eb/_membertable.py +38 -0
- ff9mapkit/eb/_optables.py +537 -0
- ff9mapkit/eb/_regen_optables.py +76 -0
- ff9mapkit/eb/cmdasm.py +323 -0
- ff9mapkit/eb/disasm.py +332 -0
- ff9mapkit/eb/edit.py +439 -0
- ff9mapkit/eb/exprasm.py +158 -0
- ff9mapkit/eb/model.py +178 -0
- ff9mapkit/eb/opcodes.py +463 -0
- ff9mapkit/eblint.py +177 -0
- ff9mapkit/editor/__init__.py +20 -0
- ff9mapkit/editor/app.py +950 -0
- ff9mapkit/editor/battle_forms.py +240 -0
- ff9mapkit/editor/breadcrumb.py +89 -0
- ff9mapkit/editor/dialogs.py +116 -0
- ff9mapkit/editor/feedback.py +208 -0
- ff9mapkit/editor/forms.py +632 -0
- ff9mapkit/editor/graphview.py +350 -0
- ff9mapkit/editor/jobs.py +342 -0
- ff9mapkit/editor/model.py +243 -0
- ff9mapkit/editor/picker.py +120 -0
- ff9mapkit/editor/theme.py +212 -0
- ff9mapkit/eventscan.py +1441 -0
- ff9mapkit/extract.py +2279 -0
- ff9mapkit/flags.py +693 -0
- ff9mapkit/forkreport.py +1383 -0
- ff9mapkit/hub.py +477 -0
- ff9mapkit/idgated.py +101 -0
- ff9mapkit/infohub.py +580 -0
- ff9mapkit/items.py +63 -0
- ff9mapkit/itemstats.py +346 -0
- ff9mapkit/journey.py +1902 -0
- ff9mapkit/keyitems.py +93 -0
- ff9mapkit/logic_add.py +632 -0
- ff9mapkit/logic_edit.py +728 -0
- ff9mapkit/logic_map.py +526 -0
- ff9mapkit/pack.py +175 -0
- ff9mapkit/playerswap.py +231 -0
- ff9mapkit/prop_archetypes.py +228 -0
- ff9mapkit/provision.py +282 -0
- ff9mapkit/refarc.py +825 -0
- ff9mapkit/save.py +337 -0
- ff9mapkit/save_items.py +1673 -0
- ff9mapkit/scene/__init__.py +11 -0
- ff9mapkit/scene/arena.py +63 -0
- ff9mapkit/scene/bgart.py +140 -0
- ff9mapkit/scene/bgi.py +732 -0
- ff9mapkit/scene/bgs.py +174 -0
- ff9mapkit/scene/bgx.py +185 -0
- ff9mapkit/scene/cam.py +345 -0
- ff9mapkit/scene/guide.py +311 -0
- ff9mapkit/scene/paint.py +506 -0
- ff9mapkit/scene/placeholder.py +107 -0
- ff9mapkit/sjbinary.py +285 -0
- ff9mapkit/sps/__init__.py +17 -0
- ff9mapkit/sps/author.py +294 -0
- ff9mapkit/sps/catalog.py +88 -0
- ff9mapkit/sps/codec.py +264 -0
- ff9mapkit/sps/edit.py +184 -0
- ff9mapkit/sps/lint.py +58 -0
- ff9mapkit/sps/render.py +116 -0
- ff9mapkit/sps/templates.py +47 -0
- ff9mapkit/sps/texture.py +131 -0
- ff9mapkit/walkmesh_hotfixes.py +163 -0
- ff9mapkit/workspace/__init__.py +18 -0
- ff9mapkit/workspace/battledoc.py +985 -0
- ff9mapkit/workspace/builddoc.py +607 -0
- ff9mapkit/workspace/forms_qt.py +586 -0
- ff9mapkit/workspace/importdoc.py +665 -0
- ff9mapkit/workspace/mapview.py +131 -0
- ff9mapkit/workspace/palette.py +85 -0
- ff9mapkit/workspace/savedoc.py +664 -0
- ff9mapkit/workspace/shell.py +6907 -0
- ff9mapkit/workspace/style.py +105 -0
- ff9mapkit/workspace/tuningdialog.py +223 -0
- ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
- ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
- ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
- ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
- ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
- 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}>"
|
ff9mapkit/eb/opcodes.py
ADDED
|
@@ -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)
|