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 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)

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""
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

View File

@ -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: