243 lines
9.9 KiB
C#
243 lines
9.9 KiB
C#
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<=0</c> 或 <c>maxCount<=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);
|
|||
|
|
}
|
|||
|
|
}
|