From 9b06a9b83147ad82b608b624ea52880c2d4a4824 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 3 Jul 2026 23:42:59 +0200 Subject: [PATCH] =?UTF-8?q?fix(streaming):=20rebuild=20the=20streaming=20w?= =?UTF-8?q?indow=20on=20a=20far=20login-spawn=20=E2=80=94=20the=20cold-spa?= =?UTF-8?q?wn=20"hole"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (confirmed live via the landblock-load probe): the streaming window marks every landblock "resident" at bootstrap (MarkResidentFromBootstrap) BEFORE their async loads land. When a character spawns far from the startup center (0xA9B4 Holtburg), the login-spawn recenter runs RecenterTo against that stale, half-loaded window — and RecenterTo trusts _tierResidence, so it never re-enqueues the old/new window overlap. The overlap band is marked resident but its loads never completed, leaving a permanent HOLE of landblocks that are never requested (zero BUILD-NULL — they're simply skipped). The player spawns in the hole: its landblock never loads, so terrain/NPCs/player never draw — the "world not loading / invisible / scenery behind" symptom. Probe evidence (spawn at 0xADAF): the player's column 0xAD loads Y=0xA3-0xAA and 0xB0-0xC0 but is MISSING Y=0xAB-0xAF — and the player is at 0xADAF (Y=0xAF), dead in the gap. 0xADAFFFFF: never [lb] ADD'd, never BUILD-NULL'd. Fix: a far login-spawn moves the render origin exactly like an outdoor teleport (which already calls StreamingController.ForceReloadWindow to drop the stale window + re-bootstrap fresh around the new origin — GameWindow.cs ~5725). The login-spawn recenter now flags the same rebuild; because the spawn handler runs on the network thread and ForceReloadWindow is render-thread-only, the flag is set there and consumed in OnUpdateFrame's streaming block before the Tick. Verified live: 0xADAFFFFF now `[lb] ADD entities=216`, full world draws (flatCount=12807), and the player transitions PENDING(hidden) -> DRAWSET PRESENT (composes with the RelocateEntity pending-recovery fix, 315af02f). No-op for a normal Holtburg login (spawn == startup center → guard false). Full suite 4007 green. Co-Authored-By: Claude Fable 5 --- src/AcDream.App/Rendering/GameWindow.cs | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e8c83238..5edfaee4 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -880,6 +880,17 @@ public sealed class GameWindow : IDisposable private AcDream.Core.Net.WorldSession? _liveSession; private int _liveCenterX; private int _liveCenterY; + // Set by the login-spawn recenter (network thread) when the spawn landblock + // differs from the startup streaming center; consumed on the render thread + // in OnUpdateFrame to trigger StreamingController.ForceReloadWindow(). A far + // login-spawn moves the render origin just like an outdoor teleport, so the + // streaming region — bootstrapped around the stale startup center — must be + // rebuilt fresh around the spawn. Without this, RecenterTo treats the + // old/new window overlap as already-resident (MarkResidentFromBootstrap marks + // residence before the async loads land), leaving a permanent hole of + // resident-but-never-loaded landblocks where the player spawns — the + // cold-spawn "invisible player / world not loading" bug (2026-07-03). + private volatile bool _pendingForceReloadWindow; private uint _liveEntityIdCounter = 1_000_000u; // well above any dat-hydrated id // K-fix1 (2026-04-26): cached at startup so per-frame branches are @@ -3197,6 +3208,13 @@ public sealed class GameWindow : IDisposable $"to ({lbX},{lbY}) for player spawn @0x{p.LandblockId:X8}"); _liveCenterX = lbX; _liveCenterY = lbY; + // The origin jumped — the streaming region is still bootstrapped + // around the stale startup center with residence marked before its + // async loads landed. Flag a full window rebuild (consumed on the + // render thread in OnUpdateFrame) so the region re-bootstraps fresh + // around the spawn, re-loading the landblocks RecenterTo would + // otherwise skip as already-resident — the cold-spawn streaming hole. + _pendingForceReloadWindow = true; } // #135: the instant we know the player spawned into a SEALED dungeon, @@ -7826,6 +7844,17 @@ public sealed class GameWindow : IDisposable observerCx = (int)((cellLb >> 8) & 0xFFu); observerCy = (int)(cellLb & 0xFFu); } + // Consume the login-spawn far-recenter flag (network thread → render + // thread): drop the stale startup window + null the region so this + // Tick re-bootstraps the whole window fresh around the spawn origin. + // Same mechanism the outdoor-teleport path uses (line ~5725). Fixes + // the cold-spawn streaming hole (resident-but-never-loaded landblocks + // the RecenterTo diff skipped as already-resident). + if (_pendingForceReloadWindow) + { + _pendingForceReloadWindow = false; + _streamingController.ForceReloadWindow(); + } _streamingController.Tick(observerCx, observerCy, insideDungeon); // Re-inject persistent entities rescued from unloaded landblocks