From cd89e9a498349d38b9a8d9c453cd21262ca3aeaf Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 19 Apr 2026 10:51:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(net+ui):=20Phase=20G.1=20=E2=80=94=20serve?= =?UTF-8?q?r=20time=20sync=20+=20debug=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorldSession now surfaces the server's PortalYearTicks via a new ServerTimeUpdated event, fired from two sources per r12 §12: 1. Initial ConnectRequest handshake (ConnectRequestServerTime field of the optional block — seeds the clock on login). 2. Every subsequent packet carrying the TimeSync header flag (0x01000000) — keeps the client clock within one TimeSync period of authoritative server time. GameWindow subscribes the event into WorldTimeService.SyncFromServer, so the day/night cycle + keyframe interpolation runs from real server time in live mode. Offline mode (ACDREAM_LIVE=0) still uses the seeded-to-noon fallback from OnLoad. DebugOverlay now exposes sky + weather + lighting state: time 0.50 Midsong (day fraction + hour name) wx Clear parts 0 (weather kind + live particle count) lit 1/4 (active / registered lights) F7 cycles a debug time override through (none → midnight → dawn → noon → dusk → none) F10 cycles weather through (Clear → Overcast → Rain → Snow → Storm). These keybinds satisfy the visual-verification tier so a user can flip through every state from the running client without touching the code. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/DebugOverlay.cs | 17 +++++- src/AcDream.App/Rendering/GameWindow.cs | 66 ++++++++++++++++++++++- src/AcDream.Core.Net/WorldSession.cs | 36 +++++++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/AcDream.App/Rendering/DebugOverlay.cs b/src/AcDream.App/Rendering/DebugOverlay.cs index 57ab741..db2ba2c 100644 --- a/src/AcDream.App/Rendering/DebugOverlay.cs +++ b/src/AcDream.App/Rendering/DebugOverlay.cs @@ -101,7 +101,14 @@ public sealed class DebugOverlay int StreamingRadius, float MouseSensitivity, float ChaseDistance, - bool RmbOrbit); + bool RmbOrbit, + // Phase G.1/G.2 — sky + weather + lighting + string HourName = "", + float DayFraction = 0f, + string Weather = "Clear", + int ActiveLights = 0, + int RegisteredLights = 0, + int ParticleCount = 0); public DebugOverlay(TextRenderer text, BitmapFont font) { @@ -205,6 +212,10 @@ public sealed class DebugOverlay ($"lb {s.LandblocksVisible,3}/{s.LandblocksTotal,3} visible", White), ($"ent {s.EntityCount,4} anim {s.AnimatedCount,3}", White), ($"coll {s.ShadowObjectCount,5} radius {s.StreamingRadius}", White), + // Phase G: sky + weather + dynamic lighting surface. + ($"time {s.DayFraction,5:F2} {s.HourName}", Cyan), + ($"wx {s.Weather,-8} parts {s.ParticleCount,5}", Cyan), + ($"lit {s.ActiveLights}/{s.RegisteredLights} ", Cyan), }; float pad = 10f; @@ -277,6 +288,8 @@ public sealed class DebugOverlay ("F4", "toggle debug HUD info panel"), ("F5", "toggle stats panel"), ("F6", "toggle compass"), + ("F7", "cycle time-of-day override (none/midnight/dawn/noon/dusk)"), + ("F10", "cycle weather (clear/overcast/rain/snow/storm)"), ("F", "toggle fly camera"), ("Tab", "toggle player mode (requires login)"), ("W A S D", "move (player mode) / fly"), @@ -388,7 +401,7 @@ public sealed class DebugOverlay private void DrawHintBar(Vector2 screenSize) { - string hint = "F1 help F2 wireframes F3 dump F4/F5/F6 panels F8/F9 sens Tab player Hold RMB orbit Wheel zoom"; + string hint = "F1 help F2 wires F3 dump F4/F5/F6 panels F7 time F8/F9 sens F10 weather Tab player RMB orbit Wheel zoom"; float w = _font.MeasureWidth(hint); float pad = 10f; float y = screenSize.Y - _font.LineHeight - pad; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a1f9e54..e87aec8 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -184,6 +184,12 @@ public sealed class GameWindow : IDisposable AcDream.Core.World.WeatherKind.Clear; private double _weatherAccum; + // F7 / F10 debug-cycle steps for time + weather. Initialized out of + // range of the real values so the first press hits index 0 of the + // cycle table cleanly. + private int _timeDebugStep = 0; + private int _weatherDebugStep = 0; + // Phase B.2: player movement mode. private AcDream.App.Input.PlayerMovementController? _playerController; private AcDream.App.Rendering.ChaseCamera? _chaseCamera; @@ -370,6 +376,51 @@ public sealed class GameWindow : IDisposable _debugOverlay.Toast($"Compass {(_debugOverlay.ShowCompass ? "ON" : "OFF")}"); } } + else if (key == Key.F7) + { + // Phase G.1: cycle debug time-of-day overrides. Useful for + // visually verifying the sun arc + keyframe transitions + // without waiting 30+ real-time hours. Cycle order: + // clear debug → 0.0 (midnight) → 0.25 (dawn) + // → 0.5 (noon) → 0.75 (dusk) → clear + _timeDebugStep = (_timeDebugStep + 1) % 5; + float? pick = _timeDebugStep switch + { + 0 => (float?)null, // server time + 1 => 0.0f, + 2 => 0.25f, + 3 => 0.5f, + 4 => 0.75f, + _ => null, + }; + if (pick.HasValue) + { + WorldTime.SetDebugTime(pick.Value); + _debugOverlay?.Toast($"Time override = {pick.Value:F2}"); + } + else + { + WorldTime.ClearDebugTime(); + _debugOverlay?.Toast("Time override cleared"); + } + } + else if (key == Key.F10) + { + // Phase G.1: cycle weather kinds manually. Useful for + // testing the rain/snow particle systems + storm/light + // fog without waiting for the daily RNG to hit. + var kinds = new[] + { + AcDream.Core.World.WeatherKind.Clear, + AcDream.Core.World.WeatherKind.Overcast, + AcDream.Core.World.WeatherKind.Rain, + AcDream.Core.World.WeatherKind.Snow, + AcDream.Core.World.WeatherKind.Storm, + }; + _weatherDebugStep = (_weatherDebugStep + 1) % kinds.Length; + Weather.ForceWeather(kinds[_weatherDebugStep]); + _debugOverlay?.Toast($"Weather = {kinds[_weatherDebugStep]}"); + } else if (key == Key.F8 || key == Key.F9) { // Adjust whichever mode's sensitivity is currently active. @@ -824,6 +875,11 @@ public sealed class GameWindow : IDisposable _liveSession.PositionUpdated += OnLivePositionUpdated; _liveSession.TeleportStarted += OnTeleportStarted; + // Phase G.1: keep the client's day/night clock in sync with + // server time. Fires once from ConnectRequest (initial seed) + // and repeatedly on TimeSync-flagged packets. + _liveSession.ServerTimeUpdated += ticks => WorldTime.SyncFromServer(ticks); + // Phase F.1-H.1: wire every parsed GameEvent into the right // Core state class (chat, combat, spellbook, items). After // this one call, server-sent ChannelBroadcast / damage @@ -3238,6 +3294,8 @@ public sealed class GameWindow : IDisposable else activeSens = _sensOrbit; + // Phase G: pull sky + weather + lighting state for the overlay. + var dayCal = WorldTime.CurrentCalendar; var snapshot = new DebugOverlay.Snapshot( Fps: (float)_lastFps, FrameTimeMs: (float)_lastFrameMs, @@ -3260,7 +3318,13 @@ public sealed class GameWindow : IDisposable StreamingRadius: _streamingRadius, MouseSensitivity: activeSens, ChaseDistance: _chaseCamera?.Distance ?? 0f, - RmbOrbit: _rmbHeld); + RmbOrbit: _rmbHeld, + HourName: dayCal.Hour.ToString(), + DayFraction: (float)WorldTime.DayFraction, + Weather: Weather.Kind.ToString(), + ActiveLights: Lighting.ActiveCount, + RegisteredLights: Lighting.RegisteredCount, + ParticleCount: _particleSystem?.ActiveParticleCount ?? 0); _debugOverlay.Update((float)deltaSeconds); var size = new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y); diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index c39bacf..e4d9649 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -109,6 +109,21 @@ public sealed class WorldSession : IDisposable /// public event Action? SpeechHeard; + /// + /// Phase G.1: latest server Portal Year tick count. Seeded from the + /// ConnectRequest handshake (r12 §1.3 — server sends absolute game + /// time as a double) and refreshed on every TimeSync-flagged packet. + /// Subscribers feed this into WorldTimeService.SyncFromServer + /// so client-local day/night stays in lockstep with the server clock. + /// + public event Action? ServerTimeUpdated; + + /// + /// Latest server tick count from + /// events. 0 until the handshake completes. + /// + public double LastServerTimeTicks { get; private set; } + /// /// Allow re-sending LoginComplete after a portal teleport. The normal /// _loginCompleteSent latch prevents duplicate sends on the initial spawn @@ -224,6 +239,14 @@ public sealed class WorldSession : IDisposable // Step 3: seed ISAAC, send ConnectResponse to port+1, with 200ms race delay var opt = cr.Optional; + + // Phase G.1: server's initial PortalYearTicks (r12 §1.3) lives + // in the ConnectRequest optional section. Publish it to + // subscribers so WorldTimeService.SyncFromServer can seed the + // client clock. + LastServerTimeTicks = opt.ConnectRequestServerTime; + ServerTimeUpdated?.Invoke(opt.ConnectRequestServerTime); + byte[] serverSeedBytes = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(serverSeedBytes, opt.ConnectRequestServerSeed); byte[] clientSeedBytes = new byte[4]; @@ -393,6 +416,19 @@ public sealed class WorldSession : IDisposable SendAck(serverHeader.Sequence); } + // Phase G.1: propagate TimeSync-flagged server time to anyone who + // needs it (sky/day-night lerp in particular). Server sends this + // periodically — no explicit opcode, just the header flag. + if ((serverHeader.Flags & PacketHeaderFlags.TimeSync) != 0) + { + double t = dec.Packet!.Optional.TimeSync; + if (t > 0) + { + LastServerTimeTicks = t; + ServerTimeUpdated?.Invoke(t); + } + } + foreach (var frag in dec.Packet!.Fragments) { var body = _assembler.Ingest(frag, out _);