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