add server-side spec

Signed-off-by: szdytom <szdytom@qq.com>
This commit is contained in:
方而静 2024-02-08 18:35:34 +08:00
parent 6fc3e2d3ab
commit 399e6f38c8
Signed by: szTom
GPG Key ID: 072D999D60C6473C

370
docs/server.md Normal file
View File

@ -0,0 +1,370 @@
## 结构化数据编码
为了实现高效的远程过程调用和数据持久化,适配器使用“结构化数据编码”将内存中的数据结构编码为二进制。
在把一些数据编码为二进制数据时,逐个将其成员按照顺序紧密地放在一起。在编码整数或浮点数时,直接使用小端序的二进制表示。在编码字符串或不定长数组时,首先给出一个 4 字节的无符号整数表示长度,然后逐个给出其成员元素。
### 结构化数据编码中的基本类型
- `u8``u16``u32``u64`:无符号 8/16/32/64 位整数;
- `i8``i16``i32``i64`:有符号 8/16/32/64 位整数;
- `str`:字符串,本质上是 `u8` 数组,使用 UTF-8 编码;
- `T[]`:其中 `T` 是一个类型,表示类型为 `T` 的不定长数组;
- `T[n]`:其中 `T` 是一个类型,`n` 是一个正整数,表示类型为 `T` 的长 $n$ 的数组。
### 通用的复合类型
**类型** `bool`:用于表示一个布尔值。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `value` | `u8` | 值 |
**类型** `void`:用于表示一个空值。
(此类型没有成员)
**类型** `uuid`:用于表示一个全局唯一标识符。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `low` | `u64` | 值的低 64 位 |
| `high` | `u64` | 值的高 64 位 |
**类型** `DateTime`:用于表示一个日期时间,精确到秒。
| 成员标识符 | 类型 | 含义 |
| :---------- | :---- | :----------------- |
| `timestamp` | `u64` | 时间的 UNIX 时间戳 |
**类型** `RGBColor`:用于表示一个颜色。
| 成员标识符 | 类型 | 含义 |
| :--------- | :--- | :------------- |
| `r` | `u8` | 颜色的红色分量 |
| `g` | `u8` | 颜色的绿色分量 |
| `b` | `u8` | 颜色的蓝色分量 |
## 与处理器交互
在需要处理游戏逻辑时,适配器应当启动一个处理器进程,并重定向其标准输入、输出流。适配器应该在控制器之间建立全双工通道。适配器向处理器传输数据简称为“给出”,处理器向适配器传输数据简称为“回答”。
对于适配器,数据向标准输入流(`stdin`)给出,结果处理器的回答返回在标准输出流(`stdout`)取得,处理器的日志在标准异常流(`stderr`)回答。注意每次在标准输入流流给出数据后要刷新缓冲区。
### 跨进程过程调用
适配器和处理器的交互使用“跨进程过程调用”这一单向协议,在此协议中,只有适配器能调用处理器的函数,处理器回答返回值。在跨进程过程调用中,我们把处理器视为一个状态机,它接受各种函数调用,并根据函数的参数和内部状态返回一些值,同时更新内部状态。
通信过程总是使用二进制,字节序为小端序,处理器实现可以假定原生端序总是小端序。适配器调用函数时,会向处理器给出一个函数调用数据包,按顺序包含如下内容:
- 函数调用的标识符8字节
- 调用的函数的标识符4字节整数
- 函数参数的数据长度4字节无符号整数
- 函数参数的数据。
函数返回时,应当向适配器回答一个函数返回数据包,按顺序包含如下内容:
- 函数调用的标识符8字节值应当与调用数据包中的调用标识符相等
- 函数返回的数据长度4字节无符号整数
- 函数返回的数据。
当函数返回的数据长度为 0xFF'FF'FF'FF 时,表示函数调用过程中遇到了异常。
### 与处理器交互时使用的类型
**类型** `Player`:用于标识一个玩家。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `id` | `u8` | 玩家编号 |
**类型** `Team`:用于标识一个队伍。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `id` | `u8` | 队伍编号 |
**类型** `ImportTerrainConfig`:用于导入地图的参数,该类型用于 `importTerrain` 的参数。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `w` | `u8` | 地图的列数 |
| `h` | `u8` | 地图的行数 |
| `value` | `str` | 导入的地图的字符串表示 |
**类型** `RandomTerrainConfig`:用于随机生成地图的参数,该类型用于 `randomTerrain` 的参数。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `w` | `u8` | 地图的列数 |
| `h` | `u8` | 地图的行数 |
| `city_dense` | `u8` | 地图生成中要塞格子的生成密度 |
| `mountain_dense` | `u8` | 地图生成中山区格子的生成密度 |
| `swamp_dense` | `u8` | 地图生成中沼泽格子的生成密度 |
| `light_dense` | `u8` | 地图生成中明示格子的生成密度 |
**类型** `PlayerMove`:用于表示一次玩家操作,该类型用于 `appendOrderQueue` 的参数。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `player` | `Player` | 操作的玩家 |
| `x` | `u8` | 移动操作出发格的 X 坐标 |
| `y` | `u8` | 移动操作出发格的 Y 坐标 |
| `dir` | `u8` | 移动的方向的通信编码 |
| `half` | `bool` | 是否只移动一半的单位 |
**类型** `PlayerRanking`:用于表示一名玩家在队伍排名上的信息,该类型用于 `TeamRanking` 的成员。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `player` | `Player` | 表示的玩家 |
| `is_defeated` | `bool` | 表示的玩家 |
| `land` | `u16` | 玩家占领的格子的总数 |
| `unit` | `i32` | 玩家拥有的单位总数 |
**类型** `TeamRanking`:用于表示一只队伍在队伍排名上的信息,该类型用于 `GameState` 的成员。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `team` | `Team` | 表示的队伍 |
| `land` | `u16` | 队伍占领的格子的总数 |
| `unit` | `i32` | 队伍拥有的单位总数 |
| `players` | `PlayerRanking[]` | 队伍中的玩家的信息 |
**类型** `Tile`:用于表示一个格子上的信息。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `owner` | `Player` | 占领此格的玩家 |
| `type` | `u8` | 格子类型的通信编码 |
| `unit` | `i32` |格子上的单位数量 |
**类型** `TileDiff`:用于表示一个格子的视野变化。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `x` | `u8` | 此格子的 X 坐标 |
| `y` | `u8` | 此格子的 Y 坐标 |
| `value` | `Tile` | 格子上的在视野内的值 |
**类型** `TeamViewDiff`:用于表示一个队伍的视野差分。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `team` | `Team` | 队伍 |
| `diffs` | `TileDiff[]` | 有变化的格子 |
**类型** `GameTickState`:用于表示一半回合结束时的状态,该类型用于 `tick` 的返回值。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `halfturn_id` | `u32` | 当前的半回合数 |
| `defeated_players` | `Player[]` | 在这个半回合内被打败的玩家列表 |
| `alive_players` | `Player[]` | 剩余的未被击败的玩家 |
| `leaderboard` | `TeamRanking[]` | 队伍排名 |
| `diffs` | `TeamViewDiff[]` | 各个队伍的视野差分 |
| `msg` | `str[]` | 消息和事件通知 |
**类型** `Modifier`:用于表示一条特殊规则,该类型用于 `InitInfo` 的成员。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `key` | `u32` | 特殊规则的通信编码 |
| `value` | `i32` | 特殊规则的参数 |
**类型** `InitInfo`:用于游戏初始化的各种信息,该类型用于 `init` 函数的参数。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `n` | `u8` | 玩家数量 |
| `player_team` | `Team[]` | 各个玩家的所在的队伍 |
| `modifiers` | `Modifier[]` | 特殊规则列表 |
**类型** `ReplayStep`:用于表示游戏回放的一个半回合内发生的信息,该类型用于 `Replay` 的成员。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `halfturn_id` | `u32` | 当前的半回合数 |
| `player_moves` | `PlayerMove[]` | 各个玩家的本半回合的操作的列表 |
| `offline_events` | `Player[]` | 本半回合内掉线的玩家列表 |
**类型** `PlayerInfo`:用于表示一名玩家的完整信息。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `uid` | `uuid` | 玩家标识符 |
| `id` | `Player` | 玩家在游戏内的编号 |
| `team` | `Team` | 玩家在游戏内的队伍 |
| `name` | `str` | 玩家的显示名 |
**类型** `Replay`:用于表示游戏回放。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `rid` | `uuid` | 回放的标识符 |
| `timestamp` | `u64` | 游戏的开始时间的Unix时间戳 |
| `speed` | `i8` | 游戏速度 |
| `init_info` | `InitInfo` | 游戏初始化的配置 |
| `length` | `u32` | 游戏进行的回合数 |
| `player_num` | `u8` | 参与游戏的玩家数 |
| `players` | `PlayerInfo[]` | 各个玩家信息 |
| `winner` | `Team` | 取得胜利的队伍 |
| `terrain` | `Tile[]` | 第 $0$ 个半回合开始时,地图各个格子的信息 |
| `steps` | `ReplayStep[]` | 各个半回合的信息 |
**类型** `GameInfo`:表示一局游戏设置的信息,用于 `saveReplay` 函数的参数。
| 成员标识符 | 类型 | 含义 |
| :--- | :--- | :--- |
| `rid` | `uuid` | 回放的标识符 |
| `speed` | `i8` | 游戏速度 |
| `players` | `PlayerInfo[]` | 各个玩家信息 |
### 处理器实现的函数
| 标识符 | 函数名 | 函数功能 | 函数参数类型名 | 函数返回值类型名 |
| :--- | :--- | :----- | :--------- | :------------ |
| 0 | `setSeed` | 设置随机种子 | `u32` | `void` |
| 1 | `setLogLevel` | 设置最低的日志等级 | `u8` | `void` |
| 2 | `getError` | 获得上一个错误 | `void` | `i32` |
| 3 | `getErrorInfo` | 获得上一个错误消息 | `void` | `str` |
| 10 | `importTerrain` | 导入预设地图 | `ImportTerrainConfig` | `bool` |
| 11 | `randomTerrain` | 生成随机地图 | `RandomTerrainConfig` | `bool` |
| 20 | `appendOrderQueue` | 向玩家的行动队列的末尾添加一个移动 | `PlayerMove` | `void` |
| 21 | `clearMoveQueue` | 清空玩家的行动队列 | `Player` | `void` |
| 22 | `popOrderQueue` | 清除玩家的行动队列的最后一项 | `Player` | `void` |
| 23 | `setOffline` | 通知玩家掉线 | `Player` | `void` |
| 30 | `init` | 初始化 | `void` | `u8[]` |
| 31 | `tick` | 执行下一个半回合 | `void` | `GameTickState` |
| 40 | `getKeyFrame` | 返回某一队伍的视野 | `Team` | `Tile[]` |
| 50 | `saveReplay` | 保存回放 | `GameInfo` | `Replay` |
### 日志等级与编码
处理器的日志分为五个等级,等级越高,错误越严重,使用如下表格:
| 等级名称 | 标识字母 | 通信编码 |
| :--- | :--- | :--- |
| 调试 | D | 0x00 |
| 信息 | I | 0x01 |
| 警告 | W | 0x02 |
| 错误 | E | 0x03 |
| 致命 | F | 0x04 |
处理器内部会使用一个日志过滤等级变量,当日志等级小于日志过滤等级时,这条日志应当被跳过。这个变量可以使用 `setLogLevel` 函数设置。在标准启动下,日志等级应当至少设置为警告,以免产生过多输出。
## 与控制器交互
在玩家进行游戏时,需要借助控制器这一用户代理,与适配器进行交互。适配器与控制器之间会通过 TCP Socket 或 WebSocket 建立全双工通道。
### 异步过程调用
适配器和控制器的交互使用“异步过程调用”这一双向协议。与“跨进程过程调用”不同的是,在此协议中,适配器和控制器是对等交流的双方,可以相互调用对方实现的函数。整个调用过程是异步的,即使上一个函数调用还未返回,也可以进行下一个函数调用;后调用的函数可以先返回;即使还有对方的函数调用没有返回,也可以对对方进行函数调用。
为了标识返回的数据到底是哪次函数调用的返回值,在进行函数调用时,调用方应当按某种方式(例如随机)生成一个不重复的 8 字节的函数调用的标识符。在函数返回时,包头部的函数调用的标识符的值应当与调用数据包头中的调用标识符相等。
通信过程总是使用二进制,字节序为小端序,处理器实现可以假定原生端序总是小端序。调用函数时,向对方发送一个函数调用数据包,按顺序包含如下内容:
- 数据包头部魔数0x3C1字节
- 函数调用的标识符8字节
- 调用的函数的标识符4字节整数
- 房间上下文的全局唯一标识符16字节
- 函数参数的数据长度4字节无符号整数
- 函数参数的数据。
函数返回时,应当向调用方回答一个函数返回数据包,按顺序包含如下内容:
- 数据包头部魔数0xFE1字节
- 函数调用的标识符8字节值应当与调用数据包中的调用标识符相等
- 函数遇到的异常4字节整数
- 函数返回的数据长度4字节无符号整数
- 函数返回的数据。
### 与控制器交互中使用的类型
对于类型 `RandomTerrainConfig``PlayerMove``PlayerRanking``TeamRanking``Tile``TileDiff``TeamViewDiff` 以及 `Modifier`,请参见“与处理器交互时使用的类型”。
**类型** `RoomState`:用于表示一个对局房间(开始前)中的信息。
| 成员标识符 | 类型 | 含义 |
| :------------ | :-------------------- | :--------------------------------- |
| `owner` | `uuid` | 房主的用户标识符 |
| `players` | `PlayerInfo[]` | 房间内的玩家状态 |
| `speed` | `i8` | 游戏速度设置 |
| `map_name` | `str` | 选择导入的地图名(空则表示不使用) |
| `terrian_cfg` | `RandomTerrainConfig` | 随机地图的生成配置 |
| `modifiers` | `Modifier[]` | 已经启用的特殊规则 |
**类型** `GameTickInfo`:用于表示游戏一个半回合的差分数据。
| 成员标识符 | 类型 | 含义 |
| :----------------- | :-------------- | :----------------------------- |
| `halfturn_id` | `u32` | 当前的半回合数 |
| `defeated_players` | `Player[]` | 在这个半回合内被打败的玩家列表 |
| `alive_players` | `Player[]` | 剩余的未被击败的玩家 |
| `leaderboard` | `TeamRanking[]` | 队伍排名 |
| `diffs` | `TeamViewDiff` | 所在队伍的视野差分 |
**类型** `Message`:用于表示一个消息或事件通知。
| 成员标识符 | 类型 | 含义 |
| :--------- | :----- | :------------- |
| `sender` | `uuid` | 发送消息的主体 |
| `msg` | `str` | 消息内容 |
## 用户账户与认证
用户账户是玩家身份的表示,每个用户账户拥有一个唯一的用户登录名和一个唯一的用户全局唯一标识符。用户登录名一旦设置则不可更改。登录名的字符集限制为大小写字母、数字和下划线的组合。用户的全局唯一标识符使用 UUID V5 生成(遵循 RFC 4122。每个用户还有一个用户显示名用于在页面各处展示该名称可以修改且不设置字符集限制使用 UTF-8 编码。
必要时,用户的登录名和显示名应用特定的屏蔽词列表。
### 用户的全局唯一标识符的生成
用户的全局唯一标识符使用 UUID V5 生成,其中的命名空间为 `aff7791d-4b71-4187-9788-13fa0c7fb51e`,名字为 `account:` 前缀加上用户登录名。例如登录名为 `alice` 的账户的全局唯一标识符为 `96464c99-5f55-5c47-95f2-3b02c46181c9`
### 用户类型
用户类型决定了用户的权限和对用户使用的约束,用户类型包括:
| 用户类型名称 | 通信编码 | 描述 |
| ------------ | -------- | ------------------------------------------------------------ |
| 封禁用户 | 0x00 | 此类用户不可登录,亦不可参与对局 |
| 普通用户 | 0x01 | 普通的人类玩家,只能同时加入一个对局 |
| 管理员 | 0x02 | 管理员用户,可以修改各类设置 |
| 机器人 | 0x03 | 机器人玩家,可以同时加入多个对局 |
| 临时用户 | 0x04 | 下次登录时此用户必须修改密码,然后转变为普通用户 |
| 超级管理员 | 0xFF | 管理员用户,可以修改各类设置,包括那些与硬件相关的设置,不可参与对局,必须设置两步验证 |
### 用户登录认证
用户登录时,主要认证手段为密码。用户在注册或修改密码时,需要至少设置一个长 6 字符的密码。这个密码会在加盐并计算 SHA-256 摘要值(遵循 RFC 6234后存储。无论何时适配器实现应该保证**从不**存储用户的明文密码,亦**不**在日志中泄露密码信息。
必要时,对用户的密码设置一个弱口令屏蔽表。
用户亦可启用时间性一次性口令(遵循 RFC 6238作为两步验证。在启用时与用户交换一个长 20 字节的随机秘钥。在登录时,要求用户给出 6 位数的一次性时间口令,其中时间步长度为 30 秒。
至少在用户启用两步验证时,需对登录接口设置至多每 30 秒 1 次的限流,防止潜在的攻击。
在认证成功后,适配器向用户发放 JWT 令牌标识身份,令牌的有效期不超过 400 天。
## 数据的持久化
适配器不仅仅在其内存中维护状态,而是要在持久化的介质中存储其需要长期保存的数据,例如对局回放、登录信息等。由于需要存储的数据量较小,且无需复杂的查询模式,适配器在存储数据时可以直接使用依托文件系统或者其他类似结构的对象存储。
### 持久化中使用的类型
**类型** `Account`:用于表示一个玩家账号。
| 成员标识符 | 类型 | 含义 |
| --------------- | ------------------- | -------------------------------------- |
| `uid` | `uuid` | 账户的全局唯一标识符(**主键** |
| `type` | `u8` | 用户类型 |
| `handle` | `str` | 用户登录名 |
| `name` | `str` | 用户显示名 |
| `passwd` | `u8[32]` | 用户密码的加盐 SHA256 摘要值 |
| `totp_key` | `u8[20]` | 用户的时间性一次性口令的秘钥 |
| `creation_time` | `DateTime` | 账户创建时间 |
| `rating` | `i32` | 用户的等级分 |
| `preference` | `AccountPreference` | 用户的偏好设置 |
| `replays` | `uuid[]` | 用户参与的比赛回放的全局唯一标识符列表 |