using System.Globalization; using Ananta.Server.Configuration; namespace Ananta.Server.ClientData.Client4229938; /// /// 一个路口(红绿灯)的相位表一行 —— 数据来自捐赠端 PrivateServerPor 的 /// Ananta.Server/Ananta.App/data/zonegraph_intersections.csv(171 行)。 /// /// /// ⛑⛑ 2026-10-02 移植自捐赠端 `Ananta.Core/ClientData/Client4229938/IntersectionTable.cs` /// (逐字照搬解析逻辑,只把路径改成走 Paths.Intersections 配置)。 /// /// 为什么需要它:主线以前 init 的 Intersections 恒为 [],客户端 /// AetherIntersectionManager 建不出 IntersectionDataEntity ⇒ 不 /// RegisterTrafficLight ⇒ **灯柱从来没被接管 ⇒ 不亮**。有了这张表才能 /// ① init 里带上真路口、② 周期发 SyncAetherAIIntersectionUpdateDa(68819947) 切相位。 /// /// ⛑ lane id 同源性已验证:本表 PeriodOpenLanes 里的车道 id 在主线 /// vehicle_lanes.csv 里 **29/29 命中**(前 6 个路口抽样),距路口 16~26m /// ⇒ 与主线车流用的是同一份 zonegraph。 /// internal sealed record TrafficIntersection4229938( int ZoneIndex, float X, float Y, float Z, int PeriodCount, byte CurrentPeriodIndex, byte NextPeriodIndex, byte RailPeriodIndex, IReadOnlyList PeriodOpenLanes) { /// 这个路口涉及的全部车道(各相位放行车道的并集)。 public IReadOnlySet AllLanes { get; } = PeriodOpenLanes.SelectMany(static a => a).ToHashSet(); } internal static class IntersectionTable { private static readonly Lazy> Cache = new(Load); internal static IReadOnlyList All => Cache.Value; internal static int Count => Cache.Value.Count; private static readonly Lazy> LaneOwner = new(() => { var d = new Dictionary(); var all = Cache.Value; for (var i = 0; i < all.Count; i++) foreach (var lane in all[i].AllLanes) d[lane] = i; return d; }); /// 这条车道属于哪个路口(表内下标),不属于任何路口返回 false。 internal static bool TryGetLaneOwner(int laneId, out int intersectionIndex) => LaneOwner.Value.TryGetValue(laneId, out intersectionIndex); // --- InstanceId → 真 ZoneIndex 映射(2026-10-02 21:2x)------------------------------------ // // ⛑⛑ 捐赠端 CSV 里那列叫 `zone_index`,**实际装的是 InstanceId**(59..2224)。 // 真 ZoneIndex 是主线自带 `data/intersections.csv` 的**第二列**(1..366)—— // 列义由 `IntersectionCatalog4229938.LoadFile` 定死:`p[0]=InstanceId, p[1]=ZoneIndex`。 // 客户端 `AetherIntersectionManager.BuildIntersectionData` 拿 ZoneIndex 去查 // `_zoneIndexToBlobIndex`(Dictionary)⇒ 填错就查不到 blob // ⇒ `IntersectionDataEntity.BlobDataIndex` 不对 ⇒ `TrafficLightSlideGroupList` 为空 // ⇒ **灯柱没人接管,永远不变色**(我们第一版就是这个症状)。 // // ⛑ 为什么不直接往 zonegraph_intersections.csv 里加一列:那张表最后一列 // `period_light_flags` 里**带逗号且逗号个数随相位变**(`0:2,2|1:4,4` / `4:2,2,2,2`), // C# 侧是裸 `Split(',')`,追加列的下标不稳定 ⇒ 单独一张映射表。 private const string ZoneMapRelativePath = "Ananta.Server/Ananta.App/data/intersection_zone_map.csv"; private static readonly Lazy> ZoneMap = new(LoadZoneMap); /// /// 取路口的**真 ZoneIndex**;查不到返回 0(调用方回退成旧的「ZoneIndex = InstanceId」填法)。 /// internal static int TrueZoneOf(int instanceId) => ZoneMap.Value.TryGetValue(instanceId, out var z) ? z : 0; /// 映射表命中了多少个路口(诊断用)。 internal static int ZoneMapHits => ZoneMap.Value.Count; private static IReadOnlyDictionary LoadZoneMap() { var d = new Dictionary(); string path; try { path = PrivateServerConfigStore.ResolveProjectPath(ZoneMapRelativePath); } catch { return d; } if (!File.Exists(path)) return d; foreach (var line in File.ReadLines(path)) { if (string.IsNullOrWhiteSpace(line)) continue; var p = line.Split(','); if (p.Length < 2) continue; if (!int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) continue; if (!int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var z) || z <= 0) continue; // 0 = 未知,不写进表(让调用方回退) d[id] = z; } return d; } private static IReadOnlyList Load() { string path; try { path = PrivateServerConfigStore.ResolveProjectPath( PrivateServerConfigStore.Current.Paths.Intersections); } catch { return []; } if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return []; var list = new List(512); using var reader = new StreamReader(path); if (reader.ReadLine() is null) // 表头 return []; while (reader.ReadLine() is { } line) { if (line.Length == 0) continue; var parts = line.Split(','); if (parts.Length < 8) continue; float F(int i) => float.TryParse(parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 0f; int I(int i) => int.TryParse(parts[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : 0; var periodCount = I(4); var perPeriod = new int[Math.Max(periodCount, 0)][]; for (var i = 0; i < perPeriod.Length; i++) perPeriod[i] = []; // 第 10 列 period_open_lanes = "0:|1:403;404;405"(| 分相位、; 分车道) if (parts.Length > 9 && parts[9].Length > 0) { foreach (var chunk in parts[9].Split('|', StringSplitOptions.RemoveEmptyEntries)) { var colon = chunk.IndexOf(':'); if (colon <= 0) continue; if (!int.TryParse(chunk.AsSpan(0, colon), NumberStyles.Integer, CultureInfo.InvariantCulture, out var pi)) continue; if ((uint)pi >= (uint)perPeriod.Length) continue; var body = chunk[(colon + 1)..]; perPeriod[pi] = body.Length == 0 ? [] : body.Split(';', StringSplitOptions.RemoveEmptyEntries) .Select(static s => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n : -1) .Where(static n => n >= 0) .ToArray(); } } list.Add(new TrafficIntersection4229938( I(0), F(1), F(2), F(3), periodCount, (byte)I(5), (byte)I(6), (byte)I(7), perPeriod)); } return list; } /// /// 按距 (px,pz) 由近到远返回路口下标;radiusMeters<=0 或 maxCount<=0 时返回全部。 /// 用于把相位推送限制在玩家附近(省包量),近的优先。 /// internal static List OrderByDistance(float px, float pz, float radiusMeters, int maxCount) { var all = All; var idx = new List(all.Count); for (var i = 0; i < all.Count; i++) { if (radiusMeters > 0f) { var dx = all[i].X - px; var dz = all[i].Z - pz; if (dx * dx + dz * dz > radiusMeters * radiusMeters) continue; } idx.Add(i); } if (radiusMeters > 0f) idx.Sort((a, b) => { var da = (all[a].X - px) * (all[a].X - px) + (all[a].Z - pz) * (all[a].Z - pz); var db = (all[b].X - px) * (all[b].X - px) + (all[b].Z - pz) * (all[b].Z - pz); return da.CompareTo(db); }); if (maxCount > 0 && idx.Count > maxCount) idx.RemoveRange(maxCount, idx.Count - maxCount); return idx; } /// 体检用:路口数 / 总周期数 / 多相位路口数 / 首行摘要。 internal static (int Intersections, int MultiPeriod, int Lanes, string Sample) Probe() { var all = All; var multi = 0; var lanes = new HashSet(); foreach (var it in all) { if (it.PeriodCount > 1) multi++; foreach (var l in it.AllLanes) lanes.Add(l); } var sample = all.Count == 0 ? "(空 —— 检查 paths.intersections)" : $"id={all[0].ZoneIndex} 真zone={(TrueZoneOf(all[0].ZoneIndex) is var z0 && z0 > 0 ? z0.ToString() : "未知→回退id")} " + $"周期={all[0].PeriodCount} " + $"p0 放行 {(all[0].PeriodOpenLanes.Count > 0 ? all[0].PeriodOpenLanes[0].Length : 0)} 条 " + $"[zone映射命中 {ZoneMapHits}/{all.Count}]"; return (all.Count, multi, lanes.Count, sample); } }