feat(net+ui): Phase G.1 — server time sync + debug controls
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) <noreply@anthropic.com>
This commit is contained in:
parent
7b9a66c9ea
commit
cd89e9a498
3 changed files with 116 additions and 3 deletions
|
|
@ -101,7 +101,14 @@ public sealed class DebugOverlay
|
||||||
int StreamingRadius,
|
int StreamingRadius,
|
||||||
float MouseSensitivity,
|
float MouseSensitivity,
|
||||||
float ChaseDistance,
|
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)
|
public DebugOverlay(TextRenderer text, BitmapFont font)
|
||||||
{
|
{
|
||||||
|
|
@ -205,6 +212,10 @@ public sealed class DebugOverlay
|
||||||
($"lb {s.LandblocksVisible,3}/{s.LandblocksTotal,3} visible", White),
|
($"lb {s.LandblocksVisible,3}/{s.LandblocksTotal,3} visible", White),
|
||||||
($"ent {s.EntityCount,4} anim {s.AnimatedCount,3}", White),
|
($"ent {s.EntityCount,4} anim {s.AnimatedCount,3}", White),
|
||||||
($"coll {s.ShadowObjectCount,5} radius {s.StreamingRadius}", 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;
|
float pad = 10f;
|
||||||
|
|
@ -277,6 +288,8 @@ public sealed class DebugOverlay
|
||||||
("F4", "toggle debug HUD info panel"),
|
("F4", "toggle debug HUD info panel"),
|
||||||
("F5", "toggle stats panel"),
|
("F5", "toggle stats panel"),
|
||||||
("F6", "toggle compass"),
|
("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"),
|
("F", "toggle fly camera"),
|
||||||
("Tab", "toggle player mode (requires login)"),
|
("Tab", "toggle player mode (requires login)"),
|
||||||
("W A S D", "move (player mode) / fly"),
|
("W A S D", "move (player mode) / fly"),
|
||||||
|
|
@ -388,7 +401,7 @@ public sealed class DebugOverlay
|
||||||
|
|
||||||
private void DrawHintBar(Vector2 screenSize)
|
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 w = _font.MeasureWidth(hint);
|
||||||
float pad = 10f;
|
float pad = 10f;
|
||||||
float y = screenSize.Y - _font.LineHeight - pad;
|
float y = screenSize.Y - _font.LineHeight - pad;
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,12 @@ public sealed class GameWindow : IDisposable
|
||||||
AcDream.Core.World.WeatherKind.Clear;
|
AcDream.Core.World.WeatherKind.Clear;
|
||||||
private double _weatherAccum;
|
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.
|
// Phase B.2: player movement mode.
|
||||||
private AcDream.App.Input.PlayerMovementController? _playerController;
|
private AcDream.App.Input.PlayerMovementController? _playerController;
|
||||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
||||||
|
|
@ -370,6 +376,51 @@ public sealed class GameWindow : IDisposable
|
||||||
_debugOverlay.Toast($"Compass {(_debugOverlay.ShowCompass ? "ON" : "OFF")}");
|
_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)
|
else if (key == Key.F8 || key == Key.F9)
|
||||||
{
|
{
|
||||||
// Adjust whichever mode's sensitivity is currently active.
|
// Adjust whichever mode's sensitivity is currently active.
|
||||||
|
|
@ -824,6 +875,11 @@ public sealed class GameWindow : IDisposable
|
||||||
_liveSession.PositionUpdated += OnLivePositionUpdated;
|
_liveSession.PositionUpdated += OnLivePositionUpdated;
|
||||||
_liveSession.TeleportStarted += OnTeleportStarted;
|
_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
|
// Phase F.1-H.1: wire every parsed GameEvent into the right
|
||||||
// Core state class (chat, combat, spellbook, items). After
|
// Core state class (chat, combat, spellbook, items). After
|
||||||
// this one call, server-sent ChannelBroadcast / damage
|
// this one call, server-sent ChannelBroadcast / damage
|
||||||
|
|
@ -3238,6 +3294,8 @@ public sealed class GameWindow : IDisposable
|
||||||
else
|
else
|
||||||
activeSens = _sensOrbit;
|
activeSens = _sensOrbit;
|
||||||
|
|
||||||
|
// Phase G: pull sky + weather + lighting state for the overlay.
|
||||||
|
var dayCal = WorldTime.CurrentCalendar;
|
||||||
var snapshot = new DebugOverlay.Snapshot(
|
var snapshot = new DebugOverlay.Snapshot(
|
||||||
Fps: (float)_lastFps,
|
Fps: (float)_lastFps,
|
||||||
FrameTimeMs: (float)_lastFrameMs,
|
FrameTimeMs: (float)_lastFrameMs,
|
||||||
|
|
@ -3260,7 +3318,13 @@ public sealed class GameWindow : IDisposable
|
||||||
StreamingRadius: _streamingRadius,
|
StreamingRadius: _streamingRadius,
|
||||||
MouseSensitivity: activeSens,
|
MouseSensitivity: activeSens,
|
||||||
ChaseDistance: _chaseCamera?.Distance ?? 0f,
|
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);
|
_debugOverlay.Update((float)deltaSeconds);
|
||||||
var size = new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y);
|
var size = new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y);
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,21 @@ public sealed class WorldSession : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<HearSpeech.Parsed>? SpeechHeard;
|
public event Action<HearSpeech.Parsed>? SpeechHeard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>WorldTimeService.SyncFromServer</c>
|
||||||
|
/// so client-local day/night stays in lockstep with the server clock.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<double>? ServerTimeUpdated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Latest server tick count from <see cref="ServerTimeUpdated"/>
|
||||||
|
/// events. 0 until the handshake completes.
|
||||||
|
/// </summary>
|
||||||
|
public double LastServerTimeTicks { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allow re-sending LoginComplete after a portal teleport. The normal
|
/// Allow re-sending LoginComplete after a portal teleport. The normal
|
||||||
/// _loginCompleteSent latch prevents duplicate sends on the initial spawn
|
/// _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
|
// Step 3: seed ISAAC, send ConnectResponse to port+1, with 200ms race delay
|
||||||
var opt = cr.Optional;
|
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];
|
byte[] serverSeedBytes = new byte[4];
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(serverSeedBytes, opt.ConnectRequestServerSeed);
|
BinaryPrimitives.WriteUInt32LittleEndian(serverSeedBytes, opt.ConnectRequestServerSeed);
|
||||||
byte[] clientSeedBytes = new byte[4];
|
byte[] clientSeedBytes = new byte[4];
|
||||||
|
|
@ -393,6 +416,19 @@ public sealed class WorldSession : IDisposable
|
||||||
SendAck(serverHeader.Sequence);
|
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)
|
foreach (var frag in dec.Packet!.Fragments)
|
||||||
{
|
{
|
||||||
var body = _assembler.Ingest(frag, out _);
|
var body = _assembler.Ingest(frag, out _);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue