@idosgames/module-sdk 0.5.0 → 0.7.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/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- 'use strict';var S=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function x(e){return typeof e=="string"&&S.test(e)}function M(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var l=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function c(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,r){if(!u(t))return `${r||"payload"} must be an object`;for(let[n,o]of Object.entries(e)){let i=r?`${r}.${n}`:n,a=t[n];if(typeof o!="string"){let p=f(o,a,i);if(p)return p;continue}let d=l.exec(o);if(!d)return `${i}: invalid descriptor "${o}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>c(s,p)))return `${i} must contain only ${s} values`;continue}if(!c(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[r,n]of Object.entries(e)){let o=t?`${t}.${r}`:r;if(typeof n=="string"){if(!l.test(n))throw new Error(`shape(): field "${o}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,o);else throw new Error(`shape(): field "${o}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!x(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var E={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var T=["currency-bar","wallet","status","account","menu"];function w(e){return typeof e=="string"&&T.includes(e)}function R(e){return e}exports.SHARED_UI_ROLES=T;exports.TOPIC_NAME_RE=S;exports.defineModule=R;exports.defineTopic=v;exports.hostTopics=E;exports.isSharedUiRole=w;exports.isTopicName=x;exports.shape=h;exports.topicNamespace=M;
1
+ 'use strict';var x=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function S(e){return typeof e=="string"&&x.test(e)}function T(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var c=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function l(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,o){if(!u(t))return `${o||"payload"} must be an object`;for(let[n,r]of Object.entries(e)){let i=o?`${o}.${n}`:n,a=t[n];if(typeof r!="string"){let p=f(r,a,i);if(p)return p;continue}let d=c.exec(r);if(!d)return `${i}: invalid descriptor "${r}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>l(s,p)))return `${i} must contain only ${s} values`;continue}if(!l(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[o,n]of Object.entries(e)){let r=t?`${t}.${o}`:o;if(typeof n=="string"){if(!c.test(n))throw new Error(`shape(): field "${r}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,r);else throw new Error(`shape(): field "${r}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!S(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var b={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var M=["currency-bar","wallet","status","account","menu","modes"];function R(e){return typeof e=="string"&&M.includes(e)}function D(e){return e}exports.SHARED_UI_ROLES=M;exports.TOPIC_NAME_RE=x;exports.defineModule=D;exports.defineTopic=v;exports.hostTopics=b;exports.isSharedUiRole=R;exports.isTopicName=S;exports.shape=h;exports.topicNamespace=T;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { IDosGamesClient } from '@idosgames/core';
1
+ import { RealtimeSession, IDosGamesClient } from '@idosgames/core';
2
2
  export { IDosGamesClient, SdkEvents } from '@idosgames/core';
3
3
  import { ComponentType } from 'react';
4
4
 
@@ -111,7 +111,7 @@ interface ModuleEventsManifest {
111
111
  * each one has to mean the same thing across the whole catalog. A new role is a minor release of
112
112
  * this package; anything that does not fit a role is plain events + SDK data.
113
113
  */
114
- declare const SHARED_UI_ROLES: readonly ["currency-bar", "wallet", "status", "account", "menu"];
114
+ declare const SHARED_UI_ROLES: readonly ["currency-bar", "wallet", "status", "account", "menu", "modes"];
115
115
  type SharedUiRole = (typeof SHARED_UI_ROLES)[number];
116
116
  /** Static declaration on a module (`Module.sharedUi`). */
117
117
  interface ModuleSharedUi {
@@ -125,8 +125,8 @@ interface ModuleSharedUi {
125
125
  /**
126
126
  * Roles this module draws INSIDE ITS OWN MODE while nobody `provides` them for everybody — a
127
127
  * template with a full-screen UI (bottom tab bar) puts "Log out" and the player's ID into its own
128
- * header instead of having the host float them over its layout. Only "account" and "menu" are
129
- * host-drawn, so those are the roles this matters for: the host hides its floating copy while this
128
+ * header instead of having the host float them over its layout. Only "account", "menu" and "modes"
129
+ * are host-drawn, so those are the roles this matters for: the host hides its floating copy while this
130
130
  * module's mode is on screen and keeps drawing it in every other mode. `shouldDraw` of every module
131
131
  * is unchanged.
132
132
  */
@@ -215,7 +215,7 @@ interface FeatureScreenProps {
215
215
  close(): void;
216
216
  }
217
217
  interface FeatureDefinition {
218
- /** Unique across the game — conventionally the owning module's id without "-ui" ("store"). */
218
+ /** Unique across the game — conventionally the owning module's id ("store"). */
219
219
  id: string;
220
220
  /** Menu label, already in the player's language. */
221
221
  label: string;
@@ -228,17 +228,27 @@ interface FeatureDefinition {
228
228
  primary?: boolean;
229
229
  /** The screen. Rendered by the presenter, inside the kit provider and the client provider. */
230
230
  Screen: ComponentType<FeatureScreenProps>;
231
+ /**
232
+ * Whether the title has something for this screen yet. Default `true`. A screen that depends on
233
+ * title data (a shop without offers is an empty tab) registers with `false` and calls
234
+ * `setAvailable(id, true)` once it has checked — so the tab never flashes in and out.
235
+ */
236
+ available?: boolean;
231
237
  }
232
- /** A registered feature plus its live badge. */
238
+ /** A registered feature plus its live badge and availability. */
233
239
  interface FeatureEntry extends FeatureDefinition {
234
240
  badge: number;
235
241
  /** The module that registered it. */
236
242
  moduleId: string;
243
+ /** Menus list only available features; `open()` of an unavailable one is refused. */
244
+ available: boolean;
245
+ /** Why it is hidden, in plain words — shown to the AI Coder's agent, never to players. */
246
+ unavailableReason?: string;
237
247
  }
238
248
  /**
239
249
  * Shows a feature's screen. The menu owner registers one; the most recent presenter wins, and
240
250
  * unregistering hands the job back to the previous one (the lobby unmounts when the player switches
241
- * to another mode → the HUD's ☰ or the host's button presents again).
251
+ * to a game → the host's window presents again).
242
252
  */
243
253
  type FeaturePresenter = (id: string, args?: Record<string, unknown>) => boolean;
244
254
  /** `ctx.features` — the registry of the game's features, shared by every module of the session. */
@@ -248,16 +258,105 @@ interface FeatureRegistry {
248
258
  /** Features in registration order (menus sort them themselves). */
249
259
  list(): FeatureEntry[];
250
260
  get(id: string): FeatureEntry | undefined;
251
- /** Opens a feature's screen through the current presenter. `false` — no such feature or no presenter. */
261
+ /**
262
+ * Opens a feature's screen through the current presenter. `false` — no such feature, it is not
263
+ * available on this title, or no presenter.
264
+ */
252
265
  open(id: string, args?: Record<string, unknown>): boolean;
253
266
  /** The "something to claim" counter of a feature; 0 hides it. */
254
267
  setBadge(id: string, count: number): void;
268
+ /**
269
+ * Show or hide a feature by what the title has (see `FeatureDefinition.available`). `reason` says
270
+ * why it is hidden ("no store offers configured on the title") — the agent reads it.
271
+ */
272
+ setAvailable(id: string, available: boolean, reason?: string): void;
255
273
  /** Called on every registration, removal and badge change. */
256
274
  subscribe(listener: () => void): () => void;
257
275
  /** For menu owners: become the one who shows screens. Returns an unregister function. */
258
276
  setPresenter(presenter: FeaturePresenter): () => void;
259
277
  }
260
278
 
279
+ /** A room setting the host picks when creating a room for the game (guest mode, cheats…). */
280
+ interface MultiplayerSetting {
281
+ /** Key in `MultiplayerRoom.settings`. */
282
+ id: string;
283
+ /** Label, already in the player's language. */
284
+ label: string;
285
+ /** A choice of options, or an on/off switch. */
286
+ kind: "choice" | "toggle";
287
+ /** For `choice`: value + label of every option. */
288
+ options?: Array<{
289
+ value: string;
290
+ label: string;
291
+ }>;
292
+ /** Value when the host does not change it. */
293
+ default: string | boolean;
294
+ }
295
+ /** What a game declares about playing online (`ctx.multiplayer.registerGame`). */
296
+ interface MultiplayerGame {
297
+ /** The game's mode (its route id) — the room's game and where "Play" takes the room. */
298
+ mode: string;
299
+ /** Label of the game in the rooms screen; defaults to the route's label. */
300
+ label?: string;
301
+ /**
302
+ * Who connects to whom: `"Host"` (star — the host links to everyone, the others only to the host)
303
+ * or `"Mesh"` (everyone to everyone). Default `"Mesh"`.
304
+ */
305
+ topology?: "Host" | "Mesh";
306
+ /**
307
+ * `"P2P"` — the game has its own chat over the DataChannels, the lobby draws none;
308
+ * `"ServerRelay"` (default) — the lobby's room chat goes through the server.
309
+ */
310
+ chatMode?: "P2P" | "ServerRelay";
311
+ /** Most players in one room (the title's limit caps it). Default 8. */
312
+ maxPlayers?: number;
313
+ /**
314
+ * A player may join a room whose game is already running — the rooms screen lets them in and
315
+ * `start` gets called for them right away (the game's own "late joiner" path). Default `false`.
316
+ */
317
+ joinInProgress?: boolean;
318
+ /** Room settings the host picks on creation; the game reads them in `room.settings`. */
319
+ settings?: MultiplayerSetting[];
320
+ /**
321
+ * The room goes into this game: switch the game to network play over `room.session`. Called on
322
+ * EVERY member's device (the host's first) — after the app switched to the game's mode.
323
+ */
324
+ start(room: MultiplayerRoom): void | Promise<void>;
325
+ }
326
+ /** The room handed to a game. */
327
+ interface MultiplayerRoom {
328
+ /** The live session — links, messages, members, host changes. Opened by the rooms system. */
329
+ readonly session: RealtimeSession;
330
+ /** The game's mode. */
331
+ readonly mode: string;
332
+ /** Whether this player created the game (the host at start). Live host: `session.hostId`. */
333
+ readonly isHost: boolean;
334
+ /** The room's settings as the host picked them (defaults filled in). */
335
+ readonly settings: Readonly<Record<string, string | boolean>>;
336
+ /**
337
+ * The game joins a running room (`joinInProgress`) rather than starting together with it.
338
+ */
339
+ readonly lateJoin: boolean;
340
+ /** Leave the room for good: closes the session and the rooms screen forgets it. */
341
+ leave(): Promise<void>;
342
+ }
343
+ /** What `ctx.features.open("multiplayer", args)` understands. */
344
+ interface MultiplayerScreenArgs {
345
+ /** Show only this game's rooms and preselect it in "Create a room". */
346
+ mode?: string;
347
+ /** Open straight on "Create a room" (a game's "Open for friends"). */
348
+ create?: boolean;
349
+ }
350
+ /** `ctx.multiplayer` — the host's registry of games that play online, shared by every module. */
351
+ interface MultiplayerRegistry {
352
+ /** A game declares that it plays online. Returns an unregister function. */
353
+ registerGame(game: MultiplayerGame): () => void;
354
+ getGame(mode: string): MultiplayerGame | undefined;
355
+ listGames(): MultiplayerGame[];
356
+ /** Called when a game is registered or removed. */
357
+ subscribe(listener: () => void): () => void;
358
+ }
359
+
261
360
  /** What a module is, for catalog/UI purposes. Not every module is a game. */
262
361
  type ModuleType = "game" | "app" | "ai-app";
263
362
  /**
@@ -309,6 +408,20 @@ interface ModuleManifest {
309
408
  * `"feature"`.
310
409
  */
311
410
  type?: "template" | "feature";
411
+ /**
412
+ * Part of the base: the platform installs this system into EVERY new project together with the base
413
+ * (sign-in + the lobby) — only profile, inventory, invite, friends, quests and mailbox (owner's rule
414
+ * 29.09.2026). Every other module is installed only when it is needed: a game brings the ones it
415
+ * `uses`, the AI Coder installs any other one as soon as the task needs what it `provides`. A creator removes it like any module.
416
+ */
417
+ inBase?: boolean;
418
+ /**
419
+ * A game's modules: catalog ids of the systems this game plugs into (`ctx.multiplayer` →
420
+ * `"multiplayer"`, `ctx.content` → `"workshop"`, …). The platform installs them together with the game
421
+ * — a new title on this game gets them, and so does a project the AI Coder adds the game to.
422
+ * `npm run check` makes a module that registers in `ctx.multiplayer` / `ctx.content` list them.
423
+ */
424
+ uses?: string[];
312
425
  /** Overrides the catalog display name; defaults to `ModuleMeta.name` from `module.ts`. */
313
426
  name?: string;
314
427
  /** One-line pitch shown on the catalog card. */
@@ -409,8 +522,20 @@ interface ModuleContext {
409
522
  */
410
523
  features: FeatureRegistry;
411
524
  /**
412
- * Switch the host to another mode (a route id registered by any module) — e.g. the Workshop opens a
413
- * world and hands the screen to the game. Unknown ids are ignored.
525
+ * Games that play online and the rooms system that runs them (see `MultiplayerGame`). A game
526
+ * registers here (`registerGame`) and takes the room in `start`; it never creates or joins rooms
527
+ * itself. Without the multiplayer system installed nothing ever calls `start`.
528
+ */
529
+ multiplayer: MultiplayerRegistry;
530
+ /**
531
+ * The app's modes — every route any module registered, and which one is on screen. The lobby
532
+ * builds its "Play" buttons from it; a game can tell whether a lobby exists to return to.
533
+ */
534
+ modes: ModeRegistry;
535
+ /**
536
+ * Switch the host to another mode (a route id registered by any module) — e.g. the lobby starts a
537
+ * game, the Workshop opens a world and hands the screen to the game. Unknown ids are ignored. Called
538
+ * inside `setup()` it picks the mode the app starts in.
414
539
  */
415
540
  navigate(modeId: string): void;
416
541
  /**
@@ -535,6 +660,30 @@ interface RouteEntry {
535
660
  icon?: string;
536
661
  order?: number;
537
662
  }
663
+ /** A mode of the app as every module sees it (`ctx.modes`): a registered route plus its owner. */
664
+ interface ModeEntry {
665
+ id: string;
666
+ label: string;
667
+ icon?: string;
668
+ /** Resolved order (0 when the route gave none); the list is sorted by it. */
669
+ order: number;
670
+ /** The module that registered the route. */
671
+ moduleId: string;
672
+ /** That module's `meta.type` — a lobby offers "Play" for `game` modes. */
673
+ moduleType: ModuleType;
674
+ }
675
+ /** `ctx.modes` — the modes of the app, live. Switch with `ctx.navigate(id)`. */
676
+ interface ModeRegistry {
677
+ /**
678
+ * Every mode, sorted by `order`. Filled once every module's `setup()` has run — read it at render
679
+ * time or subscribe, not inside `setup()`.
680
+ */
681
+ list(): ModeEntry[];
682
+ /** The mode on screen; `null` until the app is shown. */
683
+ current(): string | null;
684
+ /** Called when the list or the current mode changes. Returns the unsubscribe function. */
685
+ subscribe(listener: () => void): () => void;
686
+ }
538
687
  /**
539
688
  * @deprecated The pre-0.2 string-topic bus. `ctx.events` is now `ModuleEvents` (typed topic tokens,
540
689
  * see `defineTopic`); this type stays only so older code that named it still compiles.
@@ -547,4 +696,4 @@ interface SharedEventBus<Events extends Record<string, unknown> = Record<string,
547
696
  /** Identity helper that pins a module literal to the `Module` type for editor help and errors. */
548
697
  declare function defineModule(module: Module): Module;
549
698
 
550
- export { type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LocalContentItem, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
699
+ export { type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LocalContentItem, type ModeEntry, type ModeRegistry, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type MultiplayerGame, type MultiplayerRegistry, type MultiplayerRoom, type MultiplayerScreenArgs, type MultiplayerSetting, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IDosGamesClient } from '@idosgames/core';
1
+ import { RealtimeSession, IDosGamesClient } from '@idosgames/core';
2
2
  export { IDosGamesClient, SdkEvents } from '@idosgames/core';
3
3
  import { ComponentType } from 'react';
4
4
 
@@ -111,7 +111,7 @@ interface ModuleEventsManifest {
111
111
  * each one has to mean the same thing across the whole catalog. A new role is a minor release of
112
112
  * this package; anything that does not fit a role is plain events + SDK data.
113
113
  */
114
- declare const SHARED_UI_ROLES: readonly ["currency-bar", "wallet", "status", "account", "menu"];
114
+ declare const SHARED_UI_ROLES: readonly ["currency-bar", "wallet", "status", "account", "menu", "modes"];
115
115
  type SharedUiRole = (typeof SHARED_UI_ROLES)[number];
116
116
  /** Static declaration on a module (`Module.sharedUi`). */
117
117
  interface ModuleSharedUi {
@@ -125,8 +125,8 @@ interface ModuleSharedUi {
125
125
  /**
126
126
  * Roles this module draws INSIDE ITS OWN MODE while nobody `provides` them for everybody — a
127
127
  * template with a full-screen UI (bottom tab bar) puts "Log out" and the player's ID into its own
128
- * header instead of having the host float them over its layout. Only "account" and "menu" are
129
- * host-drawn, so those are the roles this matters for: the host hides its floating copy while this
128
+ * header instead of having the host float them over its layout. Only "account", "menu" and "modes"
129
+ * are host-drawn, so those are the roles this matters for: the host hides its floating copy while this
130
130
  * module's mode is on screen and keeps drawing it in every other mode. `shouldDraw` of every module
131
131
  * is unchanged.
132
132
  */
@@ -215,7 +215,7 @@ interface FeatureScreenProps {
215
215
  close(): void;
216
216
  }
217
217
  interface FeatureDefinition {
218
- /** Unique across the game — conventionally the owning module's id without "-ui" ("store"). */
218
+ /** Unique across the game — conventionally the owning module's id ("store"). */
219
219
  id: string;
220
220
  /** Menu label, already in the player's language. */
221
221
  label: string;
@@ -228,17 +228,27 @@ interface FeatureDefinition {
228
228
  primary?: boolean;
229
229
  /** The screen. Rendered by the presenter, inside the kit provider and the client provider. */
230
230
  Screen: ComponentType<FeatureScreenProps>;
231
+ /**
232
+ * Whether the title has something for this screen yet. Default `true`. A screen that depends on
233
+ * title data (a shop without offers is an empty tab) registers with `false` and calls
234
+ * `setAvailable(id, true)` once it has checked — so the tab never flashes in and out.
235
+ */
236
+ available?: boolean;
231
237
  }
232
- /** A registered feature plus its live badge. */
238
+ /** A registered feature plus its live badge and availability. */
233
239
  interface FeatureEntry extends FeatureDefinition {
234
240
  badge: number;
235
241
  /** The module that registered it. */
236
242
  moduleId: string;
243
+ /** Menus list only available features; `open()` of an unavailable one is refused. */
244
+ available: boolean;
245
+ /** Why it is hidden, in plain words — shown to the AI Coder's agent, never to players. */
246
+ unavailableReason?: string;
237
247
  }
238
248
  /**
239
249
  * Shows a feature's screen. The menu owner registers one; the most recent presenter wins, and
240
250
  * unregistering hands the job back to the previous one (the lobby unmounts when the player switches
241
- * to another mode → the HUD's ☰ or the host's button presents again).
251
+ * to a game → the host's window presents again).
242
252
  */
243
253
  type FeaturePresenter = (id: string, args?: Record<string, unknown>) => boolean;
244
254
  /** `ctx.features` — the registry of the game's features, shared by every module of the session. */
@@ -248,16 +258,105 @@ interface FeatureRegistry {
248
258
  /** Features in registration order (menus sort them themselves). */
249
259
  list(): FeatureEntry[];
250
260
  get(id: string): FeatureEntry | undefined;
251
- /** Opens a feature's screen through the current presenter. `false` — no such feature or no presenter. */
261
+ /**
262
+ * Opens a feature's screen through the current presenter. `false` — no such feature, it is not
263
+ * available on this title, or no presenter.
264
+ */
252
265
  open(id: string, args?: Record<string, unknown>): boolean;
253
266
  /** The "something to claim" counter of a feature; 0 hides it. */
254
267
  setBadge(id: string, count: number): void;
268
+ /**
269
+ * Show or hide a feature by what the title has (see `FeatureDefinition.available`). `reason` says
270
+ * why it is hidden ("no store offers configured on the title") — the agent reads it.
271
+ */
272
+ setAvailable(id: string, available: boolean, reason?: string): void;
255
273
  /** Called on every registration, removal and badge change. */
256
274
  subscribe(listener: () => void): () => void;
257
275
  /** For menu owners: become the one who shows screens. Returns an unregister function. */
258
276
  setPresenter(presenter: FeaturePresenter): () => void;
259
277
  }
260
278
 
279
+ /** A room setting the host picks when creating a room for the game (guest mode, cheats…). */
280
+ interface MultiplayerSetting {
281
+ /** Key in `MultiplayerRoom.settings`. */
282
+ id: string;
283
+ /** Label, already in the player's language. */
284
+ label: string;
285
+ /** A choice of options, or an on/off switch. */
286
+ kind: "choice" | "toggle";
287
+ /** For `choice`: value + label of every option. */
288
+ options?: Array<{
289
+ value: string;
290
+ label: string;
291
+ }>;
292
+ /** Value when the host does not change it. */
293
+ default: string | boolean;
294
+ }
295
+ /** What a game declares about playing online (`ctx.multiplayer.registerGame`). */
296
+ interface MultiplayerGame {
297
+ /** The game's mode (its route id) — the room's game and where "Play" takes the room. */
298
+ mode: string;
299
+ /** Label of the game in the rooms screen; defaults to the route's label. */
300
+ label?: string;
301
+ /**
302
+ * Who connects to whom: `"Host"` (star — the host links to everyone, the others only to the host)
303
+ * or `"Mesh"` (everyone to everyone). Default `"Mesh"`.
304
+ */
305
+ topology?: "Host" | "Mesh";
306
+ /**
307
+ * `"P2P"` — the game has its own chat over the DataChannels, the lobby draws none;
308
+ * `"ServerRelay"` (default) — the lobby's room chat goes through the server.
309
+ */
310
+ chatMode?: "P2P" | "ServerRelay";
311
+ /** Most players in one room (the title's limit caps it). Default 8. */
312
+ maxPlayers?: number;
313
+ /**
314
+ * A player may join a room whose game is already running — the rooms screen lets them in and
315
+ * `start` gets called for them right away (the game's own "late joiner" path). Default `false`.
316
+ */
317
+ joinInProgress?: boolean;
318
+ /** Room settings the host picks on creation; the game reads them in `room.settings`. */
319
+ settings?: MultiplayerSetting[];
320
+ /**
321
+ * The room goes into this game: switch the game to network play over `room.session`. Called on
322
+ * EVERY member's device (the host's first) — after the app switched to the game's mode.
323
+ */
324
+ start(room: MultiplayerRoom): void | Promise<void>;
325
+ }
326
+ /** The room handed to a game. */
327
+ interface MultiplayerRoom {
328
+ /** The live session — links, messages, members, host changes. Opened by the rooms system. */
329
+ readonly session: RealtimeSession;
330
+ /** The game's mode. */
331
+ readonly mode: string;
332
+ /** Whether this player created the game (the host at start). Live host: `session.hostId`. */
333
+ readonly isHost: boolean;
334
+ /** The room's settings as the host picked them (defaults filled in). */
335
+ readonly settings: Readonly<Record<string, string | boolean>>;
336
+ /**
337
+ * The game joins a running room (`joinInProgress`) rather than starting together with it.
338
+ */
339
+ readonly lateJoin: boolean;
340
+ /** Leave the room for good: closes the session and the rooms screen forgets it. */
341
+ leave(): Promise<void>;
342
+ }
343
+ /** What `ctx.features.open("multiplayer", args)` understands. */
344
+ interface MultiplayerScreenArgs {
345
+ /** Show only this game's rooms and preselect it in "Create a room". */
346
+ mode?: string;
347
+ /** Open straight on "Create a room" (a game's "Open for friends"). */
348
+ create?: boolean;
349
+ }
350
+ /** `ctx.multiplayer` — the host's registry of games that play online, shared by every module. */
351
+ interface MultiplayerRegistry {
352
+ /** A game declares that it plays online. Returns an unregister function. */
353
+ registerGame(game: MultiplayerGame): () => void;
354
+ getGame(mode: string): MultiplayerGame | undefined;
355
+ listGames(): MultiplayerGame[];
356
+ /** Called when a game is registered or removed. */
357
+ subscribe(listener: () => void): () => void;
358
+ }
359
+
261
360
  /** What a module is, for catalog/UI purposes. Not every module is a game. */
262
361
  type ModuleType = "game" | "app" | "ai-app";
263
362
  /**
@@ -309,6 +408,20 @@ interface ModuleManifest {
309
408
  * `"feature"`.
310
409
  */
311
410
  type?: "template" | "feature";
411
+ /**
412
+ * Part of the base: the platform installs this system into EVERY new project together with the base
413
+ * (sign-in + the lobby) — only profile, inventory, invite, friends, quests and mailbox (owner's rule
414
+ * 29.09.2026). Every other module is installed only when it is needed: a game brings the ones it
415
+ * `uses`, the AI Coder installs any other one as soon as the task needs what it `provides`. A creator removes it like any module.
416
+ */
417
+ inBase?: boolean;
418
+ /**
419
+ * A game's modules: catalog ids of the systems this game plugs into (`ctx.multiplayer` →
420
+ * `"multiplayer"`, `ctx.content` → `"workshop"`, …). The platform installs them together with the game
421
+ * — a new title on this game gets them, and so does a project the AI Coder adds the game to.
422
+ * `npm run check` makes a module that registers in `ctx.multiplayer` / `ctx.content` list them.
423
+ */
424
+ uses?: string[];
312
425
  /** Overrides the catalog display name; defaults to `ModuleMeta.name` from `module.ts`. */
313
426
  name?: string;
314
427
  /** One-line pitch shown on the catalog card. */
@@ -409,8 +522,20 @@ interface ModuleContext {
409
522
  */
410
523
  features: FeatureRegistry;
411
524
  /**
412
- * Switch the host to another mode (a route id registered by any module) — e.g. the Workshop opens a
413
- * world and hands the screen to the game. Unknown ids are ignored.
525
+ * Games that play online and the rooms system that runs them (see `MultiplayerGame`). A game
526
+ * registers here (`registerGame`) and takes the room in `start`; it never creates or joins rooms
527
+ * itself. Without the multiplayer system installed nothing ever calls `start`.
528
+ */
529
+ multiplayer: MultiplayerRegistry;
530
+ /**
531
+ * The app's modes — every route any module registered, and which one is on screen. The lobby
532
+ * builds its "Play" buttons from it; a game can tell whether a lobby exists to return to.
533
+ */
534
+ modes: ModeRegistry;
535
+ /**
536
+ * Switch the host to another mode (a route id registered by any module) — e.g. the lobby starts a
537
+ * game, the Workshop opens a world and hands the screen to the game. Unknown ids are ignored. Called
538
+ * inside `setup()` it picks the mode the app starts in.
414
539
  */
415
540
  navigate(modeId: string): void;
416
541
  /**
@@ -535,6 +660,30 @@ interface RouteEntry {
535
660
  icon?: string;
536
661
  order?: number;
537
662
  }
663
+ /** A mode of the app as every module sees it (`ctx.modes`): a registered route plus its owner. */
664
+ interface ModeEntry {
665
+ id: string;
666
+ label: string;
667
+ icon?: string;
668
+ /** Resolved order (0 when the route gave none); the list is sorted by it. */
669
+ order: number;
670
+ /** The module that registered the route. */
671
+ moduleId: string;
672
+ /** That module's `meta.type` — a lobby offers "Play" for `game` modes. */
673
+ moduleType: ModuleType;
674
+ }
675
+ /** `ctx.modes` — the modes of the app, live. Switch with `ctx.navigate(id)`. */
676
+ interface ModeRegistry {
677
+ /**
678
+ * Every mode, sorted by `order`. Filled once every module's `setup()` has run — read it at render
679
+ * time or subscribe, not inside `setup()`.
680
+ */
681
+ list(): ModeEntry[];
682
+ /** The mode on screen; `null` until the app is shown. */
683
+ current(): string | null;
684
+ /** Called when the list or the current mode changes. Returns the unsubscribe function. */
685
+ subscribe(listener: () => void): () => void;
686
+ }
538
687
  /**
539
688
  * @deprecated The pre-0.2 string-topic bus. `ctx.events` is now `ModuleEvents` (typed topic tokens,
540
689
  * see `defineTopic`); this type stays only so older code that named it still compiles.
@@ -547,4 +696,4 @@ interface SharedEventBus<Events extends Record<string, unknown> = Record<string,
547
696
  /** Identity helper that pins a module literal to the `Module` type for editor help and errors. */
548
697
  declare function defineModule(module: Module): Module;
549
698
 
550
- export { type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LocalContentItem, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
699
+ export { type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LocalContentItem, type ModeEntry, type ModeRegistry, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type MultiplayerGame, type MultiplayerRegistry, type MultiplayerRoom, type MultiplayerScreenArgs, type MultiplayerSetting, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var S=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function x(e){return typeof e=="string"&&S.test(e)}function M(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var l=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function c(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,r){if(!u(t))return `${r||"payload"} must be an object`;for(let[n,o]of Object.entries(e)){let i=r?`${r}.${n}`:n,a=t[n];if(typeof o!="string"){let p=f(o,a,i);if(p)return p;continue}let d=l.exec(o);if(!d)return `${i}: invalid descriptor "${o}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>c(s,p)))return `${i} must contain only ${s} values`;continue}if(!c(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[r,n]of Object.entries(e)){let o=t?`${t}.${r}`:r;if(typeof n=="string"){if(!l.test(n))throw new Error(`shape(): field "${o}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,o);else throw new Error(`shape(): field "${o}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!x(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var E={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var T=["currency-bar","wallet","status","account","menu"];function w(e){return typeof e=="string"&&T.includes(e)}function R(e){return e}export{T as SHARED_UI_ROLES,S as TOPIC_NAME_RE,R as defineModule,v as defineTopic,E as hostTopics,w as isSharedUiRole,x as isTopicName,h as shape,M as topicNamespace};
1
+ var x=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function S(e){return typeof e=="string"&&x.test(e)}function T(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var c=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function l(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,o){if(!u(t))return `${o||"payload"} must be an object`;for(let[n,r]of Object.entries(e)){let i=o?`${o}.${n}`:n,a=t[n];if(typeof r!="string"){let p=f(r,a,i);if(p)return p;continue}let d=c.exec(r);if(!d)return `${i}: invalid descriptor "${r}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>l(s,p)))return `${i} must contain only ${s} values`;continue}if(!l(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[o,n]of Object.entries(e)){let r=t?`${t}.${o}`:o;if(typeof n=="string"){if(!c.test(n))throw new Error(`shape(): field "${r}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,r);else throw new Error(`shape(): field "${r}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!S(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var b={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var M=["currency-bar","wallet","status","account","menu","modes"];function R(e){return typeof e=="string"&&M.includes(e)}function D(e){return e}export{M as SHARED_UI_ROLES,x as TOPIC_NAME_RE,D as defineModule,v as defineTopic,b as hostTopics,R as isSharedUiRole,S as isTopicName,h as shape,T as topicNamespace};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@idosgames/module-sdk",
3
- "version": "0.5.0",
4
- "description": "The composable-module contract for the iDosGames host shell: Module / ModuleContext types plus tiny authoring helpers. Framework-neutral at runtime — a module can be a game (Three/Phaser), a plain app, or an AI app.",
3
+ "version": "0.7.0",
4
+ "description": "The composable-module contract for the iDosGames host shell: Module / ModuleContext types plus tiny authoring helpers. Framework-neutral at runtime — a module can be a game (Three/Phaser), a plain app, or an AI app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "iDos Games",
@@ -44,7 +44,7 @@
44
44
  "build": "tsup",
45
45
  "typecheck": "tsc --noEmit -p tsconfig.json"
46
46
  },
47
- "//": "@idosgames/core is a real range, not '*': a published contract must name the version of core whose IDosGamesClient/SdkEvents shapes it references. Bump in step with core (see RELEASING.md). react is an OPTIONAL peer — the contract references React's ComponentType type only (a UI panel is a React component), which is erased at build, so a runtime-only module needs no React.",
47
+ "//": "@idosgames/core is a real range, not '*': a published contract must name the version of core whose IDosGamesClient/SdkEvents shapes it references. Bump in step with core (see RELEASING.md). react is an OPTIONAL peer — the contract references React's ComponentType type only (a UI panel is a React component), which is erased at build, so a runtime-only module needs no React.",
48
48
  "dependencies": {
49
49
  "@idosgames/core": "^0.19.0"
50
50
  },