feat: Add metro transit, gadgets, shop, social apps, and traffic models checkpoint
This commit is contained in:
1 parent
caa42b84ed
commit
74e2963a12
79 files changed
+527962
-496
No files matched your search
@@ -2,16 +2,56 @@ using Ananta.SDK.Network;
|
||||
using Ananta.SDK.Serialization;
|
||||
using Ananta.Server.Gameplay.Crowd;
|
||||
using Ananta.Server.Gameplay.Traffic;
|
||||
using Ananta.Server.Gameplay.World;
|
||||
using Ananta.Server.Protocol.Client4229938;
|
||||
using Ananta.Server.RpcTypes.Client4229938;
|
||||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||||
|
||||
namespace Ananta.Server.Handlers.Game;
|
||||
|
||||
internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowdDriver
|
||||
internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowdDriver, IGadgetStreamDriver
|
||||
{
|
||||
public static TrafficAndCrowdDriver Instance { get; } = new();
|
||||
|
||||
public async Task SendGadgetGridAoiAsync(
|
||||
TcpSession session, IReadOnlyList<(int X, int Z)> cells, IReadOnlyList<int> sectorIds, (int X, int Z) playerCell, bool firstPush)
|
||||
{
|
||||
var packet = new SceneMethods.SyncGadgetGridAOIDecrease4229938
|
||||
{
|
||||
AoiInfo = new SceneMethods.GadgetGridAOIIncrease4229938
|
||||
{
|
||||
PlayerStandardIndex = new SceneMethods.GadgetGridIndex4229938(playerCell.Item1, playerCell.Item2),
|
||||
IndexList = cells.Select(c => new SceneMethods.GadgetGridIndex4229938(c.X, c.Z)).ToList(),
|
||||
// Init on the first push of a session, AOIMove afterwards.
|
||||
Reason = firstPush ? (byte)0 : (byte)2,
|
||||
},
|
||||
// Sector list comes from the baked cell map (CellSectors.json) so indoor cells
|
||||
// address their building's sector; falls back to outdoor sector 0.
|
||||
AddGridInfo = new SceneMethods.GridAOIDecrease4229938
|
||||
{
|
||||
SectorIdList = sectorIds.ToList(),
|
||||
BuildingId = 0,
|
||||
StandardIndexList = cells.Select(c => new SceneMethods.GadgetGridIndex4229938(c.X, c.Z)).ToList(),
|
||||
},
|
||||
};
|
||||
await session.NotifyAsync(MethodId.SyncGadgetGridAOIDecrease, UxSerializer.Serialize(packet), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendSceneItemGridAoiAsync(
|
||||
TcpSession session, IReadOnlyList<(int X, int Z)> cells, (int X, int Z) playerCell, bool firstPush)
|
||||
{
|
||||
var packet = new SceneMethods.SyncDestructibleGridAOIIncrease4229938
|
||||
{
|
||||
AoiInfo = new SceneMethods.DestructibleGridAOIIncrease4229938
|
||||
{
|
||||
PlayerStandardIndex = new SceneMethods.GadgetGridIndex4229938(playerCell.Item1, playerCell.Item2),
|
||||
IndexList = cells.Select(c => new SceneMethods.GadgetGridIndex4229938(c.X, c.Z)).ToList(),
|
||||
Reason = firstPush ? (byte)0 : (byte)2,
|
||||
},
|
||||
};
|
||||
await session.NotifyAsync(MethodId.SyncDestructibleGridAOIIncrease, UxSerializer.Serialize(packet), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<(bool Ok, ulong EntityId)> SpawnTrafficVehicleAsync(
|
||||
TcpSession session, uint configId, float x, float y, float z, float yaw)
|
||||
{
|
||||
@@ -27,6 +67,13 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd
|
||||
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, int> _moveTokens = new();
|
||||
|
||||
/// <summary>
|
||||
/// Reset flag for the SyncTeleportVehicle reposition channel. true mirrors the proven
|
||||
/// panel "To me" teleport semantics (hard snap each packet); false lets the client blend
|
||||
/// using the scalar Velocity. Toggle live via POST /api/traffic/config {"moveReset":false}.
|
||||
/// </summary>
|
||||
public static bool VehicleMoveReset = true;
|
||||
|
||||
public async Task SendVehicleMoveAsync(
|
||||
TcpSession session, ulong entityId, float x, float y, float z, float yaw, float vx, float vz)
|
||||
{
|
||||
@@ -42,9 +89,270 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd
|
||||
MoveToken = token,
|
||||
};
|
||||
await session.NotifyAsync(MethodId.SyncVehicleMove, UxSerializer.Serialize(moveData), CancellationToken.None);
|
||||
|
||||
// Channel B: SyncTeleportVehicle — the server->client reposition packet the client is
|
||||
// known to apply (same channel as the panel's "To me"). Sent alongside the mirror
|
||||
// packet above so whichever channel this client build honors moves the car.
|
||||
var teleport = new SceneMethods.SyncTeleportVehicle
|
||||
{
|
||||
EntityId = entityId,
|
||||
Position = new SceneMethods.UxVector3(x, y, z),
|
||||
Rotation = new SceneMethods.UxVector3(0f, yaw, 0f),
|
||||
Velocity = MathF.Sqrt(vx * vx + vz * vz),
|
||||
Reset = VehicleMoveReset,
|
||||
MoveToken = token,
|
||||
};
|
||||
await session.NotifyAsync(MethodId.SyncTeleportVehicle, UxSerializer.Serialize(teleport), CancellationToken.None);
|
||||
|
||||
GameRouter.TrackVehicleMove(entityId, x, y, z, yaw);
|
||||
}
|
||||
|
||||
public async Task SpawnAetherVehicleAsync(
|
||||
TcpSession session, ulong entityId, uint configId, uint colorPartId, int laneHandle,
|
||||
float distanceAlongLane, float speed, double timestamp, float x, float y, float z, float yaw)
|
||||
{
|
||||
// Aether vehicles live only in the client's traffic simulation (VehicleControlType.AetherVehicle);
|
||||
// they are intentionally NOT registered as scene entities / SummonedVehicles.
|
||||
await session.NotifyAsync(MethodId.SyncAetherAIVehicleAddData, UxSerializer.Serialize(
|
||||
new SceneMethods.ClientVehicleInitData4229938
|
||||
{
|
||||
Id = entityId,
|
||||
Position = new SceneMethods.UxVector3(x, y, z),
|
||||
Facing = yaw,
|
||||
EulerAngles = new SceneMethods.UxVector3(0f, yaw, 0f),
|
||||
VehicleConfigId = configId,
|
||||
VehicleColorPartId = colorPartId,
|
||||
VehicleLightState = 0,
|
||||
LaneHandle = laneHandle,
|
||||
DistanceAlongLane = distanceAlongLane,
|
||||
NextVehicleId = 0,
|
||||
Timestamp = timestamp,
|
||||
ControlType = 1,
|
||||
Speed = speed,
|
||||
DustRatio = 0f,
|
||||
Parts = [],
|
||||
SuitId = 0,
|
||||
RandomFraction = Random.Shared.NextSingle(),
|
||||
}), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SpawnAetherVehiclesAsync(TcpSession session, IReadOnlyList<AetherVehicleInit> vehicles)
|
||||
{
|
||||
// The 4229938 client only implements the SINGLE SyncAetherAIVehicleAddData (68916691);
|
||||
// a previous batch id (68571034) does not exist in the client's method table and its
|
||||
// packets were dropped, so flush each spawn as its own notify.
|
||||
//
|
||||
// Full sequence per vehicle (battle-tested in the PrivateServer1 pack): the LEGACY
|
||||
// scene-entity spawn (SyncLogicVehicleEnter + SyncSpawnVehicle) is what creates the
|
||||
// drive-layer BaseVehicle — without it AetherVehicleLifecycle flush drops every car
|
||||
// with "Flush dropped missing BaseVehicle". ChangeVehicleControl pins it to the
|
||||
// aether AI and ForceGo starts the drive immediately.
|
||||
foreach (var v in vehicles)
|
||||
{
|
||||
var pos = new SceneMethods.UxVector3(v.X, v.Y, v.Z);
|
||||
var euler = new SceneMethods.UxVector3(0f, v.Yaw, 0f);
|
||||
await session.NotifyAsync(MethodId.SyncLogicVehicleEnter, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncLogicVehicleEnter
|
||||
{
|
||||
EntityId = v.EntityId,
|
||||
VehicleConfigId = v.ConfigId,
|
||||
CreateSourceType = 0,
|
||||
Parts = [],
|
||||
SuitId = 0,
|
||||
LicensePlate = string.Empty,
|
||||
Interactable = false,
|
||||
MoveToken = 0,
|
||||
Position = pos,
|
||||
EulerAngles = euler,
|
||||
VehicleSpoonName = null,
|
||||
}), CancellationToken.None);
|
||||
await session.NotifyAsync(MethodId.SyncSpawnVehicle, UxSerializer.Serialize(
|
||||
new SceneMethods.VehicleClientInfo
|
||||
{
|
||||
ControllerPid = 0,
|
||||
CreateSourceType = 0,
|
||||
EntityId = v.EntityId,
|
||||
VehicleConfigId = v.ConfigId,
|
||||
Parts = [],
|
||||
SuitId = 0,
|
||||
Position = pos,
|
||||
Facing = v.Yaw,
|
||||
EulerAngles = euler,
|
||||
Velocity = v.Speed,
|
||||
IsStatic = false,
|
||||
DeformStatus = 0,
|
||||
SeatInfos = [],
|
||||
SpoonId = 0,
|
||||
IsDynamicGo = false,
|
||||
VehicleEnemyId = 0,
|
||||
DisableNavigation = false,
|
||||
Interactable = false,
|
||||
MoveToken = 0,
|
||||
LicensePlate = string.Empty,
|
||||
}), CancellationToken.None);
|
||||
await session.NotifyAsync(MethodId.SyncAetherAIVehicleAddData, UxSerializer.Serialize(
|
||||
new SceneMethods.ClientVehicleInitData4229938
|
||||
{
|
||||
VehicleConfigId = v.ConfigId,
|
||||
VehicleColorPartId = v.ColorPartId,
|
||||
VehicleLightState = 0,
|
||||
LaneHandle = v.LaneHandle,
|
||||
DistanceAlongLane = v.Distance,
|
||||
NextVehicleId = 0,
|
||||
Timestamp = v.Timestamp,
|
||||
ControlType = 1,
|
||||
Speed = v.Speed,
|
||||
DustRatio = 0f,
|
||||
Parts = [],
|
||||
SuitId = 0,
|
||||
RandomFraction = Random.Shared.NextSingle(),
|
||||
Id = v.EntityId,
|
||||
Position = new SceneMethods.UxVector3(v.X, v.Y, v.Z),
|
||||
Facing = v.Yaw,
|
||||
EulerAngles = new SceneMethods.UxVector3(0f, v.Yaw, 0f),
|
||||
}), CancellationToken.None);
|
||||
await session.NotifyAsync(MethodId.SyncAetherAIChangeVehicleControl, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncAetherAIChangeVehicleControlType4229938
|
||||
{
|
||||
VehicleId = v.EntityId,
|
||||
ControlType = 1, // VehicleControlType.AetherVehicle
|
||||
}), CancellationToken.None);
|
||||
await session.NotifyAsync(MethodId.SyncAetherAIVehicleForceGo, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncAetherAIVehicleForceGo4229938
|
||||
{
|
||||
instanceId = v.EntityId,
|
||||
forceGo = true,
|
||||
}), CancellationToken.None);
|
||||
// Hand driving to the client's own traffic AI: cruise through the lane chain
|
||||
// (stop for cars/peds/lights, steer around obstacles) — the server does not
|
||||
// stream lane positions in this mode.
|
||||
if (v.CruisePoints is { Count: > 0 })
|
||||
{
|
||||
var cruise = new SceneMethods.CruiseAITaskParameters4229938
|
||||
{
|
||||
TargetPointList = v.CruisePoints.Select(p => new Ananta.Server.RpcTypes.Client4229938.Auto.UXVector3 { X = p.X, Y = p.Y, Z = p.Z }).ToList(),
|
||||
CruiseType = (byte)SceneMethods.E_TaskVehicleCruiseType4229938.GoToTargetInOrderLoop,
|
||||
configFlags = (byte)(SceneMethods.TaskVehicleCruiseConfigFlags4229938.SlowDownWhenArriveTarget
|
||||
| SceneMethods.TaskVehicleCruiseConfigFlags4229938.DynamicSpeed),
|
||||
minSpeed = MathF.Max(1f, v.Speed * 0.5f),
|
||||
maxSpeed = MathF.Max(2f, v.Speed * 1.3f),
|
||||
ArrivalDistance = 6f,
|
||||
Token = (ulong)Interlocked.Increment(ref _aiTaskTokenSeq),
|
||||
taskAIConfigId = AiConfigPool[Random.Shared.Next(AiConfigPool.Length)],
|
||||
defaultSpeed = v.Speed,
|
||||
drivingFlags = (int)(SceneMethods.VehicleTaskDrivingFlags4229938.DFStopForCars
|
||||
| SceneMethods.VehicleTaskDrivingFlags4229938.DFStopForPeds
|
||||
| SceneMethods.VehicleTaskDrivingFlags4229938.DFStopAtLights
|
||||
| SceneMethods.VehicleTaskDrivingFlags4229938.DFSteerAroundObjects),
|
||||
initSpeed = v.Speed,
|
||||
};
|
||||
await session.NotifyAsync(MethodId.SyncAssignVehicleAITask, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncAssignVehicleAITask4229938
|
||||
{
|
||||
vehicleUId = v.EntityId,
|
||||
parameters = cruise,
|
||||
}), CancellationToken.None);
|
||||
await session.NotifyAsync(MethodId.SyncChangeVehicleAITaskState, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncChangeVehicleAITaskState4229938
|
||||
{
|
||||
vehicleUID = v.EntityId,
|
||||
taskToken = cruise.Token,
|
||||
status = (byte)SceneMethods.VehicleAIStatus4229938.Running,
|
||||
}), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly uint[] AiConfigPool = [16, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30];
|
||||
private static long _aiTaskTokenSeq;
|
||||
|
||||
public async Task SpawnAetherVehicleNpcAsync(
|
||||
TcpSession session, ulong npcId, uint npcFormworkId, ulong vehicleId, byte seatIndex)
|
||||
{
|
||||
await session.NotifyAsync(MethodId.SyncAetherAIVehicleNpcAdd, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncAetherAIVehicleNpcAdd4229938
|
||||
{
|
||||
initData = new SceneMethods.ClientVehicleNpcInitData4229938
|
||||
{
|
||||
Id = npcId,
|
||||
NpcFormworkId = npcFormworkId,
|
||||
BindVehicleId = vehicleId,
|
||||
SeatIndex = seatIndex,
|
||||
},
|
||||
}), CancellationToken.None);
|
||||
|
||||
// Give the driver its drive behavior (PrivateServer1: VehicleNpcTaskId from the
|
||||
// behavior-task catalog) — without it the driver NPC is invisible/passive.
|
||||
var taskId = Ananta.Server.ClientData.Client4229938.BehaviorTaskCatalog.VehicleNpcTaskId;
|
||||
if (taskId != 0)
|
||||
{
|
||||
await session.NotifyAsync(MethodId.SyncDoAetherAgentBehaviorTaskDef, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncDoAetherAgentBehaviorTaskDefine4229938
|
||||
{
|
||||
agentEntityId = npcId,
|
||||
taskId = (ulong)Interlocked.Increment(ref _behaviorTaskSeq),
|
||||
behaviorTaskId = taskId,
|
||||
}), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private static long _behaviorTaskSeq;
|
||||
|
||||
public Task RemoveAetherVehicleNpcAsync(TcpSession session, ulong npcId)
|
||||
{
|
||||
// SyncAetherAINpcRemove (68332284) is the only Aether NPC remove in the client's method
|
||||
// table; the previously used id 68173578 ("SyncAetherAIVehicleNpcRemove") does not exist.
|
||||
return session.NotifyAsync(MethodId.SyncAetherAINpcRemove, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncAetherAINpcRemove4229938 { Pid = npcId }), CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task SendAetherIntersectionUpdateAsync(
|
||||
TcpSession session, ulong intersectionInstanceId, int currentPeriod, int nextPeriod, byte state)
|
||||
{
|
||||
return session.NotifyAsync(MethodId.SyncAetherAIIntersectionUpdateData, UxSerializer.Serialize(
|
||||
new SceneMethods.ClientTrafficIntersectionPeriodUpdateInfo4229938
|
||||
{
|
||||
IntersectionIndex = intersectionInstanceId,
|
||||
CurrentState = state,
|
||||
CurrentPeriodIndex = (byte)currentPeriod,
|
||||
NextPeriodIndex = (byte)nextPeriod,
|
||||
RailPeriodIndex = 0,
|
||||
}), CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task SendAetherLaneDatasAsync(
|
||||
TcpSession session, List<AetherLaneSample> samples, double timestamp)
|
||||
{
|
||||
var packet = new SceneMethods.SyncAetherAIVehicleLaneDatas4229938
|
||||
{
|
||||
Data = samples.Select(s => new SceneMethods.ClientVehicleLaneData4229938
|
||||
{
|
||||
Id = s.EntityId,
|
||||
LaneHandle = s.LaneHandle,
|
||||
DistanceAlongLane = s.Distance,
|
||||
Status = s.Status,
|
||||
}).ToList(),
|
||||
LaneChangeData = [],
|
||||
TimeStamp = timestamp,
|
||||
};
|
||||
return session.NotifyAsync(MethodId.SyncAetherAIVehicleLaneDatas, UxSerializer.Serialize(packet), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task RemoveAetherVehicleAsync(TcpSession session, ulong entityId)
|
||||
{
|
||||
// Aether unregister + legacy scene-entity destroy (the BaseVehicle pair).
|
||||
await session.NotifyAsync(MethodId.SyncAetherAIVehicleRemove, UxSerializer.Serialize(
|
||||
new SceneMethods.SyncAetherAIVehicleRemove4229938 { Pid = entityId }), CancellationToken.None);
|
||||
await session.NotifyAsync(MethodId.SyncDestroyVehicle, UxSerializer.Serialize(
|
||||
new SceneMethods.DestroyVehicle
|
||||
{
|
||||
VehicleEntityId = entityId,
|
||||
VehicleDestroyType = 0,
|
||||
Distance = 0,
|
||||
DynamicGoId = 0,
|
||||
}), CancellationToken.None);
|
||||
}
|
||||
|
||||
public bool IsVehiclePlayerControlled(ulong entityId)
|
||||
{
|
||||
return GameRouter.TryGetSummoned(entityId, out var summoned) && summoned?.ControllerPid != 0;
|
||||
@@ -52,18 +360,31 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd
|
||||
|
||||
public (bool HasPlayer, float X, float Y, float Z, float Yaw, uint RaidId) GetPlayerPosition(TcpSession session)
|
||||
{
|
||||
var state = GameRouter.GetStateIfExists(session);
|
||||
if (state is null)
|
||||
return (false, 0, 0, 0, 0, 0);
|
||||
|
||||
lock (state.SyncRoot)
|
||||
// Sessions disposed right after a client disconnect are probed by the background
|
||||
// loops for a few ticks — treat them as "no player" instead of throwing.
|
||||
try
|
||||
{
|
||||
if (!state.Ready || !state.HasLastReportedPlayerTransform)
|
||||
var state = GameRouter.GetStateIfExists(session);
|
||||
if (state is null)
|
||||
return (false, 0, 0, 0, 0, 0);
|
||||
|
||||
var pos = state.LastReportedPlayerPosition;
|
||||
var rot = state.LastReportedPlayerRotation;
|
||||
return (true, pos.X, pos.Y, pos.Z, rot.Y, state.ActiveRaidId);
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (!state.Ready || !state.HasLastReportedPlayerTransform)
|
||||
return (false, 0, 0, 0, 0, 0);
|
||||
|
||||
var pos = state.LastReportedPlayerPosition;
|
||||
var rot = state.LastReportedPlayerRotation;
|
||||
return (true, pos.X, pos.Y, pos.Z, rot.Y, state.ActiveRaidId);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return (false, 0, 0, 0, 0, 0);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (false, 0, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user