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,664 @@
|
|
|
1
|
+
"""Qt save-editor documents for the Workspace (Phase 5b) -- the cross-cutting STATE layer.
|
|
2
|
+
|
|
3
|
+
A save isn't tied to a campaign/field; it's the player's story + inventory state. This module hosts the
|
|
4
|
+
Qt documents that read/EDIT it, reusing the kit's tk-free save backends verbatim (the same code the
|
|
5
|
+
tkinter ``ff9_storystate`` / ``ff9_items`` apps call):
|
|
6
|
+
|
|
7
|
+
* :class:`StoryStateDoc` -- ScenarioCounter + story flags (``save.inspect`` / ``flags.render_report`` /
|
|
8
|
+
``flags.diff_reports`` / ``save.apply_story_edit``). Inspect / Diff / Edit, BACKUP-guarded +
|
|
9
|
+
reserved-region-refused (the edit path shares the CLI's guards). (5b-i)
|
|
10
|
+
|
|
11
|
+
Editing the encrypted ``SavedData_ww.dat`` needs pycryptodome; inspect/diff also read a Memoria
|
|
12
|
+
plaintext extra-save or an exported save JSON. Provenance-clean: only the user's own save, only on Apply.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import Qt
|
|
20
|
+
from PySide6.QtWidgets import (
|
|
21
|
+
QComboBox, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QMessageBox, QPlainTextEdit,
|
|
22
|
+
QPushButton, QSplitter, QTabWidget, QVBoxLayout, QWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .. import flags as _flags
|
|
26
|
+
from .. import save as _save
|
|
27
|
+
from .. import save_items as _si
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StoryStateDoc(QWidget):
|
|
31
|
+
"""Inspect / Diff / EDIT a save's gEventGlobal story state (ScenarioCounter + story bits)."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, palette, output=None):
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.pal = palette
|
|
36
|
+
self._output = output # an output sink callable(text); None = an in-pane console (standalone)
|
|
37
|
+
self.reports = [] # [(label, SaveReport)] for the loaded save (A)
|
|
38
|
+
self.blocks = [] # editable block per report (None unless an encrypted .dat)
|
|
39
|
+
self.path = ""
|
|
40
|
+
self.flag_names = {} # {absolute bit: authored [[flag]] name} from the OPEN project (annotation only)
|
|
41
|
+
self.reports_b = [] # the compare-against save (B)
|
|
42
|
+
|
|
43
|
+
v = QVBoxLayout(self)
|
|
44
|
+
bar = QHBoxLayout()
|
|
45
|
+
self.open_btn = QPushButton("Open Save…")
|
|
46
|
+
self.open_btn.clicked.connect(self.browse)
|
|
47
|
+
self.path_lbl = QLabel("No save loaded.")
|
|
48
|
+
self.path_lbl.setStyleSheet(f"color:{palette['muted']};")
|
|
49
|
+
bar.addWidget(self.open_btn)
|
|
50
|
+
bar.addWidget(self.path_lbl, 1)
|
|
51
|
+
v.addLayout(bar)
|
|
52
|
+
|
|
53
|
+
split = QSplitter(Qt.Horizontal)
|
|
54
|
+
v.addWidget(split, 1)
|
|
55
|
+
self.slots = QListWidget()
|
|
56
|
+
self.slots.currentRowChanged.connect(lambda _r: self._on_slot())
|
|
57
|
+
split.addWidget(self.slots)
|
|
58
|
+
|
|
59
|
+
self.tabs = QTabWidget()
|
|
60
|
+
self.inspect = QPlainTextEdit()
|
|
61
|
+
self.inspect.setReadOnly(True)
|
|
62
|
+
self.tabs.addTab(self.inspect, "Inspect")
|
|
63
|
+
self.tabs.addTab(self._build_diff(), "Diff")
|
|
64
|
+
self.tabs.addTab(self._build_edit(), "Edit")
|
|
65
|
+
split.addWidget(self.tabs)
|
|
66
|
+
split.setSizes([240, 620])
|
|
67
|
+
|
|
68
|
+
self.status = QLabel("Open a SavedData_ww.dat (or a Memoria extra-save / save JSON) to inspect or edit.")
|
|
69
|
+
self.status.setStyleSheet(f"color:{palette['muted']};")
|
|
70
|
+
v.addWidget(self.status)
|
|
71
|
+
|
|
72
|
+
def _show_output(self, text):
|
|
73
|
+
"""Preview/Apply console output -> the workspace's bottom panel when docked, else the in-pane box."""
|
|
74
|
+
if self._output is not None:
|
|
75
|
+
self._output(text)
|
|
76
|
+
elif getattr(self, "edit_txt", None) is not None:
|
|
77
|
+
self.edit_txt.setPlainText(text)
|
|
78
|
+
|
|
79
|
+
# ---- view scaffolding ----
|
|
80
|
+
def _build_diff(self):
|
|
81
|
+
page = QWidget()
|
|
82
|
+
lay = QVBoxLayout(page)
|
|
83
|
+
row = QHBoxLayout()
|
|
84
|
+
row.addWidget(QLabel("Compare A against B:"))
|
|
85
|
+
self.b_btn = QPushButton("Open B…")
|
|
86
|
+
self.b_btn.clicked.connect(self.browse_b)
|
|
87
|
+
row.addWidget(self.b_btn)
|
|
88
|
+
row.addWidget(QLabel("B slot:"))
|
|
89
|
+
self.b_slot = QComboBox()
|
|
90
|
+
row.addWidget(self.b_slot)
|
|
91
|
+
cmp_btn = QPushButton("Compare A → B")
|
|
92
|
+
cmp_btn.clicked.connect(self._compare)
|
|
93
|
+
row.addWidget(cmp_btn)
|
|
94
|
+
row.addStretch(1)
|
|
95
|
+
lay.addLayout(row)
|
|
96
|
+
self.diff_txt = QPlainTextEdit()
|
|
97
|
+
self.diff_txt.setReadOnly(True)
|
|
98
|
+
lay.addWidget(self.diff_txt, 1)
|
|
99
|
+
return page
|
|
100
|
+
|
|
101
|
+
def _build_edit(self):
|
|
102
|
+
page = QWidget()
|
|
103
|
+
lay = QVBoxLayout(page)
|
|
104
|
+
self.edit_target = QLabel("(no save selected)")
|
|
105
|
+
self.edit_target.setStyleSheet(f"color:{self.pal['muted']};")
|
|
106
|
+
self.edit_target.setWordWrap(True)
|
|
107
|
+
lay.addWidget(self.edit_target)
|
|
108
|
+
for label, attr, hint in (
|
|
109
|
+
("Scenario:", "sc_var", 'a value or area name (e.g. "Ice Cavern")'),
|
|
110
|
+
("Set flags:", "set_var", "comma-separated bit indices (custom band ≥ 8512)"),
|
|
111
|
+
("Clear flags:", "clear_var", "comma-separated bit indices")):
|
|
112
|
+
row = QHBoxLayout()
|
|
113
|
+
row.addWidget(QLabel(label))
|
|
114
|
+
le = QLineEdit()
|
|
115
|
+
setattr(self, attr, le)
|
|
116
|
+
row.addWidget(le, 1)
|
|
117
|
+
h = QLabel(hint)
|
|
118
|
+
h.setStyleSheet(f"color:{self.pal['muted']};font-size:11px;")
|
|
119
|
+
row.addWidget(h)
|
|
120
|
+
lay.addLayout(row)
|
|
121
|
+
btns = QHBoxLayout()
|
|
122
|
+
self.preview_btn = QPushButton("Preview")
|
|
123
|
+
self.preview_btn.clicked.connect(self._preview)
|
|
124
|
+
self.apply_btn = QPushButton("Apply (backup + write)")
|
|
125
|
+
self.apply_btn.setObjectName("accent")
|
|
126
|
+
self.apply_btn.clicked.connect(self._apply)
|
|
127
|
+
btns.addWidget(self.preview_btn)
|
|
128
|
+
btns.addWidget(self.apply_btn)
|
|
129
|
+
btns.addStretch(1)
|
|
130
|
+
lay.addLayout(btns)
|
|
131
|
+
if self._output is None: # standalone: an in-pane console; docked -> the bottom panel
|
|
132
|
+
self.edit_txt = QPlainTextEdit()
|
|
133
|
+
self.edit_txt.setReadOnly(True)
|
|
134
|
+
lay.addWidget(self.edit_txt, 1)
|
|
135
|
+
return page
|
|
136
|
+
|
|
137
|
+
# ---- loading (A) ----
|
|
138
|
+
def browse(self):
|
|
139
|
+
from PySide6.QtWidgets import QFileDialog
|
|
140
|
+
f, _ = QFileDialog.getOpenFileName(self, "Pick a save (SavedData_ww.dat / extra-save / JSON)",
|
|
141
|
+
_save.default_save_dir() or "",
|
|
142
|
+
"FF9 save (*.dat);;Save JSON / Base64 (*.json *.txt);;All files (*)")
|
|
143
|
+
if f:
|
|
144
|
+
self.load(f)
|
|
145
|
+
|
|
146
|
+
def crumb_label(self):
|
|
147
|
+
"""A short 'you are editing X' label for the breadcrumb when the Story State tab is active."""
|
|
148
|
+
return os.path.basename(self.path) if self.path else "no save loaded"
|
|
149
|
+
|
|
150
|
+
def set_flag_names(self, names):
|
|
151
|
+
"""Push the OPEN project's ``{absolute bit: authored [[flag]] name}`` map (the shell builds it) so the
|
|
152
|
+
Inspect/Diff views label custom-band bits with the modder's names. Re-renders the current slot so an
|
|
153
|
+
already-open save picks up a newly-opened project. Empty == no annotation (the doc stands alone)."""
|
|
154
|
+
self.flag_names = dict(names or {})
|
|
155
|
+
if self.reports:
|
|
156
|
+
self._on_slot()
|
|
157
|
+
|
|
158
|
+
def load(self, path, select=0) -> bool:
|
|
159
|
+
try:
|
|
160
|
+
self.reports = _save.inspect(path)
|
|
161
|
+
except Exception as e: # noqa: BLE001
|
|
162
|
+
self.reports, self.blocks, self.path = [], [], ""
|
|
163
|
+
self.slots.clear()
|
|
164
|
+
self.inspect.setPlainText(f"Could not read story state from:\n{path}\n\n{e}\n\n"
|
|
165
|
+
"(An encrypted SavedData_ww.dat needs pycryptodome.)")
|
|
166
|
+
self.status.setText("no story state decoded")
|
|
167
|
+
return False
|
|
168
|
+
self.path = path
|
|
169
|
+
self.blocks = self._editable_blocks(path, len(self.reports))
|
|
170
|
+
self.path_lbl.setText(str(path))
|
|
171
|
+
self.slots.clear()
|
|
172
|
+
for label, rep in self.reports:
|
|
173
|
+
beat = rep.milestone[1] if rep.milestone else "(pre-story)"
|
|
174
|
+
self.slots.addItem(f"{label} — SC {rep.scenario_counter} · {beat}")
|
|
175
|
+
ro = "" if any(b is not None for b in self.blocks) else \
|
|
176
|
+
" (read-only: editing needs the encrypted SavedData_ww.dat + pycryptodome)"
|
|
177
|
+
self.status.setText(f"{len(self.reports)} populated save(s){ro}")
|
|
178
|
+
self._refresh_b_slots()
|
|
179
|
+
if self.reports:
|
|
180
|
+
self.slots.setCurrentRow(select if 0 <= select < len(self.reports) else 0)
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def _editable_blocks(path, n):
|
|
185
|
+
try:
|
|
186
|
+
pops = _save.FF9Save.load(path).populated()
|
|
187
|
+
except Exception: # noqa: BLE001 -- not an encrypted .dat / no crypto
|
|
188
|
+
return [None] * n
|
|
189
|
+
return [p.block for p in pops] if len(pops) == n else [None] * n
|
|
190
|
+
|
|
191
|
+
def _refresh_b_slots(self):
|
|
192
|
+
reps = self.reports_b or self.reports
|
|
193
|
+
self.b_slot.clear()
|
|
194
|
+
for i, (label, rep) in enumerate(reps):
|
|
195
|
+
self.b_slot.addItem(f"{i}: {label} (SC {rep.scenario_counter})", i)
|
|
196
|
+
|
|
197
|
+
def _on_slot(self):
|
|
198
|
+
i = self.slots.currentRow()
|
|
199
|
+
if not (0 <= i < len(self.reports)):
|
|
200
|
+
return
|
|
201
|
+
label, rep = self.reports[i]
|
|
202
|
+
self.inspect.setPlainText(f"{label}\n\n" + _flags.render_report(rep, names=self.flag_names))
|
|
203
|
+
blk = self.blocks[i] if i < len(self.blocks) else None
|
|
204
|
+
if blk is None:
|
|
205
|
+
self.edit_target.setText("Editing disabled — load the encrypted SavedData_ww.dat (read-only).")
|
|
206
|
+
self.preview_btn.setEnabled(False)
|
|
207
|
+
self.apply_btn.setEnabled(False)
|
|
208
|
+
else:
|
|
209
|
+
self.edit_target.setText(f"Editing: {label} (block {blk}). Reserved-region flags are refused; "
|
|
210
|
+
"a .bak is written before any change.")
|
|
211
|
+
self.preview_btn.setEnabled(True)
|
|
212
|
+
self.apply_btn.setEnabled(True)
|
|
213
|
+
|
|
214
|
+
# ---- diff (B) ----
|
|
215
|
+
def browse_b(self):
|
|
216
|
+
from PySide6.QtWidgets import QFileDialog
|
|
217
|
+
f, _ = QFileDialog.getOpenFileName(self, "Pick the second save (B)", _save.default_save_dir() or "",
|
|
218
|
+
"FF9 save (*.dat);;Save JSON / Base64 (*.json *.txt);;All files (*)")
|
|
219
|
+
if not f:
|
|
220
|
+
return
|
|
221
|
+
try:
|
|
222
|
+
self.reports_b = _save.inspect(f)
|
|
223
|
+
except Exception as e: # noqa: BLE001
|
|
224
|
+
self.reports_b = []
|
|
225
|
+
self.diff_txt.setPlainText(f"Could not read save B:\n{f}\n\n{e}")
|
|
226
|
+
return
|
|
227
|
+
self._refresh_b_slots()
|
|
228
|
+
self.status.setText(f"B: {len(self.reports_b)} populated save(s) — pick a B slot, then Compare")
|
|
229
|
+
|
|
230
|
+
def _compare(self):
|
|
231
|
+
i = self.slots.currentRow()
|
|
232
|
+
if not (0 <= i < len(self.reports)):
|
|
233
|
+
self.diff_txt.setPlainText("Select a save on the left (A) first.")
|
|
234
|
+
return
|
|
235
|
+
reps_b = self.reports_b or self.reports # no B file -> compare two slots of A
|
|
236
|
+
j = self.b_slot.currentData()
|
|
237
|
+
j = j if isinstance(j, int) else 0
|
|
238
|
+
if not 0 <= j < len(reps_b):
|
|
239
|
+
self.diff_txt.setPlainText(f"B slot {j} out of range (B has {len(reps_b)}).")
|
|
240
|
+
return
|
|
241
|
+
(la, ra), (lb, rb) = self.reports[i], reps_b[j]
|
|
242
|
+
self.diff_txt.setPlainText(f"A: {la}\nB: {lb}\n\n"
|
|
243
|
+
+ _flags.render_diff(_flags.diff_reports(ra, rb), names=self.flag_names))
|
|
244
|
+
|
|
245
|
+
# ---- edit (write) ----
|
|
246
|
+
def _confirm(self, detail) -> bool:
|
|
247
|
+
"""The Apply confirm gate (a method so the smoke can stub it). True == the user said Yes."""
|
|
248
|
+
return QMessageBox.question(
|
|
249
|
+
self, "Apply save edit?",
|
|
250
|
+
"This edits your REAL save (a timestamped .bak is written first):\n\n"
|
|
251
|
+
+ detail + "\n\nProceed?") == QMessageBox.StandardButton.Yes
|
|
252
|
+
|
|
253
|
+
def _parse_bits(self, s):
|
|
254
|
+
return [_flags.resolve(t.strip(), {}) for t in (s or "").replace(";", ",").split(",") if t.strip()]
|
|
255
|
+
|
|
256
|
+
def _edit_args(self):
|
|
257
|
+
sc = self.sc_var.text().strip()
|
|
258
|
+
return (_flags.resolve_scenario(sc) if sc else None,
|
|
259
|
+
self._parse_bits(self.set_var.text()), self._parse_bits(self.clear_var.text()))
|
|
260
|
+
|
|
261
|
+
def _target_block(self):
|
|
262
|
+
i = self.slots.currentRow()
|
|
263
|
+
return self.blocks[i] if (0 <= i < len(self.blocks)) else None
|
|
264
|
+
|
|
265
|
+
def _preview(self):
|
|
266
|
+
blk = self._target_block()
|
|
267
|
+
if blk is None:
|
|
268
|
+
return
|
|
269
|
+
try:
|
|
270
|
+
scenario, setb, clrb = self._edit_args()
|
|
271
|
+
res = _save.apply_story_edit(self.path, block=blk, scenario=scenario,
|
|
272
|
+
set_flags=setb, clear_flags=clrb, dry_run=True)
|
|
273
|
+
except (ValueError, IndexError) as e:
|
|
274
|
+
self._show_output(f"Cannot apply:\n {e}")
|
|
275
|
+
return
|
|
276
|
+
if not res["notes"]:
|
|
277
|
+
self._show_output("Nothing to change — set a Scenario / Set flags / Clear flags.")
|
|
278
|
+
return
|
|
279
|
+
body = "PREVIEW (nothing written yet):\n" + "\n".join(f" - {n}" for n in res["notes"])
|
|
280
|
+
if res["extra"]:
|
|
281
|
+
body += "\n\n (a Memoria extra-save is present and will be patched too)"
|
|
282
|
+
self._show_output(body)
|
|
283
|
+
|
|
284
|
+
def _apply(self):
|
|
285
|
+
blk = self._target_block()
|
|
286
|
+
if blk is None:
|
|
287
|
+
return
|
|
288
|
+
try:
|
|
289
|
+
scenario, setb, clrb = self._edit_args()
|
|
290
|
+
preview = _save.apply_story_edit(self.path, block=blk, scenario=scenario,
|
|
291
|
+
set_flags=setb, clear_flags=clrb, dry_run=True)
|
|
292
|
+
except (ValueError, IndexError) as e:
|
|
293
|
+
self._show_output(f"Cannot apply:\n {e}")
|
|
294
|
+
return
|
|
295
|
+
if not preview["notes"]:
|
|
296
|
+
self._show_output("Nothing to change.")
|
|
297
|
+
return
|
|
298
|
+
if not self._confirm("\n".join(preview["notes"])):
|
|
299
|
+
self._show_output("Cancelled — nothing written.")
|
|
300
|
+
return
|
|
301
|
+
try:
|
|
302
|
+
res = _save.apply_story_edit(self.path, block=blk, scenario=scenario,
|
|
303
|
+
set_flags=setb, clear_flags=clrb)
|
|
304
|
+
except Exception as e: # noqa: BLE001
|
|
305
|
+
self._show_output(f"Write failed:\n {e}")
|
|
306
|
+
return
|
|
307
|
+
msg = ["APPLIED — your save was edited:"] + [f" - {n}" for n in res["notes"]]
|
|
308
|
+
msg += [f" backed up -> {os.path.basename(b)}" for b in res["backups"]]
|
|
309
|
+
if res["extra"]:
|
|
310
|
+
msg.append(" [OK] Memoria extra-save patched + verified — this IS the gEventGlobal the game loads."
|
|
311
|
+
if res.get("extra_patched") else
|
|
312
|
+
" [WARN] a Memoria extra-save is present but could NOT be verified-patched.")
|
|
313
|
+
else:
|
|
314
|
+
msg.append(" (no Memoria extra-save for this slot — the main save block governs)")
|
|
315
|
+
msg.append("\nReload the save in-game to see it.")
|
|
316
|
+
self._show_output("\n".join(msg))
|
|
317
|
+
self.status.setText("save edited (backup written) — reload it in-game")
|
|
318
|
+
self.load(self.path, select=self.slots.currentRow()) # refresh, KEEPING the edited slot selected
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ItemEquipDoc(QWidget):
|
|
322
|
+
"""Inspect / EDIT a save's gil, inventory, equipment, stats, abilities and key items (``save_items``).
|
|
323
|
+
|
|
324
|
+
A SEPARATE surface from Story State (it touches only ``save_items``, per the branch-lane rule). Each
|
|
325
|
+
slot resolves to a target ``{label, report, extra, container, block}``: a Memoria slot dual-writes the
|
|
326
|
+
main block + the extra mirror, a vanilla (no-extra) slot edits the encrypted main block directly.
|
|
327
|
+
Every write is PREVIEWable (dry-run) and Apply is backup-guarded (a timestamped .bak first)."""
|
|
328
|
+
|
|
329
|
+
_STATS = ["Speed", "Strength", "Magic", "Spirit"]
|
|
330
|
+
|
|
331
|
+
def __init__(self, palette, output=None):
|
|
332
|
+
super().__init__()
|
|
333
|
+
self.pal = palette
|
|
334
|
+
self._output = output # an output sink callable(text); None = an in-pane console (standalone)
|
|
335
|
+
self.targets = [] # [{label, report, extra, container, block}] per populated slot
|
|
336
|
+
self.path = ""
|
|
337
|
+
|
|
338
|
+
v = QVBoxLayout(self)
|
|
339
|
+
bar = QHBoxLayout()
|
|
340
|
+
self.open_btn = QPushButton("Open Save…")
|
|
341
|
+
self.open_btn.clicked.connect(self.browse)
|
|
342
|
+
self.path_lbl = QLabel("No save loaded.")
|
|
343
|
+
self.path_lbl.setStyleSheet(f"color:{palette['muted']};")
|
|
344
|
+
bar.addWidget(self.open_btn)
|
|
345
|
+
bar.addWidget(self.path_lbl, 1)
|
|
346
|
+
v.addLayout(bar)
|
|
347
|
+
|
|
348
|
+
split = QSplitter(Qt.Horizontal)
|
|
349
|
+
v.addWidget(split, 1)
|
|
350
|
+
self.slots = QListWidget()
|
|
351
|
+
self.slots.currentRowChanged.connect(lambda _r: self._on_slot())
|
|
352
|
+
split.addWidget(self.slots)
|
|
353
|
+
self.tabs = QTabWidget()
|
|
354
|
+
self.inspect = QPlainTextEdit()
|
|
355
|
+
self.inspect.setReadOnly(True)
|
|
356
|
+
self.tabs.addTab(self.inspect, "Inspect")
|
|
357
|
+
self.tabs.addTab(self._build_edit(), "Edit")
|
|
358
|
+
split.addWidget(self.tabs)
|
|
359
|
+
split.setSizes([240, 620])
|
|
360
|
+
self.status = QLabel("Open a save to read/edit gil, inventory, equipment, stats, abilities, key items.")
|
|
361
|
+
self.status.setStyleSheet(f"color:{palette['muted']};")
|
|
362
|
+
v.addWidget(self.status)
|
|
363
|
+
|
|
364
|
+
def _show_output(self, text):
|
|
365
|
+
"""Preview/Apply console output -> the workspace's bottom panel when docked, else the in-pane box."""
|
|
366
|
+
if self._output is not None:
|
|
367
|
+
self._output(text)
|
|
368
|
+
elif getattr(self, "edit_txt", None) is not None:
|
|
369
|
+
self.edit_txt.setPlainText(text)
|
|
370
|
+
|
|
371
|
+
# ---- edit UI ----
|
|
372
|
+
def _section(self, parent_lay, title, widgets, buttons):
|
|
373
|
+
box = QGroupBox(title)
|
|
374
|
+
row = QHBoxLayout(box)
|
|
375
|
+
for w in widgets:
|
|
376
|
+
row.addWidget(QLabel(w[0])) if isinstance(w, tuple) else None
|
|
377
|
+
row.addWidget(w[1] if isinstance(w, tuple) else w)
|
|
378
|
+
row.addStretch(1)
|
|
379
|
+
for label, cb in buttons:
|
|
380
|
+
b = QPushButton(label)
|
|
381
|
+
if label == "Apply":
|
|
382
|
+
b.setObjectName("accent")
|
|
383
|
+
b.clicked.connect(lambda _=False, c=cb: c())
|
|
384
|
+
row.addWidget(b)
|
|
385
|
+
parent_lay.addWidget(box)
|
|
386
|
+
|
|
387
|
+
def _build_edit(self):
|
|
388
|
+
from PySide6.QtWidgets import QScrollArea
|
|
389
|
+
outer = QWidget()
|
|
390
|
+
ov = QVBoxLayout(outer)
|
|
391
|
+
self.edit_target = QLabel("(no save selected)")
|
|
392
|
+
self.edit_target.setWordWrap(True)
|
|
393
|
+
self.edit_target.setStyleSheet(f"color:{self.pal['muted']};")
|
|
394
|
+
ov.addWidget(self.edit_target)
|
|
395
|
+
# Only the edit SECTIONS scroll; the console (edit_txt) is pinned BELOW so Preview/Apply feedback
|
|
396
|
+
# is always visible even on a short window (the bug: a single scroll hid the console off-screen).
|
|
397
|
+
page = QWidget()
|
|
398
|
+
lay = QVBoxLayout(page)
|
|
399
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
400
|
+
|
|
401
|
+
self.gil_var = QLineEdit()
|
|
402
|
+
self.gil_var.setFixedWidth(120)
|
|
403
|
+
self._section(lay, "Gil", [self.gil_var, QLabel(f"(0–{_si.GIL_CAP:,})")],
|
|
404
|
+
[("Preview", lambda: self._edit("gil", False)), ("Apply", lambda: self._edit("gil", True))])
|
|
405
|
+
|
|
406
|
+
self.item_var = QLineEdit()
|
|
407
|
+
self.count_var = QLineEdit("1")
|
|
408
|
+
self.count_var.setFixedWidth(48)
|
|
409
|
+
self._section(lay, "Item (count 0 removes; clamps to 99)",
|
|
410
|
+
[("name/id:", self.item_var), ("count:", self.count_var)],
|
|
411
|
+
[("Preview", lambda: self._edit("item", False)), ("Apply", lambda: self._edit("item", True))])
|
|
412
|
+
|
|
413
|
+
self.char_combo = QComboBox()
|
|
414
|
+
self.slot_combo = QComboBox()
|
|
415
|
+
self.slot_combo.addItems(list(_si.EQUIP_SLOTS))
|
|
416
|
+
self.eqitem_var = QLineEdit()
|
|
417
|
+
self._section(lay, "Equipment (item 'empty' unequips)",
|
|
418
|
+
[("who:", self.char_combo), ("slot:", self.slot_combo), ("item:", self.eqitem_var)],
|
|
419
|
+
[("Preview", lambda: self._edit("equip", False)), ("Apply", lambda: self._edit("equip", True))])
|
|
420
|
+
|
|
421
|
+
self.stat_char_combo = QComboBox()
|
|
422
|
+
self.stat_kind_combo = QComboBox()
|
|
423
|
+
self.stat_kind_combo.addItems(self._STATS)
|
|
424
|
+
self.stat_kind_combo.setCurrentText("Strength") # match the tkinter editor's default
|
|
425
|
+
self.stat_val_var = QLineEdit("50")
|
|
426
|
+
self.stat_val_var.setFixedWidth(48)
|
|
427
|
+
self._section(lay, "Stats (permanent: writes basis + the equipment bonus)",
|
|
428
|
+
[("who:", self.stat_char_combo), ("stat:", self.stat_kind_combo), ("value:", self.stat_val_var)],
|
|
429
|
+
[("Preview", lambda: self._edit_stat(False)), ("Apply", lambda: self._edit_stat(True))])
|
|
430
|
+
|
|
431
|
+
self.ap_char_combo = QComboBox()
|
|
432
|
+
self.ap_abil_var = QLineEdit("all")
|
|
433
|
+
self.ap_val_var = QLineEdit("master")
|
|
434
|
+
self.ap_val_var.setFixedWidth(90)
|
|
435
|
+
self._section(lay, "Abilities (AP / mastery — name / AA:X / SA:X / id / all)",
|
|
436
|
+
[("who:", self.ap_char_combo), ("ability:", self.ap_abil_var), ("AP:", self.ap_val_var)],
|
|
437
|
+
[("Preview", lambda: self._edit_ap(False)), ("Apply", lambda: self._edit_ap(True))])
|
|
438
|
+
|
|
439
|
+
self.ki_var = QLineEdit()
|
|
440
|
+
self._section(lay, "Key items (give / remove an important item by name)", [("name/id:", self.ki_var)],
|
|
441
|
+
[("Preview", lambda: self._edit_keyitem(False, True)),
|
|
442
|
+
("Give", lambda: self._edit_keyitem(True, True)),
|
|
443
|
+
("Remove", lambda: self._edit_keyitem(True, False))])
|
|
444
|
+
|
|
445
|
+
lay.addStretch(1)
|
|
446
|
+
scroll = QScrollArea()
|
|
447
|
+
scroll.setWidgetResizable(True)
|
|
448
|
+
scroll.setWidget(page)
|
|
449
|
+
ov.addWidget(scroll, 1) # the middle (sections) takes the stretch + scrolls
|
|
450
|
+
if self._output is None: # docked -> output goes to the workspace bottom panel,
|
|
451
|
+
self.edit_txt = QPlainTextEdit() # so the sections reclaim the whole height; standalone
|
|
452
|
+
self.edit_txt.setReadOnly(True) # keeps an in-pane console pinned below.
|
|
453
|
+
self.edit_txt.setMinimumHeight(120)
|
|
454
|
+
ov.addWidget(self.edit_txt)
|
|
455
|
+
return outer
|
|
456
|
+
|
|
457
|
+
# ---- loading ----
|
|
458
|
+
def browse(self):
|
|
459
|
+
from PySide6.QtWidgets import QFileDialog
|
|
460
|
+
f, _ = QFileDialog.getOpenFileName(self, "Pick a save (SavedData_ww.dat or a Memoria extra-save)",
|
|
461
|
+
_save.default_save_dir() or "", "FF9 save (*.dat);;All files (*)")
|
|
462
|
+
if f:
|
|
463
|
+
self.load(f)
|
|
464
|
+
|
|
465
|
+
def crumb_label(self):
|
|
466
|
+
"""A short 'you are editing X' label for the breadcrumb when the Item & Equip tab is active."""
|
|
467
|
+
return os.path.basename(self.path) if self.path else "no save loaded"
|
|
468
|
+
|
|
469
|
+
def load(self, path, select=0) -> bool:
|
|
470
|
+
try:
|
|
471
|
+
self.targets = self._resolve_targets(path)
|
|
472
|
+
except Exception as e: # noqa: BLE001
|
|
473
|
+
self.targets, self.path = [], ""
|
|
474
|
+
self.slots.clear()
|
|
475
|
+
self.inspect.setPlainText(f"Could not read items/equipment from:\n{path}\n\n{e}\n\n"
|
|
476
|
+
"(A SavedData_ww.dat container needs pycryptodome; a Memoria extra-save "
|
|
477
|
+
"opens without it.)")
|
|
478
|
+
self.status.setText("no items/equipment decoded")
|
|
479
|
+
return False
|
|
480
|
+
self.path = path
|
|
481
|
+
self.path_lbl.setText(str(path))
|
|
482
|
+
self.slots.clear()
|
|
483
|
+
for t in self.targets:
|
|
484
|
+
self.slots.addItem(t["label"])
|
|
485
|
+
editable = sum(1 for t in self.targets if t["report"] is not None)
|
|
486
|
+
self.status.setText(f"{len(self.targets)} populated save(s); {editable} editable")
|
|
487
|
+
if self.targets:
|
|
488
|
+
self.slots.setCurrentRow(select if 0 <= select < len(self.targets) else 0)
|
|
489
|
+
return True
|
|
490
|
+
|
|
491
|
+
@staticmethod
|
|
492
|
+
def _resolve_targets(path):
|
|
493
|
+
common = _si.load_extra_common(path)[0]
|
|
494
|
+
if common is not None: # a Memoria extra-save, opened directly
|
|
495
|
+
return [{"label": "Memoria extra-save", "report": _si.report_from_common(common),
|
|
496
|
+
"extra": path, "container": None, "block": None}]
|
|
497
|
+
sv = _save.FF9Save.load(path) # the encrypted container (needs pycryptodome)
|
|
498
|
+
out = []
|
|
499
|
+
for s in sv.populated():
|
|
500
|
+
extra = _save.extra_file_path(path, s.block)
|
|
501
|
+
has_extra = bool(extra and os.path.isfile(extra))
|
|
502
|
+
if has_extra:
|
|
503
|
+
rep = _si.report_from_common(_si.load_extra_common(extra)[0])
|
|
504
|
+
lbl = _save._slot_label(s) + " · extra"
|
|
505
|
+
else:
|
|
506
|
+
rep = _si.decode_main_block(path, s.block)
|
|
507
|
+
lbl = _save._slot_label(s) + (" · main (vanilla)" if rep is not None else " · (unreadable)")
|
|
508
|
+
out.append({"label": lbl, "report": rep, "extra": extra if has_extra else None,
|
|
509
|
+
"container": path, "block": s.block})
|
|
510
|
+
if not out:
|
|
511
|
+
raise ValueError("no populated save slots found in this file")
|
|
512
|
+
return out
|
|
513
|
+
|
|
514
|
+
def _target(self):
|
|
515
|
+
i = self.slots.currentRow()
|
|
516
|
+
return self.targets[i] if (0 <= i < len(self.targets)) else None
|
|
517
|
+
|
|
518
|
+
def _on_slot(self):
|
|
519
|
+
t = self._target()
|
|
520
|
+
if t is None:
|
|
521
|
+
return
|
|
522
|
+
rep, extra, container = t["report"], t["extra"], t["container"]
|
|
523
|
+
self.inspect.setPlainText(f"{t['label']}\n\n" + _si.render_report(rep))
|
|
524
|
+
names = [pc["name"] or f"slot {pc['slot_no']}" for pc in (rep.equipment if rep else [])]
|
|
525
|
+
for combo in (self.char_combo, self.stat_char_combo, self.ap_char_combo):
|
|
526
|
+
keep = combo.currentText()
|
|
527
|
+
combo.clear()
|
|
528
|
+
combo.addItems(names)
|
|
529
|
+
if keep in names:
|
|
530
|
+
combo.setCurrentText(keep)
|
|
531
|
+
editable = rep is not None and (container is not None or extra is not None)
|
|
532
|
+
if not editable:
|
|
533
|
+
self.edit_target.setText("Editing disabled — this slot could not be decoded.")
|
|
534
|
+
self.gil_var.setText("")
|
|
535
|
+
elif extra is None:
|
|
536
|
+
self.edit_target.setText(f"Editing: {t['label']} (vanilla — main block). Gil, items, equipment "
|
|
537
|
+
"(by old-slot; slots 5-7 shared), stats, abilities, key items. Backed up first.")
|
|
538
|
+
self.gil_var.setText(str(rep.gil) if rep.gil is not None else "")
|
|
539
|
+
else:
|
|
540
|
+
where = "the extra file" if container is None else "the main block + the extra mirror"
|
|
541
|
+
self.edit_target.setText(f"Editing: {t['label']}. Writes {where}; a timestamped .bak is made first. "
|
|
542
|
+
"Reload the save in-game (no relaunch).")
|
|
543
|
+
self.gil_var.setText(str(rep.gil) if rep.gil is not None else "")
|
|
544
|
+
|
|
545
|
+
# ---- edit (write) ----
|
|
546
|
+
def _confirm(self, detail) -> bool:
|
|
547
|
+
"""The Apply confirm gate (a method so the smoke can stub it). True == the user said Yes."""
|
|
548
|
+
return QMessageBox.question(
|
|
549
|
+
self, "Apply save edit?",
|
|
550
|
+
"This edits your REAL save (a timestamped .bak is written first):\n\n"
|
|
551
|
+
+ detail + "\n\nProceed?") == QMessageBox.StandardButton.Yes
|
|
552
|
+
|
|
553
|
+
def _apply_plan(self, render, preview, do, apply):
|
|
554
|
+
if not apply:
|
|
555
|
+
self._show_output("PREVIEW (nothing written yet):\n" + render(preview))
|
|
556
|
+
return
|
|
557
|
+
if not self._confirm(render(preview)):
|
|
558
|
+
self._show_output("Cancelled — nothing written.")
|
|
559
|
+
return
|
|
560
|
+
try:
|
|
561
|
+
res = do()
|
|
562
|
+
except Exception as e: # noqa: BLE001
|
|
563
|
+
self._show_output(f"Write failed:\n {e}")
|
|
564
|
+
return
|
|
565
|
+
self._show_output(render(res) + "\n\nReload the save in-game to see it (no relaunch needed).")
|
|
566
|
+
self.status.setText("save edited (backup written) — reload it in-game")
|
|
567
|
+
self.load(self.path, select=self.slots.currentRow())
|
|
568
|
+
|
|
569
|
+
def _edit(self, kind, apply):
|
|
570
|
+
t = self._target()
|
|
571
|
+
if t is None or t["report"] is None:
|
|
572
|
+
self._show_output("Select a decodable slot on the left first.")
|
|
573
|
+
return
|
|
574
|
+
extra, container, block = t["extra"], t["container"], t["block"]
|
|
575
|
+
try:
|
|
576
|
+
if kind == "gil":
|
|
577
|
+
val = int(self.gil_var.text())
|
|
578
|
+
trio = ((_si.render_gil_dual, _si.set_gil_in_save(container, block, val, dry_run=True),
|
|
579
|
+
lambda: _si.set_gil_in_save(container, block, val, dry_run=False)) if container is not None
|
|
580
|
+
else (_si.render_gil_write, _si.set_gil(extra, val, dry_run=True),
|
|
581
|
+
lambda: _si.set_gil(extra, val, dry_run=False)))
|
|
582
|
+
elif kind == "item":
|
|
583
|
+
item, cnt = self.item_var.text().strip(), int(self.count_var.text())
|
|
584
|
+
trio = ((_si.render_item_dual, _si.set_item_in_save(container, block, item, cnt, dry_run=True),
|
|
585
|
+
lambda: _si.set_item_in_save(container, block, item, cnt, dry_run=False)) if container is not None
|
|
586
|
+
else (_si.render_item_write, _si.set_item(extra, item, cnt, dry_run=True),
|
|
587
|
+
lambda: _si.set_item(extra, item, cnt, dry_run=False)))
|
|
588
|
+
else:
|
|
589
|
+
char, slot, item = self.char_combo.currentText(), self.slot_combo.currentText(), self.eqitem_var.text().strip()
|
|
590
|
+
trio = ((_si.render_equip_dual, _si.set_equip_in_save(container, block, char, slot, item, dry_run=True),
|
|
591
|
+
lambda: _si.set_equip_in_save(container, block, char, slot, item, dry_run=False)) if container is not None
|
|
592
|
+
else (_si.render_equip_write, _si.set_equip(extra, char, slot, item, dry_run=True),
|
|
593
|
+
lambda: _si.set_equip(extra, char, slot, item, dry_run=False)))
|
|
594
|
+
except ValueError as e:
|
|
595
|
+
self._show_output(f"Cannot apply:\n {e}")
|
|
596
|
+
return
|
|
597
|
+
self._apply_plan(*trio, apply)
|
|
598
|
+
|
|
599
|
+
def _edit_stat(self, apply):
|
|
600
|
+
t = self._target()
|
|
601
|
+
if t is None or t["report"] is None:
|
|
602
|
+
self._show_output("Select a decodable slot on the left first.")
|
|
603
|
+
return
|
|
604
|
+
extra, container, block = t["extra"], t["container"], t["block"]
|
|
605
|
+
char, stat = self.stat_char_combo.currentText(), self.stat_kind_combo.currentText()
|
|
606
|
+
try:
|
|
607
|
+
val = int(self.stat_val_var.text())
|
|
608
|
+
if container is not None:
|
|
609
|
+
trio = (_si.render_stat_dual, _si.set_stat_in_save(container, block, char, stat, val, dry_run=True),
|
|
610
|
+
lambda: _si.set_stat_in_save(container, block, char, stat, val, dry_run=False))
|
|
611
|
+
elif extra is not None:
|
|
612
|
+
trio = (_si.render_stat_write, _si.set_stat_extra(extra, char, stat, val, dry_run=True),
|
|
613
|
+
lambda: _si.set_stat_extra(extra, char, stat, val, dry_run=False))
|
|
614
|
+
else:
|
|
615
|
+
self._show_output("Select an editable slot first.")
|
|
616
|
+
return
|
|
617
|
+
except ValueError as e:
|
|
618
|
+
self._show_output(f"Cannot apply:\n {e}")
|
|
619
|
+
return
|
|
620
|
+
self._apply_plan(*trio, apply)
|
|
621
|
+
|
|
622
|
+
def _edit_ap(self, apply):
|
|
623
|
+
t = self._target()
|
|
624
|
+
if t is None or t["report"] is None:
|
|
625
|
+
self._show_output("Select a decodable slot on the left first.")
|
|
626
|
+
return
|
|
627
|
+
extra, container, block = t["extra"], t["container"], t["block"]
|
|
628
|
+
char, ability, value = self.ap_char_combo.currentText(), self.ap_abil_var.text().strip(), self.ap_val_var.text().strip()
|
|
629
|
+
try:
|
|
630
|
+
if container is not None:
|
|
631
|
+
trio = (_si.render_ability_dual, _si.set_ap_in_save(container, block, char, ability, value, dry_run=True),
|
|
632
|
+
lambda: _si.set_ap_in_save(container, block, char, ability, value, dry_run=False))
|
|
633
|
+
elif extra is not None:
|
|
634
|
+
trio = (_si.render_ability_write, _si.set_ap_extra(extra, char, ability, value, dry_run=True),
|
|
635
|
+
lambda: _si.set_ap_extra(extra, char, ability, value, dry_run=False))
|
|
636
|
+
else:
|
|
637
|
+
self._show_output("Select an editable slot first.")
|
|
638
|
+
return
|
|
639
|
+
except (ValueError, TypeError) as e:
|
|
640
|
+
self._show_output(f"Cannot apply:\n {e}")
|
|
641
|
+
return
|
|
642
|
+
self._apply_plan(*trio, apply)
|
|
643
|
+
|
|
644
|
+
def _edit_keyitem(self, apply, obtained):
|
|
645
|
+
t = self._target()
|
|
646
|
+
if t is None or t["report"] is None:
|
|
647
|
+
self._show_output("Select a decodable slot on the left first.")
|
|
648
|
+
return
|
|
649
|
+
extra, container, block = t["extra"], t["container"], t["block"]
|
|
650
|
+
name = self.ki_var.text().strip()
|
|
651
|
+
try:
|
|
652
|
+
if container is not None:
|
|
653
|
+
trio = (_si.render_keyitem_dual, _si.set_keyitem_in_save(container, block, name, obtained=obtained, dry_run=True),
|
|
654
|
+
lambda: _si.set_keyitem_in_save(container, block, name, obtained=obtained, dry_run=False))
|
|
655
|
+
elif extra is not None:
|
|
656
|
+
trio = (_si.render_keyitem_write, _si.set_keyitem_extra(extra, name, obtained=obtained, dry_run=True),
|
|
657
|
+
lambda: _si.set_keyitem_extra(extra, name, obtained=obtained, dry_run=False))
|
|
658
|
+
else:
|
|
659
|
+
self._show_output("Select an editable slot first.")
|
|
660
|
+
return
|
|
661
|
+
except ValueError as e:
|
|
662
|
+
self._show_output(f"Cannot apply:\n {e}")
|
|
663
|
+
return
|
|
664
|
+
self._apply_plan(*trio, apply)
|