Files
goldeneye-pc-port/tools_pc/dist/prepare-assets/prepare-assets.py
T
James Dansereau 00fdd94c8d 2-4 player split-screen (#99): D416-D419, D422, D428, D448, D450
Virtual seats, per-seat camera and input routing, aim modes, hot-plugged
controllers reopening via a coalesced rescan (D450, dolent's PR #122,
merged as the original commit). Rule-2
sign-off for D417.

Refs: #99 D416 D417 D418 D419 D422 D428 D448 D450 #122
2026-10-06 18:35:40 -04:00

207 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""One-time asset preparation for the GoldenEye 007 PC port (alpha).
The port loads PC-layout model / display-list / collision / nav data from two
directories that are derived from a *GoldenEye* ROM and therefore are NOT
shipped in this package:
data/pcmodels-<region>/ (pcmodels.bin + manifest.csv)
data/pccg-<region>/ (pccg.bin + manifest.csv)
This script regenerates them from a ROM you legally own. It calls only the
Python standard library -- no compiler, no N64 toolchain, no network. The
conversion is deterministic: the same ROM always produces byte-identical
output.
Usage (from the folder this script lives in, or anywhere):
python prepare-assets.py [--rom PATH] [--out DIR] [--keep-temp]
By default it looks for the ROM at <bundle>/data/*.z64 (and a few other
obvious spots), verifies its SHA-1 to pick the region, and writes the two
directories under <bundle>/data/ , where <bundle> is the folder that
contains this "prepare-assets" folder (i.e. next to ge007.x86_64.exe).
"""
import argparse
import hashlib
import os
import runpy
import shutil
import subprocess
import sys
from pathlib import Path
# When frozen (PyInstaller --onefile, shipped as ge007-convert in the release
# bundle) __file__ is not the install location — the exe lives inside the
# prepare-assets/ folder next to vendor/, so use the executable's directory.
FROZEN = getattr(sys, "frozen", False)
if FROZEN:
HERE = Path(sys.executable).resolve().parent
else:
HERE = Path(__file__).resolve().parent
VENDOR = HERE / "vendor"
# SHA-1 of each supported retail ROM (big-endian .z64) -> region id the
# emit scripts expect. Mirrors ge007.{u,e,j}.sha1 in the source tree.
ROM_SHA1 = {
"abe01e4aeb033b6c0836819f549c791b26cfde83": "ntsc-final", # NTSC-U (US)
"167c3c433dec1f1eb921736f7d53fac8cb45ee31": "pal-final", # PAL (EU)
"2a5dade32f7fad6c73c659d2026994632c1b3174": "jpn-final", # NTSC-J (JP)
}
# (script, args, produces) — run in order. d88 has no output of its own: it
# appends the 21 solo + 13 multiplayer stage-setup files to the pccg dir d69 just made,
# and --regen makes that idempotent.
EMIT_STEPS = (
("d43_emit.py", (), "pcmodels-{region}/pcmodels.bin"),
("d69_emit.py", (), "pccg-{region}/pccg.bin"),
("d88_emit.py", ("--regen",), "pccg-{region}/pccg.bin"),
)
OUTPUT_DIRS = ("pcmodels-{region}", "pccg-{region}")
def die(msg):
print(f"\nprepare-assets: error: {msg}\n", file=sys.stderr)
sys.exit(1)
def sha1_of(path):
h = hashlib.sha1()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def find_rom(explicit, bundle_root):
if explicit:
p = Path(explicit).expanduser()
if not p.is_file():
die(f"--rom {p} : not a file")
return p
search = []
for base in (bundle_root, bundle_root / "data", HERE, HERE / "vendor" / "data"):
search += sorted(base.glob("*.z64")) + sorted(base.glob("*.n64")) + sorted(base.glob("*.v64"))
# de-dup, keep order
seen, cand = set(), []
for p in search:
if p.resolve() not in seen:
seen.add(p.resolve())
cand.append(p)
if not cand:
die("no .z64 ROM found. Put your GoldenEye ROM at "
f"{bundle_root / 'data'} (create the 'data' folder) or pass --rom PATH.")
if len(cand) > 1:
print("prepare-assets: multiple ROM candidates found:")
for p in cand:
print(f" {p}")
print("prepare-assets: using the first; pass --rom PATH to choose.")
return cand[0]
def run_emit(script, argv_tail, cwd):
"""Run one emit script with CWD=cwd. Unfrozen: a subprocess (as before).
Frozen: in-process via runpy — there is no user Python to exec. The
scripts are pure-stdlib; d88 also imports its sibling d88_propdefs, so
this directory must be on sys.path for the duration (run_path does NOT
do that — only a real `python script.py` launch does)."""
if not FROZEN:
r = subprocess.run([sys.executable, str(HERE / script), *argv_tail], cwd=cwd)
return r.returncode
# Frozen sys.executable carries Windows backslashes on MSYS2; the
# import machinery wants a plain directory string.
here = str(HERE).replace("\\", "/")
old_argv, old_cwd = sys.argv, os.getcwd()
sys.argv = [here + "/" + script, *argv_tail]
sys.path.insert(0, here)
os.chdir(cwd)
rc = 0
try:
runpy.run_path(here + "/" + script, run_name="__main__")
except SystemExit as e:
rc = e.code if isinstance(e.code, int) else (1 if e.code else 0)
finally:
sys.argv = old_argv
sys.path.pop(0)
os.chdir(old_cwd)
return rc
def main():
ap = argparse.ArgumentParser(description="Generate the GoldenEye PC port's ROM-derived asset dirs.")
ap.add_argument("--rom", help="path to your .z64 ROM (default: auto-detect near the bundle)")
ap.add_argument("--out", help="bundle root to write data/ into (default: the folder holding prepare-assets)")
ap.add_argument("--keep-temp", action="store_true", help="keep the temporary ROM copy under vendor/data/")
args = ap.parse_args()
if not VENDOR.is_dir():
die(f"missing {VENDOR} -- this script must stay inside the 'prepare-assets' folder from the zip.")
for script, _extra, _produces in EMIT_STEPS:
if not (HERE / script).is_file():
die(f"missing {HERE / script}")
if not (HERE / "d88_propdefs.py").is_file():
die(f"missing {HERE / 'd88_propdefs.py'} (imported by d88_emit.py)")
bundle_root = Path(args.out).expanduser().resolve() if args.out else HERE.parent
rom = find_rom(args.rom, bundle_root)
print(f"prepare-assets: ROM {rom}")
digest = sha1_of(rom)
region = ROM_SHA1.get(digest)
if region is None:
die(f"ROM SHA-1 {digest} is not a recognised retail GoldenEye 007 image.\n"
" Supported: NTSC-U (US), big-endian .z64.\n"
" A byte-swapped (.n64/.v64) or overdumped ROM will not work -- convert to .z64 first.")
if region != "ntsc-final":
die(f"this release only supports the NTSC-U (US) ROM; your ROM is the "
f"{region} version.\n"
" PAL / JP support is on the roadmap but not in this build.")
print(f"prepare-assets: SHA-1 {digest} -> region '{region}'")
# The emit scripts assume CWD is a tree with scripts/ assets/ data/ .
work = VENDOR
(work / "data").mkdir(exist_ok=True)
staged_rom = work / "data" / f"ge007.{region}.z64"
if not (staged_rom.exists() and staged_rom.stat().st_size == rom.stat().st_size):
shutil.copy2(rom, staged_rom)
# Fresh start so a re-run is deterministic (d69 recreates its dir; d88
# --regen edits it in place).
for d in OUTPUT_DIRS:
p = work / "data" / d.format(region=region)
if p.exists():
shutil.rmtree(p)
for script, extra, produces in EMIT_STEPS:
print(f"prepare-assets: running {script} {region} {' '.join(extra)} ...")
rc = run_emit(script, (region, *extra), work)
if rc != 0:
die(f"{script} exited {rc}")
want = work / "data" / produces.format(region=region)
if not want.is_file():
die(f"{script} did not produce {want}")
dest_data = bundle_root / "data"
dest_data.mkdir(parents=True, exist_ok=True)
for d in OUTPUT_DIRS:
src = work / "data" / d.format(region=region)
dest = dest_data / d.format(region=region)
if dest.resolve() == src.resolve():
continue
if dest.exists():
shutil.rmtree(dest)
shutil.move(str(src), str(dest))
n = sum(1 for _ in dest.rglob("*") if _.is_file())
print(f"prepare-assets: wrote {dest} ({n} files)")
if not args.keep_temp and staged_rom.exists() and staged_rom.resolve() != rom.resolve():
staged_rom.unlink()
print("\nprepare-assets: done. You can now run the game from the bundle folder.")
print(f" (Your ROM must also be at {dest_data / f'ge007.{region}.z64'} for the game itself.)")
if __name__ == "__main__":
main()