@homie-rocks/studio 0.47.0 → 0.48.1
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 +91 -0
- package/guides/game/RULES.md +189 -4
- package/guides/game/SKILL.md +27 -0
- package/lib/doctor.mjs +8 -6
- package/lib/mcp-tools.mjs +2 -1
- package/lib/physics-mesh.mjs +40 -0
- package/lib/provider-key.mjs +21 -0
- package/lib/rules-build.mjs +18 -10
- package/lib/rules-check-worker.mjs +3 -0
- package/lib/rules-check.mjs +1 -1
- package/lib/rules-guard.mjs +4 -3
- package/lib/rules-types.mjs +1 -1
- package/lib/template-history.json +84 -0
- package/netplay/NETPLAY.md +20 -0
- package/package.json +11 -2
- package/rules/core.ts +63 -4
- package/rules/host.ts +38 -9
- package/rules/math.ts +10 -1
- package/rules/pack.ts +4 -2
- package/rules/physics-engine.ts +73 -0
- package/rules/physics.ts +26 -0
- package/rules/rules.ts +14 -4
- package/rules/substeps.ts +39 -0
- package/rules/types.ts +3 -2
- package/rules/view.ts +150 -44
- package/worker/build-id.mjs +1 -1
- package/worker/hosted.mjs +16 -0
- package/worker/index.mjs +19 -4
- package/worker/version.mjs +1 -1
package/worker/hosted.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { compileMap, compileRules } from '../rules/rules.ts';
|
|
|
19
19
|
/** game id → { rules, tune, map, settings, seats, build } as the build wrote it, and what compiling each gave. */
|
|
20
20
|
const games = new Map();
|
|
21
21
|
const compiled = new Map();
|
|
22
|
+
const readiness = new WeakMap();
|
|
22
23
|
|
|
23
24
|
/** The table of this studio's server-hosted games (the build's site/src/rules/index.mjs). Called once, at load. */
|
|
24
25
|
export function hostRules(table) {
|
|
@@ -33,6 +34,21 @@ export function hostRules(table) {
|
|
|
33
34
|
export const hostedGame = (id) => games.has(id);
|
|
34
35
|
export const hostedBuild = (id) => games.get(id);
|
|
35
36
|
|
|
37
|
+
/** Optional engines load at room start. Ordinary games keep their synchronous startup path. */
|
|
38
|
+
export function prepareHost(id) {
|
|
39
|
+
const entry = games.get(id);
|
|
40
|
+
if (!entry?.initialize) return;
|
|
41
|
+
const previous = readiness.get(entry);
|
|
42
|
+
if (previous === true) return;
|
|
43
|
+
if (previous) return previous;
|
|
44
|
+
const pending = Promise.resolve().then(() => entry.initialize()).then(
|
|
45
|
+
() => { readiness.set(entry, true); },
|
|
46
|
+
error => { readiness.delete(entry); throw error; },
|
|
47
|
+
);
|
|
48
|
+
readiness.set(entry, pending);
|
|
49
|
+
return pending;
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
/** A game's rules, compiled once per isolate. Throws, naming the declaration, when they do not fit the contract. */
|
|
37
53
|
export function compiledFor(id) {
|
|
38
54
|
if (compiled.has(id)) return compiled.get(id);
|
package/worker/index.mjs
CHANGED
|
@@ -89,7 +89,7 @@ import { appRole, openPath } from './app-format.mjs';
|
|
|
89
89
|
*/
|
|
90
90
|
import { playerEnabled, EMBED_GAME_JS } from './embed.mjs';
|
|
91
91
|
import { NetRoom, WATCH_POLICIES, departure, errorLine, versionOf } from './room.mjs';
|
|
92
|
-
import { hostedGame, hostedBuild, compiledFor, startHost } from './hosted.mjs';
|
|
92
|
+
import { hostedGame, hostedBuild, compiledFor, prepareHost, startHost } from './hosted.mjs';
|
|
93
93
|
import { roomStore, restoreDelay, LOOSE_SAVE } from './room-store.mjs';
|
|
94
94
|
import { appCors, isAppOrigin } from './standalone.mjs';
|
|
95
95
|
import { ROOM_ID, badRoomPage, embedAncestors, embedPreview, frameAncestors, noWatchPage, playPage, shellKey, shellStandIn, watchPage } from './pages.mjs';
|
|
@@ -1592,13 +1592,13 @@ export class Table {
|
|
|
1592
1592
|
this.say({ ev: 'persist-failed', build: hostedBuild(this.game)?.build, error: this.storageHealth.message });
|
|
1593
1593
|
}
|
|
1594
1594
|
|
|
1595
|
-
readRoom(validateHost = false) {
|
|
1595
|
+
readRoom(validateHost = false, alreadyRead) {
|
|
1596
1596
|
if (this.skipSave) { this.skipSave = false; return null; }
|
|
1597
1597
|
try {
|
|
1598
1598
|
const boot = this.durable?.boots();
|
|
1599
1599
|
this.persistentStored = boot?.persistent === true;
|
|
1600
1600
|
if (boot?.retired === true) { this.clearRoom(); return null; }
|
|
1601
|
-
const saved = this.durable?.read();
|
|
1601
|
+
const saved = alreadyRead ?? this.durable?.read();
|
|
1602
1602
|
if (!saved) return null;
|
|
1603
1603
|
if (saved.v !== 1 || typeof saved.game !== 'string' || typeof saved.room !== 'string' ||
|
|
1604
1604
|
this.game && saved.game !== this.game || this.code && saved.room !== this.code ||
|
|
@@ -1759,7 +1759,18 @@ export class Table {
|
|
|
1759
1759
|
if (!room || !this.hostRt) {
|
|
1760
1760
|
if (this.ctx.storage.sql?.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'save'").toArray().length) {
|
|
1761
1761
|
this.durable ??= this.openStore(false);
|
|
1762
|
-
|
|
1762
|
+
let saved = this.cleanupPending ? null : this.readRoom();
|
|
1763
|
+
if (saved) {
|
|
1764
|
+
const generation = this.cleanupGeneration;
|
|
1765
|
+
const preparing = prepareHost(saved.game);
|
|
1766
|
+
if (preparing) {
|
|
1767
|
+
await preparing;
|
|
1768
|
+
// A join or office action can arrive while the optional engine loads.
|
|
1769
|
+
// Re-read that lifecycle instead of writing the pre-await checkpoint.
|
|
1770
|
+
if (this.room !== room || this.hostRt || this.cleanupGeneration !== generation) return this.runRoomAlarm();
|
|
1771
|
+
}
|
|
1772
|
+
saved = this.readRoom(true, saved);
|
|
1773
|
+
}
|
|
1763
1774
|
if (saved) {
|
|
1764
1775
|
this.game = saved.game; this.code = saved.room;
|
|
1765
1776
|
if (this.isPersistent()) return;
|
|
@@ -2167,6 +2178,8 @@ export class Table {
|
|
|
2167
2178
|
if (url.pathname === '/__recall') {
|
|
2168
2179
|
const { account } = await request.json();
|
|
2169
2180
|
return this.withCharacter(account, async () => {
|
|
2181
|
+
const preparing = prepareHost(game);
|
|
2182
|
+
if (preparing) await preparing;
|
|
2170
2183
|
this.ensureHost(room, true);
|
|
2171
2184
|
if (!this.hostRt || this.persistentFailed || room.hostFailed) return json({ ok: false }, 503);
|
|
2172
2185
|
await this.flushCharacters();
|
|
@@ -2232,6 +2245,8 @@ export class Table {
|
|
|
2232
2245
|
const said = url.searchParams.has('cur');
|
|
2233
2246
|
if (said) { room.setStall(url.searchParams.get('stall')); room.setCurrent(url.searchParams.get('cur') || null); }
|
|
2234
2247
|
// Restore before applying the Worker's current policy: a save must not replace a newer policy.
|
|
2248
|
+
const preparing = prepareHost(game);
|
|
2249
|
+
if (preparing) await preparing;
|
|
2235
2250
|
this.ensureHost(room, url.searchParams.get('host') === 'server');
|
|
2236
2251
|
// The room's policy (section 17), composed by the Worker for every socket: a newer one than the room's applies.
|
|
2237
2252
|
const pol = decodeFacts(url.searchParams.get('pol'));
|
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.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.48.1';
|