Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
bb8b570f3600…
Author-recorded commit
073e2d2c0e63…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/SuperSonicRCRevive

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

src/game/onlineProtocol.ts
import { Quaternion, Vector3 } from 'three';

export const PHYSICS_VERSION = 'ssrc-rapier-v1';
/** Part of the WebRTC wire scope and room namespace: other physics releases never meet. */
export const ONLINE_PROTOCOL = `ssrc-free-v2.${PHYSICS_VERSION}`;
/** The host keeps at most eight remote peers per session; one spare slot lets a newcomer hear "full". */
export const ROOM_CAPACITY = 8;
export const HASH = /^[a-f0-9]{64}$/;
export interface Descriptor { pack: string; level: string | null; physics: string; packServers?: string[]; levelServers?: string[] }
/** Assets of the room's first player; the earliest founding wins when two groups meet. */
export interface RoomDescriptor extends Descriptor { founded: number; nonce: string }
export interface Pose {
  p: number[];
  q: number[];
  /** Wheel positions in body space, steering and spin in degrees. */
  w: number[][];
}
export interface Snapshot extends Pose { type: 'car'; seq: number; time: number }
export type Message = Snapshot
  | { type: 'hello' }
  /** profile: the sender's own Nostr pubkey, claimed for a name tag only (unverified). */
  | { type: 'room'; descriptor: RoomDescriptor; count: number; profile?: string }
  | { type: 'full' }
  | { type: 'bye' };

const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
const numbers = (v: unknown, size: number, bound: number): v is number[] => Array.isArray(v) && v.length === size && v.every(n => typeof n === 'number' && Number.isFinite(n) && Math.abs(n) <= bound);
// Both hint lists stay well below WEBRTC's 16 KiB message limit.
const servers = (v: unknown): boolean => v === undefined || Array.isArray(v) && v.length <= 8 && v.every(s => {
  if (typeof s !== 'string' || s.length > 512) return false;
  try { const url = new URL(s); return url.protocol === 'https:' && url.origin === s; } catch { return false; }
});
export function validDescriptor(v: unknown): v is Descriptor {
  return object(v) && typeof v.pack === 'string' && HASH.test(v.pack)
    && (v.level === null || typeof v.level === 'string' && HASH.test(v.level))
    && typeof v.physics === 'string' && v.physics.length <= 64
    && servers(v.packServers) && servers(v.levelServers);
}
function validRoomDescriptor(v: unknown): v is RoomDescriptor {
  return validDescriptor(v) && object(v) && Number.isSafeInteger(v.founded) && Number(v.founded) > 0
    && typeof v.nonce === 'string' && /^[a-f0-9]{32}$/.test(v.nonce);
}
export function mismatch(a: Descriptor, b: Descriptor): string | null {
  if (a.pack !== b.pack) return 'Asset packs differ. Load the room’s asset pack, then join again.';
  if (a.level !== b.level) return 'Levels differ. Load the room’s level (including any custom GLB), then join again.';
  if (a.physics !== b.physics) return 'Game versions differ. Open the same game release as the other players.';
  return null;
}
/** True when room a was founded first; nonce breaks exact ties identically for everyone. */
export function olderRoom(a: RoomDescriptor, b: RoomDescriptor): boolean {
  return a.founded !== b.founded ? a.founded < b.founded : a.nonce < b.nonce;
}
export function decodeMessage(v: unknown): Message | null {
  if (!object(v)) return null;
  switch (v.type) {
    case 'hello': return { type: 'hello' };
    case 'room': return validRoomDescriptor(v.descriptor) && Number.isInteger(v.count) && Number(v.count) >= 1 && Number(v.count) <= 64
      && (v.profile === undefined || typeof v.profile === 'string' && HASH.test(v.profile)) ? v as unknown as Message : null;
    case 'full': return { type: 'full' };
    case 'bye': return { type: 'bye' };
    case 'car': {
      if (!Number.isSafeInteger(v.seq) || Number(v.seq) < 0 || typeof v.time !== 'number' || !Number.isFinite(v.time) || v.time < 0 || v.time > 1e12
        || !numbers(v.p, 3, 1e7) || !numbers(v.q, 4, 1) || !Array.isArray(v.w) || v.w.length !== 4 || !v.w.every(w => numbers(w, 5, 1e7))) return null;
      const norm = v.q.reduce((sum, n) => sum + n*n, 0);
      return Math.abs(norm - 1) < .01 ? v as unknown as Message : null;
    }
    default: return null;
  }
}

/** No 0/O, 1/I/L: codes are read aloud and typed on phones. */
const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
export function privateCode(random: (bytes: Uint8Array) => Uint8Array = b => crypto.getRandomValues(b)): string {
  const bytes = random(new Uint8Array(6));
  return Array.from(bytes, b => CODE_ALPHABET[b % CODE_ALPHABET.length]).join('');
}
/** Accepts any case, spaces and dashes; returns the six-character code. */
export function parsePrivateCode(input: string): string {
  const code = input.toUpperCase().replace(/[\s-]/g, '');
  if (code.length !== 6 || ![...code].every(c => CODE_ALPHABET.includes(c))) throw new Error('Room codes are six letters or numbers, like K7Q-4MZ.');
  return code;
}
export const formatCode = (code: string): string => `${code.slice(0, 3)}-${code.slice(3)}`;
/** The code is the whole rendezvous: anyone who knows it can join, nobody else is listed. */
export const privateRoom = (code: string): string => `ssrc-private:${code}`;
/** Short public room tag so newcomers prefer rooms that already use their assets. */
export const assetTag = (d: Descriptor): string => `${d.pack.slice(0, 8)}.${d.level?.slice(0, 8) ?? '--------'}`;

/** Sender clock mapped to local monotonic arrival time; never compare wall clocks. */
export class RemoteCar {
  private samples: Array<{ state: Snapshot; at: number }> = [];
  private lastSeq = -1;
  private anchor = 0;
  private senderStart = 0;
  private lastArrival = 0;
  push(state: Snapshot, now: number): boolean {
    if (state.seq <= this.lastSeq) return false;
    const previous = this.samples.at(-1);
    if (previous && state.time < previous.state.time) return false;
    this.lastSeq = state.seq;
    // Re-anchor after a stale stream or teleport; do not draw a journey through walls.
    if (!previous || now - this.lastArrival > 1000 || new Vector3().fromArray(state.p).distanceTo(new Vector3().fromArray(previous.state.p)) > 500) {
      this.samples = [];
      this.anchor = now;
      this.senderStart = state.time;
    }
    this.lastArrival = now;
    this.samples.push({ state, at: this.anchor + state.time - this.senderStart });
    if (this.samples.length > 12) this.samples.shift();
    return true;
  }
  sample(now: number): Pose | null {
    if (!this.samples.length || now - this.lastArrival > 3000) return null;
    const target = now - 100;
    while (this.samples.length > 2 && this.samples[1].at < target) this.samples.shift();
    const a = this.samples[0], b = this.samples[1];
    if (!b || target <= a.at) return a.state;
    const t = Math.max(0, Math.min(1, (target - a.at) / Math.max(1, b.at - a.at)));
    const mix = (x: number[], y: number[]) => x.map((n, i) => n + (y[i] - n) * t);
    return { p: mix(a.state.p, b.state.p), q: new Quaternion().fromArray(a.state.q).slerp(new Quaternion().fromArray(b.state.q), t).toArray(), w: a.state.w.map((w, i) => mix(w, b.state.w[i])) };
  }
}