acdream/tests/AcDream.Core.Tests/World/StreamingReadinessGateTests.cs
Erik fa9aedca0a fix(#192): close login streaming race — gate on a known real position, not session state
User-reported: logging in at a non-Holtburg position sometimes showed
stabs/scenery floating in the wrong place. Root-caused via a threading/
lifecycle trace, not inference: WorldSession transitions to InWorld
immediately after the login handshake (WorldSession.cs:608) — well
before the player's own spawn CreateObject (which carries their real
position) has arrived over the network. The streaming gate's old
condition (`!IsLiveModeWaitingForLogin || liveInWorld`) opened the
instant InWorld fired, letting the background streaming worker
(LandblockStreamer's dedicated Thread) build real landblocks using
whatever _liveCenterX/Y held at that moment — the Holtburg startup
placeholder, not a "not known yet" sentinel. Landblocks that started
building in that window bake their world offset from that placeholder
at build time; if their build was still in flight when the real spawn
arrived and recentered the world (ForceReloadWindow, which only
unloads already-RESIDENT landblocks), they finished and got applied
anyway — stale-positioned geometry landing wherever the guess put it
relative to whatever streamed in afterward with the corrected center.

Confirmed with the user this wasn't "wrong placeholder, need a better
one" — any placeholder racing against the real answer reproduces the
same bug. The fix removes the race instead: StreamingReadinessGate
.ShouldStream requires an explicit liveCenterKnown flag (true only once
the player's own spawn has been processed) in addition to liveInWorld.
Nothing streams in live mode until the real position is confirmed — no
placeholder value is ever acted on. Preserves the #106 gate-3 fix this
gate originally existed for (auto-entry waits for terrain under the
spawn; terrain streaming must not wait for chase mode in turn, or the
two deadlock) since liveCenterKnown becomes true independently of chase
mode, driven purely by the spawn packet's arrival.

The stricter render gate (GameWindow.cs:9596, hides ALL world geometry
until chase mode engages, no relaxation) already provided a partial
safety net and is unchanged — this fix stops the stale geometry from
ever being built, rather than relying on the render gate to hide it
until the reveal.

Core 2680+2skip / App 741+2skip / UI 425 / Net 385 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 15:57:43 +02:00

72 lines
3.4 KiB
C#

using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.World;
/// <summary>
/// #192 (2026-07-09) — login into a non-Holtburg position sometimes showed
/// stabs/scenery floating in the wrong place. Root cause: the streaming/render
/// gate opened as soon as <c>WorldSession</c> reached <c>InWorld</c>
/// (<c>GameWindow.cs</c>'s old <c>!IsLiveModeWaitingForLogin || liveInWorld</c>
/// condition) — but <c>InWorld</c> fires immediately after the login handshake
/// (<c>WorldSession.cs:608</c>), BEFORE the player's own spawn <c>CreateObject</c>
/// (which carries their real position) has even arrived over the network. Any
/// landblock streamed in that window baked its world offset from
/// <c>_liveCenterX/Y</c>'s STARTUP placeholder (Holtburg, 0xA9B4) — a real
/// position, not a "we don't know yet" sentinel — and that stale-baked geometry
/// still got applied once it finished building, landing wherever the guess put
/// it relative to whatever streamed in afterward with the corrected center.
///
/// The fix isn't "pick a different placeholder" (any placeholder racing against
/// the real answer reproduces the same bug) — it's gating on whether the real
/// position is actually known yet, independent of session state.
/// </summary>
public class StreamingReadinessGateTests
{
[Fact]
public void ShouldStream_OfflineMode_AlwaysTrue()
{
// Not live mode at all — no race is possible, no real position to wait for.
Assert.True(StreamingReadinessGate.ShouldStream(
liveModeEnabled: false, chaseModeEverEntered: false, liveInWorld: false, liveCenterKnown: false));
}
[Fact]
public void ShouldStream_ChaseModeEverEntered_AlwaysTrueRegardlessOfCenterKnown()
{
// Once chase mode has engaged even once, the gate latches open for the
// rest of the session (matches the pre-existing IsLiveModeWaitingForLogin
// latch semantics) — this is well past the login race window.
Assert.True(StreamingReadinessGate.ShouldStream(
liveModeEnabled: true, chaseModeEverEntered: true, liveInWorld: true, liveCenterKnown: false));
}
[Fact]
public void ShouldStream_LiveModeNotYetInWorld_False()
{
Assert.False(StreamingReadinessGate.ShouldStream(
liveModeEnabled: true, chaseModeEverEntered: false, liveInWorld: false, liveCenterKnown: false));
}
[Fact]
public void ShouldStream_LiveModeInWorldButCenterUnknown_False()
{
// THE REGRESSION TEST. Under the old code, liveInWorld alone opened the
// gate here — this is exactly the window where InWorld has fired but the
// player's own spawn CreateObject (and hence the real center) hasn't
// arrived yet. Must stay closed.
Assert.False(StreamingReadinessGate.ShouldStream(
liveModeEnabled: true, chaseModeEverEntered: false, liveInWorld: true, liveCenterKnown: false));
}
[Fact]
public void ShouldStream_LiveModeInWorldAndCenterKnown_True()
{
// Once the real position is confirmed, stream even though chase mode
// hasn't engaged yet — this preserves the #106 gate-3 fix (auto-entry
// waits for terrain under the spawn; terrain streaming must not wait for
// auto-entry in turn, or the two deadlock each other).
Assert.True(StreamingReadinessGate.ShouldStream(
liveModeEnabled: true, chaseModeEverEntered: false, liveInWorld: true, liveCenterKnown: true));
}
}