Back to Supersonic RC Revive
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.

docs/online-multiplayer.md
# Online Free Driving: public and private rooms

Redesigned 2026-10-07. This replaces the first version (2026-10-04), where a
creator hosted an unlisted 4-player CVM room behind a long `SSRC1.<room>.<actor>`
invite and the session ended when the creator left. Implemented locally, not published.

## Modes

| Mode | Rendezvous | Membership | Capacity |
| --- | --- | --- | --- |
| **Play public** | Listed CVM room (`soy.rooms.v1`), auto-selected or created | CVM roster, renewed every 3 s | 8 per room; more rooms open as needed |
| **Private room** | NAP-WEBRTC room scope `ssrc-private:<CODE>` only; **no CVM calls** | Peers on the same code | 8 |

Both modes then run the same host-less protocol over one WebRTC session per room.

## Why eight players

The soyLI host (checked in the `soyli` 0.24 binary) keeps at most **eight remote
peers per WebRTC session**. Further `hello`s are silently ignored, and a newcomer
whose peers never answer has its whole session closed after 45 s ("Peer connection
timed out"). A full mesh therefore holds nine at most. We cap rooms at eight so every
member keeps one spare slot: a ninth player still connects, hears `full`, and gets a
clear message. The CVM room service also defaults to eight players per room. A
host cannot raise the per-session limit, and the napplet cannot change it.

## What NAP-WEBRTC provides without CVM

A room scope's wire ID is `hash("soy-rtc/1", napplet identity, ["room", room], channel, protocol)`.
Signaling uses kind 25050 events on the backend's relays. Any room string works, so a
short code is a complete rendezvous. `ONLINE_PROTOCOL` (`ssrc-free-v2.ssrc-rapier-v1`)
is part of the hash, so different physics releases never meet.

The host still gets ICE/TURN credentials from the configured backend (`soy_ice`) and
signals on its relays. Keep `.napplet-space/soy-backend.json` and the backend
configuration: without them the host offers no ICE servers and play across the
internet mostly fails. Only the game's own calls avoid CVM in private rooms.

Private codes are six characters from a 31-symbol alphabet without 0/O/1/I/L, shown
as `ABC-DEF`. Input ignores case, spaces and dashes. A code is an invitation, not a
password: anyone who knows or guesses it can join, and the 8-player cap is
enforced only by the members' own replies.

## Host-less room protocol

- **Founding.** A player who hears no room within the founding wait (4 s private,
  10 s public joiner; immediately for the creator of a new public room) founds it.
  Its asset descriptor (pack and level SHA-256, physics version, Blossom hints)
  becomes the room's, stamped with `founded` time and a random `nonce`.
- **Beacons.** Admitted players broadcast `room {descriptor, count}` every 2 s and on
  each `hello`/peer join. Newcomers send `hello`. A newcomer that hears a room adopts
  its descriptor. If the assets differ, it first downloads, verifies and installs
  them through NAP-RESOURCE (`loadHostAssets`), with a two-minute deadline.
- **Admission.** Car snapshots count only from peers that beacon the same descriptor,
  and in public rooms only from current CVM members. A newcomer spawns in lane `count`
  (110 units apart, max 7).
- **Merge.** If two groups founded the same code before hearing each other, the
  earliest `founded` wins, with `nonce` breaking ties. Players with matching assets
  switch silently. A player with different assets is told to join again; reloading
  assets mid-drive is not attempted.
- **Leaving.** `bye` or a WebRTC `left` removes a car. Nobody's departure ends the
  room, including the founder's.
- **Full.** Members answer `hello` with `full` at 8 players. `full` bypasses the
  4-in-flight control cap. A beacon with `count ≥ 8` also ends joining.

### Public room selection

`soy_room_list({napplet, protocol})`:
1. Drop rooms that are full or already include this player.
2. Prefer rooms whose name ends with this player's asset tag
   (`Public · <pack8>.<level8|-->`), so most joins need no download.
3. Then prefer the fullest room.
4. Try every candidate (another player can take the last seat). If they were all
   taken, or the directory call failed, list once more and try again. Only then
   create a listed room with capacity 8.

**Consolidation.** Players who press Play at the same moment can each create an
empty room. A player who is still alone (no connected peers, sole CVM member) checks
the directory every third renewal (~9 s). It moves into a listed room that has the
same asset tag and either has players, or is another lone room with a lower ID, so
only one of two lone players moves. The move joins the new room, closes the old
WebRTC session and leaves the old room, then opens the new one. The player keeps
driving, and the usual merge rule settles the room descriptor. Rooms that need
other assets are never auto-joined mid-drive.

The public WebRTC session is an **open rendezvous on the CVM room ID**, opened once.
The first version passed a peer allowlist and reopened the whole session on every
membership change. With eight players, each join made every member reconnect, and
the burst exceeded the host's limit of 32 signals per second per session. Membership
is now enforced per message from the renewed roster. Peers that aren't members can
connect if they know the room ID, which uses a peer slot, but their messages are ignored.

Renewal tolerates transient provider errors: a failed `soy_room_status` only ends play
when the room has expired or no renewal has succeeded for 45 s (the membership lease is
60 s). Under the eight-browser test load, the local provider occasionally timed out
single calls.

## Name tags

A signed-in player adds their own Nostr pubkey (`identity.getPublicKey()`) as
`profile` in their room beacons. Guests and hosts without NAP-IDENTITY send nothing.
Other players look it up once per session with NAP-COMMON `common.getProfile`
(`src/game/onlineNames.ts`). The tag uses `displayName`, else `name`, and the text
is cleaned up:
- invisible format characters are stripped;
- controls and line breaks collapse to spaces;
- names longer than 16 characters are cut to 15 plus "…".

Until a profile loads, or when it is missing, the tag shows the first four
characters of the WebRTC key. That key is a per-tab session key, not the player's
Nostr key, which is why the Nostr pubkey has to be sent explicitly.

The shared pubkey is an **unverified claim**: the napplet has no signer to prove
ownership, so a player could pose as someone else's profile. Tags are cosmetic and
grant nothing. Signed-in players reveal their pubkey to everyone in the room,
including strangers in public rooms.

## Simulation (unchanged)

Each machine owns its physics world and drives only its own car. Snapshots
(position, orientation, wheels) go out at about 20 Hz with one send in flight.
Remote cars use a 100 ms interpolation buffer and are visual ghosts: no collisions,
no engine audio, and no shared prop physics. The local car is yellow, labelled
"You". Each remote car gets a stable colour from its key (seven others) and a
name tag (see above); the marker widens into a pill for names. Pack switches or a Konami reload leave online play first.

## Verification evidence (2026-10-07)

- `pnpm verify`: guidance, level, local multiplayer, 32 online tests, Konami,
  type-check and build all pass. The online tests cover:
  - capabilities: private needs WebRTC only, public needs CVM + WebRTC;
  - codes;
  - beacon validation and tie-break;
  - private founding with zero CVM calls;
  - adoption and lanes;
  - the 8-player cap;
  - merge;
  - asset sync, failure, cancellation and physics mismatch;
  - public create, prefer and retry;
  - the creator leaving with roster enforcement;
  - renewal tolerance;
  - cancellation, backpressure and cleanup.
- `pnpm test:conformance`: 6 passed, 0 failed, 4 skipped (signed-manifest evidence absent).
- `pnpm test:online:hosted` with real soyLI services, 50 ms latency, 15 ms jitter:
  - **Public, 8 players** (9 assertions). Every player joins one room, and a guest
    with a different pack downloads the room pack through RESOURCE before spawning.
    All seven remote cars render for everyone and all 56 peer links connect. Driving
    feedback was about 8 ms; movement reaches other players. Leaving removes the car,
    and the room continues after its first player leaves.
  - **Private, 4 players** (11 assertions). A short code is shown and joined with a
    lowercase entry. The remote tags show profile names: identity and getProfile
    answers are stubbed because test guests have no Nostr login, but the keys
    travel in real beacons. The first player made zero CVM calls, and the asset download,
    rendering, movement, leaving and continuation checks pass as above.
  - **Simultaneous public start, 3 players** (2 assertions, added 2026-10-07). All
    press Play public at once and end up in one room within 60 s.
  - One 8-player run timed out waiting for every remote car; the rerun passed.
- Frame screenshots at 200, 320, 900 and 1440 px wide: the title actions, private
  dialog and HUD fit, and the dialog scrolls internally at the smallest size.
- Still manual: separate public networks and forced TURN (coturn not installed),
  packet loss, long sessions, phones and physical controllers.

Run `pnpm test:online` for unit tests and, after `soyli build`,
`pnpm test:online:hosted` for the browser scenarios.