Skip to content

Commit da808bb

Browse files
authored
sprite fixes (#1230)
1 parent 93c7e0f commit da808bb

File tree

3 files changed

+157
-39
lines changed

3 files changed

+157
-39
lines changed

tools/build/sprite/npc_sprite.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from math import floor
44
from sys import argv, path
55
from pathlib import Path
6-
from typing import List, Tuple
6+
from typing import List, Dict, Tuple
77
import xml.etree.ElementTree as ET
88
import png # type: ignore
99

@@ -78,7 +78,9 @@ def from_dir(
7878

7979
palettes = []
8080
palette_names: List[str] = []
81-
for Palette in SpriteSheet.findall("./PaletteList/Palette"):
81+
palette_map: Dict[str, int] = {}
82+
83+
for idx, Palette in enumerate(SpriteSheet.findall("./PaletteList/Palette")):
8284
if asset_stack is not None and load_images:
8385
img_name = Palette.attrib["src"]
8486
img_path = resolve_image_path(sprite_dir, "palettes", img_name, asset_stack)
@@ -91,11 +93,15 @@ def from_dir(
9193

9294
palettes.append(palette)
9395

94-
palette_names.append(Palette.get("name", Palette.attrib["src"].split(".png")[0]))
96+
pal_name = Palette.get("name", Palette.attrib["src"].split(".png")[0])
97+
palette_names.append(pal_name)
98+
palette_map[pal_name] = idx
9599

96100
images = []
97101
image_names: List[str] = []
98-
for Raster in SpriteSheet.findall("./RasterList/Raster"):
102+
image_map: Dict[str, int] = {}
103+
104+
for idx, Raster in enumerate(SpriteSheet.findall("./RasterList/Raster")):
99105
if asset_stack is not None and load_images:
100106
img_name = Raster.attrib["src"]
101107
img_path = resolve_image_path(sprite_dir, "rasters", img_name, asset_stack)
@@ -109,14 +115,19 @@ def from_dir(
109115

110116
images.append(image)
111117

112-
image_names.append(Raster.attrib["src"].split(".png")[0])
118+
img_name = Raster.attrib["src"].split(".png")[0]
119+
image_names.append(img_name)
120+
image_map[img_name] = idx
113121

114122
animations = []
115123
animation_names: List[str] = []
116124
for Animation in SpriteSheet.findall("./AnimationList/Animation"):
125+
# get a mapping of component names -> list indices
126+
comp_map = {comp_xml.attrib["name"]: idx for idx, comp_xml in enumerate(Animation)}
127+
# read each component
117128
comps: List[AnimComponent] = []
118129
for comp_xml in Animation:
119-
comp: AnimComponent = AnimComponent.from_xml(comp_xml)
130+
comp: AnimComponent = AnimComponent.from_xml(comp_xml, comp_map, image_map, palette_map)
120131
comps.append(comp)
121132
animation_names.append(Animation.attrib["name"])
122133
animations.append(comps)

tools/build/sprite/sprites.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -141,20 +141,31 @@ def player_raster_from_xml(xml: ET.Element, back: bool = False) -> PlayerRaster:
141141
)
142142

143143

144-
def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[bytes]:
144+
def player_xml_to_bytes(sprite_xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[bytes]:
145145
out_bytes = b""
146146
back_out_bytes = b""
147147

148-
max_components = int(xml.attrib[MAX_COMPONENTS_XML])
149-
num_variations = int(xml.attrib[PALETTE_GROUPS_XML])
150-
has_back = xml.attrib[HAS_BACK_XML] == "true"
148+
max_components = int(sprite_xml.attrib[MAX_COMPONENTS_XML])
149+
num_variations = int(sprite_xml.attrib[PALETTE_GROUPS_XML])
150+
has_back = sprite_xml.attrib[HAS_BACK_XML] == "true"
151+
152+
anim_elems: List[ET.Element] = sprite_xml.findall("./AnimationList/Animation")
153+
img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster")
154+
pal_elems: List[ET.Element] = sprite_xml.findall("./PaletteList/Palette")
155+
156+
palette_map = {palette_xml.attrib["name"]: idx for idx, palette_xml in enumerate(pal_elems)}
157+
158+
image_map = {image_xml.attrib["name"]: idx for idx, image_xml in enumerate(img_elems)}
151159

152160
# Animations
153161
animations: List[List[AnimComponent]] = []
154-
for anim_xml in xml[2]:
162+
for anim_xml in anim_elems:
163+
# get a mapping of component names -> list indices
164+
comp_map = {comp_xml.attrib["name"]: idx for idx, comp_xml in enumerate(anim_xml)}
165+
# read each component
155166
comps: List[AnimComponent] = []
156167
for comp_xml in anim_xml:
157-
comp: AnimComponent = AnimComponent.from_xml(comp_xml)
168+
comp: AnimComponent = AnimComponent.from_xml(comp_xml, comp_map, image_map, palette_map)
158169
comps.append(comp)
159170
animations.append(comps)
160171

@@ -216,7 +227,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[
216227
palette_list_start_back = len(back_out_bytes) + 0x10
217228
palette_bytes: bytes = b""
218229
palette_bytes_back: bytes = b""
219-
for palette_xml in xml[0]:
230+
for palette_xml in pal_elems:
220231
source = palette_xml.attrib["src"]
221232
front_only = bool(palette_xml.get("front_only", False))
222233
if source not in PALETTE_CACHE:
@@ -252,15 +263,15 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[
252263
raster_bytes: bytes = b""
253264
raster_bytes_back: bytes = b""
254265
raster_offset = 0
255-
for raster_xml in xml[1]:
266+
for raster_xml in img_elems:
256267
r = player_raster_from_xml(raster_xml, back=False)
257268
raster_bytes += struct.pack(">IBBBB", raster_offset, r.width, r.height, r.palette_idx, 0xFF)
258269

259270
raster_offset += r.width * r.height // 2
260271

261272
if has_back:
262273
raster_offset = 0
263-
for raster_xml in xml[1]:
274+
for raster_xml in img_elems:
264275
is_back = False
265276

266277
r = player_raster_from_xml(raster_xml, back=is_back)
@@ -290,7 +301,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[
290301
# Raster file offsets
291302
raster_offsets_bytes = b""
292303
raster_offsets_bytes_back = b""
293-
for i in range(len(xml[1])):
304+
for i in range(len(img_elems)):
294305
raster_offsets_bytes += int.to_bytes(raster_list_start + i * 8, 4, "big")
295306
raster_offsets_bytes_back += int.to_bytes(raster_list_start_back + i * 8, 4, "big")
296307
raster_offsets_bytes += LIST_END_BYTES
@@ -304,7 +315,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[
304315
palette_list_offset_back = len(back_out_bytes) + 0x10
305316
palette_offsets_bytes = b""
306317
palette_offsets_bytes_back = b""
307-
for i, palette_xml in enumerate(xml[0]):
318+
for i, palette_xml in enumerate(pal_elems):
308319
palette_offsets_bytes += int.to_bytes(palette_list_start + i * 0x20, 4, "big")
309320
front_only = bool(palette_xml.attrib.get("front_only", False))
310321
if not front_only:
@@ -358,17 +369,21 @@ def write_player_sprite_header(
358369
sprite_xml = PLAYER_XML_CACHE[sprite_name]
359370
has_back = sprite_xml.attrib[HAS_BACK_XML] == "true"
360371

372+
anim_elems: List[ET.Element] = sprite_xml.findall("./AnimationList/Animation")
373+
img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster")
374+
pal_elems: List[ET.Element] = sprite_xml.findall("./PaletteList/Palette")
375+
361376
player_sprites[f"SPR_{sprite_name}"] = sprite_id
362377
player_rasters[sprite_name] = {}
363378
player_palettes[sprite_name] = {}
364379
player_anims[sprite_name] = {}
365380

366-
for palette_xml in sprite_xml[0]:
381+
for palette_xml in pal_elems:
367382
palette_id = int(palette_xml.attrib["id"], 0x10)
368383
palette_name = palette_xml.attrib["name"]
369384
player_palettes[sprite_name][f"SPR_PAL_{sprite_name}_{palette_name}"] = palette_id
370385

371-
for anim_id, anim_xml in enumerate(sprite_xml[2]):
386+
for anim_id, anim_xml in enumerate(anim_elems):
372387
anim_name = anim_xml.attrib["name"]
373388
if palette_id > 0:
374389
anim_name = f"{palette_name}_{anim_name}"
@@ -377,7 +392,7 @@ def write_player_sprite_header(
377392
)
378393

379394
max_size = 0
380-
for raster_xml in sprite_xml[1]:
395+
for raster_xml in img_elems:
381396
raster_id = int(raster_xml.attrib["id"], 0x10)
382397
raster_name = raster_xml.attrib["name"]
383398
player_rasters[sprite_name][f"SPR_IMG_{sprite_name}_{raster_name}"] = raster_id
@@ -393,7 +408,7 @@ def write_player_sprite_header(
393408
player_sprites[f"SPR_{sprite_name}_Back"] = sprite_id
394409

395410
max_size = 0
396-
for raster_xml in sprite_xml[1]:
411+
for raster_xml in img_elems:
397412
if "back" in raster_xml.attrib:
398413
raster = RASTER_CACHE[raster_xml.attrib["back"][:-4]]
399414
if max_size < raster.size:
@@ -513,8 +528,10 @@ def build_player_rasters(sprite_order: List[str], raster_order: List[str]) -> by
513528
sheet_rtes: List[RasterTableEntry] = []
514529
sheet_rtes_back: List[RasterTableEntry] = []
515530

531+
img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster")
532+
516533
has_back = False
517-
for raster_xml in sprite_xml[1]:
534+
for raster_xml in img_elems:
518535
if "back" in raster_xml.attrib:
519536
has_back = True
520537

tools/splat_ext/sprite_common.py

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ class SetParent(Animation):
200200

201201
def get_attributes(self):
202202
return {
203-
XML_ATTR_INDEX: str(self.index),
203+
XML_ATTR_INDEX: f"{self.index:X}",
204204
}
205205

206206

@@ -214,11 +214,6 @@ def get_attributes(self):
214214
}
215215

216216

217-
@dataclass
218-
class Keyframe(Animation):
219-
pass
220-
221-
222217
@dataclass
223218
class AnimComponent:
224219
x: int
@@ -255,7 +250,7 @@ def parse_commands(command_list: List[int]) -> List[Animation]:
255250
elif cmd_op == CMD.SET_SCALE:
256251
i += 1
257252
elif cmd_op == CMD.LOOP:
258-
dest = command_list[i + 1]
253+
dest = cmd_arg
259254
if dest in boundaries and dest not in labels:
260255
labels[dest] = f"Pos_{dest}"
261256
i += 1
@@ -313,8 +308,8 @@ def to_signed(value):
313308
palette = -1
314309
ret.append(SetPalette(palette))
315310
elif cmd_op == CMD.LOOP:
316-
count = cmd_arg
317-
dest = command_list[i + 1]
311+
dest = cmd_arg
312+
count = command_list[i + 1]
318313
if dest in labels:
319314
lbl_name = labels[dest]
320315
ret.append(Loop(count, lbl_name, 0))
@@ -349,18 +344,27 @@ def animations(self) -> List[Animation]:
349344
return AnimComponent.parse_commands(self.commands)
350345

351346
@staticmethod
352-
def from_xml(xml: ET.Element):
347+
def from_xml(xml: ET.Element, comp_map: Dict[str, int], image_map: Dict[str, int], palette_map: Dict[str, int]):
353348
commands: List[int] = []
354349
labels = {}
355350
for cmd in xml:
356351
if cmd.tag == "Label":
357-
idx = len(commands)
358-
labels[cmd.attrib[XML_ATTR_NAME]] = idx
352+
labels[cmd.attrib[XML_ATTR_NAME]] = len(commands)
359353
elif cmd.tag == "Wait":
360354
duration = int(cmd.attrib[XML_ATTR_DURATION])
361355
commands.append(duration & 0xFFF)
362356
elif cmd.tag == "SetRaster":
363-
raster = int(cmd.attrib[XML_ATTR_INDEX], 0x10)
357+
raster = -1
358+
# prioritize selecting rasters by name, falling back to hardcoded IDs if name attribute is missing
359+
if XML_ATTR_NAME in cmd.attrib:
360+
img_name = cmd.attrib[XML_ATTR_NAME]
361+
if img_name != "":
362+
raster = image_map.get(img_name)
363+
if raster is None:
364+
raise Exception("Undefined raster name for SetRaster: " + img_name)
365+
else:
366+
raster = int(cmd.attrib[XML_ATTR_INDEX], 0x10)
367+
# why is this here? necessary?
364368
if raster == -1:
365369
raster = 0xFFF
366370
commands.append(0x1000 + (raster & 0xFFF))
@@ -393,7 +397,17 @@ def from_xml(xml: ET.Element):
393397
commands.append(0x5000 + mode)
394398
commands.append(percent)
395399
elif cmd.tag == "SetPalette":
396-
palette = int(cmd.attrib[XML_ATTR_INDEX], 0x10)
400+
palette = -1
401+
# prioritize selecting palettes by name, falling back to hardcoded IDs if name attribute is missing
402+
if XML_ATTR_NAME in cmd.attrib:
403+
pal_name = cmd.attrib[XML_ATTR_NAME]
404+
if pal_name != "":
405+
palette = palette_map.get(pal_name)
406+
if palette is None:
407+
raise Exception("Undefined palette name for SetPalette: " + pal_name)
408+
else:
409+
palette = int(cmd.attrib[XML_ATTR_INDEX], 0x10)
410+
# why is this here? necessary?
397411
if palette == -1:
398412
palette = 0xFFF
399413
commands.append(0x6000 + (palette & 0xFFF))
@@ -408,14 +422,90 @@ def from_xml(xml: ET.Element):
408422
if not lbl_name in labels:
409423
raise Exception("Label missing for Loop dest: " + lbl_name)
410424
pos = labels[lbl_name]
411-
commands.append(0x7000 + (count & 0xFFF))
412-
commands.append(pos)
425+
commands.append(0x7000 + (pos & 0xFFF))
426+
commands.append(count)
413427
elif cmd.tag == "Unknown":
414428
commands.append(0x8000 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF))
415429
elif cmd.tag == "SetParent":
416-
commands.append(0x8100 + (int(cmd.attrib[XML_ATTR_INDEX]) & 0xFF))
430+
parent = -1
431+
# prioritize selecting palettes by name, falling back to hardcoded IDs if name attribute is missing
432+
if XML_ATTR_NAME in cmd.attrib:
433+
par_name = cmd.attrib[XML_ATTR_NAME]
434+
if par_name != "":
435+
parent = comp_map.get(par_name)
436+
if parent is None:
437+
raise Exception("Undefined component name for SetParent: " + par_name)
438+
else:
439+
parent = int(cmd.attrib[XML_ATTR_INDEX], 0x10)
440+
if parent == -1:
441+
raise Exception("Invalid component for SetParent: " + par_name)
442+
commands.append(0x8100 + (parent & 0xFF))
417443
elif cmd.tag == "SetNotify":
418444
commands.append(0x8200 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF))
445+
elif cmd.tag == "Keyframe":
446+
# treat keyframes as labels
447+
labels[cmd.attrib[XML_ATTR_NAME]] = len(commands)
448+
# check for non-default transformations
449+
duration = int(cmd.attrib[XML_ATTR_DURATION])
450+
if duration > 0:
451+
if "pos" in cmd.attrib:
452+
dx, dy, dz = map(int, cmd.attrib["pos"].split(","))
453+
if dx != 0 or dy != 0 or dz != 0:
454+
commands.append(0x3000)
455+
commands.append(dx & 0xFFFF)
456+
commands.append(dy & 0xFFFF)
457+
commands.append(dz & 0xFFFF)
458+
if "rot" in cmd.attrib:
459+
rx, ry, rz = map(int, cmd.attrib["rot"].split(","))
460+
if rx != 0 or ry != 0 or rz != 0:
461+
commands.append(0x4000 + (rx & 0xFFF))
462+
commands.append(ry & 0xFFFF)
463+
commands.append(rz & 0xFFFF)
464+
if "scale" in cmd.attrib:
465+
sx, sy, sz = map(int, cmd.attrib["scale"].split(","))
466+
if sx != 100 or sy != 100 or sz != 100:
467+
# check for uniform scale before generating a command for each coord
468+
if sx == sy == sz:
469+
commands.append(0x5000)
470+
commands.append(sx)
471+
else:
472+
if sx != 100:
473+
commands.append(0x5001)
474+
commands.append(sx)
475+
if sy != 100:
476+
commands.append(0x5002)
477+
commands.append(sy)
478+
if sz != 100:
479+
commands.append(0x5003)
480+
commands.append(sz)
481+
# check for img
482+
img_name = cmd.attrib.get("img")
483+
if img_name is not None:
484+
if img_name == "":
485+
raster = -1
486+
else:
487+
raster = image_map.get(img_name)
488+
if raster is None:
489+
raise Exception("Undefined raster for Keyframe: " + img_name)
490+
# why is this here? necessary?
491+
if raster == -1:
492+
raster = 0xFFF
493+
commands.append(0x1000 + (raster & 0xFFF))
494+
# check for pal
495+
pal_name = cmd.attrib.get("pal")
496+
if pal_name is not None:
497+
if pal_name == "":
498+
palette = -1
499+
else:
500+
palette = palette_map.get(pal_name)
501+
if palette is None:
502+
raise Exception("Undefined palette for Keyframe: " + pal_name)
503+
# why is this here? necessary?
504+
if palette == -1:
505+
palette = 0xFFF
506+
commands.append(0x6000 + (palette & 0xFFF))
507+
# append wait command
508+
commands.append(duration & 0xFFF)
419509
elif cmd.tag == "Command": # old Star Rod compatibility
420510
commands.append(int(cmd.attrib["val"], 16))
421511
else:

0 commit comments

Comments
 (0)