refactor(runtime): own world reveal generation

This commit is contained in:
Erik 2026-07-26 17:09:54 +02:00
parent 4ab98b080e
commit a6860d5563
26 changed files with 1294 additions and 502 deletions

View file

@ -26,6 +26,7 @@ using AcDream.Core.Selection;
using AcDream.Core.Vfx; using AcDream.Core.Vfx;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
using DatReaderWriter; using DatReaderWriter;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
using Silk.NET.OpenGL; using Silk.NET.OpenGL;
@ -71,6 +72,7 @@ internal sealed record LivePresentationDependencies(
DeferredCanonicalWorldEntityCountSource? DevWorldEntities, DeferredCanonicalWorldEntityCountSource? DevWorldEntities,
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics, DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics, DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
Action<string> Log,
Action<string>? Toast); Action<string>? Toast);
internal sealed record LivePresentationResult( internal sealed record LivePresentationResult(
@ -80,6 +82,7 @@ internal sealed record LivePresentationResult(
EntitySpawnAdapter EntitySpawnAdapter, EntitySpawnAdapter EntitySpawnAdapter,
EntityScriptActivator EntityScriptActivator, EntityScriptActivator EntityScriptActivator,
RetailStaticAnimatingObjectScheduler StaticAnimationScheduler, RetailStaticAnimatingObjectScheduler StaticAnimationScheduler,
RuntimeWorldTransitState WorldTransit,
WorldGenerationAvailabilityState WorldAvailability, WorldGenerationAvailabilityState WorldAvailability,
GpuWorldState WorldState, GpuWorldState WorldState,
RenderSceneShadowRuntime? RenderSceneShadow, RenderSceneShadowRuntime? RenderSceneShadow,
@ -333,7 +336,9 @@ internal sealed class LivePresentationCompositionPhase
staticAnimationScheduler.Unregister, staticAnimationScheduler.Unregister,
(entity, info) => staticAnimationScheduler.Rebind(entity, info)); (entity, info) => staticAnimationScheduler.Rebind(entity, info));
var worldAvailability = new WorldGenerationAvailabilityState(); var worldTransit = new RuntimeWorldTransitState(d.Log);
var worldAvailability =
new WorldGenerationAvailabilityState(worldTransit);
var worldState = new GpuWorldState( var worldState = new GpuWorldState(
wbSpawnAdapter, wbSpawnAdapter,
d.ClassificationCache.InvalidateLandblock, d.ClassificationCache.InvalidateLandblock,
@ -583,6 +588,7 @@ internal sealed class LivePresentationCompositionPhase
entitySpawnAdapter, entitySpawnAdapter,
entityScriptActivator, entityScriptActivator,
staticAnimationScheduler, staticAnimationScheduler,
worldTransit,
worldAvailability, worldAvailability,
worldState, worldState,
renderSceneShadow, renderSceneShadow,
@ -630,6 +636,7 @@ internal sealed class LivePresentationCompositionPhase
EntitySpawnAdapter entitySpawnAdapter, EntitySpawnAdapter entitySpawnAdapter,
EntityScriptActivator entityScriptActivator, EntityScriptActivator entityScriptActivator,
RetailStaticAnimatingObjectScheduler staticAnimationScheduler, RetailStaticAnimatingObjectScheduler staticAnimationScheduler,
RuntimeWorldTransitState worldTransit,
WorldGenerationAvailabilityState worldAvailability, WorldGenerationAvailabilityState worldAvailability,
GpuWorldState worldState, GpuWorldState worldState,
RenderSceneShadowRuntime? renderSceneShadow, RenderSceneShadowRuntime? renderSceneShadow,
@ -1026,6 +1033,7 @@ internal sealed class LivePresentationCompositionPhase
entitySpawnAdapter, entitySpawnAdapter,
entityScriptActivator, entityScriptActivator,
staticAnimationScheduler, staticAnimationScheduler,
worldTransit,
worldAvailability, worldAvailability,
worldState, worldState,
renderSceneShadow, renderSceneShadow,

View file

@ -325,7 +325,6 @@ internal sealed class SessionPlayerCompositionPhase
content.Dats, content.Dats,
d.DatLock); d.DatLock);
var worldQuiescence = new WorldGenerationQuiescence( var worldQuiescence = new WorldGenerationQuiescence(
live.WorldAvailability,
d.Actions.Selection, d.Actions.Selection,
live.WorldState, live.WorldState,
guid => live.LiveEntities.TryGetProjectionKey( guid => live.LiveEntities.TryGetProjectionKey(
@ -340,6 +339,7 @@ internal sealed class SessionPlayerCompositionPhase
foundation.MeshAdapter.SetDestinationRevealUploadPriority, foundation.MeshAdapter.SetDestinationRevealUploadPriority,
foundation.TextureCache.SetDestinationRevealUploadPriority); foundation.TextureCache.SetDestinationRevealUploadPriority);
var worldReveal = new WorldRevealCoordinator( var worldReveal = new WorldRevealCoordinator(
live.WorldTransit,
streaming.IsRenderNeighborhoodResident, streaming.IsRenderNeighborhoodResident,
d.PhysicsEngine.IsSpawnCellReady, d.PhysicsEngine.IsSpawnCellReady,
d.PhysicsEngine.IsNeighborhoodTerrainResident, d.PhysicsEngine.IsNeighborhoodTerrainResident,
@ -359,7 +359,6 @@ internal sealed class SessionPlayerCompositionPhase
live.DrawDispatcher.InvalidateCompositeWarmupReadiness(); live.DrawDispatcher.InvalidateCompositeWarmupReadiness();
}, },
spawnClaimClassifier.IsUnhydratable, spawnClaimClassifier.IsUnhydratable,
d.Log,
worldQuiescence, worldQuiescence,
streaming, streaming,
revealRenderResources); revealRenderResources);
@ -880,7 +879,7 @@ internal sealed class SessionPlayerCompositionPhase
d.Actions, d.Actions,
d.PlayerController, d.PlayerController,
d.WorldEnvironment.Runtime, d.WorldEnvironment.Runtime,
worldReveal, live.WorldTransit,
d.UpdateClock, d.UpdateClock,
live.SelectionInteractions); live.SelectionInteractions);
bindings.Adopt("current game runtime adapter", gameRuntime); bindings.Adopt("current game runtime adapter", gameRuntime);

View file

@ -5,6 +5,7 @@ using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.UI.Testing; using AcDream.App.UI.Testing;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Runtime;
namespace AcDream.App.Diagnostics; namespace AcDream.App.Diagnostics;
@ -101,7 +102,7 @@ internal sealed record WorldLifecycleCheckpoint(
string Name, string Name,
DateTime TimestampUtc, DateTime TimestampUtc,
int ProcessId, int ProcessId,
WorldRevealLifecycleSnapshot Reveal, RuntimePortalSnapshot Reveal,
RenderFrameOutcome Render, RenderFrameOutcome Render,
WorldLifecycleResourceSnapshot Resources); WorldLifecycleResourceSnapshot Resources);
@ -175,7 +176,7 @@ internal sealed class WorldLifecycleAutomationController :
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; };
private readonly Func<WorldRevealLifecycleSnapshot> _getReveal; private readonly Func<RuntimePortalSnapshot> _getReveal;
private readonly Func<int> _getPortalMaterializationCount; private readonly Func<int> _getPortalMaterializationCount;
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
_captureResources; _captureResources;
@ -189,7 +190,7 @@ internal sealed class WorldLifecycleAutomationController :
private bool _disposed; private bool _disposed;
public WorldLifecycleAutomationController( public WorldLifecycleAutomationController(
Func<WorldRevealLifecycleSnapshot> getReveal, Func<RuntimePortalSnapshot> getReveal,
Func<int> getPortalMaterializationCount, Func<int> getPortalMaterializationCount,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources, Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
FrameScreenshotController screenshots, FrameScreenshotController screenshots,

View file

@ -1350,6 +1350,7 @@ public sealed class GameWindow :
settingsDevTools.DevTools?.LateBindings.WorldEntities, settingsDevTools.DevTools?.LateBindings.WorldEntities,
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics, settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
_uiFrameDiagnostics, _uiFrameDiagnostics,
Console.WriteLine,
compositionToast), compositionToast),
this).Compose( this).Compose(
platformResult, platformResult,

View file

@ -1,7 +1,6 @@
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Interaction; using AcDream.App.Interaction;
using AcDream.App.Net; using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Runtime; using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
@ -40,7 +39,7 @@ internal sealed class CurrentGameRuntimeAdapter
RuntimeActionState actions, RuntimeActionState actions,
RuntimeLocalPlayerMovementState movement, RuntimeLocalPlayerMovementState movement,
RuntimeWorldEnvironmentState environment, RuntimeWorldEnvironmentState environment,
WorldRevealCoordinator worldReveal, RuntimeWorldTransitState worldTransit,
IGameRuntimeClock clock, IGameRuntimeClock clock,
SelectionInteractionController selection) SelectionInteractionController selection)
{ {
@ -54,7 +53,7 @@ internal sealed class CurrentGameRuntimeAdapter
actions, actions,
movement, movement,
environment, environment,
worldReveal, worldTransit,
clock); clock);
entityObjects.BindEventContext( entityObjects.BindEventContext(
() => _view.Generation, () => _view.Generation,

View file

@ -1,6 +1,5 @@
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Net; using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.Core.Chat; using AcDream.Core.Chat;
using AcDream.Core.Items; using AcDream.Core.Items;
using AcDream.Runtime; using AcDream.Runtime;
@ -29,7 +28,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
private readonly IRuntimeActionView _actionView; private readonly IRuntimeActionView _actionView;
private readonly IRuntimeMovementView _movementView; private readonly IRuntimeMovementView _movementView;
private readonly IRuntimeWorldEnvironmentView _environmentView; private readonly IRuntimeWorldEnvironmentView _environmentView;
private readonly PortalView _portalView; private readonly IRuntimePortalView _portalView;
private bool _active = true; private bool _active = true;
public CurrentGameRuntimeViewAdapter( public CurrentGameRuntimeViewAdapter(
@ -42,7 +41,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
RuntimeActionState actions, RuntimeActionState actions,
RuntimeLocalPlayerMovementState movement, RuntimeLocalPlayerMovementState movement,
RuntimeWorldEnvironmentState environment, RuntimeWorldEnvironmentState environment,
WorldRevealCoordinator worldReveal, RuntimeWorldTransitState worldTransit,
IGameRuntimeClock clock) IGameRuntimeClock clock)
{ {
_session = session ?? throw new ArgumentNullException(nameof(session)); _session = session ?? throw new ArgumentNullException(nameof(session));
@ -66,8 +65,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
movement ?? throw new ArgumentNullException(nameof(movement))).View; movement ?? throw new ArgumentNullException(nameof(movement))).View;
_environmentView = environment _environmentView = environment
?? throw new ArgumentNullException(nameof(environment)); ?? throw new ArgumentNullException(nameof(environment));
_portalView = new PortalView( _portalView = worldTransit
worldReveal ?? throw new ArgumentNullException(nameof(worldReveal))); ?? throw new ArgumentNullException(nameof(worldTransit));
} }
internal bool IsActive => _active && !_session.IsDisposalComplete; internal bool IsActive => _active && !_session.IsDisposalComplete;
@ -130,33 +129,4 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
_portalView.Snapshot); _portalView.Snapshot);
internal void Deactivate() => _active = false; internal void Deactivate() => _active = false;
private sealed class PortalView(WorldRevealCoordinator owner)
: IRuntimePortalView
{
public RuntimePortalSnapshot Snapshot
{
get
{
WorldRevealLifecycleSnapshot snapshot = owner.Snapshot;
RuntimePortalKind kind = snapshot.Generation == 0
? RuntimePortalKind.None
: snapshot.Kind switch
{
WorldRevealKind.Login => RuntimePortalKind.Login,
WorldRevealKind.Portal => RuntimePortalKind.Portal,
_ => RuntimePortalKind.None,
};
return new RuntimePortalSnapshot(
snapshot.Generation,
kind,
snapshot.Readiness.DestinationCell,
snapshot.IsReady,
snapshot.Materialized,
snapshot.Completed,
snapshot.Cancelled,
snapshot.WorldViewportObserved);
}
}
}
} }

View file

@ -10,6 +10,7 @@ using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
namespace AcDream.App.Streaming; namespace AcDream.App.Streaming;
@ -668,7 +669,7 @@ internal sealed class LocalPlayerTeleportController
// Retail SmartBox enters blocking_for_cells before old-world object, // Retail SmartBox enters blocking_for_cells before old-world object,
// physics, landscape, and ambient owners may advance again. Begin the // physics, landscape, and ambient owners may advance again. Begin the
// reveal generation before a recenter can start detaching that world. // reveal generation before a recenter can start detaching that world.
_worldReveal.Begin(WorldRevealKind.Portal, position.LandblockId); _worldReveal.Begin(RuntimePortalKind.Portal, position.LandblockId);
if (!IsCurrentLifetime(generation, sequence)) if (!IsCurrentLifetime(generation, sequence))
return false; return false;
@ -737,7 +738,10 @@ internal sealed class LocalPlayerTeleportController
if (_lifetimeGeneration != generation) if (_lifetimeGeneration != generation)
return generation; return generation;
_worldReveal.Cancel(); if (clearSession)
_worldReveal.ResetSession();
else
_worldReveal.Cancel();
if (_lifetimeGeneration != generation) if (_lifetimeGeneration != generation)
return generation; return generation;

View file

@ -1,6 +1,7 @@
using AcDream.App.Audio; using AcDream.App.Audio;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming; namespace AcDream.App.Streaming;
@ -24,57 +25,51 @@ internal sealed class AlwaysAvailableWorldGeneration
} }
/// <summary> /// <summary>
/// Single-writer state behind <see cref="IWorldGenerationAvailability"/>. /// Read-only graphical-host projection of the canonical Runtime transit
/// Superseding reveal generations stay continuously quiesced; only the exact /// lifetime. It stores no availability or generation state.
/// active generation may reopen the world.
/// </summary> /// </summary>
internal sealed class WorldGenerationAvailabilityState internal sealed class WorldGenerationAvailabilityState
: IWorldGenerationAvailability : IWorldGenerationAvailability
{ {
public bool IsWorldAvailable => QuiescedGeneration == 0; private readonly RuntimeWorldTransitState _transit;
public long QuiescedGeneration { get; private set; }
public void Begin(long generation) public WorldGenerationAvailabilityState(
RuntimeWorldTransitState transit)
{ {
if (generation <= 0) _transit = transit ?? throw new ArgumentNullException(nameof(transit));
throw new ArgumentOutOfRangeException(nameof(generation));
QuiescedGeneration = generation;
} }
public bool End(long generation) public bool IsWorldAvailable =>
{ _transit.IsWorldSimulationAvailable;
if (generation <= 0 || QuiescedGeneration != generation)
return false;
QuiescedGeneration = 0; public long QuiescedGeneration =>
return true; IsWorldAvailable ? 0 : _transit.Snapshot.Generation;
}
} }
internal readonly record struct WorldGenerationQuiescenceEdge(
bool ShouldApply,
bool ClearWorldSelection);
/// <summary> /// <summary>
/// Owns the immediate observable edge of retail /// Projects the immediate graphical/audio edge of retail
/// <c>SmartBox::UseTime @ 0x00455410</c>'s <c>blocking_for_cells</c> /// <c>SmartBox::UseTime @ 0x00455410</c>'s <c>blocking_for_cells</c>
/// branch. Deferred teardown may retain memory, but the old generation cannot /// branch. Runtime owns whether the world is available; this class only
/// keep rendering, targeting, ticking, or producing world audio. /// applies selection and audio side effects at the accepted Runtime edge.
/// </summary> /// </summary>
internal sealed class WorldGenerationQuiescence internal sealed class WorldGenerationQuiescence
{ {
private readonly WorldGenerationAvailabilityState _availability;
private readonly SelectionState _selection; private readonly SelectionState _selection;
private readonly GpuWorldState _world; private readonly GpuWorldState _world;
private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey; private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey;
private readonly IWorldAudioQuiescence? _audio; private readonly IWorldAudioQuiescence? _audio;
private bool _effectsQuiesced;
public WorldGenerationQuiescence( public WorldGenerationQuiescence(
WorldGenerationAvailabilityState availability,
SelectionState selection, SelectionState selection,
GpuWorldState world, GpuWorldState world,
Func<uint, RuntimeEntityKey?> resolveProjectionKey, Func<uint, RuntimeEntityKey?> resolveProjectionKey,
IWorldAudioQuiescence? audio) IWorldAudioQuiescence? audio)
{ {
_availability = availability
?? throw new ArgumentNullException(nameof(availability));
_selection = selection ?? throw new ArgumentNullException(nameof(selection)); _selection = selection ?? throw new ArgumentNullException(nameof(selection));
_world = world ?? throw new ArgumentNullException(nameof(world)); _world = world ?? throw new ArgumentNullException(nameof(world));
_resolveProjectionKey = resolveProjectionKey _resolveProjectionKey = resolveProjectionKey
@ -82,22 +77,33 @@ internal sealed class WorldGenerationQuiescence
_audio = audio; _audio = audio;
} }
public IWorldGenerationAvailability Availability => _availability; /// <summary>
/// Captures the pre-quiescence graphical selection fact before Runtime
public void Begin(long generation) /// closes world availability. The returned edge contains no authority and
/// is committed only after Runtime accepts the new generation.
/// </summary>
public WorldGenerationQuiescenceEdge CaptureBegin()
{ {
bool firstEdge = _availability.IsWorldAvailable; if (_effectsQuiesced)
uint? selected = firstEdge ? _selection.SelectedObjectId : null; return default;
uint? selected = _selection.SelectedObjectId;
bool clearWorldSelection = bool clearWorldSelection =
selected is uint selectedGuid selected is uint selectedGuid
&& _resolveProjectionKey(selectedGuid) is RuntimeEntityKey key && _resolveProjectionKey(selectedGuid) is RuntimeEntityKey key
&& _world.IsLiveEntityVisible(key); && _world.IsLiveEntityVisible(key);
return new WorldGenerationQuiescenceEdge(
ShouldApply: true,
clearWorldSelection);
}
_availability.Begin(generation); public void CommitBegin(in WorldGenerationQuiescenceEdge edge)
if (!firstEdge) {
if (!edge.ShouldApply || _effectsQuiesced)
return; return;
if (clearWorldSelection) _effectsQuiesced = true;
if (edge.ClearWorldSelection)
{ {
_selection.Clear( _selection.Clear(
SelectionChangeSource.System, SelectionChangeSource.System,
@ -107,9 +113,12 @@ internal sealed class WorldGenerationQuiescence
_audio?.SuspendWorldAudio(); _audio?.SuspendWorldAudio();
} }
public void End(long generation) public void ObserveReleased()
{ {
if (_availability.End(generation)) if (!_effectsQuiesced)
_audio?.ResumeWorldAudio(); return;
_effectsQuiesced = false;
_audio?.ResumeWorldAudio();
} }
} }

View file

@ -1,9 +1,13 @@
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming; namespace AcDream.App.Streaming;
/// <summary> /// <summary>
/// Generation-scoped destination-work seam. The reveal coordinator is the /// Generation-scoped destination-work seam. The reveal coordinator is the
/// canonical owner of login/portal lifetime; the streaming scheduler only /// graphical host for Runtime's canonical login/portal lifetime; the
/// reserves capacity for the exact generation and destination it is given. /// streaming scheduler only reserves capacity for the exact generation and
/// destination it is given.
/// </summary> /// </summary>
internal interface IWorldRevealStreamingScheduler internal interface IWorldRevealStreamingScheduler
{ {
@ -26,23 +30,23 @@ internal interface IWorldRevealRenderResourceScheduler
} }
/// <summary> /// <summary>
/// Owns one login/portal reveal lifetime across readiness and lifecycle /// Graphical-host adapter for one canonical Runtime login/portal reveal
/// diagnostics. This keeps the destination barrier and its observations on a /// lifetime. App owns render/collision readiness and destination resource
/// single seam so callers cannot begin, prepare, or evaluate one owner while /// reservations; Runtime owns generation, destination, materialization,
/// forgetting the other. /// simulation availability, completion, and cancellation.
/// </summary> /// </summary>
internal sealed class WorldRevealCoordinator internal sealed class WorldRevealCoordinator
{ {
private readonly RuntimeWorldTransitState _transit;
private readonly WorldRevealReadinessBarrier _readiness; private readonly WorldRevealReadinessBarrier _readiness;
private readonly WorldRevealLifecycleTelemetry _lifecycle;
private readonly WorldGenerationQuiescence? _quiescence; private readonly WorldGenerationQuiescence? _quiescence;
private readonly IWorldRevealStreamingScheduler? _streaming; private readonly IWorldRevealStreamingScheduler? _streaming;
private readonly IWorldRevealRenderResourceScheduler? _renderResources; private readonly IWorldRevealRenderResourceScheduler? _renderResources;
private long _activeGeneration; private long _reservationGeneration;
private bool _worldSimulationReleased;
private bool _worldViewportReleased; private bool _worldViewportReleased;
public WorldRevealCoordinator( public WorldRevealCoordinator(
RuntimeWorldTransitState transit,
Func<uint, int, bool> isRenderNeighborhoodReady, Func<uint, int, bool> isRenderNeighborhoodReady,
Func<uint, bool> isSpawnCellReady, Func<uint, bool> isSpawnCellReady,
Func<uint, int, bool> isTerrainNeighborhoodReady, Func<uint, int, bool> isTerrainNeighborhoodReady,
@ -50,11 +54,11 @@ internal sealed class WorldRevealCoordinator
Action<uint, int> prepareCompositeTextures, Action<uint, int> prepareCompositeTextures,
Action invalidateCompositeTextures, Action invalidateCompositeTextures,
Func<uint, bool> isSpawnClaimUnhydratable, Func<uint, bool> isSpawnClaimUnhydratable,
Action<string>? log = null,
WorldGenerationQuiescence? quiescence = null, WorldGenerationQuiescence? quiescence = null,
IWorldRevealStreamingScheduler? streaming = null, IWorldRevealStreamingScheduler? streaming = null,
IWorldRevealRenderResourceScheduler? renderResources = null) IWorldRevealRenderResourceScheduler? renderResources = null)
{ {
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_readiness = new WorldRevealReadinessBarrier( _readiness = new WorldRevealReadinessBarrier(
isRenderNeighborhoodReady, isRenderNeighborhoodReady,
isSpawnCellReady, isSpawnCellReady,
@ -63,27 +67,28 @@ internal sealed class WorldRevealCoordinator
prepareCompositeTextures, prepareCompositeTextures,
invalidateCompositeTextures, invalidateCompositeTextures,
isSpawnClaimUnhydratable); isSpawnClaimUnhydratable);
_lifecycle = new WorldRevealLifecycleTelemetry(log);
_quiescence = quiescence; _quiescence = quiescence;
_streaming = streaming; _streaming = streaming;
_renderResources = renderResources; _renderResources = renderResources;
} }
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot; public RuntimePortalSnapshot Snapshot => _transit.Snapshot;
public int PortalMaterializationCount => _lifecycle.PortalMaterializationCount; public int PortalMaterializationCount =>
public bool WaitCueShown => _lifecycle.WaitCueShown; _transit.Snapshot.PortalMaterializationCount;
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
public long Begin(WorldRevealKind kind, uint destinationCell) public long Begin(RuntimePortalKind kind, uint destinationCell)
{ {
if (destinationCell == 0u) if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell)); throw new ArgumentOutOfRangeException(nameof(destinationCell));
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
_readiness.Begin(); _readiness.Begin();
long generation = _lifecycle.Begin(kind); long generation = _transit.BeginReveal(kind, destinationCell);
_activeGeneration = generation; _quiescence?.CommitBegin(quiescenceEdge);
_worldSimulationReleased = false; _reservationGeneration = generation;
_worldViewportReleased = false; _worldViewportReleased = false;
_quiescence?.Begin(generation);
_streaming?.BeginDestinationReservation( _streaming?.BeginDestinationReservation(
generation, generation,
destinationCell, destinationCell,
@ -105,7 +110,20 @@ internal sealed class WorldRevealCoordinator
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell) public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
{ {
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell); WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
_lifecycle.ObserveReadiness(snapshot); RuntimePortalSnapshot portal = _transit.Snapshot;
if (portal.Generation != 0)
{
_transit.AcknowledgeDestinationReadiness(
new RuntimeDestinationReadiness(
portal.Generation,
snapshot.DestinationCell,
snapshot.IsIndoor,
snapshot.IsUnhydratable,
snapshot.RequiredRenderRadius,
snapshot.IsRenderNeighborhoodReady,
snapshot.AreCompositeTexturesReady,
snapshot.IsCollisionReady));
}
return snapshot; return snapshot;
} }
@ -122,15 +140,30 @@ internal sealed class WorldRevealCoordinator
/// </remarks> /// </remarks>
public void ObserveMaterialized() public void ObserveMaterialized()
{ {
_lifecycle.ObserveMaterialized(); RuntimePortalSnapshot snapshot = _transit.Snapshot;
ReleaseWorldSimulation(); if (snapshot.Generation == 0)
return;
bool wasAvailable = _transit.IsWorldSimulationAvailable;
_transit.AcknowledgeMaterialized(
snapshot.Generation,
snapshot.DestinationCell);
ObserveSimulationRelease(wasAvailable);
} }
public void ObserveWorldViewportVisible() => public void ObserveWorldViewportVisible()
_lifecycle.ObserveWorldViewportVisible(); {
long generation = _transit.Snapshot.Generation;
if (generation != 0)
_transit.AcknowledgeWorldViewportVisible(generation);
}
public bool ObserveWait(TimeSpan elapsed) => public bool ObserveWait(TimeSpan elapsed)
_lifecycle.ObserveWait(elapsed); {
long generation = _transit.Snapshot.Generation;
return generation != 0
&& _transit.ObserveWait(generation, elapsed);
}
/// <summary> /// <summary>
/// Releases the destination streaming/resource reservation at retail's /// Releases the destination streaming/resource reservation at retail's
@ -140,7 +173,7 @@ internal sealed class WorldRevealCoordinator
/// </summary> /// </summary>
public void RevealWorldViewport() public void RevealWorldViewport()
{ {
long generation = _activeGeneration; long generation = _reservationGeneration;
if (generation == 0 || _worldViewportReleased) if (generation == 0 || _worldViewportReleased)
return; return;
@ -151,36 +184,53 @@ internal sealed class WorldRevealCoordinator
public void Complete() public void Complete()
{ {
_lifecycle.Complete(); long generation = _transit.Snapshot.Generation;
EndLifetime(); if (generation == 0)
return;
bool wasAvailable = _transit.IsWorldSimulationAvailable;
_transit.Complete(generation);
ObserveSimulationRelease(wasAvailable);
EndHostLifetime();
} }
public void Cancel() public void Cancel()
{ {
_lifecycle.Cancel(); long generation = _transit.Snapshot.Generation;
EndLifetime(); if (generation == 0)
} return;
private void EndLifetime() bool wasAvailable = _transit.IsWorldSimulationAvailable;
{ _transit.Cancel(generation);
long generation = _activeGeneration; ObserveSimulationRelease(wasAvailable);
EndHostLifetime();
}
public void ResetSession()
{
bool wasAvailable = _transit.IsWorldSimulationAvailable;
long generation = _transit.Snapshot.Generation;
if (generation != 0)
_transit.Cancel(generation);
ObserveSimulationRelease(wasAvailable);
EndHostLifetime();
_transit.ResetSession();
}
private void EndHostLifetime()
{
long generation = _reservationGeneration;
if (generation == 0) if (generation == 0)
return; return;
ReleaseWorldSimulation();
RevealWorldViewport(); RevealWorldViewport();
_activeGeneration = 0; _reservationGeneration = 0;
_worldSimulationReleased = false;
_worldViewportReleased = false; _worldViewportReleased = false;
} }
private void ReleaseWorldSimulation() private void ObserveSimulationRelease(bool wasAvailable)
{ {
long generation = _activeGeneration; if (!wasAvailable && _transit.IsWorldSimulationAvailable)
if (generation == 0 || _worldSimulationReleased) _quiescence?.ObserveReleased();
return;
_quiescence?.End(generation);
_worldSimulationReleased = true;
} }
} }

View file

@ -1,187 +0,0 @@
using System.Globalization;
namespace AcDream.App.Streaming;
internal enum WorldRevealKind
{
Login,
Portal,
}
internal readonly record struct WorldRevealLifecycleSnapshot(
long Generation,
WorldRevealKind Kind,
WorldRevealReadinessSnapshot Readiness,
bool Materialized,
bool Completed,
bool Cancelled,
bool WorldViewportObserved,
int InvariantFailureCount)
{
public bool IsActive => Generation != 0 && !Cancelled;
public bool IsReady => Readiness.IsReady;
}
/// <summary>
/// Observes logical login/portal reveal generations without owning rendering or
/// changing gameplay state. The owner turns the canonical readiness decision
/// into deterministic lifecycle markers for connected automation and records a
/// hard diagnostic failure if normal world geometry becomes visible early.
/// </summary>
internal sealed class WorldRevealLifecycleTelemetry
{
internal static readonly TimeSpan RetailWaitCueDelay =
TimeSpan.FromSeconds(5);
private readonly Action<string> _log;
private WorldRevealLifecycleSnapshot _snapshot;
private long _nextGeneration;
private int _portalMaterializationCount;
private bool _waitCueShown;
public WorldRevealLifecycleTelemetry(Action<string>? log = null)
{
_log = log ?? (_ => { });
}
public WorldRevealLifecycleSnapshot Snapshot => _snapshot;
public int PortalMaterializationCount => _portalMaterializationCount;
public bool WaitCueShown => _waitCueShown;
public long Begin(WorldRevealKind kind)
{
if (_snapshot.Generation != 0
&& !_snapshot.Cancelled
&& !_snapshot.WorldViewportObserved)
{
Log("superseded", _snapshot);
}
long generation = checked(++_nextGeneration);
_waitCueShown = false;
_snapshot = new WorldRevealLifecycleSnapshot(
generation,
kind,
default,
Materialized: false,
Completed: false,
Cancelled: false,
WorldViewportObserved: false,
InvariantFailureCount: _snapshot.InvariantFailureCount);
Log("begin", _snapshot);
return generation;
}
public bool ObserveWait(TimeSpan elapsed)
{
if (_snapshot.Generation == 0
|| _snapshot.Cancelled
|| _snapshot.Completed
|| elapsed < RetailWaitCueDelay)
{
return false;
}
if (!_waitCueShown)
{
_waitCueShown = true;
_log(
$"[world-reveal] event=wait-cue elapsedMs={elapsed.TotalMilliseconds:F0} "
+ Describe(_snapshot));
}
return true;
}
public void ObserveReadiness(WorldRevealReadinessSnapshot readiness)
{
if (_snapshot.Generation == 0 || _snapshot.Cancelled)
return;
if (_snapshot.Readiness == readiness)
return;
if (_snapshot.Readiness.DestinationCell != 0u
&& readiness.DestinationCell != 0u
&& _snapshot.Readiness.DestinationCell != readiness.DestinationCell)
{
FailInvariant(
"destination-changed",
$"from=0x{_snapshot.Readiness.DestinationCell:X8} to=0x{readiness.DestinationCell:X8}");
}
_snapshot = _snapshot with { Readiness = readiness };
Log("readiness", _snapshot);
}
public void ObserveMaterialized()
{
if (_snapshot.Generation == 0 || _snapshot.Cancelled || _snapshot.Materialized)
return;
_snapshot = _snapshot with { Materialized = true };
if (_snapshot.Kind == WorldRevealKind.Portal)
_portalMaterializationCount++;
Log("materialized", _snapshot);
}
public void ObserveWorldViewportVisible()
{
if (_snapshot.Generation == 0 || _snapshot.Cancelled || _snapshot.WorldViewportObserved)
return;
if (!_snapshot.Readiness.IsReady)
FailInvariant("viewport-before-ready", null);
_snapshot = _snapshot with { WorldViewportObserved = true };
Log("world-visible", _snapshot);
}
public void Complete()
{
if (_snapshot.Generation == 0 || _snapshot.Cancelled || _snapshot.Completed)
return;
_snapshot = _snapshot with { Completed = true };
Log("complete", _snapshot);
}
public void Cancel()
{
if (_snapshot.Generation == 0
|| _snapshot.Cancelled
|| _snapshot.Completed)
return;
_snapshot = _snapshot with { Cancelled = true };
Log("cancel", _snapshot);
}
private void FailInvariant(string reason, string? detail)
{
_snapshot = _snapshot with
{
InvariantFailureCount = checked(_snapshot.InvariantFailureCount + 1),
};
string suffix = string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
_log($"[world-reveal] event=invariant-failure reason={reason}{suffix} {Describe(_snapshot)}");
}
private void Log(string eventName, WorldRevealLifecycleSnapshot snapshot) =>
_log($"[world-reveal] event={eventName} {Describe(snapshot)}");
private static string Describe(WorldRevealLifecycleSnapshot snapshot)
{
WorldRevealReadinessSnapshot ready = snapshot.Readiness;
return string.Create(
CultureInfo.InvariantCulture,
$"generation={snapshot.Generation} kind={snapshot.Kind} " +
$"cell=0x{ready.DestinationCell:X8} indoor={ready.IsIndoor} " +
$"radius={ready.RequiredRenderRadius} unhydratable={ready.IsUnhydratable} " +
$"render={ready.IsRenderNeighborhoodReady} composites={ready.AreCompositeTexturesReady} " +
$"collision={ready.IsCollisionReady} ready={ready.IsReady} " +
$"materialized={snapshot.Materialized} completed={snapshot.Completed} " +
$"cancelled={snapshot.Cancelled} visible={snapshot.WorldViewportObserved} " +
$"failures={snapshot.InvariantFailureCount}");
}
}

View file

@ -5,6 +5,7 @@ using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
namespace AcDream.App.World; namespace AcDream.App.World;
@ -237,7 +238,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
lbY, lbY,
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon( isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
position.LandblockId)); position.LandblockId));
_worldReveal.Begin(WorldRevealKind.Login, position.LandblockId); _worldReveal.Begin(RuntimePortalKind.Login, position.LandblockId);
return new( return new(
true, true,

View file

@ -92,15 +92,59 @@ public enum RuntimePortalKind
Portal, Portal,
} }
public readonly record struct RuntimeDestinationReadiness(
long Generation,
uint DestinationCell,
bool IsIndoor,
bool IsUnhydratable,
int RequiredRenderRadius,
bool IsRenderNeighborhoodReady,
bool AreCompositeTexturesReady,
bool IsCollisionReady)
{
public bool HasDestination => DestinationCell != 0u;
public bool IsReady => HasDestination
&& (IsUnhydratable
|| (IsRenderNeighborhoodReady
&& AreCompositeTexturesReady
&& IsCollisionReady));
}
public readonly record struct RuntimePortalSnapshot( public readonly record struct RuntimePortalSnapshot(
long Generation, long Generation,
RuntimePortalKind Kind, RuntimePortalKind Kind,
uint DestinationCell, RuntimeDestinationReadiness Readiness,
bool IsReady, bool Materialized,
bool IsMaterialized, bool Completed,
bool IsCompleted, bool Cancelled,
bool IsCancelled, bool WorldViewportObserved,
bool IsWorldVisible); bool WorldSimulationAvailable,
int InvariantFailureCount,
bool WaitCueShown,
int PortalMaterializationCount)
{
public static RuntimePortalSnapshot Idle { get; } = new(
Generation: 0,
RuntimePortalKind.None,
Readiness: default,
Materialized: false,
Completed: false,
Cancelled: false,
WorldViewportObserved: false,
WorldSimulationAvailable: true,
InvariantFailureCount: 0,
WaitCueShown: false,
PortalMaterializationCount: 0);
public uint DestinationCell => Readiness.DestinationCell;
public bool IsReady => Readiness.IsReady;
public bool IsMaterialized => Materialized;
public bool IsCompleted => Completed;
public bool IsCancelled => Cancelled;
public bool IsWorldVisible => WorldViewportObserved;
public bool IsActive => Generation != 0 && !Cancelled;
}
public interface IRuntimePortalView public interface IRuntimePortalView
{ {

View file

@ -0,0 +1,385 @@
using System.Globalization;
namespace AcDream.Runtime.World;
/// <summary>
/// Canonical presentation-independent owner of one login/portal reveal
/// generation. The graphical or headless host supplies typed destination
/// readiness acknowledgements; Runtime owns which generation and destination
/// those acknowledgements are allowed to advance.
/// </summary>
/// <remarks>
/// Retail ordering follows <c>SmartBox::TeleportPlayer @ 0x00453910</c>,
/// <c>SmartBox::UseTime @ 0x00455410</c>, and
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c>. Destination
/// materialization resumes simulation before the later normal-world viewport
/// edge.
/// </remarks>
public sealed class RuntimeWorldTransitState
: IRuntimePortalView
{
public static readonly TimeSpan RetailWaitCueDelay =
TimeSpan.FromSeconds(5);
private readonly Action<string> _log;
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
private long _nextGeneration;
public RuntimeWorldTransitState(Action<string>? log = null)
{
_log = log ?? (_ => { });
}
public RuntimePortalSnapshot Snapshot => _snapshot;
public bool IsWorldSimulationAvailable =>
_snapshot.WorldSimulationAvailable;
public int DiagnosticFailureCount { get; private set; }
public Exception? LastDiagnosticFailure { get; private set; }
public long BeginReveal(
RuntimePortalKind kind,
uint destinationCell)
{
if (kind is RuntimePortalKind.None)
throw new ArgumentOutOfRangeException(nameof(kind));
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
if (_snapshot.Generation != 0
&& !_snapshot.Cancelled
&& !_snapshot.WorldViewportObserved)
{
Log("superseded", _snapshot);
}
long generation = checked(++_nextGeneration);
int failures = _snapshot.InvariantFailureCount;
int materializations = _snapshot.PortalMaterializationCount;
_snapshot = new RuntimePortalSnapshot(
generation,
kind,
new RuntimeDestinationReadiness(
generation,
destinationCell,
IsIndoor: IsIndoor(destinationCell),
IsUnhydratable: false,
RequiredRenderRadius: 0,
IsRenderNeighborhoodReady: false,
AreCompositeTexturesReady: false,
IsCollisionReady: false),
Materialized: false,
Completed: false,
Cancelled: false,
WorldViewportObserved: false,
WorldSimulationAvailable: false,
InvariantFailureCount: failures,
WaitCueShown: false,
PortalMaterializationCount: materializations);
Log("begin", _snapshot);
return generation;
}
public bool AcknowledgeDestinationReadiness(
in RuntimeDestinationReadiness acknowledgement)
{
if (!ValidateActive(
acknowledgement.Generation,
acknowledgement.DestinationCell,
"readiness"))
{
return false;
}
if (_snapshot.Readiness.IsReady)
{
if (_snapshot.Readiness == acknowledgement)
return false;
FailInvariant(
"readiness-changed-after-ready",
$"accepted={DescribeReadiness(_snapshot.Readiness)} "
+ $"actual={DescribeReadiness(acknowledgement)}");
return false;
}
bool isIndoor = IsIndoor(acknowledgement.DestinationCell);
int requiredRenderRadius = isIndoor ? 0 : 1;
if (acknowledgement.IsIndoor != isIndoor
|| acknowledgement.RequiredRenderRadius
!= requiredRenderRadius)
{
FailInvariant(
"invalid-readiness-shape",
$"indoor={acknowledgement.IsIndoor} "
+ $"expectedIndoor={isIndoor} "
+ $"radius={acknowledgement.RequiredRenderRadius} "
+ $"expectedRadius={requiredRenderRadius}");
return false;
}
if (_snapshot.Readiness == acknowledgement)
return false;
_snapshot = _snapshot with { Readiness = acknowledgement };
Log("readiness", _snapshot);
return true;
}
public bool AcknowledgeMaterialized(
long generation,
uint destinationCell)
{
if (!ValidateActive(generation, destinationCell, "materialized"))
return false;
if (_snapshot.Materialized)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("materialized-before-ready", null);
return false;
}
int count = _snapshot.PortalMaterializationCount;
if (_snapshot.Kind == RuntimePortalKind.Portal)
count = checked(count + 1);
_snapshot = _snapshot with
{
Materialized = true,
WorldSimulationAvailable = true,
PortalMaterializationCount = count,
};
Log("materialized", _snapshot);
return true;
}
public bool AcknowledgeWorldViewportVisible(long generation)
{
if (!ValidateGeneration(
generation,
"world-visible",
allowCompleted: true))
{
return false;
}
if (_snapshot.WorldViewportObserved)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("viewport-before-ready", null);
return false;
}
_snapshot = _snapshot with { WorldViewportObserved = true };
Log("world-visible", _snapshot);
return true;
}
public bool ObserveWait(long generation, TimeSpan elapsed)
{
if (generation == 0
|| generation != _snapshot.Generation
|| _snapshot.Cancelled
|| _snapshot.Completed
|| elapsed < RetailWaitCueDelay)
{
return false;
}
if (!_snapshot.WaitCueShown)
{
_snapshot = _snapshot with { WaitCueShown = true };
SafeLog(
$"[world-reveal] event=wait-cue "
+ $"elapsedMs={elapsed.TotalMilliseconds:F0} "
+ Describe(_snapshot));
}
return true;
}
public bool Complete(long generation)
{
if (!ValidateGeneration(
generation,
"complete",
allowCompleted: true))
{
return false;
}
if (_snapshot.Completed)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("complete-before-ready", null);
return false;
}
if (_snapshot.Kind == RuntimePortalKind.Portal
&& !_snapshot.Materialized)
{
FailInvariant("portal-complete-before-materialized", null);
return false;
}
_snapshot = _snapshot with
{
Completed = true,
WorldSimulationAvailable = true,
};
Log("complete", _snapshot);
return true;
}
public bool Cancel(long generation)
{
if (generation == 0
|| generation != _snapshot.Generation
|| _snapshot.Cancelled
|| _snapshot.Completed)
{
return false;
}
_snapshot = _snapshot with
{
Cancelled = true,
WorldSimulationAvailable = true,
};
Log("cancel", _snapshot);
return true;
}
/// <summary>
/// Clears session-visible state while retaining the monotonically
/// increasing generation counter so a delayed prior-session host
/// acknowledgement cannot match a future generation.
/// </summary>
public void ResetSession()
{
_snapshot = RuntimePortalSnapshot.Idle;
}
private bool ValidateActive(
long generation,
uint destinationCell,
string eventName)
{
if (!ValidateGeneration(
generation,
eventName,
allowCompleted: false))
{
return false;
}
if (destinationCell == 0u
|| destinationCell != _snapshot.DestinationCell)
{
FailInvariant(
$"{eventName}-destination-mismatch",
$"expected=0x{_snapshot.DestinationCell:X8} "
+ $"actual=0x{destinationCell:X8}");
return false;
}
return true;
}
private bool ValidateGeneration(
long generation,
string eventName,
bool allowCompleted)
{
if (generation == 0 || generation != _snapshot.Generation)
{
LogRejected(
$"{eventName}-generation-mismatch",
$"expected={_snapshot.Generation} actual={generation}");
return false;
}
if (_snapshot.Cancelled
|| (!allowCompleted && _snapshot.Completed))
{
LogRejected(
$"{eventName}-after-terminal",
$"completed={_snapshot.Completed} "
+ $"cancelled={_snapshot.Cancelled}");
return false;
}
return true;
}
private void FailInvariant(string reason, string? detail)
{
_snapshot = _snapshot with
{
InvariantFailureCount =
checked(_snapshot.InvariantFailureCount + 1),
};
string suffix =
string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
SafeLog(
$"[world-reveal] event=invariant-failure "
+ $"reason={reason}{suffix} {Describe(_snapshot)}");
}
private void LogRejected(string reason, string? detail)
{
string suffix =
string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
SafeLog(
$"[world-reveal] event=rejected "
+ $"reason={reason}{suffix} {Describe(_snapshot)}");
}
private void Log(string eventName, RuntimePortalSnapshot snapshot) =>
SafeLog($"[world-reveal] event={eventName} {Describe(snapshot)}");
private void SafeLog(string message)
{
try
{
_log(message);
}
catch (Exception error)
{
DiagnosticFailureCount =
checked(DiagnosticFailureCount + 1);
LastDiagnosticFailure = error;
}
}
private static string Describe(RuntimePortalSnapshot snapshot)
{
RuntimeDestinationReadiness ready = snapshot.Readiness;
return string.Create(
CultureInfo.InvariantCulture,
$"generation={snapshot.Generation} kind={snapshot.Kind} "
+ $"cell=0x{ready.DestinationCell:X8} "
+ $"indoor={ready.IsIndoor} "
+ $"radius={ready.RequiredRenderRadius} "
+ $"unhydratable={ready.IsUnhydratable} "
+ $"render={ready.IsRenderNeighborhoodReady} "
+ $"composites={ready.AreCompositeTexturesReady} "
+ $"collision={ready.IsCollisionReady} ready={ready.IsReady} "
+ $"materialized={snapshot.Materialized} "
+ $"completed={snapshot.Completed} "
+ $"cancelled={snapshot.Cancelled} "
+ $"visible={snapshot.WorldViewportObserved} "
+ $"simulation={snapshot.WorldSimulationAvailable} "
+ $"failures={snapshot.InvariantFailureCount}");
}
private static string DescribeReadiness(
in RuntimeDestinationReadiness readiness) =>
string.Create(
CultureInfo.InvariantCulture,
$"indoor={readiness.IsIndoor},"
+ $"unhydratable={readiness.IsUnhydratable},"
+ $"radius={readiness.RequiredRenderRadius},"
+ $"render={readiness.IsRenderNeighborhoodReady},"
+ $"composites={readiness.AreCompositeTexturesReady},"
+ $"collision={readiness.IsCollisionReady}");
private static bool IsIndoor(uint cellId) =>
(cellId & 0xFFFFu) >= 0x0100u;
}

View file

@ -5,6 +5,7 @@ using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.UI.Testing; using AcDream.App.UI.Testing;
using AcDream.Runtime;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
namespace AcDream.App.Tests.Diagnostics; namespace AcDream.App.Tests.Diagnostics;
@ -95,10 +96,11 @@ public sealed class WorldLifecycleAutomationControllerTests
public void Checkpoint_WritesCanonicalRevealAndResourceSnapshot() public void Checkpoint_WritesCanonicalRevealAndResourceSnapshot()
{ {
string directory = NewDirectory(); string directory = NewDirectory();
var reveal = new WorldRevealLifecycleSnapshot( var reveal = new RuntimePortalSnapshot(
Generation: 7, Generation: 7,
Kind: WorldRevealKind.Portal, Kind: RuntimePortalKind.Portal,
Readiness: new WorldRevealReadinessSnapshot( Readiness: new RuntimeDestinationReadiness(
Generation: 7,
0x8A020164u, 0x8A020164u,
IsIndoor: true, IsIndoor: true,
IsUnhydratable: false, IsUnhydratable: false,
@ -110,7 +112,10 @@ public sealed class WorldLifecycleAutomationControllerTests
Completed: true, Completed: true,
Cancelled: false, Cancelled: false,
WorldViewportObserved: true, WorldViewportObserved: true,
InvariantFailureCount: 0); WorldSimulationAvailable: true,
InvariantFailureCount: 0,
WaitCueShown: false,
PortalMaterializationCount: 3);
var resources = EmptyResources() with var resources = EmptyResources() with
{ {
LoadedLandblocks = 1, LoadedLandblocks = 1,

View file

@ -6,6 +6,8 @@ using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.Rendering; namespace AcDream.App.Tests.Rendering;
@ -25,8 +27,9 @@ public sealed class WorldSceneRendererTests
[Fact] [Fact]
public void QuiescedGeneration_PublishesEmptySelectionFrameAndSkipsEveryWorldOwner() public void QuiescedGeneration_PublishesEmptySelectionFrameAndSkipsEveryWorldOwner()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
availability.Begin(4); var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
var rig = new Rig( var rig = new Rig(
portalVisible: false, portalVisible: false,
waitingForLogin: false, waitingForLogin: false,

View file

@ -65,7 +65,7 @@ public sealed class CurrentGameRuntimeAdapterTests
}; };
harness.Objects.AddOrUpdate(item); harness.Objects.AddOrUpdate(item);
harness.Chat.OnSystemMessage("runtime parity", 0x1Au); harness.Chat.OnSystemMessage("runtime parity", 0x1Au);
harness.WorldReveal.Begin(WorldRevealKind.Portal, 0x12340001u); harness.WorldReveal.Begin(RuntimePortalKind.Portal, 0x12340001u);
WorldRevealReadinessSnapshot readiness = WorldRevealReadinessSnapshot readiness =
harness.WorldReveal.PrepareAndEvaluate(0x12340001u); harness.WorldReveal.PrepareAndEvaluate(0x12340001u);
Assert.True(readiness.IsReady); Assert.True(readiness.IsReady);
@ -792,7 +792,9 @@ public sealed class CurrentGameRuntimeAdapterTests
mouseLook: null, mouseLook: null,
new NoopCombatInput()); new NoopCombatInput());
Clock = new UpdateFrameClock(); Clock = new UpdateFrameClock();
WorldTransit = new RuntimeWorldTransitState();
WorldReveal = new WorldRevealCoordinator( WorldReveal = new WorldRevealCoordinator(
WorldTransit,
static (_, _) => true, static (_, _) => true,
static _ => true, static _ => true,
static (_, _) => true, static (_, _) => true,
@ -836,7 +838,7 @@ public sealed class CurrentGameRuntimeAdapterTests
Actions, Actions,
MovementState, MovementState,
Environment, Environment,
WorldReveal, WorldTransit,
Clock, Clock,
selectionController); selectionController);
} }
@ -861,6 +863,7 @@ public sealed class CurrentGameRuntimeAdapterTests
public GameplayInputFrameController GameplayInput { get; } public GameplayInputFrameController GameplayInput { get; }
public RecordingCombatModeOperations CombatMode { get; } public RecordingCombatModeOperations CombatMode { get; }
public UpdateFrameClock Clock { get; } public UpdateFrameClock Clock { get; }
public RuntimeWorldTransitState WorldTransit { get; }
public WorldRevealCoordinator WorldReveal { get; } public WorldRevealCoordinator WorldReveal { get; }
public RecordingCommandRouting Commands { get; } public RecordingCommandRouting Commands { get; }
public TestTransport Transport { get; } public TestTransport Transport { get; }

View file

@ -0,0 +1,91 @@
using System.Text.RegularExpressions;
using AcDream.App.Runtime;
using AcDream.App.Streaming;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.Runtime;
public sealed class RuntimeWorldTransitOwnershipTests
{
[Fact]
public void ProductionConstructsOneRuntimeTransitOwnerAndNoAppLifecycleOwner()
{
string root = FindRepositoryRoot();
string appRoot = Path.Combine(root, "src", "AcDream.App");
string app = string.Join(
"\n",
Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories)
.Select(File.ReadAllText));
Assert.False(File.Exists(Path.Combine(
appRoot,
"Streaming",
"WorldRevealLifecycleTelemetry.cs")));
Assert.DoesNotContain(
"class WorldRevealLifecycleTelemetry",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"record struct WorldRevealLifecycleSnapshot",
app,
StringComparison.Ordinal);
Assert.Single(Regex.Matches(
app,
@"new\s+RuntimeWorldTransitState\s*\(")
.Cast<Match>());
}
[Fact]
public void GraphicalAdaptersBorrowRuntimeStateWithoutReconstructingPortalView()
{
var availabilityFields = typeof(WorldGenerationAvailabilityState)
.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Single(availabilityFields);
Assert.Equal(
typeof(RuntimeWorldTransitState),
availabilityFields[0].FieldType);
var viewFields = typeof(CurrentGameRuntimeViewAdapter)
.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Contains(
viewFields,
field => field.FieldType == typeof(IRuntimePortalView));
Assert.DoesNotContain(
typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes(
System.Reflection.BindingFlags.NonPublic),
type => type.Name.Contains(
"PortalView",
StringComparison.Ordinal));
var coordinatorFields = typeof(WorldRevealCoordinator)
.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Contains(
coordinatorFields,
field => field.FieldType == typeof(RuntimeWorldTransitState));
Assert.DoesNotContain(
coordinatorFields,
field => field.Name is "_activeGeneration"
or "_worldSimulationReleased"
or "_lifecycle");
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
return current.FullName;
current = current.Parent;
}
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
}
}

View file

@ -5,7 +5,9 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Streaming; namespace AcDream.App.Tests.Streaming;
@ -617,8 +619,10 @@ public sealed class GpuWorldStateVisibilityTests
const uint landblock = 0x0101FFFFu; const uint landblock = 0x0101FFFFu;
const uint cell = 0x01010001u; const uint cell = 0x01010001u;
const uint guid = 0x70000021u; const uint guid = 0x70000021u;
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
availability.Begin(7); var availability = new WorldGenerationAvailabilityState(transit);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, cell);
var state = new GpuWorldState(availability: availability); var state = new GpuWorldState(availability: availability);
state.AddLandblock(new LoadedLandblock( state.AddLandblock(new LoadedLandblock(
landblock, landblock,
@ -644,7 +648,18 @@ public sealed class GpuWorldStateVisibilityTests
Assert.True(record.IsSpatiallyVisible); Assert.True(record.IsSpatiallyVisible);
Assert.Equal([true], edges); Assert.Equal([true], edges);
Assert.True(availability.End(7)); Assert.True(transit.AcknowledgeDestinationReadiness(
new RuntimeDestinationReadiness(
generation,
cell,
IsIndoor: false,
IsUnhydratable: false,
RequiredRenderRadius: 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true)));
Assert.True(transit.AcknowledgeMaterialized(generation, cell));
Assert.True(transit.Complete(generation));
Assert.True(state.IsLiveEntityVisible(projectionKey)); Assert.True(state.IsLiveEntityVisible(projectionKey));
Assert.True(record.IsSpatiallyVisible); Assert.True(record.IsSpatiallyVisible);
} }
@ -688,8 +703,9 @@ public sealed class GpuWorldStateVisibilityTests
[Fact] [Fact]
public void ReadinessNeighborhood_CopiesStaticAndLiveWhileWorldIsQuiesced() public void ReadinessNeighborhood_CopiesStaticAndLiveWhileWorldIsQuiesced()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
availability.Begin(7); var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x10100001u);
var centerStatic = Entity(1, 0u); var centerStatic = Entity(1, 0u);
var centerLive = Entity(2, 0x70000001u); var centerLive = Entity(2, 0x70000001u);
var neighborStatic = Entity(3, 0u); var neighborStatic = Entity(3, 0u);

View file

@ -8,6 +8,8 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.Streaming; namespace AcDream.App.Tests.Streaming;
@ -57,7 +59,7 @@ public sealed class LocalPlayerTeleportControllerTests
(1L, 0x20210001u, WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius), (1L, 0x20210001u, WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius),
Assert.Single(harness.Streaming.Reservations)); Assert.Single(harness.Streaming.Reservations));
Assert.True(harness.Reveal.Snapshot.IsActive); Assert.True(harness.Reveal.Snapshot.IsActive);
Assert.Equal(WorldRevealKind.Portal, harness.Reveal.Snapshot.Kind); Assert.Equal(RuntimePortalKind.Portal, harness.Reveal.Snapshot.Kind);
harness.Controller.OfferDestination( harness.Controller.OfferDestination(
destination, destination,
@ -310,7 +312,7 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.ResetSession(); harness.Controller.ResetSession();
Assert.False(harness.Controller.IsActive); Assert.False(harness.Controller.IsActive);
Assert.True(harness.Reveal.Snapshot.Cancelled); Assert.Equal(RuntimePortalSnapshot.Idle, harness.Reveal.Snapshot);
Assert.True(harness.Streaming.LastResetWasSessionEnding); Assert.True(harness.Streaming.LastResetWasSessionEnding);
var failed = new Harness(); var failed = new Harness();
@ -665,6 +667,7 @@ public sealed class LocalPlayerTeleportControllerTests
Session = new FakeSession(order); Session = new FakeSession(order);
Presentation = new FakePresentation(order); Presentation = new FakePresentation(order);
Reveal = new WorldRevealCoordinator( Reveal = new WorldRevealCoordinator(
new RuntimeWorldTransitState(order.Add),
isRenderNeighborhoodReady: (_, _) => worldReady, isRenderNeighborhoodReady: (_, _) => worldReady,
isSpawnCellReady: _ => worldReady, isSpawnCellReady: _ => worldReady,
isTerrainNeighborhoodReady: (_, _) => worldReady, isTerrainNeighborhoodReady: (_, _) => worldReady,
@ -672,7 +675,6 @@ public sealed class LocalPlayerTeleportControllerTests
prepareCompositeTextures: (_, _) => { }, prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { }, invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false, isSpawnClaimUnhydratable: _ => false,
log: order.Add,
streaming: Streaming); streaming: Streaming);
Controller = new LocalPlayerTeleportController( Controller = new LocalPlayerTeleportController(
Authority, Authority,

View file

@ -3,7 +3,9 @@ using AcDream.App.Audio;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Streaming; namespace AcDream.App.Tests.Streaming;
@ -11,12 +13,14 @@ namespace AcDream.App.Tests.Streaming;
public sealed class WorldGenerationQuiescenceTests public sealed class WorldGenerationQuiescenceTests
{ {
private const uint LandblockId = 0x1010FFFFu; private const uint LandblockId = 0x1010FFFFu;
private const uint DestinationCell = 0x10100001u;
private const uint ServerGuid = 0x70000001u; private const uint ServerGuid = 0x70000001u;
[Fact] [Fact]
public void ActiveGeneration_HidesWorldQueriesButRetainsCanonicalOwners() public void RuntimeGeneration_HidesWorldQueriesButRetainsCanonicalOwners()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability); var world = new GpuWorldState(availability: availability);
WorldEntity entity = Entity(); WorldEntity entity = Entity();
world.AddLandblock(new LoadedLandblock( world.AddLandblock(new LoadedLandblock(
@ -32,10 +36,12 @@ public sealed class WorldGenerationQuiescenceTests
Assert.Single(world.LandblockEntries); Assert.Single(world.LandblockEntries);
Assert.Single(world.LandblockBounds); Assert.Single(world.LandblockBounds);
availability.Begin(17); long generation =
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby); world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
Assert.False(availability.IsWorldAvailable); Assert.False(availability.IsWorldAvailable);
Assert.Equal(generation, availability.QuiescedGeneration);
Assert.False(world.IsLiveEntityVisible(key)); Assert.False(world.IsLiveEntityVisible(key));
Assert.Empty(world.LandblockEntries); Assert.Empty(world.LandblockEntries);
Assert.Empty(world.LandblockBounds); Assert.Empty(world.LandblockBounds);
@ -43,17 +49,25 @@ public sealed class WorldGenerationQuiescenceTests
Assert.Same(entity, Assert.Single(world.Entities)); Assert.Same(entity, Assert.Single(world.Entities));
Assert.True(world.IsLoaded(LandblockId)); Assert.True(world.IsLoaded(LandblockId));
Assert.False(availability.End(16)); Assert.False(transit.Complete(generation + 1));
Assert.False(availability.IsWorldAvailable); Assert.False(availability.IsWorldAvailable);
Assert.True(availability.End(17)); Assert.True(transit.AcknowledgeDestinationReadiness(
Ready(generation, DestinationCell)));
Assert.True(transit.AcknowledgeMaterialized(
generation,
DestinationCell));
Assert.True(transit.Complete(generation));
Assert.True(world.IsLiveEntityVisible(key)); Assert.True(world.IsLiveEntityVisible(key));
Assert.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single()); Assert.Same(
entity,
Assert.Single(world.LandblockEntries).Entities.Single());
} }
[Fact] [Fact]
public void SupersededReveal_StaysContinuouslySilentAndOnlyLatestGenerationReopens() public void SupersededReveal_StaysSilentAndOnlyLatestRuntimeEdgeResumesAudio()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability); var world = new GpuWorldState(availability: availability);
world.AddLandblock(new LoadedLandblock( world.AddLandblock(new LoadedLandblock(
LandblockId, LandblockId,
@ -65,23 +79,36 @@ public sealed class WorldGenerationQuiescenceTests
selection.Select(ServerGuid, SelectionChangeSource.World); selection.Select(ServerGuid, SelectionChangeSource.World);
var audio = new RecordingAudioQuiescence(); var audio = new RecordingAudioQuiescence();
var quiescence = new WorldGenerationQuiescence( var quiescence = new WorldGenerationQuiescence(
availability,
selection, selection,
world, world,
guid => guid == ServerGuid ? Key(entity) : null, guid => guid == ServerGuid ? Key(entity) : null,
audio); audio);
quiescence.Begin(1); WorldGenerationQuiescenceEdge firstEdge =
quiescence.Begin(2); quiescence.CaptureBegin();
quiescence.End(1); long first =
transit.BeginReveal(RuntimePortalKind.Login, DestinationCell);
quiescence.CommitBegin(firstEdge);
WorldGenerationQuiescenceEdge secondEdge =
quiescence.CaptureBegin();
long second =
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
quiescence.CommitBegin(secondEdge);
Assert.False(transit.Complete(first));
Assert.False(availability.IsWorldAvailable); Assert.False(availability.IsWorldAvailable);
Assert.Equal(2, availability.QuiescedGeneration); Assert.Equal(second, availability.QuiescedGeneration);
Assert.Null(selection.SelectedObjectId); Assert.Null(selection.SelectedObjectId);
Assert.Equal(1, audio.SuspendCalls); Assert.Equal(1, audio.SuspendCalls);
Assert.Equal(0, audio.ResumeCalls); Assert.Equal(0, audio.ResumeCalls);
quiescence.End(2); Assert.True(transit.AcknowledgeDestinationReadiness(
Ready(second, DestinationCell)));
Assert.True(transit.AcknowledgeMaterialized(
second,
DestinationCell));
Assert.True(transit.Complete(second));
quiescence.ObserveReleased();
Assert.True(availability.IsWorldAvailable); Assert.True(availability.IsWorldAvailable);
Assert.Equal(1, audio.ResumeCalls); Assert.Equal(1, audio.ResumeCalls);
@ -91,22 +118,37 @@ public sealed class WorldGenerationQuiescenceTests
public void Quiesce_PreservesNonWorldInventorySelection() public void Quiesce_PreservesNonWorldInventorySelection()
{ {
const uint inventoryGuid = 0x80000001u; const uint inventoryGuid = 0x80000001u;
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability); var world = new GpuWorldState(availability: availability);
var selection = new SelectionState(); var selection = new SelectionState();
selection.Select(inventoryGuid, SelectionChangeSource.Inventory); selection.Select(inventoryGuid, SelectionChangeSource.Inventory);
var quiescence = new WorldGenerationQuiescence( var quiescence = new WorldGenerationQuiescence(
availability,
selection, selection,
world, world,
_ => null, _ => null,
audio: null); audio: null);
quiescence.Begin(3); WorldGenerationQuiescenceEdge edge = quiescence.CaptureBegin();
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
quiescence.CommitBegin(edge);
Assert.Equal(inventoryGuid, selection.SelectedObjectId); Assert.Equal(inventoryGuid, selection.SelectedObjectId);
} }
private static RuntimeDestinationReadiness Ready(
long generation,
uint destinationCell) =>
new(
generation,
destinationCell,
IsIndoor: false,
IsUnhydratable: false,
RequiredRenderRadius: 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true);
private static WorldEntity Entity() => new() private static WorldEntity Entity() => new()
{ {
Id = 1, Id = 1,

View file

@ -1,6 +1,8 @@
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.Streaming; namespace AcDream.App.Tests.Streaming;
@ -17,21 +19,27 @@ public sealed class WorldRevealCoordinatorTests
public WorldRevealCoordinator Build( public WorldRevealCoordinator Build(
List<string>? logs = null, List<string>? logs = null,
IWorldRevealStreamingScheduler? streaming = null, IWorldRevealStreamingScheduler? streaming = null,
IWorldRevealRenderResourceScheduler? renderResources = null) => new( IWorldRevealRenderResourceScheduler? renderResources = null,
isRenderNeighborhoodReady: (_, _) => RenderReady, RuntimeWorldTransitState? transit = null)
isSpawnCellReady: _ => CollisionReady, {
isTerrainNeighborhoodReady: (_, _) => CollisionReady, transit ??= new RuntimeWorldTransitState(
areCompositeTexturesReady: () => CompositesReady, logs is null ? null : new Action<string>(logs.Add));
prepareCompositeTextures: (_, _) => PrepareCount++, return new WorldRevealCoordinator(
invalidateCompositeTextures: () => transit,
{ isRenderNeighborhoodReady: (_, _) => RenderReady,
InvalidateCount++; isSpawnCellReady: _ => CollisionReady,
CompositesReady = false; isTerrainNeighborhoodReady: (_, _) => CollisionReady,
}, areCompositeTexturesReady: () => CompositesReady,
isSpawnClaimUnhydratable: _ => false, prepareCompositeTextures: (_, _) => PrepareCount++,
log: logs is null ? null : new Action<string>(logs.Add), invalidateCompositeTextures: () =>
streaming: streaming, {
renderResources: renderResources); InvalidateCount++;
CompositesReady = false;
},
isSpawnClaimUnhydratable: _ => false,
streaming: streaming,
renderResources: renderResources);
}
} }
[Fact] [Fact]
@ -40,12 +48,12 @@ public sealed class WorldRevealCoordinatorTests
var state = new State { CompositesReady = true }; var state = new State { CompositesReady = true };
WorldRevealCoordinator coordinator = state.Build(); WorldRevealCoordinator coordinator = state.Build();
long generation = coordinator.Begin(WorldRevealKind.Login, 0x11340021u); long generation = coordinator.Begin(RuntimePortalKind.Login, 0x11340021u);
Assert.Equal(1, generation); Assert.Equal(1, generation);
Assert.Equal(1, state.InvalidateCount); Assert.Equal(1, state.InvalidateCount);
Assert.False(state.CompositesReady); Assert.False(state.CompositesReady);
Assert.Equal(WorldRevealKind.Login, coordinator.Snapshot.Kind); Assert.Equal(RuntimePortalKind.Login, coordinator.Snapshot.Kind);
} }
[Fact] [Fact]
@ -58,7 +66,7 @@ public sealed class WorldRevealCoordinatorTests
CollisionReady = true, CollisionReady = true,
}; };
WorldRevealCoordinator coordinator = state.Build(); WorldRevealCoordinator coordinator = state.Build();
coordinator.Begin(WorldRevealKind.Login, outdoorCell); coordinator.Begin(RuntimePortalKind.Login, outdoorCell);
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell); WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
state.CompositesReady = true; state.CompositesReady = true;
@ -67,7 +75,8 @@ public sealed class WorldRevealCoordinatorTests
Assert.False(first.IsReady); Assert.False(first.IsReady);
Assert.True(ready.IsReady); Assert.True(ready.IsReady);
Assert.Equal(2, state.PrepareCount); Assert.Equal(2, state.PrepareCount);
Assert.Equal(ready, coordinator.Snapshot.Readiness); Assert.Equal(ready.DestinationCell, coordinator.Snapshot.DestinationCell);
Assert.Equal(ready.IsReady, coordinator.Snapshot.IsReady);
} }
[Fact] [Fact]
@ -82,7 +91,7 @@ public sealed class WorldRevealCoordinatorTests
CompositesReady = true, CompositesReady = true,
}; };
WorldRevealCoordinator coordinator = state.Build(logs); WorldRevealCoordinator coordinator = state.Build(logs);
coordinator.Begin(WorldRevealKind.Portal, outdoorCell); coordinator.Begin(RuntimePortalKind.Portal, outdoorCell);
state.CompositesReady = true; state.CompositesReady = true;
Assert.True(coordinator.Evaluate(outdoorCell).IsReady); Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
@ -109,7 +118,7 @@ public sealed class WorldRevealCoordinatorTests
CompositesReady = true, CompositesReady = true,
}; };
WorldRevealCoordinator coordinator = state.Build(); WorldRevealCoordinator coordinator = state.Build();
coordinator.Begin(WorldRevealKind.Portal, 0x11340021u); coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u);
coordinator.Cancel(); coordinator.Cancel();
coordinator.PrepareAndEvaluate(0x11340021u); coordinator.PrepareAndEvaluate(0x11340021u);
@ -118,23 +127,25 @@ public sealed class WorldRevealCoordinatorTests
coordinator.Complete(); coordinator.Complete();
Assert.True(coordinator.Snapshot.Cancelled); Assert.True(coordinator.Snapshot.Cancelled);
Assert.Equal(default, coordinator.Snapshot.Readiness); Assert.Equal(0x11340021u, coordinator.Snapshot.DestinationCell);
Assert.False(coordinator.Snapshot.IsReady);
Assert.Equal(0, coordinator.PortalMaterializationCount); Assert.Equal(0, coordinator.PortalMaterializationCount);
} }
[Fact] [Fact]
public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens() public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability); var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence(); var audio = new RecordingAudioQuiescence();
var quiescence = new WorldGenerationQuiescence( var quiescence = new WorldGenerationQuiescence(
availability,
new SelectionState(), new SelectionState(),
world, world,
_ => null, _ => null,
audio); audio);
var coordinator = new WorldRevealCoordinator( var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true, isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true, isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true, isTerrainNeighborhoodReady: (_, _) => true,
@ -144,12 +155,14 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false, isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence); quiescence: quiescence);
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login, 0x11340021u)); Assert.Equal(1, coordinator.Begin(RuntimePortalKind.Login, 0x11340021u));
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal, 0x11340021u)); Assert.Equal(2, coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u));
Assert.False(availability.IsWorldAvailable); Assert.False(availability.IsWorldAvailable);
Assert.Equal(2, availability.QuiescedGeneration); Assert.Equal(2, availability.QuiescedGeneration);
Assert.Equal(1, audio.SuspendCalls); Assert.Equal(1, audio.SuspendCalls);
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
coordinator.ObserveMaterialized();
coordinator.Complete(); coordinator.Complete();
Assert.True(availability.IsWorldAvailable); Assert.True(availability.IsWorldAvailable);
@ -159,17 +172,18 @@ public sealed class WorldRevealCoordinatorTests
[Fact] [Fact]
public void Materialization_ReopensWorldBeforePortalViewportRelease() public void Materialization_ReopensWorldBeforePortalViewportRelease()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability); var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence(); var audio = new RecordingAudioQuiescence();
var quiescence = new WorldGenerationQuiescence( var quiescence = new WorldGenerationQuiescence(
availability,
new SelectionState(), new SelectionState(),
world, world,
_ => null, _ => null,
audio); audio);
var scheduler = new RecordingDestinationScheduler(); var scheduler = new RecordingDestinationScheduler();
var coordinator = new WorldRevealCoordinator( var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true, isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true, isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true, isTerrainNeighborhoodReady: (_, _) => true,
@ -180,10 +194,11 @@ public sealed class WorldRevealCoordinatorTests
quiescence: quiescence, quiescence: quiescence,
streaming: scheduler); streaming: scheduler);
long generation = coordinator.Begin( long generation = coordinator.Begin(
WorldRevealKind.Portal, RuntimePortalKind.Portal,
0x3032001Cu); 0x3032001Cu);
Assert.False(availability.IsWorldAvailable); Assert.False(availability.IsWorldAvailable);
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
coordinator.ObserveMaterialized(); coordinator.ObserveMaterialized();
@ -211,16 +226,17 @@ public sealed class WorldRevealCoordinatorTests
[Fact] [Fact]
public void TunnelContinue_TicksDestinationObjectsBehindPortalViewport() public void TunnelContinue_TicksDestinationObjectsBehindPortalViewport()
{ {
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability); var world = new GpuWorldState(availability: availability);
var quiescence = new WorldGenerationQuiescence( var quiescence = new WorldGenerationQuiescence(
availability,
new SelectionState(), new SelectionState(),
world, world,
_ => null, _ => null,
audio: null); audio: null);
var scheduler = new RecordingDestinationScheduler(); var scheduler = new RecordingDestinationScheduler();
var coordinator = new WorldRevealCoordinator( var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => true, isRenderNeighborhoodReady: (_, _) => true,
isSpawnCellReady: _ => true, isSpawnCellReady: _ => true,
isTerrainNeighborhoodReady: (_, _) => true, isTerrainNeighborhoodReady: (_, _) => true,
@ -243,11 +259,12 @@ public sealed class WorldRevealCoordinatorTests
() => calls.Add("spatial")), () => calls.Add("spatial")),
availability); availability);
coordinator.Begin(WorldRevealKind.Portal, 0x3032001Cu); coordinator.Begin(RuntimePortalKind.Portal, 0x3032001Cu);
frame.Tick(1f / 60f); frame.Tick(1f / 60f);
Assert.Equal(["network", "commands"], calls); Assert.Equal(["network", "commands"], calls);
calls.Clear(); calls.Clear();
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
coordinator.ObserveMaterialized(); coordinator.ObserveMaterialized();
frame.Tick(1f / 60f); frame.Tick(1f / 60f);
@ -256,6 +273,43 @@ public sealed class WorldRevealCoordinatorTests
Assert.False(coordinator.Snapshot.WorldViewportObserved); Assert.False(coordinator.Snapshot.WorldViewportObserved);
} }
[Fact]
public void SessionReset_ReleasesHostEdgesAndClearsRuntimeSnapshot()
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
var world = new GpuWorldState(availability: availability);
var audio = new RecordingAudioQuiescence();
var quiescence = new WorldGenerationQuiescence(
new SelectionState(),
world,
_ => null,
audio);
var scheduler = new RecordingDestinationScheduler();
var coordinator = new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => false,
isSpawnCellReady: _ => false,
isTerrainNeighborhoodReady: (_, _) => false,
areCompositeTexturesReady: () => false,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
long generation = coordinator.Begin(
RuntimePortalKind.Portal,
0x3032001Cu);
coordinator.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
Assert.True(availability.IsWorldAvailable);
Assert.Equal(1, audio.SuspendCalls);
Assert.Equal(1, audio.ResumeCalls);
Assert.Equal([generation], scheduler.Ends);
}
[Fact] [Fact]
public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion() public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion()
{ {
@ -264,10 +318,10 @@ public sealed class WorldRevealCoordinatorTests
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler); WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
long first = coordinator.Begin( long first = coordinator.Begin(
WorldRevealKind.Portal, RuntimePortalKind.Portal,
0x11340021u); 0x11340021u);
long second = coordinator.Begin( long second = coordinator.Begin(
WorldRevealKind.Portal, RuntimePortalKind.Portal,
0x20210123u); 0x20210123u);
Assert.Equal( Assert.Equal(
@ -289,10 +343,10 @@ public sealed class WorldRevealCoordinatorTests
state.Build(renderResources: resources); state.Build(renderResources: resources);
long first = coordinator.Begin( long first = coordinator.Begin(
WorldRevealKind.Portal, RuntimePortalKind.Portal,
0x11340021u); 0x11340021u);
long second = coordinator.Begin( long second = coordinator.Begin(
WorldRevealKind.Portal, RuntimePortalKind.Portal,
0x20210123u); 0x20210123u);
Assert.Equal([first, second], resources.Begins); Assert.Equal([first, second], resources.Begins);

View file

@ -1,105 +0,0 @@
using AcDream.App.Streaming;
namespace AcDream.App.Tests.Streaming;
public sealed class WorldRevealLifecycleTelemetryTests
{
private static WorldRevealReadinessSnapshot Ready(uint cell = 0x11340021u) => new(
cell,
IsIndoor: false,
IsUnhydratable: false,
RequiredRenderRadius: 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true);
[Fact]
public void Login_RecordsReadyThenFirstVisibleFrameExactlyOnce()
{
var logs = new List<string>();
var telemetry = new WorldRevealLifecycleTelemetry(logs.Add);
long generation = telemetry.Begin(WorldRevealKind.Login);
telemetry.ObserveReadiness(Ready());
telemetry.ObserveWorldViewportVisible();
telemetry.ObserveWorldViewportVisible();
telemetry.Complete();
Assert.Equal(generation, telemetry.Snapshot.Generation);
Assert.True(telemetry.Snapshot.IsReady);
Assert.True(telemetry.Snapshot.WorldViewportObserved);
Assert.True(telemetry.Snapshot.Completed);
Assert.Equal(0, telemetry.Snapshot.InvariantFailureCount);
Assert.Single(logs, line => line.Contains("event=world-visible", StringComparison.Ordinal));
}
[Fact]
public void EarlyWorldViewport_IsARecordedHardInvariantFailure()
{
var logs = new List<string>();
var telemetry = new WorldRevealLifecycleTelemetry(logs.Add);
telemetry.Begin(WorldRevealKind.Login);
telemetry.ObserveReadiness(Ready() with { AreCompositeTexturesReady = false });
telemetry.ObserveWorldViewportVisible();
Assert.Equal(1, telemetry.Snapshot.InvariantFailureCount);
Assert.Contains(logs, line => line.Contains("reason=viewport-before-ready", StringComparison.Ordinal));
}
[Fact]
public void NewGeneration_SupersedesOldStateAndRejectsDestinationMutation()
{
var logs = new List<string>();
var telemetry = new WorldRevealLifecycleTelemetry(logs.Add);
long first = telemetry.Begin(WorldRevealKind.Login);
telemetry.ObserveReadiness(Ready(0x11340021u));
long second = telemetry.Begin(WorldRevealKind.Portal);
telemetry.ObserveReadiness(Ready(0x3032001Cu));
telemetry.ObserveReadiness(Ready(0xC95B0001u));
Assert.True(second > first);
Assert.Equal(WorldRevealKind.Portal, telemetry.Snapshot.Kind);
Assert.Equal(0xC95B0001u, telemetry.Snapshot.Readiness.DestinationCell);
Assert.Equal(1, telemetry.Snapshot.InvariantFailureCount);
Assert.Contains(logs, line => line.Contains("event=superseded", StringComparison.Ordinal));
Assert.Contains(logs, line => line.Contains("reason=destination-changed", StringComparison.Ordinal));
}
[Fact]
public void PortalMaterialization_IsCountedOncePerGeneration()
{
var telemetry = new WorldRevealLifecycleTelemetry();
telemetry.Begin(WorldRevealKind.Portal);
telemetry.ObserveReadiness(Ready());
telemetry.ObserveMaterialized();
telemetry.ObserveMaterialized();
telemetry.Begin(WorldRevealKind.Portal);
telemetry.ObserveReadiness(Ready(0x3032001Cu));
telemetry.ObserveMaterialized();
Assert.Equal(2, telemetry.PortalMaterializationCount);
}
[Fact]
public void CancelledGeneration_IgnoresLateAsyncObservations()
{
var telemetry = new WorldRevealLifecycleTelemetry();
telemetry.Begin(WorldRevealKind.Portal);
telemetry.Cancel();
telemetry.ObserveReadiness(Ready());
telemetry.ObserveMaterialized();
telemetry.ObserveWorldViewportVisible();
telemetry.Complete();
Assert.True(telemetry.Snapshot.Cancelled);
Assert.False(telemetry.Snapshot.IsReady);
Assert.False(telemetry.Snapshot.Materialized);
Assert.False(telemetry.Snapshot.WorldViewportObserved);
Assert.False(telemetry.Snapshot.Completed);
Assert.Equal(0, telemetry.PortalMaterializationCount);
}
}

View file

@ -2,6 +2,7 @@ using AcDream.App.Streaming;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Runtime.World;
using AcDream.Core.World; using AcDream.Core.World;
namespace AcDream.App.Tests.World; namespace AcDream.App.Tests.World;
@ -80,6 +81,7 @@ public sealed class LiveEntityWorldOriginCoordinatorTests
farRadius: 2); farRadius: 2);
private static WorldRevealCoordinator Reveal() => new( private static WorldRevealCoordinator Reveal() => new(
new RuntimeWorldTransitState(),
(_, _) => true, (_, _) => true,
_ => true, _ => true,
(_, _) => true, (_, _) => true,

View file

@ -6,6 +6,8 @@ using AcDream.App.World;
using AcDream.Content.Vfx; using AcDream.Content.Vfx;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
using DatReaderWriter; using DatReaderWriter;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
using DatReaderWriter.Options; using DatReaderWriter.Options;
@ -42,8 +44,9 @@ public sealed class RecallTeleportAnimationTests
public void LiveFrame_WhileCellsAreBlocked_DrainsNetworkAndCommandsWithoutWorldTicks() public void LiveFrame_WhileCellsAreBlocked_DrainsNetworkAndCommandsWithoutWorldTicks()
{ {
var calls = new List<string>(); var calls = new List<string>();
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
availability.Begin(1); var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
var frame = new RetailLiveFrameCoordinator( var frame = new RetailLiveFrameCoordinator(
new TestLiveObjectFramePhase(_ => calls.Add("objects")), new TestLiveObjectFramePhase(_ => calls.Add("objects")),
new GpuWorldState(availability: availability), new GpuWorldState(availability: availability),
@ -61,8 +64,9 @@ public sealed class RecallTeleportAnimationTests
public void LiveFrame_WhileCellsAreBlocked_SynchronizesOnlyRenderProjection() public void LiveFrame_WhileCellsAreBlocked_SynchronizesOnlyRenderProjection()
{ {
var calls = new List<string>(); var calls = new List<string>();
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
availability.Begin(1); var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
var frame = new RetailLiveFrameCoordinator( var frame = new RetailLiveFrameCoordinator(
new TestLiveObjectFramePhase(_ => calls.Add("objects")), new TestLiveObjectFramePhase(_ => calls.Add("objects")),
new GpuWorldState(availability: availability), new GpuWorldState(availability: availability),

View file

@ -3,6 +3,8 @@ using AcDream.App.Rendering;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.Update; using AcDream.App.Update;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests.World; namespace AcDream.App.Tests.World;
@ -137,8 +139,10 @@ public sealed class UpdateFrameOrchestratorTests
{ {
var calls = new List<string>(); var calls = new List<string>();
var observed = new FrameObservations(); var observed = new FrameObservations();
var availability = new WorldGenerationAvailabilityState(); var transit = new RuntimeWorldTransitState();
availability.Begin(7); var availability = new WorldGenerationAvailabilityState(transit);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
UpdateFrameOrchestrator frame = Create( UpdateFrameOrchestrator frame = Create(
calls, calls,
observed: observed, observed: observed,
@ -146,7 +150,20 @@ public sealed class UpdateFrameOrchestratorTests
frame.Tick(new UpdateFrameInput(0.25)); frame.Tick(new UpdateFrameInput(0.25));
frame.Tick(new UpdateFrameInput(0.25)); frame.Tick(new UpdateFrameInput(0.25));
availability.End(7); Assert.True(transit.AcknowledgeDestinationReadiness(
new RuntimeDestinationReadiness(
generation,
0x11340021u,
IsIndoor: false,
IsUnhydratable: false,
RequiredRenderRadius: 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true)));
Assert.True(transit.AcknowledgeMaterialized(
generation,
0x11340021u));
transit.Complete(generation);
frame.Tick(new UpdateFrameInput(0.25)); frame.Tick(new UpdateFrameInput(0.25));
Assert.Equal([0.0, 0.0, 0.25], observed.PublishedTimes); Assert.Equal([0.0, 0.0, 0.25], observed.PublishedTimes);

View file

@ -0,0 +1,374 @@
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests.World;
public sealed class RuntimeWorldTransitStateTests
{
private const uint OutdoorCell = 0x11340021u;
private const uint OtherCell = 0x3032001Cu;
[Fact]
public void BeginReveal_OwnsGenerationDestinationAndSimulationGate()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
Assert.Equal(1, generation);
Assert.Equal(generation, state.Snapshot.Generation);
Assert.Equal(RuntimePortalKind.Login, state.Snapshot.Kind);
Assert.Equal(OutdoorCell, state.Snapshot.DestinationCell);
Assert.False(state.IsWorldSimulationAvailable);
Assert.False(state.Snapshot.IsReady);
}
[Fact]
public void TypedReadiness_RejectsWrongGenerationAndDestination()
{
List<string> log = [];
var state = new RuntimeWorldTransitState(log.Add);
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(generation + 1, OutdoorCell)));
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(generation, OtherCell)));
Assert.Equal(OutdoorCell, state.Snapshot.DestinationCell);
Assert.False(state.Snapshot.IsReady);
Assert.Equal(1, state.Snapshot.InvariantFailureCount);
Assert.Equal(
1,
log.Count(value => value.Contains(
"event=invariant-failure",
StringComparison.Ordinal)));
Assert.Single(
log,
value => value.Contains(
"event=rejected",
StringComparison.Ordinal));
}
[Fact]
public void Materialization_ReleasesSimulationBeforeViewportAndCompletion()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.True(state.AcknowledgeMaterialized(
generation,
OutdoorCell));
Assert.True(state.IsWorldSimulationAvailable);
Assert.True(state.Snapshot.Materialized);
Assert.False(state.Snapshot.WorldViewportObserved);
Assert.False(state.Snapshot.Completed);
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
Assert.False(state.AcknowledgeMaterialized(
generation,
OutdoorCell));
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
Assert.True(state.Complete(generation));
Assert.True(state.Snapshot.WorldViewportObserved);
Assert.True(state.Snapshot.Completed);
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
}
[Fact]
public void MaterializationBeforeReadiness_IsRejectedWithoutOpeningWorld()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
Assert.False(state.AcknowledgeMaterialized(
generation,
OutdoorCell));
Assert.False(state.IsWorldSimulationAvailable);
Assert.False(state.Snapshot.Materialized);
Assert.Equal(1, state.Snapshot.InvariantFailureCount);
Assert.Equal(0, state.Snapshot.PortalMaterializationCount);
}
[Fact]
public void LoginCompletion_ReleasesSimulationBeforeFirstVisibleFrame()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell));
Assert.True(state.Complete(generation));
Assert.True(state.IsWorldSimulationAvailable);
Assert.False(state.Snapshot.WorldViewportObserved);
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
Assert.True(state.Snapshot.WorldViewportObserved);
Assert.Equal(0, state.Snapshot.InvariantFailureCount);
}
[Fact]
public void EarlyViewport_IsRejectedWithoutFabricatingVisibility()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
Assert.False(state.AcknowledgeWorldViewportVisible(generation));
Assert.False(state.Snapshot.IsReady);
Assert.False(state.Snapshot.WorldViewportObserved);
Assert.Equal(1, state.Snapshot.InvariantFailureCount);
}
[Fact]
public void ReadinessShape_MustMatchDestinationCell()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
RuntimeDestinationReadiness invalid =
Ready(generation, OutdoorCell) with
{
IsIndoor = true,
RequiredRenderRadius = 0,
};
Assert.False(state.AcknowledgeDestinationReadiness(invalid));
Assert.False(state.Snapshot.IsReady);
Assert.Equal(1, state.Snapshot.InvariantFailureCount);
}
[Fact]
public void AcceptedReadiness_IsGenerationSticky()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
RuntimeDestinationReadiness accepted =
Ready(generation, OutdoorCell);
Assert.True(state.AcknowledgeDestinationReadiness(accepted));
RuntimeDestinationReadiness regressed = accepted with
{
IsCollisionReady = false,
};
Assert.False(state.AcknowledgeDestinationReadiness(regressed));
Assert.Equal(accepted, state.Snapshot.Readiness);
Assert.True(state.Snapshot.IsReady);
Assert.Equal(1, state.Snapshot.InvariantFailureCount);
}
[Fact]
public void Completion_RejectsReorderedLoginAndPortalEdges()
{
var state = new RuntimeWorldTransitState();
long login =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
Assert.False(state.Complete(login));
Assert.False(state.Snapshot.Completed);
long portal =
state.BeginReveal(RuntimePortalKind.Portal, OtherCell);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(portal, OtherCell)));
Assert.False(state.Complete(portal));
Assert.False(state.Snapshot.Completed);
Assert.Equal(2, state.Snapshot.InvariantFailureCount);
}
[Fact]
public void Supersession_RejectsOldAcknowledgementsAndKeepsLatestDestination()
{
var state = new RuntimeWorldTransitState();
long first =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
long second =
state.BeginReveal(RuntimePortalKind.Portal, OtherCell);
Assert.True(second > first);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(first, OutdoorCell)));
Assert.False(state.AcknowledgeMaterialized(first, OutdoorCell));
Assert.Equal(second, state.Snapshot.Generation);
Assert.Equal(OtherCell, state.Snapshot.DestinationCell);
Assert.False(state.Snapshot.IsReady);
Assert.False(state.IsWorldSimulationAvailable);
Assert.Equal(0, state.Snapshot.InvariantFailureCount);
}
[Fact]
public void Cancel_ReleasesSimulationAndRejectsLateHostWork()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
Assert.True(state.Cancel(generation));
Assert.True(state.IsWorldSimulationAvailable);
Assert.True(state.Snapshot.Cancelled);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.False(state.AcknowledgeMaterialized(
generation,
OutdoorCell));
Assert.False(state.Complete(generation));
Assert.False(state.Snapshot.Materialized);
Assert.False(state.Snapshot.Completed);
}
[Fact]
public void MaterializedThenCancelled_RemainsAvailableAndCannotComplete()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell));
state.AcknowledgeMaterialized(generation, OutdoorCell);
Assert.True(state.Cancel(generation));
Assert.True(state.IsWorldSimulationAvailable);
Assert.True(state.Snapshot.Materialized);
Assert.True(state.Snapshot.Cancelled);
Assert.False(state.Complete(generation));
Assert.False(state.Snapshot.Completed);
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
}
[Fact]
public void WaitCue_UsesRetailFiveSecondEdgeWithoutCompletingReveal()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
Assert.False(state.ObserveWait(
generation,
RuntimeWorldTransitState.RetailWaitCueDelay
- TimeSpan.FromMilliseconds(1)));
Assert.True(state.ObserveWait(
generation,
RuntimeWorldTransitState.RetailWaitCueDelay));
Assert.True(state.ObserveWait(
generation,
RuntimeWorldTransitState.RetailWaitCueDelay
+ TimeSpan.FromSeconds(20)));
Assert.True(state.Snapshot.WaitCueShown);
Assert.False(state.Snapshot.Completed);
Assert.False(state.IsWorldSimulationAvailable);
}
[Fact]
public void UnhydratableDestination_IsAnExplicitReadyAcknowledgement()
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
var acknowledgement = new RuntimeDestinationReadiness(
generation,
OutdoorCell,
IsIndoor: false,
IsUnhydratable: true,
RequiredRenderRadius: 1,
IsRenderNeighborhoodReady: false,
AreCompositeTexturesReady: false,
IsCollisionReady: false);
Assert.True(state.AcknowledgeDestinationReadiness(acknowledgement));
Assert.True(state.Snapshot.IsReady);
Assert.True(state.AcknowledgeMaterialized(
generation,
OutdoorCell));
}
[Fact]
public void ResetSession_ClearsVisibleStateButNeverReusesGeneration()
{
var state = new RuntimeWorldTransitState();
long first =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
state.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, state.Snapshot);
Assert.True(state.IsWorldSimulationAvailable);
long second =
state.BeginReveal(RuntimePortalKind.Login, OtherCell);
Assert.True(second > first);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(first, OutdoorCell)));
Assert.Equal(second, state.Snapshot.Generation);
Assert.Equal(OtherCell, state.Snapshot.DestinationCell);
}
[Fact]
public void Instances_IsolateGenerationReadinessAndMaterializationCounts()
{
var first = new RuntimeWorldTransitState();
var second = new RuntimeWorldTransitState();
long firstGeneration =
first.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
long secondGeneration =
second.BeginReveal(RuntimePortalKind.Login, OtherCell);
first.AcknowledgeDestinationReadiness(
Ready(firstGeneration, OutdoorCell));
first.AcknowledgeMaterialized(firstGeneration, OutdoorCell);
Assert.Equal(1, first.Snapshot.PortalMaterializationCount);
Assert.Equal(0, second.Snapshot.PortalMaterializationCount);
Assert.True(first.IsWorldSimulationAvailable);
Assert.False(second.IsWorldSimulationAvailable);
Assert.Equal(OtherCell, second.Snapshot.DestinationCell);
Assert.Equal(1, secondGeneration);
}
[Fact]
public void DiagnosticCallbackFailure_CannotInterruptCanonicalTransition()
{
var state = new RuntimeWorldTransitState(
_ => throw new InvalidOperationException("diagnostic failed"));
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.True(state.AcknowledgeMaterialized(
generation,
OutdoorCell));
Assert.True(state.Complete(generation));
Assert.True(state.Snapshot.Completed);
Assert.True(state.IsWorldSimulationAvailable);
Assert.True(state.DiagnosticFailureCount >= 4);
Assert.IsType<InvalidOperationException>(
state.LastDiagnosticFailure);
}
private static RuntimeDestinationReadiness Ready(
long generation,
uint cell) =>
new(
generation,
cell,
IsIndoor: (cell & 0xFFFFu) >= 0x0100u,
IsUnhydratable: false,
RequiredRenderRadius: (cell & 0xFFFFu) >= 0x0100u ? 0 : 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true);
}