Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.PoliceButtons.cs

160 lines
10 KiB
C#
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.
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") | <tag> | 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(); }
}
}