Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.VehicleControls.cs

212 lines
12 KiB
C#

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>(T parameters) where T : new() => UxSerializer.Serialize(
new GameMethods.AssignVehicleControl4229938<T> { 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}"); }
}
}