@homie-rocks/studio 0.48.1 → 0.48.4
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.
- package/CHANGELOG.md +39 -0
- package/lib/build.mjs +1 -0
- package/lib/check.mjs +9 -5
- package/lib/rules-wiring.mjs +30 -0
- package/lib/scaffold.mjs +1 -1
- package/lib/template-history.json +84 -0
- package/netplay/NETPLAY.md +6 -1
- package/package.json +1 -1
- package/rules/view.ts +28 -2
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +28 -9
- package/worker/pages.mjs +4 -2
- package/worker/rules-wiring.mjs +2 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,45 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.48.4] - 2026-10-13
|
|
21
|
+
|
|
22
|
+
**Plugin 0.48.4** · Deploys go right.
|
|
23
|
+
|
|
24
|
+
Requested deploys proceed, visitors reach the current build, and paired screens keep their private room.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- An owner's task to run the studio's deploy authorizes it and its retries in Claude, Codex and Grok. Codex also recovers the original user request from its session transcript when a prompt event is absent; unrelated follow-ups do not erase it. Unrequested deploys still explain how to approve them.
|
|
29
|
+
- The Lobby retires rooms that the current hosting kind or server build cannot run before matching visitors, including after storage recovery and at a server's room allowance. Late reports cannot bring those rooms back.
|
|
30
|
+
- A TV and its paired phone keep their private room name through an incompatible build. The room starts its new match immediately, and page rematching preserves private room addresses instead of choosing quick play.
|
|
31
|
+
- Building the first server-rules game wires an untouched generated Worker or stops with the exact hostRules fix for a custom Worker. Live check reports that same fix before opening browsers instead of waiting for seats.
|
|
32
|
+
|
|
33
|
+
### Upgrade notes
|
|
34
|
+
|
|
35
|
+
Update the toolkit and plugin together. A custom site/src/worker.mjs must import the generated rules table and call hostRules at module scope; build prints the two lines when needed. No new studio limits or owner confirmation steps.
|
|
36
|
+
|
|
37
|
+
## [0.48.3] - 2026-10-13
|
|
38
|
+
|
|
39
|
+
**Plugin 0.48.3** · Restore rewind-based hits with input history.
|
|
40
|
+
|
|
41
|
+
Rules input history fits the wire without starving current aim and trigger controls.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **0.48.1 misjudged rewind-based hits.** Repeated input history and sub-tick
|
|
46
|
+
metadata could exceed the relay's 2 KB input limit, even with no latency or
|
|
47
|
+
loss. The server then kept stale aim and trigger state while the player's
|
|
48
|
+
predictor showed hits against the current aim. Input frames now fit that
|
|
49
|
+
existing byte limit, prioritizing current controls and missing receipts.
|
|
50
|
+
- Optional sub-tick metadata uses only the space left after complete input
|
|
51
|
+
values and their sequence numbers. Integrated held durations, reliable
|
|
52
|
+
pulses, measured round trips and the rewind cap keep their existing meaning.
|
|
53
|
+
Bounded history recovery, application receipts and correction blending remain.
|
|
54
|
+
|
|
55
|
+
### Upgrade notes
|
|
56
|
+
|
|
57
|
+
- Existing games need no code changes.
|
|
58
|
+
|
|
20
59
|
## [0.48.1] - 2026-10-13
|
|
21
60
|
|
|
22
61
|
**Plugin 0.48.1** · Input history under loss.
|
package/lib/build.mjs
CHANGED
|
@@ -510,6 +510,7 @@ export function orderGames(games, order, log = () => {}) {
|
|
|
510
510
|
export async function build(root, { only = null, log = () => {}, deploy = process.env.WORKERS_CI === '1', maps = false, types = false, beforePublish = async () => {}, longCheck = false } = {}) {
|
|
511
511
|
const esbuild = await studioEsbuild(root);
|
|
512
512
|
const studio = readStudio(root);
|
|
513
|
+
if (listGames(root).some(g => isRulesGame(g) && g.room?.host !== 'browser' && g.room?.host !== 'offline')) await (await import('./rules-wiring.mjs')).ensureRulesWiring(root);
|
|
513
514
|
await (await import('./tools-build.mjs')).buildTools(root, esbuild);
|
|
514
515
|
await (await import('./functions-build.mjs')).buildFunctions(root, esbuild);
|
|
515
516
|
// The shop first (shop/SHOP.md): invalid shop settings stop the build before anything is built.
|
package/lib/check.mjs
CHANGED
|
@@ -129,10 +129,6 @@ export function judgeRounds(browsers, missed = new Map()) {
|
|
|
129
129
|
|
|
130
130
|
export async function check({ url, game, roundTimeoutMs = 150_000, rounds = 3, shots = null, userAgentTag = 'homie-studio-check', log = () => {}, report = null }) {
|
|
131
131
|
if (!url || !game) throw new Error('usage: homie-studio check <game> --url <site url>');
|
|
132
|
-
const chrome = findChrome();
|
|
133
|
-
if (!chrome) return { ok: false, command: 'check', why: noChrome() };
|
|
134
|
-
let puppeteer;
|
|
135
|
-
try { puppeteer = (await import('puppeteer-core')).default; } catch { return { ok: false, command: 'check', why: 'puppeteer-core is not installed (it comes with @homie-rocks/studio; run npm install)' }; }
|
|
136
132
|
const base = String(url).replace(/\/+$/, '');
|
|
137
133
|
const play = `${base}/${game}/play`;
|
|
138
134
|
// One preflight from this process, in the same words as perf, shoot and port check (lib/net.mjs siteRefusal).
|
|
@@ -143,7 +139,15 @@ export async function check({ url, game, roundTimeoutMs = 150_000, rounds = 3, s
|
|
|
143
139
|
const cannotAsk = siteRefusal(await reachSite(base, { path: `/${game}/play`, timeout: 15_000 }), { command: 'check', play });
|
|
144
140
|
if (cannotAsk?.preflight === 'local') return cannotAsk;
|
|
145
141
|
let appMeta = null;
|
|
146
|
-
try {
|
|
142
|
+
try {
|
|
143
|
+
const meta = (await (await fetch(`${base}/api/games`, { signal: AbortSignal.timeout(5000) })).json()).games?.find(g => g.id === game);
|
|
144
|
+
if (meta?.rulesError) return { ok: false, command: 'check', why: meta.rulesError };
|
|
145
|
+
if (meta?.kind === 'app') appMeta = meta;
|
|
146
|
+
} catch { /* browser preflight below still names failures */ }
|
|
147
|
+
const chrome = findChrome();
|
|
148
|
+
if (!chrome) return { ok: false, command: 'check', why: noChrome() };
|
|
149
|
+
let puppeteer;
|
|
150
|
+
try { puppeteer = (await import('puppeteer-core')).default; } catch { return { ok: false, command: 'check', why: 'puppeteer-core is not installed (it comes with @homie-rocks/studio; run npm install)' }; }
|
|
147
151
|
if (appMeta) return checkApp({ url: base, game, meta: appMeta, shots, log, report, puppeteer, chrome });
|
|
148
152
|
if (shots) mkdirSync(shots, { recursive: true });
|
|
149
153
|
const profiles = [];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import { RULES_WIRING_MESSAGE } from '../worker/rules-wiring.mjs';
|
|
5
|
+
|
|
6
|
+
/** Check executable module statements, never a comment or a function nobody calls. */
|
|
7
|
+
export function registersRules(text) {
|
|
8
|
+
const body = parse(text, { sourceType: 'module' }).program.body;
|
|
9
|
+
const hosts = new Set(), tables = new Set();
|
|
10
|
+
for (const node of body) if (node.type === 'ImportDeclaration') {
|
|
11
|
+
for (const s of node.specifiers) {
|
|
12
|
+
if (/^@homie-rocks\/studio\/worker(?:\/selling)?$/.test(node.source.value) && s.imported?.name === 'hostRules') hosts.add(s.local.name);
|
|
13
|
+
if (node.source.value === './rules/index.mjs' && s.type === 'ImportDefaultSpecifier') tables.add(s.local.name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return body.some(n => n.type === 'ExpressionStatement' && n.expression.type === 'CallExpression' && hosts.has(n.expression.callee.name) && tables.has(n.expression.arguments[0]?.name));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureRulesWiring(root) {
|
|
20
|
+
const file = join(root, 'site/src/worker.mjs');
|
|
21
|
+
let text;
|
|
22
|
+
try { text = readFileSync(file, 'utf8'); } catch { throw new Error(RULES_WIRING_MESSAGE); }
|
|
23
|
+
if (registersRules(text)) return;
|
|
24
|
+
// Upgrade already knows every exact generated Worker fingerprint. Reuse that
|
|
25
|
+
// ownership check; a comment claiming to be generated is not ownership.
|
|
26
|
+
const { upgradePlan } = await import('./upgrade.mjs');
|
|
27
|
+
const update = upgradePlan(root).changes?.find(c => c.file === 'site/src/worker.mjs' && c.kind === 'update-file');
|
|
28
|
+
if (update && registersRules(update.text)) { writeFileSync(file, update.text); return; }
|
|
29
|
+
throw new Error(RULES_WIRING_MESSAGE);
|
|
30
|
+
}
|
package/lib/scaffold.mjs
CHANGED
|
@@ -555,7 +555,7 @@ chat's card follows the work from then on) and, for a studio still being set up,
|
|
|
555
555
|
the budget until the person says Proceed, and takes keys out of command output. In Codex the plugin's hooks hold
|
|
556
556
|
unrequested deploys and the other held calls until the person's own \`proceed <code>\`, once the person has trusted them, and in Grok Build once
|
|
557
557
|
the plugin is trusted. \`npx --no-install homie-studio setup status --client codex\` (or \`grok\`) says whether Homie's
|
|
558
|
-
holds are on. A deploy the person requested in their own words is already approved for that task; do not ask again or demand a typed code. Stop an unrequested deploy and ask once
|
|
558
|
+
holds are on. A deploy the person requested in their own words is already approved for that task; do not ask again or demand a typed code. The original task also counts in non-interactive sessions, including a prompt piped into codex exec. Stop an unrequested deploy and ask once: the person can say “Please deploy this studio.” When a call is refused, say what you
|
|
559
559
|
meant to do and why; do not retry it unless the person asks.
|
|
560
560
|
|
|
561
561
|
## Telling Homie
|
|
@@ -3027,6 +3027,90 @@
|
|
|
3027
3027
|
}
|
|
3028
3028
|
}
|
|
3029
3029
|
}
|
|
3030
|
+
},
|
|
3031
|
+
"0.48.3": {
|
|
3032
|
+
"files": {
|
|
3033
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3034
|
+
"README.md": "684b3cca8001673c",
|
|
3035
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3036
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3037
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3038
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3039
|
+
"posts/README.md": "37841aae476bd540",
|
|
3040
|
+
"site/README.md": "732894273a32dc3f",
|
|
3041
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3042
|
+
},
|
|
3043
|
+
"sections": {
|
|
3044
|
+
"AGENTS.md": {
|
|
3045
|
+
"(intro)": "394cbbe934e80722",
|
|
3046
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3047
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3048
|
+
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
3049
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
3050
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3051
|
+
"## Making games": "bb715272fd1a615b",
|
|
3052
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3053
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3054
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3055
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3056
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
3057
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3058
|
+
"## Rules": "27881d11d3ff3803",
|
|
3059
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3060
|
+
"## Beta": "9d02638207a26e73"
|
|
3061
|
+
}
|
|
3062
|
+
},
|
|
3063
|
+
"named": {
|
|
3064
|
+
"Homie Arcade": {
|
|
3065
|
+
"sections": {
|
|
3066
|
+
"AGENTS.md": {
|
|
3067
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
},
|
|
3073
|
+
"0.48.4": {
|
|
3074
|
+
"files": {
|
|
3075
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
3076
|
+
"README.md": "684b3cca8001673c",
|
|
3077
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
3078
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
3079
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
3080
|
+
"videos/README.md": "16babc3437d5545b",
|
|
3081
|
+
"posts/README.md": "37841aae476bd540",
|
|
3082
|
+
"site/README.md": "732894273a32dc3f",
|
|
3083
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
3084
|
+
},
|
|
3085
|
+
"sections": {
|
|
3086
|
+
"AGENTS.md": {
|
|
3087
|
+
"(intro)": "394cbbe934e80722",
|
|
3088
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
3089
|
+
"## Layout": "f3a280b8c96cd068",
|
|
3090
|
+
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
3091
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
3092
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
3093
|
+
"## Making games": "bb715272fd1a615b",
|
|
3094
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
3095
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
3096
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
3097
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
3098
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
3099
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
3100
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
3101
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
3102
|
+
"## Beta": "9d02638207a26e73"
|
|
3103
|
+
}
|
|
3104
|
+
},
|
|
3105
|
+
"named": {
|
|
3106
|
+
"Homie Arcade": {
|
|
3107
|
+
"sections": {
|
|
3108
|
+
"AGENTS.md": {
|
|
3109
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3030
3114
|
}
|
|
3031
3115
|
}
|
|
3032
3116
|
}
|
package/netplay/NETPLAY.md
CHANGED
|
@@ -2336,7 +2336,12 @@ A rules step keeps its `[tickOffset, ...fields, ...ownerClaim]` prefix. Current
|
|
|
2336
2336
|
views append `[-471, sequence, changeCount, ...changes]`, where each change is
|
|
2337
2337
|
`[fieldIndex, changeSequence, fractionOfTick, value]`. All components are numbers.
|
|
2338
2338
|
Input frames repeat recent entries, sorted by tick; the retained window is
|
|
2339
|
-
4–64 ticks based on measured authority RTT and jitter.
|
|
2339
|
+
4–64 ticks based on measured authority RTT and jitter. Each frame also fits the
|
|
2340
|
+
existing 2 KB input cap: current controls first, then missing receipts, then
|
|
2341
|
+
acknowledged history. Optional sub-tick metadata uses the remaining bytes; the
|
|
2342
|
+
complete row retains its integrated held values and sequence even when the
|
|
2343
|
+
metadata is omitted. Retained entries not selected for this frame remain
|
|
2344
|
+
available for subsequent recovery frames. The host deduplicates
|
|
2340
2345
|
entry sequences, including history received after a missing frame. Duplicate
|
|
2341
2346
|
history is not a fresh lead observation. Frames without the trailer keep the
|
|
2342
2347
|
legacy input semantics.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.4",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/rules/view.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { RayQueries, RayOptions } from './types.ts';
|
|
|
6
6
|
import { snapshotDecoder } from './interest.mjs';
|
|
7
7
|
import { collisionMap, collisionTargets, revisionAt, type CollisionRevision } from './live.ts';
|
|
8
8
|
import { rulesOutput } from '../netplay/rules-output.mjs';
|
|
9
|
-
import { rulesCaps, rulesRates } from '../worker/limits.mjs';
|
|
9
|
+
import { LIMITS, rulesCaps, rulesRates } from '../worker/limits.mjs';
|
|
10
10
|
/*
|
|
11
11
|
* view.ts — what a rules game's view imports: the room, as a browser sees it.
|
|
12
12
|
* =============================================================================
|
|
@@ -608,7 +608,33 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
608
608
|
const windowTicks = Math.min(64, Math.max(4, recoveryTicks, Math.ceil((2 * authorityRtt() + 250) / period + targetLead())));
|
|
609
609
|
sentHistory = sentHistory.filter(e => e.t >= t - windowTicks).slice(-64);
|
|
610
610
|
const first = Math.min(k, sentHistory[0]?.t ?? k);
|
|
611
|
-
|
|
611
|
+
// Repeated history must fit the relay's existing byte cap, including its
|
|
612
|
+
// envelope and snapshot acknowledgement. Always carry the newest controls;
|
|
613
|
+
// spend the remaining space on missing receipts before acknowledged history.
|
|
614
|
+
// Sub-tick samples are optional metadata: the row already contains the held
|
|
615
|
+
// duration integrated by heldMean. Never truncate its values or sequence.
|
|
616
|
+
const budget = LIMITS.in - 128;
|
|
617
|
+
const selected = new Map<number, StepEntry>();
|
|
618
|
+
let bytes = 2;
|
|
619
|
+
const newest = sentHistory.at(-1);
|
|
620
|
+
const candidates = [...(newest ? [newest] : []),
|
|
621
|
+
...sentHistory.filter(e => e !== newest && !applications.has(e.row[marker + 1])),
|
|
622
|
+
...sentHistory.filter(e => e !== newest && applications.has(e.row[marker + 1])).reverse()];
|
|
623
|
+
for (const e of candidates) {
|
|
624
|
+
const row = [e.t - first, ...e.row.slice(0, marker + 2), 0];
|
|
625
|
+
const size = JSON.stringify(row).length + (selected.size ? 1 : 0);
|
|
626
|
+
if (bytes + size > budget) continue;
|
|
627
|
+
selected.set(e.t, row); bytes += size;
|
|
628
|
+
}
|
|
629
|
+
// Use spare bytes for sample metadata only after reserving control history.
|
|
630
|
+
for (const e of candidates) {
|
|
631
|
+
const compact = selected.get(e.t);
|
|
632
|
+
if (!compact) continue;
|
|
633
|
+
const row = [e.t - first, ...e.row];
|
|
634
|
+
const extra = JSON.stringify(row).length - JSON.stringify(compact).length;
|
|
635
|
+
if (bytes + extra <= budget) { selected.set(e.t, row); bytes += extra; }
|
|
636
|
+
}
|
|
637
|
+
const rows = [...selected].sort((a, b) => a[0] - b[0]).map(([, row]) => row);
|
|
612
638
|
entries = [];
|
|
613
639
|
if (net.steps(epoch, first, rows, mine.r)) lastSendAt = now;
|
|
614
640
|
}
|
package/worker/build-id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
|
|
2
|
-
export const STUDIO_BUILD = Object.freeze({"version":"0.48.
|
|
2
|
+
export const STUDIO_BUILD = Object.freeze({"version":"0.48.4","sha256":"5a637b57c0a41bb68ece5691250f4f8e7f646d489608a0a2721f571053602b2f"});
|
package/worker/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RULES_WIRING_MESSAGE } from './rules-wiring.mjs';
|
|
1
2
|
import { characterStore, characterRequest } from './characters.mjs';
|
|
2
3
|
export { useFunctions } from './functions.mjs';
|
|
3
4
|
import { appAccess, appRecordsRoute } from './app-records.mjs';
|
|
@@ -763,7 +764,7 @@ async function route(request, env, ctx) {
|
|
|
763
764
|
}
|
|
764
765
|
if (path === '/api/games') {
|
|
765
766
|
const cat = await getCat();
|
|
766
|
-
return json({ studio: cat.studio, games: cat.games, live: await liveCounts(env, cat.games ?? []) });
|
|
767
|
+
return json({ studio: cat.studio, games: (cat.games ?? []).map(g => g.room?.host === 'server' && !hostedGame(g.id) ? { ...g, rulesError: RULES_WIRING_MESSAGE } : g), live: await liveCounts(env, cat.games ?? []) });
|
|
767
768
|
}
|
|
768
769
|
if (path === '/api/rooms') {
|
|
769
770
|
const cat = await getCat();
|
|
@@ -1051,7 +1052,7 @@ async function route(request, env, ctx) {
|
|
|
1051
1052
|
// The big screen picks its room now, so the QR it shows puts every phone in that same room.
|
|
1052
1053
|
let room = asked;
|
|
1053
1054
|
if (!room) {
|
|
1054
|
-
try { room = (await (await lobby().fetch(`https://lobby/join?max=${humanSeats(pol)}&server=${srv.id}&rooms=${srv.roomsMax}${versionOf(meta?.netplay?.version) ? `&ver=${encodeURIComponent(versionOf(meta.netplay.version))}` : ''}`, { method: 'POST' })).json()).room ?? null; } catch { room = null; }
|
|
1055
|
+
try { room = (await (await lobby().fetch(`https://lobby/join?host=${meta?.room?.host === 'server' ? 'server' : 'browser'}&max=${humanSeats(pol)}&server=${srv.id}&rooms=${srv.roomsMax}${versionOf(meta?.netplay?.version) ? `&ver=${encodeURIComponent(versionOf(meta.netplay.version))}` : ''}`, { method: 'POST' })).json()).room ?? null; } catch { room = null; }
|
|
1055
1056
|
}
|
|
1056
1057
|
const joinUrl = `${url.origin}${openPath(meta)}${room ? `?room=${encodeURIComponent(room)}` : ''}`;
|
|
1057
1058
|
// Local development: no phone can open this computer's own address, so no code for it; the card says to deploy.
|
|
@@ -1124,7 +1125,7 @@ async function route(request, env, ctx) {
|
|
|
1124
1125
|
if (not.split(',').includes(room)) return json({ ok: false, error: 'world-unavailable', message: 'This world is unavailable. Choose another named world.' }, 409);
|
|
1125
1126
|
return json({ room, players: 0, max: humanSeats(pol), server: srv.id });
|
|
1126
1127
|
}
|
|
1127
|
-
return lobby().fetch(`https://lobby/join?max=${humanSeats(pol)}&server=${srv.id}&rooms=${srv.roomsMax}${not ? `¬=${encodeURIComponent(not)}` : ''}${ver ? `&ver=${encodeURIComponent(ver)}` : ''}${app ? '&app=1' : ''}`, { method: 'POST' });
|
|
1128
|
+
return lobby().fetch(`https://lobby/join?host=${meta?.room?.host === 'server' ? 'server' : 'browser'}&max=${humanSeats(pol)}&server=${srv.id}&rooms=${srv.roomsMax}${not ? `¬=${encodeURIComponent(not)}` : ''}${ver ? `&ver=${encodeURIComponent(ver)}` : ''}${app ? '&app=1' : ''}`, { method: 'POST' });
|
|
1128
1129
|
}
|
|
1129
1130
|
if (sub === 'api/agent') {
|
|
1130
1131
|
// An AI's seat (worker/agents.mjs): its pass, the server's policy, a room with people in it, a ticket.
|
|
@@ -1136,7 +1137,7 @@ async function route(request, env, ctx) {
|
|
|
1136
1137
|
join: async (srv, pol) => {
|
|
1137
1138
|
try {
|
|
1138
1139
|
const reserve = pol.aiSeats + pol.guides;
|
|
1139
|
-
const r = await (await lobby().fetch(`https://lobby/join?max=${max}&server=${srv.id}&agent=1&ai=${reserve}${versionOf(meta?.netplay?.version) ? `&ver=${encodeURIComponent(versionOf(meta.netplay.version))}` : ''}`, { method: 'POST' })).json();
|
|
1140
|
+
const r = await (await lobby().fetch(`https://lobby/join?host=${meta?.room?.host === 'server' ? 'server' : 'browser'}&max=${max}&server=${srv.id}&agent=1&ai=${reserve}${versionOf(meta?.netplay?.version) ? `&ver=${encodeURIComponent(versionOf(meta.netplay.version))}` : ''}`, { method: 'POST' })).json();
|
|
1140
1141
|
return r.room ?? null;
|
|
1141
1142
|
} catch { return null; }
|
|
1142
1143
|
},
|
|
@@ -1429,7 +1430,7 @@ export class Table {
|
|
|
1429
1430
|
ensureHost(room, hosted) {
|
|
1430
1431
|
if (this.hostRt || !this.game) return;
|
|
1431
1432
|
if (!hostedGame(this.game)) {
|
|
1432
|
-
if (hosted) room.failServerHost(
|
|
1433
|
+
if (hosted) room.failServerHost(RULES_WIRING_MESSAGE);
|
|
1433
1434
|
return;
|
|
1434
1435
|
}
|
|
1435
1436
|
let restoring = false;
|
|
@@ -1461,7 +1462,7 @@ export class Table {
|
|
|
1461
1462
|
this.finishRoom(why);
|
|
1462
1463
|
room.forget(); room.ended = null;
|
|
1463
1464
|
// Only a holder returning to the old match is rematched. A new visitor uses this name immediately.
|
|
1464
|
-
if (!expired) room.endedMatch = { why, ver: build.build, tokens, until: Date.now() + ROOM_PAUSE_MS };
|
|
1465
|
+
if (!expired && pooledRoom(this.code)) room.endedMatch = { why, ver: build.build, tokens, until: Date.now() + ROOM_PAUSE_MS };
|
|
1465
1466
|
this.room = room; saved = null; restoring = false; boot = { epoch: 0, count: 0 };
|
|
1466
1467
|
}
|
|
1467
1468
|
this.roomSave = saved;
|
|
@@ -2469,7 +2470,7 @@ export class Table {
|
|
|
2469
2470
|
const lobby = this.env.LOBBY.get(this.env.LOBBY.idFromName(this.game));
|
|
2470
2471
|
// The build this room runs (section 23): the Lobby sends a visitor only to a room of the live build.
|
|
2471
2472
|
const ver = room.gameVer === undefined ? undefined : room.gameVer ?? '';
|
|
2472
|
-
this.ctx.waitUntil(lobby.fetch(`https://lobby/report?game=${encodeURIComponent(this.game)}`, { method: 'POST', body: JSON.stringify({ room: this.code, players, people, agents, ai, ...(ver !== undefined ? { ver } : {}) }) }).catch(() => {}));
|
|
2473
|
+
this.ctx.waitUntil(lobby.fetch(`https://lobby/report?game=${encodeURIComponent(this.game)}`, { method: 'POST', body: JSON.stringify({ room: this.code, players, people, agents, ai, host: room.server ? 'server' : 'browser', ...(ver !== undefined ? { ver } : {}) }) }).catch(() => {}));
|
|
2473
2474
|
}
|
|
2474
2475
|
|
|
2475
2476
|
/** A round the host called over becomes one D1 row (once per round number). */
|
|
@@ -2523,6 +2524,7 @@ export class Lobby {
|
|
|
2523
2524
|
ctx.blockConcurrencyWhile(async () => {
|
|
2524
2525
|
const saved = await ctx.storage.get('lobby');
|
|
2525
2526
|
if (saved) {
|
|
2527
|
+
this.current = saved.current ?? null;
|
|
2526
2528
|
this.next = typeof saved.next === 'number' ? { public: saved.next } : saved.next && typeof saved.next === 'object' ? { public: 1, ...saved.next } : { public: 1 };
|
|
2527
2529
|
for (const r of saved.rooms ?? []) this.rooms.set(r.name, { ...r, server: r.server ?? roomServer(r.name), agents: r.agents ?? 0, ai: r.ai ?? 0, ver: typeof r.ver === 'string' ? r.ver : '', pending: [], agentPending: [] });
|
|
2528
2530
|
for (const [name, until] of saved.closed ?? []) if (until > Date.now()) this.closed.set(name, until);
|
|
@@ -2550,7 +2552,7 @@ export class Lobby {
|
|
|
2550
2552
|
if (list.length) this.ctx.waitUntil(this.env.DB.batch(list).catch(() => {}));
|
|
2551
2553
|
}
|
|
2552
2554
|
|
|
2553
|
-
save() { this.ctx.storage.put('lobby', { next: this.next, rooms: [...this.rooms.values()].map(({ name, players, agents, ai, server, at, ver }) => ({ name, players, agents, ai, server, at, ...(ver ? { ver } : {}) })), closed: [...this.closed] }).catch(() => {}); }
|
|
2555
|
+
save() { this.ctx.storage.put('lobby', { current: this.current ?? null, next: this.next, rooms: [...this.rooms.values()].map(({ name, players, agents, ai, server, at, ver, host }) => ({ name, players, agents, ai, server, at, ...(host ? { host } : {}), ...(ver ? { ver } : {}) })), closed: [...this.closed] }).catch(() => {}); }
|
|
2554
2556
|
|
|
2555
2557
|
prune(now) {
|
|
2556
2558
|
for (const [name, r] of this.rooms) {
|
|
@@ -2578,6 +2580,17 @@ export class Lobby {
|
|
|
2578
2580
|
// the build of a game that names none ('').
|
|
2579
2581
|
const ver = versionOf(url.searchParams.get('ver')) ?? '';
|
|
2580
2582
|
if (url.pathname === '/join') {
|
|
2583
|
+
// A server build can run only its current rules. Persisted browser rooms
|
|
2584
|
+
// (including pre-kind records) and old server revisions must not consume
|
|
2585
|
+
// the pool or become the fallback of a server with roomsMax=1.
|
|
2586
|
+
const host = url.searchParams.get('host');
|
|
2587
|
+
if ((host === 'server' || host === 'browser') && (this.current?.host !== host || this.current?.ver !== ver)) {
|
|
2588
|
+
this.current = { host, ver };
|
|
2589
|
+
for (const [name, r] of this.rooms) if (
|
|
2590
|
+
host === 'server' ? (r.host ?? 'browser') !== host || (r.ver ?? '') !== ver : r.host === 'server'
|
|
2591
|
+
) { this.rooms.delete(name); this.seen.delete(name); this.live.delete(name); }
|
|
2592
|
+
this.save();
|
|
2593
|
+
}
|
|
2581
2594
|
const max = seatCount(url.searchParams.get('max'));
|
|
2582
2595
|
const sid = url.searchParams.get('server') ?? 'public';
|
|
2583
2596
|
const server = sid === 'public' || SERVER_ID.test(sid) ? sid : 'public';
|
|
@@ -2640,7 +2653,7 @@ export class Lobby {
|
|
|
2640
2653
|
if (!room) {
|
|
2641
2654
|
let n = this.next[server] ?? 1;
|
|
2642
2655
|
while (not.has(roomCode(server, n)) || this.closed.has(roomCode(server, n)) || this.rooms.has(roomCode(server, n))) n += 1;
|
|
2643
|
-
room = { name: roomCode(server, n), server, players: 0, agents: 0, ai: 0, at: now, ver, pending: [], agentPending: [] };
|
|
2656
|
+
room = { name: roomCode(server, n), server, players: 0, agents: 0, ai: 0, at: now, ver, ...(host ? { host } : {}), pending: [], agentPending: [] };
|
|
2644
2657
|
this.next[server] = n + 1;
|
|
2645
2658
|
this.rooms.set(room.name, room);
|
|
2646
2659
|
}
|
|
@@ -2659,6 +2672,11 @@ export class Lobby {
|
|
|
2659
2672
|
this.save();
|
|
2660
2673
|
return json({ ok: true, closed: body.closed > now });
|
|
2661
2674
|
}
|
|
2675
|
+
// A retiring isolate may send its last report after the new pool was
|
|
2676
|
+
// selected. It cannot resurrect an incompatible public room.
|
|
2677
|
+
if (pooledRoom(body.room) && this.current && (this.current.host === 'server'
|
|
2678
|
+
? (body.host ?? 'browser') !== 'server' || (body.ver ?? '') !== this.current.ver
|
|
2679
|
+
: body.host === 'server')) return json({ ok: true, retired: true });
|
|
2662
2680
|
const seated = Math.max(0, Math.floor(Number(body.players) || 0));
|
|
2663
2681
|
const agents = Math.max(0, Math.floor(Number(body.agents) || 0));
|
|
2664
2682
|
const ai = Math.max(agents, Math.floor(Number(body.ai) || 0));
|
|
@@ -2674,6 +2692,7 @@ export class Lobby {
|
|
|
2674
2692
|
const rver = typeof body.ver === 'string' ? versionOf(body.ver) ?? '' : r.ver ?? '';
|
|
2675
2693
|
const changed = !this.rooms.has(r.name) || r.players !== seated || (r.agents ?? 0) !== agents || (r.ai ?? 0) !== ai || (r.ver ?? '') !== rver;
|
|
2676
2694
|
r.ver = rver;
|
|
2695
|
+
if (body.host === 'server' || body.host === 'browser') r.host = body.host;
|
|
2677
2696
|
r.pending.splice(0, grew);
|
|
2678
2697
|
if ((r.agents ?? 0) < agents) (r.agentPending ?? []).splice(0, agents - (r.agents ?? 0));
|
|
2679
2698
|
r.players = seated;
|
package/worker/pages.mjs
CHANGED
|
@@ -597,7 +597,8 @@ const SHELL_JS = String.raw`${BUILD_RETRY_JS}(function () {
|
|
|
597
597
|
// playing in its own room: it loads again at the round's break, or at once on a big screen (nothing to lose).
|
|
598
598
|
if (m.what === 'rematch' && m.room === room) {
|
|
599
599
|
var fresh = new URL(location.href);
|
|
600
|
-
fresh.searchParams.delete('room'); fresh.searchParams.set('not', room);
|
|
600
|
+
if (/^(?:pub-[1-9][0-9]*|s-[a-z0-9][a-z0-9-]*-[1-9][0-9]*)$/.test(room)) { fresh.searchParams.delete('room'); fresh.searchParams.set('not', room); }
|
|
601
|
+
else { fresh.searchParams.set('room', room); fresh.searchParams.delete('not'); }
|
|
601
602
|
location.replace(fresh.href);
|
|
602
603
|
return;
|
|
603
604
|
}
|
|
@@ -1357,7 +1358,8 @@ const WATCH_JS = String.raw`${BUILD_RETRY_JS}(function () {
|
|
|
1357
1358
|
var m = ev.data;
|
|
1358
1359
|
if (!m || typeof m !== 'object' || m.t !== 'homie-net') return;
|
|
1359
1360
|
if (m.what === 'rematch' && m.room === room) {
|
|
1360
|
-
var fresh = new URL(location.href); fresh.searchParams.delete('room'); fresh.searchParams.set('not', room);
|
|
1361
|
+
var fresh = new URL(location.href); if (/^(?:pub-[1-9][0-9]*|s-[a-z0-9][a-z0-9-]*-[1-9][0-9]*)$/.test(room)) { fresh.searchParams.delete('room'); fresh.searchParams.set('not', room); }
|
|
1362
|
+
else { fresh.searchParams.set('room', room); fresh.searchParams.delete('not'); }
|
|
1361
1363
|
location.replace(fresh.href); return;
|
|
1362
1364
|
}
|
|
1363
1365
|
if (m.what === 'wait') updating.stop();
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Shared build/live diagnosis: the studio's Worker must register its generated rules. */
|
|
2
|
+
export const RULES_WIRING_MESSAGE = "site/src/worker.mjs does not register this studio's server rules. Add hostRules to its import from '@homie-rocks/studio/worker', then add these two lines at module scope:\nimport rules from './rules/index.mjs';\nhostRules(rules);\nBuild and deploy again.";
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.48.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.48.4';
|