@zhin.js/game-kit 2.0.1 → 2.0.2

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.
Files changed (116) hide show
  1. package/README.md +73 -16
  2. package/lib/base-session-service.d.ts +54 -0
  3. package/lib/base-session-service.d.ts.map +1 -0
  4. package/lib/base-session-service.js +139 -0
  5. package/lib/base-session-service.js.map +1 -0
  6. package/lib/command-alias.d.ts +3 -1
  7. package/lib/command-alias.d.ts.map +1 -1
  8. package/lib/command-alias.js.map +1 -1
  9. package/lib/command-message.d.ts +3 -0
  10. package/lib/command-message.d.ts.map +1 -1
  11. package/lib/command-message.js +0 -6
  12. package/lib/command-message.js.map +1 -1
  13. package/lib/define-game-plugin.d.ts +32 -0
  14. package/lib/define-game-plugin.d.ts.map +1 -0
  15. package/lib/define-game-plugin.js +87 -0
  16. package/lib/define-game-plugin.js.map +1 -0
  17. package/lib/deterministic-random.d.ts +16 -0
  18. package/lib/deterministic-random.d.ts.map +1 -0
  19. package/lib/deterministic-random.js +68 -0
  20. package/lib/deterministic-random.js.map +1 -0
  21. package/lib/game-events.d.ts +53 -0
  22. package/lib/game-events.d.ts.map +1 -0
  23. package/lib/game-events.js +26 -0
  24. package/lib/game-events.js.map +1 -0
  25. package/lib/game-interactive.d.ts +0 -22
  26. package/lib/game-interactive.d.ts.map +1 -1
  27. package/lib/game-interactive.js +1 -57
  28. package/lib/game-interactive.js.map +1 -1
  29. package/lib/game-records.d.ts +0 -1
  30. package/lib/game-records.d.ts.map +1 -1
  31. package/lib/game-records.js +0 -60
  32. package/lib/game-records.js.map +1 -1
  33. package/lib/game-session.d.ts +0 -59
  34. package/lib/game-session.d.ts.map +1 -1
  35. package/lib/game-session.js +0 -29
  36. package/lib/game-session.js.map +1 -1
  37. package/lib/index.d.ts +6 -9
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js +6 -9
  40. package/lib/index.js.map +1 -1
  41. package/lib/memory-db.d.ts +6 -0
  42. package/lib/memory-db.d.ts.map +1 -1
  43. package/lib/memory-db.js +8 -0
  44. package/lib/memory-db.js.map +1 -1
  45. package/lib/session-coordinator.d.ts +27 -0
  46. package/lib/session-coordinator.d.ts.map +1 -0
  47. package/lib/session-coordinator.js +57 -0
  48. package/lib/session-coordinator.js.map +1 -0
  49. package/lib/versioned-session-service.d.ts +48 -0
  50. package/lib/versioned-session-service.d.ts.map +1 -0
  51. package/lib/versioned-session-service.js +116 -0
  52. package/lib/versioned-session-service.js.map +1 -0
  53. package/package.json +7 -6
  54. package/src/base-session-service.ts +210 -0
  55. package/src/command-alias.ts +3 -1
  56. package/src/command-message.ts +4 -0
  57. package/src/define-game-plugin.ts +162 -0
  58. package/src/deterministic-random.ts +81 -0
  59. package/src/game-events.ts +75 -0
  60. package/src/game-interactive.ts +1 -80
  61. package/src/game-records.ts +0 -73
  62. package/src/game-session.ts +0 -83
  63. package/src/index.ts +6 -9
  64. package/src/memory-db.ts +12 -0
  65. package/src/session-coordinator.ts +74 -0
  66. package/src/versioned-session-service.ts +162 -0
  67. package/lib/command-help.d.ts +0 -19
  68. package/lib/command-help.d.ts.map +0 -1
  69. package/lib/command-help.js +0 -91
  70. package/lib/command-help.js.map +0 -1
  71. package/lib/game-commands.d.ts +0 -3
  72. package/lib/game-commands.d.ts.map +0 -1
  73. package/lib/game-commands.js +0 -5
  74. package/lib/game-commands.js.map +0 -1
  75. package/lib/game-hub-feature.d.ts +0 -58
  76. package/lib/game-hub-feature.d.ts.map +0 -1
  77. package/lib/game-hub-feature.js +0 -102
  78. package/lib/game-hub-feature.js.map +0 -1
  79. package/lib/game-hub-flow.d.ts +0 -9
  80. package/lib/game-hub-flow.d.ts.map +0 -1
  81. package/lib/game-hub-flow.js +0 -90
  82. package/lib/game-hub-flow.js.map +0 -1
  83. package/lib/game-hub-menu-context.d.ts +0 -25
  84. package/lib/game-hub-menu-context.d.ts.map +0 -1
  85. package/lib/game-hub-menu-context.js +0 -64
  86. package/lib/game-hub-menu-context.js.map +0 -1
  87. package/lib/game-hub-menu.d.ts +0 -37
  88. package/lib/game-hub-menu.d.ts.map +0 -1
  89. package/lib/game-hub-menu.js +0 -162
  90. package/lib/game-hub-menu.js.map +0 -1
  91. package/lib/game-hub-mount.d.ts +0 -13
  92. package/lib/game-hub-mount.d.ts.map +0 -1
  93. package/lib/game-hub-mount.js +0 -89
  94. package/lib/game-hub-mount.js.map +0 -1
  95. package/lib/game-middleware.d.ts +0 -6
  96. package/lib/game-middleware.d.ts.map +0 -1
  97. package/lib/game-middleware.js +0 -9
  98. package/lib/game-middleware.js.map +0 -1
  99. package/lib/game-onboarding.d.ts +0 -8
  100. package/lib/game-onboarding.d.ts.map +0 -1
  101. package/lib/game-onboarding.js +0 -67
  102. package/lib/game-onboarding.js.map +0 -1
  103. package/lib/help-card.d.ts +0 -12
  104. package/lib/help-card.d.ts.map +0 -1
  105. package/lib/help-card.js +0 -129
  106. package/lib/help-card.js.map +0 -1
  107. package/src/command-help.ts +0 -117
  108. package/src/game-commands.ts +0 -4
  109. package/src/game-hub-feature.ts +0 -154
  110. package/src/game-hub-flow.ts +0 -114
  111. package/src/game-hub-menu-context.ts +0 -94
  112. package/src/game-hub-menu.ts +0 -223
  113. package/src/game-hub-mount.ts +0 -147
  114. package/src/game-middleware.ts +0 -14
  115. package/src/game-onboarding.ts +0 -71
  116. package/src/help-card.ts +0 -188
package/README.md CHANGED
@@ -11,27 +11,27 @@ pnpm add @zhin.js/game-kit @zhin.js/command @zhin.js/middleware
11
11
 
12
12
  ## Runtime 插件
13
13
 
14
- 游戏包通过 `plugin.ts` 在 generation 生命周期登记大厅元数据,并由约定目录暴露交互:
14
+ 游戏包通过 `defineGamePlugin()` 声明数据库表、服务和大厅元数据。数据库双轨、
15
+ 服务发布、跨游戏会话协调、超时 cron 与 HMR 清理由 game-kit 统一装配:
15
16
 
16
17
  ```ts
17
- import { definePlugin } from '@zhin.js/plugin-runtime';
18
- import { registerRuntimeGame } from '@zhin.js/game-kit';
18
+ import { defineGamePlugin } from '@zhin.js/game-kit';
19
19
 
20
- export default definePlugin({
20
+ export default defineGamePlugin({
21
21
  name: 'my-game',
22
- setup(context) {
23
- const services = createMyGameServices();
24
- context.resources.provide(myGameServicesToken, services);
25
- const dispose = registerRuntimeGame({
26
- id: 'my-game',
27
- title: 'My Game',
28
- icon: 'GAME',
29
- description: 'A turn-based game',
30
- commandPrefix: '/my-game',
31
- quickStart: 'start',
32
- });
33
- context.lifecycle.add(dispose);
22
+ game: {
23
+ id: 'my-game',
24
+ title: 'My Game',
25
+ icon: 'GAME',
26
+ description: 'A turn-based game',
27
+ commandPrefix: '/my-game',
28
+ quickStart: 'start',
34
29
  },
30
+ tables: ['my_game_sessions'],
31
+ servicesToken: myGameServicesToken,
32
+ defineHostTables,
33
+ createServices,
34
+ session: (services) => services.sessions,
35
35
  });
36
36
  ```
37
37
 
@@ -39,9 +39,65 @@ export default definePlugin({
39
39
  - `middlewares/` 处理按钮 payload、裸文本答案和旧命令别名。
40
40
  - `registerRuntimeGame()` / `getRuntimeGames()` 是大厅 SSOT,dispose 时对称移除。
41
41
  - `DEFAULT_GAME_STALE_CRON` 与 `scheduleHostToken` 用于清理超时会话。
42
+ - `defineGamePlugin()` 在存在 `outboundHostToken` 时自动通知超时会话所在频道。
42
43
  - 每个游戏包定义自己的 typed service token;`plugin.ts` 提供资源,command/middleware
43
44
  通过 Capability Context 的 `use(token)` 消费。禁止以模块变量保存当前 SessionService。
44
45
 
46
+ ## 会话与事件
47
+
48
+ 游戏 SessionService 继承 `BaseSessionService<TRow>`,只负责构造游戏自己的初始行:
49
+
50
+ ```ts
51
+ class SessionService extends BaseSessionService<MySessionRow> {
52
+ constructor(database: MyDatabase) {
53
+ super(database, {
54
+ gameId: 'my-game',
55
+ table: 'my_game_sessions',
56
+ userFields: ['player_id'],
57
+ });
58
+ }
59
+
60
+ createSession(input: StartInput) {
61
+ return this.createRow(createInitialRow(input));
62
+ }
63
+ }
64
+ ```
65
+
66
+ 基类统一提供 `getById()`、用户/频道活动会话查询、`updateSession()`、`abortStale()`
67
+ 和跨游戏冲突检查。`gameEvents` 发布 `game:start`、`game:end`、
68
+ `turn:change`、`session:timeout`;监听器失败不会回滚已经持久化的会话变化。
69
+
70
+ SessionService 可用 `projectOutcomes` 把终局行投影为玩家结果。`defineGamePlugin()` 订阅
71
+ `game:end` 并统一写入战绩,game-flow 不再直接触碰战绩数据库。
72
+
73
+ `GameSessionCoordinator` 使用按 gameId 分组的 generation 栈。热更新先安装新代、再释放
74
+ 旧代时,查询始终落到最新服务;dispose 新代后仍可恢复旧代,避免短暂注册空窗。
75
+
76
+ ### 长生命周期与并发
77
+
78
+ 多人或长生命周期游戏继承 `VersionedSessionService<TRow>`。Session 行额外保存
79
+ `revision` 和 `processed_actions`:
80
+
81
+ ```ts
82
+ const result = await sessions.mutateSession(sessionId, {
83
+ actionId: `${messageId}:attack`,
84
+ expectedRevision: buttonRevision,
85
+ apply: (session) => ({
86
+ hp: session.hp - 10,
87
+ }),
88
+ });
89
+ ```
90
+
91
+ - `SessionActionGate` 串行化同一进程内针对同一 Session 的操作。
92
+ - `expectedRevision` 提供持久化乐观锁,拒绝旧按钮和跨进程并发覆盖。
93
+ - `processed_actions` 保存有界幂等历史,Adapter 重投不会重复结算。
94
+ - 超时清理以扫描到的 `updated_at` 作为更新条件;扫描后发生的新动作不会被误终结。
95
+ - `DeterministicRandom` 可保存 `state` 并由 `fromState()` 恢复,用于存档续玩、回放和
96
+ 大规模确定性模拟;密钥等安全场景仍必须使用 `secureRandomInt()`。
97
+
98
+ 完整参考实现见
99
+ [`plugins/games/dungeon-expedition`](../../plugins/games/dungeon-expedition/)。
100
+
45
101
  ## 交互消息
46
102
 
47
103
  ```ts
@@ -75,6 +131,7 @@ const board = buildGridKeyboard({
75
131
  ## 数据
76
132
 
77
133
  - `createInMemoryGameDb()`:测试与无数据库配置时的完整内存实现。
134
+ - `createMemoryGameServices()`:把 typed service factory 接到唯一内存实现。
78
135
  - `createHostGameDb()`:把 generation-owned `DatabaseHost` 转成 SessionService 所需接口。
79
136
  - `initGameRecordHost()` / `recordGameOutcome()`:统一战绩表与结果写入。
80
137
  - `channelKey()` / `generateSessionId()`:稳定会话身份。
@@ -0,0 +1,54 @@
1
+ import { type GameEventSession, type GameOutcome } from './game-events.js';
2
+ import { type GameSessionProvider } from './session-coordinator.js';
3
+ export interface BaseGameSessionRow extends GameEventSession {
4
+ id: string;
5
+ channel_key: string;
6
+ status: string;
7
+ updated_at: number;
8
+ created_at: number;
9
+ turn?: unknown;
10
+ }
11
+ export interface GameSessionModel<TRow extends BaseGameSessionRow> {
12
+ findAll(query?: Record<string, unknown>): Promise<TRow[]>;
13
+ findOne(query?: Record<string, unknown>): Promise<TRow | null>;
14
+ create(row: TRow): Promise<unknown>;
15
+ updateWhere(where: Record<string, unknown>, patch: Record<string, unknown>): Promise<unknown>;
16
+ }
17
+ export interface GameSessionDatabase<TRow extends BaseGameSessionRow> {
18
+ readonly models: {
19
+ get(name: string): GameSessionModel<TRow> | undefined;
20
+ };
21
+ }
22
+ export interface BaseSessionServiceOptions<TRow extends BaseGameSessionRow> {
23
+ readonly gameId: string;
24
+ readonly table: string;
25
+ readonly userFields: readonly (keyof TRow & string)[];
26
+ readonly activeStatus?: string;
27
+ readonly abortedStatus?: string;
28
+ readonly projectOutcomes?: (session: Readonly<TRow>, previous: Readonly<TRow>) => readonly GameOutcome[];
29
+ }
30
+ export type SessionTimeoutListener<TRow extends BaseGameSessionRow> = (session: TRow) => void | Promise<void>;
31
+ /**
32
+ * Shared session lifecycle for every game. Subclasses own row construction and
33
+ * game-specific tables; this module owns persistence invariants and events.
34
+ */
35
+ export declare abstract class BaseSessionService<TRow extends BaseGameSessionRow> implements GameSessionProvider {
36
+ #private;
37
+ readonly gameId: string;
38
+ protected constructor(database: GameSessionDatabase<TRow>, options: BaseSessionServiceOptions<TRow>);
39
+ getById(id: string): Promise<TRow | null>;
40
+ getActiveByChannel(channelKey: string): Promise<TRow | null>;
41
+ getActiveForUser(channelKey: string, userId: string): Promise<TRow | null>;
42
+ getLatestForUser(channelKey: string, userId: string): Promise<TRow | null>;
43
+ updateSession(id: string, patch: Partial<TRow>): Promise<void>;
44
+ /**
45
+ * Persist one transition and emit lifecycle events from the row that was
46
+ * actually stored. Versioned services use `where` for optimistic locking.
47
+ */
48
+ protected commitSession(previous: TRow, patch: Partial<TRow>, where: Record<string, unknown>): Promise<TRow | null>;
49
+ abortStale(idleMs: number, onTimeout?: SessionTimeoutListener<TRow>): Promise<number>;
50
+ registerCoordinator(): () => void;
51
+ protected createRow(row: TRow): Promise<TRow>;
52
+ protected get model(): GameSessionModel<TRow>;
53
+ }
54
+ //# sourceMappingURL=base-session-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-session-service.d.ts","sourceRoot":"","sources":["../src/base-session-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ import { gameEvents, } from './game-events.js';
2
+ import { gameSessionCoordinator } from './session-coordinator.js';
3
+ /**
4
+ * Shared session lifecycle for every game. Subclasses own row construction and
5
+ * game-specific tables; this module owns persistence invariants and events.
6
+ */
7
+ export class BaseSessionService {
8
+ gameId;
9
+ #activeStatus;
10
+ #abortedStatus;
11
+ #userFields;
12
+ #projectOutcomes;
13
+ #model;
14
+ constructor(database, options) {
15
+ const model = database.models.get(options.table);
16
+ if (!model)
17
+ throw new Error(`${options.table} not registered`);
18
+ this.#model = model;
19
+ this.gameId = options.gameId;
20
+ this.#userFields = options.userFields;
21
+ this.#projectOutcomes = options.projectOutcomes;
22
+ this.#activeStatus = options.activeStatus ?? 'active';
23
+ this.#abortedStatus = options.abortedStatus ?? 'aborted';
24
+ }
25
+ async getById(id) {
26
+ return this.#model.findOne({ id });
27
+ }
28
+ async getActiveByChannel(channelKey) {
29
+ const rows = await this.#model.findAll({
30
+ channel_key: channelKey,
31
+ status: this.#activeStatus,
32
+ });
33
+ return rows[0] ?? null;
34
+ }
35
+ async getActiveForUser(channelKey, userId) {
36
+ const rows = await this.#model.findAll({
37
+ channel_key: channelKey,
38
+ status: this.#activeStatus,
39
+ });
40
+ return rows.find((row) => this.#userFields.some((field) => row[field] === userId)) ?? null;
41
+ }
42
+ async getLatestForUser(channelKey, userId) {
43
+ const rows = await this.#model.findAll({ channel_key: channelKey });
44
+ const matches = rows.filter((row) => this.#userFields.some((field) => row[field] === userId));
45
+ return matches.reduce((latest, row) => !latest || row.updated_at >= latest.updated_at ? row : latest, null);
46
+ }
47
+ async updateSession(id, patch) {
48
+ const previous = await this.getById(id);
49
+ if (!previous)
50
+ return;
51
+ await this.commitSession(previous, patch, { id });
52
+ }
53
+ /**
54
+ * Persist one transition and emit lifecycle events from the row that was
55
+ * actually stored. Versioned services use `where` for optimistic locking.
56
+ */
57
+ async commitSession(previous, patch, where) {
58
+ const updatedAt = Date.now();
59
+ const changed = await this.#model.updateWhere(where, { ...patch, updated_at: updatedAt });
60
+ if (typeof changed === 'number' && changed === 0)
61
+ return null;
62
+ const session = await this.getById(previous.id);
63
+ if (!session)
64
+ return null;
65
+ if (patch.turn !== undefined && patch.turn !== previous.turn) {
66
+ await gameEvents.emit('turn:change', {
67
+ gameId: this.gameId,
68
+ session,
69
+ previousTurn: previous.turn,
70
+ turn: patch.turn,
71
+ });
72
+ }
73
+ if (patch.status !== undefined
74
+ && previous.status === this.#activeStatus
75
+ && patch.status !== this.#activeStatus) {
76
+ await gameEvents.emit('game:end', {
77
+ gameId: this.gameId,
78
+ session,
79
+ previousStatus: previous.status,
80
+ outcomes: this.#projectOutcomes?.(session, previous) ?? [],
81
+ });
82
+ }
83
+ return session;
84
+ }
85
+ async abortStale(idleMs, onTimeout) {
86
+ const cutoff = Date.now() - idleMs;
87
+ const rows = await this.#model.findAll({ status: this.#activeStatus });
88
+ let count = 0;
89
+ for (const row of rows) {
90
+ if (row.updated_at >= cutoff)
91
+ continue;
92
+ // The timestamp condition prevents a cleanup scan from aborting a
93
+ // session that received a player action after this row was selected.
94
+ const session = await this.commitSession(row, { status: this.#abortedStatus }, {
95
+ id: row.id,
96
+ status: this.#activeStatus,
97
+ updated_at: row.updated_at,
98
+ });
99
+ if (!session)
100
+ continue;
101
+ await gameEvents.emit('session:timeout', {
102
+ gameId: this.gameId,
103
+ session,
104
+ idleMs,
105
+ });
106
+ // A broken outbound adapter must not prevent later stale sessions from
107
+ // being finalized. Persistence and lifecycle events already succeeded.
108
+ if (onTimeout) {
109
+ try {
110
+ await onTimeout(session);
111
+ }
112
+ catch {
113
+ // Notification is best-effort; the next cleanup pass stays healthy.
114
+ }
115
+ }
116
+ count += 1;
117
+ }
118
+ return count;
119
+ }
120
+ registerCoordinator() {
121
+ return gameSessionCoordinator.register(this);
122
+ }
123
+ async createRow(row) {
124
+ const userIds = [];
125
+ for (const field of this.#userFields) {
126
+ const value = row[field];
127
+ if (typeof value === 'string')
128
+ userIds.push(value);
129
+ }
130
+ await gameSessionCoordinator.assertAvailable(this.gameId, row.channel_key, userIds);
131
+ await this.#model.create(row);
132
+ await gameEvents.emit('game:start', { gameId: this.gameId, session: row });
133
+ return row;
134
+ }
135
+ get model() {
136
+ return this.#model;
137
+ }
138
+ }
139
+ //# sourceMappingURL=base-session-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-session-service.js","sourceRoot":"","sources":["../src/base-session-service.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { type MiddlewareContext } from '@zhin.js/middleware';
2
2
  import type { Message } from '@zhin.js/core/runtime';
3
+ type AliasReply = Parameters<Message['$reply']>[0];
3
4
  /**
4
5
  * 游戏 legacy 命令别名路由。
5
6
  *
@@ -15,7 +16,8 @@ export interface GameCommandAliasRoute {
15
16
  * 与 `commands/<cmd>/[action:string=].ts` 的 execute 逻辑保持一致:
16
17
  * action 为别名后剩余文本(无参数时为 '');返回回复文本,null/undefined 表示放行。
17
18
  */
18
- run(action: string, input: unknown, context: MiddlewareContext<Message>): Promise<string | null | undefined>;
19
+ run(action: string, input: unknown, context: MiddlewareContext<Message>): Promise<AliasReply | null | undefined>;
19
20
  }
20
21
  export declare function defineGameCommandAliasMiddleware(route: GameCommandAliasRoute): Readonly<import("@zhin.js/middleware").MiddlewareDefinition<Message, unknown>>;
22
+ export {};
21
23
  //# sourceMappingURL=command-alias.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command-alias.d.ts","sourceRoot":"","sources":["../src/command-alias.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;OAGG;IACH,GAAG,CACD,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACvC;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,qBAAqB,kFAmB5E"}
1
+ {"version":3,"file":"command-alias.d.ts","sourceRoot":"","sources":["../src/command-alias.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;OAGG;IACH,GAAG,CACD,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,qBAAqB,kFAmB5E"}
@@ -1 +1 @@
1
- {"version":3,"file":"command-alias.js","sourceRoot":"","sources":["../src/command-alias.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0B,MAAM,qBAAqB,CAAC;AAyB/E,MAAM,UAAU,gCAAgC,CAAC,KAA4B;IAC3E,OAAO,gBAAgB,CAAU;QAC/B,MAAM,EAAE,SAAS;QACjB,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI;YACxB,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YACtD,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1D,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,MAAM,IAAI,EAAE,CAAC;gBACb,OAAO;YACT,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACtE,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAClC,OAAO;YACT,CAAC;YACD,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"command-alias.js","sourceRoot":"","sources":["../src/command-alias.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0B,MAAM,qBAAqB,CAAC;AA2B/E,MAAM,UAAU,gCAAgC,CAAC,KAA4B;IAC3E,OAAO,gBAAgB,CAAU;QAC/B,MAAM,EAAE,SAAS;QACjB,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI;YACxB,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YACtD,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1D,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3C,MAAM,IAAI,EAAE,CAAC;gBACb,OAAO;YACT,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACtE,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAClC,OAAO;YACT,CAAC;YACD,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -4,6 +4,9 @@
4
4
  * Runtime Message: { adapter, target, sender?, metadata? }
5
5
  * Classic Message: { $adapter, $endpoint, $channel, $sender }
6
6
  */
7
+ import type { SendContent } from '@zhin.js/core';
8
+ /** A game returns semantic content; Runtime owns native rendering and text fallback. */
9
+ export type GameReply = SendContent;
7
10
  export interface GameMessageLike {
8
11
  $adapter: string;
9
12
  $endpoint: string;
@@ -1 +1 @@
1
- {"version":3,"file":"command-message.d.ts","sourceRoot":"","sources":["../src/command-message.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAKvC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AA8BD,gFAAgF;AAChF,wBAAgB,gBAAgB,IAAI,eAAe,CAOlD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAoBvE"}
1
+ {"version":3,"file":"command-message.d.ts","sourceRoot":"","sources":["../src/command-message.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,wFAAwF;AACxF,MAAM,MAAM,SAAS,GAAG,WAAW,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAKvC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AA8BD,gFAAgF;AAChF,wBAAgB,gBAAgB,IAAI,eAAe,CAOlD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAoBvE"}
@@ -1,9 +1,3 @@
1
- /**
2
- * Map Plugin Runtime CommandContext.input → classic Message-like shape used by game SessionService.
3
- *
4
- * Runtime Message: { adapter, target, sender?, metadata? }
5
- * Classic Message: { $adapter, $endpoint, $channel, $sender }
6
- */
7
1
  function isClassicMessage(input) {
8
2
  if (!input || typeof input !== 'object')
9
3
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"command-message.js","sourceRoot":"","sources":["../src/command-message.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,CAAC,CAAC,QAAQ,IAAI,IAAI;QAClB,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,CAAC,CAAC,OAAO,IAAI,IAAI;QACjB,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAC9B,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IAMtC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,SAAS,IAAI,CAAC;QACd,QAAQ,IAAI,CAAC;QACb,QAAQ,IAAI,CAAC;QACb,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CACvD,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,QAAQ,EAAE,SAAS;QACnB,SAAS,EAAE,SAAS;QACpB,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE;QAC1C,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;KACxC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,IAAI,gBAAgB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE1C,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC;QACnE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC,CAAC;QACvE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,IAAI,SAAS,CAAC,CAAC;QACvE,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC;QACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;QAClE,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC;QACpE,OAAO;YACL,QAAQ,EAAE,OAAO;YACjB,SAAS,EAAE,QAAQ;YACnB,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE;YAC9C,OAAO,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE;SAC5C,CAAC;IACJ,CAAC;IAED,OAAO,gBAAgB,EAAE,CAAC;AAC5B,CAAC"}
1
+ {"version":3,"file":"command-message.js","sourceRoot":"","sources":["../src/command-message.ts"],"names":[],"mappings":"AAsBA,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,CAAC,CAAC,QAAQ,IAAI,IAAI;QAClB,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,CAAC,CAAC,OAAO,IAAI,IAAI;QACjB,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAC9B,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IAMtC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,SAAS,IAAI,CAAC;QACd,QAAQ,IAAI,CAAC;QACb,QAAQ,IAAI,CAAC;QACb,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CACvD,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,QAAQ,EAAE,SAAS;QACnB,SAAS,EAAE,SAAS;QACpB,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE;QAC1C,OAAO,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;KACxC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,IAAI,gBAAgB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE1C,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC;QACnE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC,CAAC;QACvE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,IAAI,SAAS,CAAC,CAAC;QACvE,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC;QACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;QAClE,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC;QACpE,OAAO;YACL,QAAQ,EAAE,OAAO;YACjB,SAAS,EAAE,QAAQ;YACnB,QAAQ,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE;YAC9C,OAAO,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE;SAC5C,CAAC;IACJ,CAAC;IAED,OAAO,gBAAgB,EAAE,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type DatabaseHost, type PluginMetadata, type PluginSetupContext, type Token } from '@zhin.js/plugin-runtime';
2
+ import { type InMemoryGameDb } from './memory-db.js';
3
+ import { type RuntimeRegisteredGame } from './runtime-hub.js';
4
+ import type { BaseGameSessionRow, BaseSessionService } from './base-session-service.js';
5
+ export interface GamePluginSession<TRow extends BaseGameSessionRow = BaseGameSessionRow> {
6
+ abortStale(idleMs: number, onTimeout?: (session: TRow) => void | Promise<void>): Promise<number>;
7
+ registerCoordinator?(): () => void;
8
+ }
9
+ export interface DefineGamePluginOptions<TServices, TRow extends BaseGameSessionRow = BaseGameSessionRow, TConfig = unknown> {
10
+ readonly name: string;
11
+ readonly metadata?: PluginMetadata;
12
+ readonly game: RuntimeRegisteredGame;
13
+ readonly tables: readonly string[];
14
+ readonly servicesToken: Token<TServices>;
15
+ readonly defineHostTables: (host: DatabaseHost) => void;
16
+ readonly createServices: (database: InMemoryGameDb) => TServices;
17
+ readonly session: (services: TServices) => GamePluginSession<TRow> | BaseSessionService<TRow>;
18
+ readonly stale?: {
19
+ readonly cron?: string;
20
+ readonly idleMs?: number;
21
+ readonly description?: string;
22
+ readonly message?: string | ((session: TRow) => string);
23
+ readonly onTimeout?: (session: TRow, context: PluginSetupContext<TConfig>) => void | Promise<void>;
24
+ } | false;
25
+ readonly setup?: (context: PluginSetupContext<TConfig>, services: TServices) => void | (() => void) | Promise<void | (() => void)>;
26
+ }
27
+ /**
28
+ * Declarative Plugin Runtime assembly for games. Database selection, service
29
+ * publication, Hub registration, stale cleanup and HMR disposal live here.
30
+ */
31
+ export declare function defineGamePlugin<TServices, TRow extends BaseGameSessionRow = BaseGameSessionRow, TConfig = unknown>(options: DefineGamePluginOptions<TServices, TRow, TConfig>): Readonly<import("@zhin.js/plugin-runtime").PluginDefinition<TConfig>>;
32
+ //# sourceMappingURL=define-game-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-game-plugin.d.ts","sourceRoot":"","sources":["../src/define-game-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACX,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AAOxB,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,iBAAiB,CAChC,IAAI,SAAS,kBAAkB,GAAG,kBAAkB;IAEpD,UAAU,CACR,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClD,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,mBAAmB,CAAC,IAAI,MAAM,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,uBAAuB,CACtC,SAAS,EACT,IAAI,SAAS,kBAAkB,GAAG,kBAAkB,EACpD,OAAO,GAAG,OAAO;IAEjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxD,QAAQ,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,SAAS,CAAC;IACjE,QAAQ,CAAC,OAAO,EAAE,CAChB,QAAQ,EAAE,SAAS,KAChB,iBAAiB,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC;QACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,CACnB,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,KACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAC3B,GAAG,KAAK,CAAC;IACV,QAAQ,CAAC,KAAK,CAAC,EAAE,CACf,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,EACpC,QAAQ,EAAE,SAAS,KAChB,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;CACzD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EACT,IAAI,SAAS,kBAAkB,GAAG,kBAAkB,EACpD,OAAO,GAAG,OAAO,EAEjB,OAAO,EAAE,uBAAuB,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,yEA+E3D"}
@@ -0,0 +1,87 @@
1
+ import { databaseHostToken, definePlugin, outboundHostToken, scheduleHostToken, } from '@zhin.js/plugin-runtime';
2
+ import { createHostGameDb, createInMemoryGameDb, } from './memory-db.js';
3
+ import { initGameRecordHost, recordGameOutcome, } from './game-records.js';
4
+ import { gameEvents } from './game-events.js';
5
+ import { DEFAULT_GAME_STALE_CRON, DEFAULT_GAME_STALE_IDLE_MS, registerRuntimeGame, } from './runtime-hub.js';
6
+ /**
7
+ * Declarative Plugin Runtime assembly for games. Database selection, service
8
+ * publication, Hub registration, stale cleanup and HMR disposal live here.
9
+ */
10
+ export function defineGamePlugin(options) {
11
+ return definePlugin({
12
+ name: options.name,
13
+ metadata: options.metadata,
14
+ async setup(context) {
15
+ let database;
16
+ if (context.resources.has(databaseHostToken)) {
17
+ const host = context.resources.use(databaseHostToken);
18
+ options.defineHostTables(host);
19
+ database = createHostGameDb(host, options.tables);
20
+ context.lifecycle.add(initGameRecordHost(host));
21
+ }
22
+ else {
23
+ database = createInMemoryGameDb(options.tables);
24
+ }
25
+ const services = options.createServices(database);
26
+ context.resources.provide(options.servicesToken, services);
27
+ const session = options.session(services);
28
+ const disposeCoordinator = session.registerCoordinator?.();
29
+ if (disposeCoordinator)
30
+ context.lifecycle.add(disposeCoordinator);
31
+ context.lifecycle.add(registerRuntimeGame(options.game));
32
+ context.lifecycle.add(gameEvents.on('game:end', async (event) => {
33
+ if (event.gameId !== options.game.id || event.outcomes.length === 0)
34
+ return;
35
+ const row = event.session;
36
+ if (!row.adapter || !row.endpoint || !row.channel_type || !row.channel_id)
37
+ return;
38
+ for (const outcome of event.outcomes) {
39
+ await recordGameOutcome({
40
+ $adapter: row.adapter,
41
+ $endpoint: row.endpoint,
42
+ $channel: { type: row.channel_type, id: row.channel_id },
43
+ $sender: {
44
+ id: outcome.userId,
45
+ name: outcome.userName ?? outcome.userId,
46
+ },
47
+ }, event.gameId, outcome.result, outcome.score);
48
+ }
49
+ }));
50
+ if (options.stale !== false && context.resources.has(scheduleHostToken)) {
51
+ const stale = options.stale ?? {};
52
+ const schedule = context.resources.use(scheduleHostToken);
53
+ context.lifecycle.add(schedule.register({
54
+ id: `${options.game.id}/abort-stale`,
55
+ cron: stale.cron ?? DEFAULT_GAME_STALE_CRON,
56
+ description: stale.description
57
+ ?? `Abort stale ${options.name} sessions`,
58
+ async execute() {
59
+ await session.abortStale(stale.idleMs ?? DEFAULT_GAME_STALE_IDLE_MS, async (row) => {
60
+ await stale.onTimeout?.(row, context);
61
+ if (!context.resources.has(outboundHostToken))
62
+ return;
63
+ if (!row.adapter || !row.endpoint
64
+ || !row.channel_type || !row.channel_id)
65
+ return;
66
+ const outbound = context.resources.use(outboundHostToken);
67
+ await outbound.send({
68
+ adapter: row.adapter,
69
+ endpointId: row.endpoint,
70
+ channelType: row.channel_type,
71
+ channelId: row.channel_id,
72
+ content: typeof stale.message === 'function'
73
+ ? stale.message(row)
74
+ : stale.message
75
+ ?? `${options.game.icon} ${options.game.title}对局因长时间无操作已结束。`,
76
+ });
77
+ });
78
+ },
79
+ }));
80
+ }
81
+ const dispose = await options.setup?.(context, services);
82
+ if (dispose)
83
+ context.lifecycle.add(dispose);
84
+ },
85
+ });
86
+ }
87
+ //# sourceMappingURL=define-game-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-game-plugin.js","sourceRoot":"","sources":["../src/define-game-plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Serializable xorshift32 generator for replayable game simulations.
3
+ * Security-sensitive randomness must continue using secureRandomInt.
4
+ */
5
+ export declare class DeterministicRandom {
6
+ #private;
7
+ private constructor();
8
+ static fromSeed(seed: string | number): DeterministicRandom;
9
+ static fromState(state: number): DeterministicRandom;
10
+ get state(): number;
11
+ nextUint32(): number;
12
+ int(maxExclusive: number): number;
13
+ intInclusive(min: number, max: number): number;
14
+ item<T>(items: readonly T[]): T;
15
+ }
16
+ //# sourceMappingURL=deterministic-random.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministic-random.d.ts","sourceRoot":"","sources":["../src/deterministic-random.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,qBAAa,mBAAmB;;IAG9B,OAAO;IAIP,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,mBAAmB;IAM3D,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB;IAIpD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,UAAU,IAAI,MAAM;IASpB,GAAG,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAgBjC,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;IAO9C,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC;CAMhC"}
@@ -0,0 +1,68 @@
1
+ const UINT32_RANGE = 0x1_0000_0000;
2
+ const ZERO_STATE_FALLBACK = 0x6d2b79f5;
3
+ /**
4
+ * Serializable xorshift32 generator for replayable game simulations.
5
+ * Security-sensitive randomness must continue using secureRandomInt.
6
+ */
7
+ export class DeterministicRandom {
8
+ #state;
9
+ constructor(state) {
10
+ this.#state = normalizeState(state);
11
+ }
12
+ static fromSeed(seed) {
13
+ return new DeterministicRandom(typeof seed === 'number' ? seed : hashSeed(seed));
14
+ }
15
+ static fromState(state) {
16
+ return new DeterministicRandom(state);
17
+ }
18
+ get state() {
19
+ return this.#state;
20
+ }
21
+ nextUint32() {
22
+ let value = this.#state;
23
+ value ^= value << 13;
24
+ value ^= value >>> 17;
25
+ value ^= value << 5;
26
+ this.#state = value >>> 0;
27
+ return this.#state;
28
+ }
29
+ int(maxExclusive) {
30
+ if (!Number.isSafeInteger(maxExclusive)
31
+ || maxExclusive <= 0
32
+ || maxExclusive > UINT32_RANGE) {
33
+ throw new RangeError('maxExclusive must be an integer between 1 and 2^32');
34
+ }
35
+ const limit = Math.floor(UINT32_RANGE / maxExclusive) * maxExclusive;
36
+ let value;
37
+ do {
38
+ value = this.nextUint32();
39
+ } while (value >= limit);
40
+ return value % maxExclusive;
41
+ }
42
+ intInclusive(min, max) {
43
+ if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max) || max < min) {
44
+ throw new RangeError('invalid inclusive random range');
45
+ }
46
+ return min + this.int(max - min + 1);
47
+ }
48
+ item(items) {
49
+ if (items.length === 0) {
50
+ throw new RangeError('cannot choose from an empty array');
51
+ }
52
+ return items[this.int(items.length)];
53
+ }
54
+ }
55
+ function normalizeState(state) {
56
+ if (!Number.isFinite(state))
57
+ throw new RangeError('state must be finite');
58
+ return (state >>> 0) || ZERO_STATE_FALLBACK;
59
+ }
60
+ function hashSeed(seed) {
61
+ let hash = 0x811c9dc5;
62
+ for (let index = 0; index < seed.length; index += 1) {
63
+ hash ^= seed.charCodeAt(index);
64
+ hash = Math.imul(hash, 0x01000193);
65
+ }
66
+ return hash >>> 0;
67
+ }
68
+ //# sourceMappingURL=deterministic-random.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministic-random.js","sourceRoot":"","sources":["../src/deterministic-random.ts"],"names":[],"mappings":"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"}