using Ananta.SDK.Network;
using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
using Ananta.Server.Gameplay;
using Ananta.Server.Protocol.Client4229938;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
///
/// ★★ 路口相位 / 红绿灯(2026-10-02 从捐赠端 PrivateServerPor 移植)—— **让灯柱真的亮起来**。
///
/// 背景(详见 modules/红绿灯与路口相位.md):
/// 主线以前 init 的 Intersections 恒为 []、运行期相位包
/// SyncAetherAIIntersectionUpdateDa(68819947) **一次都没发过** ⇒ 客户端
/// AetherIntersectionManager 建不出 IntersectionDataEntity ⇒ 不
/// RegisterTrafficLight ⇒ 灯柱从来没被接管。trafficSignalEnabled 那套
/// 只是服务端自己压车速,跟灯柱没关系。
///
/// 捐赠端的三条腿(我们照搬):
/// ① init 带真路口表 → GameAdminBridge 的 -donor 变体(另见该文件)
/// ② 登录后全量推一次 →
/// ③ 之后周期切档 → (本文件的后台循环)
///
/// ⛑⛑ 循环**不依赖车流开关**:用户 2026-10-02 19:38 把 trafficEnabled 关了,
/// 灯仍然要亮 ⇒ 这里自己起 loop,不挂在 TrafficLoop 里。
///
internal sealed partial class GameRouter
{
private const string IntersectionSignalsKey = "drmk.intersection.signals";
private sealed class IntersectionSignalState
{
/// 每个路口当前处在第几相位(下标与 IntersectionTable.All 对齐)。
internal byte[] PeriodIndex = [];
/// 每个路口下一次切档的墙钟秒(TickCount64/1000);0 = 还没排。
internal double[] NextSwitchAt = [];
/// 这个路口是否已经推过至少一次(玩家走进半径后要补推)。
internal bool[] Pushed = [];
internal CancellationTokenSource? Cts;
internal Task? Loop;
}
private static IntersectionSignalState SignalsOf(TcpSession session)
{
lock (session.Items)
{
if (session.Items.TryGetValue(IntersectionSignalsKey, out var raw)
&& raw is IntersectionSignalState s)
return s;
var count = IntersectionTable.Count;
var state = new IntersectionSignalState
{
PeriodIndex = new byte[count],
NextSwitchAt = new double[count],
Pushed = new bool[count],
};
var all = IntersectionTable.All;
for (var i = 0; i < count; i++)
state.PeriodIndex[i] = all[i].CurrentPeriodIndex;
session.Items[IntersectionSignalsKey] = state;
return state;
}
}
private static bool UseZoneIndex()
{
var src = PrivateServerConfigStore.Current.Gameplay.Aether.IntersectionIndexSource;
return !string.Equals(src?.Trim(), "listIndex", StringComparison.OrdinalIgnoreCase);
}
///
/// 登录后给每个路口推一次 68819947(初始相位)。
/// ★ 这一步是「灯从灭到亮」的开关 —— 只发 init 不发它,客户端只知道有路口、不知道当前相位。
///
internal static async Task PushIntersectionUpdatesAsync(TcpSession session, CancellationToken token)
{
var cfg = PrivateServerConfigStore.Current.Gameplay.Aether;
if (!cfg.IntersectionUpdateEnabled)
return 0;
var all = IntersectionTable.All;
if (all.Count == 0)
return 0;
var state = SignalsOf(session);
var useZone = UseZoneIndex();
float px = 0f, pz = 0f;
var w = GetStateIfExists(session);
if (w is not null)
{
Vec3 player;
lock (w.SyncRoot)
player = w.LastReportedPlayerPosition;
px = player.X;
pz = player.Z;
}
var targets = IntersectionTable.OrderByDistance(px, pz, cfg.IntersectionRadiusMeters, cfg.IntersectionMaxCount);
var sent = 0;
var nowSec = Environment.TickCount64 / 1000.0;
var period = Math.Clamp(cfg.IntersectionPeriodSeconds, 1.0, 120.0);
foreach (var i in targets)
{
// ★ 幂等:只推**还没推过**的路口。这样多处调用(Vehicle init / static-NPC 批次)
// 都安全,也天然支持「玩家后来走进半径 ⇒ 补推新路口」。
if (state.Pushed[i])
continue;
var it = all[i];
var cur = state.PeriodIndex[i];
var count = Math.Max(1, it.PeriodCount);
await session.NotifyAsync(MethodId.SyncAetherAIIntersectionUpdateDa, UxSerializer.Serialize(
new GameMethods.SyncAetherAIIntersectionUpdateData4229938
{
data = new GameMethods.ClientTrafficIntersectionPeriodUpdateInfo4229938
{
IntersectionIndex = useZone ? (ulong)it.ZoneIndex : (ulong)i,
CurrentState = GameMethods.MassTrafficIntersectionState4229938.Start,
CurrentPeriodIndex = cur,
NextPeriodIndex = (byte)((cur + 1) % count),
RailPeriodIndex = it.RailPeriodIndex,
},
}), token);
state.Pushed[i] = true;
// ★ 错开各路口的切档时刻:否则 171 个包每周期同一瞬间全发(尖峰)。
state.NextSwitchAt[i] = nowSec + period * (0.5 + 0.5 * ((i * 37 % 100) / 100.0));
sent++;
// 规则 225/238:大批量 S2C 必须后台分批,不能同步连发。
if (sent % 16 == 0)
await Task.Delay(8, token).ConfigureAwait(false);
}
session.Log.Info($"[SIGNAL] 路口相位初始推送 {sent}/{all.Count} 个" +
$"(68819947,index 取自 {(useZone ? "zoneIndex" : "listIndex")},周期 {period:F0}s)");
return sent;
}
/// 启动(或确认已在跑)路口相位循环。与车流开关无关。
internal static void StartIntersectionSignals(TcpSession session)
{
var cfg = PrivateServerConfigStore.Current.Gameplay.Aether;
if (!cfg.IntersectionUpdateEnabled)
return;
if (IntersectionTable.Count == 0)
{
session.Log.Warn("[SIGNAL] 路口表为空 —— 检查 paths.intersections(灯不会亮)");
return;
}
var state = SignalsOf(session);
lock (session.Items)
{
if (state.Loop is { IsCompleted: false })
return;
state.Cts?.Dispose();
state.Cts = new CancellationTokenSource();
state.Loop = Task.Run(() => RunIntersectionSignalLoopAsync(session, state.Cts.Token));
}
}
/// 断开 / 切场景时停掉循环。
internal static void StopIntersectionSignals(TcpSession session)
{
lock (session.Items)
{
if (!session.Items.TryGetValue(IntersectionSignalsKey, out var raw)
|| raw is not IntersectionSignalState state)
return;
state.Cts?.Cancel();
state.Cts?.Dispose();
state.Cts = null;
}
}
private static async Task RunIntersectionSignalLoopAsync(TcpSession session, CancellationToken token)
{
var period = Math.Clamp(
PrivateServerConfigStore.Current.Gameplay.Aether.IntersectionPeriodSeconds, 1.0, 120.0);
var state = SignalsOf(session);
var useZone = UseZoneIndex();
// 会话销毁后(切角色 / 断线)循环要自己退 —— 主线没有统一的 session 级 CancellationToken
// (`StopTraffic` 同样没人调),所以这里用「连续取不到 world state」当退出信号。
var miss = 0;
session.Log.Info($"[SIGNAL] 相位循环启动 路口={IntersectionTable.Count} 周期={period:F0}s");
try
{
while (!token.IsCancellationRequested)
{
// 每 2 秒扫一次:既检查「该不该切档」,也给「玩家新走近的路口」补推。
await Task.Delay(2000, token).ConfigureAwait(false);
if (IsBackgroundPushPaused(session))
continue;
var cfg = PrivateServerConfigStore.Current.Gameplay.Aether;
if (!cfg.IntersectionUpdateEnabled)
continue;
var w = GetStateIfExists(session);
if (w is null)
{
if (++miss >= 5)
{
session.Log.Info("[SIGNAL] 相位循环退出(会话已销毁)");
return;
}
continue;
}
miss = 0;
if (!w.Ready || !w.HasLastReportedPlayerTransform)
continue;
Vec3 player;
lock (w.SyncRoot)
player = w.LastReportedPlayerPosition;
var all = IntersectionTable.All;
var targets = IntersectionTable.OrderByDistance(
player.X, player.Z, cfg.IntersectionRadiusMeters, cfg.IntersectionMaxCount);
var nowSec = Environment.TickCount64 / 1000.0;
var sent = 0;
foreach (var i in targets)
{
var it = all[i];
var count = Math.Max(1, it.PeriodCount);
// 还没推过(玩家刚走近)⇒ 先推当前相位。
if (!state.Pushed[i])
{
state.NextSwitchAt[i] = nowSec + period;
}
else if (nowSec < state.NextSwitchAt[i])
{
continue;
}
else
{
// 切档
state.PeriodIndex[i] = (byte)((state.PeriodIndex[i] + 1) % count);
state.NextSwitchAt[i] = nowSec + period;
}
var cur = state.PeriodIndex[i];
await session.NotifyAsync(MethodId.SyncAetherAIIntersectionUpdateDa,
UxSerializer.Serialize(new GameMethods.SyncAetherAIIntersectionUpdateData4229938
{
data = new GameMethods.ClientTrafficIntersectionPeriodUpdateInfo4229938
{
IntersectionIndex = useZone ? (ulong)it.ZoneIndex : (ulong)i,
CurrentState = GameMethods.MassTrafficIntersectionState4229938.Start,
CurrentPeriodIndex = cur,
NextPeriodIndex = (byte)((cur + 1) % count),
RailPeriodIndex = it.RailPeriodIndex,
},
}), token);
state.Pushed[i] = true;
sent++;
if (sent % 16 == 0)
await Task.Delay(8, token).ConfigureAwait(false);
}
if (sent > 0)
session.Log.Info($"[SIGNAL] 相位推进 {sent} 个路口(周期 {period:F0}s)");
}
}
catch (OperationCanceledException)
{
// 正常退出
}
catch (Exception ex)
{
session.Log.Warn($"[SIGNAL] 相位循环停止: {ex.Message}");
}
}
///
/// 体检用:路口数 / 多相位路口数 / 涉及车道数 / 首行摘要(启动时打一行,便于对账)。
///
internal static (int Intersections, int MultiPeriod, int Lanes, string Sample) ProbeIntersections()
=> IntersectionTable.Probe();
}