麻将系统架构

麻将系统架构


修订记录

版本 日期 说明
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 侧 :玩家标记为 OFFLINEnLeaveSceneTime 记录断线时刻。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 恢复
状态冲突
无优先级依据
持续振荡

时序矛盾

  1. 玩家切图断线 → GS 通知 SceneServer 离线
  2. PlayServer 超时 → 若反向通知 SceneServer "离线"
  3. 同一时刻玩家重连成功 → GS 通知 SceneServer "上线"
  4. 两个通知通过网络传输,由于重排/重传/PCB 差异,谁先到无法确定
  5. 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 = nilnFrozen > 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.tPlayerstActionData 等),外部模块无法理解,造成不必要的耦合和潜在的类型错误。

麻将 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
相关推荐
蝶恋舞者44 分钟前
DNS 服务器(后续持续更新)
运维·服务器
海外数字观察家1 小时前
品未云:泰国华商批发零售一体化 ERP,破解本土软件适配难题|CSDN 技术分享
网络·数据库·人工智能
Mortalbreeze1 小时前
深入理解 Linux 线程机制(四):线程同步——条件变量与信号量
linux·运维·服务器·开发语言·c++
测功机之家1 小时前
精密传动,可靠测试:杭州索川科技减速机测试台解决方案深度解析
大数据·网络·科技
Huangjin007_1 小时前
【Linux 系统篇(一)】入门基础指令详解(一)
linux·服务器
河南花仙子科技2 小时前
小游戏开发中常见的BUG类型及成因
人工智能·科技·游戏·bug
#痴心绝对2 小时前
校园网综合实验20260716
网络
会飞的小新2 小时前
《深入理解 DDR Memory Margin》系列之第一章:从 DDR 到 Memory Margin——为什么服务器需要进行内存裕量测试?
linux·服务器
汤愈韬2 小时前
KYCA运维认证
运维·服务器·网络
利来利往2 小时前
如果你的电脑可以ping ip无法ping域名
网络·网络协议·tcp/ip