325 lines
21 KiB
Go
325 lines
21 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// ★★★ R182(2026-09-29)—— 象棋「能交互、开局后黑屏」的修复 ★★★
|
||
// ── 病灶(R181 定案,这一轮修)────────────────────────────────────────────
|
||
// 黑屏由 ChineseChessMgr.EnterGame_Async 一进来就开,只在
|
||
// OnEnterZoneSuccess 那条协程跑到**最后一行** CloseTransition 时才关。
|
||
// 那条协程的第一个门是:
|
||
// <code>
|
||
// while true do
|
||
// if self.Player1 and self.Player2 and self.Player1.IsModelLoaded and self.Player2.IsModelLoaded then break end
|
||
// coroutine.yield(nil)
|
||
// end
|
||
// </code>
|
||
// 而 Player2(对手)的加载卡在 ChineseChessPlayer.lua:224:
|
||
// <code>
|
||
// if not gChineseChessMgr.OpponentAgentInstanceId or not ulong.Greater(..., 0) then
|
||
// coroutine.yield(gChineseChessMgr.WaitOpponentAgentInstanceIdToken) -- ★ 无超时
|
||
// end
|
||
// </code>
|
||
// ⇒ 服务端必须做两件事,缺一不可:
|
||
// ① **spawn 一个世界里真实存在的对手单位**(客户端 CheckAiAgent 会
|
||
// gCS.SceneDataMgr.TryGetOrConvertToUnit(instanceId, 256) 去查它);
|
||
// ② 推 SyncGameGroundZoneInfo,且 ParticipantInfos 里那一条的
|
||
// AgentInstanceId = ①的那个 uid —— 这是
|
||
// gChineseChessMgr.OpponentAgentInstanceId 的**唯一**来源。
|
||
// ── 为什么不能靠 SyncGameGroundZonePlayerInfo(那条推送)────────────────────
|
||
// GameSceneToClientImpl.lua:1162 对它加了闸:
|
||
// <code>
|
||
// if participantInfo.Pid == gPlayerManager.infoLogin.bindData.pid then
|
||
// ... gChineseChessMgr:OnSyncGameGroundZonePlayerInfo(...)
|
||
// end
|
||
// </code>
|
||
// 对手的 Pid 不是我的 pid ⇒ **那条推送永远送不到象棋管理器**。
|
||
// 唯一能遍历全部参与方的是 OnSyncGameGroundZoneInfo(它直接 ipairs(zoneInfo.ParticipantInfos))。
|
||
// ── 与五子棋(R153/R155/R156/R157)的异同 ────────────────────────────────
|
||
// · 同:都靠「注册 logic-agent uid + 用客户端给的 agentId 走 Aether 静态 NPC 通道把身份送过去」,
|
||
// 座位都取自官方摆位,且**一个会话一张桌只 spawn 一次**(R157 的复用法)。
|
||
// · 异:五子棋的入口返回**裸 u64**(客户端 TryGetLogicAgent 要用),象棋三个入口
|
||
// midToReturnMessageReader 全是 function(reader) end ⇒ **void**,空 ok 即可。
|
||
// ★ 也正因为回执本来就没错(unknown-methods.log 里那两个包是 typed-default 但 err=0,
|
||
// 客户端 .Callback 只判 err)—— 所以本轮的修法**只加推送,不改回执形状**。
|
||
// · 异:象棋的参与方是**多态列表元素**(tag 7,字段比 base 多一个 IsRed);
|
||
// 五子棋那次 ParticipantInfos 一直是空的 ⇒ 那条路从没被走过。见 ChineseChessWireContracts.cs。
|
||
// ── 本轮的边界(说清楚,别把它当「象棋能玩了」)──────────────────────────
|
||
// 本轮只打通到 **「棋盘可见、黑屏关掉」**。走子还需要第二批:
|
||
// 收 AskChineseChessMovePiece → **服务端算应手** → 推 SyncChineseChessMove +
|
||
// SyncGameGroundZoneTurnChange。已确认 **AI 不在客户端**
|
||
// (ChineseChessSearchChart.lua 在本仓 2349 个 Lua 里只被 require、零调用点)。
|
||
// ⛑ 回滚:把 GameRouter.cs 的 Enabled4229938MethodIds 里那 4 行象棋 mid 删掉即可
|
||
// (handler 不注册 = 走 typed-default 兜底),不用删本文件。
|
||
//
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// ① 标准对局(人机)—— 本轮实测命中的就是这一条
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
//
|
||
// AskChineseChessEnterZoneDoubleAI (67307903)
|
||
// C2S = u64 gadgetuid + u32 agentid + bool useredpiece + byte difficulty;RET = **void**。
|
||
// 契约第一现场:ClientToGameSceneDelegate.lua:1790 AskChineseChessEnterZoneDoubleAI_Serializer
|
||
// WritePrimitive(gadgetuid, WriteUInt64) / (agentid, WriteUInt32) /
|
||
// (useredpiece, WriteBoolean) / (CheckEnum(difficulty,53,0), WriteByte)
|
||
// 实测 body=14b:25 00 48 B1 7E CB 92 74 | BB 5B 64 02 | 01 | 00
|
||
// ⇒ gadgetUid 8400000000000000037(= R179 补摆进凌云的那张棋盘,已核对到 u64)
|
||
// agentId 40131515(NpcCatalog「Xian Yougen」)/ useRedPiece true / difficulty 0。
|
||
// 返回读取器 midToReturnMessageReader[67307903] = function(reader) end ⇒ 空体 = void。
|
||
//
|
||
func GameRouter_ChineseChess_AskChineseChessEnterZoneDoubleAIAccept(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_ChineseChess_AskChineseChessEnterZoneDoubleAIAccept
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// ② 残局挑战 —— 同族同形,只是多一个 endGameId
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
//
|
||
// AskChineseChessEnterZoneEndGame (67616104)
|
||
// C2S = u64 gadgetuid + u32 agentid + u32 endgameid;RET = **void**
|
||
// (midToReturnMessageReader[67616104] = function(reader) end)。
|
||
// 客户端调用点 ChineseChessMgr.lua:195:self.IsRed = endGameCfg.IsRedFirst
|
||
// —— 残局里执红与否由 PoiGameChineseChessEndGameConfig 决定(20 条,已确认存在),
|
||
// 所以「玩家执红」这件事只能靠请求里的 agentid 之外的信息;本实现按**请求缺省 = 玩家执红**
|
||
// 处理(与五子棋残局同策略),并把 AI 放黑位。
|
||
//
|
||
func GameRouter_ChineseChess_AskChineseChessEnterZoneEndGameAccept(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_ChineseChess_AskChineseChessEnterZoneEndGameAccept
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// ③ 离开 —— 只回执;**故意不清理对手**(同五子棋 R157 的理由)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
//
|
||
// AskChineseChessLeaveZone (67667163) —— C2S = u64 gadgetuid;RET = **void**。
|
||
// ★ 与五子棋 LeaveGomoku 完全同源:本版客户端**没有可靠的静态 NPC 删除通道**
|
||
// (SyncAetherAIStaticNpcRemove* 在 MethodId 里不存在;SyncRemoveNpc 装不下
|
||
// 830… 段的 u64;SyncRemoveManagedAgent 全 Lua 0 命中)。既然删不掉,就**只建一次**:
|
||
// 清缓存只会让下次进来 Next() 出第二个 uid,旧的那个照样留在场上。
|
||
//
|
||
func GameRouter_ChineseChess_AskChineseChessLeaveZoneAccept(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_ChineseChess_AskChineseChessLeaveZoneAccept
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// ④ 玩家棋力/残局进度查询 —— 返回真类型,别让开始面板的字典悬空
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
//
|
||
// QueryChineseChessPlayerInfo (67492072) —— C2S = 空;RET = complex
|
||
// (midToReturnMessageReader[67492072] = Base.ReadComplex(reader, Auto.Reader[681]))。
|
||
// 消费者 ChineseChessStartPanelStore.lua:61:
|
||
// <code>QueryChineseChessPlayerInfo().Callback = function (err, info)
|
||
// if err == Ok and info then
|
||
// self.passedEndGameIds = info.DroppedEndGameIdSet
|
||
// if self.selectedMode ~= Late then self:InitEndGameLevelSelector() end
|
||
// end
|
||
// end</code>
|
||
// 走 typed-default 时字典字段是 nil ⇒「已通关残局」判定悬空。这里补一个**真空表**。
|
||
// ⚠ 两个字典是 Base.WriteDict(Int32 计数),不是 7bit —— 见 ChineseChessWireContracts.cs。
|
||
//
|
||
func GameRouter_ChineseChess_QueryChineseChessPlayerInfoAccept(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_ChineseChess_QueryChineseChessPlayerInfoAccept
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// AskChineseChessMovePiece (67463405)
|
||
// C2S = u64 gadgetuid + u32 fromX + u32 fromY + u32 toX + u32 toY = **24 B**;RET = void
|
||
// (midToReturnMessageReader[67463405] 是空体)。
|
||
// 逐字节对照实测报文(`logs/packets-20260929-101956.log:19087+`):
|
||
// 25 00 48 B1 7E CB 92 74 | 0A 00 00 00 | 05 00 00 00 | 07 00 00 00 | 05 00 00 00
|
||
// ⇒ gadgetUid 8400000000000000037 / from (10,5) / to (7,5)
|
||
// (红炮在 (10,5) —— 与 `ChineseChessChart.InitChartBoard` 的 `GetPointByPosition(10,5)` 对上,
|
||
// 这条一致性就是「坐标 = (x,y)」这个约定的锚点)。
|
||
// 签名权威 ClientToGameSceneDelegate.lua:2608 AskChineseChessMovePiece_Serializer;
|
||
// 调用点 ChineseChessMgr.lua:1016。
|
||
// ★ 回执策略:**任何**路径都回 Ok(连非法着法也回)——
|
||
// 客户端的 .Callback 对 Ok 是「什么都不做」(真值形态 if err ~= Ok then 显示错误),
|
||
// 而**不回执**会让 IsWaitingServerMove 永远停在 true ⇒ 此后一发不可收拾。
|
||
// 非法着法的表现 = 「推回一个 move」这件事没发生 ⇒ 棋盘不动、可以重选,正是我们想要的。
|
||
//
|
||
func GameRouter_ChineseChess_AskChineseChessMovePieceAccept(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_ChineseChess_AskChineseChessMovePieceAccept
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// AskChineseChessSurrender (67452562) —— C2S = u64 gadgetuid;RET = **void**
|
||
// (midToReturnMessageReader[67452562] 空体)。
|
||
// ★ R183 顺手补的第三条(可单独回滚):不做它 ⇒ 玩家点「认输」什么都没发生。
|
||
// 客户端 GiveUp 先置 _isGiveUp = true 再发包;收尾在服务端推的
|
||
// SyncChineseChessGameOver 里(TryFinalizeGame 读到 _isGiveUp 后才走
|
||
// 「黑屏退出」那一支)。
|
||
// ★ 认输 = **对手胜** ⇒ winner 取对手的颜色(0 红 / 1 黑)。
|
||
//
|
||
func GameRouter_ChineseChess_AskChineseChessSurrenderAccept(ctx context.Context, conn any, msg any) any {
|
||
// Router implementation for GameRouter_ChineseChess_AskChineseChessSurrenderAccept
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 盘面状态:建 / 重置 / 取
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 新开一局 ⇒ 把盘面重置成标准开局,并记下这一局的参数。
|
||
func GameRouter_ChineseChess_ResetChineseChessGame(ctx context.Context, conn any, gadgetUid uint64, playerUsesRed bool, difficulty byte, aiUid uint64) any {
|
||
// Router implementation for GameRouter_ChineseChess_ResetChineseChessGame
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 取这一局的盘面;没有就**懒建**一个。
|
||
// 懒建存在的理由:玩家可能不经我们记录的入口就发出落子包(客户端重启、快速重进),
|
||
// 这时服务端仍要有盘面才算得出应手 —— 拿标准开局 + 会话里缓存的颜色/难度是最合理的近似。
|
||
//
|
||
func GameRouter_ChineseChess_GetOrCreateChineseChessGame(ctx context.Context, conn any, gadgetUid uint64) any {
|
||
// Router implementation for GameRouter_ChineseChess_GetOrCreateChineseChessGame
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// ★ 锁内:校验玩家的着法、落子、算 AI 应手、判是否终局。**只算不推**(推送在锁外)。
|
||
// 拒绝清单(任一命中就只回 ok、什么都不推)—— 每一项都能在日志里看到确切原因,
|
||
// 这就是「服务端不认这一步」与「服务端根本没收到这一步」的分界线。
|
||
//
|
||
func GameRouter_ChineseChess_BuildChineseChessMovePlan(ctx context.Context, game any, fromX int32, fromY int32, toX int32, toY int32, plan any) any {
|
||
// Router implementation for GameRouter_ChineseChess_BuildChineseChessMovePlan
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 拼一条 SyncGameGroundZoneTurnChange(21 B,Inline,无标记)。
|
||
func GameRouter_ChineseChess_MakeTurnChange(ctx context.Context, gadgetUid uint64, round uint32, redToMove bool) any {
|
||
// Router implementation for GameRouter_ChineseChess_MakeTurnChange
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 玩家执黑时替 AI 走红方开局(红先手)。
|
||
// 为什么必须有:客户端 `IsMyAction()` = 「我的颜色 == `Chart.IsRedPlayChess`」,
|
||
// 而 `Chart.IsRedPlayChess` 开局恒为 true(红先)⇒ 玩家执黑时它一直是 false
|
||
// ⇒ `Board.ClickPoint` 第一个门就 `return` ⇒ **连棋子都选不了**。
|
||
// 所以玩家执黑时,服务端必须先把红方那一手推过去,客户端才轮到玩家。
|
||
// ⚠ 这两条推送会落在客户端的 `_pendingInGameMessages` 里(此刻 `IsPlaying` 还是 false),
|
||
// 由 `OnEnterZoneSuccess` 里的 `FlushPendingMessages()` 在 `IsPlaying = true` 之后补放 ——
|
||
// 那一步在 `Board` 已建成之后,所以 `board.Board` 非空、不会丢。
|
||
//
|
||
func GameRouter_ChineseChess_PushChineseChessAiOpeningIfNeededAsync(ctx context.Context, conn any, gadgetUid uint64, playerUsesRed bool) string {
|
||
// Router implementation for GameRouter_ChineseChess_PushChineseChessAiOpeningIfNeededAsync
|
||
return ""
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 核心:推 zone info(唯一能送达 OpponentAgentInstanceId 的通道)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
//
|
||
// ★★★ R182 核心:把「这局开了、对手是这个单位」推给客户端。
|
||
// 三条独立证据要求它必须存在:
|
||
// ① ChineseChessMgr.lua:1203 是 OpponentAgentInstanceId 的**唯一**赋值点,
|
||
// 而它只在 OnSyncGameGroundZonePlayerInfo 里,唯一能送达对手的调用者是
|
||
// OnSyncGameGroundZoneInfo 的 ipairs(zoneInfo.ParticipantInfos)。
|
||
// ② 按 pid 过滤的那条专用推送(GameSceneToClientImpl.lua:1162)对手永远进不去。
|
||
// ③ 实测 packets:SyncGameGroundZoneInfo 在全部历史日志里 **0 命中** —— 客户端从没收到过。
|
||
// 字段取值依据(值不是猜的):
|
||
// ZoneType = 12(GameGroundZoneType.ChineseChess,来自 interop
|
||
// Auto.Client.dll 的 Constant 表:Dart0/Chef1/Bowling2/Gomoku3/Racing4/Basketball5/
|
||
// Balloon6/Scratch7/FryChestnut8/Glue9/Washer10/BBQ11/ChineseChess12/PartyDance13/
|
||
// RingToss14/Dice15)
|
||
// ZoneState = GameStart(2)、SyncReason = Enter(0)、StartReason = Single(0)
|
||
// CurrentRound = 1、CurrentTurn = 0(本条推送的这两个字段客户端不读 ——
|
||
// 回合走的是另一条 SyncGameGroundZoneTurnChange)
|
||
// ScoreInfo 非空但**全空集合**(随 Client4229938 契约文件里同一块名字)。
|
||
// ★ ParticipantInfos 是本轮唯一真正的新东西:多态列表、元素 tag = 7、
|
||
// 字段 = IsRed | Pid | NpcCultivationId | AIAgentInfo | AgentInstanceId | ...(Reader[880])。
|
||
// AgentInstanceId 必须 = 世界里真实存在的单位 uid,否则客户端
|
||
// TryGetOrConvertToUnit 查不到 → 10 秒后打
|
||
// [ChineseChess] 等待服务器对手 NPC 单位超时(它有超时的那一支会打日志,
|
||
// 没超时的那一支不会 —— 这正是 R181 用来区分两个等待点的指纹)。
|
||
// 回滚 = 删两个 handler 里对本方法的调用(两行),白名单那几行留着无害。
|
||
//
|
||
func GameRouter_ChineseChess_PushChineseChessZoneInfoAsync(ctx context.Context, conn any, gadgetUid uint64, gameType byte, aiUid uint64, playerUsesRed bool, endGameId uint32) any {
|
||
// Router implementation for GameRouter_ChineseChess_PushChineseChessZoneInfoAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 对手单位:与五子棋 R153/R155/R157 三步完全同形
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
//
|
||
// 建一个「客户端查得到的 AI 对手」—— 三步(与 SpawnGomokuAiAsync 同源):
|
||
// ① 分配 logic-agent uid 并 SyncLogicAgentEnter 注册;
|
||
// ② 用**客户端给的 agentId** 走 Aether 静态 NPC 通道
|
||
// (NpcFormworkId 就是 AgentConfig id;不送客户端建 Unit 时报
|
||
// 创建AgentUnit失败!AgentConfigId为0! —— R155 的血泪);
|
||
// ③ 把同一个 uid 交回调用方,由调用方写进 ParticipantInfos[].AgentInstanceId。
|
||
// ★ R157 的复用:同一会话同一张桌**只 spawn 一次**。缓存键带 gadgetUid,因为凌云有 2 张棋盘
|
||
// (8400000000000000036 @(-4617.82,189.51,-2605.60) /
|
||
// 8400000000000000037 @(-4563.78,188.85,-2628.81)),要各有一个对手。
|
||
//
|
||
func GameRouter_ChineseChess_SpawnChineseChessAiAsync(ctx context.Context, conn any, gadgetUid uint64, agentId uint32, playerUsesRed bool, tag string, difficulty byte) any {
|
||
// Router implementation for GameRouter_ChineseChess_SpawnChineseChessAiAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
//
|
||
// 对手该站哪:棋盘根位姿取 WorldEntryState.SpawnedGadgets(服务端 spawn 时登记了
|
||
// X/Y/Z + PathId),yaw 再从 PropRestock.json 按 (pathId, x, z) 精确匹配
|
||
// (棋盘是 R179 经 PropRestock 摆进来的 ⇒ 权威表就是它,不是 MiniGameStalls)。
|
||
// AI 放在棋盘 **+forward×1.5**、朝向 yaw+180(面朝棋盘);实测玩家站在 −forward 侧,
|
||
// 所以两者不会重叠。查不到返回 null(调用方退回「只注册 uid」的老行为,不硬造坐标)。
|
||
//
|
||
func GameRouter_ChineseChess_ResolveChineseChessAiSeat(ctx context.Context, conn any, gadgetUid uint64, playerUsesRed bool) any {
|
||
// Router implementation for GameRouter_ChineseChess_ResolveChineseChessAiSeat
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 从补货表按 (pathId, x, z) 反查棋盘的 yaw;找不到给 0(只会让 AI 朝向偏一点)。
|
||
func GameRouter_ChineseChess_LookupChineseChessBoardYaw(ctx context.Context, pathId int32, x float32, z float32) float32 {
|
||
// Router implementation for GameRouter_ChineseChess_LookupChineseChessBoardYaw
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// 一局棋的全部可变状态。★ 所有读写都进 SyncRoot —— 同一条连接上的
|
||
// 多个 handler 是异步交错的(`await` 出点),而盘面是纯可变结构。
|
||
//
|
||
type ChineseChessGameSession struct {
|
||
SyncRoot any `json:"syncRoot"`
|
||
// 规则引擎(9×10 盘面 + 走法生成 + 将军/绝杀 + AI)。
|
||
Board ChineseChessBoard `json:"board"`
|
||
GadgetUid uint64 `json:"gadgetUid"`
|
||
AiUid uint64 `json:"aiUid"`
|
||
// 玩家执红?(AI = 另一方)
|
||
PlayerUsesRed bool `json:"playerUsesRed"`
|
||
// 客户端在进入请求里给的 AI 难度(0 = Beginner 实测值)。
|
||
Difficulty byte `json:"difficulty"`
|
||
// 下一步棋的序号(写进 ChineseChessMove.MoveIndex)。
|
||
NextMoveIndex uint32 `json:"nextMoveIndex"`
|
||
// 已分胜负?是则后续落子一律拒绝。
|
||
Over bool `json:"over"`
|
||
AiIsRed bool `json:"aiIsRed"`
|
||
}
|
||
|
||
//
|
||
// 「这一步算完之后要往客户端推什么」的清单 —— 锁内产出、锁外消费。
|
||
// ★ 这么拆是因为推送是 await,而 `await` **不能**放在锁里(会把整条连接的其它 handler 一起堵住)。
|
||
//
|
||
type ChineseChessMovePlan struct {
|
||
// 非空 = 这一步不合法,只回 ok 不推任何东西(客户端可重选)。
|
||
Reject *string `json:"reject"`
|
||
PlayerMove *ChineseChessMove `json:"playerMove"`
|
||
TurnAfterPlayer *GameGroundZoneTurnChangePush `json:"turnAfterPlayer"`
|
||
AiMove *ChineseChessMove `json:"aiMove"`
|
||
TurnAfterAi *GameGroundZoneTurnChangePush `json:"turnAfterAi"`
|
||
// null = 未分胜负;0 = 红胜 / 1 = 黑胜(颜色,见 ChineseChessWireContracts)。
|
||
Winner *int32 `json:"winner"`
|
||
OverReason byte `json:"overReason"`
|
||
}
|
||
|