Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions tools/build/sprite/npc_sprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
49 changes: 33 additions & 16 deletions tools/build/sprite/sprites.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -252,15 +263,15 @@ 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)

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

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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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}"
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down
124 changes: 107 additions & 17 deletions tools/splat_ext/sprite_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
}


Expand All @@ -214,11 +214,6 @@ def get_attributes(self):
}


@dataclass
class Keyframe(Animation):
pass


@dataclass
class AnimComponent:
x: int
Expand Down Expand Up @@ -255,7 +250,7 @@ def parse_commands(command_list: List[int]) -> List[Animation]:
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
Expand Down Expand Up @@ -313,8 +308,8 @@ def to_signed(value):
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))
Expand Down Expand Up @@ -349,18 +344,27 @@ def animations(self) -> List[Animation]:
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))
Expand Down Expand Up @@ -393,7 +397,17 @@ def from_xml(xml: ET.Element):
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))
Expand All @@ -408,14 +422,90 @@ def from_xml(xml: ET.Element):
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:
Expand Down