opengenerals/docs/server.md

21 KiB
Raw Permalink Blame History

结构化数据编码

为了实现高效的远程过程调用和数据持久化,适配器使用“结构化数据编码”将内存中的数据结构编码为二进制。

在把一些数据编码为二进制数据时,逐个将其成员按照顺序紧密地放在一起。在编码整数或浮点数时,直接使用小端序的二进制表示。在编码字符串或不定长数组时,首先给出一个 4 字节的无符号整数表示长度,然后逐个给出其成员元素。

结构化数据编码中的基本类型

  • u8u16u32u64:无符号 8/16/32/64 位整数;
  • i8i16i32i64:有符号 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字节无符号整数
  • 函数返回的数据。

与控制器交互中使用的类型

对于类型 RandomTerrainConfigPlayerMovePlayerRankingTeamRankingTileTileDiffTeamViewDiff 以及 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 消息内容

适配器实现的函数

标识符 函数名 函数功能 函数参数类型名 函数返回值类型名
10 joinRoom 加入房间 void void
11 joinTeam 加入队伍 Team void
20 giveOwner 转让房主 uuid void
21 setImportMapName 设置导入的地图名称 str void
22 setRandomTerrianConfig 设置随机地图配置 RandomTerrianConfig void
30 takeOwner 取得房主 void void
40 requestStart 开始游戏 void void
50 appendOrderQueue 向玩家的行动队列的末尾添加一个移动 PlayerMove void
51 clearMoveQueue 清空玩家的行动队列 void void
52 popOrderQueue 清除玩家的行动队列的最后一项 void void
53 surrender 投降 void void

用户账户与认证

用户账户是玩家身份的表示,每个用户账户拥有一个唯一的用户登录名和一个唯一的用户全局唯一标识符。用户登录名一旦设置则不可更改。登录名的字符集限制为大小写字母、数字和下划线的组合。用户的全局唯一标识符使用 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] 用户的时间性一次性口令的秘钥
created_at DateTime 账户创建时间
rating i32 用户的等级分
preference AccountPreference 用户的偏好设置
replays uuid[] 用户参与的比赛回放的全局唯一标识符列表