package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 巡卫官(里希 / 15021021)正向「逮捕 NPC」—— 与 GameRouter.Wanted.cs 那条反向链路 // (玩家犯罪 → 警车追 → 警车抓玩家)共用同一个 S2C,只是把 target 从玩家换成 NPC。 // // SyncPoliceBeginArrest(68839124) = { u64 agentId(执行者), u64 targetPid(被捕者), byte ArrestType } // // 反向链路里 agentId 是警车的 entity id、targetPid 是 Profile.PlayerPid、 // ArrestType=1(GameRouter.Wanted.cs:723)。正向这里 agentId 换成玩家自己的 unit id。 // ⛑⛑ 能确定与不能确定的,分开写清楚: // · 能确定:68839124 是 LuaOnly、有 lua reader(RPCDeserializeAuto.lua:6986), // 我方 Wanted.cs 已经在发且客户端吃过(玩家被捕演出成立)⇒ 帧本身是通的。 // · 不能确定:客户端什么时候会发盘查/逮捕的 C2S。盘查面板与审讯一样挂在 // gPoliceJobManager.cs(C#)与 Spoon 上;AskPoliceExamInteract(63233964) 在 lua 里 // 零引用,RpcSurface.json 里也没有 ⇒ 形状未知、触发时机未知。 // ⇒ 所以本文件做的是「把入口全都接住 + 留取证口子」,不假设客户端一定会发。 // 三条取证口子(实机跑一次就有答案): // 1. [POLICE-ARREST] exam-interact hex=… —— 盘查选项 RPC 的真实字节; // 2. [POLICE-ARREST] interact-cmd hex=… —— 按 F 交互那条 72 字节 NOTIFY 的内容(前 5 次); // 3. GM 面板 / POST /api/police/arrest —— 不等客户端,直接验证演出帧能不能出。 // ⛑ 硬规则 94:写完 [Handler] 还要去 GameRouter.cs 的 Enabled4229938MethodIds // 登记,否则编译通过、功能零生效,而这些都是 INVOKE(客户端会 await 到超时)。 // // // 汇总当前会话里服务端已知的全部可抓单位,按到玩家的距离升序。 // ⛑ 三个账本的 state 方法有各自的 SyncRoot,逐个 lock 取快照,不在锁里做 I/O。 // ⛑ 玩家自己的单位(ActiveSpiritUnitId / Profile.InitialUnitId)会被剔除 —— // 自己抓自己会走到「玩家被捕」那条演出上,与正向逮捕完全相反。 // func GameRouter_PoliceArrest_CollectPoliceTargets(ctx context.Context, session *GameSession, max int32) []any { // Router implementation for GameRouter_PoliceArrest_CollectPoliceTargets return nil } // ================================================================================= // ⓪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 就能看到它贴在脸上。 // ================================================================================= // 把 瞬移到玩家正前方 米处。 func GameRouter_PoliceArrest_PullTargetToPlayerAsync(ctx context.Context, session *GameSession, targetUid uint64, frontMeters float32, token context.Context) any { // Router implementation for GameRouter_PoliceArrest_PullTargetToPlayerAsync var zero any; return zero } // ================================================================================= // ① C2S:查询类(回执即可,防 await 卡死) // ================================================================================= // // C2S AskGetArrestTimes(63170713),INVOKE,返回 UXRPCTask<int> // (RpcSurface.json:5907)—— 面板上的「今日逮捕次数」。回本局累计值即可。 // func GameRouter_PoliceArrest_AskGetArrestTimes(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_PoliceArrest_AskGetArrestTimes var zero any; return zero } // // C2S AskIsArrested(63307468),INVOKE,无返回值 —— 「当前是否处于被捕状态」。 // 我们这边玩家永远不会被自己抓,回空 OK 即可(客户端读的是 SyncPlayerArrestStateNotify)。 // func GameRouter_PoliceArrest_AskIsArrested(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_PoliceArrest_AskIsArrested var zero any; return zero } // // C2S AskPoliceEffectiveExam(63851241),INVOKE,无返回值 —— // 盘查链路里唯一由 lua 直接发的一条(PoliceExamineManager.lua:545, // 盘查选项命中 4 项以上时发一次,用来触发"成功执法"演出)。 // 回执即可;演出在客户端本地(successPoliceEffect 标志),服务端无帧可推。 // func GameRouter_PoliceArrest_AskPoliceEffectiveExam(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_PoliceArrest_AskPoliceEffectiveExam var zero any; return zero } // ================================================================================= // ② C2S AskPoliceExamInteract(63233964) —— 盘查面板的选项(形状未知,先接住再取证) // ================================================================================= // // 盘查面板里点下某个选项(查身份证 / 搜身 / 酒测 / 毒测 / 罚款 / 逮捕 / 放行)时由 // C# 侧发出来的 INVOKE。 // ⛑ 形状是猜的(见 AskPoliceExamInteract4229938 的注释):用 TryGetArgs, // 解析失败也照常回执,只把 hex 打出来。等拿到真实字节再收紧契约与 option 过滤。 // func GameRouter_PoliceArrest_AskPoliceExamInteract(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_PoliceArrest_AskPoliceExamInteract var zero any; return zero } // ================================================================================= // ③ C2S AskInteractCmd(67390408) —— ⛔ 别在这里加 handler,已经有了 // // GameRouter.Vehicle.cs:268 那条就是它(Notify,带完整解码): // · CmdType=19 是 2 次/秒的心跳,不打日志; // · CmdType=33 = 目标选择,stringParam1 是目标单位 id ⇒ 存进 // _lastInteractTargetUnitId; // · CmdType=34 = 交互心跳,offset 24 是交互状态。 // 2026-09-25 14:08:14 在警卫署点「审讯」抓到的那条 72 字节样本,日志里就是 // [INTERACT] cmd=… 那一行。 // ⇒ 将来要做"对着 NPC 按 F 就逮捕",目标直接用 _lastInteractTargetUnitId, // 不用重新解析这条包。本次不动它(它是全局交互通道,改了会误伤开门/上车)。 // ================================================================================= // ④ 逮捕动作本体 // ================================================================================= // // 推一次 SyncPoliceBeginArrest(68839124) 并记账。 // ⛑ 关于 targetPid:客户端那边是 unit.Pid(见 // PoliceExamineManager 里 gPoliceJobManager.cs:ClickOption_Story(self.unit.Pid, …)), // 而我们服务端生成的单位手里只有 uid。反向链路用的 targetPid 是 Profile.PlayerPid // (一个 pid 语义的值),所以这里先按 uid 当 pid 用,实机看客户端认不认; // 不认就得回头找 uid→pid 的映射(SyncManagedLogicAgent 那条通道里有 pid)。 // func GameRouter_PoliceArrest_PerformArrestAsync(ctx context.Context, rpcCtx *RpcContext, targetUid uint64, source string) bool { // Router implementation for GameRouter_PoliceArrest_PerformArrestAsync return true } // // 逮捕本体(与调用方无关的那份)。GM 面板 / /api/police/arrest 直接走这里, // 因为那边手里是 (session, state),没有 RpcContext。 // // 执行者单位 id(玩家自己);传 0 ⇒ 退回 Profile.InitialUnitId。 func GameRouter_PoliceArrest_ArrestBySessionAsync(ctx context.Context, session *GameSession, actorUnit uint64, targetUid uint64, source string) bool { // Router implementation for GameRouter_PoliceArrest_ArrestBySessionAsync return true } // // 发 SyncPoliceBeginArrest(68839124)。与 GameRouter.Wanted.cs:723 用的是同一帧, // 序列化方式也保持一致(UxSerializer.Serialize + 三参数 NotifyAsync)。 // func GameRouter_PoliceArrest_SendPoliceArrestAsync(ctx context.Context, session *GameSession, actorUnitId uint64, targetPid uint64, arrestType byte, token context.Context) any { // Router implementation for GameRouter_PoliceArrest_SendPoliceArrestAsync var zero any; return zero } func GameRouter_PoliceArrest_GetOrCreatePoliceArrest(ctx context.Context, session *GameSession) any { // Router implementation for GameRouter_PoliceArrest_GetOrCreatePoliceArrest var zero any; return zero } func GameRouter_PoliceArrest_HexPrefix(ctx context.Context, body []byte, max int32) string { // Router implementation for GameRouter_PoliceArrest_HexPrefix return "" } type PoliceArrestState struct { // 本局已逮捕次数(喂 AskGetArrestTimes)。 Count int32 `json:"count"` // 最近一次被捕目标的 uid(同时当 pid 用,见 SendPoliceArrestAsync)。 LastTarget uint64 `json:"lastTarget"` } // ================================================================================= // ⓪ 「抓谁?」—— 服务端账本里的可抓目标清单 // // 2026-09-25 用户提问的根因在这里,先写死在代码里免得下次再踩: // **城里绝大多数路人是客户端本地实体,服务端根本没有它们的 id。** // 实据:AskInteractCmd CmdType=33(目标选择)每秒回报 3~4 次,s1 形如 // `8300000000000000188`(= 0x732F860653BE00BC),既不在我方 uid 段 // (0x200000000000 + ServerSpawnUidFloor 起)也不连续 ⇒ 客户端自己随机分配的单位。 // 这类目标服务端无法点名(没有稳定 id,且下一秒就换了一个)。 // // 能抓的只有服务端自己生成并推过 SyncManagedLogicAgent / SyncLogicAgentEnter 的单位: // · Ambient 行人(GameRouter.Ambient 的 Peds 账本,带服务端坐标) // · 地盘敌人(EnemyTerritory Active:uid -> spot) // · 功能 NPC(FunctionalNpc Active:uid -> spot) // · 最近一次 GM 召唤的 NPC(state.LastSummonedNpcEntityId,无坐标,按 0 距离排最前) // ⇒ 所以这里不做「对着路人按 F」,而是把账本摊开、按到玩家的距离排序点名。 // ================================================================================= // 一个可抓目标。Distance 为米;-1 = 服务端不知道它在哪(GM 召唤的那个)。 type PoliceTarget struct { Uid uint64 `json:"uid"` Kind string `json:"kind"` Label string `json:"label"` Distance float32 `json:"distance"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` }