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.
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])) };
}
}
