@lizardon386/core-abi 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +8 -2
- package/dist/client.d.ts +14 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/codes.d.ts +6 -0
- package/dist/codes.d.ts.map +1 -0
- package/dist/codes.js +28 -0
- package/dist/codes.js.map +1 -0
- package/dist/hash.d.ts +10 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +86 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +2 -0
- package/dist/manifest.js.map +1 -0
- package/dist/module-api.d.ts +103 -0
- package/dist/module-api.d.ts.map +1 -0
- package/dist/module-api.js +7 -0
- package/dist/module-api.js.map +1 -0
- package/dist/seats.d.ts +20 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +4 -0
- package/dist/seats.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +40 -4
- package/src/client.ts +11 -0
- package/src/codes.ts +33 -0
- package/src/hash.ts +87 -0
- package/src/index.ts +7 -0
- package/src/manifest.ts +34 -0
- package/src/module-api.ts +98 -0
- package/src/seats.ts +22 -0
- package/src/version.ts +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 3178-battle-platform contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lizardon386/core-abi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
3178 对战平台的游戏 ABI:`GameModule`、`GameInstance`、`Effect`、`GameContext`、房间/座位视图和版本常量。
|
|
4
|
+
|
|
5
|
+
游戏工程通常通过 `@lizardon386/game-sdk` 使用这些类型,不需要直接安装本包。需要单独引用 ABI 时可以直接依赖。
|
|
6
|
+
|
|
7
|
+
接入文档:[INTEGRATION_GUIDE.md](https://github.com/Lizardon386/3178-battle-platform/blob/master/INTEGRATION_GUIDE.md)
|
|
8
|
+
|
|
9
|
+
License: MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GameClientContract {
|
|
2
|
+
/** 把对局状态渲染进容器;返回的视图要能在下一次 mount 时替换。 */
|
|
3
|
+
mount(host: HTMLElement, view: unknown, actions: GameActions): void;
|
|
4
|
+
unmount?(host: HTMLElement): void;
|
|
5
|
+
onEvent?(event: {
|
|
6
|
+
name: string;
|
|
7
|
+
payload?: unknown;
|
|
8
|
+
}, actions: GameActions): void;
|
|
9
|
+
}
|
|
10
|
+
export interface GameActions {
|
|
11
|
+
send(name: string, payload?: unknown): void;
|
|
12
|
+
seatId(): string | null;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IACpE,OAAO,CAAC,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,OAAO,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5C,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC;CACzB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":""}
|
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** 平台错误码和通知码:游戏会用到,放进 ABI;默认文案留在 protocol。 */
|
|
2
|
+
export declare const PLATFORM_CODES: readonly ["PROTOCOL", "VERSION", "RULES", "UNKNOWN_GAME", "NOROOM", "NOSEAT", "FULL", "NOTHOST", "HOSTONLY", "PASSWORD", "PHASE", "RATE", "SEATLOST", "BADMSG", "INTERNAL"];
|
|
3
|
+
export type PlatformCode = (typeof PLATFORM_CODES)[number];
|
|
4
|
+
export declare const NOTICE_CODES: readonly ["HOSTLEFT", "HOST_CHANGED", "ROOM_CLOSED", "SEAT_EJECTED", "RULES_MISMATCH", "REJOINED", "TOO_FEW"];
|
|
5
|
+
export type NoticeCode = (typeof NOTICE_CODES)[number];
|
|
6
|
+
//# sourceMappingURL=codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../src/codes.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,eAAO,MAAM,cAAc,6KAgBjB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,eAAO,MAAM,YAAY,+GAQf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** 平台错误码和通知码:游戏会用到,放进 ABI;默认文案留在 protocol。 */
|
|
2
|
+
export const PLATFORM_CODES = [
|
|
3
|
+
'PROTOCOL',
|
|
4
|
+
'VERSION',
|
|
5
|
+
'RULES',
|
|
6
|
+
'UNKNOWN_GAME',
|
|
7
|
+
'NOROOM',
|
|
8
|
+
'NOSEAT',
|
|
9
|
+
'FULL',
|
|
10
|
+
'NOTHOST',
|
|
11
|
+
'HOSTONLY',
|
|
12
|
+
'PASSWORD',
|
|
13
|
+
'PHASE',
|
|
14
|
+
'RATE',
|
|
15
|
+
'SEATLOST',
|
|
16
|
+
'BADMSG',
|
|
17
|
+
'INTERNAL',
|
|
18
|
+
];
|
|
19
|
+
export const NOTICE_CODES = [
|
|
20
|
+
'HOSTLEFT',
|
|
21
|
+
'HOST_CHANGED',
|
|
22
|
+
'ROOM_CLOSED',
|
|
23
|
+
'SEAT_EJECTED',
|
|
24
|
+
'RULES_MISMATCH',
|
|
25
|
+
'REJOINED',
|
|
26
|
+
'TOO_FEW',
|
|
27
|
+
];
|
|
28
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","sourceRoot":"","sources":["../src/codes.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,UAAU;IACV,SAAS;IACT,OAAO;IACP,cAAc;IACd,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,SAAS;IACT,UAAU;IACV,UAAU;IACV,OAAO;IACP,MAAM;IACN,UAAU;IACV,QAAQ;IACR,UAAU;CACF,CAAC;AAIX,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU;IACV,cAAc;IACd,aAAa;IACb,cAAc;IACd,gBAAgB;IAChB,UAAU;IACV,SAAS;CACD,CAAC"}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 稳定摘要:把任意 JSON 值规范化后算出可跨端复现的短哈希。
|
|
3
|
+
* 只回答"两端配置是否一致",不做安全用途;包完整性另走 sha256。
|
|
4
|
+
*/
|
|
5
|
+
/** 键排序、数组保序、undefined 归 null 的确定性 JSON。 */
|
|
6
|
+
export declare function canonicalJson(value: unknown): string;
|
|
7
|
+
/** 16 位十六进制稳定摘要。同一输入在 Node 与浏览器下结果必须一致。 */
|
|
8
|
+
export declare function stableDigest(value: unknown): string;
|
|
9
|
+
export declare function digestsMatch(a: string | undefined, b: string | undefined): boolean;
|
|
10
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA0DH,2CAA2C;AAC3C,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIpD;AAYD,2CAA2C;AAC3C,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGlF"}
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 稳定摘要:把任意 JSON 值规范化后算出可跨端复现的短哈希。
|
|
3
|
+
* 只回答"两端配置是否一致",不做安全用途;包完整性另走 sha256。
|
|
4
|
+
*/
|
|
5
|
+
const FNV_OFFSET = 0xcbf29ce484222325n;
|
|
6
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
7
|
+
const MASK_64 = (1n << 64n) - 1n;
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
const proto = Object.getPrototypeOf(value);
|
|
10
|
+
return proto === Object.prototype || proto === null;
|
|
11
|
+
}
|
|
12
|
+
function encode(value, seen, out) {
|
|
13
|
+
if (value === null) {
|
|
14
|
+
out.push('null');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number') {
|
|
18
|
+
out.push(JSON.stringify(value) ?? 'null');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') {
|
|
22
|
+
out.push('null');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === 'bigint') {
|
|
26
|
+
out.push(`${value.toString(16)}n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!isPlainObject(value) && !Array.isArray(value)) {
|
|
30
|
+
out.push(`"${String(value)}"`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (seen.has(value))
|
|
34
|
+
throw new TypeError('规范化失败:存在循环引用');
|
|
35
|
+
seen.add(value);
|
|
36
|
+
try {
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
out.push('[');
|
|
39
|
+
value.forEach((item, i) => {
|
|
40
|
+
if (i)
|
|
41
|
+
out.push(',');
|
|
42
|
+
encode(item, seen, out);
|
|
43
|
+
});
|
|
44
|
+
out.push(']');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const obj = value;
|
|
48
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort();
|
|
49
|
+
out.push('{');
|
|
50
|
+
keys.forEach((k, i) => {
|
|
51
|
+
if (i)
|
|
52
|
+
out.push(',');
|
|
53
|
+
out.push(JSON.stringify(k), ':');
|
|
54
|
+
encode(obj[k], seen, out);
|
|
55
|
+
});
|
|
56
|
+
out.push('}');
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
seen.delete(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** 键排序、数组保序、undefined 归 null 的确定性 JSON。 */
|
|
63
|
+
export function canonicalJson(value) {
|
|
64
|
+
const out = [];
|
|
65
|
+
encode(value, new Set(), out);
|
|
66
|
+
return out.join('');
|
|
67
|
+
}
|
|
68
|
+
function fnv1a64(text) {
|
|
69
|
+
const bytes = new TextEncoder().encode(text);
|
|
70
|
+
let hash = FNV_OFFSET;
|
|
71
|
+
for (const b of bytes) {
|
|
72
|
+
hash ^= BigInt(b);
|
|
73
|
+
hash = (hash * FNV_PRIME) & MASK_64;
|
|
74
|
+
}
|
|
75
|
+
return hash;
|
|
76
|
+
}
|
|
77
|
+
/** 16 位十六进制稳定摘要。同一输入在 Node 与浏览器下结果必须一致。 */
|
|
78
|
+
export function stableDigest(value) {
|
|
79
|
+
return fnv1a64(canonicalJson(value)).toString(16).padStart(16, '0');
|
|
80
|
+
}
|
|
81
|
+
export function digestsMatch(a, b) {
|
|
82
|
+
if (typeof a !== 'string' || typeof b !== 'string')
|
|
83
|
+
return false;
|
|
84
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=hash.js.map
|
package/dist/hash.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SeatRole } from './seats.js';
|
|
2
|
+
export interface GameUiOptions {
|
|
3
|
+
/** 游戏内聊天由平台聊天卡提供,还是由游戏自己的界面提供。 */
|
|
4
|
+
chat?: 'platform' | 'game';
|
|
5
|
+
}
|
|
6
|
+
/** 游戏模块对外声明的自己:房主建房、客户端校验、模块下发三处都要用它。 */
|
|
7
|
+
export interface GameManifest {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
/** 语义化版本,只用于展示与"版本不同"的提示文案。 */
|
|
11
|
+
version: string;
|
|
12
|
+
/** 房规摘要:由 create 时的 config 算出,与 config 一起构成入房闸。 */
|
|
13
|
+
rulesHash: string;
|
|
14
|
+
seats: {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
};
|
|
18
|
+
allowSpectate: boolean;
|
|
19
|
+
/** 游戏是否支持 AI 填充席位(war3 的"电脑玩家")。 */
|
|
20
|
+
soloFill: boolean;
|
|
21
|
+
/** 需要的席位角色集合,平台据此拒绝模块乱用角色。 */
|
|
22
|
+
roles: SeatRole[];
|
|
23
|
+
/** 客户端 UI 入口,相对该模块的 base URL。 */
|
|
24
|
+
entry: string;
|
|
25
|
+
/** 下发给客户端的资源清单;hash 为 'sha256:<hex>' 或裸 hex。 */
|
|
26
|
+
assets?: GameAsset[];
|
|
27
|
+
/** 可选 UI 能力;缺省时按平台默认(聊天卡由平台显示)。 */
|
|
28
|
+
ui?: GameUiOptions;
|
|
29
|
+
}
|
|
30
|
+
export interface GameAsset {
|
|
31
|
+
path: string;
|
|
32
|
+
hash: string;
|
|
33
|
+
bytes?: number;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC5B;AAED,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,aAAa,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,mCAAmC;IACnC,EAAE,CAAC,EAAE,aAAa,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { NoticeCode, PlatformCode } from './codes.js';
|
|
2
|
+
import type { GameUiOptions } from './manifest.js';
|
|
3
|
+
import type { RoomPhase, SeatRole } from './seats.js';
|
|
4
|
+
/**
|
|
5
|
+
* 宿主与游戏模块之间的 ABI。
|
|
6
|
+
* 1. 游戏状态不进平台对象,平台只搬 opaque blob。
|
|
7
|
+
* 2. 游戏不直接碰 socket,对外投递只通过 Effect[]。
|
|
8
|
+
* 3. 游戏不改房间相位、座位表或房主权限。
|
|
9
|
+
*/
|
|
10
|
+
export type ViewerId = string | null;
|
|
11
|
+
export type Effect = {
|
|
12
|
+
kind: 'broadcast';
|
|
13
|
+
name: string;
|
|
14
|
+
payload?: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'toSeat';
|
|
17
|
+
seatId: string;
|
|
18
|
+
name: string;
|
|
19
|
+
payload?: unknown;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'notice';
|
|
22
|
+
code: NoticeCode;
|
|
23
|
+
text?: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'system';
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'end';
|
|
29
|
+
reason?: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'kick';
|
|
32
|
+
seatId: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
};
|
|
35
|
+
export interface Reject {
|
|
36
|
+
reject: string;
|
|
37
|
+
code?: PlatformCode;
|
|
38
|
+
}
|
|
39
|
+
export type Outcome = Effect[] | Reject;
|
|
40
|
+
export declare function isReject(value: Outcome): value is Reject;
|
|
41
|
+
export declare function reject(message: string, code?: PlatformCode): Reject;
|
|
42
|
+
export interface GameAction {
|
|
43
|
+
name: string;
|
|
44
|
+
payload?: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** 传给游戏模块的座位信息:不含 socket、不含票据,游戏拿不到传输细节。 */
|
|
47
|
+
export interface GameSeatInfo {
|
|
48
|
+
seatId: string;
|
|
49
|
+
slot: number;
|
|
50
|
+
name: string;
|
|
51
|
+
role: SeatRole;
|
|
52
|
+
team: string | number | null;
|
|
53
|
+
ready: boolean;
|
|
54
|
+
connected: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface GameContext {
|
|
57
|
+
readonly roomCode: string;
|
|
58
|
+
readonly config: Record<string, unknown>;
|
|
59
|
+
/** 注入时钟与随机源,让规则测试可确定性推进。 */
|
|
60
|
+
now(): number;
|
|
61
|
+
random(): number;
|
|
62
|
+
seats(): GameSeatInfo[];
|
|
63
|
+
seat(seatId: string): GameSeatInfo | undefined;
|
|
64
|
+
}
|
|
65
|
+
export interface GameInstance {
|
|
66
|
+
/** 按视角裁剪后的状态;`null` 表示观战视角。 */
|
|
67
|
+
serialize(viewer: ViewerId): unknown;
|
|
68
|
+
handle(action: GameAction, by: string): Outcome;
|
|
69
|
+
/** 开局。平台已确认发话人是房主且所有人类玩家都 Ready。 */
|
|
70
|
+
begin(): Outcome;
|
|
71
|
+
snapshot(): unknown;
|
|
72
|
+
/** 心跳:规则计时窗口在这里推进(旧项目心跳里的玩法部分)。 */
|
|
73
|
+
tick?(now: number): Effect[];
|
|
74
|
+
onSeatJoin?(seat: GameSeatInfo): Effect[];
|
|
75
|
+
onSeatLeave?(seat: GameSeatInfo, phase: RoomPhase): Effect[];
|
|
76
|
+
/** 座位超时。托管与罚则属于游戏,不属于平台。 */
|
|
77
|
+
onTimeout?(seat: GameSeatInfo): Effect[];
|
|
78
|
+
dispose?(): void;
|
|
79
|
+
}
|
|
80
|
+
export interface GameModule {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
version: string;
|
|
84
|
+
entry: string;
|
|
85
|
+
seats: {
|
|
86
|
+
min: number;
|
|
87
|
+
max: number;
|
|
88
|
+
};
|
|
89
|
+
allowSpectate: boolean;
|
|
90
|
+
soloFill: boolean;
|
|
91
|
+
roles: SeatRole[];
|
|
92
|
+
/** 把房主原始设置规范化;返回的形状决定 rulesHash 与 create 的输入。 */
|
|
93
|
+
normalizeConfig(raw: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
94
|
+
/** 只摘要真正影响对局的字段,避免"改了房间名也算房规不同"。 */
|
|
95
|
+
rulesHashOf(config: Record<string, unknown>): string;
|
|
96
|
+
create(config: Record<string, unknown>, ctx: GameContext): GameInstance;
|
|
97
|
+
resume(snapshot: unknown, ctx: GameContext): GameInstance;
|
|
98
|
+
/** 存档快照的兼容性闸;返回 false 则平台丢弃快照并报错给房主。 */
|
|
99
|
+
acceptsSnapshot?(snapshot: unknown): boolean;
|
|
100
|
+
/** 可选 UI 声明;例如让游戏自己的聊天面板替代平台聊天卡。 */
|
|
101
|
+
ui?: GameUiOptions;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=module-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-api.d.ts","sourceRoot":"","sources":["../src/module-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-api.js","sourceRoot":"","sources":["../src/module-api.ts"],"names":[],"mappings":"AA4BA,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,OAAQ,KAAgB,EAAE,MAAM,KAAK,QAAQ,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,OAAe,EAAE,IAAmB;IACzD,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAChE,CAAC"}
|
package/dist/seats.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 游戏 ABI 和客户端都认的座位/相位形状;平台的房间视图另在 protocol。 */
|
|
2
|
+
export declare const SEAT_ROLES: readonly ["host", "player", "spectator", "ai"];
|
|
3
|
+
export type SeatRole = (typeof SEAT_ROLES)[number];
|
|
4
|
+
export declare const ROOM_PHASES: readonly ["lobby", "playing", "ended"];
|
|
5
|
+
export type RoomPhase = (typeof ROOM_PHASES)[number];
|
|
6
|
+
/** 座位视图(下发给客户端和游戏界面)。 */
|
|
7
|
+
export interface SeatView {
|
|
8
|
+
seatId: string;
|
|
9
|
+
slot: number;
|
|
10
|
+
name: string;
|
|
11
|
+
role: SeatRole;
|
|
12
|
+
ready: boolean;
|
|
13
|
+
connected: boolean;
|
|
14
|
+
/** 距上次心跳的毫秒数;离线座位为 null。 */
|
|
15
|
+
ageMs: number | null;
|
|
16
|
+
team: string | number | null;
|
|
17
|
+
/** 是否为房主座位(role==='host' 的冗余标记,便于前端画 🎮)。 */
|
|
18
|
+
isHost: boolean;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=seats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seats.d.ts","sourceRoot":"","sources":["../src/seats.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAEhD,eAAO,MAAM,UAAU,gDAAiD,CAAC;AACzE,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,WAAW,wCAAyC,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,yBAAyB;AACzB,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC7B,6CAA6C;IAC7C,MAAM,EAAE,OAAO,CAAC;CACjB"}
|
package/dist/seats.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seats.js","sourceRoot":"","sources":["../src/seats.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAEhD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAU,CAAC;AAGzE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAU,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 游戏 ABI 和宿主支持的模式/接口版本。 */
|
|
2
|
+
export declare const API_VERSION = 1;
|
|
3
|
+
export declare const SUPPORTED_API_VERSIONS: readonly number[];
|
|
4
|
+
export declare const GAME_MODES: readonly ["turn", "realtime"];
|
|
5
|
+
export type GameMode = (typeof GAME_MODES)[number];
|
|
6
|
+
/** 当前宿主只实际支持 turn;realtime 是规划中的 profile。 */
|
|
7
|
+
export declare const SUPPORTED_MODES: Readonly<Record<GameMode, readonly number[]>>;
|
|
8
|
+
export declare const DEFAULT_MODE: GameMode;
|
|
9
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAE5B,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAAkB,CAAC;AAEvE,eAAO,MAAM,UAAU,+BAAgC,CAAC;AACxD,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,6CAA6C;AAC7C,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC,CAGzE,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,QAAiB,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 游戏 ABI 和宿主支持的模式/接口版本。 */
|
|
2
|
+
export const API_VERSION = 1;
|
|
3
|
+
export const SUPPORTED_API_VERSIONS = [API_VERSION];
|
|
4
|
+
export const GAME_MODES = ['turn', 'realtime'];
|
|
5
|
+
/** 当前宿主只实际支持 turn;realtime 是规划中的 profile。 */
|
|
6
|
+
export const SUPPORTED_MODES = {
|
|
7
|
+
turn: [1],
|
|
8
|
+
realtime: [],
|
|
9
|
+
};
|
|
10
|
+
export const DEFAULT_MODE = 'turn';
|
|
11
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAE5B,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B,MAAM,CAAC,MAAM,sBAAsB,GAAsB,CAAC,WAAW,CAAC,CAAC;AAEvE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,UAAU,CAAU,CAAC;AAGxD,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAkD;IAC5E,IAAI,EAAE,CAAC,CAAC,CAAC;IACT,QAAQ,EAAE,EAAE;CACb,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAa,MAAM,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lizardon386/core-abi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "3178 游戏接口层:GameModule / GameInstance / Effect 等宿主与游戏共同遵守的 ABI 和数据类型",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "3178-battle-platform contributors",
|
|
7
|
+
"homepage": "https://github.com/Lizardon386/3178-battle-platform#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/Lizardon386/3178-battle-platform/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Lizardon386/3178-battle-platform.git",
|
|
14
|
+
"directory": "packages/core-abi"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.19.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"3178",
|
|
21
|
+
"game-platform",
|
|
22
|
+
"game-abi",
|
|
23
|
+
"types"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface GameClientContract {
|
|
2
|
+
/** 把对局状态渲染进容器;返回的视图要能在下一次 mount 时替换。 */
|
|
3
|
+
mount(host: HTMLElement, view: unknown, actions: GameActions): void;
|
|
4
|
+
unmount?(host: HTMLElement): void;
|
|
5
|
+
onEvent?(event: { name: string; payload?: unknown }, actions: GameActions): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GameActions {
|
|
9
|
+
send(name: string, payload?: unknown): void;
|
|
10
|
+
seatId(): string | null;
|
|
11
|
+
}
|
package/src/codes.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 平台错误码和通知码:游戏会用到,放进 ABI;默认文案留在 protocol。 */
|
|
2
|
+
|
|
3
|
+
export const PLATFORM_CODES = [
|
|
4
|
+
'PROTOCOL',
|
|
5
|
+
'VERSION',
|
|
6
|
+
'RULES',
|
|
7
|
+
'UNKNOWN_GAME',
|
|
8
|
+
'NOROOM',
|
|
9
|
+
'NOSEAT',
|
|
10
|
+
'FULL',
|
|
11
|
+
'NOTHOST',
|
|
12
|
+
'HOSTONLY',
|
|
13
|
+
'PASSWORD',
|
|
14
|
+
'PHASE',
|
|
15
|
+
'RATE',
|
|
16
|
+
'SEATLOST',
|
|
17
|
+
'BADMSG',
|
|
18
|
+
'INTERNAL',
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type PlatformCode = (typeof PLATFORM_CODES)[number];
|
|
22
|
+
|
|
23
|
+
export const NOTICE_CODES = [
|
|
24
|
+
'HOSTLEFT',
|
|
25
|
+
'HOST_CHANGED',
|
|
26
|
+
'ROOM_CLOSED',
|
|
27
|
+
'SEAT_EJECTED',
|
|
28
|
+
'RULES_MISMATCH',
|
|
29
|
+
'REJOINED',
|
|
30
|
+
'TOO_FEW',
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type NoticeCode = (typeof NOTICE_CODES)[number];
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 稳定摘要:把任意 JSON 值规范化后算出可跨端复现的短哈希。
|
|
3
|
+
* 只回答"两端配置是否一致",不做安全用途;包完整性另走 sha256。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const FNV_OFFSET = 0xcbf29ce484222325n;
|
|
7
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
8
|
+
const MASK_64 = (1n << 64n) - 1n;
|
|
9
|
+
|
|
10
|
+
function isPlainObject(value: object): boolean {
|
|
11
|
+
const proto = Object.getPrototypeOf(value);
|
|
12
|
+
return proto === Object.prototype || proto === null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function encode(value: unknown, seen: Set<object>, out: string[]): void {
|
|
16
|
+
if (value === null) {
|
|
17
|
+
out.push('null');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number') {
|
|
21
|
+
out.push(JSON.stringify(value) ?? 'null');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') {
|
|
25
|
+
out.push('null');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'bigint') {
|
|
29
|
+
out.push(`${value.toString(16)}n`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!isPlainObject(value) && !Array.isArray(value)) {
|
|
33
|
+
out.push(`"${String(value)}"`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (seen.has(value)) throw new TypeError('规范化失败:存在循环引用');
|
|
37
|
+
seen.add(value);
|
|
38
|
+
try {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
out.push('[');
|
|
41
|
+
value.forEach((item, i) => {
|
|
42
|
+
if (i) out.push(',');
|
|
43
|
+
encode(item, seen, out);
|
|
44
|
+
});
|
|
45
|
+
out.push(']');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const obj = value as Record<string, unknown>;
|
|
49
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort();
|
|
50
|
+
out.push('{');
|
|
51
|
+
keys.forEach((k, i) => {
|
|
52
|
+
if (i) out.push(',');
|
|
53
|
+
out.push(JSON.stringify(k), ':');
|
|
54
|
+
encode(obj[k], seen, out);
|
|
55
|
+
});
|
|
56
|
+
out.push('}');
|
|
57
|
+
} finally {
|
|
58
|
+
seen.delete(value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 键排序、数组保序、undefined 归 null 的确定性 JSON。 */
|
|
63
|
+
export function canonicalJson(value: unknown): string {
|
|
64
|
+
const out: string[] = [];
|
|
65
|
+
encode(value, new Set(), out);
|
|
66
|
+
return out.join('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fnv1a64(text: string): bigint {
|
|
70
|
+
const bytes = new TextEncoder().encode(text);
|
|
71
|
+
let hash = FNV_OFFSET;
|
|
72
|
+
for (const b of bytes) {
|
|
73
|
+
hash ^= BigInt(b);
|
|
74
|
+
hash = (hash * FNV_PRIME) & MASK_64;
|
|
75
|
+
}
|
|
76
|
+
return hash;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 16 位十六进制稳定摘要。同一输入在 Node 与浏览器下结果必须一致。 */
|
|
80
|
+
export function stableDigest(value: unknown): string {
|
|
81
|
+
return fnv1a64(canonicalJson(value)).toString(16).padStart(16, '0');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function digestsMatch(a: string | undefined, b: string | undefined): boolean {
|
|
85
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
86
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
87
|
+
}
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SeatRole } from './seats.js';
|
|
2
|
+
|
|
3
|
+
export interface GameUiOptions {
|
|
4
|
+
/** 游戏内聊天由平台聊天卡提供,还是由游戏自己的界面提供。 */
|
|
5
|
+
chat?: 'platform' | 'game';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 游戏模块对外声明的自己:房主建房、客户端校验、模块下发三处都要用它。 */
|
|
9
|
+
export interface GameManifest {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
/** 语义化版本,只用于展示与"版本不同"的提示文案。 */
|
|
13
|
+
version: string;
|
|
14
|
+
/** 房规摘要:由 create 时的 config 算出,与 config 一起构成入房闸。 */
|
|
15
|
+
rulesHash: string;
|
|
16
|
+
seats: { min: number; max: number };
|
|
17
|
+
allowSpectate: boolean;
|
|
18
|
+
/** 游戏是否支持 AI 填充席位(war3 的"电脑玩家")。 */
|
|
19
|
+
soloFill: boolean;
|
|
20
|
+
/** 需要的席位角色集合,平台据此拒绝模块乱用角色。 */
|
|
21
|
+
roles: SeatRole[];
|
|
22
|
+
/** 客户端 UI 入口,相对该模块的 base URL。 */
|
|
23
|
+
entry: string;
|
|
24
|
+
/** 下发给客户端的资源清单;hash 为 'sha256:<hex>' 或裸 hex。 */
|
|
25
|
+
assets?: GameAsset[];
|
|
26
|
+
/** 可选 UI 能力;缺省时按平台默认(聊天卡由平台显示)。 */
|
|
27
|
+
ui?: GameUiOptions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GameAsset {
|
|
31
|
+
path: string;
|
|
32
|
+
hash: string;
|
|
33
|
+
bytes?: number;
|
|
34
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { NoticeCode, PlatformCode } from './codes.js';
|
|
2
|
+
import type { GameUiOptions } from './manifest.js';
|
|
3
|
+
import type { RoomPhase, SeatRole } from './seats.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 宿主与游戏模块之间的 ABI。
|
|
7
|
+
* 1. 游戏状态不进平台对象,平台只搬 opaque blob。
|
|
8
|
+
* 2. 游戏不直接碰 socket,对外投递只通过 Effect[]。
|
|
9
|
+
* 3. 游戏不改房间相位、座位表或房主权限。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ViewerId = string | null;
|
|
13
|
+
|
|
14
|
+
export type Effect =
|
|
15
|
+
| { kind: 'broadcast'; name: string; payload?: unknown }
|
|
16
|
+
| { kind: 'toSeat'; seatId: string; name: string; payload?: unknown }
|
|
17
|
+
| { kind: 'notice'; code: NoticeCode; text?: string }
|
|
18
|
+
| { kind: 'system'; text: string }
|
|
19
|
+
| { kind: 'end'; reason?: string }
|
|
20
|
+
| { kind: 'kick'; seatId: string; reason?: string };
|
|
21
|
+
|
|
22
|
+
export interface Reject {
|
|
23
|
+
reject: string;
|
|
24
|
+
code?: PlatformCode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Outcome = Effect[] | Reject;
|
|
28
|
+
|
|
29
|
+
export function isReject(value: Outcome): value is Reject {
|
|
30
|
+
return !Array.isArray(value) && typeof (value as Reject)?.reject === 'string';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function reject(message: string, code?: PlatformCode): Reject {
|
|
34
|
+
return code ? { reject: message, code } : { reject: message };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GameAction {
|
|
38
|
+
name: string;
|
|
39
|
+
payload?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 传给游戏模块的座位信息:不含 socket、不含票据,游戏拿不到传输细节。 */
|
|
43
|
+
export interface GameSeatInfo {
|
|
44
|
+
seatId: string;
|
|
45
|
+
slot: number;
|
|
46
|
+
name: string;
|
|
47
|
+
role: SeatRole;
|
|
48
|
+
team: string | number | null;
|
|
49
|
+
ready: boolean;
|
|
50
|
+
connected: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GameContext {
|
|
54
|
+
readonly roomCode: string;
|
|
55
|
+
readonly config: Record<string, unknown>;
|
|
56
|
+
/** 注入时钟与随机源,让规则测试可确定性推进。 */
|
|
57
|
+
now(): number;
|
|
58
|
+
random(): number;
|
|
59
|
+
seats(): GameSeatInfo[];
|
|
60
|
+
seat(seatId: string): GameSeatInfo | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GameInstance {
|
|
64
|
+
/** 按视角裁剪后的状态;`null` 表示观战视角。 */
|
|
65
|
+
serialize(viewer: ViewerId): unknown;
|
|
66
|
+
handle(action: GameAction, by: string): Outcome;
|
|
67
|
+
/** 开局。平台已确认发话人是房主且所有人类玩家都 Ready。 */
|
|
68
|
+
begin(): Outcome;
|
|
69
|
+
snapshot(): unknown;
|
|
70
|
+
/** 心跳:规则计时窗口在这里推进(旧项目心跳里的玩法部分)。 */
|
|
71
|
+
tick?(now: number): Effect[];
|
|
72
|
+
onSeatJoin?(seat: GameSeatInfo): Effect[];
|
|
73
|
+
onSeatLeave?(seat: GameSeatInfo, phase: RoomPhase): Effect[];
|
|
74
|
+
/** 座位超时。托管与罚则属于游戏,不属于平台。 */
|
|
75
|
+
onTimeout?(seat: GameSeatInfo): Effect[];
|
|
76
|
+
dispose?(): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GameModule {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
version: string;
|
|
83
|
+
entry: string;
|
|
84
|
+
seats: { min: number; max: number };
|
|
85
|
+
allowSpectate: boolean;
|
|
86
|
+
soloFill: boolean;
|
|
87
|
+
roles: SeatRole[];
|
|
88
|
+
/** 把房主原始设置规范化;返回的形状决定 rulesHash 与 create 的输入。 */
|
|
89
|
+
normalizeConfig(raw: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
90
|
+
/** 只摘要真正影响对局的字段,避免"改了房间名也算房规不同"。 */
|
|
91
|
+
rulesHashOf(config: Record<string, unknown>): string;
|
|
92
|
+
create(config: Record<string, unknown>, ctx: GameContext): GameInstance;
|
|
93
|
+
resume(snapshot: unknown, ctx: GameContext): GameInstance;
|
|
94
|
+
/** 存档快照的兼容性闸;返回 false 则平台丢弃快照并报错给房主。 */
|
|
95
|
+
acceptsSnapshot?(snapshot: unknown): boolean;
|
|
96
|
+
/** 可选 UI 声明;例如让游戏自己的聊天面板替代平台聊天卡。 */
|
|
97
|
+
ui?: GameUiOptions;
|
|
98
|
+
}
|
package/src/seats.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** 游戏 ABI 和客户端都认的座位/相位形状;平台的房间视图另在 protocol。 */
|
|
2
|
+
|
|
3
|
+
export const SEAT_ROLES = ['host', 'player', 'spectator', 'ai'] as const;
|
|
4
|
+
export type SeatRole = (typeof SEAT_ROLES)[number];
|
|
5
|
+
|
|
6
|
+
export const ROOM_PHASES = ['lobby', 'playing', 'ended'] as const;
|
|
7
|
+
export type RoomPhase = (typeof ROOM_PHASES)[number];
|
|
8
|
+
|
|
9
|
+
/** 座位视图(下发给客户端和游戏界面)。 */
|
|
10
|
+
export interface SeatView {
|
|
11
|
+
seatId: string;
|
|
12
|
+
slot: number;
|
|
13
|
+
name: string;
|
|
14
|
+
role: SeatRole;
|
|
15
|
+
ready: boolean;
|
|
16
|
+
connected: boolean;
|
|
17
|
+
/** 距上次心跳的毫秒数;离线座位为 null。 */
|
|
18
|
+
ageMs: number | null;
|
|
19
|
+
team: string | number | null;
|
|
20
|
+
/** 是否为房主座位(role==='host' 的冗余标记,便于前端画 🎮)。 */
|
|
21
|
+
isHost: boolean;
|
|
22
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 游戏 ABI 和宿主支持的模式/接口版本。 */
|
|
2
|
+
|
|
3
|
+
export const API_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
export const SUPPORTED_API_VERSIONS: readonly number[] = [API_VERSION];
|
|
6
|
+
|
|
7
|
+
export const GAME_MODES = ['turn', 'realtime'] as const;
|
|
8
|
+
export type GameMode = (typeof GAME_MODES)[number];
|
|
9
|
+
|
|
10
|
+
/** 当前宿主只实际支持 turn;realtime 是规划中的 profile。 */
|
|
11
|
+
export const SUPPORTED_MODES: Readonly<Record<GameMode, readonly number[]>> = {
|
|
12
|
+
turn: [1],
|
|
13
|
+
realtime: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_MODE: GameMode = 'turn';
|