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.
- ff9mapkit/__init__.py +18 -0
- ff9mapkit/__main__.py +36 -0
- ff9mapkit/_animdb.py +2994 -0
- ff9mapkit/_animdb_all.py +14125 -0
- ff9mapkit/_fieldtable.py +1516 -0
- ff9mapkit/_fieldtext.py +845 -0
- ff9mapkit/_held_poses.py +44 -0
- ff9mapkit/_itemdb.py +65 -0
- ff9mapkit/_modeldb.py +725 -0
- ff9mapkit/_narrowmap_data.py +10 -0
- ff9mapkit/_npcparams.py +634 -0
- ff9mapkit/_regen_animdb.py +72 -0
- ff9mapkit/_regen_animdb_all.py +66 -0
- ff9mapkit/_regen_fieldtable.py +95 -0
- ff9mapkit/_regen_fieldtext.py +66 -0
- ff9mapkit/_regen_modeldb.py +67 -0
- ff9mapkit/_regen_npcparams.py +123 -0
- ff9mapkit/_regen_scenedb.py +57 -0
- ff9mapkit/_scenedb.py +869 -0
- ff9mapkit/abilities.py +225 -0
- ff9mapkit/animations.py +120 -0
- ff9mapkit/archetypes.py +218 -0
- ff9mapkit/areatitle.py +76 -0
- ff9mapkit/battle/__init__.py +21 -0
- ff9mapkit/battle/abilityfeatures.py +294 -0
- ff9mapkit/battle/actiondelta.py +441 -0
- ff9mapkit/battle/aiauthor.py +305 -0
- ff9mapkit/battle/ailint.py +140 -0
- ff9mapkit/battle/aipatch.py +175 -0
- ff9mapkit/battle/battleai.py +148 -0
- ff9mapkit/battle/battlecsv.py +390 -0
- ff9mapkit/battle/battlepatch.py +395 -0
- ff9mapkit/battle/build.py +558 -0
- ff9mapkit/battle/camera_codec.py +332 -0
- ff9mapkit/battle/camera_data.py +128 -0
- ff9mapkit/battle/characterdelta.py +789 -0
- ff9mapkit/battle/event_data.py +72 -0
- ff9mapkit/battle/extract.py +540 -0
- ff9mapkit/battle/fbx.py +223 -0
- ff9mapkit/battle/reskin.py +149 -0
- ff9mapkit/battle/scene_codec.py +314 -0
- ff9mapkit/battle/scene_data.py +369 -0
- ff9mapkit/battle/scenelint.py +125 -0
- ff9mapkit/battle/seqasm.py +131 -0
- ff9mapkit/battle/seqauthor.py +220 -0
- ff9mapkit/battle/seqcodec.py +300 -0
- ff9mapkit/battle/seqdis.py +106 -0
- ff9mapkit/battle/seqpatch.py +137 -0
- ff9mapkit/battle_bgm.py +133 -0
- ff9mapkit/binutils.py +60 -0
- ff9mapkit/build.py +5445 -0
- ff9mapkit/campaign.py +1276 -0
- ff9mapkit/catalog.py +316 -0
- ff9mapkit/chain.py +358 -0
- ff9mapkit/cli.py +3114 -0
- ff9mapkit/config.py +360 -0
- ff9mapkit/content/__init__.py +13 -0
- ff9mapkit/content/areatitle.py +36 -0
- ff9mapkit/content/ate.py +118 -0
- ff9mapkit/content/camera.py +123 -0
- ff9mapkit/content/chest.py +186 -0
- ff9mapkit/content/choice.py +163 -0
- ff9mapkit/content/conductor.py +217 -0
- ff9mapkit/content/cutscene.py +290 -0
- ff9mapkit/content/encounter.py +41 -0
- ff9mapkit/content/entry_settle.py +50 -0
- ff9mapkit/content/equipment.py +93 -0
- ff9mapkit/content/event.py +191 -0
- ff9mapkit/content/gateway.py +101 -0
- ff9mapkit/content/inventory.py +59 -0
- ff9mapkit/content/itemdata.py +644 -0
- ff9mapkit/content/itemtext.py +168 -0
- ff9mapkit/content/jump.py +114 -0
- ff9mapkit/content/ladder.py +633 -0
- ff9mapkit/content/movement.py +53 -0
- ff9mapkit/content/music.py +97 -0
- ff9mapkit/content/npc.py +348 -0
- ff9mapkit/content/object.py +340 -0
- ff9mapkit/content/onentry.py +135 -0
- ff9mapkit/content/party.py +111 -0
- ff9mapkit/content/pathfind.py +138 -0
- ff9mapkit/content/platform.py +314 -0
- ff9mapkit/content/player.py +168 -0
- ff9mapkit/content/prop.py +75 -0
- ff9mapkit/content/region.py +340 -0
- ff9mapkit/content/reinit.py +59 -0
- ff9mapkit/content/savepoint.py +90 -0
- ff9mapkit/content/shop.py +178 -0
- ff9mapkit/content/sps_trigger.py +66 -0
- ff9mapkit/content/startup.py +71 -0
- ff9mapkit/content/synthesis.py +106 -0
- ff9mapkit/content/text.py +183 -0
- ff9mapkit/content/textcarry.py +290 -0
- ff9mapkit/content/verbatim.py +86 -0
- ff9mapkit/content/walkmesh_hotfix.py +38 -0
- ff9mapkit/data/__init__.py +48 -0
- ff9mapkit/data/_regen_provenance.py +142 -0
- ff9mapkit/data/provenance/blank.es.patch +1 -0
- ff9mapkit/data/provenance/blank.fr.patch +1 -0
- ff9mapkit/data/provenance/blank.gr.patch +1 -0
- ff9mapkit/data/provenance/blank.it.patch +1 -0
- ff9mapkit/data/provenance/blank.jp.patch +1 -0
- ff9mapkit/data/provenance/blank.uk.patch +1 -0
- ff9mapkit/data/provenance/blank.us.patch +1 -0
- ff9mapkit/data/provenance/manifest.json +65 -0
- ff9mapkit/data/provenance/region_template.patch +1 -0
- ff9mapkit/data/reference_arcs.toml +89 -0
- ff9mapkit/data/region_catalog.toml +593 -0
- ff9mapkit/deploystack.py +358 -0
- ff9mapkit/dialogue.py +803 -0
- ff9mapkit/eb/__init__.py +12 -0
- ff9mapkit/eb/_exprtable.py +59 -0
- ff9mapkit/eb/_membertable.py +38 -0
- ff9mapkit/eb/_optables.py +537 -0
- ff9mapkit/eb/_regen_optables.py +76 -0
- ff9mapkit/eb/cmdasm.py +323 -0
- ff9mapkit/eb/disasm.py +332 -0
- ff9mapkit/eb/edit.py +439 -0
- ff9mapkit/eb/exprasm.py +158 -0
- ff9mapkit/eb/model.py +178 -0
- ff9mapkit/eb/opcodes.py +463 -0
- ff9mapkit/eblint.py +177 -0
- ff9mapkit/editor/__init__.py +20 -0
- ff9mapkit/editor/app.py +950 -0
- ff9mapkit/editor/battle_forms.py +240 -0
- ff9mapkit/editor/breadcrumb.py +89 -0
- ff9mapkit/editor/dialogs.py +116 -0
- ff9mapkit/editor/feedback.py +208 -0
- ff9mapkit/editor/forms.py +632 -0
- ff9mapkit/editor/graphview.py +350 -0
- ff9mapkit/editor/jobs.py +342 -0
- ff9mapkit/editor/model.py +243 -0
- ff9mapkit/editor/picker.py +120 -0
- ff9mapkit/editor/theme.py +212 -0
- ff9mapkit/eventscan.py +1441 -0
- ff9mapkit/extract.py +2279 -0
- ff9mapkit/flags.py +693 -0
- ff9mapkit/forkreport.py +1383 -0
- ff9mapkit/hub.py +477 -0
- ff9mapkit/idgated.py +101 -0
- ff9mapkit/infohub.py +580 -0
- ff9mapkit/items.py +63 -0
- ff9mapkit/itemstats.py +346 -0
- ff9mapkit/journey.py +1902 -0
- ff9mapkit/keyitems.py +93 -0
- ff9mapkit/logic_add.py +632 -0
- ff9mapkit/logic_edit.py +728 -0
- ff9mapkit/logic_map.py +526 -0
- ff9mapkit/pack.py +175 -0
- ff9mapkit/playerswap.py +231 -0
- ff9mapkit/prop_archetypes.py +228 -0
- ff9mapkit/provision.py +282 -0
- ff9mapkit/refarc.py +825 -0
- ff9mapkit/save.py +337 -0
- ff9mapkit/save_items.py +1673 -0
- ff9mapkit/scene/__init__.py +11 -0
- ff9mapkit/scene/arena.py +63 -0
- ff9mapkit/scene/bgart.py +140 -0
- ff9mapkit/scene/bgi.py +732 -0
- ff9mapkit/scene/bgs.py +174 -0
- ff9mapkit/scene/bgx.py +185 -0
- ff9mapkit/scene/cam.py +345 -0
- ff9mapkit/scene/guide.py +311 -0
- ff9mapkit/scene/paint.py +506 -0
- ff9mapkit/scene/placeholder.py +107 -0
- ff9mapkit/sjbinary.py +285 -0
- ff9mapkit/sps/__init__.py +17 -0
- ff9mapkit/sps/author.py +294 -0
- ff9mapkit/sps/catalog.py +88 -0
- ff9mapkit/sps/codec.py +264 -0
- ff9mapkit/sps/edit.py +184 -0
- ff9mapkit/sps/lint.py +58 -0
- ff9mapkit/sps/render.py +116 -0
- ff9mapkit/sps/templates.py +47 -0
- ff9mapkit/sps/texture.py +131 -0
- ff9mapkit/walkmesh_hotfixes.py +163 -0
- ff9mapkit/workspace/__init__.py +18 -0
- ff9mapkit/workspace/battledoc.py +985 -0
- ff9mapkit/workspace/builddoc.py +607 -0
- ff9mapkit/workspace/forms_qt.py +586 -0
- ff9mapkit/workspace/importdoc.py +665 -0
- ff9mapkit/workspace/mapview.py +131 -0
- ff9mapkit/workspace/palette.py +85 -0
- ff9mapkit/workspace/savedoc.py +664 -0
- ff9mapkit/workspace/shell.py +6907 -0
- ff9mapkit/workspace/style.py +105 -0
- ff9mapkit/workspace/tuningdialog.py +223 -0
- ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
- ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
- ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
- ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
- ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
- ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Lossless codec for a battle raw17 SFXDataCamera block -- parse -> edit -> RE-SERIALIZE (with the
|
|
2
|
+
offset-table repack), so a minted battle can author a FROM-SCRATCH opening camera (tier ii), not just
|
|
3
|
+
offset the donor's keyframes in place (tier i, ``camera_data.py``).
|
|
4
|
+
|
|
5
|
+
The native plugin reads the camera block at ``camOffset`` (raw17 header's 2nd int16); ``UpdateBSC``
|
|
6
|
+
(SFXDataCamera.cs:131-203) is Memoria's re-serializer of that block and the exact spec this mirrors. The
|
|
7
|
+
block is self-contained from ``camOffset`` to end-of-file, so editing = parse the block, replace one
|
|
8
|
+
camera's sequence, re-serialize, splice ``raw17[:camOffset] + new_block``.
|
|
9
|
+
|
|
10
|
+
Block layout: a UInt16 set-offset table (one per camera; the first entry == table size, so cameraCount =
|
|
11
|
+
table[0]/2), then each camera at its set-offset = ``Flags u16`` + a UInt16 offset entry per present flag
|
|
12
|
+
(HAS_SEQUENCE_0/1/2=0x01/02/04, HAS_UNKNOWN=0x08, HAS_CUSTOM_POSITION=0xF0) + the pointed-at blocks. A
|
|
13
|
+
sequence block is a Code stream: ``frame u16`` (0=end), ``CodeFlags u16``, then conditional sub-blocks
|
|
14
|
+
(cameraPosition 6B, cameraMovement 4B, target 6B+4B, signing 2B, focal 4B, unknown 2/2/4/4 B).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import struct
|
|
19
|
+
|
|
20
|
+
HAS_SEQ = (0x01, 0x02, 0x04)
|
|
21
|
+
HAS_UNKNOWN = 0x08
|
|
22
|
+
HAS_CUSTOM_POSITION = 0xF0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CameraCodecError(ValueError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _u16(b, o):
|
|
30
|
+
return struct.unpack_from("<H", b, o)[0]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ----------------------------------------------------------------- Code stream (one sequence)
|
|
34
|
+
def parse_sequence(b, off, end):
|
|
35
|
+
"""Parse a Code stream [off, end) -> list of Codes. Each Code = dict(frame, flags, block:bytes); the
|
|
36
|
+
terminator is dict(frame=0). ``block`` is the raw conditional sub-block bytes (kept verbatim)."""
|
|
37
|
+
codes = []
|
|
38
|
+
while off < end:
|
|
39
|
+
frame = _u16(b, off); off += 2
|
|
40
|
+
if frame == 0:
|
|
41
|
+
codes.append({"frame": 0})
|
|
42
|
+
return codes, off
|
|
43
|
+
flags = _u16(b, off); off += 2
|
|
44
|
+
blk = off
|
|
45
|
+
size = 0
|
|
46
|
+
if flags & 0x03:
|
|
47
|
+
size += 6 # cameraPosition
|
|
48
|
+
if flags & 0x02:
|
|
49
|
+
size += 4 # cameraMovement
|
|
50
|
+
if flags & 0x04: # HAS_UNKNOWN_1 aborts the reader
|
|
51
|
+
codes.append({"frame": frame, "flags": flags, "block": bytes(b[blk:off]), "abort": True})
|
|
52
|
+
return codes, off
|
|
53
|
+
if flags & 0x18:
|
|
54
|
+
size += 6 # targetPosition
|
|
55
|
+
if flags & 0x10:
|
|
56
|
+
size += 4 # targetMovement
|
|
57
|
+
if flags & 0x20: # HAS_UNKNOWN_2 aborts
|
|
58
|
+
codes.append({"frame": frame, "flags": flags, "block": bytes(b[blk:off]), "abort": True})
|
|
59
|
+
return codes, off
|
|
60
|
+
if flags & 0x40:
|
|
61
|
+
size += 2 # signing
|
|
62
|
+
if flags & 0x200:
|
|
63
|
+
size += 2
|
|
64
|
+
if flags & 0x400:
|
|
65
|
+
size += 2
|
|
66
|
+
if flags & 0x800:
|
|
67
|
+
size += 4 # focal
|
|
68
|
+
if flags & 0x1000:
|
|
69
|
+
size += 4
|
|
70
|
+
if flags & 0x4000:
|
|
71
|
+
size += 2 # setting
|
|
72
|
+
if flags & 0x8000:
|
|
73
|
+
size += 4
|
|
74
|
+
codes.append({"frame": frame, "flags": flags, "block": bytes(b[off:off + size])})
|
|
75
|
+
off += size
|
|
76
|
+
return codes, off
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def serialize_sequence(codes) -> bytes:
|
|
80
|
+
out = bytearray()
|
|
81
|
+
for c in codes:
|
|
82
|
+
out += struct.pack("<H", c["frame"])
|
|
83
|
+
if c["frame"] == 0:
|
|
84
|
+
break
|
|
85
|
+
out += struct.pack("<H", c["flags"]) + c.get("block", b"")
|
|
86
|
+
if c.get("abort"):
|
|
87
|
+
break
|
|
88
|
+
return bytes(out)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ----------------------------------------------------------------- one camera + the whole block
|
|
92
|
+
def _parse_camera(b, base, end):
|
|
93
|
+
"""Parse a camera at [base, end). Returns dict(flags, sequences:[[Code]], unknown:bytes|None,
|
|
94
|
+
position:bytes|None). Blocks are delimited by the offset entries + the camera end -> lossless."""
|
|
95
|
+
flags = _u16(b, base)
|
|
96
|
+
present = [("seq", bit) for bit in HAS_SEQ if flags & bit]
|
|
97
|
+
if flags & HAS_UNKNOWN:
|
|
98
|
+
present.append(("unknown", HAS_UNKNOWN))
|
|
99
|
+
if flags & HAS_CUSTOM_POSITION:
|
|
100
|
+
present.append(("position", HAS_CUSTOM_POSITION))
|
|
101
|
+
offs = [base + _u16(b, base + 2 + i * 2) for i in range(len(present))]
|
|
102
|
+
bounds = offs + [end] # each block runs to the next offset (or camera end)
|
|
103
|
+
cam = {"flags": flags, "sequences": [], "unknown": None, "position": None}
|
|
104
|
+
for i, (kind, _bit) in enumerate(present):
|
|
105
|
+
lo, hi = bounds[i], bounds[i + 1]
|
|
106
|
+
if kind == "seq":
|
|
107
|
+
codes, _ = parse_sequence(b, lo, hi)
|
|
108
|
+
cam["sequences"].append(codes)
|
|
109
|
+
elif kind == "unknown":
|
|
110
|
+
cam["unknown"] = bytes(b[lo:hi])
|
|
111
|
+
else:
|
|
112
|
+
cam["position"] = bytes(b[lo:hi])
|
|
113
|
+
return cam
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_block(raw17):
|
|
117
|
+
"""(camOffset, [camera dicts]) from a raw17. Cameras keep their full structure for lossless re-serialize."""
|
|
118
|
+
cam_off = struct.unpack_from("<h", raw17, 2)[0]
|
|
119
|
+
if cam_off <= 0 or cam_off >= len(raw17):
|
|
120
|
+
raise CameraCodecError(f"bad camOffset {cam_off}")
|
|
121
|
+
table0 = _u16(raw17, cam_off)
|
|
122
|
+
n = table0 // 2
|
|
123
|
+
set_offs = [_u16(raw17, cam_off + 2 * i) for i in range(n)]
|
|
124
|
+
cams = []
|
|
125
|
+
for i in range(n):
|
|
126
|
+
base = cam_off + set_offs[i]
|
|
127
|
+
end = cam_off + (set_offs[i + 1] if i + 1 < n else (len(raw17) - cam_off))
|
|
128
|
+
cams.append(_parse_camera(raw17, base, end))
|
|
129
|
+
return cam_off, cams
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _serialize_camera(cam) -> bytes:
|
|
133
|
+
flags = cam["flags"]
|
|
134
|
+
blocks = []
|
|
135
|
+
for bit in HAS_SEQ:
|
|
136
|
+
if flags & bit:
|
|
137
|
+
blocks.append(serialize_sequence(cam["sequences"][len(blocks)]))
|
|
138
|
+
if flags & HAS_UNKNOWN:
|
|
139
|
+
blocks.append(cam["unknown"] or b"")
|
|
140
|
+
if flags & HAS_CUSTOM_POSITION:
|
|
141
|
+
blocks.append(cam["position"] or b"")
|
|
142
|
+
data_start = 2 + len(blocks) * 2 # flags + one UInt16 offset entry per block
|
|
143
|
+
table = bytearray(len(blocks) * 2)
|
|
144
|
+
body = bytearray()
|
|
145
|
+
cur = data_start
|
|
146
|
+
for i, blk in enumerate(blocks):
|
|
147
|
+
struct.pack_into("<H", table, i * 2, cur)
|
|
148
|
+
body += blk
|
|
149
|
+
cur += len(blk)
|
|
150
|
+
return struct.pack("<H", flags) + bytes(table) + bytes(body)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def serialize_block(cams) -> bytes:
|
|
154
|
+
"""Re-serialize the camera list -> block bytes (set-offset table + cameras), mirroring UpdateBSC."""
|
|
155
|
+
n = len(cams)
|
|
156
|
+
table = bytearray(n * 2)
|
|
157
|
+
body = bytearray()
|
|
158
|
+
cur = n * 2 # cameras start right after the set-offset table
|
|
159
|
+
for i, cam in enumerate(cams):
|
|
160
|
+
struct.pack_into("<H", table, i * 2, cur)
|
|
161
|
+
cb = _serialize_camera(cam)
|
|
162
|
+
body += cb
|
|
163
|
+
cur += len(cb)
|
|
164
|
+
return bytes(table) + bytes(body)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def splice_block(raw17, cams) -> bytes:
|
|
168
|
+
"""raw17 with its camera block replaced by ``serialize_block(cams)`` (camOffset unchanged)."""
|
|
169
|
+
cam_off = struct.unpack_from("<h", raw17, 2)[0]
|
|
170
|
+
return bytes(raw17[:cam_off]) + serialize_block(cams)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ----------------------------------------------------------------- from-scratch keyframe authoring (tier ii)
|
|
174
|
+
# Grounded in the REAL opening-camera grammar shared by every shipping battle (surveyed across EF_R007,
|
|
175
|
+
# BU_R002, CM_R000, BB_R000, CA_E013, AC_E031 -- see tools/dump_battle_camera.py). A real opening is:
|
|
176
|
+
# 1. an ESTABLISHING code @ frame 1: cameraPosition + targetPosition [+ focal], no movement -> the camera
|
|
177
|
+
# is placed instantly at a start pose, looking at a fixed target.
|
|
178
|
+
# 2. a CHAIN of 2-4 MOVEMENT segments: each is cameraPosition + cameraMovement(duration, easing), firing
|
|
179
|
+
# at prev_frame + prev_duration, so the swoop is continuous (durations seen: 15-70 frames).
|
|
180
|
+
# 3. a HANDOFF code (SAVE_FOR_FIXED|SETTING type=1 = SetCameraPhase(1)) a few frames after the last move
|
|
181
|
+
# settles -- THIS is what flips GetCameraPhase()==1 (SFX.cs:1606) and ends the intro; without it the
|
|
182
|
+
# battle hangs (root-caused via an ultracode workflow). + a trailing UNK6(0x21) marker, then END.
|
|
183
|
+
# We reproduce that grammar, ANCHORED on the donor's proven FIXED-CAMERA pose -- its SETTLE pose, i.e. the
|
|
184
|
+
# LAST cameraPosition code's pose + the LAST targetPosition (the on-fight look-at). That settle pose is what
|
|
185
|
+
# the battle saves via SAVE_FOR_FIXED and uses as its normal camera, so it is GUARANTEED to frame the fight.
|
|
186
|
+
#
|
|
187
|
+
# THE ORIGIN MATTERS AS MUCH AS THE MOTION. The battle centre is the world origin (0,0,0), ground at y=0;
|
|
188
|
+
# the default cameras (BattleMapCameraController) sit ~4500-5900 world units out -> a settle distance byte
|
|
189
|
+
# ~0x0a-0x17 = ~4500-5900w, so 1 distance unit ~= 450-500 world units, NOT the 63 the SFXDataCamera comment
|
|
190
|
+
# guesses. Crucially, the camera distance is measured FROM THE TARGET, so the target's own offset is part of
|
|
191
|
+
# the framing -- zeroing it (or freezing the far establishing target) mis-origins the shot. Rather than
|
|
192
|
+
# reverse-engineer the closed plugin's absolute scale, keyframes are expressed RELATIVE to the proven settle
|
|
193
|
+
# pose: yaw/pitch/roll are degree OFFSETS and `zoom` is a distance MULTIPLIER (consistent with tier i's
|
|
194
|
+
# camera_yaw/pitch/zoom). So offset 0 / zoom 1 == the game's normal framing -- a sweep can't be mis-origined
|
|
195
|
+
# or super-zoomed; it orbits/cranes around the exact shot the battle settles into.
|
|
196
|
+
_EASE = {"linear": 0, "in": 1, "out": 2, "sinusin": 1, "sinusout": 2}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _split_code(flags, block):
|
|
200
|
+
"""Slice a donor Code's ``block`` (the bytes after frame+flags) into named sub-blocks, per the exact
|
|
201
|
+
field order of SFXDataCamera.Sequence.Read. Returns {campos, cammove, tgtpos, tgtmove, focal, ...}
|
|
202
|
+
with each value the verbatim bytes (or None). Lossless for the fields a normal opening uses."""
|
|
203
|
+
o, p = 0, {}
|
|
204
|
+
if flags & 0x03:
|
|
205
|
+
p["campos"] = bytes(block[o:o + 6]); o += 6
|
|
206
|
+
if flags & 0x02:
|
|
207
|
+
p["cammove"] = bytes(block[o:o + 4]); o += 4
|
|
208
|
+
if flags & 0x04:
|
|
209
|
+
return p # HAS_UNKNOWN_1 aborts the reader
|
|
210
|
+
if flags & 0x18:
|
|
211
|
+
p["tgtpos"] = bytes(block[o:o + 6]); o += 6
|
|
212
|
+
if flags & 0x10:
|
|
213
|
+
p["tgtmove"] = bytes(block[o:o + 4]); o += 4
|
|
214
|
+
if flags & 0x20:
|
|
215
|
+
return p
|
|
216
|
+
if flags & 0x40:
|
|
217
|
+
p["sign"] = bytes(block[o:o + 2]); o += 2
|
|
218
|
+
if flags & 0x200:
|
|
219
|
+
p["unk3"] = bytes(block[o:o + 2]); o += 2
|
|
220
|
+
if flags & 0x400:
|
|
221
|
+
p["unk4"] = bytes(block[o:o + 2]); o += 2
|
|
222
|
+
if flags & 0x800:
|
|
223
|
+
p["focal"] = bytes(block[o:o + 4]); o += 4
|
|
224
|
+
if flags & 0x1000:
|
|
225
|
+
p["unk5"] = bytes(block[o:o + 4]); o += 4
|
|
226
|
+
if flags & 0x4000:
|
|
227
|
+
p["setting"] = bytes(block[o:o + 2]); o += 2
|
|
228
|
+
if flags & 0x8000:
|
|
229
|
+
p["unk6"] = bytes(block[o:o + 4]); o += 4
|
|
230
|
+
return p
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _pose_bytes(base6, kf):
|
|
234
|
+
"""A new 6-byte cameraPosition by ADJUSTING the proven settle pose ``base6`` (code,flags,pitch,ori,roll,
|
|
235
|
+
dist): ``yaw``/``pitch``/``roll`` are degree OFFSETS (pitch/roll 0x80=360, orientation 0x40=360) and
|
|
236
|
+
``zoom`` is a distance MULTIPLIER. Offset 0 / zoom 1 reproduces ``base6`` byte-for-byte. The pitch/roll
|
|
237
|
+
HIGH BIT (the plugin's signed-rotation convention) is preserved."""
|
|
238
|
+
out = bytearray(base6)
|
|
239
|
+
p_off = round(float(kf.get("pitch", 0)) / 360.0 * 0x80)
|
|
240
|
+
y_off = round(float(kf.get("yaw", 0)) / 360.0 * 0x40)
|
|
241
|
+
r_off = round(float(kf.get("roll", 0)) / 360.0 * 0x80)
|
|
242
|
+
out[2] = (base6[2] & 0x80) | ((base6[2] + p_off) & 0x7F)
|
|
243
|
+
out[3] = (base6[3] + y_off) % 0x40
|
|
244
|
+
out[4] = (base6[4] & 0x80) | ((base6[4] + r_off) & 0x7F)
|
|
245
|
+
out[5] = max(0, min(255, round(base6[5] * float(kf.get("zoom", 1.0)))))
|
|
246
|
+
return bytes(out)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _settle_pose(donor_codes, pos_idxs):
|
|
250
|
+
"""The donor's proven fixed-camera pose: the LAST cameraPosition code's campos (where the swoop ends ==
|
|
251
|
+
the SAVE_FOR_FIXED snapshot the battle uses) + the LAST targetPosition in the opening (the on-fight
|
|
252
|
+
look-at) + a focal. Returns (base6, target_bytes, focal_bytes)."""
|
|
253
|
+
settle = _split_code(donor_codes[pos_idxs[-1]]["flags"], donor_codes[pos_idxs[-1]]["block"])
|
|
254
|
+
base6 = settle.get("campos", b"\x15\x80\xfb\x19\x80\x0a")
|
|
255
|
+
tgts = [_split_code(c["flags"], c["block"]).get("tgtpos") for c in donor_codes if c.get("frame")]
|
|
256
|
+
tgts = [t for t in tgts if t]
|
|
257
|
+
tgt = tgts[-1] if tgts else b""
|
|
258
|
+
if not settle.get("focal"): # focal may live on the establishing code instead
|
|
259
|
+
est = _split_code(donor_codes[pos_idxs[0]]["flags"], donor_codes[pos_idxs[0]]["block"])
|
|
260
|
+
return base6, tgt, est.get("focal", b"")
|
|
261
|
+
return base6, tgt, settle["focal"]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_sequence(donor_codes, keyframes, *, start_delay=1, handoff_gap=5, default_move=30):
|
|
265
|
+
"""Build a real-grammar opening Code list from ``keyframes`` (ordered), ANCHORED on the donor's proven
|
|
266
|
+
settle pose: the FIRST keyframe is the instant START pose, each later one is a swoop segment that MOVES
|
|
267
|
+
the camera there over ``move`` frames (default ``default_move``), easing ``ease`` (in|out|linear).
|
|
268
|
+
Frames chain automatically. The donor's on-fight target, focal and handoff/terminator codes are kept, so
|
|
269
|
+
the battle starts AND stays framed around the exact shot it settles into.
|
|
270
|
+
|
|
271
|
+
Each keyframe ADJUSTS the settle pose: {yaw?, pitch?, roll? (degree offsets), zoom? (distance multiplier,
|
|
272
|
+
default 1.0), move?, ease?}. The natural last keyframe is {} (offset 0 / zoom 1) == the game's normal
|
|
273
|
+
framing. Needs >= 2 keyframes (a start + at least one move); fewer is static and belongs to tier i."""
|
|
274
|
+
if len(keyframes) < 2:
|
|
275
|
+
raise CameraCodecError("[[scene.camera_keyframes]] needs >= 2 keyframes (a start pose + at least one "
|
|
276
|
+
"move); for a static nudge use [scene] camera_yaw/pitch/zoom instead")
|
|
277
|
+
pos_idxs = [i for i, c in enumerate(donor_codes) if c.get("frame") and (c["flags"] & 0x03)]
|
|
278
|
+
if not pos_idxs:
|
|
279
|
+
raise CameraCodecError("donor opening camera has no cameraPosition code to template from")
|
|
280
|
+
base6, tgt, focal = _settle_pose(donor_codes, pos_idxs)
|
|
281
|
+
# the donor's control/handoff codes = everything after its LAST cameraPosition code (SAVE_FIXED|SETTING
|
|
282
|
+
# SetCameraPhase(1) + the UNK6 marker). Re-framed after the authored sweep settles -> battle starts.
|
|
283
|
+
trailing = [c for c in donor_codes[pos_idxs[-1] + 1:] if c.get("frame")]
|
|
284
|
+
|
|
285
|
+
out = []
|
|
286
|
+
# 1) establishing code @ frame `start_delay` (== 1, like every donor): instant pose + on-fight target + focal
|
|
287
|
+
f0 = 0x01 | (0x08 if tgt else 0) | (0x800 if focal else 0)
|
|
288
|
+
out.append({"frame": start_delay, "flags": f0, "block": _pose_bytes(base6, keyframes[0]) + tgt + focal})
|
|
289
|
+
# 2) chained movement segments (each starts where the previous ended; first starts at the establish frame)
|
|
290
|
+
t = start_delay
|
|
291
|
+
for kf in keyframes[1:]:
|
|
292
|
+
dur = max(1, int(kf.get("move", default_move)))
|
|
293
|
+
ease = _EASE.get(str(kf.get("ease", "out")).lower(), 2)
|
|
294
|
+
fl = 0x03 | (0x08 if tgt else 0) # CAMPOS+CAMMOVE (+ static on-fight TGTPOS)
|
|
295
|
+
blk = _pose_bytes(base6, kf) + struct.pack("<H", dur) + bytes([ease, 0]) + tgt
|
|
296
|
+
out.append({"frame": t, "flags": fl, "block": blk})
|
|
297
|
+
t += dur
|
|
298
|
+
# 3) handoff, re-framed just after the sweep settles (keep the donor's relative spacing)
|
|
299
|
+
if trailing:
|
|
300
|
+
base = trailing[0]["frame"]
|
|
301
|
+
for c in trailing:
|
|
302
|
+
out.append({"frame": t + handoff_gap + (c["frame"] - base), "flags": c["flags"],
|
|
303
|
+
"block": c["block"]})
|
|
304
|
+
else: # synthesize a minimal handoff if the donor had none
|
|
305
|
+
out.append({"frame": t + handoff_gap, "flags": 0x4080, "block": b"\x01\x00"})
|
|
306
|
+
out.append({"frame": t + handoff_gap + 1, "flags": 0x8000, "block": b"\x21\x00\x00\x00"})
|
|
307
|
+
out.append({"frame": 0}) # terminator
|
|
308
|
+
return out
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def author_opening(raw17, cam_indices, keyframes) -> bytes:
|
|
312
|
+
"""Replace the opening camera(s) ``cam_indices`` with a from-scratch sweep from ``keyframes`` (keeping
|
|
313
|
+
each donor camera's static target, focal and handoff). Re-serializes the whole block (offset repack).
|
|
314
|
+
Cameras without a usable cameraPosition opening sequence are skipped (cam[1]/[2] are often empty)."""
|
|
315
|
+
if not keyframes:
|
|
316
|
+
return bytes(raw17)
|
|
317
|
+
cam_off, cams = parse_block(raw17)
|
|
318
|
+
n = len(cams)
|
|
319
|
+
authored = 0
|
|
320
|
+
for idx in cam_indices:
|
|
321
|
+
if not 0 <= idx < n or not cams[idx]["sequences"]:
|
|
322
|
+
continue
|
|
323
|
+
donor = cams[idx]["sequences"][0]
|
|
324
|
+
if not any(c.get("frame") and (c["flags"] & 0x03) for c in donor):
|
|
325
|
+
continue # no cameraPosition to anchor on -> skip (e.g. empty default cam)
|
|
326
|
+
seq = build_sequence(donor, keyframes)
|
|
327
|
+
cams[idx]["sequences"] = [list(seq) for _ in cams[idx]["sequences"]]
|
|
328
|
+
authored += 1
|
|
329
|
+
if authored == 0:
|
|
330
|
+
raise CameraCodecError(f"no opening camera among {list(cam_indices)} had a cameraPosition sequence to "
|
|
331
|
+
f"author from (this raw17 has {n} cameras); pin [scene] camera = 0")
|
|
332
|
+
return splice_block(raw17, cams)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""In-place tweaks to a minted battle's OPENING camera (raw17 SFXDataCamera) -- yaw / pitch / zoom.
|
|
2
|
+
|
|
3
|
+
The native FF9SpecialEffectPlugin.dll reads the raw17 camera keyframes directly (it's a data consumer, not
|
|
4
|
+
a wall -- see project_ff9_battle_backgrounds). The opening shot is ``cameraList[CameraNo]`` where CameraNo =
|
|
5
|
+
the raw16 pattern ``Camera`` byte (0-2; random if >=3). This rotates/tilts/zooms that opening sweep by
|
|
6
|
+
adding a constant offset to every ``cameraPosition`` spherical coord IN PLACE -- no byte-count change, so no
|
|
7
|
+
camera offset-table repack (full keyframe authoring, which CAN change lengths, is a separate tier). Only
|
|
8
|
+
``cameraPosition`` (the camera's own position) is changed, not the target, so the battle stays framed.
|
|
9
|
+
|
|
10
|
+
raw17 camera format: header ``int16 seqBlockOffset; int16 camOffset``; at ``camOffset`` a UInt16 set-offset
|
|
11
|
+
table (one per camera); each camera = ``Flags u16`` + per-flag sequence-offset table + a Code stream
|
|
12
|
+
(``frame u16`` [0=end], ``CodeFlags u16``, then ``cameraPosition``[code,flags,PITCH,ORIENTATION,roll,DIST]
|
|
13
|
+
6B, ``cameraMovement`` 4B, target 6B+4B, ...). pitch/roll 0-0x80 = 360deg; orientation 0-0x40 = 360deg.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
|
|
19
|
+
_PITCH_FULL = 0x80 # pitch/roll: 0..0x80 = 360 degrees
|
|
20
|
+
_YAW_FULL = 0x40 # orientation: 0..0x40 = 360 degrees
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CameraEditError(ValueError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _u16(b, o):
|
|
28
|
+
return struct.unpack_from("<H", b, o)[0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def camera_count(raw17) -> int:
|
|
32
|
+
cam_off = struct.unpack_from("<h", raw17, 2)[0]
|
|
33
|
+
return _u16(raw17, cam_off) // 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _campos_fields(raw17, cam_index):
|
|
37
|
+
"""Yield (pitch_off, orientation_off, distance_off) for every cameraPosition in cameraList[cam_index]."""
|
|
38
|
+
cam_off = struct.unpack_from("<h", raw17, 2)[0]
|
|
39
|
+
n = _u16(raw17, cam_off) // 2
|
|
40
|
+
if not 0 <= cam_index < n:
|
|
41
|
+
return
|
|
42
|
+
base = cam_off + _u16(raw17, cam_off + 2 * cam_index)
|
|
43
|
+
flags = _u16(raw17, base)
|
|
44
|
+
seqs, oo = [], 2
|
|
45
|
+
for bit in (1, 2, 4): # HAS_SEQUENCE_0/1/2
|
|
46
|
+
if flags & bit:
|
|
47
|
+
seqs.append(base + _u16(raw17, base + oo)); oo += 2
|
|
48
|
+
for so in seqs:
|
|
49
|
+
off = so
|
|
50
|
+
while True:
|
|
51
|
+
frame = _u16(raw17, off); off += 2
|
|
52
|
+
if frame == 0:
|
|
53
|
+
break
|
|
54
|
+
fl = _u16(raw17, off); off += 2
|
|
55
|
+
if fl & 3: # HAS_CAMERA_POSITION (6B): code,flags,pitch,ori,roll,dist
|
|
56
|
+
yield (off + 2, off + 3, off + 5); off += 6
|
|
57
|
+
if fl & 2:
|
|
58
|
+
off += 4 # cameraMovement
|
|
59
|
+
if fl & 4:
|
|
60
|
+
break # HAS_UNKNOWN_1 aborts
|
|
61
|
+
if fl & 0x18:
|
|
62
|
+
off += 6 # targetPosition
|
|
63
|
+
if fl & 0x10:
|
|
64
|
+
off += 4 # targetMovement
|
|
65
|
+
if fl & 0x20:
|
|
66
|
+
break
|
|
67
|
+
if fl & 0x40:
|
|
68
|
+
off += 2 # signing
|
|
69
|
+
if fl & 0x200:
|
|
70
|
+
off += 2
|
|
71
|
+
if fl & 0x400:
|
|
72
|
+
off += 2
|
|
73
|
+
if fl & 0x800:
|
|
74
|
+
off += 4 # focal
|
|
75
|
+
if fl & 0x1000:
|
|
76
|
+
off += 4
|
|
77
|
+
if fl & 0x4000:
|
|
78
|
+
off += 2 # setting
|
|
79
|
+
if fl & 0x8000:
|
|
80
|
+
off += 4
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def tweak_opening(raw17, cam_indices, *, yaw_deg=0.0, pitch_deg=0.0, zoom=1.0) -> tuple:
|
|
84
|
+
"""Rotate (yaw_deg around the target), tilt (pitch_deg), and/or zoom (MAGNIFY: ``zoom`` > 1 moves the camera
|
|
85
|
+
CLOSER -- distance / zoom -- so it reads like a real camera zoom, not a distance scale) the opening
|
|
86
|
+
camera(s) ``cam_indices`` in place. Returns ``(new_raw17_bytes, report)`` -- ``report`` is a list of
|
|
87
|
+
human lines (one per touched camera: keyframe count + a representative distance before->after) so the
|
|
88
|
+
build can SURFACE what the tweak did instead of failing silently. Same length (no offset repack)."""
|
|
89
|
+
if zoom <= 0:
|
|
90
|
+
raise CameraEditError(f"camera_zoom {zoom} must be > 0")
|
|
91
|
+
b = bytearray(raw17)
|
|
92
|
+
dyaw = round(yaw_deg / 360.0 * _YAW_FULL)
|
|
93
|
+
dpitch = round(pitch_deg / 360.0 * _PITCH_FULL)
|
|
94
|
+
touched, report = 0, []
|
|
95
|
+
for idx in cam_indices:
|
|
96
|
+
n_kf, d_before, d_after = 0, None, None
|
|
97
|
+
for p_off, o_off, d_off in _campos_fields(b, idx):
|
|
98
|
+
if d_before is None:
|
|
99
|
+
d_before = b[d_off]
|
|
100
|
+
if dpitch:
|
|
101
|
+
b[p_off] = (b[p_off] + dpitch) % _PITCH_FULL
|
|
102
|
+
if dyaw:
|
|
103
|
+
b[o_off] = (b[o_off] + dyaw) % _YAW_FULL
|
|
104
|
+
if zoom != 1.0:
|
|
105
|
+
b[d_off] = max(0, min(255, round(b[d_off] / zoom))) # zoom>1 -> closer (magnify), like a real zoom
|
|
106
|
+
if d_after is None:
|
|
107
|
+
d_after = b[d_off]
|
|
108
|
+
n_kf += 1
|
|
109
|
+
touched += 1
|
|
110
|
+
if n_kf:
|
|
111
|
+
note = f"camera {idx}: {n_kf} keyframe(s)"
|
|
112
|
+
if zoom != 1.0:
|
|
113
|
+
note += f", distance {d_before}->{d_after}"
|
|
114
|
+
if d_before == d_after:
|
|
115
|
+
note += " (no change -- the distance byte had no headroom to scale)"
|
|
116
|
+
report.append(note)
|
|
117
|
+
if touched == 0:
|
|
118
|
+
raise CameraEditError(f"no cameraPosition keyframes found in cameras {list(cam_indices)} "
|
|
119
|
+
f"(this raw17 has {camera_count(raw17)} cameras)")
|
|
120
|
+
return bytes(b), report
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def opening_indices(camera_selector) -> list:
|
|
124
|
+
"""Which cameraList indices a tweak targets. A pinned ``[scene] camera`` 0-2 -> just that one; otherwise
|
|
125
|
+
(unset / >=3 random) -> all three possible openings [0,1,2] so whichever the engine rolls is tweaked."""
|
|
126
|
+
if isinstance(camera_selector, int) and 0 <= camera_selector < 3:
|
|
127
|
+
return [camera_selector]
|
|
128
|
+
return [0, 1, 2]
|