feat(release): self-contained asset-prep tool for the alpha bundle

The Windows bundle from bundle-win.sh / CI was unrunnable on its own (D179):
the port loads two ROM-derived directories (data/pcmodels-<region>/ and
data/pccg-<region>/) that are gitignored and cannot be shipped.

Add tools_pc/dist/prepare-assets/prepare-assets.py — a stdlib-only wrapper
(no compiler, no N64 toolchain, no network) that:
  - auto-detects the ROM near the bundle, verifies SHA-1 -> region
    (NTSC-U / PAL / NTSC-J), rejects unrecognised / byte-swapped dumps
  - runs d43_emit.py + d69_emit.py against a vendored input tree
  - writes data/pcmodels-<region>/ and data/pccg-<region>/ next to the exe

bundle-win.sh assembles prepare-assets/ fresh at package time (emit scripts
from tools_pc/, filelist.u.csv + file_resource_table.inc.c + the 512
modelFileHeader.inc.c files as vendored inputs) so it can never drift from
the scripts. Vendored inputs are decomp layout/symbol metadata, not game
assets. bundled README gets a one-time "prepare-assets" setup step.

Verified end-to-end: from a bundle containing only exe + DLLs + a US ROM,
prepare-assets.py produces all four output files byte-identical to
d43_emit.py / d69_emit.py run from the repo root. Bundle 14.1 MB (< 60 MB
guard). Error paths (no ROM / unknown SHA-1) tested.

Resolves BACKLOG "Alpha release — asset-prep tool" steps 1-3; step 4
(clean-machine smoke test with the game actually running) still owed.
This commit is contained in:
James Dansereau committed 2026-09-02 20:43:55 -04:00
1 parent ce9e090527
commit dfab1876e4
3 files changed
+196 -8

No files matched your search

+20
View File
@@ -78,6 +78,26 @@ for l in SDL2 zlib gcc-libs libwinpthread mingw-w64; do
[ -d "$MINGW/share/licenses/$l" ] && cp -r "$MINGW/share/licenses/$l" "$OUT/licenses/$l"
done
# --- asset-prep tool -------------------------------------------------
# The port needs two ROM-derived directories (data/pcmodels-<region>/ and
# data/pccg-<region>/) that we cannot ship. prepare-assets.py regenerates
# them from the user's own ROM using only the Python standard library. It is
# assembled fresh from the tree here so it can never drift from the emit
# scripts. The vendored inputs are decomp layout/symbol metadata (.inc.c /
# .csv), NOT game assets (no textures, audio, models, levels, or text).
PREP="$OUT/prepare-assets"
mkdir -p "$PREP/vendor/scripts" "$PREP/vendor/assets/obseg"
cp tools_pc/dist/prepare-assets/prepare-assets.py "$PREP/"
cp tools_pc/d43_emit.py tools_pc/d69_emit.py "$PREP/"
cp scripts/filelist.u.csv "$PREP/vendor/scripts/"
cp assets/obseg/file_resource_table.inc.c "$PREP/vendor/assets/obseg/"
# d43_emit.py os.walk()s assets/ for every *modelFileHeader.inc.c — preserve paths.
( cd . && find assets -iname 'modelfileheader.inc.c' -print0 \
| xargs -0 -I{} cp --parents {} "$PREP/vendor/" )
nmh="$(find "$PREP/vendor/assets" -iname 'modelfileheader.inc.c' | wc -l)"
echo " + prepare-assets/ (emit scripts + $nmh model headers)"
[ "$nmh" -gt 400 ] || { echo "error: prepare-assets vendored only $nmh model headers — expected ~512" >&2; exit 1; }
# --- guard: no ROM / game data snuck in --------------------------------
if find "$OUT" -type f \( -iname '*.z64' -o -iname '*.n64' -o -iname '*.v64' \) | grep -q .; then
echo "error: bundle contains a ROM image — aborting" >&2
+19 -8
View File
@@ -28,20 +28,31 @@ NTSC-U (US):
Only the US ROM is supported in this alpha.
## 2. Run
## 2. Set up (one time)
1. Unzip this folder anywhere.
2. Make a `data` folder next to `ge007.x86_64.exe`.
3. Copy your ROM into it, named exactly: `data\ge007.ntsc-final.z64`
4. Run `ge007.x86_64.exe` **from this folder**.
4. Prepare the ROM-derived data (needs Python 3.8+ on your PATH):
Configuration is written to `ge007.ini` on first run.
```
python prepare-assets\prepare-assets.py
```
> If the game exits immediately reporting missing data, your ROM's assets need
> the one-time extraction step described under "Building" in the project README
> — the alpha binary expects the same asset layout a source build produces.
This reads your ROM and writes `data\pcmodels-ntsc-final\` and
`data\pccg-ntsc-final\` next to the exe. It uses only the Python standard
library — no compiler or N64 toolchain — and takes a few seconds. Run it
again only if you replace the ROM.
## 3. Controls (defaults)
## 3. Run
Run `ge007.x86_64.exe` **from this folder**. Configuration is written to
`ge007.ini` on first run.
> If the game exits immediately reporting missing data, re-run step 2.4 above
> and check it printed `ALL CHECKS PASSED` for both passes.
## 4. Controls (defaults)
| Action | Keyboard / mouse |
|---|---|
@@ -55,7 +66,7 @@ Configuration is written to `ge007.ini` on first run.
Mouse sensitivity and Y-inversion are in the `[Input]` section of `ge007.ini`.
## 4. Legal
## 5. Legal
Non-commercial fan preservation / research project. **Not** affiliated with,
endorsed by, or sponsored by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON
+157
View File
@@ -0,0 +1,157 @@
#!/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 shutil
import subprocess
import sys
from pathlib import Path
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)
}
EMIT_STEPS = (
("d43_emit.py", "pcmodels"), # models / display lists -> data/pcmodels-<region>/
("d69_emit.py", "pccg"), # collision + stan nav -> data/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 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, _ in EMIT_STEPS:
if not (HERE / script).is_file():
die(f"missing {HERE / script}")
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 / PAL / NTSC-J, big-endian .z64.\n"
" A byte-swapped (.n64/.v64) or overdumped ROM will not work -- convert to .z64 first.")
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)
out_dirs = []
for script, tag in EMIT_STEPS:
src = work / "data" / f"{tag}-{region}"
if src.exists():
shutil.rmtree(src)
print(f"prepare-assets: running {script} {region} ...")
r = subprocess.run([sys.executable, str(HERE / script), region], cwd=work)
if r.returncode != 0:
die(f"{script} exited {r.returncode}")
if not (src / f"{tag}.bin").is_file():
die(f"{script} did not produce {src / (tag + '.bin')}")
out_dirs.append((src, tag))
dest_data = bundle_root / "data"
dest_data.mkdir(parents=True, exist_ok=True)
for src, tag in out_dirs:
dest = dest_data / f"{tag}-{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 ge007.x86_64.exe from the bundle folder.")
print(f" (Your ROM must also be at {dest_data / f'ge007.{region}.z64'} for the game itself.)")
if region != "ntsc-final":
print(" NOTE: only the NTSC-U ROM is validated in this alpha; "
f"'{region}' is experimental.")
if __name__ == "__main__":
main()