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.

tools/sim/replay.mjs
#!/usr/bin/env node
// Replay an original-game log (from the Wine logger) through the port.
//   node tools/sim/replay.mjs <original-log.txt> <out.txt> [havokOverridesJSON]
// Writes the port's rows in the logger's column layout for tools/sim/compare.py.
import { existsSync, readFileSync, writeFileSync } 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)), '../..');
// SIM_LEVEL: another level folder or Blender level .glb (default: the extracted originals).
const game = resolve(root, process.env.SIM_LEVEL ?? 'ref-out/game');
const [logPath, outPath, overrides = '{}'] = process.argv.slice(2);
const lines = readFileSync(logPath, 'utf8').split(/\r?\n|\r/).map((l) => l.trim().split(/\s+/));
// "EXP n ..." lines (make_logger_wrapper.py SSRC_EXPERIMENTS) precede the row of
// the frame after which the logger set the car's state; SSRC_EXPERIMENTS here
// is the same list as JSON: [[t, [x,y,z], [ax,ay,az], angle, [vx,vy,vz]], ...].
const expList = JSON.parse(process.env.SSRC_EXPERIMENTS ?? '[]');
const rows = [];
const expAtRow = {};
for (const l of lines) {
  if (l[0] === 'EXP') expAtRow[rows.length] = expList[Number(l[1]) - 1].slice(1);
  else if (l.length >= 33) rows.push(l);
}
// Each logged row is one (or, rarely, a few) Director frames. Physics depends only
// on the chunk sequence drivingManager feeds Havok, which the simTime delta
// encodes: delta / 2.5 = remainder (3..15 ms) + n full 20 ms chunks (clamped 15).
// Key columns 27..31 (126 125 123 124 space).
const frames = [];
const chunks = [];
let simPrev = 0;
const experiments = {};
for (const [ri, r] of rows.entries()) {
  let left = (Number(r[1]) - simPrev) / 2.5;
  simPrev = Number(r[1]);
  if (expAtRow[ri]) experiments[chunks.length - (left < 0.0029 ? 1 : 0)] = expAtRow[ri];
  if (left < 0.0029) continue;
  const n = Math.max(0, Math.floor((left - 0.003) / 0.015 + 1e-6));
  const rem = left - n * 0.015;
  frames.push(r.slice(27, 32).map(Number));
  chunks.push([Math.round(rem * 1e6) / 1000, ...Array(n).fill(20)]);
}
// The logger samples keyPressed() at idle time, after that frame's update: a
// key it reports at frame k was first read by the wheels during frame k + 1.
frames.unshift([0, 0, 0, 0, 0]);
frames.pop();
/** The level: a Blender level GLB, or a folder of legacy files (or holding only level.glb). */
function readLevel(path) {
  const bytes = (file) => new Uint8Array(readFileSync(file)).buffer; // copy out of Node's shared pool
  if (path.endsWith('.glb')) return { levelGlb: bytes(path) };
  if (!existsSync(resolve(path, 'collision.json'))) return { levelGlb: bytes(resolve(path, 'level.glb')) };
  return {
    markers: JSON.parse(readFileSync(resolve(path, 'markers.json'), 'utf8')),
    collision: JSON.parse(readFileSync(resolve(path, 'collision.json'), 'utf8')),
    rayMeshBuffer: bytes(resolve(path, 'raymesh.bin')),
  };
}
const server = await createServer({ root, logLevel: 'error', server: { middlewareMode: true }, appType: 'custom', optimizeDeps: { noDiscovery: true, entries: [] } });
try {
  const { runReplay } = await server.ssrLoadModule('/src/game/sim/headless.ts');
  const level = readLevel(game);
  const out = await runReplay(level, frames, JSON.parse(overrides), chunks, experiments);
  writeFileSync(outPath, out.map((r) => r.map((x) => (Number.isInteger(x) ? x : x.toFixed(6))).join(' ')).join('\n') + '\n');
  console.log(`replayed ${frames.length} frames -> ${outPath}`);
} finally {
  await server.close();
}