feat(traffic): implement autonomous City Traffic Engine and Living Urban Crowd Engine from CBT dump

This commit is contained in:
CAtlas0784 committed 2026-09-18 14:19:52 +07:00
1 parent f2a36fe188
commit 67bdbae24b
17 files changed
+18759

No files matched your search

@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Ananta.SDK\Ananta.SDK.csproj" />
<ProjectReference Include="..\Ananta.Core\Ananta.Core.csproj" />
<ProjectReference Include="..\Ananta.Gameplay\Ananta.Gameplay.csproj" />
<ProjectReference Include="..\Ananta.Handlers\Ananta.Handlers.csproj" />
<ProjectReference Include="..\Ananta.Network\Ananta.Network.csproj" />
</ItemGroup>
@@ -6,6 +6,8 @@ using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Handlers.Game;
using Ananta.Server.Gameplay.Traffic;
using Ananta.Server.Gameplay.Crowd;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.RpcTypes.Client4229938;
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
@@ -151,6 +153,18 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
await WriteJsonAsync(ctx, Scenes(), token);
else if (method == "GET" && path == "/api/npc/catalog")
await WriteJsonAsync(ctx, NpcCatalog(ctx.Request), token);
else if (method == "GET" && path == "/api/traffic/status")
await WriteJsonAsync(ctx, TrafficStatus(), token);
else if (method == "POST" && path == "/api/traffic/config")
await WriteJsonAsync(ctx, await TrafficConfigAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else if (method == "POST" && path == "/api/traffic/clear")
await WriteJsonAsync(ctx, await TrafficClearAsync(token), token);
else if (method == "POST" && path == "/api/traffic/spawn-wave")
await WriteJsonAsync(ctx, await TrafficSpawnWaveAsync(token), token);
else if (method == "GET" && path == "/api/crowd/status")
await WriteJsonAsync(ctx, CrowdStatus(), token);
else if (method == "POST" && path == "/api/crowd/config")
await WriteJsonAsync(ctx, await CrowdConfigAsync(await ReadBodyAsync(ctx.Request, token), token), token);
else
await WriteJsonAsync(ctx, new { ok = false, error = "unknown route" }, token, 404);
}
@@ -1401,6 +1415,83 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
};
}
private object TrafficStatus()
{
return new
{
ok = true,
traffic = CityTrafficEngine.Instance.GetStatus(),
crowd = UrbanCrowdEngine.Instance.GetStatus()
};
}
private Task<object> TrafficConfigAsync(string body, CancellationToken token)
{
try
{
using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(body) ? "{}" : body);
var root = doc.RootElement;
if (root.TryGetProperty("enabled", out var en))
CityTrafficEngine.Instance.Enabled = en.GetBoolean();
if (root.TryGetProperty("density", out var den))
CityTrafficEngine.Instance.TargetDensity = Math.Clamp(den.GetInt32(), 0, 100);
if (root.TryGetProperty("speedScale", out var spd))
CityTrafficEngine.Instance.SpeedScale = Math.Clamp((float)spd.GetDouble(), 0.1f, 5.0f);
return Task.FromResult<object>(new { ok = true, status = CityTrafficEngine.Instance.GetStatus() });
}
catch (Exception ex)
{
return Task.FromResult<object>(new { ok = false, error = ex.Message });
}
}
private async Task<object> TrafficClearAsync(CancellationToken token)
{
var session = hub.Current;
if (session is null)
return new { ok = false, error = "no active game session" };
await CityTrafficEngine.Instance.ClearAllTrafficAsync(session);
return new { ok = true, message = "traffic cleared" };
}
private async Task<object> TrafficSpawnWaveAsync(CancellationToken token)
{
var session = hub.Current;
if (session is null)
return new { ok = false, error = "no active game session" };
var spawned = await CityTrafficEngine.Instance.SpawnWaveAheadAsync(session, 5);
return new { ok = true, spawned };
}
private object CrowdStatus()
{
return new
{
ok = true,
crowd = UrbanCrowdEngine.Instance.GetStatus()
};
}
private Task<object> CrowdConfigAsync(string body, CancellationToken token)
{
try
{
using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(body) ? "{}" : body);
var root = doc.RootElement;
if (root.TryGetProperty("enabled", out var en))
UrbanCrowdEngine.Instance.Enabled = en.GetBoolean();
if (root.TryGetProperty("density", out var den))
UrbanCrowdEngine.Instance.TargetPedestrianDensity = Math.Clamp(den.GetInt32(), 0, 100);
if (root.TryGetProperty("populateShops", out var pop))
UrbanCrowdEngine.Instance.PopulateShops = pop.GetBoolean();
return Task.FromResult<object>(new { ok = true, status = UrbanCrowdEngine.Instance.GetStatus() });
}
catch (Exception ex)
{
return Task.FromResult<object>(new { ok = false, error = ex.Message });
}
}
private static async Task<string> ReadBodyAsync(HttpListenerRequest request, CancellationToken token)
{
if (!request.HasEntityBody)
@@ -295,6 +295,69 @@
<div class="muted" style="margin-top:6px">เปิดตู้เกมอาร์เคด (KOF '97, Metal Slug) หรือมินิเกมดนตรี/กีฬาในโลกได้ทันที พร้อมปุ่มควบคุมแบบเต็มหน้าจอ</div>
</div>
<div class="card" style="border-color:#10b981">
<h2 style="color:#34d399">🚗 City Traffic & Urban Crowd Controller (ระบบจราจรและชาวเมือง)</h2>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px">
<div style="background:#1e293b;padding:8px 14px;border-radius:6px;border:1px solid #334155;flex:1;min-width:140px">
<div style="font-size:12px;color:#94a3b8">Active Traffic Cars</div>
<div style="font-size:20px;font-weight:bold;color:#38bdf8" id="trafficActiveCount">0</div>
<div style="font-size:11px;color:#64748b" id="trafficRoadCount">42,128 road lanes</div>
</div>
<div style="background:#1e293b;padding:8px 14px;border-radius:6px;border:1px solid #334155;flex:1;min-width:140px">
<div style="font-size:12px;color:#94a3b8">Living Citizens / Crowd</div>
<div style="font-size:20px;font-weight:bold;color:#a78bfa" id="crowdActiveCount">0</div>
<div style="font-size:11px;color:#64748b" id="crowdShopsCount">1,207 shops registered</div>
</div>
<div style="background:#1e293b;padding:8px 14px;border-radius:6px;border:1px solid #334155;flex:1;min-width:140px">
<div style="font-size:12px;color:#94a3b8">Engine Status</div>
<div style="font-size:16px;font-weight:bold;color:#4ade80" id="trafficEngineStatus">Running 🟢</div>
<div style="font-size:11px;color:#64748b">10 ticks/sec real-time</div>
</div>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<div style="flex:1;min-width:220px">
<label style="font-weight:600;font-size:13px;display:flex;justify-content:space-between">
<span>🚗 Traffic Density (ความหนาแน่นรถยนต์):</span>
<span id="trafficDensityLabel" style="color:#38bdf8;font-weight:bold">20 cars</span>
</label>
<input type="range" id="trafficDensitySlider" min="0" max="50" value="20" style="width:100%" oninput="onTrafficDensitySlider(this.value)" onchange="applyTrafficConfig()">
<div style="display:flex;gap:6px;margin-top:4px">
<button style="background:#334155;color:#e2e8f0;font-size:11px;padding:3px 8px" onclick="setTrafficDensityPreset(0)">Off (0)</button>
<button style="background:#334155;color:#e2e8f0;font-size:11px;padding:3px 8px" onclick="setTrafficDensityPreset(10)">Light (10)</button>
<button style="background:#0284c7;color:white;font-size:11px;padding:3px 8px" onclick="setTrafficDensityPreset(20)">Normal (20)</button>
<button style="background:#b91c1c;color:white;font-size:11px;padding:3px 8px" onclick="setTrafficDensityPreset(40)">Rush Hour (40)</button>
</div>
</div>
<div style="flex:1;min-width:220px">
<label style="font-weight:600;font-size:13px;display:flex;justify-content:space-between">
<span>🚶 Pedestrian Density (คนเดินถนนและร้านค้า):</span>
<span id="crowdDensityLabel" style="color:#a78bfa;font-weight:bold">15 npcs</span>
</label>
<input type="range" id="crowdDensitySlider" min="0" max="40" value="15" style="width:100%" oninput="onCrowdDensitySlider(this.value)" onchange="applyCrowdConfig()">
<div style="display:flex;gap:6px;margin-top:4px">
<button style="background:#334155;color:#e2e8f0;font-size:11px;padding:3px 8px" onclick="setCrowdDensityPreset(0)">Off (0)</button>
<button style="background:#334155;color:#e2e8f0;font-size:11px;padding:3px 8px" onclick="setCrowdDensityPreset(10)">Quiet (10)</button>
<button style="background:#7c3aed;color:white;font-size:11px;padding:3px 8px" onclick="setCrowdDensityPreset(20)">Lively (20)</button>
<label style="display:inline-flex;align-items:center;gap:4px;font-size:11px;color:#cbd5e1;margin-left:auto">
<input type="checkbox" id="populateShopsCheck" checked onchange="applyCrowdConfig()"> Populate Shops
</label>
</div>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:8px">
<button style="background:#059669;color:white;font-weight:600;padding:8px 14px" onclick="spawnTrafficWave()">⚡ Spawn Wave Ahead (ปล่อยรถ 5 คันข้างหน้าทันที)</button>
<button style="background:#dc2626;color:white;padding:8px 14px" onclick="clearAllTraffic()">🧹 Clear All Traffic (ล้างรถทั้งหมด)</button>
<button style="background:#475569;color:#e2e8f0;padding:8px 14px" onclick="refreshTrafficStatus()">🔄 Refresh Status</button>
</div>
<div class="muted" style="margin-top:6px">
จำลองการจราจรอัจฉริยะแบบเรียลไทม์บนเส้นทาง 42,128 เลนตามแผนผังเมืองจริง รถจะวิ่งตามเลน เลี้ยวตามทางแยก และผู้เล่นสามารถกด <strong>[F]</strong> เพื่อเปิดประตูขึ้นไปขับได้ทันทีแบบ GTA
</div>
</div>
<div class="card" style="border-color:#e53935">
<h2 style="color:#ff8a80">Emergency Unstuck & Black Screen Recovery</h2>
<div>
@@ -821,6 +884,74 @@ async function setCustomWeather() {
await setWeather(id);
}
function onTrafficDensitySlider(val) {
document.getElementById('trafficDensityLabel').textContent = val + ' cars';
}
function onCrowdDensitySlider(val) {
document.getElementById('crowdDensityLabel').textContent = val + ' npcs';
}
async function setTrafficDensityPreset(val) {
document.getElementById('trafficDensitySlider').value = val;
onTrafficDensitySlider(val);
await applyTrafficConfig();
}
async function setCrowdDensityPreset(val) {
document.getElementById('crowdDensitySlider').value = val;
onCrowdDensitySlider(val);
await applyCrowdConfig();
}
async function applyTrafficConfig() {
const density = parseInt(document.getElementById('trafficDensitySlider')?.value || '20', 10);
const enabled = density > 0;
const body = { enabled, density, speedScale: 1.0 };
const r = await api('/api/traffic/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
log('traffic config: ' + JSON.stringify(r));
await refreshTrafficStatus();
}
async function applyCrowdConfig() {
const density = parseInt(document.getElementById('crowdDensitySlider')?.value || '15', 10);
const populateShops = document.getElementById('populateShopsCheck')?.checked ?? true;
const enabled = density > 0 || populateShops;
const body = { enabled, density, populateShops };
const r = await api('/api/crowd/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
log('crowd config: ' + JSON.stringify(r));
await refreshTrafficStatus();
}
async function spawnTrafficWave() {
const r = await api('/api/traffic/spawn-wave', { method: 'POST' });
log('spawn wave: ' + JSON.stringify(r));
await refreshTrafficStatus();
}
async function clearAllTraffic() {
const r = await api('/api/traffic/clear', { method: 'POST' });
log('clear traffic: ' + JSON.stringify(r));
await refreshTrafficStatus();
}
async function refreshTrafficStatus() {
try {
const s = await api('/api/traffic/status');
if (s && s.ok) {
if (s.traffic) {
document.getElementById('trafficActiveCount').textContent = s.traffic.activeVehicles;
document.getElementById('trafficRoadCount').textContent = (s.traffic.totalRoadLanes || 42128).toLocaleString() + ' road lanes';
document.getElementById('trafficEngineStatus').textContent = s.traffic.enabled ? 'Running 🟢' : 'Disabled 🔴';
document.getElementById('trafficEngineStatus').style.color = s.traffic.enabled ? '#4ade80' : '#f87171';
}
if (s.crowd) {
document.getElementById('crowdActiveCount').textContent = s.crowd.activeCrowdCount;
document.getElementById('crowdShopsCount').textContent = s.crowd.populatedShopsCount + ' / 1,207 shops populated';
}
}
} catch (e) { }
}
loadFleet();
loadScenes();
@@ -828,6 +959,8 @@ loadNpcCatalog();
loadCutsceneCatalog();
refresh();
setInterval(refresh, 2000);
refreshTrafficStatus();
setInterval(refreshTrafficStatus, 2500);
document.getElementById('timeslider').addEventListener('input', e => {
timeDragging = true;
document.getElementById('clock').textContent = fmtClock(parseInt(e.target.value || '720', 10));
@@ -5,6 +5,8 @@ using Ananta.Server.Configuration;
using Ananta.Server.Handlers.Game;
using Ananta.Server.Handlers.LoginGate;
using Ananta.Server.Protocol.Client4229938;
using Ananta.Server.Gameplay.Traffic;
using Ananta.Server.Gameplay.Crowd;
using Ananta.Server.Network;
namespace Ananta.Server.App;
@@ -32,6 +34,20 @@ internal sealed class PrivateServerApplication(PrivateServerConfig config)
CreateServer("game", bindHost, config.Network.GamePort, gameRouter, gameSessions),
};
// Initialize Autonomous City Traffic Engine and Living Urban Crowd Engine
var clientDataFolder = Path.GetDirectoryName(PrivateServerConfigStore.ResolveProjectPath(config.Paths.ClientConfigs))
?? PrivateServerConfigStore.ResolveProjectPath(config.Paths.ClientConfigs);
CityTrafficEngine.Instance.Initialize(
clientDataFolder,
TrafficAndCrowdDriver.Instance,
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
UrbanCrowdEngine.Instance.Initialize(
clientDataFolder,
TrafficAndCrowdDriver.Instance,
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
// Localhost debug API for debug-panel.py (optional, never blocks the game stack).
using var debugApi = config.Debug.Enabled ? new DebugApiServer(config, gameSessions) : null;
var webInfo = "web=off";
@@ -5,6 +5,8 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Ananta.SDK\Ananta.SDK.csproj" />
<ProjectReference Include="..\Ananta.Core\Ananta.Core.csproj" />
<ProjectReference Include="..\Ananta.RpcTypes\Ananta.RpcTypes.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
using Ananta.SDK.Network;
namespace Ananta.Server.Gameplay.Crowd;
public interface IUrbanCrowdDriver
{
Task<(bool Ok, ulong[] EntityIds)> SpawnCrowdPedestriansAsync(
TcpSession session, uint npcFormworkId, uint poiActionId, float x, float y, float z, float facing);
(bool HasPlayer, float X, float Y, float Z, float Yaw, uint RaidId) GetPlayerPosition(TcpSession session);
}
@@ -0,0 +1,358 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using Ananta.SDK.Network;
namespace Ananta.Server.Gameplay.Crowd;
public sealed class InteriorsFile
{
[JsonPropertyName("metadata")]
public InteriorMetadata Metadata { get; set; } = new();
[JsonPropertyName("spatialGrid")]
public Dictionary<string, List<int>> SpatialGrid { get; set; } = new();
[JsonPropertyName("interiors")]
public List<InteriorEntry> Interiors { get; set; } = [];
}
public sealed class InteriorMetadata
{
[JsonPropertyName("totalInteriors")]
public int TotalInteriors { get; set; }
[JsonPropertyName("gridSize")]
public float GridSize { get; set; } = 100f;
}
public sealed class InteriorEntry
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("sector")]
public string Sector { get; set; } = string.Empty;
[JsonPropertyName("pos")]
public List<float> Pos { get; set; } = [];
}
public sealed class PedestrianNetworkFile
{
[JsonPropertyName("metadata")]
public PedestrianMetadata Metadata { get; set; } = new();
[JsonPropertyName("spatialGrid")]
public Dictionary<string, List<int>> SpatialGrid { get; set; } = new();
[JsonPropertyName("lanes")]
public List<PedestrianLaneEntry> Lanes { get; set; } = [];
}
public sealed class PedestrianMetadata
{
[JsonPropertyName("totalLanes")]
public int TotalLanes { get; set; }
[JsonPropertyName("gridSize")]
public float GridSize { get; set; } = 100f;
}
public sealed class PedestrianLaneEntry
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = [];
[JsonPropertyName("pts")]
public List<List<float>> Pts { get; set; } = [];
[JsonPropertyName("isCrosswalk")]
public bool IsCrosswalk { get; set; }
}
public sealed class UrbanCrowdEngine
{
public static UrbanCrowdEngine Instance { get; } = new();
private IUrbanCrowdDriver? _driver;
private Func<IEnumerable<TcpSession>>? _sessionProvider;
private InteriorsFile? _interiorsData;
private PedestrianNetworkFile? _pedestrianData;
private readonly ConcurrentDictionary<int, DateTime> _populatedInteriors = new();
private readonly ConcurrentDictionary<ulong, ActiveCrowdNpc> _activeCrowd = new();
private CancellationTokenSource? _cts;
private Task? _crowdLoopTask;
public bool Enabled { get; set; } = true;
public int TargetPedestrianDensity { get; set; } = 15;
public bool PopulateShops { get; set; } = true;
// Verified civilian NPCs from Camp 2 with models
private static readonly uint[] CitizenFormworkPool =
[
40120883, 40120884, 40120885, 40120886, 40120887,
40120888, 40120889, 40120890, 40120891, 40120892,
40120893, 40120894, 40120895, 40120896, 40120897
];
// Common idle / chatter / phone POI action IDs
private static readonly uint[] AmbientPoiActions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
private readonly Random _rng = new();
public sealed class ActiveCrowdNpc
{
public ulong EntityId { get; set; }
public uint FormworkId { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public DateTime SpawnedAt { get; set; }
}
public void Initialize(string clientDataPath, IUrbanCrowdDriver driver, Func<IEnumerable<TcpSession>> sessionProvider)
{
_driver = driver;
_sessionProvider = sessionProvider;
var interiorsPath = Path.Combine(clientDataPath, "World", "Interiors.json");
var pedPath = Path.Combine(clientDataPath, "World", "PedestrianNetwork.json");
try
{
if (File.Exists(interiorsPath))
{
var json = File.ReadAllText(interiorsPath);
_interiorsData = JsonSerializer.Deserialize<InteriorsFile>(json);
Console.WriteLine($"[CROWD] Loaded {_interiorsData?.Interiors.Count ?? 0} shop interiors.");
}
if (File.Exists(pedPath))
{
var json = File.ReadAllText(pedPath);
_pedestrianData = JsonSerializer.Deserialize<PedestrianNetworkFile>(json);
Console.WriteLine($"[CROWD] Loaded {_pedestrianData?.Lanes.Count ?? 0} pedestrian sidewalk/crosswalk lanes.");
}
}
catch (Exception ex)
{
Console.WriteLine($"[CROWD] Error loading world crowd data: {ex.Message}");
}
Start();
}
public void Start()
{
if (_crowdLoopTask is not null && !_crowdLoopTask.IsCompleted)
return;
_cts = new CancellationTokenSource();
_crowdLoopTask = Task.Run(() => CrowdLoopAsync(_cts.Token));
Console.WriteLine("[CROWD] Living Urban Crowd Engine started.");
}
public void Stop()
{
_cts?.Cancel();
}
private async Task CrowdLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (Enabled && _driver is not null && _sessionProvider is not null)
{
var sessions = _sessionProvider().ToList();
foreach (var session in sessions)
{
await TickSessionCrowdAsync(session);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[CROWD-ERROR] Tick error: {ex.Message}");
}
try
{
// Run crowd checks every 2.5 seconds (crowd spawns do not require high frequency updates)
await Task.Delay(2500, token);
}
catch (OperationCanceledException)
{
break;
}
}
}
private async Task TickSessionCrowdAsync(TcpSession session)
{
if (_driver is null)
return;
var (hasPlayer, px, py, pz, pyaw, _) = _driver.GetPlayerPosition(session);
if (!hasPlayer || !float.IsFinite(px) || !float.IsFinite(pz))
return;
// 1. Populate nearby shops and cafes (within 60m)
if (PopulateShops && _interiorsData is not null)
{
var grid = _interiorsData.SpatialGrid;
var gridSize = _interiorsData.Metadata.GridSize;
var centerCx = (int)MathF.Floor(px / gridSize);
var centerCz = (int)MathF.Floor(pz / gridSize);
for (var cx = centerCx - 1; cx <= centerCx + 1; cx++)
{
for (var cz = centerCz - 1; cz <= centerCz + 1; cz++)
{
var ckey = $"{cx}_{cz}";
if (grid.TryGetValue(ckey, out var interiorIndices))
{
foreach (var idx in interiorIndices)
{
if (idx >= 0 && idx < _interiorsData.Interiors.Count)
{
var shop = _interiorsData.Interiors[idx];
if (shop.Pos.Count < 3) continue;
var sx = shop.Pos[0];
var sy = shop.Pos[1];
var sz = shop.Pos[2];
var dx = sx - px;
var dz = sz - pz;
var distSq = dx * dx + dz * dz;
if (distSq <= 60f * 60f && !_populatedInteriors.ContainsKey(shop.Id))
{
_populatedInteriors[shop.Id] = DateTime.UtcNow;
// Spawn 1 shopkeeper/patron inside
var formworkId = CitizenFormworkPool[_rng.Next(CitizenFormworkPool.Length)];
var poiAction = AmbientPoiActions[_rng.Next(AmbientPoiActions.Length)];
var facing = _rng.Next(0, 360);
var (ok, ids) = await _driver.SpawnCrowdPedestriansAsync(
session, formworkId, poiAction, sx, sy, sz, facing);
if (ok && ids.Length > 0)
{
_activeCrowd[ids[0]] = new ActiveCrowdNpc
{
EntityId = ids[0],
FormworkId = formworkId,
X = sx,
Y = sy,
Z = sz,
SpawnedAt = DateTime.UtcNow
};
Console.WriteLine($"[CROWD] Populated shop '{shop.Name}' with citizen NPC {formworkId}!");
}
}
}
}
}
}
}
}
// 2. Sidewalk pedestrians
if (_pedestrianData is not null && _activeCrowd.Count < TargetPedestrianDensity)
{
var pGrid = _pedestrianData.SpatialGrid;
var pGridSize = _pedestrianData.Metadata.GridSize;
var centerCx = (int)MathF.Floor(px / pGridSize);
var centerCz = (int)MathF.Floor(pz / pGridSize);
var candidateWaypoints = new List<(float X, float Y, float Z)>();
for (var cx = centerCx - 1; cx <= centerCx + 1; cx++)
{
for (var cz = centerCz - 1; cz <= centerCz + 1; cz++)
{
var ckey = $"{cx}_{cz}";
if (pGrid.TryGetValue(ckey, out var laneIds))
{
foreach (var laneId in laneIds)
{
if (laneId >= 0 && laneId < _pedestrianData.Lanes.Count)
{
var lane = _pedestrianData.Lanes[laneId];
foreach (var pt in lane.Pts)
{
if (pt.Count < 3) continue;
var ldx = pt[0] - px;
var ldz = pt[2] - pz;
var dSq = ldx * ldx + ldz * ldz;
if (dSq >= 25f * 25f && dSq <= 80f * 80f)
{
candidateWaypoints.Add((pt[0], pt[1], pt[2]));
}
}
}
}
}
}
}
if (candidateWaypoints.Count > 0)
{
var wp = candidateWaypoints[_rng.Next(candidateWaypoints.Count)];
var formworkId = CitizenFormworkPool[_rng.Next(CitizenFormworkPool.Length)];
var poiAction = AmbientPoiActions[_rng.Next(AmbientPoiActions.Length)];
var facing = _rng.Next(0, 360);
var (ok, ids) = await _driver.SpawnCrowdPedestriansAsync(
session, formworkId, poiAction, wp.X, wp.Y, wp.Z, facing);
if (ok && ids.Length > 0)
{
_activeCrowd[ids[0]] = new ActiveCrowdNpc
{
EntityId = ids[0],
FormworkId = formworkId,
X = wp.X,
Y = wp.Y,
Z = wp.Z,
SpawnedAt = DateTime.UtcNow
};
}
}
}
}
public CrowdStatus GetStatus() => new()
{
Enabled = Enabled,
TargetDensity = TargetPedestrianDensity,
ActiveCrowdCount = _activeCrowd.Count,
PopulatedShopsCount = _populatedInteriors.Count,
TotalInteriors = _interiorsData?.Interiors.Count ?? 0,
TotalPedestrianLanes = _pedestrianData?.Lanes.Count ?? 0
};
}
public sealed class CrowdStatus
{
public bool Enabled { get; set; }
public int TargetDensity { get; set; }
public int ActiveCrowdCount { get; set; }
public int PopulatedShopsCount { get; set; }
public int TotalInteriors { get; set; }
public int TotalPedestrianLanes { get; set; }
}
@@ -0,0 +1,377 @@
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<IEnumerable<TcpSession>>? _sessionProvider;
private readonly ConcurrentDictionary<ulong, SimulatedVehicle> _simulatedVehicles = new();
private RoadNetworkFile? _roadNetwork;
private readonly Dictionary<int, RoadLaneData> _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
private static readonly uint[] TrafficVehiclePool =
[
81001001, // Sunset Skywing (Sedan)
81001002, // Korou RV6 (SUV)
81001003, // Kazama Voyage (Sedan)
81000007, // Sunset GT-X Specter (Sports Car)
81000002, // Korou Transporter M (Truck)
81000005, // Erebos Black Box (Van)
];
private readonly Random _rng = new();
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<IEnumerable<TcpSession>> 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<RoadNetworkFile>(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<bool> 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<RoadLaneData>();
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<int> 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; }
}
@@ -0,0 +1,19 @@
using Ananta.SDK.Network;
namespace Ananta.Server.Gameplay.Traffic;
public interface ITrafficVehicleDriver
{
Task<(bool Ok, ulong EntityId)> SpawnTrafficVehicleAsync(
TcpSession session, uint configId, float x, float y, float z, float yaw);
Task DestroyTrafficVehicleAsync(
TcpSession session, ulong entityId);
Task SendVehicleMoveAsync(
TcpSession session, ulong entityId, float x, float y, float z, float yaw, float vx, float vz);
bool IsVehiclePlayerControlled(ulong entityId);
(bool HasPlayer, float X, float Y, float Z, float Yaw, uint RaidId) GetPlayerPosition(TcpSession session);
}
@@ -0,0 +1,167 @@
using System.Text.Json.Serialization;
using Ananta.SDK.Network;
using Ananta.Server.RpcTypes.Client4229938;
namespace Ananta.Server.Gameplay.Traffic;
public sealed class RoadNetworkFile
{
[JsonPropertyName("metadata")]
public RoadMetadata Metadata { get; set; } = new();
[JsonPropertyName("spatialGrid")]
public Dictionary<string, List<int>> SpatialGrid { get; set; } = new();
[JsonPropertyName("lanes")]
public List<RoadLaneData> Lanes { get; set; } = [];
}
public sealed class RoadMetadata
{
[JsonPropertyName("totalLanes")]
public int TotalLanes { get; set; }
[JsonPropertyName("gridSize")]
public float GridSize { get; set; } = 100f;
[JsonPropertyName("totalGridCells")]
public int TotalGridCells { get; set; }
}
public sealed class RoadLaneData
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; } = string.Empty;
[JsonPropertyName("width")]
public float Width { get; set; }
[JsonPropertyName("speed")]
public float Speed { get; set; }
[JsonPropertyName("turn")]
public string Turn { get; set; } = "Straight";
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = [];
[JsonPropertyName("pts")]
public List<List<float>> Pts { get; set; } = [];
[JsonPropertyName("tangents")]
public List<List<float>> Tangents { get; set; } = [];
[JsonPropertyName("next")]
public List<int> Next { get; set; } = [];
[JsonPropertyName("adj")]
public List<int> Adj { get; set; } = [];
[JsonPropertyName("bbox")]
public List<float> Bbox { get; set; } = [];
// Pre-calculated runtime fields
[JsonIgnore]
public float TotalLength { get; private set; }
[JsonIgnore]
public float[] SegmentLengths { get; private set; } = [];
[JsonIgnore]
public float[] CumulativeDistances { get; private set; } = [];
public void Precompute()
{
if (Pts.Count < 2)
{
TotalLength = 0f;
SegmentLengths = [];
CumulativeDistances = [0f];
return;
}
SegmentLengths = new float[Pts.Count - 1];
CumulativeDistances = new float[Pts.Count];
CumulativeDistances[0] = 0f;
float total = 0f;
for (var i = 0; i < Pts.Count - 1; i++)
{
var p1 = Pts[i];
var p2 = Pts[i + 1];
var dx = p2[0] - p1[0];
var dy = p2[1] - p1[1];
var dz = p2[2] - p1[2];
var segLen = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
SegmentLengths[i] = segLen;
total += segLen;
CumulativeDistances[i + 1] = total;
}
TotalLength = total;
}
public (float X, float Y, float Z, float Yaw, float Vx, float Vz) Evaluate(float distance, float speed)
{
if (Pts.Count == 0)
return (0, 0, 0, 0, 0, 0);
if (Pts.Count == 1 || distance <= 0f)
{
var p = Pts[0];
var yaw = 0f;
if (Tangents.Count > 0 && Tangents[0].Count >= 3)
yaw = MathF.Atan2(Tangents[0][0], Tangents[0][2]) * (180f / MathF.PI);
var vx = MathF.Sin(yaw * (MathF.PI / 180f)) * speed;
var vz = MathF.Cos(yaw * (MathF.PI / 180f)) * speed;
return (p[0], p[1], p[2], yaw, vx, vz);
}
if (distance >= TotalLength)
{
var p = Pts[^1];
var yaw = 0f;
if (Tangents.Count > 0 && Tangents[^1].Count >= 3)
yaw = MathF.Atan2(Tangents[^1][0], Tangents[^1][2]) * (180f / MathF.PI);
var vx = MathF.Sin(yaw * (MathF.PI / 180f)) * speed;
var vz = MathF.Cos(yaw * (MathF.PI / 180f)) * speed;
return (p[0], p[1], p[2], yaw, vx, vz);
}
// Binary search or linear search for segment
var segIdx = 0;
for (var i = 0; i < SegmentLengths.Length; i++)
{
if (distance <= CumulativeDistances[i + 1])
{
segIdx = i;
break;
}
}
var pA = Pts[segIdx];
var pB = Pts[segIdx + 1];
var segStart = CumulativeDistances[segIdx];
var segLen = SegmentLengths[segIdx];
var t = segLen > 0.001f ? (distance - segStart) / segLen : 0f;
t = Math.Clamp(t, 0f, 1f);
var x = pA[0] + (pB[0] - pA[0]) * t;
var y = pA[1] + (pB[1] - pA[1]) * t;
var z = pA[2] + (pB[2] - pA[2]) * t;
// Yaw from segment direction
var dx = pB[0] - pA[0];
var dz = pB[2] - pA[2];
var yawDeg = MathF.Atan2(dx, dz) * (180f / MathF.PI);
var rad = yawDeg * (MathF.PI / 180f);
var vX = MathF.Sin(rad) * speed;
var vZ = MathF.Cos(rad) * speed;
return (x, y, z, yawDeg, vX, vZ);
}
}
@@ -156,6 +156,94 @@ internal sealed partial class GameRouter
return (true, $"sent spawn for {ids.Count} static NPC(s) in front of you", ids.ToArray());
}
internal static async Task<(bool Ok, string Message, ulong[] Ids)> SpawnStaticNpcAtAsync(
TcpSession session, uint npcFormworkId, uint poiActionId, float x, float y, float z, float facing)
{
if (npcFormworkId > int.MaxValue || !NpcCatalog4229938.TryGet(npcFormworkId, out var npc))
return (false, $"unknown NPC formwork {npcFormworkId}", []);
if (poiActionId != 0 && !PoiActionCatalog4229938.TryGet(poiActionId, out _))
return (false, $"unknown NPC POI action {poiActionId}", []);
var state = GetStateIfExists(session);
if (state is null)
return (false, "no player world state", []);
var id = unchecked((ulong)Interlocked.Increment(ref s_nextStaticNpcId));
PoiActionCatalog4229938.TryGet(poiActionId, out var poi);
var mainActionId = poi?.SelectMainAction(id) ?? 0;
var position = new SceneMethods.UxVector3(x, y, z);
var item = new AdminNpcSpawnItem(npcFormworkId, poiActionId);
var agentSyncClientInfo = CreateStaticNpcSyncInfo(item, npc, poi, mainActionId);
try
{
await SendAetherInitForNpcAsync(session);
await session.NotifyAsync(MethodId.SyncAetherAIStaticNpcAddData,
UxSerializer.Serialize(new GameMethods.ClientStaticNpcInitData4229938
{
StaticNpcInfoId = npcFormworkId,
NpcFormworkId = npcFormworkId,
AgentPersonaId = npc.AgentPersonaId,
SPoiActionId = 0,
CPoiActionId = poiActionId,
UrbanDiversityId = 88888000,
IgnoreAllStim = false,
TaskRelated = false,
NpcPid = (int)npcFormworkId,
AgentSyncClientInfo = agentSyncClientInfo,
ForceGo = true,
SourceType = 1,
Id = id,
Position = position,
Facing = facing,
EulerAngles = new SceneMethods.UxVector3(0, facing, 0),
}), CancellationToken.None);
lock (state.SyncRoot)
state.StaticNpcPreparedPlotEvents[id] = 0;
await session.NotifyAsync(MethodId.SyncManagedLogicAgent,
UxSerializer.Serialize(WorldCodec.ManagedLogicAgent(id, Profile.PlayerPid, 0)), CancellationToken.None);
if (poi is { UsageId: not 0 })
{
await session.NotifyAsync(MethodId.SyncAgentBelongings,
UxSerializer.Serialize(new GameMethods.SyncNpcBelongings4229938
{
Id = id,
Items = poi.BelongingSceneItems.Select(configId => new GameMethods.NpcBelongingItem4229938
{
InstanceId = unchecked((ulong)Interlocked.Increment(ref s_nextNpcBelongingId)),
ConfigId = configId,
}).ToList(),
DebugAgentId = npcFormworkId,
}), CancellationToken.None);
await session.NotifyAsync(MethodId.SyncAgentUseBelongingChanged,
UxSerializer.Serialize(new GameMethods.SyncNpcBelongingUsage4229938
{
Id = id,
UsageIds = [poi.UsageId],
}), CancellationToken.None);
}
if (poiActionId != 0)
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncAgentPlayPOIAction,
UxSerializer.Serialize(new GameMethods.ClientNpcPoiActionData4229938
{
Id = id,
CPoiActionId = poiActionId,
IsActive = true,
PlayPoiSpeed = 1f,
}), CancellationToken.None);
return (true, "spawned", [id]);
}
catch (Exception ex)
{
return (false, ex.Message, []);
}
}
private static async Task SendAetherInitForNpcAsync(TcpSession session)
{
var settings = PrivateServerConfigStore.Current.Gameplay.Vehicles;
@@ -0,0 +1,73 @@
using Ananta.SDK.Network;
using Ananta.SDK.Serialization;
using Ananta.Server.Gameplay.Crowd;
using Ananta.Server.Gameplay.Traffic;
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
{
public static TrafficAndCrowdDriver Instance { get; } = new();
public async Task<(bool Ok, ulong EntityId)> SpawnTrafficVehicleAsync(
TcpSession session, uint configId, float x, float y, float z, float yaw)
{
var result = await GameRouter.SpawnDirectAsync(
session, configId, new Vec3(x, y, z), yaw, rightOffset: 0f, sourceType: 2, reason: "city-traffic");
return (result.Ok, result.EntityId);
}
public async Task DestroyTrafficVehicleAsync(TcpSession session, ulong entityId)
{
await GameRouter.DestroyDirectAsync(session, entityId, "traffic-despawn");
}
public async Task SendVehicleMoveAsync(
TcpSession session, ulong entityId, float x, float y, float z, float yaw, float vx, float vz)
{
var moveData = new SceneMethods.RaidVehicleSyncData
{
Id = entityId,
Position = new SceneMethods.UxVector3(x, y, z),
FacingDirection = yaw,
EulerAngles = new SceneMethods.UxVector3(0f, yaw, 0f),
Velocity = new SceneMethods.UxVector3(vx, 0f, vz),
Bits = [],
MoveToken = 0,
};
await session.NotifyAsync(MethodId.SyncVehicleMove, UxSerializer.Serialize(moveData), CancellationToken.None);
GameRouter.TrackVehicleMove(entityId, x, y, z, yaw);
}
public bool IsVehiclePlayerControlled(ulong entityId)
{
return GameRouter.TryGetSummoned(entityId, out var summoned) && summoned?.ControllerPid != 0;
}
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)
{
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);
}
}
public async Task<(bool Ok, ulong[] EntityIds)> SpawnCrowdPedestriansAsync(
TcpSession session, uint npcFormworkId, uint poiActionId, float x, float y, float z, float facing)
{
var result = await GameRouter.SpawnStaticNpcAtAsync(session, npcFormworkId, poiActionId, x, y, z, facing);
return (result.Ok, result.Ids);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
{"metadata":{"totalLanes":21333,"gridSize":100.0,"totalGridCells":1623},"spatialGrid":{"-1_-7":[0,1,2,3,6568,6569,6570,6571,6572,9772,12612,12613,12614,12615,12619,16787,16788,20982,20983,20984,20985,20986,20987,20988,21017,21018,21023,21051,21112,21199],"-1_-8":[3,4,5,20870,20871,20872,20873,20986,20988,20989,20990,20991,21002,21023,21052,21103,21104,21111,21112],"-2_-8":[5,6,7,631,632,633,1597,6573,6574,6575,6576,9060,9123,9237,9238,9239,9240,9241,9246,9247,9531,9532,9533,9534,9535,9536,9537,9538,12616,12618,12623,20989,20990,20992,20993,20994,20995,20996,20997,20998,20999,21002,21103,21111,21127,21128],"-21_12":[8,9,12,13,14,15,16,17,18,49,50,51,52,53,54,55,1435,1436,1437,1438,1439,1440,2300,3614,3615,3616,4260,4263,4788,5510,5511,5512,5513,5514,5515,5519,5520,14195],"-21_13":[8,9,27,28,29,30,31,32,33,34,35,36,37,38,39,40,1437,1438,1440,3735,4170,9672,9673,9674,9675,13640,13642,14196,16785,16786],"-25_-16":[10,3535,16240,16571,16572,16573,16574,16576,16577,16578],"-25_-15":[10,11,3535,16225,16226,16227,16228,16229,16230,16240,16241,16242,16574,16575,16576,16578],"-14_-11":[19,20,21,22,23,24,25,26,238,6094,6095,6096,6097,6098,6100,7019,7020,7021,7022,7023,7024,7025,7026,7027,7029,8267,8268,8269,8270,8288,8289,8290,9118,9119,9151,9258,9262,9553],"-13_-11":[26,6217,8270,9180,9181,9245,9267,9269,9270,9553],"-20_13":[33,250,252,253,4082,8863,8864,10670,10671,10672,10673,10674,10675,10676,10677,10678,10679,10680,10681,10690,10691,10692,13631,13632,13633,13634,13635,13636,13637,13638,13639,13641,13642,16435],"-22_14":[41,42,43,44,45,46,47,48,194,1073,1074,1075,1076,1077,1078,1079,1080,1081,5711,5712,5713,5714,5955,8517,8518,8519,8520,14750,14751,14752,14753,14754,14755,14756,15123,15124,15125,15126,15127,15128,15129,15130,16484,16485,16486,16487,16488,16489,16490],"-21_14":[44,56,57,58,59,60,61,62,3659,3660,3661,3735,3831,8497,8498,8499,8500,8501,8502,8503,8504,8865,8866],"1_-1":[63,86,643,4749,4750,4751,4752,4753,4754,4755,4756,4757,4894,4895,4896,4897,4898,4900,4921,4922,4923,4924],"-23_12":[64,75,2308,2309,2310,3177,3179,8330,8331,8332,8333,8352,8677,8678,8679,8680,11664,11665,11666,11667,11668,11669,11673,11674,16636],"7_2":[65,169,320,2769,2770,2771,2772,4340,8903,8904,8914,8915,8916,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12436,13195,13196,13197,13198,13199,13200,13201,13202,13203,13856,13857,13858,13859,13860,13861,13862,13863,13864,13865,13866,13867,13868,13869,13870],"7_3":[65,320,321,2771,2772,2773,2774,2775,2776,2777,8884,8890,8891,10091,10092,10093,10094,10095,10096,10097,10098,10099,10100,10101,10102,10103,10104,10105,10106,12452,12453,12454,12455,12456,12457,12458,12459,13280,13281,13282,13283,13295,13622,13623,13624,13625,13626,16301],"6_5":[66,67,2921,2922,2923,2924,2925,2926,2927,2928,2930,3108,3109,4847,5311,5312,5313,5314,5315,7088,7089,7090,7091,7092,9321,9429,9431,9432,9651,9652,9653,9654,11564,11565,11566,11567,11587,11857],"-15_8":[68,1494,1495,1496,1497,1498,1857,1858,11249,11250,15395,15396,15397,15402,15403,16665,17082,17468,17469],"-7_5":[69,106,107,108,109,499,7643,8687,8688,8689,8690,8706,8707,8708,8709,10044,10045,10046,10047,10071,10072,10073,10074,11356,11357,11358,11359,11379,11380,11381,11382,11383,12498,12499,12500,12501,12502,12503,12504,12505,12513,12514,12515,12516,12517,12518,12519,12520,12521,16295],"-6_5":[69,144,376,377,378,379,3035,3036,3037,3038,3039,3040,3041,3042,3062,3063,3064,3065,3066,5353,5354,5355,5356,5357,5358,5359,5360,5361,7643,7644,7645,7646,7647,7648,7649,7650,7651,7652,7653,7654,7655,8702,8703,8704,8705,8706,12474,12475,12476,12477,12478,12479,12480,12481,12490,12491,12492,12493,12494,12495,12496,12497,16637],"8_3":[70,319,12437,12438,12439,12440,12448,12449,12450,12451,12452,12915,13626,16301],"8_4":[70,224,2400,2401,2402,2403,12915,13607,13608,13609,13610,13611,13613,13626,13627,13628,13629,13630],"-23_15":[71,332,333,7105,7107,7112,7113,7114,7115,7116,7120,7121,7122,7123,7124,11915,11916,11917,11918,11919,13387,13388,13389,13390,13391,13392,13393,16361],"-23_14":[72,637,1070,1071,1072,1073,1081,1082,1083,1084,6227,6228,6229,6230,6231,6232,6233,6234,6235,6236,6237,6238,6239,8882,10610,10612,11915,14756,16388],"-14_1":[73,757,4157,6308,6309,6310,6311,6312,6313,6314,6315,6316,16544,16545,16546,16564,16565],"6_4":[74,649,652,5311,5315,5316,5317,5318,7088,8298,11567,11568,11569,11570,11571,11572,11573,11574,11575,11583,11584,11585,11586,11587,16457],"-22_12":[75,1434,1435,1436,1437,1438,2300,2301,2302,2303,2304,2305,2306,2307,2308,3261,3262,3263,4260,4261,4262,5876,5877,5878,5879,5921,8893,10601,10602,10603,10604,10605,10606,10607,10608,10609,10613,10614,10615,11246,11247,11248],"4_0":[76,77,79,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,4330,4331,11603,11604,11605,11606,11607,13650,13651,13652,13653,13654,13655,13656,13657,13658,13659,13660,13661,13662,13663,13664,13665,13666],"3_0":[77,1553,1554,1555,1556,1557,1Line truncated
@@ -0,0 +1 @@
{"metadata":{"totalLanes":42128,"gridSize":100.0,"totalGridCells":5016},"spatialGrid":{"6_13":[0,2,3,4,5,7,8,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,1735,1736,1737,2612,2613,2614,2615,2616,2617,2618,2619,2620,2621,2622,2623,2624,2625,2626,2627,2628,2629,2630,2631,2632,2633,2634,2635,2636,2637,2638,2639,2640,2809,2811,2812,2813,2814,2816,2817,2819,4118,4119,4120,4121,4122,4123,7453,7454,7455,7456,7457,7458,7459,7460,8295,8296,8297,8298,8299,8300,8708,8709,8710,9035,9036,9325,9326,9327,9541,9542,9543,9609,9610,9611,9612,9613,9614,9615,10789,10790,11638,11639,11640,11641,11642,11643,12161,12162,12478,12479,12480,13223,13224],"6_14":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,17,18,19,20,23,24,25,26,27,28,29,30,32,33,34,37,38,39,2612,2613,2614,2615,2618,2619,2620,2621,2624,2625,2626,2627,2628,2629,2630,2631,2633,2634,2635,2638,2639,2640,2761,2762,2763,2764,2765,2766,2767,2768,2769,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,3178,3179,3180,7113,7114,7115,7116,7117,7118,7297,7298,7299,7300,7301,7302,7303,7438,7439,7440,7441,7442,7443,7453,7454,7455,7456,7457,7458,7459,7460,9258,9259,9260,9261,9262,9263,9348,9349,9350,9351,9352,9353,9354,9355,10077,10078,10079,10586,10587,10588,10789,10790,11667,11668,11669,12040,12041,12042,12161,12162,12478,12479,12480],"-6_-9":[40,41,46,47,48,243,244,245,453,454,455,460,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363,7496,7497,7498,8736,8737,8738,8739,8740,16698,16699,16700,16701,16702,16703,16704,16912,16913,17196,17197,17198,17199,17200,17201,17202,21712,21713,21714,21715,21716,21717,21718,32389,32390,38214,38215,38216,38217,38218,39590,39591,39856,39857,40089,40090,40091],"-6_-8":[40,41,42,43,44,45,46,47,48,49,532,533,534,730,731,732,733,734,735,736,737,738,929,930,932,935,936,937,1106,1107,1108,7496,7497,7498,8736,8737,8738,8739,8740,16698,16699,16700,16701,16702,16703,16704,16912,16913,16914,16915,16916,16917,17150,17151,17152,21712,21713,21714,21715,21716,21717,21718,24525,24526,24527,24528,24529,24530,24531,24532,24533,24534,24535,30852,30853,30854,30855,30856,30857,30858,30859,39983,39984,39985,39986,39987,39988,40089,40090,40091,40092,40093,40094,40095,40096,40385,40386,40387,40388,40389,40390,40391,40392],"37_12":[50,51,52,1376,1377,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,2995,3570,3571,3572,3573,3574,3575,4455,4456,4695,4696,4697,4819,4820,4821,4822,4823,4824,6558,6559,6560,6561,7263,7264,7265,9667,9668,9669,9670,9671,9672,9673,9674,9675,9676,9677,9678,10094,10095,10374,10375,10379,10381,10383,10386,10388,10811,10812,10813,10814,10983,10984,11320,11321,12663,12664,12665,12666,12667,12668],"-10_-15":[53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,642,643,644,645,646,647,648,816,817,818,837,838,839,840,841],"-10_-14":[54,56,57,58,59,60,61,62,64,66,68,642,643,644,645,646,647,816,817,818,4699,4700,4701,4702,7496,7497,7498],"-9_-15":[54,60,61,62,65,67,68,86,87,88,642,643,644,645,646,647,648,1090,1091,1092,1093],"-9_-14":[54,60,61,62,68,86,87,88,642,643,644,645,646,647,1090,1091,1092,1093,4698,4699,4700,4701,4702,7496,7497,7498],"28_12":[69,70,71,72,73,431,432,9190,9191,9192,9193,10054,10055,10056,10057,10058,10059,10060,10061,13114,13115,13116,13117,13118],"31_27":[74,75,76,77,78,79,80,81,82,83,84,85,132,133,134,207,208,209,210,211,212,783,784,790,791,798,799,800,1000,1001,1094,1095,1096,1097,1098,1099,1187,1188,1189,2172,2173,2174,2175,2176,2177,2178],"32_27":[74,75,76,77,80,82,256,257,258,271,272,273,274,275,276,277,278,279,583,584,585,780,781,782,861,862,863,864,865,866,867,868,869,1085,1086,1087,1187,1188,1189,2172,2173,2174,2175,2176,2177,2178,13238,13239,13240],"30_27":[83,84,85,580,581,582,883,884,885,1147,1148,2956,2957,2958,2959,2960,2961,2962,2963,2964],"-9_-13":[86,87,88,151,152,153,154,155,705,706,709,710,713,714,717,718,722,723,1090,1091,1092,1093,4698,4699,4700,4701,4702,7496,7497,7498],"-8_-15":[86,87,88,1090,1091,1092,1093],"-8_-14":[86,87,88,1090,1091,1092,1093,7496,7497,7498],"-8_-13":[86,87,88,151,152,154,155,227,228,229,231,490,491,492,493,494,495,496,497,566,567,568,569,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,1090,1091,1092,1093,1169,1170,1171,4124,4125,4126,4127,4958,4959,4960,4961,7496,7497,7498],"19_25":[89,90,91,92,93,94,7967,7968,7969,7970,7971,7974,7975,7977,7978,7979,7980,7981,7983,7984,7985,10100,10101,10102,10103,10530,10531,10532,10533,10534,10535,10536,10537,11056,11057,11058,11059,11060,11061,13637,13638,13639,13640,13641,13642,13643,13644,13645,13646,13647,13648,13649,13650,13651,13652,13653,13654,13655,13656,13709,13710,13711],"12_17":[95,96,97,98,99,100,101,102,1241,1242,1269,1270,1272,1274,1276,1277,1278,1280,1281,1282,1284,1286,1919,1920,1921,1922,6854,6855,6856,6857,6858,7143,7144,7145,11114,11115,11116,11117,11121,11122,11131,11132,11140,11141,11424,11425,11426,11427,11428,11477,11478,11479,11480,11481,11482],"31_17":[103,104,105,106,107,108,109,110,111,1393,1394,1395,1583,1584,1585,1586,1587,1588,1589,1590,4207,4208,4Line truncated
File diff suppressed because it is too large. Load diff
+315
View File
@@ -0,0 +1,315 @@
"""
Extracts Traffic Lanes, Pedestrian Splines, Interiors, and Traffic Signals from
readable_world_dump_4229938_final.zip without unpacking the full 5.1 GB archive.
Produces high-performance, compact JSON caches in Ananta.Server/ClientData/4229938/World/.
"""
import sys
import os
import io
import json
import math
import csv
import zipfile
import time
ZIP_PATH = r"c:\Users\Phitchayut\Desktop\Drop asset\readable_world_dump_4229938_final.zip"
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"Ananta.Server", "ClientData", "4229938", "World")
os.makedirs(OUT_DIR, exist_ok=True)
print(f"[EXTRACT] Reading from: {ZIP_PATH}")
print(f"[EXTRACT] Target output directory: {OUT_DIR}")
t0 = time.time()
with zipfile.ZipFile(ZIP_PATH, "r") as z:
# -------------------------------------------------------------
# 1. Extract Road Networks (Vehicles)
# -------------------------------------------------------------
road_sources = [
("city", "readable_world_dump_4229938_final/03_ZONEGRAPHS/full_json/vfc_1286_e108_h2b81bc528d8b6797.json"),
("airport", "readable_world_dump_4229938_final/03_ZONEGRAPHS/full_json/vfc_999_e28_hcec9b1c776a677f4.json"),
("longqi", "readable_world_dump_4229938_final/03_ZONEGRAPHS/full_json/vfc_997_e3_h859e50d46cdca6f9.json"),
]
all_road_lanes = []
spatial_grid = {} # "cellX_cellZ" -> [globalLaneIndex]
grid_size = 100.0 # 100 meter spatial buckets
global_lane_counter = 0
for region_name, zip_entry in road_sources:
print(f"[ROAD] Processing {region_name} from {zip_entry}...")
with z.open(zip_entry) as f:
data = json.load(f)
lanes = data.get("lanes", [])
pts = data.get("lanePoints", [])
tangents = data.get("laneTangentVectors", [])
links = data.get("laneLinks", [])
# Offset to map local destLaneIndex to globalLaneIndex
base_lane_idx = global_lane_counter
for local_id, lane in enumerate(lanes):
tag_names = lane.get("tagNames", [])
# Filter for vehicle road lanes
is_vehicle = "Vehicle" in tag_names or "CapillaryRoad" in tag_names or "Freeway" in tag_names
p_begin = lane.get("pointsBegin", 0)
p_end = lane.get("pointsEnd", 0)
lane_pts = pts[p_begin:p_end]
if not lane_pts or len(lane_pts) < 2:
continue
lane_tangents = tangents[p_begin:p_end] if tangents else []
# Gather outgoing and adjacent links
l_begin = lane.get("linksBegin", 0)
l_end = lane.get("linksEnd", 0)
lane_links = links[l_begin:l_end] if links else []
outgoing = []
adjacent = []
for link in lane_links:
dest = link.get("destLaneIndex", -1)
l_type = link.get("type", 0)
if dest >= 0 and dest < len(lanes):
if l_type == 1: # Outgoing
outgoing.append(base_lane_idx + dest)
elif l_type == 4: # Adjacent (lane change)
adjacent.append(base_lane_idx + dest)
# Determine speed limit: Freeway = 16.67 m/s (60km/h), Normal = 11.11 m/s (40km/h), Alley = 8.33 m/s (30km/h)
if "Freeway" in tag_names:
speed_limit = 16.67
elif "VehicleAlley" in tag_names:
speed_limit = 8.33
else:
speed_limit = 11.11
# Compute bounding box
xs = [p[0] for p in lane_pts]
ys = [p[1] for p in lane_pts]
zs = [p[2] for p in lane_pts]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
min_z, max_z = min(zs), max(zs)
# Compact points: [[x, y, z], ...] rounded to 2 decimals
compact_pts = [[round(p[0], 2), round(p[1], 2), round(p[2], 2)] for p in lane_pts]
compact_tangents = [[round(t[0], 3), round(t[1], 3), round(t[2], 3)] for t in lane_tangents] if lane_tangents else []
g_idx = global_lane_counter
global_lane_counter += 1
lane_entry = {
"id": g_idx,
"region": region_name,
"width": round(lane.get("width", 4.0), 1),
"speed": speed_limit,
"turn": lane.get("turnDirectionName", "Straight"),
"tags": tag_names,
"pts": compact_pts,
"tangents": compact_tangents,
"next": outgoing,
"adj": adjacent,
"bbox": [round(min_x, 1), round(min_y, 1), round(min_z, 1),
round(max_x, 1), round(max_y, 1), round(max_z, 1)]
}
all_road_lanes.append(lane_entry)
# Register in spatial grid cells
min_cx = int(math.floor(min_x / grid_size))
max_cx = int(math.floor(max_x / grid_size))
min_cz = int(math.floor(min_z / grid_size))
max_cz = int(math.floor(max_z / grid_size))
for cx in range(min_cx, max_cx + 1):
for cz in range(min_cz, max_cz + 1):
ckey = f"{cx}_{cz}"
if ckey not in spatial_grid:
spatial_grid[ckey] = []
spatial_grid[ckey].append(g_idx)
road_cache = {
"metadata": {
"totalLanes": len(all_road_lanes),
"gridSize": grid_size,
"totalGridCells": len(spatial_grid)
},
"spatialGrid": spatial_grid,
"lanes": all_road_lanes
}
road_out_path = os.path.join(OUT_DIR, "RoadNetwork.json")
print(f"[ROAD] Writing {len(all_road_lanes)} road lanes to {road_out_path}...")
with open(road_out_path, "w", encoding="utf-8") as out_f:
json.dump(road_cache, out_f, separators=(",", ":"))
print(f"[ROAD] Done! File size: {os.path.getsize(road_out_path) / 1024 / 1024:.2f} MB")
# -------------------------------------------------------------
# 2. Extract Pedestrian Sidewalks & Crosswalks
# -------------------------------------------------------------
ped_source = "readable_world_dump_4229938_final/03_ZONEGRAPHS/full_json/vfc_1000_e8_hfef331e26e35309d.json"
print(f"[PED] Processing pedestrian graph from {ped_source}...")
with z.open(ped_source) as f:
data = json.load(f)
ped_lanes = data.get("lanes", [])
ped_pts = data.get("lanePoints", [])
ped_tangents = data.get("laneTangentVectors", [])
ped_links = data.get("laneLinks", [])
compact_ped_lanes = []
ped_spatial_grid = {}
for local_id, lane in enumerate(ped_lanes):
p_begin = lane.get("pointsBegin", 0)
p_end = lane.get("pointsEnd", 0)
lane_pts = ped_pts[p_begin:p_end]
if not lane_pts or len(lane_pts) < 2:
continue
tag_names = lane.get("tagNames", [])
compact_pts = [[round(p[0], 2), round(p[1], 2), round(p[2], 2)] for p in lane_pts]
xs = [p[0] for p in lane_pts]
zs = [p[2] for p in lane_pts]
min_x, max_x = min(xs), max(xs)
min_z, max_z = min(zs), max(zs)
ped_entry = {
"id": local_id,
"tags": tag_names,
"pts": compact_pts,
"isCrosswalk": "Crosswalk" in tag_names
}
compact_ped_lanes.append(ped_entry)
min_cx = int(math.floor(min_x / grid_size))
max_cx = int(math.floor(max_x / grid_size))
min_cz = int(math.floor(min_z / grid_size))
max_cz = int(math.floor(max_z / grid_size))
for cx in range(min_cx, max_cx + 1):
for cz in range(min_cz, max_cz + 1):
ckey = f"{cx}_{cz}"
if ckey not in ped_spatial_grid:
ped_spatial_grid[ckey] = []
ped_spatial_grid[ckey].append(local_id)
ped_cache = {
"metadata": {
"totalLanes": len(compact_ped_lanes),
"gridSize": grid_size,
"totalGridCells": len(ped_spatial_grid)
},
"spatialGrid": ped_spatial_grid,
"lanes": compact_ped_lanes
}
ped_out_path = os.path.join(OUT_DIR, "PedestrianNetwork.json")
print(f"[PED] Writing {len(compact_ped_lanes)} pedestrian lanes to {ped_out_path}...")
with open(ped_out_path, "w", encoding="utf-8") as out_f:
json.dump(ped_cache, out_f, separators=(",", ":"))
print(f"[PED] Done! File size: {os.path.getsize(ped_out_path) / 1024 / 1024:.2f} MB")
# -------------------------------------------------------------
# 3. Extract Interiors & Shops
# -------------------------------------------------------------
print("[INTERIORS] Processing interiors from interiors_full.csv...")
interiors_list = []
interior_spatial_grid = {}
with z.open("readable_world_dump_4229938_final/00_INDEX/interiors_full.csv") as f:
reader = csv.reader(io.TextIOWrapper(f, encoding="utf-8-sig"))
header = next(reader)
for row in reader:
indoor_id = row[2]
name = row[3]
sector = row[4]
wx_s, wy_s, wz_s = row[7], row[8], row[9]
if not wx_s or not wy_s or not wz_s:
continue
try:
wx = round(float(wx_s), 2)
wy = round(float(wy_s), 2)
wz = round(float(wz_s), 2)
except ValueError:
continue
idx = len(interiors_list)
entry = {
"id": int(indoor_id) if indoor_id.isdigit() else 0,
"name": name,
"sector": sector,
"pos": [wx, wy, wz]
}
interiors_list.append(entry)
cx = int(math.floor(wx / grid_size))
cz = int(math.floor(wz / grid_size))
ckey = f"{cx}_{cz}"
if ckey not in interior_spatial_grid:
interior_spatial_grid[ckey] = []
interior_spatial_grid[ckey].append(idx)
interiors_cache = {
"metadata": {
"totalInteriors": len(interiors_list),
"gridSize": grid_size
},
"spatialGrid": interior_spatial_grid,
"interiors": interiors_list
}
interiors_out_path = os.path.join(OUT_DIR, "Interiors.json")
print(f"[INTERIORS] Writing {len(interiors_list)} interiors to {interiors_out_path}...")
with open(interiors_out_path, "w", encoding="utf-8") as out_f:
json.dump(interiors_cache, out_f, indent=2, ensure_ascii=False)
print(f"[INTERIORS] Done! File size: {os.path.getsize(interiors_out_path) / 1024:.2f} KB")
# -------------------------------------------------------------
# 4. Extract Traffic Signals & Intersections
# -------------------------------------------------------------
print("[SIGNALS] Processing traffic lights and intersections...")
with z.open("readable_world_dump_4229938_final/04_WORLD_JSON/full_json/vfc_47_e11_h7140c25172fd7615.json") as f:
lights_raw = json.load(f).get("TrafficLights", [])
with z.open("readable_world_dump_4229938_final/04_WORLD_JSON/full_json/vfc_47_e13_h64c11d788d8b4dd9.json") as f:
inter_raw = json.load(f).get("interInfos", [])
compact_lights = []
for l in lights_raw:
pos = l.get("position", {})
fwd = l.get("forward", {})
compact_lights.append({
"handle": l.get("trafficLightHandle", 0),
"inter": l.get("inter", -1),
"zbr": l.get("zbr", -1),
"pos": [round(pos.get("x", 0), 2), round(pos.get("y", 0), 2), round(pos.get("z", 0), 2)],
"fwd": [round(fwd.get("x", 0), 3), round(fwd.get("y", 0), 3), round(fwd.get("z", 0), 3)],
})
compact_inter = []
for it in inter_raw:
compact_inter.append({
"id": it.get("intersection", 0),
"zebras": [z.get("zebra") for z in it.get("zebraConnector", [])]
})
signals_cache = {
"lights": compact_lights,
"intersections": compact_inter
}
signals_out_path = os.path.join(OUT_DIR, "TrafficSignals.json")
print(f"[SIGNALS] Writing {len(compact_lights)} lights and {len(compact_inter)} intersections to {signals_out_path}...")
with open(signals_out_path, "w", encoding="utf-8") as out_f:
json.dump(signals_cache, out_f, indent=2, ensure_ascii=False)
print(f"[SIGNALS] Done! File size: {os.path.getsize(signals_out_path) / 1024:.2f} KB")
t_elapsed = time.time() - t0
print(f"[ALL COMPLETE] World and Traffic data extraction completed in {t_elapsed:.2f} seconds!")