fix(streaming): block login on complete world reveal

Make initial login and portal arrival consume one WorldRevealReadinessBarrier that joins near-tier mesh publication, destination composite uploads, and collision residency before normal world geometry becomes visible. This ports retail SmartBox's blocking-cell completion edge into the asynchronous client instead of exposing a ground-only login.

Add focused outdoor, indoor, texture, and invalid-claim tests; update the retail pseudocode, architecture, divergence record, issue ledger, roadmap baseline, and synchronized agent guidance.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-20 16:08:03 +02:00
parent 6c3bd4ce4b
commit b60cb67009
14 changed files with 360 additions and 102 deletions

View file

@ -0,0 +1,124 @@
using AcDream.App.Streaming;
namespace AcDream.App.Tests.Streaming;
public sealed class WorldRevealReadinessBarrierTests
{
private sealed class State
{
public bool RenderReady;
public bool SpawnCellReady;
public bool TerrainReady;
public bool CompositeReady;
public bool Unhydratable;
public int Invalidations;
public int Preparations;
public uint PreparedCell;
public int PreparedRadius = -1;
public int RenderRadius = -1;
public int TerrainRadius = -1;
public WorldRevealReadinessBarrier Build() => new(
isRenderNeighborhoodReady: (cell, radius) =>
{
RenderRadius = radius;
return RenderReady;
},
isSpawnCellReady: _ => SpawnCellReady,
isTerrainNeighborhoodReady: (cell, radius) =>
{
TerrainRadius = radius;
return TerrainReady;
},
areCompositeTexturesReady: () => CompositeReady,
prepareCompositeTextures: (cell, radius) =>
{
Preparations++;
PreparedCell = cell;
PreparedRadius = radius;
},
invalidateCompositeTextures: () => Invalidations++,
isSpawnClaimUnhydratable: _ => Unhydratable);
}
[Fact]
public void Begin_InvalidatesPriorDestinationTextureReadiness()
{
var state = new State();
var barrier = state.Build();
barrier.Begin();
Assert.Equal(1, state.Invalidations);
}
[Fact]
public void OutdoorReveal_JoinsNearRenderTexturesAndTerrain()
{
const uint outdoorCell = 0x11340021u;
var state = new State();
var barrier = state.Build();
Assert.False(barrier.IsReady(outdoorCell));
Assert.Equal(WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius, state.RenderRadius);
state.RenderReady = true;
barrier.Prepare(outdoorCell);
Assert.Equal(1, state.Preparations);
Assert.Equal(outdoorCell, state.PreparedCell);
Assert.Equal(WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius, state.PreparedRadius);
state.CompositeReady = true;
Assert.False(barrier.IsReady(outdoorCell));
state.TerrainReady = true;
Assert.True(barrier.IsReady(outdoorCell));
Assert.Equal(WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius, state.TerrainRadius);
}
[Fact]
public void IndoorReveal_UsesCenterRenderAndExactEnvCellPhysics()
{
const uint indoorCell = 0x11340100u;
var state = new State
{
RenderReady = true,
CompositeReady = true,
TerrainReady = true,
};
var barrier = state.Build();
barrier.Prepare(indoorCell);
Assert.Equal(0, state.PreparedRadius);
Assert.False(barrier.IsReady(indoorCell));
Assert.Equal(-1, state.TerrainRadius);
state.SpawnCellReady = true;
Assert.True(barrier.IsReady(indoorCell));
Assert.Equal(0, state.RenderRadius);
}
[Fact]
public void Prepare_WaitsForStaticMeshPublication()
{
var state = new State();
var barrier = state.Build();
barrier.Prepare(0x11340021u);
Assert.Equal(0, state.Preparations);
}
[Fact]
public void ImpossibleClaim_CrossesExistingLoudRecoveryPath()
{
var state = new State { Unhydratable = true };
var barrier = state.Build();
barrier.Prepare(0x113401FFu);
Assert.True(barrier.IsReady(0x113401FFu));
Assert.Equal(0, state.Preparations);
Assert.Equal(-1, state.RenderRadius);
}
}

View file

@ -24,9 +24,9 @@ public sealed class AutoEnterPlayerModeTests
public bool LiveInWorld;
public bool PlayerEntityPresent;
public bool PlayerControllerReady;
// Defaults TRUE: the hydration hold is not under test in the
// original K.2 cases (see SpawnGroundNotReady test for it).
public bool SpawnGroundReady = true;
// Defaults TRUE: the world-reveal hold is not under test in the
// original K.2 cases (see WorldNotReady test for it).
public bool WorldReady = true;
public int EnteredCount;
public PlayerModeAutoEntry Build() =>
@ -34,23 +34,20 @@ public sealed class AutoEnterPlayerModeTests
isLiveInWorld: () => LiveInWorld,
isPlayerEntityPresent: () => PlayerEntityPresent,
isPlayerControllerReady: () => PlayerControllerReady,
isSpawnGroundReady: () => SpawnGroundReady,
isWorldReady: () => WorldReady,
enterPlayerMode: () => EnteredCount++);
}
[Fact]
public void TryEnter_Armed_SpawnGroundNotReady_DoesNotFire()
public void TryEnter_Armed_WorldNotReady_DoesNotFire()
{
// #106 gate-2 (2026-06-09): player physics must not start before
// the terrain under the spawn position has streamed in — entering
// earlier free-falls the player into the void (gravity integrates
// against an empty world; retail never has this state because it
// loads cells synchronously). The guard holds until the streaming
// pipeline registers the spawn landblock.
// #229 extends #106's ground-only hold to retail's complete blocking
// cell-load edge. Player mode must not expose the viewport until the
// render, composite-texture, and collision domains have all converged.
var s = new State
{
LiveInWorld = true, PlayerEntityPresent = true,
PlayerControllerReady = true, SpawnGroundReady = false,
PlayerControllerReady = true, WorldReady = false,
};
var guard = s.Build();
guard.Arm();
@ -59,8 +56,8 @@ public sealed class AutoEnterPlayerModeTests
Assert.Equal(0, s.EnteredCount);
Assert.True(guard.IsArmed);
// Terrain hydrates → fires on the next tick.
s.SpawnGroundReady = true;
// Render, texture, and collision domains converge → next tick fires.
s.WorldReady = true;
Assert.True(guard.TryEnter());
Assert.Equal(1, s.EnteredCount);
}