using Ananta.SDK.Network; using Ananta.SDK.Serialization; using Ananta.Server.Protocol.Client4229938; namespace Ananta.Server.Handlers.Game; internal sealed partial class GameRouter { // Source Interrogate=7. Reserve an index outside the authored table/task // button ranges. The exported RemoveBtnByType calls each GetRealIndex with // this ORIGINAL index (not 8770512). Never remove the entire NPC button list. private const int PoliceButtonIndex4229938 = 8700512; private static async Task SendPoliceButtons4229938(TcpSession session, ulong[] ids, ulong owner, bool add, bool releasePassenger = false) { // ⛔⛔ 2026-10-02 18:4x 已回滚(实验结论):**注入的这条按钮就是玩家看到的 F**。 // 证据:对比两个窗口的 S2C —— 04:30:24 出过 `F interaction tag=19` 的窗口里 // 有 `SyncDoToLuaMaster` ×2(= 本脚本);18:36 关掉注入后该包为 0 且全程无 F。 // 且 04:30 那条 `F interaction` 与捐赠端 16:43:12 是**同一条服务端日志** ⇒ // 主线的注入按钮本来就能走通和捐赠端一样的入口。 // ⇒ 关掉注入 = 保证没有 F,此路不通。不要再关。 // ⛑ 原生 F 另有客户端 lua 门禁且带官方 bug(见 2026-10-02.md §56),不可依赖。 if (add && PoliceRuntimes.TryGetValue(session, out var prison) && prison.Detained != 0) ids = ids.Where(id => id != prison.Detained).ToArray(); // ★★ 2026-10-02 18:1x(用户提出的假设,已实证支持): // 押送中的 NPC 靠近警车时,**客户端原生**就有「押送上车」(按 X)。 // 而我们注入的假 F 按钮通过 InteractBtnMgr:AddBtn / RemoveBtnByType 落地 —— // 脚本注释自承 "AddBtn/RemoveBtnByType already invalidate the native list", // 即每次刷新都会让客户端的**原生按钮列表失效** ⇒ 原生菜单被顶掉 ⇒ X 不出现。 // ⛑ 致命时机:interaction=19(拘捕/押送开始)之后 InteractEnd 会走到 // LixiPolice.cs:667 重新 AddBtn —— 正好在玩家走到车边要按 X 的时刻。 // ⇒ 只要 NPC 处于「步行押送中」,就不再给它挂注入按钮,把原生菜单让出来。 // 判据:最近一次交互是 19(拘捕),且既不在暂停押送、也还没上车。 if (add && PoliceRuntimes.TryGetValue(session, out var escort) && escort.LastInteraction == 19) { var before = ids.Length; // 只摘掉「正在步行押送中」的那一个;暂离(18)与已上车(Passengers)仍保留按钮, // 别的 NPC 不受影响。 ids = ids.Where(id => id != escort.Agent || escort.PausedEscorts.Contains(id) || escort.Passengers.ContainsKey(id)).ToArray(); if (ids.Length != before) session.Log.Info($"[LIXI-POLICE] F button suppressed for escorted agent={escort.Agent}; native menu handed back (X = 押送上车)"); } if (ids.Length == 0 || !session.Items.TryGetValue(PoliceGateSessionKey4229938, out var value) || value is not TcpSession gate || gate.ClosedToken.IsCancellationRequested) return; foreach (var batch in ids.Distinct().Chunk(24)) { var w = new UxWriter().UxString(PoliceButtonsScript4229938(batch, owner, add, releasePassenger)); await gate.NotifyAsync(MethodId.SyncDoToLuaMaster, w.ToArray(), session.ClosedToken); } session.Log.Info($"[LIXI-POLICE] independent F buttons {(add ? "sent" : "removed")} count={ids.Length}; purchase buttons preserved; awaiting client verification"); } internal static string PoliceButtonsScript4229938(ulong[] ids, ulong owner, bool add, bool releasePassenger = false) { static string U64(ulong id) => FormattableString.Invariant($"ulong.new({(uint)id}, {id >> 32})"); // ★★ 2026-10-02 20:4x: 这条按钮原本发的是 PoliceFRequest (52b, GATE 私有消息), // 服务端接住后跑 PrepareData_Story 握手, 最终以 ServerInitiate=true 建 S002 节点。 // 捐赠端成功那次客户端发的是**原生** NpcInteract (38b), 字节布局见 2026-10-02.md §59.2: // FF | Int7(1) | 04 | nid u32 | UxString("NpcInteract") | | AgentId u64 | [StartInteract bool] | I64(0) | Bool(false) // 只有 GameRouter.LixiPolice.cs:593-643 这条分支会打出 `F interaction ... wire=escort-entry-r7`, // 而主线全历史该日志 = 0 次 ⇒ 原生盘查入口从未跑过 ⇒ 面板缺「押送上车」(interaction=20)。 // tag: 19=EnterExamRequest (开盘查) / 20=EscortFromVehicleRequest (押送下车)。 // 长度自校:tag19 => 38b、tag20/21 => 37b,与捐赠端实测包长逐一吻合。 // 回滚: cp GameRouter.PoliceButtons.cs.bak-20261002-nativef。 static string Request(ulong id, byte tag) { var w = new UxWriter().U8(255).Int7(1).U8(4).U32(PoliceRootNid4229938) .UxString(StoryS002Codec4229938.NpcInteract).U8(tag); // payload 形状随 tag 变: 19 = {AgentId u64, StartInteract bool};20/21 = {AgentId u64}。 w.U64(id); if (tag == StoryS002Codec4229938.EnterExamRequestTag) w.Bool(true); w.I64(0).Bool(false); return string.Join(",", w.ToArray()); } byte tag = releasePassenger ? StoryS002Codec4229938.EscortFromVehicleRequestTag : StoryS002Codec4229938.EnterExamRequestTag; var entries = string.Join(",\n", ids.Distinct().Select(id => $"{{pid={U64(id)}, bytes={{{Request(id, tag)}}}}}")); return $$""" local ok, err = pcall(function() local mgr = L50.L50App.L50Game.InteractBtnMgr local source = 7 local owner = {{U64(owner)}} local entries = { {{entries}} } for _, entry in ipairs(entries) do local npc = gCS.SceneDataMgr.GetUnit(entry.pid) if npc then mgr:RemoveBtnByType(entry.pid, LX6.Interact.BtnTargetType.Unit, {{PoliceButtonIndex4229938}}) if {{(add ? "true" : "false")}} then local function visible() local player = gCS.MyPlayerManager.PlayerUnit return player ~= nil and player.Pid == owner and gPoliceJobManager ~= nil and gCS.SceneDataMgr.GetUnit(entry.pid) == npc end -- ★ 2026-10-02 21:1x: 原生 F 的客户端前置步骤。 -- 捐赠端按原生 F 时,客户端本地先跑 PrepareData_Story(pid) 建 examineMgr.enterExamData; -- 随后 C# 的 SInteract 在 story 节点 OnEnable 里调 OpenPanel_Story(pid),它第一行就索引 enterExamData.npcTargetDir。 -- 我们这条是合成的按钮,本地没跑过 Prepare ⇒ enterExamData 为 nil -- ⇒ 盘查动画播了、面板静默失败(2026-10-02 21:0x 实机)。 -- 顺序与 Drmk.Proxy/proxy/tools/police_examine.lua:389 (OpenExam) 一致:Prepare 在前,建节点仍由服务端 NpcInteract 分支完成。 -- ⛑ 不要自己 CheckShow: 那样弹出的是不挂 story 的纯 UI 面板,按钮不会发 InteractionMessage。 local function click() if not visible() then return end pcall(function() gPoliceJobManager:PrepareData_Story(entry.pid) end) -- 兜底: Prepare 没建出来时补一个最小壳,否则 OpenPanel_Story 读 npcTargetDir 会炸。 local em = gPoliceJobManager.examineMgr if type(em) == 'table' then if type(em.enterExamData) ~= 'table' then em.enterExamData = { npcUnit = npc } elseif em.enterExamData.npcUnit == nil then em.enterExamData.npcUnit = npc end end gClientToGameSceneDelegate:Notify(67130745, function(writer) for i = 1, #entry.bytes do writer:WriteByte(entry.bytes[i]) end end) end local btn = LX6.Interact.UnitBtnInfo.New(source, entry.pid, {{PoliceButtonIndex4229938}}, click, visible, 0, 0) btn.exText = "{{(releasePassenger ? "押送下车" : "里希盘查")}}" btn.Dis = 3 btn.pcHideText = false mgr:AddBtn(btn) end end end -- AddBtn/RemoveBtnByType already invalidate the native list. -- SetBtnDirty and UnitBtnInfo's C# movement properties are NOT -- exposed in this build's Lua wrapper: do not call them here. end) if not ok then print_error("[LIXI-POLICE] independent F button failed: " .. tostring(err)) end """; } private static async Task UnregisterPoliceNpc4229938(TcpSession session, ulong id) { if (!PoliceRuntimes.TryGetValue(session, out var r)) return; await r.Gate.WaitAsync(session.ClosedToken); try { await ReleasePolicePassenger4229938(session, r, id, besidePlayer: false); if (r.Detained == id) { r.Detained = 0; r.PrisonCorrectionAt = 0; await SendPolicePrisonButtons4229938(session, r, true); } if (!r.Npcs.Remove(id)) return; r.PausedEscorts.Remove(id); await SendPoliceButtons4229938(session, [id], 0, false); if (r.PrepareTarget == id) r.PrepareNonce = null; if (r.Agent == id) await ClosePoliceInteraction4229938(session, r, "NPC retired"); } finally { r.Gate.Release(); } } }