fix(streaming): rebuild the streaming window on a far login-spawn — the cold-spawn "hole"
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 <noreply@anthropic.com>
This commit is contained in:
parent
315af02f8a
commit
9b06a9b831
1 changed files with 29 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue