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/disasm.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""FF9 field event-script (.eb) disassembler — decodes the bytecode stream.
|
|
2
|
+
|
|
3
|
+
This is the read side of the ``.eb`` library. It decodes one instruction at a time using the
|
|
4
|
+
baked opcode tables (``_optables``), so it needs no Memoria source at runtime. ``read_code``
|
|
5
|
+
returns a structured :class:`Instr` (offset, opcode, decoded immediate args, byte length) that
|
|
6
|
+
the model and the content injectors use to locate features symbolically — e.g. "find the
|
|
7
|
+
``Wait(2)`` in Main_Init" — instead of relying on hardcoded byte offsets.
|
|
8
|
+
|
|
9
|
+
The decoding mirrors Memoria's ``EventEngine`` byte reader exactly:
|
|
10
|
+
* a leading ``0xFF`` byte selects the extended (2-byte) opcode page,
|
|
11
|
+
* opcodes >= 0x10 with operands carry a 1-byte ``argFlag`` bitmask; a set bit means that
|
|
12
|
+
operand is an *expression* (RPN-ish token stream) rather than a fixed-width immediate,
|
|
13
|
+
* a few opcodes have a variable operand count read from the stream (0x06 switch, 0x0B, 0x0D).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from ._optables import OP_ARG_COUNT, OP_ARG_SIZE, OP_NAMES
|
|
21
|
+
|
|
22
|
+
SWITCH_OPS = (0x06, 0x0B, 0x0D) # JMP_SWITCHEX (explicit value/offset pairs) / JMP_SWITCH (contiguous range) /
|
|
23
|
+
# JMP_SWITCH with a 2-byte case count. See decode_switch.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def op_name(op: int) -> str:
|
|
27
|
+
return OP_NAMES.get(op, f"op_{op:02X}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def argsize(op: int, i: int) -> int:
|
|
31
|
+
"""Byte width of operand *i* of *op* (immediate form)."""
|
|
32
|
+
if op == 0x29:
|
|
33
|
+
return 4
|
|
34
|
+
if op in (0x06, 0x0B, 0x0D):
|
|
35
|
+
return 2
|
|
36
|
+
a = OP_ARG_SIZE[op] if op < len(OP_ARG_SIZE) else None
|
|
37
|
+
return a[i] if (a and i < len(a)) else 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Instr:
|
|
42
|
+
"""One decoded instruction.
|
|
43
|
+
|
|
44
|
+
off : absolute byte offset where the instruction begins
|
|
45
|
+
op : opcode (0x100 | x for extended/0xFF-prefixed opcodes)
|
|
46
|
+
args : decoded operands — ints for immediates, str tokens for expression operands
|
|
47
|
+
arg_is_expr: parallel bool list; True where the operand was an expression
|
|
48
|
+
length : total bytes consumed (off .. off+length)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
off: int
|
|
52
|
+
op: int
|
|
53
|
+
args: list = field(default_factory=list)
|
|
54
|
+
arg_is_expr: list = field(default_factory=list)
|
|
55
|
+
length: int = 0
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def name(self) -> str:
|
|
59
|
+
return op_name(self.op)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def end(self) -> int:
|
|
63
|
+
return self.off + self.length
|
|
64
|
+
|
|
65
|
+
def imm(self, i: int):
|
|
66
|
+
"""Immediate operand *i* as int, or None if it was an expression."""
|
|
67
|
+
return self.args[i] if (i < len(self.args) and not self.arg_is_expr[i]) else None
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_switch(self) -> bool:
|
|
71
|
+
return self.op in SWITCH_OPS
|
|
72
|
+
|
|
73
|
+
def switch(self) -> "SwitchInfo | None":
|
|
74
|
+
"""Structured (case value -> absolute target) decode if this is a switch; else None."""
|
|
75
|
+
return decode_switch(self)
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
parts = []
|
|
79
|
+
for v, is_expr in zip(self.args, self.arg_is_expr):
|
|
80
|
+
parts.append(v if is_expr else str(v))
|
|
81
|
+
return f"[{self.off}] {self.name}({', '.join(parts)})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class SwitchEdge:
|
|
86
|
+
"""One arm of a switch: a selector ``value`` (None = the default arm) -> an absolute byte ``target``."""
|
|
87
|
+
value: int | None
|
|
88
|
+
target: int
|
|
89
|
+
is_default: bool = False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class SwitchInfo:
|
|
94
|
+
"""A decoded switch dispatch table. ``base`` = the lowest selector value of the contiguous-range form
|
|
95
|
+
(0x0B/0x0D), or None for the explicit value/offset form (0x06). ``edges`` = the cases then the default.
|
|
96
|
+
Targets are ABSOLUTE byte offsets (same space as ``Instr.off`` / ``Func.abs_start``), valid only within
|
|
97
|
+
the owning function. The selector itself is popped from the expression stack at runtime (pushed by the
|
|
98
|
+
preceding ``0x05``), so it is not part of this inline-table decode."""
|
|
99
|
+
op: int
|
|
100
|
+
base: int | None
|
|
101
|
+
edges: list
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _sx_hi(w: int) -> int:
|
|
105
|
+
"""Sign-extend only the HIGH byte of a 16-bit word -- the engine reads the contiguous-form base as
|
|
106
|
+
``offsetL | ((SByte)offsetH << 8)`` (EBin.cs JMP_SWITCH), so a base 0xFFFE means selector -2, not 65534."""
|
|
107
|
+
return (w & 0xFF) | ((((w >> 8) & 0xFF) ^ 0x80) - 0x80) * 256
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def decode_switch(instr: Instr) -> "SwitchInfo | None":
|
|
111
|
+
"""Decode a switch instruction (0x06 / 0x0B / 0x0D) into a :class:`SwitchInfo` of absolute case+default
|
|
112
|
+
targets, or None if *instr* isn't a switch (or its operands aren't plain immediates). Derived from the
|
|
113
|
+
Memoria engine (EBin.cs JMP_SWITCH / JMP_SWITCHEX) and validated 100% boundary-aligned across all 5563
|
|
114
|
+
switches in the 676 shipping fields.
|
|
115
|
+
|
|
116
|
+
Layout (O = ``instr.off``; ``a`` = the flat 2-byte operands :attr:`Instr.args`):
|
|
117
|
+
* 0x06 (explicit): ``a[0]`` = default reloffset, then n pairs ``(value=a[1+2k], reloffset=a[2+2k])``;
|
|
118
|
+
anchor = O+4; target = anchor + reloffset.
|
|
119
|
+
* 0x0B (contiguous): ``base = sx_hi(a[0])``, ``a[1]`` = default reloffset, ``a[2..]`` = n contiguous
|
|
120
|
+
case reloffsets for selectors base..base+n-1; anchor = O+1.
|
|
121
|
+
* 0x0D (contiguous, 2-byte count): identical to 0x0B with anchor = O+2 (none ship; by-construction).
|
|
122
|
+
All reloffsets are unsigned u16 (the engine only jumps forward)."""
|
|
123
|
+
op = instr.op
|
|
124
|
+
if op not in SWITCH_OPS:
|
|
125
|
+
return None
|
|
126
|
+
a = instr.args
|
|
127
|
+
if any(not isinstance(x, int) for x in a): # a switch never has expression operands; bail if malformed
|
|
128
|
+
return None
|
|
129
|
+
O = instr.off
|
|
130
|
+
if op == 0x06:
|
|
131
|
+
if not a:
|
|
132
|
+
return None
|
|
133
|
+
anchor = O + 4
|
|
134
|
+
n = (len(a) - 1) // 2
|
|
135
|
+
edges = [SwitchEdge(a[1 + 2 * k], anchor + a[2 + 2 * k]) for k in range(n)]
|
|
136
|
+
edges.append(SwitchEdge(None, anchor + a[0], True))
|
|
137
|
+
return SwitchInfo(op, None, edges)
|
|
138
|
+
if len(a) < 2:
|
|
139
|
+
return None
|
|
140
|
+
anchor = O + (2 if op == 0x0D else 1)
|
|
141
|
+
base = _sx_hi(a[0])
|
|
142
|
+
n = len(a) - 2
|
|
143
|
+
edges = [SwitchEdge(base + i, anchor + a[2 + i]) for i in range(n)]
|
|
144
|
+
edges.append(SwitchEdge(None, anchor + a[1], True))
|
|
145
|
+
return SwitchInfo(op, base, edges)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --- control-flow facts (engine ABI) ----------------------------------------------------------------
|
|
149
|
+
# The flow-TERMINATOR opcodes: a path reaching one ENDS (the engine's per-function dispatch stops via
|
|
150
|
+
# adFin(); the IP never advances into adjacent bytecode). RET 0x04 / TerminateEntry 0x1C + the high ops
|
|
151
|
+
# whose DoEventCode return code routes through adFin(): Battle 0x2A / Field 0x2B / STOP 0x4F /
|
|
152
|
+
# TetraMaster 0xAE / WorldMap 0xB6 / GameOver 0xF5 (verified vs EBin.cs). NOTE: battle/ailint.py keeps a
|
|
153
|
+
# parallel private copy (it predates this) -- both are guarded by their whole-corpus soundness sweeps, which
|
|
154
|
+
# would fail if either drifted from the engine.
|
|
155
|
+
TERMINATOR_OPS = frozenset({0x04, 0x1C, 0x2A, 0x2B, 0x4F, 0xAE, 0xB6, 0xF5})
|
|
156
|
+
JUMP_OPS = frozenset({0x01, 0x02, 0x03}) # JMP / JMP_IFNOT / JMP_IF -- a 2-byte relative offset operand
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def jump_target(ins: Instr) -> "int | None":
|
|
160
|
+
"""The absolute byte target of a relative jump (0x01/0x02/0x03), or None if its offset is an expression.
|
|
161
|
+
Signedness MATCHES the engine: JMP (0x01) / JMP_IF (0x03) read a SIGNED int16; JMP_IFNOT (0x02) reads its
|
|
162
|
+
skip UNSIGNED -- so a backward JMP_IFNOT becomes a huge forward target a bounds check catches."""
|
|
163
|
+
if not ins.arg_is_expr or ins.arg_is_expr[0]:
|
|
164
|
+
return None
|
|
165
|
+
raw = ins.imm(0)
|
|
166
|
+
if raw is None:
|
|
167
|
+
return None
|
|
168
|
+
if ins.op == 0x02: # JMP_IFNOT -- engine reads this UNSIGNED
|
|
169
|
+
return ins.end + raw
|
|
170
|
+
return ins.end + (raw - 0x10000 if raw >= 0x8000 else raw) # JMP / JMP_IF -- signed int16
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def read_expr(raw: bytes, pos: int) -> tuple[str, int]:
|
|
174
|
+
"""Decode an expression token stream; returns (text, new_pos). Mirrors the engine."""
|
|
175
|
+
ops = []
|
|
176
|
+
while True:
|
|
177
|
+
o = raw[pos]; pos += 1
|
|
178
|
+
isconst = o in (0x7D, 0x7E)
|
|
179
|
+
isvar = o >= 0xC0 or o in (0x29, 0x5F, 0x78, 0x79, 0x7A)
|
|
180
|
+
if not isconst and not isvar:
|
|
181
|
+
ops.append(f"op{o:02X}")
|
|
182
|
+
if o == 0x7F:
|
|
183
|
+
break
|
|
184
|
+
continue
|
|
185
|
+
if o == 0x7E:
|
|
186
|
+
a = [raw[pos], raw[pos + 1], raw[pos + 2], raw[pos + 3]]; pos += 4
|
|
187
|
+
elif o >= 0xE0 or o in (0x78, 0x7D):
|
|
188
|
+
a = [raw[pos], raw[pos + 1]]; pos += 2
|
|
189
|
+
else:
|
|
190
|
+
a = [raw[pos]]; pos += 1
|
|
191
|
+
ops.append(f"op{o:02X}({','.join(str(x) for x in a)})")
|
|
192
|
+
return "{" + " ".join(ops) + "}", pos
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def pretty_expr(raw: bytes, pos: int) -> tuple[str, int]:
|
|
196
|
+
"""Decode an expression token stream to a HUMAN-READABLE form; returns (text, new_pos). Same byte-walk as
|
|
197
|
+
:func:`read_expr` but names each operator via the ``op_binary`` table and decodes a variable token into its
|
|
198
|
+
``Source.Type[index]`` form (so a story-flag read shows as ``Global.Bit[8512]``, an enemy-HP read as
|
|
199
|
+
``B_CURHP``). The read side of the battle-AI inspector; field scripts read the same way."""
|
|
200
|
+
from ._exprtable import expr_op_name, decode_var
|
|
201
|
+
out = []
|
|
202
|
+
while True:
|
|
203
|
+
o = raw[pos]; pos += 1
|
|
204
|
+
isconst = o in (0x7D, 0x7E)
|
|
205
|
+
isvar = o >= 0xC0 or o in (0x29, 0x5F, 0x78, 0x79, 0x7A)
|
|
206
|
+
if not isconst and not isvar: # a pure operator (no inline operand bytes)
|
|
207
|
+
out.append(expr_op_name(o))
|
|
208
|
+
if o == 0x7F: # B_EXPR_END
|
|
209
|
+
break
|
|
210
|
+
continue
|
|
211
|
+
if o == 0x7E: # B_CONST4 -- a 4-byte literal (distinct token so an
|
|
212
|
+
v = raw[pos] | (raw[pos + 1] << 8) | (raw[pos + 2] << 16) | (raw[pos + 3] << 24); pos += 4
|
|
213
|
+
out.append(f"const4({v})") # assemble() can round-trip it back to B_CONST4)
|
|
214
|
+
elif o == 0x7D: # B_CONST -- a 2-byte literal
|
|
215
|
+
v = raw[pos] | (raw[pos + 1] << 8); pos += 2
|
|
216
|
+
out.append(f"const({v})")
|
|
217
|
+
elif o == 0x78: # B_OBJSPECA -- obj-var read: uid (hi) + field (lo)
|
|
218
|
+
out.append(f"obj(uid={raw[pos]}).f[{raw[pos + 1]}]"); pos += 2
|
|
219
|
+
elif o in (0x79, 0x7A): # B_SYSLIST / B_SYSVAR -- 1-byte index
|
|
220
|
+
out.append(f"{expr_op_name(o)}[{raw[pos]}]"); pos += 1
|
|
221
|
+
elif o in (0x29, 0x5F): # B_MEMBER / B_PTR -- 1-byte operand
|
|
222
|
+
out.append(f"{expr_op_name(o)}({raw[pos]})"); pos += 1
|
|
223
|
+
elif o >= 0xE0: # a long-index variable (2-byte index)
|
|
224
|
+
out.append(decode_var(o, raw[pos] | (raw[pos + 1] << 8))); pos += 2
|
|
225
|
+
else: # a short-index variable (0xC0..0xDF, 1-byte index)
|
|
226
|
+
out.append(decode_var(o, raw[pos])); pos += 1
|
|
227
|
+
return "{" + " ".join(out) + "}", pos
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def read_code(raw: bytes, pos: int) -> tuple[Instr, int]:
|
|
231
|
+
"""Decode one instruction at *pos*; returns (Instr, new_pos)."""
|
|
232
|
+
start = pos
|
|
233
|
+
op = raw[pos]; pos += 1
|
|
234
|
+
if op == 0xFF:
|
|
235
|
+
op = 0x100 | raw[pos]; pos += 1
|
|
236
|
+
ac = OP_ARG_COUNT[op] if op < len(OP_ARG_COUNT) else 0
|
|
237
|
+
arg_flag = 0
|
|
238
|
+
if op >= 0x10 and ac != 0:
|
|
239
|
+
arg_flag = raw[pos]; pos += 1
|
|
240
|
+
if op == 0x05:
|
|
241
|
+
arg_flag = 1
|
|
242
|
+
if ac < 0:
|
|
243
|
+
ac = raw[pos]; pos += 1
|
|
244
|
+
if op == 0x0D:
|
|
245
|
+
ac |= raw[pos] << 8; pos += 1
|
|
246
|
+
if op == 0x06:
|
|
247
|
+
ac = 1 + 2 * ac
|
|
248
|
+
elif op in (0x0B, 0x0D):
|
|
249
|
+
ac = 2 + ac
|
|
250
|
+
args: list = []
|
|
251
|
+
is_expr: list[bool] = []
|
|
252
|
+
for i in range(ac):
|
|
253
|
+
if arg_flag & (1 << i):
|
|
254
|
+
s, pos = read_expr(raw, pos)
|
|
255
|
+
args.append(s); is_expr.append(True)
|
|
256
|
+
else:
|
|
257
|
+
sz = argsize(op, i)
|
|
258
|
+
v = 0
|
|
259
|
+
for k in range(sz):
|
|
260
|
+
v |= raw[pos + k] << (8 * k)
|
|
261
|
+
pos += sz
|
|
262
|
+
args.append(v); is_expr.append(False)
|
|
263
|
+
return Instr(start, op, args, is_expr, pos - start), pos
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def iter_code(raw: bytes, start: int, end: int):
|
|
267
|
+
"""Yield Instr objects decoded from raw[start:end]. Stops cleanly at *end*."""
|
|
268
|
+
pos = start
|
|
269
|
+
guard = 0
|
|
270
|
+
while pos < end and guard < 100000:
|
|
271
|
+
instr, pos = read_code(raw, pos)
|
|
272
|
+
yield instr
|
|
273
|
+
guard += 1
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _expr_uid_offsets(raw: bytes, pos: int) -> tuple[int, list]:
|
|
277
|
+
"""Walk one expression token stream (mirrors :func:`read_expr`); return (new_pos, uid_offsets) where
|
|
278
|
+
each uid_offset is the absolute byte offset of a ``0x78`` (B_OBJSPECA) token's UID operand byte (the
|
|
279
|
+
first of its two data bytes -- ``78 <uid> <field>``, uid first)."""
|
|
280
|
+
offs = []
|
|
281
|
+
while True:
|
|
282
|
+
o = raw[pos]; pos += 1
|
|
283
|
+
isconst = o in (0x7D, 0x7E)
|
|
284
|
+
isvar = o >= 0xC0 or o in (0x29, 0x5F, 0x78, 0x79, 0x7A)
|
|
285
|
+
if not isconst and not isvar:
|
|
286
|
+
if o == 0x7F:
|
|
287
|
+
break
|
|
288
|
+
continue
|
|
289
|
+
if o == 0x7E:
|
|
290
|
+
pos += 4
|
|
291
|
+
elif o >= 0xE0 or o in (0x78, 0x7D):
|
|
292
|
+
if o == 0x78:
|
|
293
|
+
offs.append(pos) # the UID byte (first data byte of the obj-var token)
|
|
294
|
+
pos += 2
|
|
295
|
+
else:
|
|
296
|
+
pos += 1
|
|
297
|
+
return pos, offs
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def expr_obj_uid_offsets(raw: bytes, start: int, end: int) -> list:
|
|
301
|
+
"""Absolute byte offsets of every ``0x78`` (B_OBJSPECA, obj-var read) token's UID operand byte in
|
|
302
|
+
``raw[start:end]``. Decodes instruction-by-instruction exactly like :func:`read_code` and walks each
|
|
303
|
+
EXPRESSION operand's token stream -- NOT a raw-byte ``0x78`` scan (which false-positives on const data,
|
|
304
|
+
per docs/OBJECT_CARRY.md S3 invariant 2). The object graft uses this to remap a sibling uid read inside
|
|
305
|
+
an expression operand (a same-length 1-byte patch). Mirrors ``read_code``'s operand decode."""
|
|
306
|
+
out = []
|
|
307
|
+
pos = start
|
|
308
|
+
while pos < end:
|
|
309
|
+
op = raw[pos]; pos += 1
|
|
310
|
+
if op == 0xFF:
|
|
311
|
+
op = 0x100 | raw[pos]; pos += 1
|
|
312
|
+
ac = OP_ARG_COUNT[op] if op < len(OP_ARG_COUNT) else 0
|
|
313
|
+
arg_flag = 0
|
|
314
|
+
if op >= 0x10 and ac != 0:
|
|
315
|
+
arg_flag = raw[pos]; pos += 1
|
|
316
|
+
if op == 0x05:
|
|
317
|
+
arg_flag = 1
|
|
318
|
+
if ac < 0:
|
|
319
|
+
ac = raw[pos]; pos += 1
|
|
320
|
+
if op == 0x0D:
|
|
321
|
+
ac |= raw[pos] << 8; pos += 1
|
|
322
|
+
if op == 0x06:
|
|
323
|
+
ac = 1 + 2 * ac
|
|
324
|
+
elif op in (0x0B, 0x0D):
|
|
325
|
+
ac = 2 + ac
|
|
326
|
+
for i in range(ac):
|
|
327
|
+
if arg_flag & (1 << i):
|
|
328
|
+
pos, uoffs = _expr_uid_offsets(raw, pos)
|
|
329
|
+
out.extend(uoffs)
|
|
330
|
+
else:
|
|
331
|
+
pos += argsize(op, i)
|
|
332
|
+
return out
|