Files

244 lines
9.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}