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(); } } }