Files

159 lines
10 KiB
C#
Raw Permalink Normal View History

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(); }
}
}