传奇客户端源码剖析:GameSrv响应回流与CGameProcess消息分发全链路

来源: 作者: 点击:
传奇客户端(WindHorn / Mir2Ex 工程)在进入 _GAME_PROC 状态后,所有游戏期逻辑都围绕一条主轴转:CClientSocket 收 GameGate 转发包 → PacketQ 排队 → CGameProcess::OnTimer 轮询 → ProcessPacket / ProcessDefaultPacket 分发 → 本玩家 Hero 或 m_xActorList 中 NPC/怪物/其他玩家对象消费。服务器端不是“直接改画面”,而是回送 TDEFAULTMESSAGE + 正文(加密体或非加密体),客户端按 Recog 标识路由,再驱动 DX 渲染与状态机。

一、网络入口:CClientSocket::OnSocketMessage(FD_READ)
WSAAsyncSelect 模型下,GameGate 推来的 TCP 数据在 CClientSocket::OnSocketMessage 的 FD_READ 分支被读入缓冲区,随即:
PacketQ.PushQ((BYTE*)pszPacket);

仅压队列,不在此处解包。真正消费在 CGameProcess::Load() 后由主循环 OnTimer 驱动:
OnMessageReceive();
ProcessPacket();
ProcessDefaultPacket();

_GAME_PROC 状态下 g_bProcState 决定走 CLoginProcess / CCharacterProcess / CGameProcess 各自的 OnMessageReceive 虚实现。

二、OnMessageReceive 与心跳
OnMessageReceive 先做心跳判定:收到心跳包回 '*' 并回发心跳;随后调 OnSocketMessageRecieve。包首字节决定分支:
- 以 '+' 开头 → OnProcPacketNotEncode(非编码明文指令:GOOD / FAIL / LNG / WID / FIR 等)
• 否则按 _TDEFAULTMESSAGE 结构解头(nRecog / wIdent / wParam / lParam / dwBodySize),进入游戏逻辑分发

三、ProcessPacket:Recog 路由核心
ProcessPacket 处理 m_xWaitPacketQueue 里的包,关键判断:
if (lpPacketMsg->stDefMsg.nRecog == m_xMyHero.m_dwIdentity) {
// 服务器发给“我”的本玩家消息
} else {
// 其他玩家 / 怪物 / NPC 消息
// 遍历 m_xActorList,找不到则按 stFeature.bGender 建对象:
// _GENDER_MAN → CHero;_GENDER_WOMAN → CHero;_GENDER_NPC → CNPC;_GENDER_MON → CActor
pxActor->m_xPacketQueue.PushQ(lpPacketMsg);
}

本玩家重要消息(SM_ABILITY / SM_NEWMAP / SM_LOGON / SM_SENDUSEITEMS / SM_SENDMYMAGIC 等)当场处理;其余压入 m_xMyHero.m_xPacketQueue。Actor 对象各自在 UpdatePacketState()(CActor / CNPC / CHero 均实现)里消费自身队列。

四、服务器端响应典型 SM_* 与客户端动作对照
• SM_NEWMAP / SM_MAPDESCRIPTION:加载地图块、设置场景光照(SM_DAYCHANGING 控制昼夜雾效)

• SM_LOGON:本地创建 Hero 对象,回发 CM_QUERYBAGITEMS 拉背包

- SM_SENDNOTICE:系统公告框
• SM_ABILITY:写金钱、职业、基础属性(m_Ability)

• SM_SUBABILITY:技能等级、毒/恢复/速度参数

• SM_SENDUSEITEMS:穿戴装备清单刷新渲染

- SM_SENDMYMAGIC:魔法列表写 UI
• SM_CHARSTATUSCHANGED:攻击力/状态位(中毒、麻痹、隐身)变更

• SM_TURN / SM_WALK / SM_RUN / SM_STRUCK / SM_MAGSTRUCK / SM_HIT / SM_DEATH:驱动 Actor 动作状态机

- SM_CLEAROBJECT / SM_DISAPPEAR:从 m_xActorList 移除对象,释放 DX 资源

五、走动指令闭环(CM_WALK → 服务端判定 → 回流)
客户端:CGameProcess::OnLButtonDown → 计算目标格 → g_xClientSocket.Send(CM_WALK,...)
GameSrv:ProcessUserHuman 线程遍历 UserInfoList → pUserInfo->Operate() → m_pxPlayerObject->Operate() → 死亡判 m_fIsDead 回 _MSG_FAIL,否则 WalkTo()(WalkNextPos 八向随机、CheckDoorEvent 门状态、m_pMap->CanMove、MoveToMovingObject)→ 成功回 _MSG_GOOD 并 AddRefMsg(RM_WALK) 广播周围 26×26 格内单元格玩家
客户端回流处理:
• _MSG_FAIL → CGameProcess::OnProcPacketNotEncode → m_xMyHero.SetOldPosition() 拉回移动前坐标

- _MSG_GOOD → m_bMotionLock = false,解除本地预测锁,正式播走路帧
• 他人 RM_WALK → 对应 CHero/CActor UpdatePacketState 播位移

六、怪物 / NPC / 物品可见性回流
GameSrv CPlayerObject::SearchViewRange() 算出 m_xVisibleObjectList / m_xVisibleItemList / m_xVisibleEventList,对每项目 AddProcess(RM_TURN / RM_ITEMSHOW / RM_SHOWEVENT),经 GameGate 下发 SM_TURN / SM_ITEMSHOW。客户端首次见到时建对象并常驻 ActorList,离开视距收 SM_DISAPPEAR 销毁。ProcessMonster 线程独立跑 AI:CMonsterObject::SearchViewRange() → Operate() 过滤 RM_DOOPENHEALTH / RM_STRUCK / RM_MAGSTRUCK 事件下发。

七、战斗与拾取回流
客户端发 CM_HIT / CM_SPELL / CM_PICKUP / CM_DROPITEM → GameSrv ProcessUserMessage() 路由到 CPlayerObject 战斗/施法/捡物分支 → 算命中、伤害、掉落 → 回 SM_STRUCK / SM_MAGSTRUCK / SM_DROPITEM_SUCCESS / SM_PICKUP_FAIL 等。客户端在 Hero/Actor 的 UpdatePacketState 里扣血、播受击、刷掉落物列表 m_stMapItemList。

八、编码与分包结构
TDEFAULTMESSAGE 头固定长度,后续 Body 走服务端 Encode / 客户端 Decode(异或+查表+位移,LoginGate 下发的 dyncode 参与密钥派生)。'+' 前缀包跳过 Decode 直交 OnProcPacketNotEncode,用于运动锁、坐标校正、简单确认,降低关键帧延迟。

九、对象生命周期与内存归属
• m_xMyHero:唯一本控角色,dwIdentity 由 SM_LOGON 回包写入

• m_xActorList:CHero / CNPC / CActor 混合链表,按 Recog 索引

- m_stMapItemList:地面物品/金币/装备,SM_ITEMSHOW 增、SM_CLEARITEM 删
• PacketQueue 双层:全局 PacketQ(CClientSocket 收)→ 对象级 m_xPacketQueue(分发后)→ UpdatePacketState 消费

- 切场景 / 回城 / 掉线:清空 ActorList、MapItemList、PacketQueue,重建 Hero 状态

十、服务端回流链路总览(组件级)
Client → GameGate(转发/心跳/加密) → GameSrv(ProcessUserHuman / ProcessMonster / UserInfo::Operate / CPlayerObject::Operate) → AddRefMsg 按地图单元格广播 → GameGate → Client CGameProcess 分发 → Hero/Actor 状态机 → DX 渲染

服务器端响应本质不是“推送画面”,而是回送带 Recog 的指令流;客户端所有位移、受击、装备、魔法、昼夜、可见性,都是对 SM_/RM_ 消息的解释执行,Hero 本地只做预测与回滚,权威状态始终在 GameSrv 的 CPlayerObject 与地图单元格里。