2021-01-24 15:45:43 +01:00
|
|
|
from segtypes.n64.segment import N64Segment
|
2020-11-19 10:31:10 +01:00
|
|
|
from pathlib import Path
|
2021-06-15 10:05:16 +02:00
|
|
|
import struct
|
2021-01-24 15:45:43 +01:00
|
|
|
from util.n64 import Yay0decompress
|
2020-11-19 10:31:10 +01:00
|
|
|
from util.iter import iter_in_groups
|
|
|
|
from util.color import unpack_color
|
2021-03-23 03:29:47 +01:00
|
|
|
from util import options
|
2020-11-19 10:31:10 +01:00
|
|
|
import png
|
|
|
|
import xml.etree.ElementTree as ET
|
2021-06-16 11:52:15 +02:00
|
|
|
import struct
|
2020-11-19 10:31:10 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
import pylibyaml
|
|
|
|
import yaml
|
|
|
|
|
2020-11-19 10:31:10 +01:00
|
|
|
class Sprite:
|
|
|
|
def __init__(self):
|
|
|
|
self.max_components = 0
|
|
|
|
self.num_variations = 0
|
|
|
|
|
|
|
|
self.images = []
|
|
|
|
self.palettes = []
|
|
|
|
self.animations = []
|
|
|
|
|
|
|
|
self.image_names = []
|
|
|
|
self.palette_names = []
|
|
|
|
self.animation_names = []
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_bytes(data):
|
|
|
|
self = Sprite()
|
|
|
|
|
|
|
|
image_offsets = Sprite.read_offset_list(data[int.from_bytes(data[0:4], byteorder="big"):])
|
|
|
|
palette_offsets = Sprite.read_offset_list(data[int.from_bytes(data[4:8], byteorder="big"):])
|
|
|
|
self.max_components = int.from_bytes(data[8:0xC], byteorder="big")
|
|
|
|
self.num_variations = int.from_bytes(data[0xC:0x10], byteorder="big")
|
|
|
|
animation_offsets = Sprite.read_offset_list(data[0x10:])
|
|
|
|
|
|
|
|
for offset in palette_offsets:
|
|
|
|
# 16 colors
|
|
|
|
color_data = data[offset : offset + 16 * 2]
|
|
|
|
self.palettes.append([unpack_color(c) for c in iter_in_groups(color_data, 2)])
|
|
|
|
|
|
|
|
for offset in image_offsets:
|
|
|
|
img = Image.from_bytes(data[offset:], data)
|
|
|
|
self.images.append(img)
|
|
|
|
|
|
|
|
for offset in animation_offsets:
|
|
|
|
anim = []
|
|
|
|
|
|
|
|
for comp_offset in Sprite.read_offset_list(data[offset:]):
|
|
|
|
comp = Component.from_bytes(data[comp_offset:], data)
|
|
|
|
anim.append(comp)
|
|
|
|
|
|
|
|
self.animations.append(anim)
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def read_offset_list(data):
|
|
|
|
l = []
|
|
|
|
pos = 0
|
|
|
|
|
2021-06-15 10:05:16 +02:00
|
|
|
for offset in struct.iter_unpack(">i", data):
|
|
|
|
if offset[0] == -1:
|
2020-11-19 10:31:10 +01:00
|
|
|
break
|
2021-06-15 10:05:16 +02:00
|
|
|
l.append(offset[0])
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
return l
|
|
|
|
|
|
|
|
def write_to_dir(self, path):
|
|
|
|
SpriteSheet = ET.Element("SpriteSheet", {
|
2021-04-13 09:47:52 +02:00
|
|
|
"maxComponents": str(self.max_components),
|
|
|
|
"paletteGroups": str(self.num_variations),
|
2020-11-19 10:31:10 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
PaletteList = ET.SubElement(SpriteSheet, "PaletteList")
|
|
|
|
RasterList = ET.SubElement(SpriteSheet, "RasterList")
|
|
|
|
AnimationList = ET.SubElement(SpriteSheet, "AnimationList")
|
|
|
|
|
|
|
|
palette_to_raster = {}
|
|
|
|
|
|
|
|
for i, image in enumerate(self.images):
|
|
|
|
name = self.image_names[i] if self.image_names else f"Raster_{i:02X}"
|
|
|
|
image.write(path / (name + ".png"), self.palettes[image.palette_index])
|
|
|
|
|
|
|
|
if image.palette_index not in palette_to_raster:
|
|
|
|
palette_to_raster[image.palette_index] = []
|
|
|
|
palette_to_raster[image.palette_index].append(image)
|
|
|
|
|
|
|
|
ET.SubElement(RasterList, "Raster", {
|
|
|
|
"id": f"{i:X}",
|
|
|
|
"palette": f"{image.palette_index:X}",
|
|
|
|
"src": name + ".png",
|
|
|
|
})
|
|
|
|
|
|
|
|
for i, palette in enumerate(self.palettes):
|
2021-04-28 10:58:21 +02:00
|
|
|
name = self.palette_names[i] if (self.palette_names and i < len(self.palette_names)) else f"Palette_{i:02X}"
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
if i in palette_to_raster:
|
|
|
|
img = palette_to_raster[i][0]
|
|
|
|
else:
|
|
|
|
img = self.images[0]
|
|
|
|
|
|
|
|
img.write(path / (name + ".png"), palette)
|
|
|
|
|
|
|
|
ET.SubElement(PaletteList, "Palette", {
|
|
|
|
"id": f"{i:X}",
|
|
|
|
"src": name + ".png",
|
|
|
|
})
|
|
|
|
|
|
|
|
for i, components in enumerate(self.animations):
|
|
|
|
Animation = ET.SubElement(AnimationList, "Animation", {
|
|
|
|
"name": self.animation_names[i] if self.animation_names else f"Anim_{i:X}",
|
|
|
|
})
|
|
|
|
|
|
|
|
for j, comp in enumerate(components):
|
|
|
|
Component = ET.SubElement(Animation, "Component", {
|
|
|
|
"name": f"Comp_{j:X}",
|
|
|
|
"xyz": ",".join(map(str, [comp.x, comp.y, comp.z])),
|
|
|
|
})
|
|
|
|
|
|
|
|
for cmd in comp.commands:
|
|
|
|
ET.SubElement(Component, "Command", {"val": f"{cmd:X}"})
|
|
|
|
|
|
|
|
xml = ET.ElementTree(SpriteSheet)
|
|
|
|
|
|
|
|
# pretty print (Python 3.9+)
|
|
|
|
if hasattr(ET, "indent"):
|
|
|
|
ET.indent(xml, " ")
|
|
|
|
|
|
|
|
xml.write(str(path / "SpriteSheet.xml"), encoding="unicode")
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_dir(path, read_images=True):
|
|
|
|
self = Sprite()
|
|
|
|
|
|
|
|
xml = ET.parse(str(path / "SpriteSheet.xml"))
|
|
|
|
SpriteSheet = xml.getroot()
|
|
|
|
|
|
|
|
true_max_components = 0
|
2021-03-30 12:12:32 +02:00
|
|
|
self.max_components = int(SpriteSheet.get("a") or SpriteSheet.get("maxComponents")) # ignored
|
|
|
|
self.num_variations = int(SpriteSheet.get("b") or SpriteSheet.get("paletteGroups"))
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
for Palette in SpriteSheet.findall("./PaletteList/Palette"):
|
|
|
|
if read_images:
|
|
|
|
img = png.Reader(str(path / Palette.get("src")))
|
|
|
|
img.preamble(True)
|
|
|
|
palette = img.palette(alpha="force")
|
|
|
|
|
2021-03-30 12:12:32 +02:00
|
|
|
palette = palette[0:16]
|
2020-11-19 10:31:10 +01:00
|
|
|
assert len(palette) == 16
|
|
|
|
|
|
|
|
self.palettes.append(palette)
|
|
|
|
|
2021-03-30 12:12:32 +02:00
|
|
|
self.palette_names.append(Palette.get("name", Palette.get("src").split(".png")[0]))
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
for Raster in SpriteSheet.findall("./RasterList/Raster"):
|
|
|
|
if read_images:
|
|
|
|
img_path = str(path / Raster.get("src"))
|
|
|
|
width, height, raster, info = png.Reader(img_path).read_flat()
|
|
|
|
|
|
|
|
image = Image()
|
|
|
|
image.width = width
|
|
|
|
image.height = height
|
|
|
|
image.raster = raster
|
|
|
|
image.palette_index = int(Raster.get("palette"), base=16)
|
|
|
|
|
|
|
|
assert (image.width % 8) == 0, f"{img_path} width is not a multiple of 8"
|
|
|
|
assert (image.height % 8) == 0, f"{img_path} height is not a multiple of 8"
|
|
|
|
|
|
|
|
self.images.append(image)
|
|
|
|
|
|
|
|
self.image_names.append(Raster.get("src").split(".png")[0])
|
|
|
|
|
|
|
|
for i, Animation in enumerate(SpriteSheet.findall("./AnimationList/Animation")):
|
|
|
|
components = []
|
|
|
|
|
|
|
|
for ComponentEl in Animation.findall("Component"):
|
|
|
|
comp = Component()
|
|
|
|
|
|
|
|
x, y, z = ComponentEl.get("xyz", "0,0,0").split(",")
|
|
|
|
comp.x = int(x)
|
|
|
|
comp.y = int(y)
|
|
|
|
comp.z = int(z)
|
|
|
|
|
|
|
|
for Command in ComponentEl:
|
|
|
|
comp.commands.append(int(Command.get("val"), base=16))
|
|
|
|
|
|
|
|
components.append(comp)
|
|
|
|
|
|
|
|
self.animation_names.append(Animation.get("name"))
|
|
|
|
self.animations.append(components)
|
|
|
|
|
|
|
|
if len(components) > true_max_components:
|
|
|
|
true_max_components = len(components)
|
|
|
|
|
2021-03-30 12:12:32 +02:00
|
|
|
self.max_components = true_max_components
|
|
|
|
#assert self.max_components == true_max_components, f"{true_max_components} component(s) used, but SpriteSheet.a = {self.max_components}"
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
class Image:
|
|
|
|
@staticmethod
|
|
|
|
def from_bytes(data, sprite_data):
|
|
|
|
self = Image()
|
|
|
|
|
|
|
|
raster_offset = int.from_bytes(data[0:4], byteorder="big")
|
|
|
|
self.width = data[4] & 0xFF
|
|
|
|
self.height = data[5] & 0xFF
|
|
|
|
self.palette_index = data[6]
|
|
|
|
assert data[7] == 0xFF
|
|
|
|
|
|
|
|
self.set_raster_from_bytes(sprite_data[raster_offset:])
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
# CI-4
|
|
|
|
def set_raster_from_bytes(self, data):
|
|
|
|
self.raster = bytearray()
|
|
|
|
|
|
|
|
for i in range(self.width * self.height // 2):
|
|
|
|
self.raster.append(data[i] >> 4)
|
|
|
|
self.raster.append(data[i] & 0xF)
|
|
|
|
|
|
|
|
def write(self, path, palette):
|
|
|
|
w = png.Writer(self.width, self.height, palette=palette)
|
|
|
|
|
|
|
|
with open(path, "wb") as f:
|
|
|
|
w.write_array(f, self.raster)
|
|
|
|
|
|
|
|
class Component:
|
|
|
|
def __init__(self):
|
|
|
|
self.x = 0
|
|
|
|
self.y = 0
|
|
|
|
self.z = 0
|
|
|
|
self.commands = []
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_bytes(data, sprite_data):
|
|
|
|
self = Component()
|
|
|
|
|
|
|
|
commands_offset = int.from_bytes(data[0:4], byteorder="big")
|
|
|
|
commands_size = int.from_bytes(data[4:6], byteorder="big") # size in bytes, not length!
|
|
|
|
commands_data = sprite_data[commands_offset : commands_offset + commands_size]
|
|
|
|
self.commands = [int.from_bytes(d[0:2], byteorder="big") for d in iter_in_groups(commands_data, 2)]
|
|
|
|
|
2020-12-22 00:46:14 +01:00
|
|
|
self.x = int.from_bytes(data[6:8], byteorder="big", signed=True)
|
|
|
|
self.y = int.from_bytes(data[8:10], byteorder="big", signed=True)
|
|
|
|
self.z = int.from_bytes(data[10:12], byteorder="big", signed=True)
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
return self
|
|
|
|
|
2021-07-16 11:28:37 +02:00
|
|
|
class N64SegPm_npc_sprites(N64Segment):
|
2020-11-19 10:31:10 +01:00
|
|
|
DEFAULT_SPRITE_NAMES = [f"{i:02X}" for i in range(0xEA)]
|
|
|
|
|
2021-06-16 11:52:15 +02:00
|
|
|
def __init__(self, rom_start, rom_end, type, name, vram_start, extract, given_subalign, given_is_overlay, given_dir, args, yml):
|
|
|
|
super().__init__(rom_start, rom_end, type, name, vram_start, extract, given_subalign, given_is_overlay, given_dir, args, yml)
|
2021-04-13 09:47:52 +02:00
|
|
|
|
2021-06-16 11:52:15 +02:00
|
|
|
self.files = yml["files"]
|
2020-11-19 10:31:10 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
with (Path(__file__).parent / f"{self.name}.yaml").open("r") as f:
|
|
|
|
self.sprite_cfg = yaml.load(f.read(), Loader=yaml.SafeLoader)
|
2020-11-19 10:31:10 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
def split(self, rom_bytes):
|
|
|
|
out_dir = options.get_asset_path() / self.dir / self.name
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
data = rom_bytes[self.rom_start:self.rom_end]
|
|
|
|
pos = 0
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
for i, sprite_name in enumerate(self.files):
|
|
|
|
#self.log(f"Splitting sprite {sprite_name}...")
|
2020-11-19 10:31:10 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
sprite_dir = out_dir / sprite_name
|
|
|
|
sprite_dir.mkdir(parents=True, exist_ok=True)
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
start = int.from_bytes(data[i * 4 : (i + 1) * 4], byteorder="big")
|
|
|
|
end = int.from_bytes(data[(i + 1) * 4 : (i + 2) * 4], byteorder="big")
|
|
|
|
|
|
|
|
sprite_data = Yay0decompress.decompress_yay0(data[start:end])
|
|
|
|
sprite = Sprite.from_bytes(sprite_data)
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
if sprite_name in self.sprite_cfg:
|
|
|
|
sprite.image_names = self.sprite_cfg[sprite_name].get("frames", [])
|
|
|
|
sprite.palette_names = self.sprite_cfg[sprite_name].get("palettes", [])
|
|
|
|
sprite.animation_names = self.sprite_cfg[sprite_name].get("animations", [])
|
2020-11-19 10:31:10 +01:00
|
|
|
|
|
|
|
sprite.write_to_dir(sprite_dir)
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
def get_linker_entries(self):
|
|
|
|
from segtypes.linker_entry import LinkerEntry
|
2020-11-19 10:31:10 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
basepath = options.get_asset_path() / "sprite" / f"{self.name}"
|
|
|
|
out_paths = [options.get_asset_path() / "sprite" / self.name / (f["name"] if type(f) is dict else f)
|
|
|
|
for f in self.files]
|
|
|
|
|
|
|
|
return [LinkerEntry(self, out_paths, basepath, ".data")]
|
|
|
|
|
|
|
|
def cache(self):
|
2021-06-16 11:52:15 +02:00
|
|
|
return (self.yaml, self.rom_end, self.sprite_cfg)
|