papermario/tools/build/sprite/sprite_shading_profiles.py

175 lines
5.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
from dataclasses import dataclass, field
from enum import Enum
import argparse
import json
from pathlib import Path
import struct
from typing import List, Literal
class LightMode(Enum):
UNIFORM = 0
LINEAR = 4
QUADRATIC = 8
@dataclass
class Light:
flags: int
rgb: List[int]
pos: List[int]
falloff: float
def to_json(self):
d = self.__dict__
d["mode"] = LightMode(d["flags"] - 1).name.lower()
del d["flags"]
return d
@dataclass
class SpriteShadingProfile:
name: str
ambient: List[int]
power: int
lights: List[Light] = field(default_factory=lambda: [])
def to_json(self):
d = self.__dict__
d["lights"] = [light.to_json() for light in self.lights]
return d
@dataclass
class SpriteShadingGroup:
area: str
profiles: List[SpriteShadingProfile] = field(default_factory=lambda: [])
def to_json(self):
d = self.__dict__
d["profiles"] = [profile.to_json() for profile in self.profiles]
return d
def groups_from_json(data) -> List[SpriteShadingGroup]:
groups = []
for group in data:
profiles = []
for profile in group["profiles"]:
lights = []
for light in profile["lights"]:
enabled = light.get("enabled", True)
lights.append(
Light(
flags=LightMode[light["mode"].upper()].value + (1 if enabled else 0),
rgb=light["rgb"],
pos=light["pos"],
falloff=light["falloff"],
)
)
profiles.append(
SpriteShadingProfile(
name=profile["name"],
ambient=profile["ambient"],
power=profile["power"],
lights=lights,
)
)
groups.append(
SpriteShadingGroup(
area=group["area"],
profiles=profiles,
)
)
return groups
def build(
input: Path,
bin_out: Path,
header_out: Path,
endian: Literal["big", "little"] = "big",
matching: bool = True,
):
END = ">" if endian == "big" else "<"
with open(input, "r") as f:
json_data = json.load(f)
groups = groups_from_json(json_data)
# Header creation
with open(header_out, "w") as f:
f.write("#ifndef SHADING_PROFILES_H\n")
f.write("#define SHADING_PROFILES_H\n")
f.write(f"/* This file is auto-generated from {input.name}. Do not edit. */\n\n")
f.write("enum ShadingProfile {\n")
f.write(" SHADING_NONE = 0xFFFFFFFF,\n")
for g, group in enumerate(groups):
for i, profile in enumerate(group.profiles):
f.write(f" SHADING_{profile.name.upper()} = 0x{(g << 16) | i:08X},\n")
f.write("};\n")
f.write("#endif // SHADING_PROFILES_H\n")
data_offset = 0
profile_list_offset = len(groups) * 8
offsets_table = bytearray()
profile_lists = bytearray()
data_table = bytearray()
for g, group in enumerate(groups):
offsets_table.extend(struct.pack(END + "i", data_offset))
offsets_table.extend(struct.pack(END + "i", profile_list_offset))
group_data_offset = 0
for profile in group.profiles:
profile_lists.extend(struct.pack(END + "i", group_data_offset))
data_table.extend(struct.pack(END + "B", len(profile.lights)))
data_table.append(0)
data_table.extend(struct.pack(END + "B", profile.ambient[0]))
data_table.extend(struct.pack(END + "B", profile.ambient[1]))
data_table.extend(struct.pack(END + "B", profile.ambient[2]))
data_table.extend(struct.pack(END + "B", profile.power))
group_data_offset += 6
profile_list_offset += 4
for light in profile.lights:
data_table.extend(struct.pack(END + "B", light.flags))
data_table.extend(struct.pack(END + "B", light.rgb[0]))
data_table.extend(struct.pack(END + "B", light.rgb[1]))
data_table.extend(struct.pack(END + "B", light.rgb[2]))
data_table.extend(struct.pack(END + "h", light.pos[0]))
data_table.extend(struct.pack(END + "h", light.pos[1]))
data_table.extend(struct.pack(END + "h", light.pos[2]))
data_table.extend(struct.pack(END + "f", light.falloff))
data_table.append(0)
data_table.append(0)
group_data_offset += 0x10
data_offset += group_data_offset
offsets_table.extend(profile_lists)
if matching:
offsets_table += b"\0" * (0x1D0 - len(offsets_table)) # Pad to 0x1D0
final_data = offsets_table + data_table
if matching:
final_data += b"\0" * (0xE70 - len(final_data)) # Pad to 0xE70
with open(bin_out, "wb") as f:
f.write(final_data)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Sprite shading profiles")
parser.add_argument("input", type=Path, help="Input JSON file")
parser.add_argument("bin_out", type=Path, help="Output binary file path")
parser.add_argument("header_out", type=Path, help="Output header file path")
parser.add_argument("--endian", choices=["big", "little"], default="big", help="Output endianness")
parser.add_argument("--matching", help="Pad to matching size", default=True)
args = parser.parse_args()
build(args.input, args.bin_out, args.header_out, args.endian, args.matching)