package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 骰子(DiceGame)—— 酒吧「午夜回响」吧台的玩法, // C# 侧 L50.Gameplay.DiceGame.DiceGameManager,UI 侧 // Lua\LuaFiles\LX6\SGUI\StoreDefine\BarGameStartPanelStore.lua。 // ★ 根因链(2026-09-26 实机取证,逐条实锤) // // 第一版:AskEnterDiceZoneDoubleAI 没注册 ⇒ 框架 typed-default 兜底返回 0 // ⇒ 客户端 BarGameStartPanelStore.lua:162「AgentId为0,无法继续游戏」。已修(注册 handler)。 // 第二版(frida 实锤):返回编造的 uid 也不行。客户端 // lastReadyDiag = "等待 Agent 实体同步 agentId=35184381626017",必须返回 // 世界里真实存在的实体 uid。用 frida 把真实 NPC 的 uid 直写进 // diceOpponentAgentId 后,客户端立刻跑出 // SetPlayerPrepareMotionFinish → TransitionTo → DecidePlayerActionOrder → // PlaceDices → GenDice×5,状态从 prepareState 进到 beforeShakeState。 // // 所以这一版:服务端真的造一个 NPC 站到玩家旁边,把它的 uid 交回去。 // ★ 为什么不是「抄扇巴掌」 // 扇巴掌(L18.Gameplay.SlapSceneNode)的 NPC 对手是场景里预设好的 GameObject // (字段 palmKingSlapSceneNpcList),整个类只有 SetCameraActive / .ctor // 两个方法,纯本地、不经过服务端、也不是真实 Agent 实体 —— 没有可移植的服务端实现。 // 但它印证了同一件事:这类 POI 小游戏的对手就是「场景里一个真实存在的 NPC」。 // 我们这边没有现成的吧台 NPC 可复用,所以走服务端造 NPC 的通道(SendCivilianAsync), // 造出来的实体跟官方 static NPC 同段 uid,客户端一样认。 // ★ 迭代方式(重要) // 服务端→客户端那六条 Sync* 的载荷布局没有离线权威来源(MethodId 表 / RpcSurface / // 代理 lua 都是旧版,没有骰子)。字段顺序来自 IL2CPP dump,但「枚举线宽」「complex vs struct」 // 两处只能靠实机收敛。为此加了 /api/dice/raw(见 AdminWebServer): // 可以直接把任意 methodId + hex 推给客户端,不用重启服务端就能反复试格式。 // ⛑ 回滚:把 GameRouter.cs 的 Enabled4229938MethodIds 里这四行删掉即可。 // func GameRouter_Dice_GetDiceSession(ctx context.Context, rpcCtx *RpcContext) any { // Router implementation for GameRouter_Dice_GetDiceSession var zero any; return zero } // --------------------------------------------------------------------------------------- // AskEnterDiceZoneDoubleAI (67898934) —— 「开始游戏」按钮打过来的那条 // --------------------------------------------------------------------------------------- func GameRouter_Dice_AskEnterDiceZoneDoubleAI4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Dice_AskEnterDiceZoneDoubleAI4229938 var zero any; return zero } // // 服务端当主机,驱动整局骰子对战。 // ★ 2026-09-26 实机实锤的命令序列(frida 逐个验证,看客户端状态机推进) // // SyncDiceCommand(gadgetUId, StartShake=1) -> DoTransitionToShakeState -> shakeState // SyncDiceCommand(gadgetUId, OpenBox=2) -> DoTransitionToAfterShakeState -> afterShakeState // SyncDiceCommand(gadgetUId, SkipToSettle=6) -> -> roundSettleState // 再发 StartShake -> 自动换手(roundPlayer 从 DicePlayer 变 RemotePlayer) // // // ★★ 最关键的一条:命令的 targetUid 是 gadgetUId(玩法区实体), // 不是玩家 uid、也不是对手 uid。发给 uid=0 或对手 uid 时客户端只收包不推进状态, // 只有发 gadgetUId 才会真的推进状态机。这个坑排查了很久。 // // // ⛑ 另外别信 lastReadyDiag:那是某一时刻的一次性快照,对局中不会再刷新。 // 实测 main.isMotionInit 已经是 true、op.isSkillInit 手动置 true 之后, // 这条 diag 字符串一个字都没变。真正的实时状态看 nowState / roundPlayer。 // // func GameRouter_Dice_RunDiceHostAsync(ctx context.Context, session *GameSession, st any) any { // Router implementation for GameRouter_Dice_RunDiceHostAsync var zero any; return zero } // // 组装 SyncDiceScoreInfo 里的 seat -> 分数表。 // ⛑ seat 0 是玩家、seat 1 是对手。传的是「含本手的累计分」, // 因为这条分数字是在本手加分之前下发的。 // func GameRouter_Dice_DiceScores(ctx context.Context, st any, isPlayerTurn bool, turnScore int32) map[string]any { // Router implementation for GameRouter_Dice_DiceScores return nil } // 目前领先的一方:0 = 玩家(seat 0),1 = 对手(seat 1)。平局算玩家。 func GameRouter_Dice_LeaderOf(ctx context.Context, st any, isPlayerTurn bool, turnScore int32) int32 { // Router implementation for GameRouter_Dice_LeaderOf return 0 } // 掷 n 颗骰子(1..6)。 func GameRouter_Dice_RollDice(ctx context.Context, rnd any, n int32) []any { // Router implementation for GameRouter_Dice_RollDice return nil } // // 一手得分。 // // Simple(mode=1):返回**牌型等级** DiceHandRank(0..8),本回合双方比大小。 // Classic(mode=0):返回**原版计分** DiceBasicScore(点数分 × 组合倍数),累加进总分。 // // // ★ 2026-09-27 从 7k-v0.5 移植:那一版的 DiceBasicScore / DiceHandRank 与 // DiceGame_Rules.md(frida 挂客户端读 DiceHandTypeCalculator 的实测值)逐项吻合, // 比原先的「五颗点数之和」占位规则更接近原版。客户端 BaseDicePlayer 的 // nowRoundScore 等字段在服务端不下发时恒为 0 ⇒ 分数仍由我们当权威,改算法不影响链路。 // // func GameRouter_Dice_ScoreOf(ctx context.Context, dice []any, simpleMode bool) int32 { // Router implementation for GameRouter_Dice_ScoreOf return 0 } // // 原版「牌型」判定(DiceHandTypeCalculator.GetHandType / ClassifyHand 的等价实现)。 // 0 高牌 / 1 一对 / 2 两对 / 3 三条 / 4 四连顺 / 5 葫芦 / 6 四条 / 7 五连顺 / 8 五条。 // func GameRouter_Dice_DiceHandRank(ctx context.Context, dice []any) int32 { // Router implementation for GameRouter_Dice_DiceHandRank return 0 } // // 原版一手得分 = Σ 点数分(literal)× 命中组合的倍数。 // literal:1→10, 2→2, 3→3, 4→4, 5→5, 6→8(1 和 6 是特殊值)。 // 组合倍数:对子 4 / 三条 10 / 四条 30 / 五条 80 / 六条 150;顺子 4 连 14 / 5 连 24 / 6 连 45。 // ⛑ 简化:最低 ×1,不实现徽章改倍数与爆点惩罚(我们也不下发徽章)。 // func GameRouter_Dice_DiceBasicScore(ctx context.Context, dice []any) int32 { // Router implementation for GameRouter_Dice_DiceBasicScore return 0 } // // 等玩家操作;超时则记一条日志并返回 false(自动代打)。 // 玩家操作的信号是客户端上报 AskDicePlayerAction,在它的 handler 里 SetResult。 // func GameRouter_Dice_WaitPlayerOrTimeout(ctx context.Context, session *GameSession, st any, timeoutMs int32, stepName string) bool { // Router implementation for GameRouter_Dice_WaitPlayerOrTimeout return true } // // 下发包一层:Sync* 里除了 SyncDiceCommand 之外都没实机验证过载荷, // 万一客户端解析报错也不该把整个对局主机带崩。 // func GameRouter_Dice_SafePush(ctx context.Context, session *GameSession, push any) any { // Router implementation for GameRouter_Dice_SafePush var zero any; return zero } // // 在玩家身前造一个市民 NPC,返回它的真实 uid。 // 走的是 /api/civilians/spawn-group 那条已验证可见的通道(SendCivilianAsync), // uid 与官方 static NPC 同段(StaticNpcIdFloor),客户端能找到这个实体。 // func GameRouter_Dice_SpawnDiceOpponentAsync(ctx context.Context, conn any) any { // Router implementation for GameRouter_Dice_SpawnDiceOpponentAsync var zero any; return zero } // // 把这一局造出来的对手 NPC 撤掉(2026-09-27 新增,用户要求「游戏结束后清除」)。 // ★ 两条通道都发:static NPC 没有专属的 *StaticNpcRemove // (MethodId 表里只有 AddData / ConvertToPe / VehicleRemove), // 而 SyncAetherAINpcRemove(68332284) 管的是 ambient peds / functional NPC 那本账, // 我们对不对得上无法离线判定 ⇒ 两条都发,哪条生效算哪条(多发一条无效包客户端是忽略的)。 // ⛑⛑ 服务端账本必须一起清:StaticNpcPoses / StaticNpcPreparedPlotEvents // 留着的话,玩家走远再回来触发重扫(每 120s 全量重扫)会把它再摆回来。 // ⛑ :传了就只撤这一个 uid(对局主机用,防止旧局收尾 // 误删新局 NPC);不传就撤「当前对手」(玩家主动离开时用)。 // 只有「要撤的正好还是当前对手」时才清 st.OpponentUid 指针。 // func GameRouter_Dice_RemoveDiceOpponentAsync(ctx context.Context, session *GameSession, st any, reason string, expectedUid uint64) any { // Router implementation for GameRouter_Dice_RemoveDiceOpponentAsync var zero any; return zero } // 挑一个市民模板;优先「非系统行为」组里第一个可用的。 func GameRouter_Dice_PickDiceOpponentItem(ctx context.Context, ) any { // Router implementation for GameRouter_Dice_PickDiceOpponentItem var zero any; return zero } // 把角度收进 (-180, 180]。 func GameRouter_Dice_WrapDegrees(ctx context.Context, deg float32) float32 { // Router implementation for GameRouter_Dice_WrapDegrees return 0 } // // 造 NPC 失败时的兜底 uid。刻意落在 ServerSpawnUidFloor 之上再偏 0x900000, // 避免与玩家/敌人账本串味。客户端会卡在「等待 Agent 实体同步」,但至少不是 0。 // func GameRouter_Dice_FallbackUid(ctx context.Context, ) uint64 { // Router implementation for GameRouter_Dice_FallbackUid return 0 } // --------------------------------------------------------------------------------------- // 其余三条:都是 void 返回,空包 + err=0 就是客户端要的 OK 回执。 // --------------------------------------------------------------------------------------- // // AskDicePlayerAction (67747541) —— 玩家操作上报。单独拆出来是为了看清 // 「按空格」时客户端到底有没有上报、上报的命令是什么: // 线上布局与 SyncDiceCommand 同源,u64 + 1 字节命令 + FF + Int7(n) + n×Int32 + u32。 // func GameRouter_Dice_AskDicePlayerAction4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Dice_AskDicePlayerAction4229938 var zero any; return zero } // // 解析 AskDicePlayerAction 的载荷。 // 线上布局(与 SyncDiceCommand 同源,2026-09-26 实证): // u64 who | 1字节 cmd | FF | Int7(n) | n×Int32 | u32 badge。 // 实测空参数时是 00*8 05 FF 01 00000000 = 15 字节。 // func GameRouter_Dice_ParsePlayerAction(ctx context.Context, body []byte) any { // Router implementation for GameRouter_Dice_ParsePlayerAction var zero any; return zero } // AskLeaveDiceZone —— 玩家退出玩法区,通知主机尽快收摊,并顺手把对手撤掉。 func GameRouter_Dice_AskLeaveDiceZone4229938(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Dice_AskLeaveDiceZone4229938 var zero any; return zero } func GameRouter_Dice_DiceAck(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_Dice_DiceAck var zero any; return zero } // --------------------------------------------------------------------------------------- // 服务端→客户端下发(给 /api/dice/* 用的公共入口,方便不重启地试格式) // --------------------------------------------------------------------------------------- // SyncDiceGameStart (68696343)。OnSyncDiceGameStart(u64,u32,i32,i32,i32) = 24 字节平铺。 func GameRouter_Dice_PushDiceGameStartAsync(ctx context.Context, session *GameSession, gadgetUId uint64, opponentModelId uint32, totalRounds int32, targetScore int32, firstPlayer int32) any { // Router implementation for GameRouter_Dice_PushDiceGameStartAsync var zero any; return zero } // // SyncDiceCommand (68028865) —— 唯一**实机验证过**的下发通道。 // 线上布局:u64 + 1 字节命令 + FF + Int7(数量) + 数量×Int32 + u32。 // ★ 2026-09-26 21:24 实证:命令按 1 字节写时客户端 OnSyncDiceCommand → // ReceiveRemoteCommand 都会触发;按 4 字节写则完全不触发。 // func GameRouter_Dice_PushDiceCommandAsync(ctx context.Context, session *GameSession, targetUid uint64, command int32, intParams []any, badgeId uint32, enumWidth int32) any { // Router implementation for GameRouter_Dice_PushDiceCommandAsync var zero any; return zero } // SyncDiceRoundActions (68060649)。OnSyncDiceRoundActions(u64, DiceRoundActions)。 func GameRouter_Dice_PushDiceRoundActionsAsync(ctx context.Context, session *GameSession, targetUid uint64, targetPlayer int32, actions []any) any { // Router implementation for GameRouter_Dice_PushDiceRoundActionsAsync var zero any; return zero } // SyncDiceRoundSettle (68568673)。OnSyncDiceRoundSettle(u64, DiceRoundSettleInfo)。 func GameRouter_Dice_PushDiceRoundSettleAsync(ctx context.Context, session *GameSession, targetUid uint64, round int32, scoreA int32, scoreB int32, roundScore int32, isGameEnd bool, winner int32) any { // Router implementation for GameRouter_Dice_PushDiceRoundSettleAsync var zero any; return zero } // SyncDiceScoreInfo (68500287)。OnSyncDiceScoreInfo(u64, DiceScoreInfo)。 func GameRouter_Dice_PushDiceScoreInfoAsync(ctx context.Context, session *GameSession, targetUid uint64, currentRound int32, currentTurn int32, turnScore int32, winner int32, dice []any, isBust bool, phase int32, scores map[string]any) any { // Router implementation for GameRouter_Dice_PushDiceScoreInfoAsync var zero any; return zero } // SyncDiceGameEnd (68018652)。OnSyncDiceGameEnd(u64, DiceGameEndInfo)。 func GameRouter_Dice_PushDiceGameEndAsync(ctx context.Context, session *GameSession, targetUid uint64, winner int32, scoreA int32, scoreB int32, drops []any) any { // Router implementation for GameRouter_Dice_PushDiceGameEndAsync var zero any; return zero } // 把任意字节推给客户端(/api/dice/raw 用),用于不重启地试载荷格式。 func GameRouter_Dice_PushDiceRawAsync(ctx context.Context, session *GameSession, methodId uint32, bytes []byte) any { // Router implementation for GameRouter_Dice_PushDiceRawAsync var zero any; return zero } type DiceSession struct { GadgetUId uint64 `json:"gadgetUId"` OpponentUid uint64 `json:"opponentUid"` // SyncDiceGameStart 的四个可变参数。默认 gameAIId=100(与客户端一致)、3 回合、不设目标分、玩家先手。 OpponentModelId uint32 `json:"opponentModelId"` TotalRounds int32 `json:"totalRounds"` TargetScore int32 `json:"targetScore"` FirstPlayer int32 `json:"firstPlayer"` // // 客户端下发的模式:UX.Game.DiceGameType — 1 = Simple(5 颗,比牌型)、0 = Classic(6 颗,比分数)。 // 实机(2026-09-27 进局包)客户端恒发 u8@12 = 1,即 Simple。 // Mode int32 `json:"mode"` // 客户端下发的难度(PoiGameDiceAIConfig 100..103),目前只用于日志。 Level int32 `json:"level"` // 本局骰子数:Simple 5 颗 / Classic 6 颗(客户端 SimpleDiceNum=5 / DefaultDiceNum=6)。 DiceCount int32 `json:"diceCount"` // Simple = 比牌型(一回合一个胜场),Classic = 比分数值(累加)。 SimpleMode bool `json:"simpleMode"` GameStartSent bool `json:"gameStartSent"` // 主机循环是否已启动(用 Interlocked 保证一次点开始只起一个)。 HostStarted int32 `json:"hostStarted"` Round int32 `json:"round"` // ScoreA = 玩家(seat 0,mainPlayer),ScoreB = 对手(seat 1)。 ScoreA int32 `json:"scoreA"` ScoreB int32 `json:"scoreB"` // 本回合内玩家/对手各掷出的分,用于判定「本回合赢家」。 RoundScoreA int32 `json:"roundScoreA"` RoundScoreB int32 `json:"roundScoreB"` StartedUtcTicks int64 `json:"startedUtcTicks"` // ---- 玩家回合协作 ------------------------------------------------------------- // 玩家离开玩法区后置 true,主机循环会尽快退出。 LeaveRequested bool `json:"leaveRequested"` // 主机正在等的「玩家操作」信号。非 null 表示正在等玩家。 PlayerSignal *any `json:"playerSignal"` // 正在等玩家的这一步的名字(只用于日志)。 WaitingFor string `json:"waitingFor"` // 玩家最近一次出手上报的命令(0 = 还没有)。 LastPlayerCmd int32 `json:"lastPlayerCmd"` // 玩家最近一次出手上报的参数。 LastPlayerParams []int32 `json:"lastPlayerParams"` // 玩家这一局是否真的出过手(用于「玩家是否在挂机」的自适应)。 PlayerEverActed bool `json:"playerEverActed"` } // UX.Game.DiceRemoteCommand 的线上取值(1 字节)。 type DiceCommand struct { } const ( DiceCommand_UseSkill int32 = 0 DiceCommand_StartShake int32 = 1 DiceCommand_OpenBox int32 = 2 DiceCommand_SelectDices int32 = 3 DiceCommand_Rethrow int32 = 4 DiceCommand_Settle int32 = 5 DiceCommand_SkipToSettle int32 = 6 DiceCommand_BustRethrow int32 = 7 DiceCommand_BustCancel int32 = 8 DiceCommand_DiceResult int32 = 9 DiceCommand_InitSkills int32 = 10 )