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,72 @@
|
|
|
1
|
+
"""Regenerate ``_animdb.py`` (FF9 main-character animation id -> name) from a Memoria checkout.
|
|
2
|
+
|
|
3
|
+
A *maintainer* tool, not part of the runtime -- the table is baked into ``_animdb.py`` so the kit can
|
|
4
|
+
offer an author-facing animation catalog (pick a cutscene gesture by name) with no Memoria source at
|
|
5
|
+
runtime. Same provenance category as ``_fieldtable.py``: it's Memoria's OPEN-SOURCE ``AnimationDB``
|
|
6
|
+
mapping (Memoria's reverse-engineered labels for FF9's anim resource ids), not extracted game data.
|
|
7
|
+
|
|
8
|
+
python -m ff9mapkit._regen_animdb --memoria "C:/path/to/Memoria"
|
|
9
|
+
|
|
10
|
+
It reads ``Global/ff9/FF9DBAll.Animation.cs`` -> ``AnimationDB`` (id <-> "ANH_..") and keeps only the
|
|
11
|
+
8 playable characters' anims (the cutscene presets). Anim names encode model + action:
|
|
12
|
+
``ANH_MAIN_F0_VIV_TALK_3_1`` -> model token ``VIV`` (Vivi), form ``F0``, action ``TALK_3_1``. The
|
|
13
|
+
engine loads an anim by name->id onto the matching model on demand (AnimationFactory), so any anim
|
|
14
|
+
tokened to a character's model plays on that model. To widen coverage (NPC/monster models), add their
|
|
15
|
+
tokens to ``MAIN_TOKENS`` below.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# the 8 playable characters' anim-name tokens (ZDN Zidane, VIV Vivi, GRN Garnet/Dagger, STN Steiner,
|
|
25
|
+
# FRJ Freya, KUI Quina, EIK Eiko, SLM Amarant "Salamander"). Field cutscene presets draw from these.
|
|
26
|
+
MAIN_TOKENS = ("ZDN", "VIV", "GRN", "STN", "FRJ", "KUI", "EIK", "SLM")
|
|
27
|
+
|
|
28
|
+
_PAIR = re.compile(r'\{\s*(\d+)\s*,\s*"(ANH_[^"]+)"\s*\}')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_table(memoria_root: Path) -> dict:
|
|
32
|
+
src = (memoria_root / "Assembly-CSharp" / "Global" / "ff9" / "FF9DBAll.Animation.cs"
|
|
33
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
34
|
+
keep = re.compile(r"^ANH_MAIN_F\d_(?:" + "|".join(MAIN_TOKENS) + r")_")
|
|
35
|
+
table = {}
|
|
36
|
+
for i, name in _PAIR.findall(src):
|
|
37
|
+
if keep.match(name):
|
|
38
|
+
table[int(i)] = name
|
|
39
|
+
return table
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render(table: dict) -> str:
|
|
43
|
+
header = ('"""Auto-generated FF9 main-character animation table: anim id -> name.\n\n'
|
|
44
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_animdb --memoria <path>\n"
|
|
45
|
+
"Source: Memoria Assembly-CSharp/Global/ff9/FF9DBAll.Animation.cs (AnimationDB, open-source).\n\n"
|
|
46
|
+
"Names encode model + action: ANH_MAIN_F0_VIV_TALK_3_1 -> Vivi ('VIV'), form F0, action\n"
|
|
47
|
+
"TALK_3_1. Limited to the 8 playable characters (the field cutscene presets). The catalog\n"
|
|
48
|
+
"in ff9mapkit.animations turns these into pick-by-name gestures.\n"
|
|
49
|
+
'"""\n')
|
|
50
|
+
lines = [header, "", "MAIN_ANIMATIONS = {"]
|
|
51
|
+
for anim_id in sorted(table, key=lambda i: (table[i], i)): # by name (groups by character), then id
|
|
52
|
+
lines.append(f' {anim_id}: {table[anim_id]!r},')
|
|
53
|
+
lines.append("}")
|
|
54
|
+
return "\n".join(lines) + "\n"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(argv=None) -> int:
|
|
58
|
+
ap = argparse.ArgumentParser(description="Regenerate _animdb.py from Memoria source.")
|
|
59
|
+
ap.add_argument("--memoria", required=True, help="path to a Memoria source checkout")
|
|
60
|
+
args = ap.parse_args(argv)
|
|
61
|
+
table = parse_table(Path(args.memoria))
|
|
62
|
+
target = Path(__file__).with_name("_animdb.py")
|
|
63
|
+
target.write_text(render(table), encoding="utf-8", newline="\n")
|
|
64
|
+
per = {}
|
|
65
|
+
for name in table.values():
|
|
66
|
+
per[name.split("_")[3]] = per.get(name.split("_")[3], 0) + 1
|
|
67
|
+
print(f"wrote {target} ({len(table)} anims: " + ", ".join(f"{t}={per.get(t, 0)}" for t in MAIN_TOKENS) + ")")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Regenerate ``_animdb_all.py`` (the FULL FF9 animation id -> name table) from a Memoria checkout.
|
|
2
|
+
|
|
3
|
+
A *maintainer* tool, not part of the runtime. Same provenance category as ``_animdb.py`` -- it
|
|
4
|
+
transcribes Memoria's OPEN-SOURCE ``AnimationDB`` (id<->"ANH_.." labels), NOT extracted game data; no
|
|
5
|
+
animation binary is read or shipped.
|
|
6
|
+
|
|
7
|
+
python -m ff9mapkit._regen_animdb_all --memoria "C:/path/to/Memoria"
|
|
8
|
+
|
|
9
|
+
This is the COMPREHENSIVE table (every ``ANH_*`` animation, all model groups), used by
|
|
10
|
+
:mod:`ff9mapkit.catalog` to list any model's gestures via the (group, token) join. It is a superset of
|
|
11
|
+
``_animdb.py`` (which keeps only the 8 playable characters for the cutscene author-convenience and is
|
|
12
|
+
what the *build* path imports). They are regenerated independently on purpose so the build's animation
|
|
13
|
+
resolution is untouched by Info-Hub changes; a future cleanup could derive ``_animdb.py`` from this.
|
|
14
|
+
|
|
15
|
+
An anim name encodes model + action: ``ANH_<group>_<form>_<token>_<action>`` -- e.g.
|
|
16
|
+
``ANH_MAIN_F0_VIV_TALK_3_1`` (Vivi, field form, action TALK_3_1) or ``ANH_NPC_F0_BAR_*``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
_PAIR = re.compile(r'\{\s*(\d+)\s*,\s*"(ANH_[^"]+)"\s*\}')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_table(memoria_root: Path) -> dict:
|
|
29
|
+
src = (memoria_root / "Assembly-CSharp" / "Global" / "ff9" / "FF9DBAll.Animation.cs"
|
|
30
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
31
|
+
return {int(i): name for i, name in _PAIR.findall(src)}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render(table: dict) -> str:
|
|
35
|
+
header = ('"""Auto-generated FULL FF9 animation table: anim id -> name (all model groups).\n\n'
|
|
36
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_animdb_all --memoria <path>\n"
|
|
37
|
+
"Source: Memoria Assembly-CSharp/Global/ff9/FF9DBAll.Animation.cs (AnimationDB, open-source) --\n"
|
|
38
|
+
"the same id<->name table Memoria publishes; NOT extracted game data (see docs/PROVENANCE.md).\n\n"
|
|
39
|
+
"ANIMATIONS[id] = 'ANH_<group>_<form>_<token>_<action>'. Superset of _animdb.py (which is the\n"
|
|
40
|
+
"8-playable subset used by the build's cutscene path). ff9mapkit.catalog joins these to a\n"
|
|
41
|
+
"model by (group, token) -> the model's gesture list.\n"
|
|
42
|
+
'"""\n')
|
|
43
|
+
lines = [header, "", "ANIMATIONS = {"]
|
|
44
|
+
for anim_id in sorted(table, key=lambda i: (table[i], i)): # by name (groups by model), then id
|
|
45
|
+
lines.append(f" {anim_id}: {table[anim_id]!r},")
|
|
46
|
+
lines.append("}")
|
|
47
|
+
return "\n".join(lines) + "\n"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv=None) -> int:
|
|
51
|
+
ap = argparse.ArgumentParser(description="Regenerate _animdb_all.py from Memoria source.")
|
|
52
|
+
ap.add_argument("--memoria", required=True, help="path to a Memoria source checkout")
|
|
53
|
+
args = ap.parse_args(argv)
|
|
54
|
+
table = parse_table(Path(args.memoria))
|
|
55
|
+
target = Path(__file__).with_name("_animdb_all.py")
|
|
56
|
+
target.write_text(render(table), encoding="utf-8", newline="\n")
|
|
57
|
+
groups = {}
|
|
58
|
+
for name in table.values():
|
|
59
|
+
groups[name.split("_")[1]] = groups.get(name.split("_")[1], 0) + 1
|
|
60
|
+
print(f"wrote {target} ({len(table)} anims: "
|
|
61
|
+
+ ", ".join(f"{g}={n}" for g, n in sorted(groups.items())) + ")")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Regenerate ``_fieldtable.py`` (FBG field-folder -> event-script name) from a Memoria checkout.
|
|
2
|
+
|
|
3
|
+
This is a *maintainer* tool, not part of the runtime -- the table is baked into ``_fieldtable.py``
|
|
4
|
+
so the kit needs no Memoria source to map an imported field's background folder to its event ``.eb``.
|
|
5
|
+
Run it only when updating to a newer Memoria whose field registry changed:
|
|
6
|
+
|
|
7
|
+
python -m ff9mapkit._regen_fieldtable --memoria "C:/path/to/Memoria"
|
|
8
|
+
|
|
9
|
+
It reads two base (vanilla) registries, both keyed by field id:
|
|
10
|
+
* ``Global/Event/Engine/EventEngineUtils.cs`` -> ``eventIDToFBGID`` (id -> "FBG_N..")
|
|
11
|
+
* ``Global/ff9/FF9DBAll.Events.cs`` -> ``EventDB`` (id -> "EVT_..")
|
|
12
|
+
and joins them on field id to emit ``FBG_TO_EVT = {fbg_lower: [field_id, "EVT_.."]}`` -- exactly the
|
|
13
|
+
mapping ``import`` needs (an imported field is named by its FBG background folder; its script is the
|
|
14
|
+
EVT_ event binary in p0data). EventDB also holds battle/world/startup events; the join on FBG ids
|
|
15
|
+
keeps only the field maps.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
_PAIR = re.compile(r'\{\s*(\d+)\s*,\s*"([^"]+)"\s*\}')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _dict_block(src: str, name: str) -> str:
|
|
28
|
+
"""The text of a ``name = new Dictionary<Int32, String> { ... };`` literal (up to the first ``};``)."""
|
|
29
|
+
m = re.search(re.escape(name) + r"\s*=\s*new\s+Dictionary<Int32,\s*String>\s*\{(.*?)\n\s*\};",
|
|
30
|
+
src, re.S)
|
|
31
|
+
if not m:
|
|
32
|
+
raise ValueError(f"could not find dictionary {name!r}")
|
|
33
|
+
return m.group(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_tables(memoria_root: Path):
|
|
37
|
+
eng = (memoria_root / "Assembly-CSharp" / "Global" / "Event" / "Engine" / "EventEngineUtils.cs"
|
|
38
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
39
|
+
evt = (memoria_root / "Assembly-CSharp" / "Global" / "ff9" / "FF9DBAll.Events.cs"
|
|
40
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
41
|
+
id_to_fbg = {int(i): name for i, name in _PAIR.findall(_dict_block(eng, "eventIDToFBGID"))}
|
|
42
|
+
id_to_evt = {int(i): name for i, name in _PAIR.findall(_dict_block(evt, "EventDB"))}
|
|
43
|
+
table = {}
|
|
44
|
+
by_id = {}
|
|
45
|
+
for fid, fbg in id_to_fbg.items():
|
|
46
|
+
evt_name = id_to_evt.get(fid)
|
|
47
|
+
if evt_name: # join on field id; skip ids with no event entry
|
|
48
|
+
# FOLDER-keyed (lossy: several field ids can share ONE background folder -- the same room at
|
|
49
|
+
# different story beats -- so the LAST wins; fine for the NAME-based `import` resolver).
|
|
50
|
+
table[fbg.lower()] = [fid, evt_name]
|
|
51
|
+
# ID-keyed (COMPLETE: every field id kept, so a shared-folder field like 52/3008 isn't dropped).
|
|
52
|
+
# The chain walk + whole-zone fork key on this so they don't miss ~142 of the 818 real fields.
|
|
53
|
+
by_id[fid] = [fbg.lower(), evt_name]
|
|
54
|
+
return table, by_id, len(id_to_fbg), len(id_to_evt)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render(table: dict, by_id: dict) -> str:
|
|
58
|
+
header = ('"""Auto-generated FF9 field registry: background folder (FBG) <-> event-script name.\n\n'
|
|
59
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_fieldtable\n"
|
|
60
|
+
"Source (both keyed by field id, joined here on id):\n"
|
|
61
|
+
" Memoria Assembly-CSharp/Global/Event/Engine/EventEngineUtils.cs (eventIDToFBGID)\n"
|
|
62
|
+
" Assembly-CSharp/Global/ff9/FF9DBAll.Events.cs (EventDB)\n\n"
|
|
63
|
+
" FBG_TO_EVT[fbg_folder_lowercase] = [field_id, 'EVT_<name>'] -- FOLDER-keyed (NAME import).\n"
|
|
64
|
+
" FIELD_BY_ID[field_id] = [fbg_folder_lowercase, 'EVT_<name>'] -- ID-keyed, COMPLETE.\n\n"
|
|
65
|
+
"Several field ids can share ONE FBG folder (the same room at different story beats), so the\n"
|
|
66
|
+
"folder-keyed FBG_TO_EVT drops all but one -- the id-keyed FIELD_BY_ID keeps every field, which\n"
|
|
67
|
+
"the chain walk + whole-zone fork need (else ~142 of the 818 real fields go missing + their\n"
|
|
68
|
+
"warps leak to the live game).\n"
|
|
69
|
+
'"""\n')
|
|
70
|
+
lines = [header, "", "FBG_TO_EVT = {"]
|
|
71
|
+
for fbg in sorted(table):
|
|
72
|
+
fid, evt = table[fbg]
|
|
73
|
+
lines.append(f' {fbg!r}: [{fid}, {evt!r}],')
|
|
74
|
+
lines += ["}", "", "FIELD_BY_ID = {"]
|
|
75
|
+
for fid in sorted(by_id):
|
|
76
|
+
fbg, evt = by_id[fid]
|
|
77
|
+
lines.append(f' {fid}: [{fbg!r}, {evt!r}],')
|
|
78
|
+
lines.append("}")
|
|
79
|
+
return "\n".join(lines) + "\n"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv=None) -> int:
|
|
83
|
+
ap = argparse.ArgumentParser(description="Regenerate _fieldtable.py from Memoria source.")
|
|
84
|
+
ap.add_argument("--memoria", required=True, help="path to a Memoria source checkout")
|
|
85
|
+
args = ap.parse_args(argv)
|
|
86
|
+
table, by_id, n_fbg, n_evt = parse_tables(Path(args.memoria))
|
|
87
|
+
target = Path(__file__).with_name("_fieldtable.py")
|
|
88
|
+
target.write_text(render(table, by_id), encoding="utf-8", newline="\n")
|
|
89
|
+
print(f"wrote {target} (eventIDToFBGID={n_fbg}, EventDB={n_evt}, folder maps={len(table)}, "
|
|
90
|
+
f"id maps={len(by_id)})")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Regenerate ``_fieldtext.py`` (field map-id -> text-block / MES id) from a Memoria checkout.
|
|
2
|
+
|
|
3
|
+
A maintainer tool, not part of the runtime -- the table is baked into ``_fieldtext.py`` so the kit
|
|
4
|
+
needs no Memoria source to find which ``<mes-id>.mes`` holds a real field's dialogue. The engine names a
|
|
5
|
+
field's text file by this id (``FF9TextTool.GetFieldTextFileName`` == the id as a string), and looks it up
|
|
6
|
+
exactly here -- ``EventEngineUtils.eventIDToMESID[fldMapNo]`` (used in FF9UIDataTool). Run it only when
|
|
7
|
+
updating to a newer Memoria whose registry changed:
|
|
8
|
+
|
|
9
|
+
python -m ff9mapkit._regen_fieldtext --memoria "C:/path/to/Memoria"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
_PAIR = re.compile(r"\{\s*(\d+)\s*,\s*(\d+)\s*\}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _dict_block(src: str, name: str) -> str:
|
|
22
|
+
"""The body of a ``name = new Dictionary<Int32, Int32> { ... };`` literal (up to the first ``};``)."""
|
|
23
|
+
m = re.search(re.escape(name) + r"\s*=\s*new\s+Dictionary<Int32,\s*Int32>\s*\(\s*\)\s*\{(.*?)\n\s*\};",
|
|
24
|
+
src, re.S)
|
|
25
|
+
if not m:
|
|
26
|
+
m = re.search(re.escape(name) + r"\s*=\s*new\s+Dictionary<Int32,\s*Int32>\s*\{(.*?)\n\s*\};", src, re.S)
|
|
27
|
+
if not m:
|
|
28
|
+
raise ValueError(f"could not find dictionary {name!r}")
|
|
29
|
+
return m.group(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_table(memoria_root: Path) -> dict:
|
|
33
|
+
eng = (memoria_root / "Assembly-CSharp" / "Global" / "Event" / "Engine" / "EventEngineUtils.cs"
|
|
34
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
35
|
+
return {int(k): int(v) for k, v in _PAIR.findall(_dict_block(eng, "eventIDToMESID"))}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render(table: dict) -> str:
|
|
39
|
+
header = ('"""Auto-generated FF9 field text registry: field map-id -> text-block (MES) id.\n\n'
|
|
40
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_fieldtext\n"
|
|
41
|
+
"Source: Memoria Assembly-CSharp/Global/Event/Engine/EventEngineUtils.cs (eventIDToMESID)\n\n"
|
|
42
|
+
"A field's dialogue lives in ``<mes-id>.mes`` (the engine names it by this id -- see\n"
|
|
43
|
+
"FF9TextTool.GetFieldTextFileName / EventEngineUtils.eventIDToMESID). EVENT_ID_TO_MES[field_id]\n"
|
|
44
|
+
"is how `dialogue-import` reads the RIGHT text block for a real field (txids are 0-based\n"
|
|
45
|
+
"positions shared by every field, so the block can't be found by txid alone).\n"
|
|
46
|
+
'"""\n')
|
|
47
|
+
lines = [header, "", "EVENT_ID_TO_MES = {"]
|
|
48
|
+
for fid in sorted(table):
|
|
49
|
+
lines.append(f" {fid}: {table[fid]},")
|
|
50
|
+
lines.append("}")
|
|
51
|
+
return "\n".join(lines) + "\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv=None) -> int:
|
|
55
|
+
ap = argparse.ArgumentParser(description="Regenerate _fieldtext.py from Memoria source.")
|
|
56
|
+
ap.add_argument("--memoria", required=True, help="path to a Memoria source checkout")
|
|
57
|
+
args = ap.parse_args(argv)
|
|
58
|
+
table = parse_table(Path(args.memoria))
|
|
59
|
+
target = Path(__file__).with_name("_fieldtext.py")
|
|
60
|
+
target.write_text(render(table), encoding="utf-8", newline="\n")
|
|
61
|
+
print(f"wrote {target} (eventIDToMESID entries={len(table)})")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Regenerate ``_modeldb.py`` (FF9 actor/field model id -> GEO name) from a Memoria checkout.
|
|
2
|
+
|
|
3
|
+
A *maintainer* tool, not part of the runtime. Same provenance category as ``_fieldtable.py`` /
|
|
4
|
+
``_animdb.py``: it transcribes Memoria's OPEN-SOURCE ``FF9BattleDB.GEO`` id<->name table (Memoria's
|
|
5
|
+
reverse-engineered labels for FF9's model resource ids), NOT extracted game data -- no model geometry
|
|
6
|
+
is ever read or shipped.
|
|
7
|
+
|
|
8
|
+
python -m ff9mapkit._regen_modeldb --memoria "C:/path/to/Memoria"
|
|
9
|
+
|
|
10
|
+
It reads ``Global/ff9/Battle/FF9BattleDB.GEO.cs`` -> ``GEO`` (model id <-> "GEO_<grp>_<form>_<token>").
|
|
11
|
+
A model name encodes group + form + token:
|
|
12
|
+
* ``GEO_MAIN_F0_VIV`` -> group MAIN (playable), form F0 (field), token VIV (Vivi)
|
|
13
|
+
* ``GEO_NPC_F0_BAR`` -> an NPC field model; ``GEO_MON_F0_*`` a monster; ``GEO_ACC_F0_*`` a prop
|
|
14
|
+
The token ties a model to its animations: an anim ``ANH_<grp>_<form>_<token>_<action>`` plays on the
|
|
15
|
+
model sharing (group, token). That join is what :mod:`ff9mapkit.catalog` uses for "this model's
|
|
16
|
+
gestures" (verified: model id 8 = ``GEO_MAIN_F0_VIV``, whose (MAIN, VIV) anims are 148/571/419/...).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
_PAIR = re.compile(r'\{\s*(\d+)\s*,\s*"(GEO_[^"]+)"\s*\}')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_table(memoria_root: Path) -> dict:
|
|
29
|
+
src = (memoria_root / "Assembly-CSharp" / "Global" / "ff9" / "Battle" / "FF9BattleDB.GEO.cs"
|
|
30
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
31
|
+
return {int(i): name for i, name in _PAIR.findall(src)}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render(table: dict) -> str:
|
|
35
|
+
header = ('"""Auto-generated FF9 model registry: actor/field model id -> GEO resource name.\n\n'
|
|
36
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_modeldb --memoria <path>\n"
|
|
37
|
+
"Source: Memoria Assembly-CSharp/Global/ff9/Battle/FF9BattleDB.GEO.cs (GEO, open-source) --\n"
|
|
38
|
+
"the same id<->name table Memoria publishes; NOT extracted game data (see docs/PROVENANCE.md).\n\n"
|
|
39
|
+
"MODELS[id] = 'GEO_<group>_<form>_<token>'. group: MAIN playable / NPC townsfolk / MON\n"
|
|
40
|
+
"monster / ACC prop / SUB sub-character / WEP weapon. form: F* field, B* battle, W* world.\n"
|
|
41
|
+
"The token links a model to its animations (ANH_<group>_*_<token>_<action>); see\n"
|
|
42
|
+
"ff9mapkit.catalog.animations_for_model. The model id is the value SetModel() takes.\n"
|
|
43
|
+
'"""\n')
|
|
44
|
+
lines = [header, "", "MODELS = {"]
|
|
45
|
+
for mid in sorted(table, key=lambda i: (table[i], i)): # by name (groups by category), then id
|
|
46
|
+
lines.append(f" {mid}: {table[mid]!r},")
|
|
47
|
+
lines.append("}")
|
|
48
|
+
return "\n".join(lines) + "\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv=None) -> int:
|
|
52
|
+
ap = argparse.ArgumentParser(description="Regenerate _modeldb.py from Memoria source.")
|
|
53
|
+
ap.add_argument("--memoria", required=True, help="path to a Memoria source checkout")
|
|
54
|
+
args = ap.parse_args(argv)
|
|
55
|
+
table = parse_table(Path(args.memoria))
|
|
56
|
+
target = Path(__file__).with_name("_modeldb.py")
|
|
57
|
+
target.write_text(render(table), encoding="utf-8", newline="\n")
|
|
58
|
+
groups = {}
|
|
59
|
+
for name in table.values():
|
|
60
|
+
groups[name.split("_")[1]] = groups.get(name.split("_")[1], 0) + 1
|
|
61
|
+
print(f"wrote {target} ({len(table)} models: "
|
|
62
|
+
+ ", ".join(f"{g}={n}" for g, n in sorted(groups.items())) + ")")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Regenerate ``_npcparams.py`` -- the per-model NPC OBJECT params (animset / head-focus / logical-size /
|
|
2
|
+
movement clips) that :func:`ff9mapkit.content.npc.build_npc_init` uses to emit a byte-faithful standing NPC
|
|
3
|
+
for ANY model, not just moogles.
|
|
4
|
+
|
|
5
|
+
We scan every real field's ``.eb`` for non-player object entries (those WITHOUT ``DefinePlayerCharacter``),
|
|
6
|
+
read each one's Init -- ``SetModel`` (model + animset), ``SetObjectLogicalSize``, ``SetHeadFocusMask``, and
|
|
7
|
+
the five movement-anim setters (ops 0x33/0x34/0x35/0x7A/0x7B) -- and bake, per model, the MODAL (most common)
|
|
8
|
+
value of each. Only ``GEO_NPC_*`` / ``GEO_MON_*`` models with a COMPLETE set (animset + head-focus +
|
|
9
|
+
logical-size + five clips) are kept: those are the real standing-NPC/creature rigs. Party (``GEO_MAIN``) and
|
|
10
|
+
accessory (``GEO_ACC``) models are excluded -- party characters are the player's job and accessories are
|
|
11
|
+
props (their own ``inject_prop`` path), so they must not drive generic NPC synthesis.
|
|
12
|
+
|
|
13
|
+
Provenance: the output is DERIVED METADATA (model ids + small ints only -- no Square-Enix bytes), exactly
|
|
14
|
+
like ``_modeldb`` / ``_animdb``. Run with the install reachable (``$FF9_GAME_PATH`` or run from the game dir):
|
|
15
|
+
|
|
16
|
+
python -m ff9mapkit._regen_npcparams
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections import Counter, defaultdict
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# op -> the movement-clip slot it sets (the from-scratch Init's five setters, in ANIM_ORDER)
|
|
24
|
+
_ANIM_OPS = {0x33: "stand", 0x34: "walk", 0x35: "run", 0x7A: "left", 0x7B: "right"}
|
|
25
|
+
_SLOTS = ("stand", "walk", "run", "left", "right")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _scan() -> dict:
|
|
29
|
+
"""``{model: {animset, head_focus, logical_size, anims}}`` -- the modal real-NPC params per model."""
|
|
30
|
+
from . import extract
|
|
31
|
+
from .eb import EbScript
|
|
32
|
+
from ._modeldb import MODELS
|
|
33
|
+
|
|
34
|
+
bundle = extract.EventBundle()
|
|
35
|
+
acc: dict = defaultdict(lambda: {"animset": Counter(), "hf": Counter(), "ls": Counter(), "anims": Counter()})
|
|
36
|
+
for fid in extract.ID_TO_FBG:
|
|
37
|
+
eb_bytes = bundle.eb_for_id(fid)
|
|
38
|
+
if not eb_bytes:
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
eb = EbScript.from_bytes(eb_bytes)
|
|
42
|
+
except Exception: # noqa: BLE001 -- a field we can't parse: skip
|
|
43
|
+
continue
|
|
44
|
+
for e in eb.entries:
|
|
45
|
+
if e.empty:
|
|
46
|
+
continue
|
|
47
|
+
f0 = e.func_by_tag(0)
|
|
48
|
+
if f0 is None:
|
|
49
|
+
continue
|
|
50
|
+
ins = list(eb.instrs(f0))
|
|
51
|
+
if any(i.op == 0x2C for i in ins): # the player (DefinePlayerCharacter) -- not an NPC
|
|
52
|
+
continue
|
|
53
|
+
sm = next((i for i in ins if i.op == 0x2F), None)
|
|
54
|
+
if sm is None:
|
|
55
|
+
continue
|
|
56
|
+
model, animset = int(sm.args[0]), int(sm.args[1])
|
|
57
|
+
a = acc[model]
|
|
58
|
+
a["animset"][animset] += 1
|
|
59
|
+
ls = next((tuple(i.args) for i in ins if i.op == 0x4B), None)
|
|
60
|
+
if ls:
|
|
61
|
+
a["ls"][ls] += 1
|
|
62
|
+
hf = next((tuple(i.args) for i in ins if i.op == 0x8B), None)
|
|
63
|
+
if hf:
|
|
64
|
+
a["hf"][hf] += 1
|
|
65
|
+
clips = {}
|
|
66
|
+
for i in ins:
|
|
67
|
+
if i.op in _ANIM_OPS and _ANIM_OPS[i.op] not in clips:
|
|
68
|
+
clips[_ANIM_OPS[i.op]] = int(i.args[0])
|
|
69
|
+
if len(clips) == 5:
|
|
70
|
+
a["anims"][tuple(clips[s] for s in _SLOTS)] += 1
|
|
71
|
+
|
|
72
|
+
out = {}
|
|
73
|
+
for model, c in acc.items():
|
|
74
|
+
name = MODELS.get(model, "")
|
|
75
|
+
if not (name.startswith("GEO_NPC") or name.startswith("GEO_MON")):
|
|
76
|
+
continue # only real NPC / creature rigs (no party / accessory)
|
|
77
|
+
if not (c["animset"] and c["hf"] and c["ls"] and c["anims"]):
|
|
78
|
+
continue # incomplete -> the defaults in npc.py cover it
|
|
79
|
+
anims = c["anims"].most_common(1)[0][0]
|
|
80
|
+
out[model] = {
|
|
81
|
+
"animset": c["animset"].most_common(1)[0][0],
|
|
82
|
+
"head_focus": tuple(c["hf"].most_common(1)[0][0]),
|
|
83
|
+
"logical_size": tuple(c["ls"].most_common(1)[0][0]),
|
|
84
|
+
"anims": {s: anims[i] for i, s in enumerate(_SLOTS)},
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _render(params: dict) -> str:
|
|
90
|
+
from ._modeldb import MODELS
|
|
91
|
+
L = ['"""Auto-generated per-model NPC OBJECT params -- the canonical (modal) animset / head-focus /',
|
|
92
|
+
"logical-size / movement clips real standing NPCs use, so ``content.npc.build_npc_init`` emits a",
|
|
93
|
+
"byte-faithful NPC for any GEO_NPC_* / GEO_MON_* model (not just moogles).",
|
|
94
|
+
"",
|
|
95
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_npcparams",
|
|
96
|
+
"Provenance: derived metadata (model ids + small ints), no Square-Enix bytes.",
|
|
97
|
+
'"""',
|
|
98
|
+
"",
|
|
99
|
+
"NPC_PARAMS = {"]
|
|
100
|
+
for model in sorted(params):
|
|
101
|
+
p = params[model]
|
|
102
|
+
a = p["anims"]
|
|
103
|
+
L.append(f" {model}: {{ # {MODELS.get(model, '?')}")
|
|
104
|
+
L.append(f" \"animset\": {p['animset']}, \"head_focus\": {p['head_focus']}, "
|
|
105
|
+
f"\"logical_size\": {p['logical_size']},")
|
|
106
|
+
L.append(f" \"anims\": {{\"stand\": {a['stand']}, \"walk\": {a['walk']}, \"run\": {a['run']}, "
|
|
107
|
+
f"\"left\": {a['left']}, \"right\": {a['right']}}},")
|
|
108
|
+
L.append(" },")
|
|
109
|
+
L.append("}")
|
|
110
|
+
L.append("")
|
|
111
|
+
return "\n".join(L)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main() -> int:
|
|
115
|
+
params = _scan()
|
|
116
|
+
dest = Path(__file__).resolve().parent / "_npcparams.py"
|
|
117
|
+
dest.write_text(_render(params), encoding="utf-8", newline="\n")
|
|
118
|
+
print(f"wrote {dest} ({len(params)} models)")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Regenerate ``_scenedb.py`` (FF9 battle-scene name -> encounter id) from a Memoria checkout.
|
|
2
|
+
|
|
3
|
+
A *maintainer* tool, not part of the runtime. Same provenance category as ``_fieldtable.py``: it
|
|
4
|
+
transcribes Memoria's OPEN-SOURCE ``FF9BattleDB.SceneData`` name<->id table, NOT extracted game data
|
|
5
|
+
(no battle binary, enemy stats, or roster bytes are read or shipped -- only the id<->name labels).
|
|
6
|
+
|
|
7
|
+
python -m ff9mapkit._regen_scenedb --memoria "C:/path/to/Memoria"
|
|
8
|
+
|
|
9
|
+
It reads ``Global/ff9/Battle/FF9BattleDB.SceneData.cs`` -> ``SceneData`` ("BSC_<region>_<n>" <-> id).
|
|
10
|
+
A scene id is what a field encounter points at (``SetRandomBattles`` slots); the name encodes the
|
|
11
|
+
region (BSC_AC_* = Alexandria Castle, BSC_B3_* = a disc-3 battle, ...). This is a *reference* catalog
|
|
12
|
+
for picking/identifying encounter ids by name; enemy rosters/stats are NOT here (they live in p0data).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
_PAIR = re.compile(r'\{\s*"(BSC_[^"]+)"\s*,\s*(\d+)\s*\}')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_table(memoria_root: Path) -> dict:
|
|
25
|
+
src = (memoria_root / "Assembly-CSharp" / "Global" / "ff9" / "Battle" / "FF9BattleDB.SceneData.cs"
|
|
26
|
+
).read_text(encoding="utf-8", errors="replace")
|
|
27
|
+
return {name: int(i) for name, i in _PAIR.findall(src)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render(table: dict) -> str:
|
|
31
|
+
header = ('"""Auto-generated FF9 battle-scene registry: scene name -> encounter id.\n\n'
|
|
32
|
+
"DO NOT EDIT BY HAND. Regenerate with: python -m ff9mapkit._regen_scenedb --memoria <path>\n"
|
|
33
|
+
"Source: Memoria Assembly-CSharp/Global/ff9/Battle/FF9BattleDB.SceneData.cs (SceneData,\n"
|
|
34
|
+
"open-source) -- the same id<->name table Memoria publishes; NOT extracted game data.\n\n"
|
|
35
|
+
"SCENES['BSC_<region>_<n>'] = encounter_id. A field's encounter points SetRandomBattles at\n"
|
|
36
|
+
"these ids; the name encodes the region. Enemy rosters/stats are NOT here (they're in p0data).\n"
|
|
37
|
+
'"""\n')
|
|
38
|
+
lines = [header, "", "SCENES = {"]
|
|
39
|
+
for name in sorted(table):
|
|
40
|
+
lines.append(f" {name!r}: {table[name]},")
|
|
41
|
+
lines.append("}")
|
|
42
|
+
return "\n".join(lines) + "\n"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main(argv=None) -> int:
|
|
46
|
+
ap = argparse.ArgumentParser(description="Regenerate _scenedb.py from Memoria source.")
|
|
47
|
+
ap.add_argument("--memoria", required=True, help="path to a Memoria source checkout")
|
|
48
|
+
args = ap.parse_args(argv)
|
|
49
|
+
table = parse_table(Path(args.memoria))
|
|
50
|
+
target = Path(__file__).with_name("_scenedb.py")
|
|
51
|
+
target.write_text(render(table), encoding="utf-8", newline="\n")
|
|
52
|
+
print(f"wrote {target} ({len(table)} battle scenes)")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|