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 { cvm, webrtc, type WebrtcEvent, type WebrtcOpenRequest } from '@napplet/sdk';
import backend from '../../.napplet-space/soy-backend.json';
import { runtimeHasDomain } from '../domain-availability.js';
import { assetServers, currentLevelOverlay, currentPackBuffer } from './assets.js';
import { ProfileNames } from './onlineNames.js';
import { assetTag, decodeMessage, formatCode, HASH, mismatch, olderRoom, ONLINE_PROTOCOL, PHYSICS_VERSION, privateRoom, RemoteCar, ROOM_CAPACITY, type Descriptor, type Message, type Pose, type RoomDescriptor, type Snapshot } from './onlineProtocol.js';
export interface Room { room: string; name?: string; protocol: string; peers: string[]; capacity: number }
export type OnlineMode = { kind: 'public' } | { kind: 'private'; code: string };
export interface OnlineTransport {
call(tool: string, args: Record<string, unknown>): Promise<unknown>;
open(request: WebrtcOpenRequest): Promise<{ session: { id: string } }>;
send(id: string, payload: unknown): Promise<void>;
close(id: string, reason?: string): Promise<void>;
events(handler: (event: WebrtcEvent) => void): { close(): void };
}
const transport: OnlineTransport = {
async call(tool, args) {
const result = backend.provider
? await cvm.callTool(backend.provider, tool, args)
: await cvm.registry.call('soy.rooms.v1', tool, args);
if (result.isError) throw new Error(result.content.filter(c => c.type === 'text').map(c => c.text).join('\n') || 'Online service unavailable.');
return result.structuredContent;
},
open: request => webrtc.open(request),
send: (id, payload) => webrtc.send(id, payload),
close: (id, reason) => webrtc.close(id, reason),
events: handler => webrtc.onEvent(handler),
};
/** Private rooms are pure NAP-WEBRTC; public rooms also need the CVM room directory. */
export function privateAvailable(): boolean { return runtimeHasDomain('webrtc'); }
export function publicAvailable(): boolean { return runtimeHasDomain('cvm') && runtimeHasDomain('webrtc'); }
export async function assetDescriptor(): Promise<Descriptor> {
const pack = currentPackBuffer();
if (!pack) throw new Error('Load an asset pack before playing online.');
if (typeof crypto === 'undefined' || !crypto.subtle) throw new Error('This browser cannot verify asset hashes. Open the game in a secure napplet host to play online.');
const hash = async (bytes: ArrayBuffer) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))].map(b => b.toString(16).padStart(2, '0')).join('');
const overlay = currentLevelOverlay();
const [packHash, levelHash] = await Promise.all([hash(pack), overlay ? hash(overlay) : Promise.resolve(null)]);
return { pack: packHash, level: levelHash, physics: PHYSICS_VERSION, packServers: assetServers(pack), levelServers: overlay ? assetServers(overlay) : [] };
}
function roomSnapshot(v: unknown): Room {
const r = v as Room;
if (!r || typeof r.room !== 'string' || r.protocol !== ONLINE_PROTOCOL || r.capacity !== ROOM_CAPACITY || !Array.isArray(r.peers)
|| r.peers.length > ROOM_CAPACITY || !r.peers.every(p => typeof p === 'string' && HASH.test(p)) || new Set(r.peers).size !== r.peers.length) throw new Error('The room is incompatible with this game version.');
return r;
}
const nonce = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('');
/** Stable per-player colour index 1–7; 0 is always the local car. */
export const remoteColor = (actor: string): number => 1 + parseInt(actor.slice(0, 8), 16) % 7;
/**
* Host-less rendezvous and admission. Physics stays entirely in App's local world.
* The first player founds the room's asset descriptor; everyone repeats it in beacons,
* so the room continues after any player, including its founder, leaves.
*/
export class OnlineSession {
actor = '';
room: Room | null = null;
roomDescriptor: RoomDescriptor | null = null;
readonly remote = new Map<string, RemoteCar>();
ready = false;
/** Spawn offset index, chosen from the players already driving at admission. */
lane = 0;
/** This player's Nostr pubkey, shared so others can show a profile name; null stays anonymous. */
profile: string | null = null;
names = new ProfileNames();
private readonly profiles = new Map<string, string>();
status = 'Connecting…';
onChange: () => void = () => {};
onReady: () => void = () => {};
onEnd: (reason: string) => void = () => {};
onAssetsRequired: ((descriptor: Descriptor, isActive: () => boolean) => Promise<Descriptor>) | null = null;
private syncingAssets = false;
private active = true;
private sessionId: string | null = null;
private readonly connected = new Set<string>();
/** Connected peers that beacon the same room descriptor as this player. */
private readonly admitted = new Set<string>();
private offered: RoomDescriptor | null = null;
private offeredCount = 0;
private polling = false;
private renewedAt = 0;
private polls = 0;
private subscription: { close(): void } | null = null;
private pollTimer: ReturnType<typeof setInterval> | null = null;
private beaconTimer: ReturnType<typeof setInterval> | null = null;
private waitTimer: ReturnType<typeof setTimeout> | null = null;
private sendingState = false;
private controlsInFlight = 0;
private sequence = 0;
private sendAt = 0;
constructor(public descriptor: Descriptor, readonly mode: OnlineMode, private readonly io = transport, private readonly namespace = backend.napplet,
private readonly foundingWait = mode.kind === 'private' ? 4000 : 10000) {}
get code(): string { return this.mode.kind === 'private' ? formatCode(this.mode.code) : ''; }
get players(): number { return this.admitted.size + 1; }
async start(): Promise<void> {
try {
if (this.mode.kind === 'public') {
const result = await this.io.call('soy_session', {}) as { actor: string };
if (!this.active) return;
if (!result || !HASH.test(result.actor)) throw new Error('The online service returned an invalid player identity.');
this.actor = result.actor;
const room = await this.findPublicRoom();
// Cancellation can happen while create/join is awaiting the provider.
if (!this.active) { await this.io.call('soy_room_leave', { room: room.room }); return; }
this.room = room;
this.renewedAt = Date.now();
}
this.subscription = this.io.events(e => this.event(e));
await this.open();
if (!this.active) return;
if (this.room) this.pollTimer = setInterval(() => void this.poll(), 3000);
this.beaconTimer = setInterval(() => this.beacon(), 2000);
// A public room this player just created has nobody else to wait for.
if (this.room?.peers.length === 1) this.found();
else this.waitTimer = setTimeout(() => { if (!this.ready && !this.syncingAssets && !this.offered) this.found(); }, this.foundingWait);
this.changed();
} catch (error) {
if (this.active) this.fail(error instanceof Error ? error.message : String(error));
throw error;
}
}
/** Listed public rooms with space, matching assets first, then fullest; null when the directory fails. */
private async publicRooms(): Promise<Room[] | null> {
const tag = assetTag(this.descriptor);
let listed: { rooms?: unknown[] } | null;
try { listed = await this.io.call('soy_room_list', { napplet: this.namespace, protocol: ONLINE_PROTOCOL }) as typeof listed; }
catch (error) { console.log('[online] public room list unavailable', error); return null; }
return (Array.isArray(listed?.rooms) ? listed.rooms : []).flatMap(r => { try { return [roomSnapshot(r)]; } catch { return []; } })
.filter(r => r.peers.length < ROOM_CAPACITY && !r.peers.includes(this.actor))
.sort((a, b) => Number(b.name?.endsWith(tag) ?? false) - Number(a.name?.endsWith(tag) ?? false) || b.peers.length - a.peers.length);
}
/**
* Join an existing room whenever one has space: try every listed candidate, and list once more
* when seats were taken meanwhile or the directory failed. Create a room only when none fits.
*/
private async findPublicRoom(): Promise<Room> {
for (let attempt = 0; attempt < 2 && this.active; ++attempt) {
const rooms = await this.publicRooms();
if (rooms && !rooms.length) break;
for (const candidate of rooms ?? []) {
if (!this.active) break;
// Another player may take the last seat between listing and joining.
try { return roomSnapshot(await this.io.call('soy_room_join', { room: candidate.room })); }
catch (error) { console.log('[online] public room unavailable', error); }
}
}
return roomSnapshot(await this.io.call('soy_room_create', { napplet: this.namespace, protocol: ONLINE_PROTOCOL, name: `Public · ${assetTag(this.descriptor)}`, capacity: ROOM_CAPACITY, listed: true }));
}
private changed(): void {
const where = this.mode.kind === 'private' ? `Room ${this.code}` : 'Public';
this.status = this.syncingAssets ? 'Downloading and loading the room’s game data…'
: this.ready ? `${where} · ${this.players}/${ROOM_CAPACITY} players` : this.connected.size ? 'Joining players…' : 'Looking for players…';
this.onChange();
}
/** Nobody answered: this player's assets become the room's. */
private found(): void {
if (!this.active || this.ready) return;
this.roomDescriptor = { ...this.descriptor, founded: Date.now(), nonce: nonce() };
this.admit();
}
private admit(players = 0): void {
if (this.ready) return;
this.ready = true;
this.lane = Math.min(players, ROOM_CAPACITY - 1);
if (this.waitTimer) clearTimeout(this.waitTimer);
this.waitTimer = null;
console.log('[online] player admitted', { players: this.players });
this.onReady();
this.beacon();
this.changed();
}
private beacon(): void {
if (!this.connected.size) return;
if (this.ready && this.roomDescriptor) void this.control({ type: 'room', descriptor: this.roomDescriptor, count: this.players, ...this.profile ? { profile: this.profile } : {} });
else void this.control({ type: 'hello' });
}
/** Beacons repeat, so a burst may drop them; "full" must always reach the overflow player. */
private async control(message: Message): Promise<void> {
const id = this.sessionId;
if (!this.active || !id || !this.connected.size || this.controlsInFlight >= 4 && message.type !== 'full') return;
++this.controlsInFlight;
try { await this.io.send(id, message); }
catch (error) { console.log('[online] control delivery deferred', error); }
finally { --this.controlsInFlight; }
}
/**
* One open rendezvous per room: a private code, or the public room's CVM id. The host cannot
* update a peer allowlist in place, and reopening the whole mesh on every join floods signaling,
* so public membership is enforced per message from the renewed CVM roster instead.
*/
private async open(): Promise<void> {
const room = this.mode.kind === 'private' ? privateRoom(this.mode.code) : this.room!.room;
const { session } = await this.io.open({ scope: { type: 'room', room }, channel: 'cars', protocol: ONLINE_PROTOCOL });
if (!this.active) { await this.io.close(session.id, 'Cancelled'); return; }
this.sessionId = session.id;
console.log('[online] connecting', { mode: this.mode.kind });
}
private async poll(): Promise<void> {
if (this.polling || !this.active || !this.room) return;
this.polling = true;
try {
const next = roomSnapshot(await this.io.call('soy_room_status', { room: this.room.room }));
if (!this.active) return;
this.room = next;
this.renewedAt = Date.now();
for (const actor of [...this.admitted]) if (!next.peers.includes(actor)) this.admitted.delete(actor);
for (const actor of this.remote.keys()) if (!this.admitted.has(actor)) this.remote.delete(actor);
this.changed();
if (++this.polls % 3 === 0 && this.alone()) await this.consolidate();
} catch (error) {
// Membership lasts 60 s: a slow or busy provider only ends play once renewal is about to lapse.
const message = error instanceof Error ? error.message : String(error);
if (!this.active) return;
if (/expired|join this room/i.test(message) || Date.now() - this.renewedAt > 45000) this.fail(`Online connection lost: ${message}`);
else console.log('[online] room renewal retrying', message);
} finally { this.polling = false; }
}
private alone(): boolean {
return this.ready && !this.syncingAssets && !this.connected.size && this.room?.peers.length === 1;
}
/**
* Players who started together can each open an empty room. A lone public player moves into a
* listed room with the same assets: any room with players, or the lower ID of two lone rooms so
* exactly one side moves. Same assets means the move never downloads or interrupts driving.
*/
private async consolidate(): Promise<void> {
const current = this.room!, tag = assetTag(this.descriptor);
const target = (await this.publicRooms())?.find(r => r.room !== current.room && r.name?.endsWith(tag)
&& (r.peers.length > 1 || r.room < current.room));
if (!target || !this.active || !this.alone()) return;
let next: Room;
try { next = roomSnapshot(await this.io.call('soy_room_join', { room: target.room })); }
catch (error) { console.log('[online] room move skipped', error); return; }
// The player may have left, or someone may have connected, while the join was in flight.
if (!this.active || !this.alone()) { void this.io.call('soy_room_leave', { room: next.room }).catch(() => {}); return; }
const old = this.sessionId;
this.sessionId = null;
this.room = next;
this.renewedAt = Date.now();
console.log('[online] moving to a room with players', { players: next.peers.length });
const results = await Promise.allSettled([old ? this.io.close(old, 'Moving room') : Promise.resolve(), this.io.call('soy_room_leave', { room: current.room })]);
for (const r of results) if (r.status === 'rejected') console.log('[online] cleanup deferred to room expiry', r.reason);
try { if (this.active) await this.open(); }
catch (error) { if (this.active) this.fail(`Online connection lost: ${error instanceof Error ? error.message : String(error)}`); }
this.changed();
}
private member(actor: string): boolean {
return actor !== this.actor && this.connected.has(actor) && (!this.room || this.room.peers.includes(actor));
}
private event(e: WebrtcEvent): void {
if (!this.active || e.sessionId !== this.sessionId) return;
if (e.type === 'closed') { this.fail(e.reason || 'Online connection closed. Join again to reconnect.'); return; }
if (e.type === 'peer') {
// A new public member can connect before this player's next roster poll; messages wait for it.
if (e.pubkey === this.actor) return;
if (e.state === 'joined') { this.connected.add(e.pubkey); this.beacon(); }
else { this.connected.delete(e.pubkey); this.admitted.delete(e.pubkey); this.remote.delete(e.pubkey); this.profiles.delete(e.pubkey); }
console.log('[online] peer', { actor: e.pubkey, state: e.state });
this.changed();
return;
}
if (e.type !== 'message' || !this.member(e.from)) return;
const m = decodeMessage(e.payload);
if (!m) return;
if (m.type === 'hello') {
if (!this.ready) return;
// The session keeps one spare peer slot so an overflow player still hears this.
if (this.players >= ROOM_CAPACITY) void this.control({ type: 'full' });
else this.beacon();
} else if (m.type === 'room') {
if (m.profile) this.profiles.set(e.from, m.profile);
this.roomBeacon(e.from, m.descriptor, m.count);
}
else if (m.type === 'full') {
if (!this.ready && !this.syncingAssets) this.fail(`This room is full (${ROOM_CAPACITY} players). Try again later.`);
} else if (m.type === 'bye') {
this.admitted.delete(e.from);
this.remote.delete(e.from);
this.changed();
} else if (m.type === 'car' && this.ready && this.admitted.has(e.from)) {
let car = this.remote.get(e.from);
if (!car) { car = new RemoteCar(); this.remote.set(e.from, car); }
car.push(m, performance.now());
}
}
private roomBeacon(from: string, theirs: RoomDescriptor, count: number): void {
if (theirs.physics !== this.descriptor.physics) { if (!this.ready) this.fail('Game versions differ. Open the same game release as the other players.'); return; }
const ours = this.roomDescriptor;
if (ours && ours.founded === theirs.founded && ours.nonce === theirs.nonce) {
if (!this.admitted.has(from)) { this.admitted.add(from); this.changed(); }
return;
}
if (!this.ready) {
if (this.syncingAssets || this.offered && !olderRoom(theirs, this.offered)) return;
if (count >= ROOM_CAPACITY) { this.fail(`This room is full (${ROOM_CAPACITY} players). Try again later.`); return; }
this.offered = theirs;
this.offeredCount = count;
if (!mismatch(this.descriptor, theirs)) { this.roomDescriptor = theirs; this.admitted.add(from); this.admit(count); }
else if (this.onAssetsRequired) void this.syncAssets(theirs, from);
else this.fail(mismatch(this.descriptor, theirs)!);
return;
}
// Two groups met (both founded before hearing each other): the older room wins.
if (!ours || !olderRoom(theirs, ours)) { this.beacon(); return; }
if (mismatch(this.descriptor, theirs)) { this.fail('Another group was already driving here with different game data. Join again to load it.'); return; }
this.roomDescriptor = theirs;
this.admitted.clear();
this.admitted.add(from);
this.beacon();
this.changed();
}
private async syncAssets(target: RoomDescriptor, from: string): Promise<void> {
this.syncingAssets = true;
if (this.waitTimer) clearTimeout(this.waitTimer);
this.waitTimer = setTimeout(() => this.fail('Downloading the room’s game data timed out. Try joining again.'), 120000);
this.changed();
try {
const descriptor = await this.onAssetsRequired!(target, () => this.active);
if (!this.active) return;
const reason = mismatch(target, descriptor);
if (reason) throw new Error(reason);
this.descriptor = descriptor;
this.syncingAssets = false;
this.roomDescriptor = target;
if (this.member(from)) this.admitted.add(from);
this.admit(this.offeredCount);
} catch (error) {
if (this.active) this.fail(`Could not load the room’s game data: ${error instanceof Error ? error.message : String(error)}`);
} finally { this.syncingAssets = false; }
}
/** One outstanding snapshot only: render frames never build an outbound backlog. */
update(pose: Pose, now: number): void {
if (!this.active || !this.ready || !this.sessionId || !this.admitted.size || this.sendingState || now < this.sendAt) return;
this.sendAt = now + 50;
const message: Snapshot = { ...pose, type: 'car', seq: this.sequence++, time: now };
this.sendingState = true;
void this.io.send(this.sessionId, message).catch(error => console.log('[online] snapshot dropped', error)).finally(() => { this.sendingState = false; });
}
remotePlayers(now: number): Array<{ actor: string; pose: Pose }> {
return [...this.admitted].sort().flatMap(actor => {
const pose = this.connected.has(actor) ? this.remote.get(actor)?.sample(now) : null;
return pose ? [{ actor, pose }] : [];
});
}
/** Profile name when the player shares one and NAP-COMMON finds it, else a short key tag. */
label(actor: string): string {
return this.names.name(this.profiles.get(actor)) ?? actor.slice(0, 4).toUpperCase();
}
private fail(reason: string): void {
console.log('[online] session ended', reason);
void this.close();
this.onEnd(reason);
}
async close(): Promise<void> {
if (!this.active) return;
const id = this.sessionId, room = this.room;
// Send departure before releasing the channel so others drop the car at once.
const bye = id && this.connected.size ? this.io.send(id, { type: 'bye' }).catch(() => {}) : Promise.resolve();
this.active = false;
this.sessionId = null;
if (this.pollTimer) clearInterval(this.pollTimer);
if (this.beaconTimer) clearInterval(this.beaconTimer);
if (this.waitTimer) clearTimeout(this.waitTimer);
this.subscription?.close();
this.remote.clear();
// Closing must release RTC even if a departure message is buffered indefinitely.
let timeout: ReturnType<typeof setTimeout> | undefined;
await Promise.race([bye, new Promise<void>(resolve => { timeout = setTimeout(resolve, 500); })]);
if (timeout) clearTimeout(timeout);
const results = await Promise.allSettled([id ? this.io.close(id, 'Leaving game') : Promise.resolve(), room ? this.io.call('soy_room_leave', { room: room.room }) : Promise.resolve()]);
for (const r of results) if (r.status === 'rejected') console.log('[online] cleanup deferred to room expiry', r.reason);
}
}
