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.

vite.config.ts
import { existsSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
import { fileURLToPath } from 'node:url';
import { defineConfig, loadEnv } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';
import type { Plugin } from 'vite';
import { assetPack, CUSTOM_PACK } from './tools/build/asset-pack';

/**
 * rapier3d-compat inlines its WASM as plain base64 (4.1 MB of the bundle).
 * Re-embed it gzip-compressed and inflate it in memory with
 * DecompressionStream at init, as soyLI's own WASM recipe does. Bindings the
 * game never calls are stripped first (tools/build/rapier-slim.ts): ≈1.1 MB.
 *
 * wasm-bindgen's loader also keeps a dormant `fetch(url)` branch for URL
 * inputs. Napplets have no network authority, so replace that branch with an
 * explicit error.
 */
function rapierWasm(full: boolean): Plugin {
  return {
    name: 'ssrc-rapier-wasm',
    async transform(code, id) {
      if (!id.includes('rapier3d-compat')) return null;
      this.addWatchFile(fileURLToPath(new URL('./tools/build/rapier-exports.json', import.meta.url)));
      const embedded = /[\w$]+\.toByteArray\("(AGFzbQ[A-Za-z0-9+/=]+)"\)/.exec(code);
      let wasm: Uint8Array | null = embedded ? Buffer.from(embedded[1], 'base64') : null;
      if (wasm && !full) {
        // binaryen is large; load it only when there is a module to strip.
        const { rapierKeepList, slimRapierWasm } = await import('./tools/build/rapier-slim');
        wasm = slimRapierWasm(wasm, rapierKeepList(fileURLToPath(new URL('.', import.meta.url))));
      }
      const out = code
        .replace(/\(A=fetch\(A\)\)/g, '(A=(()=>{throw new Error("network WASM loading is not available")})())')
        .replace(/[\w$]+\.toByteArray\("(AGFzbQ[A-Za-z0-9+/=]+)"\)/, () => {
          const packed = gzipSync(wasm!, { level: 9 }).toString('base64');
          return (
            // The caller reads `.buffer` off the decoded bytes, so yield a Uint8Array.
            'new Uint8Array(yield (typeof DecompressionStream>"u"' +
            '?Promise.reject(new Error("This browser is too old to run the physics engine (no DecompressionStream)."))' +
            `:new Response(new Blob([Uint8Array.from(atob("${packed}"),c=>c.charCodeAt(0))]).stream()` +
            '.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer()))'
          );
        });
      return out === code ? null : { code: out, map: null };
    },
  };
}

export default defineConfig(({ command, mode }) => {
  // Original SuperSonic RC assets (ref-out/game, built by tools/extract) are
  // copyrighted reference material. They are bundled only when explicitly
  // requested for local development; any other build gets the custom bundle
  // (src/game/assets-custom, generated by tools/assets).
  // Request them with SSRC_ORIGINAL_ASSETS=1 in the shell, or in the gitignored
  // .env.local for `soyli dev`: soyli runs Vite with a scrubbed environment, so
  // only the file reaches it. The file is honoured only by the dev server and watch
  // builds (how soyli dev builds), never by the one-shot builds behind soyli
  // build/publish.
  const fileEnv = loadEnv(mode, process.cwd(), 'SSRC_');
  const env = { ...fileEnv, ...process.env };
  const watching = command === 'serve' || process.argv.includes('--watch') || process.argv.includes('-w');
  const originals = fileURLToPath(new URL('./ref-out/game', import.meta.url));
  const requested = process.env.SSRC_ORIGINAL_ASSETS === '1' || (watching && fileEnv.SSRC_ORIGINAL_ASSETS === '1');
  const useOriginals = requested && existsSync(originals);
  if (useOriginals) {
    console.warn('\n⚠  SSRC_ORIGINAL_ASSETS=1: bundling ORIGINAL game assets. Do not publish this build.\n');
  } else if (requested) {
    console.warn('\n⚠  SSRC_ORIGINAL_ASSETS=1 but ref-out/game is missing (see tools/README.md); using the custom bundle.\n');
  } else if (fileEnv.SSRC_ORIGINAL_ASSETS === '1') {
    console.warn('\n.env.local asks for the original assets; ignored outside watch builds (soyli dev). This build gets the custom bundle.\n');
  }

  // Release builds leave the custom pack out of index.html: it is a registered
  // soyli asset (pnpm assets:register) that soyli publish uploads to Blossom,
  // and the napplet downloads it at runtime by hash. Dev and
  // watch builds (soyli dev serves only registered soyli assets) and builds
  // with the originals embed it; SSRC_EMBED_ASSETS=1 forces a single file.
  // SSRC_EXTERNAL_ASSETS=1 (shell or .env.local) tries the download/side-load
  // path in a watch build.
  const external = process.env.SSRC_EXTERNAL_ASSETS === '1' || (watching && fileEnv.SSRC_EXTERNAL_ASSETS === '1');
  const embed = useOriginals || (watching && !external) || env.SSRC_EMBED_ASSETS === '1';

  return {
    build: {
      // Vite's module-preload polyfill calls `fetch`; one inlined entry needs no
      // preload graph, and NIP-5D napplet code has no ambient network authority.
      modulePreload: { polyfill: false },
    },
    plugins: [
      rapierWasm(env.SSRC_RAPIER_FULL === '1'),
      assetPack({
        dir: useOriginals ? originals : fileURLToPath(new URL(`./${CUSTOM_PACK.dir}`, import.meta.url)),
        meta: useOriginals ? { title: 'SuperSonic RC (original assets)', original: true } : CUSTOM_PACK.meta,
        embed,
        outDir: fileURLToPath(new URL('./dist-pack', import.meta.url)),
        inventory: watching ? null : fileURLToPath(new URL('./napplet.assets.json', import.meta.url)),
        levels: fileURLToPath(new URL('./napplet.assets.json', import.meta.url)),
      }),
      // Produce one self-contained `/index.html` for NIP-5D `srcdoc` loading,
      // then content-address it with the NIP-5A tag/hash schema.
      nip5aManifest({
        nappletType: 'my-napplet',
        artifactMode: 'single-file',
        // NAP-INTENT: other napplets open this one by role to play a linked
        // asset pack. Keep in step with GAME_ARCHETYPE and PLAY_CONVENTION in
        // src/napplet-intent.ts.
        archetypes: [{ slug: 'supersonicrc', naps: ['napplet:supersonicrc/play'] }],
      }),
    ],
  };
});