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.
// The game's asset pack (.ssrcpack, see tools/build/asset-pack.ts). Dev and
// original-asset builds embed it; release builds name it by SHA-256 and
// download it from Blossom through NAP-RESOURCE. A player can also side-load a
// pack file through the host's file picker, or paste a Blossom link or hash.
// Compressible files arrive gzip-compressed and are inflated here in memory,
// without network access.
import { FS_DOMAIN, fs, RESOURCE_DOMAIN, resource } from '@napplet/sdk';
import source from 'virtual:ssrc-assets';
import { runtimeHasDomain } from '../domain-availability.js';
import { type CollisionFile, type LevelData, type Markers, parseRayMesh } from './level/data.js';
import { deriveLevel, isEditorLevel, levelSettings } from './level/editorLevel.js';
import { parseGlb } from './render/glb.js';
const MAGIC = [0x89, 0x53, 0x53, 0x52, 0x43, 0x50, 0x4b, 0x0a];
const VERSION = 1;
/** NAP-RESOURCE and fs session limits are 10 MiB per file. */
const MAX_PACK_BYTES = 10 * 1024 * 1024;
const FS_CHUNK = 256 * 1024;
export interface PackMeta {
title: string;
/** Built from the 2004 game's files: local use only (flagged in the UI). */
original: boolean;
}
interface Entry {
mime: string;
gz: boolean;
bytes: Uint8Array<ArrayBuffer>;
}
let entries = new Map<string, Entry>();
let packMeta: PackMeta | null = null;
let packBuffer: ArrayBuffer | null = null;
/** A level .glb played on top of the installed pack (installLevelOverlay). */
let overlay: { buffer: ArrayBuffer; meta: PackMeta } | null = null;
const dataUrls = new Map<string, string>();
export class PackError extends Error {}
/** Whether this build carries its pack, or has to download / side-load one. */
export const PACK_EMBEDDED = source.kind === 'embedded';
/** The installed pack's own metadata, ignoring any level overlay. */
export function installedPack(): PackMeta | null {
return packMeta;
}
/** The installed pack's metadata, or the level overlay's (null until a pack is installed). */
export function currentPack(): PackMeta | null {
return overlay?.meta ?? packMeta;
}
/** The installed pack's bytes, to reinstall it if a switch to another pack fails. */
export function currentPackBuffer(): ArrayBuffer | null {
return packBuffer;
}
/** Whether bytes are a glTF binary (a level .glb) rather than a pack. */
export function isGlb(buffer: ArrayBuffer): boolean {
return buffer.byteLength >= 4 && new DataView(buffer).getUint32(0, true) === 0x46546c67;
}
/** The level .glb played on top of the pack, if any (to restore it if a switch fails). */
export function currentLevelOverlay(): ArrayBuffer | null {
return overlay?.buffer ?? null;
}
/**
* Play a level .glb (one exported from Blender, docs/level-editing.md) with
* the installed pack's car, props, sounds and screens. The pack's own level
* files are hidden while it is installed; null removes it. Throws if the
* bytes are not a readable GLB.
*/
export function installLevelOverlay(buffer: ArrayBuffer | null): PackMeta | null {
if (!buffer) {
overlay = null;
dataUrls.clear();
return null;
}
if (!packMeta) throw new PackError('A level file plays with the game data; load an asset pack first.');
let settings;
try {
settings = levelSettings(parseGlb(buffer).doc);
} catch (error) {
throw new PackError(`That level file cannot be read (${errorDetail(error)}).`);
}
overlay = { buffer, meta: { title: settings?.title ?? 'Custom level', original: settings?.original === true } };
dataUrls.clear();
return overlay.meta;
}
/** Pack files a level overlay replaces (its level.glb) or hides (the legacy level's). */
const OVERLAID = /^(level\.glb|collision\.json|raymesh\.bin|lightmaps\/.*)$/i;
/** Parse and install a pack, replacing any previous one (and any level overlay). */
export function installPack(buffer: ArrayBuffer): PackMeta {
if (isGlb(buffer)) throw new PackError('That is a level file (.glb); load it from the title screen’s Asset pack… button.');
const bytes = new Uint8Array(buffer);
if (bytes.length < 16 || MAGIC.some((b, i) => bytes[i] !== b)) throw new PackError('That file is not a SuperSonic RC asset pack.');
const view = new DataView(buffer);
if (view.getUint32(8, true) !== VERSION) throw new PackError('This asset pack was made for a different version of the game.');
const indexLength = view.getUint32(12, true);
let index: { meta: PackMeta; files: Array<{ name: string; mime: string; gz: 0 | 1; offset: number; length: number }> };
try {
index = JSON.parse(new TextDecoder().decode(bytes.subarray(16, 16 + indexLength)));
} catch {
throw new PackError('The asset pack is damaged (unreadable index).');
}
const base = 16 + indexLength;
const next = new Map<string, Entry>();
for (const f of index.files) {
if (base + f.offset + f.length > bytes.length) throw new PackError('The asset pack is damaged (truncated).');
next.set(f.name, { mime: f.mime, gz: f.gz === 1, bytes: bytes.subarray(base + f.offset, base + f.offset + f.length) });
}
entries = next;
overlay = null;
dataUrls.clear();
packBuffer = buffer;
packMeta = { title: String(index.meta?.title ?? 'Asset pack'), original: index.meta?.original === true };
return packMeta;
}
function decodeBase64(base64: string): Uint8Array<ArrayBuffer> {
const bin = atob(base64);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function encodeBase64(bytes: Uint8Array): string {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
return btoa(bin);
}
async function sha256Hex(buffer: ArrayBuffer): Promise<string | null> {
if (typeof crypto === 'undefined' || !crypto.subtle) return null;
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', buffer));
return [...digest].map((b) => b.toString(16).padStart(2, '0')).join('');
}
/** Whether a release build can download its pack here. */
export function canDownloadPack(): boolean {
return source.kind === 'external' && runtimeHasDomain(RESOURCE_DOMAIN);
}
/** Whether the host offers a file picker for side-loading a pack. */
export function canSideLoadPack(): boolean {
return runtimeHasDomain(FS_DOMAIN);
}
/** Whether the host can download a pack the player links by hash. */
export function canLoadPackLink(): boolean {
return runtimeHasDomain(RESOURCE_DOMAIN);
}
/**
* A pasted pack link: its SHA-256, the Blossom servers it names (hints for the
* host), and the direct blob URLs to try if the host cannot find it.
*/
export interface PackLink {
sha256: string;
servers: string[];
urls: string[];
}
const HASH = /(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])/gi;
/** A server hint as an https origin ("cdn.example.com" or "https://cdn.example.com/"), else null. */
function serverOrigin(hint: string): string | null {
try {
const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(hint) ? hint : `https://${hint}`);
return url.protocol === 'https:' ? url.origin : null;
} catch {
return null;
}
}
/**
* Read a pasted pack link: a bare SHA-256, "blossom:sha256:<hash>", a BUD-10
* "blossom:<hash>.ssrcpack?xs=<server>&xs=…" URI, or a Blossom blob URL such
* as "https://server/<hash>.ssrcpack". The hash is the last 64-hex run of the
* path (where Blossom puts it). A blob URL's own server and every "xs" hint
* become Blossom servers; the blob URL, and the hash's URL on each "xs"
* server, are the direct fallbacks. Null when there is no hash.
*/
export function parsePackLink(text: string): PackLink | null {
const trimmed = text.trim();
let url: URL | null = null;
if (/^https?:\/\//i.test(trimmed)) {
try {
url = new URL(trimmed);
} catch {
return null;
}
}
const [path = '', query = ''] = url ? [url.pathname, url.search.slice(1)] : trimmed.split('?', 2);
const hashes = path.match(HASH);
if (!hashes) return null;
const sha256 = hashes[hashes.length - 1]!.toLowerCase();
const servers = new Set<string>();
const urls = new Set<string>();
if (url?.protocol === 'https:') {
servers.add(url.origin);
urls.add(url.href);
}
if (url || /^blossom:/i.test(path)) {
// Keep the file extension from a BUD-10 URI ("<hash>.ssrcpack") on the fallback URLs.
const ext = url ? '' : (/[0-9a-f]{64}(\.[a-z0-9]+)$/i.exec(path)?.[1] ?? '');
for (const hint of new URLSearchParams(query).getAll('xs')) {
const origin = serverOrigin(hint);
if (!origin) continue;
servers.add(origin);
urls.add(`${origin}/${sha256}${ext}`);
}
}
return { sha256, servers: [...servers], urls: [...urls] };
}
/**
* Download a pack by hash through NAP-RESOURCE: first as a Blossom blob, with
* any server hints, so the host picks the servers and verifies the hash; then
* from each direct URL in turn. Checks the hash either way. Not installed.
*/
async function downloadPack(sha256: string, what: string, servers: string[] = [], urls: string[] = []): Promise<ArrayBuffer> {
if (!runtimeHasDomain(RESOURCE_DOMAIN)) throw new PackError(`This host cannot download ${what}.`);
const attempts = [
() => resource.bytes(`blossom:sha256:${sha256}`, servers.length ? { servers } : undefined),
...urls.map((url) => () => resource.bytes(url)),
];
let failure: unknown;
for (const attempt of attempts) {
try {
const blob = await attempt();
if (blob.size > MAX_PACK_BYTES) throw new PackError('That file is too large to be an asset pack.');
const buffer = await blob.arrayBuffer();
// The host verifies Blossom hashes but not plain https bytes; check where WebCrypto is available.
const hash = await sha256Hex(buffer);
if (hash !== null && hash !== sha256) throw new PackError(`The download of ${what} does not match its hash.`);
return buffer;
} catch (error) {
failure = error;
}
}
if (failure instanceof PackError) throw failure;
throw new PackError(`Could not download ${what} (${errorDetail(failure)}).`, { cause: failure });
}
/** A NAP-RESOURCE rejection's code: its `code`, or the "code: …" prefix some runtimes put in the message. */
function resourceErrorCode(error: unknown): string | undefined {
const code = (error as { code?: unknown } | null)?.code;
if (typeof code === 'string') return code;
return error instanceof Error ? /^([a-z]+(?:-[a-z]+)*):/.exec(error.message)?.[1] : undefined;
}
function errorDetail(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/** Install the build's own pack: decode the embedded one, or download it from Blossom. */
export async function loadBuildPack(): Promise<PackMeta> {
if (source.kind === 'embedded') return installPack(decodeBase64(source.pack).buffer);
return installPack(await downloadPack(source.sha256, 'the game data'));
}
/** An extra level registered as a soyli asset ("level-<name>", tools/assets/register.ts). */
export type RegisteredLevel = (typeof source.levels)[number];
/** The extra levels this build offers (empty without the host's resource domain, which downloads them). */
export function registeredLevels(): RegisteredLevel[] {
return canLoadPackLink() ? source.levels : [];
}
const levelDownloads = new Map<string, Promise<ArrayBuffer>>();
/** Download a registered level's GLB by its hash (not installed); kept for the session. */
export function fetchRegisteredLevel(level: RegisteredLevel): Promise<ArrayBuffer> {
let pending = levelDownloads.get(level.sha256);
if (!pending) {
pending = downloadPack(level.sha256, `the level “${level.title}”`);
pending.catch(() => levelDownloads.delete(level.sha256));
levelDownloads.set(level.sha256, pending);
}
// A copy: installing the level keeps (and may transfer) the buffer it is given.
return pending.then((buffer) => buffer.slice(0));
}
/** Download the pack a pasted link or hash names (not installed). */
export async function fetchPackLink(text: string): Promise<ArrayBuffer> {
const link = parsePackLink(text);
if (!link) throw new PackError('Paste a Blossom link or the 64-character SHA-256 hash of an .ssrcpack or level .glb file.');
try {
return await downloadPack(link.sha256, 'that asset pack', link.servers, link.urls);
} catch (error) {
// A bare hash the host's own Blossom servers do not have (they report not-found or blocked-by-policy).
const code = error instanceof PackError ? resourceErrorCode(error.cause) : undefined;
if (!link.servers.length && (code === 'not-found' || code === 'blocked-by-policy'))
throw new PackError("This host's Blossom servers do not have that asset pack. Paste the full link to it instead.");
throw error;
}
}
/**
* The original-assets pack: a BUD-10 URI naming its SHA-256 (the byte-
* deterministic output of tools/build-original-assets.sh) and the Blossom
* servers that carry it. The napplet only ever names those bytes by hash — it
* never bundles or uploads them — so the download resolves only where a listed
* server has the blob. See the secret code in src/main.ts.
*/
export const ORIGINAL_PACK_LINK =
'blossom:9cbb6eed57dc0a276d4e0925f20704c5dd6a4a69aad94e1a7ce6c678cde12518.ssrcpack?xs=blossom.ditto.pub&xs=blossom.napplet.soy';
/** Download the original-assets pack by its built-in link (not installed). */
export async function fetchOriginalPack(): Promise<ArrayBuffer> {
const link = parsePackLink(ORIGINAL_PACK_LINK);
if (!link) throw new PackError('The built-in original asset pack link is malformed.');
try {
return await downloadPack(link.sha256, 'the original assets', link.servers, link.urls);
} catch (error) {
if (error instanceof PackError) throw error;
const servers = link.servers.map((s) => s.replace(/^https?:\/\//, '')).join(' or ');
throw new PackError(`The original asset pack is not available${servers ? ` (tried ${servers})` : ''}.`, { cause: error });
}
}
/** Ask the player for a .ssrcpack or level .glb file and read it (not installed). Resolves null if they cancel. */
export async function pickPackFile(): Promise<ArrayBuffer | null> {
let picked;
try {
picked = await fs.pickFile({
accept: [{ extension: '.ssrcpack' }, { extension: '.glb' }],
description: 'Choose a SuperSonic RC asset pack (.ssrcpack) or level (.glb)',
});
} catch (error) {
if (error instanceof Error && /cancel/i.test(error.message)) return null;
throw new PackError(`Could not open the file (${errorDetail(error)}).`);
}
const entry = picked.entries.find((e) => e.kind === 'file');
if (!entry) return null;
const chunks: Uint8Array[] = [];
let offset = 0;
for (;;) {
const part = await fs.read(entry.path, { offset, length: FS_CHUNK });
const bytes = decodeBase64(part.data);
chunks.push(bytes);
offset += part.bytesRead;
if (offset > MAX_PACK_BYTES) throw new PackError('That file is too large to be an asset pack.');
if (part.eof || part.bytesRead === 0) break;
}
const all = new Uint8Array(offset);
let at = 0;
for (const c of chunks) {
all.set(c, at);
at += c.length;
}
return all.buffer;
}
const stem = (name: string) => name.replace(/\.[^./]+$/, '').toLowerCase();
/**
* The entry for "dir/Name.ext". Names are matched without their extension, so
* an asset re-encoded to another format (PNG→WebP, WAV→MP3) still resolves.
*/
function find(name: string): Entry | undefined {
if (overlay && OVERLAID.test(name)) {
return /^level\.glb$/i.test(name) ? { mime: 'model/gltf-binary', gz: false, bytes: new Uint8Array(overlay.buffer) } : undefined;
}
const exact = entries.get(name);
if (exact) return exact;
const want = stem(name);
for (const [key, entry] of entries) if (stem(key) === want) return entry;
return undefined;
}
async function inflate(bytes: Uint8Array<ArrayBuffer>): Promise<ArrayBuffer> {
if (typeof DecompressionStream === 'undefined') throw new Error('This browser is too old to unpack the game data (no DecompressionStream).');
return new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer();
}
/** Data URL for an image (e.g. "lightmaps/001LightingMap.jpg"), if the pack has it. */
export function assetUrl(name: string): string | undefined {
const entry = find(name);
if (!entry) return undefined;
if (entry.gz) throw new Error(`${name} is stored compressed; use assetBytes`);
let url = dataUrls.get(name);
if (!url) dataUrls.set(name, (url = `data:${entry.mime};base64,${encodeBase64(entry.bytes)}`));
return url;
}
export async function assetBytes(name: string): Promise<ArrayBuffer | undefined> {
const entry = find(name);
if (!entry) return undefined;
return entry.gz ? inflate(entry.bytes) : entry.bytes.slice().buffer;
}
export async function assetJson<T>(name: string): Promise<T | undefined> {
const bytes = await assetBytes(name);
return bytes ? (JSON.parse(new TextDecoder().decode(bytes)) as T) : undefined;
}
/**
* Whether the pack's level.glb is a Blender-authored level (everything derived
* from the GLB) rather than a legacy one with collision.json and raymesh.bin.
*/
export function levelIsEditor(levelGlb: ArrayBuffer): boolean {
return isEditorLevel(levelGlb) || !find('collision.json') || !find('raymesh.bin');
}
export async function loadLevelData(): Promise<LevelData | undefined> {
const glb = await assetBytes('level.glb');
if (glb && levelIsEditor(glb)) {
// A legacy markers.json still supplies the car hull when the level has none.
const legacy = await assetJson<Markers>('markers.json');
const { level, warnings } = deriveLevel(glb, { carHull: legacy?.car_hull });
for (const w of warnings) console.warn(`level.glb: ${w}`);
return level;
}
const [markers, collision, rays] = await Promise.all([
assetJson<Markers>('markers.json'),
assetJson<CollisionFile>('collision.json'),
assetBytes('raymesh.bin'),
]);
if (!markers || !collision || !rays) return undefined;
return { markers, collision, rayMesh: parseRayMesh(rays) };
}
