mirror of https://github.com/ACreTeam/ac-decomp
563 lines
18 KiB
Python
563 lines
18 KiB
Python
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
current_path = sys.path.copy()
|
|
sys.path.append(str(Path(__file__).parent.parent))
|
|
|
|
sys.path = current_path
|
|
|
|
|
|
def is_windows() -> bool:
|
|
return os.name == "nt"
|
|
|
|
|
|
def unswizzle(input_list, width, height, pixels_per_block_w=8, pixels_per_block_h=8):
|
|
if width * height > len(input_list):
|
|
raise Exception(
|
|
f"There are not enough elements in input_list for the specified Width and Height!"
|
|
f"\nExpected a length of {width * height}, but got a length of {len(input_list)}!"
|
|
)
|
|
|
|
block_x_count = width // pixels_per_block_w
|
|
block_y_count = height // pixels_per_block_h
|
|
if block_y_count < 1:
|
|
block_y_count = 1 # Hack for small textures -- Not sure if this is correct.
|
|
|
|
output_buffer = [None] * len(input_list)
|
|
pixel_index = 0
|
|
|
|
for y_block in range(block_y_count):
|
|
for x_block in range(block_x_count):
|
|
for y_pixel in range(pixels_per_block_h):
|
|
for x_pixel in range(pixels_per_block_w):
|
|
output_buffer_index = (
|
|
(width * pixels_per_block_h * y_block)
|
|
+ y_pixel * width
|
|
+ x_block * pixels_per_block_w
|
|
+ x_pixel
|
|
)
|
|
output_buffer[output_buffer_index] = input_list[pixel_index]
|
|
pixel_index += 1
|
|
|
|
return output_buffer
|
|
|
|
|
|
def swizzle(input_list, width, height, pixels_per_block_w=8, pixels_per_block_h=8):
|
|
if width * height > len(input_list):
|
|
raise Exception(
|
|
f"There are not enough elements in input_list for the specified Width and Height!\n"
|
|
f"Width = {width} | Height = {height} | Width * Height = {width * height} | Input List Length = {len(input_list)}"
|
|
)
|
|
|
|
block_x_count = width // pixels_per_block_w
|
|
block_y_count = height // pixels_per_block_h
|
|
|
|
output_buffer = [None] * len(input_list)
|
|
output_buffer_index = 0
|
|
|
|
for y_block in range(block_y_count):
|
|
for x_block in range(block_x_count):
|
|
for y_pixel in range(pixels_per_block_h):
|
|
for x_pixel in range(pixels_per_block_w):
|
|
pixel_index = (
|
|
(width * pixels_per_block_h * y_block)
|
|
+ y_pixel * width
|
|
+ x_block * pixels_per_block_w
|
|
+ x_pixel
|
|
)
|
|
output_buffer[output_buffer_index] = input_list[pixel_index]
|
|
output_buffer_index += 1
|
|
|
|
return output_buffer
|
|
|
|
|
|
def read_rgb5a3_colors(input_bytes, n):
|
|
"""
|
|
Reads N unsigned 16-bit values from an input bytearray as big-endian RGB5A3 colors.
|
|
|
|
Args:
|
|
- input_bytes (bytearray): The input bytearray.
|
|
- n (int): The number of 16-bit values to read.
|
|
|
|
Returns:
|
|
- List of tuples representing RGB5A3 colors.
|
|
"""
|
|
if len(input_bytes) < n * 2:
|
|
raise ValueError("Input bytearray does not contain enough data for N colors")
|
|
|
|
colors = []
|
|
for i in range(n):
|
|
# Extract 16-bit value for each color, assuming big-endian order
|
|
color_value = int.from_bytes(input_bytes[i * 2 : i * 2 + 2], "big")
|
|
colors.append(color_value)
|
|
|
|
return colors
|
|
|
|
|
|
def to_argb8(pixel):
|
|
if (pixel & 0x8000) == 0x8000:
|
|
# No Alpha Channel
|
|
a = 0xFF
|
|
|
|
# Separate RGB from bits
|
|
r = (pixel & 0x7C00) >> 10
|
|
g = (pixel & 0x03E0) >> 5
|
|
b = pixel & 0x001F
|
|
|
|
# Convert to RGB8 values
|
|
r = (r << (8 - 5)) | (r >> (10 - 8))
|
|
g = (g << (8 - 5)) | (g >> (10 - 8))
|
|
b = (b << (8 - 5)) | (b >> (10 - 8))
|
|
else:
|
|
# An Alpha Channel Exists, 3 bits for Alpha Channel and 4 bits each for RGB
|
|
a = (pixel & 0x7000) >> 12
|
|
r = (pixel & 0x0F00) >> 8
|
|
g = (pixel & 0x00F0) >> 4
|
|
b = pixel & 0x000F
|
|
|
|
a = (a << (8 - 3)) | (a << (8 - 6)) | (a >> (9 - 8))
|
|
r = (r << (8 - 4)) | r
|
|
g = (g << (8 - 4)) | g
|
|
b = (b << (8 - 4)) | b
|
|
|
|
# Ensure the values are byte-sized (0-255)
|
|
a = a & 0xFF
|
|
r = r & 0xFF
|
|
g = g & 0xFF
|
|
b = b & 0xFF
|
|
|
|
return r, g, b, a
|
|
|
|
|
|
def rgb5a3_to_argb8(colors):
|
|
"""
|
|
Converts an array of RGB5A3 colors to standard ARGB8 colors.
|
|
|
|
Args:
|
|
- colors (List[int]): List of integers representing RGB5A3 colors.
|
|
|
|
Returns:
|
|
- List of tuples representing ARGB8 colors.
|
|
"""
|
|
argb8_colors = []
|
|
for color in colors:
|
|
argb8_colors.append((to_argb8(color)))
|
|
|
|
return argb8_colors
|
|
|
|
|
|
def extract_texture_asset(byte_array, width, height, tex_count):
|
|
texture_data_size = (width * height) // 2
|
|
entry_size = 2 * 16 + texture_data_size * tex_count
|
|
n_entries = len(byte_array) // entry_size
|
|
entries = []
|
|
|
|
for entry_index in range(n_entries):
|
|
offset = entry_index * entry_size
|
|
palette_bytes = byte_array[offset : offset + 2 * 16]
|
|
palette_colors = read_rgb5a3_colors(palette_bytes, 16)
|
|
|
|
textures = []
|
|
for texture_index in range(tex_count):
|
|
texture_offset = offset + 2 * 16 + (texture_index * texture_data_size)
|
|
texture_bytes = byte_array[
|
|
texture_offset : texture_offset + texture_data_size
|
|
]
|
|
textures.append(texture_bytes)
|
|
|
|
entries.append({"palette": palette_colors, "textures": textures})
|
|
|
|
return entries
|
|
|
|
|
|
def generate_c_source_entries_with_all_textures(entries, width):
|
|
"""
|
|
Generates a list of C source strings from extracted palette and textures data.
|
|
Each string contains one palette and all textures formatted for a C source file,
|
|
where each entry is represented as a separate string in the list.
|
|
|
|
Args:
|
|
- entries (List[Dict]): Extracted data containing 'palette' and 'textures' for each entry.
|
|
|
|
Returns:
|
|
- List of strings, each representing the C source code for one entry including one palette and all textures.
|
|
"""
|
|
c_source_entries = []
|
|
for i, entry in enumerate(entries):
|
|
c_source = ""
|
|
# Palette
|
|
c_source += f"// clang-format off\nunsigned short floor{i:02d}_pal[] = {{\n "
|
|
for j, color in enumerate(entry["palette"]):
|
|
c_source += f"0x{color:04X},"
|
|
if (j + 1) % 8 == 0 and (j + 1) < 16: # Newline after every 8 entries
|
|
c_source += "\n "
|
|
else:
|
|
c_source += " "
|
|
c_source = (
|
|
c_source.rstrip(", \n") + "\n};\n// clang-format on\n\n"
|
|
) # Remove trailing comma and add closing bracket
|
|
|
|
# Textures
|
|
c_source += f"// clang-format off\nunsigned char floor{i:02d}_tex[] = {{\n // texture 0\n "
|
|
for texture_index, texture in enumerate(entry["textures"]):
|
|
for k, pixel in enumerate(texture):
|
|
c_source += f"0x{pixel:02X},"
|
|
if (k + 1) % (width // 2) == 0: # Newline after width/2 texture pixels
|
|
if (
|
|
k + 1 != len(texture)
|
|
or texture_index < len(entry["textures"]) - 1
|
|
):
|
|
c_source += "\n "
|
|
else:
|
|
c_source += " "
|
|
if texture_index < len(entry["textures"]) - 1:
|
|
c_source += f"// texture {texture_index + 1}\n " # Add a newline after each texture except the last
|
|
c_source = (
|
|
c_source.rstrip(", \n") + "\n};\n// clang-format on\n"
|
|
) # Close texture array
|
|
|
|
c_source_entries.append(c_source)
|
|
|
|
return c_source_entries
|
|
|
|
|
|
def save_palette_as_hex(palette, directory_path):
|
|
"""
|
|
Saves an RGB5A3 palette list to a text file with hex values.
|
|
|
|
Args:
|
|
- palette (List[int]): List of colors in RGB5A3 format.
|
|
- directory_path (Path): The directory where 'palette.txt' will be saved.
|
|
"""
|
|
# Ensure the directory exists
|
|
directory_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Define the file path
|
|
file_path = directory_path / "palette.txt"
|
|
|
|
with file_path.open(mode="w") as file:
|
|
for color in palette:
|
|
# Write each color as a hexadecimal value
|
|
file.write(f"{color:04X}\n")
|
|
|
|
|
|
def save_textures_as_png(base_path, entries, width, height, base_name):
|
|
for i, entry in enumerate(entries):
|
|
folder_name = base_path / f"{base_name}{i:02d}"
|
|
folder_name.mkdir(
|
|
parents=True, exist_ok=True
|
|
) # Create the directory, including any necessary parent directories
|
|
|
|
# Save RGB5A3 palette to 'palette.txt'
|
|
save_palette_as_hex(entry["palette"], folder_name)
|
|
|
|
# Convert to RGBA8
|
|
palette = rgb5a3_to_argb8(entry["palette"])
|
|
|
|
for j, texture_data in enumerate(entry["textures"]):
|
|
# Construct an image from the 4-bit indexed data
|
|
img_data = bytearray(
|
|
len(texture_data) * 2
|
|
) # Each byte in texture_data represents two pixels
|
|
for idx, byte in enumerate(texture_data):
|
|
img_data[idx * 2] = byte >> 4 # High nibble for the first pixel
|
|
img_data[idx * 2 + 1] = byte & 0x0F # Low nibble for the second pixel
|
|
|
|
# Map the indexed pixels to RGBA using the palette
|
|
rgba_img = [palette[pix] for pix in img_data]
|
|
|
|
# Unswizzle the RGBA image data
|
|
unswizzled_data = unswizzle(
|
|
rgba_img, width, height
|
|
) # Assume unswizzle is defined elsewhere
|
|
|
|
# Create the final image and save it
|
|
img = Image.new("RGBA", (width, height))
|
|
img.putdata(unswizzled_data)
|
|
img_file_path = folder_name / f"texture{j:02d}.png"
|
|
img.save(img_file_path)
|
|
|
|
|
|
def generate_includes(n, input_string):
|
|
includes = ""
|
|
for i in range(n):
|
|
includes += f'#include "./{input_string}{i:02d}.c"\n'
|
|
return includes
|
|
|
|
|
|
def extract_player_room_floor(byte_array: bytearray, out_dir: str):
|
|
entries = extract_texture_asset(byte_array, 64, 64, 4)
|
|
entry_sources = generate_c_source_entries_with_all_textures(entries, 64)
|
|
Path(out_dir).mkdir(parents=True, exist_ok=True) # make dirs
|
|
for i in range(len(entries)):
|
|
path = Path(out_dir) / ("floor%02d.c" % i)
|
|
with path.open("w") as f:
|
|
f.write(entry_sources[i])
|
|
save_textures_as_png(Path(out_dir) / "tex", entries, 64, 64, "floor")
|
|
|
|
# write aggregate file
|
|
with (Path(out_dir) / "player_room_floor.c").open("w") as f:
|
|
f.write(generate_includes(len(entries), "floor"))
|
|
|
|
|
|
def extract_player_room_wall(byte_array: bytearray, out_dir: str):
|
|
entries = extract_texture_asset(byte_array, 64, 64, 2)
|
|
entry_sources = generate_c_source_entries_with_all_textures(entries, 64)
|
|
Path(out_dir).mkdir(parents=True, exist_ok=True) # make dirs
|
|
for i in range(len(entries)):
|
|
path = Path(out_dir) / ("wall%02d.c" % i)
|
|
with path.open("w") as f:
|
|
f.write(entry_sources[i])
|
|
save_textures_as_png(Path(out_dir) / "tex", entries, 64, 64, "wall")
|
|
|
|
# write aggregate file
|
|
with (Path(out_dir) / "player_room_wall.c").open("w") as f:
|
|
f.write(generate_includes(len(entries), "wall"))
|
|
|
|
|
|
# packing
|
|
|
|
|
|
def rgba8_to_rgb5a3(r, g, b, a):
|
|
if a >= 224: # Treat as opaque
|
|
return (1 << 15) | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)
|
|
else: # Use 3 bits for alpha
|
|
return ((a >> 5) << 12) | ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4)
|
|
|
|
|
|
def find_closest_color(color, palette):
|
|
min_dist, index = float("inf"), -1
|
|
for i, p in enumerate(palette):
|
|
# Directly compare the integer values
|
|
dist = (color - p) ** 2
|
|
if dist < min_dist:
|
|
min_dist, index = dist, i
|
|
return index
|
|
|
|
|
|
def load_palette_from_hex(directory_path):
|
|
"""
|
|
Loads an RGB5A3 palette list from a text file containing hex values.
|
|
|
|
Args:
|
|
- directory_path (Path): The directory where 'palette.txt' is located.
|
|
|
|
Returns:
|
|
- List[int]: List of colors in RGB5A3 format.
|
|
"""
|
|
# Define the file path
|
|
file_path = directory_path / "palette.txt"
|
|
|
|
palette = []
|
|
with file_path.open(mode="r") as file:
|
|
for line in file:
|
|
# Convert each hexadecimal string back to an integer
|
|
color = int(line.strip(), 16)
|
|
palette.append(color)
|
|
|
|
return palette
|
|
|
|
|
|
def process_png_image(image_path, palette):
|
|
img = Image.open(image_path).convert("RGBA")
|
|
pixels = np.array(img)
|
|
|
|
# Create a texture map with indices
|
|
texture_map = []
|
|
for row in pixels:
|
|
row_indices = []
|
|
for r, g, b, a in row:
|
|
rgb5a3 = rgba8_to_rgb5a3(r, g, b, a)
|
|
closest_index = find_closest_color(rgb5a3, palette)
|
|
row_indices.append(closest_index)
|
|
texture_map.extend(row_indices)
|
|
|
|
# Swizzle texture
|
|
swizzled_texture = swizzle(texture_map, img.width, img.height)
|
|
|
|
# Pack texture into 4bpp texels
|
|
packed_texture_map = [0] * (len(swizzled_texture) // 2)
|
|
for i, p in enumerate(swizzled_texture):
|
|
if (i % 2) == 0:
|
|
packed_texture_map[i // 2] = (p & 0xF) << 4
|
|
else:
|
|
packed_texture_map[i // 2] |= p & 0xF
|
|
|
|
return packed_texture_map
|
|
|
|
|
|
def pack_player_room_floor(main_path: Path):
|
|
objects = []
|
|
for dir in (main_path / "tex").iterdir():
|
|
# load palette file
|
|
palette = load_palette_from_hex(dir)
|
|
|
|
# load texture00.png-texture03.png
|
|
textures = []
|
|
for i in range(0, 4):
|
|
textures.append(process_png_image(dir / f"texture{i:02d}.png", palette))
|
|
|
|
obj = {
|
|
"palette": palette,
|
|
"textures": textures,
|
|
"idx": int(dir.name.replace("floor", "")),
|
|
}
|
|
objects.append(obj)
|
|
|
|
# sort list
|
|
objects.sort(key=lambda x: x["idx"])
|
|
|
|
# process texture data into C source
|
|
c_source = generate_c_source_entries_with_all_textures(objects, 64)
|
|
|
|
# output C source to files
|
|
for i in range(len(objects)):
|
|
with (main_path / f"floor{i:02d}.c").open("w") as f:
|
|
f.write(c_source[i])
|
|
|
|
# build elf file and dump .data section
|
|
os.chdir(str(Path(__file__).parent.parent))
|
|
|
|
out_elf = main_path / f"{main_path.name}.o"
|
|
|
|
if not is_windows():
|
|
subprocess.run(
|
|
[
|
|
"wibo",
|
|
"./build/compilers/1.3.2/mwcceppc.exe",
|
|
f"-I{str(main_path)}",
|
|
"-c",
|
|
main_path / f"{main_path.name}.c",
|
|
"-o",
|
|
out_elf,
|
|
]
|
|
)
|
|
else:
|
|
subprocess.run(
|
|
[
|
|
"./build/compilers/1.3.2/mwcceppc.exe",
|
|
f"-I{str(main_path)}",
|
|
"-c",
|
|
main_path / f"{main_path.name}.c",
|
|
"-o",
|
|
out_elf,
|
|
]
|
|
)
|
|
|
|
out_obj = main_path / f"{main_path.name}.bin"
|
|
subprocess.run(
|
|
["powerpc-eabi-objcopy", "--dump-section", f".data={out_obj}", out_elf]
|
|
)
|
|
|
|
# restore current dir
|
|
os.chdir(str(Path(__file__).parent))
|
|
|
|
|
|
def pack_player_room_wall(main_path: Path):
|
|
objects = []
|
|
for dir in (main_path / "tex").iterdir():
|
|
# load palette file
|
|
palette = load_palette_from_hex(dir)
|
|
|
|
# load texture00.png-texture03.png
|
|
textures = []
|
|
for i in range(0, 2):
|
|
textures.append(process_png_image(dir / f"texture{i:02d}.png", palette))
|
|
|
|
obj = {
|
|
"palette": palette,
|
|
"textures": textures,
|
|
"idx": int(dir.name.replace("wall", "")),
|
|
}
|
|
objects.append(obj)
|
|
|
|
# sort list
|
|
objects.sort(key=lambda x: x["idx"])
|
|
|
|
# process texture data into C source
|
|
c_source = generate_c_source_entries_with_all_textures(objects, 64)
|
|
|
|
# output C source to files
|
|
for i in range(len(objects)):
|
|
with (main_path / f"wall{i:02d}.c").open("w") as f:
|
|
f.write(c_source[i])
|
|
|
|
# build elf file and dump .data section
|
|
os.chdir(str(Path(__file__).parent.parent))
|
|
|
|
out_elf = main_path / f"{main_path.name}.o"
|
|
|
|
if not is_windows():
|
|
subprocess.run(
|
|
[
|
|
"wibo",
|
|
"./build/compilers/1.3.2/mwcceppc.exe",
|
|
f"-I{str(main_path)}",
|
|
"-c",
|
|
main_path / f"{main_path.name}.c",
|
|
"-o",
|
|
out_elf,
|
|
]
|
|
)
|
|
else:
|
|
subprocess.run(
|
|
[
|
|
"./build/compilers/1.3.2/mwcceppc.exe",
|
|
f"-I{str(main_path)}",
|
|
"-c",
|
|
main_path / f"{main_path.name}.c",
|
|
"-o",
|
|
out_elf,
|
|
]
|
|
)
|
|
|
|
out_obj = main_path / f"{main_path.name}.bin"
|
|
subprocess.run(
|
|
["powerpc-eabi-objcopy", "--dump-section", f".data={out_obj}", out_elf]
|
|
)
|
|
|
|
|
|
def unpack():
|
|
with open("src/data/bin2/data/player_room_floor.bin", "rb") as f:
|
|
extract_player_room_floor(
|
|
bytearray(f.read()), "src/data/item/player_room_floor"
|
|
)
|
|
|
|
with open("src/data/bin2/data/player_room_wall.bin", "rb") as f:
|
|
extract_player_room_wall(bytearray(f.read()), "src/data/item/player_room_wall")
|
|
|
|
|
|
def pack():
|
|
cwd = os.getcwd()
|
|
|
|
pack_player_room_floor(Path("src/data/item/player_room_floor"))
|
|
os.chdir(cwd)
|
|
|
|
pack_player_room_wall(Path("src/data/item/player_room_wall"))
|
|
os.chdir(cwd)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Pack or dump Animal Crossing player_room_[floor][wall].bin files."
|
|
)
|
|
parser.add_argument("-m", help="The mode to run. Valid arguments are un[pack].")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.m.lower() == "pack":
|
|
pack()
|
|
elif args.m.lower() == "unpack":
|
|
unpack()
|
|
else:
|
|
raise Exception("Invalid mode! Please use -m un[pack]")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|