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