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,665 @@
|
|
|
1
|
+
"""The FFIX Import document for the Workspace (Phase 6b) -- the tkinter ff9_import, folded in.
|
|
2
|
+
|
|
3
|
+
Bring content in from the real FF9 install: fork a field (Verbatim = the truest fork, the default — ships the
|
|
4
|
+
real script + dialogue, runs the real logic; or Re-authorable, the fidelity options as plain checkboxes),
|
|
5
|
+
preview fork fidelity, or read a field's dialogue / inspect a save / list fields. Every action
|
|
6
|
+
shells out to ``py -m ff9mapkit <cmd>`` and STREAMS into the shell's Output panel via ``run`` (the shell's
|
|
7
|
+
run_job). Only this view is Qt -- the argv is :func:`..editor.jobs.import_args`, the streaming + verdict
|
|
8
|
+
are the shell's, and the commands are the same CLI the terminal loop uses.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import Qt
|
|
17
|
+
from PySide6.QtWidgets import (
|
|
18
|
+
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFrame, QGroupBox, QHBoxLayout, QLabel,
|
|
19
|
+
QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QRadioButton, QScrollArea,
|
|
20
|
+
QVBoxLayout, QWidget,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ImportDoc(QWidget):
|
|
25
|
+
"""Fork-from-game + read/inspect, as a Workspace document. ``run`` = ``shell.run_job`` (streams a CLI
|
|
26
|
+
job to the Output panel + posts a verdict); ``problems`` = ``shell._show_problems`` (unused here -- the
|
|
27
|
+
shell-outs have only a stream + a return code, so the verdict comes from run_job)."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, pal, kit_root, *, run, problems=None, on_forked=None):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.pal = pal
|
|
32
|
+
self.kit = Path(kit_root) # `-m ff9mapkit` cwd (this worktree's package)
|
|
33
|
+
self._run = run
|
|
34
|
+
self._on_forked = on_forked # called with the output DIR on a clean fork -> shell opens it
|
|
35
|
+
# The tab body SCROLLS: this view stacks five tall group boxes, so a short window would otherwise
|
|
36
|
+
# cram/overlap them (and the inflated minimum height blocks the bottom Output dock from growing). The
|
|
37
|
+
# scroll area keeps THIS widget's min height small so the dock is resizable + the boxes never collide.
|
|
38
|
+
outer = QVBoxLayout(self)
|
|
39
|
+
outer.setContentsMargins(0, 0, 0, 0)
|
|
40
|
+
scroll = QScrollArea()
|
|
41
|
+
scroll.setWidgetResizable(True)
|
|
42
|
+
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
43
|
+
inner = QWidget()
|
|
44
|
+
root = QVBoxLayout(inner)
|
|
45
|
+
root.setContentsMargins(14, 14, 14, 14)
|
|
46
|
+
root.setSpacing(10)
|
|
47
|
+
intro = QLabel("Bring content in from your real FF9 install (needs UnityPy). Fork a single real field, "
|
|
48
|
+
"fork a whole connected REGION as one campaign, preview how faithfully it forks, or read "
|
|
49
|
+
"its dialogue / inspect a save.")
|
|
50
|
+
intro.setWordWrap(True)
|
|
51
|
+
intro.setStyleSheet(f"color:{pal['muted']};")
|
|
52
|
+
root.addWidget(intro)
|
|
53
|
+
root.addWidget(self._fork_box())
|
|
54
|
+
root.addWidget(self._region_box())
|
|
55
|
+
root.addWidget(self._repaint_box())
|
|
56
|
+
root.addWidget(self._read_box())
|
|
57
|
+
root.addStretch(1)
|
|
58
|
+
scroll.setWidget(inner)
|
|
59
|
+
outer.addWidget(scroll)
|
|
60
|
+
self._buttons = [self.find_btn, self.preview_btn, self.import_btn, self.dryrun_btn,
|
|
61
|
+
self.fork_region_btn, self.catalog_btn, self.rp_unpack_btn, self.rp_pack_btn,
|
|
62
|
+
self.dlg_btn, self.save_btn, self.list_btn, self.tpl_btn]
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------ fork-a-field
|
|
65
|
+
def _fork_box(self):
|
|
66
|
+
box = QGroupBox("Fork a real field")
|
|
67
|
+
v = QVBoxLayout(box)
|
|
68
|
+
lbl = QLabel("ONE screen — an id, or an FBG-name substring (e.g. 100, grgr, alxt_map016). For a whole "
|
|
69
|
+
"connected AREA (many screens wired together), use Fork a region below. "
|
|
70
|
+
"Find… looks up exact names/ids; Preview shows what a fork will/won't reproduce.")
|
|
71
|
+
lbl.setWordWrap(True)
|
|
72
|
+
lbl.setStyleSheet(f"color:{self.pal['muted']};")
|
|
73
|
+
v.addWidget(lbl)
|
|
74
|
+
row = QHBoxLayout()
|
|
75
|
+
self.field = QLineEdit()
|
|
76
|
+
self.field.setPlaceholderText("field id or name")
|
|
77
|
+
self.find_btn = QPushButton("Find…")
|
|
78
|
+
self.find_btn.clicked.connect(self.on_find)
|
|
79
|
+
self.preview_btn = QPushButton("Preview fidelity")
|
|
80
|
+
self.preview_btn.clicked.connect(self.on_preview)
|
|
81
|
+
row.addWidget(self.field, 1)
|
|
82
|
+
row.addWidget(self.find_btn)
|
|
83
|
+
row.addWidget(self.preview_btn)
|
|
84
|
+
v.addLayout(row)
|
|
85
|
+
|
|
86
|
+
mode = QGroupBox("Fork mode")
|
|
87
|
+
mv = QVBoxLayout(mode)
|
|
88
|
+
self.mode_verbatim = QRadioButton("Verbatim — the truest fork (recommended)")
|
|
89
|
+
self.mode_authorable = QRadioButton("Re-authorable — editable [[npc]]/content, but you rebuild the "
|
|
90
|
+
"field's logic & story gating yourself (advanced)")
|
|
91
|
+
self.mode_verbatim.setChecked(True)
|
|
92
|
+
mv.addWidget(self.mode_verbatim)
|
|
93
|
+
mv.addWidget(self.mode_authorable)
|
|
94
|
+
mhint = QLabel("Verbatim ships the field's real event script + dialogue WHOLE — it runs the original "
|
|
95
|
+
"logic, story gating, real doors and rotating cast (the proven faithful path), carrying "
|
|
96
|
+
"every NPC/prop/line itself. A verbatim fork boots at scenario zero — use Preview fidelity "
|
|
97
|
+
"for the suggested starting beat, then add a [startup] block in the editor. The scene + "
|
|
98
|
+
"carry options appear only in Re-authorable mode (which drops the real logic for editable "
|
|
99
|
+
"[[npc]]/content you re-author).")
|
|
100
|
+
mhint.setWordWrap(True)
|
|
101
|
+
mhint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
102
|
+
mv.addWidget(mhint)
|
|
103
|
+
self.mode_verbatim.toggled.connect(self._sync_mode)
|
|
104
|
+
v.addWidget(mode)
|
|
105
|
+
|
|
106
|
+
self.art_box = QGroupBox("Background art")
|
|
107
|
+
av = QVBoxLayout(self.art_box)
|
|
108
|
+
self.art_native = QRadioButton("Native — seamless, faithful occlusion + lighting; ANY field (recommended)")
|
|
109
|
+
self.art_borrow = QRadioButton("BG-borrow — reuse the real art via DictionaryPatch (fast; area ≥ 10)")
|
|
110
|
+
self.art_editable = QRadioButton("Editable scene — repaintable per-depth layers, but SEAM-PRONE "
|
|
111
|
+
"(to repaint seamlessly, fork Native + use ‘Repaint a native fork’ below)")
|
|
112
|
+
self.art_native.setChecked(True)
|
|
113
|
+
for r in (self.art_native, self.art_borrow, self.art_editable):
|
|
114
|
+
av.addWidget(r)
|
|
115
|
+
v.addWidget(self.art_box)
|
|
116
|
+
|
|
117
|
+
self.carry_box = QGroupBox("Carry from the real field")
|
|
118
|
+
cv = QVBoxLayout(self.carry_box)
|
|
119
|
+
self.carry_npcs = QCheckBox("NPCs & props faithfully (their push/talk interactions fire)")
|
|
120
|
+
self.carry_text = QCheckBox("Real dialogue, verbatim (per language) — carried NPCs speak the real words")
|
|
121
|
+
self.dialogue_stubs = QCheckBox("Dialogue as editable [[npc]] stubs (to RE-AUTHOR, not carry)")
|
|
122
|
+
self.save_moogle = QCheckBox("Save point — the hidden Moogle + the save flourish (if the field has one)")
|
|
123
|
+
self.carry_npcs.setChecked(True)
|
|
124
|
+
self.carry_text.setChecked(True)
|
|
125
|
+
for c in (self.carry_npcs, self.carry_text, self.dialogue_stubs, self.save_moogle):
|
|
126
|
+
cv.addWidget(c)
|
|
127
|
+
v.addWidget(self.carry_box)
|
|
128
|
+
# NB: the initial _sync_mode() is deferred to the END of this method -- it now reads swap_player +
|
|
129
|
+
# writes mode_chip, neither of which exists yet here.
|
|
130
|
+
|
|
131
|
+
# Walk as (player swap): who you CONTROL in the fork, decoupled from [party] MEMBERSHIP. Applies to
|
|
132
|
+
# EITHER fork mode (the CLI forces --verbatim when set), so it lives OUTSIDE the verbatim-only carry box.
|
|
133
|
+
swap = QHBoxLayout()
|
|
134
|
+
swap.addWidget(QLabel("Walk as:"))
|
|
135
|
+
self.swap_player = QComboBox()
|
|
136
|
+
self.swap_player.setEditable(True)
|
|
137
|
+
self.swap_player.addItems(["", "zidane", "vivi", "steiner", "garnet", "freya", "quina", "eiko",
|
|
138
|
+
"amarant"])
|
|
139
|
+
self.swap_player.setCurrentText("")
|
|
140
|
+
self.swap_player.currentTextChanged.connect(self._sync_mode) # a Walk-as FORCES verbatim -> reflect it live
|
|
141
|
+
swap.addWidget(self.swap_player, 1)
|
|
142
|
+
self.neutralize = QCheckBox("Neutralize scripted gestures")
|
|
143
|
+
swap.addWidget(self.neutralize)
|
|
144
|
+
v.addLayout(swap)
|
|
145
|
+
swap_hint = QLabel("Optional: control a different character (a playable name) or any model (a GEO id, "
|
|
146
|
+
"e.g. a moogle 199) instead of the field's real player. Implies a verbatim fork. On "
|
|
147
|
+
"a story field the swapped rig's scripted GESTURES glitch — tick Neutralize to stand "
|
|
148
|
+
"cleanly (it won't emote), or fork a free-roam field.")
|
|
149
|
+
swap_hint.setWordWrap(True)
|
|
150
|
+
swap_hint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
151
|
+
v.addWidget(swap_hint)
|
|
152
|
+
|
|
153
|
+
out = QHBoxLayout()
|
|
154
|
+
out.addWidget(QLabel("Write to:"))
|
|
155
|
+
self.out = QLineEdit(str(self.kit.parent / "imported"))
|
|
156
|
+
browse = QPushButton("Browse…")
|
|
157
|
+
browse.clicked.connect(self.browse_out)
|
|
158
|
+
out.addWidget(self.out, 1)
|
|
159
|
+
out.addWidget(browse)
|
|
160
|
+
v.addLayout(out)
|
|
161
|
+
self.mode_chip = QLabel("") # live 'what will ACTUALLY run' -- the mode can be forced by Walk-as
|
|
162
|
+
self.mode_chip.setWordWrap(True)
|
|
163
|
+
v.addWidget(self.mode_chip)
|
|
164
|
+
ids = QHBoxLayout()
|
|
165
|
+
ids.addWidget(QLabel("Field id:"))
|
|
166
|
+
self.fid = QLineEdit("4003")
|
|
167
|
+
self.fid.setFixedWidth(80)
|
|
168
|
+
ids.addWidget(self.fid)
|
|
169
|
+
ids.addWidget(QLabel("Name (optional):"))
|
|
170
|
+
self.name = QLineEdit()
|
|
171
|
+
self.name.setFixedWidth(160)
|
|
172
|
+
ids.addWidget(self.name)
|
|
173
|
+
ids.addStretch(1)
|
|
174
|
+
self.import_btn = QPushButton("Import field")
|
|
175
|
+
self.import_btn.clicked.connect(self.on_import)
|
|
176
|
+
ids.addWidget(self.import_btn)
|
|
177
|
+
v.addLayout(ids)
|
|
178
|
+
hint = QLabel("→ then deploy what you made on the Build & Deploy tab.")
|
|
179
|
+
hint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
180
|
+
v.addWidget(hint)
|
|
181
|
+
self._sync_mode() # now swap_player + mode_chip exist: set art/carry visibility + the resolved-mode chip
|
|
182
|
+
return box
|
|
183
|
+
|
|
184
|
+
def _art(self):
|
|
185
|
+
return "borrow" if self.art_borrow.isChecked() else "editable" if self.art_editable.isChecked() else "native"
|
|
186
|
+
|
|
187
|
+
def _effective_verbatim(self):
|
|
188
|
+
"""What the fork will ACTUALLY be. A non-empty 'Walk as' FORCES verbatim regardless of the mode radio
|
|
189
|
+
(the CLI requires --verbatim for --swap-player), so the true mode is mode_verbatim OR a swap."""
|
|
190
|
+
swap = self.swap_player.currentText().strip() if hasattr(self, "swap_player") else ""
|
|
191
|
+
return self.mode_verbatim.isChecked() or bool(swap)
|
|
192
|
+
|
|
193
|
+
def _sync_mode(self, *_):
|
|
194
|
+
"""Keep the UI honest about what will run. Verbatim carries every NPC/prop/line + implies --native, so
|
|
195
|
+
the art + carry boxes are IRRELEVANT -- HIDE them (a greyed box reads as 'blocked', a hidden one as
|
|
196
|
+
'not part of this choice') + pin art to Native. Crucially this keys off EFFECTIVE verbatim (mode OR a
|
|
197
|
+
Walk-as), so setting 'Walk as' visibly hides the editable scene/carry options (they won't apply) -- you
|
|
198
|
+
can never pick 'Editable scene' and silently get verbatim."""
|
|
199
|
+
verbatim = self._effective_verbatim()
|
|
200
|
+
self.art_box.setVisible(not verbatim)
|
|
201
|
+
self.carry_box.setVisible(not verbatim)
|
|
202
|
+
if verbatim:
|
|
203
|
+
self.art_native.setChecked(True)
|
|
204
|
+
if hasattr(self, "mode_chip"):
|
|
205
|
+
self._refresh_mode_chip()
|
|
206
|
+
|
|
207
|
+
def _refresh_mode_chip(self):
|
|
208
|
+
"""The live 'Will fork: …' label by the Import button -- so you SEE the resolved mode before clicking,
|
|
209
|
+
and the Walk-as override is never silent."""
|
|
210
|
+
swap = self.swap_player.currentText().strip()
|
|
211
|
+
forced = bool(swap) and not self.mode_verbatim.isChecked()
|
|
212
|
+
if self._effective_verbatim():
|
|
213
|
+
txt = "Will fork: VERBATIM" + (" — forced by ‘Walk as’ (editable-scene / carry options ignored)"
|
|
214
|
+
if forced else " (real script + dialogue, not editable content)")
|
|
215
|
+
col = self.pal["warn"] if forced else self.pal["accent"]
|
|
216
|
+
else:
|
|
217
|
+
txt = "Will fork: RE-AUTHORABLE (editable [[npc]]/content)"
|
|
218
|
+
col = self.pal["accent"]
|
|
219
|
+
self.mode_chip.setText(txt)
|
|
220
|
+
self.mode_chip.setStyleSheet(f"color:{col};font-weight:600;")
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------------------ fork-a-region (import-chain)
|
|
223
|
+
def _region_box(self):
|
|
224
|
+
muted = f"color:{self.pal['muted']};"
|
|
225
|
+
box = QGroupBox("Fork a region (a connected multi-field chain → one campaign)")
|
|
226
|
+
v = QVBoxLayout(box)
|
|
227
|
+
lbl = QLabel("Fork a whole connected AREA at once — the workflow behind the disc-1 opening. Enter one "
|
|
228
|
+
"or more seed fields (or click Browse FF9 regions… for a catalog of FF9's areas — pick one, "
|
|
229
|
+
"or several to compose into one campaign); the chain forks everything they reach into a "
|
|
230
|
+
"single campaign, doors rewired in-fork. Dry-run first to see the blast radius.")
|
|
231
|
+
lbl.setWordWrap(True)
|
|
232
|
+
lbl.setStyleSheet(muted)
|
|
233
|
+
v.addWidget(lbl)
|
|
234
|
+
row = QHBoxLayout()
|
|
235
|
+
row.addWidget(QLabel("Seeds:"))
|
|
236
|
+
self.seeds = QLineEdit()
|
|
237
|
+
self.seeds.setPlaceholderText("field ids/names, comma-separated — e.g. 300 or 50,100,64")
|
|
238
|
+
self._region_ids = None # cluster member ids from the last catalog pick (-> --ids); a
|
|
239
|
+
self.seeds.textEdited.connect(self._clear_region_ids) # hand-edit clears it (the cluster no longer applies)
|
|
240
|
+
self.catalog_btn = QPushButton("Browse FF9 regions…")
|
|
241
|
+
self.catalog_btn.clicked.connect(self.open_region_catalog)
|
|
242
|
+
row.addWidget(self.seeds, 1)
|
|
243
|
+
row.addWidget(self.catalog_btn)
|
|
244
|
+
v.addLayout(row)
|
|
245
|
+
self.rg_whole = QCheckBox("Whole zone — fork ALL story-state visits of each seed's zone (override the "
|
|
246
|
+
"catalog's single-visit scope; more fields/ids — Dry-run to preview)")
|
|
247
|
+
self.rg_verbatim = QCheckBox("Verbatim — each member ships its real script + dialogue, runs the real "
|
|
248
|
+
"logic (recommended; uncheck to fork re-authorable members you rebuild yourself)")
|
|
249
|
+
self.rg_whole.setChecked(False) # default = the catalog region's own visit (--ids, lean + fast)
|
|
250
|
+
self.rg_verbatim.setChecked(True)
|
|
251
|
+
v.addWidget(self.rg_whole)
|
|
252
|
+
v.addWidget(self.rg_verbatim)
|
|
253
|
+
# Walk as (player swap) for the WHOLE chain -- the region analogue of the single-fork swap.
|
|
254
|
+
rswap = QHBoxLayout()
|
|
255
|
+
rswap.addWidget(QLabel("Walk as:"))
|
|
256
|
+
self.rg_swap = QComboBox()
|
|
257
|
+
self.rg_swap.setEditable(True)
|
|
258
|
+
self.rg_swap.addItems(["", "zidane", "vivi", "steiner", "garnet", "freya", "quina", "eiko", "amarant"])
|
|
259
|
+
self.rg_swap.setCurrentText("")
|
|
260
|
+
rswap.addWidget(self.rg_swap, 1)
|
|
261
|
+
self.rg_neutralize = QCheckBox("Neutralize scripted gestures")
|
|
262
|
+
rswap.addWidget(self.rg_neutralize)
|
|
263
|
+
v.addLayout(rswap)
|
|
264
|
+
swap_hint = QLabel("Optional: play the WHOLE chain as a different character (a playable name) or any "
|
|
265
|
+
"model (a GEO id). Implies verbatim; cutscene members' scripted gestures glitch — "
|
|
266
|
+
"tick Neutralize to stand cleanly through them.")
|
|
267
|
+
swap_hint.setWordWrap(True)
|
|
268
|
+
swap_hint.setStyleSheet(muted)
|
|
269
|
+
v.addWidget(swap_hint)
|
|
270
|
+
out = QHBoxLayout()
|
|
271
|
+
out.addWidget(QLabel("Write campaign to:"))
|
|
272
|
+
self.rg_out = QLineEdit(str(self.kit.parent / "campaign"))
|
|
273
|
+
rbrowse = QPushButton("Browse…")
|
|
274
|
+
rbrowse.clicked.connect(self.browse_region_out)
|
|
275
|
+
out.addWidget(self.rg_out, 1)
|
|
276
|
+
out.addWidget(rbrowse)
|
|
277
|
+
v.addLayout(out)
|
|
278
|
+
ids = QHBoxLayout()
|
|
279
|
+
ids.addWidget(QLabel("id base:"))
|
|
280
|
+
self.rg_idbase = QLineEdit()
|
|
281
|
+
self.rg_idbase.setFixedWidth(70)
|
|
282
|
+
self.rg_idbase.setPlaceholderText("6000") # blank -> the CLI/.ff9deploy.toml default applies
|
|
283
|
+
ids.addWidget(self.rg_idbase)
|
|
284
|
+
ids.addWidget(QLabel("Name prefix:"))
|
|
285
|
+
self.rg_prefix = QLineEdit()
|
|
286
|
+
self.rg_prefix.setFixedWidth(110)
|
|
287
|
+
self.rg_prefix.setPlaceholderText("e.g. dali_ (stacking)")
|
|
288
|
+
ids.addWidget(self.rg_prefix)
|
|
289
|
+
self.rg_fresh = QCheckBox("Re-allocate ids (--fresh-ids)")
|
|
290
|
+
ids.addWidget(self.rg_fresh)
|
|
291
|
+
ids.addStretch(1)
|
|
292
|
+
v.addLayout(ids)
|
|
293
|
+
collide_hint = QLabel("Field ids are GLOBAL across every stacked mod folder — to keep TWO regions side by "
|
|
294
|
+
"side, give each a DISTINCT id base AND a unique Name prefix, or the second black-"
|
|
295
|
+
"screens. The shipped disc-1 opening occupies ~6000–6371.")
|
|
296
|
+
collide_hint.setWordWrap(True)
|
|
297
|
+
collide_hint.setStyleSheet(muted)
|
|
298
|
+
v.addWidget(collide_hint)
|
|
299
|
+
fresh_hint = QLabel("Re-forking into the SAME folder reuses the prior fork's ids by default, so in-fork "
|
|
300
|
+
"saves survive. Tick --fresh-ids only to re-number from scratch.")
|
|
301
|
+
fresh_hint.setWordWrap(True)
|
|
302
|
+
fresh_hint.setStyleSheet(muted)
|
|
303
|
+
v.addWidget(fresh_hint)
|
|
304
|
+
btns = QHBoxLayout()
|
|
305
|
+
self.dryrun_btn = QPushButton("Dry-run (preview blast radius)")
|
|
306
|
+
self.dryrun_btn.clicked.connect(self.on_region_dryrun)
|
|
307
|
+
self.fork_region_btn = QPushButton("Fork region")
|
|
308
|
+
self.fork_region_btn.setObjectName("accent")
|
|
309
|
+
self.fork_region_btn.clicked.connect(self.on_fork_region)
|
|
310
|
+
btns.addWidget(self.dryrun_btn)
|
|
311
|
+
btns.addStretch(1)
|
|
312
|
+
btns.addWidget(self.fork_region_btn)
|
|
313
|
+
v.addLayout(btns)
|
|
314
|
+
hint = QLabel("→ then open the campaign on the Build & Deploy tab to compile + deploy the whole chain.")
|
|
315
|
+
hint.setStyleSheet(muted)
|
|
316
|
+
v.addWidget(hint)
|
|
317
|
+
return box
|
|
318
|
+
|
|
319
|
+
def _clear_region_ids(self, *_a):
|
|
320
|
+
"""A hand-edit of the Seeds box drops the catalog cluster (its member ids no longer describe the seeds),
|
|
321
|
+
so the fork falls back to whole-zone for the typed seeds."""
|
|
322
|
+
self._region_ids = None
|
|
323
|
+
|
|
324
|
+
def _region_args(self, *, out):
|
|
325
|
+
from ..editor import jobs
|
|
326
|
+
idb = self.rg_idbase.text().strip()
|
|
327
|
+
# Cluster scope (--ids) by default when a catalog region is picked; "Whole zone" overrides it, and a
|
|
328
|
+
# raw typed seed (no cluster) always forks whole-zone (its safe default -- catches cutscene screens).
|
|
329
|
+
whole = self.rg_whole.isChecked() or not self._region_ids
|
|
330
|
+
ids = None if whole else self._region_ids
|
|
331
|
+
swap = self.rg_swap.currentText().strip()
|
|
332
|
+
return jobs.import_chain_args(
|
|
333
|
+
self.seeds.text().strip(), out=out,
|
|
334
|
+
whole_zone=whole, ids=ids, verbatim=self.rg_verbatim.isChecked() or bool(swap),
|
|
335
|
+
id_base=int(idb) if idb.isdigit() else None,
|
|
336
|
+
name_prefix=self.rg_prefix.text().strip() or None, fresh_ids=self.rg_fresh.isChecked(),
|
|
337
|
+
swap_player=swap or None, neutralize_gestures=self.rg_neutralize.isChecked())
|
|
338
|
+
|
|
339
|
+
def _region_swap_ok(self):
|
|
340
|
+
"""False (after a warning) if Neutralize is ticked with no 'Walk as' character -- the CLI rejects that."""
|
|
341
|
+
if self.rg_neutralize.isChecked() and not self.rg_swap.currentText().strip():
|
|
342
|
+
self._warn("Neutralize needs a character",
|
|
343
|
+
"Pick a 'Walk as' character first — Neutralize only applies with a swap.")
|
|
344
|
+
return False
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------ FF9 region catalog
|
|
348
|
+
def _apply_region_selection(self, arcset, keys):
|
|
349
|
+
"""Compose the chosen catalog regions (``refarc.compose_region_fork``) into the Fork-a-region box:
|
|
350
|
+
seeds (one region, or several composed into one campaign) + a suggested name prefix. Returns the seeds
|
|
351
|
+
string. Dialog-free so it's headlessly testable."""
|
|
352
|
+
from .. import refarc as RA
|
|
353
|
+
seeds, prefix, _n = RA.compose_region_fork(arcset, keys)
|
|
354
|
+
self.seeds.setText(seeds)
|
|
355
|
+
self._region_ids = RA.compose_region_ids(arcset, keys) # the picked visit(s)' ids -> --ids (lean fork)
|
|
356
|
+
self.rg_prefix.setText(prefix) # ALWAYS (a composed multi-region pick clears a stale single-region tag)
|
|
357
|
+
self.seeds.setFocus()
|
|
358
|
+
return seeds
|
|
359
|
+
|
|
360
|
+
def open_region_catalog(self):
|
|
361
|
+
"""A pickable catalog of FF9's forkable regions (refarc's ``reference_arcs.toml``). Check ONE region to
|
|
362
|
+
fork it alone, or SEVERAL to compose their seeds into ONE campaign; 'Use selected' fills the Fork-a-
|
|
363
|
+
region box. Replaces the old New-Journey 'FF9 reference arc' (which scaffolded an incomplete multi-
|
|
364
|
+
campaign disc-1 journey) with a region-fork scaffold."""
|
|
365
|
+
from .. import refarc as RA
|
|
366
|
+
try:
|
|
367
|
+
arcset = RA.load_region_catalog()
|
|
368
|
+
except Exception as e: # noqa: BLE001
|
|
369
|
+
return self._warn("Region catalog", f"Couldn't load the FF9 region catalog: {e}")
|
|
370
|
+
dlg = QDialog(self)
|
|
371
|
+
dlg.setWindowTitle("Fork FF9 regions")
|
|
372
|
+
lay = QVBoxLayout(dlg)
|
|
373
|
+
intro = QLabel(f"<b>{arcset.title}</b> — pick FF9 regions to fork. Check ONE to fork it alone, or "
|
|
374
|
+
"SEVERAL to compose their seeds into ONE campaign. 'Use selected' fills the Fork-a-region "
|
|
375
|
+
"box (review id base / name prefix, then Dry-run or Fork).")
|
|
376
|
+
intro.setWordWrap(True)
|
|
377
|
+
intro.setStyleSheet(f"color:{self.pal['muted']};")
|
|
378
|
+
lay.addWidget(intro)
|
|
379
|
+
lst = QListWidget()
|
|
380
|
+
for a in arcset.arcs:
|
|
381
|
+
count = f" · {len(a.members)} fields" if a.members else ""
|
|
382
|
+
it = QListWidgetItem(f"{a.name} (seed {a.seed}{count})")
|
|
383
|
+
it.setFlags(it.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
|
384
|
+
it.setCheckState(Qt.CheckState.Unchecked)
|
|
385
|
+
it.setData(Qt.ItemDataRole.UserRole, a.key)
|
|
386
|
+
if a.note:
|
|
387
|
+
it.setToolTip(a.note)
|
|
388
|
+
lst.addItem(it)
|
|
389
|
+
lay.addWidget(lst)
|
|
390
|
+
foot = QLabel("Each region forks just its OWN story-state visit (the revisits of a place are separate "
|
|
391
|
+
"regions). Check 'Whole zone' on the Fork box to fork all visits instead. A region's "
|
|
392
|
+
"starting beat isn't applied here — add a [startup] beat in the editor after forking.")
|
|
393
|
+
foot.setWordWrap(True)
|
|
394
|
+
foot.setStyleSheet(f"color:{self.pal['muted']};")
|
|
395
|
+
lay.addWidget(foot)
|
|
396
|
+
bb = QDialogButtonBox()
|
|
397
|
+
bb.addButton("Use selected", QDialogButtonBox.ButtonRole.AcceptRole)
|
|
398
|
+
bb.addButton(QDialogButtonBox.StandardButton.Cancel)
|
|
399
|
+
bb.accepted.connect(dlg.accept)
|
|
400
|
+
bb.rejected.connect(dlg.reject)
|
|
401
|
+
lay.addWidget(bb)
|
|
402
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
403
|
+
return
|
|
404
|
+
keys = [lst.item(i).data(Qt.ItemDataRole.UserRole) for i in range(lst.count())
|
|
405
|
+
if lst.item(i).checkState() == Qt.CheckState.Checked]
|
|
406
|
+
if not keys:
|
|
407
|
+
return self._warn("No region", "Check at least one region to fork (or Cancel).")
|
|
408
|
+
self._apply_region_selection(arcset, keys)
|
|
409
|
+
|
|
410
|
+
# ------------------------------------------------------------------ read & inspect
|
|
411
|
+
def _read_box(self):
|
|
412
|
+
box = QGroupBox("Read & inspect (read-only / maintenance)")
|
|
413
|
+
v = QVBoxLayout(box)
|
|
414
|
+
dlg = QHBoxLayout()
|
|
415
|
+
dlg.addWidget(QLabel("Dialogue of field:"))
|
|
416
|
+
self.dlg_field = QLineEdit()
|
|
417
|
+
self.dlg_field.setFixedWidth(150)
|
|
418
|
+
dlg.addWidget(self.dlg_field)
|
|
419
|
+
dlg.addWidget(QLabel("Lang:"))
|
|
420
|
+
self.dlg_lang = QComboBox()
|
|
421
|
+
self.dlg_lang.addItems(["us", "uk", "fr", "gr", "it", "es", "jp"])
|
|
422
|
+
dlg.addWidget(self.dlg_lang)
|
|
423
|
+
self.dlg_btn = QPushButton("View dialogue")
|
|
424
|
+
self.dlg_btn.clicked.connect(self.on_view_dialogue)
|
|
425
|
+
dlg.addWidget(self.dlg_btn)
|
|
426
|
+
dlg.addStretch(1)
|
|
427
|
+
v.addLayout(dlg)
|
|
428
|
+
|
|
429
|
+
sav = QHBoxLayout()
|
|
430
|
+
sav.addWidget(QLabel("Inspect save:"))
|
|
431
|
+
self.save_path = QLineEdit()
|
|
432
|
+
browse_s = QPushButton("Browse…")
|
|
433
|
+
browse_s.clicked.connect(self.browse_save)
|
|
434
|
+
self.save_btn = QPushButton("Inspect")
|
|
435
|
+
self.save_btn.clicked.connect(self.on_inspect_save)
|
|
436
|
+
sav.addWidget(self.save_path, 1)
|
|
437
|
+
sav.addWidget(browse_s)
|
|
438
|
+
sav.addWidget(self.save_btn)
|
|
439
|
+
v.addLayout(sav)
|
|
440
|
+
|
|
441
|
+
lst = QHBoxLayout()
|
|
442
|
+
lst.addWidget(QLabel("List fields, filter:"))
|
|
443
|
+
self.list_filter = QLineEdit()
|
|
444
|
+
self.list_filter.setFixedWidth(150)
|
|
445
|
+
self.list_btn = QPushButton("List fields")
|
|
446
|
+
self.list_btn.clicked.connect(self.on_list_fields)
|
|
447
|
+
lst.addWidget(self.list_filter)
|
|
448
|
+
lst.addWidget(self.list_btn)
|
|
449
|
+
lst.addStretch(1)
|
|
450
|
+
v.addLayout(lst)
|
|
451
|
+
|
|
452
|
+
tpl = QHBoxLayout()
|
|
453
|
+
self.tpl_btn = QPushButton("Regenerate base templates")
|
|
454
|
+
self.tpl_btn.clicked.connect(self.on_templates)
|
|
455
|
+
tplhint = QLabel("rebuild the kit's base assets from YOUR install (ships no game data)")
|
|
456
|
+
tplhint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
457
|
+
tpl.addWidget(self.tpl_btn)
|
|
458
|
+
tpl.addWidget(tplhint, 1)
|
|
459
|
+
v.addLayout(tpl)
|
|
460
|
+
return box
|
|
461
|
+
|
|
462
|
+
# ------------------------------------------------------------------ repaint a native fork
|
|
463
|
+
def _repaint_box(self):
|
|
464
|
+
muted = f"color:{self.pal['muted']};"
|
|
465
|
+
box = QGroupBox("Repaint a native fork's art (seamless HD round-trip)")
|
|
466
|
+
v = QVBoxLayout(box)
|
|
467
|
+
lbl = QLabel("A NATIVE fork ships its background as a tile-packed atlas.png — awkward to paint by hand. "
|
|
468
|
+
"Unpack it into spatial Overlay*.png layers (one per depth band, the same picture the engine "
|
|
469
|
+
"renders), repaint them in any editor, then Pack them back into atlas.png — SEAMLESS, no game "
|
|
470
|
+
"needed. The atlas stays byte-identical until you actually change a layer. THEN deploy the "
|
|
471
|
+
"NATIVE *.field.toml (not an editable .bgx fork — those are seam-prone).")
|
|
472
|
+
lbl.setWordWrap(True)
|
|
473
|
+
lbl.setStyleSheet(muted)
|
|
474
|
+
v.addWidget(lbl)
|
|
475
|
+
row = QHBoxLayout()
|
|
476
|
+
row.addWidget(QLabel("Native project:"))
|
|
477
|
+
self.rp_proj = QLineEdit()
|
|
478
|
+
self.rp_proj.setPlaceholderText("the NATIVE fork folder or its .field.toml — needs scene.bgs.bytes + atlas.png")
|
|
479
|
+
rb = QPushButton("Browse…")
|
|
480
|
+
rb.clicked.connect(self.browse_repaint)
|
|
481
|
+
row.addWidget(self.rp_proj, 1)
|
|
482
|
+
row.addWidget(rb)
|
|
483
|
+
v.addLayout(row)
|
|
484
|
+
btns = QHBoxLayout()
|
|
485
|
+
self.rp_unpack_btn = QPushButton("1. Unpack to layers")
|
|
486
|
+
self.rp_unpack_btn.clicked.connect(self.on_repaint_unpack)
|
|
487
|
+
self.rp_pack_btn = QPushButton("2. Pack into atlas")
|
|
488
|
+
self.rp_pack_btn.setObjectName("accent")
|
|
489
|
+
self.rp_pack_btn.clicked.connect(self.on_repaint_pack)
|
|
490
|
+
btns.addWidget(self.rp_unpack_btn)
|
|
491
|
+
btns.addStretch(1)
|
|
492
|
+
btns.addWidget(self.rp_pack_btn)
|
|
493
|
+
v.addLayout(btns)
|
|
494
|
+
hint = QLabel("Native forks only — this is the seamless+repaintable path. (BG-borrow reuses the real "
|
|
495
|
+
"art; an editable .bgx fork is repaintable too but SEAM-PRONE.) → then deploy the NATIVE "
|
|
496
|
+
"field.toml on Build & Deploy to see the repaint in-game.")
|
|
497
|
+
hint.setWordWrap(True)
|
|
498
|
+
hint.setStyleSheet(muted)
|
|
499
|
+
v.addWidget(hint)
|
|
500
|
+
return box
|
|
501
|
+
|
|
502
|
+
def browse_repaint(self):
|
|
503
|
+
d = QFileDialog.getExistingDirectory(self, "The native fork project folder")
|
|
504
|
+
if d:
|
|
505
|
+
self.rp_proj.setText(d)
|
|
506
|
+
|
|
507
|
+
def on_repaint_unpack(self):
|
|
508
|
+
proj = self.rp_proj.text().strip()
|
|
509
|
+
if not proj:
|
|
510
|
+
return self._warn("No project", "Pick the native fork project folder (it has scene.bgs.bytes + "
|
|
511
|
+
"atlas.png). Forking a field above with Native art produces one.")
|
|
512
|
+
self._kit(["repaint-native", str(Path(proj).resolve())], subject="Unpack repaint layers",
|
|
513
|
+
ok_next="Repaint the Overlay*.png files in the project's repaint/ folder, then Pack into atlas.")
|
|
514
|
+
|
|
515
|
+
def on_repaint_pack(self):
|
|
516
|
+
proj = self.rp_proj.text().strip()
|
|
517
|
+
if not proj:
|
|
518
|
+
return self._warn("No project", "Pick the native fork project folder first, then Unpack to layers.")
|
|
519
|
+
self._kit(["repaint-native", str(Path(proj).resolve()), "--pack"], subject="Pack atlas",
|
|
520
|
+
ok_next="Repacked the layers into atlas.png (the old one is backed up) — deploy the project on "
|
|
521
|
+
"Build & Deploy to see the repaint in-game.")
|
|
522
|
+
|
|
523
|
+
# ------------------------------------------------------------------ run helpers
|
|
524
|
+
def _confirm(self, title, text):
|
|
525
|
+
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
|
|
526
|
+
|
|
527
|
+
def _warn(self, title, text):
|
|
528
|
+
QMessageBox.warning(self, title, text)
|
|
529
|
+
|
|
530
|
+
def _busy(self, b):
|
|
531
|
+
for btn in self._buttons:
|
|
532
|
+
btn.setEnabled(not b)
|
|
533
|
+
|
|
534
|
+
def _kit(self, args, *, subject, ok_next="", forked=None):
|
|
535
|
+
"""Stream ``py -m ff9mapkit <args>`` from the kit root into the Output panel via run_job. When
|
|
536
|
+
``forked`` is the output DIR of a fork job, a clean exit fires ``on_forked`` so the shell opens the
|
|
537
|
+
forked project (the Import->author handoff, instead of 'now go open it on Build & Deploy')."""
|
|
538
|
+
self._busy(True)
|
|
539
|
+
|
|
540
|
+
def _done(code):
|
|
541
|
+
self._busy(False)
|
|
542
|
+
if code == 0 and forked is not None and self._on_forked is not None:
|
|
543
|
+
self._on_forked(forked)
|
|
544
|
+
|
|
545
|
+
started = self._run([sys.executable, "-m", "ff9mapkit", *args], cwd=self.kit, subject=subject,
|
|
546
|
+
ok_headline=f"{subject} — done", ok_next=ok_next,
|
|
547
|
+
fail_hint="See the Output panel (importing needs UnityPy + your FF9 install).",
|
|
548
|
+
on_finished=_done)
|
|
549
|
+
if not started:
|
|
550
|
+
self._busy(False) # a job was already running; nothing started
|
|
551
|
+
|
|
552
|
+
# ------------------------------------------------------------------ actions
|
|
553
|
+
def browse_out(self):
|
|
554
|
+
d = QFileDialog.getExistingDirectory(self, "Folder to write the imported field into")
|
|
555
|
+
if d:
|
|
556
|
+
self.out.setText(d)
|
|
557
|
+
|
|
558
|
+
def browse_save(self):
|
|
559
|
+
f, _ = QFileDialog.getOpenFileName(self, "A save file (SavedData_ww.dat / extra-save / JSON)")
|
|
560
|
+
if f:
|
|
561
|
+
self.save_path.setText(f)
|
|
562
|
+
|
|
563
|
+
def browse_region_out(self):
|
|
564
|
+
d = QFileDialog.getExistingDirectory(self, "Folder to write the campaign into")
|
|
565
|
+
if d:
|
|
566
|
+
self.rg_out.setText(d)
|
|
567
|
+
|
|
568
|
+
def on_region_dryrun(self):
|
|
569
|
+
if not self.seeds.text().strip():
|
|
570
|
+
return self._warn("No seeds", "Enter one or more seed field ids/names to preview the region fork.")
|
|
571
|
+
if not self._region_swap_ok():
|
|
572
|
+
return
|
|
573
|
+
self._kit(self._region_args(out=None), subject="Region dry-run",
|
|
574
|
+
ok_next="Review the BLAST RADIUS + any under-forked zones, then Fork region.")
|
|
575
|
+
|
|
576
|
+
def on_fork_region(self):
|
|
577
|
+
seeds = self.seeds.text().strip()
|
|
578
|
+
if not seeds:
|
|
579
|
+
return self._warn("No seeds", "Enter one or more seed field ids/names (an id, or an FBG substring).")
|
|
580
|
+
out = self.rg_out.text().strip()
|
|
581
|
+
if not out:
|
|
582
|
+
return self._warn("No output folder", "Pick a folder to write the campaign into.")
|
|
583
|
+
if not self._region_swap_ok():
|
|
584
|
+
return
|
|
585
|
+
idb = self.rg_idbase.text().strip()
|
|
586
|
+
if idb and not idb.isdigit():
|
|
587
|
+
return self._warn("Bad id base", "id base must be a number (e.g. 6000) — or blank for the default.")
|
|
588
|
+
Path(out).mkdir(parents=True, exist_ok=True)
|
|
589
|
+
self._kit(self._region_args(out=str(Path(out).resolve())), subject=f"Fork region {seeds}",
|
|
590
|
+
forked=str(Path(out).resolve()),
|
|
591
|
+
ok_next=f"Forked the chain to {out} — it opens automatically here. Then Build & Deploy → Deploy; "
|
|
592
|
+
f"to make New Game start here, use Build & Deploy → New Game entry (point it at the entry "
|
|
593
|
+
f"id) and relaunch.")
|
|
594
|
+
|
|
595
|
+
def on_find(self):
|
|
596
|
+
flt = self.field.text().strip()
|
|
597
|
+
self._kit(["list-fields", flt] if flt else ["list-fields"], subject="Find fields")
|
|
598
|
+
|
|
599
|
+
def on_preview(self):
|
|
600
|
+
field = self.field.text().strip()
|
|
601
|
+
if not field:
|
|
602
|
+
return self._warn("No field", "Enter a real field id or name to preview its fork fidelity.")
|
|
603
|
+
self._kit(["fork-report", field], subject="Fork preview",
|
|
604
|
+
ok_next="Read the fidelity report (it recommends a fork mode). Verbatim is the faithful "
|
|
605
|
+
"default — note its suggested [startup] scenario, Import, then add that beat in the "
|
|
606
|
+
"editor; or switch to Re-authorable to carry NPCs/dialogue as editable content.")
|
|
607
|
+
|
|
608
|
+
def on_import(self):
|
|
609
|
+
from ..editor import jobs
|
|
610
|
+
field = self.field.text().strip()
|
|
611
|
+
if not field:
|
|
612
|
+
return self._warn("No field", "Enter a real field id or name (use Find… to look it up).")
|
|
613
|
+
out = self.out.text().strip()
|
|
614
|
+
if not out:
|
|
615
|
+
return self._warn("No output folder", "Pick a folder to write the imported field into.")
|
|
616
|
+
try:
|
|
617
|
+
fid = int(self.fid.text().strip())
|
|
618
|
+
except ValueError:
|
|
619
|
+
return self._warn("Bad field id", "Field id must be a number (e.g. 4003).")
|
|
620
|
+
Path(out).mkdir(parents=True, exist_ok=True)
|
|
621
|
+
swap = self.swap_player.currentText().strip()
|
|
622
|
+
neutralize = self.neutralize.isChecked()
|
|
623
|
+
if neutralize and not swap:
|
|
624
|
+
return self._warn("Neutralize needs a character",
|
|
625
|
+
"Pick a 'Walk as' character first — Neutralize rewrites the swapped rig's "
|
|
626
|
+
"gestures, so it only applies with a swap.")
|
|
627
|
+
verbatim = self.mode_verbatim.isChecked() or bool(swap) # --swap-player forces verbatim in the CLI
|
|
628
|
+
args = jobs.import_args(field, out=str(Path(out).resolve()), field_id=fid,
|
|
629
|
+
name=self.name.text().strip() or None, art=self._art(),
|
|
630
|
+
carry_npcs=self.carry_npcs.isChecked(), carry_text=self.carry_text.isChecked(),
|
|
631
|
+
dialogue_stubs=self.dialogue_stubs.isChecked(),
|
|
632
|
+
save_moogle=self.save_moogle.isChecked(), verbatim=verbatim,
|
|
633
|
+
swap_player=swap or None, neutralize_gestures=neutralize)
|
|
634
|
+
swapped = f", walking as {swap}" if swap else ""
|
|
635
|
+
mode = "verbatim — real script + dialogue, real logic" if verbatim else "re-authorable carry"
|
|
636
|
+
if self._art() == "native": # a native fork is repaintable -> pre-aim the repaint box at it
|
|
637
|
+
self.rp_proj.setText(str(Path(out).resolve()))
|
|
638
|
+
self._kit(args, subject=f"Import {field}", forked=str(Path(out).resolve()),
|
|
639
|
+
ok_next=f"Forked ({mode}{swapped}) to {out} — it opens automatically here; Build & Deploy is "
|
|
640
|
+
"pre-aimed at it" + ("; then add a [startup] beat (Editor tab → the field's Field form) "
|
|
641
|
+
"so it boots the right story state instead of scenario zero." if verbatim else "."))
|
|
642
|
+
|
|
643
|
+
def on_view_dialogue(self):
|
|
644
|
+
field = self.dlg_field.text().strip()
|
|
645
|
+
if not field:
|
|
646
|
+
return self._warn("No field", "Enter a real field id or name to read its dialogue.")
|
|
647
|
+
self._kit(["dialogue-import", field, "--lang", self.dlg_lang.currentText()], subject="Read dialogue")
|
|
648
|
+
|
|
649
|
+
def on_inspect_save(self):
|
|
650
|
+
save = self.save_path.text().strip()
|
|
651
|
+
if not save:
|
|
652
|
+
return self._warn("No save", "Pick a save file to inspect.")
|
|
653
|
+
self._kit(["flags-inspect", save], subject="Inspect save")
|
|
654
|
+
|
|
655
|
+
def on_list_fields(self):
|
|
656
|
+
flt = self.list_filter.text().strip()
|
|
657
|
+
self._kit(["list-fields", flt] if flt else ["list-fields"], subject="List fields")
|
|
658
|
+
|
|
659
|
+
def on_templates(self):
|
|
660
|
+
if not self._confirm("Regenerate templates",
|
|
661
|
+
"Rebuild the kit's base templates from your FF9 install? "
|
|
662
|
+
"(Reads your install; writes only into the kit's data dir.)"):
|
|
663
|
+
return
|
|
664
|
+
self._kit(["extract-templates"], subject="Regenerate templates",
|
|
665
|
+
ok_next="Templates rebuilt from your install (ships no game data).")
|