207 lines
12 KiB
Go
207 lines
12 KiB
Go
package routers
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
)
|
||
|
||
var _ = sync.Mutex{}
|
||
var _ = context.Background
|
||
|
||
//
|
||
// 巡卫官(里希 / 15021021)正向「逮捕 NPC」—— 与 GameRouter.Wanted.cs 那条反向链路
|
||
// (玩家犯罪 → 警车追 → 警车抓玩家)共用同一个 S2C,只是把 target 从玩家换成 NPC。
|
||
// <code>
|
||
// SyncPoliceBeginArrest(68839124) = { u64 agentId(执行者), u64 targetPid(被捕者), byte ArrestType }
|
||
// </code>
|
||
// 反向链路里 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 ⇒ 存进
|
||
// <c>_lastInteractTargetUnitId</c>;
|
||
// · CmdType=34 = 交互心跳,offset 24 是交互状态。
|
||
// 2026-09-25 14:08:14 在警卫署点「审讯」抓到的那条 72 字节样本,日志里就是
|
||
// <c>[INTERACT] cmd=…</c> 那一行。
|
||
// ⇒ 将来要做"对着 NPC 按 F 就逮捕",目标直接用 <c>_lastInteractTargetUnitId</c>,
|
||
// 不用重新解析这条包。本次不动它(它是全局交互通道,改了会误伤开门/上车)。
|
||
// =================================================================================
|
||
// ④ 逮捕动作本体
|
||
// =================================================================================
|
||
//
|
||
// 推一次 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 起)也不连续 ⇒ 客户端自己随机分配的单位。
|
||
// 这类目标服务端<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」,而是把账本摊开、按到玩家的距离排序点名。
|
||
// =================================================================================
|
||
// 一个可抓目标。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"`
|
||
}
|
||
|