using Ananta.Server.Configuration;
namespace Ananta.Server.Handlers.Game;
///
/// 服务端自建「红绿灯相位」表 —— 给 的车流提供「这条车道的尽头是不是
/// 一个路口、我是哪个进口道、现在放不放行」。
///
/// ★ 为什么不用客户端那套(详见 PrivateServerConfig.TrafficSignalEnabled 的注释):
/// 放行关系在客户端专属 blob 里,服务端够不着,只能自己算。
///
/// ★ 相位模型对齐官方 MassIntersectionPeriodDetail:
/// 一个相位 = 一段时长(PeriodTime)+ 一组放行的车道(VehicleLaneMetas)。
/// 官方由服务端只发 CurrentPeriodIndex / NextPeriodIndex / RailPeriodIndex 三个 byte,
/// 我们这里直接把「相位号 → 放行腿」这件事在服务端查表完成,省掉一次 S2C。
///
/// 数据表(_gen_junctions.py 生成):
/// data/junctions.csv = id;cx;cz;groupCount;legCount;laneCount
/// data/junction_lanes.csv = laneHandle;junctionId;groupIndex
///
internal static class JunctionSignals
{
internal readonly struct StopLine
{
/// 所属路口 id(仅用于日志/错峰)。
internal readonly int JunctionId;
/// 本进口道所属的相位组序号。
internal readonly byte GroupIndex;
/// 该路口一共有几个相位组(轮转周期长度)。
internal readonly byte GroupCount;
/// 该路口的错峰偏移(毫秒),避免全城同时翻灯。
internal readonly int OffsetMs;
internal StopLine(int junctionId, byte groupIndex, byte groupCount, int offsetMs)
{
JunctionId = junctionId;
GroupIndex = groupIndex;
GroupCount = groupCount;
OffsetMs = offsetMs;
}
}
private sealed class Table
{
internal Dictionary ByLane = [];
internal int Junctions;
internal bool Ok;
}
private const string JunctionRelativePath = "Ananta.Server/Ananta.App/data/junctions.csv";
private const string LaneRelativePath = "Ananta.Server/Ananta.App/data/junction_lanes.csv";
private static readonly Lazy Cache = new(Load);
internal static bool Available => Cache.Value.Ok;
internal static int JunctionCount => Cache.Value.Junctions;
internal static int LaneCount => Cache.Value.ByLane.Count;
internal static bool TryGet(int laneHandle, out StopLine line)
=> Cache.Value.ByLane.TryGetValue(laneHandle, out line);
///
/// 现在这条进口道是不是绿灯。
/// 周期 = 组数 × (绿灯 + 全红清空);组内前 greenSec 放行,其余为清空。
///
internal static bool IsGreen(in StopLine line, long nowMs, float greenSec, float clearSec)
{
if (line.GroupCount <= 1)
return true; // 单组路口 = 不冲突,恒绿
var greenMs = (long)(greenSec * 1000f);
var slotMs = greenMs + (long)(clearSec * 1000f);
if (slotMs <= 0)
return true;
var t = nowMs + line.OffsetMs;
var slot = t / slotMs;
var within = t % slotMs;
var current = (int)(slot % line.GroupCount);
return current == line.GroupIndex && within < greenMs;
}
/// 距离下一次本组变绿还有多少毫秒(用于「别在红灯还剩 0.3 秒时刹停」的滞回)。
internal static long MsToNextGreen(in StopLine line, long nowMs, float greenSec, float clearSec)
{
if (line.GroupCount <= 1)
return 0;
var greenMs = (long)(greenSec * 1000f);
var slotMs = greenMs + (long)(clearSec * 1000f);
if (slotMs <= 0)
return 0;
var t = nowMs + line.OffsetMs;
var within = t % slotMs;
var current = (int)((t / slotMs) % line.GroupCount);
if (current == line.GroupIndex)
return within < greenMs ? 0 : slotMs * line.GroupCount - within; // 自己的清空段:等到下一轮
// 本组在后面的某个 slot:算还要过几个 slot 才轮到
var ahead = (line.GroupIndex - current + line.GroupCount) % line.GroupCount;
return ahead * slotMs - within;
}
private static Table Load()
{
var table = new Table();
var junctionPath = PrivateServerConfigStore.ResolveProjectPath(JunctionRelativePath);
var lanePath = PrivateServerConfigStore.ResolveProjectPath(LaneRelativePath);
if (!File.Exists(junctionPath) || !File.Exists(lanePath))
return table;
// id -> groupCount
var groupCount = new Dictionary();
foreach (var raw in File.ReadLines(junctionPath))
{
if (raw.Length == 0)
continue;
var p = raw.Split(';');
if (p.Length < 4 || !int.TryParse(p[0], out var id) || !int.TryParse(p[3], out var gc))
continue;
groupCount[id] = gc;
table.Junctions++;
}
foreach (var raw in File.ReadLines(lanePath))
{
if (raw.Length == 0)
continue;
var p = raw.Split(';');
if (p.Length < 3
|| !int.TryParse(p[0], out var lane)
|| !int.TryParse(p[1], out var jid)
|| !int.TryParse(p[2], out var gi))
continue;
if (!groupCount.TryGetValue(jid, out var gc) || gc <= 0)
continue;
// ⛑️ 错峰:按路口 id 打散到 0..29 秒,避免全城同拍翻灯(视觉上像故障)
var offset = (jid * 137) % 30000;
table.ByLane[lane] = new StopLine(jid, (byte)gi, (byte)Math.Min(gc, 255), offset);
}
table.Ok = table.ByLane.Count > 0;
return table;
}
}