Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.PoliceVehicle.cs
T

248 lines
13 KiB
C#

using Ananta.SDK.Network;
using Ananta.SDK.Serialization;
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;
internal sealed partial class GameRouter
{
private sealed record PolicePassenger4229938(ulong Agent, ulong Vehicle, byte Seat);
private static async Task TrackPoliceNpcMovement4229938(TcpSession session, WorldEntryState state,
SceneMethods.LogicAgentSyncData sample)
{
var p = sample.Position;
if (!float.IsFinite(p.X) || !float.IsFinite(p.Y) || !float.IsFinite(p.Z)
|| !state.Ready || state.PendingTeleportId != 0
|| !PoliceRuntimes.TryGetValue(session, out var r)) return;
await r.Gate.WaitAsync(session.ClosedToken);
try
{
if (r.Generation != state.WorldEntryControlGeneration || state.PendingTeleportId != 0
|| r.Passengers.ContainsKey(sample.AgentId)
|| !r.Npcs.TryGetValue(sample.AgentId, out var npc)) return;
if (sample.AgentId == r.Detained)
{
await HoldPolicePrisonNpc4229938(session, r, new Vec3(p.X, p.Y, p.Z));
return;
}
// Do not consume the sample: the caller must still update the combat
// registry when the same action NPC is registered there as well.
r.Npcs[sample.AgentId] = (new Vec3(p.X, p.Y, p.Z), npc.Template);
}
finally { r.Gate.Release(); }
}
private static void RememberPoliceEscortPosition4229938(TcpSession session, PoliceRuntime4229938 r)
{
// Native multi-interaction moves the pair locally, sometimes without a
// separate NPC movement report. At a confirmed escort transition the
// partner is beside the player, never at its original spawn position.
var state = GetStateIfExists(session);
if (state is not { HasLastReportedPlayerTransform: true }
|| !r.Npcs.TryGetValue(r.Agent, out var npc)) return;
var p = state.LastReportedPlayerPosition;
if (float.IsFinite(p.X) && float.IsFinite(p.Y) && float.IsFinite(p.Z))
r.Npcs[r.Agent] = (p, npc.Template);
}
// All helpers except ReleasePoliceVehiclePassengers4229938 own the caller's
// police Gate. Never acquire it while holding a vehicle SyncRoot lock.
private static bool ReservePoliceSeat4229938(TcpSession session, PoliceRuntime4229938 r,
PoliceStoryPayload4229938 request)
{
if (r.Passengers.ContainsKey(r.Agent) || r.PendingBoarding != null) return false;
var vehicles = GetVehicleRuntimeState(session);
lock (vehicles.SyncRoot)
{
if (!PoliceEscortVehicleRequestValid4229938(session, request)
|| r.Passengers.Values.Any(x => x.Vehicle == request.VehicleId && x.Seat == request.SeatIndex)) return false;
var passenger = new PolicePassenger4229938(r.Agent, request.VehicleId, (byte)request.SeatIndex);
if (vehicles.Vehicles.TryGetValue(passenger.Vehicle, out var vehicle))
vehicle.SeatReservations[passenger.Seat] = passenger.Agent;
r.PendingBoarding = passenger;
return true;
}
}
private static void CancelPoliceSeatReservation4229938(TcpSession session, PoliceRuntime4229938 r)
{
if (r.PendingBoarding is not { } passenger) return;
var vehicles = GetVehicleRuntimeState(session);
lock (vehicles.SyncRoot)
if (vehicles.Vehicles.TryGetValue(passenger.Vehicle, out var vehicle)
&& vehicle.SeatReservations.TryGetValue(passenger.Seat, out var owner) && owner == passenger.Agent)
vehicle.SeatReservations.Remove(passenger.Seat);
r.PendingBoarding = null;
}
internal static byte[] PoliceBoardingInfoWire4229938(ulong agent, ulong vehicle, byte seat)
{
// Auto.WriteClientBoardingInfo, then SyncUnitBoardingInfo's force bool.
// 127C5F90 -> 162ABD00 -> LogicAgentAttachVehicleHandle.Setup: native
// seat offsets/rotation and the attach handle follow the vehicle each frame.
// VehicleUId=0 removes BOTH the attachment and the native seat registration.
var w = new UxWriter().U8(255).U64(agent).U64(vehicle).U8(seat);
for (int i = 0; i < 6; i++) w.F32(0);
return w.Bool(false).Bool(false).U32(0).U32(0).Bool(true).ToArray();
}
private static async Task SendPolicePassengerBinding4229938(TcpSession session, PolicePassenger4229938 p, bool board)
{
// The current boarding state selects the vehicle-authored seated action,
// not a generic POI sitting animation. Also send the NPC attachment RPC:
// the client supports both relation-center and legacy agent attachment paths.
await session.NotifyAsync(MethodId.SyncUnitVehicleStatus, UxSerializer.Serialize(
new GameMethods.NewClientBoardingInfo4229938
{ EntityId = p.Agent, VehicleUId = p.Vehicle, SeatIndex = p.Seat, Status = board ? (byte)4 : (byte)0 }), session.ClosedToken);
await session.NotifyAsync(MethodId.SyncUnitBoardingInfo,
PoliceBoardingInfoWire4229938(p.Agent, board ? p.Vehicle : 0, p.Seat), session.ClosedToken);
}
private static async Task PreparePoliceAlighting4229938(TcpSession session, PolicePassenger4229938 p)
{
// The persistent legacy handle pins the NPC root in LateUpdate. Native
// TryRemoveUnitAttachToVehicle only handles the other attachment path.
// Clear that client handle, then restore vehicle/seat identity in the
// native Leaving state (5). Keep server Passengers/SeatOccupants intact.
// No position RPC: the authored vehicle action owns movement from here.
try
{
await session.NotifyAsync(MethodId.SyncUnitBoardingInfo,
PoliceBoardingInfoWire4229938(p.Agent, 0, p.Seat), session.ClosedToken);
await session.NotifyAsync(MethodId.SyncUnitVehicleStatus, UxSerializer.Serialize(
new GameMethods.NewClientBoardingInfo4229938
{ EntityId = p.Agent, VehicleUId = p.Vehicle, SeatIndex = p.Seat, Status = 5 }), session.ClosedToken);
session.Log.Info($"[LIXI-POLICE] alight-handoff-v1 agent={p.Agent} vehicle={p.Vehicle} seat={p.Seat}; server occupancy retained, no teleport");
}
catch
{
await SendPolicePassengerBinding4229938(session, p, board: true);
throw;
}
}
private static async Task CommitPolicePassenger4229938(TcpSession session, PoliceRuntime4229938 r, PolicePassenger4229938 p)
{
var vehicles = GetVehicleRuntimeState(session);
lock (vehicles.SyncRoot)
{
if (vehicles.Vehicles.TryGetValue(p.Vehicle, out var vehicle))
{
if (vehicle.SeatOccupants.TryGetValue(p.Seat, out var occupant) && occupant != p.Agent
|| vehicle.SeatReservations.TryGetValue(p.Seat, out var reserved) && reserved != p.Agent)
{ session.Log.Warn($"[LIXI-POLICE] boarding commit seat changed vehicle={p.Vehicle} seat={p.Seat}"); return; }
vehicle.SeatReservations.Remove(p.Seat);
vehicle.SeatOccupants[p.Seat] = p.Agent;
}
r.Passengers[p.Agent] = p;
r.PausedEscorts.Remove(p.Agent);
}
try
{
await SendPolicePassengerBinding4229938(session, p, true);
await SendPoliceButtons4229938(session, [p.Agent], GetStateIfExists(session)?.ActiveSpiritUnitId ?? 0,
true, releasePassenger: true);
session.Log.Info($"[LIXI-POLICE] passenger-bound agent={p.Agent} vehicle={p.Vehicle} seat={p.Seat} native=SyncUnitBoardingInfo+SyncUnitVehicleStatus; get-out enabled");
}
catch
{
await ReleasePolicePassenger4229938(session, r, p.Agent, besidePlayer: false);
throw;
}
}
private static Vec3 PoliceTargetPosition4229938(TcpSession session, PoliceRuntime4229938 r, ulong id, Vec3 fallback)
{
if (!r.Passengers.TryGetValue(id, out var passenger)) return fallback;
var vehicles = GetVehicleRuntimeState(session);
lock (vehicles.SyncRoot)
if (vehicles.Vehicles.TryGetValue(passenger.Vehicle, out var vehicle))
return new Vec3(vehicle.Position.X, vehicle.Position.Y, vehicle.Position.Z);
// Unknown map vehicles have no server transform. Do NOT pretend the
// original spawn point is still the passenger's location.
return new Vec3(float.NaN, float.NaN, float.NaN);
}
private static async Task ReleasePolicePassenger4229938(TcpSession session, PoliceRuntime4229938 r, ulong agent, bool besidePlayer)
{
if (!r.Passengers.TryGetValue(agent, out var p)) return;
await PoliceAlightVisualHeight4229938(session, agent, restore: true);
var state = GetStateIfExists(session);
var previous = r.Npcs.TryGetValue(agent, out var npc) ? npc.Position : default;
var position = PoliceTargetPosition4229938(session, r, agent, previous);
float facing = 0;
if (besidePlayer && state is { HasLastReportedPlayerTransform: true })
{
facing = float.IsFinite(state.LastReportedPlayerRotation.Y) ? state.LastReportedPlayerRotation.Y : 0;
float radians = facing * MathF.PI / 180f;
position = new Vec3(state.LastReportedPlayerPosition.X + MathF.Cos(radians) * 1.5f,
state.LastReportedPlayerPosition.Y, state.LastReportedPlayerPosition.Z - MathF.Sin(radians) * 1.5f);
}
else if (float.IsFinite(position.X) && float.IsFinite(position.Y) && float.IsFinite(position.Z))
{
// Cleanup must not leave the detached NPC at the car's center.
// Vehicle removal/scene cleanup has no surviving paired animation.
position = new Vec3(position.X + (p.Seat % 2 == 0 ? 2f : -2f), position.Y, position.Z);
}
await SendPolicePassengerBinding4229938(session, p, false);
if (float.IsFinite(position.X) && float.IsFinite(position.Y) && float.IsFinite(position.Z))
await session.NotifyAsync(MethodId.SyncUnitPosition_P,
UxSerializer.Serialize(WorldCodec.PositionP(agent, position, facing, setPositionType: 6)), session.ClosedToken);
var vehicles = GetVehicleRuntimeState(session);
lock (vehicles.SyncRoot)
{
if (vehicles.Vehicles.TryGetValue(p.Vehicle, out var vehicle))
{
if (vehicle.SeatOccupants.TryGetValue(p.Seat, out var occupant) && occupant == agent)
vehicle.SeatOccupants.Remove(p.Seat);
if (vehicle.SeatReservations.TryGetValue(p.Seat, out var reserved) && reserved == agent)
vehicle.SeatReservations.Remove(p.Seat);
}
}
r.Passengers.Remove(agent);
r.PausedEscorts.Add(agent);
if (r.Npcs.ContainsKey(agent) && float.IsFinite(position.X) && float.IsFinite(position.Y) && float.IsFinite(position.Z))
r.Npcs[agent] = (position, npc.Template);
await SendPoliceButtons4229938(session, [agent], state?.ActiveSpiritUnitId ?? 0, LixiReady(state));
session.Log.Info($"[LIXI-POLICE] passenger-released agent={agent} vehicle={p.Vehicle} seat={p.Seat} fallback={besidePlayer}");
}
private static async Task WatchPoliceAlighting4229938(TcpSession session, uint child, int generation, ulong owner)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(20), session.ClosedToken);
if (!PoliceRuntimes.TryGetValue(session, out var r)) return;
await r.Gate.WaitAsync(session.ClosedToken);
try
{
if (r.Child != child || !r.Alighting || r.Generation != generation
|| GetStateIfExists(session)?.ActiveSpiritUnitId != owner) return;
// Close restores the original seated attachment on failure.
await ClosePoliceInteraction4229938(session, r, "get-out timeout; restore original seat");
}
finally { r.Gate.Release(); }
}
catch (OperationCanceledException) when (session.ClosedToken.IsCancellationRequested) { }
catch (Exception ex) { session.Log.Warn($"[LIXI-POLICE] get-out cleanup failed: {ex.Message}"); }
}
private static async Task ReleasePoliceVehiclePassengers4229938(TcpSession session, ulong vehicleId)
{
if (!PoliceRuntimes.TryGetValue(session, out var r)) return;
await r.Gate.WaitAsync(session.ClosedToken);
try
{
if (r.PendingBoarding?.Vehicle == vehicleId)
await ClosePoliceInteraction4229938(session, r, "boarding vehicle removed");
foreach (var passenger in r.Passengers.Values.Where(x => x.Vehicle == vehicleId).ToArray())
await ReleasePolicePassenger4229938(session, r, passenger.Agent, besidePlayer: false);
}
finally { r.Gate.Release(); }
}
}