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/eblint.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Phase-3: the field event-script (``.eb``) structural LINTER -- the ``ailint`` analogue for FIELD scripts.
|
|
2
|
+
|
|
3
|
+
Validates a field's ``.eb`` control flow OFFLINE (the "I can't see the game" superpower applied to the field
|
|
4
|
+
stack), so an in-place edit (Phases 2/4) can be re-checked before deploy and a verbatim fork can be vetted up
|
|
5
|
+
front. The ERROR checks are SOUND -- every shipping field lints with ZERO errors (proven by a sweep over all
|
|
6
|
+
676 real fields; 29382 functions, 0 decode/jump/switch/reachability faults), so an error only ever flags a
|
|
7
|
+
genuine structural break.
|
|
8
|
+
|
|
9
|
+
Checks (per entry/function):
|
|
10
|
+
* decode (error) -- the body decodes cleanly to its declared boundary (a truncated/desynced eb).
|
|
11
|
+
* jump bounds (error) -- every relative jump (JMP/JMP_IFNOT/JMP_IF) lands ON an instruction boundary inside
|
|
12
|
+
its own function (engine-correct signedness; reuses :func:`eb.disasm.jump_target`).
|
|
13
|
+
* switch bounds (error) -- every switch (0x06/0x0B/0x0D) case + default target lands on an instruction
|
|
14
|
+
boundary (:func:`eb.disasm.decode_switch`, Phase 1) -- the field stack's primary dispatch, which the battle
|
|
15
|
+
linter treats as opaque. A switch whose operands are computed is a WARNING (its targets can't be checked).
|
|
16
|
+
* reachable terminator (error) -- a forward walk (follow jumps + SWITCH ARMS + fall-through, bounded by
|
|
17
|
+
visited offsets) flags a path that falls off the function end without a terminator (the engine would run
|
|
18
|
+
the IP into adjacent bytecode at runtime).
|
|
19
|
+
* call resolution (warning) -- a ``RunScript[Async|Sync](uid, tag)`` whose uid resolves statically (self /
|
|
20
|
+
Main_Init / a sibling object entry) should call a tag that entry defines; a dangling call is the #5
|
|
21
|
+
softlock class. WARNING not error: 25 shipping fields have a statically-dangling call (engine no-ops it or
|
|
22
|
+
it's an unreachable beat), so it can't be a soundness error -- but it's exactly what an editor wants flagged.
|
|
23
|
+
Player/party/computed targets are skipped (not resolvable offline).
|
|
24
|
+
|
|
25
|
+
Read-only + offline. ``clean`` == zero ERRORS (warnings are advisory).
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
from .eb import disasm
|
|
32
|
+
from .eb.disasm import JUMP_OPS, SWITCH_OPS, TERMINATOR_OPS, decode_switch, jump_target
|
|
33
|
+
from .eb.model import EbScript
|
|
34
|
+
|
|
35
|
+
_RUNSCRIPT_OPS = frozenset({0x10, 0x12, 0x14}) # RunScript[Async|Sync](level, uid, tag)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class EbIssue:
|
|
40
|
+
severity: str # "error" | "warning"
|
|
41
|
+
where: str # e.g. "entry7/tag3 @4210"
|
|
42
|
+
message: str
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return f"[{self.severity}] {self.where}: {self.message}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _lint_function(eb, e, f, *, player_entries) -> list:
|
|
49
|
+
data, start, end, where = eb.data, f.abs_start, f.abs_end, f"entry{e.index}/tag{f.tag}"
|
|
50
|
+
issues: list = []
|
|
51
|
+
instrs: dict = {}
|
|
52
|
+
try:
|
|
53
|
+
for ins in disasm.iter_code(data, start, end):
|
|
54
|
+
instrs[ins.off] = ins
|
|
55
|
+
except (IndexError, KeyError):
|
|
56
|
+
return [EbIssue("error", where, "bytecode does not decode cleanly (truncated/corrupt)")]
|
|
57
|
+
if not instrs:
|
|
58
|
+
return [EbIssue("error", where, "empty function body")]
|
|
59
|
+
last = instrs[max(instrs)]
|
|
60
|
+
if last.end != end:
|
|
61
|
+
return [EbIssue("error", where, f"bytecode does not decode to the function boundary "
|
|
62
|
+
f"(last instr ends at {last.end}, boundary {end})")]
|
|
63
|
+
|
|
64
|
+
def _bad(t): # a target that isn't an in-function instr boundary
|
|
65
|
+
return t < start or t >= end or t not in instrs
|
|
66
|
+
|
|
67
|
+
for off, ins in instrs.items():
|
|
68
|
+
if ins.op in JUMP_OPS:
|
|
69
|
+
tgt = jump_target(ins)
|
|
70
|
+
if tgt is not None and _bad(tgt):
|
|
71
|
+
issues.append(EbIssue("error", f"{where} @{off}",
|
|
72
|
+
f"{disasm.op_name(ins.op)} target {tgt} is outside the function / not an "
|
|
73
|
+
f"instruction boundary [{start}..{end})"))
|
|
74
|
+
elif ins.op in SWITCH_OPS:
|
|
75
|
+
sw = decode_switch(ins)
|
|
76
|
+
if sw is None:
|
|
77
|
+
issues.append(EbIssue("warning", f"{where} @{off}",
|
|
78
|
+
"switch operands are computed -- its targets can't be validated"))
|
|
79
|
+
else:
|
|
80
|
+
for ed in sw.edges:
|
|
81
|
+
if _bad(ed.target):
|
|
82
|
+
arm = "default" if ed.is_default else f"case {ed.value}"
|
|
83
|
+
issues.append(EbIssue("error", f"{where} @{off}",
|
|
84
|
+
f"switch {arm} target {ed.target} is outside the function / not an "
|
|
85
|
+
f"instruction boundary [{start}..{end})"))
|
|
86
|
+
elif ins.op in _RUNSCRIPT_OPS:
|
|
87
|
+
issues += _check_call(eb, e.index, off, ins, where, player_entries)
|
|
88
|
+
|
|
89
|
+
# reachability -- forward walk, follow jumps + switch arms + fall-through; flag a path off the end.
|
|
90
|
+
seen: set = set()
|
|
91
|
+
stack = [start]
|
|
92
|
+
ran_off = False
|
|
93
|
+
while stack:
|
|
94
|
+
o = stack.pop()
|
|
95
|
+
if o >= end:
|
|
96
|
+
ran_off = True
|
|
97
|
+
continue
|
|
98
|
+
if o in seen or o not in instrs: # explored, or a bad target (already flagged)
|
|
99
|
+
continue
|
|
100
|
+
seen.add(o)
|
|
101
|
+
ins = instrs[o]
|
|
102
|
+
op = ins.op
|
|
103
|
+
if op in TERMINATOR_OPS:
|
|
104
|
+
continue
|
|
105
|
+
if op in SWITCH_OPS: # dispatches to its arms; never falls through
|
|
106
|
+
sw = decode_switch(ins)
|
|
107
|
+
if sw is None: # computed switch -> can't follow; treat as a stop
|
|
108
|
+
continue
|
|
109
|
+
for ed in sw.edges:
|
|
110
|
+
stack.append(ed.target)
|
|
111
|
+
continue
|
|
112
|
+
if op == 0x01: # unconditional JMP -> its target only
|
|
113
|
+
tgt = jump_target(ins)
|
|
114
|
+
stack.append(tgt if tgt is not None else ins.end)
|
|
115
|
+
elif op in (0x02, 0x03): # conditional -> target AND fall-through
|
|
116
|
+
tgt = jump_target(ins)
|
|
117
|
+
if tgt is not None:
|
|
118
|
+
stack.append(tgt)
|
|
119
|
+
stack.append(ins.end)
|
|
120
|
+
else:
|
|
121
|
+
stack.append(ins.end)
|
|
122
|
+
if ran_off:
|
|
123
|
+
issues.append(EbIssue("error", where, "a control-flow path runs off the end of the function without a "
|
|
124
|
+
"terminator (RET/TerminateEntry) -- the engine would execute "
|
|
125
|
+
"adjacent bytecode at runtime"))
|
|
126
|
+
return issues
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _check_call(eb, entry_index, off, ins, where, player_entries) -> list:
|
|
130
|
+
"""A RunScript(uid, tag) whose uid resolves to a concrete entry (self/Main_Init/sibling object) must call a
|
|
131
|
+
tag that entry defines -- else a dangling dispatch (warning; 25 shipping fields trip it, so not an error)."""
|
|
132
|
+
from . import eventscan
|
|
133
|
+
uid, tag = ins.imm(1), ins.imm(2)
|
|
134
|
+
if uid is None or tag is None:
|
|
135
|
+
return []
|
|
136
|
+
kind, targets = eventscan.resolve_uid(uid, entry_index, player_entries, eb.entry_count)
|
|
137
|
+
if kind == "player": # any player entry defining the tag is fine
|
|
138
|
+
if targets and not any(0 <= t < eb.entry_count and not eb.entry(t).empty
|
|
139
|
+
and eb.entry(t).func_by_tag(tag) is not None for t in targets):
|
|
140
|
+
return [EbIssue("warning", f"{where} @{off}",
|
|
141
|
+
f"RunScript directs the player at tag {tag}, which no player entry defines")]
|
|
142
|
+
return []
|
|
143
|
+
if kind not in ("self", "main", "object") or not targets:
|
|
144
|
+
return [] # party / computed / unknown -> not checkable
|
|
145
|
+
t = targets[0]
|
|
146
|
+
te = eb.entry(t) if 0 <= t < eb.entry_count else None
|
|
147
|
+
if te is not None and not te.empty and te.func_by_tag(tag) is None:
|
|
148
|
+
return [EbIssue("warning", f"{where} @{off}",
|
|
149
|
+
f"RunScript to entry {t} tag {tag}, which that entry doesn't define (dangling call)")]
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def lint_eb(eb_bytes: bytes) -> list:
|
|
154
|
+
"""Lint a field's ``.eb`` -> a list of :class:`EbIssue` (no ERRORS == structurally clean; warnings are
|
|
155
|
+
advisory). Read-only + offline."""
|
|
156
|
+
if not eb_bytes:
|
|
157
|
+
return [EbIssue("error", "eb", "empty or missing .eb data")]
|
|
158
|
+
try:
|
|
159
|
+
eb = EbScript.from_bytes(eb_bytes)
|
|
160
|
+
except (ValueError, IndexError, TypeError) as ex:
|
|
161
|
+
return [EbIssue("error", "eb", f"malformed field .eb: {type(ex).__name__}: {ex}")]
|
|
162
|
+
from . import eventscan
|
|
163
|
+
try: # the player-entry pre-pass DECODES every function;
|
|
164
|
+
player_entries = eventscan.resolve_player_entries(eb) # malformed bytecode here would crash before the
|
|
165
|
+
except (IndexError, KeyError): # per-function decode guard can flag it -> degrade so
|
|
166
|
+
player_entries = [] # _lint_function emits the clean "doesn't decode" error
|
|
167
|
+
issues: list = []
|
|
168
|
+
for e in eb.entries:
|
|
169
|
+
if e.empty:
|
|
170
|
+
continue
|
|
171
|
+
for f in e.funcs:
|
|
172
|
+
issues += _lint_function(eb, e, f, player_entries=player_entries)
|
|
173
|
+
return issues
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def errors(issues) -> list:
|
|
177
|
+
return [i for i in issues if i.severity == "error"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The FF9 Map Kit field-logic editor.
|
|
2
|
+
|
|
3
|
+
A friendly front-end for the *logic* half of a field (dialogue, events, flags, encounters, music,
|
|
4
|
+
cutscenes) so authors never hand-write TOML. Spatial placement (camera / walkmesh / positions /
|
|
5
|
+
zones) stays in Blender; this editor owns the ``<field>.field.toml`` logic file, leaving any sibling
|
|
6
|
+
``<field>.scene.toml`` (Blender-owned) untouched.
|
|
7
|
+
|
|
8
|
+
- ``model`` - bpy/tk-FREE: load/edit/serialize a field.toml (round-trip-safe TOML writer +
|
|
9
|
+
a ``FieldDoc`` that preserves the scene/field split). Fully unit-testable.
|
|
10
|
+
- ``app`` - the Tkinter UI over the model (imported lazily so the package stays importable
|
|
11
|
+
on a headless machine without a display).
|
|
12
|
+
|
|
13
|
+
Launch with ``ff9mapkit edit [field.toml]``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from . import model # noqa: F401 (re-export the bpy/tk-free core)
|
|
19
|
+
|
|
20
|
+
__all__ = ["model"]
|