《MMO 服务器开发》13.2 公会仓库与捐献流水
公会仓库是多人共享的资产容器,捐献则是一份只增不改的流水账。本节用 aegis 从零实现:仓库按物品种类限槽但允许无限堆叠,捐献用请求号幂等去重避免重试翻倍,提现按职位控权并按日限额。重点讲清「先判幂等再写库存」的顺序为什么是正确性的关键,并用真跑的 L1 测试验证重试不翻倍、超额提现被拦、跨日重置生效。
category
公会仓库是多人共享的资产容器,捐献则是一份只增不改的流水账。本节用 aegis 从零实现:仓库按物品种类限槽但允许无限堆叠,捐献用请求号幂等去重避免重试翻倍,提现按职位控权并按日限额。重点讲清「先判幂等再写库存」的顺序为什么是正确性的关键,并用真跑的 L1 测试验证重试不翻倍、超额提现被拦、跨日重置生效。
公会系统是 MMO 里第一个跨玩家的持久组织:入会有自由与审批两种策略,职位有会长到成员五级,权限是一张职位到动作的位掩码表。本节用 aegis 从零实现单服公会模型——把权限收敛成一张可审计的表、让入会与审批幂等、把职位变更约束在层级之内,并用真跑的 L1 测试钉死满员竞态、重复申请与越级任免三条边界。
队长掉线了,谁带队进副本?本节把「掉线」与「退队」拆成两件事:掉线只灭在线标志、保留座位等重连,超时未回才回收;队长一掉线立刻按加入顺序把队长位交给下一位在线成员,且重连后不归还。全部用 go test -race 真跑,并讲清它与卷一顶号、重连的衔接。
上一节把入口控制收成了一处判断,但「先 Count 再 Add」这个写法本身在并发下就是错的:两个邀请可能都看到空位,把队伍撑到 5 人。本节把满员判断与写入压进同一个临界区,再把「满员校验 + 匹配锁定」也做成原子操作,用 go test -race 复现并证明修复。
副本、排位、世界 Boss 都要先问一句:这个玩家能不能进这支队伍。本节把队伍建成一个带生命周期状态机的服务端对象,用状态、队长身份、成员集合三道闸把入口控制钉死,并用 Go 标准库写出可直接 go test 的 L1 实现,再把每道门逐一撞开。
好友关系是 MMO 里最容易不一致的共享状态:它是双向的,一次只写一半就会留下「半边关系」。本节把关系图建成有向边、把双向一致写死成不变量、用全局唯一的 opID 幂等窗口挡住重发,并给出半边检测与补偿的实现,呼应卷一的幂等窗口与顺序保证,让关系一致从口头约定变成机器可验证的断言。
敏感词与限速是聊天管线的两级治理:前者挡内容,后者挡频率,两者都必须便宜到能挂在每条消息的同步路径上。本节用 AC 自动机实现一次扫描命中所有敏感词、用每玩家每频道的令牌桶限速,再把禁言、限速、过滤串成一条顺序固定、可 go test 的三级管线,并给出本机实测的匹配开销对照。
聊天是 MMO 里最容易被低估的系统:频道种类多、投递范围各异、发言资格还会随等级、队伍与活动变化。本节把频道建模成数据、把投递范围收敛成一份最小世界视图、把权限判断写成纯函数,用三个能 go test 通过的 L1 实现讲清频道模型与权限的边界。
每日任务几点重置,是整条时间链上最容易被时区坑掉的地方:主机在哪个时区、夏令时怎么切、时钟被 NTP 往回拨一下会不会重置两次。本节把「今天」钉在一个固定时区上,用「日键」而不是时间减法判断跨天,并让重置跟踪器只向前触发,最后用真跑的测试覆盖边界、周界与时钟回拨。
战斗里杀一只怪、背包里捡一件东西,凭什么变成任务进度条上的 +1?本节把玩法事件收敛成一个统一的 Event 结构,让进度服务只认「类型、目标、数量」三元组,再演示计数封顶、就绪转换,以及用请求序号做重传去重,保证同一条上报不会把进度加两次。
任务表里一条前置关系画错,整条任务线就会锁死:玩家接了 A 却发现要先完成 B,而 B 又要求先完成 A。本节把任务配置建成有向图,用 Kahn 算法做拓扑排序,用三色 DFS 把设计者画错的那一圈环精确指出来,再用一组真跑的 Go 测试证明正常表能排出合法顺序、环表能被定位到具体节点。
离线收益与体力是同一类问题的两面:玩家下线后时间还在走,服务端要在他回来时把这段时间补上。本节从零写 internal/attr 的 Stamina 与 SettleOffline,用整数周期推进避免短会话丢点,并处理离线封顶、时钟回拨与满体力不攒点三类边界,全部 go test 验收。
等级是成长的核心状态机:一次经验结算可能连升多级、可能撞到满级、还要把溢出经验正确结转。本节从零写 internal/attr 的 Curve 与 Progression,用相位 idle/levelup/max 约束迁移,并用 go test 钉住连升、结转、封顶与非法迁移四类边界。
属性是玩法系统的地基,装备、buff、称号都会改数值,聚合顺序写错就会算错伤害。本节从零写 internal/attr 包,用万分比定点做「平加 → 加算百分比 → 乘算百分比」三层聚合,实现同类取最强与缓存失效,并复用卷一 CmdAttrSync 0x0103 做增量同步,附 go test 验收。
装备是背包里唯一同时带「位置」和「个体属性」的东西。本节实现装备位与换装(旧装备换下回背包),给出按等级配置成功率、材料消耗与保底等级的强化表;强化在同一事务里扣材料、失败掉级、材料不足整笔回滚,随机源由调用方注入以保证可复现,产出的强化等级作为第 9 章属性计算的输入。
同一个背包会被战斗结算、任务奖励、交易行回调同时修改。本节用「副本 + 提交 + 回滚」把一组改动包成事务,用原子指针让读路径完全无锁,并用操作号实现幂等,挡住客户端重发与上游事件重放;配套的 32 goroutine 并发加物用例在 go test -race 下真跑。
背包最容易被低估的是「一个物品到底是什么」。本节把配置表物品 ID 与背包内实例 ID 拆开,用一套结构同时表达可堆叠与不可堆叠物品,并把堆叠合并、容量预检与「失败即整体不动」的原子性写成一份能跑过的 Go 实现,为 8.2 的并发事务打底。
纯随机会让倒霉的玩家永远倒霉,所以要有保底;纯随机又让客服无法自证清白,所以要有审计。本节在 internal/loot 里加两个部件:硬保底与软保底计数器,以及把每次掉落串成哈希链的审计日志,让整条掉落链路可复算、可追责,并全部用 go test 真跑验证。
本节定稿全卷的随机口径:用 math/rand/v2 的 PCG,为每个实体单独构造 PRNG,种子 = hash(玩家ID, 掉落批次序号),禁止全局 rand。把为什么全局随机源会让结果不可复算讲透,并给出可测的种子推导与重放实现,这是后续所有随机玩法的地基。
掉落是玩法系统的第一个随机点:怪物死了掉什么、掉几个。本节从零写 internal/loot 包,用权重表描述候选物品、在 [Min,Max] 区间取数量,把多次抽取聚合成一次可复算的掉落结算,并附一份能直接 go test 跑过的 L1 测试。