package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // ★★ GATE Lua 通道:在客户端进程里执行任意 Lua(2026-09-30 新增)。 // 原理(照 v0.5-2 捐赠端 GameRouter.LixiPolice.cs:378-382 的注释): // // 4229938 MasterToClientImpl.SyncDoToLuaMaster (0x1281DDE0) 调用内置的 // gCsToLuaHandler.GmRunLua —— 注册在 **Gate** 上 (0x14883B90)。 // // ⇒ 客户端本来就自带「服务端叫它跑一段 Lua」的能力,只是必须发在 // GATE 那条连接上,发在 game 连接上客户端不认。 // 主线此前缺的不是链路,是调用: // PrivateServerApplication.cs:21-23 已经建好 GateSessionHub 并注入两个 router, // LoginGateRouter.cs:114 每次登录都 _gateSessions.Touch() // (日志 [GATE] authenticated session registered), // GameRouter.cs:14 也拿到了 _gateSessions —— 但全仓零使用。 // 这里补上「绑定 + 下发」两步。 // ⛑ 三个必须遵守的坑(都是捐赠端踩出来的): // // 必须在 Gate 会话上发 SyncDoToLuaMaster(45437825); // 客户端 GmUtils.ParseUlongToLua(0x14677AC0) 用 // ulong.new(low32, high32) ⇒ uid 必须拆成两个 32 位传, // 直接写十进制数字会被 float64 四舍五入(8300000000000000001 这种 id 必炸); // 脚本一律 pcall 包裹 + print_error,否则报错静默, // 且客户端有 print_debug / print_error 可直接写进 Player.log 做回执。 // // ⛑ 主线 TcpSession 没有 ClosedToken(捐赠端 13 处都在用), // 这里统一用 Client.Connected + CancellationToken.None。 // // // 在 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)。 // ⛑⛑ payload mark 必须用 44(ULongPayload)。捐赠端用的是 42(StringPayload), // 但主线 StoryVehicleCodec4229938.ParsePayload 只认 5/6/12/44/35/36/37, // 遇到别的 mark 会 throw InvalidDataException ⇒ 整条命令在 // commands.Add 之前就被丢弃 ⇒ 服务端什么都看不到。 // 44 还能顺带把 uid 原样带回来,正好用。 // 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。 // 这是 GATE Lua 通不通的唯一硬判据:不依赖 print_debug // 到底往不往 Player.log 写,也不依赖按钮有没有显示出来。 // 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 } // // ★★ 修「盘查面板里点任何选项都没反应」。 // 根因(2026-10-01 实机日志取证)——客户端原生 // LX6/Manager/GamePlay/PoliceJob/PoliceExamineManager.lua 的两个入口都有反编译翻错: // // :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) // // ⛑⛑ 这个文件是 luajit 反编译产物,**比较符整体不可信**(~=/==、 // <=/>= 都翻过)。别照抄它的判断,直接换掉函数本体。 // 日志佐证:[DRMK-EXAM] ClickOption_Story optionId=8 unit=true … block=nil, // unit=true 正好命中第二处早退条件。 // ⛑ 面板的唯一点击入口是 PoliceInquiryPanelStore:OnListBtnClick // (self.mgr:ClickOption(itemData.optionCfgId),self.mgr = gPoliceJobManager.examineMgr) // ⇒ **必须换 ClickOption 本身**,只换 ClickOption_Story 有可能整条链都走不到。 // 两个都换,双保险。 // 修法:真的调 cs:ClickOption_Story,同时把 optionId 回执给服务端; // 服务端再推 SyncPoliceExamReaction(64480222)(主线注释里"唯一能让面板有反应的帧")。 // ⛑ 必须先写 self.PreOptionId —— 客户端 OnReceiveReaction 靠它取 option 配置。 // // // ★★ 直接把「对方反应」喂给客户端 —— 绕开 SyncPoliceExamReaction(64480222)。 // ⛑⛑ 为什么不能用那条 RPC(2026-10-01 15:0x 实机取证): // // 客户端 RPCDeserializeAuto.lua:350 把它标成 LuaOnly,也有 reader(:3281), // 但**全仓没有任何 Lua 分发器**会把它交给 PoliceJobManager.OnReceiveReaction_Story; // 官方链路里 OnReceiveReaction_Story 是**被 L50 story(S002) 调**的 // (捐赠端 StoryS002Codec4229938.Reaction(child, section, reaction)), // 主线没有 S002 codec ⇒ 这条 RPC 发了等于石沉大海。 // 实机证据:推 reaction 后 Player.log 里 [DRMK] OnReceiveReaction **一行都没有**。 // // ⇒ 反正 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)。 // 捐赠端注释的要点:调**原始** PrepareData_Story(就像 // S002PlayerNet.EnterExam 做的那样),不用自造追问都不拷南瓜 WM 的替身函数。 // 这是"盘查面板里所有选项点了没反应"的核心补救: // 面板 UI 出得来,但 gPoliceJobManager.examineMgr.enterExamData 是空的, // 所以每个选项都点不动。 // 回执 DrmkExamPrepared 的 payload:1=ok,0=failed。 // 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"` }