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; } }