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/sim/inject.mjs
#!/usr/bin/env node
// Start the port from one logged original state and replay the next frames.
//   node tools/sim/inject.mjs <original-log.txt> <row> <count> [havokOverridesJSON]
// Prints original vs port side by side (z, vz, speed, tilt, springs).
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer } from 'vite';

const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const game = resolve(root, 'ref-out/game');
const [logPath, startArg, countArg = '40', overrides = '{}'] = process.argv.slice(2);
const rows = readFileSync(logPath, 'utf8').split(/\r?\n|\r/).map((l) => l.trim().split(/\s+/).map(Number)).filter((r) => r.length >= 33);
const start = Number(startArg);
const count = Number(countArg);
// Same chunk decoding and one-frame key lag as replay.mjs.
const frames = [];
const chunks = [];
for (let i = start + 1; i <= start + count && i < rows.length; i++) {
  const left = (rows[i][1] - rows[i - 1][1]) / 2.5;
  const n = Math.max(0, Math.floor((left - 0.003) / 0.015 + 1e-6));
  const rem = left - n * 0.015;
  chunks.push(left < 0.0029 ? [] : [Math.round(rem * 1e6) / 1000, ...Array(n).fill(20)]);
  frames.push(rows[i - 1].slice(27, 32));
}
const server = await createServer({ root, logLevel: 'error', server: { middlewareMode: true }, appType: 'custom', optimizeDeps: { noDiscovery: true, entries: [] } });
try {
  const { runInject } = await server.ssrLoadModule('/src/game/sim/headless.ts');
  const level = {
    markers: JSON.parse(readFileSync(resolve(game, 'markers.json'), 'utf8')),
    collision: JSON.parse(readFileSync(resolve(game, 'collision.json'), 'utf8')),
    // Copy out: small files come back as views into Node's shared buffer pool.
    rayMeshBuffer: new Uint8Array(readFileSync(resolve(game, 'raymesh.bin'))).buffer,
  };
  const out = await runInject(level, rows[start], frames, chunks, JSON.parse(overrides));
  const tilt = (r) => {
    const n = Math.hypot(r[5], r[6], r[7]) || 1;
    const az = r[7] / n, a = (r[8] * Math.PI) / 180;
    const c = Math.cos(a);
    return (Math.acos(Math.max(-1, Math.min(1, c + az * az * (1 - c)))) * 180) / Math.PI;
  };
  const f = (x, w = 7, d = 1) => x.toFixed(d).padStart(w);
  const desc = (r) => `${f(r[4])}${f(r[11])}${f(Math.hypot(r[9], r[10]), 6, 0)}${f(tilt(r), 6)}  ${[16, 19, 22, 25].map((c) => (r[c - 1] ? r[c].toFixed(1) : ' air')).join(' ')} ${process.env.LOADS ? [37, 41, 45, 49].map((c) => f(r[c] ?? 0, 6, 0)).join('') : ''}`;
  console.log('   sim |       z     vz   spd  tilt  springs (orig)           |       z     vz   spd  tilt  springs (port)           | dpos');
  let sum = 0;
  out.forEach((p, k) => {
    const o = rows[start + 1 + k];
    const d = Math.hypot(o[2] - p[2], o[3] - p[3], o[4] - p[4]);
    sum += d;
    console.log(`${f(o[1], 6, 2)} | ${desc(o)} | ${desc(p)} | ${f(d, 5)}`);
  });
  console.log(`SUMMARY meanPosErr=${(sum / out.length).toFixed(2)} finalErr=${Math.hypot(...[2, 3, 4].map((c) => rows[start + out.length][c] - out[out.length - 1][c])).toFixed(1)}`);
} finally {
  await server.close();
}