using System.Collections.Concurrent; using System.Text.Json; using Ananta.SDK.Network; namespace Ananta.Server.Gameplay.Traffic; public sealed class CityTrafficEngine { public static CityTrafficEngine Instance { get; } = new(); private ITrafficVehicleDriver? _driver; private Func>? _sessionProvider; private readonly ConcurrentDictionary _simulatedVehicles = new(); private RoadNetworkFile? _roadNetwork; private readonly Dictionary _laneLookup = new(); private CancellationTokenSource? _cts; private Task? _simulationLoopTask; // Traffic Configuration public bool Enabled { get; set; } = true; public int TargetDensity { get; set; } = 20; public float SpeedScale { get; set; } = 1.0f; public float MinSpawnDistance { get; set; } = 50f; public float MaxSpawnDistance { get; set; } = 160f; public float DespawnDistance { get; set; } = 220f; // Pool of verified vehicle configs with full models and multi-seat support (no damaged/wrecked models) private static readonly uint[] TrafficVehiclePool = [ 81001001, // Sunset Skywing (Sedan) 81001002, // Korou RV6 (SUV) 81001003, // Kazama Voyage (Sedan) 81000007, // Sunset GT-X Specter (Sports Car) 81004001, // Kazama CRN6 (Taxi) 81004027, // Kazama Sandstorm (Police Cruiser) ]; private readonly Random _rng = new(); public void NotifyVehicleBoarding(ulong entityId) { if (_simulatedVehicles.TryRemove(entityId, out var v)) { v.HijackedByPlayer = true; Console.WriteLine($"[TRAFFIC] Vehicle entity={entityId} boarded by player. Detached from traffic loop."); } } public sealed class SimulatedVehicle { public ulong EntityId { get; set; } public uint ConfigId { get; set; } public int CurrentLaneId { get; set; } public float DistanceOnLane { get; set; } public float Speed { get; set; } public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float Yaw { get; set; } public DateTime CreatedAt { get; set; } public bool HijackedByPlayer { get; set; } } public void Initialize(string clientDataPath, ITrafficVehicleDriver driver, Func> sessionProvider) { _driver = driver; _sessionProvider = sessionProvider; var roadPath = Path.Combine(clientDataPath, "World", "RoadNetwork.json"); if (!File.Exists(roadPath)) { Console.WriteLine($"[TRAFFIC] RoadNetwork.json not found at: {roadPath}"); return; } try { Console.WriteLine($"[TRAFFIC] Loading road network from: {roadPath}..."); var json = File.ReadAllText(roadPath); _roadNetwork = JsonSerializer.Deserialize(json); if (_roadNetwork is not null) { foreach (var lane in _roadNetwork.Lanes) { lane.Precompute(); _laneLookup[lane.Id] = lane; } Console.WriteLine($"[TRAFFIC] Road network loaded: {_roadNetwork.Lanes.Count} lanes precomputed across {_roadNetwork.SpatialGrid.Count} spatial cells."); } } catch (Exception ex) { Console.WriteLine($"[TRAFFIC] Failed to load road network: {ex.Message}"); } Start(); } public void Start() { if (_simulationLoopTask is not null && !_simulationLoopTask.IsCompleted) return; _cts = new CancellationTokenSource(); _simulationLoopTask = Task.Run(() => SimulationLoopAsync(_cts.Token)); Console.WriteLine("[TRAFFIC] Autonomous City Traffic Engine started."); } public void Stop() { _cts?.Cancel(); } private async Task SimulationLoopAsync(CancellationToken token) { const int tickIntervalMs = 100; // 10 ticks per second for smooth client replication const float dt = tickIntervalMs / 1000f; while (!token.IsCancellationRequested) { try { if (Enabled && _driver is not null && _sessionProvider is not null && _roadNetwork is not null) { var sessions = _sessionProvider().ToList(); foreach (var session in sessions) { await TickSessionTrafficAsync(session, dt); } } } catch (Exception ex) { // Engine tick exception guard Console.WriteLine($"[TRAFFIC-ERROR] Loop exception: {ex.Message}"); } try { await Task.Delay(tickIntervalMs, token); } catch (OperationCanceledException) { break; } } } private async Task TickSessionTrafficAsync(TcpSession session, float dt) { if (_driver is null || _roadNetwork is null) return; var (hasPlayer, px, py, pz, pyaw, raidId) = _driver.GetPlayerPosition(session); if (!hasPlayer || !float.IsFinite(px) || !float.IsFinite(pz)) return; // 1. Move and update existing simulated vehicles var vehicleEntries = _simulatedVehicles.Values.ToList(); foreach (var v in vehicleEntries) { if (v.HijackedByPlayer) continue; // Check if player has boarded or driven this vehicle if (_driver.IsVehiclePlayerControlled(v.EntityId)) { v.HijackedByPlayer = true; _simulatedVehicles.TryRemove(v.EntityId, out _); Console.WriteLine($"[TRAFFIC] Player took control of traffic vehicle entity={v.EntityId}!"); continue; } // Distance to player var dx = v.X - px; var dz = v.Z - pz; var distSq = dx * dx + dz * dz; if (distSq > DespawnDistance * DespawnDistance) { // Despawn far away vehicle _simulatedVehicles.TryRemove(v.EntityId, out _); await _driver.DestroyTrafficVehicleAsync(session, v.EntityId); continue; } // Advance vehicle along lane if (!_laneLookup.TryGetValue(v.CurrentLaneId, out var currentLane) || currentLane.TotalLength <= 0.1f) { _simulatedVehicles.TryRemove(v.EntityId, out _); await _driver.DestroyTrafficVehicleAsync(session, v.EntityId); continue; } v.DistanceOnLane += v.Speed * SpeedScale * dt; // Check if vehicle reached the end of the lane if (v.DistanceOnLane >= currentLane.TotalLength) { if (currentLane.Next.Count > 0) { // Pick next connected lane var nextLaneId = currentLane.Next[_rng.Next(currentLane.Next.Count)]; if (_laneLookup.TryGetValue(nextLaneId, out var nextLane)) { v.CurrentLaneId = nextLaneId; v.DistanceOnLane = 0f; v.Speed = nextLane.Speed > 0 ? nextLane.Speed : currentLane.Speed; currentLane = nextLane; } else { // Unresolved next lane -> despawn _simulatedVehicles.TryRemove(v.EntityId, out _); await _driver.DestroyTrafficVehicleAsync(session, v.EntityId); continue; } } else { // Dead end -> despawn _simulatedVehicles.TryRemove(v.EntityId, out _); await _driver.DestroyTrafficVehicleAsync(session, v.EntityId); continue; } } // Calculate new spline position & velocity var (nx, ny, nz, nyaw, vx, vz) = currentLane.Evaluate(v.DistanceOnLane, v.Speed * SpeedScale); v.X = nx; v.Y = ny; v.Z = nz; v.Yaw = nyaw; // Send smooth SyncVehicleMove to client await _driver.SendVehicleMoveAsync(session, v.EntityId, nx, ny, nz, nyaw, vx, vz); } // 2. Spawn new vehicles if below target density var activeCount = _simulatedVehicles.Count; if (activeCount < TargetDensity) { await TrySpawnTrafficVehicleAroundPlayerAsync(session, px, py, pz, pyaw); } } private async Task TrySpawnTrafficVehicleAroundPlayerAsync( TcpSession session, float px, float py, float pz, float pyaw) { if (_driver is null || _roadNetwork is null) return false; // Query spatial cells around player var grid = _roadNetwork.SpatialGrid; var gridSize = _roadNetwork.Metadata.GridSize; var centerCx = (int)MathF.Floor(px / gridSize); var centerCz = (int)MathF.Floor(pz / gridSize); var candidateLanes = new List(); var minSq = MinSpawnDistance * MinSpawnDistance; var maxSq = MaxSpawnDistance * MaxSpawnDistance; // Search 3x3 surrounding cells (~300m range) for (var cx = centerCx - 2; cx <= centerCx + 2; cx++) { for (var cz = centerCz - 2; cz <= centerCz + 2; cz++) { var ckey = $"{cx}_{cz}"; if (grid.TryGetValue(ckey, out var laneIds)) { foreach (var lId in laneIds) { if (_laneLookup.TryGetValue(lId, out var lane) && lane.TotalLength > 10f) { var laneStart = lane.Pts[0]; var ldx = laneStart[0] - px; var ldz = laneStart[2] - pz; var dSq = ldx * ldx + ldz * ldz; if (dSq >= minSq && dSq <= maxSq) { candidateLanes.Add(lane); } } } } } } if (candidateLanes.Count == 0) return false; // Pick a random lane from candidates var chosenLane = candidateLanes[_rng.Next(candidateLanes.Count)]; // Check if there is already a vehicle near the start of this lane foreach (var existing in _simulatedVehicles.Values) { if (existing.CurrentLaneId == chosenLane.Id && existing.DistanceOnLane < 20f) return false; // Lane entry occupied } // Evaluate spawn coordinates at start of lane var (sx, sy, sz, syaw, _, _) = chosenLane.Evaluate(0f, 0f); // Pick vehicle config var configId = TrafficVehiclePool[_rng.Next(TrafficVehiclePool.Length)]; var (ok, entityId) = await _driver.SpawnTrafficVehicleAsync(session, configId, sx, sy, sz, syaw); if (!ok || entityId == 0) return false; var simVehicle = new SimulatedVehicle { EntityId = entityId, ConfigId = configId, CurrentLaneId = chosenLane.Id, DistanceOnLane = 0f, Speed = chosenLane.Speed > 0f ? chosenLane.Speed : 11.11f, X = sx, Y = sy, Z = sz, Yaw = syaw, CreatedAt = DateTime.UtcNow, HijackedByPlayer = false }; _simulatedVehicles[entityId] = simVehicle; return true; } public async Task SpawnWaveAheadAsync(TcpSession session, int count = 5) { if (_driver is null || _roadNetwork is null) return 0; var (hasPlayer, px, py, pz, pyaw, _) = _driver.GetPlayerPosition(session); if (!hasPlayer) return 0; var spawned = 0; for (var i = 0; i < count; i++) { if (await TrySpawnTrafficVehicleAroundPlayerAsync(session, px, py, pz, pyaw)) spawned++; } return spawned; } public async Task ClearAllTrafficAsync(TcpSession session) { if (_driver is null) return; var ids = _simulatedVehicles.Keys.ToList(); _simulatedVehicles.Clear(); foreach (var id in ids) { await _driver.DestroyTrafficVehicleAsync(session, id); } Console.WriteLine($"[TRAFFIC] Cleared {ids.Count} traffic vehicles."); } public TrafficStatus GetStatus() => new() { Enabled = Enabled, TargetDensity = TargetDensity, SpeedScale = SpeedScale, ActiveVehicles = _simulatedVehicles.Count, TotalRoadLanes = _roadNetwork?.Lanes.Count ?? 0, TotalSpatialCells = _roadNetwork?.SpatialGrid.Count ?? 0 }; } public sealed class TrafficStatus { public bool Enabled { get; set; } public int TargetDensity { get; set; } public float SpeedScale { get; set; } public int ActiveVehicles { get; set; } public int TotalRoadLanes { get; set; } public int TotalSpatialCells { get; set; } }