Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

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

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

tools/build/asset-pack.ts
// Vite plugin and file format for the game's asset pack (.ssrcpack).
//
// A pack holds every file of one asset folder. Files that gzip well (models,
// PCM audio, JSON, raw data) are stored gzip-compressed and inflated in memory
// at runtime by src/game/assets.ts; already-compressed formats (JPEG, PNG,
// WebP, MP3) stay as-is so they can be used directly as data URLs.
//
// Layout (little-endian):
//   0  8 bytes  magic "\x89SSRCPK\n" (binary first byte: hosts sniff for text)
//   8  u32      format version (1)
//  12  u32      index length N
//  16  N bytes  UTF-8 JSON PackIndex
//  16+N         file bytes, at the offsets the index gives (relative to here)
//
// `virtual:ssrc-assets` then either embeds the pack (dev, watch and original
// builds) or names it by SHA-256 so the napplet downloads it from Blossom
// through NAP-RESOURCE (release builds). External packs are written to
// dist-pack/<sha256>.ssrcpack (plus current.json naming it). soyli uploads the
// pack at publish once it is a registered asset (pnpm assets:register), so a
// release build fails while napplet.assets.json names a different pack.
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { extname, join, relative, sep } from 'node:path';
import { gzipSync } from 'node:zlib';
import type { Plugin } from 'vite';

const ID = 'virtual:ssrc-assets';
const RESOLVED = '\0' + ID;
export const PACK_MAGIC = Buffer.from([0x89, 0x53, 0x53, 0x52, 0x43, 0x50, 0x4b, 0x0a]);
const PACK_VERSION = 1;

const MIME: Record<string, string> = {
  '.glb': 'model/gltf-binary',
  '.bin': 'application/octet-stream',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.webp': 'image/webp',
  '.wav': 'audio/wav',
  '.mp3': 'audio/mpeg',
  '.ogg': 'audio/ogg',
};
/** Formats whose bytes are already entropy-coded; gzip would not pay for itself. */
const PRECOMPRESSED = new Set(['.png', '.jpg', '.webp', '.mp3', '.ogg']);
/** Only keep the gzip form when it saves at least this fraction. */
const MIN_SAVING = 0.1;

/** The managed soyli asset the release pack is registered as (napplet.assets.json). */
export const RELEASE_ASSET_ID = 'game-pack';
/** The custom pack every non-original build ships. */
export const CUSTOM_PACK = { dir: 'src/game/assets-custom', meta: { title: 'Bedroom Grand Prix', original: false } } as const;

export interface PackMeta {
  /** Shown to the player when a pack is side-loaded. */
  title: string;
  /** Built from the 2004 game's files: local use only, flagged in the UI. */
  original: boolean;
}

export interface PackIndex {
  meta: PackMeta;
  files: Array<{ name: string; mime: string; gz: 0 | 1; offset: number; length: number }>;
}

function walk(dir: string): string[] {
  return readdirSync(dir).flatMap((name) => {
    const path = join(dir, name);
    return statSync(path).isDirectory() ? walk(path) : [path];
  });
}

/** Every bundlable file of `dir` (sorted, so the pack and its hash are deterministic). */
function packFiles(dir: string): string[] {
  return walk(dir)
    .sort()
    .filter((path) => MIME[extname(path).toLowerCase()]);
}

export function buildPack(dir: string, meta: PackMeta): Buffer {
  const index: PackIndex = { meta, files: [] };
  const blobs: Buffer[] = [];
  let offset = 0;
  for (const path of packFiles(dir)) {
    const ext = extname(path).toLowerCase();
    const raw = readFileSync(path);
    const packed = PRECOMPRESSED.has(ext) ? null : gzipSync(raw, { level: 9 });
    const gz = packed !== null && packed.length <= raw.length * (1 - MIN_SAVING);
    const bytes = gz ? packed : raw;
    index.files.push({ name: relative(dir, path).split(sep).join('/'), mime: MIME[ext], gz: gz ? 1 : 0, offset, length: bytes.length });
    blobs.push(bytes);
    offset += bytes.length;
  }
  const json = Buffer.from(JSON.stringify(index));
  const header = Buffer.alloc(16);
  PACK_MAGIC.copy(header, 0);
  header.writeUInt32LE(PACK_VERSION, 8);
  header.writeUInt32LE(json.length, 12);
  return Buffer.concat([header, json, ...blobs]);
}

export interface AssetPackOptions {
  dir: string;
  meta: PackMeta;
  /** Embed the pack in the bundle, or reference it by hash for a Blossom download. */
  embed: boolean;
  /** Where external packs are written (one file per hash). */
  outDir: string;
  /**
   * napplet.assets.json: an external pack must be registered there under
   * RELEASE_ASSET_ID with the same hash, or soyli would publish a build whose
   * pack it never uploads. Null skips the check (watch builds testing the
   * download path).
   */
  inventory: string | null;
}

/** The hash napplet.assets.json registers for the release pack, if any. */
export function registeredPackHash(inventory: string): string | null {
  if (!existsSync(inventory)) return null;
  const assets = (JSON.parse(readFileSync(inventory, 'utf8')) as { assets?: Array<{ id: string; hash: string; storage: string }> }).assets ?? [];
  const entry = assets.find((a) => a.id === RELEASE_ASSET_ID && a.storage === 'external');
  return entry?.hash ?? null;
}

export function assetPack(o: AssetPackOptions): Plugin {
  return {
    name: 'ssrc-asset-pack',
    resolveId: (id) => (id === ID ? RESOLVED : null),
    load(id) {
      if (id !== RESOLVED) return null;
      this.addWatchFile(o.dir);
      for (const path of packFiles(o.dir)) this.addWatchFile(path);
      const pack = buildPack(o.dir, o.meta);
      if (o.embed) return `export default ${JSON.stringify({ kind: 'embedded', pack: pack.toString('base64') })};\n`;
      const sha256 = createHash('sha256').update(pack).digest('hex');
      mkdirSync(o.outDir, { recursive: true });
      writeFileSync(join(o.outDir, `${sha256}.ssrcpack`), pack);
      // The pack the latest release build points at, for the upload script.
      writeFileSync(join(o.outDir, 'current.json'), JSON.stringify({ sha256, bytes: pack.length, file: `${sha256}.ssrcpack` }) + '\n');
      if (o.inventory && registeredPackHash(o.inventory) !== sha256) {
        this.error(
          `asset pack ${sha256} is not the registered soyli asset "${RELEASE_ASSET_ID}"; ` +
            'run pnpm assets:register so soyli publish uploads it',
        );
      }
      this.info(`asset pack ${sha256} (${pack.length} bytes) -> ${relative(process.cwd(), o.outDir)}/, registered as "${RELEASE_ASSET_ID}"`);
      return `export default ${JSON.stringify({ kind: 'external', sha256, bytes: pack.length, title: o.meta.title })};\n`;
    },
  };
}