@homie-rocks/studio 0.44.3 → 0.45.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 +21 -0
- package/README.md +20 -22
- package/guides/game/REWRITE.md +4 -2
- package/guides/game/RULES.md +31 -11
- package/guides/game/SKILL.md +9 -7
- package/lib/build.mjs +11 -8
- package/lib/cloudflare.mjs +2 -1
- package/lib/mcp.mjs +1 -1
- package/lib/rules-build.mjs +2 -1
- package/lib/scaffold.mjs +9 -9
- package/lib/template-history.json +58 -3
- package/lib/typecheck.mjs +16 -2
- package/lib/worker-build.mjs +5 -2
- package/netplay/NETPLAY.md +22 -177
- package/netplay/netplay.ts +14 -9
- package/package.json +1 -1
- package/rules/host.ts +2 -0
- package/rules/interest.mjs +106 -21
- package/rules/rules.ts +3 -3
- package/starters/coin-dash/game.json +14 -4
- package/starters/ember-vale/game.json +0 -4
- package/starters/ember-vale/src/view.ts +2 -2
- package/starters/gem-rush/game.json +0 -4
- package/starters/gem-rush-3d/game.json +0 -4
- package/starters/hero-rush-3d/game.json +0 -4
- package/worker/gate.mjs +151 -0
- package/worker/index.mjs +21 -11
- package/worker/mcp-room.mjs +0 -1
- package/worker/office.mjs +3 -3
- package/worker/room.mjs +35 -19
- package/worker/seats.mjs +9 -13
- package/worker/servers.mjs +6 -6
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,27 @@ 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.45.0] - 2026-10-10
|
|
21
|
+
|
|
22
|
+
**Plugin 0.45.0** · [#85](https://github.com/homie-rocks/homie/pull/85)
|
|
23
|
+
|
|
24
|
+
Choose room sizes in the hundreds and reduce the state each phone downloads.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Gate connections and batched concentrator links, with seat resumption after a link restart.
|
|
29
|
+
- Studio-selected `room.view.precisionM`, `nearM` and `farHz` for distant bodies. The controlled body keeps its exact movement state.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- For rules games, public room admission, rules compilation and deploy planning use the studio's requested `players.max` without the 32-player clamp.
|
|
34
|
+
- Ordered snapshot deltas and a shared spatial index reduce repeated state work. Rules rooms have no automatic guest ceiling for shared carrier addresses.
|
|
35
|
+
|
|
36
|
+
### Upgrade notes
|
|
37
|
+
|
|
38
|
+
- Rebuilding generates the Gate and Concentrator exports, bindings and SQLite migration, including for studios whose Worker entry predates Gates. Custom Worker source and routes are preserved.
|
|
39
|
+
- Existing browser-hosted netplay games keep building, checking and running with the restored port host/HUD helpers and importer. Their 32-seat limit remains; a larger request prints the rewrite guide. New games and starters use rules plus view. A release gate packs this version and checks two browsers finishing a round in the unchanged released 0.32.1 starter. Retirement is a later step, after studio migrations and a shipped one-command assisted rewrite.
|
|
40
|
+
|
|
20
41
|
## [0.44.3] - 2026-10-10
|
|
21
42
|
|
|
22
43
|
**Plugin 0.44.3**
|
package/README.md
CHANGED
|
@@ -202,30 +202,28 @@ game its studio chooses to share, each with its own licence ("Game parts", below
|
|
|
202
202
|
RIGHTS.md has no "A remix" line. Whether a file may be handed on in a shared part is still its licence's.
|
|
203
203
|
- `homie-studio game remix` answers with a sentence that points to parts; `office launch --remixable` is ignored.
|
|
204
204
|
|
|
205
|
-
##
|
|
205
|
+
## Room size and delivery
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
`players.max` in game.json is the room's chosen positive integer capacity; omitted means 8.
|
|
208
|
+
There is no 32-player ceiling or default per-address admission limit. Public games use
|
|
209
|
+
rules plus view with `room.host: "server"`. Gates carry the browser connections and
|
|
210
|
+
concentrators combine Gate links for larger rooms. Server rules survive browser departures.
|
|
211
|
+
New browser/offline games use the same rules contract. Existing netplay games and their
|
|
212
|
+
port host helpers remain supported for upgrades, with their original 32-seat limit.
|
|
213
|
+
For larger rooms, follow `guides/game/REWRITE.md`.
|
|
213
214
|
|
|
214
|
-
|
|
215
|
+
For example, a 300-player room can choose spatial delivery explicitly:
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
game.json `"netplay"` says more than the room's size. All of it is optional, and `homie-studio build`
|
|
221
|
-
writes it to the catalogue for the site's rooms (`netplay/NETPLAY.md` sections 22 to 24):
|
|
217
|
+
```json
|
|
218
|
+
{"players":{"min":1,"max":300},"entry":"src/view.ts","room":{"host":"server","view":{"radiusM":12,"precisionM":0.01,"nearM":4,"farHz":5}}}
|
|
219
|
+
```
|
|
222
220
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
The studio chooses the radius, precision and distant update rate for its game. The
|
|
222
|
+
controlled body stays precise. Whole-room state remains the default. The build runs a
|
|
223
|
+
local capacity trial; its numbers are not Cloudflare capacity guarantees. Test the actual
|
|
224
|
+
mechanic at the chosen size. Rules builds derive their revision from rules and view;
|
|
225
|
+
do not manually bump `netplay.version`. Optional `netplay.params` still selects play-page
|
|
226
|
+
query parameters passed to the view, beside `debug` and `q`.
|
|
229
227
|
|
|
230
228
|
The game's frame is sandboxed without `allow-same-origin` (it must never read the site's storage or
|
|
231
229
|
the owner's session), so `localStorage` throws inside it: a setting or a personal best goes in
|
|
@@ -1238,8 +1236,8 @@ npx homie-studio port check my-game --url http://127.0.0.1:8787 # the owner te
|
|
|
1238
1236
|
| `template/` (repository root), `scripts/template.mjs` | The public "Deploy to Cloudflare" template, generated from `new --template`. |
|
|
1239
1237
|
| `lib/port.mjs` | `port plan` (reads a game and grades the port) and `port import`. |
|
|
1240
1238
|
| `lib/port-check.mjs` | `port check`: held and alternating directions on keys, Android Chrome and iPhone WebKit touch, UI cover, two browsers finishing a round, a killed host, a late joiner, the big screen. |
|
|
1241
|
-
| `port/` | The port toolkit (`@homie-rocks/studio/port`, or `window.HomiePort` from `homie-port.js` in a static game):
|
|
1242
|
-
| `worker/index.mjs` | The site Worker and the `Table` (netplay relay, `room.mjs`) and `Lobby` Durable Objects; `/<game>/tv` is the big screen with a join QR (`qr.mjs`); `/<game>/watch` watches a live room from any player's view (NETPLAY.md section 16); `/music/<slug>/` and `/videos/<slug>/` are song and video pages (their files served with byte ranges, from the site's files or, once moved, from the studio's R2 at the same address; a loose file from `media put` at `/media/<key>`). `seats.mjs`: room sizes
|
|
1239
|
+
| `port/` | The port toolkit (`@homie-rocks/studio/port`, or `window.HomiePort` from `homie-port.js` in a static game): the touch kit, keys, camera rules, bots, sandbox shims, first-touch audio, `exposePort`; `fitView` and `createLabels` (`port/view.ts`): a flat world on every screen (the whole of it where it reads, else filling a phone held upright and following the player) and names that never pile up when bodies crowd (yours placed first, the rest moved or faded). |
|
|
1240
|
+
| `worker/index.mjs` | The site Worker and the `Table` (netplay relay, `room.mjs`) and `Lobby` Durable Objects; `/<game>/tv` is the big screen with a join QR (`qr.mjs`); `/<game>/watch` watches a live room from any player's view (NETPLAY.md section 16); `/music/<slug>/` and `/videos/<slug>/` are song and video pages (their files served with byte ranges, from the site's files or, once moved, from the studio's R2 at the same address; a loose file from `media put` at `/media/<key>`). `seats.mjs`: studio-chosen room sizes. |
|
|
1243
1241
|
| `worker/site.mjs`, `lib/site.mjs`, `lib/markdown.mjs`, `site/SITE.md` | The site: its sections, each game's landing, posts and their feeds, the look (theme tokens) and what the studio's `site/` folder overrides; the safe markdown posts are written in. `worker/pages.mjs`: the play page; `worker/arrival.mjs`: its arrival card. |
|
|
1244
1242
|
| `worker/stats.mjs`, `worker/stats-page.mjs`, `lib/stats.mjs` | The studio's own stats: what is counted and how, the owner-only `/api/stats` and `/_studio/stats`, and `homie-studio stats`. |
|
|
1245
1243
|
| `lib/media.mjs`, `media/MEDIA.md` | The `music/` and `videos/` manifests: which entries get a page, and where each file's bytes come from (the site itself up to 25 MiB a file, the studio's R2, or a link). `media list` shows it; `media move` puts the big ones in R2 and records each one's SHA-256; `media put` uploads a loose file to `/media/<key>`. |
|
package/guides/game/REWRITE.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Rewrite a browser-hosted game as rules plus view
|
|
2
2
|
|
|
3
3
|
Use this path when the owner asks to move an old game's rules onto the server.
|
|
4
|
-
There is no automatic converter.
|
|
5
|
-
|
|
4
|
+
There is no automatic converter yet. Version 0.45.0 keeps existing netplay games
|
|
5
|
+
and their port host helpers working, with their original 32-seat limit. Larger
|
|
6
|
+
rooms require this rewrite. Keep a baseline copy and preserve the game throughout.
|
|
7
|
+
Read RULES.md before starting.
|
|
6
8
|
|
|
7
9
|
1. Read this game's codex, manifest, entry, host loop, input, packing/checkpoint
|
|
8
10
|
code, bots, saves and tests. Record what players must keep: controls, timings,
|
package/guides/game/RULES.md
CHANGED
|
@@ -36,7 +36,7 @@ export default defineRules({
|
|
|
36
36
|
|
|
37
37
|
The map must contain a nonempty `spots.start` list for that example:
|
|
38
38
|
`{"bounds":{"min":[-12,-7],"max":[12,7]},"boxes":[],"spots":{"start":[[-5,0],[5,0]]}}`.
|
|
39
|
-
Declare `players.max` in game.json (
|
|
39
|
+
Declare `players.max` in game.json (a positive integer, including hundreds); use `entry: "src/view.ts"` and
|
|
40
40
|
`room: { host: "server" }`. Only public tunables are visible in move.ts.
|
|
41
41
|
|
|
42
42
|
`index.html` is the browser entry document; include `<script type="module" src="./assets/main.js"></script>`. The manifest names the source, but HTML loads this relative built bundle. Never point HTML at /src/view.ts: that URL is not served. The build rewrites assets/main.js to its hashed bundle. A minimal manifest is:
|
|
@@ -134,7 +134,7 @@ with a real touchscreen tap in Chrome; calling `element.click()` does not test
|
|
|
134
134
|
touch delivery.
|
|
135
135
|
Use `rounds: { seconds: 0, breakSeconds: 0 }` for manual rounds, and no fill bots
|
|
136
136
|
for an app. A wall/TV watches without taking a seat. Each interactive phone
|
|
137
|
-
takes
|
|
137
|
+
takes one of the room's declared seats; shared state is replicated, public and transient. An app's
|
|
138
138
|
lasting, authorized business data belongs in `createAppRecords`, not in room fields.
|
|
139
139
|
|
|
140
140
|
## State and field types
|
|
@@ -680,8 +680,7 @@ check green. Never rename rules.ts away or fall back to old-style hosting.
|
|
|
680
680
|
`room.probe` adds truthful frame/mechanic/render counters to the built-in rules probe.
|
|
681
681
|
Use the normal build, local dev and two-browser check, then play the requested mechanic
|
|
682
682
|
for a minute with delay. Movement should respond before the authoritative response;
|
|
683
|
-
score and other shared outcomes still arrive with the network.
|
|
684
|
-
up to 32 seats; neither frame rate nor feel is guaranteed by a passing build.
|
|
683
|
+
score and other shared outcomes still arrive with the network. The studio chooses `players.max`; neither frame rate nor feel is guaranteed by a passing build.
|
|
685
684
|
|
|
686
685
|
Room state is saved automatically (default movementSeconds: 1, plus round end,
|
|
687
686
|
pause and finish). After 60 seconds with no person the room ends even if screens or
|
|
@@ -690,7 +689,7 @@ server-verified money. All replicated fields are public, including disguised rol
|
|
|
690
689
|
Use a separate authorized records service for private or lasting app records.
|
|
691
690
|
|
|
692
691
|
|
|
693
|
-
## Spatial delivery (0.
|
|
692
|
+
## Spatial delivery (0.45.0; milestone 2 slice 2)
|
|
694
693
|
|
|
695
694
|
For a server game with a larger map, the studio's AI can set
|
|
696
695
|
`"room": { "view": { "radiusM": 32 } }` in game.json to send each player only
|
|
@@ -708,12 +707,33 @@ may be absent from room.each while its geometry still blocks movement.
|
|
|
708
707
|
Shared state, effects, rosters and watcher state are still public: this is not a
|
|
709
708
|
hidden-information feature. Browser hosting sends the whole room.
|
|
710
709
|
|
|
711
|
-
|
|
712
|
-
public
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
710
|
+
Set the room size in `players.max`. The same setting feeds rules compilation,
|
|
711
|
+
public admission and the automatic Gate layout; there is no 32-seat clamp.
|
|
712
|
+
For example, in game.json:
|
|
713
|
+
|
|
714
|
+
```json
|
|
715
|
+
{
|
|
716
|
+
"players": { "min": 1, "max": 300 },
|
|
717
|
+
"room": {
|
|
718
|
+
"host": "server",
|
|
719
|
+
"view": { "radiusM": 12, "precisionM": 0.01, "nearM": 4, "farHz": 5 }
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
These are example visual settings, not defaults. `precisionM` rounds remote
|
|
725
|
+
positions and velocities in the delivered view; controlled bodies stay exact.
|
|
726
|
+
Remote bodies outside `nearM` update at `farHz`; visibility exits are immediate.
|
|
727
|
+
Rules, collision, saves and shared state stay authoritative and unchanged.
|
|
728
|
+
Views must interpolate distant motion and dispose/recreate entities on view exit
|
|
729
|
+
and entry. Omit the settings for full precision and the room's normal tick rate.
|
|
730
|
+
|
|
731
|
+
A small room embeds delivery in its Table. Larger rooms open Gate connections;
|
|
732
|
+
more than eight Gates use concentrators. The studio template supplies their exports, bindings and
|
|
733
|
+
migration; upgrade the template before deploying an existing studio. No infrastructure naming or studio admission ceiling is required.
|
|
734
|
+
Read the local measurements and their limitations in
|
|
735
|
+
`docs/rooms-milestone-2-notes.md` before making a capacity claim.
|
|
736
|
+
|
|
717
737
|
## Live collision geometry (0.44.1)
|
|
718
738
|
|
|
719
739
|
```ts
|
package/guides/game/SKILL.md
CHANGED
|
@@ -30,9 +30,10 @@ other games and every existing test file, including its import paths. If an old
|
|
|
30
30
|
do not redirect the tests to a new helper file. Run the original test command.
|
|
31
31
|
|
|
32
32
|
For a request involving a larger room or map, read RULES.md's **Spatial delivery**
|
|
33
|
-
section.
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
section. `players.max` chooses the room size and Gate layout. Choose `room.view.radiusM`
|
|
34
|
+
for the playable neighbourhood, and visual precision and far update rates to fit
|
|
35
|
+
the game. Local measurements are in docs/rooms-milestone-2-notes.md; they are not
|
|
36
|
+
Cloudflare or physical-phone capacity guarantees.
|
|
36
37
|
|
|
37
38
|
## Start it
|
|
38
39
|
|
|
@@ -80,7 +81,7 @@ make every change it needs in ONE file_edit with an edits list. Build after each
|
|
|
80
81
|
coherent change across rules, movement and view; use diagnostics to guide the next edit.
|
|
81
82
|
|
|
82
83
|
New games use `game.json` `"entry": "src/view.ts"`, `"room": { "host": "server" }`,
|
|
83
|
-
and `players.max
|
|
84
|
+
and a studio-chosen `players.max`. HTML loads `<script type="module"
|
|
84
85
|
src="./assets/main.js"></script>`, not the source-file URL. Choose seats, tick rate (20 by default), dimensions and
|
|
85
86
|
starter yourself from the request; never ask the person an engineering question.
|
|
86
87
|
`coin-dash` is the smallest reference; `gem-rush` adds bumps; `ember-vale` adds
|
|
@@ -142,9 +143,10 @@ a plain browser bundle is not a multiplayer game.
|
|
|
142
143
|
responsible half, preserve unrelated play, and rebuild. The build/state hashes
|
|
143
144
|
manage compatibility automatically; do not hand-pack snapshots or bump netplay.version.
|
|
144
145
|
Recheck the changed mechanic, a late join and a reload in two browsers.
|
|
145
|
-
- **Changing an old-style game:**
|
|
146
|
-
|
|
147
|
-
follow [REWRITE.md](REWRITE.md).
|
|
146
|
+
- **Changing an old-style game:** existing netplay games keep working on 0.45.0
|
|
147
|
+
with their original 32-seat limit. Preserve their behavior on ordinary edits.
|
|
148
|
+
For a server migration or larger room, follow [REWRITE.md](REWRITE.md).
|
|
149
|
+
New games use rules plus view; there is no automatic converter yet.
|
|
148
150
|
- Phones get touch, computers get keys; typed names and chat are text only.
|
|
149
151
|
Keep native DOM buttons outside `guardGestures`' touch-drag selector; otherwise bind
|
|
150
152
|
touch pointers explicitly. Verify actual touchscreen taps, not only `.click()`.
|
package/lib/build.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import { PACKAGE_ROOT, isRulesGame, listExperiences as listGames, readStudio } f
|
|
|
62
62
|
// module is: Homie for Claude Desktop starts the toolkit with no node_modules beside it (scripts/desktop.mjs).
|
|
63
63
|
let rulesBuildModule = null;
|
|
64
64
|
const rulesBuild = () => (rulesBuildModule ??= import('./rules-build.mjs'));
|
|
65
|
-
import { SEAT_MAX, netplayRow } from '../worker/seats.mjs';
|
|
65
|
+
import { seatCount, SEAT_MAX, netplayRow } from '../worker/seats.mjs';
|
|
66
66
|
import { STUDIO_VERSION } from './version.mjs';
|
|
67
67
|
import { basedOnRow, licenseOf, retiredKeys } from '../worker/license.mjs';
|
|
68
68
|
import { openStage, swapIn } from './stage.mjs';
|
|
@@ -266,10 +266,14 @@ export function netplayOf(g, out = null) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
269
|
+
* Room size comes from game.json's players.max, else 8 (32 for apps).
|
|
270
|
+
* Rules games have no admission ceiling; existing netplay games retain their manifest precedence and 32-seat limit.
|
|
271
271
|
*/
|
|
272
272
|
export function seatsFor(g, net = netplayOf(g)) {
|
|
273
|
+
if (isRulesGame(g)) {
|
|
274
|
+
const max = seatCount(g.players?.max);
|
|
275
|
+
return { min: Math.min(max, seatCount(g.players?.min, 1)), max, asked: max };
|
|
276
|
+
}
|
|
273
277
|
const named = [net.maxPlayers, net.players?.max, g.players?.max].map((n) => Math.floor(Number(n))).find((n) => Number.isFinite(n) && n >= 1);
|
|
274
278
|
const asked = named ?? (g.kind === 'app' ? 32 : 8);
|
|
275
279
|
const max = Math.min(SEAT_MAX, asked);
|
|
@@ -390,8 +394,7 @@ export async function buildGameFiles(esbuild, root, g, out, { maps = false, sour
|
|
|
390
394
|
let chunks = [];
|
|
391
395
|
const budgets = modelBudgetsOf(g, log);
|
|
392
396
|
// RULES PLUS VIEW (rooms on the server). A game with "room" in game.json and a src/rules.ts has its rules checked, guarded and loaded first:
|
|
393
|
-
// a rule the wall refuses stops the build here, with the line named.
|
|
394
|
-
// browser exactly as before, and cannot ask for the server.
|
|
397
|
+
// a rule the wall refuses stops the build here, with the line named. Existing netplay games retain their browser host.
|
|
395
398
|
const ruled = isRulesGame(g);
|
|
396
399
|
if (!ruled && g.room?.host === 'server') {
|
|
397
400
|
throw new Error(`${label}/${manifest} asks for "room": { "host": "server" }, but ${mode === 'bundle' ? 'this game has no src/rules.ts: its rules are inside its own code and run in a player\'s browser. Ask for it to be rewritten as rules plus view' : 'a ported game is someone else\'s browser code, which the server cannot run. It stays hosted by a player\'s browser'}.`);
|
|
@@ -517,8 +520,8 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
|
|
|
517
520
|
for (const g of games) if (g.kind === 'app') { const bad = appProblems(g); if (bad.length) throw new Error(`apps/${g.id}/app.json: ${bad.join('; ')}`); }
|
|
518
521
|
// `--types`: the games' TypeScript is checked first (esbuild only strips types, it never reads them), and a type
|
|
519
522
|
// error stops the build before anything is built.
|
|
520
|
-
// Rules always use
|
|
521
|
-
//
|
|
523
|
+
// Rules always use generated capability types, including the view. Existing netplay games and local apps
|
|
524
|
+
// use the source type checker.
|
|
522
525
|
const legacy = games.filter((g) => !isRulesGame(g));
|
|
523
526
|
const typed = types && legacy.length ? typecheck(root, legacy, { log }) : null;
|
|
524
527
|
// What the build before this one made, to say which games changed.
|
|
@@ -561,7 +564,7 @@ async function buildInto(dist, { esbuild, studio, shop, live, games, before, typ
|
|
|
561
564
|
const bytes = bundle ? statSync(join(out, bundle)).size : dirBytes(out);
|
|
562
565
|
const later = chunks.reduce((n, c) => n + statSync(join(out, c)).size, 0);
|
|
563
566
|
const seats = seatsFor(g, netplayOf(g, out));
|
|
564
|
-
if (seats.asked > SEAT_MAX) log(`
|
|
567
|
+
if (seats.asked > SEAT_MAX && !rules) log(`games/${g.id} asks for ${seats.asked} players; browser-hosted netplay rooms retain their ${SEAT_MAX}-player limit, so this build uses ${SEAT_MAX} seats. For larger rooms, rewrite as rules plus view: node_modules/@homie-rocks/studio/guides/game/REWRITE.md`);
|
|
565
568
|
built.push({
|
|
566
569
|
id: g.id, name: g.name, mode, bytes, ms: Date.now() - started, warnings, seats: seats.max,
|
|
567
570
|
...(rules ? { capacityTrial: rules.check.capacityTrial } : {}),
|
package/lib/cloudflare.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {workerEntry} from './worker-build.mjs';
|
|
2
|
+
import { seatCount } from '../worker/seats.mjs';
|
|
2
3
|
import { refreshWorkerConfig } from './worker-config.mjs';
|
|
3
4
|
import { paidReleases, uploadPaidParts } from './parts-upload.mjs';
|
|
4
5
|
/**
|
|
@@ -184,7 +185,7 @@ export function deployPlan(root) {
|
|
|
184
185
|
const studio = readStudio(root);
|
|
185
186
|
// Games whose rules run on the server (a src/rules.ts, and not "room": { "host": "browser" }): one line each.
|
|
186
187
|
const hosted = listGames(root).filter((g) => isRulesGame(g) && g.room?.host !== 'browser').map((g) => {
|
|
187
|
-
const seats =
|
|
188
|
+
const seats = seatCount(g.players?.max);
|
|
188
189
|
const tickHz = Math.max(1, Math.min(60, Math.floor(Number(g.room?.tickHz)) || 20));
|
|
189
190
|
const use = hostedUse({ seats, inputHz: Math.max(1, Math.min(tickHz, Math.floor(Number(g.room?.inputHz)) || tickHz)) });
|
|
190
191
|
return { id: g.id, name: g.name ?? g.id, ...use };
|
package/lib/mcp.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export const INSTRUCTIONS = `Homie Studio, on this computer: make game studios a
|
|
|
35
35
|
|
|
36
36
|
Carry the person's request through to a working result without a lesson or an interview. Use their name or business name, otherwise "My Studio". Setup, dependencies, a plan, building and checking are AI work, not approval gates. Choose engineering and design defaults from the request; record them in CODEX.md so the person can change them later by asking. Phone and computer controls, a small first version, local sound and CC0 art are the defaults. No paid media unless requested with a budget. A business or charity may need an app or a site, not a game: do not make them play a demo or choose a genre.
|
|
37
37
|
Run setup_status (for a studio that is not made yet, pass its name as studio), studio_scaffold (NO game until one is requested), then build the requested thing and check it. A demo and a small-change lesson are optional only when requested. Look for existing engine mechanisms and parts_find before building systems. Use few, large edits: one file_edit carries every change to a file in its edits list. game_plan records your decisions; it does not require an interview. Install missing prerequisites yourself with setup_prepare; follow studio_job without asking the person to retry. Optional providers never block.
|
|
38
|
-
New games and apps needing transient shared live state use rules plus view with room.host server. Read studio_guide topic game and its file RULES.md before writing rules: declared entity/shared state and events, guarded deterministic move for immediate local prediction, and a browser view for input and rendering. Read file REWRITE.md when an owner asks to move an old game's rules to the server; no automatic converter exists
|
|
38
|
+
New games and apps needing transient shared live state use rules plus view with room.host server. Read studio_guide topic game and its file RULES.md before writing rules: declared entity/shared state and events, guarded deterministic move for immediate local prediction, and a browser view for input and rendering. Read file REWRITE.md when an owner asks to move an old game's rules to the server; no automatic converter exists; existing netplay games keep building and running with their original 32-seat limit; larger rooms require a rewrite. Repair the full build diagnostics and prove the requested behavior in two browsers. Set players.max to the chosen room size, including hundreds; public connections use Gates. Choose room.view radiusM, precisionM, nearM and farHz explicitly and measure at the requested size. Private per-seat state is not delivered. Apps use authorized app records for lasting/private business data; room hosting is not staff permission.
|
|
39
39
|
Show progress briefly and give the preview link as soon as it works. Only when going live was requested, studio_deploy with plan: true says what it creates and costs; cloudflare_login asks for browser approval once; then studio_deploy. Publish to the directory only when requested (offer once after going live). Honour an existing request to deploy or list; do not ask again except for a host's mandatory approval.
|
|
40
40
|
For a shop, stripe_login (shop connect) syncs Products, Prices, Payment Links and the webhook through one Stripe browser approval. No human-handled key. Follow studio_job and the result's single next step; rerun after shop.json changes. Test purchase, webhook, grant and refund with stand-ins before going live. Live payments only when requested.
|
|
41
41
|
For a custom domain already on Cloudflare, studio_domain prepares the exact hostname route using the existing sign-in; studio_deploy applies it and Cloudflare creates DNS and TLS. Never ask for zone or account ids. Keep workers.dev until a domain is requested.
|
package/lib/rules-build.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { seatsOf } from '../worker/seats.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* What `homie-studio build` does for a game written as rules plus view (rooms-milestone-1-design.md, section 8).
|
|
3
4
|
*
|
|
@@ -145,7 +146,7 @@ export async function prepareRules(esbuild, root, g, { log = () => {}, longCheck
|
|
|
145
146
|
if (!guarded.ok) throw new Error(`${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: its rules were refused.\n${guarded.problems.slice(0, 20).map((p) => ` ${problemLine(p)}`).join('\n')}${guarded.problems.length > 20 ? `\n … and ${guarded.problems.length - 20} more` : ''}`);
|
|
146
147
|
const tune = readJson(join(g.dir, 'tunables.json')) ?? {};
|
|
147
148
|
const map = readMap(g);
|
|
148
|
-
const seats =
|
|
149
|
+
const seats = seatsOf(g);
|
|
149
150
|
const bundle = await loadRules(esbuild, root, g.id, guarded.code, { bundleOnly: true });
|
|
150
151
|
const data = { id: g.id, tune, map, room: g.room, seats, vocab: readJson(join(g.dir, 'agents.json')), sites: guarded.sites };
|
|
151
152
|
// The declarations first, for the type check: a wrong program is named by the compiler before anything is played.
|
package/lib/scaffold.mjs
CHANGED
|
@@ -71,7 +71,7 @@ export function slugify(name) {
|
|
|
71
71
|
export function wranglerConfig({ worker, name, d1, d1Id = null, r2 = null, layout = 'root', main = null, ai = false, routes = null, paidParts = false, partsRateLimit = null, triggers = null }) {
|
|
72
72
|
const at = layout === 'site' ? { schema: '../node_modules', main: 'src/runtime/worker.js', dist: './dist', migrations: 'migrations' }
|
|
73
73
|
: { schema: 'node_modules', main: 'site/src/runtime/worker.js', dist: './site/dist', migrations: 'site/migrations' };
|
|
74
|
-
const rooms = [{ name: 'TABLE', class_name: 'Table' }, { name: 'LOBBY', class_name: 'Lobby' }];
|
|
74
|
+
const rooms = [{ name: 'TABLE', class_name: 'Table' }, { name: 'LOBBY', class_name: 'Lobby' }, { name: 'GATE', class_name: 'Gate' }, { name: 'CONCENTRATOR', class_name: 'Concentrator' }];
|
|
75
75
|
const config = {
|
|
76
76
|
$schema: `${at.schema}/wrangler/config-schema.json`,
|
|
77
77
|
name: worker,
|
|
@@ -94,7 +94,7 @@ export function wranglerConfig({ worker, name, d1, d1Id = null, r2 = null, layou
|
|
|
94
94
|
...(triggers ? { triggers: paidParts ? { ...triggers, crons: [...new Set([...(triggers.crons ?? []), '*/5 * * * *'])] } : triggers } : {}),
|
|
95
95
|
assets: { directory: at.dist, binding: 'ASSETS', run_worker_first: true },
|
|
96
96
|
durable_objects: { bindings: rooms },
|
|
97
|
-
migrations: [{ tag: 'v1', new_sqlite_classes: ['Table', 'Lobby'] }],
|
|
97
|
+
migrations: [{ tag: 'v1', new_sqlite_classes: ['Table', 'Lobby'] }, { tag: 'v2-gates', new_sqlite_classes: ['Gate', 'Concentrator'] }],
|
|
98
98
|
d1_databases: [{ binding: 'DB', database_name: d1, ...(d1Id ? { database_id: d1Id } : {}), migrations_dir: at.migrations }],
|
|
99
99
|
...(r2 ? { r2_buckets: [{ binding: 'MEDIA', bucket_name: r2 }, ...(paidParts ? [{ binding: 'PURCHASE_MEDIA', bucket_name: `${r2.slice(0, 52)}-purchases` }] : [])] } : {}),
|
|
100
100
|
// Workers AI, only when a server's AI guides think with it (agents_brain workers-ai; deploy adds it, 0.17.0).
|
|
@@ -291,7 +291,7 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
291
291
|
Start with coin-dash, gem-rush, ember-vale, gem-rush-3d or hero-rush-3d; all five follow this contract.
|
|
292
292
|
In games/<id>/src/rules.ts, defineRules declares entities, fields, handlers, events, rounds and companions.
|
|
293
293
|
The view calls openRoom and sends input/commands; it draws, plays sound and handles controls. It never
|
|
294
|
-
decides a hit, score, spawn, turn or result. Keep game.json room.host set to server
|
|
294
|
+
decides a hit, score, spawn, turn or result. Keep game.json room.host set to server and players.max to the chosen room size.
|
|
295
295
|
- **Movement feels immediate through prediction.** Put the shared defineMove callback in src/move.ts;
|
|
296
296
|
it runs authoritatively on the server and immediately for your own view, then reconciles. Other bodies
|
|
297
297
|
interpolate. Use the starter's sweep/slide and room.me/room.each poses; no second browser physics loop,
|
|
@@ -312,10 +312,10 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
312
312
|
then check with two browsers and play the requested mechanic with network delay. Info is not a failure.
|
|
313
313
|
Never remove rules, weaken checks, forge a probe or switch hosting to get past a message.
|
|
314
314
|
Rules hashes and room saves are automatic; character saves are player-owned, untrusted input. Room state
|
|
315
|
-
is replicated, not secret.
|
|
315
|
+
is replicated, not secret. Rooms may have hundreds of seats; choose players.max and spatial delivery explicitly and measure. Private state is not delivered.
|
|
316
316
|
- **Existing games:** change rules truth in rules and presentation in view. Browser hosting is for offline,
|
|
317
|
-
local development and private friends games.
|
|
318
|
-
|
|
317
|
+
local development and private friends games. Existing netplay games and port host helpers remain supported at their original 32-seat limit;
|
|
318
|
+
to rewrite one, read the game skill's REWRITE.md and preserve its art, view and feel. There is
|
|
319
319
|
no automatic converter. NETPLAY.md section 29 is the rules contract; earlier sections describe older APIs.
|
|
320
320
|
- Phones and computers: touch controls on phones only, keys on computers; keep the
|
|
321
321
|
centre of the screen clear during play.
|
|
@@ -473,7 +473,7 @@ studio's pinned copy, never a registry lookup of the bare name.
|
|
|
473
473
|
aimNoise, aggression, positioning }\`. The party sets it by voting on a card in the play page (the middle vote
|
|
474
474
|
wins, capped by the server's ceiling). Make a game's bots honour it: \`net.skillOf(slot)\` in their step (the
|
|
475
475
|
snippet is in \`NETPLAY.md\` section 17; \`BotBrain\` from the port kit reads it with a rebuild), and declare
|
|
476
|
-
\`caps: ['skill', 'agents']\` in \`createNetplay\` (
|
|
476
|
+
\`caps: ['skill', 'agents']\` in \`createNetplay\` (rules rooms provide the agent runtime). A build
|
|
477
477
|
from before 0.16.0 still plays on every server; the office says it predates servers until it is rebuilt.
|
|
478
478
|
- **Agent passes:** \`npx --no-install homie-studio agents pass <id> --label Claude [--server <server>]\` gives an
|
|
479
479
|
AI its way into a seat (shown once; \`agents revoke <pass>\` ends it). It sits with \`POST /<id>/api/agent\`
|
|
@@ -545,7 +545,7 @@ chat's card follows the work from then on) and, for a studio still being set up,
|
|
|
545
545
|
- The site's workers.dev address names the Cloudflare account (often after its owner): \`deploy\` keeps it in
|
|
546
546
|
\`.studio/local.json\`, which git ignores. Never copy it into a committed file. A custom domain goes in
|
|
547
547
|
studio.json as \`cloudflare.domain\`.
|
|
548
|
-
- A game's room size is
|
|
548
|
+
- A game's room size is game.json \`players.max\`, a positive integer including hundreds; measure the chosen size.
|
|
549
549
|
- Nothing in this studio needs \`~/.homie\` or a Homie box.
|
|
550
550
|
- This studio is this folder. A studio in a folder beside it (another folder with its own \`studio.json\`) is
|
|
551
551
|
somebody else's work: never read it or copy from it unless the person asks.
|
|
@@ -783,7 +783,7 @@ import rules from './rules/index.mjs';
|
|
|
783
783
|
|
|
784
784
|
hostRules(rules);
|
|
785
785
|
useTools(async () => (await import('./tools/index.mjs')).default);
|
|
786
|
-
export { default, Table, Lobby } from '@homie-rocks/studio/worker';
|
|
786
|
+
export { default, Table, Lobby, Gate, Concentrator } from '@homie-rocks/studio/worker';
|
|
787
787
|
`,
|
|
788
788
|
'site/src/rules/index.mjs': rulesIndex([]),
|
|
789
789
|
'site/migrations/0001_studio.sql': MIGRATION,
|
|
@@ -2447,14 +2447,24 @@
|
|
|
2447
2447
|
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
2448
2448
|
"## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
|
|
2449
2449
|
"## The site": "0cb8dd9a6f8a0096",
|
|
2450
|
-
"## Making games":
|
|
2450
|
+
"## Making games": [
|
|
2451
|
+
"78950c6b4eb98ea6",
|
|
2452
|
+
"796cc9389236c713",
|
|
2453
|
+
"9e44559de535d842"
|
|
2454
|
+
],
|
|
2451
2455
|
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2452
2456
|
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2453
2457
|
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2454
|
-
"## Servers and AI seats":
|
|
2458
|
+
"## Servers and AI seats": [
|
|
2459
|
+
"1ccfd549d2369a60",
|
|
2460
|
+
"d0028361fe567add"
|
|
2461
|
+
],
|
|
2455
2462
|
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2456
2463
|
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2457
|
-
"## Rules":
|
|
2464
|
+
"## Rules": [
|
|
2465
|
+
"2e6b29afb85dcf7e",
|
|
2466
|
+
"c48971c1437414c2"
|
|
2467
|
+
],
|
|
2458
2468
|
"## Telling Homie": "80b46fdd9e578db8",
|
|
2459
2469
|
"## Beta": "9d02638207a26e73"
|
|
2460
2470
|
}
|
|
@@ -2510,6 +2520,51 @@
|
|
|
2510
2520
|
}
|
|
2511
2521
|
}
|
|
2512
2522
|
}
|
|
2523
|
+
},
|
|
2524
|
+
"0.45.0": {
|
|
2525
|
+
"files": {
|
|
2526
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2527
|
+
"README.md": "684b3cca8001673c",
|
|
2528
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2529
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2530
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2531
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2532
|
+
"posts/README.md": "37841aae476bd540",
|
|
2533
|
+
"site/README.md": "732894273a32dc3f",
|
|
2534
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
2535
|
+
},
|
|
2536
|
+
"sections": {
|
|
2537
|
+
"AGENTS.md": {
|
|
2538
|
+
"(intro)": "214aa46eb3a895d5",
|
|
2539
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
2540
|
+
"## Layout": "f3a280b8c96cd068",
|
|
2541
|
+
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
2542
|
+
"## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
|
|
2543
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2544
|
+
"## Making games": [
|
|
2545
|
+
"78950c6b4eb98ea6",
|
|
2546
|
+
"bb715272fd1a615b"
|
|
2547
|
+
],
|
|
2548
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2549
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2550
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2551
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
2552
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2553
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2554
|
+
"## Rules": "c48971c1437414c2",
|
|
2555
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2556
|
+
"## Beta": "9d02638207a26e73"
|
|
2557
|
+
}
|
|
2558
|
+
},
|
|
2559
|
+
"named": {
|
|
2560
|
+
"Homie Arcade": {
|
|
2561
|
+
"sections": {
|
|
2562
|
+
"AGENTS.md": {
|
|
2563
|
+
"## Commands (all through the pinned CLI in node_modules)": "2cdd530e48170bfd"
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2513
2568
|
}
|
|
2514
2569
|
}
|
|
2515
2570
|
}
|
package/lib/typecheck.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {listOwnParts, readOrigins, vendorDir} from './parts.mjs';
|
|
2
|
+
import {resolvePartImport} from './parts-build.mjs';
|
|
1
3
|
import { runRulesCompiler } from './rules-compiler.mjs';
|
|
2
4
|
/**
|
|
3
5
|
* `homie-studio build --types` — the games' TypeScript, checked before anything is built.
|
|
@@ -7,7 +9,7 @@ import { runRulesCompiler } from './rules-compiler.mjs';
|
|
|
7
9
|
* of its own to run `npx tsc` with.
|
|
8
10
|
*
|
|
9
11
|
* - The compiler is the STUDIO'S OWN (`typescript` in its node_modules, which a new studio's package.json asks
|
|
10
|
-
* for). This optional check for legacy games keeps using that compiler. Rules games use the toolkit's own
|
|
12
|
+
* for). This optional check for legacy games and local apps keeps using that compiler. Rules games use the toolkit's own
|
|
11
13
|
* pinned compiler through typecheckRules below, on every build.
|
|
12
14
|
* - A game with a tsconfig.json of its own is checked with it. Any other game is checked the way esbuild reads
|
|
13
15
|
* it: its entry and whatever that imports, as ES2022 for a browser, strictly, with the pictures, sounds and
|
|
@@ -15,7 +17,7 @@ import { runRulesCompiler } from './rules-compiler.mjs';
|
|
|
15
17
|
* - Only errors in the game's own files stop the build. A game imports the toolkit's helpers as TypeScript
|
|
16
18
|
* source, and a creator can do nothing about a line in node_modules; those are counted and said, never fatal.
|
|
17
19
|
*
|
|
18
|
-
* The legacy check runs only when asked with --types. The rules check is mandatory.
|
|
20
|
+
* The legacy/app check runs only when asked with --types. The rules check is mandatory.
|
|
19
21
|
*/
|
|
20
22
|
import { spawnSync } from 'node:child_process';
|
|
21
23
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
@@ -116,8 +118,17 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
|
|
|
116
118
|
writeFileSync(join(dir, 'rules.d.ts'), rulesTypes({ ...checked, answerScopes: await rulesAnswerScopes(join(g.dir, 'src/rules.ts')) }, tune));
|
|
117
119
|
writeFileSync(join(dir, 'view.d.ts'), rulesViewTypes(checked));
|
|
118
120
|
writeFileSync(join(dir, 'files.d.ts'), AMBIENT);
|
|
121
|
+
const partPaths = {};
|
|
122
|
+
const origins = readOrigins(root);
|
|
123
|
+
for (const ref of [...listOwnParts(root).map(p => p.id), ...Object.keys(origins.parts)]) {
|
|
124
|
+
const base = origins.parts[ref] ? vendorDir(root, ref) : join(root, 'parts', ref);
|
|
125
|
+
partPaths[`@parts/${ref}/*`] = [join(base, '*')];
|
|
126
|
+
const entry = resolvePartImport(root, `@parts/${ref}`, origins);
|
|
127
|
+
if (entry.path) partPaths[`@parts/${ref}`] = [entry.path];
|
|
128
|
+
}
|
|
119
129
|
const project = join(dir, 'tsconfig.json');
|
|
120
130
|
writeFileSync(project, `${JSON.stringify({ compilerOptions: { ...OPTIONS, checkJs: true, paths: {
|
|
131
|
+
...partPaths,
|
|
121
132
|
...Object.fromEntries(Object.entries(JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).exports)
|
|
122
133
|
.filter(([, entry]) => entry?.types).map(([name, entry]) => [`@homie-rocks/studio/${name.slice(2)}`, [join(PACKAGE_ROOT, entry.types)]])),
|
|
123
134
|
'@homie-rocks/studio/rules': [join(dir, 'rules.d.ts')],
|
|
@@ -131,6 +142,9 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
|
|
|
131
142
|
const at = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/.exec(line);
|
|
132
143
|
if (at) {
|
|
133
144
|
const file = resolve(at[1]);
|
|
145
|
+
const missingPart = /Cannot find module '(@parts\/[^']+)'/.exec(at[5]);
|
|
146
|
+
if (missingPart) { const result = resolvePartImport(root, missingPart[1]); if (result.error) at[5] = result.error; }
|
|
147
|
+
|
|
134
148
|
// Imported game helpers count too. Only the toolkit's own implementation is outside the author's remit.
|
|
135
149
|
if (!file.startsWith(`${PACKAGE_ROOT}${sep}`) && !file.includes(`${sep}node_modules${sep}`) || file.startsWith(`${g.dir}${sep}`)) {
|
|
136
150
|
const sourceLine = readFileSync(file,'utf8').split(/\r?\n/)[Number(at[2])-1] ?? '';
|
package/lib/worker-build.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {pathToFileURL} from 'node:url';
|
|
|
4
4
|
import {existsSync,mkdirSync,writeFileSync,renameSync,readFileSync,mkdtempSync,rmSync} from 'node:fs';
|
|
5
5
|
import {join,relative,basename,sep,resolve} from 'node:path';
|
|
6
6
|
import {readConfig} from './routes.mjs';
|
|
7
|
-
import {workerDir} from './studio.mjs';
|
|
7
|
+
import {PACKAGE_ROOT,workerDir} from './studio.mjs';
|
|
8
8
|
import {paidReleases} from './parts-upload.mjs';
|
|
9
9
|
/** Preserve dynamic imports as real Worker modules: page startup never evaluates MCP libraries. */
|
|
10
10
|
export async function buildWorker(root,esbuild) {
|
|
@@ -15,7 +15,10 @@ export async function buildWorker(root,esbuild) {
|
|
|
15
15
|
const toolModule=join(root,'site/src/tools/index.mjs');
|
|
16
16
|
const pricedTools=existsSync(toolModule) && (await import(pathToFileURL(toolModule).href+'?worker='+Date.now())).default.some(tool=>tool.price);
|
|
17
17
|
if(paidReleases(root).length || existsSync(join(root,'shop.json')) || pricedTools)alias['@homie-rocks/studio/worker']='@homie-rocks/studio/worker/selling';
|
|
18
|
-
|
|
18
|
+
// Older studio entries export only Table and Lobby. Supply the new toolkit-owned
|
|
19
|
+
// classes in the generated bundle without rewriting the studio's source/customizations.
|
|
20
|
+
const contents=`export * from ${JSON.stringify(source)};\nexport { default } from ${JSON.stringify(source)};\nexport { Gate, Concentrator } from ${JSON.stringify(join(PACKAGE_ROOT,'worker/gate.mjs'))};\n`;
|
|
21
|
+
const result=await esbuild.build({stdin:{contents,resolveDir:root,sourcefile:'worker.mjs'},entryNames:'worker',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'});
|
|
19
22
|
// A watcher may already have read the previous entry while this build is
|
|
20
23
|
// publishing. Keep its entire graph available. Publish immutable modules first,
|
|
21
24
|
// then switch the single entry atomically; never remove a running graph.
|