using System.Buffers.Binary; using Ananta.SDK.Network; 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; namespace Ananta.Server.Handlers.Game; // T1-16 载具骇入 — ported from donor_7k GameRouter.VehicleControls.cs. // Client actions 1..6 arrive through AskHackVehicle and are forwarded to the stock // vehicle AI as SyncAssignVehicleAITask. Action 0 is an internal stop, never client-sent. internal sealed partial class GameRouter { // Client actions 1..6. Action 0 is an explicit GM-only stop, not a client hack. internal static bool TryDecodeVehicleControl(byte[] body, out ulong vehicleId, out byte action, out ulong targetId) { vehicleId = targetId = 0; action = 0; if (body.Length < 10) return false; var id = BinaryPrimitives.ReadUInt64LittleEndian(body); var kind = body[8]; if (id == 0 || kind is < 1 or > 6) return false; ulong target = 0; if (kind == 5) { // Native WriteVehicleHackChaseTargetParameter_Check: type mark 2, // u64 target, nullable uint[] (00 or FF + biased Int7 + values). if (body.Length < 19 || body[9] != 2) return false; target = BinaryPrimitives.ReadUInt64LittleEndian(body.AsSpan(10)); if (target == 0 || target == id) return false; if (body[18] == 0) { if (body.Length != 19) return false; } else { // Bound the array before parsing. Buff IDs are never applied from client input. if (body[18] != 255 || body.Length < 20 || body[19] is < 1 or > 17 || body.Length != 20 + (body[19] - 1) * 4) return false; } } else if (body.Length != 10 || body[9] != 0) return false; vehicleId = id; action = kind; targetId = target; return true; } internal static bool IsValidVehicleChaseTarget(VehicleRuntimeState runtime, VehicleRecord vehicle, ulong targetId) { if (targetId == vehicle.EntityId || !runtime.Vehicles.TryGetValue(targetId, out var target) || !target.AetherRegistered || !target.Interactable) return false; var dx = target.Position.X - vehicle.Position.X; var dy = target.Position.Y - vehicle.Position.Y; var dz = target.Position.Z - vehicle.Position.Z; var distanceSquared = dx * dx + dy * dy + dz * dz; return float.IsFinite(distanceSquared) && distanceSquared <= 100f * 100f; } internal static Vec3 VehicleReverseDestination(Vec3 position, float facing) { var yaw = facing * MathF.PI / 180f; // Unity forward is (sin(yaw), 0, cos(yaw)); retreat 6m, not a teleport. return new(position.X - MathF.Sin(yaw) * 6f, position.Y, position.Z - MathF.Cos(yaw) * 6f); } internal static byte[] BuildVehicleControlPacket(ulong id, ulong token, byte action, ulong target, Vec3 position, float facing) { var tail = new GameMethods.VehicleTaskTail4229938 { Token = token }; byte[] Pack(T parameters) where T : new() => UxSerializer.Serialize( new GameMethods.AssignVehicleControl4229938 { VehicleUId = id, Parameters = parameters }); switch (action) { case 0: tail.DefaultSpeed = 0f; return Pack(new GameMethods.VehicleStopParameters4229938 { Base = tail }); case 1: // Keep the in-game verified accelerate packet byte-for-byte unchanged. return UxSerializer.Serialize(new GameMethods.AssignVehicleRush4229938 { VehicleUId = id, Parameters = new() { Token = token } }); case 2: case 3: return Pack(new GameMethods.VehicleTurnParameters4229938 { TurnAction = (byte)(action - 2), Base = tail }); case 4: var destination = VehicleReverseDestination(position, facing); tail.DefaultSpeed = 4f; tail.DrivingFlags = 1 | 1024 | 16777216; // StopForCars, DriveInReverse, ForceStraightLine. return Pack(new GameMethods.VehicleCruiseParameters4229938 { TargetPointList = [new(destination.X, destination.Y, destination.Z)], Base = tail }); case 5: if (target == 0 || target == id) throw new ArgumentException("Invalid chase target"); return Pack(new GameMethods.VehicleChaseParameters4229938 { TargetUid = target, Base = tail }); case 6: // Native TaskFactory initial speed provides the momentum needed to drift from rest. tail.InitSpeed = 8f; return Pack(new GameMethods.VehicleDriftParameters4229938 { Base = tail }); default: throw new ArgumentOutOfRangeException(nameof(action)); } } internal static string VehicleControlName(byte action) => action switch { 0 => "停止", 1 => "加速", 2 => "左冲", 3 => "右冲", 4 => "后冲/倒车", 5 => "追赶目标车辆", 6 => "漂移", _ => "未知", }; internal static bool CanStopHackVehicle(VehicleRuntimeState runtime, VehicleRecord vehicle, ulong expectedToken = 0) => vehicle.ControllerPid == Profile.PlayerPid && vehicle.HackTaskToken != 0 && vehicle.TaskToken == vehicle.HackTaskToken && (expectedToken == 0 || expectedToken == vehicle.HackTaskToken) && runtime.CurrentVehicleId != vehicle.EntityId && !vehicle.IsDriving && runtime.StoryEnterVehicleId != vehicle.EntityId && vehicle.SeatOccupants.Count == 0 && vehicle.SeatReservations.Count == 0; internal static async Task<(bool Ok, string Message)> ExecuteVehicleControlAsync( TcpSession session, ulong vehicleId, byte action, ulong targetId, ulong expectedToken = 0) { if (action > 6) return (false, "不支持的车辆动作"); var state = GetStateIfExists(session); if (state is null) return (false, "角色尚未进入场景"); Vec3 player; lock (state.SyncRoot) { if (!state.HasLastReportedPlayerTransform) return (false, "尚未收到角色位置"); player = state.LastReportedPlayerPosition; } var runtime = GetVehicleRuntimeState(session); await runtime.HackDispatchGate.WaitAsync(); try { ulong token; byte[] packet; VehicleRecord? trafficTarget = null; lock (runtime.SyncRoot) { if (!runtime.Vehicles.TryGetValue(vehicleId, out var vehicle)) return (false, "车辆不在当前服务端场景记录中"); if (action == 0) { if (!CanStopHackVehicle(runtime, vehicle, expectedToken)) return (false, "没有可停止的空车骇入任务,或任务已被替换"); } else { if (!CanRushVehicle(runtime, vehicle, player.X, player.Y, player.Z, DateTime.UtcNow) || runtime.StoryEnterVehicleId == vehicleId || !float.IsFinite(vehicle.Facing)) { // ★ 诊断(2026-09-22):返回给客户端的这句话把六种原因合并成一句,无法定位。 // 用户实测「很靠近车按加速也无效」,已排除「距离」这个猜测, // 所以把每个子条件的实际值打出来,跑一次就能定性(不再靠猜)。 var ddx = vehicle.Position.X - player.X; var ddy = vehicle.Position.Y - player.Y; var ddz = vehicle.Position.Z - player.Z; session.Log.Warn($"[VEHICLE-HACK] rush-deny entity={vehicleId} " + $"dist={MathF.Sqrt(ddx * ddx + ddy * ddy + ddz * ddz):F1}m " + $"aether={vehicle.AetherRegistered} interactable={vehicle.Interactable} " + $"ctrl={vehicle.ControllerPid} cur={runtime.CurrentVehicleId} " + $"occ={vehicle.SeatOccupants.Count} res={vehicle.SeatReservations.Count} " + $"driving={vehicle.IsDriving} busy={vehicle.HackBusyUntilUtc > DateTime.UtcNow} " + $"storyEnter={runtime.StoryEnterVehicleId} facing={vehicle.Facing} " + $"player=({player.X:F1},{player.Y:F1},{player.Z:F1}) " + $"car=({vehicle.Position.X:F1},{vehicle.Position.Y:F1},{vehicle.Position.Z:F1})"); return (false, "车辆未登记、被占用、超出30米、其他玩家主控或仍在冷却"); } if (action == 5 && !IsValidVehicleChaseTarget(runtime, vehicle, targetId)) return (false, "请选择100米内另一辆已登记的汽车作为追赶目标"); } token = unchecked((ulong)IdCounterStore.Next(VehicleTokenCounter, VehicleTokenFloor)); packet = BuildVehicleControlPacket(vehicleId, token, action, targetId, vehicle.Position, vehicle.Facing); if (action == 0) { vehicle.TaskToken = vehicle.HackTaskToken = token; vehicle.HackTaskStatus = 0; } else if (!TryPrepareVehicleRush(runtime, vehicle, player.X, player.Y, player.Z, DateTime.UtcNow, token)) return (false, "车辆状态已变化,请重新尝试"); vehicle.HackBusyUntilUtc = DateTime.UtcNow.AddSeconds(action == 6 ? 3 : 2); // out var 出不了 lock 块,用外层变量把记录带出去给下面的摘出逻辑。 if (vehicle.IsTraffic) trafficTarget = vehicle; } // ★ 车流车(T3-7):玩家接管前必须先摘出车道流。 // 否则服务端每 50ms 的 SyncAetherAIVehicleLaneDatas 会持续覆盖里程, // 把刚下发的 AI 任务瞬间拽回去 —— 就是「能显示骇入但加速没反应」。 // 必须在 SyncAssignVehicleAITask 之前做,且幂等(非车流车不会进到这里)。 if (trafficTarget is not null) await DetachTrafficVehicleFromLaneAsync(session, trafficTarget); await session.NotifyAsync(MethodId.SyncChangeVehicleController, UxSerializer.Serialize( new GameMethods.SyncChangeVehicleController4229938 { vehicleInstanceId = vehicleId, controllerPid = Profile.PlayerPid }), CancellationToken.None); session.Log.Info($"[VEHICLE-HACK] physics host assigned entity={vehicleId} pid={Profile.PlayerPid} token={token}"); await session.NotifyAsync(MethodId.SyncAssignVehicleAITask, packet, CancellationToken.None); session.Log.Info($"[VEHICLE-HACK] control sent action={action} name={VehicleControlName(action)} entity={vehicleId} target={targetId} token={token} bytes={packet.Length}"); if (action is 4 or 5 or 6) _ = StopVehicleControlAfterAsync(session, vehicleId, token, action == 5 ? 10_000 : action == 4 ? 4_000 : 3_000); return (true, $"已下发{VehicleControlName(action)}:车辆 {vehicleId},任务 {token};实际效果请在游戏内确认"); } finally { runtime.HackDispatchGate.Release(); } } private static async Task StopVehicleControlAfterAsync(TcpSession session, ulong id, ulong token, int milliseconds) { try { await Task.Delay(milliseconds); // Token, current scene membership, seats and ownership are revalidated // inside the same dispatch gate. Never stop a newer task or a player's car. await ExecuteVehicleControlAsync(session, id, 0, 0, token); } catch (Exception ex) { session.Log.Warn($"[VEHICLE-HACK] bounded stop ended entity={id}: {ex.Message}"); } } }