This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/internal/gameserver/routers/game_router_chinese_chess.go

325 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"`
}