Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
1ba42f1ca1d6…
Author-recorded commit
baecad10b1cd…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tools/extract/extract_assets.py
#!/usr/bin/env python3
"""Extract cast-member assets from the original SuperSonic RC game.dcr.

Dev-only tooling. Uses ProjectorRays' --dump-chunks output (decompressed
per-resource chunks) and writes named assets to ref-out/assets/.

    python3 tools/extract/extract_assets.py

Outputs are derived from copyrighted originals: they stay in the gitignored
ref-out/ folder and must never be committed or published.
"""
import glob
import io
import json
import os
import struct
import subprocess
import sys
import wave

from PIL import Image

ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
DCR = os.path.join(ROOT, "ref", "SuperSonicRC", "game.dcr")
PR = os.path.join(ROOT, "tools", "vendor", "ProjectorRays", "projectorrays")
DUMP = os.path.join(ROOT, "ref-out", "chunks")
OUT = os.path.join(ROOT, "ref-out", "assets")

MEMBER_TYPES = {1: "bitmap", 3: "field", 4: "palette", 6: "sound", 11: "script", 15: "xtra"}


def dump_chunks():
    chunks = os.path.join(DUMP, "game", "chunks")
    if not os.path.isdir(chunks):
        os.makedirs(DUMP, exist_ok=True)
        subprocess.run([PR, "decompile", DCR, "--dump-chunks", "--dump-json", "-o", DUMP], check=True)
    return chunks


def read_key_table(chunks):
    """KEY* maps chunk section IDs to the CASt section that owns them."""
    k = open(glob.glob(os.path.join(chunks, "KEY_-*.bin"))[0], "rb").read()
    used = struct.unpack("<I", k[8:12])[0]
    owned = {}
    for i in range(used):
        sect, cast, tag = struct.unpack("<II4s", k[12 + 12 * i:24 + 12 * i])
        owned.setdefault(cast, {})[tag[::-1].decode("latin1")] = sect
    return owned


def chunk(chunks, tag, sect):
    return open(os.path.join(chunks, f"{tag.replace('*', '_')}-{sect}.bin"), "rb").read()


def unpack_rle(data, size):
    """Director's PackBits variant used by BITD and ALFA."""
    out = bytearray()
    i = 0
    while i < len(data) and len(out) < size:
        n = data[i]
        i += 1
        if n & 0x80:
            out += bytes([data[i]]) * (257 - n)
            i += 1
        else:
            out += data[i:i + n + 1]
            i += n + 1
    return bytes(out[:size]).ljust(size, b"\0")


def bitmap_spec(spec):
    pitch = struct.unpack(">H", spec[0:2])[0] & 0x7FFF
    top, left, bottom, right = struct.unpack(">hhhh", spec[2:10])
    reg_y, reg_x = struct.unpack(">hh", spec[18:22])
    bpp = spec[23] if len(spec) > 23 else 8
    return dict(pitch=pitch, width=right - left, height=bottom - top, bpp=bpp, reg=(reg_x, reg_y))


def decode_bitd(raw, s):
    w, h, pitch, bpp = s["width"], s["height"], s["pitch"], s["bpp"]
    data = raw if len(raw) == pitch * h else unpack_rle(raw, pitch * h)
    if bpp == 32:
        img = Image.new("RGBA", (w, h))
        px = []
        for y in range(h):
            row = data[y * pitch:(y + 1) * pitch]
            a, r, g, b = (row[c * w:(c + 1) * w] for c in range(4))
            px += [(r[x], g[x], b[x], a[x]) for x in range(w)]
        img.putdata(px)
        return img
    if bpp == 16:
        img = Image.new("RGB", (w, h))
        px = []
        for y in range(h):
            row = data[y * pitch:(y + 1) * pitch]
            hi, lo = row[:w], row[w:2 * w]
            for x in range(w):
                v = (hi[x] << 8) | lo[x]
                px.append((((v >> 10) & 31) * 255 // 31, ((v >> 5) & 31) * 255 // 31, (v & 31) * 255 // 31))
        img.putdata(px)
        return img
    # 8-bit and lower: no custom palette data in this game, emit grayscale.
    return Image.frombytes("L", (w, h), bytes(data[y * pitch + x] for y in range(h) for x in range(w)))


def apply_alpha(img, alfa, w, h):
    # ALFA rows are padded to an even width (the 291px buttons store 292).
    pitch = (w + 1) & ~1
    a = alfa if len(alfa) == pitch * h else unpack_rle(alfa, pitch * h)
    a = b"".join(a[y * pitch:y * pitch + w] for y in range(h))
    img = img.convert("RGBA")
    img.putalpha(Image.frombytes("L", (w, h), a))
    return img


def mac_snd_to_wav(raw, path):
    """Format-2 Mac 'snd ' resource with a standard or extended sound header."""
    hdr = struct.unpack(">I", raw[10:14])[0]
    h = raw[hdr:]
    rate = struct.unpack(">I", h[8:12])[0] >> 16
    enc = h[20]
    if enc == 0x00:
        n = struct.unpack(">I", h[4:8])[0]
        pcm, ch, width = h[22:22 + n], 1, 1
        pcm = bytes(pcm)  # 8-bit offset binary is WAV's native 8-bit format
    elif enc == 0xFF:
        ch = struct.unpack(">I", h[4:8])[0]
        frames = struct.unpack(">I", h[22:26])[0]
        width = struct.unpack(">H", h[48:50])[0] // 8
        pcm = h[64:64 + frames * ch * width]
        if width == 2:
            pcm = b"".join(pcm[i + 1:i + 2] + pcm[i:i + 1] for i in range(0, len(pcm) - 1, 2))
    else:
        raise ValueError(f"unsupported snd encoding 0x{enc:02x}")
    with wave.open(path, "wb") as w:
        w.setnchannels(ch)
        w.setsampwidth(width)
        w.setframerate(rate)
        w.writeframes(pcm)
    return dict(rate=rate, channels=ch, bits=width * 8)


def swa_to_mp3(raw):
    """Shockwave Audio is MP3 frames behind a Macromedia header."""
    start = struct.unpack(">I", raw[0:4])[0]
    i = start
    while i < len(raw) - 1 and not (raw[i] == 0xFF and raw[i + 1] & 0xE0 == 0xE0):
        i += 1
    return raw[i:]


def xmed_kind(data):
    if data[:4] == b"3DEM":
        return "w3d"
    if b"MAX_exported_world" in data[:64]:
        return "hke"
    if data[:4] == b"FFFF":
        return "text-xtra"
    return "bin"


def main():
    chunks = dump_chunks()
    owned = read_key_table(chunks)
    manifest = []
    for sub in ("bitmaps", "sounds", "3d", "text"):
        os.makedirs(os.path.join(OUT, sub), exist_ok=True)

    for jf in sorted(glob.glob(os.path.join(chunks, "CASt-*.json")), key=lambda p: int(p.rsplit("-", 1)[1][:-5])):
        sect = int(jf.rsplit("-", 1)[1][:-5])
        meta = json.load(open(jf))
        raw = open(jf[:-5] + ".bin", "rb").read()
        name = meta["info"].get("name") or f"member{sect}"
        spec = raw[12 + meta["infoLen"]:12 + meta["infoLen"] + meta["specificDataLen"]]
        kind = MEMBER_TYPES.get(meta["type"], str(meta["type"]))
        # Keep only chunks that were actually published (KEY* can list stale ones).
        res = {t: n for t, n in owned.get(sect, {}).items()
               if os.path.exists(os.path.join(chunks, f"{t.replace('*', '_')}-{n}.bin"))}
        entry = dict(section=sect, name=name, type=kind)

        if kind == "bitmap":
            s = bitmap_spec(spec)
            entry.update(width=s["width"], height=s["height"], bpp=s["bpp"], reg=s["reg"])
            if "ediM" in res:
                img = Image.open(io.BytesIO(chunk(chunks, "ediM", res["ediM"]))).convert("RGB")
            elif "BITD" in res:
                img = decode_bitd(chunk(chunks, "BITD", res["BITD"]), s)
            else:
                continue
            if "ALFA" in res:
                img = apply_alpha(img, chunk(chunks, "ALFA", res["ALFA"]), s["width"], s["height"])
            entry["file"] = f"bitmaps/{name}.png"
            img.save(os.path.join(OUT, entry["file"]))
        elif kind == "sound":
            if "snd " in res:
                entry["file"] = f"sounds/{name}.wav"
                entry.update(mac_snd_to_wav(chunk(chunks, "snd ", res["snd "]), os.path.join(OUT, entry["file"])))
            elif "ediM" in res:
                entry["file"] = f"sounds/{name}.mp3"
                open(os.path.join(OUT, entry["file"]), "wb").write(swa_to_mp3(chunk(chunks, "ediM", res["ediM"])))
            else:
                continue
        elif kind == "xtra" and "XMED" in res:
            data = chunk(chunks, "XMED", res["XMED"])
            ext = xmed_kind(data)
            entry.update(xtra=ext, file=f"3d/{name or sect}.{ext}")
            open(os.path.join(OUT, entry["file"]), "wb").write(data)
        elif kind == "field" and "STXT" in res:
            data = chunk(chunks, "STXT", res["STXT"])
            hdr, tlen = struct.unpack(">II", data[:8])
            entry["file"] = f"text/{name}.txt"
            open(os.path.join(OUT, entry["file"]), "wb").write(data[hdr:hdr + tlen])
        else:
            continue
        manifest.append(entry)
        print(f"{entry['type']:7} {entry['file']}")

    json.dump(manifest, open(os.path.join(OUT, "manifest.json"), "w"), indent=2)
    print(f"{len(manifest)} assets -> {OUT}", file=sys.stderr)


if __name__ == "__main__":
    main()