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/build_level.py
#!/usr/bin/env python3
"""Build game-ready reference data from the extracted original assets.

Dev-only tooling. Inputs come from extract_assets.py, hke.py, lightmap_uvs.py
and macromelt (see tools/README.md). Writes ref-out/game/:

  level.glb      visual level, Director Z-up units, with TEXCOORD_1 lightmap
                 UVs and mesh.extras.lightmap naming the lightmap texture
  car.glb, prize.glb, star.glb, arrow2.glb
  lightmaps/*.jpg, ui/*.{webp,jpg} (other bitmaps), sounds/*.{wav,mp3}

The bundle embeds all of this, so outputs are kept compact: GLBs share
duplicated textures and vertex data, UI bitmaps are WebP and one-shot sounds
are MP3 (needs ffmpeg). The engine sounds stay PCM WAV: they are queued back to
back and timed off their exact durations, which MP3 padding would change.
  collision.json Havok static bodies (world-space triangles + material)
  raymesh.bin    world-space triangles of every level model (the "3d2" copy the
                 wheels and camera raycast against): u32 counts, f32 xyz verts,
                 u32 indices, u16 model index per triangle; names in markers.json
  markers.json   W3D group nodes (checkpoints, prizes, camera targets) and
                 the level's cameras and model placements

Everything in ref-out/ is derived from copyrighted originals: local use only.
"""
import json
import os
import shutil
import struct
import subprocess

ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ASSETS = os.path.join(ROOT, "ref-out", "assets")
OUT = os.path.join(ROOT, "ref-out", "game")
MACROMELT = os.path.join(ROOT, "tools", "vendor", "macromelt", "target", "release", "macromelt")


# --- GLB helpers -------------------------------------------------------------

def read_glb(path):
    b = open(path, "rb").read()
    jlen = struct.unpack_from("<I", b, 12)[0]
    doc = json.loads(b[20:20 + jlen])
    off = 20 + jlen
    blen = struct.unpack_from("<I", b, off)[0]
    return doc, bytearray(b[off + 8:off + 8 + blen])


def write_glb(path, doc, binary):
    while len(binary) % 4:
        binary.append(0)
    doc["buffers"] = [{"byteLength": len(binary)}]
    j = json.dumps(doc, separators=(",", ":")).encode()
    j += b" " * (-len(j) % 4)
    total = 12 + 8 + len(j) + 8 + len(binary)
    with open(path, "wb") as f:
        f.write(struct.pack("<III", 0x46546C67, 2, total))
        f.write(struct.pack("<II", len(j), 0x4E4F534A) + j)
        f.write(struct.pack("<II", len(binary), 0x004E4942) + bytes(binary))


def dedup_glb(doc, binary):
    """Share byte-identical buffer views, images, textures and accessors.

    Materials, meshes and nodes are left alone (the game looks materials up by
    name). Returns the compacted binary.
    """
    # Drop accessors nothing references (e.g. replaced vertex data).
    used_acc = sorted({a for m in doc.get("meshes", []) for p in m["primitives"]
                       for a in [*p["attributes"].values(), *([p["indices"]] if "indices" in p else [])]})
    acc_keep = {a: i for i, a in enumerate(used_acc)}
    doc["accessors"] = [doc["accessors"][a] for a in used_acc]
    for m in doc.get("meshes", []):
        for p in m["primitives"]:
            p["attributes"] = {k: acc_keep[a] for k, a in p["attributes"].items()}
            if "indices" in p:
                p["indices"] = acc_keep[p["indices"]]

    def view_bytes(i):
        v = doc["bufferViews"][i]
        o = v.get("byteOffset", 0)
        return bytes(binary[o:o + v["byteLength"]])

    referenced = {a["bufferView"] for a in doc["accessors"]} | {im["bufferView"] for im in doc.get("images", [])}
    views, view_map = {}, {}
    for i, v in enumerate(doc["bufferViews"]):
        if i not in referenced:
            continue
        key = (view_bytes(i), v.get("byteStride"), v.get("target"))
        view_map[i] = views.setdefault(key, len(views))
    out = bytearray()
    new_views = []
    for (data, stride, target) in views:
        out += b"\0" * (-len(out) % 4)
        v = {"buffer": 0, "byteOffset": len(out), "byteLength": len(data)}
        if stride:
            v["byteStride"] = stride
        if target:
            v["target"] = target
        new_views.append(v)
        out += data
    doc["bufferViews"] = new_views

    def merge(items, remap_fields):
        seen, mapping, kept = {}, {}, []
        for i, item in enumerate(items):
            for f, m in remap_fields:
                if f in item:
                    item[f] = m[item[f]]
            key = json.dumps(item, sort_keys=True)
            if key not in seen:
                seen[key] = len(kept)
                kept.append(item)
            mapping[i] = seen[key]
        return kept, mapping

    doc["accessors"], acc_map = merge(doc.get("accessors", []), [("bufferView", view_map)])
    # Image names are only labels; identical bytes are the same picture.
    for im in doc.get("images", []):
        im.pop("name", None)
    doc["images"], img_map = merge(doc.get("images", []), [("bufferView", view_map)])
    doc["textures"], tex_map = merge(doc.get("textures", []), [("source", img_map)])
    for m in doc.get("materials", []):
        tex = m.get("pbrMetallicRoughness", {}).get("baseColorTexture")
        if tex:
            tex["index"] = tex_map[tex["index"]]
    for mesh in doc.get("meshes", []):
        for prim in mesh["primitives"]:
            prim["attributes"] = {k: acc_map[a] for k, a in prim["attributes"].items()}
            if "indices" in prim:
                prim["indices"] = acc_map[prim["indices"]]
    for key in ("images", "textures", "accessors"):
        if not doc[key]:
            del doc[key]
    return out


def compact_glb(src, dst):
    doc, binary = read_glb(src)
    write_glb(dst, doc, dedup_glb(doc, binary))


def accessor_floats(doc, binary, idx):
    acc = doc["accessors"][idx]
    view = doc["bufferViews"][acc["bufferView"]]
    width = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4}[acc["type"]]
    base = view.get("byteOffset", 0) + acc.get("byteOffset", 0)
    stride = view.get("byteStride", 4 * width)
    return [struct.unpack_from(f"<{width}f", binary, base + i * stride) for i in range(acc["count"])]


def add_accessor(doc, binary, values, kind="VEC"):
    """Append float vectors (kind "VEC") or triangle indices ("SCALAR")."""
    while len(binary) % 4:
        binary.append(0)
    off = len(binary)
    acc = {"bufferView": len(doc["bufferViews"]), "byteOffset": 0}
    if kind == "SCALAR":
        fmt, ctype = ("H", 5123) if max(values) < 65536 else ("I", 5125)
        binary += struct.pack(f"<{len(values)}{fmt}", *values)
        doc["bufferViews"].append({"buffer": 0, "byteOffset": off, "byteLength": len(binary) - off, "target": 34963})
        acc.update(componentType=ctype, count=len(values), type="SCALAR")
    else:
        width = len(values[0])
        for v in values:
            binary += struct.pack(f"<{width}f", *v)
        doc["bufferViews"].append({"buffer": 0, "byteOffset": off, "byteLength": 4 * width * len(values), "target": 34962})
        acc.update(componentType=5126, count=len(values), type=f"VEC{width}",
                   min=[min(v[c] for v in values) for c in range(width)],
                   max=[max(v[c] for v in values) for c in range(width)])
    del acc["byteOffset"]
    doc["accessors"].append(acc)
    return len(doc["accessors"]) - 1


# --- W3D materials (0x10): the GLB export keeps only the diffuse colour ---------

def w3d_materials(path):
    b = open(path, "rb").read()
    i = b.find(b"IFX") + 16
    out = {}
    while i + 8 <= len(b):
        kind, size = struct.unpack_from("<II", b, i)
        body = b[i + 8:i + 8 + size]
        if kind == 0xFFFFFF10:
            n = struct.unpack_from("<H", body, 0)[0]
            name = body[2:2 + n].decode("latin1")
            f = struct.unpack_from(f"<{(size - 2 - n - 4) // 4}f", body, 2 + n + 4)
            out[name] = dict(ambient=f[0:3], diffuse=f[4:7], specular=f[8:11], emissive=f[12:15],
                             shininess=f[16] if len(f) > 16 else 0, opacity=f[17] if len(f) > 17 else 1)
        i += 8 + ((size + 3) & ~3)
    return out


# --- W3D group nodes ----------------------------------------------------------

def w3d_nodes(path):
    """0x70 group, 0x72 model and 0x74 view nodes: name, parent, matrix."""
    b = open(path, "rb").read()
    i = b.find(b"IFX") + 16
    nodes = []
    while i + 8 <= len(b):
        kind, size = struct.unpack_from("<II", b, i)
        body = b[i + 8:i + 8 + size]
        if kind in (0xFFFFFF70, 0xFFFFFF72, 0xFFFFFF74):
            p = 0
            strs = []
            for _ in range(3):  # name, parent, user properties
                n = struct.unpack_from("<H", body, p)[0]
                strs.append(body[p + 2:p + 2 + n].decode("latin1"))
                p += 2 + n
            matrix = list(struct.unpack_from("<16f", body, p))
            nodes.append(dict(kind={0x70: "group", 0x72: "model", 0x74: "view"}[kind & 0xFF],
                              name=strs[0], parent=strs[1], user=strs[2], matrix=matrix))
        i += 8 + ((size + 3) & ~3)
    return nodes


def main():
    os.makedirs(os.path.join(OUT, "lightmaps"), exist_ok=True)
    geom = {e["name"]: e for e in json.load(open(os.path.join(ASSETS, "geom.json")))}
    lm = json.load(open(os.path.join(ASSETS, "lightmap_uvs.json")))
    doc, binary = read_glb(os.path.join(ASSETS, "glb", "3d.glb"))

    used_maps = set()
    for mesh in doc["meshes"]:
        name = mesh["name"].removeprefix("mesh:")
        info = lm.get(name)
        if not info or not info.get("lightmap"):
            continue
        g = geom[name]
        # lightMapManager.applyUVs sets meshDeform.mesh[n].textureLayer[2] per vertex index;
        # the blocks are contiguous per submesh, in order, like geom.json's vertices.
        flat = [uv for block in info["uvs"] for uv in block]
        assert len(flat) == len(g["positions"]), name
        assert len(mesh["primitives"]) == len(set(g["submesh_of_face"])), name
        # The GLB export reorders vertices, and seam vertices share position and normal
        # but not lightmap UV, so rebuild each primitive from the decoded mesh by index.
        texcoords = g["texcoords"]
        for k, prim in enumerate(mesh["primitives"]):
            faces = [f for f, s in zip(g["faces"], g["submesh_of_face"]) if s == k]
            used = sorted({i for f in faces for i in f})
            local = {v: j for j, v in enumerate(used)}
            attrs = prim["attributes"]
            attrs["POSITION"] = add_accessor(doc, binary, [g["positions"][i] for i in used])
            attrs["NORMAL"] = add_accessor(doc, binary, [g["normals"][i] for i in used])
            if "TEXCOORD_0" in attrs:
                # glTF's v runs down the image; Director's runs up.
                attrs["TEXCOORD_0"] = add_accessor(doc, binary, [(texcoords[i][0], 1 - texcoords[i][1]) for i in used])
            attrs["TEXCOORD_1"] = add_accessor(doc, binary, [tuple(flat[i]) for i in used])
            prim["indices"] = add_accessor(doc, binary, [local[i] for f in faces for i in f], "SCALAR")
        mesh.setdefault("extras", {})["lightmap"] = info["lightmap"] + "LightingMap"
        used_maps.add(info["lightmap"] + "LightingMap")
    write_glb(os.path.join(OUT, "level.glb"), doc, dedup_glb(doc, binary))
    print(f"level.glb: lightmaps {sorted(used_maps)}")

    # Lightmaps are used as rgba8880 (no alpha): JPEG keeps the bundle small.
    from PIL import Image
    for name in used_maps:
        Image.open(os.path.join(ASSETS, "bitmaps", f"{name}.png")).convert("RGB").save(
            os.path.join(OUT, "lightmaps", f"{name}.jpg"), quality=92)

    # UI bitmaps and sounds (lightmaps are copied above).
    for sub in ("ui", "sounds"):
        os.makedirs(os.path.join(OUT, sub), exist_ok=True)
    # Full-screen backdrops: their alpha plane is broken, the RGB is complete.
    opaque = {"Title-v8", "Help-v5", "Background"}
    for f in os.listdir(os.path.join(ASSETS, "bitmaps")):
        stem = f.rsplit(".", 1)[0]
        if "LightingMap" in f:
            continue
        if stem in opaque:
            Image.open(os.path.join(ASSETS, "bitmaps", f)).convert("RGB").save(os.path.join(OUT, "ui", f"{stem}.jpg"), quality=90)
        else:
            # Lossy colour, exact alpha: the button art keeps its hard edges.
            Image.open(os.path.join(ASSETS, "bitmaps", f)).save(
                os.path.join(OUT, "ui", f"{stem}.webp"), quality=90, alpha_quality=100, method=6)
    pcm_sounds = {"CarStart", "CarLoop", "CarEnd"}
    for f in os.listdir(os.path.join(ASSETS, "sounds")):
        stem, ext = f.rsplit(".", 1)
        src = os.path.join(ASSETS, "sounds", f)
        if ext == "wav" and stem not in pcm_sounds:
            subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", src, "-c:a", "libmp3lame", "-b:a", "32k",
                            os.path.join(OUT, "sounds", f"{stem}.mp3")], check=True)
        else:
            shutil.copy(src, os.path.join(OUT, "sounds", f))

    for src, dst in (("holypolycrap", "car"), ("prize", "prize"), ("star", "star"), ("arrow2", "arrow2")):
        compact_glb(os.path.join(ASSETS, "glb", f"{src}.glb"), os.path.join(OUT, f"{dst}.glb"))

    havok = json.load(open(os.path.join(ASSETS, "havok.json")))
    collision = dict(gravity=havok["gravity_world"], scale=havok["scale"], bodies=[
        dict(name=b["name"], convex=b["convex"], friction=b["friction"], restitution=b["restitution"],
             verts=[round(c, 3) for v in b["world_verts"] for c in v], faces=[i for f in b["faces"] for i in f])
        for b in havok["bodies"]])
    json.dump(collision, open(os.path.join(OUT, "collision.json"), "w"), separators=(",", ":"))

    # Ray mesh: every level model in world space, as in the duplicated "3d2" member.
    names, verts, tris, owner = [], [], [], []
    for gi, g in enumerate(geom.values()):
        m = g["node_matrix"]  # row-vector convention, translation in 12..14
        base = len(verts) // 3
        for x, y, z in g["positions"]:
            verts += [x * m[0] + y * m[4] + z * m[8] + m[12], x * m[1] + y * m[5] + z * m[9] + m[13],
                      x * m[2] + y * m[6] + z * m[10] + m[14]]
        for f in g["faces"]:
            tris += [base + f[0], base + f[1], base + f[2]]
            owner.append(gi)
        names.append(g["name"])
    with open(os.path.join(OUT, "raymesh.bin"), "wb") as f:
        f.write(struct.pack("<II", len(verts) // 3, len(tris) // 3))
        f.write(struct.pack(f"<{len(verts)}f", *verts))
        f.write(struct.pack(f"<{len(tris)}I", *tris))
        f.write(struct.pack(f"<{len(owner)}H", *owner))
    car = geom["car"]
    hull = dict(name="car", node_matrix=car["node_matrix"], positions=car["positions"])
    print(f"raymesh.bin: {len(verts) // 3} verts, {len(tris) // 3} tris")

    materials = {f: w3d_materials(os.path.join(ASSETS, "3d", f"{f}.w3d")) for f in ("3d", "holypolycrap", "star", "prize", "arrow2")}
    json.dump(materials, open(os.path.join(OUT, "materials.json"), "w"), indent=1)

    markers = dict(raymesh_models=names, car_hull=hull, level=w3d_nodes(os.path.join(ASSETS, "3d", "3d.w3d")),
                   car=w3d_nodes(os.path.join(ASSETS, "3d", "holypolycrap.w3d")))
    json.dump(markers, open(os.path.join(OUT, "markers.json"), "w"), indent=1)
    print(f"collision.json: {len(collision['bodies'])} bodies; markers.json: {len(markers['level'])} level nodes")


if __name__ == "__main__":
    if not os.path.exists(os.path.join(ASSETS, "geom.json")):
        subprocess.run([MACROMELT, os.path.join(ASSETS, "3d", "3d.w3d"), "-q", "--dump-geom",
                        os.path.join(ASSETS, "geom.json")], check=True)
    main()