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.
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
ARCHIVE_URL="https://archive.org/download/lego-super-sonic-rc/SuperSonicRC.zip"
ARCHIVE_SHA256="108cd542dad9bc6452d300bd5e83a1dd387aa3944bd594d09938c4d03e5c0850"
PROJECTOR_RAYS_URL="https://github.com/ProjectorRays/ProjectorRays.git"
PROJECTOR_RAYS_REV="6f9bcebf626b43719abe2affcbbcb041d154d666"
MACROMELT_URL="https://github.com/Dilaz/macromelt.git"
MACROMELT_REV="78f1ad6c5e8ce8b4f9a36dd1a25ca8682f3976e6"
REF="$ROOT/ref"
ARCHIVE="$REF/SuperSonicRC.zip"
GAME="$REF/SuperSonicRC"
OUT="$ROOT/ref-out"
VENDOR="$ROOT/tools/vendor"
PROJECTOR_RAYS="$VENDOR/ProjectorRays"
MACROMELT="$VENDOR/macromelt"
MACROMELT_PATCH="$ROOT/tools/patches/macromelt-node-user-props.patch"
PACK="$OUT/SuperSonicRC-original.ssrcpack"
EDITOR="$OUT/editor"
BLENDER=${SSRC_BLENDER:-blender}
EXTRACT_TMP=""
cleanup() {
if [[ -n $EXTRACT_TMP && -d $EXTRACT_TMP ]]; then
chmod -R u+rwX "$EXTRACT_TMP" 2>/dev/null || true
rm -rf "$EXTRACT_TMP"
fi
}
trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: tools/build-original-assets.sh [--pack-only] [--reuse]
Download the original SuperSonic RC archive, build the pinned extraction tools,
regenerate ref-out/game, pack it into ref-out/SuperSonicRC-original.ssrcpack,
convert the level for Blender (ref-out/editor/original.blend and level.glb, when
Blender 5.2 or newer is installed; SSRC_BLENDER names another binary), and
create dist/index.html with the original assets.
--pack-only stop after the asset pack (no pnpm install, no napplet build)
--reuse skip the download and extraction; pack the existing ref-out/game
The pack can be side-loaded ("Load asset pack...") into any build whose own
pack does not load, e.g. a release build on an offline machine.
The generated files contain copyrighted game assets. They are for local
preservation/development use only. Do not publish or share the resulting
napplet build or asset pack.
This script targets Ubuntu/Debian. It checks prerequisites but never installs
system packages or invokes sudo.
EOF
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
step() {
printf '\n==> %s\n' "$*"
}
PACK_ONLY=0
REUSE=0
for arg in "$@"; do
case $arg in
-h | --help)
usage
exit 0
;;
--pack-only) PACK_ONLY=1 ;;
--reuse) REUSE=1 ;;
*) die "unknown argument: $arg (try --help)" ;;
esac
done
check_node() {
command -v node >/dev/null 2>&1 || die "node is missing (run 'soyli setup' for the pinned Node toolchain)"
# tools/assets/pack.ts runs through Node's built-in TypeScript type stripping.
node -e 'process.exit(process.features.typescript ? 0 : 1)' >/dev/null 2>&1 || die \
"Node $(node --version) cannot run TypeScript files; use Node 22.18 or newer ('soyli setup')"
}
check_prerequisites() {
local missing=()
local command
local commands=(curl git make c++ python3 unzip xxd ffmpeg cargo rustc sha256sum sort)
((PACK_ONLY)) || commands+=(pnpm)
for command in "${commands[@]}"; do
command -v "$command" >/dev/null 2>&1 || missing+=("$command")
done
if ((${#missing[@]})); then
printf 'Missing commands: %s\n\n' "${missing[*]}" >&2
cat >&2 <<'EOF'
On Ubuntu/Debian, install the native prerequisites with:
sudo apt-get update
sudo apt-get install build-essential curl git unzip xxd libboost-dev zlib1g-dev libmpg123-dev ffmpeg python3 python3-pil
Install Rust 1.85 or newer through https://rustup.rs/ if your distribution's
rustc is too old. Run `soyli setup` for this project's pinned Node/pnpm tools.
EOF
exit 1
fi
python3 -c 'from PIL import Image' >/dev/null 2>&1 || die \
"Python Pillow is missing (Ubuntu/Debian: sudo apt-get install python3-pil)"
local rust_version
rust_version=$(rustc --version | awk '{print $2}')
if [[ $(printf '%s\n' "1.85.0" "$rust_version" | sort -V | head -n 1) != "1.85.0" ]]; then
die "Rust 1.85 or newer is required; found $rust_version (install through https://rustup.rs/)"
fi
local probe
probe=$(mktemp "${TMPDIR:-/tmp}/ssrc-native-check.XXXXXX")
if ! printf '%s\n' \
'#include <boost/format.hpp>' \
'#include <mpg123.h>' \
'#include <zlib.h>' \
'int main() { return mpg123_init() == MPG123_OK && zlibVersion()[0] ? 0 : 1; }' \
| c++ -std=c++17 ${CPPFLAGS:-} -x c++ - ${LDFLAGS:-} -lmpg123 -lz -o "$probe" >/dev/null 2>&1; then
rm -f "$probe"
# ProjectorRays' Makefile appends to CPPFLAGS/LDFLAGS, so headers outside the
# system paths (tools/README.md, "Manual setup") work for the probe and the build.
die "ProjectorRays headers/libraries are missing (install libboost-dev zlib1g-dev libmpg123-dev, or point CPPFLAGS/LDFLAGS at them)"
fi
rm -f "$probe"
}
ensure_repo() {
local path=$1 url=$2 rev=$3
if [[ ! -e $path ]]; then
git clone --filter=blob:none --no-checkout "$url" "$path"
git -C "$path" fetch --depth 1 origin "$rev"
git -C "$path" checkout --detach "$rev"
fi
[[ -d $path/.git ]] || die "$path exists but is not a Git checkout"
[[ $(git -C "$path" remote get-url origin) == "$url" ]] || die "$path has an unexpected origin"
[[ $(git -C "$path" rev-parse HEAD) == "$rev" ]] || die "$path is not at pinned revision $rev"
}
prepare_tools() {
mkdir -p "$VENDOR"
ensure_repo "$PROJECTOR_RAYS" "$PROJECTOR_RAYS_URL" "$PROJECTOR_RAYS_REV"
[[ -z $(git -C "$PROJECTOR_RAYS" status --porcelain) ]] || die \
"$PROJECTOR_RAYS has local changes; preserve or remove them before retrying"
make -C "$PROJECTOR_RAYS"
ensure_repo "$MACROMELT" "$MACROMELT_URL" "$MACROMELT_REV"
local status
status=$(git -C "$MACROMELT" status --porcelain)
if [[ -z $status ]]; then
git -C "$MACROMELT" apply --check "$MACROMELT_PATCH"
git -C "$MACROMELT" apply "$MACROMELT_PATCH"
elif [[ $status == " M src/chunks.rs" ]] && git -C "$MACROMELT" apply --reverse --check "$MACROMELT_PATCH"; then
: # Expected patched checkout from an earlier run.
else
die "$MACROMELT has changes other than the expected node-user-properties patch"
fi
cargo build --release --locked --manifest-path "$MACROMELT/Cargo.toml"
}
download_game() {
mkdir -p "$REF"
if [[ ! -f $ARCHIVE ]]; then
step "Downloading the original game archive"
curl --fail --location --retry 3 --show-error --output "$ARCHIVE.part" "$ARCHIVE_URL"
mv "$ARCHIVE.part" "$ARCHIVE"
fi
printf '%s %s\n' "$ARCHIVE_SHA256" "$ARCHIVE" | sha256sum --check --status || die \
"$ARCHIVE failed checksum verification; remove it and retry"
step "Extracting the verified game archive"
EXTRACT_TMP=$(mktemp -d "$REF/.extract.XXXXXX")
unzip -q "$ARCHIVE" -d "$EXTRACT_TMP"
[[ -d $EXTRACT_TMP/SuperSonicRC ]] || die "archive does not contain a SuperSonicRC directory"
# The ZIP preserves read-only DOS attributes on many Xtras. Make this local
# reference copy owner-writable so cleanup and later rebuilds can replace it.
chmod -R u+rwX "$EXTRACT_TMP/SuperSonicRC"
[[ -f $EXTRACT_TMP/SuperSonicRC/game.dcr ]] || die "archive does not contain SuperSonicRC/game.dcr"
[[ -f $EXTRACT_TMP/SuperSonicRC/GUILibrary.cct ]] || die "archive does not contain SuperSonicRC/GUILibrary.cct"
[[ -f $EXTRACT_TMP/SuperSonicRC/wrapper_SupersonicRC.dir ]] || die \
"archive does not contain SuperSonicRC/wrapper_SupersonicRC.dir"
if [[ -e $GAME ]]; then
chmod -R u+rwX "$GAME"
fi
rm -rf "$GAME"
mv "$EXTRACT_TMP/SuperSonicRC" "$GAME"
rmdir "$EXTRACT_TMP"
EXTRACT_TMP=""
}
extract_assets() {
step "Regenerating ref-out/game"
rm -rf "$OUT"
mkdir -p "$OUT/decompiled"
cp "$GAME/game.dcr" "$OUT/decompiled/"
(
cd "$OUT/decompiled"
"$PROJECTOR_RAYS/projectorrays" decompile game.dcr --dump-scripts
)
python3 "$ROOT/tools/extract/extract_assets.py"
python3 "$ROOT/tools/extract/hke.py"
python3 "$ROOT/tools/extract/lightmap_uvs.py"
mkdir -p "$OUT/assets/glb"
local w3d found=0
while IFS= read -r -d '' w3d; do
found=1
"$MACROMELT/target/release/macromelt" "$w3d" -q \
--export-glb "$OUT/assets/glb/$(basename "${w3d%.w3d}").glb"
done < <(find "$OUT/assets/3d" -maxdepth 1 -type f -name '*.w3d' -print0)
[[ $found -eq 1 ]] || die "asset extraction produced no W3D files"
python3 "$ROOT/tools/extract/build_level.py"
}
validate_assets() {
local required=(
level.glb car.glb prize.glb star.glb arrow2.glb
collision.json markers.json materials.json raymesh.bin
lightmaps/001LightingMap.jpg ui/Title-v8.jpg sounds/CarLoop.wav
)
local file
for file in "${required[@]}"; do
[[ -s $OUT/game/$file ]] || die "expected asset is missing or empty: ref-out/game/$file"
done
}
build_pack() {
step "Packing ref-out/game into an asset pack"
node "$ROOT/tools/assets/pack.ts" "$OUT/game" "$PACK" --original >/dev/null
local bytes
bytes=$(stat --format='%s' "$PACK")
# NAP-RESOURCE and the host file picker both stop at 10 MiB.
((bytes <= 10 * 1024 * 1024)) || die "$PACK is $bytes bytes, above the 10 MiB side-load limit"
cat <<EOF
Built ${PACK#"$ROOT"/} ($bytes bytes, sha256 $(sha256sum "$PACK" | cut -d' ' -f1)).
Copy it to the other machine and choose "Asset pack..." under "Click to Start" on the
title screen (or "Load asset pack..." in the "Game data" panel if the build's own pack
does not load, e.g. a release build offline).
WARNING: This pack contains copyrighted original game assets.
Use it only for local preservation/development. Do not upload, publish or share it.
EOF
}
build_editor_level() {
if ! command -v "$BLENDER" >/dev/null 2>&1; then
printf '\nBlender not found: skipped the Blender version of the level (install Blender 5.2+, then pnpm level:original).\n'
return
fi
local version
version=$("$BLENDER" --version 2>/dev/null | awk '/^Blender/ {print $2; exit}')
if [[ -z $version || $(printf '%s\n' "5.2" "$version" | sort -V | head -n 1) != "5.2" ]]; then
printf '\nBlender %s is older than 5.2: skipped the Blender version of the level.\n' "${version:-?}"
return
fi
step "Converting the level for Blender (ref-out/editor)"
mkdir -p "$EDITOR"
"$BLENDER" -b --factory-startup --python "$ROOT/tools/blender/ssrc_level.py" -- convert \
"$OUT/game" "$EDITOR/original.blend" --glb "$EDITOR/level.glb" --title "SuperSonic RC (original)" >"$EDITOR/convert.log" 2>&1 \
|| die "Blender conversion failed; see ref-out/editor/convert.log"
[[ -s $EDITOR/level.glb ]] || die "Blender conversion produced no ref-out/editor/level.glb"
printf 'Built ref-out/editor/original.blend and level.glb (sha256 %s).\n' "$(sha256sum "$EDITOR/level.glb" | cut -d' ' -f1)"
}
build_napplet() {
step "Installing locked JavaScript dependencies"
pnpm --dir "$ROOT" install --frozen-lockfile
step "Building the local napplet with original assets"
(
cd "$ROOT"
SSRC_ORIGINAL_ASSETS=1 pnpm build
)
local artifact="$ROOT/dist/index.html"
[[ -s $artifact ]] || die "napplet build did not produce dist/index.html"
local bytes
bytes=$(stat --format='%s' "$artifact")
((bytes <= 10 * 1024 * 1024)) || die "dist/index.html is $bytes bytes, above the 10 MiB napplet limit"
local asset_count
asset_count=$(find "$OUT/game" -type f | wc -l)
cat <<EOF
Built $asset_count game asset files in ref-out/game.
Built dist/index.html ($bytes bytes) with the original assets enabled.
WARNING: This build contains copyrighted original game assets.
Use it only for local preservation/development. Do not run soyli publish on it.
EOF
}
cd "$ROOT"
check_node
if ((REUSE)); then
[[ -d $OUT/game ]] || die "ref-out/game does not exist; run without --reuse first"
else
step "Checking Ubuntu/Debian build prerequisites"
check_prerequisites
download_game
step "Building pinned extraction tools"
prepare_tools
extract_assets
fi
validate_assets
build_pack
build_editor_level
if ((!PACK_ONLY)); then
command -v pnpm >/dev/null 2>&1 || die "pnpm is missing (run 'soyli setup')"
build_napplet
fi
