mirror of
https://github.com/farisawan-2000/kirby64.git
synced 2024-11-23 13:20:03 +00:00
280 lines
9.9 KiB
Python
Executable File
280 lines
9.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import sys
|
|
import os
|
|
import json
|
|
|
|
|
|
def read_asset_map():
|
|
with open("assets.json") as f:
|
|
ret = json.load(f)
|
|
return ret
|
|
|
|
|
|
def read_local_asset_list(f):
|
|
if f is None:
|
|
return []
|
|
ret = []
|
|
for line in f:
|
|
ret.append(line.strip())
|
|
return ret
|
|
|
|
|
|
def asset_needs_update(asset, version):
|
|
return False
|
|
|
|
|
|
def remove_file(fname):
|
|
os.remove(fname)
|
|
print("deleting", fname)
|
|
try:
|
|
os.removedirs(os.path.dirname(fname))
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def clean_assets(local_asset_file):
|
|
assets = set(read_asset_map().keys())
|
|
assets.update(read_local_asset_list(local_asset_file))
|
|
for fname in list(assets) + [".assets-local.txt"]:
|
|
if fname.startswith("@"):
|
|
continue
|
|
try:
|
|
remove_file(fname)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
def main():
|
|
# In case we ever need to change formats of generated files, we keep a
|
|
# revision ID in the local asset file.
|
|
new_version = 1
|
|
|
|
try:
|
|
local_asset_file = open(".assets-local.txt")
|
|
local_asset_file.readline()
|
|
local_version = int(local_asset_file.readline().strip())
|
|
except Exception:
|
|
local_asset_file = None
|
|
local_version = -1
|
|
|
|
langs = sys.argv[1:]
|
|
if langs == ["--clean"]:
|
|
clean_assets(local_asset_file)
|
|
sys.exit(0)
|
|
|
|
all_langs = ["us", "CI"]
|
|
if not langs or not all(a in all_langs for a in langs):
|
|
langs_str = " ".join("[" + lang + "]" for lang in all_langs)
|
|
print("Usage: " + sys.argv[0] + " " + langs_str)
|
|
print("For each version, baserom.<version>.z64 must exist")
|
|
sys.exit(1)
|
|
|
|
asset_map = read_asset_map()
|
|
all_assets = []
|
|
any_missing_assets = False
|
|
for asset, data in asset_map.items():
|
|
if asset.startswith("@"):
|
|
continue
|
|
if os.path.isfile(asset):
|
|
all_assets.append((asset, data, True))
|
|
else:
|
|
all_assets.append((asset, data, False))
|
|
if not any_missing_assets and any(lang in data["offsets"] for lang in langs):
|
|
any_missing_assets = True
|
|
|
|
if not any_missing_assets and local_version == new_version:
|
|
# Nothing to do, no need to read a ROM. For efficiency we don't check
|
|
# the list of old assets either.
|
|
return
|
|
|
|
# Late imports (to optimize startup perf)
|
|
import subprocess
|
|
import hashlib
|
|
import tempfile
|
|
from collections import defaultdict
|
|
import math
|
|
|
|
new_assets = {a[0] for a in all_assets}
|
|
|
|
previous_assets = read_local_asset_list(local_asset_file)
|
|
if local_version == -1:
|
|
# If we have no local asset file, we assume that files are version
|
|
# controlled and thus up to date.
|
|
local_version = new_version
|
|
|
|
# Create work list
|
|
todo = defaultdict(lambda: [])
|
|
for (asset, data, exists) in all_assets:
|
|
# Leave existing assets alone if they have a compatible version.
|
|
if exists and not asset_needs_update(asset, local_version):
|
|
continue
|
|
|
|
for lang, pos in data["offsets"].items():
|
|
if len(pos) == 1:
|
|
rom_offset = None
|
|
block_offset = None
|
|
else:
|
|
rom_offset = int(pos[0], 0)
|
|
block_offset = int(pos[1], 0)
|
|
if lang in langs:
|
|
todo[(lang, rom_offset)].append((asset, block_offset, data["meta"]))
|
|
break
|
|
|
|
# Load ROMs
|
|
roms = {}
|
|
for lang in langs:
|
|
if lang != "CI":
|
|
fname = "baserom." + lang + ".z64"
|
|
try:
|
|
with open(fname, "rb") as f:
|
|
roms[lang] = f.read()
|
|
except:
|
|
print("Failed to open " + fname + ". Please ensure it exists!")
|
|
sys.exit(1)
|
|
sha1 = hashlib.sha1(roms[lang]).hexdigest()
|
|
with open("kirby." + lang + ".sha1", "r") as f:
|
|
expected_sha1 = f.read().split()[0]
|
|
if sha1 != expected_sha1:
|
|
print(fname + " has the wrong hash! Found " + sha1 + ", expected " + expected_sha1)
|
|
sys.exit(1)
|
|
|
|
# Make sure tools exist
|
|
subprocess.check_call(
|
|
["make", "-s", "-C", "tools/", "n64graphics"]
|
|
)
|
|
|
|
# Go through the assets in roughly alphabetical order (but assets in the same
|
|
# compressed block still go together).
|
|
keys = sorted(list(todo.keys()), key=lambda k: todo[k][0][0])
|
|
# Import new assets
|
|
for key in keys:
|
|
assets = todo[key]
|
|
lang, rom_offset = key
|
|
|
|
if rom_offset is not None:
|
|
magic = roms[lang][rom_offset:rom_offset + 4]
|
|
if magic == b"MIO0":
|
|
image = subprocess.run(
|
|
[
|
|
"./tools/mio0",
|
|
"-d",
|
|
"-o", str(rom_offset),
|
|
"baserom." + lang + ".z64",
|
|
"-",
|
|
],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
).stdout
|
|
# TODO: binary assets in assets.json for TKMK00 until it is full understood
|
|
elif magic == b"TKMK" and assets[0][0].endswith(".png"):
|
|
(asset, pos, meta) = assets[0]
|
|
image = subprocess.run(
|
|
[
|
|
"./tools/tkmk00",
|
|
"-d",
|
|
"-o", str(rom_offset),
|
|
"-a", meta["alpha"],
|
|
"baserom." + lang + ".z64",
|
|
"-",
|
|
],
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
).stdout
|
|
else: # assume raw texture
|
|
# TODO: This grabs way more data than is necessary
|
|
image = roms[lang][rom_offset:]
|
|
else:
|
|
image = roms[lang]
|
|
|
|
for (asset, pos, meta) in assets:
|
|
if "CI" not in langs:
|
|
print("extracting", asset)
|
|
if "size" in meta:
|
|
# TODO: hack for extracting raw binary from MIO0 block
|
|
if magic == b"MIO0":
|
|
size = len(image)
|
|
else:
|
|
size = int(meta["size"], 0)
|
|
elif "dims" in meta:
|
|
w, h = meta["dims"]
|
|
pixels = w * h
|
|
filename_parts = asset.split(".")
|
|
fmt = filename_parts[-2]
|
|
if fmt.endswith("32"): size = pixels * 4
|
|
elif fmt.endswith("16"): size = pixels * 2
|
|
elif fmt.endswith("8"): size = pixels
|
|
elif fmt.endswith("4"): size = math.ceil(pixels / 2)
|
|
elif fmt == "ia1": size = math.ceil(pixels / 8)
|
|
input = image[pos : pos + size]
|
|
os.makedirs(os.path.dirname(asset), exist_ok=True)
|
|
if asset.endswith(".png"):
|
|
with tempfile.NamedTemporaryFile(prefix="asset") as tex_file:
|
|
tex_file.write(input)
|
|
tex_file.flush()
|
|
cmd = [
|
|
"./tools/n64graphics",
|
|
"-e", tex_file.name,
|
|
"-g", asset,
|
|
"-f", fmt,
|
|
"-w", str(w),
|
|
"-h", str(h),
|
|
]
|
|
# For CI textures, filename is <name>.rgba16.ci8.png
|
|
if fmt == "ci8" or fmt == "ci4":
|
|
ci_fmt = filename_parts[-3]
|
|
# palette can be in raw ROM, block CI data is located in, or another block
|
|
# TODO: might have offset variation in langs
|
|
pal_size = 2*256 if fmt == "ci8" else 2*16
|
|
if type(meta["pal"]) is list:
|
|
pal_offset = int(meta["pal"][0], 0)
|
|
pal_source = int(meta["pal"][1], 0)
|
|
if pal_source == 0: # 0 is ROM
|
|
pal_input = roms[lang][pal_offset : pal_offset + pal_size]
|
|
else: # assume MIO0 block
|
|
pal_blk = subprocess.run(["./tools/mio0", "-d", "-o", str(pal_source), "baserom." + lang + ".z64", "-"],
|
|
check=True, stdout=subprocess.PIPE).stdout
|
|
pal_input = pal_blk[pal_offset : pal_offset + pal_size]
|
|
else: # pull directly from same block as CI data
|
|
pal_offset = int(meta["pal"], 0)
|
|
pal_input = image[pal_offset : pal_offset + pal_size]
|
|
tmp_pal = tempfile.NamedTemporaryFile(prefix="asset_pal")
|
|
tmp_pal.write(pal_input)
|
|
tmp_pal.flush()
|
|
# append the palette commands
|
|
cmd.extend([
|
|
"-c", ci_fmt,
|
|
"-p", tmp_pal.name
|
|
])
|
|
subprocess.run(cmd, check=True)
|
|
else:
|
|
with open(asset, "wb") as f:
|
|
f.write(input)
|
|
if "geo" in asset:
|
|
if "bank_0" in asset or "bank_1" in asset or "bank_2" in asset or "bank_7" in asset:
|
|
asset_c = asset[:-3] + "c"
|
|
print("Converting %s to C..." % asset)
|
|
subprocess.run("python3 tools/scut/GeoFromBin.py %s %s" % (asset, asset_c), shell=True)
|
|
|
|
# Remove old assets
|
|
for asset in previous_assets:
|
|
if asset not in new_assets:
|
|
try:
|
|
remove_file(asset)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
# Replace the asset list
|
|
output = "\n".join(
|
|
[
|
|
"# This file tracks the assets currently extracted by extract_assets.py.",
|
|
str(new_version),
|
|
*sorted(list(new_assets)),
|
|
"",
|
|
]
|
|
)
|
|
with open(".assets-local.txt", "w") as f:
|
|
f.write(output)
|
|
|
|
|
|
main()
|