sm64pc/extract_assets.py

282 lines
9.2 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):
if version <= 5 and asset == "textures/spooky/bbh_textures.00800.rgba16.png":
return True
if version <= 4 and asset in ["textures/mountain/ttm_textures.01800.rgba16.png", "textures/mountain/ttm_textures.05800.rgba16.png"]:
return True
if version <= 3 and asset == "textures/cave/hmc_textures.01800.rgba16.png":
return True
if version <= 2 and asset == "textures/inside/inside_castle_textures.09000.rgba16.png":
return True
if version <= 1 and asset.endswith(".m64"):
return True
if version <= 0 and asset.endswith(".aiff"):
return True
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))
local_asset_file.close()
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 = 6
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 = ["jp", "us", "eu", "sh"]
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[-1] 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
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
meta = data[:-2]
size, positions = data[-2:]
for lang, pos in positions.items():
mio0 = None if len(pos) == 1 else pos[0]
pos = pos[-1]
if lang in langs:
todo[(lang, mio0)].append((asset, pos, size, meta))
break
# Load ROMs
roms = {}
for lang in langs:
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("sm64." + 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", "skyconv", "mio0", "aifc_decode"]
#)
# Go through the assets in roughly alphabetical order (but assets in the same
# mio0 file 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, mio0 = key
if mio0 == "@sound":
with tempfile.NamedTemporaryFile(prefix="ctl", delete=False) as ctl_file:
with tempfile.NamedTemporaryFile(prefix="tbl", delete=False) as tbl_file:
rom = roms[lang]
size, locs = asset_map["@sound ctl " + lang]
offset = locs[lang][0]
ctl_file.write(rom[offset : offset + size])
ctl_file.close()
size, locs = asset_map["@sound tbl " + lang]
offset = locs[lang][0]
tbl_file.write(rom[offset : offset + size])
tbl_file.close()
args = [
"python3",
"tools/disassemble_sound.py",
ctl_file.name,
tbl_file.name,
"--only-samples",
]
for (asset, pos, size, meta) in assets:
print("extracting", asset)
args.append(asset + ":" + str(pos))
try:
subprocess.run(args, check=True)
finally:
os.unlink(ctl_file.name)
os.unlink(tbl_file.name)
continue
if mio0 is not None:
image = subprocess.run(
[
"./tools/mio0",
"-d",
"-o",
str(mio0),
"baserom." + lang + ".z64",
"-",
],
check=True,
stdout=subprocess.PIPE,
).stdout
else:
image = roms[lang]
for (asset, pos, size, meta) in assets:
print("extracting", asset)
input = image[pos : pos + size]
os.makedirs(os.path.dirname(asset), exist_ok=True)
if asset.endswith(".png"):
with tempfile.NamedTemporaryFile(prefix="asset", delete=False) as png_file:
png_file.write(input)
png_file.flush()
if asset.startswith("textures/skyboxes/") or asset.startswith("levels/ending/cake"):
if asset.startswith("textures/skyboxes/"):
imagetype = "sky"
else:
imagetype = "cake" + ("-eu" if "eu" in asset else "")
subprocess.run(
[
"./tools/skyconv",
"--type",
imagetype,
"--combine",
png_file.name,
asset,
],
check=True,
)
else:
w, h = meta
fmt = asset.split(".")[-2]
subprocess.run(
[
"./tools/n64graphics",
"-e",
png_file.name,
"-g",
asset,
"-f",
fmt,
"-w",
str(w),
"-h",
str(h),
],
check=True,
)
else:
with open(asset, "wb") as f:
f.write(input)
# 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()