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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Faithful TEXT carry -- ship a donor field's referenced ``.mes`` text VERBATIM + remap the txids.
|
|
2
|
+
|
|
3
|
+
The object graft (:mod:`content.object`) and player-function graft (:mod:`content.player`) carry a real
|
|
4
|
+
field's NPCs/props + their interactions byte-for-byte. But a window those grafted bytes open
|
|
5
|
+
(``WindowSync``/``WindowAsync[Ex]``) names a donor ``.mes`` TXID -- and a fork ships its OWN ``.mes`` block
|
|
6
|
+
(authored lines from :func:`build.collect_text`, at a high band), so that donor id resolves to nothing: an
|
|
7
|
+
EMPTY window. This module closes that last gap: it CARRIES the donor's referenced field text (per language,
|
|
8
|
+
verbatim) and REMAPS each grafted window's TXID to a fresh band, so the forked interactions show the real
|
|
9
|
+
words. It is the faithful counterpart to ``import --dialogue`` (which appends EDITABLE ``[[npc]]`` stubs the
|
|
10
|
+
author re-writes); carry ships the donor strings unchanged.
|
|
11
|
+
|
|
12
|
+
It is IMPORT-ONLY and OPT-IN (the ``import --carry-text`` flag). It never touches the authored-dialogue path
|
|
13
|
+
(:mod:`content.text` / :func:`build.collect_text` stay byte-for-byte; the hut golden is preserved): carried
|
|
14
|
+
text is APPENDED after the authored block, in a disjoint txid band.
|
|
15
|
+
|
|
16
|
+
Engine / format facts this relies on (verified against the real bytes -- see docs/TEXT_CARRY.md):
|
|
17
|
+
|
|
18
|
+
* A window's TXID is a 2-BYTE immediate operand (operand 2 for ``WindowSync``/``WindowAsync`` 0x1F/0x20,
|
|
19
|
+
operand 3 for the ``...Ex`` variants 0x95/0x96 -- :data:`dialogue.WINDOW_OPS`). Re-emitting at a band
|
|
20
|
+
>= :data:`CARRY_BASE_TXID` (1000) keeps the new id in 2 bytes -> a SAME-LENGTH in-place patch via
|
|
21
|
+
:func:`content.object._arg_byte_offset` (the exact primitive the slot/uid remap uses). FF9 NEVER computes
|
|
22
|
+
a window txid (census: 0/24166 expression txids), so every one is statically remappable.
|
|
23
|
+
* The carry BAND must clear the base game's real txids (census max = 863, field 1607) AND the fork's own
|
|
24
|
+
authored band (:data:`content.text.DEFAULT_BASE_TXID` = 500). 1000 is the first unconditionally-safe
|
|
25
|
+
floor (no real field uses a txid >= 1000) and still a 2-byte immediate (<= 65535).
|
|
26
|
+
* ``.mes`` text is PER-LANGUAGE (the 7 :data:`config.LANGS`). The carried text is shipped in EVERY language
|
|
27
|
+
the build ships; a per-language entry is carried VERBATIM -- an empty entry stays empty (field 357 txid
|
|
28
|
+
470 is empty in us/uk but populated in fr/gr/it/jp -- a us-fallback would WIPE the French/German text).
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
from .. import dialogue as _dialogue
|
|
37
|
+
from ..config import LANGS
|
|
38
|
+
from ..eb import EbScript
|
|
39
|
+
from ..eb.disasm import argsize
|
|
40
|
+
from . import object as _object
|
|
41
|
+
|
|
42
|
+
# The carried-text txid band: clear of the base game (max real txid 863) AND the fork's authored 500+ band,
|
|
43
|
+
# and still a 2-byte immediate so every window remap is a same-length in-place patch. See the module note.
|
|
44
|
+
CARRY_BASE_TXID = 1000
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class CarriedEntry:
|
|
49
|
+
"""One donor ``.mes`` entry carried per-language. ``texts[lang]`` is the verbatim string for that
|
|
50
|
+
language (``''`` for an empty/absent donor entry -- carried as-is, never us-filled). ``strt``/``tail``
|
|
51
|
+
are the donor's window-geometry tags, preserved verbatim from the language the scan was keyed on (they
|
|
52
|
+
are language-independent geometry, not text)."""
|
|
53
|
+
donor_txid: int
|
|
54
|
+
new_txid: int
|
|
55
|
+
texts: dict # lang -> verbatim text (may be "")
|
|
56
|
+
strt: Optional[str] = None
|
|
57
|
+
tail: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --------------------------------------------------------------- which txids a graft will SHOW ---
|
|
61
|
+
def _grafted_object_windows(donor_eb, specs) -> set:
|
|
62
|
+
"""Every TXID a CARRIED object's grafted bytes will open. For each spec, the carried funcs are its
|
|
63
|
+
``carry_tags`` (``None`` = whole entry); we decode the donor entry and collect each window operand's txid
|
|
64
|
+
in the kept funcs. A REFUSED spec (``graft_safety == "refuse"``) carries nothing -> skipped, matching
|
|
65
|
+
:func:`content.object.graft_objects`."""
|
|
66
|
+
eb = donor_eb if isinstance(donor_eb, EbScript) else EbScript.from_bytes(donor_eb)
|
|
67
|
+
out: set = set()
|
|
68
|
+
for s in specs:
|
|
69
|
+
if s.get("graft_safety") == "refuse":
|
|
70
|
+
continue
|
|
71
|
+
slot = int(s["donor_idx"])
|
|
72
|
+
if not 0 <= slot < eb.entry_count:
|
|
73
|
+
continue
|
|
74
|
+
keep = s.get("carry_tags")
|
|
75
|
+
keep = None if keep is None else {int(t) for t in keep}
|
|
76
|
+
e = eb.entry(slot)
|
|
77
|
+
if e.empty:
|
|
78
|
+
continue
|
|
79
|
+
for f in e.funcs:
|
|
80
|
+
if keep is not None and f.tag not in keep:
|
|
81
|
+
continue
|
|
82
|
+
for ins in eb.instrs(f):
|
|
83
|
+
opnd = _dialogue.WINDOW_OPS.get(ins.op)
|
|
84
|
+
if opnd is None:
|
|
85
|
+
continue
|
|
86
|
+
if opnd < len(ins.arg_is_expr) and ins.arg_is_expr[opnd]:
|
|
87
|
+
continue # computed txid (does not occur in real fields) -- skip
|
|
88
|
+
txid = ins.imm(opnd)
|
|
89
|
+
if txid is not None:
|
|
90
|
+
out.add(int(txid))
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _grafted_player_windows(player_specs) -> set:
|
|
95
|
+
"""Every TXID a grafted PLAYER function will open. Only ``clean`` and ``text`` player funcs are carried
|
|
96
|
+
(a ``text`` func is graftable once its windows are remapped -- the whole point of carry); we decode each
|
|
97
|
+
grafted body and collect its window txids. Other safety classes are not grafted -> skipped."""
|
|
98
|
+
out: set = set()
|
|
99
|
+
for s in player_specs:
|
|
100
|
+
if s.get("safety") not in ("clean", "text"):
|
|
101
|
+
continue
|
|
102
|
+
body = s.get("body")
|
|
103
|
+
if not body:
|
|
104
|
+
continue
|
|
105
|
+
for ins in _object_iter(body):
|
|
106
|
+
opnd = _dialogue.WINDOW_OPS.get(ins.op)
|
|
107
|
+
if opnd is None:
|
|
108
|
+
continue
|
|
109
|
+
if opnd < len(ins.arg_is_expr) and ins.arg_is_expr[opnd]:
|
|
110
|
+
continue
|
|
111
|
+
txid = ins.imm(opnd)
|
|
112
|
+
if txid is not None:
|
|
113
|
+
out.add(int(txid))
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _object_iter(body):
|
|
118
|
+
from ..eb.disasm import iter_code
|
|
119
|
+
return iter_code(bytes(body), 0, len(body))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------- collect the carry plan ---
|
|
123
|
+
def collect_carry(donor_eb, object_specs, player_specs, field, lang_loader) -> list:
|
|
124
|
+
"""Build the carry plan: the donor txids the grafts will SHOW, each assigned a fresh band txid and its
|
|
125
|
+
per-language text. ``donor_eb`` is the donor field's ``.eb`` bytes; ``object_specs`` / ``player_specs``
|
|
126
|
+
are the graft specs (:func:`eventscan.scan_objects_verbatim` / :func:`scan_player_funcs`). ``field`` is
|
|
127
|
+
the donor field id (for the text loader). ``lang_loader(txids, lang) -> {txid: MesEntry}`` reads the
|
|
128
|
+
donor's per-language ``.mes`` (injected so this stays install-free + testable -- in the kit it is
|
|
129
|
+
:func:`_field_text_loader`).
|
|
130
|
+
|
|
131
|
+
Returns an ordered ``list[CarriedEntry]`` (sorted by donor txid for determinism), one per distinct
|
|
132
|
+
referenced txid. ``new_txid`` is :data:`CARRY_BASE_TXID` + i. An empty plan (no grafted windows) returns
|
|
133
|
+
``[]`` -- the build then carries no text (byte-identical to no-carry).
|
|
134
|
+
"""
|
|
135
|
+
wanted = _grafted_object_windows(donor_eb, object_specs) | _grafted_player_windows(player_specs)
|
|
136
|
+
if not wanted:
|
|
137
|
+
return []
|
|
138
|
+
donor_txids = sorted(wanted)
|
|
139
|
+
|
|
140
|
+
# per-language text. The geometry tags (strt/tail) are read once from the primary language (us); they
|
|
141
|
+
# are language-independent window geometry. Each language's TEXT is carried independently (verbatim).
|
|
142
|
+
per_lang: dict = {}
|
|
143
|
+
for lang in LANGS:
|
|
144
|
+
per_lang[lang] = lang_loader(donor_txids, lang) or {}
|
|
145
|
+
primary = per_lang.get(LANGS[0], {})
|
|
146
|
+
|
|
147
|
+
plan = []
|
|
148
|
+
for i, dt in enumerate(donor_txids):
|
|
149
|
+
texts = {}
|
|
150
|
+
for lang in LANGS:
|
|
151
|
+
e = per_lang[lang].get(dt)
|
|
152
|
+
texts[lang] = e.text if (e is not None and e.text is not None) else ""
|
|
153
|
+
pe = primary.get(dt)
|
|
154
|
+
plan.append(CarriedEntry(donor_txid=dt, new_txid=CARRY_BASE_TXID + i, texts=texts,
|
|
155
|
+
strt=(pe.strt if pe else None), tail=(pe.tail if pe else None)))
|
|
156
|
+
return plan
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _field_text_loader(field, game=None):
|
|
160
|
+
"""The kit's real per-language loader for :func:`collect_carry`: reads the donor field's ``<zone>.mes``
|
|
161
|
+
block from the install (the authoritative ``eventIDToMESID`` zone), picking the requested language. A
|
|
162
|
+
thin wrapper over :func:`dialogue._load_field_text` keyed on the field's text zone -- so the SAME block
|
|
163
|
+
selection :func:`dialogue.read_field_dialogue` proved is used (the want-txids coverage + language score
|
|
164
|
+
pick the right per-language copy)."""
|
|
165
|
+
from .._fieldtext import EVENT_ID_TO_MES
|
|
166
|
+
zone_id = EVENT_ID_TO_MES.get(int(field))
|
|
167
|
+
|
|
168
|
+
def _load(txids, lang):
|
|
169
|
+
return _dialogue._load_field_text(txids, lang, game=game, zone_id=zone_id)
|
|
170
|
+
return _load
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --------------------------------------------------------------- remap the grafted windows ---
|
|
174
|
+
def _remap_windows_in_entry(eb_bytes, entry_index, txid_map, carry_tags=None) -> bytes:
|
|
175
|
+
"""Same-length, in-place remap of every window TXID a grafted entry's CARRIED funcs reference, donor ->
|
|
176
|
+
carried band, via the decoder-derived 2-byte offset (:func:`content.object._arg_byte_offset`). Only the
|
|
177
|
+
``carry_tags`` funcs are patched (``None`` = whole entry), mirroring what was actually grafted. A txid
|
|
178
|
+
not in ``txid_map`` (e.g. a system window pointing at a base id we chose not to carry) is left
|
|
179
|
+
untouched. The patch is byte-for-byte length-preserving (2-byte immediate -> 2-byte immediate)."""
|
|
180
|
+
eb = EbScript.from_bytes(eb_bytes)
|
|
181
|
+
b = bytearray(eb_bytes)
|
|
182
|
+
keep = None if carry_tags is None else {int(t) for t in carry_tags}
|
|
183
|
+
for f in eb.entry(entry_index).funcs:
|
|
184
|
+
if keep is not None and f.tag not in keep:
|
|
185
|
+
continue
|
|
186
|
+
for ins in eb.instrs(f):
|
|
187
|
+
opnd = _dialogue.WINDOW_OPS.get(ins.op)
|
|
188
|
+
if opnd is None:
|
|
189
|
+
continue
|
|
190
|
+
if opnd < len(ins.arg_is_expr) and ins.arg_is_expr[opnd]:
|
|
191
|
+
continue
|
|
192
|
+
txid = ins.imm(opnd)
|
|
193
|
+
if txid is None or int(txid) not in txid_map:
|
|
194
|
+
continue
|
|
195
|
+
new = txid_map[int(txid)]
|
|
196
|
+
bo = _object._arg_byte_offset(ins, opnd)
|
|
197
|
+
if bo is None or argsize(ins.op, opnd) != 2:
|
|
198
|
+
continue # only same-length 2-byte window operands are patchable
|
|
199
|
+
b[ins.off + bo] = new & 0xFF
|
|
200
|
+
b[ins.off + bo + 1] = (new >> 8) & 0xFF
|
|
201
|
+
return bytes(b)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def remap_object_windows(eb_bytes, object_specs, slot_map, txid_map) -> bytes:
|
|
205
|
+
"""Patch each carried OBJECT's grafted entry (now at its fork slot ``slot_map[donor_idx]``) so its
|
|
206
|
+
window TXIDs point at the carried band. ``slot_map`` is the donor_idx -> fork-slot map the object graft
|
|
207
|
+
built; ``txid_map`` is donor_txid -> carried_txid. A refused/un-slotted spec is skipped."""
|
|
208
|
+
data = eb_bytes
|
|
209
|
+
for s in object_specs:
|
|
210
|
+
if s.get("graft_safety") == "refuse":
|
|
211
|
+
continue
|
|
212
|
+
slot = slot_map.get(int(s["donor_idx"]))
|
|
213
|
+
if slot is None:
|
|
214
|
+
continue
|
|
215
|
+
data = _remap_windows_in_entry(data, slot, txid_map, s.get("carry_tags"))
|
|
216
|
+
return data
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def remap_player_func_windows(eb_bytes, player_entry, tag_map, txid_map) -> bytes:
|
|
220
|
+
"""Patch each grafted PLAYER function (now at its fork tag ``tag_map[donor_tag]`` on the player entry) so
|
|
221
|
+
its window TXIDs point at the carried band. ``tag_map`` is donor_tag -> fork-tag; only the grafted tags
|
|
222
|
+
are touched. Same same-length 2-byte patch as the object path."""
|
|
223
|
+
eb = EbScript.from_bytes(eb_bytes)
|
|
224
|
+
fork_tags = set(tag_map.values())
|
|
225
|
+
data = eb_bytes
|
|
226
|
+
for f in eb.entry(player_entry).funcs:
|
|
227
|
+
if f.tag not in fork_tags:
|
|
228
|
+
continue
|
|
229
|
+
data = _remap_windows_in_entry(data, player_entry, txid_map, [f.tag])
|
|
230
|
+
return data
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# --------------------------------------------------------------- emit the carried .mes ---
|
|
234
|
+
def _mes_entry_verbatim(entry: CarriedEntry, lang: str) -> str:
|
|
235
|
+
"""One carried ``.mes`` entry for ``lang``, re-emitted FAITHFULLY at its new txid: the donor's exact STRT
|
|
236
|
+
(window geometry, kept verbatim) + its TAIL only if the donor had one + the language's verbatim text. NOT
|
|
237
|
+
:func:`content.text.mes_entry` (which forces a default STRT/TAIL) -- a carried entry must preserve the
|
|
238
|
+
donor's own geometry tags, else the window resizes."""
|
|
239
|
+
strt = entry.strt if entry.strt is not None else "10,1"
|
|
240
|
+
tail = f"[TAIL={entry.tail}]" if entry.tail else ""
|
|
241
|
+
return f"_[TXID={entry.new_txid}][STRT={strt}]{tail}{entry.texts.get(lang, '')}[ENDN]"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def carried_mes_body(plan, lang: str) -> str:
|
|
245
|
+
"""The carried ``.mes`` block for one language: every :class:`CarriedEntry` re-emitted at its new txid,
|
|
246
|
+
verbatim. Each entry carries its own ``[TXID=]`` re-index (so it is position-independent), letting this
|
|
247
|
+
block be APPENDED after the fork's authored block without disturbing it. Empty plan -> ``''``."""
|
|
248
|
+
if not plan:
|
|
249
|
+
return ""
|
|
250
|
+
return "\n".join(_mes_entry_verbatim(e, lang) for e in plan) + "\n"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# --------------------------------------------------------------- the sidecar (import write / build read) ---
|
|
254
|
+
SIDECAR_VERSION = 1
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def plan_to_sidecar(plan, *, field=None) -> dict:
|
|
258
|
+
"""Serialise the carry plan to a JSON-able dict (the ``<name>.carrytext.json`` sidecar). One record per
|
|
259
|
+
carried entry: the donor/new txids, the donor geometry tags, and the per-language verbatim text. The
|
|
260
|
+
strings are SE-derived, so the sidecar is gitignored (mirrors ``.object*.bin`` / ``.dialogue.json``)."""
|
|
261
|
+
return {
|
|
262
|
+
"version": SIDECAR_VERSION,
|
|
263
|
+
"field": (int(field) if field is not None else None),
|
|
264
|
+
"base_txid": CARRY_BASE_TXID,
|
|
265
|
+
"langs": list(LANGS),
|
|
266
|
+
"entries": [{"donor_txid": e.donor_txid, "new_txid": e.new_txid,
|
|
267
|
+
"strt": e.strt, "tail": e.tail, "texts": dict(e.texts)} for e in plan],
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def write_sidecar(path, plan, *, field=None) -> None:
|
|
272
|
+
"""Write the carry sidecar to ``path`` (UTF-8 JSON). No-op file is still written for an empty plan so the
|
|
273
|
+
build's ``[carry_text] bin`` ref always resolves."""
|
|
274
|
+
from pathlib import Path
|
|
275
|
+
Path(path).write_text(json.dumps(plan_to_sidecar(plan, field=field), ensure_ascii=False, indent=1),
|
|
276
|
+
encoding="utf-8")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def load_sidecar(path) -> list:
|
|
280
|
+
"""Read a ``<name>.carrytext.json`` sidecar back into an ordered ``list[CarriedEntry]`` (the build's
|
|
281
|
+
consume side). Each entry's ``texts`` is filled for every shipped language (missing -> ``''``, so an
|
|
282
|
+
absent language never errors -- it ships an empty window, harmless)."""
|
|
283
|
+
from pathlib import Path
|
|
284
|
+
raw = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
285
|
+
out = []
|
|
286
|
+
for rec in raw.get("entries", []):
|
|
287
|
+
texts = {lang: (rec.get("texts", {}).get(lang) or "") for lang in LANGS}
|
|
288
|
+
out.append(CarriedEntry(donor_txid=int(rec["donor_txid"]), new_txid=int(rec["new_txid"]),
|
|
289
|
+
texts=texts, strt=rec.get("strt"), tail=rec.get("tail")))
|
|
290
|
+
return out
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Verbatim-`.eb` fork -- ship a real field's WHOLE event script instead of re-synthesizing it.
|
|
2
|
+
|
|
3
|
+
The faithful realization of "entry-0 carry" (docs/FORK_FIDELITY.md): a story field's entry-0 ``Main_Init``
|
|
4
|
+
arms its objects/regions and gates the cast by ScenarioCounter, and the gated doors read MAP vars it sets --
|
|
5
|
+
so the only way those references resolve is to keep the donor's WHOLE entry layout. This mode does exactly
|
|
6
|
+
that: the build ships the donor's `.eb` verbatim (entry-0 + every object + every gateway, slots intact) and
|
|
7
|
+
only **remaps the `Field()` destinations**; the field then runs its real logic. Proven in-game on Dali Inn
|
|
8
|
+
(the gated door opens, the cast gates by story beat).
|
|
9
|
+
|
|
10
|
+
The declarative content blocks ([[npc]]/[[gateway]]/...) are NOT used in this mode -- the `.eb` is whole, so
|
|
11
|
+
there is nothing to synthesize. Pair with a `[startup]` block to boot a chosen beat. LIMITS (vs a perfect
|
|
12
|
+
clone): the donor `.mes` text is a separate carry (TXIDs may not resolve until then), and a fork reached by
|
|
13
|
+
F6-warp has no entrance fade to mask first-frame model streaming.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import struct
|
|
19
|
+
|
|
20
|
+
from ..eb import EbScript
|
|
21
|
+
|
|
22
|
+
FIELD_OP = 0x2B # Field(dest) -- the warp; dest is a 2-byte literal at instruction offset +2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def remap_fields(eb_bytes: bytes, retarget: dict) -> bytes:
|
|
26
|
+
"""Patch every ``Field(id)`` literal whose id is in ``retarget`` (real destination -> fork id). Ids NOT in
|
|
27
|
+
the map are left as live seams (the door warps back into the real game). Empty ``retarget`` -> unchanged."""
|
|
28
|
+
if not retarget:
|
|
29
|
+
return eb_bytes
|
|
30
|
+
eb = EbScript.from_bytes(eb_bytes)
|
|
31
|
+
buf = bytearray(eb_bytes)
|
|
32
|
+
for e in eb.entries:
|
|
33
|
+
if e.empty:
|
|
34
|
+
continue
|
|
35
|
+
for f in e.funcs:
|
|
36
|
+
for i in eb.instrs(f):
|
|
37
|
+
if i.op == FIELD_OP and i.imm(0) in retarget:
|
|
38
|
+
struct.pack_into("<H", buf, i.off + 2, int(retarget[i.imm(0)]) & 0xFFFF)
|
|
39
|
+
return bytes(buf)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_retarget(dests, id_remap=None):
|
|
43
|
+
"""The ``[verbatim_eb] retarget`` portion for a verbatim fork's ``Field()`` exits, plus the count of
|
|
44
|
+
exits actually retargeted.
|
|
45
|
+
|
|
46
|
+
``dests`` = the field's distinct ``Field(id)`` destinations (real ids). With ``id_remap`` (a
|
|
47
|
+
``{real_id: fork_id}`` map from import-chain) this emits a LIVE ``retarget = {...}`` table for the
|
|
48
|
+
in-chain destinations and a comment listing the rest (left as live seams back into the real game) --
|
|
49
|
+
so a forked CHAIN's doors warp into its OWN member forks. Without ``id_remap`` (single-field
|
|
50
|
+
``import --verbatim``) it emits the commented-out fill-in template the author edits by hand, BYTE-FOR-BYTE
|
|
51
|
+
as before (so the single-field golden is unchanged). Returns ``(toml_text, n_retargeted)``."""
|
|
52
|
+
dests = list(dests)
|
|
53
|
+
if id_remap:
|
|
54
|
+
inchain = [(d, int(id_remap[d])) for d in dests if d in id_remap]
|
|
55
|
+
if inchain:
|
|
56
|
+
seams = [d for d in dests if d not in id_remap]
|
|
57
|
+
tbl = "retarget = { " + ", ".join(f"{a} = {b}" for a, b in inchain) + " }\n"
|
|
58
|
+
note = ("# (the rest are live seams back into the real game -- not in this chain: "
|
|
59
|
+
+ ", ".join(map(str, seams)) + ")\n") if seams else ""
|
|
60
|
+
return tbl + note, len(inchain)
|
|
61
|
+
body = "".join(f"# {d} = 0\n" for d in dests) or "# (this field has no Field() exits)\n"
|
|
62
|
+
return ("# retarget = {\n" + body + "# }\n"), 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def verbatim_eb(project):
|
|
66
|
+
"""The verbatim `.eb` to ship for ``project`` (from its ``[verbatim_eb]`` block, ``bin`` + optional
|
|
67
|
+
``retarget``), Field-remapped -- or ``None`` if the project isn't a verbatim fork (the build then
|
|
68
|
+
synthesizes from the field.toml as usual). The same bytecode ships for every language (it is
|
|
69
|
+
language-identical; only the cosmetic name field differs, which the donor's already carries)."""
|
|
70
|
+
spec = project.raw.get("verbatim_eb")
|
|
71
|
+
if not spec or not spec.get("bin"):
|
|
72
|
+
return None
|
|
73
|
+
retarget = {int(k): int(v) for k, v in (spec.get("retarget") or {}).items()}
|
|
74
|
+
return remap_fields(project.path(spec["bin"]).read_bytes(), retarget)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def verbatim_mes(project, lang: str):
|
|
78
|
+
"""The donor field's WHOLE `.mes` text body to ship for ``lang`` (from the ``[verbatim_eb] text`` JSON
|
|
79
|
+
sidecar, ``{lang: body}``) -- the verbatim `.eb`'s index-txids resolve straight into it. Falls back to the
|
|
80
|
+
``us`` body for a language the dialogue reader couldn't distinguish. ``None`` if the fork carries no text."""
|
|
81
|
+
spec = project.raw.get("verbatim_eb") or {}
|
|
82
|
+
tf = spec.get("text")
|
|
83
|
+
if not tf:
|
|
84
|
+
return None
|
|
85
|
+
data = json.loads(project.path(tf).read_text(encoding="utf-8"))
|
|
86
|
+
return data.get(lang) or data.get("us")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Reproduce a real field's LOAD-TIME engine walkmesh hotfix in a fork.
|
|
2
|
+
|
|
3
|
+
A few real fields rely on a hardcoded Memoria hotfix (keyed on the real ``fldMapNo``) that toggles
|
|
4
|
+
walkmesh-triangle active-state at field load -- e.g. Gulug/Room (2356) deactivates the broken-wall triangles
|
|
5
|
+
so the player can't walk through the gap. A verbatim/native fork runs at a custom id (>= 4000), so that
|
|
6
|
+
``fldMapNo`` guard is false and the hotfix never fires -> the forked walkmesh is wrong there. See the catalog
|
|
7
|
+
and the two tractability classes in :mod:`ff9mapkit.walkmesh_hotfixes`.
|
|
8
|
+
|
|
9
|
+
This module reproduces the AUTO (load-time, unconditional) class: it prepends ``EnablePathTriangle(tri, state)``
|
|
10
|
+
-- opcode 0x9A, whose engine handler IS ``WalkMesh.BGI_triSetActive`` -- to ``Main_Init`` (entry-0 tag-0), so
|
|
11
|
+
the triangles are in the right state from the first frame, exactly as the engine sets them at load. The
|
|
12
|
+
``.bgi`` stays byte-verbatim (the fix lives in the script layer). A tag-0 prepend (``rel_off == 0``) is
|
|
13
|
+
shift-safe even on a jump-table donor, and ``EnablePathTriangle`` is language-identical. No toggles -> the eb
|
|
14
|
+
is returned unchanged. Mirrors :mod:`ff9mapkit.content.areatitle` / :mod:`ff9mapkit.content.startup`.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ..eb import edit, opcodes
|
|
19
|
+
|
|
20
|
+
ENABLE_PATH_TRIANGLE = 0x9A # EnablePathTriangle(triId, active) -- the engine handler is BGI_triSetActive
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def toggles_body(toggles) -> bytes:
|
|
24
|
+
"""The bare ``EnablePathTriangle(tri, state)`` sequence for ``toggles`` (an iterable of ``(tri, state)``),
|
|
25
|
+
or ``b""`` when there are none. ``state`` is coerced to 0/1 (1 = active/walkable)."""
|
|
26
|
+
out = b""
|
|
27
|
+
for tri, state in (toggles or ()):
|
|
28
|
+
out += opcodes.encode(ENABLE_PATH_TRIANGLE, int(tri), 1 if int(state) else 0)
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def apply_tri_toggles(eb_bytes, toggles) -> bytes:
|
|
33
|
+
"""Prepend the load-time triangle toggles to ``Main_Init`` (entry-0 tag-0). Returns ``eb_bytes`` unchanged
|
|
34
|
+
when ``toggles`` is empty (so a field with no walkmesh hotfix builds byte-for-byte as before)."""
|
|
35
|
+
body = toggles_body(toggles)
|
|
36
|
+
if not body:
|
|
37
|
+
return eb_bytes
|
|
38
|
+
return edit.insert_in_function(eb_bytes, 0, 0, 0, body)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Bundled binary data + accessors.
|
|
2
|
+
|
|
3
|
+
Contents
|
|
4
|
+
--------
|
|
5
|
+
blank_field/<lang>.eb.bytes
|
|
6
|
+
The canonical *blank field* event script (956 bytes), one per language. This is the
|
|
7
|
+
proven minimal playable field used as the starting point for every built field: a clean
|
|
8
|
+
Main_Init (no stray popups, standard movement) plus a single player object. Content
|
|
9
|
+
injectors clone/extend it; the builder writes it (with content) per language.
|
|
10
|
+
|
|
11
|
+
region_template.bin
|
|
12
|
+
The 272-byte field-exit region body (a SetRegion polygon -> CalculateExitPosition /
|
|
13
|
+
ExitField -> PreloadField -> set FieldEntrance -> Field(target)). The gateway injector
|
|
14
|
+
patches its polygon / entrance / target and appends it as a new entry.
|
|
15
|
+
|
|
16
|
+
Provenance / distribution note
|
|
17
|
+
------------------------------
|
|
18
|
+
These blobs are DERIVED from Final Fantasy IX field data (the blank field is a cleaned clone of a
|
|
19
|
+
base field; the region template is a base field's exit region). To avoid redistributing Square Enix
|
|
20
|
+
game bytes, the public repo ships **none** of them -- they are regenerated from the user's own,
|
|
21
|
+
legally-owned FF9 install by ``ff9mapkit extract-templates`` (see :mod:`ff9mapkit.provision` and
|
|
22
|
+
docs/PROVENANCE.md) into a local, gitignored cache. The accessors below read that cache and raise a
|
|
23
|
+
clear "run extract-templates" message if it isn't present yet.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from ..config import LANGS
|
|
29
|
+
from .. import provision
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def blank_field_bytes(lang: str = "us") -> bytes:
|
|
33
|
+
"""Bytes of the blank field event script for *lang* (defaults to 'us'). Regenerated from the
|
|
34
|
+
user's FF9 install by ``ff9mapkit extract-templates``; raises if that hasn't been run."""
|
|
35
|
+
if lang not in LANGS:
|
|
36
|
+
raise ValueError(f"unknown language {lang!r}; expected one of {LANGS}")
|
|
37
|
+
p = provision.blank_dir() / f"{lang}.eb.bytes"
|
|
38
|
+
if not p.is_file():
|
|
39
|
+
raise FileNotFoundError(provision.MISSING_MSG)
|
|
40
|
+
return p.read_bytes()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def region_template() -> bytes:
|
|
44
|
+
"""The 272-byte field-exit region template (regenerated from the user's install)."""
|
|
45
|
+
p = provision.region_template_path()
|
|
46
|
+
if not p.is_file():
|
|
47
|
+
raise FileNotFoundError(provision.MISSING_MSG)
|
|
48
|
+
return p.read_bytes()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""MAINTAINER tool: regenerate the provenance artifacts (patches + manifest) from a vanilla FF9
|
|
2
|
+
install, so the public repo can ship ZERO Square Enix bytes.
|
|
3
|
+
|
|
4
|
+
Run from a repo checkout with $FF9_GAME_PATH set (or --game), against an UNMODIFIED install:
|
|
5
|
+
|
|
6
|
+
python -m ff9mapkit.data._regen_provenance
|
|
7
|
+
|
|
8
|
+
It writes ff9mapkit/data/provenance/{manifest.json, blank.<lang>.patch, region_template.patch}
|
|
9
|
+
-- all OURS (copy/insert diffs + hashes + source field names; no game bytes) -- and verifies that
|
|
10
|
+
`ff9mapkit extract-templates` would reproduce the current on-disk blobs byte-for-byte. The actual
|
|
11
|
+
game-derived blobs (blank_field/, region_template.bin, the binary test fixtures) are NOT committed;
|
|
12
|
+
end users regenerate them locally from their own install.
|
|
13
|
+
|
|
14
|
+
This is the inverse bookend of provision.extract_templates: this AUTHORS the patches; that APPLIES them.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .. import provision
|
|
24
|
+
from ..config import LANGS
|
|
25
|
+
|
|
26
|
+
HERE = Path(__file__).resolve().parent
|
|
27
|
+
PROV = HERE / "provenance"
|
|
28
|
+
|
|
29
|
+
# --- the base fields the kit's assets are derived from (all present in a vanilla install) ----------
|
|
30
|
+
BLANK_SRC = "fbg_n11_ldbm_map203_lb_hng_0" # field 1357 (L.Castle/Hangar) -> the cleaned blank field
|
|
31
|
+
REGION_SRC = "fbg_n01_alxt_map031_at_wpn_0" # ALEX3_AT_WEAPON -> the exit-region template
|
|
32
|
+
ALEX_SRC = "fbg_n01_alxt_map016_at_msa_0" # ALEX1_AT_STREET_A (vanilla field 100) -> eventscan oracle
|
|
33
|
+
GRGR_SRC = "fbg_n21_grgr_map420_gr_cen_0" # Gargan Roo centre -> camera fixture
|
|
34
|
+
MULTIFLOOR_SRC = "fbg_n00_tshp_map008_th_upr_0" # Prima Vista upper deck (3 floors) -> multi-floor walkmesh
|
|
35
|
+
# (chosen because its real .bgi round-trips byte-exact through the kit's codec AND is seam-clean:
|
|
36
|
+
# fully walk-reachable, strands on obj re-export, reconciles -- so it exercises the seam machinery.)
|
|
37
|
+
|
|
38
|
+
# the Session-12 Alexandria door, re-injected onto the vanilla field so the eventscan oracle keeps its
|
|
39
|
+
# "3 real exits + 1 injected door" shape without redistributing AlternateFantasy's modified bytes.
|
|
40
|
+
ALEX_DOOR = {"target": 4000, "entrance": 0,
|
|
41
|
+
"zone": [[-700, 2200], [200, 2200], [200, 3400], [-700, 3400]]}
|
|
42
|
+
|
|
43
|
+
sha = lambda b: hashlib.sha256(b).hexdigest()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv=None) -> int:
|
|
47
|
+
import argparse
|
|
48
|
+
from .. import extract
|
|
49
|
+
ap = argparse.ArgumentParser(description="regenerate ff9mapkit provenance patches + manifest")
|
|
50
|
+
ap.add_argument("--game", help="FF9 install path (else $FF9_GAME_PATH / config)")
|
|
51
|
+
args = ap.parse_args(argv)
|
|
52
|
+
game = args.game
|
|
53
|
+
|
|
54
|
+
PROV.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
manifest: dict = {
|
|
56
|
+
"_note": ("ff9mapkit ships NO Final Fantasy IX game data. These entries describe how to "
|
|
57
|
+
"regenerate the small set of base assets the kit needs from YOUR OWN install via "
|
|
58
|
+
"`ff9mapkit extract-templates`. The .patch files contain only our edits + copy "
|
|
59
|
+
"offsets (never game bytes). See docs/PROVENANCE.md."),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# 1) blank field: per-language copy/insert patch (1357 -> our cleaned blank)
|
|
63
|
+
blank_sha = {}
|
|
64
|
+
for lang in LANGS:
|
|
65
|
+
src = extract.extract_event_script(BLANK_SRC, game=game, lang=lang)
|
|
66
|
+
dst = (HERE / "blank_field" / f"{lang}.eb.bytes").read_bytes() # current on-disk blank
|
|
67
|
+
patch = provision.make_patch(src, dst)
|
|
68
|
+
(PROV / f"blank.{lang}.patch").write_text(json.dumps(patch), encoding="utf-8")
|
|
69
|
+
assert provision.apply_patch(src, patch) == dst, f"blank {lang} patch round-trip failed"
|
|
70
|
+
bad = provision.patch_game_runs(src, patch)
|
|
71
|
+
assert not bad, f"blank {lang}: patch would ship game-byte runs {bad}"
|
|
72
|
+
blank_sha[lang] = sha(dst)
|
|
73
|
+
print(f" blank.{lang}.patch insert={patch['insert_bytes']}B (no game runs) -> reproduces blank OK")
|
|
74
|
+
manifest["blank"] = {"source_fbg": BLANK_SRC, "patch": "blank.{lang}.patch", "sha256": blank_sha}
|
|
75
|
+
|
|
76
|
+
# 2) region template: single patch (ALEX3_AT_WEAPON -> our 272B exit-region template)
|
|
77
|
+
src = extract.extract_event_script(REGION_SRC, game=game, lang="us")
|
|
78
|
+
dst = (HERE / "region_template.bin").read_bytes()
|
|
79
|
+
patch = provision.make_patch(src, dst)
|
|
80
|
+
(PROV / "region_template.patch").write_text(json.dumps(patch), encoding="utf-8")
|
|
81
|
+
assert provision.apply_patch(src, patch) == dst, "region patch round-trip failed"
|
|
82
|
+
assert not provision.patch_game_runs(src, patch), "region patch would ship game-byte runs"
|
|
83
|
+
manifest["region_template"] = {"source_fbg": REGION_SRC, "lang": "us",
|
|
84
|
+
"patch": "region_template.patch", "sha256": sha(dst)}
|
|
85
|
+
print(f" region_template.patch insert={patch['insert_bytes']}B -> reproduces template OK")
|
|
86
|
+
|
|
87
|
+
# 3) test fixtures, regenerated from the install (sha recorded so extract-templates self-verifies)
|
|
88
|
+
fixtures = {}
|
|
89
|
+
fix_dir = HERE.parent.parent / "tests" / "fixtures"
|
|
90
|
+
|
|
91
|
+
# alex100: vanilla field 100 + the kit's own door injection (no AlternateFantasy bytes)
|
|
92
|
+
from ..content import gateway as _gw
|
|
93
|
+
van = extract.extract_event_script(ALEX_SRC, game=game, lang="us")
|
|
94
|
+
door = _gw.inject_gateway(van, ALEX_DOOR["target"], entrance=ALEX_DOOR["entrance"],
|
|
95
|
+
zone=_gw.quad_zone(ALEX_DOOR["zone"]))
|
|
96
|
+
fixtures["alex100-us.eb.bytes"] = {"source_fbg": ALEX_SRC, "kind": "event_with_gateway",
|
|
97
|
+
"lang": "us", "gateway": ALEX_DOOR, "sha256": sha(door)}
|
|
98
|
+
if fix_dir.is_dir():
|
|
99
|
+
(fix_dir / "alex100-us.eb.bytes").write_bytes(door)
|
|
100
|
+
|
|
101
|
+
# grgr.bgx: the real GRGR camera (extracted as a borrowable .bgx)
|
|
102
|
+
# multifloor.bgi.bytes: a real 3-floor walkmesh that round-trips byte-exact (codec + seam tests)
|
|
103
|
+
import tempfile
|
|
104
|
+
tg = Path(tempfile.mkdtemp())
|
|
105
|
+
extract.extract_field(GRGR_SRC, tg, game=game)
|
|
106
|
+
grgr_bgx = (tg / "camera.bgx").read_bytes()
|
|
107
|
+
tm = Path(tempfile.mkdtemp())
|
|
108
|
+
extract.extract_field(MULTIFLOOR_SRC, tm, game=game)
|
|
109
|
+
mf_bgi = (tm / "walkmesh.bgi").read_bytes()
|
|
110
|
+
fixtures["grgr.bgx"] = {"source_fbg": GRGR_SRC, "kind": "camera_bgx", "sha256": sha(grgr_bgx)}
|
|
111
|
+
fixtures["multifloor.bgi.bytes"] = {"source_fbg": MULTIFLOOR_SRC, "kind": "walkmesh_verbatim",
|
|
112
|
+
"sha256": sha(mf_bgi)}
|
|
113
|
+
if fix_dir.is_dir():
|
|
114
|
+
(fix_dir / "grgr.bgx").write_bytes(grgr_bgx)
|
|
115
|
+
(fix_dir / "multifloor.bgi.bytes").write_bytes(mf_bgi)
|
|
116
|
+
manifest["fixtures"] = fixtures
|
|
117
|
+
print(f" fixtures: alex100-us({len(door)}B) grgr.bgx({len(grgr_bgx)}B) "
|
|
118
|
+
f"multifloor.bgi({len(mf_bgi)}B)")
|
|
119
|
+
|
|
120
|
+
# 4) build goldens: the hut example's build outputs are DERIVATIVE (embed the blank), so we ship a
|
|
121
|
+
# hash, not the bytes. The build test compares fresh build output's hash to this.
|
|
122
|
+
manifest["goldens"] = _golden_hashes(game)
|
|
123
|
+
print(f" goldens (hash-only): {list(manifest['goldens'])}")
|
|
124
|
+
|
|
125
|
+
(PROV / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
126
|
+
print(f"\nwrote {PROV/'manifest.json'} + patches")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _golden_hashes(game) -> dict:
|
|
131
|
+
"""SHA-256 of the hut example's build outputs (the independent build-golden reference)."""
|
|
132
|
+
import tempfile
|
|
133
|
+
from ..build import FieldProject, build_mod, ModLayout
|
|
134
|
+
example = HERE.parent.parent / "examples" / "vivi-hut" / "hut_int.field.toml"
|
|
135
|
+
out = Path(tempfile.mkdtemp())
|
|
136
|
+
build_mod([FieldProject.load(example)], out, mod_name="GoldenCheck")
|
|
137
|
+
L = ModLayout(out)
|
|
138
|
+
return {"EVT_HUT_INT.eb.bytes/us": sha(L.eb_path("us", "EVT_HUT_INT.eb.bytes").read_bytes())}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "b536766f4e15836b9e83851bae513512be410b291829de3187d8345ef45c28b2", "out_len": 956, "out_sha256": "eb6ac7333a273333b9d7e296408a2cfad41b059acd06679c3fb2a50b815234ac", "insert_bytes": 16, "ops": [["c", 0, 4], ["i", "826b"], ["c", 12, 4], ["c", 4, 8], ["c", 26, 3], ["i", "85"], ["c", 32, 12], ["i", "82928294816682938140"], ["c", 44, 86], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 68, 60], ["c", 68, 8], ["c", 637, 319]]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "2ccd924b63800c95703ce7e766dde0036a9d6dd94b619d0bcfeb848ce1a7c49e", "out_len": 956, "out_sha256": "b06d7768c8485ee4c48443e2384730a0d447d2af8e84d87a20a3ee2a23c45f6b", "insert_bytes": 22, "ops": [["c", 0, 4], ["i", "826b"], ["c", 22, 5], ["i", "62828182938294828c8285815e"], ["c", 4, 12], ["i", "8294"], ["c", 16, 1], ["i", "66"], ["c", 18, 1], ["i", "93"], ["c", 24, 2], ["c", 44, 86], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 68, 60], ["c", 68, 8], ["c", 637, 319]]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "08b0cba27ddafe91062b7d565e4aece258c6b51d0fa82bfc2a999b14caa0fd09", "out_len": 956, "out_sha256": "5676ef381393767a4fef715cdfe1eca947fef10b1d0ec80dd247a0301fa66a34", "insert_bytes": 21, "ops": [["c", 0, 5], ["i", "6b81448140"], ["c", 6, 1], ["i", "62"], ["c", 8, 1], ["i", "8182938294"], ["c", 10, 3], ["i", "85"], ["c", 24, 2], ["c", 28, 13], ["i", "948166"], ["c", 42, 1], ["i", "938140"], ["c", 44, 86], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 68, 60], ["c", 68, 8], ["c", 637, 319]]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "251dfb52f8c962a0cc5868c5c7723066c445058cc09acf187b48c99fa8262569", "out_len": 956, "out_sha256": "d968b0d1bafe3b2c60ee02c91a435986f71c23b4afb1e78fa65a67294c5b290b", "insert_bytes": 15, "ops": [["c", 0, 5], ["i", "6b"], ["c", 6, 5], ["i", "62"], ["c", 12, 1], ["i", "81"], ["c", 14, 1], ["i", "93"], ["c", 16, 1], ["i", "94"], ["c", 20, 3], ["i", "85"], ["c", 26, 15], ["i", "948166"], ["c", 42, 1], ["i", "938140"], ["c", 44, 86], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 70, 58], ["c", 70, 10], ["c", 637, 319]]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "89ef1de34133a8b8e22a1502cf4fb65ab2c77237a80ef63d9b5548066c40edf7", "out_len": 956, "out_sha256": "e5595df0fcd0685bf8702b2286ac28bc0f8c54169e4c881e61ff5c0ec09c1219", "insert_bytes": 39, "ops": [["c", 0, 4], ["i", "826b814481408262828182"], ["c", 7, 1], ["i", "8294828c828581"], ["c", 19, 1], ["i", "82678281828e8287828182928294816682"], ["c", 23, 1], ["c", 26, 1], ["i", "40"], ["c", 44, 86], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 64, 64], ["c", 28, 4], ["c", 637, 319]]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "ccb9439159f288a85b231bff945e52dac7b5dc01662f09484479a06d0c627df1", "out_len": 956, "out_sha256": "2289085a5dc1030d8cd1541619e34a0bd722a0957d0849bfaf077c7e30e90452", "insert_bytes": 3, "ops": [["c", 0, 130], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 70, 58], ["c", 70, 10], ["c", 637, 319]]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"src_sha256": "ccb9439159f288a85b231bff945e52dac7b5dc01662f09484479a06d0c627df1", "out_len": 956, "out_sha256": "2289085a5dc1030d8cd1541619e34a0bd722a0957d0849bfaf077c7e30e90452", "insert_bytes": 3, "ops": [["c", 0, 130], ["i", "6c"], ["c", 131, 302], ["i", "ffff"], ["c", 435, 29], ["c", 532, 105], ["c", 70, 58], ["c", 70, 10], ["c", 637, 319]]}
|