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,105 @@
|
|
|
1
|
+
"""A Qt Style Sheet (QSS) for the workspace shell, generated from a theme palette.
|
|
2
|
+
|
|
3
|
+
PySide6-FREE -- a pure ``str``-building function over a palette dict, so it's unit-testable on a headless
|
|
4
|
+
machine (the same discipline as :mod:`..editor.theme`, whose ``LIGHT``/``DARK`` palettes this consumes).
|
|
5
|
+
QSS uses ``{`` / ``}`` heavily, so the template uses ``string.Template``'s ``$name`` placeholders (which
|
|
6
|
+
leave braces alone) rather than ``str.format``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from string import Template
|
|
12
|
+
|
|
13
|
+
# Every $name below must be a key in the palette (editor.theme LIGHT/DARK provide them all).
|
|
14
|
+
_QSS = Template(
|
|
15
|
+
"""
|
|
16
|
+
* { outline: 0; }
|
|
17
|
+
QWidget { background-color: $bg; color: $text; font-family: "Segoe UI"; font-size: 13px; }
|
|
18
|
+
QMainWindow::separator { background: $border; width: 1px; height: 1px; }
|
|
19
|
+
|
|
20
|
+
QToolBar { background: $surface; border: 0; border-bottom: 1px solid $border; padding: 5px 8px; spacing: 8px; }
|
|
21
|
+
QToolButton, QPushButton {
|
|
22
|
+
background: $surface_btn; color: $text; border: 1px solid $border;
|
|
23
|
+
border-radius: 6px; padding: 6px 12px;
|
|
24
|
+
}
|
|
25
|
+
QToolButton:hover, QPushButton:hover { background: $hover; }
|
|
26
|
+
QPushButton:pressed, QToolButton:pressed { background: $pressed; }
|
|
27
|
+
QPushButton:disabled { color: $muted; background: $bg; }
|
|
28
|
+
QPushButton#accent { background: $accent; color: $accent_fg; border: 1px solid $accent; }
|
|
29
|
+
QPushButton#accent:hover { background: $accent_hover; }
|
|
30
|
+
QPushButton#accent:pressed { background: $accent_pressed; }
|
|
31
|
+
/* a disabled accent button (e.g. Save with nothing to save) must grey out -- the #accent id
|
|
32
|
+
selector otherwise out-ranks the generic :disabled rule and would stay blue. */
|
|
33
|
+
QPushButton#accent:disabled { background: $surface_btn; color: $muted; border: 1px solid $border; }
|
|
34
|
+
|
|
35
|
+
/* Indicators MUST be fully specified: once a stylesheet touches a QCheckBox/QRadioButton, Qt stops
|
|
36
|
+
drawing the native checked dot, so without this the selected state renders INVISIBLE. */
|
|
37
|
+
QCheckBox, QRadioButton { background: transparent; spacing: 7px; }
|
|
38
|
+
QCheckBox::indicator, QRadioButton::indicator {
|
|
39
|
+
width: 15px; height: 15px; border: 1px solid $border; background: $field;
|
|
40
|
+
}
|
|
41
|
+
QRadioButton::indicator { border-radius: 8px; }
|
|
42
|
+
QCheckBox::indicator { border-radius: 4px; }
|
|
43
|
+
QCheckBox::indicator:hover, QRadioButton::indicator:hover { border: 1px solid $accent; }
|
|
44
|
+
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
|
45
|
+
background: $accent; border: 1px solid $accent;
|
|
46
|
+
}
|
|
47
|
+
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled { border: 1px solid $muted; background: $bg; }
|
|
48
|
+
|
|
49
|
+
QLineEdit {
|
|
50
|
+
background: $field; color: $text; border: 1px solid $border; border-radius: 6px;
|
|
51
|
+
padding: 6px 9px; selection-background-color: $accent; selection-color: $accent_fg;
|
|
52
|
+
}
|
|
53
|
+
QLineEdit:focus { border: 1px solid $accent; }
|
|
54
|
+
QLineEdit#search { background: $surface; color: $muted; }
|
|
55
|
+
|
|
56
|
+
QTreeWidget, QTreeView, QListWidget {
|
|
57
|
+
background: $surface; border: 1px solid $border; border-radius: 8px; padding: 4px;
|
|
58
|
+
}
|
|
59
|
+
QTreeView::item, QListWidget::item { padding: 5px 4px; border-radius: 4px; }
|
|
60
|
+
QTreeView::item:hover, QListWidget::item:hover { background: $hover; }
|
|
61
|
+
QTreeView::item:selected, QListWidget::item:selected { background: $accent; color: $accent_fg; }
|
|
62
|
+
QHeaderView::section { background: $surface_btn; color: $muted; border: 0; padding: 5px; }
|
|
63
|
+
|
|
64
|
+
QTabWidget::pane { border: 1px solid $border; border-radius: 8px; top: -1px; }
|
|
65
|
+
QTabBar::tab {
|
|
66
|
+
background: $surface_btn; color: $muted; padding: 7px 16px; border: 1px solid $border;
|
|
67
|
+
border-bottom: 0; border-top-left-radius: 6px; border-top-right-radius: 6px; margin-right: 2px;
|
|
68
|
+
}
|
|
69
|
+
QTabBar::tab:selected { background: $bg; color: $text; }
|
|
70
|
+
QTabBar::tab:hover { color: $text; }
|
|
71
|
+
|
|
72
|
+
QPlainTextEdit, QTextEdit {
|
|
73
|
+
background: $log_bg; color: $log_fg; border: 1px solid $border; border-radius: 8px;
|
|
74
|
+
font-family: "Cascadia Code", "Consolas", monospace; font-size: 12px; padding: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* dropdown menus (the toolbar Field / Campaign / Journey buttons) */
|
|
78
|
+
QMenu { background: $surface; border: 1px solid $border; border-radius: 6px; padding: 4px; }
|
|
79
|
+
QMenu::item { padding: 6px 22px; border-radius: 4px; }
|
|
80
|
+
QMenu::item:selected { background: $accent; color: $accent_fg; }
|
|
81
|
+
QMenu::separator { height: 1px; background: $border; margin: 4px 6px; }
|
|
82
|
+
|
|
83
|
+
QDockWidget { color: $muted; }
|
|
84
|
+
QDockWidget::title { background: $surface; padding: 6px 9px; border-bottom: 1px solid $border; }
|
|
85
|
+
|
|
86
|
+
QScrollBar:vertical { background: $bg; width: 12px; margin: 0; }
|
|
87
|
+
QScrollBar::handle:vertical { background: $scroll; border-radius: 5px; min-height: 28px; }
|
|
88
|
+
QScrollBar:horizontal { background: $bg; height: 12px; margin: 0; }
|
|
89
|
+
QScrollBar::handle:horizontal { background: $scroll; border-radius: 5px; min-width: 28px; }
|
|
90
|
+
QScrollBar::add-line, QScrollBar::sub-line { width: 0; height: 0; }
|
|
91
|
+
QScrollBar::add-page, QScrollBar::sub-page { background: transparent; }
|
|
92
|
+
|
|
93
|
+
QSplitter::handle { background: $border; }
|
|
94
|
+
QSplitter::handle:horizontal { width: 1px; }
|
|
95
|
+
QSplitter::handle:vertical { height: 1px; }
|
|
96
|
+
QLabel { background: transparent; }
|
|
97
|
+
QStatusBar { background: $surface; color: $muted; border-top: 1px solid $border; }
|
|
98
|
+
QToolTip { background: $surface; color: $text; border: 1px solid $border; }
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def qss(palette: dict) -> str:
|
|
104
|
+
"""Render the workspace stylesheet for ``palette`` (an :mod:`..editor.theme` LIGHT/DARK dict)."""
|
|
105
|
+
return _QSS.substitute(palette)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""A modal editor for a journey's ``[journey.tuning]`` -- the mod-GLOBAL player/ability CSV deltas (BaseStats /
|
|
2
|
+
abilities / leveling / status / ...). It REUSES the battle "Party & abilities" specs verbatim
|
|
3
|
+
(:data:`ff9mapkit.editor.battle_forms.PLAYER_TABLES`) over the shared tk-free form machinery
|
|
4
|
+
(:mod:`ff9mapkit.editor.forms` + :mod:`forms_qt`), so the same tables a battle.toml carries are authored here at
|
|
5
|
+
the journey level -- the placement the user chose (mod-global tuning = journey).
|
|
6
|
+
|
|
7
|
+
The dialog is self-contained: a left row-list (one per tuning entry, ``<table> · <selector>``), a right form host,
|
|
8
|
+
Add / Remove, and OK/Cancel. ``result_tuning`` holds the edited ``{block: [rows]}`` dict on accept (else None); the
|
|
9
|
+
caller (:meth:`ff9mapkit.workspace.shell.Workspace.on_set_journey_tuning`) writes it back with
|
|
10
|
+
:func:`ff9mapkit.journey.set_journey_tuning`. The nested player tables (``[[learn]]`` / ``[[ability_feature]]`` /
|
|
11
|
+
``[[status_set]]`` / ``[[magic_sword_set]]``) stay hand-authored -- as everywhere else -- and are left untouched.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from PySide6.QtWidgets import (
|
|
16
|
+
QComboBox, QDialog, QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QListWidget, QPushButton, QScrollArea,
|
|
17
|
+
QSplitter, QVBoxLayout, QWidget,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ..editor import battle_forms as bf
|
|
21
|
+
from ..editor import forms
|
|
22
|
+
from .forms_qt import build_form, read
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TuningDialog(QDialog):
|
|
26
|
+
"""Edit a journey's ``[journey.tuning]`` player/ability CSV blocks. ``tuning`` is the current ``{block:
|
|
27
|
+
[rows]}`` (only the KNOWN player-table blocks are editable; unknown/nested ones are preserved by the caller's
|
|
28
|
+
text writer, untouched here). On accept, :attr:`result_tuning` is the edited dict (blocks with no rows
|
|
29
|
+
dropped); on cancel it's None."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, parent, palette, jid, tuning, *, is_bare=False):
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
self.pal = palette
|
|
34
|
+
self.jid = jid
|
|
35
|
+
self.setWindowTitle(f"Tuning — {jid}")
|
|
36
|
+
self.resize(660, 470)
|
|
37
|
+
# a working COPY of just the FORM-editable blocks (the 7 PLAYER_SPECS); deep enough that Cancel discards.
|
|
38
|
+
self.tuning = {k: [dict(r) for r in (tuning.get(k) or []) if isinstance(r, dict)]
|
|
39
|
+
for k in bf.PLAYER_SPECS if tuning.get(k)}
|
|
40
|
+
# the nested blocks this dialog does NOT edit (learn / ability_feature / status_set / magic_sword_set, +
|
|
41
|
+
# any unknown key) — carried through verbatim so an edit never DESTROYS hand-authored tuning.
|
|
42
|
+
self._untouched = {k: v for k, v in (tuning or {}).items() if k not in bf.PLAYER_SPECS}
|
|
43
|
+
self._rows: list = [] # [(block, idx)] parallel to the list widget
|
|
44
|
+
self._ctx = None # {block, idx, spec, getters} for the mounted form
|
|
45
|
+
self.result_tuning = None
|
|
46
|
+
self._build_ui(is_bare)
|
|
47
|
+
self._rebuild()
|
|
48
|
+
if self.rows.count():
|
|
49
|
+
self.rows.setCurrentRow(0)
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------ UI
|
|
52
|
+
def _build_ui(self, is_bare):
|
|
53
|
+
outer = QVBoxLayout(self)
|
|
54
|
+
intro = QLabel("Mod-GLOBAL player/ability tuning for this journey — the same BaseStats / abilities / "
|
|
55
|
+
"leveling deltas a field.toml carries, injected into the entry member at deploy. One CSV "
|
|
56
|
+
"per mod (shared across a multi-journey hub).")
|
|
57
|
+
intro.setWordWrap(True)
|
|
58
|
+
intro.setStyleSheet(f"color:{self.pal['muted']};")
|
|
59
|
+
outer.addWidget(intro)
|
|
60
|
+
if is_bare:
|
|
61
|
+
warn = QLabel("⚠ This is a BARE single-field journey — tuning is injected into a MULTI-campaign entry "
|
|
62
|
+
"member, so it WON'T apply here. Put the deltas on the entry field's own field.toml.")
|
|
63
|
+
warn.setWordWrap(True)
|
|
64
|
+
warn.setStyleSheet(f"color:{self.pal['warn']};")
|
|
65
|
+
outer.addWidget(warn)
|
|
66
|
+
|
|
67
|
+
split = QSplitter()
|
|
68
|
+
left = QWidget()
|
|
69
|
+
lv = QVBoxLayout(left)
|
|
70
|
+
lv.setContentsMargins(0, 0, 0, 0)
|
|
71
|
+
self.rows = QListWidget()
|
|
72
|
+
self.rows.currentRowChanged.connect(self._on_row)
|
|
73
|
+
lv.addWidget(self.rows, 1)
|
|
74
|
+
self.add_btn = QPushButton("Add tuning…")
|
|
75
|
+
self.add_btn.clicked.connect(self._add)
|
|
76
|
+
lv.addWidget(self.add_btn)
|
|
77
|
+
self.del_btn = QPushButton("Remove selected")
|
|
78
|
+
self.del_btn.clicked.connect(self._remove)
|
|
79
|
+
self.del_btn.setEnabled(False)
|
|
80
|
+
lv.addWidget(self.del_btn)
|
|
81
|
+
split.addWidget(left)
|
|
82
|
+
|
|
83
|
+
right = QWidget()
|
|
84
|
+
rv = QVBoxLayout(right)
|
|
85
|
+
rv.setContentsMargins(0, 0, 0, 0)
|
|
86
|
+
self.host_scroll = QScrollArea()
|
|
87
|
+
self.host_scroll.setWidgetResizable(True)
|
|
88
|
+
self.host_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
89
|
+
self.host = QWidget()
|
|
90
|
+
self.host_lay = QVBoxLayout(self.host)
|
|
91
|
+
self.host_scroll.setWidget(self.host)
|
|
92
|
+
rv.addWidget(self.host_scroll, 1)
|
|
93
|
+
split.addWidget(right)
|
|
94
|
+
split.setSizes([220, 430])
|
|
95
|
+
outer.addWidget(split, 1)
|
|
96
|
+
|
|
97
|
+
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
98
|
+
bb.accepted.connect(self._accept)
|
|
99
|
+
bb.rejected.connect(self.reject)
|
|
100
|
+
outer.addWidget(bb)
|
|
101
|
+
self._placeholder("Add a tuning row, or pick one to edit.")
|
|
102
|
+
|
|
103
|
+
def _clear(self):
|
|
104
|
+
while self.host_lay.count():
|
|
105
|
+
it = self.host_lay.takeAt(0)
|
|
106
|
+
w = it.widget()
|
|
107
|
+
if w is not None:
|
|
108
|
+
w.deleteLater()
|
|
109
|
+
|
|
110
|
+
def _placeholder(self, text):
|
|
111
|
+
self._clear()
|
|
112
|
+
lbl = QLabel(text)
|
|
113
|
+
lbl.setStyleSheet(f"color:{self.pal['muted']};")
|
|
114
|
+
lbl.setWordWrap(True)
|
|
115
|
+
self.host_lay.addWidget(lbl)
|
|
116
|
+
self.host_lay.addStretch(1)
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------ row list
|
|
119
|
+
def _all_rows(self):
|
|
120
|
+
return [(block, i) for block in bf.PLAYER_SPECS for i in range(len(self.tuning.get(block) or []))]
|
|
121
|
+
|
|
122
|
+
def _rebuild(self):
|
|
123
|
+
self.rows.blockSignals(True)
|
|
124
|
+
self.rows.clear()
|
|
125
|
+
self._rows = []
|
|
126
|
+
for block, i in self._all_rows():
|
|
127
|
+
row = self.tuning[block][i]
|
|
128
|
+
self.rows.addItem(f"{bf.PLAYER_LABEL[block]} · {row.get(bf.PLAYER_SELECTOR[block], i)}")
|
|
129
|
+
self._rows.append((block, i))
|
|
130
|
+
self.rows.blockSignals(False)
|
|
131
|
+
|
|
132
|
+
def _on_row(self, r):
|
|
133
|
+
if not (0 <= r < len(self._rows)):
|
|
134
|
+
self.del_btn.setEnabled(False)
|
|
135
|
+
return
|
|
136
|
+
self._commit()
|
|
137
|
+
block, idx = self._rows[r]
|
|
138
|
+
self.del_btn.setEnabled(True)
|
|
139
|
+
if 0 <= idx < len(self.tuning.get(block) or []):
|
|
140
|
+
self._mount(block, idx)
|
|
141
|
+
|
|
142
|
+
def _mount(self, block, idx):
|
|
143
|
+
self._clear()
|
|
144
|
+
spec = bf.PLAYER_SPECS[block]
|
|
145
|
+
form, getters = build_form(spec, forms.entity_to_values(spec, self.tuning[block][idx]), self.pal)
|
|
146
|
+
self.host_lay.addWidget(form)
|
|
147
|
+
self.host_lay.addStretch(1)
|
|
148
|
+
self._ctx = {"block": block, "idx": idx, "spec": spec, "getters": getters}
|
|
149
|
+
|
|
150
|
+
def _fold(self, ctx) -> bool:
|
|
151
|
+
try:
|
|
152
|
+
entity = forms.build_entity(ctx["spec"], read(ctx["getters"]))
|
|
153
|
+
except ValueError:
|
|
154
|
+
return False
|
|
155
|
+
lst = self.tuning.get(ctx["block"]) or []
|
|
156
|
+
if not (0 <= ctx["idx"] < len(lst)):
|
|
157
|
+
return False # a stale ctx (its row was removed)
|
|
158
|
+
lst[ctx["idx"]] = entity
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
def _commit(self) -> bool:
|
|
162
|
+
return self._fold(self._ctx) if self._ctx else True
|
|
163
|
+
|
|
164
|
+
def _select(self, block, idx):
|
|
165
|
+
for r, node in enumerate(self._rows):
|
|
166
|
+
if node == (block, idx):
|
|
167
|
+
self.rows.setCurrentRow(r)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------ add / remove
|
|
171
|
+
def _add(self):
|
|
172
|
+
self._commit()
|
|
173
|
+
block = self._pick_table()
|
|
174
|
+
if not block:
|
|
175
|
+
return
|
|
176
|
+
self.tuning.setdefault(block, []).append(dict(bf.PLAYER_DEFAULT[block]))
|
|
177
|
+
self._rebuild()
|
|
178
|
+
self._select(block, len(self.tuning[block]) - 1)
|
|
179
|
+
|
|
180
|
+
def _pick_table(self):
|
|
181
|
+
dlg = QDialog(self)
|
|
182
|
+
dlg.setWindowTitle("Add tuning")
|
|
183
|
+
lay = QVBoxLayout(dlg)
|
|
184
|
+
lay.addWidget(QLabel("Tune which player-side table? It's mod-global — applied to the whole journey."))
|
|
185
|
+
combo = QComboBox()
|
|
186
|
+
for key, label, *_ in bf.PLAYER_TABLES:
|
|
187
|
+
combo.addItem(label, key)
|
|
188
|
+
lay.addWidget(combo)
|
|
189
|
+
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
190
|
+
bb.accepted.connect(dlg.accept)
|
|
191
|
+
bb.rejected.connect(dlg.reject)
|
|
192
|
+
lay.addWidget(bb)
|
|
193
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
194
|
+
return None
|
|
195
|
+
return combo.currentData()
|
|
196
|
+
|
|
197
|
+
def _remove(self):
|
|
198
|
+
r = self.rows.currentRow()
|
|
199
|
+
if not (0 <= r < len(self._rows)):
|
|
200
|
+
return
|
|
201
|
+
block, idx = self._rows[r]
|
|
202
|
+
lst = self.tuning.get(block) or []
|
|
203
|
+
if not (0 <= idx < len(lst)):
|
|
204
|
+
return
|
|
205
|
+
self._ctx = None # the mounted form's row is going away — don't commit it
|
|
206
|
+
del lst[idx]
|
|
207
|
+
if not lst:
|
|
208
|
+
self.tuning.pop(block, None)
|
|
209
|
+
self._rebuild()
|
|
210
|
+
nxt = min(r, self.rows.count() - 1)
|
|
211
|
+
if 0 <= nxt < self.rows.count():
|
|
212
|
+
self.rows.setCurrentRow(nxt)
|
|
213
|
+
else:
|
|
214
|
+
self._placeholder("Add a tuning row, or pick one to edit.")
|
|
215
|
+
self.del_btn.setEnabled(False)
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------------ accept
|
|
218
|
+
def _accept(self):
|
|
219
|
+
if not self._commit():
|
|
220
|
+
return # an invalid field stays open (the form highlights it)
|
|
221
|
+
# the edited form blocks OVER the carried-through nested blocks (disjoint keys -> a clean union)
|
|
222
|
+
self.result_tuning = {**self._untouched, **{b: rows for b, rows in self.tuning.items() if rows}}
|
|
223
|
+
self.accept()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ff9mapkit
|
|
3
|
+
Version: 1.0.0b3
|
|
4
|
+
Summary: Author novel custom field maps for Final Fantasy IX (Memoria engine) from a declarative TOML project file.
|
|
5
|
+
Author: GameJawnsInc
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GameJawnsInc/Dream-World-IX
|
|
8
|
+
Project-URL: Repository, https://github.com/GameJawnsInc/Dream-World-IX
|
|
9
|
+
Project-URL: Issues, https://github.com/GameJawnsInc/Dream-World-IX/issues
|
|
10
|
+
Keywords: final-fantasy-ix,ff9,memoria,modding,field,map
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Games/Entertainment
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: Pillow>=9.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-xdist>=3; extra == "dev"
|
|
28
|
+
Provides-Extra: save
|
|
29
|
+
Requires-Dist: pycryptodome>=3.10; extra == "save"
|
|
30
|
+
Provides-Extra: assets
|
|
31
|
+
Requires-Dist: UnityPy; extra == "assets"
|
|
32
|
+
Provides-Extra: gui
|
|
33
|
+
Requires-Dist: PySide6-Essentials>=6.5; extra == "gui"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# FF9 Map Kit (`ff9mapkit`)
|
|
37
|
+
|
|
38
|
+
Author **novel custom field maps** for *Final Fantasy IX* (Steam, via the
|
|
39
|
+
[Memoria engine](https://github.com/Albeoris/Memoria)) from a single declarative
|
|
40
|
+
`field.toml`, compiled into a drop-in Memoria mod.
|
|
41
|
+
|
|
42
|
+
> Part of the **[Dream World IX](../README.md)** project — `ff9mapkit` is the toolkit/package name
|
|
43
|
+
> (unchanged), `pip install`ed and imported as `ff9mapkit`.
|
|
44
|
+
|
|
45
|
+
> **Feature-complete and in-game-verified.** The productized form of a proven
|
|
46
|
+
> pipeline for minting brand-new playable FF9 fields, end to end. A **novel** field runs on a
|
|
47
|
+
> **stock, unmodified Memoria install**; a **forked** field needs the small bundled engine patch
|
|
48
|
+
> set for fork fidelity (see [`docs/ENGINE.md`](docs/ENGINE.md)).
|
|
49
|
+
|
|
50
|
+
**Headline capabilities:** author **any camera angle** from scratch (single / scrolling / multi-camera)
|
|
51
|
+
with a pixel-accurate paint guide · **fork any of ~674 real fields** — camera, walkmesh, art, *and* its
|
|
52
|
+
exits/encounters/music · NPCs, dialogue, gateways, encounters, events, story branching, and cutscenes from
|
|
53
|
+
one `field.toml`. Author it in TOML, a **form-based editor**, or a **[Blender add-on](blender/README.md)**.
|
|
54
|
+
|
|
55
|
+
> **Full capability list & command reference → [`docs/FEATURES.md`](docs/FEATURES.md)** (with a before/now
|
|
56
|
+
> comparison) and [`SETUP.md`](../SETUP.md) (the 59-command CLI reference). [`docs/gallery/`](docs/gallery/)
|
|
57
|
+
> collects screenshots/GIFs as they're captured.
|
|
58
|
+
|
|
59
|
+
## What it does
|
|
60
|
+
|
|
61
|
+
Given a `field.toml` describing one field — its camera, painted background layers, walkmesh,
|
|
62
|
+
NPCs, dialogue, gateways, encounter, and music — `ff9mapkit build` emits everything a custom
|
|
63
|
+
field needs:
|
|
64
|
+
|
|
65
|
+
- the background scene (`.bgx` camera + overlay PNGs) and walkmesh (`.bgi`),
|
|
66
|
+
- the field event script (`.eb`) for all seven languages,
|
|
67
|
+
- dialogue text (`.mes`),
|
|
68
|
+
- and the `DictionaryPatch` / `BattlePatch` registration + `ModDescription.xml`.
|
|
69
|
+
|
|
70
|
+
## What stays a human task — the way the originals were made
|
|
71
|
+
|
|
72
|
+
FF9's backgrounds are **pre-rendered**: the original artists built each room as a 3D scene, shot it
|
|
73
|
+
through a fixed camera to bake a 2D plate, and the game projects the live 3D characters back onto that
|
|
74
|
+
plate through the *same* camera. `ff9mapkit` deliberately follows that pipeline instead of hiding it.
|
|
75
|
+
You place the camera; the kit hands you a **pixel-accurate paint guide** — the floor and walls
|
|
76
|
+
projected onto the canvas, the modern stand-in for the layout render the original artists painted over
|
|
77
|
+
— and you paint the background to match. Your hand-modeled `.obj` walkmesh is converted to the
|
|
78
|
+
engine's `.bgi` and projected through that identical camera, so characters stand exactly where the art
|
|
79
|
+
says they should. Painting the art and (optionally) modeling the geometry stay yours; everything in
|
|
80
|
+
between is the kit.
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
pip install -e . # from the ff9mapkit\ package dir
|
|
86
|
+
py -m ff9mapkit doctor # verify it found your FF9 install
|
|
87
|
+
py -m ff9mapkit import <field> --out myroom --verbatim # fork a real field — or `new` for original art
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> **Full setup → [`SETUP.md`](../SETUP.md)**: extras (`gui`/`save`/`dev`), game-path resolution, the
|
|
91
|
+
> one-time `extract-templates` (the kit ships no game data — see [Provenance](docs/PROVENANCE.md)),
|
|
92
|
+
> `doctor`, the dev loop, and a guided first-field walkthrough.
|
|
93
|
+
|
|
94
|
+
**Prefer not to touch TOML?** Author the *logic* (dialogue, events, story flags, encounters, music,
|
|
95
|
+
cutscenes) in the form-based editor — `ff9mapkit edit <field.toml>`. The visual side has a front-end too:
|
|
96
|
+
the [**Blender add-on**](blender/README.md) poses the camera, models the walkmesh, places markers, and
|
|
97
|
+
writes a `scene.toml`. So the suite splits cleanly — **Blender = where things are, the editor = what they
|
|
98
|
+
do** — and `build` compiles both. There's also a one-window [PySide6 Workspace GUI](../SETUP.md#6-the-gui-workspace-optional).
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
|
|
102
|
+
59 subcommands — run `ff9mapkit -h` (or `py -m ff9mapkit -h`) for the full list. A taste of the families:
|
|
103
|
+
|
|
104
|
+
- **Author** — `new` (scaffold) · `guide` (paint guide for your camera) · `walkmesh` · `edit` (form editor)
|
|
105
|
+
- **Build & ship** — `build` · `lint` · `pack` · `export-art`
|
|
106
|
+
- **Fork a real field** — `import` (`--editable`/`--native`/`--verbatim`) · `import-chain` · `fork-report`
|
|
107
|
+
- **Campaigns & journeys** — `new-campaign` / `build-all` · `gen-hub` / `assemble-journey`
|
|
108
|
+
- **Battle maps & tuning** — `battle-import` / `battle-build` · `battle-scene` / `battle-ai`
|
|
109
|
+
- **Dialogue, catalogs & saves** — `dialogue` · `catalog` / `models` / `archetypes` · `flags-inspect` · `items-inspect`
|
|
110
|
+
|
|
111
|
+
> **The full grouped command reference (all 59, with flags) is in [`SETUP.md` §7](../SETUP.md#7-cli-command-reference).**
|
|
112
|
+
|
|
113
|
+
## Docs
|
|
114
|
+
|
|
115
|
+
- [`SETUP.md`](../SETUP.md) — **start here:** install, configure, the dev loop, and your first field (setup + quickstart).
|
|
116
|
+
- [`docs/TUTORIAL.md`](docs/TUTORIAL.md) — the focused ~10-minute first-field walkthrough.
|
|
117
|
+
- [`docs/FEATURES.md`](docs/FEATURES.md) — **the full capability list** (+ before/now comparison).
|
|
118
|
+
- [`docs/gallery/`](docs/gallery/) — collects screenshots/GIFs as they're captured.
|
|
119
|
+
- [`docs/FORMAT.md`](docs/FORMAT.md) — the `field.toml` schema.
|
|
120
|
+
- [`docs/PIPELINE.md`](docs/PIPELINE.md) — the full authoring workflow.
|
|
121
|
+
- [`docs/ENGINE.md`](docs/ENGINE.md) — engine requirements (stock Memoria) + provenance notes.
|
|
122
|
+
- [`docs/PROVENANCE.md`](docs/PROVENANCE.md) — **the kit ships no game data**: how the base assets are
|
|
123
|
+
regenerated from your own FF9 install (`extract-templates`), and why that's legally clean.
|
|
124
|
+
- [`docs/TECHNICAL.md`](docs/TECHNICAL.md) — the hard problems solved (camera math, `.eb` format, import).
|
|
125
|
+
- [`docs/GLOSSARY.md`](docs/GLOSSARY.md) — terms used across the docs (walkmesh, gateway, fork, GLOB flag…).
|
|
126
|
+
- [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — common first-run failures → fixes.
|
|
127
|
+
- [`docs/KNOWN_ISSUES.md`](docs/KNOWN_ISSUES.md) — beta limitations + the Workspace GUI gaps.
|
|
128
|
+
- [`examples/vivi-hut/`](examples/vivi-hut) — a complete worked example.
|
|
129
|
+
- [`blender/`](blender/README.md) — the **Blender add-on**: visually author the camera + walkmesh,
|
|
130
|
+
then export a `field.toml` for `build` (Blender 4.2+/5.x).
|
|
131
|
+
|
|
132
|
+
## How it's built / trusted
|
|
133
|
+
|
|
134
|
+
The library is split into `eb` (the event-script codec + content injectors), `scene`
|
|
135
|
+
(camera math, `.bgx`, `.bgi` walkmesh, paint guides), `build` (the `field.toml` compiler),
|
|
136
|
+
and `pack`. Correctness is proven by an **offline golden-master test suite**: every codec
|
|
137
|
+
round-trips your install's field assets byte-for-byte (regenerated locally via `extract-templates`
|
|
138
|
+
— the kit ships none), and compiling the example reproduces an in-game-verified field's script exactly.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pip install -e ".[dev]" && pytest # the full suite
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## About
|
|
145
|
+
|
|
146
|
+
I make games — including an FFIX-inspired RPG of my own — so this started as the tool I wanted while
|
|
147
|
+
learning how FF9's fields actually work, not a drive-by experiment. The aim was to build a new room
|
|
148
|
+
the way the game's creators did: paint a background against a 3D-derived guide, then walk on geometry
|
|
149
|
+
projected through the same camera — so authoring a field feels like level design rather than blind
|
|
150
|
+
byte-hacking. Months of reverse-engineering the field format, the projection math, and the event
|
|
151
|
+
bytecode went into making that the easy path. If you're poking at FF9's internals too, I hope it
|
|
152
|
+
saves you the same dig.
|
|
153
|
+
|
|
154
|
+
Built on (and grateful for) the [Memoria engine](https://github.com/Albeoris/Memoria) — none of this
|
|
155
|
+
is possible without it.
|