ff9mapkit 1.0.0b3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. ff9mapkit/__init__.py +18 -0
  2. ff9mapkit/__main__.py +36 -0
  3. ff9mapkit/_animdb.py +2994 -0
  4. ff9mapkit/_animdb_all.py +14125 -0
  5. ff9mapkit/_fieldtable.py +1516 -0
  6. ff9mapkit/_fieldtext.py +845 -0
  7. ff9mapkit/_held_poses.py +44 -0
  8. ff9mapkit/_itemdb.py +65 -0
  9. ff9mapkit/_modeldb.py +725 -0
  10. ff9mapkit/_narrowmap_data.py +10 -0
  11. ff9mapkit/_npcparams.py +634 -0
  12. ff9mapkit/_regen_animdb.py +72 -0
  13. ff9mapkit/_regen_animdb_all.py +66 -0
  14. ff9mapkit/_regen_fieldtable.py +95 -0
  15. ff9mapkit/_regen_fieldtext.py +66 -0
  16. ff9mapkit/_regen_modeldb.py +67 -0
  17. ff9mapkit/_regen_npcparams.py +123 -0
  18. ff9mapkit/_regen_scenedb.py +57 -0
  19. ff9mapkit/_scenedb.py +869 -0
  20. ff9mapkit/abilities.py +225 -0
  21. ff9mapkit/animations.py +120 -0
  22. ff9mapkit/archetypes.py +218 -0
  23. ff9mapkit/areatitle.py +76 -0
  24. ff9mapkit/battle/__init__.py +21 -0
  25. ff9mapkit/battle/abilityfeatures.py +294 -0
  26. ff9mapkit/battle/actiondelta.py +441 -0
  27. ff9mapkit/battle/aiauthor.py +305 -0
  28. ff9mapkit/battle/ailint.py +140 -0
  29. ff9mapkit/battle/aipatch.py +175 -0
  30. ff9mapkit/battle/battleai.py +148 -0
  31. ff9mapkit/battle/battlecsv.py +390 -0
  32. ff9mapkit/battle/battlepatch.py +395 -0
  33. ff9mapkit/battle/build.py +558 -0
  34. ff9mapkit/battle/camera_codec.py +332 -0
  35. ff9mapkit/battle/camera_data.py +128 -0
  36. ff9mapkit/battle/characterdelta.py +789 -0
  37. ff9mapkit/battle/event_data.py +72 -0
  38. ff9mapkit/battle/extract.py +540 -0
  39. ff9mapkit/battle/fbx.py +223 -0
  40. ff9mapkit/battle/reskin.py +149 -0
  41. ff9mapkit/battle/scene_codec.py +314 -0
  42. ff9mapkit/battle/scene_data.py +369 -0
  43. ff9mapkit/battle/scenelint.py +125 -0
  44. ff9mapkit/battle/seqasm.py +131 -0
  45. ff9mapkit/battle/seqauthor.py +220 -0
  46. ff9mapkit/battle/seqcodec.py +300 -0
  47. ff9mapkit/battle/seqdis.py +106 -0
  48. ff9mapkit/battle/seqpatch.py +137 -0
  49. ff9mapkit/battle_bgm.py +133 -0
  50. ff9mapkit/binutils.py +60 -0
  51. ff9mapkit/build.py +5445 -0
  52. ff9mapkit/campaign.py +1276 -0
  53. ff9mapkit/catalog.py +316 -0
  54. ff9mapkit/chain.py +358 -0
  55. ff9mapkit/cli.py +3114 -0
  56. ff9mapkit/config.py +360 -0
  57. ff9mapkit/content/__init__.py +13 -0
  58. ff9mapkit/content/areatitle.py +36 -0
  59. ff9mapkit/content/ate.py +118 -0
  60. ff9mapkit/content/camera.py +123 -0
  61. ff9mapkit/content/chest.py +186 -0
  62. ff9mapkit/content/choice.py +163 -0
  63. ff9mapkit/content/conductor.py +217 -0
  64. ff9mapkit/content/cutscene.py +290 -0
  65. ff9mapkit/content/encounter.py +41 -0
  66. ff9mapkit/content/entry_settle.py +50 -0
  67. ff9mapkit/content/equipment.py +93 -0
  68. ff9mapkit/content/event.py +191 -0
  69. ff9mapkit/content/gateway.py +101 -0
  70. ff9mapkit/content/inventory.py +59 -0
  71. ff9mapkit/content/itemdata.py +644 -0
  72. ff9mapkit/content/itemtext.py +168 -0
  73. ff9mapkit/content/jump.py +114 -0
  74. ff9mapkit/content/ladder.py +633 -0
  75. ff9mapkit/content/movement.py +53 -0
  76. ff9mapkit/content/music.py +97 -0
  77. ff9mapkit/content/npc.py +348 -0
  78. ff9mapkit/content/object.py +340 -0
  79. ff9mapkit/content/onentry.py +135 -0
  80. ff9mapkit/content/party.py +111 -0
  81. ff9mapkit/content/pathfind.py +138 -0
  82. ff9mapkit/content/platform.py +314 -0
  83. ff9mapkit/content/player.py +168 -0
  84. ff9mapkit/content/prop.py +75 -0
  85. ff9mapkit/content/region.py +340 -0
  86. ff9mapkit/content/reinit.py +59 -0
  87. ff9mapkit/content/savepoint.py +90 -0
  88. ff9mapkit/content/shop.py +178 -0
  89. ff9mapkit/content/sps_trigger.py +66 -0
  90. ff9mapkit/content/startup.py +71 -0
  91. ff9mapkit/content/synthesis.py +106 -0
  92. ff9mapkit/content/text.py +183 -0
  93. ff9mapkit/content/textcarry.py +290 -0
  94. ff9mapkit/content/verbatim.py +86 -0
  95. ff9mapkit/content/walkmesh_hotfix.py +38 -0
  96. ff9mapkit/data/__init__.py +48 -0
  97. ff9mapkit/data/_regen_provenance.py +142 -0
  98. ff9mapkit/data/provenance/blank.es.patch +1 -0
  99. ff9mapkit/data/provenance/blank.fr.patch +1 -0
  100. ff9mapkit/data/provenance/blank.gr.patch +1 -0
  101. ff9mapkit/data/provenance/blank.it.patch +1 -0
  102. ff9mapkit/data/provenance/blank.jp.patch +1 -0
  103. ff9mapkit/data/provenance/blank.uk.patch +1 -0
  104. ff9mapkit/data/provenance/blank.us.patch +1 -0
  105. ff9mapkit/data/provenance/manifest.json +65 -0
  106. ff9mapkit/data/provenance/region_template.patch +1 -0
  107. ff9mapkit/data/reference_arcs.toml +89 -0
  108. ff9mapkit/data/region_catalog.toml +593 -0
  109. ff9mapkit/deploystack.py +358 -0
  110. ff9mapkit/dialogue.py +803 -0
  111. ff9mapkit/eb/__init__.py +12 -0
  112. ff9mapkit/eb/_exprtable.py +59 -0
  113. ff9mapkit/eb/_membertable.py +38 -0
  114. ff9mapkit/eb/_optables.py +537 -0
  115. ff9mapkit/eb/_regen_optables.py +76 -0
  116. ff9mapkit/eb/cmdasm.py +323 -0
  117. ff9mapkit/eb/disasm.py +332 -0
  118. ff9mapkit/eb/edit.py +439 -0
  119. ff9mapkit/eb/exprasm.py +158 -0
  120. ff9mapkit/eb/model.py +178 -0
  121. ff9mapkit/eb/opcodes.py +463 -0
  122. ff9mapkit/eblint.py +177 -0
  123. ff9mapkit/editor/__init__.py +20 -0
  124. ff9mapkit/editor/app.py +950 -0
  125. ff9mapkit/editor/battle_forms.py +240 -0
  126. ff9mapkit/editor/breadcrumb.py +89 -0
  127. ff9mapkit/editor/dialogs.py +116 -0
  128. ff9mapkit/editor/feedback.py +208 -0
  129. ff9mapkit/editor/forms.py +632 -0
  130. ff9mapkit/editor/graphview.py +350 -0
  131. ff9mapkit/editor/jobs.py +342 -0
  132. ff9mapkit/editor/model.py +243 -0
  133. ff9mapkit/editor/picker.py +120 -0
  134. ff9mapkit/editor/theme.py +212 -0
  135. ff9mapkit/eventscan.py +1441 -0
  136. ff9mapkit/extract.py +2279 -0
  137. ff9mapkit/flags.py +693 -0
  138. ff9mapkit/forkreport.py +1383 -0
  139. ff9mapkit/hub.py +477 -0
  140. ff9mapkit/idgated.py +101 -0
  141. ff9mapkit/infohub.py +580 -0
  142. ff9mapkit/items.py +63 -0
  143. ff9mapkit/itemstats.py +346 -0
  144. ff9mapkit/journey.py +1902 -0
  145. ff9mapkit/keyitems.py +93 -0
  146. ff9mapkit/logic_add.py +632 -0
  147. ff9mapkit/logic_edit.py +728 -0
  148. ff9mapkit/logic_map.py +526 -0
  149. ff9mapkit/pack.py +175 -0
  150. ff9mapkit/playerswap.py +231 -0
  151. ff9mapkit/prop_archetypes.py +228 -0
  152. ff9mapkit/provision.py +282 -0
  153. ff9mapkit/refarc.py +825 -0
  154. ff9mapkit/save.py +337 -0
  155. ff9mapkit/save_items.py +1673 -0
  156. ff9mapkit/scene/__init__.py +11 -0
  157. ff9mapkit/scene/arena.py +63 -0
  158. ff9mapkit/scene/bgart.py +140 -0
  159. ff9mapkit/scene/bgi.py +732 -0
  160. ff9mapkit/scene/bgs.py +174 -0
  161. ff9mapkit/scene/bgx.py +185 -0
  162. ff9mapkit/scene/cam.py +345 -0
  163. ff9mapkit/scene/guide.py +311 -0
  164. ff9mapkit/scene/paint.py +506 -0
  165. ff9mapkit/scene/placeholder.py +107 -0
  166. ff9mapkit/sjbinary.py +285 -0
  167. ff9mapkit/sps/__init__.py +17 -0
  168. ff9mapkit/sps/author.py +294 -0
  169. ff9mapkit/sps/catalog.py +88 -0
  170. ff9mapkit/sps/codec.py +264 -0
  171. ff9mapkit/sps/edit.py +184 -0
  172. ff9mapkit/sps/lint.py +58 -0
  173. ff9mapkit/sps/render.py +116 -0
  174. ff9mapkit/sps/templates.py +47 -0
  175. ff9mapkit/sps/texture.py +131 -0
  176. ff9mapkit/walkmesh_hotfixes.py +163 -0
  177. ff9mapkit/workspace/__init__.py +18 -0
  178. ff9mapkit/workspace/battledoc.py +985 -0
  179. ff9mapkit/workspace/builddoc.py +607 -0
  180. ff9mapkit/workspace/forms_qt.py +586 -0
  181. ff9mapkit/workspace/importdoc.py +665 -0
  182. ff9mapkit/workspace/mapview.py +131 -0
  183. ff9mapkit/workspace/palette.py +85 -0
  184. ff9mapkit/workspace/savedoc.py +664 -0
  185. ff9mapkit/workspace/shell.py +6907 -0
  186. ff9mapkit/workspace/style.py +105 -0
  187. ff9mapkit/workspace/tuningdialog.py +223 -0
  188. ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
  189. ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
  190. ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
  191. ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
  192. ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
  193. ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,314 @@
1
+ """Full read-only codec for a BTL_SCENE ``dbfile0000.raw16`` -- the per-scene binary that holds every
2
+ enemy's stats / affinities / rewards + the spawn patterns + the enemy attack table.
3
+
4
+ This is the *parse* side of battle tuning (the SCANNER): it reads the WHOLE scene into named records so a
5
+ tool (or the offline lint suite) can SEE every field -- not just the ~9 that :mod:`scene_data` byte-patches.
6
+ :func:`serialize_scene` re-emits byte-for-byte, including the engine-ignored TAIL after the attack block, so
7
+ ``serialize_scene(parse_scene(x)) == x`` is the golden round-trip that PROVES the offset map (and converts
8
+ the kit's "copy-identity" forks into "codec-identity").
9
+
10
+ Layout authority = Memoria ``Global/BTL_SCENE.cs`` (the ``ReadBattleScene`` BinaryReader order) and
11
+ ``Global/SB2/SB2_MON_PARM.cs``. All multi-byte fields are little-endian. NOTE the disk widths differ from the
12
+ runtime struct: e.g. MaxHP/PhysicalDefence are widened to UInt32/Int32 in memory but are u16/u8 ON DISK
13
+ (`ReadUInt16`/`ReadByte`) -- this codec follows the DISK widths.
14
+
15
+ This file is a pure codec (no Square-Enix bytes); it reads bytes the caller supplies (a forked donor read
16
+ live from the install) and ships nothing.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import struct
21
+ from dataclasses import dataclass, field as _dcfield
22
+
23
+ _HDR = 8
24
+ _PAT = 56
25
+ _MON = 116
26
+ _PUT = 12
27
+ _ATK = 16
28
+
29
+ # ---- struct formats (little-endian; verified to sum to the fixed record sizes) -------------------------
30
+ # SB2_PATTERN (56B): Rate,MonsterCount,Camera,Pad0 (4B) + AP (u32) + 4x SB2_PUT (12B each).
31
+ _PAT_FMT = "<4BI" + "4B4h" * 4
32
+ # SB2_MON_PARM (116B): the exact ReadBattleScene order (BTL_SCENE.cs:54-122).
33
+ _MON_FMT = (
34
+ "<3I" # ResistStatus, AutoStatus, InitialStatus (u32 BattleStatus masks) @0
35
+ "4H" # MaxHP, MaxMP, WinGil, WinExp @12
36
+ "4B" # WinItems[4] (drop item ids; 255=none) @20
37
+ "4B" # StealItems[4] @24
38
+ "H" # Radius @28
39
+ "h" # Geo (i16, model) @30
40
+ "6H" # Mot[6] (movement/attack anim ids) @32
41
+ "2H" # Mesh[2] @44
42
+ "H" # Flags (per-enemy MON flags) @48
43
+ "H" # AP (per-type; the GAMEPLAY AP is the pattern AP) @50
44
+ "8B" # Element: Speed,Strength,Magic,Spirit, pad,trans,cur_capa,max_capa @52
45
+ "5B" # GuardElement,AbsorbElement,HalfElement,WeakElement, Level @60
46
+ "7B" # Category,HitRate,PhysicalDefence,PhysicalEvade,MagicalDefence,MagicalEvade,BlueMagic @65
47
+ "4B" # Bone[4] @72
48
+ "H" # DieSfx @76
49
+ "2B" # Konran, MesCnt @78
50
+ "6B" # IconBone[6] @80
51
+ "6b" # IconY[6] (sbyte) @86
52
+ "6b" # IconZ[6] (sbyte) @92
53
+ "3H" # StartSfx, ShadowX, ShadowZ @98
54
+ "2B" # ShadowBone, WinCard @104
55
+ "2h" # ShadowOfsX, ShadowOfsZ (i16) @106
56
+ "2B" # ShadowBone2, Pad0 @110
57
+ "2H" # Pad1, Pad2 @112
58
+ )
59
+ # AA_DATA (16B): packed info (u32) + 8 single bytes + Vfx2 (u16) + Name (u16).
60
+ _ATK_FMT = "<I8B2H"
61
+
62
+ assert struct.calcsize(_PAT_FMT) == _PAT
63
+ assert struct.calcsize(_MON_FMT) == _MON
64
+ assert struct.calcsize(_ATK_FMT) == _ATK
65
+
66
+
67
+ class SceneCodecError(ValueError):
68
+ pass
69
+
70
+
71
+ @dataclass
72
+ class Put:
73
+ """SB2_PUT -- one formation slot's enemy type + placement."""
74
+ type_no: int
75
+ flags: int
76
+ pease: int
77
+ pad: int
78
+ x: int
79
+ y: int
80
+ z: int
81
+ rot: int
82
+
83
+ @property
84
+ def targetable(self) -> bool:
85
+ return bool(self.flags & 1) # FLG_TARGETABLE
86
+
87
+
88
+ @dataclass
89
+ class Pattern:
90
+ """SB2_PATTERN -- one formation (spawn weight, count, camera, AP, 4 placements)."""
91
+ rate: int
92
+ monster_count: int
93
+ camera: int
94
+ pad0: int
95
+ ap: int
96
+ puts: list # 4x Put
97
+
98
+
99
+ @dataclass
100
+ class MonParm:
101
+ """SB2_MON_PARM -- one enemy TYPE's full record (every disk field, named).
102
+
103
+ The gameplay-relevant fields are scalars; cosmetic/pad/array fields are kept verbatim (so the record
104
+ round-trips) but rarely interesting. Element masks (`guard/absorb/half/weak_element`) and the status
105
+ masks (`resist/auto/initial_status`) are raw ints -- decode with :mod:`scene_data` helpers."""
106
+ resist_status: int
107
+ auto_status: int
108
+ initial_status: int
109
+ hp: int
110
+ mp: int
111
+ gil: int
112
+ exp: int
113
+ drop: tuple # WinItems[4]
114
+ steal: tuple # StealItems[4]
115
+ radius: int
116
+ geo: int
117
+ mot: tuple # [6]
118
+ mesh: tuple # [2]
119
+ flags: int
120
+ ap: int
121
+ speed: int
122
+ strength: int
123
+ magic: int
124
+ spirit: int
125
+ elem_pad: int
126
+ trans: int
127
+ cur_capa: int
128
+ max_capa: int
129
+ guard_element: int
130
+ absorb_element: int
131
+ half_element: int
132
+ weak_element: int
133
+ level: int
134
+ category: int
135
+ hit_rate: int
136
+ phys_def: int
137
+ phys_evade: int
138
+ mag_def: int
139
+ mag_evade: int
140
+ blue_magic: int
141
+ bone: tuple # [4]
142
+ die_sfx: int
143
+ konran: int
144
+ mes_cnt: int
145
+ icon_bone: tuple # [6]
146
+ icon_y: tuple # [6] sbyte
147
+ icon_z: tuple # [6] sbyte
148
+ start_sfx: int
149
+ shadow_x: int
150
+ shadow_z: int
151
+ shadow_bone: int
152
+ win_card: int
153
+ shadow_ofs_x: int
154
+ shadow_ofs_z: int
155
+ shadow_bone2: int
156
+ pad0: int
157
+ pad1: int
158
+ pad2: int
159
+
160
+ @classmethod
161
+ def unpack(cls, buf, off=0) -> "MonParm":
162
+ t = struct.unpack_from(_MON_FMT, buf, off)
163
+ i = iter(t)
164
+ n = lambda: next(i)
165
+ take = lambda k: tuple(next(i) for _ in range(k))
166
+ return cls(
167
+ resist_status=n(), auto_status=n(), initial_status=n(),
168
+ hp=n(), mp=n(), gil=n(), exp=n(),
169
+ drop=take(4), steal=take(4),
170
+ radius=n(), geo=n(), mot=take(6), mesh=take(2),
171
+ flags=n(), ap=n(),
172
+ speed=n(), strength=n(), magic=n(), spirit=n(),
173
+ elem_pad=n(), trans=n(), cur_capa=n(), max_capa=n(),
174
+ guard_element=n(), absorb_element=n(), half_element=n(), weak_element=n(),
175
+ level=n(), category=n(), hit_rate=n(),
176
+ phys_def=n(), phys_evade=n(), mag_def=n(), mag_evade=n(), blue_magic=n(),
177
+ bone=take(4), die_sfx=n(), konran=n(), mes_cnt=n(),
178
+ icon_bone=take(6), icon_y=take(6), icon_z=take(6),
179
+ start_sfx=n(), shadow_x=n(), shadow_z=n(),
180
+ shadow_bone=n(), win_card=n(), shadow_ofs_x=n(), shadow_ofs_z=n(),
181
+ shadow_bone2=n(), pad0=n(), pad1=n(), pad2=n())
182
+
183
+ def pack(self) -> bytes:
184
+ return struct.pack(
185
+ _MON_FMT, self.resist_status, self.auto_status, self.initial_status,
186
+ self.hp, self.mp, self.gil, self.exp, *self.drop, *self.steal,
187
+ self.radius, self.geo, *self.mot, *self.mesh, self.flags, self.ap,
188
+ self.speed, self.strength, self.magic, self.spirit,
189
+ self.elem_pad, self.trans, self.cur_capa, self.max_capa,
190
+ self.guard_element, self.absorb_element, self.half_element, self.weak_element,
191
+ self.level, self.category, self.hit_rate,
192
+ self.phys_def, self.phys_evade, self.mag_def, self.mag_evade, self.blue_magic,
193
+ *self.bone, self.die_sfx, self.konran, self.mes_cnt,
194
+ *self.icon_bone, *self.icon_y, *self.icon_z,
195
+ self.start_sfx, self.shadow_x, self.shadow_z,
196
+ self.shadow_bone, self.win_card, self.shadow_ofs_x, self.shadow_ofs_z,
197
+ self.shadow_bone2, self.pad0, self.pad1, self.pad2)
198
+
199
+
200
+ @dataclass
201
+ class Attack:
202
+ """AA_DATA -- one enemy attack (the per-scene atk[] entries; NOT Actions.csv)."""
203
+ info: int # packed BattleCommandInfo (Target/VfxIndex/ForDead/... )
204
+ script_id: int
205
+ power: int
206
+ elements: int
207
+ rate: int
208
+ category: int
209
+ add_status: int # StatusSetId
210
+ mp: int
211
+ type: int
212
+ vfx2: int
213
+ name: int
214
+
215
+
216
+ @dataclass
217
+ class Scene:
218
+ """A parsed BTL_SCENE. ``head`` keeps the 8 header bytes verbatim (Ver/counts/Flags/pad); ``tail`` is
219
+ every byte after the attack block (overwhelmingly zero + engine-ignored, kept for exact re-emit)."""
220
+ head: bytes
221
+ patterns: list # list[Pattern]
222
+ monsters: list # list[MonParm]
223
+ attacks: list # list[Attack]
224
+ tail: bytes = b""
225
+
226
+ @property
227
+ def pat_count(self) -> int:
228
+ return self.head[1]
229
+
230
+ @property
231
+ def typ_count(self) -> int:
232
+ return self.head[2]
233
+
234
+ @property
235
+ def atk_count(self) -> int:
236
+ return self.head[3]
237
+
238
+ @property
239
+ def back_attack(self) -> bool:
240
+ return bool(self.scene_flags & 2)
241
+
242
+ @property
243
+ def can_escape(self) -> bool:
244
+ return not (self.scene_flags & 32) # Runaway = (flags & 32) == 0
245
+
246
+ @property
247
+ def no_exp(self) -> bool:
248
+ return bool(self.scene_flags & 8)
249
+
250
+ @property
251
+ def preemptive(self) -> bool:
252
+ return bool(self.scene_flags & 1) # SpecialStart
253
+
254
+ @property
255
+ def scene_flags(self) -> int:
256
+ return struct.unpack_from("<H", self.head, 4)[0]
257
+
258
+
259
+ def parse_scene(raw16: bytes) -> Scene:
260
+ """Parse a whole ``dbfile0000.raw16`` into a :class:`Scene` (read-only scanner)."""
261
+ if len(raw16) < _HDR:
262
+ raise SceneCodecError("raw16 too short for a header")
263
+ head = bytes(raw16[:_HDR])
264
+ pat_count, typ_count, atk_count = head[1], head[2], head[3]
265
+ need = _HDR + _PAT * pat_count + _MON * typ_count + _ATK * atk_count
266
+ if len(raw16) < need:
267
+ raise SceneCodecError(f"raw16 truncated: need {need} bytes for "
268
+ f"{pat_count} pattern(s)/{typ_count} type(s)/{atk_count} attack(s), "
269
+ f"have {len(raw16)}")
270
+ off = _HDR
271
+ patterns = []
272
+ for _ in range(pat_count):
273
+ t = struct.unpack_from(_PAT_FMT, raw16, off)
274
+ rate, mc, cam, p0, ap = t[0], t[1], t[2], t[3], t[4]
275
+ puts = []
276
+ for j in range(4):
277
+ b = 5 + j * 8 # 5 scalars before the 4 puts in the tuple
278
+ puts.append(Put(t[b], t[b + 1], t[b + 2], t[b + 3], t[b + 4], t[b + 5], t[b + 6], t[b + 7]))
279
+ patterns.append(Pattern(rate, mc, cam, p0, ap, puts))
280
+ off += _PAT
281
+ monsters = []
282
+ for _ in range(typ_count):
283
+ monsters.append(MonParm.unpack(raw16, off))
284
+ off += _MON
285
+ attacks = []
286
+ for _ in range(atk_count):
287
+ attacks.append(Attack(*struct.unpack_from(_ATK_FMT, raw16, off)))
288
+ off += _ATK
289
+ tail = bytes(raw16[off:])
290
+ return Scene(head=head, patterns=patterns, monsters=monsters, attacks=attacks, tail=tail)
291
+
292
+
293
+ def serialize_scene(scene: Scene) -> bytes:
294
+ """Re-emit a :class:`Scene` to raw16 bytes. ``serialize_scene(parse_scene(x)) == x`` for any valid x."""
295
+ out = bytearray(scene.head)
296
+ for p in scene.patterns:
297
+ puts = []
298
+ for q in p.puts:
299
+ puts += [q.type_no, q.flags, q.pease, q.pad, q.x, q.y, q.z, q.rot]
300
+ out += struct.pack(_PAT_FMT, p.rate, p.monster_count, p.camera, p.pad0, p.ap, *puts)
301
+ for m in scene.monsters:
302
+ out += m.pack()
303
+ for a in scene.attacks:
304
+ out += struct.pack(_ATK_FMT, a.info, a.script_id, a.power, a.elements, a.rate,
305
+ a.category, a.add_status, a.mp, a.type, a.vfx2, a.name)
306
+ out += scene.tail
307
+ return bytes(out)
308
+
309
+
310
+ def scene_counts(raw16: bytes):
311
+ """(pat_count, typ_count, atk_count) -- a cheap header peek without a full parse."""
312
+ if len(raw16) < _HDR:
313
+ raise SceneCodecError("raw16 too short")
314
+ return raw16[1], raw16[2], raw16[3]