mirror of
				https://github.com/coop-deluxe/sm64coopdx.git
				synced 2025-10-30 08:01:01 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			286 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			286 lines
		
	
	
	
		
			9.3 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))
 | 
						|
    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 Exception as e:
 | 
						|
            print("Failed to open " + fname + "! " + str(e))
 | 
						|
            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"):
 | 
						|
                png_file = tempfile.NamedTemporaryFile(prefix="asset", delete=False)
 | 
						|
                try:
 | 
						|
                    png_file.write(input)
 | 
						|
                    png_file.flush()
 | 
						|
                    png_file.close()
 | 
						|
                    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,
 | 
						|
                        )
 | 
						|
                finally:
 | 
						|
                    png_file.close()
 | 
						|
                    os.remove(png_file.name)
 | 
						|
            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()
 |