From da808bbd171a13165781a25fc00dcb33d55e5dd6 Mon Sep 17 00:00:00 2001 From: z64a <53960937+z64a@users.noreply.github.com> Date: Sat, 8 Mar 2025 23:04:41 -0500 Subject: [PATCH] sprite fixes (#1230) --- tools/build/sprite/npc_sprite.py | 23 ++++-- tools/build/sprite/sprites.py | 49 ++++++++---- tools/splat_ext/sprite_common.py | 124 ++++++++++++++++++++++++++----- 3 files changed, 157 insertions(+), 39 deletions(-) diff --git a/tools/build/sprite/npc_sprite.py b/tools/build/sprite/npc_sprite.py index c0a2c17d13..3544193f07 100644 --- a/tools/build/sprite/npc_sprite.py +++ b/tools/build/sprite/npc_sprite.py @@ -3,7 +3,7 @@ from math import floor from sys import argv, path from pathlib import Path -from typing import List, Tuple +from typing import List, Dict, Tuple import xml.etree.ElementTree as ET import png # type: ignore @@ -78,7 +78,9 @@ def from_dir( palettes = [] palette_names: List[str] = [] - for Palette in SpriteSheet.findall("./PaletteList/Palette"): + palette_map: Dict[str, int] = {} + + for idx, Palette in enumerate(SpriteSheet.findall("./PaletteList/Palette")): if asset_stack is not None and load_images: img_name = Palette.attrib["src"] img_path = resolve_image_path(sprite_dir, "palettes", img_name, asset_stack) @@ -91,11 +93,15 @@ def from_dir( palettes.append(palette) - palette_names.append(Palette.get("name", Palette.attrib["src"].split(".png")[0])) + pal_name = Palette.get("name", Palette.attrib["src"].split(".png")[0]) + palette_names.append(pal_name) + palette_map[pal_name] = idx images = [] image_names: List[str] = [] - for Raster in SpriteSheet.findall("./RasterList/Raster"): + image_map: Dict[str, int] = {} + + for idx, Raster in enumerate(SpriteSheet.findall("./RasterList/Raster")): if asset_stack is not None and load_images: img_name = Raster.attrib["src"] img_path = resolve_image_path(sprite_dir, "rasters", img_name, asset_stack) @@ -109,14 +115,19 @@ def from_dir( images.append(image) - image_names.append(Raster.attrib["src"].split(".png")[0]) + img_name = Raster.attrib["src"].split(".png")[0] + image_names.append(img_name) + image_map[img_name] = idx animations = [] animation_names: List[str] = [] for Animation in SpriteSheet.findall("./AnimationList/Animation"): + # get a mapping of component names -> list indices + comp_map = {comp_xml.attrib["name"]: idx for idx, comp_xml in enumerate(Animation)} + # read each component comps: List[AnimComponent] = [] for comp_xml in Animation: - comp: AnimComponent = AnimComponent.from_xml(comp_xml) + comp: AnimComponent = AnimComponent.from_xml(comp_xml, comp_map, image_map, palette_map) comps.append(comp) animation_names.append(Animation.attrib["name"]) animations.append(comps) diff --git a/tools/build/sprite/sprites.py b/tools/build/sprite/sprites.py index 393a5635b4..6cc5f59f0c 100755 --- a/tools/build/sprite/sprites.py +++ b/tools/build/sprite/sprites.py @@ -141,20 +141,31 @@ def player_raster_from_xml(xml: ET.Element, back: bool = False) -> PlayerRaster: ) -def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[bytes]: +def player_xml_to_bytes(sprite_xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[bytes]: out_bytes = b"" back_out_bytes = b"" - max_components = int(xml.attrib[MAX_COMPONENTS_XML]) - num_variations = int(xml.attrib[PALETTE_GROUPS_XML]) - has_back = xml.attrib[HAS_BACK_XML] == "true" + max_components = int(sprite_xml.attrib[MAX_COMPONENTS_XML]) + num_variations = int(sprite_xml.attrib[PALETTE_GROUPS_XML]) + has_back = sprite_xml.attrib[HAS_BACK_XML] == "true" + + anim_elems: List[ET.Element] = sprite_xml.findall("./AnimationList/Animation") + img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster") + pal_elems: List[ET.Element] = sprite_xml.findall("./PaletteList/Palette") + + palette_map = {palette_xml.attrib["name"]: idx for idx, palette_xml in enumerate(pal_elems)} + + image_map = {image_xml.attrib["name"]: idx for idx, image_xml in enumerate(img_elems)} # Animations animations: List[List[AnimComponent]] = [] - for anim_xml in xml[2]: + for anim_xml in anim_elems: + # get a mapping of component names -> list indices + comp_map = {comp_xml.attrib["name"]: idx for idx, comp_xml in enumerate(anim_xml)} + # read each component comps: List[AnimComponent] = [] for comp_xml in anim_xml: - comp: AnimComponent = AnimComponent.from_xml(comp_xml) + comp: AnimComponent = AnimComponent.from_xml(comp_xml, comp_map, image_map, palette_map) comps.append(comp) animations.append(comps) @@ -216,7 +227,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[ palette_list_start_back = len(back_out_bytes) + 0x10 palette_bytes: bytes = b"" palette_bytes_back: bytes = b"" - for palette_xml in xml[0]: + for palette_xml in pal_elems: source = palette_xml.attrib["src"] front_only = bool(palette_xml.get("front_only", False)) if source not in PALETTE_CACHE: @@ -252,7 +263,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[ raster_bytes: bytes = b"" raster_bytes_back: bytes = b"" raster_offset = 0 - for raster_xml in xml[1]: + for raster_xml in img_elems: r = player_raster_from_xml(raster_xml, back=False) raster_bytes += struct.pack(">IBBBB", raster_offset, r.width, r.height, r.palette_idx, 0xFF) @@ -260,7 +271,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[ if has_back: raster_offset = 0 - for raster_xml in xml[1]: + for raster_xml in img_elems: is_back = False 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[ # Raster file offsets raster_offsets_bytes = b"" raster_offsets_bytes_back = b"" - for i in range(len(xml[1])): + for i in range(len(img_elems)): raster_offsets_bytes += int.to_bytes(raster_list_start + i * 8, 4, "big") raster_offsets_bytes_back += int.to_bytes(raster_list_start_back + i * 8, 4, "big") raster_offsets_bytes += LIST_END_BYTES @@ -304,7 +315,7 @@ def player_xml_to_bytes(xml: ET.Element, asset_stack: Tuple[Path, ...]) -> List[ palette_list_offset_back = len(back_out_bytes) + 0x10 palette_offsets_bytes = b"" palette_offsets_bytes_back = b"" - for i, palette_xml in enumerate(xml[0]): + for i, palette_xml in enumerate(pal_elems): palette_offsets_bytes += int.to_bytes(palette_list_start + i * 0x20, 4, "big") front_only = bool(palette_xml.attrib.get("front_only", False)) if not front_only: @@ -358,17 +369,21 @@ def write_player_sprite_header( sprite_xml = PLAYER_XML_CACHE[sprite_name] has_back = sprite_xml.attrib[HAS_BACK_XML] == "true" + anim_elems: List[ET.Element] = sprite_xml.findall("./AnimationList/Animation") + img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster") + pal_elems: List[ET.Element] = sprite_xml.findall("./PaletteList/Palette") + player_sprites[f"SPR_{sprite_name}"] = sprite_id player_rasters[sprite_name] = {} player_palettes[sprite_name] = {} player_anims[sprite_name] = {} - for palette_xml in sprite_xml[0]: + for palette_xml in pal_elems: palette_id = int(palette_xml.attrib["id"], 0x10) palette_name = palette_xml.attrib["name"] player_palettes[sprite_name][f"SPR_PAL_{sprite_name}_{palette_name}"] = palette_id - for anim_id, anim_xml in enumerate(sprite_xml[2]): + for anim_id, anim_xml in enumerate(anim_elems): anim_name = anim_xml.attrib["name"] if palette_id > 0: anim_name = f"{palette_name}_{anim_name}" @@ -377,7 +392,7 @@ def write_player_sprite_header( ) max_size = 0 - for raster_xml in sprite_xml[1]: + for raster_xml in img_elems: raster_id = int(raster_xml.attrib["id"], 0x10) raster_name = raster_xml.attrib["name"] player_rasters[sprite_name][f"SPR_IMG_{sprite_name}_{raster_name}"] = raster_id @@ -393,7 +408,7 @@ def write_player_sprite_header( player_sprites[f"SPR_{sprite_name}_Back"] = sprite_id max_size = 0 - for raster_xml in sprite_xml[1]: + for raster_xml in img_elems: if "back" in raster_xml.attrib: raster = RASTER_CACHE[raster_xml.attrib["back"][:-4]] if max_size < raster.size: @@ -513,8 +528,10 @@ def build_player_rasters(sprite_order: List[str], raster_order: List[str]) -> by sheet_rtes: List[RasterTableEntry] = [] sheet_rtes_back: List[RasterTableEntry] = [] + img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster") + has_back = False - for raster_xml in sprite_xml[1]: + for raster_xml in img_elems: if "back" in raster_xml.attrib: has_back = True diff --git a/tools/splat_ext/sprite_common.py b/tools/splat_ext/sprite_common.py index e81313c2c4..a2267ea283 100644 --- a/tools/splat_ext/sprite_common.py +++ b/tools/splat_ext/sprite_common.py @@ -200,7 +200,7 @@ class SetParent(Animation): def get_attributes(self): return { - XML_ATTR_INDEX: str(self.index), + XML_ATTR_INDEX: f"{self.index:X}", } @@ -214,11 +214,6 @@ class SetNotify(Animation): } -@dataclass -class Keyframe(Animation): - pass - - @dataclass class AnimComponent: x: int @@ -255,7 +250,7 @@ class AnimComponent: elif cmd_op == CMD.SET_SCALE: i += 1 elif cmd_op == CMD.LOOP: - dest = command_list[i + 1] + dest = cmd_arg if dest in boundaries and dest not in labels: labels[dest] = f"Pos_{dest}" i += 1 @@ -313,8 +308,8 @@ class AnimComponent: palette = -1 ret.append(SetPalette(palette)) elif cmd_op == CMD.LOOP: - count = cmd_arg - dest = command_list[i + 1] + dest = cmd_arg + count = command_list[i + 1] if dest in labels: lbl_name = labels[dest] ret.append(Loop(count, lbl_name, 0)) @@ -349,18 +344,27 @@ class AnimComponent: return AnimComponent.parse_commands(self.commands) @staticmethod - def from_xml(xml: ET.Element): + def from_xml(xml: ET.Element, comp_map: Dict[str, int], image_map: Dict[str, int], palette_map: Dict[str, int]): commands: List[int] = [] labels = {} for cmd in xml: if cmd.tag == "Label": - idx = len(commands) - labels[cmd.attrib[XML_ATTR_NAME]] = idx + labels[cmd.attrib[XML_ATTR_NAME]] = len(commands) elif cmd.tag == "Wait": duration = int(cmd.attrib[XML_ATTR_DURATION]) commands.append(duration & 0xFFF) elif cmd.tag == "SetRaster": - raster = int(cmd.attrib[XML_ATTR_INDEX], 0x10) + raster = -1 + # prioritize selecting rasters by name, falling back to hardcoded IDs if name attribute is missing + if XML_ATTR_NAME in cmd.attrib: + img_name = cmd.attrib[XML_ATTR_NAME] + if img_name != "": + raster = image_map.get(img_name) + if raster is None: + raise Exception("Undefined raster name for SetRaster: " + img_name) + else: + raster = int(cmd.attrib[XML_ATTR_INDEX], 0x10) + # why is this here? necessary? if raster == -1: raster = 0xFFF commands.append(0x1000 + (raster & 0xFFF)) @@ -393,7 +397,17 @@ class AnimComponent: commands.append(0x5000 + mode) commands.append(percent) elif cmd.tag == "SetPalette": - palette = int(cmd.attrib[XML_ATTR_INDEX], 0x10) + palette = -1 + # prioritize selecting palettes by name, falling back to hardcoded IDs if name attribute is missing + if XML_ATTR_NAME in cmd.attrib: + pal_name = cmd.attrib[XML_ATTR_NAME] + if pal_name != "": + palette = palette_map.get(pal_name) + if palette is None: + raise Exception("Undefined palette name for SetPalette: " + pal_name) + else: + palette = int(cmd.attrib[XML_ATTR_INDEX], 0x10) + # why is this here? necessary? if palette == -1: palette = 0xFFF commands.append(0x6000 + (palette & 0xFFF)) @@ -408,14 +422,90 @@ class AnimComponent: if not lbl_name in labels: raise Exception("Label missing for Loop dest: " + lbl_name) pos = labels[lbl_name] - commands.append(0x7000 + (count & 0xFFF)) - commands.append(pos) + commands.append(0x7000 + (pos & 0xFFF)) + commands.append(count) elif cmd.tag == "Unknown": commands.append(0x8000 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF)) elif cmd.tag == "SetParent": - commands.append(0x8100 + (int(cmd.attrib[XML_ATTR_INDEX]) & 0xFF)) + parent = -1 + # prioritize selecting palettes by name, falling back to hardcoded IDs if name attribute is missing + if XML_ATTR_NAME in cmd.attrib: + par_name = cmd.attrib[XML_ATTR_NAME] + if par_name != "": + parent = comp_map.get(par_name) + if parent is None: + raise Exception("Undefined component name for SetParent: " + par_name) + else: + parent = int(cmd.attrib[XML_ATTR_INDEX], 0x10) + if parent == -1: + raise Exception("Invalid component for SetParent: " + par_name) + commands.append(0x8100 + (parent & 0xFF)) elif cmd.tag == "SetNotify": commands.append(0x8200 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF)) + elif cmd.tag == "Keyframe": + # treat keyframes as labels + labels[cmd.attrib[XML_ATTR_NAME]] = len(commands) + # check for non-default transformations + duration = int(cmd.attrib[XML_ATTR_DURATION]) + if duration > 0: + if "pos" in cmd.attrib: + dx, dy, dz = map(int, cmd.attrib["pos"].split(",")) + if dx != 0 or dy != 0 or dz != 0: + commands.append(0x3000) + commands.append(dx & 0xFFFF) + commands.append(dy & 0xFFFF) + commands.append(dz & 0xFFFF) + if "rot" in cmd.attrib: + rx, ry, rz = map(int, cmd.attrib["rot"].split(",")) + if rx != 0 or ry != 0 or rz != 0: + commands.append(0x4000 + (rx & 0xFFF)) + commands.append(ry & 0xFFFF) + commands.append(rz & 0xFFFF) + if "scale" in cmd.attrib: + sx, sy, sz = map(int, cmd.attrib["scale"].split(",")) + if sx != 100 or sy != 100 or sz != 100: + # check for uniform scale before generating a command for each coord + if sx == sy == sz: + commands.append(0x5000) + commands.append(sx) + else: + if sx != 100: + commands.append(0x5001) + commands.append(sx) + if sy != 100: + commands.append(0x5002) + commands.append(sy) + if sz != 100: + commands.append(0x5003) + commands.append(sz) + # check for img + img_name = cmd.attrib.get("img") + if img_name is not None: + if img_name == "": + raster = -1 + else: + raster = image_map.get(img_name) + if raster is None: + raise Exception("Undefined raster for Keyframe: " + img_name) + # why is this here? necessary? + if raster == -1: + raster = 0xFFF + commands.append(0x1000 + (raster & 0xFFF)) + # check for pal + pal_name = cmd.attrib.get("pal") + if pal_name is not None: + if pal_name == "": + palette = -1 + else: + palette = palette_map.get(pal_name) + if palette is None: + raise Exception("Undefined palette for Keyframe: " + pal_name) + # why is this here? necessary? + if palette == -1: + palette = 0xFFF + commands.append(0x6000 + (palette & 0xFFF)) + # append wait command + commands.append(duration & 0xFFF) elif cmd.tag == "Command": # old Star Rod compatibility commands.append(int(cmd.attrib["val"], 16)) else: