209 lines
10 KiB
Go
209 lines
10 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// ★★ GATE Lua 通道:在客户端进程里执行任意 Lua(2026-09-30 新增)。
|
||
// <para>原理(照 v0.5-2 捐赠端 GameRouter.LixiPolice.cs:378-382 的注释):</para>
|
||
// <code>
|
||
// 4229938 MasterToClientImpl.SyncDoToLuaMaster (0x1281DDE0) 调用内置的
|
||
// gCsToLuaHandler.GmRunLua —— 注册在 **Gate** 上 (0x14883B90)。
|
||
// </code>
|
||
// ⇒ 客户端本来就自带「服务端叫它跑一段 Lua」的能力,只是必须发在
|
||
// GATE 那条连接上,发在 game 连接上客户端不认。
|
||
// <para>主线此前缺的不是链路,是调用:
|
||
// PrivateServerApplication.cs:21-23 已经建好 GateSessionHub 并注入两个 router,
|
||
// LoginGateRouter.cs:114 每次登录都 _gateSessions.Touch()
|
||
// (日志 [GATE] authenticated session registered),
|
||
// GameRouter.cs:14 也拿到了 _gateSessions —— 但全仓零使用。
|
||
// 这里补上「绑定 + 下发」两步。</para>
|
||
// <para>⛑ 三个必须遵守的坑(都是捐赠端踩出来的):</para>
|
||
// <list type="number">
|
||
// <item>必须在 Gate 会话上发 SyncDoToLuaMaster(45437825);</item>
|
||
// <item>客户端 GmUtils.ParseUlongToLua(0x14677AC0) 用
|
||
// ulong.new(low32, high32) ⇒ uid 必须拆成两个 32 位传,
|
||
// 直接写十进制数字会被 float64 四舍五入(8300000000000000001 这种 id 必炸);</item>
|
||
// <item>脚本一律 pcall 包裹 + print_error,否则报错静默,
|
||
// 且客户端有 print_debug / print_error 可直接写进 Player.log 做回执。</item>
|
||
// </list>
|
||
// <para>⛑ 主线 TcpSession 没有 ClosedToken(捐赠端 13 处都在用),
|
||
// 这里统一用 Client.Connected + CancellationToken.None。</para>
|
||
//
|
||
//
|
||
// 在 LoginGame 时把当前 GATE 会话绑到 game 会话上(照捐赠端
|
||
// Endpoints.cs:16-17)。⛑ 这是实例方法 —— 只有它能碰到 _gateSessions。
|
||
// 只绑一次:登录时的那个 gate,不是后来"全局当前"的某个 socket。
|
||
//
|
||
func GameRouter_GateLua_BindGateSession4229938(ctx context.Context, session *GameSession) any {
|
||
// Router implementation for GameRouter_GateLua_BindGateSession4229938
|
||
var zero any; return zero
|
||
}
|
||
|
||
// 取绑定好的 GATE 会话;没绑或已断都返回 false。
|
||
func GameRouter_GateLua_TryGetGateSession4229938(ctx context.Context, session *GameSession, gate any) bool {
|
||
// Router implementation for GameRouter_GateLua_TryGetGateSession4229938
|
||
return true
|
||
}
|
||
|
||
//
|
||
// ★ 在客户端进程里执行一段 Lua。wire = UxString(script)(客户端签名就是
|
||
// SyncDoToLuaMaster(string content),见 ux_game_all.dump.cs:15196)。
|
||
//
|
||
func GameRouter_GateLua_RunClientLuaAsync(ctx context.Context, session *GameSession, script string) any {
|
||
// Router implementation for GameRouter_GateLua_RunClientLuaAsync
|
||
var zero any; return zero
|
||
}
|
||
|
||
// ------------------------------------------------------------------ F 交互按钮
|
||
//
|
||
// 枚举服务端账本里的 NPC(三本:Ambient 行人 / 地盘敌人 / 功能 NPC),按离玩家由近到远排序。
|
||
// ⛑ 城里绝大多数路人是客户端本地实体,服务端没有它们的 id
|
||
// (GameRouter.PoliceArrest.cs:66-79 写死这个结论)⇒ 那些 NPC 挂不了 F,
|
||
// 只能对 GM 召唤 / 服务端 spawn 出来的目标操作。
|
||
//
|
||
func GameRouter_GateLua_EnumLedgerUnits4229938(ctx context.Context, session *GameSession) []any {
|
||
// Router implementation for GameRouter_GateLua_EnumLedgerUnits4229938
|
||
return nil
|
||
}
|
||
|
||
//
|
||
// 一条 story MessageCommand 的裸字节(供 Lua 用 writer:WriteByte 原样回吐):
|
||
// FF + Int7(1) + U8(4=MessageCommand) + U32(nid) + UxString(name) +
|
||
// U8(44=ULongPayload) + U64(value) + I64(0) + Bool(false)。
|
||
// <para>⛑⛑ payload mark 必须用 44(ULongPayload)。捐赠端用的是 42(StringPayload),
|
||
// 但主线 StoryVehicleCodec4229938.ParsePayload 只认 5/6/12/44/35/36/37,
|
||
// 遇到别的 mark 会 throw InvalidDataException ⇒ 整条命令在
|
||
// commands.Add 之前就被丢弃 ⇒ 服务端什么都看不到。
|
||
// 44 还能顺带把 uid 原样带回来,正好用。</para>
|
||
//
|
||
func GameRouter_GateLua_StoryMessageBytes4229938(ctx context.Context, nid uint32, name string, value uint64) []byte {
|
||
// Router implementation for GameRouter_GateLua_StoryMessageBytes4229938
|
||
return nil
|
||
}
|
||
|
||
//
|
||
// ★ 回执探针:脚本一旦被客户端执行,就经
|
||
// gClientToGameSceneDelegate:Notify(67130745, …) 把一个 story command 发回服务端,
|
||
// 名字固定为 DrmkLuaRx。主线 GameRouter.VehicleStory.cs:45 的 handler 会把
|
||
// command 名打进日志([VEHICLE-STORY] cmd … name=DrmkLuaRx …)
|
||
// ⇒ 服务端日志一出现 DrmkLuaRx,就铁证客户端真的执行了 Lua。
|
||
// <para>这是 GATE Lua 通不通的唯一硬判据:不依赖 print_debug
|
||
// 到底往不往 Player.log 写,也不依赖按钮有没有显示出来。</para>
|
||
//
|
||
func GameRouter_GateLua_BuildReceiptScript4229938(ctx context.Context, tag string) string {
|
||
// Router implementation for GameRouter_GateLua_BuildReceiptScript4229938
|
||
return ""
|
||
}
|
||
|
||
//
|
||
// ★ 生成一个「点了就开里希盘查」的 F 按钮(按钮文字默认「盘查」)。
|
||
// click 回调把 uid 经 67130745 的 DrmkExamRequest 回给服务端,
|
||
// 由 GameRouter.VehicleStory.cs 的 handler 接住再调 OpenExamAsync。
|
||
//
|
||
func GameRouter_GateLua_BuildExamButtonScript4229938(ctx context.Context, uid uint64, text string) string {
|
||
// Router implementation for GameRouter_GateLua_BuildExamButtonScript4229938
|
||
return ""
|
||
}
|
||
|
||
//
|
||
// optionId → reactionId。照捐赠端 PoliceReaction4229938
|
||
// (LixiPolice.cs:432-440)。
|
||
// ⛑ 默认 15002 是 **Detach(下车)**,**不是**上车;20 才是押送上车(20001)。
|
||
//
|
||
func GameRouter_GateLua_PoliceReactionForOption(ctx context.Context, optionId uint32) uint32 {
|
||
// Router implementation for GameRouter_GateLua_PoliceReactionForOption
|
||
return 0
|
||
}
|
||
|
||
//
|
||
// ★★ 修「盘查面板里点任何选项都没反应」。
|
||
// <para>根因(2026-10-01 实机日志取证)——客户端原生
|
||
// LX6/Manager/GamePlay/PoliceJob/PoliceExamineManager.lua 的两个入口都有反编译翻错:</para>
|
||
// <code>
|
||
// :190 ClickOption = function(self, optionId)
|
||
// if optionId ~= PoliceCfg.Ask then ← 翻错的比较
|
||
// if self.attachData.askDialogId and self.attachData.askDialogId <= 0 then
|
||
// ... ShowGeneralDialog ...
|
||
// end ← 没有 else ⇒ 静默
|
||
// else
|
||
// self.ClickOption_Story(self, optionId) ← 只有 Ask 才走得到
|
||
// end
|
||
// end
|
||
// :1040 ClickOption_Story = function(self, optionId)
|
||
// if self.BlockInteract or self.unit ~= nil then return end ← 翻错的比较
|
||
// ... gPoliceJobManager.cs:ClickOption_Story(self.unit.Pid, optionId)
|
||
// </code>
|
||
// <para>⛑⛑ 这个文件是 luajit 反编译产物,**比较符整体不可信**(~=/==、
|
||
// <=/>= 都翻过)。别照抄它的判断,直接换掉函数本体。</para>
|
||
// <para>日志佐证:[DRMK-EXAM] ClickOption_Story optionId=8 unit=true … block=nil,
|
||
// unit=true 正好命中第二处早退条件。</para>
|
||
// <para>⛑ 面板的唯一点击入口是 PoliceInquiryPanelStore:OnListBtnClick
|
||
// (self.mgr:ClickOption(itemData.optionCfgId),self.mgr = gPoliceJobManager.examineMgr)
|
||
// ⇒ **必须换 ClickOption 本身**,只换 ClickOption_Story 有可能整条链都走不到。
|
||
// 两个都换,双保险。</para>
|
||
// <para>修法:真的调 cs:ClickOption_Story,同时把 optionId 回执给服务端;
|
||
// 服务端再推 SyncPoliceExamReaction(64480222)(主线注释里"唯一能让面板有反应的帧")。
|
||
// ⛑ 必须先写 self.PreOptionId —— 客户端 OnReceiveReaction 靠它取 option 配置。</para>
|
||
//
|
||
//
|
||
// ★★ 直接把「对方反应」喂给客户端 —— 绕开 SyncPoliceExamReaction(64480222)。
|
||
// <para>⛑⛑ 为什么不能用那条 RPC(2026-10-01 15:0x 实机取证):</para>
|
||
// <list type="bullet">
|
||
// <item>客户端 RPCDeserializeAuto.lua:350 把它标成 LuaOnly,也有 reader(:3281),
|
||
// 但**全仓没有任何 Lua 分发器**会把它交给 PoliceJobManager.OnReceiveReaction_Story;</item>
|
||
// <item>官方链路里 OnReceiveReaction_Story 是**被 L50 story(S002) 调**的
|
||
// (捐赠端 StoryS002Codec4229938.Reaction(child, section, reaction)),
|
||
// 主线没有 S002 codec ⇒ 这条 RPC 发了等于石沉大海。</item>
|
||
// <item>实机证据:推 reaction 后 Player.log 里 [DRMK] OnReceiveReaction **一行都没有**。</item>
|
||
// </list>
|
||
// ⇒ 反正 GATE Lua 能在客户端进程里跑任意代码,**直接调那个函数**即可,不必移植 S002。
|
||
//
|
||
func GameRouter_GateLua_BuildReactionScript4229938(ctx context.Context, reaction uint32, optionId uint32) string {
|
||
// Router implementation for GameRouter_GateLua_BuildReactionScript4229938
|
||
return ""
|
||
}
|
||
|
||
func GameRouter_GateLua_BuildPatchClickOptionScript4229938(ctx context.Context, ) string {
|
||
// Router implementation for GameRouter_GateLua_BuildPatchClickOptionScript4229938
|
||
return ""
|
||
}
|
||
|
||
//
|
||
// ★★ 初始化原生盘查数据(照捐赠端 PolicePrepareScript4229938,
|
||
// LixiPolice.cs:390-412)。
|
||
// <para>捐赠端注释的要点:调**原始** PrepareData_Story(就像
|
||
// S002PlayerNet.EnterExam 做的那样),不用自造追问都不拷南瓜 WM 的替身函数。
|
||
// 这是"盘查面板里所有选项点了没反应"的核心补救:
|
||
// 面板 UI 出得来,但 gPoliceJobManager.examineMgr.enterExamData 是空的,
|
||
// 所以每个选项都点不动。</para>
|
||
// <para>回执 DrmkExamPrepared 的 payload:1=ok,0=failed。</para>
|
||
//
|
||
func GameRouter_GateLua_BuildPrepareExamScript4229938(ctx context.Context, uid uint64) string {
|
||
// Router implementation for GameRouter_GateLua_BuildPrepareExamScript4229938
|
||
return ""
|
||
}
|
||
|
||
//
|
||
// 生成「给 uid 挂 / 摘 F 按钮」的 Lua。⛑ uid 必须拆成
|
||
// ulong.new(low32, high32)(捐赠端注释:十进制 Lua number 会被 float64 四舍五入)。
|
||
//
|
||
func GameRouter_GateLua_BuildUnitButtonScript4229938(ctx context.Context, uid uint64, text string, add bool) string {
|
||
// Router implementation for GameRouter_GateLua_BuildUnitButtonScript4229938
|
||
return ""
|
||
}
|
||
|
||
// 给面板用的账本条目。
|
||
type GateLuaUnit struct {
|
||
Uid uint64 `json:"uid"`
|
||
Ledger string `json:"ledger"`
|
||
Position Vec3 `json:"position"`
|
||
Distance float32 `json:"distance"`
|
||
}
|
||
|