ff9mapkit 1.0.0b4__tar.gz → 1.0.0b6__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.0b4 → ff9mapkit-1.0.0b6}/PKG-INFO +1 -1
  2. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/__init__.py +1 -1
  3. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/build.py +14 -3
  4. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/config.py +120 -16
  5. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/jobs.py +7 -0
  6. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/extract.py +15 -5
  7. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/builddoc.py +41 -4
  8. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/importdoc.py +6 -2
  9. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit.egg-info/PKG-INFO +1 -1
  10. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/pyproject.toml +1 -1
  11. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_ate.py +17 -0
  12. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_setup.py +50 -0
  13. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_verbatim.py +59 -0
  14. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/LICENSE +0 -0
  15. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/README.md +0 -0
  16. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/__main__.py +0 -0
  17. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_animdb.py +0 -0
  18. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_animdb_all.py +0 -0
  19. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_fieldtable.py +0 -0
  20. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_fieldtext.py +0 -0
  21. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_held_poses.py +0 -0
  22. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_itemdb.py +0 -0
  23. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_modeldb.py +0 -0
  24. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_narrowmap_data.py +0 -0
  25. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_npcparams.py +0 -0
  26. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_animdb.py +0 -0
  27. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_animdb_all.py +0 -0
  28. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_fieldtable.py +0 -0
  29. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_fieldtext.py +0 -0
  30. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_modeldb.py +0 -0
  31. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_npcparams.py +0 -0
  32. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_regen_scenedb.py +0 -0
  33. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/_scenedb.py +0 -0
  34. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/abilities.py +0 -0
  35. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/animations.py +0 -0
  36. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/archetypes.py +0 -0
  37. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/areatitle.py +0 -0
  38. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/__init__.py +0 -0
  39. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/abilityfeatures.py +0 -0
  40. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/actiondelta.py +0 -0
  41. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/aiauthor.py +0 -0
  42. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/ailint.py +0 -0
  43. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/aipatch.py +0 -0
  44. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/battleai.py +0 -0
  45. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/battlecsv.py +0 -0
  46. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/battlepatch.py +0 -0
  47. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/build.py +0 -0
  48. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/camera_codec.py +0 -0
  49. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/camera_data.py +0 -0
  50. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/characterdelta.py +0 -0
  51. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/event_data.py +0 -0
  52. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/extract.py +0 -0
  53. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/fbx.py +0 -0
  54. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/reskin.py +0 -0
  55. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/scene_codec.py +0 -0
  56. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/scene_data.py +0 -0
  57. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/scenelint.py +0 -0
  58. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/seqasm.py +0 -0
  59. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/seqauthor.py +0 -0
  60. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/seqcodec.py +0 -0
  61. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/seqdis.py +0 -0
  62. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle/seqpatch.py +0 -0
  63. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/battle_bgm.py +0 -0
  64. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/binutils.py +0 -0
  65. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/campaign.py +0 -0
  66. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/catalog.py +0 -0
  67. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/chain.py +0 -0
  68. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/cli.py +0 -0
  69. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/__init__.py +0 -0
  70. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/areatitle.py +0 -0
  71. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/ate.py +0 -0
  72. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/camera.py +0 -0
  73. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/chest.py +0 -0
  74. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/choice.py +0 -0
  75. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/conductor.py +0 -0
  76. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/cutscene.py +0 -0
  77. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/encounter.py +0 -0
  78. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/entry_settle.py +0 -0
  79. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/equipment.py +0 -0
  80. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/event.py +0 -0
  81. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/gateway.py +0 -0
  82. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/inventory.py +0 -0
  83. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/itemdata.py +0 -0
  84. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/itemtext.py +0 -0
  85. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/jump.py +0 -0
  86. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/ladder.py +0 -0
  87. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/movement.py +0 -0
  88. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/music.py +0 -0
  89. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/npc.py +0 -0
  90. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/object.py +0 -0
  91. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/onentry.py +0 -0
  92. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/party.py +0 -0
  93. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/pathfind.py +0 -0
  94. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/platform.py +0 -0
  95. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/player.py +0 -0
  96. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/prop.py +0 -0
  97. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/region.py +0 -0
  98. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/reinit.py +0 -0
  99. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/savepoint.py +0 -0
  100. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/shop.py +0 -0
  101. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/sps_trigger.py +0 -0
  102. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/startup.py +0 -0
  103. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/synthesis.py +0 -0
  104. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/text.py +0 -0
  105. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/textcarry.py +0 -0
  106. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/verbatim.py +0 -0
  107. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/content/walkmesh_hotfix.py +0 -0
  108. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/__init__.py +0 -0
  109. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/_regen_provenance.py +0 -0
  110. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.es.patch +0 -0
  111. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.fr.patch +0 -0
  112. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.gr.patch +0 -0
  113. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.it.patch +0 -0
  114. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.jp.patch +0 -0
  115. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.uk.patch +0 -0
  116. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/blank.us.patch +0 -0
  117. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/manifest.json +0 -0
  118. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/provenance/region_template.patch +0 -0
  119. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/reference_arcs.toml +0 -0
  120. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/data/region_catalog.toml +0 -0
  121. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/deploystack.py +0 -0
  122. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/dialogue.py +0 -0
  123. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/__init__.py +0 -0
  124. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/_exprtable.py +0 -0
  125. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/_membertable.py +0 -0
  126. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/_optables.py +0 -0
  127. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/_regen_optables.py +0 -0
  128. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/cmdasm.py +0 -0
  129. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/disasm.py +0 -0
  130. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/edit.py +0 -0
  131. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/exprasm.py +0 -0
  132. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/model.py +0 -0
  133. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eb/opcodes.py +0 -0
  134. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eblint.py +0 -0
  135. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/__init__.py +0 -0
  136. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/app.py +0 -0
  137. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/battle_forms.py +0 -0
  138. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/breadcrumb.py +0 -0
  139. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/dialogs.py +0 -0
  140. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/feedback.py +0 -0
  141. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/forms.py +0 -0
  142. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/graphview.py +0 -0
  143. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/model.py +0 -0
  144. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/picker.py +0 -0
  145. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/editor/theme.py +0 -0
  146. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/eventscan.py +0 -0
  147. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/flags.py +0 -0
  148. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/forkreport.py +0 -0
  149. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/hub.py +0 -0
  150. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/idgated.py +0 -0
  151. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/infohub.py +0 -0
  152. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/items.py +0 -0
  153. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/itemstats.py +0 -0
  154. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/journey.py +0 -0
  155. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/keyitems.py +0 -0
  156. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/logic_add.py +0 -0
  157. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/logic_edit.py +0 -0
  158. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/logic_map.py +0 -0
  159. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/memoria.py +0 -0
  160. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/pack.py +0 -0
  161. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/playerswap.py +0 -0
  162. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/prop_archetypes.py +0 -0
  163. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/provision.py +0 -0
  164. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/refarc.py +0 -0
  165. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/save.py +0 -0
  166. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/save_items.py +0 -0
  167. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/__init__.py +0 -0
  168. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/arena.py +0 -0
  169. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/bgart.py +0 -0
  170. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/bgi.py +0 -0
  171. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/bgs.py +0 -0
  172. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/bgx.py +0 -0
  173. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/cam.py +0 -0
  174. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/guide.py +0 -0
  175. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/paint.py +0 -0
  176. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/scene/placeholder.py +0 -0
  177. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sjbinary.py +0 -0
  178. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/__init__.py +0 -0
  179. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/author.py +0 -0
  180. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/catalog.py +0 -0
  181. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/codec.py +0 -0
  182. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/edit.py +0 -0
  183. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/lint.py +0 -0
  184. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/render.py +0 -0
  185. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/templates.py +0 -0
  186. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/sps/texture.py +0 -0
  187. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/walkmesh_hotfixes.py +0 -0
  188. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/__init__.py +0 -0
  189. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/battledoc.py +0 -0
  190. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/forms_qt.py +0 -0
  191. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/mapview.py +0 -0
  192. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/palette.py +0 -0
  193. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/savedoc.py +0 -0
  194. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/shell.py +0 -0
  195. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/style.py +0 -0
  196. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit/workspace/tuningdialog.py +0 -0
  197. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit.egg-info/SOURCES.txt +0 -0
  198. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit.egg-info/dependency_links.txt +0 -0
  199. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit.egg-info/entry_points.txt +0 -0
  200. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit.egg-info/requires.txt +0 -0
  201. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/ff9mapkit.egg-info/top_level.txt +0 -0
  202. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/setup.cfg +0 -0
  203. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_abilities.py +0 -0
  204. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_abilityfeatures.py +0 -0
  205. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_actiondelta.py +0 -0
  206. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_ai_phase_insert.py +0 -0
  207. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_ai_phase_insert_adversary.py +0 -0
  208. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_aiauthor.py +0 -0
  209. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_ailint.py +0 -0
  210. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_aipatch.py +0 -0
  211. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_animations.py +0 -0
  212. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_archetypes.py +0 -0
  213. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_areatitle.py +0 -0
  214. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_arming.py +0 -0
  215. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battle.py +0 -0
  216. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battle_bgm.py +0 -0
  217. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battle_forms.py +0 -0
  218. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battle_scene_codec.py +0 -0
  219. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battle_seq.py +0 -0
  220. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battleai.py +0 -0
  221. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battlecsv.py +0 -0
  222. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_battlepatch.py +0 -0
  223. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_bgart.py +0 -0
  224. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_bgs.py +0 -0
  225. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_build.py +0 -0
  226. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_cameras.py +0 -0
  227. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_campaign.py +0 -0
  228. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_capstone.py +0 -0
  229. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_carry_text_lint.py +0 -0
  230. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_catalog.py +0 -0
  231. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_chain.py +0 -0
  232. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_characterdelta.py +0 -0
  233. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_choice.py +0 -0
  234. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_cli_entry.py +0 -0
  235. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_cmdasm.py +0 -0
  236. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_cmdasm_relocate.py +0 -0
  237. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_content.py +0 -0
  238. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_deploy_campaign.py +0 -0
  239. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_deploystack.py +0 -0
  240. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_dialogue.py +0 -0
  241. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_eb.py +0 -0
  242. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_eblint.py +0 -0
  243. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_app.py +0 -0
  244. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_breadcrumb.py +0 -0
  245. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_feedback.py +0 -0
  246. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_forms.py +0 -0
  247. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_integration.py +0 -0
  248. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_jobs.py +0 -0
  249. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_model.py +0 -0
  250. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_editor_theme.py +0 -0
  251. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_entry_settle.py +0 -0
  252. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_eventscan.py +0 -0
  253. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_export.py +0 -0
  254. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_exprasm.py +0 -0
  255. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_extract_area.py +0 -0
  256. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_find_field.py +0 -0
  257. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_flags.py +0 -0
  258. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_forkreport.py +0 -0
  259. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_gateway_advance.py +0 -0
  260. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_graphview.py +0 -0
  261. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_hub_gen.py +0 -0
  262. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_idgated.py +0 -0
  263. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_import_borrow.py +0 -0
  264. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_infohub.py +0 -0
  265. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_itemdata.py +0 -0
  266. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_items.py +0 -0
  267. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_itemstats.py +0 -0
  268. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_itemtext.py +0 -0
  269. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_journey.py +0 -0
  270. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_journey_merge.py +0 -0
  271. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_jump.py +0 -0
  272. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_ladder.py +0 -0
  273. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_lint.py +0 -0
  274. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_logic_add.py +0 -0
  275. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_logic_edit.py +0 -0
  276. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_logic_map.py +0 -0
  277. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_movement.py +0 -0
  278. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_npc_model.py +0 -0
  279. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_npc_verbatim.py +0 -0
  280. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_npcparams.py +0 -0
  281. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_object_graft.py +0 -0
  282. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_occlusion.py +0 -0
  283. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_on_entry.py +0 -0
  284. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_pack.py +0 -0
  285. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_paint.py +0 -0
  286. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_party.py +0 -0
  287. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_platform.py +0 -0
  288. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_player_graft.py +0 -0
  289. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_playerswap.py +0 -0
  290. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_prop_archetypes.py +0 -0
  291. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_provision.py +0 -0
  292. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_refarc.py +0 -0
  293. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_repaint_native.py +0 -0
  294. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_reskin.py +0 -0
  295. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_save.py +0 -0
  296. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_save_items.py +0 -0
  297. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_savepoint.py +0 -0
  298. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_scene.py +0 -0
  299. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_scenelint.py +0 -0
  300. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_scroll.py +0 -0
  301. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_shared_text_block.py +0 -0
  302. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_shop.py +0 -0
  303. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_showcase.py +0 -0
  304. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_sjbinary.py +0 -0
  305. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_spawn.py +0 -0
  306. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_sps.py +0 -0
  307. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_startstate.py +0 -0
  308. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_startup.py +0 -0
  309. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_synthesis.py +0 -0
  310. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_text.py +0 -0
  311. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_textcarry.py +0 -0
  312. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_walkmesh_hotfix.py +0 -0
  313. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_workspace_style.py +0 -0
  314. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/tests/test_world_hub.py +0 -0
  315. {ff9mapkit-1.0.0b4 → ff9mapkit-1.0.0b6}/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.0b4
3
+ Version: 1.0.0b6
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.0b4" # keep in lockstep with [project] version in pyproject.toml
18
+ __version__ = "1.0.0b6" # keep in lockstep with [project] version in pyproject.toml
@@ -1839,6 +1839,16 @@ def lint_logic(project: FieldProject) -> list[str]:
1839
1839
  for ln in _text.overflow_lines(t, wrap):
1840
1840
  out.append(f"{who} has a word too wide to fit one line ({ln!r}) -- it will overflow; "
1841
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.")
1842
1852
 
1843
1853
  # reference-data sanity (Info Hub): an [[npc]] model id / animation id the engine won't recognise.
1844
1854
  # A model NAME is handled by validate() (fatal); here we WARN on a raw id outside the known tables
@@ -3183,9 +3193,10 @@ _UID_HOTFIX_DONORS = frozenset((900, 2803))
3183
3193
 
3184
3194
 
3185
3195
  def _verbatim_donor_id(project: FieldProject):
3186
- """Best-effort donor field id of a verbatim fork (for engine-hotfix warnings). ``import --verbatim``
3187
- records it as ``[verbatim_eb] donor``; ``None`` when an older fork's toml lacks it (the warn is then
3188
- 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)."""
3189
3200
  for blk, key in (("verbatim_eb", "donor"), ("field", "source_field"), ("field", "borrow_field")):
3190
3201
  v = (project.raw.get(blk) or {}).get(key)
3191
3202
  if isinstance(v, bool):
@@ -46,6 +46,15 @@ _COMMON_STEAM_PATHS = (
46
46
  r"D:\SteamLibrary\steamapps\common\FINAL FANTASY IX",
47
47
  )
48
48
 
49
+ # FF9 store identifiers -- used to read the per-game install path from the Windows registry (the SAME keys
50
+ # Memoria's patcher reads, so we resolve exactly the folder it patches -- correct even on a secondary drive
51
+ # / custom Steam library). Steam + GOG are the same Unity port with an identical on-disk layout; the
52
+ # Microsoft Store / Xbox Game Pass build is a different, non-Unity, DRM-locked package Memoria can't patch,
53
+ # so the detector never targets it. Refs: Albeoris/Memoria Memoria.Patcher/GameInfo/.
54
+ _STEAM_APPID = "377840"
55
+ _GOG_GAMEID = "1375008492"
56
+ _FF9_DIRNAME = "FINAL FANTASY IX"
57
+
49
58
 
50
59
  class ConfigError(RuntimeError):
51
60
  """Raised when a required path cannot be resolved or does not exist."""
@@ -80,34 +89,129 @@ def save_game_path(game_path: str | os.PathLike) -> Path:
80
89
  return USER_CONFIG
81
90
 
82
91
 
92
+ def _is_ff9_root(p: Path) -> bool:
93
+ """True if ``p`` is a real, MODDABLE FF9 install (Steam or GOG -- same layout). Keys off Memoria's own
94
+ sentinel (``FF9_Launcher.exe``) plus ``StreamingAssets`` and the managed-DLL tree -- which also rejects
95
+ the un-moddable Microsoft Store build (no launcher / no Managed dir) and stale empty folders."""
96
+ try:
97
+ return ((p / "FF9_Launcher.exe").is_file()
98
+ and (p / "StreamingAssets").is_dir()
99
+ and ((p / "x64" / "FF9_Data" / "Managed").is_dir()
100
+ or (p / "x86" / "FF9_Data" / "Managed").is_dir()))
101
+ except OSError:
102
+ return False
103
+
104
+
105
+ def _reg_str(hive, subkey: str, value: str) -> str | None:
106
+ """Read one registry string value, trying the 64-bit THEN 32-bit view (FF9's keys live under
107
+ ``WOW6432Node`` on 64-bit Windows). Windows-only; returns None elsewhere or if the key/value is absent."""
108
+ if os.name != "nt":
109
+ return None
110
+ import winreg # noqa: PLC0415 - Windows-only, lazy
111
+ for view in (winreg.KEY_WOW64_64KEY, winreg.KEY_WOW64_32KEY):
112
+ try:
113
+ with winreg.OpenKeyEx(hive, subkey, 0, winreg.KEY_READ | view) as key:
114
+ data, _ = winreg.QueryValueEx(key, value)
115
+ if data:
116
+ return str(data)
117
+ except OSError:
118
+ continue
119
+ return None
120
+
121
+
122
+ def _registry_candidates() -> list[Path]:
123
+ """FF9 roots from the per-game Steam + GOG registry keys (the exact keys Memoria reads). These point at
124
+ the ACTUAL install dir, so they find FF9 even on a secondary drive or a custom-named Steam library."""
125
+ if os.name != "nt":
126
+ return []
127
+ import winreg # noqa: PLC0415
128
+ out: list[Path] = []
129
+ steam = _reg_str(winreg.HKEY_LOCAL_MACHINE,
130
+ rf"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App {_STEAM_APPID}",
131
+ "InstallLocation")
132
+ if steam:
133
+ out.append(Path(steam))
134
+ gog = _reg_str(winreg.HKEY_LOCAL_MACHINE, rf"SOFTWARE\GOG.com\Games\{_GOG_GAMEID}", "path")
135
+ if gog:
136
+ out.append(Path(gog))
137
+ return out
138
+
139
+
140
+ def _parse_vdf_library_paths(text: str) -> list[str]:
141
+ """Library-folder paths from a Steam ``libraryfolders.vdf`` -- handles both the old ``"1" "<path>"`` and
142
+ the new ``"path" "<path>"`` schemas (and ignores the new schema's ``"apps"`` appid->buildid map). The
143
+ file doubles its backslashes, so they're unescaped here."""
144
+ keyed = re.findall(r'"path"\s+"([^"]+)"', text) if '"path"' in text \
145
+ else re.findall(r'"\d+"\s+"([^"]+)"', text)
146
+ return [p.replace("\\\\", "\\") for p in keyed]
147
+
148
+
149
+ def _steam_library_candidates() -> list[Path]:
150
+ """FF9 under every Steam library (a fallback when the per-game Uninstall key is missing): locate Steam
151
+ via the registry, parse ``libraryfolders.vdf``, and join ``steamapps/common/FINAL FANTASY IX``."""
152
+ if os.name != "nt":
153
+ return []
154
+ import winreg # noqa: PLC0415
155
+ steam = (_reg_str(winreg.HKEY_CURRENT_USER, r"Software\Valve\Steam", "SteamPath")
156
+ or _reg_str(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Valve\Steam", "InstallPath"))
157
+ if not steam:
158
+ return []
159
+ steam_dir = Path(steam)
160
+ out: list[Path] = [steam_dir / "steamapps" / "common" / _FF9_DIRNAME]
161
+ for vdf in (steam_dir / "steamapps" / "libraryfolders.vdf", steam_dir / "config" / "libraryfolders.vdf"):
162
+ try:
163
+ text = vdf.read_text(encoding="utf-8", errors="ignore")
164
+ except OSError:
165
+ continue
166
+ for lib in _parse_vdf_library_paths(text):
167
+ out.append(Path(lib) / "steamapps" / "common" / _FF9_DIRNAME)
168
+ return out
169
+
170
+
171
+ def _fallback_candidates() -> list[Path]:
172
+ """Last-resort default install folders (Steam + GOG) if registry/library lookups don't resolve."""
173
+ gog = [Path(b) / _FF9_DIRNAME for b in
174
+ (r"C:\GOG Games", r"D:\GOG Games", r"C:\Program Files (x86)\GOG Galaxy\Games")]
175
+ return [Path(p) for p in _COMMON_STEAM_PATHS] + gog
176
+
177
+
83
178
  def find_game_path(explicit: str | os.PathLike | None = None) -> Path:
84
179
  """Resolve the Final Fantasy IX install folder.
85
180
 
86
- Order: explicit arg > $FF9_GAME_PATH > ~/.ff9mapkit.toml(game_path) > common Steam dirs.
87
- Raises ConfigError with actionable guidance if none of them point at a real folder.
181
+ Order: explicit arg > ``$FF9_GAME_PATH`` > ``~/.ff9mapkit.toml`` (``game_path``) > auto-detect. The
182
+ auto-detector mirrors Memoria's: the per-game Steam + GOG registry keys, then a Steam-library scan, then
183
+ the common default folders. Explicit/env/config paths are trusted if they exist; auto-detected ones are
184
+ validated as a real, moddable FF9 install (which also skips the un-moddable Microsoft Store build).
185
+ Raises ConfigError with actionable guidance if nothing resolves.
88
186
  """
89
- candidates: list[Path] = []
90
- if explicit:
91
- candidates.append(Path(explicit))
92
- env = os.environ.get("FF9_GAME_PATH")
93
- if env:
94
- candidates.append(Path(env))
95
- cfg = _read_user_config().get("game_path")
96
- if cfg:
97
- candidates.append(Path(cfg))
98
- candidates.extend(Path(p) for p in _COMMON_STEAM_PATHS)
99
-
100
- for c in candidates:
187
+ # 1) user-specified -- trusted on existence
188
+ for c in (Path(p) for p in (explicit, os.environ.get("FF9_GAME_PATH"),
189
+ _read_user_config().get("game_path")) if p):
101
190
  if c.is_dir():
102
191
  return c.resolve()
103
192
 
193
+ # 2) auto-detect Steam + GOG, validated as a real install; de-dupe by resolved path
194
+ seen: set[Path] = set()
195
+ for c in _registry_candidates() + _steam_library_candidates() + _fallback_candidates():
196
+ try:
197
+ rc = c.resolve()
198
+ except OSError:
199
+ continue
200
+ if rc in seen:
201
+ continue
202
+ seen.add(rc)
203
+ if _is_ff9_root(c):
204
+ return rc
205
+
104
206
  raise ConfigError(
105
- "Could not locate the Final Fantasy IX install folder.\n"
207
+ "Could not locate the Final Fantasy IX install folder (checked Steam + GOG via the registry,\n"
208
+ "the Steam libraries, and the common install paths).\n"
106
209
  "Set it one of these ways:\n"
107
210
  " - pass --game \"<path>\" on the command line\n"
108
211
  " - export FF9_GAME_PATH=\"<path>\"\n"
109
212
  f" - add game_path = \"<path>\" to {USER_CONFIG}\n"
110
- "The folder should contain FF9_Launcher.exe and a StreamingAssets directory."
213
+ "The folder should contain FF9_Launcher.exe and a StreamingAssets directory.\n"
214
+ "(The Microsoft Store / Xbox Game Pass version is not moddable -- use the Steam or GOG release.)"
111
215
  )
112
216
 
113
217
 
@@ -80,6 +80,13 @@ def detect_deploy_target(repo_root):
80
80
  return mod, fid
81
81
 
82
82
 
83
+ def has_deploy_tools(repo_root) -> bool:
84
+ """True if the deploy SCRIPTS (``tools/deploy_field.py`` etc.) are present -- i.e. this is a repo checkout,
85
+ not an installed wheel (the wheel ships no ``tools/``). The Workspace uses it to hide the dev-only deploy
86
+ paths (test-slot / campaign / journey / battle / reverts) for an installed copy."""
87
+ return (Path(repo_root) / "tools" / "deploy_field.py").is_file()
88
+
89
+
83
90
  def detect_deployed_fields(mod_folder):
84
91
  """``[(id, name), ...]`` of the FieldScene lines in the worktree mod folder's DictionaryPatch -- the
85
92
  fields whose encounter a battle-mint can repoint (the valid 'trigger field' choices)."""
@@ -1829,6 +1829,16 @@ def write_native_project(field: str, out_dir, *, name: str | None = None, field_
1829
1829
  wb = meta["walkmesh_bounds"]
1830
1830
  x, z = meta["player_start"]
1831
1831
  scroll = "[camera.scroll]\nenabled = true\n" if meta["scrolling"] else ""
1832
+ # The donor's REAL field id (FBG -> id): a fork MIRRORS this field's geometry, so deploy auto-emits the
1833
+ # ForkDonorPatch `<forkId> <donorId>` -> the engine's name-keyed fidelity (s31 occlusion z-offset,
1834
+ # FieldLocationName) resolves for the custom id. Recorded as `[verbatim_eb] donor` (verbatim) OR
1835
+ # `[field] source_field` (native/synth) -- both read by build._verbatim_donor_id + emitted by deploy_field.
1836
+ from .dialogue import _resolve_field_id as _rfi
1837
+ try:
1838
+ _src_fid = _rfi(field)
1839
+ except (FileNotFoundError, ValueError):
1840
+ _src_fid = None
1841
+ source_field_line = ""
1832
1842
  if verbatim:
1833
1843
  # VERBATIM .eb fork (docs/FORK_FIDELITY.md, the entry-0 carry): ship the donor's WHOLE event script;
1834
1844
  # the build runs the real logic instead of synthesizing. No declarative content (it's all in the .eb).
@@ -1845,11 +1855,7 @@ def write_native_project(field: str, out_dir, *, name: str | None = None, field_
1845
1855
  # Donor battle BGM: a verbatim fork carries the real Battle()/BattleEx() ops, but its custom id misses
1846
1856
  # the engine's (fldMapNo, scene) song lookup -> the boss/special theme is lost. Auto-emit [[battle_bgm]]
1847
1857
  # for the donor's scripted battle scenes whose song is non-zero (build -> a scene-keyed Music: line).
1848
- from .dialogue import _resolve_field_id
1849
- try:
1850
- _donor_fid = _resolve_field_id(field)
1851
- except (FileNotFoundError, ValueError):
1852
- _donor_fid = None
1858
+ _donor_fid = _src_fid
1853
1859
  bgm_pairs = _donor_battle_bgm_pairs(donor_eb, _donor_fid, game)
1854
1860
  bgm_blocks = _render_battle_bgm_blocks(bgm_pairs)
1855
1861
  # retarget the Field() exits: import-chain pre-fills a LIVE table (doors warp into the chain's own
@@ -1894,6 +1900,9 @@ def write_native_project(field: str, out_dir, *, name: str | None = None, field_
1894
1900
  field, game, out_dir=out, name=name, id_remap=id_remap, live_seams=live_seams,
1895
1901
  graft_player_funcs=graft_player_funcs, carry_text=carry_text, graft_savepoint=graft_savepoint)
1896
1902
  meta["imported_content"] = content_summary
1903
+ if _src_fid is not None and _src_fid != field_id: # record the donor -> deploy auto-emits ForkDonorPatch
1904
+ source_field_line = (f"source_field = {_src_fid} # the real field this NATIVE fork mirrors; deploy "
1905
+ f"emits ForkDonorPatch so name-keyed fidelity (occlusion/location) resolves\n")
1897
1906
  control_line = (f"control_direction = {control_dir} # imported WASD-vs-camera tuning\n"
1898
1907
  if control_dir is not None else "")
1899
1908
  content_tail = (
@@ -1913,6 +1922,7 @@ def write_native_project(field: str, out_dir, *, name: str | None = None, field_
1913
1922
  f'name = "{name}"\n'
1914
1923
  f"area = {safe_area}\n"
1915
1924
  f"text_block = {text_block}\n"
1925
+ f"{source_field_line}"
1916
1926
  f"{_walkmesh_hotfix_line(field)}"
1917
1927
  f"{_area_title_hide_lines(meta, verbatim=verbatim)}"
1918
1928
  f'bgs = "scene.bgs.bytes" # NATIVE scene (per-tile depth) -> seamless render, NO .bgx / no tile seams\n'
@@ -32,6 +32,10 @@ class BuildDoc(QWidget):
32
32
  self.pal = pal
33
33
  self.repo = Path(repo_root)
34
34
  self.kit = self.repo / "ff9mapkit" # `-m ff9mapkit build` cwd (local pkg shadows)
35
+ self.kit_cwd = self.kit if self.kit.is_dir() else None # None -> run_job falls back to KIT (always valid)
36
+ # A repo checkout has the deploy scripts at <repo>/tools/; an installed copy (pip/uv/.exe) does NOT,
37
+ # so the test-slot / campaign / journey DEPLOYS (+ the F6 loop) are unavailable there. `build` works either way.
38
+ self.has_tools = jobs.has_deploy_tools(self.repo)
35
39
  self._run = run
36
40
  self._problems = problems
37
41
  self.kind = "field"
@@ -102,7 +106,7 @@ class BuildDoc(QWidget):
102
106
  tid = self.worktree_id or 4003
103
107
  self.rb_test = QRadioButton(f"Test slot {tid} — quick + reversible; play via F6 → Warp"
104
108
  + (" (or New Game → hut door)" if tid == 4003 else ""))
105
- self.rb_test.setChecked(True)
109
+ self.rb_test.setChecked(self.has_tools) # installed copy: no F6 dev engine -> default to Install to game
106
110
  self.rb_game = QRadioButton(f"Install to game (shipping mod folder): {self.game_mod}"
107
111
  if self.game_mod else "Install to game — (game install not found)")
108
112
  if not self.game_mod:
@@ -126,6 +130,10 @@ class BuildDoc(QWidget):
126
130
  self.dest.setWordWrap(True)
127
131
  self.dest.setStyleSheet(f"color:{self.pal['accent']};")
128
132
  gv.addWidget(self.dest)
133
+ if not self.has_tools: # installed: no test-slot/F6 -> default to Install to game / Build only
134
+ self.rb_test.setEnabled(False)
135
+ self.rb_test.setText(self.rb_test.text() + " (dev repo only)")
136
+ (self.rb_game if self.game_mod else self.rb_other).setChecked(True) # safe now: self.dest exists
129
137
  self.field_box = box
130
138
  return box
131
139
 
@@ -375,6 +383,21 @@ class BuildDoc(QWidget):
375
383
  def _info(self, title, text):
376
384
  QMessageBox.information(self, title, text)
377
385
 
386
+ def _require_tools(self, what):
387
+ """Installed (non-repo) copies don't ship the deploy SCRIPTS (tools/). Show a clear, actionable
388
+ message instead of a cryptic 'no such file' and return False; True when the repo tools are present."""
389
+ if self.has_tools:
390
+ return True
391
+ self._warn(
392
+ f"{what} needs the source repo",
393
+ f"'{what}' runs ff9mapkit's development deploy scripts (the repo's tools/), which aren't part of "
394
+ "an installed copy.\n\n"
395
+ "To get a custom field into your game from an installed ff9mapkit:\n"
396
+ " - use Build to -> 'Install to game' (it writes the mod into your FF9 folder; Memoria detects\n"
397
+ " it automatically), then reach it via a [[gateway]] from an early field or by wiring New Game.\n\n"
398
+ "(The test-slot + F6 loop and reversible campaign/journey deploys are a dev-repo workflow.)")
399
+ return False
400
+
378
401
  def _picked(self):
379
402
  f = self.path.text().strip().strip('"')
380
403
  if not f or not Path(f).is_file():
@@ -463,6 +486,8 @@ class BuildDoc(QWidget):
463
486
 
464
487
  def _go_field(self, field):
465
488
  if self.rb_test.isChecked():
489
+ if not self._require_tools("Deploy to test slot"):
490
+ return
466
491
  tid = self.worktree_id or 4003
467
492
  reach = ("New Game → walk to the hut door (or F6 → Warp)" if tid == 4003
468
493
  else f"F6 → Warp to field {tid}")
@@ -477,20 +502,22 @@ class BuildDoc(QWidget):
477
502
  if self._confirm("Install to game",
478
503
  f"Build this field into the game mod folder?\n\n{self.game_mod}\n\n"
479
504
  "Writes the field at its real id (may overwrite a field with the same id)."):
480
- self._stream(jobs.build_argv(field, str(self.game_mod)), cwd=self.kit,
505
+ self._stream(jobs.build_argv(field, str(self.game_mod)), cwd=self.kit_cwd,
481
506
  subject="Install to game", ok_headline=f"Built into {self.game_mod}")
482
507
  else:
483
508
  out = self.other.text().strip()
484
509
  if not out:
485
510
  return self._warn("No folder", "Pick an output folder.")
486
- self._stream(jobs.build_argv(field, out), cwd=self.kit, subject="Build",
511
+ self._stream(jobs.build_argv(field, out), cwd=self.kit_cwd, subject="Build",
487
512
  ok_headline=f"Built into {out}")
488
513
 
489
514
  def _go_campaign(self, path):
490
515
  if self.rb_camp_build.isChecked():
491
- self._stream(jobs.build_campaign_argv(path), cwd=self.kit, subject="Build campaign",
516
+ self._stream(jobs.build_campaign_argv(path), cwd=self.kit_cwd, subject="Build campaign",
492
517
  ok_headline=f"Built campaign {self.plan.name}")
493
518
  return
519
+ if not self._require_tools("Deploy campaign"):
520
+ return
494
521
  wire = self.wire_newgame.isChecked()
495
522
  route = ("It also wires New Game to enter the chain (experimental)." if wire
496
523
  else "Reach each screen in-game via F6 → Warp.")
@@ -505,6 +532,8 @@ class BuildDoc(QWidget):
505
532
  ok_next=f"Relaunch once (new DictionaryPatch), then F6 → Warp → {entry} to walk the chain.")
506
533
 
507
534
  def _go_journey(self, path):
535
+ if not self._require_tools("Deploy journey"):
536
+ return
508
537
  if self.rb_jour_preview.isChecked(): # dry-run: print the playbook, no game writes -> no confirm
509
538
  self._stream(jobs.deploy_journey_argv(self.repo, path), cwd=self.repo,
510
539
  subject="Journey deploy playbook (dry-run)",
@@ -550,6 +579,8 @@ class BuildDoc(QWidget):
550
579
  ok_next=stackmsg)
551
580
 
552
581
  def _go_battle(self, battle):
582
+ if not self._require_tools("Deploy battle map"):
583
+ return
553
584
  trig = self.trigger.text().strip()
554
585
  if trig and not trig.isdigit():
555
586
  return self._warn("Bad trigger field", "Trigger field must be a field id number (or blank).")
@@ -566,6 +597,8 @@ class BuildDoc(QWidget):
566
597
 
567
598
  # ------------------------------------------------------------------ New Game entry (hub-less)
568
599
  def on_set_newgame(self):
600
+ if not self._require_tools("Set New Game entry"):
601
+ return
569
602
  fid = self.newgame_id.text().strip()
570
603
  if not fid.isdigit():
571
604
  return self._warn("Bad field id", "Enter the numeric field id New Game should land on "
@@ -580,6 +613,8 @@ class BuildDoc(QWidget):
580
613
  ok_next="Relaunch the game, then New Game. Undo with 'Revert New Game'.")
581
614
 
582
615
  def on_revert_newgame(self):
616
+ if not self._require_tools("Revert New Game"):
617
+ return
583
618
  argv = jobs.revert_newgame_argv(self.repo) # most-recent New-Game revert (from-stock OR retarget)
584
619
  if argv is None or not Path(argv[-1]).exists():
585
620
  return self._info("Nothing to revert", "No New-Game change to undo yet.")
@@ -590,6 +625,8 @@ class BuildDoc(QWidget):
590
625
 
591
626
  # ------------------------------------------------------------------ Revert
592
627
  def on_revert(self):
628
+ if not self._require_tools("Revert"):
629
+ return
593
630
  if self.kind == "battle":
594
631
  argv, what = jobs.revert_battle_argv(self.repo), "battle"
595
632
  elif self.kind == "campaign":
@@ -30,6 +30,10 @@ class ImportDoc(QWidget):
30
30
  super().__init__()
31
31
  self.pal = pal
32
32
  self.kit = Path(kit_root) # `-m ff9mapkit` cwd (this worktree's package)
33
+ # Default output base: the repo parent for a checkout; an installed copy's package dir is inside a
34
+ # venv, so write forked projects to a discoverable user folder instead (not buried in site-packages).
35
+ self.proj_base = (self.kit.parent if (self.kit / "pyproject.toml").is_file()
36
+ else Path.home() / "Dream World IX")
33
37
  self._run = run
34
38
  self._on_forked = on_forked # called with the output DIR on a clean fork -> shell opens it
35
39
  # The tab body SCROLLS: this view stacks five tall group boxes, so a short window would otherwise
@@ -152,7 +156,7 @@ class ImportDoc(QWidget):
152
156
 
153
157
  out = QHBoxLayout()
154
158
  out.addWidget(QLabel("Write to:"))
155
- self.out = QLineEdit(str(self.kit.parent / "imported"))
159
+ self.out = QLineEdit(str(self.proj_base / "imported"))
156
160
  browse = QPushButton("Browse…")
157
161
  browse.clicked.connect(self.browse_out)
158
162
  out.addWidget(self.out, 1)
@@ -269,7 +273,7 @@ class ImportDoc(QWidget):
269
273
  v.addWidget(swap_hint)
270
274
  out = QHBoxLayout()
271
275
  out.addWidget(QLabel("Write campaign to:"))
272
- self.rg_out = QLineEdit(str(self.kit.parent / "campaign"))
276
+ self.rg_out = QLineEdit(str(self.proj_base / "campaign"))
273
277
  rbrowse = QPushButton("Browse…")
274
278
  rbrowse.clicked.connect(self.browse_region_out)
275
279
  out.addWidget(self.rg_out, 1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ff9mapkit
3
- Version: 1.0.0b4
3
+ Version: 1.0.0b6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ff9mapkit"
7
- version = "1.0.0b4"
7
+ version = "1.0.0b6"
8
8
  description = "Author novel custom field maps for Final Fantasy IX (Memoria engine) from a declarative TOML project file."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11" # tomllib is stdlib from 3.11
@@ -369,3 +369,20 @@ def _w(tmp_path, toml):
369
369
  p = tmp_path / "ok.field.toml"
370
370
  p.write_text(toml, encoding="utf-8")
371
371
  return p
372
+
373
+
374
+ def test_legacy_cutscene_ate_is_deprecated(tmp_path):
375
+ """`[cutscene] ate = true` (the OLD held-banner model) now lint-warns toward the faithful `[[gateway]] ate`
376
+ warp-in trigger -- it still builds (a warning, not an error)."""
377
+ from ff9mapkit.build import FieldProject, lint_logic, validate
378
+ base = ('[field]\nid = 4003\nname = "D"\narea = 11\ntext_block = 1073\n\n'
379
+ '[camera]\npitch = 45\nfov = 42.2\n\n'
380
+ '[walkmesh]\nquad = [[-200,-200],[200,-200],[200,200],[-200,200]]\n\n'
381
+ '[player]\nspawn = [0, 0]\n\n')
382
+ p = tmp_path / "d.field.toml"
383
+ p.write_text(base + '[cutscene]\nate = true\nsteps = [ { say = "An ATE." } ]\n', encoding="utf-8")
384
+ proj = FieldProject.load(p)
385
+ assert any("held-banner" in w and "[[gateway]] ate" in w for w in lint_logic(proj))
386
+ assert validate(proj) == [] # deprecated, but still valid (warning, not error)
387
+ p.write_text(base + '[cutscene]\nsteps = [ { say = "Hi." } ]\n', encoding="utf-8") # no ate -> no warning
388
+ assert not any("held-banner" in w for w in lint_logic(FieldProject.load(p)))
@@ -132,3 +132,53 @@ def test_install_engine_refuses_without_memoria(tmp_path):
132
132
  def test_bundle_missing_dll_raises(tmp_path):
133
133
  with pytest.raises(ValueError):
134
134
  memoria.bundle_dll_members(_make_bundle(tmp_path, complete=False))
135
+
136
+
137
+ # ---- game-install detection (Steam + GOG; rejects MS Store) ----------------------------------------
138
+ def _make_ff9_root(tmp_path, *, launcher=True, streaming=True, managed=True):
139
+ root = tmp_path / "FINAL FANTASY IX"
140
+ root.mkdir(parents=True, exist_ok=True)
141
+ if launcher:
142
+ (root / "FF9_Launcher.exe").write_bytes(b"")
143
+ if streaming:
144
+ (root / "StreamingAssets").mkdir(exist_ok=True)
145
+ if managed:
146
+ (root / "x64" / "FF9_Data" / "Managed").mkdir(parents=True, exist_ok=True)
147
+ return root
148
+
149
+
150
+ def test_is_ff9_root_accepts_real_layout(tmp_path):
151
+ assert config._is_ff9_root(_make_ff9_root(tmp_path)) is True
152
+
153
+
154
+ def test_is_ff9_root_rejects_incomplete(tmp_path):
155
+ assert config._is_ff9_root(_make_ff9_root(tmp_path / "a", launcher=False)) is False # MS Store-like
156
+ assert config._is_ff9_root(_make_ff9_root(tmp_path / "b", streaming=False)) is False
157
+ assert config._is_ff9_root(_make_ff9_root(tmp_path / "c", managed=False)) is False
158
+ assert config._is_ff9_root(tmp_path / "nope") is False
159
+
160
+
161
+ def test_parse_vdf_new_schema():
162
+ text = (
163
+ '"libraryfolders"\n{\n'
164
+ ' "0"\n {\n'
165
+ ' "path" "C:\\\\Program Files (x86)\\\\Steam"\n'
166
+ ' "apps" { "377840" "12345678" }\n' # appid->buildid must NOT be read as a library
167
+ ' }\n'
168
+ ' "1"\n {\n "path" "E:\\\\SteamLibrary"\n }\n}\n'
169
+ )
170
+ assert config._parse_vdf_library_paths(text) == [r"C:\Program Files (x86)\Steam", r"E:\SteamLibrary"]
171
+
172
+
173
+ def test_parse_vdf_old_schema():
174
+ text = '"LibraryFolders"\n{\n "1" "E:\\\\Games\\\\Steam"\n "2" "F:\\\\Steam"\n}\n'
175
+ assert config._parse_vdf_library_paths(text) == [r"E:\Games\Steam", r"F:\Steam"]
176
+
177
+
178
+ # ---- installed vs repo: the GUI deploy-tools gate (PySide6-free; the Qt shell is covered by --smoke) ----
179
+ def test_has_deploy_tools(tmp_path):
180
+ from ff9mapkit.editor import jobs
181
+ assert jobs.has_deploy_tools(tmp_path) is False # installed-like: no tools/ in the wheel
182
+ (tmp_path / "tools").mkdir()
183
+ (tmp_path / "tools" / "deploy_field.py").write_text("", encoding="utf-8")
184
+ assert jobs.has_deploy_tools(tmp_path) is True # repo checkout
@@ -90,6 +90,21 @@ def test_remap_fields_patches_destinations():
90
90
  assert _vb.remap_fields(eb, {}) == eb
91
91
 
92
92
 
93
+ def test_fork_donor_id_reads_native_source_field():
94
+ """deploy_field auto-emits ForkDonorPatch for NATIVE/SYNTH forks too: build._verbatim_donor_id resolves the
95
+ donor from `[field] source_field` (the native import's record), not only `[verbatim_eb] donor`."""
96
+ from ff9mapkit.build import _verbatim_donor_id
97
+
98
+ class _P:
99
+ def __init__(self, raw):
100
+ self.raw = raw
101
+
102
+ assert _verbatim_donor_id(_P({"field": {"source_field": 351}})) == 351 # native fork
103
+ assert _verbatim_donor_id(_P({"verbatim_eb": {"donor": 1860}})) == 1860 # verbatim fork (unchanged)
104
+ assert _verbatim_donor_id(_P({"field": {"borrow_field": 100}})) == 100 # BG-borrow form
105
+ assert _verbatim_donor_id(_P({"field": {}})) is None # a non-fork synth field -> no emit
106
+
107
+
93
108
  def _game_ready():
94
109
  try:
95
110
  import UnityPy # noqa: F401,PLC0415
@@ -136,6 +151,22 @@ def test_import_verbatim_ships_the_whole_donor_eb(tmp_path):
136
151
  assert 4100 in _fields(shipped) and exits[0] not in _fields(shipped)
137
152
 
138
153
 
154
+ @pytest.mark.skipif(not _game_ready(), reason="needs the FF9 install + UnityPy")
155
+ def test_native_import_records_donor_for_forkdonorpatch(tmp_path):
156
+ # A NATIVE (non-verbatim) import now records `[field] source_field = <donor real id>`, so deploy_field can
157
+ # auto-emit ForkDonorPatch (name-keyed occlusion/location fidelity) -- no more hand-written `<fork> <donor>`.
158
+ from ff9mapkit import extract
159
+ from ff9mapkit.build import FieldProject, _verbatim_donor_id
160
+ from ff9mapkit.dialogue import _resolve_field_id
161
+ fbg = "fbg_n06_vgdl_map101_dl_inn_0"
162
+ donor = _resolve_field_id(fbg)
163
+ _m, toml = extract.write_native_project(fbg, tmp_path, name="DV") # native scene, NOT verbatim
164
+ proj = FieldProject.load(toml)
165
+ assert "verbatim_eb" not in proj.raw # it's a native fork
166
+ assert proj.raw["field"].get("source_field") == donor # donor recorded in [field]
167
+ assert _verbatim_donor_id(proj) == donor # -> deploy_field emits ForkDonorPatch
168
+
169
+
139
170
  @pytest.mark.skipif(not _game_ready(), reason="needs the FF9 install + UnityPy")
140
171
  def test_build_field_verbatim_with_logic_edit_end_to_end(tmp_path):
141
172
  # REGRESSION: a FULL build of a verbatim fork must run build_field's per-language loop (which reads
@@ -374,6 +405,34 @@ def test_build_field_verbatim_player_walk_end_to_end(tmp_path):
374
405
  assert (2, 250, 20) in calls # conductor RunScriptSync(2, player=250, 20)
375
406
 
376
407
 
408
+ @pytest.mark.skipif(not _game_ready(), reason="needs the FF9 install + UnityPy")
409
+ def test_build_field_verbatim_conductor_exit_warp_end_to_end(tmp_path):
410
+ # A conductor on a verbatim fork with `exit_warp`: the below-band director ENDS with a fade + Field(target)
411
+ # (the warp-back) instead of EnableMove -- the player is warped out after the scene (the same lever the
412
+ # forced-ATE scene uses to return the player). exit_warp sits OUTSIDE the once-gate so it always fires.
413
+ from ff9mapkit import build, extract
414
+ from ff9mapkit.eb import EbScript
415
+ from ff9mapkit.content import object as _object
416
+ _meta, toml = extract.write_native_project("fbg_n06_vgdl_map101_dl_inn_0", tmp_path, name="DV", verbatim=True)
417
+ donor = EbScript.from_bytes(extract.extract_event_script("fbg_n06_vgdl_map101_dl_inn_0"))
418
+ project = build.FieldProject.load(toml)
419
+ project.raw["npc"] = [{"name": "lefty", "preset": "vivi", "pos": [100, 200], "dialogue": "."}]
420
+ project.raw["cutscene"] = {"once": True, "actor": ["lefty"], "exit_warp": 1153,
421
+ "steps": [{"actor": "lefty", "say": "The scene ends -- and out you go."}]}
422
+ assert build.validate(project) == []
423
+ out = tmp_path / "mod"
424
+ build.build_mod([project], out, mod_name="FF9CustomMap") # must not raise
425
+ band_lo = donor.entry_count - _object.PARTY_BAND_SIZE # lefty=band_lo, conductor=band_lo+1
426
+ ebs = [p for p in out.rglob("*.eb.bytes")]
427
+ assert ebs
428
+ for p in ebs:
429
+ s = EbScript.from_bytes(p.read_bytes())
430
+ cond = s.entry(band_lo + 1).func_by_tag(0)
431
+ ops = [i.op for i in s.instrs(cond)]
432
+ assert 1153 in [i.imm(0) for i in s.instrs(cond) if i.op == 0x2B] # ends with Field(exit_warp) -- the warp-back
433
+ assert 0x2E not in ops # NO EnableMove (the destination restores control)
434
+
435
+
377
436
  @pytest.mark.skipif(not _game_ready(), reason="needs the FF9 install + UnityPy")
378
437
  def test_build_field_verbatim_with_readable_prop_end_to_end(tmp_path):
379
438
  # A readable [[prop]] (dialogue=) ADDED to a verbatim fork: the below-band prop object is NON-bare -- it
File without changes
File without changes