@looop-games/cli 0.1.30 → 0.1.31

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 CHANGED
@@ -14,6 +14,15 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.1.31] - 2026-08-21
18
+
19
+ ### Changed
20
+ - `looop dev` no longer starts a multiplayer server for a **single-player** game.
21
+ A game with no `config({ multiplayer: true })` runs its whole simulation in the
22
+ browser, so the local PartyKit server was dead weight — dev now skips it and
23
+ says so. Structural hot-reload (adding an entity/trait) still works. A
24
+ multiplayer game is unaffected.
25
+
17
26
  ## [0.1.30] - 2026-08-21
18
27
 
19
28
  ### Fixed
package/lib/dev.mjs CHANGED
@@ -17,7 +17,7 @@ import { createStaticServer } from './static-server.mjs';
17
17
  import { createLlmShim, DEFAULT_API_BASE } from './llm-shim.mjs';
18
18
  import { getToken, getApiBase } from './config.mjs';
19
19
  import { resolvePorts, portInUse, killPort, lanIp } from './ports.mjs';
20
- import { assertNoInertOverride, isFrameworkV2, scanPrimitives } from './primitives.mjs';
20
+ import { assertNoInertOverride, isFrameworkV2, isSinglePlayerV2, scanPrimitives } from './primitives.mjs';
21
21
  import { buildDevRoomServer, DEV_OVERRIDES_DIR } from './room-server.mjs';
22
22
  import { createFileWatcher, createRoomReloader } from './room-reload.mjs';
23
23
  import { hasDeclaredAssets, assetsPageUrl } from './assets-page.mjs';
@@ -151,8 +151,40 @@ export async function dev({ cwd = process.cwd(), port, noMp = process.env.NO_MP
151
151
  log(`→ static :${ports.static} (serving ${project.dir}, engine ${engine.version}, auto-reload on)`);
152
152
 
153
153
  // ─── multiplayer (partykit on the bundle's room code) ───
154
- if (noMp) {
155
- log(`→ multiplayer :${ports.mp} (skippedNO_MP=1)`);
154
+ // A single-player framework-v2 game (no config({ multiplayer: true })) runs its
155
+ // authoritative sim in-process via LocalRoom its client never opens a socket,
156
+ // so standing up partykit here just burns a port and a workerd process. Skip it,
157
+ // mirroring publish, which deploys no per-game room worker for the same game.
158
+ const singlePlayer = isSinglePlayerV2({ projectDir: project.dir, skeleton: roomSkeleton });
159
+ if (noMp || singlePlayer) {
160
+ log(
161
+ singlePlayer && !noMp
162
+ ? `→ multiplayer :${ports.mp} (skipped — single-player; add config({ multiplayer: true }) to enable)`
163
+ : `→ multiplayer :${ports.mp} (skipped — NO_MP=1)`,
164
+ );
165
+ // No room worker, but the client still assembles its graph against the injected
166
+ // skeleton — so a STRUCTURAL edit (new entity/trait) must rebuild the skeleton
167
+ // and push it to the static server, or the page reloads onto a stale skeleton
168
+ // and assembleGraph throws ("re-ran N ticks but the skeleton has M"). The full
169
+ // partykit reloader below does this as a side effect; single-player needs a
170
+ // stripped-down version with no child to respawn. (NO_MP on a real multiplayer
171
+ // game has no client to serve a skeleton to, so it needs none.)
172
+ if (singlePlayer) {
173
+ const skelWatcher = createFileWatcher({
174
+ files: roomInputs,
175
+ dirs: ['entities', 'components', 'overrides/shared/ui/room'].map((d) => join(project.dir, d)),
176
+ onChange: async () => {
177
+ try {
178
+ const built = await buildDevRoomServer({ projectDir: project.dir, engine });
179
+ staticServer.setSkeleton(built.skeleton ?? null);
180
+ skelWatcher.setFiles(built.inputs);
181
+ } catch (err) {
182
+ log(`[skeleton] rebuild failed — fix the error and save again: ${err.message}`);
183
+ }
184
+ },
185
+ });
186
+ servers.push({ close: () => skelWatcher.close() });
187
+ }
156
188
  } else {
157
189
  if (tookOver && (await portInUse(ports.mp))) {
158
190
  log(`→ multiplayer :${ports.mp} (our stale server — reclaiming)`);
@@ -166,6 +166,17 @@ export function isServerBacked(projectDir) {
166
166
  });
167
167
  }
168
168
 
169
+ // VQ7M2K: a framework-v2 game is SINGLE-PLAYER unless it declares
170
+ // config({ multiplayer: true }). A single-player game runs its authoritative sim
171
+ // in-process (the LocalRoom transport) with no wire — so dev stands up no partykit
172
+ // room and publish deploys no per-game room worker. The multiplayer flag lives in
173
+ // the built skeleton's config (emitSkeleton → graph.config, default false); a game
174
+ // with no skeleton (a v1 primitive/entity game) is never single-player in this
175
+ // sense and keeps its room. Callers pass the skeleton they already built.
176
+ export function isSinglePlayerV2({ projectDir, skeleton }) {
177
+ return isFrameworkV2(projectDir) && skeleton?.config?.multiplayer !== true;
178
+ }
179
+
169
180
  // The one cycle shadowing makes possible, caught in the creator's own words.
170
181
  //
171
182
  // A NEW primitive may subclass an engine one — that is the RECOMMENDED way to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looop-games/cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Looop game development CLI — dev server, login, and publishing for standalone Looop games.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",