248 lines
13 KiB
C#
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(); }
|
|
}
|
|
}
|