Files
Ananta-ps/Ananta.Server/Ananta.Core/ClientData/Client4229938/IntersectionTable.cs
T

243 lines
9.9 KiB
C#
Raw Normal View History

using System.Globalization;
using Ananta.Server.Configuration;
namespace Ananta.Server.ClientData.Client4229938;
/// <summary>
/// 一个路口(红绿灯)的<b>相位表</b>一行 —— 数据来自捐赠端 PrivateServerPor 的
/// <c>Ananta.Server/Ananta.App/data/zonegraph_intersections.csv</c>(171 行)。
/// </summary>
/// <remarks>
/// ⛑⛑ 2026-10-02 移植自捐赠端 `Ananta.Core/ClientData/Client4229938/IntersectionTable.cs`
/// (逐字照搬解析逻辑,只把路径改成走 <c>Paths.Intersections</c> 配置)。
///
/// 为什么需要它:主线以前 init 的 <c>Intersections</c> 恒为 <c>[]</c>,客户端
/// <c>AetherIntersectionManager</c> 建不出 <c>IntersectionDataEntity</c> ⇒ 不
/// <c>RegisterTrafficLight</c> ⇒ **灯柱从来没被接管 ⇒ 不亮**。有了这张表才能
/// ① init 里带上真路口、② 周期发 <c>SyncAetherAIIntersectionUpdateDa</c>(68819947) 切相位。
///
/// ⛑ lane id 同源性已验证:本表 <c>PeriodOpenLanes</c> 里的车道 id 在主线
/// <c>vehicle_lanes.csv</c> 里 **29/29 命中**(前 6 个路口抽样),距路口 16~26m
/// ⇒ 与主线车流用的是同一份 zonegraph。
/// </remarks>
internal sealed record TrafficIntersection4229938(
int ZoneIndex,
float X, float Y, float Z,
int PeriodCount,
byte CurrentPeriodIndex,
byte NextPeriodIndex,
byte RailPeriodIndex,
IReadOnlyList<int[]> PeriodOpenLanes)
{
/// <summary>这个路口涉及的全部车道(各相位放行车道的并集)。</summary>
public IReadOnlySet<int> AllLanes { get; } =
PeriodOpenLanes.SelectMany(static a => a).ToHashSet();
}
internal static class IntersectionTable
{
private static readonly Lazy<IReadOnlyList<TrafficIntersection4229938>> Cache = new(Load);
internal static IReadOnlyList<TrafficIntersection4229938> All => Cache.Value;
internal static int Count => Cache.Value.Count;
private static readonly Lazy<IReadOnlyDictionary<int, int>> LaneOwner = new(() =>
{
var d = new Dictionary<int, int>();
var all = Cache.Value;
for (var i = 0; i < all.Count; i++)
foreach (var lane in all[i].AllLanes)
d[lane] = i;
return d;
});
/// <summary>这条车道属于哪个路口(表内下标),不属于任何路口返回 false。</summary>
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<int,int>)⇒ 填错就查不到 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<IReadOnlyDictionary<int, int>> ZoneMap = new(LoadZoneMap);
/// <summary>
/// 取路口的**真 ZoneIndex**;查不到返回 0(调用方回退成旧的「ZoneIndex = InstanceId」填法)。
/// </summary>
internal static int TrueZoneOf(int instanceId)
=> ZoneMap.Value.TryGetValue(instanceId, out var z) ? z : 0;
/// <summary>映射表命中了多少个路口(诊断用)。</summary>
internal static int ZoneMapHits => ZoneMap.Value.Count;
private static IReadOnlyDictionary<int, int> LoadZoneMap()
{
var d = new Dictionary<int, int>();
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<TrafficIntersection4229938> 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<TrafficIntersection4229938>(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;
}
/// <summary>
/// 按距 (px,pz) 由近到远返回路口下标;<c>radiusMeters&lt;=0</c> 或 <c>maxCount&lt;=0</c> 时返回全部。
/// 用于把相位推送限制在玩家附近(省包量),近的优先。
/// </summary>
internal static List<int> OrderByDistance(float px, float pz, float radiusMeters, int maxCount)
{
var all = All;
var idx = new List<int>(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;
}
/// <summary>体检用:路口数 / 总周期数 / 多相位路口数 / 首行摘要。</summary>
internal static (int Intersections, int MultiPeriod, int Lanes, string Sample) Probe()
{
var all = All;
var multi = 0;
var lanes = new HashSet<int>();
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);
}
}