This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.Wanted.cs
T

859 lines
36 KiB
C#
Raw Normal View History

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;
/// <summary>
/// 开放世界通缉系统(Wanted)——「警车版」。
///
/// 2026-09-23 自捐赠端 <c>PrivateServer修改_miyuki_fix</c> 的 <c>GameRouter.Wanted.cs</c> 移植。
///
/// ★ 与捐赠端的关键差异(都是刻意的):
/// 1. **只做警车,不做步行警员**。捐赠端的步行警员靠它自己的 crowd agent 管线
/// (<c>CrowdAgent</c> / <c>PushLogicAgentHandoff</c> / <c>PushCrowdTaskCommand</c> /
/// <c>CrowdAdvanceEstimate</c>),本方没有那套类型,且它那份日志早于它自己的最后一次改动
/// ⇒ 步行警员那半根本没被实机验证过。警车由**客户端自己的车辆 AI** 驱动
/// (<c>NavConfigId=1002</c> / <c>ChaseDirectlySpeed=26</c>),服务端只负责"派"和"撤"。
/// 2. 因此**自动逃脱/被捕的判据换了**。捐赠端用"最近警员距离"(它自己算的位置),
/// 警车版拿不到警车位置(客户端 AI 不外报),改成两个服务端真信号:
/// · 通缉持续超过 <see cref="WantedPursuitTimeoutSeconds4229938"/> ⇒ 自动逃脱;
/// · 玩家倒地(<c>state.PlayerDowned</c>)⇒ 自动被捕。
/// 3. 没有 <c>resetAfterArrest</c> / <c>RestartCurrentWorld</c>(本方没有那个方法),
/// 被捕就是清场 + 罚款。
/// 4. 不碰 <c>CrimeGameplay</c>(商店抢劫/偷车任务链),那半依赖本方完全没有的
/// GmTasks 任务状态物件。
///
/// ⛑️ 完全绕开官方犯罪通道:不接 SyncAgentCrimeAdd(68831313)、
/// ReportPoliceCrimeEvent(67146776)、GmAddCrimeValue(69946581)。犯罪值是服务端自己加的,
/// 代价是"只有服务端认可的行为会涨值",好处是不会被官方那些未摸清的帧时序炸客户端。
///
/// 回滚:把 <c>wanted.enabled</c> 置 false(或直接删本文件 + 摘掉三处挂钩)。
/// </summary>
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";
/// <summary>警车实体号段 430_000_000_000+(不与本方车辆 8_200e15 / 行人 8_300e15 冲突)。</summary>
private const int WantedPursuitTickMs4229938 = 1_000;
private const int WantedPursuitTimeoutSeconds4229938 = 90;
/// <summary>
/// 三台真警车模板(已在本方 <c>ClientData/4229938/Configs/VehicleConfig.json</c> 复核):
/// 81007029 = T_PoliceCar01a、81007078 = U_NewPolice01a、81004027 = U_PoliceCar02a。
/// </summary>
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<TcpSession, WantedPursuitRuntime> 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<object> 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);
}
/// <summary>
/// 犯罪事件入口。由三处挂钩调用:npc-hit / npc-kill(玩家打路人、敌人)、
/// vehicle-hit / vehicle-destroy(玩家砸车)。targetId=0 直接忽略。
/// </summary>
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<GameMethods.PoliceVehicleSpawnClientInfo4229938>(count);
var ids = new List<ulong>(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<byte>(), 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,
};
/// <summary>警车刷在玩家背后 30~54 m(背后 = 玩家朝向的反方向),横向错开 ±7 m。</summary>
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;
}
}