@homie-rocks/studio 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/agents/agents.ts +8 -6
  3. package/bin/homie-studio.mjs +9 -6
  4. package/lab/lab.ts +15 -0
  5. package/lib/build.mjs +20 -15
  6. package/lib/dev.mjs +16 -1
  7. package/lib/lab-check.mjs +18 -3
  8. package/lib/lab.mjs +21 -7
  9. package/lib/mcp-tools.mjs +2 -1
  10. package/lib/rules-build.mjs +97 -49
  11. package/lib/rules-check-worker.mjs +405 -0
  12. package/lib/rules-check.mjs +69 -0
  13. package/lib/rules-compiler.mjs +30 -0
  14. package/lib/rules-guard.mjs +26 -7
  15. package/lib/rules-types.mjs +167 -0
  16. package/lib/shop-catalog.mjs +20 -1
  17. package/lib/shop-links.mjs +3 -1
  18. package/lib/shop.mjs +14 -8
  19. package/lib/standalone-files.mjs +2 -2
  20. package/lib/standalone.mjs +6 -2
  21. package/lib/stripe-connect.mjs +18 -7
  22. package/lib/template-history.json +83 -0
  23. package/lib/typecheck.mjs +52 -3
  24. package/netplay/NETPLAY.md +123 -6
  25. package/netplay/netplay.ts +289 -53
  26. package/netplay/rules-output.mjs +85 -0
  27. package/package.json +3 -2
  28. package/rules/browser.ts +100 -0
  29. package/rules/core.ts +159 -58
  30. package/rules/guard.ts +11 -4
  31. package/rules/host.ts +90 -15
  32. package/rules/math.ts +16 -0
  33. package/rules/pack.ts +94 -17
  34. package/rules/rules.ts +21 -13
  35. package/rules/types.ts +63 -0
  36. package/rules/view.ts +36 -33
  37. package/standalone/shell/shell.js +8 -6
  38. package/worker/brain.mjs +2 -1
  39. package/worker/hosted.mjs +1 -1
  40. package/worker/index.mjs +7 -11
  41. package/worker/limits.mjs +41 -0
  42. package/worker/pages.mjs +4 -2
  43. package/worker/room.mjs +136 -63
  44. package/worker/stripe.mjs +2 -2
  45. package/worker/version.mjs +1 -1
@@ -1,10 +1,17 @@
1
- # Homie netplay contract, v1 (revision 10)
1
+ # Homie netplay contract, v1 (revision 11)
2
2
 
3
- Status: **v1, revision 10** (2026-10-07, `@homie-rocks/studio` 0.33.0). The wire
4
- version is `v: 1`. Everything revisions 2 to 10 added is either an optional field, a new message type,
3
+ Status: **v1, revision 11** (2026-10-08, `@homie-rocks/studio` 0.33.1). The wire
4
+ version is `v: 1`. Everything revisions 2 to 11 added is either an optional field, a new message type,
5
5
  a new refusal, or a change of pace inside the old caps, and both sides ignore types they do
6
6
  not know. A change to the contract bumps `v` and keeps v1 working.
7
7
 
8
+ **Revision 11** adds browser rules hosting and offline play (29.7): `held` occupants on
9
+ welcome/promotion, explicit `rules: true` on runtime output, `hosted` relay events,
10
+ `view` player events, `retime` clock-only round frames, terminal `rules-end`, and
11
+ `host` with `why: 'host-kept'` to a rules host whose yield elected nobody.
12
+ The hosting page's unmarked frames have ordinary player allowances. Old-style games
13
+ keep revision 10's character counts and rates.
14
+
8
15
  **What revision 10 added** (a game that knows none of it plays exactly as before, hosted by a player's
9
16
  browser; nothing here changes a frame such a game sends or receives):
10
17
  - **The server as host** (section 29): a game written as rules plus view (`src/rules.ts`, `src/move.ts`,
@@ -295,7 +302,7 @@ it.
295
302
  | The host closes or says `bye` | Sends `leave`, then elects a replacement. |
296
303
  | **Stall:** the host has sent no snapshot for **1.5 s** (the game's own `netplay.stallMs`, 1.5 to 10 s; section 22) while others are present | Demotes it (`role replica, why host-stalled`) and elects another. A host whose frames hitch sends a heartbeat meanwhile, for 4 s. |
297
304
  | **Frozen host at a newcomer's hello:** the host is hidden, or has sent nothing for 2.5 s | Makes the newcomer host at once. The old host gets `role replica`. |
298
- | **Hidden tab** | The helper sends a checkpoint, then `yield`. `ping {hid: true}` has the same effect. |
305
+ | **Hidden tab** | The helper sends a checkpoint, then `yield`. `ping {hid: true}` has the same effect. A rules host is answered when nobody could take the role (section 29.7). |
299
306
  | **Silent socket:** nothing received for **10 s** | Closes it (a slept phone, a half-open link). The seat's body turns into a bot. The helper pings every 2 s. |
300
307
 
301
308
  - **Ranking candidates:**
@@ -2247,8 +2254,8 @@ The state hash includes ordered declarations and handler names/presence, body/pl
2247
2254
  field options, shared state, shapes, bots, asks, map, dimensions, tick rate, rounds and contract.
2248
2255
  Handler implementations, tunables and view code do not change it. The build hash includes
2249
2256
  built rules, view, tunables and map, excluding build time, commit and other games. It replaces
2250
- `netplay.version` for rules games. Browser-hosted games keep the existing version and checkpoint
2251
- behaviour. An old standalone app must update before joining a rules game's live room.
2257
+ `netplay.version` for rules games in either hosting mode. Browser-hosted games use the
2258
+ checkpoint handover described below. An old standalone app must update before joining a rules game's live room.
2252
2259
 
2253
2260
  A restored run increments `boots.count` at its first tick and clears it after ten seconds of
2254
2261
  ticking. The second successive restored run waits a deterministic room-specific delay of up
@@ -2259,3 +2266,113 @@ After sixty seconds paused without a returning person, the room ends even if wat
2259
2266
  Ending deletes `save`, `boots` and `net`, preserves `office` and `recorded`, and keeps an `ended`
2260
2267
  marker so a late reconnect cannot reopen that room. The Lobby forgets it. The stored alarm
2261
2268
  also cleans up a room whose object restarted with nobody returning.
2269
+ ### 29.7 The same runtime in a browser and offline
2270
+
2271
+ `room.host: 'browser'` bundles the guarded host runtime and announces `rules: true` in
2272
+ `hello`. The relay elects a browser using the existing election and visibility yield.
2273
+ Only an elected host that announced rules receives step `in` frames (`e`, `k`, `s`, `r`);
2274
+ an older host receives the original input format. The relay supplies `from` from the socket's
2275
+ seat. It preserves `e` on a rules host's snapshots, and seat expiry sends `{ t: 'free', seat }`
2276
+ to a rules host. An old-style host is sent neither, and its `welcome` and `role` carry no
2277
+ `held`: nothing an old-style game sends or receives differs from revision 9. A page that
2278
+ announces rules with a revision below 11 is never elected (its output would be unmarked).
2279
+ Joins, AI identity and policy are the relay's facts, never fields copied from a player's input.
2280
+
2281
+ A rules checkpoint holds `{ rules: build, data: runtimeSave }`. Promotion validates it and
2282
+ restores one complete state through `createHost` and the single `createCore` save validator, with a fresh input epoch. `restoreEpoch` is the single replacement epoch option; `startPaused: false` lets browser promotion resume with the relay’s current occupants, while server recovery starts paused. A malformed or incompatible checkpoint
2283
+ starts a fresh round, says so, and immediately replaces the relay checkpoint. Abrupt loss
2284
+ can rewind to the last checkpoint; the replacement host republishes the round's wall-clock
2285
+ deadline from its restored tick. No newer display snapshot is mixed into the save.
2286
+
2287
+ An elected rules host may send `{ t: 'rules-end', rules: true, why }` when its runtime ends. The relay
2288
+ accepts this only from that host in a game the server catalogue identifies as rules, sends terminal `room-over` to everyone, closes sockets,
2289
+ and forgets the room and checkpoint. A later visitor starts fresh. Replicas cannot end it.
2290
+ `host-failed` and `room-over` are terminal helper errors; neither starts offline play or
2291
+ restores the failed state automatically.
2292
+
2293
+ Browser simulation uses `performance.now()`. The helper converts outgoing timestamps to
2294
+ the room's wall clock with rising snapshot stamps. Clock corrections republish the round deadline. A callback up to a tenth of a second late (two tick periods, if that is longer) is caught up, as on the server; a later one rebases the simulation clock without blaming rules, so a browser whose timers are that late runs slow and yields to a faster one.
2295
+ Hidden local hosts pause; visible hosts resume at the same tick. With another player present,
2296
+ the hidden host checkpoints and yields as before. Server isolate overrun enforcement is
2297
+ explicitly enabled only by the server adapter.
2298
+
2299
+ Server-hosted games with offline enabled fetch a separate rules entry after playable.
2300
+ Private games (`offline: false`) have no local rules import. Local play sends no snapshots,
2301
+ checkpoints, results or state to the relay. A dropped connection tries for five seconds
2302
+ before local play starts when rules are available; reconnecting announces the switch to
2303
+ the online round. Production bundles do not read Lab stage or tuning globals.
2304
+
2305
+ The browser relay and rules build share `worker/limits.mjs`. The rules view receives the
2306
+ same table without adding imports to vendored connection helpers. Rules sizes are UTF-8
2307
+ JSON bytes, including the runtime marker. Old-style games count characters, as before.
2308
+ For tick rate H and seat count S, the shared allowances are:
2309
+
2310
+ | Frame | Bytes through 16 seats | Bytes above 16 seats | Tokens/second |
2311
+ | --- | --- | --- | --- |
2312
+ | Snapshot | 16,384 | 16,384 | max(30, ceil(1.25 H)) |
2313
+ | Checkpoint | 65,536 | 131,072 | 4 |
2314
+ | Shared state | 8,192 | 8,192 | H + 64 |
2315
+ | Event, including effects | 4,096 | 4,096 | ceil((H + 10S + 32) / 0.8) |
2316
+ | Round | 8,192 | 16,384 | 8 |
2317
+ | Roster | 4,096 | 8,192 | 2S + 8 |
2318
+ | Decision | 6,144 | 6,144 | ceil((H + 5S + 8) / 0.8) |
2319
+ | Caps / rules-end | 512 | 512 | 4 |
2320
+
2321
+ The relay's token bucket holds two seconds of each allowance. For every kind but the
2322
+ snapshot the sender refills at 80% of that rate and holds one second of tokens, which
2323
+ leaves headroom for transport batching and late timers. A snapshot spends the relay's
2324
+ whole rate with two tokens in hand: a tick that runs on time sends its snapshot at once,
2325
+ so a browser host sends a snapshot a tick at any tick rate, as the server does, and the
2326
+ quarter above the tick rate absorbs the ticks a late timer runs together. Only snapshots
2327
+ coalesce. All other runtime output, including checkpoints and decisions, shares one
2328
+ ordered FIFO, and each frame leaves as soon as its own kind has a token: nothing reliable
2329
+ waits for a snapshot's.
2330
+ A snapshot over the relay allowance drops. Reliable output over it moves the rules to
2331
+ another player with `host-fault` explaining the change; that socket is not elected again
2332
+ (a page that reconnects is a newcomer, behind every other candidate). The
2333
+ room ends only if no eligible host remains. Player rate errors never end the room.
2334
+
2335
+ **Giving the role up is one ordered act.** A rules host that yields (its tab is hidden, it
2336
+ runs slow, the game calls `handOff()`) saves a checkpoint and puts `yield` behind it in the
2337
+ same FIFO, so the relay elects with the tick the room is on. From the yield on, the sender
2338
+ holds what the rules make until the relay answers: a `role` to somebody else discards it
2339
+ (the next host makes those ticks itself), and `{ t: 'host', host, why: 'host-kept' }`, sent
2340
+ to everyone in a rules room whose host's yield elected nobody, releases it in order. Two seconds without an answer
2341
+ release it too. `ping` does not report `hid: true` while a yield is still queued. A page
2342
+ that closes (`pagehide`, `close()`) sends what its allowances permit, in order, then its
2343
+ last checkpoint whatever the checkpoint allowance says (the relay's bucket holds twice the
2344
+ sender's), then `bye`; a frame it could not send is covered by that checkpoint.
2345
+
2346
+ **Everything the rules say goes one way.** The helper sends no `round`, `roster`, `state`,
2347
+ `caps`, `snap`, `ckpt` or `decide` of its own in a rules game. A host that reconnects and
2348
+ is still the host (the relay may have restarted with an old save or none) asks its runtime
2349
+ to announce: the round with its results, the roster, the shared state and the capabilities
2350
+ leave as marked output, then a checkpoint. A promoted host that restored a checkpoint says
2351
+ its shared state with its round, since the checkpoint may be older than what the relay kept.
2352
+ A modified host can already fabricate rules output; its marker adds no authority.
2353
+
2354
+ Effects share one frame per tick. Shared state also has a room total of 64 keys and
2355
+ 65,536 bytes. Build smoke checks measure every runtime kind; browser size excesses
2356
+ fail with a measured explanation, and server builds warn. These samples are not a
2357
+ proof of every future round. Oversized runtime frames remain terminal with the
2358
+ measurement on every page. Clock-only round retimings carry no results and do not
2359
+ fire another round event. Offline players retry at most 2.5 seconds apart, and the
2360
+ browser's `online` event retries immediately.
2361
+
2362
+ The view's guide helper never owns guides. Online commands, asks, inputs and speech go
2363
+ through the relay even on the page running the runtime. Runtime events return through the
2364
+ relay with a `hosted` mark (which a player's event cannot supply), and rosters and results
2365
+ return with the same AI labels all other views receive. A rules host's relayed speech keeps
2366
+ a live person's seat or null; older hosts retain the original sender stamping. Offline
2367
+ play uses the same runtime boundary locally and has bots, without online guides or AI.
2368
+
2369
+ A slow host reports its achieved speed. A replacement needs a recent event-loop measurement
2370
+ at least fifty percent faster plus ten percentage points, and any measured hosting speed
2371
+ must also pass that threshold. Two equally slow hosts keep one authority. Hidden tabs and
2372
+ lost hosts still use the existing election path. Welcome and promotion frames include held
2373
+ seat identities, without tokens, so an absent person's body stays theirs after handover.
2374
+
2375
+ A missing offline module leaves the connection helper alive and says the game needs a
2376
+ connection. A failed offline runtime stops locally while reconnection continues. An online
2377
+ runtime failure ends the room. Snapshots never grow above 16,384 bytes, including above
2378
+ sixteen seats.