642 lines
26 KiB
C#
642 lines
26 KiB
C#
using Ananta.SDK.Network;
|
|
using Ananta.SDK.Rpc;
|
|
using Ananta.SDK.Serialization;
|
|
using Ananta.Server.ClientData.Client4229938;
|
|
using Ananta.Server.Configuration;
|
|
using Ananta.Server.Protocol.Client4229938;
|
|
using Ananta.Server.RpcTypes.Client4229938;
|
|
using Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
|
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
|
|
|
namespace Ananta.Server.Handlers.Game;
|
|
|
|
/// <summary>
|
|
/// Private-server vehicle surface (build 4229938): summon + owned fleet + drive-loop accepts.
|
|
/// Spawn uses the proven 3-notify sequence (SyncLogicVehicleEnter + SyncSpawnVehicle +
|
|
/// SyncChangeVehicleInteractable) with 5.5m-right/+0.15m offsets; driving itself is
|
|
/// simulated client-side.
|
|
/// </summary>
|
|
internal sealed partial class GameRouter
|
|
{
|
|
private static long _vehicleEntitySeq = 300000000000L;
|
|
private static long _vehicleTaskSeq;
|
|
private static readonly Dictionary<ulong, SummonedVehicle> SummonedVehicles = new();
|
|
private static readonly object SummonedVehiclesSync = new();
|
|
|
|
internal sealed class SummonedVehicle
|
|
{
|
|
internal uint ConfigId;
|
|
internal float X;
|
|
internal float Y;
|
|
internal float Z;
|
|
internal float Yaw;
|
|
internal bool HasFix;
|
|
// Boarding state (S011 story flow): seat layout + occupancy.
|
|
internal int SeatCount = 2;
|
|
internal bool Interactable = true;
|
|
internal ulong ControllerPid;
|
|
internal readonly Dictionary<byte, ulong> SeatReservations = new();
|
|
internal readonly Dictionary<byte, ulong> SeatOccupants = new();
|
|
}
|
|
|
|
private static ulong _lastSummonedEntity;
|
|
|
|
internal static ulong LastSummonedEntity()
|
|
{
|
|
lock (SummonedVehiclesSync)
|
|
return _lastSummonedEntity;
|
|
}
|
|
|
|
internal static bool TryGetSummoned(ulong entityId, out SummonedVehicle? vehicle)
|
|
{
|
|
lock (SummonedVehiclesSync)
|
|
return SummonedVehicles.TryGetValue(entityId, out vehicle);
|
|
}
|
|
|
|
internal static List<(ulong EntityId, uint ConfigId, float X, float Y, float Z, float Yaw, bool HasFix)> SummonedSnapshot()
|
|
{
|
|
lock (SummonedVehiclesSync)
|
|
return SummonedVehicles
|
|
.Select(kv => (kv.Key, kv.Value.ConfigId, kv.Value.X, kv.Value.Y, kv.Value.Z, kv.Value.Yaw, kv.Value.HasFix))
|
|
.ToList();
|
|
}
|
|
|
|
internal static void RegisterSummoned(ulong entityId, uint configId)
|
|
{
|
|
lock (SummonedVehiclesSync)
|
|
{
|
|
if (!SummonedVehicles.TryGetValue(entityId, out var existing))
|
|
SummonedVehicles[entityId] = existing = new SummonedVehicle { ConfigId = configId };
|
|
else
|
|
existing.ConfigId = configId;
|
|
existing.SeatCount = LookupSeatCount(configId);
|
|
existing.Interactable = true;
|
|
_lastSummonedEntity = entityId;
|
|
}
|
|
}
|
|
|
|
internal static void RegisterSummoned(ulong entityId, uint configId, float x, float y, float z, float yaw)
|
|
{
|
|
lock (SummonedVehiclesSync)
|
|
{
|
|
SummonedVehicles[entityId] = new SummonedVehicle
|
|
{
|
|
ConfigId = configId,
|
|
X = x,
|
|
Y = y,
|
|
Z = z,
|
|
Yaw = yaw,
|
|
HasFix = float.IsFinite(x) && float.IsFinite(y) && float.IsFinite(z),
|
|
SeatCount = LookupSeatCount(configId),
|
|
Interactable = true,
|
|
};
|
|
_lastSummonedEntity = entityId;
|
|
}
|
|
}
|
|
|
|
private static int LookupSeatCount(uint configId)
|
|
{
|
|
try
|
|
{
|
|
if (VehicleCatalog4229938.TryGet(configId, out var config)
|
|
&& config.SeatCount is >= 1 and <= 16)
|
|
return config.SeatCount;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
internal static void ForgetSummoned(ulong entityId)
|
|
{
|
|
lock (SummonedVehiclesSync)
|
|
SummonedVehicles.Remove(entityId);
|
|
}
|
|
|
|
internal static void TrackVehicleMove(ulong entityId, float x, float y, float z, float yaw)
|
|
{
|
|
if (!float.IsFinite(x) || !float.IsFinite(y) || !float.IsFinite(z) || !float.IsFinite(yaw))
|
|
return;
|
|
lock (SummonedVehiclesSync)
|
|
{
|
|
// Only track server-summoned entities; traffic/AI vehicles stay untracked
|
|
// so the registry cannot grow unbounded.
|
|
if (SummonedVehicles.TryGetValue(entityId, out var existing))
|
|
{
|
|
existing.X = x;
|
|
existing.Y = y;
|
|
existing.Z = z;
|
|
existing.Yaw = yaw;
|
|
existing.HasFix = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Handler(MethodId.AskSummonVehicle, HandlerPacketKind.Invoke)]
|
|
private async Task AskSummonVehicle(Connection conn, UxRpcMessage msg)
|
|
{
|
|
if (!PrivateServerConfigStore.Current.Gameplay.Vehicles.Enabled)
|
|
{
|
|
await conn.ReturnEmptyAsync(msg, 1);
|
|
return;
|
|
}
|
|
if (!msg.TryGetArgs<SceneMethods.AskSummonVehicle>(out var args) || args is null
|
|
|| msg.Body.Length != 20 || args.VehicleConfigId == 0
|
|
|| !float.IsFinite(args.Position.X) || !float.IsFinite(args.Position.Y) || !float.IsFinite(args.Position.Z)
|
|
|| !float.IsFinite(args.FacingDirection))
|
|
{
|
|
conn.Log.Warn($"[VEHICLE] AskSummonVehicle bad args bytes={msg.Body.Length}");
|
|
await conn.ReturnEmptyAsync(msg, 1);
|
|
return;
|
|
}
|
|
|
|
var result = await SpawnDirectAsync(
|
|
msg.Context.Session,
|
|
args.VehicleConfigId,
|
|
new Vec3(args.Position.X, args.Position.Y, args.Position.Z),
|
|
args.FacingDirection,
|
|
rightOffset: 5.5f,
|
|
sourceType: 2,
|
|
reason: "client-summon");
|
|
if (!result.Ok)
|
|
{
|
|
conn.Log.Warn($"[VEHICLE] summon rejected: {result.Message}");
|
|
await conn.ReturnEmptyAsync(msg, 2);
|
|
return;
|
|
}
|
|
await conn.ReturnAsync(msg, new SceneMethods.SummonVehicleResult
|
|
{
|
|
VehicleEntityId = result.EntityId,
|
|
TaskToken = result.Token,
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Direct scene spawn shared by client summon and the debug panel: SyncLogicVehicleEnter
|
|
/// + SyncSpawnVehicle (interactable) + SyncChangeVehicleInteractable. Offsets proven by V2:
|
|
/// rightOffset meters to the right of the facing, +0.15m up.
|
|
/// </summary>
|
|
internal static async Task<(bool Ok, string Message, ulong EntityId, ulong Token, Vec3 SpawnPos)> SpawnDirectAsync(
|
|
TcpSession session, uint configId, Vec3 nearPos, float facing, float rightOffset, byte sourceType, string reason)
|
|
{
|
|
if (configId == 0)
|
|
return (false, "vehicle 0 does not exist", 0, 0, default);
|
|
|
|
// Unknown / model-less ids are allowed with a warning (newer client data may know
|
|
// more vehicles than our dump); the client validates against its own configs.
|
|
var seatCount = 2;
|
|
if (VehicleCatalog4229938.TryGet(configId, out var config))
|
|
{
|
|
if (config.SeatCount is >= 1 and <= 16)
|
|
seatCount = config.SeatCount;
|
|
if (string.IsNullOrWhiteSpace(config.Model))
|
|
session.Log.Warn($"[VEHICLE] config {configId} has no prefab model in dump, trying anyway");
|
|
}
|
|
else
|
|
{
|
|
session.Log.Warn($"[VEHICLE] config {configId} not in VehicleConfig dump, trying with {seatCount} seats");
|
|
}
|
|
|
|
var yaw = facing * (MathF.PI / 180f);
|
|
var spawn = new Vec3(
|
|
nearPos.X + MathF.Cos(yaw) * rightOffset,
|
|
nearPos.Y + 0.15f,
|
|
nearPos.Z - MathF.Sin(yaw) * rightOffset);
|
|
var entityId = (ulong)Interlocked.Increment(ref _vehicleEntitySeq);
|
|
var token = (ulong)Interlocked.Increment(ref _vehicleTaskSeq);
|
|
RegisterSummoned(entityId, configId, spawn.X, spawn.Y, spawn.Z, facing);
|
|
|
|
var euler = new SceneMethods.UxVector3(0f, facing, 0f);
|
|
var pos = new SceneMethods.UxVector3(spawn.X, spawn.Y, spawn.Z);
|
|
await session.NotifyAsync(MethodId.SyncLogicVehicleEnter, UxSerializer.Serialize(new SceneMethods.SyncLogicVehicleEnter
|
|
{
|
|
EntityId = entityId,
|
|
VehicleConfigId = configId,
|
|
CreateSourceType = sourceType,
|
|
Parts = [],
|
|
SuitId = 0,
|
|
LicensePlate = string.Empty,
|
|
Interactable = true,
|
|
MoveToken = 0,
|
|
Position = pos,
|
|
EulerAngles = euler,
|
|
VehicleSpoonName = null,
|
|
}), CancellationToken.None);
|
|
await session.NotifyAsync(MethodId.SyncSpawnVehicle, UxSerializer.Serialize(new SceneMethods.VehicleClientInfo
|
|
{
|
|
ControllerPid = 0,
|
|
CreateSourceType = sourceType,
|
|
EntityId = entityId,
|
|
VehicleConfigId = configId,
|
|
Parts = [],
|
|
SuitId = 0,
|
|
Position = pos,
|
|
Facing = facing,
|
|
EulerAngles = euler,
|
|
Velocity = 0,
|
|
IsStatic = false,
|
|
DeformStatus = 0,
|
|
SeatInfos = Enumerable.Range(0, seatCount).Select(i => new SceneMethods.RaidVehicleSeatInfo
|
|
{
|
|
EntityId = 0,
|
|
SeatIndex = (byte)i,
|
|
SeatState = 0,
|
|
DestroyRelated = false,
|
|
}).ToList(),
|
|
SpoonId = 0,
|
|
IsDynamicGo = false,
|
|
VehicleEnemyId = 0,
|
|
DisableNavigation = false,
|
|
Interactable = true,
|
|
MoveToken = 0,
|
|
LicensePlate = string.Empty,
|
|
}), CancellationToken.None);
|
|
await session.NotifyAsync(MethodId.SyncChangeVehicleInteractable, UxSerializer.Serialize(
|
|
new SceneMethods.SyncChangeVehicleInteractable { VehicleInstanceId = entityId, Interactable = true }),
|
|
CancellationToken.None);
|
|
session.Log.Info($"[VEHICLE] DIRECT_SPAWN reason={reason} config={configId} entity={entityId} token={token} seats={seatCount} spawn=({spawn.X:F1},{spawn.Y:F1},{spawn.Z:F1})");
|
|
return (true, "spawned", entityId, token, spawn);
|
|
}
|
|
|
|
internal static async Task DestroyDirectAsync(TcpSession session, ulong entityId, string reason)
|
|
{
|
|
ForgetSummoned(entityId);
|
|
ResetVehicleStoryForEntity(session, entityId);
|
|
await session.NotifyAsync(MethodId.SyncDestroyVehicle, UxSerializer.Serialize(
|
|
new SceneMethods.DestroyVehicle { VehicleEntityId = entityId, VehicleDestroyType = 0, Distance = 0, DynamicGoId = 0 }),
|
|
CancellationToken.None);
|
|
session.Log.Info($"[VEHICLE] destroy entity={entityId} reason={reason}");
|
|
}
|
|
|
|
internal static SceneMethods.AskGetUnlockedVehiclesResult BuildUnlockedResult()
|
|
{
|
|
var fleet = PrivateServerConfigStore.Current.Gameplay.Vehicles.FleetIds;
|
|
return new SceneMethods.AskGetUnlockedVehiclesResult
|
|
{
|
|
Vehicles = fleet.Select(id => new SceneMethods.PlayerVehicleClientDetail
|
|
{
|
|
Id = id,
|
|
Parts = [],
|
|
SuitId = 0,
|
|
IsPersistent = true,
|
|
}).ToList(),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Push the owned fleet right after world entry finalization (first gameplay movement),
|
|
/// so the phone/garage UI is populated without a manual resync. Once per entry.
|
|
/// </summary>
|
|
internal static async Task PublishGarageAsync(RpcContext ctx)
|
|
{
|
|
if (!PrivateServerConfigStore.Current.Gameplay.Vehicles.Enabled)
|
|
return;
|
|
if (!(ctx.Session.Items.TryGetValue(WorldStateKey, out var raw) && raw is WorldEntryState world))
|
|
return;
|
|
bool first;
|
|
lock (world.SyncRoot)
|
|
{
|
|
first = !world.GaragePublished;
|
|
if (first)
|
|
world.GaragePublished = true;
|
|
}
|
|
if (!first)
|
|
return;
|
|
var result = BuildUnlockedResult();
|
|
await ctx.NotifyAsync(MethodId.SyncAllUnlockedVehicles, result);
|
|
ctx.Session.Log.Info($"[VEHICLE] garage auto-published count={result.Vehicles.Count}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Push Aether vehicle-AI init once per world entry (mirrors V2): RaidId +
|
|
/// zone-graph handle + empty lists. The client DriveManager gates vehicle
|
|
/// materialization on this init; without it spawns stay invisible.
|
|
/// </summary>
|
|
internal static async Task PublishAetherInitAsync(RpcContext ctx)
|
|
{
|
|
if (!PrivateServerConfigStore.Current.Gameplay.Vehicles.Enabled)
|
|
return;
|
|
if (!(ctx.Session.Items.TryGetValue(WorldStateKey, out var raw) && raw is WorldEntryState world))
|
|
return;
|
|
bool first;
|
|
uint raidId;
|
|
lock (world.SyncRoot)
|
|
{
|
|
first = !world.AetherVehicleInitSent;
|
|
if (first)
|
|
world.AetherVehicleInitSent = true;
|
|
raidId = world.ActiveRaidId;
|
|
}
|
|
if (!first)
|
|
return;
|
|
var settings = PrivateServerConfigStore.Current.Gameplay.Vehicles;
|
|
var hasZoneGraph = raidId == PrivateServerConfigStore.Current.World.RaidId;
|
|
await ctx.NotifyAsync(MethodId.SyncAetherAIInitDatas, new SceneMethods.AetherAIInitData
|
|
{
|
|
RaidId = raidId,
|
|
HasZoneGraph = hasZoneGraph,
|
|
ZoneStorageDataHandle = hasZoneGraph ? settings.ZoneStorageDataHandle : 0,
|
|
Intersections = [],
|
|
Vehicles = [],
|
|
StaticVehicles = [],
|
|
});
|
|
ctx.Session.Log.Info($"[VEHICLE] aether-init raid={raidId} zoneGraph={hasZoneGraph} lists=empty");
|
|
}
|
|
|
|
[Handler(MethodId.AskGetUnlockedVehicles, HandlerPacketKind.Invoke)]
|
|
private Task AskGetUnlockedVehicles(Connection conn, UxRpcMessage msg)
|
|
{
|
|
var result = BuildUnlockedResult();
|
|
conn.Log.Info($"[VEHICLE] unlocked list -> {result.Vehicles.Count} vehicles");
|
|
return conn.ReturnAsync(msg, result);
|
|
}
|
|
|
|
// Client-driven enter/exit + movement reports. The client owns the simulation;
|
|
// the server accepts them so the drive loop never stalls waiting for a reply.
|
|
// AskVehicleMove additionally feeds the debug panel's live coordinates.
|
|
[Handler(MethodId.AskVehicleMove, HandlerPacketKind.Notify)]
|
|
private Task AskVehicleMove(Connection conn, UxRpcMessage msg)
|
|
{
|
|
try
|
|
{
|
|
var data = msg.GetArgs<SceneMethods.RaidVehicleSyncData>();
|
|
TrackVehicleMove(data.Id, data.Position.X, data.Position.Y, data.Position.Z, data.FacingDirection);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
conn.Log.Warn($"[VEHICLE] move parse failed, accepted anyway: {ex.Message}");
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
[Handler(MethodId.AskPlayerStartEnterOrExitVehicle, HandlerPacketKind.Notify)]
|
|
private async Task AskPlayerStartEnterOrExitVehicle(Connection conn, UxRpcMessage msg)
|
|
{
|
|
try
|
|
{
|
|
var drive = msg.GetArgs<SceneMethods.PlayerVehicleDriveStateInfo>();
|
|
conn.Log.Info($"[VEHICLE] AskPlayerStartEnterOrExitVehicle vehicle={drive.VehicleEntityId} enter={drive.EnterOrLeave} seat={drive.SeatIndex}");
|
|
if (drive.EnterOrLeave)
|
|
{
|
|
Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.NotifyVehicleBoarding(drive.VehicleEntityId);
|
|
await ForceEnterVehicleAsync(conn.Session, drive.VehicleEntityId);
|
|
}
|
|
else
|
|
{
|
|
await ForceExitVehicleAsync(conn.Session);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
conn.Log.Warn($"[VEHICLE] AskPlayerStartEnterOrExitVehicle failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
[Handler(MethodId.AskPlayerFinishEnterOrExitVehicle, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskVehicleStartMove, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskVehicleStopMove, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskVehicleHorn, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskEnterVehicleIndoor, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskExitVehicleIndoor, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.ReportDrivingVehicle, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskKillVehicle, HandlerPacketKind.Notify)]
|
|
private Task VehicleNotifyAccept(Connection conn, UxRpcMessage msg) => Task.CompletedTask;
|
|
|
|
// Drive-loop invokes with void returns (combat/contact/horn-nitro/state signals).
|
|
[Handler(MethodId.VehicleDriveStateChange, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskChangeGoVehicleDriveState, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskVehicleDeadEnd, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskVehicleNitro, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskVehicleStuck, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskVehicleHit, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskVehicleHitEnd, HandlerPacketKind.Invoke)]
|
|
private Task VehicleInvokeAccept(Connection conn, UxRpcMessage msg)
|
|
=> conn.ReturnEmptyOkAsync(msg);
|
|
|
|
// Autonomous Driving & Vehicle Pathfinder Handlers
|
|
[Handler(MethodId.AskVehicleStartAutonomousDriving, HandlerPacketKind.Invoke)]
|
|
private async Task AskVehicleStartAutonomousDriving(Connection conn, UxRpcMessage msg)
|
|
{
|
|
var vehicleEntityId = LastSummonedEntity();
|
|
conn.Log.Info($"[AUTODRIVE] start requested vehicleEntity={vehicleEntityId}");
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|
|
|
var syncVehicle = new SceneMethods.SyncVehicleAutonomousDrivingState
|
|
{
|
|
VehicleEntityId = vehicleEntityId,
|
|
IsStart = true
|
|
};
|
|
await conn.NotifyAsync(MethodId.IGameSceneToClient_SyncVehicleAutonomousDrivingState, syncVehicle);
|
|
|
|
var syncPlayer = new SceneMethods.SyncPlayerAutonomousDrivingState
|
|
{
|
|
IsInOverrideMode = false,
|
|
IsAutoDrivingBlocked = false,
|
|
IsImmersiveModeBlocked = false
|
|
};
|
|
await conn.NotifyAsync(MethodId.IGameSceneToClient_SyncPlayerAutonomousDrivingState, syncPlayer);
|
|
}
|
|
|
|
[Handler(MethodId.AskVehicleStopAutonomousDriving, HandlerPacketKind.Invoke)]
|
|
private async Task AskVehicleStopAutonomousDriving(Connection conn, UxRpcMessage msg)
|
|
{
|
|
var vehicleEntityId = LastSummonedEntity();
|
|
conn.Log.Info($"[AUTODRIVE] stop requested vehicleEntity={vehicleEntityId}");
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|
|
|
var syncVehicle = new SceneMethods.SyncVehicleAutonomousDrivingState
|
|
{
|
|
VehicleEntityId = vehicleEntityId,
|
|
IsStart = false
|
|
};
|
|
await conn.NotifyAsync(MethodId.IGameSceneToClient_SyncVehicleAutonomousDrivingState, syncVehicle);
|
|
}
|
|
|
|
[Handler(MethodId.AskVehicleChangeAutonomousDrivingTarget, HandlerPacketKind.Invoke)]
|
|
private Task AskVehicleChangeAutonomousDrivingTarget(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[AUTODRIVE] target position changed");
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|
}
|
|
|
|
[Handler(MethodId.AskVehicleCancelAutonomousDrivingTarget, HandlerPacketKind.Invoke)]
|
|
private Task AskVehicleCancelAutonomousDrivingTarget(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[AUTODRIVE] target position cancelled");
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|
}
|
|
|
|
[Handler(MethodId.AskVehicleNavigationPathPoints, HandlerPacketKind.Invoke)]
|
|
private Task AskVehicleNavigationPathPoints(Connection conn, UxRpcMessage msg)
|
|
{
|
|
uint navReqId = 0;
|
|
SceneMethods.UxVector3 targetPos = new(0, 0, 0);
|
|
try
|
|
{
|
|
var args = msg.GetArgs<SceneMethods.AskVehicleNavigationPathPointsArgs>();
|
|
navReqId = args.NavReqId;
|
|
targetPos = args.TargetPosition;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
conn.Log.Warn($"[NAV] parse path points failed: {ex.Message}");
|
|
}
|
|
|
|
var state = conn.Session is not null ? GetStateIfExists(conn.Session) : null;
|
|
var playerPos = state?.LastReportedPlayerPosition ?? Profile.WorldSpawn;
|
|
var startUx = new SceneMethods.UxVector3(playerPos.X, playerPos.Y, playerPos.Z);
|
|
|
|
// Generate intermediate waypoints between player and target so GPS renders along roads
|
|
var points = GenerateNavWaypoints(startUx, targetPos);
|
|
|
|
var result = new SceneMethods.VehicleNavigationPathPointsResult
|
|
{
|
|
NavReqId = navReqId,
|
|
Points = points,
|
|
CenterPoints = points
|
|
};
|
|
conn.Log.Info($"[NAV] path points req={navReqId} pts={points.Count} from=({playerPos.X:F1},{playerPos.Y:F1},{playerPos.Z:F1}) to=({targetPos.X:F1},{targetPos.Y:F1},{targetPos.Z:F1})");
|
|
return conn.ReturnAsync(msg, result);
|
|
}
|
|
|
|
private static List<SceneMethods.UxVector3> GenerateNavWaypoints(SceneMethods.UxVector3 start, SceneMethods.UxVector3 end)
|
|
{
|
|
var list = new List<SceneMethods.UxVector3> { start };
|
|
float dx = end.X - start.X;
|
|
float dy = end.Y - start.Y;
|
|
float dz = end.Z - start.Z;
|
|
float dist = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
// Generate smooth path segments for vehicle road line
|
|
if (dist > 25f)
|
|
{
|
|
int steps = Math.Clamp((int)(dist / 20f), 2, 12);
|
|
for (int i = 1; i < steps; i++)
|
|
{
|
|
float t = (float)i / steps;
|
|
list.Add(new SceneMethods.UxVector3(start.X + dx * t, start.Y + dy * t, start.Z + dz * t));
|
|
}
|
|
}
|
|
list.Add(end);
|
|
return list;
|
|
}
|
|
|
|
[Handler(MethodId.AskVehicleNavigationPathLength, HandlerPacketKind.Invoke)]
|
|
private Task AskVehicleNavigationPathLength(Connection conn, UxRpcMessage msg)
|
|
{
|
|
var state = conn.Session is not null ? GetStateIfExists(conn.Session) : null;
|
|
var playerPos = state?.LastReportedPlayerPosition ?? Profile.WorldSpawn;
|
|
return conn.ReturnAsync(msg, 150.0f);
|
|
}
|
|
|
|
[Handler(MethodId.AskVehicleNavigationPathLengthList, HandlerPacketKind.Invoke)]
|
|
private Task AskVehicleNavigationPathLengthList(Connection conn, UxRpcMessage msg)
|
|
{
|
|
return conn.ReturnAsync(msg, new List<float> { 150.0f });
|
|
}
|
|
|
|
// Metro / Subway System Handlers
|
|
[Handler(MethodId.AskGetAllMetroInfos, HandlerPacketKind.Invoke)]
|
|
private Task AskGetAllMetroInfos(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[METRO] GetAllMetroInfos requested -> returning active metro train lines");
|
|
// Active trains for rail lines 1, 2, 3, 4 across the city
|
|
var list = new List<MetroClientInfo>
|
|
{
|
|
new() { Id = 1001, LineId = 1, ElapsedTime = 12.0f, IsFinalTrain = false },
|
|
new() { Id = 1002, LineId = 2, ElapsedTime = 25.0f, IsFinalTrain = false },
|
|
new() { Id = 1003, LineId = 3, ElapsedTime = 38.0f, IsFinalTrain = false },
|
|
new() { Id = 1004, LineId = 4, ElapsedTime = 5.0f, IsFinalTrain = false }
|
|
};
|
|
return conn.ReturnAsync(msg, list);
|
|
}
|
|
|
|
[Handler(MethodId.AskMetroGadgetIds, HandlerPacketKind.Invoke)]
|
|
private Task AskMetroGadgetIds(Connection conn, UxRpcMessage msg)
|
|
{
|
|
return conn.ReturnAsync(msg, new List<MetroCarriageGadgetInfos>());
|
|
}
|
|
|
|
[Handler(MethodId.AskPlayerOnMetro, HandlerPacketKind.Invoke)]
|
|
private Task AskPlayerOnMetro(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[METRO] player entered/boarded metro train");
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|
}
|
|
|
|
[Handler(MethodId.AskOnMetroEnterStation, HandlerPacketKind.Notify)]
|
|
private Task AskOnMetroEnterStation(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[METRO] player entered station");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
[Handler(MethodId.AskOnMetroExitStation, HandlerPacketKind.Notify)]
|
|
private Task AskOnMetroExitStation(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[METRO] player exited station");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
[Handler(MethodId.AskPlayerLeaveMetro, HandlerPacketKind.Invoke)]
|
|
private Task AskPlayerLeaveMetro(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[METRO] player leave metro requested");
|
|
return conn.ReturnEmptyOkAsync(msg);
|
|
}
|
|
|
|
// Taffy Moto / Monowheel Handlers
|
|
[Handler(MethodId.AskTaffyMotoEnterRush, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskTaffyMotoEnterRush, HandlerPacketKind.Notify)]
|
|
private Task AskTaffyMotoEnterRush(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[TAFFY_MOTO] AskTaffyMotoEnterRush");
|
|
return msg.IsInvoke ? conn.ReturnEmptyOkAsync(msg) : Task.CompletedTask;
|
|
}
|
|
|
|
[Handler(MethodId.AskTaffyMotoLeaveRush, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskTaffyMotoLeaveRush, HandlerPacketKind.Notify)]
|
|
private Task AskTaffyMotoLeaveRush(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[TAFFY_MOTO] AskTaffyMotoLeaveRush");
|
|
return msg.IsInvoke ? conn.ReturnEmptyOkAsync(msg) : Task.CompletedTask;
|
|
}
|
|
|
|
[Handler(MethodId.OnTafeiMotorColliding, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.OnTafeiMotorColliding, HandlerPacketKind.Notify)]
|
|
private Task OnTafeiMotorColliding(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[TAFFY_MOTO] OnTafeiMotorColliding");
|
|
return msg.IsInvoke ? conn.ReturnEmptyOkAsync(msg) : Task.CompletedTask;
|
|
}
|
|
|
|
[Handler(MethodId.AskGetOffMotor, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskGetOffMotor, HandlerPacketKind.Notify)]
|
|
private Task AskGetOffMotor(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[TAFFY_MOTO] AskGetOffMotor");
|
|
return msg.IsInvoke ? conn.ReturnEmptyOkAsync(msg) : Task.CompletedTask;
|
|
}
|
|
|
|
|
|
[Handler(MethodId.AskReleaseVehicleSeat, HandlerPacketKind.Notify)]
|
|
[Handler(MethodId.AskReleaseVehicleSeat, HandlerPacketKind.Invoke)]
|
|
private async Task AskReleaseVehicleSeat(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[VEHICLE] AskReleaseVehicleSeat received");
|
|
if (msg.IsInvoke)
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|
await ForceExitVehicleAsync(conn.Session);
|
|
}
|
|
|
|
[Handler(MethodId.AskChangeCanMoveToDriveSeat, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskChangeCanMoveToDriveSeat, HandlerPacketKind.Notify)]
|
|
private Task AskChangeCanMoveToDriveSeat(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info("[VEHICLE] AskChangeCanMoveToDriveSeat");
|
|
return msg.IsInvoke ? conn.ReturnEmptyOkAsync(msg) : Task.CompletedTask;
|
|
}
|
|
}
|
|
|