353 lines
17 KiB
Go
353 lines
17 KiB
Go
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。
|
||
// <para>★ 根因链(2026-09-26 实机取证,逐条实锤)</para>
|
||
// <list type="number">
|
||
// <item>第一版:AskEnterDiceZoneDoubleAI 没注册 ⇒ 框架 typed-default 兜底返回 0
|
||
// ⇒ 客户端 BarGameStartPanelStore.lua:162「AgentId为0,无法继续游戏」。已修(注册 handler)。</item>
|
||
// <item>第二版(frida 实锤):返回编造的 uid 也不行。客户端
|
||
// lastReadyDiag = "等待 Agent 实体同步 agentId=35184381626017",必须返回
|
||
// 世界里真实存在的实体 uid。用 frida 把真实 NPC 的 uid 直写进
|
||
// diceOpponentAgentId 后,客户端立刻跑出
|
||
// SetPlayerPrepareMotionFinish → TransitionTo → DecidePlayerActionOrder →
|
||
// PlaceDices → GenDice×5,状态从 prepareState 进到 beforeShakeState。</item>
|
||
// </list>
|
||
// 所以这一版:服务端真的造一个 NPC 站到玩家旁边,把它的 uid 交回去。
|
||
// <para>★ 为什么不是「抄扇巴掌」</para>
|
||
// 扇巴掌(L18.Gameplay.SlapSceneNode)的 NPC 对手是场景里预设好的 GameObject
|
||
// (字段 palmKingSlapSceneNpcList),整个类只有 SetCameraActive / .ctor
|
||
// 两个方法,纯本地、不经过服务端、也不是真实 Agent 实体 —— 没有可移植的服务端实现。
|
||
// 但它印证了同一件事:这类 POI 小游戏的对手就是「场景里一个真实存在的 NPC」。
|
||
// 我们这边没有现成的吧台 NPC 可复用,所以走服务端造 NPC 的通道(SendCivilianAsync),
|
||
// 造出来的实体跟官方 static NPC 同段 uid,客户端一样认。
|
||
// <para>★ 迭代方式(重要)</para>
|
||
// 服务端→客户端那六条 Sync* 的载荷布局没有离线权威来源(MethodId 表 / RpcSurface /
|
||
// 代理 lua 都是旧版,没有骰子)。字段顺序来自 IL2CPP dump,但「枚举线宽」「complex vs struct」
|
||
// 两处只能靠实机收敛。为此加了 /api/dice/raw(见 AdminWebServer):
|
||
// 可以直接把任意 methodId + hex 推给客户端,不用重启服务端就能反复试格式。
|
||
// <para>⛑ 回滚:把 GameRouter.cs 的 Enabled4229938MethodIds 里这四行删掉即可。</para>
|
||
//
|
||
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
|
||
}
|
||
|
||
//
|
||
// 服务端当主机,驱动整局骰子对战。
|
||
// <para>★ 2026-09-26 实机实锤的命令序列(frida 逐个验证,看客户端状态机推进)</para>
|
||
// <code>
|
||
// SyncDiceCommand(gadgetUId, StartShake=1) -> DoTransitionToShakeState -> shakeState
|
||
// SyncDiceCommand(gadgetUId, OpenBox=2) -> DoTransitionToAfterShakeState -> afterShakeState
|
||
// SyncDiceCommand(gadgetUId, SkipToSettle=6) -> -> roundSettleState
|
||
// 再发 StartShake -> 自动换手(roundPlayer 从 DicePlayer 变 RemotePlayer)
|
||
// </code>
|
||
// <para>
|
||
// ★★ 最关键的一条:命令的 targetUid 是 gadgetUId(玩法区实体),
|
||
// 不是玩家 uid、也不是对手 uid。发给 uid=0 或对手 uid 时客户端只收包不推进状态,
|
||
// 只有发 gadgetUId 才会真的推进状态机。这个坑排查了很久。
|
||
// </para>
|
||
// <para>
|
||
// ⛑ 另外别信 lastReadyDiag:那是某一时刻的一次性快照,对局中不会再刷新。
|
||
// 实测 main.isMotionInit 已经是 true、op.isSkillInit 手动置 true 之后,
|
||
// 这条 diag 字符串一个字都没变。真正的实时状态看 nowState / roundPlayer。
|
||
// </para>
|
||
//
|
||
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
|
||
}
|
||
|
||
//
|
||
// 一手得分。
|
||
// <list type="bullet">
|
||
// <item>Simple(mode=1):返回**牌型等级** DiceHandRank(0..8),本回合双方比大小。</item>
|
||
// <item>Classic(mode=0):返回**原版计分** DiceBasicScore(点数分 × 组合倍数),累加进总分。</item>
|
||
// </list>
|
||
// <para>
|
||
// ★ 2026-09-27 从 7k-v0.5 移植:那一版的 DiceBasicScore / DiceHandRank 与
|
||
// DiceGame_Rules.md(frida 挂客户端读 DiceHandTypeCalculator 的实测值)逐项吻合,
|
||
// 比原先的「五颗点数之和」占位规则更接近原版。客户端 BaseDicePlayer 的
|
||
// nowRoundScore 等字段在服务端不下发时恒为 0 ⇒ 分数仍由我们当权威,改算法不影响链路。
|
||
// </para>
|
||
//
|
||
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 新增,用户要求「游戏结束后清除」)。
|
||
// <para>★ 两条通道都发:static NPC 没有专属的 *StaticNpcRemove
|
||
// (MethodId 表里只有 AddData / ConvertToPe / VehicleRemove),
|
||
// 而 SyncAetherAINpcRemove(68332284) 管的是 ambient peds / functional NPC 那本账,
|
||
// 我们对不对得上无法离线判定 ⇒ 两条都发,哪条生效算哪条(多发一条无效包客户端是忽略的)。</para>
|
||
// <para>⛑⛑ 服务端账本必须一起清:StaticNpcPoses / StaticNpcPreparedPlotEvents
|
||
// 留着的话,玩家走远再回来触发重扫(每 120s 全量重扫)会把它再摆回来。</para>
|
||
// <para>⛑ :传了就只撤这一个 uid(对局主机用,防止旧局收尾
|
||
// 误删新局 NPC);不传就撤「当前对手」(玩家主动离开时用)。
|
||
// 只有「要撤的正好还是当前对手」时才清 st.OpponentUid 指针。</para>
|
||
//
|
||
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
|
||
)
|
||
|