SOURCE / PINNED RELEASE
Made of little things.
Napplet Machines V8
- Release
- f041ff5dc40c…
- Author-recorded commit
- 71c68951e1dd…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a
Archive hash verified: 996282af7a380b0d…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { cvm, webrtc, type Subscription } from '@napplet/sdk';
import backend from './backend-context.json';
import { validateTrack, type Track } from './track.js';
import { type World, type Input, type Driver } from './physics.js';
import { runtimeHasDomain } from './domain-availability.js';
import {
encodeState,
decodeState,
stateBase64,
stateBytes,
type StatePacket,
} from './network-codec.js';
export const PROTOCOL = 'pocket-circuit-v7';
// The pinned SDK forwards open/events, but predates #145's sendBytes helper.
// Use only the injected domain operation; no app-owned port or signaling.
type RealtimeBinding = { sendBytes?: (id: string, bytes: string) => Promise<void> };
const realtimeBinding = () =>
(window as Window & { napplet?: { webrtc?: RealtimeBinding } }).napplet?.webrtc;
export type Room = {
room: string;
name: string;
capacity: number;
peers: string[];
authority?: string;
};
// Soy rooms have no creator field and sort peer keys. Carry the public creator in
// the application-owned label, then verify it against current CVM membership.
function roomLabel(name: string, actor: string) {
return (
name.slice(0, 30) +
'|' +
btoa(String.fromCharCode(...actor.match(/../g)!.map((h) => parseInt(h, 16)))).replace(/=+$/, '')
);
}
function decodeRoom(r: Room): Room {
const split = r.name.lastIndexOf('|');
let authority = '';
try {
authority = Array.from(atob(r.name.slice(split + 1)), (c) =>
c.charCodeAt(0).toString(16).padStart(2, '0'),
).join('');
} catch {}
if (split < 0 || !key(authority) || !r.peers.includes(authority))
throw new Error('The room host has left or the room is incompatible.');
return { ...r, name: r.name.slice(0, split), authority };
}
export type Profile = { name: string; car: number; ready: boolean };
export type Command = { seq: number; input: Input };
export type Start = { type: 'start'; race: string; track: Track; world: World };
const record = (v: unknown): v is Record<string, unknown> =>
!!v && typeof v === 'object' && !Array.isArray(v);
const key = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
export function validRoom(v: unknown): v is Room {
return (
record(v) &&
typeof v.room === 'string' &&
v.room.length < 160 &&
typeof v.name === 'string' &&
v.name.length <= 80 &&
Number.isInteger(v.capacity) &&
Number(v.capacity) >= 2 &&
Number(v.capacity) <= 6 &&
Array.isArray(v.peers) &&
v.peers.length <= 6 &&
v.peers.every(key) &&
new Set(v.peers).size === v.peers.length
);
}
export function validInput(v: unknown): v is Input {
return (
record(v) &&
typeof v.throttle === 'number' &&
Number.isFinite(v.throttle) &&
Math.abs(v.throttle) <= 1 &&
typeof v.steer === 'number' &&
Number.isFinite(v.steer) &&
Math.abs(v.steer) <= 1 &&
typeof v.brake === 'boolean' &&
typeof v.boost === 'boolean' &&
typeof v.reset === 'boolean'
);
}
export function validWorld(v: unknown): v is World {
if (
!record(v) ||
!Number.isSafeInteger(v.tick) ||
Number(v.tick) < 0 ||
Number(v.tick) > 1e8 ||
typeof v.time !== 'number' ||
v.time < 0 ||
v.time > 1000 ||
!Number.isFinite(v.time) ||
typeof v.countdown !== 'number' ||
v.countdown < 0 ||
v.countdown > 4 ||
!Number.isFinite(v.countdown) ||
typeof v.ended !== 'boolean' ||
!Array.isArray(v.bodies) ||
v.bodies.length < 1 ||
v.bodies.length > 6
)
return false;
const ids = new Set();
return v.bodies.every((b) => {
if (
!record(b) ||
typeof b.id !== 'string' ||
b.id.length > 80 ||
ids.has(b.id) ||
typeof b.name !== 'string' ||
b.name.length > 18 ||
!Number.isInteger(b.car) ||
Number(b.car) < 0 ||
Number(b.car) > 3 ||
typeof b.ai !== 'boolean' ||
typeof b.grounded !== 'boolean' ||
typeof b.boosting !== 'boolean'
)
return false;
ids.add(b.id);
return (
[
'x',
'y',
'z',
'vx',
'vy',
'vz',
'angle',
'spin',
'steering',
'handbrake',
'progress',
'lastDistance',
'safeDistance',
'lap',
'finished',
'falls',
'respawn',
'boost',
'pitch',
'roll',
'ack',
'nextGate',
'stuck',
].every(
(k) => typeof b[k] === 'number' && Number.isFinite(b[k]) && Math.abs(Number(b[k])) < 1e7,
) &&
Math.abs(Number(b.x)) < 256 &&
Math.abs(Number(b.y)) < 256 &&
Math.abs(Number(b.z)) < 100 &&
Math.abs(Number(b.steering)) <= 1 &&
Number(b.handbrake) >= 0 &&
Number(b.handbrake) <= 1 &&
Number(b.boost) >= 0 &&
Number(b.boost) <= 1 &&
Number.isSafeInteger(b.ack) &&
Number(b.ack) >= 0
);
});
}
export class Multiplayer {
actor = '';
authority = '';
room: Room | null = null;
session = '';
stateSession = '';
transient = false;
private statePeers = new Set<string>();
private transportPeers = new Set<string>();
private binaryPeers = new Set<string>();
private binaryDisabled = false;
private lastWorldTick = -1;
private retrySend = 0;
private lastInput = new Map<string, number>();
private lastReset = new Map<string, number>();
get transportLabel() {
return this.transient && [...this.connected].every((k) => this.statePeers.has(k))
? 'Transient state · reliable control'
: 'Reliable JSON fallback';
}
members = new Map<string, Profile>();
connected = new Set<string>();
profile: Profile = { name: 'Racer', car: 0, ready: false };
start: Start | null = null;
private sub: Subscription | null = null;
private poll = 0;
private heartbeat = 0;
private revision = 0;
private signature = '';
private sending = false;
private queued: StatePacket | null = null;
private polling = false;
onUpdate: () => void = () => {};
onError: (m: string) => void = () => {};
onStart: (s: Start) => void = () => {};
onSnapshot: (w: World) => void = () => {};
onReset: (from: string, seq: number) => void = () => {};
onInput: (from: string, c: Command[]) => void = () => {};
onEnded: (reason: string) => void = () => {};
get available() {
return runtimeHasDomain('cvm') && runtimeHasDomain('webrtc');
}
get isHost() {
return !!this.actor && this.actor === this.authority;
}
async call(tool: string, args: Record<string, unknown> = {}): Promise<unknown> {
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') || 'Room service unavailable',
);
if (!result.structuredContent) throw new Error('Room service returned no data');
return result.structuredContent;
}
async identify() {
if (!this.available)
throw new Error('Online rooms are unavailable in this host. Local AI racing still works.');
const s = await this.call('soy_session');
if (!record(s) || !key(s.actor)) throw new Error('Invalid session');
this.actor = s.actor;
}
async list(): Promise<Room[]> {
await this.identify();
const r = await this.call('soy_room_list', {
napplet: backend.napplet,
protocol: PROTOCOL,
});
if (!record(r) || !Array.isArray(r.rooms)) throw new Error('Invalid room list');
return r.rooms.filter(validRoom).flatMap((r) => {
try {
return [decodeRoom(r)];
} catch {
return [];
}
});
}
async host(name: string, capacity: number) {
await this.identify();
const r = await this.call('soy_room_create', {
napplet: backend.napplet,
protocol: PROTOCOL,
name: roomLabel(name || 'Pocket race', this.actor),
capacity,
listed: true,
});
if (!validRoom(r)) throw new Error('Invalid room response');
this.authority = this.actor;
await this.enter(decodeRoom(r));
}
async join(id: string) {
await this.identify();
const r = await this.call('soy_room_join', { room: id.trim() });
if (!validRoom(r)) throw new Error('Invalid room response');
const decoded = decodeRoom(r);
this.authority = decoded.authority!;
await this.enter(decoded);
}
private async enter(room: Room) {
this.room = room;
this.profile.ready = this.isHost;
this.members.set(this.actor, { ...this.profile });
this.sub = webrtc.onEvent((base) => {
const e = base as
typeof base | { type: 'bytes'; sessionId: string; from: string; bytes: string };
if (e.sessionId === this.stateSession) {
if (e.type === 'peer') {
if (e.state === 'joined') this.statePeers.add(e.pubkey);
else this.statePeers.delete(e.pubkey);
}
if (e.type === 'closed') {
this.stateSession = '';
this.statePeers.clear();
this.transient = false;
}
if (e.type === 'bytes' && this.start && this.room?.peers.includes(e.from)) {
const bytes = stateBytes(e.bytes);
const packet = bytes && decodeState(bytes, this.start.race, this.start.world.bodies);
if (packet) this.receive(e.from, packet);
}
if (
e.type === 'message' &&
this.room?.peers.includes(e.from) &&
record(e.payload) &&
['world', 'input'].includes(String(e.payload.type))
)
this.receive(e.from, e.payload);
this.onUpdate();
return;
}
if (e.sessionId !== this.session) return;
if (e.type === 'peer') {
if (e.state === 'joined') this.transportPeers.add(e.pubkey);
else {
this.transportPeers.delete(e.pubkey);
this.binaryPeers.delete(e.pubkey);
}
if (!this.room?.peers.includes(e.pubkey)) return;
if (e.state === 'joined') {
this.connected.add(e.pubkey);
void this.hello();
if (this.isHost && this.start) void this.sendControl(this.start);
} else this.connected.delete(e.pubkey);
this.onUpdate();
}
if (e.type === 'message' && this.room?.peers.includes(e.from))
this.receive(e.from, e.payload);
if (e.type === 'closed') {
this.connected.clear();
this.signature = '';
this.onError(e.reason || 'Connection closed. Use reconnect.');
this.onUpdate();
}
});
// A failed permission request keeps the room visible with an explicit retry.
try {
await this.reconnect();
} catch (e) {
this.onError(errorText(e));
}
this.poll = window.setInterval(() => void this.refresh(), 2500);
this.heartbeat = window.setInterval(() => void this.hello(), 1500);
this.onUpdate();
}
async refresh() {
if (!this.room || this.polling) return;
this.polling = true;
const id = this.room.room;
try {
const r = await this.call('soy_room_status', { room: id });
if (!validRoom(r)) throw new Error('Room expired. Leave and join again.');
if (this.room?.room !== id) return;
if (!r.peers.includes(this.authority)) {
await this.leave();
this.onEnded('The host left. Join or host a new race.');
return;
}
const changed = [...r.peers].sort().join(',') !== [...this.room.peers].sort().join(',');
this.room = decodeRoom(r);
for (const k of this.members.keys()) if (!r.peers.includes(k)) this.members.delete(k);
if (changed) {
this.connected = new Set([...this.transportPeers].filter((k) => r.peers.includes(k)));
await this.hello();
if (!this.isHost) await this.sendControl({ type: 'sync' });
}
this.onUpdate();
} catch (e) {
this.onError(errorText(e));
} finally {
this.polling = false;
}
}
async reconnect() {
if (!this.room) return;
const generation = ++this.revision;
const peers = this.isHost ? undefined : [this.authority];
const signature = this.room.room + ':' + this.authority;
if (signature === this.signature && this.session) return;
const old = this.session;
this.session = '';
this.connected.clear();
this.transportPeers.clear();
this.binaryPeers.clear();
this.statePeers.clear();
this.binaryDisabled = false;
const oldState = this.stateSession;
this.stateSession = '';
this.transient = false;
if (oldState) await webrtc.close(oldState, 'Reconnecting');
if (old) await webrtc.close(old, 'Membership updated');
const { session } = await webrtc.open({
scope: { type: 'room', room: this.room.room, peers },
channel: 'race',
protocol: PROTOCOL,
});
if (generation !== this.revision || !this.room) {
await webrtc.close(session.id, 'Cancelled');
return;
}
this.session = session.id;
this.signature = signature;
// Presence of the new binding avoids sending extension fields to old hosts.
if (realtimeBinding()?.sendBytes) {
try {
const request = {
scope: { type: 'room' as const, room: this.room.room, peers },
channel: 'state',
protocol: PROTOCOL,
ordered: false,
maxRetransmits: 0,
};
const opened = await webrtc.open(request);
const actual = opened.session as typeof opened.session & {
ordered?: boolean;
maxRetransmits?: number;
maxPayloadBytes?: number;
};
if (
generation !== this.revision ||
!this.room ||
actual.ordered !== false ||
actual.maxRetransmits !== 0 ||
(actual.maxPayloadBytes ?? 0) < 1024
) {
await webrtc.close(actual.id, 'Transient delivery unavailable');
} else {
this.stateSession = actual.id;
this.transient = true;
}
} catch {
/* Explicit reliable JSON fallback; control connection remains usable. */
}
}
this.onUpdate();
}
async retry() {
this.signature = '';
await this.reconnect();
}
async sendControl(payload: unknown) {
if (!this.session || !this.connected.size) return;
try {
await webrtc.send(this.session, payload);
} catch (e) {
this.onError(errorText(e));
}
}
// Only one pending send plus the newest replacement is retained under backpressure.
async sendLatest(payload: StatePacket) {
if (!this.session || !this.connected.size) return;
this.queued = structuredClone(payload);
if (this.sending) return;
this.sending = true;
try {
while (this.queued && this.session) {
const next = this.queued;
this.queued = null;
const state = this.stateSession && [...this.connected].every((k) => this.statePeers.has(k));
const binary =
state &&
!this.binaryDisabled &&
[...this.connected].every((k) => this.binaryPeers.has(k));
const bytes = binary ? encodeState(next) : undefined;
try {
if (bytes) await realtimeBinding()!.sendBytes!(this.stateSession, stateBase64(bytes));
else await webrtc.send(state ? this.stateSession : this.session, next);
} catch (e) {
const reason = errorText(e);
if (reason === 'unsupported' && bytes) {
// Explicit rejection guarantees nothing was sent. This payload is safe to retry.
this.binaryDisabled = true;
await webrtc.send(this.stateSession, next);
} else if (
reason === 'QUEUE_FULL' ||
reason.includes('rate limit') ||
reason.includes('quota')
) {
this.queued ??= next;
clearTimeout(this.retrySend);
this.retrySend = window.setTimeout(() => {
const latest = this.queued;
if (latest) void this.sendLatest(latest);
}, 50);
break;
} else {
// In particular: a timeout is delivery-unknown. Never resend it as JSON.
this.binaryDisabled = true;
throw e;
}
}
}
} catch (e) {
this.queued = null;
this.onError(errorText(e));
} finally {
this.sending = false;
}
}
async hello() {
if (!this.room) return;
this.members.set(this.actor, { ...this.profile });
await this.sendControl({ type: 'features', binary: !!realtimeBinding()?.sendBytes });
await this.sendControl({ type: 'profile', ...this.profile });
if (this.isHost)
await this.sendControl({
type: 'lobby',
members: Array.from(this.members, ([id, p]) => ({ id, ...p })),
});
}
private receive(from: string, p: unknown) {
if (!record(p) || typeof p.type !== 'string') return;
if (
this.isHost &&
p.type === 'reset' &&
p.race === this.start?.race &&
Number.isSafeInteger(p.seq) &&
Number(p.seq) > (this.lastReset.get(from) ?? 0) &&
Number(p.seq) < 1e8
) {
this.lastReset.set(from, Number(p.seq));
this.onReset(from, Number(p.seq));
return;
}
if (this.isHost && p.type === 'sync' && this.start) {
void this.sendControl(this.start);
return;
}
if (p.type === 'features') {
if (p.binary === true) this.binaryPeers.add(from);
else this.binaryPeers.delete(from);
return;
}
if (
p.type === 'profile' &&
this.isHost &&
typeof p.name === 'string' &&
p.name.length <= 18 &&
Number.isInteger(p.car) &&
Number(p.car) >= 0 &&
Number(p.car) < 4 &&
typeof p.ready === 'boolean'
) {
this.members.set(from, {
name: p.name,
car: Number(p.car),
ready: p.ready,
});
this.onUpdate();
return;
}
if (from === this.authority) {
if (p.type === 'lobby' && Array.isArray(p.members) && p.members.length <= 6) {
for (const m of p.members)
if (
record(m) &&
key(m.id) &&
this.room?.peers.includes(m.id) &&
typeof m.name === 'string' &&
m.name.length <= 18 &&
Number.isInteger(m.car) &&
Number(m.car) >= 0 &&
Number(m.car) < 4 &&
typeof m.ready === 'boolean'
)
this.members.set(m.id, {
name: m.name,
car: Number(m.car),
ready: m.ready,
});
this.onUpdate();
}
if (
p.type === 'start' &&
typeof p.race === 'string' &&
p.race.length <= 64 &&
validateTrack(p.track) &&
validWorld(p.world)
) {
if (p.world.bodies.some((b) => !b.ai && !this.room?.peers.includes(b.id))) return;
if (this.start?.race !== p.race) {
this.start = p as unknown as Start;
this.lastWorldTick = -1;
this.lastInput.clear();
this.lastReset.clear();
this.onStart(this.start);
}
return;
}
if (
p.type === 'world' &&
p.race === this.start?.race &&
validWorld(p.world) &&
p.world.tick > this.lastWorldTick
) {
this.lastWorldTick = p.world.tick;
this.onSnapshot(p.world);
}
if (p.type === 'return') {
this.start = null;
this.onEnded('Back in the room. Ready for another race.');
}
}
if (
this.isHost &&
p.type === 'input' &&
p.race === this.start?.race &&
Array.isArray(p.commands) &&
p.commands.length <= 120
) {
const commands = p.commands.filter(
(c): c is Command =>
record(c) &&
Number.isSafeInteger(c.seq) &&
Number(c.seq) > 0 &&
Number(c.seq) < 1e8 &&
validInput(c.input),
);
if (commands.length !== p.commands.length) return;
const newest = commands.at(-1);
if (
!newest ||
newest.seq <= (this.lastInput.get(from) ?? 0) ||
commands.some((c, i) => i > 0 && c.seq <= commands[i - 1].seq)
)
return;
this.lastInput.set(from, newest.seq);
this.onInput(from, commands);
}
}
async begin(track: Track, world: World) {
if (!this.isHost) throw new Error('Only the host can start');
if (!validateTrack(track)) {
throw new Error('Move crowded track points at least 3 m apart before hosting a race.');
}
this.start = {
type: 'start',
race: crypto.randomUUID(),
track: structuredClone(track),
world: structuredClone(world),
};
this.onStart(this.start);
this.lastWorldTick = -1;
this.lastInput.clear();
this.lastReset.clear();
await this.sendControl(this.start);
}
roster(): Driver[] {
return (this.room?.peers || []).map((id) => ({
id,
name: this.members.get(id)?.name || 'Racer',
car: this.members.get(id)?.car || 0,
ai: false,
}));
}
async leave() {
const room = this.room,
session = this.session;
const stateSession = this.stateSession;
this.stateSession = '';
this.statePeers.clear();
this.binaryPeers.clear();
this.transportPeers.clear();
this.lastInput.clear();
this.lastReset.clear();
this.transient = false;
clearTimeout(this.retrySend);
this.room = null;
this.session = '';
this.start = null;
this.revision++;
this.signature = '';
this.queued = null;
clearInterval(this.poll);
clearInterval(this.heartbeat);
this.sub?.close();
this.sub = null;
this.connected.clear();
this.members.clear();
this.onUpdate();
const jobs = [];
if (session) jobs.push(webrtc.close(session, 'Left room'));
if (stateSession) jobs.push(webrtc.close(stateSession, 'Left room'));
if (room) jobs.push(this.call('soy_room_leave', { room: room.room }));
await Promise.allSettled(jobs);
}
}
export const errorText = (e: unknown) =>
e instanceof Error ? e.message : 'Operation failed. Please retry.';
