feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio
This commit is contained in:
1 parent
74e2963a12
commit
3e5adf1dc5
1571 files changed
+336468
-6348
No files matched your search
@@ -0,0 +1,293 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// ★★ 路口相位 / 红绿灯(2026-10-02 从捐赠端 PrivateServerPor 移植)—— **让灯柱真的亮起来**。
|
||||
///
|
||||
/// 背景(详见 <c>modules/红绿灯与路口相位.md</c>):
|
||||
/// 主线以前 init 的 <c>Intersections</c> 恒为 <c>[]</c>、运行期相位包
|
||||
/// <c>SyncAetherAIIntersectionUpdateDa</c>(68819947) **一次都没发过** ⇒ 客户端
|
||||
/// <c>AetherIntersectionManager</c> 建不出 <c>IntersectionDataEntity</c> ⇒ 不
|
||||
/// <c>RegisterTrafficLight</c> ⇒ 灯柱从来没被接管。<c>trafficSignalEnabled</c> 那套
|
||||
/// 只是服务端自己压车速,跟灯柱没关系。
|
||||
///
|
||||
/// 捐赠端的三条腿(我们照搬):
|
||||
/// ① init 带真路口表 → <c>GameAdminBridge</c> 的 <c>-donor</c> 变体(另见该文件)
|
||||
/// ② 登录后全量推一次 → <see cref="PushIntersectionUpdatesAsync"/>
|
||||
/// ③ 之后周期切档 → <see cref="TickIntersectionSignalsAsync"/>(本文件的后台循环)
|
||||
///
|
||||
/// ⛑⛑ 循环**不依赖车流开关**:用户 2026-10-02 19:38 把 <c>trafficEnabled</c> 关了,
|
||||
/// 灯仍然要亮 ⇒ 这里自己起 loop,不挂在 TrafficLoop 里。
|
||||
/// </summary>
|
||||
internal sealed partial class GameRouter
|
||||
{
|
||||
private const string IntersectionSignalsKey = "drmk.intersection.signals";
|
||||
|
||||
private sealed class IntersectionSignalState
|
||||
{
|
||||
/// <summary>每个路口当前处在第几相位(下标与 <c>IntersectionTable.All</c> 对齐)。</summary>
|
||||
internal byte[] PeriodIndex = [];
|
||||
|
||||
/// <summary>每个路口下一次切档的墙钟秒(<c>TickCount64/1000</c>);0 = 还没排。</summary>
|
||||
internal double[] NextSwitchAt = [];
|
||||
|
||||
/// <summary>这个路口是否已经推过至少一次(玩家走进半径后要补推)。</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登录后给每个路口推一次 68819947(初始相位)。
|
||||
/// ★ 这一步是「灯从灭到亮」的开关 —— 只发 init 不发它,客户端只知道有路口、不知道当前相位。
|
||||
/// </summary>
|
||||
internal static async Task<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>启动(或确认已在跑)路口相位循环。与车流开关无关。</summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>断开 / 切场景时停掉循环。</summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 体检用:路口数 / 多相位路口数 / 涉及车道数 / 首行摘要(启动时打一行,便于对账)。
|
||||
/// </summary>
|
||||
internal static (int Intersections, int MultiPeriod, int Lanes, string Sample) ProbeIntersections()
|
||||
=> IntersectionTable.Probe();
|
||||
}
|
||||
Reference in new issue
Block a user