ff9mapkit 1.0.0b3__tar.gz → 1.0.0b4__tar.gz

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 (315) hide show
  1. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/PKG-INFO +1 -1
  2. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/__init__.py +1 -1
  3. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/build.py +144 -48
  4. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/cli.py +86 -0
  5. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/config.py +20 -0
  6. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/conductor.py +137 -51
  7. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/cutscene.py +70 -0
  8. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/text.py +7 -3
  9. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/dialogue.py +1 -1
  10. ff9mapkit-1.0.0b4/ff9mapkit/memoria.py +91 -0
  11. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/provision.py +29 -1
  12. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit.egg-info/PKG-INFO +1 -1
  13. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit.egg-info/SOURCES.txt +2 -0
  14. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/pyproject.toml +1 -1
  15. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_ate.py +107 -0
  16. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_campaign.py +1 -1
  17. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_content.py +122 -13
  18. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_flags.py +1 -1
  19. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_on_entry.py +2 -2
  20. ff9mapkit-1.0.0b4/tests/test_setup.py +134 -0
  21. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_text.py +4 -4
  22. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_verbatim.py +63 -0
  23. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/LICENSE +0 -0
  24. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/README.md +0 -0
  25. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/__main__.py +0 -0
  26. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_animdb.py +0 -0
  27. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_animdb_all.py +0 -0
  28. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_fieldtable.py +0 -0
  29. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_fieldtext.py +0 -0
  30. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_held_poses.py +0 -0
  31. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_itemdb.py +0 -0
  32. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_modeldb.py +0 -0
  33. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_narrowmap_data.py +0 -0
  34. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_npcparams.py +0 -0
  35. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_animdb.py +0 -0
  36. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_animdb_all.py +0 -0
  37. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_fieldtable.py +0 -0
  38. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_fieldtext.py +0 -0
  39. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_modeldb.py +0 -0
  40. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_npcparams.py +0 -0
  41. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_regen_scenedb.py +0 -0
  42. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/_scenedb.py +0 -0
  43. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/abilities.py +0 -0
  44. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/animations.py +0 -0
  45. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/archetypes.py +0 -0
  46. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/areatitle.py +0 -0
  47. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/__init__.py +0 -0
  48. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/abilityfeatures.py +0 -0
  49. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/actiondelta.py +0 -0
  50. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/aiauthor.py +0 -0
  51. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/ailint.py +0 -0
  52. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/aipatch.py +0 -0
  53. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/battleai.py +0 -0
  54. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/battlecsv.py +0 -0
  55. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/battlepatch.py +0 -0
  56. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/build.py +0 -0
  57. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/camera_codec.py +0 -0
  58. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/camera_data.py +0 -0
  59. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/characterdelta.py +0 -0
  60. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/event_data.py +0 -0
  61. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/extract.py +0 -0
  62. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/fbx.py +0 -0
  63. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/reskin.py +0 -0
  64. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/scene_codec.py +0 -0
  65. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/scene_data.py +0 -0
  66. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/scenelint.py +0 -0
  67. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/seqasm.py +0 -0
  68. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/seqauthor.py +0 -0
  69. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/seqcodec.py +0 -0
  70. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/seqdis.py +0 -0
  71. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle/seqpatch.py +0 -0
  72. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/battle_bgm.py +0 -0
  73. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/binutils.py +0 -0
  74. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/campaign.py +0 -0
  75. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/catalog.py +0 -0
  76. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/chain.py +0 -0
  77. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/__init__.py +0 -0
  78. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/areatitle.py +0 -0
  79. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/ate.py +0 -0
  80. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/camera.py +0 -0
  81. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/chest.py +0 -0
  82. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/choice.py +0 -0
  83. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/encounter.py +0 -0
  84. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/entry_settle.py +0 -0
  85. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/equipment.py +0 -0
  86. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/event.py +0 -0
  87. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/gateway.py +0 -0
  88. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/inventory.py +0 -0
  89. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/itemdata.py +0 -0
  90. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/itemtext.py +0 -0
  91. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/jump.py +0 -0
  92. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/ladder.py +0 -0
  93. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/movement.py +0 -0
  94. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/music.py +0 -0
  95. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/npc.py +0 -0
  96. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/object.py +0 -0
  97. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/onentry.py +0 -0
  98. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/party.py +0 -0
  99. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/pathfind.py +0 -0
  100. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/platform.py +0 -0
  101. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/player.py +0 -0
  102. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/prop.py +0 -0
  103. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/region.py +0 -0
  104. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/reinit.py +0 -0
  105. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/savepoint.py +0 -0
  106. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/shop.py +0 -0
  107. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/sps_trigger.py +0 -0
  108. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/startup.py +0 -0
  109. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/synthesis.py +0 -0
  110. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/textcarry.py +0 -0
  111. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/verbatim.py +0 -0
  112. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/content/walkmesh_hotfix.py +0 -0
  113. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/__init__.py +0 -0
  114. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/_regen_provenance.py +0 -0
  115. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.es.patch +0 -0
  116. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.fr.patch +0 -0
  117. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.gr.patch +0 -0
  118. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.it.patch +0 -0
  119. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.jp.patch +0 -0
  120. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.uk.patch +0 -0
  121. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/blank.us.patch +0 -0
  122. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/manifest.json +0 -0
  123. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/provenance/region_template.patch +0 -0
  124. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/reference_arcs.toml +0 -0
  125. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/data/region_catalog.toml +0 -0
  126. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/deploystack.py +0 -0
  127. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/__init__.py +0 -0
  128. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/_exprtable.py +0 -0
  129. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/_membertable.py +0 -0
  130. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/_optables.py +0 -0
  131. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/_regen_optables.py +0 -0
  132. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/cmdasm.py +0 -0
  133. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/disasm.py +0 -0
  134. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/edit.py +0 -0
  135. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/exprasm.py +0 -0
  136. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/model.py +0 -0
  137. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eb/opcodes.py +0 -0
  138. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eblint.py +0 -0
  139. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/__init__.py +0 -0
  140. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/app.py +0 -0
  141. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/battle_forms.py +0 -0
  142. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/breadcrumb.py +0 -0
  143. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/dialogs.py +0 -0
  144. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/feedback.py +0 -0
  145. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/forms.py +0 -0
  146. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/graphview.py +0 -0
  147. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/jobs.py +0 -0
  148. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/model.py +0 -0
  149. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/picker.py +0 -0
  150. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/editor/theme.py +0 -0
  151. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/eventscan.py +0 -0
  152. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/extract.py +0 -0
  153. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/flags.py +0 -0
  154. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/forkreport.py +0 -0
  155. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/hub.py +0 -0
  156. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/idgated.py +0 -0
  157. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/infohub.py +0 -0
  158. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/items.py +0 -0
  159. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/itemstats.py +0 -0
  160. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/journey.py +0 -0
  161. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/keyitems.py +0 -0
  162. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/logic_add.py +0 -0
  163. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/logic_edit.py +0 -0
  164. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/logic_map.py +0 -0
  165. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/pack.py +0 -0
  166. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/playerswap.py +0 -0
  167. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/prop_archetypes.py +0 -0
  168. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/refarc.py +0 -0
  169. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/save.py +0 -0
  170. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/save_items.py +0 -0
  171. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/__init__.py +0 -0
  172. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/arena.py +0 -0
  173. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/bgart.py +0 -0
  174. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/bgi.py +0 -0
  175. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/bgs.py +0 -0
  176. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/bgx.py +0 -0
  177. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/cam.py +0 -0
  178. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/guide.py +0 -0
  179. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/paint.py +0 -0
  180. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/scene/placeholder.py +0 -0
  181. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sjbinary.py +0 -0
  182. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/__init__.py +0 -0
  183. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/author.py +0 -0
  184. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/catalog.py +0 -0
  185. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/codec.py +0 -0
  186. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/edit.py +0 -0
  187. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/lint.py +0 -0
  188. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/render.py +0 -0
  189. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/templates.py +0 -0
  190. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/sps/texture.py +0 -0
  191. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/walkmesh_hotfixes.py +0 -0
  192. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/__init__.py +0 -0
  193. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/battledoc.py +0 -0
  194. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/builddoc.py +0 -0
  195. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/forms_qt.py +0 -0
  196. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/importdoc.py +0 -0
  197. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/mapview.py +0 -0
  198. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/palette.py +0 -0
  199. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/savedoc.py +0 -0
  200. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/shell.py +0 -0
  201. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/style.py +0 -0
  202. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit/workspace/tuningdialog.py +0 -0
  203. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit.egg-info/dependency_links.txt +0 -0
  204. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit.egg-info/entry_points.txt +0 -0
  205. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit.egg-info/requires.txt +0 -0
  206. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/ff9mapkit.egg-info/top_level.txt +0 -0
  207. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/setup.cfg +0 -0
  208. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_abilities.py +0 -0
  209. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_abilityfeatures.py +0 -0
  210. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_actiondelta.py +0 -0
  211. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_ai_phase_insert.py +0 -0
  212. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_ai_phase_insert_adversary.py +0 -0
  213. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_aiauthor.py +0 -0
  214. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_ailint.py +0 -0
  215. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_aipatch.py +0 -0
  216. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_animations.py +0 -0
  217. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_archetypes.py +0 -0
  218. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_areatitle.py +0 -0
  219. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_arming.py +0 -0
  220. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battle.py +0 -0
  221. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battle_bgm.py +0 -0
  222. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battle_forms.py +0 -0
  223. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battle_scene_codec.py +0 -0
  224. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battle_seq.py +0 -0
  225. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battleai.py +0 -0
  226. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battlecsv.py +0 -0
  227. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_battlepatch.py +0 -0
  228. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_bgart.py +0 -0
  229. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_bgs.py +0 -0
  230. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_build.py +0 -0
  231. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_cameras.py +0 -0
  232. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_capstone.py +0 -0
  233. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_carry_text_lint.py +0 -0
  234. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_catalog.py +0 -0
  235. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_chain.py +0 -0
  236. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_characterdelta.py +0 -0
  237. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_choice.py +0 -0
  238. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_cli_entry.py +0 -0
  239. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_cmdasm.py +0 -0
  240. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_cmdasm_relocate.py +0 -0
  241. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_deploy_campaign.py +0 -0
  242. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_deploystack.py +0 -0
  243. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_dialogue.py +0 -0
  244. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_eb.py +0 -0
  245. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_eblint.py +0 -0
  246. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_app.py +0 -0
  247. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_breadcrumb.py +0 -0
  248. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_feedback.py +0 -0
  249. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_forms.py +0 -0
  250. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_integration.py +0 -0
  251. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_jobs.py +0 -0
  252. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_model.py +0 -0
  253. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_editor_theme.py +0 -0
  254. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_entry_settle.py +0 -0
  255. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_eventscan.py +0 -0
  256. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_export.py +0 -0
  257. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_exprasm.py +0 -0
  258. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_extract_area.py +0 -0
  259. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_find_field.py +0 -0
  260. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_forkreport.py +0 -0
  261. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_gateway_advance.py +0 -0
  262. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_graphview.py +0 -0
  263. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_hub_gen.py +0 -0
  264. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_idgated.py +0 -0
  265. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_import_borrow.py +0 -0
  266. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_infohub.py +0 -0
  267. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_itemdata.py +0 -0
  268. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_items.py +0 -0
  269. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_itemstats.py +0 -0
  270. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_itemtext.py +0 -0
  271. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_journey.py +0 -0
  272. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_journey_merge.py +0 -0
  273. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_jump.py +0 -0
  274. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_ladder.py +0 -0
  275. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_lint.py +0 -0
  276. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_logic_add.py +0 -0
  277. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_logic_edit.py +0 -0
  278. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_logic_map.py +0 -0
  279. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_movement.py +0 -0
  280. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_npc_model.py +0 -0
  281. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_npc_verbatim.py +0 -0
  282. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_npcparams.py +0 -0
  283. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_object_graft.py +0 -0
  284. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_occlusion.py +0 -0
  285. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_pack.py +0 -0
  286. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_paint.py +0 -0
  287. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_party.py +0 -0
  288. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_platform.py +0 -0
  289. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_player_graft.py +0 -0
  290. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_playerswap.py +0 -0
  291. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_prop_archetypes.py +0 -0
  292. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_provision.py +0 -0
  293. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_refarc.py +0 -0
  294. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_repaint_native.py +0 -0
  295. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_reskin.py +0 -0
  296. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_save.py +0 -0
  297. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_save_items.py +0 -0
  298. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_savepoint.py +0 -0
  299. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_scene.py +0 -0
  300. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_scenelint.py +0 -0
  301. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_scroll.py +0 -0
  302. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_shared_text_block.py +0 -0
  303. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_shop.py +0 -0
  304. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_showcase.py +0 -0
  305. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_sjbinary.py +0 -0
  306. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_spawn.py +0 -0
  307. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_sps.py +0 -0
  308. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_startstate.py +0 -0
  309. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_startup.py +0 -0
  310. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_synthesis.py +0 -0
  311. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_textcarry.py +0 -0
  312. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_walkmesh_hotfix.py +0 -0
  313. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_workspace_style.py +0 -0
  314. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_world_hub.py +0 -0
  315. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b4}/tests/test_yaw_movement.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ff9mapkit
3
- Version: 1.0.0b3
3
+ Version: 1.0.0b4
4
4
  Summary: Author novel custom field maps for Final Fantasy IX (Memoria engine) from a declarative TOML project file.
5
5
  Author: GameJawnsInc
6
6
  License-Expression: MIT
@@ -15,4 +15,4 @@ Public surface is organized as:
15
15
  ff9mapkit.battle — the battle.toml -> custom battle-background (BBG) builder (fork/edit/build a 3D battle map)
16
16
  """
17
17
 
18
- __version__ = "1.0.0b3" # keep in lockstep with [project] version in pyproject.toml
18
+ __version__ = "1.0.0b4" # keep in lockstep with [project] version in pyproject.toml
@@ -863,6 +863,22 @@ def validate(project: FieldProject) -> list[str]:
863
863
  # on-exit story advance: set_scenario / set_flags fire when the player takes this exit
864
864
  _validate_story_writes(gw, "[[gateway]]", story_names, problems,
865
865
  scenario_key="set_scenario", flags_key="set_flags")
866
+ # forced-ATE warp-in: ate = true flashes the grey banner WARNING before this exit warps (the faithful
867
+ # grey-ATE trigger -- pair with a plain [cutscene] + exit_warp on the destination, which returns you).
868
+ if "ate_mode" in gw and not gw.get("ate"):
869
+ problems.append("[[gateway]] ate_mode is set but ate is not true -- set ate = true to make this "
870
+ "exit a forced-ATE warp (grey banner warning, then warp), or drop ate_mode.")
871
+ if gw.get("ate"):
872
+ m = gw.get("ate_mode", _cutscene.ATE_DEFAULT_MODE)
873
+ if not isinstance(m, int) or isinstance(m, bool) or not (0 <= m <= 255):
874
+ problems.append(f"[[gateway]] ate_mode {m!r} must be an int 0..255 "
875
+ f"(6 = grey unskippable ATE banner; ATE 0xD7 mode).")
876
+ if "ate_title" in gw:
877
+ if not gw.get("ate"):
878
+ problems.append("[[gateway]] ate_title is set but ate is not true -- set ate = true (it's the "
879
+ "title window the forced-ATE warp-in shows), or drop ate_title.")
880
+ elif not isinstance(gw["ate_title"], str) or not gw["ate_title"].strip():
881
+ problems.append("[[gateway]] ate_title must be a non-empty string (the ATE title-window text).")
866
882
  for ev in project.raw.get("event", []):
867
883
  z = ev.get("zone", [])
868
884
  if len(z) not in (4, 5):
@@ -2475,7 +2491,9 @@ def _gateway_on_exit_body(gw: dict, names: dict) -> bytes:
2475
2491
  """The story-state advance a ``[[gateway]]`` applies when the player TAKES this exit: the raw
2476
2492
  ``set_var`` bytes from ``set_scenario`` (ScenarioCounter) + ``set_flags`` (gEventGlobal bits), built
2477
2493
  with the shared :func:`ff9mapkit.content.startup.startup_body`. ``b""`` when the gateway has neither
2478
- (so the build is byte-identical to a gateway without on-exit writes)."""
2494
+ (so the build is byte-identical to a gateway without on-exit writes). (NB ``ate = true`` is NOT handled
2495
+ here -- a forced-ATE warp-in routes to :func:`cutscene.inject_forced_ate` instead, because the banner +
2496
+ timed warp must run in a RunScript'd player func, not inline in the region, or the region's Wait freezes.)"""
2479
2497
  sc = gw.get("set_scenario")
2480
2498
  if isinstance(sc, str):
2481
2499
  sc = _flags.resolve_scenario(sc)
@@ -3251,6 +3269,61 @@ def _inject_verbatim_npcs(project: FieldProject, eb: bytes, npc_txids: dict, *,
3251
3269
  return eb, npc_slots
3252
3270
 
3253
3271
 
3272
+ def _gen_conductor_walk_tags(project: FieldProject, eb: bytes, steps, npc_slots):
3273
+ """Shared by the synth + verbatim conductor wiring: add the per-actor choreography tags a conductor's
3274
+ ``walk`` beats need. For each ``walk`` step, add a walk tag (20+) to the actor's entry (the conductor
3275
+ ``RunScript``s into it -- base ``Walk`` acts on the executing object, so the walk must run in the actor's
3276
+ own context). For each actor that walks inside a PARALLEL group (``with_prev``), also add ONE bare-RETURN
3277
+ join tag -- the conductor async-forks the walk then ``RunScriptSync``s the join tag to block until the
3278
+ walk frees the actor's script level. ``add_function`` grows an entry but keeps slot INDICES stable, so the
3279
+ conductor's by-uid refs (and any later band insert) stay valid. Returns ``(eb, walk_calls, join_tags)``.
3280
+
3281
+ The PLAYER walks too: ``"player"`` -> the tag goes on the player's OWN entry (``DefinePlayerCharacter``,
3282
+ located by :func:`ff9mapkit.content.player.find_player_entry`), but the conductor addresses it by the
3283
+ control-char sentinel uid 250 (``GetObjUID(250)`` -> the control character) -- so its add-target entry and
3284
+ its conductor uid differ (an NPC's are the same, slot == uid). Same recipe the ladder uses for the player."""
3285
+ walk_calls, join_tags = {}, {}
3286
+ if not any("walk" in s for s in steps):
3287
+ return eb, walk_calls, join_tags
3288
+ from .eb import edit as _eb_edit
3289
+ from .content import player as _player
3290
+ move_reg = _position_registry(project)
3291
+ # the player entry index is stable across add_function (slot indices don't move), so resolve it once
3292
+ player_entry = (_player.find_player_entry(EbScript.from_bytes(eb))
3293
+ if any(s.get("actor") == "player" and "walk" in s for s in steps) else None)
3294
+
3295
+ def _walk_target(actor):
3296
+ """(entry index to add the tag to, uid the conductor addresses). Player: its own entry, uid 250."""
3297
+ if actor == "player":
3298
+ return player_entry, _conductor.PLAYER_UID
3299
+ slot = npc_slots.get(actor) # an NPC: entry index == runtime uid
3300
+ return slot, slot
3301
+
3302
+ next_tag = {} # actor name -> next free walk tag
3303
+ for i, s in enumerate(steps):
3304
+ if "walk" not in s:
3305
+ continue
3306
+ actor = s.get("actor")
3307
+ entry_idx, uid = _walk_target(actor)
3308
+ pt = _resolve_point(s["walk"], move_reg)
3309
+ t = next_tag.get(actor, _conductor.WALK_TAG_BASE)
3310
+ next_tag[actor] = t + 1
3311
+ eb = _eb_edit.add_function(eb, entry_idx, t, _conductor.walk_tag_body(pt[0], pt[1], s.get("speed")))
3312
+ walk_calls[i] = (uid, t)
3313
+ # actors that walk inside a parallel group (>1 member) need a join tag (async-fork + sync-drain)
3314
+ parallel_actors = set()
3315
+ for g in _conductor.group_parallel(steps):
3316
+ if len(g) > 1:
3317
+ parallel_actors |= {s.get("actor") for _i, s in g if "walk" in s and s.get("actor")}
3318
+ for actor in sorted(parallel_actors):
3319
+ entry_idx, uid = _walk_target(actor)
3320
+ if entry_idx is None:
3321
+ continue
3322
+ eb = _eb_edit.add_function(eb, entry_idx, _conductor.PARALLEL_JOIN_TAG, _conductor.join_tag_body())
3323
+ join_tags[uid] = _conductor.PARALLEL_JOIN_TAG
3324
+ return eb, walk_calls, join_tags
3325
+
3326
+
3254
3327
  def _inject_verbatim_conductor(project: FieldProject, eb: bytes, npc_slots: dict, cutscene_txids, *, warnings) -> bytes:
3255
3328
  """Inject a MULTI-ACTOR ``[cutscene]`` conductor into a VERBATIM fork: ONE director code entry, seated
3256
3329
  BELOW the donor's party band (so the 9 characters stay the top slots), that drives the additive ``[[npc]]``
@@ -3263,22 +3336,8 @@ def _inject_verbatim_conductor(project: FieldProject, eb: bytes, npc_slots: dict
3263
3336
  return eb
3264
3337
  steps = _resolve_conductor_steps(cs["steps"], project)
3265
3338
  # walk beats -> a walk-choreography tag on the actor's below-band entry, RunScript'd by the conductor (the
3266
- # synth path's mechanism; here the actor entries sit below the band). add_function grows the entry but slot
3267
- # INDICES are stable, so the conductor's by-uid refs (and the later band insert) stay valid.
3268
- walk_calls = {}
3269
- if any("walk" in s for s in steps):
3270
- from .eb import edit as _eb_edit
3271
- move_reg = _position_registry(project)
3272
- next_tag = {}
3273
- for i, s in enumerate(steps):
3274
- if "walk" not in s:
3275
- continue
3276
- slot = npc_slots.get(s.get("actor")) # validated to be a real NPC actor (not player)
3277
- pt = _resolve_point(s["walk"], move_reg)
3278
- t = next_tag.get(s["actor"], _conductor.WALK_TAG_BASE)
3279
- next_tag[s["actor"]] = t + 1
3280
- eb = _eb_edit.add_function(eb, slot, t, _conductor.walk_tag_body(pt[0], pt[1], s.get("speed")))
3281
- walk_calls[i] = (slot, t)
3339
+ # synth path's mechanism; here the actor entries sit below the band); a parallel walk also gets a join tag.
3340
+ eb, walk_calls, join_tags = _gen_conductor_walk_tags(project, eb, steps, npc_slots)
3282
3341
  auto = _FlagAlloc(getattr(project, "flag_base", None)) # campaign-safe once-flag (matches the other verbatim blocks)
3283
3342
  c_fclass, c_fidx = _cutscene.once_flag_for(cs)
3284
3343
  if auto.base is not None and "flag" not in cs and cs.get("once", True):
@@ -3289,7 +3348,7 @@ def _inject_verbatim_conductor(project: FieldProject, eb: bytes, npc_slots: dict
3289
3348
  warmup=int(cs.get("warmup", _cutscene.DEFAULT_WARMUP)),
3290
3349
  owns_control=bool(cs.get("owns_control", True)),
3291
3350
  exit_warp=(int(cs["exit_warp"]) if cs.get("exit_warp") else None),
3292
- walk_calls=walk_calls, reserve_party_band=True)
3351
+ walk_calls=walk_calls, join_tags=join_tags, reserve_party_band=True)
3293
3352
 
3294
3353
 
3295
3354
  def _inject_verbatim_props(project: FieldProject, eb: bytes, prop_txids=None, *, warnings) -> bytes:
@@ -3351,6 +3410,11 @@ def _inject_verbatim_gateways(project: FieldProject, eb: bytes, *, warnings) ->
3351
3410
  zone = gw["zone"]
3352
3411
  if len(zone) == 4:
3353
3412
  zone = _gw.quad_zone(zone)
3413
+ if gw.get("ate"): # a forced-ATE warp-in, seated below the party band
3414
+ eb = _cutscene.inject_forced_ate(eb, [tuple(p) for p in zone], int(gw["to"]),
3415
+ mode=int(gw.get("ate_mode", _cutscene.ATE_DEFAULT_MODE)),
3416
+ reserve_party_band=True)
3417
+ continue
3354
3418
  gf, gs = _gate_of(gw)
3355
3419
  eb = _gw.inject_gateway(eb, int(gw["to"]), entrance=int(gw.get("entrance", 0)),
3356
3420
  zone=[tuple(p) for p in zone], gate_flag=gf, gate_require_set=gs,
@@ -3391,12 +3455,14 @@ def build_script(project: FieldProject, lang: str, dialogue_txids: dict,
3391
3455
  control_value: int = -1, event_txids: dict | None = None,
3392
3456
  cutscene_txids: list | None = None, walkmesh=None,
3393
3457
  choice_txids: dict | None = None, on_entry_txids: dict | None = None,
3394
- ate_txids: dict | None = None, chest_txids: dict | None = None) -> bytes:
3458
+ ate_txids: dict | None = None, chest_txids: dict | None = None,
3459
+ gateway_txids: dict | None = None) -> bytes:
3395
3460
  """Build one language's .eb by applying the project's content to the blank field."""
3396
3461
  _auto = _FlagAlloc(getattr(project, "flag_base", None))
3397
3462
  event_txids = event_txids or {}
3398
3463
  cutscene_txids = cutscene_txids or []
3399
3464
  choice_txids = choice_txids or {}
3465
+ gateway_txids = gateway_txids or {}
3400
3466
  on_entry_txids = on_entry_txids or {}
3401
3467
  ate_txids = ate_txids or {}
3402
3468
  chest_txids = chest_txids or {}
@@ -3557,10 +3623,15 @@ def build_script(project: FieldProject, lang: str, dialogue_txids: dict,
3557
3623
 
3558
3624
  # gateways
3559
3625
  gw_names = _story_names(project) # [[flag]] name -> index, for set_flags resolution
3560
- for gw in project.raw.get("gateway", []):
3626
+ for gi, gw in enumerate(project.raw.get("gateway", [])):
3561
3627
  zone = gw["zone"]
3562
3628
  if len(zone) == 4:
3563
3629
  zone = _gw.quad_zone(zone)
3630
+ if gw.get("ate"): # a forced-ATE warp-in: grey banner WARNING + title, then warp
3631
+ eb = _cutscene.inject_forced_ate(eb, [tuple(p) for p in zone], int(gw["to"]),
3632
+ mode=int(gw.get("ate_mode", _cutscene.ATE_DEFAULT_MODE)),
3633
+ title_txid=gateway_txids.get(gi))
3634
+ continue
3564
3635
  gf, gs = _gate_of(gw)
3565
3636
  eb = _gw.inject_gateway(eb, int(gw["to"]), entrance=int(gw.get("entrance", 0)),
3566
3637
  zone=[tuple(p) for p in zone], gate_flag=gf, gate_require_set=gs,
@@ -3705,29 +3776,16 @@ def build_script(project: FieldProject, lang: str, dialogue_txids: dict,
3705
3776
  if _auto.base is not None and "flag" not in cs and cs.get("once", True):
3706
3777
  c_fidx = _auto.cutscene() # campaign: pack into this member's flag block
3707
3778
  # walk beats can't run inline (base Walk acts on the EXECUTING object; there's no targeted WalkEx),
3708
- # so generate a per-actor walk-choreography TAG on the actor's own [[npc]] entry and RunScript into
3709
- # it (animates in the actor's context, blocks until arrival). walk_calls: step index -> (uid, tag).
3710
- walk_calls = {}
3711
- if any("walk" in s for s in c_steps):
3712
- from .eb import edit as _eb_edit
3713
- move_reg = _position_registry(project)
3714
- next_tag = {} # actor name -> next free walk tag
3715
- for i, s in enumerate(c_steps):
3716
- if "walk" not in s:
3717
- continue
3718
- slot = npc_slots.get(s.get("actor")) # validated to be a real NPC actor (not player)
3719
- pt = _resolve_point(s["walk"], move_reg)
3720
- t = next_tag.get(s["actor"], _conductor.WALK_TAG_BASE)
3721
- next_tag[s["actor"]] = t + 1
3722
- eb = _eb_edit.add_function(eb, slot, t, _conductor.walk_tag_body(pt[0], pt[1], s.get("speed")))
3723
- walk_calls[i] = (slot, t)
3779
+ # so generate a per-actor walk-choreography TAG on the actor's own [[npc]] entry and RunScript into it
3780
+ # (animates in the actor's context, blocks until arrival); a parallel walk also gets a join tag.
3781
+ eb, walk_calls, join_tags = _gen_conductor_walk_tags(project, eb, c_steps, npc_slots)
3724
3782
  eb = _conductor.inject_conductor(
3725
3783
  eb, c_steps, npc_slots, cutscene_txids,
3726
3784
  once_flag=(c_fidx if cs.get("once", True) else None), flag_class=c_fclass,
3727
3785
  warmup=int(cs.get("warmup", _cutscene.DEFAULT_WARMUP)),
3728
3786
  owns_control=bool(cs.get("owns_control", True)),
3729
3787
  exit_warp=(int(cs["exit_warp"]) if cs.get("exit_warp") else None),
3730
- say_flags=cs_say_flags, walk_calls=walk_calls)
3788
+ say_flags=cs_say_flags, walk_calls=walk_calls, join_tags=join_tags)
3731
3789
 
3732
3790
  # cutscene (narration, no actor): an ordered, control-locked sequence on entry (once), run as a
3733
3791
  # standalone director code entry. Steps = say / wait / set_flag. An ACTOR cutscene was already
@@ -4133,17 +4191,40 @@ def _validate_conductor(project, cs, problems):
4133
4191
  _animations.resolve(token, a)
4134
4192
  except ValueError as e:
4135
4193
  problems.append(f"[cutscene] step {k}: {e}")
4136
- if act == "walk":
4137
- if who == "player": # player-walk needs a tag on the player entry -- deferred
4138
- problems.append(f"[cutscene] step {k}: walk is not yet supported for \"player\" (only [[npc]] "
4139
- f"actors can walk in a cutscene for now); use turn/anim/say on the player.")
4140
- try:
4194
+ if act == "walk": # walk on "player" runs in the player's own
4195
+ try: # entry (uid 250); an [[npc]] in its slot entry
4141
4196
  _resolve_point(s["walk"], move_reg) # [x, z] or a known marker/NPC name
4142
4197
  except ValueError as e:
4143
4198
  problems.append(f"[cutscene] step {k}: {e}")
4144
4199
  t = s.get("tail")
4145
4200
  if t is not None and t not in _text.TAIL_CODES:
4146
4201
  problems.append(f"[cutscene] step {k} tail {t!r} is not a valid TAIL code")
4202
+ # parallel beats (with_prev): a step marked with_prev runs together with the preceding beat. Only
4203
+ # walk/anim/turn can run in parallel (say/wait/set_flag are sequential barriers), the group leader
4204
+ # must be one of those too, and no actor may act twice in a group (it has one execution context).
4205
+ if steps[0].get("with_prev"):
4206
+ problems.append("[cutscene] step 0 can't have with_prev = true (nothing precedes it)")
4207
+ _par_ok = ("walk", "anim", "turn")
4208
+ for g in _conductor.group_parallel(steps):
4209
+ if len(g) < 2:
4210
+ continue
4211
+ lead_k, lead_s = g[0]
4212
+ lead_act = next((key for key in _par_ok if key in lead_s), None)
4213
+ if lead_act is None:
4214
+ problems.append(f"[cutscene] step {lead_k} has a with_prev beat after it but is a "
4215
+ f"say/wait/set_flag (a sequential barrier) -- only walk/anim/turn run in parallel")
4216
+ seen = {lead_s.get("actor")} if lead_act else set()
4217
+ for k, s in g[1:]:
4218
+ act = next((key for key in _par_ok if key in s), None)
4219
+ if act is None:
4220
+ problems.append(f"[cutscene] step {k}: only walk/anim/turn can run with_prev "
4221
+ f"(say/wait/set_flag are sequential barriers)")
4222
+ continue
4223
+ who = s.get("actor")
4224
+ if who in seen:
4225
+ problems.append(f"[cutscene] step {k}: actor {who!r} already acts in this parallel group "
4226
+ f"(an actor can't do two things at once)")
4227
+ seen.add(who)
4147
4228
  if "exit_warp" in cs:
4148
4229
  ew = cs["exit_warp"]
4149
4230
  if not (isinstance(ew, int) and not isinstance(ew, bool) and ew > 0):
@@ -4432,8 +4513,9 @@ def _wrap_width(project: FieldProject):
4432
4513
 
4433
4514
  def collect_text(project: FieldProject):
4434
4515
  """Return (mes_body, npc_txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids,
4435
- chest_txids). All field text (NPC dialogue, event messages, cutscene 'say' lines, choice prompts +
4436
- replies, on-entry messages, the ATE menu, chest "Received X" boxes) shares one .mes block, in that order
4516
+ chest_txids, gateway_txids). All field text (NPC dialogue, event messages, cutscene 'say' lines, choice
4517
+ prompts + replies, on-entry messages, the ATE menu, chest "Received X" boxes, forced-ATE gateway titles)
4518
+ shares one .mes block, in that order
4437
4519
  (so a field with no events/cutscene/choices/on_entry/ate/chests is byte-identical to the old layout).
4438
4520
  ``cutscene_txids`` is a list (one per 'say' step); ``choice_txids[c]`` = ``{"prompt": id, "replies":
4439
4521
  {opt_index: id}}``; ``on_entry_txids[k]`` = the txid of hook ``k``'s message (only for hooks that have
@@ -4521,8 +4603,20 @@ def collect_text(project: FieldProject):
4521
4603
  for k, ch in enumerate(project.raw.get("chest", [])):
4522
4604
  text, strt, tail = _chest_received_box(ch)
4523
4605
  ch_pos[k] = _add_raw(text, ch.get("tail") or tail, strt=strt)
4606
+ # forced-ATE gateway titles: the winATE-captioned, CENTERED "title window" a `[[gateway]] ate = true` shows
4607
+ # as it warps. Geometry verified vs real grey ATEs 956/2211: `[STRT=W,1][IMME][CENT=W]title` -- the engine
4608
+ # auto-centers a system window from its [STRT] width (like the chest box) + [CENT] centers the text + [IMME]
4609
+ # pops it fully drawn. W ~ the rendered text width (real STRT ~= text.measure * 7.2). The default dialogue
4610
+ # geometry (10,1)+TAIL=UPR pins it TOP-RIGHT (the reported bug). Added LAST (after chests) -> byte-identical
4611
+ # for a field without one.
4612
+ gw_pos = {}
4613
+ for gi, gw in enumerate(project.raw.get("gateway", [])):
4614
+ if gw.get("ate") and gw.get("ate_title"):
4615
+ _title = str(gw["ate_title"])
4616
+ _w = max(8, round(_text.measure(_title) * 7.2)) # ~ the FF9 STRT width of the rendered title
4617
+ gw_pos[gi] = _add_raw(f"[IMME][CENT={_w}]{_title}", "", strt=(_w, 1)) # NO tail -> the real ATE's true centre
4524
4618
  if not lines:
4525
- return "", {}, {}, [], {}, {}, {}, {}
4619
+ return "", {}, {}, [], {}, {}, {}, {}, {}
4526
4620
  body, mapping = _text.build_mes(lines, start_txid=_text.DEFAULT_BASE_TXID, tails=tails, strts=strts)
4527
4621
  npc_txids = {i: mapping[p] for i, p in npc_pos.items()}
4528
4622
  event_txids = {j: mapping[p] for j, p in ev_pos.items()}
@@ -4536,7 +4630,9 @@ def collect_text(project: FieldProject):
4536
4630
  "replies": {oi: mapping[p] for oi, p in ate_reply_pos.items()}}
4537
4631
  if ate_prompt_pos is not None else {})
4538
4632
  chest_txids = {k: mapping[p] for k, p in ch_pos.items()}
4539
- return body, npc_txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids, chest_txids
4633
+ gateway_txids = {gi: mapping[p] for gi, p in gw_pos.items()} # forced-ATE title-window txids (by gw index)
4634
+ return (body, npc_txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids,
4635
+ chest_txids, gateway_txids)
4540
4636
 
4541
4637
 
4542
4638
  # --------------------------------------------------------------------------- the build
@@ -4717,7 +4813,7 @@ def build_field(project: FieldProject, layout: ModLayout, *, langs=LANGS) -> Fie
4717
4813
 
4718
4814
  _autofill_ladder_landing_y(project, cutscene_wmesh) # elevated dismount floors get their real Y
4719
4815
  # --- dialogue + per-language script ---
4720
- mes_body, txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids, chest_txids = collect_text(project)
4816
+ mes_body, txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids, chest_txids, gateway_txids = collect_text(project)
4721
4817
  control_value = resolve_control_value(project, camera)
4722
4818
  # faithful text carry: the donor's referenced dialogue, shipped VERBATIM per language and APPENDED after
4723
4819
  # the authored block (its own [TXID=>=1000] re-index keeps it disjoint -- authored text + the hut golden
@@ -4923,7 +5019,7 @@ def build_field(project: FieldProject, layout: ModLayout, *, langs=LANGS) -> Fie
4923
5019
  eb = build_script(project, lang, txids, control_value, event_txids=event_txids,
4924
5020
  cutscene_txids=cutscene_txids, walkmesh=cutscene_wmesh,
4925
5021
  choice_txids=choice_txids, on_entry_txids=on_entry_txids,
4926
- ate_txids=ate_txids, chest_txids=chest_txids)
5022
+ ate_txids=ate_txids, chest_txids=chest_txids, gateway_txids=gateway_txids)
4927
5023
  base = mes_body or ""
4928
5024
  inplace = base
4929
5025
  suffix = ""
@@ -30,6 +30,7 @@ Subcommands are wired up incrementally as the library lands:
30
30
  models/scenes/catalog - the Info Hub: browse models (+ their animations), battle scenes, or
31
31
  search every reference catalog by name
32
32
  extract-templates - regenerate base assets from the user's own FF9 install (no game data shipped)
33
+ setup - one-shot: find the FF9 install, remember it, extract base assets, report Memoria status
33
34
 
34
35
  Anything not yet implemented prints a clear "coming in Phase N" message rather than failing
35
36
  with an import error, so the installed console script is always runnable.
@@ -104,6 +105,80 @@ def _cmd_extract_templates(args: argparse.Namespace) -> int:
104
105
  return 0
105
106
 
106
107
 
108
+ def _cmd_setup(args: argparse.Namespace) -> int:
109
+ """One-shot post-install setup: find the FF9 install, remember it in the user config, regenerate the
110
+ base assets, and report the Memoria engine status. ``--install-engine <zip>`` additionally installs the
111
+ Dream World IX engine bundle (backed up first). Safe to re-run; returns non-zero only so a calling
112
+ script (e.g. the installer) can tell -- it never needs to block."""
113
+ from . import memoria, provision
114
+ from .config import save_game_path
115
+
116
+ # 1) resolve the install, and remember it so future commands need no --game/$FF9_GAME_PATH
117
+ try:
118
+ game = find_game_path(args.game)
119
+ except ConfigError as e:
120
+ print(str(e), file=sys.stderr)
121
+ print("\nOnce you know the path, run: ff9mapkit setup --game \"<path>\"", file=sys.stderr)
122
+ return 2
123
+ print(f"Found FINAL FANTASY IX: {game}")
124
+ try:
125
+ cfg = save_game_path(game)
126
+ print(f" remembered in {cfg} (future commands won't need --game)")
127
+ except OSError as e: # noqa: BLE001 - config write is best-effort
128
+ print(f" (couldn't save the config: {e})", file=sys.stderr)
129
+
130
+ rc = 0
131
+ # 2) regenerate the base assets (the one-time bring-your-own-install step; reads YOUR install)
132
+ if not args.no_extract:
133
+ if provision.templates_present() and not args.force:
134
+ print("Base assets: already extracted (use --force to redo).")
135
+ elif not _has_unitypy():
136
+ print("Base assets: SKIPPED -- UnityPy isn't installed (it reads FF9's assetbundles).\n"
137
+ " Install it: pip install UnityPy (or reinstall ff9mapkit with the [assets] extra)",
138
+ file=sys.stderr)
139
+ rc = 1
140
+ else:
141
+ print("Base assets: regenerating from your install (ff9mapkit ships no game data)...")
142
+ try:
143
+ rep = provision.extract_templates(game=str(game), fixtures=not args.no_fixtures, verbose=True)
144
+ print(f" OK -- {len(rep['verified'])} assets regenerated + verified.")
145
+ except Exception as e: # noqa: BLE001
146
+ print(f" extract-templates failed: {e}", file=sys.stderr)
147
+ rc = 1
148
+
149
+ # 3) Memoria engine status (forked fields need it; novel/from-scratch fields run on stock Memoria)
150
+ st = memoria.memoria_status(game)
151
+ if st["installed"]:
152
+ print("Memoria engine: detected -- forked real fields will work.")
153
+ else:
154
+ print("Memoria engine: NOT detected.")
155
+ print(" From-scratch / BG-borrow fields run on stock Memoria. To play FORKED real fields, install\n"
156
+ " Memoria + the Dream World IX engine bundle (dwix-custom-memoria-*.zip) -- see docs/ENGINE.md,\n"
157
+ " or re-run: ff9mapkit setup --install-engine <bundle.zip>")
158
+
159
+ # 4) opt-in: install the engine bundle (backed up; never touches Memoria.ini)
160
+ if args.install_engine:
161
+ zp = Path(args.install_engine)
162
+ if not zp.is_file():
163
+ print(f"--install-engine: file not found: {zp}", file=sys.stderr)
164
+ return 1
165
+ import datetime # noqa: PLC0415
166
+ stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
167
+ print(f"Engine bundle: installing {zp.name} (backing up the originals first)...")
168
+ try:
169
+ rep = memoria.install_engine_bundle(game, zp, stamp=stamp)
170
+ print(f" backed up {len(rep['backed_up'])} DLL(s) -> {rep['backup_root']}")
171
+ print(f" installed {len(rep['installed'])} DLL(s) into x64 + x86 Managed.")
172
+ print(" Relaunch FF9 to load it. (Undo: copy the backup DLLs back over the Managed ones.)")
173
+ except Exception as e: # noqa: BLE001
174
+ print(f" engine install failed: {e}", file=sys.stderr)
175
+ return 1
176
+
177
+ if rc == 0:
178
+ print("\nSetup complete. Try 'ff9mapkit doctor', or open the GUI with 'ff9mapkit-workspace'.")
179
+ return rc
180
+
181
+
107
182
  def _cmd_disasm(args: argparse.Namespace) -> int:
108
183
  from .eb import EbScript
109
184
 
@@ -3101,6 +3176,17 @@ def build_parser() -> argparse.ArgumentParser:
3101
3176
  xt.add_argument("--no-fixtures", action="store_true", help="skip the test fixtures (templates only)")
3102
3177
  xt.set_defaults(func=_cmd_extract_templates)
3103
3178
 
3179
+ su = sub.add_parser("setup",
3180
+ help="one-shot post-install setup: find your FF9 install, remember it, extract base "
3181
+ "assets, report Memoria status (--install-engine ZIP to install the engine bundle)")
3182
+ su.add_argument("--install-engine", metavar="ZIP", default=None,
3183
+ help="also install the Dream World IX engine bundle (dwix-custom-memoria-*.zip) -- backs "
3184
+ "up the originals; needed only to play FORKED real fields")
3185
+ su.add_argument("--force", action="store_true", help="re-extract the base assets even if already present")
3186
+ su.add_argument("--no-extract", action="store_true", help="skip the base-asset extraction step")
3187
+ su.add_argument("--no-fixtures", action="store_true", help="skip test fixtures during extraction")
3188
+ su.set_defaults(func=_cmd_setup)
3189
+
3104
3190
  return p
3105
3191
 
3106
3192
 
@@ -19,6 +19,7 @@ offline (build into a temp dir, diff against the deployed assets).
19
19
  from __future__ import annotations
20
20
 
21
21
  import os
22
+ import re
22
23
  from dataclasses import dataclass
23
24
  from pathlib import Path
24
25
 
@@ -60,6 +61,25 @@ def _read_user_config() -> dict:
60
61
  return {}
61
62
 
62
63
 
64
+ def save_game_path(game_path: str | os.PathLike) -> Path:
65
+ """Persist ``game_path`` into the user config (``~/.ff9mapkit.toml``) so later commands resolve the
66
+ install without ``--game`` / ``$FF9_GAME_PATH``. Surgical: rewrites only the ``game_path`` line and
67
+ preserves any other keys/comments. Stores the path with forward slashes so the TOML basic string can
68
+ never trip a backslash escape on Windows (``Path`` reads forward slashes fine). Returns the config path.
69
+ """
70
+ p = str(Path(game_path).resolve()).replace("\\", "/")
71
+ line = f'game_path = "{p}"'
72
+ existing = USER_CONFIG.read_text(encoding="utf-8") if USER_CONFIG.is_file() else ""
73
+ if re.search(r"(?m)^[ \t]*game_path[ \t]*=", existing):
74
+ new = re.sub(r"(?m)^[ \t]*game_path[ \t]*=.*$", line, existing)
75
+ elif existing.strip():
76
+ new = existing.rstrip("\n") + "\n" + line + "\n"
77
+ else:
78
+ new = line + "\n"
79
+ USER_CONFIG.write_text(new, encoding="utf-8")
80
+ return USER_CONFIG
81
+
82
+
63
83
  def find_game_path(explicit: str | os.PathLike | None = None) -> Path:
64
84
  """Resolve the Final Fantasy IX install folder.
65
85