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.
Files changed (193) hide show
  1. ff9mapkit/__init__.py +18 -0
  2. ff9mapkit/__main__.py +36 -0
  3. ff9mapkit/_animdb.py +2994 -0
  4. ff9mapkit/_animdb_all.py +14125 -0
  5. ff9mapkit/_fieldtable.py +1516 -0
  6. ff9mapkit/_fieldtext.py +845 -0
  7. ff9mapkit/_held_poses.py +44 -0
  8. ff9mapkit/_itemdb.py +65 -0
  9. ff9mapkit/_modeldb.py +725 -0
  10. ff9mapkit/_narrowmap_data.py +10 -0
  11. ff9mapkit/_npcparams.py +634 -0
  12. ff9mapkit/_regen_animdb.py +72 -0
  13. ff9mapkit/_regen_animdb_all.py +66 -0
  14. ff9mapkit/_regen_fieldtable.py +95 -0
  15. ff9mapkit/_regen_fieldtext.py +66 -0
  16. ff9mapkit/_regen_modeldb.py +67 -0
  17. ff9mapkit/_regen_npcparams.py +123 -0
  18. ff9mapkit/_regen_scenedb.py +57 -0
  19. ff9mapkit/_scenedb.py +869 -0
  20. ff9mapkit/abilities.py +225 -0
  21. ff9mapkit/animations.py +120 -0
  22. ff9mapkit/archetypes.py +218 -0
  23. ff9mapkit/areatitle.py +76 -0
  24. ff9mapkit/battle/__init__.py +21 -0
  25. ff9mapkit/battle/abilityfeatures.py +294 -0
  26. ff9mapkit/battle/actiondelta.py +441 -0
  27. ff9mapkit/battle/aiauthor.py +305 -0
  28. ff9mapkit/battle/ailint.py +140 -0
  29. ff9mapkit/battle/aipatch.py +175 -0
  30. ff9mapkit/battle/battleai.py +148 -0
  31. ff9mapkit/battle/battlecsv.py +390 -0
  32. ff9mapkit/battle/battlepatch.py +395 -0
  33. ff9mapkit/battle/build.py +558 -0
  34. ff9mapkit/battle/camera_codec.py +332 -0
  35. ff9mapkit/battle/camera_data.py +128 -0
  36. ff9mapkit/battle/characterdelta.py +789 -0
  37. ff9mapkit/battle/event_data.py +72 -0
  38. ff9mapkit/battle/extract.py +540 -0
  39. ff9mapkit/battle/fbx.py +223 -0
  40. ff9mapkit/battle/reskin.py +149 -0
  41. ff9mapkit/battle/scene_codec.py +314 -0
  42. ff9mapkit/battle/scene_data.py +369 -0
  43. ff9mapkit/battle/scenelint.py +125 -0
  44. ff9mapkit/battle/seqasm.py +131 -0
  45. ff9mapkit/battle/seqauthor.py +220 -0
  46. ff9mapkit/battle/seqcodec.py +300 -0
  47. ff9mapkit/battle/seqdis.py +106 -0
  48. ff9mapkit/battle/seqpatch.py +137 -0
  49. ff9mapkit/battle_bgm.py +133 -0
  50. ff9mapkit/binutils.py +60 -0
  51. ff9mapkit/build.py +5445 -0
  52. ff9mapkit/campaign.py +1276 -0
  53. ff9mapkit/catalog.py +316 -0
  54. ff9mapkit/chain.py +358 -0
  55. ff9mapkit/cli.py +3114 -0
  56. ff9mapkit/config.py +360 -0
  57. ff9mapkit/content/__init__.py +13 -0
  58. ff9mapkit/content/areatitle.py +36 -0
  59. ff9mapkit/content/ate.py +118 -0
  60. ff9mapkit/content/camera.py +123 -0
  61. ff9mapkit/content/chest.py +186 -0
  62. ff9mapkit/content/choice.py +163 -0
  63. ff9mapkit/content/conductor.py +217 -0
  64. ff9mapkit/content/cutscene.py +290 -0
  65. ff9mapkit/content/encounter.py +41 -0
  66. ff9mapkit/content/entry_settle.py +50 -0
  67. ff9mapkit/content/equipment.py +93 -0
  68. ff9mapkit/content/event.py +191 -0
  69. ff9mapkit/content/gateway.py +101 -0
  70. ff9mapkit/content/inventory.py +59 -0
  71. ff9mapkit/content/itemdata.py +644 -0
  72. ff9mapkit/content/itemtext.py +168 -0
  73. ff9mapkit/content/jump.py +114 -0
  74. ff9mapkit/content/ladder.py +633 -0
  75. ff9mapkit/content/movement.py +53 -0
  76. ff9mapkit/content/music.py +97 -0
  77. ff9mapkit/content/npc.py +348 -0
  78. ff9mapkit/content/object.py +340 -0
  79. ff9mapkit/content/onentry.py +135 -0
  80. ff9mapkit/content/party.py +111 -0
  81. ff9mapkit/content/pathfind.py +138 -0
  82. ff9mapkit/content/platform.py +314 -0
  83. ff9mapkit/content/player.py +168 -0
  84. ff9mapkit/content/prop.py +75 -0
  85. ff9mapkit/content/region.py +340 -0
  86. ff9mapkit/content/reinit.py +59 -0
  87. ff9mapkit/content/savepoint.py +90 -0
  88. ff9mapkit/content/shop.py +178 -0
  89. ff9mapkit/content/sps_trigger.py +66 -0
  90. ff9mapkit/content/startup.py +71 -0
  91. ff9mapkit/content/synthesis.py +106 -0
  92. ff9mapkit/content/text.py +183 -0
  93. ff9mapkit/content/textcarry.py +290 -0
  94. ff9mapkit/content/verbatim.py +86 -0
  95. ff9mapkit/content/walkmesh_hotfix.py +38 -0
  96. ff9mapkit/data/__init__.py +48 -0
  97. ff9mapkit/data/_regen_provenance.py +142 -0
  98. ff9mapkit/data/provenance/blank.es.patch +1 -0
  99. ff9mapkit/data/provenance/blank.fr.patch +1 -0
  100. ff9mapkit/data/provenance/blank.gr.patch +1 -0
  101. ff9mapkit/data/provenance/blank.it.patch +1 -0
  102. ff9mapkit/data/provenance/blank.jp.patch +1 -0
  103. ff9mapkit/data/provenance/blank.uk.patch +1 -0
  104. ff9mapkit/data/provenance/blank.us.patch +1 -0
  105. ff9mapkit/data/provenance/manifest.json +65 -0
  106. ff9mapkit/data/provenance/region_template.patch +1 -0
  107. ff9mapkit/data/reference_arcs.toml +89 -0
  108. ff9mapkit/data/region_catalog.toml +593 -0
  109. ff9mapkit/deploystack.py +358 -0
  110. ff9mapkit/dialogue.py +803 -0
  111. ff9mapkit/eb/__init__.py +12 -0
  112. ff9mapkit/eb/_exprtable.py +59 -0
  113. ff9mapkit/eb/_membertable.py +38 -0
  114. ff9mapkit/eb/_optables.py +537 -0
  115. ff9mapkit/eb/_regen_optables.py +76 -0
  116. ff9mapkit/eb/cmdasm.py +323 -0
  117. ff9mapkit/eb/disasm.py +332 -0
  118. ff9mapkit/eb/edit.py +439 -0
  119. ff9mapkit/eb/exprasm.py +158 -0
  120. ff9mapkit/eb/model.py +178 -0
  121. ff9mapkit/eb/opcodes.py +463 -0
  122. ff9mapkit/eblint.py +177 -0
  123. ff9mapkit/editor/__init__.py +20 -0
  124. ff9mapkit/editor/app.py +950 -0
  125. ff9mapkit/editor/battle_forms.py +240 -0
  126. ff9mapkit/editor/breadcrumb.py +89 -0
  127. ff9mapkit/editor/dialogs.py +116 -0
  128. ff9mapkit/editor/feedback.py +208 -0
  129. ff9mapkit/editor/forms.py +632 -0
  130. ff9mapkit/editor/graphview.py +350 -0
  131. ff9mapkit/editor/jobs.py +342 -0
  132. ff9mapkit/editor/model.py +243 -0
  133. ff9mapkit/editor/picker.py +120 -0
  134. ff9mapkit/editor/theme.py +212 -0
  135. ff9mapkit/eventscan.py +1441 -0
  136. ff9mapkit/extract.py +2279 -0
  137. ff9mapkit/flags.py +693 -0
  138. ff9mapkit/forkreport.py +1383 -0
  139. ff9mapkit/hub.py +477 -0
  140. ff9mapkit/idgated.py +101 -0
  141. ff9mapkit/infohub.py +580 -0
  142. ff9mapkit/items.py +63 -0
  143. ff9mapkit/itemstats.py +346 -0
  144. ff9mapkit/journey.py +1902 -0
  145. ff9mapkit/keyitems.py +93 -0
  146. ff9mapkit/logic_add.py +632 -0
  147. ff9mapkit/logic_edit.py +728 -0
  148. ff9mapkit/logic_map.py +526 -0
  149. ff9mapkit/pack.py +175 -0
  150. ff9mapkit/playerswap.py +231 -0
  151. ff9mapkit/prop_archetypes.py +228 -0
  152. ff9mapkit/provision.py +282 -0
  153. ff9mapkit/refarc.py +825 -0
  154. ff9mapkit/save.py +337 -0
  155. ff9mapkit/save_items.py +1673 -0
  156. ff9mapkit/scene/__init__.py +11 -0
  157. ff9mapkit/scene/arena.py +63 -0
  158. ff9mapkit/scene/bgart.py +140 -0
  159. ff9mapkit/scene/bgi.py +732 -0
  160. ff9mapkit/scene/bgs.py +174 -0
  161. ff9mapkit/scene/bgx.py +185 -0
  162. ff9mapkit/scene/cam.py +345 -0
  163. ff9mapkit/scene/guide.py +311 -0
  164. ff9mapkit/scene/paint.py +506 -0
  165. ff9mapkit/scene/placeholder.py +107 -0
  166. ff9mapkit/sjbinary.py +285 -0
  167. ff9mapkit/sps/__init__.py +17 -0
  168. ff9mapkit/sps/author.py +294 -0
  169. ff9mapkit/sps/catalog.py +88 -0
  170. ff9mapkit/sps/codec.py +264 -0
  171. ff9mapkit/sps/edit.py +184 -0
  172. ff9mapkit/sps/lint.py +58 -0
  173. ff9mapkit/sps/render.py +116 -0
  174. ff9mapkit/sps/templates.py +47 -0
  175. ff9mapkit/sps/texture.py +131 -0
  176. ff9mapkit/walkmesh_hotfixes.py +163 -0
  177. ff9mapkit/workspace/__init__.py +18 -0
  178. ff9mapkit/workspace/battledoc.py +985 -0
  179. ff9mapkit/workspace/builddoc.py +607 -0
  180. ff9mapkit/workspace/forms_qt.py +586 -0
  181. ff9mapkit/workspace/importdoc.py +665 -0
  182. ff9mapkit/workspace/mapview.py +131 -0
  183. ff9mapkit/workspace/palette.py +85 -0
  184. ff9mapkit/workspace/savedoc.py +664 -0
  185. ff9mapkit/workspace/shell.py +6907 -0
  186. ff9mapkit/workspace/style.py +105 -0
  187. ff9mapkit/workspace/tuningdialog.py +223 -0
  188. ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
  189. ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
  190. ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
  191. ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
  192. ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
  193. 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.