ff9mapkit 1.0.0b3__tar.gz → 1.0.0b5__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.0b5}/PKG-INFO +1 -1
  2. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/__init__.py +1 -1
  3. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/build.py +158 -51
  4. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/cli.py +86 -0
  5. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/config.py +140 -16
  6. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/conductor.py +137 -51
  7. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/cutscene.py +70 -0
  8. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/text.py +7 -3
  9. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/dialogue.py +1 -1
  10. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/extract.py +15 -5
  11. ff9mapkit-1.0.0b5/ff9mapkit/memoria.py +91 -0
  12. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/provision.py +29 -1
  13. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit.egg-info/PKG-INFO +1 -1
  14. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit.egg-info/SOURCES.txt +2 -0
  15. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/pyproject.toml +1 -1
  16. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_ate.py +124 -0
  17. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_campaign.py +1 -1
  18. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_content.py +122 -13
  19. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_flags.py +1 -1
  20. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_on_entry.py +2 -2
  21. ff9mapkit-1.0.0b5/tests/test_setup.py +175 -0
  22. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_text.py +4 -4
  23. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_verbatim.py +122 -0
  24. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/LICENSE +0 -0
  25. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/README.md +0 -0
  26. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/__main__.py +0 -0
  27. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_animdb.py +0 -0
  28. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_animdb_all.py +0 -0
  29. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_fieldtable.py +0 -0
  30. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_fieldtext.py +0 -0
  31. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_held_poses.py +0 -0
  32. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_itemdb.py +0 -0
  33. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_modeldb.py +0 -0
  34. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_narrowmap_data.py +0 -0
  35. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_npcparams.py +0 -0
  36. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_animdb.py +0 -0
  37. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_animdb_all.py +0 -0
  38. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_fieldtable.py +0 -0
  39. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_fieldtext.py +0 -0
  40. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_modeldb.py +0 -0
  41. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_npcparams.py +0 -0
  42. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_regen_scenedb.py +0 -0
  43. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/_scenedb.py +0 -0
  44. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/abilities.py +0 -0
  45. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/animations.py +0 -0
  46. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/archetypes.py +0 -0
  47. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/areatitle.py +0 -0
  48. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/__init__.py +0 -0
  49. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/abilityfeatures.py +0 -0
  50. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/actiondelta.py +0 -0
  51. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/aiauthor.py +0 -0
  52. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/ailint.py +0 -0
  53. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/aipatch.py +0 -0
  54. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/battleai.py +0 -0
  55. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/battlecsv.py +0 -0
  56. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/battlepatch.py +0 -0
  57. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/build.py +0 -0
  58. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/camera_codec.py +0 -0
  59. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/camera_data.py +0 -0
  60. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/characterdelta.py +0 -0
  61. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/event_data.py +0 -0
  62. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/extract.py +0 -0
  63. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/fbx.py +0 -0
  64. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/reskin.py +0 -0
  65. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/scene_codec.py +0 -0
  66. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/scene_data.py +0 -0
  67. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/scenelint.py +0 -0
  68. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/seqasm.py +0 -0
  69. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/seqauthor.py +0 -0
  70. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/seqcodec.py +0 -0
  71. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/seqdis.py +0 -0
  72. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle/seqpatch.py +0 -0
  73. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/battle_bgm.py +0 -0
  74. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/binutils.py +0 -0
  75. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/campaign.py +0 -0
  76. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/catalog.py +0 -0
  77. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/chain.py +0 -0
  78. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/__init__.py +0 -0
  79. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/areatitle.py +0 -0
  80. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/ate.py +0 -0
  81. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/camera.py +0 -0
  82. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/chest.py +0 -0
  83. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/choice.py +0 -0
  84. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/encounter.py +0 -0
  85. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/entry_settle.py +0 -0
  86. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/equipment.py +0 -0
  87. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/event.py +0 -0
  88. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/gateway.py +0 -0
  89. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/inventory.py +0 -0
  90. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/itemdata.py +0 -0
  91. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/itemtext.py +0 -0
  92. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/jump.py +0 -0
  93. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/ladder.py +0 -0
  94. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/movement.py +0 -0
  95. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/music.py +0 -0
  96. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/npc.py +0 -0
  97. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/object.py +0 -0
  98. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/onentry.py +0 -0
  99. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/party.py +0 -0
  100. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/pathfind.py +0 -0
  101. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/platform.py +0 -0
  102. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/player.py +0 -0
  103. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/prop.py +0 -0
  104. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/region.py +0 -0
  105. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/reinit.py +0 -0
  106. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/savepoint.py +0 -0
  107. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/shop.py +0 -0
  108. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/sps_trigger.py +0 -0
  109. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/startup.py +0 -0
  110. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/synthesis.py +0 -0
  111. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/textcarry.py +0 -0
  112. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/verbatim.py +0 -0
  113. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/content/walkmesh_hotfix.py +0 -0
  114. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/__init__.py +0 -0
  115. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/_regen_provenance.py +0 -0
  116. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.es.patch +0 -0
  117. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.fr.patch +0 -0
  118. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.gr.patch +0 -0
  119. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.it.patch +0 -0
  120. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.jp.patch +0 -0
  121. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.uk.patch +0 -0
  122. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/blank.us.patch +0 -0
  123. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/manifest.json +0 -0
  124. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/provenance/region_template.patch +0 -0
  125. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/reference_arcs.toml +0 -0
  126. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/data/region_catalog.toml +0 -0
  127. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/deploystack.py +0 -0
  128. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/__init__.py +0 -0
  129. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/_exprtable.py +0 -0
  130. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/_membertable.py +0 -0
  131. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/_optables.py +0 -0
  132. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/_regen_optables.py +0 -0
  133. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/cmdasm.py +0 -0
  134. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/disasm.py +0 -0
  135. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/edit.py +0 -0
  136. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/exprasm.py +0 -0
  137. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/model.py +0 -0
  138. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eb/opcodes.py +0 -0
  139. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eblint.py +0 -0
  140. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/__init__.py +0 -0
  141. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/app.py +0 -0
  142. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/battle_forms.py +0 -0
  143. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/breadcrumb.py +0 -0
  144. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/dialogs.py +0 -0
  145. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/feedback.py +0 -0
  146. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/forms.py +0 -0
  147. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/graphview.py +0 -0
  148. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/jobs.py +0 -0
  149. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/model.py +0 -0
  150. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/picker.py +0 -0
  151. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/editor/theme.py +0 -0
  152. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/eventscan.py +0 -0
  153. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/flags.py +0 -0
  154. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/forkreport.py +0 -0
  155. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/hub.py +0 -0
  156. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/idgated.py +0 -0
  157. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/infohub.py +0 -0
  158. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/items.py +0 -0
  159. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/itemstats.py +0 -0
  160. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/journey.py +0 -0
  161. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/keyitems.py +0 -0
  162. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/logic_add.py +0 -0
  163. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/logic_edit.py +0 -0
  164. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/logic_map.py +0 -0
  165. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/pack.py +0 -0
  166. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/playerswap.py +0 -0
  167. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/prop_archetypes.py +0 -0
  168. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/refarc.py +0 -0
  169. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/save.py +0 -0
  170. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/save_items.py +0 -0
  171. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/__init__.py +0 -0
  172. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/arena.py +0 -0
  173. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/bgart.py +0 -0
  174. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/bgi.py +0 -0
  175. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/bgs.py +0 -0
  176. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/bgx.py +0 -0
  177. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/cam.py +0 -0
  178. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/guide.py +0 -0
  179. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/paint.py +0 -0
  180. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/scene/placeholder.py +0 -0
  181. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sjbinary.py +0 -0
  182. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/__init__.py +0 -0
  183. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/author.py +0 -0
  184. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/catalog.py +0 -0
  185. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/codec.py +0 -0
  186. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/edit.py +0 -0
  187. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/lint.py +0 -0
  188. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/render.py +0 -0
  189. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/templates.py +0 -0
  190. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/sps/texture.py +0 -0
  191. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/walkmesh_hotfixes.py +0 -0
  192. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/__init__.py +0 -0
  193. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/battledoc.py +0 -0
  194. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/builddoc.py +0 -0
  195. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/forms_qt.py +0 -0
  196. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/importdoc.py +0 -0
  197. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/mapview.py +0 -0
  198. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/palette.py +0 -0
  199. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/savedoc.py +0 -0
  200. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/shell.py +0 -0
  201. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/style.py +0 -0
  202. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit/workspace/tuningdialog.py +0 -0
  203. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit.egg-info/dependency_links.txt +0 -0
  204. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit.egg-info/entry_points.txt +0 -0
  205. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit.egg-info/requires.txt +0 -0
  206. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/ff9mapkit.egg-info/top_level.txt +0 -0
  207. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/setup.cfg +0 -0
  208. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_abilities.py +0 -0
  209. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_abilityfeatures.py +0 -0
  210. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_actiondelta.py +0 -0
  211. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_ai_phase_insert.py +0 -0
  212. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_ai_phase_insert_adversary.py +0 -0
  213. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_aiauthor.py +0 -0
  214. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_ailint.py +0 -0
  215. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_aipatch.py +0 -0
  216. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_animations.py +0 -0
  217. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_archetypes.py +0 -0
  218. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_areatitle.py +0 -0
  219. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_arming.py +0 -0
  220. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battle.py +0 -0
  221. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battle_bgm.py +0 -0
  222. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battle_forms.py +0 -0
  223. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battle_scene_codec.py +0 -0
  224. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battle_seq.py +0 -0
  225. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battleai.py +0 -0
  226. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battlecsv.py +0 -0
  227. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_battlepatch.py +0 -0
  228. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_bgart.py +0 -0
  229. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_bgs.py +0 -0
  230. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_build.py +0 -0
  231. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_cameras.py +0 -0
  232. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_capstone.py +0 -0
  233. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_carry_text_lint.py +0 -0
  234. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_catalog.py +0 -0
  235. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_chain.py +0 -0
  236. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_characterdelta.py +0 -0
  237. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_choice.py +0 -0
  238. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_cli_entry.py +0 -0
  239. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_cmdasm.py +0 -0
  240. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_cmdasm_relocate.py +0 -0
  241. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_deploy_campaign.py +0 -0
  242. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_deploystack.py +0 -0
  243. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_dialogue.py +0 -0
  244. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_eb.py +0 -0
  245. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_eblint.py +0 -0
  246. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_app.py +0 -0
  247. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_breadcrumb.py +0 -0
  248. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_feedback.py +0 -0
  249. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_forms.py +0 -0
  250. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_integration.py +0 -0
  251. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_jobs.py +0 -0
  252. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_model.py +0 -0
  253. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_editor_theme.py +0 -0
  254. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_entry_settle.py +0 -0
  255. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_eventscan.py +0 -0
  256. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_export.py +0 -0
  257. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_exprasm.py +0 -0
  258. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_extract_area.py +0 -0
  259. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_find_field.py +0 -0
  260. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_forkreport.py +0 -0
  261. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_gateway_advance.py +0 -0
  262. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_graphview.py +0 -0
  263. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_hub_gen.py +0 -0
  264. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_idgated.py +0 -0
  265. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_import_borrow.py +0 -0
  266. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_infohub.py +0 -0
  267. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_itemdata.py +0 -0
  268. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_items.py +0 -0
  269. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_itemstats.py +0 -0
  270. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_itemtext.py +0 -0
  271. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_journey.py +0 -0
  272. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_journey_merge.py +0 -0
  273. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_jump.py +0 -0
  274. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_ladder.py +0 -0
  275. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_lint.py +0 -0
  276. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_logic_add.py +0 -0
  277. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_logic_edit.py +0 -0
  278. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_logic_map.py +0 -0
  279. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_movement.py +0 -0
  280. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_npc_model.py +0 -0
  281. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_npc_verbatim.py +0 -0
  282. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_npcparams.py +0 -0
  283. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_object_graft.py +0 -0
  284. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_occlusion.py +0 -0
  285. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_pack.py +0 -0
  286. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_paint.py +0 -0
  287. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_party.py +0 -0
  288. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_platform.py +0 -0
  289. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_player_graft.py +0 -0
  290. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_playerswap.py +0 -0
  291. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_prop_archetypes.py +0 -0
  292. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_provision.py +0 -0
  293. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_refarc.py +0 -0
  294. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_repaint_native.py +0 -0
  295. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_reskin.py +0 -0
  296. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_save.py +0 -0
  297. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_save_items.py +0 -0
  298. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_savepoint.py +0 -0
  299. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_scene.py +0 -0
  300. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_scenelint.py +0 -0
  301. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_scroll.py +0 -0
  302. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_shared_text_block.py +0 -0
  303. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_shop.py +0 -0
  304. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_showcase.py +0 -0
  305. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_sjbinary.py +0 -0
  306. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_spawn.py +0 -0
  307. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_sps.py +0 -0
  308. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_startstate.py +0 -0
  309. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_startup.py +0 -0
  310. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_synthesis.py +0 -0
  311. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_textcarry.py +0 -0
  312. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_walkmesh_hotfix.py +0 -0
  313. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_workspace_style.py +0 -0
  314. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/tests/test_world_hub.py +0 -0
  315. {ff9mapkit-1.0.0b3 → ff9mapkit-1.0.0b5}/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.0b5
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.0b5" # 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):
@@ -1823,6 +1839,16 @@ def lint_logic(project: FieldProject) -> list[str]:
1823
1839
  for ln in _text.overflow_lines(t, wrap):
1824
1840
  out.append(f"{who} has a word too wide to fit one line ({ln!r}) -- it will overflow; "
1825
1841
  f"shorten it or raise [dialogue] wrap.")
1842
+ # DEPRECATION: `[cutscene] ate = true` is the OLD, unfaithful forced-ATE model -- a grey banner held over
1843
+ # THIS in-place cutscene. A real grey ATE WARPS you to a dedicated scene and back (verified vs 6 real grey
1844
+ # ATEs; see project-ff9-ate-system). Steer to the faithful trigger; the held-banner still builds (not an error).
1845
+ _cs = raw.get("cutscene")
1846
+ if isinstance(_cs, dict) and _cs.get("ate"):
1847
+ out.append("[cutscene] ate = true is the OLD held-banner ATE styling (a grey banner over this in-place "
1848
+ "cutscene) -- NOT how a real grey ATE works (it WARPS you to a scene, plays it, and warps you "
1849
+ "back). For a faithful forced ATE use `[[gateway]] ate = true` (the warp-in trigger: banner "
1850
+ "warning + centered title window, then the warp) + a plain [cutscene] + exit_warp on the "
1851
+ "destination field. This held-banner flavor still builds, but it isn't faithful.")
1826
1852
 
1827
1853
  # reference-data sanity (Info Hub): an [[npc]] model id / animation id the engine won't recognise.
1828
1854
  # A model NAME is handled by validate() (fatal); here we WARN on a raw id outside the known tables
@@ -2475,7 +2501,9 @@ def _gateway_on_exit_body(gw: dict, names: dict) -> bytes:
2475
2501
  """The story-state advance a ``[[gateway]]`` applies when the player TAKES this exit: the raw
2476
2502
  ``set_var`` bytes from ``set_scenario`` (ScenarioCounter) + ``set_flags`` (gEventGlobal bits), built
2477
2503
  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)."""
2504
+ (so the build is byte-identical to a gateway without on-exit writes). (NB ``ate = true`` is NOT handled
2505
+ here -- a forced-ATE warp-in routes to :func:`cutscene.inject_forced_ate` instead, because the banner +
2506
+ timed warp must run in a RunScript'd player func, not inline in the region, or the region's Wait freezes.)"""
2479
2507
  sc = gw.get("set_scenario")
2480
2508
  if isinstance(sc, str):
2481
2509
  sc = _flags.resolve_scenario(sc)
@@ -3165,9 +3193,10 @@ _UID_HOTFIX_DONORS = frozenset((900, 2803))
3165
3193
 
3166
3194
 
3167
3195
  def _verbatim_donor_id(project: FieldProject):
3168
- """Best-effort donor field id of a verbatim fork (for engine-hotfix warnings). ``import --verbatim``
3169
- records it as ``[verbatim_eb] donor``; ``None`` when an older fork's toml lacks it (the warn is then
3170
- skipped -- only fields 900/2803 are affected, so a missing hint just means no warn)."""
3196
+ """Best-effort donor field id of ANY fork (verbatim OR native/synth), for engine-hotfix warnings AND the
3197
+ deploy-time ForkDonorPatch `<forkId> <donorId>` mapping. The import records it as ``[verbatim_eb] donor``
3198
+ (verbatim) or ``[field] source_field`` (native/synth); ``borrow_field`` is the BG-borrow form. ``None``
3199
+ when an older fork's toml lacks it (the warn/emit is then skipped -- pre-record forks need a hand-added line)."""
3171
3200
  for blk, key in (("verbatim_eb", "donor"), ("field", "source_field"), ("field", "borrow_field")):
3172
3201
  v = (project.raw.get(blk) or {}).get(key)
3173
3202
  if isinstance(v, bool):
@@ -3251,6 +3280,61 @@ def _inject_verbatim_npcs(project: FieldProject, eb: bytes, npc_txids: dict, *,
3251
3280
  return eb, npc_slots
3252
3281
 
3253
3282
 
3283
+ def _gen_conductor_walk_tags(project: FieldProject, eb: bytes, steps, npc_slots):
3284
+ """Shared by the synth + verbatim conductor wiring: add the per-actor choreography tags a conductor's
3285
+ ``walk`` beats need. For each ``walk`` step, add a walk tag (20+) to the actor's entry (the conductor
3286
+ ``RunScript``s into it -- base ``Walk`` acts on the executing object, so the walk must run in the actor's
3287
+ own context). For each actor that walks inside a PARALLEL group (``with_prev``), also add ONE bare-RETURN
3288
+ join tag -- the conductor async-forks the walk then ``RunScriptSync``s the join tag to block until the
3289
+ walk frees the actor's script level. ``add_function`` grows an entry but keeps slot INDICES stable, so the
3290
+ conductor's by-uid refs (and any later band insert) stay valid. Returns ``(eb, walk_calls, join_tags)``.
3291
+
3292
+ The PLAYER walks too: ``"player"`` -> the tag goes on the player's OWN entry (``DefinePlayerCharacter``,
3293
+ located by :func:`ff9mapkit.content.player.find_player_entry`), but the conductor addresses it by the
3294
+ control-char sentinel uid 250 (``GetObjUID(250)`` -> the control character) -- so its add-target entry and
3295
+ its conductor uid differ (an NPC's are the same, slot == uid). Same recipe the ladder uses for the player."""
3296
+ walk_calls, join_tags = {}, {}
3297
+ if not any("walk" in s for s in steps):
3298
+ return eb, walk_calls, join_tags
3299
+ from .eb import edit as _eb_edit
3300
+ from .content import player as _player
3301
+ move_reg = _position_registry(project)
3302
+ # the player entry index is stable across add_function (slot indices don't move), so resolve it once
3303
+ player_entry = (_player.find_player_entry(EbScript.from_bytes(eb))
3304
+ if any(s.get("actor") == "player" and "walk" in s for s in steps) else None)
3305
+
3306
+ def _walk_target(actor):
3307
+ """(entry index to add the tag to, uid the conductor addresses). Player: its own entry, uid 250."""
3308
+ if actor == "player":
3309
+ return player_entry, _conductor.PLAYER_UID
3310
+ slot = npc_slots.get(actor) # an NPC: entry index == runtime uid
3311
+ return slot, slot
3312
+
3313
+ next_tag = {} # actor name -> next free walk tag
3314
+ for i, s in enumerate(steps):
3315
+ if "walk" not in s:
3316
+ continue
3317
+ actor = s.get("actor")
3318
+ entry_idx, uid = _walk_target(actor)
3319
+ pt = _resolve_point(s["walk"], move_reg)
3320
+ t = next_tag.get(actor, _conductor.WALK_TAG_BASE)
3321
+ next_tag[actor] = t + 1
3322
+ eb = _eb_edit.add_function(eb, entry_idx, t, _conductor.walk_tag_body(pt[0], pt[1], s.get("speed")))
3323
+ walk_calls[i] = (uid, t)
3324
+ # actors that walk inside a parallel group (>1 member) need a join tag (async-fork + sync-drain)
3325
+ parallel_actors = set()
3326
+ for g in _conductor.group_parallel(steps):
3327
+ if len(g) > 1:
3328
+ parallel_actors |= {s.get("actor") for _i, s in g if "walk" in s and s.get("actor")}
3329
+ for actor in sorted(parallel_actors):
3330
+ entry_idx, uid = _walk_target(actor)
3331
+ if entry_idx is None:
3332
+ continue
3333
+ eb = _eb_edit.add_function(eb, entry_idx, _conductor.PARALLEL_JOIN_TAG, _conductor.join_tag_body())
3334
+ join_tags[uid] = _conductor.PARALLEL_JOIN_TAG
3335
+ return eb, walk_calls, join_tags
3336
+
3337
+
3254
3338
  def _inject_verbatim_conductor(project: FieldProject, eb: bytes, npc_slots: dict, cutscene_txids, *, warnings) -> bytes:
3255
3339
  """Inject a MULTI-ACTOR ``[cutscene]`` conductor into a VERBATIM fork: ONE director code entry, seated
3256
3340
  BELOW the donor's party band (so the 9 characters stay the top slots), that drives the additive ``[[npc]]``
@@ -3263,22 +3347,8 @@ def _inject_verbatim_conductor(project: FieldProject, eb: bytes, npc_slots: dict
3263
3347
  return eb
3264
3348
  steps = _resolve_conductor_steps(cs["steps"], project)
3265
3349
  # 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)
3350
+ # synth path's mechanism; here the actor entries sit below the band); a parallel walk also gets a join tag.
3351
+ eb, walk_calls, join_tags = _gen_conductor_walk_tags(project, eb, steps, npc_slots)
3282
3352
  auto = _FlagAlloc(getattr(project, "flag_base", None)) # campaign-safe once-flag (matches the other verbatim blocks)
3283
3353
  c_fclass, c_fidx = _cutscene.once_flag_for(cs)
3284
3354
  if auto.base is not None and "flag" not in cs and cs.get("once", True):
@@ -3289,7 +3359,7 @@ def _inject_verbatim_conductor(project: FieldProject, eb: bytes, npc_slots: dict
3289
3359
  warmup=int(cs.get("warmup", _cutscene.DEFAULT_WARMUP)),
3290
3360
  owns_control=bool(cs.get("owns_control", True)),
3291
3361
  exit_warp=(int(cs["exit_warp"]) if cs.get("exit_warp") else None),
3292
- walk_calls=walk_calls, reserve_party_band=True)
3362
+ walk_calls=walk_calls, join_tags=join_tags, reserve_party_band=True)
3293
3363
 
3294
3364
 
3295
3365
  def _inject_verbatim_props(project: FieldProject, eb: bytes, prop_txids=None, *, warnings) -> bytes:
@@ -3351,6 +3421,11 @@ def _inject_verbatim_gateways(project: FieldProject, eb: bytes, *, warnings) ->
3351
3421
  zone = gw["zone"]
3352
3422
  if len(zone) == 4:
3353
3423
  zone = _gw.quad_zone(zone)
3424
+ if gw.get("ate"): # a forced-ATE warp-in, seated below the party band
3425
+ eb = _cutscene.inject_forced_ate(eb, [tuple(p) for p in zone], int(gw["to"]),
3426
+ mode=int(gw.get("ate_mode", _cutscene.ATE_DEFAULT_MODE)),
3427
+ reserve_party_band=True)
3428
+ continue
3354
3429
  gf, gs = _gate_of(gw)
3355
3430
  eb = _gw.inject_gateway(eb, int(gw["to"]), entrance=int(gw.get("entrance", 0)),
3356
3431
  zone=[tuple(p) for p in zone], gate_flag=gf, gate_require_set=gs,
@@ -3391,12 +3466,14 @@ def build_script(project: FieldProject, lang: str, dialogue_txids: dict,
3391
3466
  control_value: int = -1, event_txids: dict | None = None,
3392
3467
  cutscene_txids: list | None = None, walkmesh=None,
3393
3468
  choice_txids: dict | None = None, on_entry_txids: dict | None = None,
3394
- ate_txids: dict | None = None, chest_txids: dict | None = None) -> bytes:
3469
+ ate_txids: dict | None = None, chest_txids: dict | None = None,
3470
+ gateway_txids: dict | None = None) -> bytes:
3395
3471
  """Build one language's .eb by applying the project's content to the blank field."""
3396
3472
  _auto = _FlagAlloc(getattr(project, "flag_base", None))
3397
3473
  event_txids = event_txids or {}
3398
3474
  cutscene_txids = cutscene_txids or []
3399
3475
  choice_txids = choice_txids or {}
3476
+ gateway_txids = gateway_txids or {}
3400
3477
  on_entry_txids = on_entry_txids or {}
3401
3478
  ate_txids = ate_txids or {}
3402
3479
  chest_txids = chest_txids or {}
@@ -3557,10 +3634,15 @@ def build_script(project: FieldProject, lang: str, dialogue_txids: dict,
3557
3634
 
3558
3635
  # gateways
3559
3636
  gw_names = _story_names(project) # [[flag]] name -> index, for set_flags resolution
3560
- for gw in project.raw.get("gateway", []):
3637
+ for gi, gw in enumerate(project.raw.get("gateway", [])):
3561
3638
  zone = gw["zone"]
3562
3639
  if len(zone) == 4:
3563
3640
  zone = _gw.quad_zone(zone)
3641
+ if gw.get("ate"): # a forced-ATE warp-in: grey banner WARNING + title, then warp
3642
+ eb = _cutscene.inject_forced_ate(eb, [tuple(p) for p in zone], int(gw["to"]),
3643
+ mode=int(gw.get("ate_mode", _cutscene.ATE_DEFAULT_MODE)),
3644
+ title_txid=gateway_txids.get(gi))
3645
+ continue
3564
3646
  gf, gs = _gate_of(gw)
3565
3647
  eb = _gw.inject_gateway(eb, int(gw["to"]), entrance=int(gw.get("entrance", 0)),
3566
3648
  zone=[tuple(p) for p in zone], gate_flag=gf, gate_require_set=gs,
@@ -3705,29 +3787,16 @@ def build_script(project: FieldProject, lang: str, dialogue_txids: dict,
3705
3787
  if _auto.base is not None and "flag" not in cs and cs.get("once", True):
3706
3788
  c_fidx = _auto.cutscene() # campaign: pack into this member's flag block
3707
3789
  # 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)
3790
+ # so generate a per-actor walk-choreography TAG on the actor's own [[npc]] entry and RunScript into it
3791
+ # (animates in the actor's context, blocks until arrival); a parallel walk also gets a join tag.
3792
+ eb, walk_calls, join_tags = _gen_conductor_walk_tags(project, eb, c_steps, npc_slots)
3724
3793
  eb = _conductor.inject_conductor(
3725
3794
  eb, c_steps, npc_slots, cutscene_txids,
3726
3795
  once_flag=(c_fidx if cs.get("once", True) else None), flag_class=c_fclass,
3727
3796
  warmup=int(cs.get("warmup", _cutscene.DEFAULT_WARMUP)),
3728
3797
  owns_control=bool(cs.get("owns_control", True)),
3729
3798
  exit_warp=(int(cs["exit_warp"]) if cs.get("exit_warp") else None),
3730
- say_flags=cs_say_flags, walk_calls=walk_calls)
3799
+ say_flags=cs_say_flags, walk_calls=walk_calls, join_tags=join_tags)
3731
3800
 
3732
3801
  # cutscene (narration, no actor): an ordered, control-locked sequence on entry (once), run as a
3733
3802
  # standalone director code entry. Steps = say / wait / set_flag. An ACTOR cutscene was already
@@ -4133,17 +4202,40 @@ def _validate_conductor(project, cs, problems):
4133
4202
  _animations.resolve(token, a)
4134
4203
  except ValueError as e:
4135
4204
  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:
4205
+ if act == "walk": # walk on "player" runs in the player's own
4206
+ try: # entry (uid 250); an [[npc]] in its slot entry
4141
4207
  _resolve_point(s["walk"], move_reg) # [x, z] or a known marker/NPC name
4142
4208
  except ValueError as e:
4143
4209
  problems.append(f"[cutscene] step {k}: {e}")
4144
4210
  t = s.get("tail")
4145
4211
  if t is not None and t not in _text.TAIL_CODES:
4146
4212
  problems.append(f"[cutscene] step {k} tail {t!r} is not a valid TAIL code")
4213
+ # parallel beats (with_prev): a step marked with_prev runs together with the preceding beat. Only
4214
+ # walk/anim/turn can run in parallel (say/wait/set_flag are sequential barriers), the group leader
4215
+ # must be one of those too, and no actor may act twice in a group (it has one execution context).
4216
+ if steps[0].get("with_prev"):
4217
+ problems.append("[cutscene] step 0 can't have with_prev = true (nothing precedes it)")
4218
+ _par_ok = ("walk", "anim", "turn")
4219
+ for g in _conductor.group_parallel(steps):
4220
+ if len(g) < 2:
4221
+ continue
4222
+ lead_k, lead_s = g[0]
4223
+ lead_act = next((key for key in _par_ok if key in lead_s), None)
4224
+ if lead_act is None:
4225
+ problems.append(f"[cutscene] step {lead_k} has a with_prev beat after it but is a "
4226
+ f"say/wait/set_flag (a sequential barrier) -- only walk/anim/turn run in parallel")
4227
+ seen = {lead_s.get("actor")} if lead_act else set()
4228
+ for k, s in g[1:]:
4229
+ act = next((key for key in _par_ok if key in s), None)
4230
+ if act is None:
4231
+ problems.append(f"[cutscene] step {k}: only walk/anim/turn can run with_prev "
4232
+ f"(say/wait/set_flag are sequential barriers)")
4233
+ continue
4234
+ who = s.get("actor")
4235
+ if who in seen:
4236
+ problems.append(f"[cutscene] step {k}: actor {who!r} already acts in this parallel group "
4237
+ f"(an actor can't do two things at once)")
4238
+ seen.add(who)
4147
4239
  if "exit_warp" in cs:
4148
4240
  ew = cs["exit_warp"]
4149
4241
  if not (isinstance(ew, int) and not isinstance(ew, bool) and ew > 0):
@@ -4432,8 +4524,9 @@ def _wrap_width(project: FieldProject):
4432
4524
 
4433
4525
  def collect_text(project: FieldProject):
4434
4526
  """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
4527
+ chest_txids, gateway_txids). All field text (NPC dialogue, event messages, cutscene 'say' lines, choice
4528
+ prompts + replies, on-entry messages, the ATE menu, chest "Received X" boxes, forced-ATE gateway titles)
4529
+ shares one .mes block, in that order
4437
4530
  (so a field with no events/cutscene/choices/on_entry/ate/chests is byte-identical to the old layout).
4438
4531
  ``cutscene_txids`` is a list (one per 'say' step); ``choice_txids[c]`` = ``{"prompt": id, "replies":
4439
4532
  {opt_index: id}}``; ``on_entry_txids[k]`` = the txid of hook ``k``'s message (only for hooks that have
@@ -4521,8 +4614,20 @@ def collect_text(project: FieldProject):
4521
4614
  for k, ch in enumerate(project.raw.get("chest", [])):
4522
4615
  text, strt, tail = _chest_received_box(ch)
4523
4616
  ch_pos[k] = _add_raw(text, ch.get("tail") or tail, strt=strt)
4617
+ # forced-ATE gateway titles: the winATE-captioned, CENTERED "title window" a `[[gateway]] ate = true` shows
4618
+ # as it warps. Geometry verified vs real grey ATEs 956/2211: `[STRT=W,1][IMME][CENT=W]title` -- the engine
4619
+ # auto-centers a system window from its [STRT] width (like the chest box) + [CENT] centers the text + [IMME]
4620
+ # pops it fully drawn. W ~ the rendered text width (real STRT ~= text.measure * 7.2). The default dialogue
4621
+ # geometry (10,1)+TAIL=UPR pins it TOP-RIGHT (the reported bug). Added LAST (after chests) -> byte-identical
4622
+ # for a field without one.
4623
+ gw_pos = {}
4624
+ for gi, gw in enumerate(project.raw.get("gateway", [])):
4625
+ if gw.get("ate") and gw.get("ate_title"):
4626
+ _title = str(gw["ate_title"])
4627
+ _w = max(8, round(_text.measure(_title) * 7.2)) # ~ the FF9 STRT width of the rendered title
4628
+ gw_pos[gi] = _add_raw(f"[IMME][CENT={_w}]{_title}", "", strt=(_w, 1)) # NO tail -> the real ATE's true centre
4524
4629
  if not lines:
4525
- return "", {}, {}, [], {}, {}, {}, {}
4630
+ return "", {}, {}, [], {}, {}, {}, {}, {}
4526
4631
  body, mapping = _text.build_mes(lines, start_txid=_text.DEFAULT_BASE_TXID, tails=tails, strts=strts)
4527
4632
  npc_txids = {i: mapping[p] for i, p in npc_pos.items()}
4528
4633
  event_txids = {j: mapping[p] for j, p in ev_pos.items()}
@@ -4536,7 +4641,9 @@ def collect_text(project: FieldProject):
4536
4641
  "replies": {oi: mapping[p] for oi, p in ate_reply_pos.items()}}
4537
4642
  if ate_prompt_pos is not None else {})
4538
4643
  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
4644
+ gateway_txids = {gi: mapping[p] for gi, p in gw_pos.items()} # forced-ATE title-window txids (by gw index)
4645
+ return (body, npc_txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids,
4646
+ chest_txids, gateway_txids)
4540
4647
 
4541
4648
 
4542
4649
  # --------------------------------------------------------------------------- the build
@@ -4717,7 +4824,7 @@ def build_field(project: FieldProject, layout: ModLayout, *, langs=LANGS) -> Fie
4717
4824
 
4718
4825
  _autofill_ladder_landing_y(project, cutscene_wmesh) # elevated dismount floors get their real Y
4719
4826
  # --- dialogue + per-language script ---
4720
- mes_body, txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids, chest_txids = collect_text(project)
4827
+ mes_body, txids, event_txids, cutscene_txids, choice_txids, on_entry_txids, ate_txids, chest_txids, gateway_txids = collect_text(project)
4721
4828
  control_value = resolve_control_value(project, camera)
4722
4829
  # faithful text carry: the donor's referenced dialogue, shipped VERBATIM per language and APPENDED after
4723
4830
  # the authored block (its own [TXID=>=1000] re-index keeps it disjoint -- authored text + the hut golden
@@ -4923,7 +5030,7 @@ def build_field(project: FieldProject, layout: ModLayout, *, langs=LANGS) -> Fie
4923
5030
  eb = build_script(project, lang, txids, control_value, event_txids=event_txids,
4924
5031
  cutscene_txids=cutscene_txids, walkmesh=cutscene_wmesh,
4925
5032
  choice_txids=choice_txids, on_entry_txids=on_entry_txids,
4926
- ate_txids=ate_txids, chest_txids=chest_txids)
5033
+ ate_txids=ate_txids, chest_txids=chest_txids, gateway_txids=gateway_txids)
4927
5034
  base = mes_body or ""
4928
5035
  inplace = base
4929
5036
  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