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:
parent
6c3bd4ce4b
commit
b60cb67009
14 changed files with 360 additions and 102 deletions
|
|
@ -147,6 +147,7 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Streaming.LandblockRetirementCoordinator? _landblockRetirements;
|
||||
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
||||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||
private AcDream.App.Streaming.WorldRevealReadinessBarrier? _worldRevealReadiness;
|
||||
private int _streamingRadius = 2; // default 5×5 (kept for debug overlay getStreamingRadius callback)
|
||||
private int _nearRadius = 4; // Phase A.5 T16: two-tier near ring (default 4 → 9×9)
|
||||
private int _farRadius = 12; // Phase A.5 T16: two-tier far ring (default 12 → 25×25)
|
||||
|
|
@ -1413,41 +1414,11 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.Net.WorldSession.State.InWorld,
|
||||
isPlayerEntityPresent: () => _entitiesByServerGuid.ContainsKey(_playerServerGuid),
|
||||
isPlayerControllerReady: () => true,
|
||||
// #106 gate-2: hold player-mode entry until the terrain under
|
||||
// the spawn position has streamed in — entering earlier
|
||||
// integrates gravity against an empty world and free-falls
|
||||
// the player into the void (retail loads cells synchronously;
|
||||
// this is the async-streaming equivalent of that invariant).
|
||||
isSpawnGroundReady: () =>
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)) return false;
|
||||
|
||||
// #107 / #135: spawn-ground readiness is spawn-claim aware. For an
|
||||
// INDOOR claim (sealed dungeon / building interior) the ground the
|
||||
// player lands on is the EnvCell FLOOR (its BSP), so gate on the
|
||||
// cell's hydration (IsSpawnCellReady) — NOT the terrain heightmap.
|
||||
// A dungeon's cells sit in their landblock at an arbitrary (often
|
||||
// negative) offset, so the spawn's WORLD position can fall in a
|
||||
// NEIGHBOUR terrain landblock that the #135 dungeon collapse
|
||||
// deliberately does not load; requiring terrain there hangs login
|
||||
// forever (cellReady true, SampleTerrainZ null). Retail loads the
|
||||
// cell synchronously and places the player on the cell floor —
|
||||
// cellReady is the faithful indoor equivalent (#106/#107, AD-2).
|
||||
// (Before #135 this only passed by accident: the 25×25 window
|
||||
// happened to stream the neighbour terrain.)
|
||||
if (LastSpawns.TryGetValue(_playerServerGuid, out var sp)
|
||||
&& sp.Position is { } spawnClaim
|
||||
&& spawnClaim.LandblockId != 0
|
||||
&& (spawnClaim.LandblockId & 0xFFFFu) >= 0x0100u
|
||||
&& !IsSpawnClaimUnhydratable(spawnClaim.LandblockId))
|
||||
return _physicsEngine.IsSpawnCellReady(spawnClaim.LandblockId);
|
||||
|
||||
// Outdoor spawn, OR an unhydratable indoor claim that will demote to
|
||||
// an outdoor position: hold until the terrain under the spawn streams
|
||||
// (the original #106 gate — entering against an empty world free-falls
|
||||
// the player into the void).
|
||||
return _physicsEngine.SampleTerrainZ(pe.Position.X, pe.Position.Y) is not null;
|
||||
},
|
||||
// Retail SmartBox::UseTime (0x00455410) completes the player
|
||||
// position only after CellManager's blocking load clears. The
|
||||
// shared acdream barrier joins collision, static-mesh upload,
|
||||
// and composite-texture readiness for both login and portals.
|
||||
isWorldReady: LoginWorldReady,
|
||||
enterPlayerMode: EnterPlayerModeFromAutoEntry);
|
||||
}
|
||||
|
||||
|
|
@ -2873,6 +2844,20 @@ public sealed class GameWindow : IDisposable
|
|||
retirementCoordinator: _landblockRetirements);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
_worldRevealReadiness = new AcDream.App.Streaming.WorldRevealReadinessBarrier(
|
||||
isRenderNeighborhoodReady: _streamingController.IsRenderNeighborhoodResident,
|
||||
isSpawnCellReady: _physicsEngine.IsSpawnCellReady,
|
||||
isTerrainNeighborhoodReady: _physicsEngine.IsNeighborhoodTerrainResident,
|
||||
areCompositeTexturesReady: () => _wbDrawDispatcher?.CompositeTexturesReady == true,
|
||||
prepareCompositeTextures: (destinationCell, radius) =>
|
||||
_wbDrawDispatcher?.PrepareCompositeTextures(
|
||||
_worldState.Entities,
|
||||
_worldState.FlatViewGeneration,
|
||||
destinationCell,
|
||||
radius),
|
||||
invalidateCompositeTextures: () =>
|
||||
_wbDrawDispatcher?.InvalidateCompositeWarmupReadiness(),
|
||||
isSpawnClaimUnhydratable: IsSpawnClaimUnhydratable);
|
||||
|
||||
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
|
||||
// enter the world as the first character on the account, and stream
|
||||
|
|
@ -3624,6 +3609,13 @@ public sealed class GameWindow : IDisposable
|
|||
isSealedDungeon: IsSealedDungeonCell(position.LandblockId));
|
||||
}
|
||||
|
||||
// Retail SmartBox::UseTime (0x00455410) does not complete the player
|
||||
// position while CellManager is blocking for destination cells. Begin
|
||||
// the same shared reveal lifetime used by portal arrivals: initial
|
||||
// login must not inherit the dispatcher's default/previous composite
|
||||
// readiness and expose a terrain-only or partially uploaded world.
|
||||
_worldRevealReadiness?.Begin();
|
||||
|
||||
// Streaming is normally gated until this point, so the next loaded-
|
||||
// landblock callback will materialize deferred records. Also cover an
|
||||
// already-resident block (tests, reconnect teardown overlap) without
|
||||
|
|
@ -7465,7 +7457,8 @@ public sealed class GameWindow : IDisposable
|
|||
// standing on loaded ground with wall collision and a non-empty immediate view. The far
|
||||
// ring (out to the streaming window) still drains at the per-frame budget after portal
|
||||
// space exits. Bigger = a more complete arrival but a longer portal-space hold.
|
||||
private const int TeleportNearRingRadius = 1;
|
||||
private const int TeleportNearRingRadius =
|
||||
AcDream.App.Streaming.WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius;
|
||||
|
||||
/// <summary>
|
||||
/// Bind a sequence-correlated teleport Position to presentation and
|
||||
|
|
@ -7544,7 +7537,7 @@ public sealed class GameWindow : IDisposable
|
|||
_pendingTeleportCell = p.LandblockId;
|
||||
_teleportHoldSeconds = 0f;
|
||||
_teleportForced = false;
|
||||
_wbDrawDispatcher?.InvalidateCompositeWarmupReadiness();
|
||||
_worldRevealReadiness?.Begin();
|
||||
if (_streamingController is not null)
|
||||
{
|
||||
_streamingController.PriorityLandblockId =
|
||||
|
|
@ -7615,18 +7608,24 @@ public sealed class GameWindow : IDisposable
|
|||
/// loudly rather than holding forever.
|
||||
/// </summary>
|
||||
private bool TeleportWorldReady(uint destCell)
|
||||
=> _worldRevealReadiness?.IsReady(destCell) == true;
|
||||
|
||||
private bool LoginWorldReady()
|
||||
{
|
||||
if (IsSpawnClaimUnhydratable(destCell)) return true;
|
||||
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
||||
int renderRadius = indoor ? 0 : TeleportNearRingRadius;
|
||||
bool renderReady = _streamingController?.IsRenderNeighborhoodResident(
|
||||
destCell,
|
||||
renderRadius) == true;
|
||||
renderReady &= _wbDrawDispatcher?.CompositeTexturesReady == true;
|
||||
return indoor
|
||||
? renderReady && _physicsEngine.IsSpawnCellReady(destCell)
|
||||
: renderReady
|
||||
&& _physicsEngine.IsNeighborhoodTerrainResident(destCell, TeleportNearRingRadius);
|
||||
return TryGetLoginWorldCell(out uint cell)
|
||||
&& _worldRevealReadiness?.IsReady(cell) == true;
|
||||
}
|
||||
|
||||
private bool TryGetLoginWorldCell(out uint cell)
|
||||
{
|
||||
cell = 0;
|
||||
if (_liveEntities is null
|
||||
|| !_liveEntities.TryGetSnapshot(_playerServerGuid, out var playerSpawn)
|
||||
|| playerSpawn.Position is not { } position)
|
||||
return false;
|
||||
|
||||
cell = position.LandblockId;
|
||||
return cell != 0;
|
||||
}
|
||||
|
||||
// The deferred snap (the original OnLivePositionUpdated steps 2-5), now run only
|
||||
|
|
@ -10113,24 +10112,13 @@ public sealed class GameWindow : IDisposable
|
|||
using (var _uplStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Upload))
|
||||
{
|
||||
_wbMeshAdapter?.Tick();
|
||||
if (_teleportTransit.IsActive && _pendingTeleportCell != 0)
|
||||
{
|
||||
int compositeRadius = (_pendingTeleportCell & 0xFFFFu) >= 0x0100u
|
||||
? 0
|
||||
: TeleportNearRingRadius;
|
||||
bool destinationMeshesPublished =
|
||||
_streamingController?.IsRenderNeighborhoodResident(
|
||||
_pendingTeleportCell,
|
||||
compositeRadius) == true;
|
||||
if (destinationMeshesPublished)
|
||||
{
|
||||
_wbDrawDispatcher?.PrepareCompositeTextures(
|
||||
_worldState.Entities,
|
||||
_worldState.FlatViewGeneration,
|
||||
_pendingTeleportCell,
|
||||
compositeRadius);
|
||||
}
|
||||
}
|
||||
uint revealCell = _teleportTransit.IsActive
|
||||
? _pendingTeleportCell
|
||||
: IsLiveModeWaitingForLogin && TryGetLoginWorldCell(out uint loginCell)
|
||||
? loginCell
|
||||
: 0u;
|
||||
if (revealCell != 0)
|
||||
_worldRevealReadiness?.Prepare(revealCell);
|
||||
_particleRenderer?.BeginFrame(_gpuFrameFlights.CurrentSlot);
|
||||
}
|
||||
if (_frameDiag)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue