Back to Napplet Machines V8
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.

src/network.ts
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.';