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

353 lines
17 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
//
// 骰子(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
)