148 lines
5.9 KiB
C#
148 lines
5.9 KiB
C#
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;
|
||
}
|
||
}
|