SOURCE / PINNED RELEASE
Made of little things.
Napplet Machines V8
- Release
- b3f614e739f0…
- Author-recorded commit
- 534f19acb4ba…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a
Archive hash verified: 0f7feb97e2c3d336…. 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';
export const PROTOCOL = 'pocket-circuit-v5';
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.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'
)
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.isSafeInteger(b.ack) &&
Number(b.ack) >= 0
);
});
}
export class Multiplayer {
actor = '';
authority = '';
room: Room | null = null;
session = '';
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: unknown = null;
private polling = false;
onUpdate: () => void = () => {};
onError: (m: string) => void = () => {};
onStart: (s: Start) => void = () => {};
onSnapshot: (w: World) => 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((e) => {
if (e.sessionId !== this.session) return;
if (e.type === 'peer') {
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) await this.reconnect();
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
? this.room.peers.filter((k) => k !== this.actor)
: [this.authority];
const signature = peers.slice().sort().join(',');
if (signature === this.signature && this.session) return;
const old = this.session;
this.session = '';
this.connected.clear();
if (old) await webrtc.close(old, 'Membership updated');
if (!peers.length) {
this.signature = signature;
this.onUpdate();
return;
}
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;
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: unknown) {
if (!this.session || !this.connected.size) return;
this.queued = payload;
if (this.sending) return;
this.sending = true;
try {
while (this.queued && this.session) {
const next = this.queued;
this.queued = null;
await webrtc.send(this.session, next);
}
} 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: '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 (
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.onStart(this.start);
}
return;
}
if (
p.type === 'world' &&
p.race === this.start?.race &&
validWorld(p.world)
)
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),
);
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);
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;
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 (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.';
