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
ff9mapkit/scene/bgs.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# FF9 field .bgs (BGSCENE_DEF) binary reader -- the BASE-GAME scene container.
|
|
3
|
+
#
|
|
4
|
+
# A real field's background scene ships as <fbg>.bgs.bytes inside StreamingAssets/p0data*.bin.
|
|
5
|
+
# It holds the field's CAMERAS (BGCAM_DEF) + overlays + animations + lights. The kit normally
|
|
6
|
+
# AUTHORS pure-Memoria .bgx scenes; but to IMPORT a real field as an editable base we must READ
|
|
7
|
+
# its native binary .bgs. This module parses the header + the camera list (what camera import
|
|
8
|
+
# needs); overlay/anim parsing can be layered on later for art extraction.
|
|
9
|
+
#
|
|
10
|
+
# Layout -- verified against Memoria source AND byte-exact vs the engine's own .bgx export
|
|
11
|
+
# (offline p0data spike, 2026-06-04: every GRGR camera field matched the FieldCreatorScene .bgx):
|
|
12
|
+
#
|
|
13
|
+
# BGSCENE_DEF.ExtractHeaderData (BGSCENE_DEF.cs:655), little-endian:
|
|
14
|
+
# u16 sceneLength, depthBitShift, animCount, overlayCount, lightCount, cameraCount
|
|
15
|
+
# u32 animOffset, overlayOffset, lightOffset, cameraOffset # cameraOffset is ABSOLUTE
|
|
16
|
+
# i16 orgZ,curZ, orgX,orgY, curX,curY, minX,maxX, minY,maxY, scrX,scrY
|
|
17
|
+
# then cameraCount BGCAM_DEF blocks at cameraOffset, 52 bytes each (BGCAM_DEF.ReadData, cs:17):
|
|
18
|
+
# u16 proj; i16 r[3][3]; i32 t[3]; i16 centerOffset[2]; i16 w,h;
|
|
19
|
+
# i16 vrpMinX,vrpMaxX,vrpMinY,vrpMaxY; i32 depthOffset
|
|
20
|
+
#
|
|
21
|
+
# The kit's Cam stores exactly these fields (cam.Cam: proj, r[3][3], t[3], centerOffset, range=[w,h],
|
|
22
|
+
# viewport=[vrpMinX,vrpMaxX,vrpMinY,vrpMaxY], depthOffset), so a parsed camera drops straight into
|
|
23
|
+
# cam.decompose / cam.to_canvas / bgx.format_bgx_camera.
|
|
24
|
+
from collections import namedtuple
|
|
25
|
+
import struct
|
|
26
|
+
|
|
27
|
+
from .cam import Cam
|
|
28
|
+
|
|
29
|
+
_HEADER = struct.Struct("<6H4I12h") # 6 counts (u16) + 4 offsets (u32) + 12 scene bounds (i16) = 52 B
|
|
30
|
+
_CAMERA = struct.Struct("<H9h3i2h2h4hi") # one BGCAM_DEF block
|
|
31
|
+
HEADER_SIZE = _HEADER.size # 52
|
|
32
|
+
CAMERA_SIZE = _CAMERA.size # 52
|
|
33
|
+
|
|
34
|
+
BgsHeader = namedtuple(
|
|
35
|
+
"BgsHeader",
|
|
36
|
+
"sceneLength depthBitShift animCount overlayCount lightCount cameraCount "
|
|
37
|
+
"animOffset overlayOffset lightOffset cameraOffset bounds",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_header(data: bytes) -> BgsHeader:
|
|
42
|
+
f = _HEADER.unpack_from(data, 0)
|
|
43
|
+
return BgsHeader(*f[:10], bounds=tuple(f[10:22]))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def camera_from_block(buf: bytes, off: int = 0) -> Cam:
|
|
47
|
+
"""Decode one 52-byte BGCAM_DEF block into a kit Cam."""
|
|
48
|
+
f = _CAMERA.unpack_from(buf, off)
|
|
49
|
+
c = Cam()
|
|
50
|
+
c.proj = f[0]
|
|
51
|
+
r = f[1:10]
|
|
52
|
+
c.r = [[r[0], r[1], r[2]], [r[3], r[4], r[5]], [r[6], r[7], r[8]]]
|
|
53
|
+
c.t = list(f[10:13])
|
|
54
|
+
c.centerOffset = list(f[13:15])
|
|
55
|
+
c.range = [f[15], f[16]]
|
|
56
|
+
c.viewport = list(f[17:21]) # vrpMinX, vrpMaxX, vrpMinY, vrpMaxY
|
|
57
|
+
c.depthOffset = f[21]
|
|
58
|
+
return c
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def camera_to_block(cam: Cam) -> bytes:
|
|
62
|
+
"""Encode a kit Cam back into a 52-byte BGCAM_DEF block (round-trip of camera_from_block)."""
|
|
63
|
+
r = cam.r
|
|
64
|
+
return _CAMERA.pack(
|
|
65
|
+
cam.proj,
|
|
66
|
+
r[0][0], r[0][1], r[0][2], r[1][0], r[1][1], r[1][2], r[2][0], r[2][1], r[2][2],
|
|
67
|
+
cam.t[0], cam.t[1], cam.t[2],
|
|
68
|
+
cam.centerOffset[0], cam.centerOffset[1],
|
|
69
|
+
cam.range[0], cam.range[1],
|
|
70
|
+
cam.viewport[0], cam.viewport[1], cam.viewport[2], cam.viewport[3],
|
|
71
|
+
cam.depthOffset,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_cameras(data: bytes) -> list:
|
|
76
|
+
"""All cameras from a .bgs byte blob, as kit Cam objects."""
|
|
77
|
+
h = parse_header(data)
|
|
78
|
+
return [camera_from_block(data, h.cameraOffset + i * CAMERA_SIZE) for i in range(h.cameraCount)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------- overlays + sprites (art)
|
|
82
|
+
# A field's background is a stack of OVERLAYS (depth layers); each overlay is a grid of 16-px TILES,
|
|
83
|
+
# each tile sampling a cell of the upscaled atlas.png. Porting BGSCENE_DEF.ExtractOverlayData/
|
|
84
|
+
# ExtractSpriteData + ExtractHeaderData lets us re-composite the real art offline (see scene.bgart).
|
|
85
|
+
_OVERLAY = struct.Struct("<I HH hhhh hhhh hh hh hh I I I I I") # BGOVERLAY_DEF.ReadData, 56 B
|
|
86
|
+
HEADER12 = struct.Struct("<12h")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _bits(v, start, n):
|
|
90
|
+
return (v >> start) & ((1 << n) - 1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Sprite:
|
|
94
|
+
__slots__ = ("offX", "offY", "depth", "trans", "alpha", "atlasX", "atlasY")
|
|
95
|
+
|
|
96
|
+
def __init__(self, offX, offY, depth, trans, alpha, atlasX=0, atlasY=0):
|
|
97
|
+
self.offX, self.offY, self.depth = offX, offY, depth
|
|
98
|
+
self.trans, self.alpha = trans, alpha
|
|
99
|
+
self.atlasX, self.atlasY = atlasX, atlasY
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Overlay:
|
|
103
|
+
__slots__ = ("curZ", "orgZ", "orgX", "orgY", "w", "h", "camNdx",
|
|
104
|
+
"spriteCount", "locOffset", "prmOffset", "sprites")
|
|
105
|
+
|
|
106
|
+
def __init__(self, **kw):
|
|
107
|
+
for k, v in kw.items():
|
|
108
|
+
setattr(self, k, v)
|
|
109
|
+
self.sprites = []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_overlays(data: bytes):
|
|
113
|
+
"""(header, [Overlay]) — overlays with their tile sprites resolved to atlas grid cells.
|
|
114
|
+
|
|
115
|
+
Sprite atlas cells follow BGSCENE_DEF's upscale layout: a global tile index laid out
|
|
116
|
+
`countPerRow = atlasW // (tile_size+4)`, `atlasX = 2 + i%cpr*(tile_size+4)`, y analogous.
|
|
117
|
+
`tile_size` defaults to 64 (the Steam 4x atlas); pass the real value if it differs."""
|
|
118
|
+
h = parse_header(data)
|
|
119
|
+
overlays = []
|
|
120
|
+
off = h.overlayOffset
|
|
121
|
+
for _ in range(h.overlayCount):
|
|
122
|
+
f = _OVERLAY.unpack_from(data, off)
|
|
123
|
+
off += _OVERLAY.size
|
|
124
|
+
buf, buf2 = f[0], f[17]
|
|
125
|
+
# buf2 packs camNdx (low 8 bits) | isXOffset (1) | viewportNdx (7) | spriteCount (high 16)
|
|
126
|
+
# -- BGOVERLAY_DEF.ReadData. camNdx = which CAMERA paints this overlay (multi-camera fields
|
|
127
|
+
# split their art across cameras), the key to a per-camera backdrop.
|
|
128
|
+
overlays.append(Overlay(
|
|
129
|
+
curZ=_bits(buf, 8, 12), orgZ=_bits(buf, 20, 12),
|
|
130
|
+
orgX=f[3], orgY=f[4], w=f[1], h=f[2], camNdx=_bits(buf2, 0, 8),
|
|
131
|
+
spriteCount=_bits(buf2, 16, 16), locOffset=f[18], prmOffset=f[19]))
|
|
132
|
+
return h, overlays
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
TILE = 16 # one background tile is 16x16 logical (pre-upscale) px (BGSCENE_DEF sprite quad)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def tile_box(sprite, mnX, mnY, upscale: int = 4, tile: int = TILE):
|
|
139
|
+
"""The crop rectangle ``(left, top, right, bottom)`` of one tile-sprite inside its overlay's
|
|
140
|
+
engine-exported ``Overlay{i}.png``.
|
|
141
|
+
|
|
142
|
+
The ``[Export] Field=1`` dump writes each overlay as a tight composite whose pixel (0,0) is the
|
|
143
|
+
overlay's MIN-offset tile (BGSCENE_DEF.cs:570-588), which is exactly where
|
|
144
|
+
``extract.compose_background`` places the whole PNG (``(sOrg+org+min(off))*upscale``, no flip), so
|
|
145
|
+
a tile at ``(offX, offY)`` sits ``(offX-mnX, offY-mnY)`` tiles in -- each ``tile`` px, upscaled.
|
|
146
|
+
Pure arithmetic so the per-tile occlusion split is unit-testable without art."""
|
|
147
|
+
x0 = (sprite.offX - mnX) * upscale
|
|
148
|
+
y0 = (sprite.offY - mnY) * upscale
|
|
149
|
+
return (x0, y0, x0 + tile * upscale, y0 + tile * upscale)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def resolve_sprites(data: bytes, overlays, atlas_w: int, tile_size: int = 64):
|
|
153
|
+
"""Fill each overlay's .sprites (offX/offY/depth + atlas cell). Mutates in place."""
|
|
154
|
+
cpr = atlas_w // (tile_size + 4)
|
|
155
|
+
idx = 0
|
|
156
|
+
for ov in overlays:
|
|
157
|
+
alpha_trans = []
|
|
158
|
+
po = ov.prmOffset
|
|
159
|
+
for _ in range(ov.spriteCount):
|
|
160
|
+
p1, p2 = struct.unpack_from("<II", data, po)
|
|
161
|
+
po += 8
|
|
162
|
+
alpha_trans.append((_bits(p1, 22, 2), _bits(p2, 28, 1))) # alpha, trans (shader)
|
|
163
|
+
lo = ov.locOffset
|
|
164
|
+
for j in range(ov.spriteCount):
|
|
165
|
+
(L,) = struct.unpack_from("<I", data, lo)
|
|
166
|
+
lo += 4
|
|
167
|
+
depth, offY, offX = _bits(L, 0, 12), _bits(L, 12, 10), _bits(L, 22, 10)
|
|
168
|
+
alpha, trans = alpha_trans[j]
|
|
169
|
+
ov.sprites.append(Sprite(
|
|
170
|
+
offX, offY, depth, trans, alpha,
|
|
171
|
+
atlasX=2 + idx % cpr * (tile_size + 4),
|
|
172
|
+
atlasY=2 + idx // cpr * (tile_size + 4)))
|
|
173
|
+
idx += 1
|
|
174
|
+
return overlays
|
ff9mapkit/scene/bgx.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""FF9 pure-Memoria background scene (``.bgx``) text format.
|
|
2
|
+
|
|
3
|
+
A ``.bgx`` is a small line-oriented text file describing how a field's background renders:
|
|
4
|
+
a sequence of blocks (``OVERLAY`` layers, a ``CAMERA``, optional ``ANIMATION`` /
|
|
5
|
+
``USE_BASE_SCENE`` / ``LANGUAGE``). Memoria parses it whitespace-insensitively (blank and
|
|
6
|
+
``#``/``//`` comment lines are skipped), so this module models blocks structurally and emits
|
|
7
|
+
a canonical form. Two helpers cover the kit's needs:
|
|
8
|
+
|
|
9
|
+
* :class:`BgxScene` — parse / serialize / inspect (overlays + camera) / surgically replace.
|
|
10
|
+
* :func:`build` — assemble a scene from a :class:`~ff9mapkit.scene.cam.Cam` + overlays.
|
|
11
|
+
|
|
12
|
+
OVERLAY keys: OverlayId? CameraId ViewportId Position(x,y,z) Size(w,h) Image Shader.
|
|
13
|
+
CAMERA keys : ViewDistance CenterOffset Position Range DepthOffset Viewport
|
|
14
|
+
OrientationMatrix (9) | OrientationAngles (3).
|
|
15
|
+
Overlay Z (Position 3rd value) is the depth: smaller = nearer the camera (drawn in front).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from . import cam as _cam
|
|
23
|
+
|
|
24
|
+
BLOCK_TYPES = ("LANGUAGE", "USE_BASE_SCENE", "OVERLAY", "ANIMATION", "CAMERA")
|
|
25
|
+
DEFAULT_SHADER = "PSX/FieldMap_Abr_None"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Overlay:
|
|
30
|
+
"""One background layer."""
|
|
31
|
+
|
|
32
|
+
image: str
|
|
33
|
+
position: tuple = (0, 0, 0) # x, y, z(depth)
|
|
34
|
+
size: tuple = (384, 448) # w, h
|
|
35
|
+
shader: str = DEFAULT_SHADER
|
|
36
|
+
camera_id: int = 0
|
|
37
|
+
viewport_id: int = 0
|
|
38
|
+
overlay_id: int | None = None
|
|
39
|
+
|
|
40
|
+
def to_lines(self) -> list[str]:
|
|
41
|
+
lines = ["OVERLAY"]
|
|
42
|
+
if self.overlay_id is not None:
|
|
43
|
+
lines.append(f"OverlayId: {self.overlay_id}")
|
|
44
|
+
lines.append(f"CameraId: {self.camera_id}")
|
|
45
|
+
lines.append(f"ViewportId: {self.viewport_id}")
|
|
46
|
+
lines.append(f"Position: {self.position[0]}, {self.position[1]}, {self.position[2]}")
|
|
47
|
+
lines.append(f"Size: {self.size[0]}, {self.size[1]}")
|
|
48
|
+
lines.append(f"Image: {self.image}")
|
|
49
|
+
lines.append(f"Shader: {self.shader}")
|
|
50
|
+
return lines
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_fields(cls, fields: list[tuple]) -> "Overlay":
|
|
54
|
+
o = cls(image="")
|
|
55
|
+
for key, val in fields:
|
|
56
|
+
args = [a.strip() for a in val.split(",")]
|
|
57
|
+
if key == "OverlayId":
|
|
58
|
+
o.overlay_id = int(args[0])
|
|
59
|
+
elif key == "CameraId":
|
|
60
|
+
o.camera_id = int(args[0])
|
|
61
|
+
elif key == "ViewportId":
|
|
62
|
+
o.viewport_id = int(args[0])
|
|
63
|
+
elif key == "Position":
|
|
64
|
+
o.position = (int(args[0]), int(args[1]), int(args[2]))
|
|
65
|
+
elif key == "Size":
|
|
66
|
+
o.size = (int(args[0]), int(args[1]))
|
|
67
|
+
elif key == "Image":
|
|
68
|
+
o.image = val.strip()
|
|
69
|
+
elif key == "Shader":
|
|
70
|
+
o.shader = val.strip()
|
|
71
|
+
return o
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _camera_to_lines(cam: _cam.Cam) -> list[str]:
|
|
75
|
+
# reuse cam.format_bgx_camera (already validated faithful) minus its trailing newline
|
|
76
|
+
return _cam.format_bgx_camera(cam).rstrip("\n").split("\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class _Block:
|
|
81
|
+
type: str
|
|
82
|
+
fields: list = field(default_factory=list) # ordered (key, value) for non-comment lines
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BgxScene:
|
|
86
|
+
"""Structured view of a ``.bgx``: leading comments, ordered blocks (overlays / camera / ...)."""
|
|
87
|
+
|
|
88
|
+
def __init__(self):
|
|
89
|
+
self.header_comments: list[str] = [] # comment lines before the first block
|
|
90
|
+
self.blocks: list[_Block] = []
|
|
91
|
+
|
|
92
|
+
# ---- parse ----
|
|
93
|
+
@classmethod
|
|
94
|
+
def parse(cls, text: str) -> "BgxScene":
|
|
95
|
+
self = cls()
|
|
96
|
+
cur: _Block | None = None
|
|
97
|
+
seen_block = False
|
|
98
|
+
for raw in text.splitlines():
|
|
99
|
+
s = raw.strip()
|
|
100
|
+
if not s:
|
|
101
|
+
continue
|
|
102
|
+
if s.startswith("#") or s.startswith("//"):
|
|
103
|
+
if not seen_block:
|
|
104
|
+
self.header_comments.append(s)
|
|
105
|
+
continue
|
|
106
|
+
if s in BLOCK_TYPES:
|
|
107
|
+
cur = _Block(s)
|
|
108
|
+
self.blocks.append(cur)
|
|
109
|
+
seen_block = True
|
|
110
|
+
continue
|
|
111
|
+
if cur is not None and ":" in s:
|
|
112
|
+
key, _, val = s.partition(":")
|
|
113
|
+
cur.fields.append((key.strip(), val.strip()))
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_file(cls, path) -> "BgxScene":
|
|
118
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
119
|
+
return cls.parse(fh.read())
|
|
120
|
+
|
|
121
|
+
# ---- serialize (canonical) ----
|
|
122
|
+
def to_text(self) -> str:
|
|
123
|
+
out: list[str] = list(self.header_comments)
|
|
124
|
+
for blk in self.blocks:
|
|
125
|
+
if blk.type == "OVERLAY":
|
|
126
|
+
out += self.overlay_of(blk).to_lines()
|
|
127
|
+
elif blk.type == "CAMERA":
|
|
128
|
+
out += _camera_to_lines(self.camera_of(blk))
|
|
129
|
+
else:
|
|
130
|
+
out.append(blk.type)
|
|
131
|
+
out += [f"{k}: {v}" for k, v in blk.fields]
|
|
132
|
+
out.append("") # blank line between blocks
|
|
133
|
+
return "\n".join(out).rstrip("\n") + "\n"
|
|
134
|
+
|
|
135
|
+
# ---- typed views ----
|
|
136
|
+
@staticmethod
|
|
137
|
+
def overlay_of(blk: _Block) -> Overlay:
|
|
138
|
+
return Overlay.from_fields(blk.fields)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def camera_of(blk: _Block) -> _cam.Cam:
|
|
142
|
+
text = "CAMERA\n" + "\n".join(f"{k}: {v}" for k, v in blk.fields) + "\n"
|
|
143
|
+
return _cam.parse_bgx_cameras_text(text)[0]
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def overlays(self) -> list[Overlay]:
|
|
147
|
+
return [self.overlay_of(b) for b in self.blocks if b.type == "OVERLAY"]
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def cameras(self) -> list[_cam.Cam]:
|
|
151
|
+
return [self.camera_of(b) for b in self.blocks if b.type == "CAMERA"]
|
|
152
|
+
|
|
153
|
+
# ---- edits ----
|
|
154
|
+
def set_camera(self, cam: _cam.Cam) -> None:
|
|
155
|
+
"""Replace the (first) CAMERA block's fields from a Cam, preserving everything else."""
|
|
156
|
+
lines = _camera_to_lines(cam)[1:] # drop the "CAMERA" header
|
|
157
|
+
fields = [tuple(s.split(": ", 1)) for s in lines]
|
|
158
|
+
for blk in self.blocks:
|
|
159
|
+
if blk.type == "CAMERA":
|
|
160
|
+
blk.fields = [(k, v) for k, v in fields]
|
|
161
|
+
return
|
|
162
|
+
self.blocks.append(_Block("CAMERA", [(k, v) for k, v in fields]))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build(camera, overlays: list[Overlay], *, header_comment: str | None = None,
|
|
166
|
+
base_scene: str | None = None) -> str:
|
|
167
|
+
"""Assemble a complete ``.bgx`` text from a camera (or list of cameras) + ordered overlays.
|
|
168
|
+
|
|
169
|
+
``camera`` may be a single ``cam.Cam`` (one CAMERA block, unchanged) or a list of Cams for a
|
|
170
|
+
MULTI-CAMERA field -- N CAMERA blocks in order (camera index = list position). Each overlay's
|
|
171
|
+
``camera_id`` selects which camera shows it; the script switches the active camera via
|
|
172
|
+
SetFieldCamera (see content.camera). The engine reads cameras in file order (camIdx 0..N-1)."""
|
|
173
|
+
cameras = camera if isinstance(camera, (list, tuple)) else [camera]
|
|
174
|
+
out: list[str] = []
|
|
175
|
+
if header_comment:
|
|
176
|
+
out.append(f"# {header_comment}")
|
|
177
|
+
if base_scene:
|
|
178
|
+
out += ["USE_BASE_SCENE", f"Name: {base_scene}", ""]
|
|
179
|
+
for ov in overlays:
|
|
180
|
+
out += ov.to_lines()
|
|
181
|
+
out.append("")
|
|
182
|
+
for c in cameras:
|
|
183
|
+
out += _camera_to_lines(c)
|
|
184
|
+
out.append("")
|
|
185
|
+
return "\n".join(out).rstrip("\n") + "\n"
|