sprite fixes (#1230)

This commit is contained in:
z64a 2025-03-08 23:04:41 -05:00 committed by GitHub
parent 93c7e0fe65
commit da808bbd17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 157 additions and 39 deletions

View File

@ -3,7 +3,7 @@
from math import floor from math import floor
from sys import argv, path from sys import argv, path
from pathlib import Path from pathlib import Path
from typing import List, Tuple from typing import List, Dict, Tuple
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import png # type: ignore import png # type: ignore
@ -78,7 +78,9 @@ def from_dir(
palettes = [] palettes = []
palette_names: List[str] = [] 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: if asset_stack is not None and load_images:
img_name = Palette.attrib["src"] img_name = Palette.attrib["src"]
img_path = resolve_image_path(sprite_dir, "palettes", img_name, asset_stack) img_path = resolve_image_path(sprite_dir, "palettes", img_name, asset_stack)
@ -91,11 +93,15 @@ def from_dir(
palettes.append(palette) 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 = [] images = []
image_names: List[str] = [] 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: if asset_stack is not None and load_images:
img_name = Raster.attrib["src"] img_name = Raster.attrib["src"]
img_path = resolve_image_path(sprite_dir, "rasters", img_name, asset_stack) img_path = resolve_image_path(sprite_dir, "rasters", img_name, asset_stack)
@ -109,14 +115,19 @@ def from_dir(
images.append(image) 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 = [] animations = []
animation_names: List[str] = [] animation_names: List[str] = []
for Animation in SpriteSheet.findall("./AnimationList/Animation"): 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] = [] comps: List[AnimComponent] = []
for comp_xml in Animation: 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) comps.append(comp)
animation_names.append(Animation.attrib["name"]) animation_names.append(Animation.attrib["name"])
animations.append(comps) animations.append(comps)

View File

@ -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"" out_bytes = b""
back_out_bytes = b"" back_out_bytes = b""
max_components = int(xml.attrib[MAX_COMPONENTS_XML]) max_components = int(sprite_xml.attrib[MAX_COMPONENTS_XML])
num_variations = int(xml.attrib[PALETTE_GROUPS_XML]) num_variations = int(sprite_xml.attrib[PALETTE_GROUPS_XML])
has_back = xml.attrib[HAS_BACK_XML] == "true" 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
animations: List[List[AnimComponent]] = [] 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] = [] comps: List[AnimComponent] = []
for comp_xml in anim_xml: 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) comps.append(comp)
animations.append(comps) 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_list_start_back = len(back_out_bytes) + 0x10
palette_bytes: bytes = b"" palette_bytes: bytes = b""
palette_bytes_back: bytes = b"" palette_bytes_back: bytes = b""
for palette_xml in xml[0]: for palette_xml in pal_elems:
source = palette_xml.attrib["src"] source = palette_xml.attrib["src"]
front_only = bool(palette_xml.get("front_only", False)) front_only = bool(palette_xml.get("front_only", False))
if source not in PALETTE_CACHE: 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: bytes = b""
raster_bytes_back: bytes = b"" raster_bytes_back: bytes = b""
raster_offset = 0 raster_offset = 0
for raster_xml in xml[1]: for raster_xml in img_elems:
r = player_raster_from_xml(raster_xml, back=False) r = player_raster_from_xml(raster_xml, back=False)
raster_bytes += struct.pack(">IBBBB", raster_offset, r.width, r.height, r.palette_idx, 0xFF) 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: if has_back:
raster_offset = 0 raster_offset = 0
for raster_xml in xml[1]: for raster_xml in img_elems:
is_back = False is_back = False
r = player_raster_from_xml(raster_xml, back=is_back) 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 file offsets
raster_offsets_bytes = b"" raster_offsets_bytes = b""
raster_offsets_bytes_back = 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 += 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_back += int.to_bytes(raster_list_start_back + i * 8, 4, "big")
raster_offsets_bytes += LIST_END_BYTES 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_list_offset_back = len(back_out_bytes) + 0x10
palette_offsets_bytes = b"" palette_offsets_bytes = b""
palette_offsets_bytes_back = 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") palette_offsets_bytes += int.to_bytes(palette_list_start + i * 0x20, 4, "big")
front_only = bool(palette_xml.attrib.get("front_only", False)) front_only = bool(palette_xml.attrib.get("front_only", False))
if not front_only: if not front_only:
@ -358,17 +369,21 @@ def write_player_sprite_header(
sprite_xml = PLAYER_XML_CACHE[sprite_name] sprite_xml = PLAYER_XML_CACHE[sprite_name]
has_back = sprite_xml.attrib[HAS_BACK_XML] == "true" 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_sprites[f"SPR_{sprite_name}"] = sprite_id
player_rasters[sprite_name] = {} player_rasters[sprite_name] = {}
player_palettes[sprite_name] = {} player_palettes[sprite_name] = {}
player_anims[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_id = int(palette_xml.attrib["id"], 0x10)
palette_name = palette_xml.attrib["name"] palette_name = palette_xml.attrib["name"]
player_palettes[sprite_name][f"SPR_PAL_{sprite_name}_{palette_name}"] = palette_id 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"] anim_name = anim_xml.attrib["name"]
if palette_id > 0: if palette_id > 0:
anim_name = f"{palette_name}_{anim_name}" anim_name = f"{palette_name}_{anim_name}"
@ -377,7 +392,7 @@ def write_player_sprite_header(
) )
max_size = 0 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_id = int(raster_xml.attrib["id"], 0x10)
raster_name = raster_xml.attrib["name"] raster_name = raster_xml.attrib["name"]
player_rasters[sprite_name][f"SPR_IMG_{sprite_name}_{raster_name}"] = raster_id 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 player_sprites[f"SPR_{sprite_name}_Back"] = sprite_id
max_size = 0 max_size = 0
for raster_xml in sprite_xml[1]: for raster_xml in img_elems:
if "back" in raster_xml.attrib: if "back" in raster_xml.attrib:
raster = RASTER_CACHE[raster_xml.attrib["back"][:-4]] raster = RASTER_CACHE[raster_xml.attrib["back"][:-4]]
if max_size < raster.size: 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: List[RasterTableEntry] = []
sheet_rtes_back: List[RasterTableEntry] = [] sheet_rtes_back: List[RasterTableEntry] = []
img_elems: List[ET.Element] = sprite_xml.findall("./RasterList/Raster")
has_back = False has_back = False
for raster_xml in sprite_xml[1]: for raster_xml in img_elems:
if "back" in raster_xml.attrib: if "back" in raster_xml.attrib:
has_back = True has_back = True

View File

@ -200,7 +200,7 @@ class SetParent(Animation):
def get_attributes(self): def get_attributes(self):
return { 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 @dataclass
class AnimComponent: class AnimComponent:
x: int x: int
@ -255,7 +250,7 @@ class AnimComponent:
elif cmd_op == CMD.SET_SCALE: elif cmd_op == CMD.SET_SCALE:
i += 1 i += 1
elif cmd_op == CMD.LOOP: elif cmd_op == CMD.LOOP:
dest = command_list[i + 1] dest = cmd_arg
if dest in boundaries and dest not in labels: if dest in boundaries and dest not in labels:
labels[dest] = f"Pos_{dest}" labels[dest] = f"Pos_{dest}"
i += 1 i += 1
@ -313,8 +308,8 @@ class AnimComponent:
palette = -1 palette = -1
ret.append(SetPalette(palette)) ret.append(SetPalette(palette))
elif cmd_op == CMD.LOOP: elif cmd_op == CMD.LOOP:
count = cmd_arg dest = cmd_arg
dest = command_list[i + 1] count = command_list[i + 1]
if dest in labels: if dest in labels:
lbl_name = labels[dest] lbl_name = labels[dest]
ret.append(Loop(count, lbl_name, 0)) ret.append(Loop(count, lbl_name, 0))
@ -349,18 +344,27 @@ class AnimComponent:
return AnimComponent.parse_commands(self.commands) return AnimComponent.parse_commands(self.commands)
@staticmethod @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] = [] commands: List[int] = []
labels = {} labels = {}
for cmd in xml: for cmd in xml:
if cmd.tag == "Label": if cmd.tag == "Label":
idx = len(commands) labels[cmd.attrib[XML_ATTR_NAME]] = len(commands)
labels[cmd.attrib[XML_ATTR_NAME]] = idx
elif cmd.tag == "Wait": elif cmd.tag == "Wait":
duration = int(cmd.attrib[XML_ATTR_DURATION]) duration = int(cmd.attrib[XML_ATTR_DURATION])
commands.append(duration & 0xFFF) commands.append(duration & 0xFFF)
elif cmd.tag == "SetRaster": 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: if raster == -1:
raster = 0xFFF raster = 0xFFF
commands.append(0x1000 + (raster & 0xFFF)) commands.append(0x1000 + (raster & 0xFFF))
@ -393,7 +397,17 @@ class AnimComponent:
commands.append(0x5000 + mode) commands.append(0x5000 + mode)
commands.append(percent) commands.append(percent)
elif cmd.tag == "SetPalette": 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: if palette == -1:
palette = 0xFFF palette = 0xFFF
commands.append(0x6000 + (palette & 0xFFF)) commands.append(0x6000 + (palette & 0xFFF))
@ -408,14 +422,90 @@ class AnimComponent:
if not lbl_name in labels: if not lbl_name in labels:
raise Exception("Label missing for Loop dest: " + lbl_name) raise Exception("Label missing for Loop dest: " + lbl_name)
pos = labels[lbl_name] pos = labels[lbl_name]
commands.append(0x7000 + (count & 0xFFF)) commands.append(0x7000 + (pos & 0xFFF))
commands.append(pos) commands.append(count)
elif cmd.tag == "Unknown": elif cmd.tag == "Unknown":
commands.append(0x8000 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF)) commands.append(0x8000 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF))
elif cmd.tag == "SetParent": 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": elif cmd.tag == "SetNotify":
commands.append(0x8200 + (int(cmd.attrib[XML_ATTR_VALUE]) & 0xFF)) 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 elif cmd.tag == "Command": # old Star Rod compatibility
commands.append(int(cmd.attrib["val"], 16)) commands.append(int(cmd.attrib["val"], 16))
else: else: