@zhin.js/game-kit
v1.1.18
Published
Zhin.js 游戏插件通用工具包
Readme
@zhin.js/game-kit
Zhin.js Plugin Runtime 游戏工具包。它提供交互键盘、游戏会话、战绩、Runtime 游戏大厅、
DatabaseHost 适配和文本 fallback,不依赖 zhin.js facade。
安装
pnpm add @zhin.js/game-kit @zhin.js/command @zhin.js/middlewareRuntime 插件
游戏包通过 defineGamePlugin() 声明数据库表、服务和大厅元数据。数据库双轨、
服务发布、跨游戏会话协调、超时 cron 与 HMR 清理由 game-kit 统一装配:
import { defineGamePlugin } from '@zhin.js/game-kit';
export default defineGamePlugin({
name: 'my-game',
game: {
id: 'my-game',
title: 'My Game',
icon: 'GAME',
description: 'A turn-based game',
commandPrefix: '/my-game',
commandPath: 'my-game',
quickStart: 'start',
},
tables: ['my_game_sessions'],
servicesToken: myGameServicesToken,
defineHostTables,
createServices,
session: (services) => services.sessions,
});commands/<name>/[[action]]/index.ts定义命令(可选参数;在defineCommand({ params })中声明action的类型与默认值)。middlewares/处理按钮 payload、裸文本答案和旧命令别名。defineGamePlugin()通过addGame发布 Game capability;GameIndex是整代 snapshot 的大厅 SSOT,命令通过context.project(gameFeatureId)读取。DEFAULT_GAME_STALE_CRON与scheduleHostToken用于清理超时会话。defineGamePlugin()在存在outboundHostToken时自动通知超时会话所在频道。- 对局创建时将
message.locale保存到会话行的可空locale字段;后续棋盘、提示和超时通知沿用该语言。旧行缺少语言时保留原有中文流程,不因升级后项目或玩家语言变化而切换。新开一局重新读取创建者的语言。 messageFromCommandInput(input)透传消息的locale与formatCommand;命令也可显式传入第二参数{ locale, formatCommand }。buildGridKeyboard({ locale })本地化默认数字提示,自定义提示保持原文。- 大厅通过
GameIndex.formatHelp(locale, context.formatCommand)从当前命令索引解析commandPath与真实插件 owner;不会将旧的commandPrefix当作当前环境的调用入口。缺少真实入口时只展示游戏说明。 - 每个游戏包定义自己的 typed service token;
plugin.ts提供资源,command/middleware 通过 Capability Context 的use(token)消费。禁止以模块变量保存当前 SessionService。
会话与事件
游戏 SessionService 继承 BaseSessionService<TRow>,只负责构造游戏自己的初始行:
class SessionService extends BaseSessionService<MySessionRow> {
constructor(database: MyDatabase) {
super(database, {
gameId: 'my-game',
table: 'my_game_sessions',
userFields: ['player_id'],
});
}
createSession(input: StartInput) {
return this.createRow(createInitialRow(input));
}
}基类统一提供 getById()、用户/频道活动会话查询、updateSession()、abortStale()
和跨游戏冲突检查。每个 SessionService 的 events 发布 game:start、game:end、
turn:change、session:timeout;事件总线属于服务实例,监听器失败不会回滚已经持久化的
会话变化。
SessionService 可用 projectOutcomes 把终局行投影为玩家结果。defineGamePlugin() 订阅
game:end 并统一写入战绩,game-flow 不再直接触碰战绩数据库。
GameIndex 从当前 snapshot 的 Game capability 一次性构造 GameSessionCoordinator,再把
该代的可用性视图绑定到各 SessionService。不同 Root 与不同 generation 没有共享注册表,
旧代释放不会改变新代的协调关系。
长生命周期与并发
多人或长生命周期游戏继承 VersionedSessionService<TRow>。Session 行额外保存
revision 和 processed_actions:
const result = await sessions.mutateSession(sessionId, {
actionId: `${messageId}:attack`,
expectedRevision: buttonRevision,
apply: (session) => ({
hp: session.hp - 10,
}),
});SessionActionGate串行化同一进程内针对同一 Session 的操作。expectedRevision提供持久化乐观锁,拒绝旧按钮和跨进程并发覆盖。processed_actions保存有界幂等历史,Adapter 重投不会重复结算。- 超时清理以扫描到的
updated_at作为更新条件;扫描后发生的新动作不会被误终结。 DeterministicRandom可保存state并由fromState()恢复,用于存档续玩、回放和 大规模确定性模拟;密钥等安全场景仍必须使用secureRandomInt()。
完整参考实现见
plugins/games/dungeon-expedition。
交互消息
import { buildChoiceKeyboard, buildGridKeyboard } from '@zhin.js/game-kit';
const menu = buildChoiceKeyboard({
gamePrefix: 'adv',
sessionId: 's123',
narrative: 'Choose a path',
choices: [
{ id: 'enter', label: 'Enter' },
{ id: 'leave', label: 'Leave', style: 'danger' },
],
interactionProfile: 'gameplay',
});
const board = buildGridKeyboard({
gamePrefix: 'ttt',
sessionId: 's123',
rows: 3,
cols: 3,
cells: Array.from({ length: 9 }, () => ({ state: 0, label: '.', disabled: false })),
statusLine: 'Your turn',
});plainTextFromSendContent() 会在无编辑能力或 text-only 路径移除按钮并保留正文;平台差异
仍由 Adapter 出站链处理。messageFromCommandInput() 把 Runtime Message 转成游戏引擎使用
的稳定 message-like 结构。
数据
createInMemoryGameDb():测试与无数据库配置时的完整内存实现。createMemoryGameServices():把 typed service factory 接到唯一内存实现。createHostGameDb():把 generation-ownedDatabaseHost转成 SessionService 所需接口。GameRecordStore:每个游戏实例显式持有自己的战绩库,并作为GameRecordPort随 Game capability 投影给大厅;不存在全局当前数据库。channelKey()/generateSessionId():稳定会话身份。
游戏包应优先使用 databaseHostToken,缺失时为每个插件实例创建独立内存库。数据库、
SessionService、cron 闭包都属于同一个 owner generation;热更卸载旧代不会覆盖新代状态。
验证
pnpm --filter @zhin.js/game-kit build
pnpm vitest run plugins/games/*/tests