Files

148 lines
5.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 Ananta.Server.Configuration;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 服务端自建「红绿灯相位」表 —— 给 <see cref="GameRouter"/> 的车流提供「这条车道的尽头是不是
/// 一个路口、我是哪个进口道、现在放不放行」。
///
/// ★ 为什么不用客户端那套(详见 <c>PrivateServerConfig.TrafficSignalEnabled</c> 的注释):
/// 放行关系在客户端专属 blob 里,服务端够不着,只能自己算。
///
/// ★ 相位模型对齐官方 <c>MassIntersectionPeriodDetail</c>:
/// 一个相位 = 一段时长(<c>PeriodTime</c>)+ 一组放行的车道(<c>VehicleLaneMetas</c>)。
/// 官方由服务端只发 <c>CurrentPeriodIndex / NextPeriodIndex / RailPeriodIndex</c> 三个 byte,
/// 我们这里直接把「相位号 → 放行腿」这件事在服务端查表完成,省掉一次 S2C。
///
/// 数据表(<c>_gen_junctions.py</c> 生成):
/// <c>data/junctions.csv</c> = id;cx;cz;groupCount;legCount;laneCount
/// <c>data/junction_lanes.csv</c> = laneHandle;junctionId;groupIndex
/// </summary>
internal static class JunctionSignals
{
internal readonly struct StopLine
{
/// <summary>所属路口 id(仅用于日志/错峰)。</summary>
internal readonly int JunctionId;
/// <summary>本进口道所属的相位组序号。</summary>
internal readonly byte GroupIndex;
/// <summary>该路口一共有几个相位组(轮转周期长度)。</summary>
internal readonly byte GroupCount;
/// <summary>该路口的错峰偏移(毫秒),避免全城同时翻灯。</summary>
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<int, StopLine> 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<Table> 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);
/// <summary>
/// 现在这条进口道是不是绿灯。
/// 周期 = 组数 × (绿灯 + 全红清空);组内前 <c>greenSec</c> 放行,其余为清空。
/// </summary>
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;
}
/// <summary>距离下一次本组变绿还有多少毫秒(用于「别在红灯还剩 0.3 秒时刹停」的滞回)。</summary>
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<int, int>();
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;
}
}