Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.PoliceArrest.cs
T

490 lines
25 KiB
C#
Raw 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 System.Text;
using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.Configuration;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.RpcTypes.Client4229938;
using PoliceMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 巡卫官(里希 / 15021021)正向「逮捕 NPC」—— 与 <c>GameRouter.Wanted.cs</c> 那条反向链路
/// (玩家犯罪 → 警车追 → 警车抓玩家)共用同一个 S2C,只是把 target 从玩家换成 NPC。
///
/// <code>
/// SyncPoliceBeginArrest(68839124) = { u64 agentId(执行者), u64 targetPid(被捕者), byte ArrestType }
/// </code>
/// 反向链路里 agentId 是<b>警车</b>的 entity id、targetPid 是 <c>Profile.PlayerPid</c>、
/// ArrestType=1(<c>GameRouter.Wanted.cs:723</c>)。正向这里 agentId 换成玩家自己的 unit id。
///
/// ⛑⛑ <b>能确定与不能确定的,分开写清楚:</b>
/// · 能确定:68839124 是 LuaOnly、有 lua reader(<c>RPCDeserializeAuto.lua:6986</c>),
/// 我方 Wanted.cs 已经在发且客户端吃过(玩家被捕演出成立)⇒ 帧本身是通的。
/// · 不能确定:<b>客户端什么时候会发盘查/逮捕的 C2S</b>。盘查面板与审讯一样挂在
/// <c>gPoliceJobManager.cs</c>(C#)与 Spoon 上;<c>AskPoliceExamInteract</c>(63233964) 在 lua 里
/// 零引用,<c>RpcSurface.json</c> 里也没有 ⇒ 形状未知、触发时机未知。
/// ⇒ 所以本文件做的是「把入口全都接住 + 留取证口子」,<b>不假设客户端一定会发</b>。
///
/// 三条取证口子(实机跑一次就有答案):
/// 1. <c>[POLICE-ARREST] exam-interact hex=…</c> —— 盘查选项 RPC 的真实字节;
/// 2. <c>[POLICE-ARREST] interact-cmd hex=…</c> —— 按 F 交互那条 72 字节 NOTIFY 的内容(前 5 次);
/// 3. GM 面板 / <c>POST /api/police/arrest</c> —— 不等客户端,直接验证演出帧能不能出。
///
/// ⛑ 硬规则 94:写完 [Handler] 还要去 <c>GameRouter.cs</c> 的 <c>Enabled4229938MethodIds</c>
/// 登记,否则编译通过、功能零生效,而这些都是 INVOKE(客户端会 await 到超时)。
/// </summary>
internal sealed partial class GameRouter
{
// ---------------------------------------------------------------------------------
// 常量
// ---------------------------------------------------------------------------------
private const string PoliceArrestStateKey = "client4229938-police-arrest";
/// <summary>
/// 与反向链路保持一致的 ArrestType=1。正向若不对,改 <c>gameplay.policeArrest.type</c> 试 0/2,
/// 不用重新编译。
/// </summary>
private const byte PoliceArrestTypeDefault = 1;
/// <summary>hex 取证最多打印的字节数。</summary>
private const int ArrestHexLogBytes = 48;
private sealed class PoliceArrestState
{
/// <summary>本局已逮捕次数(喂 <c>AskGetArrestTimes</c>)。</summary>
public int Count;
/// <summary>最近一次被捕目标的 uid(同时当 pid 用,见 <see cref="SendPoliceArrestAsync"/>)。</summary>
public ulong LastTarget;
}
// =================================================================================
// ⓪ 「抓谁?」—— 服务端账本里的可抓目标清单
//
// 2026-09-25 用户提问的根因在这里,先写死在代码里免得下次再踩:
// **城里绝大多数路人是客户端本地实体,服务端根本没有它们的 id。**
// 实据:AskInteractCmd CmdType=33(目标选择)每秒回报 3~4 次,s1 形如
// `8300000000000000188`(= 0x732F860653BE00BC),既不在我方 uid 段
// (0x200000000000 + ServerSpawnUidFloor 起)也不连续 ⇒ 客户端自己随机分配的单位。
// 这类目标服务端<b>无法点名</b>(没有稳定 id,且下一秒就换了一个)。
//
// 能抓的只有<b>服务端自己生成并推过 SyncManagedLogicAgent / SyncLogicAgentEnter 的单位</b>:
// · Ambient 行人(GameRouter.Ambient 的 Peds 账本,带服务端坐标)
// · 地盘敌人(EnemyTerritory Active:uid -> spot)
// · 功能 NPC(FunctionalNpc Active:uid -> spot)
// · 最近一次 GM 召唤的 NPC(state.LastSummonedNpcEntityId,无坐标,按 0 距离排最前)
// ⇒ 所以这里不做「对着路人按 F」,而是把账本摊开、按到玩家的距离排序点名。
// =================================================================================
/// <summary>一个可抓目标。<c>Distance</c> 为米;-1 = 服务端不知道它在哪(GM 召唤的那个)。</summary>
internal readonly record struct PoliceTarget(
ulong Uid,
string Kind,
string Label,
float Distance,
float X,
float Y,
float Z);
/// <summary>
/// 汇总当前会话里服务端已知的全部可抓单位,按到玩家的距离升序。
///
/// ⛑ 三个账本的 state 方法有各自的 SyncRoot,逐个 lock 取快照,不在锁里做 I/O。
/// ⛑ 玩家自己的单位(<c>ActiveSpiritUnitId</c> / <c>Profile.InitialUnitId</c>)会被剔除 ——
/// 自己抓自己会走到「玩家被捕」那条演出上,与正向逮捕完全相反。
/// </summary>
internal static IReadOnlyList<PoliceTarget> CollectPoliceTargets(TcpSession session, int max = 24)
{
var list = new List<PoliceTarget>();
var st = GetStateIfExists(session);
float px = 0, py = 0, pz = 0;
ulong selfUnit = 0;
var selfIds = new HashSet<ulong>();
if (st is not null)
{
lock (st.SyncRoot)
{
px = st.LastReportedPlayerPosition.X;
py = st.LastReportedPlayerPosition.Y;
pz = st.LastReportedPlayerPosition.Z;
selfUnit = st.ActiveSpiritUnitId != 0 ? st.ActiveSpiritUnitId : Profile.InitialUnitId;
// ⛑ 2026-09-25 实机:玩家「实际」单位常常是 InitialUnitId+10(100000000010),
// 而 ActiveSpiritUnitId 还是 InitialUnitId(100000000000)⇒ 只比 selfUnit 一个值
// 会把玩家自己列成「瞄准」目标。这里把两个都算作自己。
selfIds.Add(selfUnit);
selfIds.Add(Profile.InitialUnitId);
if (st.ActiveSpiritUnitId != 0) selfIds.Add(st.ActiveSpiritUnitId);
if (st.LastSummonedNpcEntityId != 0 && !selfIds.Contains(st.LastSummonedNpcEntityId))
{
list.Add(new PoliceTarget(st.LastSummonedNpcEntityId, "召唤", "最近召唤的 NPC", -1f, 0, 0, 0));
}
}
}
// ⓿ 客户端「当前瞄准的单位」—— AskInteractCmd CmdType=33 的 stringParam1,
// 由 GameRouter.Vehicle.cs:280 填进 _lastInteractTargetUnitId。
// ⛑ 实测这些 id 形如 8300000000000000188(0x732F860653BE00BC),是<b>客户端本地实体</b>,
// 服务端没有它的账本 ⇒ 抓不抓得到得实机验。列出来是为了让「我到底选中了谁」可见。
var aim = s_lastInteractTargetUnitId; // 静态镜像,见 GameRouter.Vehicle.cs 的字段注释
if (aim != 0 && !selfIds.Contains(aim))
list.Add(new PoliceTarget(aim, "瞄准",
"客户端当前瞄准的单位(本地实体,多半抓不动)", -2f, 0, 0, 0));
void Add(ulong uid, string kind, string label, float x, float y, float z)
{
if (uid == 0 || selfIds.Contains(uid)) return;
var dx = x - px; var dy = y - py; var dz = z - pz;
var d = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
list.Add(new PoliceTarget(uid, kind, label, d, x, y, z));
}
// ① Ambient 行人 —— 服务端账本里带实时坐标的那批。
var amb = AmbientState(session);
lock (amb.SyncRoot)
{
foreach (var ped in amb.Peds)
Add(ped.Id, "行人", $"路人 #{ped.Id & 0xFFFF:X}", ped.X, ped.Y, ped.Z);
}
// ② 地盘敌人
var turf = EnemyTerritoryStateOf(session);
lock (turf.SyncRoot)
{
foreach (var (uid, spot) in turf.Active)
Add(uid, "地盘敌人", spot.AreaName, spot.X, spot.Y, spot.Z);
}
// ③ 功能 NPC(36 个点:商店 / 出租 / 手机店…)
var fn = FunctionalNpcStateOf(session);
lock (fn.SyncRoot)
{
foreach (var (uid, spot) in fn.Active)
Add(uid, "功能NPC", spot.Name, spot.X, spot.Y, spot.Z);
}
// 距离升序:-2 = 瞄准(本地实体)、-1 = GM 召唤(位置不明)排最前,其后是真·有坐标的账本单位。
return list
.OrderBy(t => t.Distance)
.ThenBy(t => t.Uid)
.Take(Math.Max(1, max))
.ToArray();
}
// =================================================================================
// ⓪b 「把人拽到面前」—— 反向解法:不挪玩家,挪 NPC
//
// 2026-09-25 用户第三次反馈「面前没 NPC」之后才想明白的:
// **挪玩家那条路是死的**(两条都验过):
// · instant(SyncUnitPositionAndFacing + SwitchSpirit):帧发了(unit=100000000000),
// 玩家没动;客户端后续上报把 LastReportedPlayerPosition 覆盖回原位。
// · loading(SyncPreTeleportOption + SyncTeleport 64030755):客户端直接反序列化失败
// —— `deserialize failed mode=3 len=35 / ReadBuffer invalid size=4 remain=3
// at ReadTeleportOption`。⛑ 我方 TeleportOption **是 30B 且契约与
// RPCDeserializeAuto.lua Auto.Reader[287]、dump.cs TypeDefIndex 33016 逐字段一致**
// (_ux_wire_check 已加断言并 PASS)⇒ 35B 说明多出来的字节不在 DTO 里,
// 而是这条帧根本不该以 notify 形态发(它有 invoke 语义)。不值得再钻。
// **挪 NPC 是活的**:SyncUnitPositionAndFacing 本来就是服务端驱动市民走路用的帧
// (GameRouter.Civilians.cs:391,WorldCodec.Walking),NPC 是 proxy 单位,
// NetworkGhostModule 只挡本地角色 ⇒ 一定能落位。
// ⇒ 所以改成:把目标 NPC 瞬移到玩家正前方 2.2 m,并让服务端账本同步改写,
// 下一次 /api/police/targets 就能看到它贴在脸上。
// =================================================================================
/// <summary>把 <paramref name="targetUid"/> 瞬移到玩家正前方 <paramref name="frontMeters"/> 米处。</summary>
internal static async Task<(bool Ok, string Message)> PullTargetToPlayerAsync(
TcpSession session, ulong targetUid, float frontMeters = 2.2f, CancellationToken token = default)
{
var st = GetStateIfExists(session);
if (st is null)
return (false, "还没有玩家会话(先用客户端进一次游戏)");
float px, py, pz, pf;
lock (st.SyncRoot)
{
px = st.LastReportedPlayerPosition.X;
py = st.LastReportedPlayerPosition.Y;
pz = st.LastReportedPlayerPosition.Z;
// ⛑ 客户端上报的是 Unity 欧拉角(度)。Civilians.cs:385 直接把度喂给 MathF.Sin 是它自己的
// 老 bug(走路方向偏),这里**不要**照抄 —— 先转弧度。
pf = st.HasLastReportedPlayerTransform ? st.LastReportedPlayerRotation.Y : Profile.WorldFacing;
}
var rad = pf * MathF.PI / 180f;
var nx = px + MathF.Sin(rad) * frontMeters;
var nz = pz + MathF.Cos(rad) * frontMeters;
var ny = py;
// NPC 回头面对玩家。
var npcFacing = (pf + 180f) % 360f;
if (npcFacing > 180f) npcFacing -= 360f;
// ① 服务端账本同步改写(否则下一次点名它还在老位置,而且服务端驱动行走会把它拽回去)
var ledger = "未命中任何账本";
var amb = AmbientState(session);
lock (amb.SyncRoot)
{
var ped = amb.Peds.FirstOrDefault(p => p.Id == targetUid);
if (ped is not null)
{
ped.X = nx; ped.Y = ny; ped.Z = nz;
ped.Walking = false; // ⛑ 不关掉的话服务端路网下一拍就把它走回去
ped.Route = null;
ledger = "行人账本已改写(并停掉服务端行走)";
}
}
if (ledger == "未命中任何账本")
{
var turf = EnemyTerritoryStateOf(session);
lock (turf.SyncRoot)
{
if (turf.Active.TryGetValue(targetUid, out var spot))
{
turf.Active[targetUid] = spot with { X = nx, Y = ny, Z = nz };
ledger = "地盘敌人账本已改写";
}
}
}
if (ledger == "未命中任何账本")
{
var fn = FunctionalNpcStateOf(session);
lock (fn.SyncRoot)
{
if (fn.Active.TryGetValue(targetUid, out var spot))
{
fn.Active[targetUid] = spot with { X = nx, Y = ny, Z = nz };
ledger = "功能NPC账本已改写";
}
}
}
// ② 落位帧 —— ⛑⛑ 两拍,照 GameRouter.Civilians.cs:391 走路循环的同款,**别用硬 snap**。
// 2026-09-25 20:34 实机:只发 `PositionAndFacing`(moveId=Unknown=0、continueMove=false)
// ⇒ 客户端 `ComponentMovement.OutputVelocity` 每帧抛 NullReferenceException(一秒好几次),
// 被拽的行人直接"不存在" —— 用户原话「根本没有 NPC 在我面前」。
// 原因是 Unknown 不是一个合法步态,行为调度器拿到它就没有 velocity 可输出。
// ⇒ 第一拍 WalkFront + continueMove=true(让它"走过去"),
// 第二拍 Idle + continueMove=false 收尾(不收尾步态会一直活着,原地踏步)。
await session.NotifyAsync(MethodId.SyncUnitPositionAndFacing,
UxSerializer.Serialize(WorldCodec.Walking(targetUid, new Vec3(nx, ny, nz), npcFacing)),
token);
await Task.Delay(220, token);
await session.NotifyAsync(MethodId.SyncUnitPositionAndFacing,
UxSerializer.Serialize(WorldCodec.PositionAndFacing(
targetUid, new Vec3(nx, ny, nz), npcFacing,
WorldCodec.SetPositionType.Gm, continueMove: false, moveId: WorldCodec.MoveAction.Idle)),
token);
var msg = $"target={targetUid} 已拽到玩家前方 {frontMeters:F1}m:"
+ $"({nx:F1},{ny:F1},{nz:F1}) facing={npcFacing:F1}(玩家 ({px:F1},{py:F1},{pz:F1}) facing={pf:F1});{ledger}";
session.Log.Info($"[POLICE-PULL] {msg}");
return (true, msg);
}
// =================================================================================
// ① C2S:查询类(回执即可,防 await 卡死)
// =================================================================================
/// <summary>
/// C2S <c>AskGetArrestTimes</c>(63170713),INVOKE,返回 <c>UXRPCTask&lt;int&gt;</c>
/// (RpcSurface.json:5907)—— 面板上的「今日逮捕次数」。回本局累计值即可。
/// </summary>
[Handler(MethodId.AskGetArrestTimes, HandlerPacketKind.Invoke)]
private async Task AskGetArrestTimes(Connection conn, UxRpcMessage msg)
{
var count = GetOrCreatePoliceArrest(msg.Context.Session).Count;
await conn.ReturnAsync(msg, count);
msg.Context.Session.Log.Info($"[POLICE-ARREST] AskGetArrestTimes → {count}");
}
/// <summary>
/// C2S <c>AskIsArrested</c>(63307468),INVOKE,无返回值 —— 「当前是否处于被捕状态」。
/// 我们这边玩家永远不会被自己抓,回空 OK 即可(客户端读的是 <c>SyncPlayerArrestStateNotify</c>)。
/// </summary>
[Handler(MethodId.AskIsArrested, HandlerPacketKind.Invoke)]
private async Task AskIsArrested(Connection conn, UxRpcMessage msg)
{
await conn.ReturnEmptyOkAsync(msg);
msg.Context.Session.Log.Info("[POLICE-ARREST] AskIsArrested → false(玩家侧永远不成立)");
}
/// <summary>
/// C2S <c>AskPoliceEffectiveExam</c>(63851241),INVOKE,无返回值 ——
/// <b>盘查链路里唯一由 lua 直接发的一条</b>(<c>PoliceExamineManager.lua:545</c>,
/// 盘查选项命中 4 项以上时发一次,用来触发"成功执法"演出)。
/// 回执即可;演出在客户端本地(<c>successPoliceEffect</c> 标志),服务端无帧可推。
/// </summary>
[Handler(MethodId.AskPoliceEffectiveExam, HandlerPacketKind.Invoke)]
private async Task AskPoliceEffectiveExam(Connection conn, UxRpcMessage msg)
{
await conn.ReturnEmptyOkAsync(msg);
msg.Context.Session.Log.Info("[POLICE-ARREST] AskPoliceEffectiveExam(盘查有效)— 只回执,演出在客户端本地");
}
// =================================================================================
// ② C2S AskPoliceExamInteract(63233964) —— 盘查面板的选项(形状未知,先接住再取证)
// =================================================================================
/// <summary>
/// 盘查面板里点下某个选项(查身份证 / 搜身 / 酒测 / 毒测 / 罚款 / 逮捕 / 放行)时由
/// <b>C# 侧</b>发出来的 INVOKE。
///
/// ⛑ 形状是猜的(见 <c>AskPoliceExamInteract4229938</c> 的注释):用 <c>TryGetArgs</c>,
/// 解析失败也照常回执,只把 hex 打出来。等拿到真实字节再收紧契约与 option 过滤。
/// </summary>
[Handler(MethodId.AskPoliceExamInteract, HandlerPacketKind.Invoke)]
private async Task AskPoliceExamInteract(Connection conn, UxRpcMessage msg)
{
var hex = HexPrefix(msg.Body, ArrestHexLogBytes);
var parsed = msg.TryGetArgs<PoliceMethods.AskPoliceExamInteract4229938>(out var args);
// 先回执:INVOKE 不回 = 客户端 await 到超时,这条比什么都重要。
await conn.ReturnEmptyOkAsync(msg);
if (!parsed || args is null)
{
msg.Context.Session.Log.Warn($"[POLICE-ARREST] exam-interact 解析失败(按 pid=0 处理)hex={hex} bytes={msg.Body.Length}");
return;
}
msg.Context.Session.Log.Info($"[POLICE-ARREST] exam-interact pid={args.pid} option={args.option} hex={hex}");
if (args.pid == 0)
{
msg.Context.Session.Log.Warn("[POLICE-ARREST] exam-interact pid=0 — 没有目标,不执行逮捕");
return;
}
await PerformArrestAsync(msg.Context, args.pid, $"exam-interact option={args.option}");
}
// =================================================================================
// ③ C2S AskInteractCmd(67390408) —— ⛔ 别在这里加 handler,已经有了
//
// GameRouter.Vehicle.cs:268 那条就是它(Notify,带完整解码):
// · CmdType=19 是 2 次/秒的心跳,不打日志;
// · CmdType=33 = 目标选择,stringParam1 是目标单位 id ⇒ 存进
// <c>_lastInteractTargetUnitId</c>;
// · CmdType=34 = 交互心跳,offset 24 是交互状态。
// 2026-09-25 14:08:14 在警卫署点「审讯」抓到的那条 72 字节样本,日志里就是
// <c>[INTERACT] cmd=…</c> 那一行。
// ⇒ 将来要做"对着 NPC 按 F 就逮捕",目标直接用 <c>_lastInteractTargetUnitId</c>,
// 不用重新解析这条包。本次不动它(它是全局交互通道,改了会误伤开门/上车)。
// =================================================================================
// ④ 逮捕动作本体
// =================================================================================
/// <summary>
/// 推一次 <c>SyncPoliceBeginArrest</c>(68839124) 并记账。
///
/// ⛑ 关于 <c>targetPid</c>:客户端那边是 <c>unit.Pid</c>(见
/// <c>PoliceExamineManager</c> 里 <c>gPoliceJobManager.cs:ClickOption_Story(self.unit.Pid, …)</c>),
/// 而我们服务端生成的单位手里只有 <b>uid</b>。反向链路用的 targetPid 是 <c>Profile.PlayerPid</c>
/// (一个 pid 语义的值),所以这里<b>先按 uid 当 pid 用</b>,实机看客户端认不认;
/// 不认就得回头找 uid→pid 的映射(<c>SyncManagedLogicAgent</c> 那条通道里有 pid)。
/// </summary>
private static Task<bool> PerformArrestAsync(RpcContext ctx, ulong targetUid, string source)
{
var state = GetStateIfExists(ctx.Session);
ulong actorUnit = 0;
if (state is not null)
{
lock (state.SyncRoot)
actorUnit = state.ActiveSpiritUnitId != 0 ? state.ActiveSpiritUnitId : Profile.InitialUnitId;
}
return ArrestBySessionAsync(ctx.Session, actorUnit, targetUid, source);
}
/// <summary>
/// 逮捕本体(与调用方无关的那份)。GM 面板 / <c>/api/police/arrest</c> 直接走这里,
/// 因为那边手里是 <c>(session, state)</c>,没有 <see cref="RpcContext"/>。
/// </summary>
/// <param name="actorUnit">执行者单位 id(玩家自己);传 0 ⇒ 退回 <c>Profile.InitialUnitId</c>。</param>
internal static async Task<bool> ArrestBySessionAsync(
TcpSession session, ulong actorUnit, ulong targetUid, string source)
{
var cfg = PrivateServerConfigStore.Current.Gameplay;
if (!cfg.PoliceArrestEnabled)
{
session.Log.Info($"[POLICE-ARREST] 跳过({source})— Gameplay.PoliceArrestEnabled=false(回滚开关)");
return false;
}
if (actorUnit == 0) actorUnit = Profile.InitialUnitId;
var st = GetOrCreatePoliceArrest(session);
st.Count++;
st.LastTarget = targetUid;
session.Log.Info($"[POLICE-ARREST] arrest #{st.Count} target={targetUid} actor={actorUnit} type={cfg.PoliceArrestType} source={source}");
await SendPoliceArrestAsync(session, actorUnit, targetUid, (byte)cfg.PoliceArrestType);
// 演出结束后把人带走:城里不该一直堆着被捕的 NPC。
// ⛑ 用 Task.Run 而不是阻塞 handler —— 延迟期间玩家可能已经断线,异常一律吞掉。
var delay = cfg.PoliceArrestRemoveDelayMs;
if (!cfg.PoliceArrestRemoveEnabled || delay <= 0)
return true;
_ = Task.Run(async () =>
{
try
{
await Task.Delay(delay).ConfigureAwait(false);
// ⛑ session.NotifyAsync 的这个重载要 byte[](ctx.NotifyAsync 那个才吃对象),
// 所以这里必须自己序列化 —— 与 GameRouter.Wanted.cs 里发 S2C 的写法一致。
await session.NotifyAsync(MethodId.SyncLogicAgentLeave,
UxSerializer.Serialize(WorldCodec.LogicAgentLeave(targetUid)), CancellationToken.None);
session.Log.Info($"[POLICE-ARREST] target={targetUid} 已带走 — SyncLogicAgentLeave sent (delay={delay}ms)");
}
catch (Exception ex)
{
session.Log.Warn($"[POLICE-ARREST] 带走 target={targetUid} 失败(多半已断线): {ex.Message}");
}
});
return true;
}
/// <summary>
/// 发 <c>SyncPoliceBeginArrest</c>(68839124)。与 <c>GameRouter.Wanted.cs:723</c> 用的是同一帧,
/// 序列化方式也保持一致(<c>UxSerializer.Serialize</c> + 三参数 NotifyAsync)。
/// </summary>
internal static Task SendPoliceArrestAsync(
TcpSession session,
ulong actorUnitId,
ulong targetPid,
byte arrestType,
CancellationToken token = default)
=> session.NotifyAsync(MethodId.SyncPoliceBeginArrest, UxSerializer.Serialize(
new PoliceMethods.SyncPoliceBeginArrest4229938
{
agentId = actorUnitId,
targetPid = targetPid,
ArrestType = arrestType,
}), token);
private static PoliceArrestState GetOrCreatePoliceArrest(TcpSession session)
{
if (session.Items.TryGetValue(PoliceArrestStateKey, out var raw) && raw is PoliceArrestState st)
return st;
st = new PoliceArrestState();
session.Items[PoliceArrestStateKey] = st;
return st;
}
private static string HexPrefix(byte[] body, int max)
{
if (body is null || body.Length == 0) return "-";
var n = Math.Min(max, body.Length);
var sb = new StringBuilder(n * 2 + 8);
for (var i = 0; i < n; i++) sb.Append(body[i].ToString("X2"));
if (body.Length > n) sb.Append("…");
return sb.ToString();
}
}