麻将系统架构
修订记录
| 版本 | 日期 | 说明 |
|---|---|---|
| v1.0 | 2026-07-16 | 首次发布 |
一、系统概述
麻将(Mahjong)是某MMO项目中的一个子游戏系统,属于畅玩阁(SmallGame)体系。采用分布式跨服架构,由五个服务器角色、一个客户端和一个通用小游戏框架组成。
1.1 系统边界
#mermaid-svg-Jiab0R6o3v7neY2J{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Jiab0R6o3v7neY2J .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Jiab0R6o3v7neY2J .error-icon{fill:#552222;}#mermaid-svg-Jiab0R6o3v7neY2J .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Jiab0R6o3v7neY2J .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Jiab0R6o3v7neY2J .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Jiab0R6o3v7neY2J .marker.cross{stroke:#333333;}#mermaid-svg-Jiab0R6o3v7neY2J svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Jiab0R6o3v7neY2J p{margin:0;}#mermaid-svg-Jiab0R6o3v7neY2J .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Jiab0R6o3v7neY2J .cluster-label text{fill:#333;}#mermaid-svg-Jiab0R6o3v7neY2J .cluster-label span{color:#333;}#mermaid-svg-Jiab0R6o3v7neY2J .cluster-label span p{background-color:transparent;}#mermaid-svg-Jiab0R6o3v7neY2J .label text,#mermaid-svg-Jiab0R6o3v7neY2J span{fill:#333;color:#333;}#mermaid-svg-Jiab0R6o3v7neY2J .node rect,#mermaid-svg-Jiab0R6o3v7neY2J .node circle,#mermaid-svg-Jiab0R6o3v7neY2J .node ellipse,#mermaid-svg-Jiab0R6o3v7neY2J .node polygon,#mermaid-svg-Jiab0R6o3v7neY2J .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Jiab0R6o3v7neY2J .rough-node .label text,#mermaid-svg-Jiab0R6o3v7neY2J .node .label text,#mermaid-svg-Jiab0R6o3v7neY2J .image-shape .label,#mermaid-svg-Jiab0R6o3v7neY2J .icon-shape .label{text-anchor:middle;}#mermaid-svg-Jiab0R6o3v7neY2J .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Jiab0R6o3v7neY2J .rough-node .label,#mermaid-svg-Jiab0R6o3v7neY2J .node .label,#mermaid-svg-Jiab0R6o3v7neY2J .image-shape .label,#mermaid-svg-Jiab0R6o3v7neY2J .icon-shape .label{text-align:center;}#mermaid-svg-Jiab0R6o3v7neY2J .node.clickable{cursor:pointer;}#mermaid-svg-Jiab0R6o3v7neY2J .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Jiab0R6o3v7neY2J .arrowheadPath{fill:#333333;}#mermaid-svg-Jiab0R6o3v7neY2J .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Jiab0R6o3v7neY2J .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Jiab0R6o3v7neY2J .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jiab0R6o3v7neY2J .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Jiab0R6o3v7neY2J .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jiab0R6o3v7neY2J .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Jiab0R6o3v7neY2J .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Jiab0R6o3v7neY2J .cluster text{fill:#333;}#mermaid-svg-Jiab0R6o3v7neY2J .cluster span{color:#333;}#mermaid-svg-Jiab0R6o3v7neY2J div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Jiab0R6o3v7neY2J .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Jiab0R6o3v7neY2J rect.text{fill:none;stroke-width:0;}#mermaid-svg-Jiab0R6o3v7neY2J .icon-shape,#mermaid-svg-Jiab0R6o3v7neY2J .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jiab0R6o3v7neY2J .icon-shape p,#mermaid-svg-Jiab0R6o3v7neY2J .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Jiab0R6o3v7neY2J .icon-shape .label rect,#mermaid-svg-Jiab0R6o3v7neY2J .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jiab0R6o3v7neY2J .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Jiab0R6o3v7neY2J .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Jiab0R6o3v7neY2J :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 畅玩阁 SmallGame
麻将 Mahjong(本文档)
其他玩法(SmallGame 管理)
共享基础设施
货币 attr68/69
配置加载 smallgame_config_loader
段位系统
畅玩阁 UI changwange/
1.2 Little Game Framework
畅玩阁(SmallGame)提供通用基础设施:
| 能力 | 接口 | 说明 |
|---|---|---|
| 货币操作 | AwardCurrency / GetCurrencyBalance / GetFrozenCurrencyBalance |
属性68/69 |
| 配置加载 | smallgame_config_loader.* |
委托式访问 |
| 子游戏注册 | RegisterGameModule(nMatchType, tGameModule) |
按匹配类型注册 |
| 自走棋回调 | GameServer_OnSettlement / GameServer_QueryRewardType / GameServer_QueryConsumeType |
外游戏扩展 |
| 畅玩阁日志 | LogFunGame(player, szStepNumid, nResults, tData) |
对局生命周期日志 |
| 玩家数据 | GetRole(dwID) / GetPlayingRole(dwID) |
跨服/本服 |
1.3 模块加载顺序
modules/modulelist.xml:
modules/chat ← 第31行
modules/smallgame ← 第243行(先于麻将加载)
modules/mahjong ← 第244行
ui/modulelist_game.xml:
ui/chat ← 第10行(最早加载的UI之一)
ui/changwange ← 第161行(畅玩阁UI)
ui/mahjong ← 第162行(麻将UI)
结论:chat 最早加载,smallgame 次之,mahjong 最后。smallgame Include chat 时 chat 的全局变量已存在。
1.4 模块配置
modules/mahjong/config.xml 中每个 <节点> 子目录是引擎框架定义的运行环境。麻将按节点分别加载不同的脚本:
xml
<GameClient> ← 客户端运行时
加载:mahjong_dump, config_loader, define, voice_define, map, timer,
verifier, fan, voice, client_core, client_ai, client_recommend,
client_autoplay, client_profile (14个)
用途:客户端内置的麻将逻辑。包含 g_tData 数据源(client_core)、
胡牌检测(verifier)、番型计算(fan)、推荐引擎(recommend)、
AI托管(client_ai)、自动出牌(autoplay)。UI 通过 Include 引入
<GameServer> ← 服务器运行时
加载:mahjong_dump, config_loader, define, voice_define, verifier, map,
timer, play_server, game_server (9个)
用途:GameServer(玩家C2S入口+货币操作)和 PlayServer(对局逻辑)
同进程运行。verifier 用于服务端校验胡牌合法性
<GameCenter> ← 消息路由
加载:mahjong_dump, config_loader, define, voice_define, map, timer,
game_center, match_center (8个)
用途:game_center 负责消息转发,match_center 负责 PlayServer 分配和路由
<SceneServer> ← 房间管理
加载:mahjong_dump, config_loader, define, voice_define, map, timer,
snowflake, scene (8个)
用途:房间生命周期管理、匹配、结算重试。snowflake 生成唯一房间ID
不加载麻将脚本的节点:LogServer、ServerMergeTool
1.5 文件统计
| 目录 | 文件数 | 总行数 | 说明 |
|---|---|---|---|
modules/mahjong/ |
15 | ~23500 | 服务器 + 客户端逻辑 |
ui/mahjong/ |
8 | ~14500 | 麻将UI |
modules/smallgame/ |
5 | ~1500 | 小游戏框架(被麻将 Include) |
ui/changwange/ |
12 | --- | 畅玩阁面板 |
modules/mahjong/docs/ |
4 | --- | 文档 |
二、服务器架构
2.1 服务器身份
| 身份 | 进程 | 权威域 | 职责 |
|---|---|---|---|
| GameServer | 同进程 | 玩家数据权威 | C2S 唯一入口。SavedData 唯一读写端。货币唯一操作者(Award/Consume)。结算 Token 唯一维护者。段位分写入 |
| PlayServer | 同进程 | 对局逻辑权威 | 牌墙唯一拥有者。碰杠胡唯一检测者。分数计算。AI 决策。AutoTakeOut 结算发布 |
| GameCenter | 同进程 | 消息路由 | 无状态转发。nConnIndex → GameServer 路由。跨中心转发。SceneServer/MatchCenter 可用性检查 |
| MatchCenter | 同进程 | PlayServer 调度 | PlayServer 分配(加权选服)。路由表 g_MAHJONG_rooms_by_id。健康检查 OnTickHealthCheck。跨中心匹配 g_tCrossMatches |
| SceneServer | 独立 | 房间管理权威 | 房间创建/销毁/状态迁移。匹配池管理。玩家在线/离线跟踪。结算重试引擎。雪花ID生成 |
边界规则:
- GameServer 不碰牌墙、不检测胡牌、不创建房间
- PlayServer 不操作货币、不读写 SavedData
- SceneServer 不操作货币、不进行游戏逻辑
- GameCenter 不缓存任何数据、不做业务决策
- MatchCenter 只记录路由、不干预游戏逻辑
核心约束:
- GameServer = PlayServer 同进程(同地址空间),相同 ID
- GameCenter = MatchCenter 同进程(同地址空间),相同 ID
- 同进程内调用走直接函数调用(
g_tCrossCallTargets) - SceneServer 是唯一独立进程
2.2 切图导致断线
玩家在 GS 之间切换(切图)时,客户端主动断开与当前 GS 的连接,连接到目标 GS。数秒内玩家处于离线状态:
GameServer B(新) GameServer A(旧) Client GameServer B(新) GameServer A(旧) Client #mermaid-svg-RSmSQp8SeWHV9PvP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RSmSQp8SeWHV9PvP .error-icon{fill:#552222;}#mermaid-svg-RSmSQp8SeWHV9PvP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RSmSQp8SeWHV9PvP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RSmSQp8SeWHV9PvP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RSmSQp8SeWHV9PvP .marker.cross{stroke:#333333;}#mermaid-svg-RSmSQp8SeWHV9PvP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RSmSQp8SeWHV9PvP p{margin:0;}#mermaid-svg-RSmSQp8SeWHV9PvP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RSmSQp8SeWHV9PvP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RSmSQp8SeWHV9PvP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-RSmSQp8SeWHV9PvP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-RSmSQp8SeWHV9PvP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-RSmSQp8SeWHV9PvP .sequenceNumber{fill:white;}#mermaid-svg-RSmSQp8SeWHV9PvP #sequencenumber{fill:#333;}#mermaid-svg-RSmSQp8SeWHV9PvP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-RSmSQp8SeWHV9PvP .messageText{fill:#333;stroke:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RSmSQp8SeWHV9PvP .labelText,#mermaid-svg-RSmSQp8SeWHV9PvP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .loopText,#mermaid-svg-RSmSQp8SeWHV9PvP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RSmSQp8SeWHV9PvP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RSmSQp8SeWHV9PvP .noteText,#mermaid-svg-RSmSQp8SeWHV9PvP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-RSmSQp8SeWHV9PvP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RSmSQp8SeWHV9PvP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RSmSQp8SeWHV9PvP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RSmSQp8SeWHV9PvP .actorPopupMenu{position:absolute;}#mermaid-svg-RSmSQp8SeWHV9PvP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-RSmSQp8SeWHV9PvP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RSmSQp8SeWHV9PvP .actor-man circle,#mermaid-svg-RSmSQp8SeWHV9PvP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-RSmSQp8SeWHV9PvP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 切图触发 nSrcCenterID 判断玩家离线 数秒断线窗口 玩家以新连接上线 主动断开连接 设置 MAHJONG_PLAYER_STATUS_OFFLINE AI 托管接管出牌 连接到新 GS 触发 OnPlayerEnterScene 检查孤儿冻结 → RollbackMahjongPendingEntry
诚实标注------半开闭状态同步问题:
切图期间玩家与旧 GS 的连接已断开,但新 GS 的连接尚未建立。这构成一个半开闭状态:
- GameServer 侧 :玩家标记为
OFFLINE,nLeaveSceneTime记录断线时刻。player.mahjong清空(=nil),等待重连后 Guardians 重放再恢复 - PlayServer 侧 :无法确定玩家是在切图还是真断线。只能通过超时被动感知------玩家必须在超时前(MAHJONG_RESPONSE_TIMEOUT 客户端倒计时)做出出牌/碰杠响应,否则 AI 托管接管。PlayServer 不反向通知 SceneServer(见 §3.7),因为两个平等状态(PlayerOnline vs PlayerOffline)通过网络传输存在乱序到达风险,会导致 SceneServer 状态振荡无法收敛
- SceneServer 侧 :仅通过 GS 的单向通知(
PlayerOffline/PlayerOnline)推算玩家状态。自身不做状态仲裁 - 实际后果 :
- 玩家的出牌决策在断线窗口期由 AI 代为决策(可能不符合玩家意图)
- 若切图发生在接牌/弃牌等关键交互节点前,AI 可能错过该操作导致本轮空过
- 结算/退出/崩溃等资金操作不受影响(Token 流水线在 GS 端原子化执行,不依赖玩家状态)
- 不可消除:半开闭状态是分布式架构的固有问题。系统已通过三类防护最小化影响:AI 超时代管、Guardians 持久化保障资金安全、结算重试兜底
防护:
- 结算重试有 per-session 4次上限(约20秒,覆盖最坏10秒断线窗口),跨 session 12次上限(约1分钟,覆盖极端重连)
- AutoTakeOut 由 Guardians 持久化,不因断线丢失
- 断线时间不计入响应超时(
Room_HandleResponseTimeout对 AI 不做惩罚)
#mermaid-svg-hPNCsaFWZGqkaicr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hPNCsaFWZGqkaicr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hPNCsaFWZGqkaicr .error-icon{fill:#552222;}#mermaid-svg-hPNCsaFWZGqkaicr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hPNCsaFWZGqkaicr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hPNCsaFWZGqkaicr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hPNCsaFWZGqkaicr .marker.cross{stroke:#333333;}#mermaid-svg-hPNCsaFWZGqkaicr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hPNCsaFWZGqkaicr p{margin:0;}#mermaid-svg-hPNCsaFWZGqkaicr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hPNCsaFWZGqkaicr .cluster-label text{fill:#333;}#mermaid-svg-hPNCsaFWZGqkaicr .cluster-label span{color:#333;}#mermaid-svg-hPNCsaFWZGqkaicr .cluster-label span p{background-color:transparent;}#mermaid-svg-hPNCsaFWZGqkaicr .label text,#mermaid-svg-hPNCsaFWZGqkaicr span{fill:#333;color:#333;}#mermaid-svg-hPNCsaFWZGqkaicr .node rect,#mermaid-svg-hPNCsaFWZGqkaicr .node circle,#mermaid-svg-hPNCsaFWZGqkaicr .node ellipse,#mermaid-svg-hPNCsaFWZGqkaicr .node polygon,#mermaid-svg-hPNCsaFWZGqkaicr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hPNCsaFWZGqkaicr .rough-node .label text,#mermaid-svg-hPNCsaFWZGqkaicr .node .label text,#mermaid-svg-hPNCsaFWZGqkaicr .image-shape .label,#mermaid-svg-hPNCsaFWZGqkaicr .icon-shape .label{text-anchor:middle;}#mermaid-svg-hPNCsaFWZGqkaicr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hPNCsaFWZGqkaicr .rough-node .label,#mermaid-svg-hPNCsaFWZGqkaicr .node .label,#mermaid-svg-hPNCsaFWZGqkaicr .image-shape .label,#mermaid-svg-hPNCsaFWZGqkaicr .icon-shape .label{text-align:center;}#mermaid-svg-hPNCsaFWZGqkaicr .node.clickable{cursor:pointer;}#mermaid-svg-hPNCsaFWZGqkaicr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hPNCsaFWZGqkaicr .arrowheadPath{fill:#333333;}#mermaid-svg-hPNCsaFWZGqkaicr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hPNCsaFWZGqkaicr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hPNCsaFWZGqkaicr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hPNCsaFWZGqkaicr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hPNCsaFWZGqkaicr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hPNCsaFWZGqkaicr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hPNCsaFWZGqkaicr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hPNCsaFWZGqkaicr .cluster text{fill:#333;}#mermaid-svg-hPNCsaFWZGqkaicr .cluster span{color:#333;}#mermaid-svg-hPNCsaFWZGqkaicr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hPNCsaFWZGqkaicr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hPNCsaFWZGqkaicr rect.text{fill:none;stroke-width:0;}#mermaid-svg-hPNCsaFWZGqkaicr .icon-shape,#mermaid-svg-hPNCsaFWZGqkaicr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hPNCsaFWZGqkaicr .icon-shape p,#mermaid-svg-hPNCsaFWZGqkaicr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hPNCsaFWZGqkaicr .icon-shape .label rect,#mermaid-svg-hPNCsaFWZGqkaicr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hPNCsaFWZGqkaicr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hPNCsaFWZGqkaicr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hPNCsaFWZGqkaicr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 畅玩阁系统
SceneServer 独立进程
GameCenter + MatchCenter 同进程
GameServer + PlayServer 同进程
CallServer
CallCenter
module_award / module_consume
CallScene
CallCenter
CallServer
CallCenter
C2S协议
Include
客户端 - game*.exe
麻将UI层
mahjong_ui.lua + mahjong_main_panel.lua
Bridge层
mahjong_ui_common.lua
Core层
mahjong_client_core.lua
胡牌检测
mahjong_verifier.lua
番型计算
mahjong_fan.lua
推荐引擎
mahjong_client_recommend.lua
GameServer
C2S入口 / SavedData / 货币操作
PlayServer
对局状态机 / 算分 / AI
GameCenter
消息转发器
MatchCenter
PlayServer分配 / 动作路由 / 健康检查
SceneServer
房间管理 / 匹配池 / 结算重试 / 在线状态
SmallGame框架
AwardCurrency / GetCurrency
RegisterGameModule
畅玩阁UI
changwange_main_panel
2.3 服务器间通信
跨服调用规则:
同进程内(ID 相同):直接函数调用
跨服务器:指定目标 ID,接受方第一个参数为来源 ID
Center → 其它 Center:传目标 CenterID,接受方 srcCenterID
Center → Scene:传目标 SceneID,接受方 srcCenterID
Scene → Center/其它 Scene:传目标 ID,接受方 srcSceneID
GameServer → 同进程直调(GameServer=PlayServer 同地址空间同 ID)
GameServer ↔ GameCenter :GameServer 只能调自己的 Center(不指定 ID),Center 通过 nConnIndex(玩家连接索引)路由回对应 GameServer。
转发协议命名:
SceneServer2MatchCenter_CreateRoom 源_目标_功能名
GameCenter2SceneServer_ExitRoom 同上
Forward_ToGameServer:
lua
local function Forward_ToGameServer(dwID, szProtocol, ...)
-- 1. 玩家在本 Center 在线 → 直接调本服 GameServer
local tPlaying = GetPlayingRole(dwID)
if tPlaying then
module:CallServer(tPlaying.nConnIndex, szProtocol, ...)
return true
end
-- 2. 玩家在远程 Center → 跨中心转发
if IsRemotePlayer(dwID) then
local tBase = GetRole(dwID)
if tBase then
if not IsCenterConnected(tBase.nCenterID) then return false end
module:CallCenter(tBase.nCenterID,
"GameCenter2GameCenter_ForwardToGameServer",
dwID, szProtocol, ...)
return true
end
end
-- 3. 本 Center 离线
return false
end
nCenterID > 0 的处理:
GetRole(dwID).nCenterID > 0在正常情况下表示玩家隶属于某个远程 Center- 但在跨服降级时,
nCenterID可能为 0(数据过时) - 因此不能用
nCenterID > 0判断"玩家是否在远程" - 用
IsRemotePlayer(dwID)更可靠(框架提供,处理了边界情况) - 定位优先级:
GetPlayingRole(本服在线)>IsRemotePlayer(远程)> 离线
Forward_ToSceneServer:
lua
Forward_ToSceneServer = function(nSceneServerID, szProtocol, ...)
if not nSceneServerID or not IsSceneServerConnected(nSceneServerID) then
return false -- TOCTOU:检查与调用间可能断开,调用方用超时补偿
end
module:CallSceneServer(nSceneServerID, szProtocol, ...)
return true
end
Forward_ToMatchCenter:
lua
Forward_ToMatchCenter = function(nMatchCenterID, szProtocol, ...)
if nMatchCenterID == GetCenterID() then -- 同进程直接调
local fn = g_tCrossCallTargets[szProtocol]
if fn then fn(...); return true end
end
if IsCenterConnected(nMatchCenterID) then
module:CallCenter(nMatchCenterID, szProtocol, ...)
return true
end
return false
end
2.4 异步调用与重试策略
所有 CallCenter / CallScene / CallServer 调用是异步的,且不保证一定成功。重试取决于数据类型和重要性,分三类:
| 类型 | 触发方 | 示例 | 重试策略 | 携带数据 |
|---|---|---|---|---|
| 玩家主动重试 | 客户端 | C2S 请求超时重发 | 5 秒超时,客户端自行决定 | 全量请求参数 |
| 服务器主动重试 | 服务器 | EndGame 转发失败重试 | Settlement_RetryTick 每 5 秒,上限 3 次/局,10 次/总 | 完整结算数据(含 tResultData) |
| 不重试 | --- | 房间状态同步、Online/Offline 通知 | 不重试,丢失不补 | --- |
重试携带完整数据:
以 EndGame 为例:重试时携带的是完整的 tResultData,包含所有玩家的结算结果、手牌、番型等。GameServer 收到后:
收到完整 tResultData
→ 读取自身的 nLastProcessedSettleToken
→ 计算 nExpected = nLastProcessedSettleToken + 1
→ 比较 nSettleKey(来自 tResultData.nRound)vs nExpected
→ 理解:这次是全量结算还是已处理过的重复
→ 不需要额外状态查询
这意味着即使 GameServer 在之前的处理中崩溃丢失了内存状态,仅凭自身的 Token + PlayServer 传递的完整数据就能正确判定当前操作。这是幂等设计的核心。
不重试的场景:
- 玩家 Online / Offline 通知:状态最终由 SceneServer 通过游戏内轮询收敛
- 房间状态同步 SyncRoom:下一帧状态会覆盖前一帧
- 非真实数据(临时标记):丢失不影响资金安全
2.5 分布式事务与一致性
#mermaid-svg-e9cMkezXLcFTwcW2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-e9cMkezXLcFTwcW2 .error-icon{fill:#552222;}#mermaid-svg-e9cMkezXLcFTwcW2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-e9cMkezXLcFTwcW2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-e9cMkezXLcFTwcW2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-e9cMkezXLcFTwcW2 .marker.cross{stroke:#333333;}#mermaid-svg-e9cMkezXLcFTwcW2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-e9cMkezXLcFTwcW2 p{margin:0;}#mermaid-svg-e9cMkezXLcFTwcW2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 .cluster-label text{fill:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 .cluster-label span{color:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 .cluster-label span p{background-color:transparent;}#mermaid-svg-e9cMkezXLcFTwcW2 .label text,#mermaid-svg-e9cMkezXLcFTwcW2 span{fill:#333;color:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 .node rect,#mermaid-svg-e9cMkezXLcFTwcW2 .node circle,#mermaid-svg-e9cMkezXLcFTwcW2 .node ellipse,#mermaid-svg-e9cMkezXLcFTwcW2 .node polygon,#mermaid-svg-e9cMkezXLcFTwcW2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-e9cMkezXLcFTwcW2 .rough-node .label text,#mermaid-svg-e9cMkezXLcFTwcW2 .node .label text,#mermaid-svg-e9cMkezXLcFTwcW2 .image-shape .label,#mermaid-svg-e9cMkezXLcFTwcW2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-e9cMkezXLcFTwcW2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-e9cMkezXLcFTwcW2 .rough-node .label,#mermaid-svg-e9cMkezXLcFTwcW2 .node .label,#mermaid-svg-e9cMkezXLcFTwcW2 .image-shape .label,#mermaid-svg-e9cMkezXLcFTwcW2 .icon-shape .label{text-align:center;}#mermaid-svg-e9cMkezXLcFTwcW2 .node.clickable{cursor:pointer;}#mermaid-svg-e9cMkezXLcFTwcW2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-e9cMkezXLcFTwcW2 .arrowheadPath{fill:#333333;}#mermaid-svg-e9cMkezXLcFTwcW2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-e9cMkezXLcFTwcW2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-e9cMkezXLcFTwcW2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-e9cMkezXLcFTwcW2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-e9cMkezXLcFTwcW2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-e9cMkezXLcFTwcW2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-e9cMkezXLcFTwcW2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-e9cMkezXLcFTwcW2 .cluster text{fill:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 .cluster span{color:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-e9cMkezXLcFTwcW2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-e9cMkezXLcFTwcW2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-e9cMkezXLcFTwcW2 .icon-shape,#mermaid-svg-e9cMkezXLcFTwcW2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-e9cMkezXLcFTwcW2 .icon-shape p,#mermaid-svg-e9cMkezXLcFTwcW2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-e9cMkezXLcFTwcW2 .icon-shape .label rect,#mermaid-svg-e9cMkezXLcFTwcW2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-e9cMkezXLcFTwcW2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-e9cMkezXLcFTwcW2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-e9cMkezXLcFTwcW2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SAGA(流程编排)
AutoTakeOut 发布结算
GameServer 接收处理
Guardians 重试补偿
TCC(货币操作)
Try:ConsumeRes 冻结 attr69
Confirm:AwardCurrency 结算
Cancel:RefundFrozenOnAbnormal 回滚
整个系统没有跨服务器的分布式事务。每个服务器只在自己的数据域内做原子操作。跨服务器一致性靠两种模式混合保证:
TCC(Try-Confirm-Cancel):货币操作。
- Try:入场冻结
ConsumeRes(attr68) → attr69,资金已转移 - Confirm:结算成功
AwardCurrency,资金最终发放 - Cancel:Crash/退款
RefundFrozenOnAbnormal,资金退回 - Try 阶段已占用资源(attr69),Confirm/Cancel 二选一
SAGA(事件驱动补偿):流程编排。
- 正向操作:AutoTakeOut 发布结算 → ApplyMahjongSettlement
- 补偿操作:RollbackMahjongPendingEntry / Settlement_RetryTick
- 每个正向操作有对应的补偿操作,失败时通过补偿回到一致状态
事后补偿一致性:
- 保证 AP(可用性+分区容忍),不实时保证 C
- 真实数据(货币/段位分):AP + C,靠事后补偿闭环
- 非真实数据(房间状态):AP only,丢失不重试
- 补偿引擎:Guardians / AutoTakeOut,持久化重试保证送达
写前日志(Write-Ahead Log):结算操作前先持久化 Token,再执行 Award。服务器崩溃后重启时通过 Token 判断执行进度。这是 TCC 的 Try 阶段的实现基础。
2.7 帧同步模型
lua
-- 帧驱动:16帧 = 1秒(62.5ms/TICK)
module:StartActivate("OnTickTimer", 4) -- 每种服务器类型只能注册一个
TCP/IP 传输,帧尾按顺序发出数据包。非协程、非同步,标准异步编程结构(Actor 单调模型)。
Actor 模型规则:
- 调用方发出消息后不等待返回,继续执行
- 不保证消息送达,不保证处理顺序
- 调用方自行处理超时、重试、降级
- 调用其他服务器后不可立即访问可能被修改的数据(无同步读)
- 同进程直接函数调用不改变这些规则------调用的仍是另一个角色的函数
三、协议流转
3.1 匹配/创建/加入
SceneServer GameCenter GameServer SceneServer GameCenter GameServer #mermaid-svg-JMCLJzExObbwxmdr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JMCLJzExObbwxmdr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JMCLJzExObbwxmdr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JMCLJzExObbwxmdr .error-icon{fill:#552222;}#mermaid-svg-JMCLJzExObbwxmdr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JMCLJzExObbwxmdr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JMCLJzExObbwxmdr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JMCLJzExObbwxmdr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JMCLJzExObbwxmdr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JMCLJzExObbwxmdr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JMCLJzExObbwxmdr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JMCLJzExObbwxmdr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JMCLJzExObbwxmdr .marker.cross{stroke:#333333;}#mermaid-svg-JMCLJzExObbwxmdr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JMCLJzExObbwxmdr p{margin:0;}#mermaid-svg-JMCLJzExObbwxmdr .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JMCLJzExObbwxmdr text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JMCLJzExObbwxmdr .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JMCLJzExObbwxmdr .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JMCLJzExObbwxmdr .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JMCLJzExObbwxmdr .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JMCLJzExObbwxmdr #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JMCLJzExObbwxmdr .sequenceNumber{fill:white;}#mermaid-svg-JMCLJzExObbwxmdr #sequencenumber{fill:#333;}#mermaid-svg-JMCLJzExObbwxmdr #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JMCLJzExObbwxmdr .messageText{fill:#333;stroke:none;}#mermaid-svg-JMCLJzExObbwxmdr .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JMCLJzExObbwxmdr .labelText,#mermaid-svg-JMCLJzExObbwxmdr .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JMCLJzExObbwxmdr .loopText,#mermaid-svg-JMCLJzExObbwxmdr .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JMCLJzExObbwxmdr .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JMCLJzExObbwxmdr .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JMCLJzExObbwxmdr .noteText,#mermaid-svg-JMCLJzExObbwxmdr .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JMCLJzExObbwxmdr .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JMCLJzExObbwxmdr .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JMCLJzExObbwxmdr .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JMCLJzExObbwxmdr .actorPopupMenu{position:absolute;}#mermaid-svg-JMCLJzExObbwxmdr .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JMCLJzExObbwxmdr .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JMCLJzExObbwxmdr .actor-man circle,#mermaid-svg-JMCLJzExObbwxmdr line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JMCLJzExObbwxmdr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Room → MATCHING/WAITING Event_Publish(ROOM_INFO) Client ApplyMatch / CreateRoom / JoinByShareCode CheckMatchEntryAccess(等级+货币区间) 限流 MAHJONG_ENTRY_THROTTLE_MS=500ms CallCenter("GameServer2GameCenter_ApplyMatch", ...) Forward_ToSceneServer(sceneID, ...) Match_Apply / Room_CreateManual SceneServer2GameCenter_xxx(tResponse) Forward_ToGameServer(playerID, ...) Data_Update(tResponse) S2C_MAHJONG_ApplyMatch Client
3.2 准备与开局
PlayServer MatchCenter SceneServer GameCenter GameServer PlayServer MatchCenter SceneServer GameCenter GameServer #mermaid-svg-MM9ERCMMLLtHr1Li{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MM9ERCMMLLtHr1Li .error-icon{fill:#552222;}#mermaid-svg-MM9ERCMMLLtHr1Li .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MM9ERCMMLLtHr1Li .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MM9ERCMMLLtHr1Li .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MM9ERCMMLLtHr1Li .marker.cross{stroke:#333333;}#mermaid-svg-MM9ERCMMLLtHr1Li svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MM9ERCMMLLtHr1Li p{margin:0;}#mermaid-svg-MM9ERCMMLLtHr1Li .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MM9ERCMMLLtHr1Li text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MM9ERCMMLLtHr1Li .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-MM9ERCMMLLtHr1Li .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-MM9ERCMMLLtHr1Li #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-MM9ERCMMLLtHr1Li .sequenceNumber{fill:white;}#mermaid-svg-MM9ERCMMLLtHr1Li #sequencenumber{fill:#333;}#mermaid-svg-MM9ERCMMLLtHr1Li #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-MM9ERCMMLLtHr1Li .messageText{fill:#333;stroke:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MM9ERCMMLLtHr1Li .labelText,#mermaid-svg-MM9ERCMMLLtHr1Li .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .loopText,#mermaid-svg-MM9ERCMMLLtHr1Li .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MM9ERCMMLLtHr1Li .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-MM9ERCMMLLtHr1Li .noteText,#mermaid-svg-MM9ERCMMLLtHr1Li .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-MM9ERCMMLLtHr1Li .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MM9ERCMMLLtHr1Li .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MM9ERCMMLLtHr1Li .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MM9ERCMMLLtHr1Li .actorPopupMenu{position:absolute;}#mermaid-svg-MM9ERCMMLLtHr1Li .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-MM9ERCMMLLtHr1Li .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MM9ERCMMLLtHr1Li .actor-man circle,#mermaid-svg-MM9ERCMMLLtHr1Li line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-MM9ERCMMLLtHr1Li :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ConsumeRes attr68→attr69 nPendingFreezeToken 生成 alt 已准备(幂等) 全部准备 Client C2S_MAHJONG_Ready(dwRoomID) CheckPlayerInRoom FreezeMahjongBalanceForEntry CallCenter("GameServer2GameCenter_PlayerReady", ...) GameCenter2SceneServer_PlayerReady SUCCESS SceneServer2MatchCenter_CreateRoom Gs_SelectWithProbability(加权选服) MatchCenter2PlayServer_CreateRoom Room_Create + Game_Start(发牌/定庄) PlayServer2MatchCenter_SyncGameState MatchCenter2GameCenter_SyncGameState Forward_ToGameServer S2C_MAHJONG_SyncGameState Client
3.3 游戏动作(出牌)
PlayServer GameServer PlayServer GameServer #mermaid-svg-wH40rkAcyHF3BiPC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wH40rkAcyHF3BiPC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wH40rkAcyHF3BiPC .error-icon{fill:#552222;}#mermaid-svg-wH40rkAcyHF3BiPC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wH40rkAcyHF3BiPC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wH40rkAcyHF3BiPC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wH40rkAcyHF3BiPC .marker.cross{stroke:#333333;}#mermaid-svg-wH40rkAcyHF3BiPC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wH40rkAcyHF3BiPC p{margin:0;}#mermaid-svg-wH40rkAcyHF3BiPC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wH40rkAcyHF3BiPC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wH40rkAcyHF3BiPC .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wH40rkAcyHF3BiPC .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wH40rkAcyHF3BiPC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wH40rkAcyHF3BiPC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wH40rkAcyHF3BiPC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wH40rkAcyHF3BiPC .sequenceNumber{fill:white;}#mermaid-svg-wH40rkAcyHF3BiPC #sequencenumber{fill:#333;}#mermaid-svg-wH40rkAcyHF3BiPC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wH40rkAcyHF3BiPC .messageText{fill:#333;stroke:none;}#mermaid-svg-wH40rkAcyHF3BiPC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wH40rkAcyHF3BiPC .labelText,#mermaid-svg-wH40rkAcyHF3BiPC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wH40rkAcyHF3BiPC .loopText,#mermaid-svg-wH40rkAcyHF3BiPC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wH40rkAcyHF3BiPC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wH40rkAcyHF3BiPC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wH40rkAcyHF3BiPC .noteText,#mermaid-svg-wH40rkAcyHF3BiPC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wH40rkAcyHF3BiPC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wH40rkAcyHF3BiPC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wH40rkAcyHF3BiPC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wH40rkAcyHF3BiPC .actorPopupMenu{position:absolute;}#mermaid-svg-wH40rkAcyHF3BiPC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-wH40rkAcyHF3BiPC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wH40rkAcyHF3BiPC .actor-man circle,#mermaid-svg-wH40rkAcyHF3BiPC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wH40rkAcyHF3BiPC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 同进程 跨进程 Event_Publish(GAME_ACTION) Client C2S_Discard/Peng/Gang/Hu/Pass 直接函数调用 GameCenter → MatchCenter → PlayServer 游戏逻辑处理 GameAction 广播 S2C_MAHJONG_GameAction Client
3.4 胡牌与结算
SceneServer GameServer Guardians PlayServer SceneServer GameServer Guardians PlayServer #mermaid-svg-lnJUyc5vsHCE0cWA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lnJUyc5vsHCE0cWA .error-icon{fill:#552222;}#mermaid-svg-lnJUyc5vsHCE0cWA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lnJUyc5vsHCE0cWA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lnJUyc5vsHCE0cWA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lnJUyc5vsHCE0cWA .marker.cross{stroke:#333333;}#mermaid-svg-lnJUyc5vsHCE0cWA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lnJUyc5vsHCE0cWA p{margin:0;}#mermaid-svg-lnJUyc5vsHCE0cWA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lnJUyc5vsHCE0cWA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lnJUyc5vsHCE0cWA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lnJUyc5vsHCE0cWA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lnJUyc5vsHCE0cWA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lnJUyc5vsHCE0cWA .sequenceNumber{fill:white;}#mermaid-svg-lnJUyc5vsHCE0cWA #sequencenumber{fill:#333;}#mermaid-svg-lnJUyc5vsHCE0cWA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lnJUyc5vsHCE0cWA .messageText{fill:#333;stroke:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lnJUyc5vsHCE0cWA .labelText,#mermaid-svg-lnJUyc5vsHCE0cWA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .loopText,#mermaid-svg-lnJUyc5vsHCE0cWA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lnJUyc5vsHCE0cWA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lnJUyc5vsHCE0cWA .noteText,#mermaid-svg-lnJUyc5vsHCE0cWA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lnJUyc5vsHCE0cWA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lnJUyc5vsHCE0cWA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lnJUyc5vsHCE0cWA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lnJUyc5vsHCE0cWA .actorPopupMenu{position:absolute;}#mermaid-svg-lnJUyc5vsHCE0cWA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lnJUyc5vsHCE0cWA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lnJUyc5vsHCE0cWA .actor-man circle,#mermaid-svg-lnJUyc5vsHCE0cWA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lnJUyc5vsHCE0cWA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Game_PlayerHu nSettleKey == nExpected → Award nSettleKey < nExpected → SKIP nSettleKey > nExpected → RETRY alt 标准通道 Game_End(全量结算) AutoTakeOut 兜底(bExtraSettle=true) tCurrentRoom 已清除时走 ExtraSettlement ExtraSettlement 后发送 Ach → SceneServer 清除 pending nSettleKey = nSettleSeq + nCurrentRound AutoTakeOut(AO_SETTLE, nSettleKey) Mahjong_GuardiansSettle bSameRoom(roomID + matchType 均匹配?) ApplyMahjongSettlement OK nDelta = 全量(所有玩家均适用) PlayServer2MatchCenter_EndGame Settlement_AddPending GameCenter2GameServer_EndGame ApplyMahjongSettlement(全量) AchSettlement Settlement_RemovePending
3.5 退出房间
SceneServer GameCenter GameServer SceneServer GameCenter GameServer #mermaid-svg-VB9tNNEBja35fB26{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VB9tNNEBja35fB26 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VB9tNNEBja35fB26 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VB9tNNEBja35fB26 .error-icon{fill:#552222;}#mermaid-svg-VB9tNNEBja35fB26 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VB9tNNEBja35fB26 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VB9tNNEBja35fB26 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VB9tNNEBja35fB26 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VB9tNNEBja35fB26 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VB9tNNEBja35fB26 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VB9tNNEBja35fB26 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VB9tNNEBja35fB26 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VB9tNNEBja35fB26 .marker.cross{stroke:#333333;}#mermaid-svg-VB9tNNEBja35fB26 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VB9tNNEBja35fB26 p{margin:0;}#mermaid-svg-VB9tNNEBja35fB26 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VB9tNNEBja35fB26 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VB9tNNEBja35fB26 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VB9tNNEBja35fB26 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VB9tNNEBja35fB26 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VB9tNNEBja35fB26 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VB9tNNEBja35fB26 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VB9tNNEBja35fB26 .sequenceNumber{fill:white;}#mermaid-svg-VB9tNNEBja35fB26 #sequencenumber{fill:#333;}#mermaid-svg-VB9tNNEBja35fB26 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VB9tNNEBja35fB26 .messageText{fill:#333;stroke:none;}#mermaid-svg-VB9tNNEBja35fB26 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VB9tNNEBja35fB26 .labelText,#mermaid-svg-VB9tNNEBja35fB26 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VB9tNNEBja35fB26 .loopText,#mermaid-svg-VB9tNNEBja35fB26 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VB9tNNEBja35fB26 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VB9tNNEBja35fB26 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VB9tNNEBja35fB26 .noteText,#mermaid-svg-VB9tNNEBja35fB26 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VB9tNNEBja35fB26 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VB9tNNEBja35fB26 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VB9tNNEBja35fB26 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VB9tNNEBja35fB26 .actorPopupMenu{position:absolute;}#mermaid-svg-VB9tNNEBja35fB26 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VB9tNNEBja35fB26 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VB9tNNEBja35fB26 .actor-man circle,#mermaid-svg-VB9tNNEBja35fB26 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VB9tNNEBja35fB26 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SUCCESS → RollbackMahjongPendingEntry PLAYER_NOT_EXISTS → 清理过期 tCurrentRoom alt tCurrentRoom 不存在 tCurrentRoom 存在 Client ExitRoom(dwRoomID) CheckPlayerInRoom nFrozen>0 → RefundFrozenOnAbnormal 兜底 ROOM_NOT_EXISTS CallCenter GameCenter2SceneServer_ExitRoom Room_Exit nError + bShouldRefund S2C_MAHJONG_ExitRoom Client
3.6 异常处理(Crash)
GameServer GameCenter 断开源(Scene/MC/PS) GameServer GameCenter 断开源(Scene/MC/PS) #mermaid-svg-5o2gkIjlXDblFwYf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5o2gkIjlXDblFwYf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5o2gkIjlXDblFwYf .error-icon{fill:#552222;}#mermaid-svg-5o2gkIjlXDblFwYf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5o2gkIjlXDblFwYf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5o2gkIjlXDblFwYf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5o2gkIjlXDblFwYf .marker.cross{stroke:#333333;}#mermaid-svg-5o2gkIjlXDblFwYf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5o2gkIjlXDblFwYf p{margin:0;}#mermaid-svg-5o2gkIjlXDblFwYf .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5o2gkIjlXDblFwYf text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5o2gkIjlXDblFwYf .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5o2gkIjlXDblFwYf .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5o2gkIjlXDblFwYf .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5o2gkIjlXDblFwYf .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5o2gkIjlXDblFwYf #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5o2gkIjlXDblFwYf .sequenceNumber{fill:white;}#mermaid-svg-5o2gkIjlXDblFwYf #sequencenumber{fill:#333;}#mermaid-svg-5o2gkIjlXDblFwYf #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5o2gkIjlXDblFwYf .messageText{fill:#333;stroke:none;}#mermaid-svg-5o2gkIjlXDblFwYf .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5o2gkIjlXDblFwYf .labelText,#mermaid-svg-5o2gkIjlXDblFwYf .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5o2gkIjlXDblFwYf .loopText,#mermaid-svg-5o2gkIjlXDblFwYf .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5o2gkIjlXDblFwYf .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5o2gkIjlXDblFwYf .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5o2gkIjlXDblFwYf .noteText,#mermaid-svg-5o2gkIjlXDblFwYf .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5o2gkIjlXDblFwYf .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5o2gkIjlXDblFwYf .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5o2gkIjlXDblFwYf .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5o2gkIjlXDblFwYf .actorPopupMenu{position:absolute;}#mermaid-svg-5o2gkIjlXDblFwYf .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5o2gkIjlXDblFwYf .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5o2gkIjlXDblFwYf .actor-man circle,#mermaid-svg-5o2gkIjlXDblFwYf line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5o2gkIjlXDblFwYf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 跳过 alt == nExpected ≠ nExpected SceneServer2GameCenter_Crash GameCenter2GameServer_Crash nCrashSettleKey = nLPT + 1 RollbackMahjongPendingEntry RefundFrozenOnAbnormal AchSettlement(当前token)
3.7 PlayServer 不应反向通知玩家断线
PlayServer 采用线性串化(standing serialization)处理事件序列,通过超时被动感知玩家状态。它无法确定当前玩家是在线还是掉线,所以只能采用被动方式。
核心原则:不过度相信网络
分布式系统中,TCP/IP 及服务器间通信受限于:
- 流控重传:丢包重发导致延迟不确定
- 重排:IP 层可能改变包顺序
- PCB 控制块状态:不同服务器上的连接状态不一定同步
- 乱序:A 先发出,B 后发出,B 可能先于 A 到达
这是分布式系统的天然竞态条件。设计上不能假设网络可靠、有序。
原因:正反馈振荡 + 状态二义
#mermaid-svg-cDjGocoEBl1IJyYK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cDjGocoEBl1IJyYK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cDjGocoEBl1IJyYK .error-icon{fill:#552222;}#mermaid-svg-cDjGocoEBl1IJyYK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cDjGocoEBl1IJyYK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cDjGocoEBl1IJyYK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cDjGocoEBl1IJyYK .marker.cross{stroke:#333333;}#mermaid-svg-cDjGocoEBl1IJyYK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cDjGocoEBl1IJyYK p{margin:0;}#mermaid-svg-cDjGocoEBl1IJyYK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cDjGocoEBl1IJyYK .cluster-label text{fill:#333;}#mermaid-svg-cDjGocoEBl1IJyYK .cluster-label span{color:#333;}#mermaid-svg-cDjGocoEBl1IJyYK .cluster-label span p{background-color:transparent;}#mermaid-svg-cDjGocoEBl1IJyYK .label text,#mermaid-svg-cDjGocoEBl1IJyYK span{fill:#333;color:#333;}#mermaid-svg-cDjGocoEBl1IJyYK .node rect,#mermaid-svg-cDjGocoEBl1IJyYK .node circle,#mermaid-svg-cDjGocoEBl1IJyYK .node ellipse,#mermaid-svg-cDjGocoEBl1IJyYK .node polygon,#mermaid-svg-cDjGocoEBl1IJyYK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cDjGocoEBl1IJyYK .rough-node .label text,#mermaid-svg-cDjGocoEBl1IJyYK .node .label text,#mermaid-svg-cDjGocoEBl1IJyYK .image-shape .label,#mermaid-svg-cDjGocoEBl1IJyYK .icon-shape .label{text-anchor:middle;}#mermaid-svg-cDjGocoEBl1IJyYK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cDjGocoEBl1IJyYK .rough-node .label,#mermaid-svg-cDjGocoEBl1IJyYK .node .label,#mermaid-svg-cDjGocoEBl1IJyYK .image-shape .label,#mermaid-svg-cDjGocoEBl1IJyYK .icon-shape .label{text-align:center;}#mermaid-svg-cDjGocoEBl1IJyYK .node.clickable{cursor:pointer;}#mermaid-svg-cDjGocoEBl1IJyYK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cDjGocoEBl1IJyYK .arrowheadPath{fill:#333333;}#mermaid-svg-cDjGocoEBl1IJyYK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cDjGocoEBl1IJyYK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cDjGocoEBl1IJyYK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cDjGocoEBl1IJyYK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cDjGocoEBl1IJyYK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cDjGocoEBl1IJyYK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cDjGocoEBl1IJyYK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cDjGocoEBl1IJyYK .cluster text{fill:#333;}#mermaid-svg-cDjGocoEBl1IJyYK .cluster span{color:#333;}#mermaid-svg-cDjGocoEBl1IJyYK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cDjGocoEBl1IJyYK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cDjGocoEBl1IJyYK rect.text{fill:none;stroke-width:0;}#mermaid-svg-cDjGocoEBl1IJyYK .icon-shape,#mermaid-svg-cDjGocoEBl1IJyYK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cDjGocoEBl1IJyYK .icon-shape p,#mermaid-svg-cDjGocoEBl1IJyYK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cDjGocoEBl1IJyYK .icon-shape .label rect,#mermaid-svg-cDjGocoEBl1IJyYK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cDjGocoEBl1IJyYK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cDjGocoEBl1IJyYK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cDjGocoEBl1IJyYK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 先发出但可能晚到
后发出但可能先到
玩家切图断线
PlayServer 超时
若反向通知 SceneServer '离线'
SceneServer 标记离线
玩家重新连接
SceneServer 通知 '在线'
PlayServer 恢复
状态冲突
无优先级依据
持续振荡
时序矛盾:
- 玩家切图断线 → GS 通知 SceneServer 离线
- PlayServer 超时 → 若反向通知 SceneServer "离线"
- 同一时刻玩家重连成功 → GS 通知 SceneServer "上线"
- 两个通知通过网络传输,由于重排/重传/PCB 差异,谁先到无法确定
- SceneServer 同时持有两个平等状态 → 无法收敛 → 振荡
解决方案一:全局原子流水线
每个操作附带全局单调递增序号,接收方按序号排序,后到的覆盖先到的。需要全局统一时钟源和分布式事务协调器。代价极高,不适用。
解决方案二:接受二义,不做反向通知(本系统采用)
PlayServer 只做:
→ 超时 → AI 托管(本地决策)
→ 有响应 → 取消 AI(本地决策)
→ 不判断玩家在线/离线
玩家状态的唯一权威路径:
→ 断线 → GS → SceneServer 离线
→ 上线 → GS → SceneServer 上线
→ PlayServer 不参与状态判断
闭环系统基本要素:
状态变更只能从一个方向流入:GS → SceneServer
不过度相信网络:每个服务器只对自己域内的状态负责
#mermaid-svg-NcroO5zRSCqGYbVW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NcroO5zRSCqGYbVW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NcroO5zRSCqGYbVW .error-icon{fill:#552222;}#mermaid-svg-NcroO5zRSCqGYbVW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NcroO5zRSCqGYbVW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NcroO5zRSCqGYbVW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NcroO5zRSCqGYbVW .marker.cross{stroke:#333333;}#mermaid-svg-NcroO5zRSCqGYbVW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NcroO5zRSCqGYbVW p{margin:0;}#mermaid-svg-NcroO5zRSCqGYbVW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NcroO5zRSCqGYbVW .cluster-label text{fill:#333;}#mermaid-svg-NcroO5zRSCqGYbVW .cluster-label span{color:#333;}#mermaid-svg-NcroO5zRSCqGYbVW .cluster-label span p{background-color:transparent;}#mermaid-svg-NcroO5zRSCqGYbVW .label text,#mermaid-svg-NcroO5zRSCqGYbVW span{fill:#333;color:#333;}#mermaid-svg-NcroO5zRSCqGYbVW .node rect,#mermaid-svg-NcroO5zRSCqGYbVW .node circle,#mermaid-svg-NcroO5zRSCqGYbVW .node ellipse,#mermaid-svg-NcroO5zRSCqGYbVW .node polygon,#mermaid-svg-NcroO5zRSCqGYbVW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NcroO5zRSCqGYbVW .rough-node .label text,#mermaid-svg-NcroO5zRSCqGYbVW .node .label text,#mermaid-svg-NcroO5zRSCqGYbVW .image-shape .label,#mermaid-svg-NcroO5zRSCqGYbVW .icon-shape .label{text-anchor:middle;}#mermaid-svg-NcroO5zRSCqGYbVW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NcroO5zRSCqGYbVW .rough-node .label,#mermaid-svg-NcroO5zRSCqGYbVW .node .label,#mermaid-svg-NcroO5zRSCqGYbVW .image-shape .label,#mermaid-svg-NcroO5zRSCqGYbVW .icon-shape .label{text-align:center;}#mermaid-svg-NcroO5zRSCqGYbVW .node.clickable{cursor:pointer;}#mermaid-svg-NcroO5zRSCqGYbVW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NcroO5zRSCqGYbVW .arrowheadPath{fill:#333333;}#mermaid-svg-NcroO5zRSCqGYbVW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NcroO5zRSCqGYbVW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NcroO5zRSCqGYbVW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NcroO5zRSCqGYbVW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NcroO5zRSCqGYbVW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NcroO5zRSCqGYbVW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NcroO5zRSCqGYbVW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NcroO5zRSCqGYbVW .cluster text{fill:#333;}#mermaid-svg-NcroO5zRSCqGYbVW .cluster span{color:#333;}#mermaid-svg-NcroO5zRSCqGYbVW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NcroO5zRSCqGYbVW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NcroO5zRSCqGYbVW rect.text{fill:none;stroke-width:0;}#mermaid-svg-NcroO5zRSCqGYbVW .icon-shape,#mermaid-svg-NcroO5zRSCqGYbVW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NcroO5zRSCqGYbVW .icon-shape p,#mermaid-svg-NcroO5zRSCqGYbVW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NcroO5zRSCqGYbVW .icon-shape .label rect,#mermaid-svg-NcroO5zRSCqGYbVW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NcroO5zRSCqGYbVW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NcroO5zRSCqGYbVW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NcroO5zRSCqGYbVW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 错误开环
PlayServer(超时)
SceneServer
GS(判定上线)
振荡
正确闭环
GS 判定断线
SceneServer(状态权威)
GS 判定上线
PlayServer(执行 AI/恢复)
3.8 一致性模型
本系统采用弱一致性(最终一致性),不保证强一致性。
#mermaid-svg-fPy36ig07qQ0YuPz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fPy36ig07qQ0YuPz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fPy36ig07qQ0YuPz .error-icon{fill:#552222;}#mermaid-svg-fPy36ig07qQ0YuPz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fPy36ig07qQ0YuPz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fPy36ig07qQ0YuPz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fPy36ig07qQ0YuPz .marker.cross{stroke:#333333;}#mermaid-svg-fPy36ig07qQ0YuPz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fPy36ig07qQ0YuPz p{margin:0;}#mermaid-svg-fPy36ig07qQ0YuPz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fPy36ig07qQ0YuPz .cluster-label text{fill:#333;}#mermaid-svg-fPy36ig07qQ0YuPz .cluster-label span{color:#333;}#mermaid-svg-fPy36ig07qQ0YuPz .cluster-label span p{background-color:transparent;}#mermaid-svg-fPy36ig07qQ0YuPz .label text,#mermaid-svg-fPy36ig07qQ0YuPz span{fill:#333;color:#333;}#mermaid-svg-fPy36ig07qQ0YuPz .node rect,#mermaid-svg-fPy36ig07qQ0YuPz .node circle,#mermaid-svg-fPy36ig07qQ0YuPz .node ellipse,#mermaid-svg-fPy36ig07qQ0YuPz .node polygon,#mermaid-svg-fPy36ig07qQ0YuPz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fPy36ig07qQ0YuPz .rough-node .label text,#mermaid-svg-fPy36ig07qQ0YuPz .node .label text,#mermaid-svg-fPy36ig07qQ0YuPz .image-shape .label,#mermaid-svg-fPy36ig07qQ0YuPz .icon-shape .label{text-anchor:middle;}#mermaid-svg-fPy36ig07qQ0YuPz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fPy36ig07qQ0YuPz .rough-node .label,#mermaid-svg-fPy36ig07qQ0YuPz .node .label,#mermaid-svg-fPy36ig07qQ0YuPz .image-shape .label,#mermaid-svg-fPy36ig07qQ0YuPz .icon-shape .label{text-align:center;}#mermaid-svg-fPy36ig07qQ0YuPz .node.clickable{cursor:pointer;}#mermaid-svg-fPy36ig07qQ0YuPz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fPy36ig07qQ0YuPz .arrowheadPath{fill:#333333;}#mermaid-svg-fPy36ig07qQ0YuPz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fPy36ig07qQ0YuPz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fPy36ig07qQ0YuPz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fPy36ig07qQ0YuPz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fPy36ig07qQ0YuPz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fPy36ig07qQ0YuPz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fPy36ig07qQ0YuPz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fPy36ig07qQ0YuPz .cluster text{fill:#333;}#mermaid-svg-fPy36ig07qQ0YuPz .cluster span{color:#333;}#mermaid-svg-fPy36ig07qQ0YuPz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fPy36ig07qQ0YuPz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fPy36ig07qQ0YuPz rect.text{fill:none;stroke-width:0;}#mermaid-svg-fPy36ig07qQ0YuPz .icon-shape,#mermaid-svg-fPy36ig07qQ0YuPz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fPy36ig07qQ0YuPz .icon-shape p,#mermaid-svg-fPy36ig07qQ0YuPz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fPy36ig07qQ0YuPz .icon-shape .label rect,#mermaid-svg-fPy36ig07qQ0YuPz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fPy36ig07qQ0YuPz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fPy36ig07qQ0YuPz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fPy36ig07qQ0YuPz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 最终一致性(本系统采用)
操作完成
事后补偿
AutoTakeOut 重试
Settlement_RetryTick
Guardians 持久化
最终一致
CAP 权衡:保证 AP(可用性+分区容忍),不实时保证 C。一致性通过事后补偿拟合(Write-Ahead Log、AutoTakeOut、结算重试)。真实数据 AP+事后补偿,非真实数据 AP only。
3.9 ForwardFailure 流程
GameCenter 转发到 SceneServer 失败时触发 GameCenter2SceneServer_ForwardFailed。
GameServer GameCenter SceneServer GameServer GameCenter SceneServer #mermaid-svg-IoAEgfiVEarwm1zb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IoAEgfiVEarwm1zb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IoAEgfiVEarwm1zb .error-icon{fill:#552222;}#mermaid-svg-IoAEgfiVEarwm1zb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IoAEgfiVEarwm1zb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IoAEgfiVEarwm1zb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IoAEgfiVEarwm1zb .marker.cross{stroke:#333333;}#mermaid-svg-IoAEgfiVEarwm1zb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IoAEgfiVEarwm1zb p{margin:0;}#mermaid-svg-IoAEgfiVEarwm1zb .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IoAEgfiVEarwm1zb text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IoAEgfiVEarwm1zb .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IoAEgfiVEarwm1zb .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IoAEgfiVEarwm1zb .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IoAEgfiVEarwm1zb .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IoAEgfiVEarwm1zb #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IoAEgfiVEarwm1zb .sequenceNumber{fill:white;}#mermaid-svg-IoAEgfiVEarwm1zb #sequencenumber{fill:#333;}#mermaid-svg-IoAEgfiVEarwm1zb #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IoAEgfiVEarwm1zb .messageText{fill:#333;stroke:none;}#mermaid-svg-IoAEgfiVEarwm1zb .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IoAEgfiVEarwm1zb .labelText,#mermaid-svg-IoAEgfiVEarwm1zb .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IoAEgfiVEarwm1zb .loopText,#mermaid-svg-IoAEgfiVEarwm1zb .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IoAEgfiVEarwm1zb .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IoAEgfiVEarwm1zb .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IoAEgfiVEarwm1zb .noteText,#mermaid-svg-IoAEgfiVEarwm1zb .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IoAEgfiVEarwm1zb .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IoAEgfiVEarwm1zb .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IoAEgfiVEarwm1zb .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IoAEgfiVEarwm1zb .actorPopupMenu{position:absolute;}#mermaid-svg-IoAEgfiVEarwm1zb .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IoAEgfiVEarwm1zb .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IoAEgfiVEarwm1zb .actor-man circle,#mermaid-svg-IoAEgfiVEarwm1zb line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IoAEgfiVEarwm1zb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Forward_ToGameServer(dwID, "GameCenter2GameServer_EndGame", ...) 创建待确认结算 alt 转发成功 转发失败(GameServer 不可达) SceneServer2GameCenter_EndGame(dwID, tResultData) 正常送达 AchSettlement GameCenter2SceneServer_AchSettlement Settlement_RemovePending GameCenter2SceneServer_ForwardFailed Settlement_BuildFromForwardFailed Settlement_RetryTick 定时重试
触发 ForwardFailure 的场景:
| 协议 | 说明 |
|---|---|
MAHJONG_PROTO_APPLY_MATCH |
匹配请求转发失败 |
MAHJONG_PROTO_CREATE_ROOM |
创建房间转发失败 |
MAHJONG_PROTO_JOIN_BY_SHARE_CODE |
分享码加入转发失败 |
MAHJONG_PROTO_START_GAME |
开局通知转发失败 |
MAHJONG_PROTO_READY |
准备通知转发失败 |
MAHJONG_PROTO_END_GAME |
结算转发失败(最重要的场景) |
MAHJONG_PROTO_CRASH |
Crash通知转发失败 |
MAHJONG_PROTO_PLAYER_AFK |
暂离通知转发失败 |
MAHJONG_PROTO_PULL_RESULT |
拉取结果转发失败 |
MAHJONG_PROTO_PLAYER_SYNC |
玩家同步转发失败 |
MAHJONG_PROTO_PLAYER_ONLINE_RESULT |
上线结果转发失败 |
ForwardFailure 的最终落地:
#mermaid-svg-fHRuiIP0OnNyHlPs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fHRuiIP0OnNyHlPs .error-icon{fill:#552222;}#mermaid-svg-fHRuiIP0OnNyHlPs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fHRuiIP0OnNyHlPs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fHRuiIP0OnNyHlPs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fHRuiIP0OnNyHlPs .marker.cross{stroke:#333333;}#mermaid-svg-fHRuiIP0OnNyHlPs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fHRuiIP0OnNyHlPs p{margin:0;}#mermaid-svg-fHRuiIP0OnNyHlPs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs .cluster-label text{fill:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs .cluster-label span{color:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs .cluster-label span p{background-color:transparent;}#mermaid-svg-fHRuiIP0OnNyHlPs .label text,#mermaid-svg-fHRuiIP0OnNyHlPs span{fill:#333;color:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs .node rect,#mermaid-svg-fHRuiIP0OnNyHlPs .node circle,#mermaid-svg-fHRuiIP0OnNyHlPs .node ellipse,#mermaid-svg-fHRuiIP0OnNyHlPs .node polygon,#mermaid-svg-fHRuiIP0OnNyHlPs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fHRuiIP0OnNyHlPs .rough-node .label text,#mermaid-svg-fHRuiIP0OnNyHlPs .node .label text,#mermaid-svg-fHRuiIP0OnNyHlPs .image-shape .label,#mermaid-svg-fHRuiIP0OnNyHlPs .icon-shape .label{text-anchor:middle;}#mermaid-svg-fHRuiIP0OnNyHlPs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fHRuiIP0OnNyHlPs .rough-node .label,#mermaid-svg-fHRuiIP0OnNyHlPs .node .label,#mermaid-svg-fHRuiIP0OnNyHlPs .image-shape .label,#mermaid-svg-fHRuiIP0OnNyHlPs .icon-shape .label{text-align:center;}#mermaid-svg-fHRuiIP0OnNyHlPs .node.clickable{cursor:pointer;}#mermaid-svg-fHRuiIP0OnNyHlPs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fHRuiIP0OnNyHlPs .arrowheadPath{fill:#333333;}#mermaid-svg-fHRuiIP0OnNyHlPs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fHRuiIP0OnNyHlPs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fHRuiIP0OnNyHlPs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fHRuiIP0OnNyHlPs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fHRuiIP0OnNyHlPs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fHRuiIP0OnNyHlPs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fHRuiIP0OnNyHlPs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fHRuiIP0OnNyHlPs .cluster text{fill:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs .cluster span{color:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fHRuiIP0OnNyHlPs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fHRuiIP0OnNyHlPs rect.text{fill:none;stroke-width:0;}#mermaid-svg-fHRuiIP0OnNyHlPs .icon-shape,#mermaid-svg-fHRuiIP0OnNyHlPs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fHRuiIP0OnNyHlPs .icon-shape p,#mermaid-svg-fHRuiIP0OnNyHlPs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fHRuiIP0OnNyHlPs .icon-shape .label rect,#mermaid-svg-fHRuiIP0OnNyHlPs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fHRuiIP0OnNyHlPs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fHRuiIP0OnNyHlPs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fHRuiIP0OnNyHlPs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
成功
失败
超上限(3次/局)
超总上限(10次)
ForwardFailure 触发
Settlement_BuildFromForwardFailed
协议是 EndGame
且参数有效?
记录日志,丢弃
Settlement_AddPending
Settlement_RetryTick
每 5 秒重试
AchSettlement 到达
重试计数++
标记 bRetryExhausted
Settlement_RetryPlayer
玩家重连时恢复
永久放弃
CRITICAL 日志
EndGame 是 ForwardFailure 最重要的应用场景:
1. SceneServer 发送 EndGame 到 GameCenter
2. GameCenter 的 Forward_ToGameServer 失败(玩家所在 GS 不可达)
3. GameCenter 调 GameCenter2SceneServer_ForwardFailed
4. SceneServer 执行 Settlement_BuildFromForwardFailed
5. 创建待确认结算(Settlement_AddPending)
6. Settlement_RetryTick 每 5 秒尝试重新发送
7. 玩家重连时 Settlement_RetryPlayer 立即补发
其他协议(匹配/创建/准备等)的 ForwardFailure 不创建待确认结算,因为这些操作不涉及资金。玩家重连后通过 PlayerOnline 重新同步房间状态。
3.10 登录/EnterScene 重排到 AutoTakeOut 之后
#mermaid-svg-7loGr3xOyqG4yy9F{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7loGr3xOyqG4yy9F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7loGr3xOyqG4yy9F .error-icon{fill:#552222;}#mermaid-svg-7loGr3xOyqG4yy9F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7loGr3xOyqG4yy9F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7loGr3xOyqG4yy9F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7loGr3xOyqG4yy9F .marker.cross{stroke:#333333;}#mermaid-svg-7loGr3xOyqG4yy9F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7loGr3xOyqG4yy9F p{margin:0;}#mermaid-svg-7loGr3xOyqG4yy9F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7loGr3xOyqG4yy9F .cluster-label text{fill:#333;}#mermaid-svg-7loGr3xOyqG4yy9F .cluster-label span{color:#333;}#mermaid-svg-7loGr3xOyqG4yy9F .cluster-label span p{background-color:transparent;}#mermaid-svg-7loGr3xOyqG4yy9F .label text,#mermaid-svg-7loGr3xOyqG4yy9F span{fill:#333;color:#333;}#mermaid-svg-7loGr3xOyqG4yy9F .node rect,#mermaid-svg-7loGr3xOyqG4yy9F .node circle,#mermaid-svg-7loGr3xOyqG4yy9F .node ellipse,#mermaid-svg-7loGr3xOyqG4yy9F .node polygon,#mermaid-svg-7loGr3xOyqG4yy9F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7loGr3xOyqG4yy9F .rough-node .label text,#mermaid-svg-7loGr3xOyqG4yy9F .node .label text,#mermaid-svg-7loGr3xOyqG4yy9F .image-shape .label,#mermaid-svg-7loGr3xOyqG4yy9F .icon-shape .label{text-anchor:middle;}#mermaid-svg-7loGr3xOyqG4yy9F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7loGr3xOyqG4yy9F .rough-node .label,#mermaid-svg-7loGr3xOyqG4yy9F .node .label,#mermaid-svg-7loGr3xOyqG4yy9F .image-shape .label,#mermaid-svg-7loGr3xOyqG4yy9F .icon-shape .label{text-align:center;}#mermaid-svg-7loGr3xOyqG4yy9F .node.clickable{cursor:pointer;}#mermaid-svg-7loGr3xOyqG4yy9F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7loGr3xOyqG4yy9F .arrowheadPath{fill:#333333;}#mermaid-svg-7loGr3xOyqG4yy9F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7loGr3xOyqG4yy9F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7loGr3xOyqG4yy9F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7loGr3xOyqG4yy9F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7loGr3xOyqG4yy9F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7loGr3xOyqG4yy9F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7loGr3xOyqG4yy9F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7loGr3xOyqG4yy9F .cluster text{fill:#333;}#mermaid-svg-7loGr3xOyqG4yy9F .cluster span{color:#333;}#mermaid-svg-7loGr3xOyqG4yy9F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7loGr3xOyqG4yy9F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7loGr3xOyqG4yy9F rect.text{fill:none;stroke-width:0;}#mermaid-svg-7loGr3xOyqG4yy9F .icon-shape,#mermaid-svg-7loGr3xOyqG4yy9F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7loGr3xOyqG4yy9F .icon-shape p,#mermaid-svg-7loGr3xOyqG4yy9F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7loGr3xOyqG4yy9F .icon-shape .label rect,#mermaid-svg-7loGr3xOyqG4yy9F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7loGr3xOyqG4yy9F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7loGr3xOyqG4yy9F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7loGr3xOyqG4yy9F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 错误时序:EnterScene 先于 AutoTakeOut
晚于 EnterScene 到达
玩家登录/重连
OnPlayerEnterScene
检查 tCurrentRoom = nil
检查 nFrozen > 0
误判为孤儿冻结
RefundFrozenOnAbnormal(退款)
AutoTakeOut 队列
Mahjong_GuardiansSettle
AwardCurrency(发奖)
资金错误:退款 + 发奖同时发生
账户余额不匹配
问题 :AutoTakeOut 可能包含结算处理。如果 EnterScene 在 AutoTakeOut 之前触发,GameServer 看到玩家 tCurrentRoom = nil 且 nFrozen > 0,会判定为孤儿冻结并执行退款(RollbackMahjongPendingEntry)。随后 AutoTakeOut 到达并执行结算发奖。
结果是:玩家同一笔资金被退款一次、发奖一次,造成双重支付。
正确时序:
#mermaid-svg-Usz54vPFhrJJNH8p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Usz54vPFhrJJNH8p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Usz54vPFhrJJNH8p .error-icon{fill:#552222;}#mermaid-svg-Usz54vPFhrJJNH8p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Usz54vPFhrJJNH8p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Usz54vPFhrJJNH8p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Usz54vPFhrJJNH8p .marker.cross{stroke:#333333;}#mermaid-svg-Usz54vPFhrJJNH8p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Usz54vPFhrJJNH8p p{margin:0;}#mermaid-svg-Usz54vPFhrJJNH8p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Usz54vPFhrJJNH8p .cluster-label text{fill:#333;}#mermaid-svg-Usz54vPFhrJJNH8p .cluster-label span{color:#333;}#mermaid-svg-Usz54vPFhrJJNH8p .cluster-label span p{background-color:transparent;}#mermaid-svg-Usz54vPFhrJJNH8p .label text,#mermaid-svg-Usz54vPFhrJJNH8p span{fill:#333;color:#333;}#mermaid-svg-Usz54vPFhrJJNH8p .node rect,#mermaid-svg-Usz54vPFhrJJNH8p .node circle,#mermaid-svg-Usz54vPFhrJJNH8p .node ellipse,#mermaid-svg-Usz54vPFhrJJNH8p .node polygon,#mermaid-svg-Usz54vPFhrJJNH8p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Usz54vPFhrJJNH8p .rough-node .label text,#mermaid-svg-Usz54vPFhrJJNH8p .node .label text,#mermaid-svg-Usz54vPFhrJJNH8p .image-shape .label,#mermaid-svg-Usz54vPFhrJJNH8p .icon-shape .label{text-anchor:middle;}#mermaid-svg-Usz54vPFhrJJNH8p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Usz54vPFhrJJNH8p .rough-node .label,#mermaid-svg-Usz54vPFhrJJNH8p .node .label,#mermaid-svg-Usz54vPFhrJJNH8p .image-shape .label,#mermaid-svg-Usz54vPFhrJJNH8p .icon-shape .label{text-align:center;}#mermaid-svg-Usz54vPFhrJJNH8p .node.clickable{cursor:pointer;}#mermaid-svg-Usz54vPFhrJJNH8p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Usz54vPFhrJJNH8p .arrowheadPath{fill:#333333;}#mermaid-svg-Usz54vPFhrJJNH8p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Usz54vPFhrJJNH8p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Usz54vPFhrJJNH8p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Usz54vPFhrJJNH8p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Usz54vPFhrJJNH8p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Usz54vPFhrJJNH8p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Usz54vPFhrJJNH8p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Usz54vPFhrJJNH8p .cluster text{fill:#333;}#mermaid-svg-Usz54vPFhrJJNH8p .cluster span{color:#333;}#mermaid-svg-Usz54vPFhrJJNH8p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Usz54vPFhrJJNH8p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Usz54vPFhrJJNH8p rect.text{fill:none;stroke-width:0;}#mermaid-svg-Usz54vPFhrJJNH8p .icon-shape,#mermaid-svg-Usz54vPFhrJJNH8p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Usz54vPFhrJJNH8p .icon-shape p,#mermaid-svg-Usz54vPFhrJJNH8p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Usz54vPFhrJJNH8p .icon-shape .label rect,#mermaid-svg-Usz54vPFhrJJNH8p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Usz54vPFhrJJNH8p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Usz54vPFhrJJNH8p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Usz54vPFhrJJNH8p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 正确时序:AutoTakeOut 先于 EnterScene
是(有正常冻结)
否(已处理完毕)
玩家登录/重连
Guardians 重防线
重放所有待处理的 AutoTakeOut
Mahjong_GuardiansSettle
结算处理完毕
tCurrentRoom 正确恢复或清除
nFrozen 正确扣除
OnPlayerEnterScene
nFrozen > 0 ?
走重连恢复路径
不做任何退款
这就是登录重排:玩家上线时,Guardians 系统必须先重放所有待处理的 AutoTakeOut 事件,然后再执行 EnterScene 逻辑。
重排顺序:
1. Guardians 重放 AutoTakeOut 队列(结算处理)
2. 重放完成后的回调(更新玩家状态)
3. OnPlayerEnterScene(检查房间状态、孤儿冻结)
如果不重排:
EnterScene 在 AutoTakeOut 前执行
→ 看到无房间+有冻结 → 退款
→ AutoTakeOut 到达 → 发奖
→ 双重支付
重排后:
AutoTakeOut 先执行 → 结算处理完毕
→ EnterScene 执行时状态已正确
→ 不会误触发退款
代码位置 :mahjong_game_server.lua 中 _MahjongPlayerOnlineImpl中通过 OnPlayerEnterScene 检查孤儿冻结。该函数必须在 Guardians 重放回调完成后才被触发。
#mermaid-svg-DK7ogadYuuJbRUA9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DK7ogadYuuJbRUA9 .error-icon{fill:#552222;}#mermaid-svg-DK7ogadYuuJbRUA9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DK7ogadYuuJbRUA9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DK7ogadYuuJbRUA9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DK7ogadYuuJbRUA9 .marker.cross{stroke:#333333;}#mermaid-svg-DK7ogadYuuJbRUA9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DK7ogadYuuJbRUA9 p{margin:0;}#mermaid-svg-DK7ogadYuuJbRUA9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 .cluster-label text{fill:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 .cluster-label span{color:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 .cluster-label span p{background-color:transparent;}#mermaid-svg-DK7ogadYuuJbRUA9 .label text,#mermaid-svg-DK7ogadYuuJbRUA9 span{fill:#333;color:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 .node rect,#mermaid-svg-DK7ogadYuuJbRUA9 .node circle,#mermaid-svg-DK7ogadYuuJbRUA9 .node ellipse,#mermaid-svg-DK7ogadYuuJbRUA9 .node polygon,#mermaid-svg-DK7ogadYuuJbRUA9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DK7ogadYuuJbRUA9 .rough-node .label text,#mermaid-svg-DK7ogadYuuJbRUA9 .node .label text,#mermaid-svg-DK7ogadYuuJbRUA9 .image-shape .label,#mermaid-svg-DK7ogadYuuJbRUA9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DK7ogadYuuJbRUA9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DK7ogadYuuJbRUA9 .rough-node .label,#mermaid-svg-DK7ogadYuuJbRUA9 .node .label,#mermaid-svg-DK7ogadYuuJbRUA9 .image-shape .label,#mermaid-svg-DK7ogadYuuJbRUA9 .icon-shape .label{text-align:center;}#mermaid-svg-DK7ogadYuuJbRUA9 .node.clickable{cursor:pointer;}#mermaid-svg-DK7ogadYuuJbRUA9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DK7ogadYuuJbRUA9 .arrowheadPath{fill:#333333;}#mermaid-svg-DK7ogadYuuJbRUA9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DK7ogadYuuJbRUA9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DK7ogadYuuJbRUA9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DK7ogadYuuJbRUA9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DK7ogadYuuJbRUA9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DK7ogadYuuJbRUA9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DK7ogadYuuJbRUA9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DK7ogadYuuJbRUA9 .cluster text{fill:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 .cluster span{color:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DK7ogadYuuJbRUA9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DK7ogadYuuJbRUA9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DK7ogadYuuJbRUA9 .icon-shape,#mermaid-svg-DK7ogadYuuJbRUA9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DK7ogadYuuJbRUA9 .icon-shape p,#mermaid-svg-DK7ogadYuuJbRUA9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DK7ogadYuuJbRUA9 .icon-shape .label rect,#mermaid-svg-DK7ogadYuuJbRUA9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DK7ogadYuuJbRUA9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DK7ogadYuuJbRUA9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DK7ogadYuuJbRUA9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 系统重排约束
必须先于
必须先于
必须先于
Guardians
EnterScene
Room 恢复
Pull
四、玩家登录与状态恢复
4.1 GS 触发的事件(ON_PLAYER_LEAVE_SCENE / ON_PLAYER_LEAVEGS / PLAYER_ENTER_SCENE_COMPLETED / EVT_GUARDIANS_TAKEOUT_DONE)
#mermaid-svg-I13vmR3ihdbbvWCs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I13vmR3ihdbbvWCs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I13vmR3ihdbbvWCs .error-icon{fill:#552222;}#mermaid-svg-I13vmR3ihdbbvWCs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I13vmR3ihdbbvWCs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I13vmR3ihdbbvWCs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I13vmR3ihdbbvWCs .marker.cross{stroke:#333333;}#mermaid-svg-I13vmR3ihdbbvWCs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I13vmR3ihdbbvWCs p{margin:0;}#mermaid-svg-I13vmR3ihdbbvWCs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-I13vmR3ihdbbvWCs .cluster-label text{fill:#333;}#mermaid-svg-I13vmR3ihdbbvWCs .cluster-label span{color:#333;}#mermaid-svg-I13vmR3ihdbbvWCs .cluster-label span p{background-color:transparent;}#mermaid-svg-I13vmR3ihdbbvWCs .label text,#mermaid-svg-I13vmR3ihdbbvWCs span{fill:#333;color:#333;}#mermaid-svg-I13vmR3ihdbbvWCs .node rect,#mermaid-svg-I13vmR3ihdbbvWCs .node circle,#mermaid-svg-I13vmR3ihdbbvWCs .node ellipse,#mermaid-svg-I13vmR3ihdbbvWCs .node polygon,#mermaid-svg-I13vmR3ihdbbvWCs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I13vmR3ihdbbvWCs .rough-node .label text,#mermaid-svg-I13vmR3ihdbbvWCs .node .label text,#mermaid-svg-I13vmR3ihdbbvWCs .image-shape .label,#mermaid-svg-I13vmR3ihdbbvWCs .icon-shape .label{text-anchor:middle;}#mermaid-svg-I13vmR3ihdbbvWCs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-I13vmR3ihdbbvWCs .rough-node .label,#mermaid-svg-I13vmR3ihdbbvWCs .node .label,#mermaid-svg-I13vmR3ihdbbvWCs .image-shape .label,#mermaid-svg-I13vmR3ihdbbvWCs .icon-shape .label{text-align:center;}#mermaid-svg-I13vmR3ihdbbvWCs .node.clickable{cursor:pointer;}#mermaid-svg-I13vmR3ihdbbvWCs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-I13vmR3ihdbbvWCs .arrowheadPath{fill:#333333;}#mermaid-svg-I13vmR3ihdbbvWCs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I13vmR3ihdbbvWCs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I13vmR3ihdbbvWCs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I13vmR3ihdbbvWCs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-I13vmR3ihdbbvWCs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I13vmR3ihdbbvWCs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-I13vmR3ihdbbvWCs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I13vmR3ihdbbvWCs .cluster text{fill:#333;}#mermaid-svg-I13vmR3ihdbbvWCs .cluster span{color:#333;}#mermaid-svg-I13vmR3ihdbbvWCs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I13vmR3ihdbbvWCs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-I13vmR3ihdbbvWCs rect.text{fill:none;stroke-width:0;}#mermaid-svg-I13vmR3ihdbbvWCs .icon-shape,#mermaid-svg-I13vmR3ihdbbvWCs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I13vmR3ihdbbvWCs .icon-shape p,#mermaid-svg-I13vmR3ihdbbvWCs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-I13vmR3ihdbbvWCs .icon-shape .label rect,#mermaid-svg-I13vmR3ihdbbvWCs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I13vmR3ihdbbvWCs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-I13vmR3ihdbbvWCs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-I13vmR3ihdbbvWCs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GameServer 场景状态转移
GameServer 触发上线
GameServer 触发下线
是
否
是
否
是
否
否
是
离线通知
ON_PLAYER_LEAVE_SCENE
玩家离开场景(切图/断线)
ON_PLAYER_LEAVEGS
玩家离开 GS(保险兜底)
tCurrentRoom
且 PLAYING?
saved.nLeaveSceneTime = now
通知 SceneServer 离线
GameServer2GameCenter_PlayerOffline
nFrozen > 0?
RollbackMahjongPendingEntry(孤儿冻结)
跳过
tCurrentRoom
且 PLAYING?
saved.nLogoutTime = now
跳过
PLAYER_ENTER_SCENE_COMPLETED
玩家进入场景(重连/切图完成)
EVT_GUARDIANS_TAKEOUT_DONE
Guardians 重放完成
player.mahjong.bAutoTakeoutDone = true
_MahjongPlayerOnlineImpl
bAutoTakeoutDone?
等待 Guardians
_MahjongPlayerOnlineImpl
SS
ON
LeaveScene 与 Logout 的区别:
| 事件 | 触发条件 | 记录字段 | 宽限期 | 处罚 |
|---|---|---|---|---|
| LEAVE_SCENE | 切图/断线/强退 | saved.nLeaveSceneTime | 有(MAHJONG_DISCONNECT_GRACE_SECONDS) | 宽限期内重连赦免 |
| LEAVEGS | 离开GS/杀进程 | saved.nLogoutTime | 无 | 结算时直接处罚 |
lua
-- mahjong_game_server.lua L1705-1761
-- 受保场次:PLAYING 状态的房间才记录
-- OnPlayerLeaveScene(可靠覆盖切图、断线、杀进程)
if tRoom.nStatus == MAHJONG_STATUS_PLAYING then
saved.nLeaveSceneTime = GetCurrentTime()
end
-- 不管有无房间,都通知 SceneServer
module:CallCenter("GameServer2GameCenter_PlayerOffline", ...)
-- OnPlayerLogout(保险兜底,LEAVE_SCENE 可能漏掉强退/杀进程)
if tRoom and tRoom.nStatus == MAHJONG_STATUS_PLAYING then
saved.nLogoutTime = GetCurrentTime()
end
-- 注意:不在此处立即处罚。真正的处罚在 EndGame 中统一判断。
4.2 _MahjongPlayerOnlineImpl 完整流程
#mermaid-svg-5PokDEzjUXCrnJTU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5PokDEzjUXCrnJTU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5PokDEzjUXCrnJTU .error-icon{fill:#552222;}#mermaid-svg-5PokDEzjUXCrnJTU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5PokDEzjUXCrnJTU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5PokDEzjUXCrnJTU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5PokDEzjUXCrnJTU .marker.cross{stroke:#333333;}#mermaid-svg-5PokDEzjUXCrnJTU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5PokDEzjUXCrnJTU p{margin:0;}#mermaid-svg-5PokDEzjUXCrnJTU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5PokDEzjUXCrnJTU .cluster-label text{fill:#333;}#mermaid-svg-5PokDEzjUXCrnJTU .cluster-label span{color:#333;}#mermaid-svg-5PokDEzjUXCrnJTU .cluster-label span p{background-color:transparent;}#mermaid-svg-5PokDEzjUXCrnJTU .label text,#mermaid-svg-5PokDEzjUXCrnJTU span{fill:#333;color:#333;}#mermaid-svg-5PokDEzjUXCrnJTU .node rect,#mermaid-svg-5PokDEzjUXCrnJTU .node circle,#mermaid-svg-5PokDEzjUXCrnJTU .node ellipse,#mermaid-svg-5PokDEzjUXCrnJTU .node polygon,#mermaid-svg-5PokDEzjUXCrnJTU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5PokDEzjUXCrnJTU .rough-node .label text,#mermaid-svg-5PokDEzjUXCrnJTU .node .label text,#mermaid-svg-5PokDEzjUXCrnJTU .image-shape .label,#mermaid-svg-5PokDEzjUXCrnJTU .icon-shape .label{text-anchor:middle;}#mermaid-svg-5PokDEzjUXCrnJTU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5PokDEzjUXCrnJTU .rough-node .label,#mermaid-svg-5PokDEzjUXCrnJTU .node .label,#mermaid-svg-5PokDEzjUXCrnJTU .image-shape .label,#mermaid-svg-5PokDEzjUXCrnJTU .icon-shape .label{text-align:center;}#mermaid-svg-5PokDEzjUXCrnJTU .node.clickable{cursor:pointer;}#mermaid-svg-5PokDEzjUXCrnJTU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5PokDEzjUXCrnJTU .arrowheadPath{fill:#333333;}#mermaid-svg-5PokDEzjUXCrnJTU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5PokDEzjUXCrnJTU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5PokDEzjUXCrnJTU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5PokDEzjUXCrnJTU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5PokDEzjUXCrnJTU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5PokDEzjUXCrnJTU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5PokDEzjUXCrnJTU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5PokDEzjUXCrnJTU .cluster text{fill:#333;}#mermaid-svg-5PokDEzjUXCrnJTU .cluster span{color:#333;}#mermaid-svg-5PokDEzjUXCrnJTU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5PokDEzjUXCrnJTU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5PokDEzjUXCrnJTU rect.text{fill:none;stroke-width:0;}#mermaid-svg-5PokDEzjUXCrnJTU .icon-shape,#mermaid-svg-5PokDEzjUXCrnJTU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5PokDEzjUXCrnJTU .icon-shape p,#mermaid-svg-5PokDEzjUXCrnJTU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5PokDEzjUXCrnJTU .icon-shape .label rect,#mermaid-svg-5PokDEzjUXCrnJTU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5PokDEzjUXCrnJTU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5PokDEzjUXCrnJTU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5PokDEzjUXCrnJTU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SceneServer侧
是
否
nil
有
无
是
否
是
是
否
PLAYING
WAITING_LOAD
其他
否
是
否
_MahjongPlayerOnlineImpl(player)
读取 SavedData
nLeaveSceneTime 在
宽限期内?
清除 nLeaveSceneTime
(赦免逃跑处罚)
保留 nLeaveSceneTime
GetPlayerCurrentRoom
tCurrentRoom?
通知 GameCenter 玩家在线
CallCenter(GameServer2GameCenter_PlayerOnline)
nFrozen > 0
且 nLastMatchType > 0?
RollbackMahjongPendingEntry(孤儿冻结)
DONE1
GameCenter2SceneServer_PlayerOnline
(SceneServer 侧处理)
Settlement_RetryPlayer
玩家在房间?
恢复 nStatus = ONLINE
nAfk == AFK_OFFLINE?
清除 AFK → AFK_NONE
广播给同房间玩家
通知 PlayServer 玩家在线(→取消AI)
无需清除
根据 nGameStatus
通知 PlayServer 在线
通知 PlayServer 同步终局
更新 GameCenterID
PlayerOnlineResult(hasRoom=true)
PlayerOnlineResult(hasRoom=false)
GameServer 接收
有房间?
恢复牌桌 UI + _RefreshAll
清理 tCurrentRoom
Client_Pull(拉取状态)
DONE
4.3 断线重连处罚逻辑
EndGame 结算时判断是否处罚(mahjong_game_server.lua ~L3945):
| 字段 | 含义 | 处罚条件 |
|---|---|---|
| nLogoutTime | 离开GS时间(LEAVEGS) | 存在 > 0 → 直接处罚(无宽限期) |
| nLeaveSceneTime | 离开场景时间(LEAVE_SCENE) | 存在且超过宽限期 → 处罚 |
处罚触发:Hu 结算后(GuardiansSettle 中),输家(nDelta <= 0)且断线超过宽限期
处罚动作:AccumulateAbnormalExitBan(累计强制退出次数 → 封禁)
4.4 玩家属性与江湖令资金流
属性定义
| 属性ID | 名称 | 容器 | 操作 |
|---|---|---|---|
| 68 | 江湖令(可用余额) | rtPlayerAttr2 | 可用余额,可通过购买/结算增加 |
| 69 | 江湖令(冻结余额) | rtPlayerAttr2 | 入场时从 attr68 转入,退出时转回 attr68 |
lua
-- smallgame_define.lua
SMALLGAME_CURRENCY_ATTR_ID = 68 -- 可用
SMALLGAME_CURRENCY_FROZEN_ID = 69 -- 冻结
RES_TYPE.rtPlayerAttr2 -- 辅助属性容器
资金流完整状态机
#mermaid-svg-pHDGpYCIMY7H6T4H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pHDGpYCIMY7H6T4H .error-icon{fill:#552222;}#mermaid-svg-pHDGpYCIMY7H6T4H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pHDGpYCIMY7H6T4H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pHDGpYCIMY7H6T4H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pHDGpYCIMY7H6T4H .marker.cross{stroke:#333333;}#mermaid-svg-pHDGpYCIMY7H6T4H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pHDGpYCIMY7H6T4H p{margin:0;}#mermaid-svg-pHDGpYCIMY7H6T4H defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-pHDGpYCIMY7H6T4H g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-pHDGpYCIMY7H6T4H g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-pHDGpYCIMY7H6T4H g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-pHDGpYCIMY7H6T4H g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-pHDGpYCIMY7H6T4H g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-pHDGpYCIMY7H6T4H .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-pHDGpYCIMY7H6T4H .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-pHDGpYCIMY7H6T4H .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-pHDGpYCIMY7H6T4H .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-pHDGpYCIMY7H6T4H .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-pHDGpYCIMY7H6T4H .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-pHDGpYCIMY7H6T4H .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-pHDGpYCIMY7H6T4H .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pHDGpYCIMY7H6T4H .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pHDGpYCIMY7H6T4H .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pHDGpYCIMY7H6T4H .edgeLabel .label text{fill:#333;}#mermaid-svg-pHDGpYCIMY7H6T4H .label div .edgeLabel{color:#333;}#mermaid-svg-pHDGpYCIMY7H6T4H .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-pHDGpYCIMY7H6T4H .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-pHDGpYCIMY7H6T4H .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-pHDGpYCIMY7H6T4H .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-pHDGpYCIMY7H6T4H .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-pHDGpYCIMY7H6T4H .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pHDGpYCIMY7H6T4H .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pHDGpYCIMY7H6T4H #statediagram-barbEnd{fill:#333333;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pHDGpYCIMY7H6T4H .cluster-label,#mermaid-svg-pHDGpYCIMY7H6T4H .nodeLabel{color:#131300;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-pHDGpYCIMY7H6T4H .note-edge{stroke-dasharray:5;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-note text{fill:black;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram-note .nodeLabel{color:black;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagram .edgeLabel{color:red;}#mermaid-svg-pHDGpYCIMY7H6T4H #dependencyStart,#mermaid-svg-pHDGpYCIMY7H6T4H #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-pHDGpYCIMY7H6T4H .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pHDGpYCIMY7H6T4H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 初始状态
C2S_Ready\nConsumeRes(attr68, N)
赢家结算(nDelta>0)\nAwardCurrency(nDelta)
输家结算(nDelta<0)\nnInitialFrozen += nDelta
退出退款\nAward(N) + ConsumeRes(attr69, N)
退出退款\nAward(N+delta) + ConsumeRes(attr69, N)
Crash/孤儿退款\nRollbackMahjongPendingEntry
可用 attr68 = X
可用 attr68 = X - N\n冻结 attr69 = N
可用 attr68 += delta\n冻结 attr69 = N(保持不变)
可用 attr68 不变\n冻结镜像下调 N+delta
可用 attr68 += N\n冻结 attr69 = 0
可用 attr68 += N+delta\n冻结 attr69 = 0
入场冻结(FreezeMahjongBalanceForEntry)
lua
-- ~L914 流程:
saved.nPendingFreezeToken = 当前毫秒(单调递增)
FreezeMahjongBalance(player, tContext)
└─ ConsumeRes(attr68, nFullBalance) -- attr68 -= 全额
└─ BuildMahjongCurrentRoom(player, saved, tContext)
└─ nLastProcessedSettleToken = math_max(nLPT, nFreezeToken) -- 对齐种子
成功 → saved.nHandledFreezeToken = saved.nPendingFreezeToken
失败 → saved.nPendingFreezeToken 保留(允许重试)
结算发奖(ApplyMahjongSettlement)
nDelta > 0(赢家):
AwardCurrency(nDelta) → attr68 += nDelta
nInitialFrozen 不变(退出时全额释放)
nDelta < 0(输家):
nNewFrozen = math_max(0, nFrozen + nDelta) -- 冻结镜像下调
attr68 不变(退出时释放较少金额,自然扣损)
nDelta = 0:
无资金操作,仅更新段位分
退出退款(RefundFrozenOnAbnormal)
nAwardAmount = math_min(nFrozen, nInitialFrozen)
AwardCurrency(nAwardAmount) → attr68 += nAwardAmount
CreateAndCostRes(attr69, nFrozen) → attr69 -= nFrozen
赢家示例(初始500万,赢160万):
attr68 += 500万(退款)
attr69 -= 500万(清除冻结)
净结果:可用 +160万 + 取回500万冻结 = +160万(正确)
输家示例(初始500万,输160万):
nInitialFrozen = 340万(已下调)
nAwardAmount = min(500万, 340万) = 340万
attr68 += 340万
attr69 -= 500万
净结果:取回340万冻结 / 实际扣160万(正确)
镜像字段(SavedData)
| 字段 | 类型 | 说明 |
|---|---|---|
nJianghuLingAvailable |
number | 麻将内部记录的可用余额镜像(用于对局日志验算,非权威) |
nJianghuLingFrozen |
number | 已废弃 冻结余额镜像 |
nCurrencyBeforeGame |
number | 入场时可用余额快照(对局日志验算) |
nPendingFreezeToken |
number | 入场冻结 Token(Guardians 重试标记) |
nHandledFreezeToken |
number | 已处理/回滚的冻结 Token |
资金安全边界
AwardCurrency 与 ConsumeRes 是独立系统调用,不在同一事务内。
Award 成功后 ConsumeRes 失败 → attr69 残留 → CRITICAL 日志标记人工核查
Token 不回退(防止双重发奖),下次从钱包重读 nJianghuLingAvailable
五、数据流详解
5.1 客户端三层架构
#mermaid-svg-xaAwOyMRTH8Y5L7r{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xaAwOyMRTH8Y5L7r .error-icon{fill:#552222;}#mermaid-svg-xaAwOyMRTH8Y5L7r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xaAwOyMRTH8Y5L7r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .marker.cross{stroke:#333333;}#mermaid-svg-xaAwOyMRTH8Y5L7r svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xaAwOyMRTH8Y5L7r p{margin:0;}#mermaid-svg-xaAwOyMRTH8Y5L7r .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .cluster-label text{fill:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .cluster-label span{color:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .cluster-label span p{background-color:transparent;}#mermaid-svg-xaAwOyMRTH8Y5L7r .label text,#mermaid-svg-xaAwOyMRTH8Y5L7r span{fill:#333;color:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .node rect,#mermaid-svg-xaAwOyMRTH8Y5L7r .node circle,#mermaid-svg-xaAwOyMRTH8Y5L7r .node ellipse,#mermaid-svg-xaAwOyMRTH8Y5L7r .node polygon,#mermaid-svg-xaAwOyMRTH8Y5L7r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .rough-node .label text,#mermaid-svg-xaAwOyMRTH8Y5L7r .node .label text,#mermaid-svg-xaAwOyMRTH8Y5L7r .image-shape .label,#mermaid-svg-xaAwOyMRTH8Y5L7r .icon-shape .label{text-anchor:middle;}#mermaid-svg-xaAwOyMRTH8Y5L7r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .rough-node .label,#mermaid-svg-xaAwOyMRTH8Y5L7r .node .label,#mermaid-svg-xaAwOyMRTH8Y5L7r .image-shape .label,#mermaid-svg-xaAwOyMRTH8Y5L7r .icon-shape .label{text-align:center;}#mermaid-svg-xaAwOyMRTH8Y5L7r .node.clickable{cursor:pointer;}#mermaid-svg-xaAwOyMRTH8Y5L7r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .arrowheadPath{fill:#333333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xaAwOyMRTH8Y5L7r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xaAwOyMRTH8Y5L7r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xaAwOyMRTH8Y5L7r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xaAwOyMRTH8Y5L7r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .cluster text{fill:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r .cluster span{color:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xaAwOyMRTH8Y5L7r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xaAwOyMRTH8Y5L7r rect.text{fill:none;stroke-width:0;}#mermaid-svg-xaAwOyMRTH8Y5L7r .icon-shape,#mermaid-svg-xaAwOyMRTH8Y5L7r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xaAwOyMRTH8Y5L7r .icon-shape p,#mermaid-svg-xaAwOyMRTH8Y5L7r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xaAwOyMRTH8Y5L7r .icon-shape .label rect,#mermaid-svg-xaAwOyMRTH8Y5L7r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xaAwOyMRTH8Y5L7r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xaAwOyMRTH8Y5L7r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xaAwOyMRTH8Y5L7r :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Control · mahjong_main_panel.lua
Bridge · mahjong_ui_common.lua
Core · mahjong_client_core.lua
S2C
C2S
View · mahjong_ui.lua
纯渲染
g_tData(数据源)
S2C 处理
Request_Send
Event_Publish
SubscribeEvent
FireEvent
Request 包装
_RefreshAll
_CheckPhaseChange
GAME_ACTION 分发
服务器
5.2 数据更新决策
#mermaid-svg-N8kRwv8HA6VzWSNI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N8kRwv8HA6VzWSNI .error-icon{fill:#552222;}#mermaid-svg-N8kRwv8HA6VzWSNI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N8kRwv8HA6VzWSNI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N8kRwv8HA6VzWSNI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N8kRwv8HA6VzWSNI .marker.cross{stroke:#333333;}#mermaid-svg-N8kRwv8HA6VzWSNI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N8kRwv8HA6VzWSNI p{margin:0;}#mermaid-svg-N8kRwv8HA6VzWSNI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI .cluster-label text{fill:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI .cluster-label span{color:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI .cluster-label span p{background-color:transparent;}#mermaid-svg-N8kRwv8HA6VzWSNI .label text,#mermaid-svg-N8kRwv8HA6VzWSNI span{fill:#333;color:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI .node rect,#mermaid-svg-N8kRwv8HA6VzWSNI .node circle,#mermaid-svg-N8kRwv8HA6VzWSNI .node ellipse,#mermaid-svg-N8kRwv8HA6VzWSNI .node polygon,#mermaid-svg-N8kRwv8HA6VzWSNI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N8kRwv8HA6VzWSNI .rough-node .label text,#mermaid-svg-N8kRwv8HA6VzWSNI .node .label text,#mermaid-svg-N8kRwv8HA6VzWSNI .image-shape .label,#mermaid-svg-N8kRwv8HA6VzWSNI .icon-shape .label{text-anchor:middle;}#mermaid-svg-N8kRwv8HA6VzWSNI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N8kRwv8HA6VzWSNI .rough-node .label,#mermaid-svg-N8kRwv8HA6VzWSNI .node .label,#mermaid-svg-N8kRwv8HA6VzWSNI .image-shape .label,#mermaid-svg-N8kRwv8HA6VzWSNI .icon-shape .label{text-align:center;}#mermaid-svg-N8kRwv8HA6VzWSNI .node.clickable{cursor:pointer;}#mermaid-svg-N8kRwv8HA6VzWSNI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N8kRwv8HA6VzWSNI .arrowheadPath{fill:#333333;}#mermaid-svg-N8kRwv8HA6VzWSNI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N8kRwv8HA6VzWSNI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N8kRwv8HA6VzWSNI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N8kRwv8HA6VzWSNI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N8kRwv8HA6VzWSNI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N8kRwv8HA6VzWSNI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N8kRwv8HA6VzWSNI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N8kRwv8HA6VzWSNI .cluster text{fill:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI .cluster span{color:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N8kRwv8HA6VzWSNI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N8kRwv8HA6VzWSNI rect.text{fill:none;stroke-width:0;}#mermaid-svg-N8kRwv8HA6VzWSNI .icon-shape,#mermaid-svg-N8kRwv8HA6VzWSNI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N8kRwv8HA6VzWSNI .icon-shape p,#mermaid-svg-N8kRwv8HA6VzWSNI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N8kRwv8HA6VzWSNI .icon-shape .label rect,#mermaid-svg-N8kRwv8HA6VzWSNI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N8kRwv8HA6VzWSNI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N8kRwv8HA6VzWSNI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N8kRwv8HA6VzWSNI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SyncGameState
GameAction
NotifyAction
ApplyMatch 等
EndGame
S2C 消息到达
消息类型?
Data_ReplaceSnapshot(全量)
直接修改 g_tData
Data_Update(增量)
Data_Update(增量)
Data_Update + 缓存终局数据
保存本地字段
清除服务端字段
从快照重建
恢复本地字段
Event_Publish
六、Core 层数据源:g_tData
g_tData 是单一权威数据源(Single Source of Truth)。所有 UI 渲染只读从此表读取。
6.1 数据结构
lua
g_tData = {
-- 房间信息
dwRoomID = 0, -- 房间ID,0表示不在房间
nMatchType = 0, -- 匹配类型
nGameStatus = 0, -- 游戏状态常量
nDealerSeat = 0, -- 庄家座位
nCurrentSeat = 0, -- 当前操作玩家
nMyAfk = 0, -- 本家暂离状态
nReadyDelay = 0, -- 准备倒计时
nResponseDelay = 0, -- 响应倒计时
nRemainingMs = 0, -- 服务端广播的倒计时间
-- 玩家表(座位索引 1-4)
tPlayers = {
[1] = {
dwID, szName, nSeat, nHandCount,
tDiscardTiles = {}, -- 弃牌数组
tMelds = {}, -- 碰杠数组 { nType, tTiles, nGangType? }
nScore, nRoundScore,
bIsDealer, bIsWinner, bActive, bBankrupt,
nStatus, bReady, nHuOrder,
nQueSuit, -- 定缺花色
huTile, -- 胡牌牌值
bIsHuaZhu, bIsTing,
bIsOwner, -- 房主
nVoiceType, nCardEffectID, nBubbleID, nTableSkinID,
nRankLevel, nRankScore,
tHandTiles = {}, -- 服务端同步的手牌
}
},
-- 私有数据
tPrivate = {
tHandTiles = {}, -- 本家手牌
nGetCard = 0, -- 摸牌牌值
tAnGang = {}, -- 暗杠(保留跨同步)
tBuGang = {}, -- 补杠(保留跨同步)
},
-- 响应状态
nPendingTile = 0, -- 待响应的牌
tPendingResponses = {}, -- 可选响应 { bCanPeng, bCanGang, bCanHu, bCanPass }
-- 游戏状态
tLastDiscard = { nSeat, nTile }, -- 最后出牌
nRemainingCount = 0, -- 牌墙剩余
tDice = {}, -- 骰子 { d1, d2 }
tCurrentFlowItems = {}, -- 实时流水
tDiscardCount = {}, -- 出牌统计 { wan, tiao, tong }
tExchangeSelected = {}, -- 换三张选择
tExchangeReceived = {}, -- 换三张收到
-- EndGame 数据
_tEndGameResult = nil, -- 服务端下发的终局数据
_tSettleHuData = nil, -- 实时结算数据
_bPendingEndGameDisplay = false, -- 待渲染终局标记
_bRoomDataReset = false, -- 房间数据重置标记
}
6.2 数据更新模式
全量快照替换(Data_ReplaceSnapshot):
lua
Data_ReplaceSnapshot = function(tSnapshot)
-- 1. 保存本地字段(LOCAL_ROOT_FIELDS)
-- 2. 清除所有服务端字段(SERVER_SYNC_FIELDS)
-- 3. 从 tSnapshot 重建
-- └─ tPlayers:仅取 SERVER_PLAYER_FIELDS 列出的字段
-- └─ 未提供的字段从旧数据继承
-- └─ tPrivate:tHandTiles 覆盖,保留 tAnGang/tBuGang
-- 4. 恢复本地字段
end
增量合并(Data_Update):
lua
Data_Update = function(tNewData)
-- tPlayers:逐座位合并(新数据覆盖,旧数据保留未覆盖字段)
-- tPrivate:逐子字段合并
-- 其他字段:直接赋值
end
6.3 SERVER_PLAYER_FIELDS(44个字段)
lua
-- 这些是服务端同步的玩家字段,Data_ReplaceSnapshot 只保留这些
{
"dwID", "szName", "nVipLevel", "nLevel", "nJob", "nGender",
"nAvatarID", "nAvatarFrameID", "nSeat", "nHandCount",
"tDiscardTiles", "tMelds", "tFlowerTiles",
"nScore", "nRoundScore", "bIsDealer", "bIsWinner",
"bActive", "bBankrupt", "nStatus", "bReady", "nAfk", "nQueSuit",
"huTile", "tHandTiles", "bIsHuaZhu", "bIsTing",
"bIsOwner",
"nVoiceType", "nCardEffectID", "nBubbleID", "nTableSkinID",
"nRankLevel", "nRankScore",
}
七、事件系统
7.1 为什么不使用 FireEvent
麻将系统有三套事件体系:
| 体系 | 函数 | 范围 | 说明 |
|---|---|---|---|
| 项目全局 | FireEvent / RegisterEvent |
跨模块 | 畅玩阁、任务系统使用 |
| 麻将 Core | Event_Publish / Client_SubscribeEvent |
麻将 Core 内部 | client_core 的 local 函数 |
| 麻将 UI | MahjongUI_FireEvent / MahjongUI_SubscribeEvent |
麻将 UI | ui_common 提供 |
Client 层(mahjong_client_core.lua)不使用 FireEvent 的原因:
FireEvent 是项目全局事件总线,任何模块都可以注册监听。麻将 Core 层发布的事件(如 MAHJONG_EVENT_GAME_ACTION)如果走 FireEvent,其他模块(畅玩阁、任务等)也能收到,但这些事件的数据格式是麻将内部结构的(g_tData.tPlayers、tActionData 等),外部模块无法理解,造成不必要的耦合和潜在的类型错误。
麻将 Core 采用自己的 Event_Publish 体系,事件只在麻将系统内部传播:
#mermaid-svg-ucILrlBP8H8IKvAF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ucILrlBP8H8IKvAF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ucILrlBP8H8IKvAF .error-icon{fill:#552222;}#mermaid-svg-ucILrlBP8H8IKvAF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ucILrlBP8H8IKvAF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ucILrlBP8H8IKvAF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ucILrlBP8H8IKvAF .marker.cross{stroke:#333333;}#mermaid-svg-ucILrlBP8H8IKvAF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ucILrlBP8H8IKvAF p{margin:0;}#mermaid-svg-ucILrlBP8H8IKvAF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ucILrlBP8H8IKvAF .cluster-label text{fill:#333;}#mermaid-svg-ucILrlBP8H8IKvAF .cluster-label span{color:#333;}#mermaid-svg-ucILrlBP8H8IKvAF .cluster-label span p{background-color:transparent;}#mermaid-svg-ucILrlBP8H8IKvAF .label text,#mermaid-svg-ucILrlBP8H8IKvAF span{fill:#333;color:#333;}#mermaid-svg-ucILrlBP8H8IKvAF .node rect,#mermaid-svg-ucILrlBP8H8IKvAF .node circle,#mermaid-svg-ucILrlBP8H8IKvAF .node ellipse,#mermaid-svg-ucILrlBP8H8IKvAF .node polygon,#mermaid-svg-ucILrlBP8H8IKvAF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ucILrlBP8H8IKvAF .rough-node .label text,#mermaid-svg-ucILrlBP8H8IKvAF .node .label text,#mermaid-svg-ucILrlBP8H8IKvAF .image-shape .label,#mermaid-svg-ucILrlBP8H8IKvAF .icon-shape .label{text-anchor:middle;}#mermaid-svg-ucILrlBP8H8IKvAF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ucILrlBP8H8IKvAF .rough-node .label,#mermaid-svg-ucILrlBP8H8IKvAF .node .label,#mermaid-svg-ucILrlBP8H8IKvAF .image-shape .label,#mermaid-svg-ucILrlBP8H8IKvAF .icon-shape .label{text-align:center;}#mermaid-svg-ucILrlBP8H8IKvAF .node.clickable{cursor:pointer;}#mermaid-svg-ucILrlBP8H8IKvAF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ucILrlBP8H8IKvAF .arrowheadPath{fill:#333333;}#mermaid-svg-ucILrlBP8H8IKvAF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ucILrlBP8H8IKvAF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ucILrlBP8H8IKvAF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ucILrlBP8H8IKvAF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ucILrlBP8H8IKvAF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ucILrlBP8H8IKvAF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ucILrlBP8H8IKvAF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ucILrlBP8H8IKvAF .cluster text{fill:#333;}#mermaid-svg-ucILrlBP8H8IKvAF .cluster span{color:#333;}#mermaid-svg-ucILrlBP8H8IKvAF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ucILrlBP8H8IKvAF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ucILrlBP8H8IKvAF rect.text{fill:none;stroke-width:0;}#mermaid-svg-ucILrlBP8H8IKvAF .icon-shape,#mermaid-svg-ucILrlBP8H8IKvAF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ucILrlBP8H8IKvAF .icon-shape p,#mermaid-svg-ucILrlBP8H8IKvAF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ucILrlBP8H8IKvAF .icon-shape .label rect,#mermaid-svg-ucILrlBP8H8IKvAF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ucILrlBP8H8IKvAF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ucILrlBP8H8IKvAF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ucILrlBP8H8IKvAF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MahjongUI_FireEvent 桥接
_RefreshAll / _CheckPhaseChange
Event_Publish 麻将域
MAHJONG_EVENT_GAME_STATE
MAHJONG_EVENT_GAME_ACTION
MAHJONG_EVENT_EXIT_ROOM
MAHJONG_EVENT_SETTLE_HU
FireEvent 全局总线
SMALLGAME_EVENT_GAME_COMPLETE
SMALLGAME_EVENT_HU
Event_Publish
MahjongUI_SubscribeEvent
main_panel.lua
ui.lua
当需要通知外部模块时(如每局结束触发任务进度):
- 麻将 Core 不直接
FireEvent,而是在 S2C 处理函数末尾调用tSmallgameGameServer.FireModuleEvent - 由 SmallGame 框架代为触发全局事件
- 麻将 UI 层通过
MahjongUI_SubscribeEvent内部处理,不污染全局总线
七、UI 系统架构
7.1 主面板控件树
mahjong_main_panel (Window)
├── wnd_tishi(提示区域)
│ ├── wnd_huanpai(换牌提示)
│ └── txt_tishi(文字提示)
├── wnd_opt(操作按钮)
│ ├── btn_peng(碰)
│ ├── btn_gang(杠)
│ ├── btn_hu(胡)
│ └── btn_guo(过)
├── wnd_discard(弃牌区)
│ ├── img_arrow(弃牌箭头,锚到最后一张)
│ ├── wnd_dianpao / mov_dianpao(点炮描点,锚到被胡的牌)
│ └── list_discard1~4(四方弃牌列表)
├── wnd_player1~4(四个玩家区域)
│ ├── wnd_playerinfo(头像/名字/段位/货币)
│ ├── list_mahjong(手牌区)
│ ├── list_get_card(摸牌区)
│ ├── list_view(碰杠展示区)
│ ├── list_final(终局倒牌区)
│ ├── list_hu / card_hu(胡牌显示)
│ ├── wnd_result(番型倍率常驻显示)
│ ├── wnd_optanimate / mov_optanimate(webp方位动画)
│ └── wnd_animate(旧式纹理动画,仅保留 HU badge)
├── wnd_zhuang(中央区域)
│ ├── wnd_state(当前玩家指示器)
│ ├── wnd_view(胡牌类型纹理)
│ └── wnd_result(番型信息)
└── wnd_table(牌桌区域)
└── list_mapai1~4(四方牌墙)
7.2 事件系统
Core 事件发布(Event_Publish in mahjong_client_core.lua):
lua
-- Core 层内部事件(client_core 的 local 函数)
-- 订阅:Client_SubscribeEvent(nEventType, fnCallback)
-- 发布:Event_Publish(nEventType, tData)
-- 27 个数值事件(MAHJONG_EVENT_* 常量)
UI 桥接(MahjongUI_FireEvent in mahjong_ui_common.lua):
lua
-- UI 层订阅
MahjongUI_SubscribeEvent(MAHJONG_EVENT_GAME_STATE, function(tData)
-- UI 回调
end)
-- 内部实现:为每个事件类型创建一个 Core 订阅
-- Core 回调 → 遍历 g_tUIEventSubs[nEventType] → 调用所有 UI 回调
UI 内部事件(MahjongUI_FireEvent):
lua
-- 在 mahjong_ui_common.lua 内部使用
-- 已订阅 Core 事件的回调中调用
-- 也用于 UI 组件间通信
UI 事件订阅表与处理:
| 事件 | 处理函数 | 说明 |
|---|---|---|
| GAME_STATE | → _RefreshAll + _CheckPhaseChange |
全量刷新+阶段转换 |
| NOTIFY_ACTION | → 显示碰/杠/胡/过按钮 | 响应选项 |
| GAME_ACTION | → 出牌/碰/杠/胡/过处理 | 动作分发 |
| TIMER | → 超时自动操作 | 自动出牌/胡牌 |
| END_GAME | → 显示终局牌面 | 战绩渲染 |
| EXIT_ROOM | → 隐藏面板 | 退出清理 |
| PREPARE_GAME | → ClearTable + _RefreshAll |
新局准备 |
| ERROR | → 显示错误提示 | 错误码→字串 |
| SETTLE_HU | → 打开结算面板 | 胡牌即时结算 |
7.3 全量刷新:_RefreshAll
_RefreshAll 是统一的 UI 全重建入口。按 g_tData.nGameStatus 分派:
| nGameStatus | 渲染内容 | 重建范围 |
|---|---|---|
| EXCHANGE | 牌墙、玩家信息、换三张UI | 全部 |
| QUE_SUIT | 手牌、定缺窗口 | 全部 |
| WAITING | 玩家信息、等待界面 | 全部 |
| WAITING_LOAD | 终局牌面(碰杠+弃牌+手牌) | 弃牌重建、碰杠重建、箭头重锚 |
| PLAYING | 手牌、牌墙、碰杠、弃牌、箭头 | 全部重建 |
| WAIT_RESPONSE | 同上 + 响应按钮 | 全部重建 |
WAITING_LOAD 刷新(终局牌面):
lua
if nStatus == WAITING_LOAD then
MahjongUI_ClearAllDiscards() -- 清空弃牌
MahjongUI_ClearAllViews() -- 清空碰杠
self:_RenderEndGameTable(tData, nMySeat) -- 重建终局牌面
└─ 渲染手牌(ShowFinalCard)
└─ 渲染碰杠(AddMeld)
└─ 渲染弃牌(AddDiscard)
└─ 重建箭头(UpdateDiscardArrow)
end
阶段切换检测:
lua
g_nLastRefreshStatus = g_nLastRefreshStatus or 0
if nStatus ~= g_nLastRefreshStatus then
-- 检测到阶段转换
if g_nLastRefreshStatus == EXCHANGE and nStatus == QUE_SUIT then
-- 换三张结束,进入定缺
_RestoreAfterExchange()
elseif g_nLastRefreshStatus == WAITING_LOAD and nStatus ~= WAITING_LOAD then
-- 离开终局状态,新局开始
MahjongUI_ClearTable(nMySeat)
end
end
7.4 UI ↔ Core 约束
硬性约束(不可违反):
Modules 与 UI 之间只允许两种通信方式:
1. 事件方案:modules → FireEvent → UI(RegisterEvent 接收)
2. 回调方案:UI 传 function 引用给 modules,modules 在适当时机调用
禁止:
- modules 调 UI 函数(FindWindow、Show、SetText、GetStringEx 等)
- modules 通过任何间接方式调 UI 函数
八、动画系统
8.1 动画类型
| 动画类型 | 实现方式 | 资源格式 | 控件 | 说明 |
|---|---|---|---|---|
| 动作动画(碰/杠/胡/自摸) | MahjongUI_PlayOptAnimate |
webp | mov_optanimate |
方位动画,在对应玩家座位播放 |
| 特殊番型(天胡/地胡/杠上花等) | MahjongUI_GetActionWebp + PlayOptAnimate |
webp | mov_optanimate |
通过 tFanTypes 自动选择 webp 文件 |
| 点炮描点 | MahjongUI_ShowDianpao |
webp | mov_dianpao |
锚定到弃牌区最后一张牌 |
| 弃牌箭头 | MahjongUI_UpdateDiscardArrow |
--- | img_arrow |
锚定到最后弃牌 |
| 弃牌飞牌 | MahjongUI_PlayDiscardAnimate |
动画控件 | 手牌控件 | 弃牌从手牌飞向弃牌区 |
| 摸牌滑落 | MahjongUI_UpdateGetCard |
动画控件 | list_get_card |
摸牌从顶部滑落 |
| 番型展示 | MahjongUI_ShowResult |
纹理 | wnd_result |
常驻显示,不自动隐藏 |
| 骰子动画 | MahjongUI_ShowDice |
webp | 骰子控件 | 随机转动后定格到服务器值 |
| 开局动画 | MahjongUI_PlayStartFlow |
纹理+webp | wnd_start+骰子 |
对局开始→骰子→定庄 |
8.2 动画资源选择
lua
-- 基础动作:碰/杠/胡/自摸
MahjongUI_GetActionWebp(nil, MAHJONG_ACTION_PENG)
→ MAHJONG_ACTION_WEBP_MAP[PENG] = "W1351_2Dtx_peng"
→ "W1351_2Dtx_peng.webp"
-- 特殊番型:天胡/地胡/杠上花/杠上炮/抢杠胡/海底捞月
MahjongUI_GetActionWebp({ MAHJONG_FAN_TYPE_TIAN_HU }, nil)
→ MAHJONG_HU_WEBP_PRIORITY[TIAN_HU] = "W1351_2Dtx_tianhu"
→ "W1351_2Dtx_tianhu.webp"
-- 自摸特殊番型(自摸 + 特殊胡)
MahjongUI_GetActionWebp({ MAHJONG_FAN_TYPE_GANG_SHANG_HUA }, MAHJONG_ACTION_ZIMO)
→ MAHJONG_ZIMO_WEBP_EXTRA 匹配 → 替代动画
→ ("W1351_2Dtx_zimo.webp", "W1351_2Dtx_gskh.webp")
→ 仅使用第二返回值(特殊胡动画)
8.3 webp 资源映射
lua
-- 方位动画映射表(MAHJONG_ACTION_WEBP_MAP)
MAHJONG_ACTION_PENG = "W1351_2Dtx_peng"
MAHJONG_ACTION_GANG = "W1351_2Dtx_gang"
MAHJONG_ACTION_HU = "W1351_2Dtx_hu"
MAHJONG_ACTION_ZIMO = "W1351_2Dtx_zimo"
-- 特殊番型映射(MAHJONG_HU_WEBP_PRIORITY)
MAHJONG_FAN_TYPE_TIAN_HU = "W1351_2Dtx_tianhu"
MAHJONG_FAN_TYPE_DI_HU = "W1351_2Dtx_dihu"
MAHJONG_FAN_TYPE_GANG_SHANG_HUA = "W1351_2Dtx_gskh"
MAHJONG_FAN_TYPE_GANG_SHANG_PAO = "W1351_2Dtx_gsp"
MAHJONG_FAN_TYPE_QIANG_GANG_HU = "W1351_2Dtx_qgh"
MAHJONG_FAN_TYPE_HAI_DI_LAO_YUE = "W1351_2Dtx_hdly"
所有 webp 文件路径:ui/texture/movie/ + 文件名。文件格式为 .webp,通过 mov 控件播放。
8.4 Strand 定时器管理
lua
-- 每次 ClearTable / 新局开始时 取消所有旧定时器
MahjongUI_BeginRebuild()
└─ 取消所有 g_tMahjongStrandTimers
└─ 释放动画锁:g_bMahjongAnimating = false
└─ 清除推迟标记:g_bMahjongDeferRefresh = false
└─ 清除 g_nLastGetCardTile
└─ 清除气泡定时器
-- 创建定时器后注册到 strand
local timer_id = module_mahjong.mahjong_timer.CreateTimer(delay, 0, callback)
Mahjong_StrandRegisterTimer(timer_id)
-- 动画锁机制
g_bMahjongAnimating: 动画播放中 → _RefreshAll 推迟
g_nMahjongAnimatingCount: 动画计数(支持嵌套动画)
g_bMahjongDeferRefresh: 推迟的 _RefreshAll
g_bMahjongDeferPhaseChange: 推迟的 _CheckPhaseChange
九、畅玩阁系统(SmallGame)
麻将依附于畅玩阁(SmallGame)框架。
9.1 小游戏框架
lua
-- modules/smallgame/config.xml
-- GameServer: smallgame_config_loader + smallgame_define + smallgame_game_server
-- GameClient: smallgame_config_loader + smallgame_define + smallgame_client_data + smallgame_client_driver
-- GameCenter: smallgame_config_loader + smallgame_define
-- SceneServer: smallgame_config_loader + smallgame_define
9.2 游戏模块注册
lua
SmallgameGameServer.RegisterGameModule(nMatchType, tGameModule)
麻将通过 g_RegisteredGameModules[nMatchType] 注册回调接口:
lua
-- 麻将初始化时
tSmallgameGameServer.RegisterGameModule(nMatchType, {
OnSettlement = GameServer_OnSettlement, -- 结算回调
QueryRewardType = GameServer_QueryRewardType, -- 产出日志类型
QueryConsumeType = GameServer_QueryConsumeType, -- 消耗日志类型
OnActions = { ... }, -- 操作定义
})
9.3 货币系统
lua
SMALLGAME_CURRENCY_ATTR_ID = 68 -- 可用江湖令(rtPlayerAttr2)
SMALLGAME_CURRENCY_FROZEN_ID = 69 -- 冻结江湖令(rtPlayerAttr2)
| 操作 | 函数 | 说明 |
|---|---|---|
| 冻结 | ConsumeRes(attr68) → attr69 |
入场冻结 |
| 发放 | AwardCurrency(player, amount, reason, logType, bSkipDailyCap) |
结算赢奖 |
| 消耗 | CreateAndCostRes |
扣除冻结 |
| 读可用 | GetCurrencyBalance(player) |
获取 attr68 |
| 读冻结 | GetFrozenCurrencyBalance(player) |
获取 attr69 |
9.4 畅玩阁日志
lua
SmallgameGameServer.LogFunGame(player, szStepNumid, nResults, tData)
| 步骤号 | results | 触发时机 |
|---|---|---|
| C4101(进入) | 0 | SuccessMatch --- PLAYING 状态 |
| C4102(结束) | 1-7 | EndGame --- 对局完整结算 |
| C4103(退出) | 3 | ExitRoom --- PLAYING 中途离开 |
| C4102(崩溃) | 4 | Crash --- 补偿 |
| C4102(胡牌) | 0 | GuardiansSettle --- 胡牌即时结算 |
十、基础设施模块
10.1 配置加载(mahjong_config_loader.lua,28行)
纯委托到 smallgame_config_loader:
lua
mahjong_config_loader.OpenTable(filename)
mahjong_config_loader.GetIndexedTable(...)
mahjong_config_loader.GetGlobalTable(...)
mahjong_config_loader.GetRuleTable(...)
mahjong_config_loader.GetParamTable(...)
mahjong_config_loader.GetGroupIDByMatchType(nMatchType)
配置表:
Mahjong_Global.tab--- 全局配置Mahjong_Rule.tab--- 规则表(双键:matchType + ruleName)Mahjong_Rank.tab--- 段位配置Mahjong_Param.tab--- 参数配置
lua
-- 读取规则
GetRule(nMatchType, MAHJONG_RULE_ROUND_BASE_SCORE, 1)
-- 读取参数
GetMahjongParam(ID, DEFAULT)
10.2 定时器系统(mahjong_timer.lua,338行)
基于 mahjong_map(跳表)实现的通用一次性定时器。
| 函数 | 说明 |
|---|---|
CreateTimer(nDelayMs, dwRoomID, fnCallback) |
创建定时器,返回ID |
CloseTimer(nTimerID) |
按ID取消 |
RemoveTimersByRoom(dwRoomID) |
按房间批量取消 |
DoEvents(nNow) |
每帧执行到期定时器 |
GetRemainingTime(nTimerID, nNow) |
获取剩余时间 |
定时器数据结构 (热更新安全,g_ 前缀):
lua
g_tTimerMap = { [nTimerID] = { nTimerID, nExpire, dwRoomID, fnCallback } }
g_tTimerMapByExpire = mahjong_map.new(TimeComparator_Asc) -- 跳表按到期时间排序
g_tRoomTimerMap = { [dwRoomID] = { [nTimerID] = true } } -- 房间索引
10.3 跳表(mahjong_map.lua,272行)
有序映射(Skip List)。
| 方法 | 复杂度 | 说明 |
|---|---|---|
new(cmp) |
O(1) | 创建实例,支持自定义比较器 |
set(k, v) |
O(log n) | 插入/更新 |
get(k) |
O(log n) | 查找 |
delete(k) |
O(log n) | 删除 |
pairs() |
O(n) | 有序迭代 |
10.4 雪花ID(mahjong_snowflake.lua,234行)
仅 SceneServer 加载,用于生成唯一房间ID。
ID 布局(默认53位内):
| Timestamp (28 bits) | ServerID (7 bits) | TypeID (6 bits) | Sequence (12 bits) |
关键特性:
- 53位内兼容 Lua double-number 精度
- 时钟回拨:重用上次时间戳+递增序列号
- 序列耗尽:借用下一秒
- 崩溃恢复:持久化
(server_id, type_id) → last_sec,重启后推进1秒避免碰撞
10.5 调试转储(mahjong_dump.lua,330行)
lua
mahjong_dump.dump(value, name, indent) -- 多行格式化输出
mahjong_dump.dump_null(value) -- 安全处理 nil
mahjong_dump.to_string(value) -- 单行紧凑序列化
mahjong_dump.capture_stack_trace() -- 堆栈跟踪
十一、常量与定义
核心常量定义在 mahjong_define.lua:
11.1 错误码
| 范围 | 用途 |
|---|---|
MAHJONG_ERROR_SUCCESS (1) |
成功 |
3300001-3300010 |
房间/玩家/匹配错误 |
3300011-3300012 |
经济错误(破产/不足) |
3300015-3300017 |
服务器连接错误 |
3300018 |
封禁 |
3300019-3300020 |
准入错误(等级/货币区间) |
3300021-3300027 |
扩展错误 |
3300101+ |
客户端错误(超时等) |
11.2 游戏常量
| 类别 | 范围 |
|---|---|
| 牌值编码 | 万1-9, 条11-19, 筒21-29, 风31-34, 箭35-37, 花41-48 |
| 房间状态 | MATCHING(1), WAITING(2), PLAYING(3) |
| 游戏状态 | WAITING(1) → READY(2) → DEALING(3) → PLAYING(4) → WAIT_RESPONSE(5) → END(6) → ... |
| 番型 | 16种基础番型(1-16) + 6种叠加番型(21-26) + 2种特殊番型(31-32) |
| 协议 | 1001-1029 |
11.3 日志类型
消耗/产出基数为 23045+/13045+,公式:
lua
-- 产出(赢奖)
自摸 = Res13045 + (fanType - 1)
点炮 = Res13061 + (fanType - 1)
天胡 = Res13077(固定)
地胡 = Res13078(固定)
-- 消耗(输分)
被自摸 = Cost23045 + (fanType - 1)
被点炮 = Cost23061 + (fanType - 1)
-- 冻结/退还
入场冻结 = Cost23079
退出退款 = Res13082
stale清理 = Res13085
十二、结算系统
完整结算系统文档见 麻将结算系统.md。
12.1 核心公式
nSettleKey = nSettleSeq + nCurrentRound
nExpected = nLastProcessedSettleToken + 1
nSettleKey == nExpected → 执行
nSettleKey < nExpected → SKIP
nSettleKey > nExpected → RETRY
12.2 幂等机制
| 路径 | 幂等方法 |
|---|---|
| 结算 | Token 比较(三路分支) |
| Crash | 与结算共享流水线 |
| 退款 | 状态守卫(tCurrentRoom + nFrozen + RoomID + FreezeToken) |
| 退出 | HasMahjongPendingEntry 检查 |
12.3 三通道并行结算
| 通道 | 触发 | 执行函数 | Key 公式 |
|---|---|---|---|
| Hu AutoTakeOut(一次) | 胡牌事件 | ApplyMahjongSettlement |
nSettleSeq + nRound |
| EndGame 直发(主) | Game_End | ApplyMahjongSettlement |
nSettleSeq + nRound |
| AutoTakeOut 兜底(bExtraSettle) | 冷路径重试 | Mahjong_ExtraSettlement |
nSettleSeq + nRound(同 Key,不推进 Token) |
注:bExtraSettle 使用与 Hu/EndGame 相同的 Key,但不推进 nLastProcessedSettleToken。原因是玩家在胡牌结算后可能已退出房间并进入其他游戏(畅玩阁内其他玩法),此时玩家的 nLastProcessedSettleToken 被新游戏占用作结算流水线。如果 bExtraSettle 推进 Token,新游戏的结算会因流水线序号不匹配而失败。因此 bExtraSettle 只能走 Mahjong_ExtraSettlement 特殊通道直接 Award,不参与 Token 流水线。幂等性由 AutoTakeOut 本身保证。
十三、振荡控制
13.1 正反馈与负反馈
正反馈:系统的输出加剧系统的输入,导致状态发散、无法收敛。
错误 → 继续尝试 → 继续错误 → 无限循环
在分布式系统中,正反馈表现为:A 失败 → A 重试 → B 也失败 → B 也重试 → A 再失败 → 互相放大。
负反馈:系统的输出抑制系统的输入,导致状态收敛到稳定点。
错误 → 限速 → 降低频率 → 错误减少 → 稳定
在分布式系统中,负反馈表现为:重试计数 → 达到上限 → 停止重试 → 等待恢复 → 重连时恢复。
麻将系统的所有防护机制都是负反馈设计。
13.2 振荡来源
麻将系统中存在三类正反馈振荡:
类型 A:结算重试振荡
#mermaid-svg-ZSd65J9xJsahzyEY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZSd65J9xJsahzyEY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZSd65J9xJsahzyEY .error-icon{fill:#552222;}#mermaid-svg-ZSd65J9xJsahzyEY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZSd65J9xJsahzyEY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZSd65J9xJsahzyEY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZSd65J9xJsahzyEY .marker.cross{stroke:#333333;}#mermaid-svg-ZSd65J9xJsahzyEY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZSd65J9xJsahzyEY p{margin:0;}#mermaid-svg-ZSd65J9xJsahzyEY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZSd65J9xJsahzyEY .cluster-label text{fill:#333;}#mermaid-svg-ZSd65J9xJsahzyEY .cluster-label span{color:#333;}#mermaid-svg-ZSd65J9xJsahzyEY .cluster-label span p{background-color:transparent;}#mermaid-svg-ZSd65J9xJsahzyEY .label text,#mermaid-svg-ZSd65J9xJsahzyEY span{fill:#333;color:#333;}#mermaid-svg-ZSd65J9xJsahzyEY .node rect,#mermaid-svg-ZSd65J9xJsahzyEY .node circle,#mermaid-svg-ZSd65J9xJsahzyEY .node ellipse,#mermaid-svg-ZSd65J9xJsahzyEY .node polygon,#mermaid-svg-ZSd65J9xJsahzyEY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZSd65J9xJsahzyEY .rough-node .label text,#mermaid-svg-ZSd65J9xJsahzyEY .node .label text,#mermaid-svg-ZSd65J9xJsahzyEY .image-shape .label,#mermaid-svg-ZSd65J9xJsahzyEY .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZSd65J9xJsahzyEY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZSd65J9xJsahzyEY .rough-node .label,#mermaid-svg-ZSd65J9xJsahzyEY .node .label,#mermaid-svg-ZSd65J9xJsahzyEY .image-shape .label,#mermaid-svg-ZSd65J9xJsahzyEY .icon-shape .label{text-align:center;}#mermaid-svg-ZSd65J9xJsahzyEY .node.clickable{cursor:pointer;}#mermaid-svg-ZSd65J9xJsahzyEY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZSd65J9xJsahzyEY .arrowheadPath{fill:#333333;}#mermaid-svg-ZSd65J9xJsahzyEY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZSd65J9xJsahzyEY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZSd65J9xJsahzyEY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZSd65J9xJsahzyEY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZSd65J9xJsahzyEY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZSd65J9xJsahzyEY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZSd65J9xJsahzyEY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZSd65J9xJsahzyEY .cluster text{fill:#333;}#mermaid-svg-ZSd65J9xJsahzyEY .cluster span{color:#333;}#mermaid-svg-ZSd65J9xJsahzyEY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZSd65J9xJsahzyEY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZSd65J9xJsahzyEY rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZSd65J9xJsahzyEY .icon-shape,#mermaid-svg-ZSd65J9xJsahzyEY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZSd65J9xJsahzyEY .icon-shape p,#mermaid-svg-ZSd65J9xJsahzyEY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZSd65J9xJsahzyEY .icon-shape .label rect,#mermaid-svg-ZSd65J9xJsahzyEY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZSd65J9xJsahzyEY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZSd65J9xJsahzyEY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZSd65J9xJsahzyEY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 重发 EndGame
返回 failure(GS 不可达)
nRetryCount++ nNextRetryTime+=5s
AchSettlement(可能延迟)
Ach 在间歇到达
下个 RetryTick 发现无 pending
SceneServer RetryTick
GameServer
Settlement_RemovePending
Pending 已清除
终止
震荡条件:GS 短暂不可达时 RetryTick 每5秒重发。GS 恢复后可能同时收到多个重复 EndGame。
类型 B:AutoTakeOut 振荡
#mermaid-svg-FJ69YdC9GZEPsu98{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FJ69YdC9GZEPsu98 .error-icon{fill:#552222;}#mermaid-svg-FJ69YdC9GZEPsu98 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FJ69YdC9GZEPsu98 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FJ69YdC9GZEPsu98 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FJ69YdC9GZEPsu98 .marker.cross{stroke:#333333;}#mermaid-svg-FJ69YdC9GZEPsu98 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FJ69YdC9GZEPsu98 p{margin:0;}#mermaid-svg-FJ69YdC9GZEPsu98 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 .cluster-label text{fill:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 .cluster-label span{color:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 .cluster-label span p{background-color:transparent;}#mermaid-svg-FJ69YdC9GZEPsu98 .label text,#mermaid-svg-FJ69YdC9GZEPsu98 span{fill:#333;color:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 .node rect,#mermaid-svg-FJ69YdC9GZEPsu98 .node circle,#mermaid-svg-FJ69YdC9GZEPsu98 .node ellipse,#mermaid-svg-FJ69YdC9GZEPsu98 .node polygon,#mermaid-svg-FJ69YdC9GZEPsu98 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FJ69YdC9GZEPsu98 .rough-node .label text,#mermaid-svg-FJ69YdC9GZEPsu98 .node .label text,#mermaid-svg-FJ69YdC9GZEPsu98 .image-shape .label,#mermaid-svg-FJ69YdC9GZEPsu98 .icon-shape .label{text-anchor:middle;}#mermaid-svg-FJ69YdC9GZEPsu98 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FJ69YdC9GZEPsu98 .rough-node .label,#mermaid-svg-FJ69YdC9GZEPsu98 .node .label,#mermaid-svg-FJ69YdC9GZEPsu98 .image-shape .label,#mermaid-svg-FJ69YdC9GZEPsu98 .icon-shape .label{text-align:center;}#mermaid-svg-FJ69YdC9GZEPsu98 .node.clickable{cursor:pointer;}#mermaid-svg-FJ69YdC9GZEPsu98 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FJ69YdC9GZEPsu98 .arrowheadPath{fill:#333333;}#mermaid-svg-FJ69YdC9GZEPsu98 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FJ69YdC9GZEPsu98 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FJ69YdC9GZEPsu98 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FJ69YdC9GZEPsu98 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FJ69YdC9GZEPsu98 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FJ69YdC9GZEPsu98 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FJ69YdC9GZEPsu98 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FJ69YdC9GZEPsu98 .cluster text{fill:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 .cluster span{color:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FJ69YdC9GZEPsu98 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FJ69YdC9GZEPsu98 rect.text{fill:none;stroke-width:0;}#mermaid-svg-FJ69YdC9GZEPsu98 .icon-shape,#mermaid-svg-FJ69YdC9GZEPsu98 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FJ69YdC9GZEPsu98 .icon-shape p,#mermaid-svg-FJ69YdC9GZEPsu98 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FJ69YdC9GZEPsu98 .icon-shape .label rect,#mermaid-svg-FJ69YdC9GZEPsu98 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FJ69YdC9GZEPsu98 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FJ69YdC9GZEPsu98 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FJ69YdC9GZEPsu98 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 玩家离线期间
玩家重连触发
GameServer 处理
EnterScene 先于 AutoTakeOut 到达
资金二义
AutoTakeOut 未处理
Guardians 保留队列
重放 AutoTakeOut
正常完成
误判孤儿冻结 → 错误退款
资金安全风险
AutoTakeOut 不是在服务器之间指数退避重试的。它由 Guardians 系统持久化保留,在玩家重新登录/连接时一次性重放到 GameServer。重放时机必须在 EnterScene 之前,否则会导致资金错误。
类型 C:ForwardFailed 重入
#mermaid-svg-PwoGOsp6qomBrbiq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PwoGOsp6qomBrbiq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PwoGOsp6qomBrbiq .error-icon{fill:#552222;}#mermaid-svg-PwoGOsp6qomBrbiq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PwoGOsp6qomBrbiq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PwoGOsp6qomBrbiq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PwoGOsp6qomBrbiq .marker.cross{stroke:#333333;}#mermaid-svg-PwoGOsp6qomBrbiq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PwoGOsp6qomBrbiq p{margin:0;}#mermaid-svg-PwoGOsp6qomBrbiq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PwoGOsp6qomBrbiq .cluster-label text{fill:#333;}#mermaid-svg-PwoGOsp6qomBrbiq .cluster-label span{color:#333;}#mermaid-svg-PwoGOsp6qomBrbiq .cluster-label span p{background-color:transparent;}#mermaid-svg-PwoGOsp6qomBrbiq .label text,#mermaid-svg-PwoGOsp6qomBrbiq span{fill:#333;color:#333;}#mermaid-svg-PwoGOsp6qomBrbiq .node rect,#mermaid-svg-PwoGOsp6qomBrbiq .node circle,#mermaid-svg-PwoGOsp6qomBrbiq .node ellipse,#mermaid-svg-PwoGOsp6qomBrbiq .node polygon,#mermaid-svg-PwoGOsp6qomBrbiq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PwoGOsp6qomBrbiq .rough-node .label text,#mermaid-svg-PwoGOsp6qomBrbiq .node .label text,#mermaid-svg-PwoGOsp6qomBrbiq .image-shape .label,#mermaid-svg-PwoGOsp6qomBrbiq .icon-shape .label{text-anchor:middle;}#mermaid-svg-PwoGOsp6qomBrbiq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PwoGOsp6qomBrbiq .rough-node .label,#mermaid-svg-PwoGOsp6qomBrbiq .node .label,#mermaid-svg-PwoGOsp6qomBrbiq .image-shape .label,#mermaid-svg-PwoGOsp6qomBrbiq .icon-shape .label{text-align:center;}#mermaid-svg-PwoGOsp6qomBrbiq .node.clickable{cursor:pointer;}#mermaid-svg-PwoGOsp6qomBrbiq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PwoGOsp6qomBrbiq .arrowheadPath{fill:#333333;}#mermaid-svg-PwoGOsp6qomBrbiq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PwoGOsp6qomBrbiq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PwoGOsp6qomBrbiq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PwoGOsp6qomBrbiq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PwoGOsp6qomBrbiq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PwoGOsp6qomBrbiq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PwoGOsp6qomBrbiq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PwoGOsp6qomBrbiq .cluster text{fill:#333;}#mermaid-svg-PwoGOsp6qomBrbiq .cluster span{color:#333;}#mermaid-svg-PwoGOsp6qomBrbiq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PwoGOsp6qomBrbiq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PwoGOsp6qomBrbiq rect.text{fill:none;stroke-width:0;}#mermaid-svg-PwoGOsp6qomBrbiq .icon-shape,#mermaid-svg-PwoGOsp6qomBrbiq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PwoGOsp6qomBrbiq .icon-shape p,#mermaid-svg-PwoGOsp6qomBrbiq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PwoGOsp6qomBrbiq .icon-shape .label rect,#mermaid-svg-PwoGOsp6qomBrbiq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PwoGOsp6qomBrbiq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PwoGOsp6qomBrbiq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PwoGOsp6qomBrbiq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ForwardFailed 回调
Settlement_BuildFromForwardFailed
RetryTick
Ach 已同时到达
Pending 还在
Token < Expected
不发 Ach
继续 RetryTick
GameCenter 转发失败
SceneServer
Settlement_AddPending
重发 EndGame
Pending 已清除
重复 EndGame
GameServer SKIP
13.2 三层防护
#mermaid-svg-Rix8WK0TisTJdf0J{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Rix8WK0TisTJdf0J .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Rix8WK0TisTJdf0J .error-icon{fill:#552222;}#mermaid-svg-Rix8WK0TisTJdf0J .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Rix8WK0TisTJdf0J .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Rix8WK0TisTJdf0J .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Rix8WK0TisTJdf0J .marker.cross{stroke:#333333;}#mermaid-svg-Rix8WK0TisTJdf0J svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Rix8WK0TisTJdf0J p{margin:0;}#mermaid-svg-Rix8WK0TisTJdf0J .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Rix8WK0TisTJdf0J .cluster-label text{fill:#333;}#mermaid-svg-Rix8WK0TisTJdf0J .cluster-label span{color:#333;}#mermaid-svg-Rix8WK0TisTJdf0J .cluster-label span p{background-color:transparent;}#mermaid-svg-Rix8WK0TisTJdf0J .label text,#mermaid-svg-Rix8WK0TisTJdf0J span{fill:#333;color:#333;}#mermaid-svg-Rix8WK0TisTJdf0J .node rect,#mermaid-svg-Rix8WK0TisTJdf0J .node circle,#mermaid-svg-Rix8WK0TisTJdf0J .node ellipse,#mermaid-svg-Rix8WK0TisTJdf0J .node polygon,#mermaid-svg-Rix8WK0TisTJdf0J .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Rix8WK0TisTJdf0J .rough-node .label text,#mermaid-svg-Rix8WK0TisTJdf0J .node .label text,#mermaid-svg-Rix8WK0TisTJdf0J .image-shape .label,#mermaid-svg-Rix8WK0TisTJdf0J .icon-shape .label{text-anchor:middle;}#mermaid-svg-Rix8WK0TisTJdf0J .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Rix8WK0TisTJdf0J .rough-node .label,#mermaid-svg-Rix8WK0TisTJdf0J .node .label,#mermaid-svg-Rix8WK0TisTJdf0J .image-shape .label,#mermaid-svg-Rix8WK0TisTJdf0J .icon-shape .label{text-align:center;}#mermaid-svg-Rix8WK0TisTJdf0J .node.clickable{cursor:pointer;}#mermaid-svg-Rix8WK0TisTJdf0J .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Rix8WK0TisTJdf0J .arrowheadPath{fill:#333333;}#mermaid-svg-Rix8WK0TisTJdf0J .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Rix8WK0TisTJdf0J .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Rix8WK0TisTJdf0J .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Rix8WK0TisTJdf0J .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Rix8WK0TisTJdf0J .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Rix8WK0TisTJdf0J .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Rix8WK0TisTJdf0J .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Rix8WK0TisTJdf0J .cluster text{fill:#333;}#mermaid-svg-Rix8WK0TisTJdf0J .cluster span{color:#333;}#mermaid-svg-Rix8WK0TisTJdf0J div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Rix8WK0TisTJdf0J .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Rix8WK0TisTJdf0J rect.text{fill:none;stroke-width:0;}#mermaid-svg-Rix8WK0TisTJdf0J .icon-shape,#mermaid-svg-Rix8WK0TisTJdf0J .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Rix8WK0TisTJdf0J .icon-shape p,#mermaid-svg-Rix8WK0TisTJdf0J .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Rix8WK0TisTJdf0J .icon-shape .label rect,#mermaid-svg-Rix8WK0TisTJdf0J .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Rix8WK0TisTJdf0J .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Rix8WK0TisTJdf0J .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Rix8WK0TisTJdf0J :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Layer 3:降级恢复
Layer 2:熔断
Layer 1:限频
Settlement_RetryTick
128 pending/帧
AutoTakeOut 玩家重连触发
per-session nRetryCount 上限 5(≈25s,覆盖最坏10s掉线)
cross-session nTotalRetryCount 上限 20(≈1min,极端重连)
nGameCenterID<=0 直接放弃
bRetryExhausted
Settlement_RetryPlayer 重连恢复
Guardians 持久化不丢失
| 防护 | 机制 | 上限 | 超限行为 |
|---|---|---|---|
| 结算重试频率 | per-session nRetryCount | 4次/局(≈20秒) | 标记 bRetryExhausted,暂停 |
| 全局重试总数 | cross-session nTotalRetryCount | 12次/总(≈1分钟) | 永久放弃 + CRITICAL |
| 放弃说明 | --- | 4次×5秒=20秒覆盖单会话掉线;12次×5秒=60秒≈1分钟覆盖极端重连。 | 最坏情况下玩家断线约10秒可恢复重连,4次×5秒=20秒足够覆盖正常重连窗口。连续12次失败(1分钟)仍不成功→玩家已彻底断线或服务器致命崩溃,继续重试无意义 |
| AutoTakeOut | 玩家重连时触发放置 | --- | --- |
| 无效目标 | nGameCenterID ≤ 0 | 1次 | 直接放弃 pending |
| ForwardFailed 竞态 | 已有 pending 跳过 | --- | 跳过 |
| 单帧风暴 | MAHJONG_SETTLEMENT_RETRY_MAX_PER_TICK | 128/帧 | 推迟到下一帧 |
恢复:Settlement_RetryPlayer(玩家重连时恢复),Guardians 持久化不丢失。
十四、日志规范
| 级别 | 函数 | 说明 |
|---|---|---|
| 调试 | MJLOGT / LogRichDebug |
if MAHJONG_DEBUG then 控制 |
| 警告 | MJLOGW / LogRichWarning |
产品模式开启 |
| 错误 | MJLOGE / LogRichErr |
产品模式开启 |
日志位置:
- 客户端:
game*.log - 服务端:
../Server/logs/{类型}/{日期}/{类型}_{日期}.log
对局日志(latMahjongRound,28字段):
- settle_type: 1=胡牌即时, 2=对局结束
- 包含手牌/碰杠/番型/段位等
畅玩阁日志(C4101-C4103):
- 对局生命周期事件(进入/结束/退出/崩溃/胡牌)
十五、已知边界与限制
| 边界 | 说明 |
|---|---|
| Award/ConsumeRes 跨系统事务 | AwardCurrency 成功后 ConsumeRes 失败导致 attr69 残留。两者是独立系统调用,不在同一事务内。代码做了3次重试 + CRITICAL 日志标记人工核查 |
| 少量发奖 | AwardCurrency 成功后 SetMahjongSavedData 持久化失败导致账号余额快照未更新。Token 不回退,下次从钱包重读恢复 |
| 结算重试 | SceneServer 的结算重试:per-session 4次+跨 session 12次(≈1分钟),超限后永久放弃。 |
| 牌桌重绘 | 全量刷新时所有子控件销毁重建,持久化锚点(箭头、描点)需要重新锚定 |
| 退出响应 | C2S 直回的错误响应(无 tResponse 包装)通过 tPacket 根层读取 nError |