@homie-rocks/studio 0.40.0 → 0.43.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.
- package/CHANGELOG.md +54 -1
- package/README.md +32 -9
- package/app-starters/welcome/app.json +8 -1
- package/app-starters/welcome/map/main.json +1 -0
- package/app-starters/welcome/src/move.ts +3 -0
- package/app-starters/welcome/src/rules.ts +21 -0
- package/app-starters/welcome/src/{main.ts → view.ts} +8 -6
- package/bin/homie-studio.mjs +11 -3
- package/functions/FUNCTIONS.md +99 -0
- package/functions/functions.ts +13 -0
- package/lib/build.mjs +32 -12
- package/lib/cloudflare.mjs +9 -9
- package/lib/codex.mjs +1 -1
- package/lib/dev.mjs +2 -1
- package/lib/functions-build.mjs +36 -0
- package/lib/functions-cli.mjs +23 -0
- package/lib/mcp-tools.mjs +10 -7
- package/lib/mcp.mjs +2 -1
- package/lib/port-check.mjs +7 -4
- package/lib/port.mjs +2 -2
- package/lib/purchase-links.mjs +8 -0
- package/lib/rules-build.mjs +6 -6
- package/lib/rules-guard.mjs +9 -3
- package/lib/scaffold.mjs +58 -15
- package/lib/shop.mjs +1 -1
- package/lib/studio.mjs +3 -3
- package/lib/template-history.json +148 -1
- package/lib/tools-build.mjs +2 -0
- package/lib/trailer.mjs +7 -3
- package/lib/typecheck.mjs +10 -8
- package/lib/upgrade.mjs +2 -0
- package/lib/worker-build.mjs +8 -20
- package/lib/worker-config.mjs +28 -0
- package/netplay/NETPLAY.md +8 -0
- package/package.json +7 -2
- package/rules/host.ts +2 -2
- package/rules/view.ts +1 -0
- package/shop/SHOP.md +4 -3
- package/starters/coin-dash/src/move.ts +2 -0
- package/starters/coin-dash/src/rules.ts +2 -0
- package/starters/coin-dash/src/view.ts +2 -0
- package/starters/ember-vale/src/move.ts +2 -0
- package/starters/ember-vale/src/rules.ts +2 -0
- package/starters/ember-vale/src/view.ts +2 -0
- package/starters/gem-rush/src/move.ts +2 -0
- package/starters/gem-rush/src/rules.ts +2 -0
- package/starters/gem-rush/src/view.ts +2 -0
- package/starters/gem-rush-3d/src/move.ts +2 -0
- package/starters/gem-rush-3d/src/rules.ts +2 -0
- package/starters/gem-rush-3d/src/view.ts +2 -0
- package/starters/hero-rush-3d/src/move.ts +2 -0
- package/starters/hero-rush-3d/src/rules.ts +2 -0
- package/starters/hero-rush-3d/src/view.ts +2 -0
- package/tools/TOOLS.md +42 -2
- package/tools/tools.ts +3 -0
- package/worker/customer-mcp.mjs +77 -0
- package/worker/customer-resources.mjs +53 -0
- package/worker/customer-shop.mjs +54 -0
- package/worker/extensions.mjs +1 -0
- package/worker/functions-schema.mjs +55 -0
- package/worker/functions.mjs +93 -0
- package/worker/index.mjs +37 -2
- package/worker/machine-payments.mjs +1 -1
- package/worker/mcp-store.mjs +3 -2
- package/worker/mcp-tools.mjs +16 -4
- package/worker/mcp.mjs +31 -15
- package/worker/office-page.mjs +1 -1
- package/worker/players.mjs +5 -2
- package/worker/purchase-delivery.mjs +1 -0
- package/worker/purchase-hosted-readiness.mjs +0 -1
- package/worker/purchase-links.mjs +1 -1
- package/worker/purchase-machine.mjs +1 -0
- package/worker/purchase-office.mjs +1 -1
- package/worker/purchase-owner.mjs +0 -2
- package/worker/purchase-reconciliation.mjs +1 -0
- package/worker/purchase-routes.mjs +11 -17
- package/worker/purchase-store.mjs +1 -0
- package/worker/selling/runtime.mjs +4 -1
- package/worker/service-run.mjs +25 -0
- package/worker/service-terms.mjs +2 -0
- package/worker/shop-rules.mjs +9 -8
- package/worker/shop.mjs +22 -9
- package/worker/tool-webhook.mjs +1 -0
- package/worker/version.mjs +1 -1
package/lib/studio.mjs
CHANGED
|
@@ -108,7 +108,7 @@ function listKind(root, kind) {
|
|
|
108
108
|
/**
|
|
109
109
|
* RULES ON THE SERVER (NETPLAY.md section 29). A game written as rules plus view says so in game.json with a `"room"`
|
|
110
110
|
* object (where its rules run, its tick rate…; rules/rules.ts `roomSettings` checks them at build) and has a
|
|
111
|
-
* src/rules.ts.
|
|
111
|
+
* src/rules.ts. Other games need the browser-hosted netplay contract checked by the build. The file alone decides nothing: games made
|
|
112
112
|
* before this had their own src/rules.ts, with whatever they liked in it, and they build exactly as they did.
|
|
113
113
|
*/
|
|
114
114
|
export const isRulesGame = (g) => g?.room != null && typeof g.room === 'object' && existsSync(join(g.dir, 'src', 'rules.ts'));
|
|
@@ -129,7 +129,7 @@ export function starters() {
|
|
|
129
129
|
const dir = join(PACKAGE_ROOT, 'starters');
|
|
130
130
|
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
131
131
|
const meta = JSON.parse(readFileSync(join(dir, d.name, 'game.json'), 'utf8'));
|
|
132
|
-
// `example`:
|
|
132
|
+
// `example`: marks the smallest reference (coin-dash); all starters use server rules and can start a new game.
|
|
133
133
|
return { id: d.name, name: meta.name, blurb: meta.blurb, players: meta.players, roundSeconds: meta.roundSeconds, ...(meta.example === true ? { example: true } : {}) };
|
|
134
134
|
});
|
|
135
135
|
}
|
|
@@ -309,7 +309,7 @@ export async function newApp(root, id, { name } = {}) {
|
|
|
309
309
|
const meta = JSON.parse(readFileSync(file, 'utf8'));
|
|
310
310
|
meta.id = id; meta.name = String(name ?? 'Welcome together').slice(0, 60);
|
|
311
311
|
writeFileSync(file, `${JSON.stringify(meta, null, 2)}\n`);
|
|
312
|
-
const main = join(dest, 'src/main.ts');
|
|
312
|
+
const main = join(dest, meta.entry ?? 'src/main.ts');
|
|
313
313
|
writeFileSync(main, readFileSync(main, 'utf8').replaceAll("game: 'welcome'", `game: '${id}'`));
|
|
314
314
|
return { ok: true, command: 'app new', id, dir: dest, files: readdirSync(dest, { recursive: true }).map(String), ...addNeeds(root, meta.needs) };
|
|
315
315
|
}
|
|
@@ -2199,6 +2199,68 @@
|
|
|
2199
2199
|
}
|
|
2200
2200
|
},
|
|
2201
2201
|
"0.40.0": {
|
|
2202
|
+
"files": {
|
|
2203
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2204
|
+
"README.md": "684b3cca8001673c",
|
|
2205
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2206
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2207
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2208
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2209
|
+
"posts/README.md": "37841aae476bd540",
|
|
2210
|
+
"site/README.md": "732894273a32dc3f",
|
|
2211
|
+
"site/src/worker.mjs": [
|
|
2212
|
+
"34444a6ca4b14437",
|
|
2213
|
+
"fc90f3609cad18ba"
|
|
2214
|
+
]
|
|
2215
|
+
},
|
|
2216
|
+
"sections": {
|
|
2217
|
+
"AGENTS.md": {
|
|
2218
|
+
"(intro)": "214aa46eb3a895d5",
|
|
2219
|
+
"## Layout": [
|
|
2220
|
+
"c18362ed28975d81",
|
|
2221
|
+
"f3a280b8c96cd068"
|
|
2222
|
+
],
|
|
2223
|
+
"## Apps: one screen, roles and parts": [
|
|
2224
|
+
"4db0abe4cdfac09f",
|
|
2225
|
+
"be6f8fa45f93211a"
|
|
2226
|
+
],
|
|
2227
|
+
"## Commands (all through the pinned CLI in node_modules)": [
|
|
2228
|
+
"a7f290579585368c",
|
|
2229
|
+
"c920ac84d44c872a"
|
|
2230
|
+
],
|
|
2231
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2232
|
+
"## Making games": [
|
|
2233
|
+
"2da7fa74c8884834",
|
|
2234
|
+
"9e44559de535d842"
|
|
2235
|
+
],
|
|
2236
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2237
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2238
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2239
|
+
"## Servers and AI seats": [
|
|
2240
|
+
"98d08705087a33ec",
|
|
2241
|
+
"d0028361fe567add"
|
|
2242
|
+
],
|
|
2243
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2244
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2245
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
2246
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2247
|
+
"## Beta": "9d02638207a26e73"
|
|
2248
|
+
}
|
|
2249
|
+
},
|
|
2250
|
+
"named": {
|
|
2251
|
+
"Homie Arcade": {
|
|
2252
|
+
"sections": {
|
|
2253
|
+
"AGENTS.md": {
|
|
2254
|
+
"## Commands (all through the pinned CLI in node_modules)": [
|
|
2255
|
+
"7cad8fd10f62fef1",
|
|
2256
|
+
"cae1df6235f51d17"
|
|
2257
|
+
]
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
},
|
|
2263
|
+
"0.41.0": {
|
|
2202
2264
|
"files": {
|
|
2203
2265
|
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2204
2266
|
"README.md": "684b3cca8001673c",
|
|
@@ -2213,8 +2275,52 @@
|
|
|
2213
2275
|
"sections": {
|
|
2214
2276
|
"AGENTS.md": {
|
|
2215
2277
|
"(intro)": "214aa46eb3a895d5",
|
|
2216
|
-
"## Layout": "
|
|
2278
|
+
"## Layout": "f3a280b8c96cd068",
|
|
2217
2279
|
"## Apps: one screen, roles and parts": "be6f8fa45f93211a",
|
|
2280
|
+
"## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
|
|
2281
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2282
|
+
"## Making games": "9e44559de535d842",
|
|
2283
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2284
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2285
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2286
|
+
"## Servers and AI seats": "d0028361fe567add",
|
|
2287
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2288
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2289
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
2290
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2291
|
+
"## Beta": "9d02638207a26e73"
|
|
2292
|
+
}
|
|
2293
|
+
},
|
|
2294
|
+
"named": {
|
|
2295
|
+
"Homie Arcade": {
|
|
2296
|
+
"sections": {
|
|
2297
|
+
"AGENTS.md": {
|
|
2298
|
+
"## Commands (all through the pinned CLI in node_modules)": "2cdd530e48170bfd"
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
},
|
|
2304
|
+
"0.42.0": {
|
|
2305
|
+
"files": {
|
|
2306
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2307
|
+
"README.md": "684b3cca8001673c",
|
|
2308
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2309
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2310
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2311
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2312
|
+
"posts/README.md": "37841aae476bd540",
|
|
2313
|
+
"site/README.md": "732894273a32dc3f",
|
|
2314
|
+
"site/src/worker.mjs": "34444a6ca4b14437"
|
|
2315
|
+
},
|
|
2316
|
+
"sections": {
|
|
2317
|
+
"AGENTS.md": {
|
|
2318
|
+
"(intro)": "214aa46eb3a895d5",
|
|
2319
|
+
"## Layout": "c18362ed28975d81",
|
|
2320
|
+
"## Apps: one screen, roles and parts": [
|
|
2321
|
+
"8596f2b25edda3e2",
|
|
2322
|
+
"be6f8fa45f93211a"
|
|
2323
|
+
],
|
|
2218
2324
|
"## Commands (all through the pinned CLI in node_modules)": "a7f290579585368c",
|
|
2219
2325
|
"## The site": "0cb8dd9a6f8a0096",
|
|
2220
2326
|
"## Making games": "2da7fa74c8884834",
|
|
@@ -2238,6 +2344,47 @@
|
|
|
2238
2344
|
}
|
|
2239
2345
|
}
|
|
2240
2346
|
}
|
|
2347
|
+
},
|
|
2348
|
+
"0.43.0": {
|
|
2349
|
+
"files": {
|
|
2350
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2351
|
+
"README.md": "684b3cca8001673c",
|
|
2352
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2353
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2354
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2355
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2356
|
+
"posts/README.md": "37841aae476bd540",
|
|
2357
|
+
"site/README.md": "732894273a32dc3f",
|
|
2358
|
+
"site/src/worker.mjs": "34444a6ca4b14437"
|
|
2359
|
+
},
|
|
2360
|
+
"sections": {
|
|
2361
|
+
"AGENTS.md": {
|
|
2362
|
+
"(intro)": "214aa46eb3a895d5",
|
|
2363
|
+
"## Layout": "f3a280b8c96cd068",
|
|
2364
|
+
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
2365
|
+
"## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
|
|
2366
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2367
|
+
"## Making games": "9e44559de535d842",
|
|
2368
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2369
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2370
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2371
|
+
"## Servers and AI seats": "d0028361fe567add",
|
|
2372
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2373
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2374
|
+
"## Rules": "2e6b29afb85dcf7e",
|
|
2375
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2376
|
+
"## Beta": "9d02638207a26e73"
|
|
2377
|
+
}
|
|
2378
|
+
},
|
|
2379
|
+
"named": {
|
|
2380
|
+
"Homie Arcade": {
|
|
2381
|
+
"sections": {
|
|
2382
|
+
"AGENTS.md": {
|
|
2383
|
+
"## Commands (all through the pinned CLI in node_modules)": "2cdd530e48170bfd"
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2241
2388
|
}
|
|
2242
2389
|
}
|
|
2243
2390
|
}
|
package/lib/tools-build.mjs
CHANGED
|
@@ -38,6 +38,7 @@ export async function buildTools(root,esbuild) {
|
|
|
38
38
|
if(!['owner','signed-in','public'].includes(t.audience)&&!(t.audience&&typeof t.audience.app==='string'&&typeof t.audience.role==='string'))throw new Error(`tool ${t.name} needs an audience`);
|
|
39
39
|
if(t.inputSchema?.type!=='object')throw new Error(`tool ${t.name} input schema must be an object`);
|
|
40
40
|
validator.getValidator(t.inputSchema);
|
|
41
|
+
if(t.price && (t.kind==='prompt'||!Number.isSafeInteger(t.price.amount)||t.price.amount<=0||! /^[a-z]{3}$/.test(t.price.currency)))throw new Error(`tool ${t.name}: price needs a positive minor-unit amount and currency`);
|
|
41
42
|
if(t.kind!==undefined&&!['tool','prompt'].includes(t.kind))throw new Error(`tool ${t.name}: invalid kind`);
|
|
42
43
|
if(t.webhook&&(!/^[A-Z][A-Z0-9_]{0,63}$/.test(t.webhook.secret)||typeof t.webhook.person!=='string'||!t.webhook.person))throw new Error(`tool ${t.name}: webhook needs a named secret and account`);
|
|
43
44
|
for(const [name,event] of Object.entries(t.events??{})){
|
|
@@ -51,5 +52,6 @@ export async function buildTools(root,esbuild) {
|
|
|
51
52
|
let text=readFileSync(worker,'utf8');
|
|
52
53
|
if(!text.includes('useTools(')){text += "\nimport { useTools } from '@homie-rocks/studio/worker';\nuseTools(async () => (await import('./tools/index.mjs')).default);\n";writeFileSync(worker,text);}
|
|
53
54
|
}
|
|
55
|
+
writeFileSync(join(dir,'declarations.json'),JSON.stringify({paid:defs.some(t=>t.price)})+'\n');
|
|
54
56
|
return defs.map(t=>t.name);
|
|
55
57
|
}
|
package/lib/trailer.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `homie-studio trailer <id>`: a trailer of a game, in one command.
|
|
3
3
|
*
|
|
4
|
-
* The work is not here. It is the Homie plugin's video skill (`video.mjs trailer`):
|
|
5
|
-
*
|
|
4
|
+
* The work is not here. It is the Homie plugin's video skill (`video.mjs trailer`): live footage for server rules,
|
|
5
|
+
* or a virtual clock and rebuilt sound for older browser games, selected highlights, an end card,
|
|
6
6
|
* and 16:9, 1:1 and 9:16 deliveries. This finds that script and hands the command over, so a person at a terminal
|
|
7
7
|
* and an agent with the plugin run the same thing. The video goes to videos/<id>-trailer/ (or --slug <slug>).
|
|
8
8
|
*
|
|
@@ -36,10 +36,14 @@ export function trailerCommand(root, id, rest, { skills = null, slug = null } =
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function trailerLines(r) {
|
|
39
|
+
const live = r.capture?.mode === 'live';
|
|
40
|
+
const audio = r.capture?.audio;
|
|
41
|
+
const sound = audio?.rebuilt ? `${audio.placed} sounds rebuilt from the game's own files`
|
|
42
|
+
: audio ? 'game audio captured live' : live ? 'no audio captured' : 'the game logged no sound';
|
|
39
43
|
return [
|
|
40
44
|
...(r.problems ?? []).map((x) => `PROBLEM: ${x}`),
|
|
41
45
|
`${r.seconds} s trailer of ${r.game}: ${Object.values(r.outputs ?? {}).map((o) => o.file).join(', ')}`,
|
|
42
|
-
` filmed ${r.capture?.seconds} s frame by frame (${r.capture?.frames} frames, ${r.capture?.heldFrames} held); ${
|
|
46
|
+
` filmed ${r.capture?.seconds} s ${live ? 'live' : 'frame by frame'} (${r.capture?.frames} frames, ${r.capture?.heldFrames} held); ${sound}`,
|
|
43
47
|
` ${r.edit?.shots} shots of ${r.edit?.shotSeconds} s, picked ${r.edit?.picked}`,
|
|
44
48
|
...(r.warnings ?? []).map((w) => ` warning: ${w}`),
|
|
45
49
|
...(r.next ? [` next (the video skill's script): ${r.next}`] : []),
|
package/lib/typecheck.mjs
CHANGED
|
@@ -93,12 +93,12 @@ export function typecheck(root, games, { log = () => {} } = {}) {
|
|
|
93
93
|
}
|
|
94
94
|
// An error with no file is the project itself (a tsconfig.json that does not read): the game's to fix.
|
|
95
95
|
const bare = /^error (TS\d+): (.*)$/.exec(line);
|
|
96
|
-
if (bare) mine.push(
|
|
96
|
+
if (bare) mine.push(`${g.kind === 'app' ? 'apps' : 'games'}/${g.id}${own ? '/tsconfig.json' : ''} ${bare[1]} ${bare[2]}`);
|
|
97
97
|
}
|
|
98
98
|
rows.push({ id: g.id, checked: true, with: own ? 'its tsconfig.json' : 'the build\'s own settings', errors: mine.length, elsewhere });
|
|
99
|
-
if (elsewhere) log(`types: games/${g.id}: ${elsewhere} ${elsewhere === 1 ? 'error' : 'errors'} outside the game's own files (a package it imports as source); not the game's, so not counted`);
|
|
99
|
+
if (elsewhere) log(`types: ${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: ${elsewhere} ${elsewhere === 1 ? 'error' : 'errors'} outside the game's own files (a package it imports as source); not the game's, so not counted`);
|
|
100
100
|
if (mine.length) found.push(...mine);
|
|
101
|
-
else log(`types: games/${g.id} has no type errors (TypeScript ${ts.version ?? '?'}, ${own ? 'its tsconfig.json' : 'the build\'s own settings'})`);
|
|
101
|
+
else log(`types: ${g.kind === 'app' ? 'apps' : 'games'}/${g.id} has no type errors (TypeScript ${ts.version ?? '?'}, ${own ? 'its tsconfig.json' : 'the build\'s own settings'})`);
|
|
102
102
|
}
|
|
103
103
|
if (found.length) {
|
|
104
104
|
const shown = found.slice(0, 20);
|
|
@@ -125,7 +125,7 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
|
|
|
125
125
|
'@homie-rocks/studio/*': [join(PACKAGE_ROOT, '*')],
|
|
126
126
|
} }, files: [join(g.dir, 'src/rules.ts'), join(g.dir, g.entry ?? 'src/view.ts'), join(dir, 'files.d.ts')] }, null, 2)}\n`);
|
|
127
127
|
const res = await runRulesCompiler(compiler, ['-p', project, '--pretty', 'false']);
|
|
128
|
-
if (res.error) throw new Error(
|
|
128
|
+
if (res.error) throw new Error(`${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: the rules type check could not finish: ${res.error.message}`);
|
|
129
129
|
const messages = [];
|
|
130
130
|
for (const line of `${res.stdout ?? ''}\n${res.stderr ?? ''}`.split(/\r?\n/)) {
|
|
131
131
|
const at = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/.exec(line);
|
|
@@ -134,7 +134,9 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
|
|
|
134
134
|
// Imported game helpers count too. Only the toolkit's own implementation is outside the author's remit.
|
|
135
135
|
if (!file.startsWith(`${PACKAGE_ROOT}${sep}`) && !file.includes(`${sep}node_modules${sep}`) || file.startsWith(`${g.dir}${sep}`)) {
|
|
136
136
|
const sourceLine = readFileSync(file,'utf8').split(/\r?\n/)[Number(at[2])-1] ?? '';
|
|
137
|
-
const advice = /Property '
|
|
137
|
+
const advice = /Property 'bot'.*RosterRow/.test(at[5]) ? "Rules roster rows use driver === 'bot' or 'ai' (not row.bot); preserve the visible AI label."
|
|
138
|
+
: /'[^']*\.at' is possibly 'undefined'/.test(at[5]) ? 'Effects carry either at (position) or id (entity). Check e.at before reading coordinates, or resolve e.id with room.get.'
|
|
139
|
+
: /Property 'ask'.*type 'never'/.test(at[5]) ? 'This handler scope never calls world.ask, so no answer can arrive here. Handle the answer in the scope that asks.'
|
|
138
140
|
: /world\.round\.end/.test(sourceLine) && at[4] === 'TS2339' ? 'Only room handlers end a round. Send a declared event to a room handler that calls world.round.end().'
|
|
139
141
|
: /'check'.*does not exist/.test(at[5]) ? 'Remove the obsolete check key. Generated observations require no exemption.'
|
|
140
142
|
: /=> (?:GameDecision|GuideDecision)'/.test(at[5]) ? 'A floor returns goal and say names from agents.json with exactly the args and sayArgs each declares, or {} to decline. Forward a request as { goal: ask.k, args: ask.args }.'
|
|
@@ -150,7 +152,7 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
|
|
|
150
152
|
} else if (/^error TS/.test(line)) messages.push(line);
|
|
151
153
|
else if (/^\s+\S/.test(line) && messages.length) messages[messages.length - 1] += `\n${line.length > 360 ? line.slice(0, 220) + ' … ' + line.slice(-100) : line}`;
|
|
152
154
|
}
|
|
153
|
-
if (messages.length) throw new Error(
|
|
154
|
-
if (res.status !== 0 && !res.stdout) throw new Error(
|
|
155
|
-
log(`types: games/${g.id}: rules, move and view checked with the toolkit's strict compiler`);
|
|
155
|
+
if (messages.length) throw new Error(`${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: its rules have type errors:\n${messages.slice(0, 30).map((m) => ` ${m}`).join('\n')}`);
|
|
156
|
+
if (res.status !== 0 && !res.stdout) throw new Error(`${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: TypeScript stopped without diagnostics (${res.signal ?? res.status})`);
|
|
157
|
+
log(`types: ${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: rules, move and view checked with the toolkit's strict compiler`);
|
|
156
158
|
}
|
package/lib/upgrade.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {refreshWorkerConfig} from './worker-config.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* `homie-studio upgrade [--apply] [--diff]` — an existing studio takes what this version's template adds, and
|
|
3
4
|
* nothing it made its own is ever overwritten.
|
|
@@ -346,6 +347,7 @@ export function upgradeApply(root, plan) {
|
|
|
346
347
|
}
|
|
347
348
|
if (text !== null && (!existsSync(path) || readFileSync(path, 'utf8') !== text)) writeAtomic(path, text);
|
|
348
349
|
}
|
|
350
|
+
refreshWorkerConfig(root);
|
|
349
351
|
const pinned = done.some((c) => c.kind === 'pin');
|
|
350
352
|
return {
|
|
351
353
|
...plan, applied: true, done, skipped,
|
package/lib/worker-build.mjs
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {refreshWorkerConfig} from './worker-config.mjs';
|
|
2
|
+
import {pathToFileURL} from 'node:url';
|
|
3
|
+
import {existsSync,mkdirSync,writeFileSync,rmSync} from 'node:fs';
|
|
4
|
+
import {join,relative} from 'node:path';
|
|
3
5
|
import {readConfig} from './routes.mjs';
|
|
4
6
|
import {paidReleases} from './parts-upload.mjs';
|
|
5
|
-
import {configPath} from './studio.mjs';
|
|
6
7
|
/** Preserve dynamic imports as real Worker modules: page startup never evaluates MCP libraries. */
|
|
7
8
|
export async function buildWorker(root,esbuild) {
|
|
8
9
|
const source=join(root,'site/src/worker.mjs');if(!existsSync(source))return;
|
|
9
10
|
const out=join(root,'site/src/runtime');
|
|
11
|
+
refreshWorkerConfig(root);
|
|
10
12
|
const alias={...(readConfig(root)?.alias??{})};
|
|
11
|
-
|
|
13
|
+
const toolModule=join(root,'site/src/tools/index.mjs');
|
|
14
|
+
const pricedTools=existsSync(toolModule) && (await import(pathToFileURL(toolModule).href+'?worker='+Date.now())).default.some(tool=>tool.price);
|
|
15
|
+
if(paidReleases(root).length || existsSync(join(root,'shop.json')) || pricedTools)alias['@homie-rocks/studio/worker']='@homie-rocks/studio/worker/selling';
|
|
12
16
|
const result=await esbuild.build({entryPoints:[source],absWorkingDir:root,alias,outdir:out,bundle:true,splitting:true,format:'esm',platform:'browser',mainFields:['module','main'],conditions:['workerd','worker','browser'],external:['node:*','cloudflare:*'],target:'es2022',write:false,minify:true,metafile:true,logLevel:'silent'});
|
|
13
17
|
rmSync(out,{recursive:true,force:true});
|
|
14
18
|
mkdirSync(out,{recursive:true});
|
|
15
19
|
for(const file of result.outputFiles)writeFileSync(file.path,file.contents);
|
|
16
|
-
const config=configPath(root);
|
|
17
|
-
if(existsSync(config)){
|
|
18
|
-
const original=readFileSync(config,'utf8');
|
|
19
|
-
const json=JSON.parse(original.replace(/^\s*\/\/.*$/gm,''));
|
|
20
|
-
const before=JSON.stringify(json);
|
|
21
|
-
json.main=relative(dirname(config),join(out,'worker.js')).split('\\').join('/');
|
|
22
|
-
json.no_bundle=true;json.find_additional_modules=true;
|
|
23
|
-
json.rules=[...(json.rules??[]).filter(r=>!r.homieMcp),{type:'ESModule',globs:['**/*.js'],fallthrough:true}];
|
|
24
|
-
// No duplicate rule on rebuild.
|
|
25
|
-
json.rules=json.rules.filter((r,i,all)=>all.findIndex(x=>JSON.stringify(x)===JSON.stringify(r))===i);
|
|
26
|
-
json.compatibility_flags=[...new Set([...(json.compatibility_flags??[]),'nodejs_compat'])];
|
|
27
|
-
if(JSON.stringify(json)!==before) {
|
|
28
|
-
const comments=original.match(/^(?:\s*\/\/[^\n]*\n)*/)?.[0]??'';
|
|
29
|
-
writeFileSync(config,comments+JSON.stringify(json,null,2)+'\n');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
20
|
const sizes=Object.entries(result.metafile.outputs).map(([path,value])=>({path:relative(root,path),bytes:value.bytes,imports:value.imports}));
|
|
33
21
|
writeFileSync(join(out,'modules.json'),JSON.stringify(sizes,null,2)+'\n');
|
|
34
22
|
return sizes;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {readFileSync,writeFileSync,existsSync} from 'node:fs';
|
|
2
|
+
import {join} from 'node:path';
|
|
3
|
+
import {configPath,readStudio,layoutOf,listExperiences} from './studio.mjs';
|
|
4
|
+
import {wranglerConfig} from './scaffold.mjs';
|
|
5
|
+
import {paidReleases} from './parts-upload.mjs';
|
|
6
|
+
const parse=t=>JSON.parse(t.replace(/^\s*\/\/.*$/gm,''));
|
|
7
|
+
const read=file=>existsSync(file)?parse(readFileSync(file,'utf8')):{};
|
|
8
|
+
function merge(base,custom) {
|
|
9
|
+
if(Array.isArray(custom))return custom;
|
|
10
|
+
if(custom && typeof custom==='object')return Object.fromEntries([...new Set([...Object.keys(base??{}),...Object.keys(custom)])].sort().map(k=>[k,k in custom?merge(base?.[k],custom[k]):base[k]]));
|
|
11
|
+
return custom;
|
|
12
|
+
}
|
|
13
|
+
/** Generated output is never an input. Studio additions belong in wrangler.custom.json. */
|
|
14
|
+
export function updateWorkerConfig(root,generated) {
|
|
15
|
+
const file=configPath(root),next=merge(parse(generated),read(join(root,'wrangler.custom.json')));
|
|
16
|
+
const text='// Generated by homie-studio. Edit studio.json or wrangler.custom.json, then build.\n'+JSON.stringify(next,null,2)+'\n';
|
|
17
|
+
if(!existsSync(file)||readFileSync(file,'utf8')!==text)writeFileSync(file,text);
|
|
18
|
+
}
|
|
19
|
+
export function refreshWorkerConfig(root,overrides={}) {
|
|
20
|
+
const studio=readStudio(root),cf=studio.cloudflare;
|
|
21
|
+
if(!cf?.worker)return;
|
|
22
|
+
const metadata=read(join(root,'site/src/functions/declarations.json'));
|
|
23
|
+
const functions=Array.isArray(metadata)?metadata:[];
|
|
24
|
+
const tools=read(join(root,'site/src/tools/declarations.json'));
|
|
25
|
+
const ai=cf.ai??listExperiences(root).some(g=>g.decide===true || g.chat!==false && (g.chat?.mode??'text')==='text' && g.chat?.ai!==false);
|
|
26
|
+
const paid=paidReleases(root).length>0||existsSync(join(root,'shop.json'))||tools.paid===true;
|
|
27
|
+
updateWorkerConfig(root,wranglerConfig({worker:cf.worker,name:studio.name,d1:cf.d1,d1Id:cf.d1Id,r2:cf.r2,layout:layoutOf(root),ai,paidParts:paid,partsRateLimit:cf.purchaseRateLimit??null,routes:cf.routes??null,triggers:functions.length?{crons:[...new Set(['* * * * *',...functions.map(f=>f.schedule).filter(Boolean)])].sort()}:null,...overrides}));
|
|
28
|
+
}
|
package/netplay/NETPLAY.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Homie netplay contract, v1 (revision 11)
|
|
2
2
|
|
|
3
|
+
**Authoring now:** New games use rules plus view and the server as host. Read the
|
|
4
|
+
Homie game skill's RULES.md and section 29 below. Sections 1–28 preserve the
|
|
5
|
+
browser-hosted helper contract for existing games and describe shared shell
|
|
6
|
+
features; their createNetplay/host-loop examples are not a recipe for new games.
|
|
7
|
+
Use the skill's REWRITE.md for an owner-requested rewrite. All five starters use
|
|
8
|
+
server rules. Room seats are capped at 32; larger rooms are future work.
|
|
9
|
+
|
|
10
|
+
|
|
3
11
|
Status: **v1, revision 11** (2026-10-08, `@homie-rocks/studio` 0.33.1). The wire
|
|
4
12
|
version is `v: 1`. Everything revisions 2 to 11 added is either an optional field, a new message type,
|
|
5
13
|
a new refusal, or a change of pace inside the old caps, and both sides ignore types they do
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
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",
|
|
@@ -88,6 +88,10 @@
|
|
|
88
88
|
"./tools": {
|
|
89
89
|
"types": "./tools/tools.ts",
|
|
90
90
|
"default": "./tools/tools.ts"
|
|
91
|
+
},
|
|
92
|
+
"./functions": {
|
|
93
|
+
"types": "./functions/functions.ts",
|
|
94
|
+
"default": "./functions/functions.ts"
|
|
91
95
|
}
|
|
92
96
|
},
|
|
93
97
|
"files": [
|
|
@@ -116,7 +120,8 @@
|
|
|
116
120
|
"links/",
|
|
117
121
|
"apps/",
|
|
118
122
|
"app-starters/",
|
|
119
|
-
"tools/"
|
|
123
|
+
"tools/",
|
|
124
|
+
"functions/"
|
|
120
125
|
],
|
|
121
126
|
"engines": {
|
|
122
127
|
"node": ">=22"
|
package/rules/host.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* host.ts — the host runtime: a rules module playing the host's part on the wire.
|
|
3
3
|
* =============================================================================
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The server normally hosts the room: rules, clock and state. Browser hosting uses the same runtime
|
|
6
|
+
* for offline play, local development or private friends games. It takes a compiled rules module (rules.ts) and three things from its
|
|
7
7
|
* caller: a way to send a frame, a clock and a store. It has no transport in it and no Cloudflare in it, in the style of
|
|
8
8
|
* worker/room.mjs. Inside the `Table` (worker/index.mjs) the relay hands it parsed frames and fans out what it sends;
|
|
9
9
|
* in Node the tests and the build check drive it with a clock of their own.
|
package/rules/view.ts
CHANGED
|
@@ -672,6 +672,7 @@ export function openRoom<R = unknown>(opts: OpenRoomOptions = {}): Room<R> {
|
|
|
672
672
|
net.expose({
|
|
673
673
|
self: () => { const m = meNow(); return m ? { x: m.pos.x, y: m.pos.y, z: m.pos.z } : null; },
|
|
674
674
|
peer: (seat: number) => { if (!latest) return null; for (const u of latest.ents.values()) if (u.seat === seat) return { x: u.pos.x, y: u.pos.y, z: u.pos.z }; return null; },
|
|
675
|
+
movement: () => { const kinds = schema.kinds.filter(k => k.player); return kinds.length && kinds.every(k => k.maxSpeed === 0) ? 'stationary' : 'spatial'; },
|
|
675
676
|
prediction: () => ({ ...correction, observedLeadAt, lastSendAt, now: clock(), serverTick: latest?.k, tick: stepped, catchTick, lead: targetLead(), medianLead: medianLead(), rate: speed, rtt: authorityRtt(), hostAge: quantile(hostAges, 0.5), pending: pending.length }),
|
|
676
677
|
scores: () => roster().map((r) => ({ seat: r.seat, score: r.score })),
|
|
677
678
|
score: myScore, tick: () => latest?.k ?? 0, epoch: () => epoch, hosted: () => net.rulesHosting ? 'browser' : schema.settings.host, status: () => solo ? 'offline' : status,
|
package/shop/SHOP.md
CHANGED
|
@@ -39,9 +39,10 @@ A kind is a label, not a recurring-billing, wallet, random-reward or fulfillment
|
|
|
39
39
|
omitted limits do not apply. `referralNewPlayersOnly: true` limits attribution to new players.
|
|
40
40
|
- `catalog`: modes (`test`, `live`) populated by `homie-studio shop catalog` for named Stripe Products.
|
|
41
41
|
Prices always come from your file, even when a Stripe Product exists.
|
|
42
|
-
-
|
|
43
|
-
`purchaseAttemptsPerAddressPerMinute
|
|
44
|
-
|
|
42
|
+
- Optional flood protection: `purchaseAttemptsPerMinute`,
|
|
43
|
+
`purchaseAttemptsPerAddressPerMinute`, `guestBuyersPerAddressPerHour`.
|
|
44
|
+
Omitted or null means no limit. Explicit positive integers apply per Worker instance.
|
|
45
|
+
An explicitly set `PLAYER_LIMIT_DAILY` also applies to new shop buyers.
|
|
45
46
|
Referral delivery uses `REFERRAL_STATEMENTS_PER_MINUTE`, `REFERRAL_STATEMENTS_GLOBAL_PER_MINUTE`
|
|
46
47
|
(both 30), and `REFERRAL_STATEMENT_BYTES` (65536). `requestBytes` overrides the catalog-sized shop request
|
|
47
48
|
allowance; `SHOP_WEBHOOK_BYTES` (524288) controls the webhook byte allowance.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// This same guarded step runs on the server and immediately for the local player.
|
|
2
|
+
// Only pose/motion, input, ctx map/public tune/math/clocks: no scores, queries or random.
|
|
1
3
|
/*
|
|
2
4
|
* COIN DASH — how a runner moves. Its own file, because two places run it: the server, for every body it moves, and
|
|
3
5
|
* each player's browser, for that player's own body. So it reads only what both have: the body's own position,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Game truth lives here: declared fields, legal inputs, next-tick events and the server clock.
|
|
2
|
+
// Read the game skill RULES.md before changing the mechanic; keep presentation in view.ts.
|
|
1
3
|
/*
|
|
2
4
|
* COIN DASH — the rules: the example of a game whose rules run on the server.
|
|
3
5
|
*
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Game truth lives here: declared fields, legal inputs, next-tick events and the server clock.
|
|
2
|
+
// Read the game skill RULES.md before changing the mechanic; keep presentation in view.ts.
|
|
1
3
|
import { defineRules, f, type GameWorld, type GameSelf } from '@homie-rocks/studio/rules';
|
|
2
4
|
import { move } from './move';
|
|
3
5
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Presentation only: openRoom gives predicted own movement and interpolated other bodies.
|
|
2
|
+
// Send intent; rules decide outcomes. Keep rendered obstacles aligned with room.map.
|
|
1
3
|
/** Ember Vale: server-run hunts, with the hero save and memorials owned by its player. */
|
|
2
4
|
import { AI_MARK, BUBBLE_FONT, createBubbles, createControls, createLabels, paintBubbles, type BubbleIn, type BubbleOut, createSaves, easeView, exposePort, fitView, stripAi, type BodyBase, type Fit, type LabelIn, type LabelOut, type NetEvent } from '@homie-rocks/studio/port';
|
|
3
5
|
// The Game Lab: tunables, phases, tracks and overlays (no-ops outside the lab).
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Game truth lives here: declared fields, legal inputs, next-tick events and the server clock.
|
|
2
|
+
// Read the game skill RULES.md before changing the mechanic; keep presentation in view.ts.
|
|
1
3
|
import { defineRules, f } from '@homie-rocks/studio/rules';
|
|
2
4
|
import { move } from './move';
|
|
3
5
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Presentation only: openRoom gives predicted own movement and interpolated other bodies.
|
|
2
|
+
// Send intent; rules decide outcomes. Keep rendered obstacles aligned with room.map.
|
|
1
3
|
/** Gem Rush: the canvas draws the room; rules.ts decides the game. Coordinates here are pixels (50 per metre). */
|
|
2
4
|
import { openRoom, type Entity } from '@homie-rocks/studio/rules/view';
|
|
3
5
|
import type rules from './rules';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// This same guarded step runs on the server and immediately for the local player.
|
|
2
|
+
// Only pose/motion, input, ctx map/public tune/math/clocks: no scores, queries or random.
|
|
1
3
|
import { defineMove, type Vec3 } from '@homie-rocks/studio/rules';
|
|
2
4
|
|
|
3
5
|
import type { MoveBody, MoveContext } from '@homie-rocks/studio/rules/types';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Game truth lives here: declared fields, legal inputs, next-tick events and the server clock.
|
|
2
|
+
// Read the game skill RULES.md before changing the mechanic; keep presentation in view.ts.
|
|
1
3
|
import { defineRules, f, type GameWorld } from '@homie-rocks/studio/rules';
|
|
2
4
|
import { move } from './move';
|
|
3
5
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Presentation only: openRoom gives predicted own movement and interpolated other bodies.
|
|
2
|
+
// Send intent; rules decide outcomes. Keep rendered obstacles aligned with room.map.
|
|
1
3
|
/** Gem Rush 3D: the room runs the rules; this view draws the meadow and its animals. */
|
|
2
4
|
import {
|
|
3
5
|
AmbientLight, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CircleGeometry, Color, ConeGeometry, CylinderGeometry,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// This same guarded step runs on the server and immediately for the local player.
|
|
2
|
+
// Only pose/motion, input, ctx map/public tune/math/clocks: no scores, queries or random.
|
|
1
3
|
import { defineMove, type Vec3 } from '@homie-rocks/studio/rules';
|
|
2
4
|
|
|
3
5
|
import type { MoveBody, MoveContext } from '@homie-rocks/studio/rules/types';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Game truth lives here: declared fields, legal inputs, next-tick events and the server clock.
|
|
2
|
+
// Read the game skill RULES.md before changing the mechanic; keep presentation in view.ts.
|
|
1
3
|
import { defineRules, f, type GameWorld } from '@homie-rocks/studio/rules';
|
|
2
4
|
import { move } from './move';
|
|
3
5
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Presentation only: openRoom gives predicted own movement and interpolated other bodies.
|
|
2
|
+
// Send intent; rules decide outcomes. Keep rendered obstacles aligned with room.map.
|
|
1
3
|
/** Hero Rush 3D: server rules, predicted jumps, and the original meadow and heroes. */
|
|
2
4
|
import {
|
|
3
5
|
AmbientLight, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CircleGeometry, Color, ConeGeometry, CylinderGeometry,
|