159 lines
10 KiB
C#
159 lines
10 KiB
C#
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(); }
|
|||
|
|
}
|
|||
|
|
}
|