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:
Erik 2026-04-19 10:51:03 +02:00
parent 7b9a66c9ea
commit cd89e9a498
3 changed files with 116 additions and 3 deletions

View file

@ -109,6 +109,21 @@ public sealed class WorldSession : IDisposable
/// </summary>
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>
/// 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 _);