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 GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Handlers.Game; /// /// 开放世界通缉系统(Wanted)——「警车版」。 /// /// 2026-09-23 自捐赠端 PrivateServer修改_miyuki_fix 的 GameRouter.Wanted.cs 移植。 /// /// ★ 与捐赠端的关键差异(都是刻意的): /// 1. **只做警车,不做步行警员**。捐赠端的步行警员靠它自己的 crowd agent 管线 /// (CrowdAgent / PushLogicAgentHandoff / PushCrowdTaskCommand / /// CrowdAdvanceEstimate),本方没有那套类型,且它那份日志早于它自己的最后一次改动 /// ⇒ 步行警员那半根本没被实机验证过。警车由**客户端自己的车辆 AI** 驱动 /// (NavConfigId=1002 / ChaseDirectlySpeed=26),服务端只负责"派"和"撤"。 /// 2. 因此**自动逃脱/被捕的判据换了**。捐赠端用"最近警员距离"(它自己算的位置), /// 警车版拿不到警车位置(客户端 AI 不外报),改成两个服务端真信号: /// · 通缉持续超过 ⇒ 自动逃脱; /// · 玩家倒地(state.PlayerDowned)⇒ 自动被捕。 /// 3. 没有 resetAfterArrest / RestartCurrentWorld(本方没有那个方法), /// 被捕就是清场 + 罚款。 /// 4. 不碰 CrimeGameplay(商店抢劫/偷车任务链),那半依赖本方完全没有的 /// GmTasks 任务状态物件。 /// /// ⛑️ 完全绕开官方犯罪通道:不接 SyncAgentCrimeAdd(68831313)、 /// ReportPoliceCrimeEvent(67146776)、GmAddCrimeValue(69946581)。犯罪值是服务端自己加的, /// 代价是"只有服务端认可的行为会涨值",好处是不会被官方那些未摸清的帧时序炸客户端。 /// /// 回滚:把 wanted.enabled 置 false(或直接删本文件 + 摘掉三处挂钩)。 /// internal sealed partial class GameRouter { private const uint WantedMaxLevel4229938 = 5; private const uint WantedMaxValue4229938 = 9_999; private const double WantedRewardPerLevel4229938 = 5_000d; private const double WantedPenaltyPerLevel4229938 = 2_500d; private const long WantedVehicleHitRearmMs4229938 = 700L; // 高水位落盘(IdCounterStore §6.10):警车 entityId 重启后不能复用。 private const long WantedVehicleEntityFloor = 430_000_000_000L; private const string WantedVehicleEntityCounter = "Wanted.VehicleEntity"; /// 警车实体号段 430_000_000_000+(不与本方车辆 8_200e15 / 行人 8_300e15 冲突)。 private const int WantedPursuitTickMs4229938 = 1_000; private const int WantedPursuitTimeoutSeconds4229938 = 90; /// /// 三台真警车模板(已在本方 ClientData/4229938/Configs/VehicleConfig.json 复核): /// 81007029 = T_PoliceCar01a、81007078 = U_NewPolice01a、81004027 = U_PoliceCar02a。 /// private static readonly uint[] WantedPoliceVehicleTemplates4229938 = [ 81007029u, 81007078u, 81004027u, ]; internal sealed record WantedCrimeSettings4229938( bool Enabled, uint NpcHitPoints, uint NpcKillPoints, uint VehicleHitPoints, uint VehicleDestroyPoints, bool AutoEscapeEnabled, bool ArrestOnPlayerDown, int PursuitTimeoutSeconds, int VehiclesPerLevel, double RewardPerLevel, double PenaltyPerLevel); private static readonly object WantedCrimeSettingsLock4229938 = new(); private static WantedCrimeSettings4229938 s_wantedCrimeSettings4229938 = new( Enabled: true, NpcHitPoints: 10, NpcKillPoints: 60, VehicleHitPoints: 5, VehicleDestroyPoints: 50, AutoEscapeEnabled: true, ArrestOnPlayerDown: true, PursuitTimeoutSeconds: WantedPursuitTimeoutSeconds4229938, VehiclesPerLevel: 1, RewardPerLevel: WantedRewardPerLevel4229938, PenaltyPerLevel: WantedPenaltyPerLevel4229938); private sealed class WantedPursuitRuntime { internal CancellationTokenSource? Cancellation; internal Task? Loop; internal int Generation; } private static readonly object WantedPursuitLock4229938 = new(); private static readonly Dictionary WantedPursuitRuntimes4229938 = []; internal sealed class WantedActionOptions { public string Action { get; set; } = string.Empty; public uint Level { get; set; } public uint CrimeValue { get; set; } public uint Points { get; set; } = 25; public int Seconds { get; set; } = 10; public double? Amount { get; set; } public bool? Enabled { get; set; } public uint NpcHitPoints { get; set; } public uint NpcKillPoints { get; set; } public uint VehicleHitPoints { get; set; } public uint VehicleDestroyPoints { get; set; } public bool? AutoEscapeEnabled { get; set; } public bool? ArrestOnPlayerDown { get; set; } public int PursuitTimeoutSeconds { get; set; } public int VehiclesPerLevel { get; set; } public double RewardPerLevel { get; set; } public double PenaltyPerLevel { get; set; } } internal static WantedCrimeSettings4229938 WantedCrimeSettings() { lock (WantedCrimeSettingsLock4229938) return s_wantedCrimeSettings4229938; } private static WantedCrimeSettings4229938 SetWantedCrimeSettings(WantedActionOptions options) { lock (WantedCrimeSettingsLock4229938) { var current = s_wantedCrimeSettings4229938; return s_wantedCrimeSettings4229938 = new WantedCrimeSettings4229938( options.Enabled ?? current.Enabled, Math.Clamp(options.NpcHitPoints, 0u, WantedMaxValue4229938), Math.Clamp(options.NpcKillPoints, 0u, WantedMaxValue4229938), Math.Clamp(options.VehicleHitPoints, 0u, WantedMaxValue4229938), Math.Clamp(options.VehicleDestroyPoints, 0u, WantedMaxValue4229938), options.AutoEscapeEnabled ?? current.AutoEscapeEnabled, options.ArrestOnPlayerDown ?? current.ArrestOnPlayerDown, Math.Clamp(options.PursuitTimeoutSeconds <= 0 ? current.PursuitTimeoutSeconds : options.PursuitTimeoutSeconds, 5, 600), Math.Clamp(options.VehiclesPerLevel <= 0 ? current.VehiclesPerLevel : options.VehiclesPerLevel, 1, 3), Math.Clamp(options.RewardPerLevel <= 0d ? current.RewardPerLevel : options.RewardPerLevel, 0d, 1_000_000d), Math.Clamp(options.PenaltyPerLevel <= 0d ? current.PenaltyPerLevel : options.PenaltyPerLevel, 0d, 1_000_000d)); } } internal static object WantedReport(TcpSession? session) { var settings = WantedCrimeSettings(); var state = session is null ? null : GetStateIfExists(session); if (state is null) return new { ok = true, online = false, enabled = settings.Enabled, npcHitPoints = settings.NpcHitPoints, npcKillPoints = settings.NpcKillPoints, vehicleHitPoints = settings.VehicleHitPoints, vehicleDestroyPoints = settings.VehicleDestroyPoints, autoEscapeEnabled = settings.AutoEscapeEnabled, arrestOnPlayerDown = settings.ArrestOnPlayerDown, pursuitTimeoutSeconds = settings.PursuitTimeoutSeconds, vehiclesPerLevel = settings.VehiclesPerLevel, rewardPerLevel = settings.RewardPerLevel, penaltyPerLevel = settings.PenaltyPerLevel, note = "等待游戏会话进入世界;参数可以先设置,出警与结算需要进入世界后验证", }; lock (state.SyncRoot) { var remainingMs = state.WantedEscapeActive ? Math.Max(0, state.WantedEscapeDeadlineTicks - Environment.TickCount64) : 0; var elapsedMs = state.WantedStartedTicks == 0 ? 0 : Math.Max(0, Environment.TickCount64 - state.WantedStartedTicks); return new { ok = true, online = true, ready = state.Ready, crimeValue = state.WantedCrimeValue, wantedLevel = state.WantedLevel, chaseActive = state.WantedChaseActive, policeVehicles = state.WantedPoliceVehicleIds.Order().ToArray(), policeVehicleCount = state.WantedPoliceVehicleIds.Count, pursuitElapsedSeconds = (int)(elapsedMs / 1000), escapeActive = state.WantedEscapeActive, escapeRemainingSeconds = (int)Math.Ceiling(remainingMs / 1000d), escapeCount = state.WantedEscapeCount, arrestCount = state.WantedArrestCount, rewardTotal = state.WantedRewardTotal, penaltyTotal = state.WantedPenaltyTotal, enabled = settings.Enabled, npcHitPoints = settings.NpcHitPoints, npcKillPoints = settings.NpcKillPoints, vehicleHitPoints = settings.VehicleHitPoints, vehicleDestroyPoints = settings.VehicleDestroyPoints, autoEscapeEnabled = settings.AutoEscapeEnabled, arrestOnPlayerDown = settings.ArrestOnPlayerDown, pursuitTimeoutSeconds = settings.PursuitTimeoutSeconds, vehiclesPerLevel = settings.VehiclesPerLevel, rewardPerLevel = settings.RewardPerLevel, penaltyPerLevel = settings.PenaltyPerLevel, npcHitCount = state.WantedNpcHitCount, npcKillCount = state.WantedNpcKillCount, vehicleHitCount = state.WantedVehicleHitCount, vehicleDestroyCount = state.WantedVehicleDestroyCount, lastIncident = state.WantedLastIncident, lastOutcome = state.WantedLastOutcome, lastSync = state.WantedLastSync, note = "警车由客户端车辆 AI 驱动,服务端只负责派/撤;自动逃脱按通缉时长判定,自动被捕按玩家倒地判定", }; } } internal static async Task WantedActionAsync( TcpSession? session, WantedActionOptions options, CancellationToken token) { var action = (options.Action ?? string.Empty).Trim().ToLowerInvariant(); if (action == "configure") { SetWantedCrimeSettings(options); return WantedReport(session); } if (session is null) return new { ok = false, error = "session offline" }; var state = GetStateIfExists(session); if (state is null || !state.Ready) return new { ok = false, error = "world state not ready (enter the world first)" }; switch (action) { case "addviolation": await AddWantedCrimeAsync(session, state, options.Points == 0 ? 25u : options.Points, "gm-violation", token); break; case "setvalue": await SetWantedValueAsync(session, state, options.CrimeValue, "gm-value", token); break; case "setlevel": await SetWantedLevelAsync(session, state, Math.Clamp(options.Level == 0 ? 1u : options.Level, 1u, WantedMaxLevel4229938), "gm-level", token); break; case "escape": await BeginWantedEscapeAsync(session, state, options.Seconds, options.Amount, token); break; case "completeescape": await CompleteWantedEscapeAsync(session, state, expectedGeneration: null, options.Amount, "gm-complete", token); break; case "arrest": await CompleteWantedArrestAsync(session, state, options.Amount, token); break; case "clear": await ClearWantedAsync(session, state, "gm-clear", token); break; default: return new { ok = false, error = $"unknown wanted action '{options.Action}'" }; } return WantedReport(session); } // ── 犯罪值状态机 ──────────────────────────────────────────────────────────────────── private static async Task SetWantedValueAsync( TcpSession session, WorldEntryState state, uint requestedValue, string reason, CancellationToken token) { var value = Math.Min(requestedValue, WantedMaxValue4229938); if (value == 0) { await ClearWantedAsync(session, state, reason, token); return; } var level = WantedLevelForValue4229938(value); if (level == 0) { bool clearChase; lock (state.SyncRoot) { state.WantedCrimeValue = value; state.WantedLastOutcome = $"{reason}:below-threshold"; clearChase = state.WantedChaseActive || state.WantedLevel != 0; state.WantedLevel = 0; } if (clearChase) await ClearWantedDispatchAsync(session, state, $"below-threshold:{reason}", token); return; } await SetWantedLevelAsync(session, state, level, reason, token, value); } private static async Task AddWantedCrimeAsync( TcpSession session, WorldEntryState state, uint points, string reason, CancellationToken token) { if (points == 0) return; uint value; uint oldLevel; uint newLevel; lock (state.SyncRoot) { oldLevel = state.WantedLevel; state.WantedCrimeValue = (uint)Math.Min(WantedMaxValue4229938, (ulong)state.WantedCrimeValue + points); value = state.WantedCrimeValue; newLevel = WantedLevelForValue4229938(value); state.WantedLastOutcome = $"{reason}:+{points}"; } // 同星级内涨值只记日志,不重新派车(否则打一下就刷一批警车)。 if (newLevel == 0 || (newLevel == oldLevel && oldLevel != 0)) { session.Log.Info($"[WANTED] crime +{points} value={value} level={newLevel} reason={reason}"); return; } await SetWantedLevelAsync(session, state, newLevel, reason, token, value); } /// /// 犯罪事件入口。由三处挂钩调用:npc-hit / npc-kill(玩家打路人、敌人)、 /// vehicle-hit / vehicle-destroy(玩家砸车)。targetId=0 直接忽略。 /// internal static async Task RegisterWantedCrimeIncidentAsync( TcpSession session, string incident, ulong targetId, ulong attackerId, CancellationToken token) { var state = GetStateIfExists(session); if (state is null || !state.Ready || targetId == 0) return; var settings = WantedCrimeSettings(); if (!settings.Enabled) return; uint points; var now = Environment.TickCount64; lock (state.SyncRoot) { var playerId = state.ActiveSpiritUnitId == 0 ? Profile.InitialUnitId : state.ActiveSpiritUnitId; // 玩家自己挨打不算犯罪(AskPlayHurtEffect 也会在玩家被打时上报)。 if (targetId == playerId) return; // attackerId=0 = 挂钩点拿不到攻击者(例如客户端自己报的敌人倒地),按玩家算; // 非 0 且不是玩家 ⇒ 不是玩家干的,不计犯罪值。 if (attackerId != 0 && attackerId != playerId) return; switch (incident) { case "npc-hit": state.WantedNpcHitCount++; points = settings.NpcHitPoints; break; case "npc-kill": if (!state.WantedKilledNpcIds.Add(targetId)) return; state.WantedNpcKillCount++; points = settings.NpcKillPoints; break; case "vehicle-hit": // 一梭子/一次撞击会连报,700ms 内同一台车只记一次。 if (state.WantedLastIncidentTarget == targetId && now - state.WantedLastVehicleHitTicks < WantedVehicleHitRearmMs4229938) return; state.WantedLastVehicleHitTicks = now; state.WantedVehicleHitCount++; points = settings.VehicleHitPoints; break; case "vehicle-destroy": if (!state.WantedDestroyedVehicleIds.Add(targetId)) return; state.WantedVehicleDestroyCount++; points = settings.VehicleDestroyPoints; break; default: return; } state.WantedLastIncidentTarget = targetId; state.WantedLastIncident = $"{incident}:target={targetId}:points={points}"; } session.Log.Info($"[WANTED] incident={incident} target={targetId} attacker={attackerId} points={points}"); await AddWantedCrimeAsync(session, state, points, incident, token); } private static async Task SetWantedLevelAsync( TcpSession session, WorldEntryState state, uint level, string reason, CancellationToken token, uint? explicitValue = null) { level = Math.Clamp(level, 1u, WantedMaxLevel4229938); await ClearWantedDispatchAsync(session, state, $"replace:{reason}", token); lock (state.SyncRoot) { state.WantedLevel = level; state.WantedCrimeValue = explicitValue ?? MinimumWantedValue4229938(level); state.WantedChaseActive = true; state.WantedStartedTicks = Environment.TickCount64; CancelWantedEscapeLocked(state); state.WantedLastOutcome = reason; } // ① HUD 星级(68841232,u32 level) await session.NotifyAsync(MethodId.SyncPlayerCrimeLevel, UxSerializer.Serialize(level), token); // ② 派警车 await SpawnWantedPoliceAsync(session, state, level, token); // ③ 起追捕循环(超时逃脱 / 倒地被捕) StartWantedPursuitLoop(session, state); session.Log.Info($"[WANTED] set level={level} value={state.WantedCrimeValue} reason={reason}"); } // ── 出警 ─────────────────────────────────────────────────────────────────────────── private static async Task SpawnWantedPoliceAsync( TcpSession session, WorldEntryState state, uint level, CancellationToken token) { var settings = WantedCrimeSettings(); Vec3 playerPosition; float playerFacing; lock (state.SyncRoot) { playerPosition = state.HasLastReportedPlayerTransform ? state.LastReportedPlayerPosition : state.WorldEntryCreateHeroPosition; playerFacing = state.HasLastReportedPlayerTransform ? state.LastReportedPlayerRotation.Y : state.WorldEntryCreateHeroFacing; } var count = Math.Clamp((int)level * Math.Max(1, settings.VehiclesPerLevel), 1, WantedPoliceVehicleTemplates4229938.Length); var infos = new List(count); var ids = new List(count); for (var i = 0; i < count; i++) { var id = unchecked((ulong)IdCounterStore.Next(WantedVehicleEntityCounter, WantedVehicleEntityFloor)); var spawn = WantedPoliceSpawnPosition(playerPosition, playerFacing, i); var facing = NormalizeWantedFacing(playerFacing + 180f); ids.Add(id); infos.Add(new GameMethods.PoliceVehicleSpawnClientInfo4229938 { Id = id, VehicleId = WantedPoliceVehicleTemplates4229938[i], Position = new SceneMethods.UxVector3(spawn.X, spawn.Y, spawn.Z), Facing = facing, EulerAngles = new SceneMethods.UxVector3(0f, facing, 0f), }); } lock (state.SyncRoot) { foreach (var id in ids) state.WantedPoliceVehicleIds.Add(id); } await session.NotifyAsync(MethodId.SyncSpawnPoliceVehicles, UxSerializer.Serialize( new GameMethods.SyncSpawnPoliceVehicles4229938 { spawnInfos = infos, configInfo = new GameMethods.PoliceVehicleSpawnConfigInfo4229938 { ChaseRange = 120f, ChaseDirectlyRange = 35f, ApprehendRange = 5f, NavConfigId = 1002, ChaseDirectlyConfigId = 1001, PatrolSpeed = 10f, ChaseSpeed = 20f, ChaseDirectlySpeed = 26f, }, }), token); var targetVehicleId = GetVehicleRuntimeState(session).CurrentVehicleId; if (ids.Count != 0) { await session.NotifyAsync(MethodId.SyncPlayerPoliceChasedVehicles, UxSerializer.Serialize(ids[0]), token); } if (targetVehicleId != 0) { // 玩家在车上 ⇒ 让每台警车咬住那台车(客户端 AI 自己追)。 foreach (var id in ids) { await session.NotifyAsync(MethodId.SyncPoliceDispatchVehicleChase, UxSerializer.Serialize( new GameMethods.SyncPoliceDispatchVehicleChase4229938 { myVehicleEntityId = id, targetVehicleEntityId = targetVehicleId, }), token); } } lock (state.SyncRoot) state.WantedLastSync = $"hud-level:{level}; police:{ids.Count}; targetVehicle:{targetVehicleId}"; session.Log.Info($"[WANTED] spawned police count={ids.Count} targetVehicle={targetVehicleId} templates={string.Join(',', infos.Select(x => x.VehicleId))}"); } // ── 追捕循环(警车版:只判「超时逃脱」与「倒地被捕」)───────────────────────────── private static void StartWantedPursuitLoop(TcpSession session, WorldEntryState state) { CancellationTokenSource cts; int generation; lock (WantedPursuitLock4229938) { if (!WantedPursuitRuntimes4229938.TryGetValue(session, out var runtime)) { runtime = new WantedPursuitRuntime(); WantedPursuitRuntimes4229938[session] = runtime; } runtime.Cancellation?.Cancel(); runtime.Cancellation?.Dispose(); cts = new CancellationTokenSource(); runtime.Cancellation = cts; generation = ++runtime.Generation; runtime.Loop = RunWantedPursuitLoopAsync(session, state, generation, cts.Token); } } private static async Task RunWantedPursuitLoopAsync( TcpSession session, WorldEntryState state, int generation, CancellationToken token) { try { while (!token.IsCancellationRequested) { await Task.Delay(WantedPursuitTickMs4229938, token); // 规则 37:客户端挂起时后台推送必须停(否则 TCP 灌满 ⇒ 判定断线)。 if (IsBackgroundPushPaused(session)) continue; if (GetStateIfExists(session) is null) return; bool chaseActive; bool playerDowned; long startedTicks; lock (state.SyncRoot) { chaseActive = state.WantedChaseActive && state.WantedLevel > 0; playerDowned = state.PlayerDowned; startedTicks = state.WantedStartedTicks; } if (!chaseActive) continue; lock (WantedPursuitLock4229938) { if (!WantedPursuitRuntimes4229938.TryGetValue(session, out var runtime) || runtime.Generation != generation) return; } var settings = WantedCrimeSettings(); if (settings.ArrestOnPlayerDown && playerDowned) { session.Log.Info("[WANTED] automatic arrest (player downed)"); await CompleteWantedArrestAsync(session, state, requestedPenalty: null, CancellationToken.None); return; } if (settings.AutoEscapeEnabled && startedTicks != 0 && Environment.TickCount64 - startedTicks >= settings.PursuitTimeoutSeconds * 1_000L) { session.Log.Info($"[WANTED] automatic escape (pursuit timeout {settings.PursuitTimeoutSeconds}s)"); await CompleteWantedEscapeAsync(session, state, expectedGeneration: null, requestedReward: null, "timeout", CancellationToken.None); return; } } } catch (OperationCanceledException) { } catch (Exception ex) { session.Log.Warn($"[WANTED] pursuit loop stopped: {ex.Message}"); } } // ── 结算 ─────────────────────────────────────────────────────────────────────────── private static async Task BeginWantedEscapeAsync( TcpSession session, WorldEntryState state, int requestedSeconds, double? requestedReward, CancellationToken token) { int generation; int seconds; lock (state.SyncRoot) { if (!state.WantedChaseActive || state.WantedLevel == 0) throw new InvalidOperationException("no active pursuit"); seconds = Math.Clamp(requestedSeconds <= 0 ? 10 : requestedSeconds, 3, 120); generation = ++state.WantedEscapeGeneration; state.WantedEscapeActive = true; state.WantedEscapeDurationSeconds = seconds; state.WantedEscapeDeadlineTicks = Environment.TickCount64 + seconds * 1_000L; state.WantedPendingReward = requestedReward ?? state.WantedLevel * WantedCrimeSettings().RewardPerLevel; state.WantedLastOutcome = "escape-countdown"; } _ = CompleteWantedEscapeAfterDelayAsync(session, generation, seconds); await Task.CompletedTask; session.Log.Info($"[WANTED] escape started seconds={seconds} generation={generation}"); } private static async Task CompleteWantedEscapeAfterDelayAsync(TcpSession session, int generation, int seconds) { try { await Task.Delay(TimeSpan.FromSeconds(seconds)); var state = GetStateIfExists(session); if (state is not null) await CompleteWantedEscapeAsync(session, state, generation, null, "timer", CancellationToken.None); } catch (Exception ex) { session.Log.Warn($"[WANTED] escape timer failed: {ex.Message}"); } } private static async Task CompleteWantedEscapeAsync( TcpSession session, WorldEntryState state, int? expectedGeneration, double? requestedReward, string reason, CancellationToken token) { double reward; lock (state.SyncRoot) { if (expectedGeneration.HasValue && (!state.WantedEscapeActive || state.WantedEscapeGeneration != expectedGeneration.Value)) return; if (!state.WantedChaseActive || state.WantedLevel == 0) return; reward = requestedReward ?? (state.WantedEscapeActive ? state.WantedPendingReward : state.WantedLevel * WantedCrimeSettings().RewardPerLevel); reward = Math.Max(0d, double.IsFinite(reward) ? reward : 0d); state.WantedRewardTotal += reward; state.WantedEscapeCount++; state.WantedLastOutcome = "escape-success"; CancelWantedEscapeLocked(state); } await ClearWantedDispatchAsync(session, state, $"escape:{reason}", token); lock (state.SyncRoot) { state.WantedCrimeValue = 0; state.WantedLevel = 0; } await session.NotifyAsync(MethodId.SyncPlayerCrimeLevel, UxSerializer.Serialize(0u), token); if (reward > 0) { var changes = new EconomyChanges4229938 { MoneyDelta = reward }; await ApplyEconomy4229938Async(new Connection(session, token), state, changes, "wanted-escape"); } session.Log.Info($"[WANTED] escaped reward={reward:0.##}"); } private static async Task CompleteWantedArrestAsync( TcpSession session, WorldEntryState state, double? requestedPenalty, CancellationToken token) { double penalty; ulong arrestingVehicle; lock (state.SyncRoot) { if (!state.WantedChaseActive || state.WantedLevel == 0) throw new InvalidOperationException("no active pursuit"); penalty = requestedPenalty ?? state.WantedLevel * WantedCrimeSettings().PenaltyPerLevel; penalty = Math.Max(0d, double.IsFinite(penalty) ? penalty : 0d); state.WantedPenaltyTotal += penalty; state.WantedArrestCount++; state.WantedLastOutcome = "arrested"; arrestingVehicle = state.WantedPoliceVehicleIds.Order().FirstOrDefault(); CancelWantedEscapeLocked(state); } if (arrestingVehicle != 0) { await session.NotifyAsync(MethodId.SyncPoliceBeginArrest, UxSerializer.Serialize( new GameMethods.SyncPoliceBeginArrest4229938 { agentId = arrestingVehicle, targetPid = Profile.PlayerPid, ArrestType = 1, }), token); } await ClearWantedDispatchAsync(session, state, "arrest", token); lock (state.SyncRoot) { state.WantedCrimeValue = 0; state.WantedLevel = 0; } await session.NotifyAsync(MethodId.SyncPlayerCrimeLevel, UxSerializer.Serialize(0u), token); if (penalty > 0) { var changes = new EconomyChanges4229938 { MoneyDelta = -penalty }; await ApplyEconomy4229938Async(new Connection(session, token), state, changes, "wanted-arrest"); } session.Log.Info($"[WANTED] arrested penalty={penalty:0.##}"); } private static async Task ClearWantedAsync( TcpSession session, WorldEntryState state, string reason, CancellationToken token) { await ClearWantedDispatchAsync(session, state, reason, token); lock (state.SyncRoot) { state.WantedCrimeValue = 0; state.WantedLevel = 0; state.WantedStartedTicks = 0; state.WantedLastOutcome = "cleared"; CancelWantedEscapeLocked(state); } await session.NotifyAsync(MethodId.SyncPlayerCrimeLevel, UxSerializer.Serialize(0u), token); } private static async Task ClearWantedDispatchAsync( TcpSession session, WorldEntryState state, string reason, CancellationToken token) { ulong[] ids; bool hadChase; lock (state.SyncRoot) { ids = state.WantedPoliceVehicleIds.Order().ToArray(); hadChase = state.WantedChaseActive || ids.Length != 0; state.WantedPoliceVehicleIds.Clear(); state.WantedStartedTicks = 0; state.WantedChaseActive = false; } CancellationTokenSource? cancellation = null; lock (WantedPursuitLock4229938) { if (WantedPursuitRuntimes4229938.TryGetValue(session, out var runtime)) { cancellation = runtime.Cancellation; runtime.Cancellation = null; runtime.Generation++; } } cancellation?.Cancel(); cancellation?.Dispose(); foreach (var id in ids) { await session.NotifyAsync(MethodId.SyncPoliceDispatchVehicleChaseSt, UxSerializer.Serialize( new GameMethods.SyncPoliceDispatchVehicleChaseStop4229938 { myVehicleEntityId = id }), token); } if (ids.Length != 0) { await session.NotifyAsync(MethodId.SyncDestroyPoliceVehicles, UxSerializer.Serialize( new GameMethods.SyncDestroyPoliceVehicles4229938 { entityIds = ids.ToList() }), token); } if (hadChase) await session.NotifyAsync(MethodId.SyncPlayerPoliceChaseFinish, Array.Empty(), token); lock (state.SyncRoot) state.WantedLastSync = $"cleared:{reason}; vehicles:{ids.Length}"; session.Log.Info($"[WANTED] dispatch cleared reason={reason} vehicles={ids.Length}"); } private static void CancelWantedEscapeLocked(WorldEntryState state) { state.WantedEscapeActive = false; state.WantedEscapeDeadlineTicks = 0; state.WantedEscapeDurationSeconds = 0; state.WantedPendingReward = 0; state.WantedEscapeGeneration++; } private static uint WantedLevelForValue4229938(uint value) => value switch { < 50 => 0, < 100 => 1, < 200 => 2, < 400 => 3, < 800 => 4, _ => 5, }; private static uint MinimumWantedValue4229938(uint level) => level switch { 1 => 50, 2 => 100, 3 => 200, 4 => 400, _ => 800, }; /// 警车刷在玩家背后 30~54 m(背后 = 玩家朝向的反方向),横向错开 ±7 m。 private static Vec3 WantedPoliceSpawnPosition(Vec3 target, float facing, int ordinal) { var radians = facing * MathF.PI / 180f; var forward = new Vec3(MathF.Sin(radians), 0f, MathF.Cos(radians)); var right = new Vec3(MathF.Cos(radians), 0f, -MathF.Sin(radians)); var lateral = ordinal == 0 ? 0f : (ordinal % 2 == 1 ? 7f : -7f); var distance = 30f + ordinal * 12f; return new Vec3( target.X - forward.X * distance + right.X * lateral, target.Y, target.Z - forward.Z * distance + right.Z * lateral); } private static float NormalizeWantedFacing(float value) { var result = value % 360f; return result < 0f ? result + 360f : result; } }