using Ananta.SDK.Network; using Ananta.SDK.Rpc; using Ananta.SDK.Serialization; using Ananta.Server.Protocol.Client4229938; using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game; namespace Ananta.Server.Handlers.Game; /// /// Time + weather. The client runs its clock/atmosphere locally; these handlers make /// the in-game time UI work (accept + remember) and the admin panel can force both: /// time via SyncPlayerCurrentTime (proto-verified shape), weather via SyncPlayerWeather /// (lua-reader-verified: 3× u32). Stored per session, re-pushed after every load. /// internal sealed partial class GameRouter { private static void SetSessionTime(WorldEntryState state, uint hour, uint minute, bool fix, uint transition) { lock (state.SyncRoot) { state.TimeHour = Math.Min(hour, 23); state.TimeMinute = Math.Min(minute, 59); state.TimeFixed = fix; state.TimeTransitionSeconds = transition; state.HasExplicitTime = true; // ★ 玩家(或 GM/面板)主动推了一把时间 —— 记一次,供「赖在原地的角色该去过日子了」判据用。 state.TimeEpoch++; } } internal static async Task PushSessionTimeAsync(TcpSession session) { var state = GetStateIfExists(session); if (state is null) return; uint hour, minute, transition; bool fix; lock (state.SyncRoot) { if (!state.HasExplicitTime) return; hour = state.TimeHour; minute = state.TimeMinute; fix = state.TimeFixed; transition = state.TimeTransitionSeconds; } await session.NotifyAsync(MethodId.SyncPlayerCurrentTime, UxSerializer.Serialize( new GameMethods.SyncPlayerCurrentTime4229938 { realTime = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), raidDaySeconds = hour * 3600 + minute * 60, fix = fix, isPause = false, transitionSecond = transition, reason = 0, // RaidTimeAndWeatherChangeReason.Gm nodeId = 0, }), CancellationToken.None); session.Log.Info($"[TIME] push {hour:D2}:{minute:D2} fix={fix} transition={transition}s"); } internal static async Task PushSessionWeatherAsync(TcpSession session) { var state = GetStateIfExists(session); if (state is null) return; uint weatherId, transition; lock (state.SyncRoot) { if (!state.HasExplicitWeather) return; weatherId = state.WeatherId; transition = state.WeatherTransitionSeconds; } await session.NotifyAsync(MethodId.SyncPlayerWeather, UxSerializer.Serialize( new GameMethods.SyncPlayerWeather4229938 { weatherTypeId = weatherId, nextWeatherTypeId = weatherId, transitionSecond = transition, }), CancellationToken.None); session.Log.Info($"[WEATHER] push id={weatherId} transition={transition}s"); } [Handler(MethodId.AskPassingTime)] private async Task AskPassingTime(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { SetSessionTime(GetWorldState(msg.Context), args.hour, args.minute, true, 5); conn.Log.Info($"[TIME] AskPassingTime {args.hour:D2}:{args.minute:D2}"); await PushSessionTimeAsync(msg.Context.Session); } await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.GmPassingTime)] private async Task GmPassingTime(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { SetSessionTime(GetWorldState(msg.Context), args.hour, args.minute, true, 5); conn.Log.Info($"[TIME] GmPassingTime {args.hour:D2}:{args.minute:D2}"); await PushSessionTimeAsync(msg.Context.Session); } await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.GmSetTime)] private async Task GmSetTime(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { SetSessionTime(GetWorldState(msg.Context), args.hour, args.minute, true, args.transition); conn.Log.Info($"[TIME] GmSetTime {args.hour:D2}:{args.minute:D2} transition={args.transition}s"); await PushSessionTimeAsync(msg.Context.Session); } await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.GmFixRaidTime)] private async Task GmFixRaidTime(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var state = GetWorldState(msg.Context); if (args.clear) { lock (state.SyncRoot) { state.HasExplicitTime = false; // 「解除固定」同样是玩家主动改时间(回到实时),也要记一次。 state.TimeEpoch++; } conn.Log.Info("[TIME] GmFixRaidTime clear"); } else { SetSessionTime(state, args.hour, args.minute, true, 5); conn.Log.Info($"[TIME] GmFixRaidTime {args.hour:D2}:{args.minute:D2}"); await PushSessionTimeAsync(msg.Context.Session); } } await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.ChangePersonalTimeSetting)] private async Task ChangePersonalTimeSetting(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[TIME] personal slot={args.index} label={args.info?.Label} {args.info?.Hour:D2}:{args.info?.Minute:D2}"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.AddPersonalTimeSetting)] private async Task AddPersonalTimeSetting(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[TIME] personal add slot={args.index} label={args.info?.Label}"); await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.GmSetWeather)] private async Task GmSetWeather(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) { var state = GetWorldState(msg.Context); lock (state.SyncRoot) { state.WeatherId = args.weatherId; state.WeatherTransitionSeconds = 15; state.HasExplicitWeather = true; } conn.Log.Info($"[WEATHER] GmSetWeather id={args.weatherId}"); await PushSessionWeatherAsync(msg.Context.Session); } await conn.ReturnEmptyOkAsync(msg); } [Handler(MethodId.GmSetWeatherParam)] private async Task GmSetWeatherParam(Connection conn, UxRpcMessage msg) { if (msg.TryGetArgs(out var args) && args is not null) conn.Log.Info($"[WEATHER] GmSetWeatherParam id={args.weatherParamId}"); await conn.ReturnEmptyOkAsync(msg); } }