diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index ca59e8c8..2b75382d 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -26,6 +26,7 @@ using AcDream.Core.Selection; using AcDream.Core.Vfx; using AcDream.Core.World; using AcDream.Runtime.Entities; +using AcDream.Runtime.World; using DatReaderWriter; using DatReaderWriter.DBObjs; using Silk.NET.OpenGL; @@ -71,6 +72,7 @@ internal sealed record LivePresentationDependencies( DeferredCanonicalWorldEntityCountSource? DevWorldEntities, DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics, DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics, + Action Log, Action? Toast); internal sealed record LivePresentationResult( @@ -80,6 +82,7 @@ internal sealed record LivePresentationResult( EntitySpawnAdapter EntitySpawnAdapter, EntityScriptActivator EntityScriptActivator, RetailStaticAnimatingObjectScheduler StaticAnimationScheduler, + RuntimeWorldTransitState WorldTransit, WorldGenerationAvailabilityState WorldAvailability, GpuWorldState WorldState, RenderSceneShadowRuntime? RenderSceneShadow, @@ -333,7 +336,9 @@ internal sealed class LivePresentationCompositionPhase staticAnimationScheduler.Unregister, (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( wbSpawnAdapter, d.ClassificationCache.InvalidateLandblock, @@ -583,6 +588,7 @@ internal sealed class LivePresentationCompositionPhase entitySpawnAdapter, entityScriptActivator, staticAnimationScheduler, + worldTransit, worldAvailability, worldState, renderSceneShadow, @@ -630,6 +636,7 @@ internal sealed class LivePresentationCompositionPhase EntitySpawnAdapter entitySpawnAdapter, EntityScriptActivator entityScriptActivator, RetailStaticAnimatingObjectScheduler staticAnimationScheduler, + RuntimeWorldTransitState worldTransit, WorldGenerationAvailabilityState worldAvailability, GpuWorldState worldState, RenderSceneShadowRuntime? renderSceneShadow, @@ -1026,6 +1033,7 @@ internal sealed class LivePresentationCompositionPhase entitySpawnAdapter, entityScriptActivator, staticAnimationScheduler, + worldTransit, worldAvailability, worldState, renderSceneShadow, diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 37b9884b..e74e4f0d 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -325,7 +325,6 @@ internal sealed class SessionPlayerCompositionPhase content.Dats, d.DatLock); var worldQuiescence = new WorldGenerationQuiescence( - live.WorldAvailability, d.Actions.Selection, live.WorldState, guid => live.LiveEntities.TryGetProjectionKey( @@ -340,6 +339,7 @@ internal sealed class SessionPlayerCompositionPhase foundation.MeshAdapter.SetDestinationRevealUploadPriority, foundation.TextureCache.SetDestinationRevealUploadPriority); var worldReveal = new WorldRevealCoordinator( + live.WorldTransit, streaming.IsRenderNeighborhoodResident, d.PhysicsEngine.IsSpawnCellReady, d.PhysicsEngine.IsNeighborhoodTerrainResident, @@ -359,7 +359,6 @@ internal sealed class SessionPlayerCompositionPhase live.DrawDispatcher.InvalidateCompositeWarmupReadiness(); }, spawnClaimClassifier.IsUnhydratable, - d.Log, worldQuiescence, streaming, revealRenderResources); @@ -880,7 +879,7 @@ internal sealed class SessionPlayerCompositionPhase d.Actions, d.PlayerController, d.WorldEnvironment.Runtime, - worldReveal, + live.WorldTransit, d.UpdateClock, live.SelectionInteractions); bindings.Adopt("current game runtime adapter", gameRuntime); diff --git a/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs b/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs index 31d62d1f..e0050b3d 100644 --- a/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs +++ b/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs @@ -5,6 +5,7 @@ using AcDream.App.Rendering.Scene; using AcDream.App.Streaming; using AcDream.App.UI.Testing; using AcDream.Core.Physics; +using AcDream.Runtime; namespace AcDream.App.Diagnostics; @@ -101,7 +102,7 @@ internal sealed record WorldLifecycleCheckpoint( string Name, DateTime TimestampUtc, int ProcessId, - WorldRevealLifecycleSnapshot Reveal, + RuntimePortalSnapshot Reveal, RenderFrameOutcome Render, WorldLifecycleResourceSnapshot Resources); @@ -175,7 +176,7 @@ internal sealed class WorldLifecycleAutomationController : PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - private readonly Func _getReveal; + private readonly Func _getReveal; private readonly Func _getPortalMaterializationCount; private readonly Func _captureResources; @@ -189,7 +190,7 @@ internal sealed class WorldLifecycleAutomationController : private bool _disposed; public WorldLifecycleAutomationController( - Func getReveal, + Func getReveal, Func getPortalMaterializationCount, Func captureResources, FrameScreenshotController screenshots, diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 0ce5aa92..8cc3a206 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1350,6 +1350,7 @@ public sealed class GameWindow : settingsDevTools.DevTools?.LateBindings.WorldEntities, settingsDevTools.DevTools?.LateBindings.FrameDiagnostics, _uiFrameDiagnostics, + Console.WriteLine, compositionToast), this).Compose( platformResult, diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs index 4a9fe121..b6c0f4a3 100644 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs +++ b/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs @@ -1,7 +1,6 @@ using AcDream.App.Input; using AcDream.App.Interaction; using AcDream.App.Net; -using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Runtime; using AcDream.Runtime.Entities; @@ -40,7 +39,7 @@ internal sealed class CurrentGameRuntimeAdapter RuntimeActionState actions, RuntimeLocalPlayerMovementState movement, RuntimeWorldEnvironmentState environment, - WorldRevealCoordinator worldReveal, + RuntimeWorldTransitState worldTransit, IGameRuntimeClock clock, SelectionInteractionController selection) { @@ -54,7 +53,7 @@ internal sealed class CurrentGameRuntimeAdapter actions, movement, environment, - worldReveal, + worldTransit, clock); entityObjects.BindEventContext( () => _view.Generation, diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs index cf50cec0..677ef066 100644 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs +++ b/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs @@ -1,6 +1,5 @@ using AcDream.App.Input; using AcDream.App.Net; -using AcDream.App.Streaming; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Runtime; @@ -29,7 +28,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView private readonly IRuntimeActionView _actionView; private readonly IRuntimeMovementView _movementView; private readonly IRuntimeWorldEnvironmentView _environmentView; - private readonly PortalView _portalView; + private readonly IRuntimePortalView _portalView; private bool _active = true; public CurrentGameRuntimeViewAdapter( @@ -42,7 +41,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView RuntimeActionState actions, RuntimeLocalPlayerMovementState movement, RuntimeWorldEnvironmentState environment, - WorldRevealCoordinator worldReveal, + RuntimeWorldTransitState worldTransit, IGameRuntimeClock clock) { _session = session ?? throw new ArgumentNullException(nameof(session)); @@ -66,8 +65,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView movement ?? throw new ArgumentNullException(nameof(movement))).View; _environmentView = environment ?? throw new ArgumentNullException(nameof(environment)); - _portalView = new PortalView( - worldReveal ?? throw new ArgumentNullException(nameof(worldReveal))); + _portalView = worldTransit + ?? throw new ArgumentNullException(nameof(worldTransit)); } internal bool IsActive => _active && !_session.IsDisposalComplete; @@ -130,33 +129,4 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView _portalView.Snapshot); 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); - } - } - } } diff --git a/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs b/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs index e0b8175f..2f31bc29 100644 --- a/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs +++ b/src/AcDream.App/Streaming/LocalPlayerTeleportController.cs @@ -10,6 +10,7 @@ using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Rendering; using AcDream.Core.World; +using AcDream.Runtime; namespace AcDream.App.Streaming; @@ -668,7 +669,7 @@ internal sealed class LocalPlayerTeleportController // Retail SmartBox enters blocking_for_cells before old-world object, // physics, landscape, and ambient owners may advance again. Begin the // 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)) return false; @@ -737,7 +738,10 @@ internal sealed class LocalPlayerTeleportController if (_lifetimeGeneration != generation) return generation; - _worldReveal.Cancel(); + if (clearSession) + _worldReveal.ResetSession(); + else + _worldReveal.Cancel(); if (_lifetimeGeneration != generation) return generation; diff --git a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs index f6b12c79..4653540c 100644 --- a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs +++ b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs @@ -1,6 +1,7 @@ using AcDream.App.Audio; using AcDream.Core.Selection; using AcDream.Runtime.Entities; +using AcDream.Runtime.World; namespace AcDream.App.Streaming; @@ -24,57 +25,51 @@ internal sealed class AlwaysAvailableWorldGeneration } /// -/// Single-writer state behind . -/// Superseding reveal generations stay continuously quiesced; only the exact -/// active generation may reopen the world. +/// Read-only graphical-host projection of the canonical Runtime transit +/// lifetime. It stores no availability or generation state. /// internal sealed class WorldGenerationAvailabilityState : IWorldGenerationAvailability { - public bool IsWorldAvailable => QuiescedGeneration == 0; - public long QuiescedGeneration { get; private set; } + private readonly RuntimeWorldTransitState _transit; - public void Begin(long generation) + public WorldGenerationAvailabilityState( + RuntimeWorldTransitState transit) { - if (generation <= 0) - throw new ArgumentOutOfRangeException(nameof(generation)); - - QuiescedGeneration = generation; + _transit = transit ?? throw new ArgumentNullException(nameof(transit)); } - public bool End(long generation) - { - if (generation <= 0 || QuiescedGeneration != generation) - return false; + public bool IsWorldAvailable => + _transit.IsWorldSimulationAvailable; - QuiescedGeneration = 0; - return true; - } + public long QuiescedGeneration => + IsWorldAvailable ? 0 : _transit.Snapshot.Generation; } +internal readonly record struct WorldGenerationQuiescenceEdge( + bool ShouldApply, + bool ClearWorldSelection); + /// -/// Owns the immediate observable edge of retail +/// Projects the immediate graphical/audio edge of retail /// SmartBox::UseTime @ 0x00455410's blocking_for_cells -/// branch. Deferred teardown may retain memory, but the old generation cannot -/// keep rendering, targeting, ticking, or producing world audio. +/// branch. Runtime owns whether the world is available; this class only +/// applies selection and audio side effects at the accepted Runtime edge. /// internal sealed class WorldGenerationQuiescence { - private readonly WorldGenerationAvailabilityState _availability; private readonly SelectionState _selection; private readonly GpuWorldState _world; private readonly Func _resolveProjectionKey; private readonly IWorldAudioQuiescence? _audio; + private bool _effectsQuiesced; public WorldGenerationQuiescence( - WorldGenerationAvailabilityState availability, SelectionState selection, GpuWorldState world, Func resolveProjectionKey, IWorldAudioQuiescence? audio) { - _availability = availability - ?? throw new ArgumentNullException(nameof(availability)); _selection = selection ?? throw new ArgumentNullException(nameof(selection)); _world = world ?? throw new ArgumentNullException(nameof(world)); _resolveProjectionKey = resolveProjectionKey @@ -82,22 +77,33 @@ internal sealed class WorldGenerationQuiescence _audio = audio; } - public IWorldGenerationAvailability Availability => _availability; - - public void Begin(long generation) + /// + /// Captures the pre-quiescence graphical selection fact before Runtime + /// closes world availability. The returned edge contains no authority and + /// is committed only after Runtime accepts the new generation. + /// + public WorldGenerationQuiescenceEdge CaptureBegin() { - bool firstEdge = _availability.IsWorldAvailable; - uint? selected = firstEdge ? _selection.SelectedObjectId : null; + if (_effectsQuiesced) + return default; + + uint? selected = _selection.SelectedObjectId; bool clearWorldSelection = selected is uint selectedGuid && _resolveProjectionKey(selectedGuid) is RuntimeEntityKey key && _world.IsLiveEntityVisible(key); + return new WorldGenerationQuiescenceEdge( + ShouldApply: true, + clearWorldSelection); + } - _availability.Begin(generation); - if (!firstEdge) + public void CommitBegin(in WorldGenerationQuiescenceEdge edge) + { + if (!edge.ShouldApply || _effectsQuiesced) return; - if (clearWorldSelection) + _effectsQuiesced = true; + if (edge.ClearWorldSelection) { _selection.Clear( SelectionChangeSource.System, @@ -107,9 +113,12 @@ internal sealed class WorldGenerationQuiescence _audio?.SuspendWorldAudio(); } - public void End(long generation) + public void ObserveReleased() { - if (_availability.End(generation)) - _audio?.ResumeWorldAudio(); + if (!_effectsQuiesced) + return; + + _effectsQuiesced = false; + _audio?.ResumeWorldAudio(); } } diff --git a/src/AcDream.App/Streaming/WorldRevealCoordinator.cs b/src/AcDream.App/Streaming/WorldRevealCoordinator.cs index 1813bbb4..3b40421c 100644 --- a/src/AcDream.App/Streaming/WorldRevealCoordinator.cs +++ b/src/AcDream.App/Streaming/WorldRevealCoordinator.cs @@ -1,9 +1,13 @@ +using AcDream.Runtime; +using AcDream.Runtime.World; + namespace AcDream.App.Streaming; /// /// Generation-scoped destination-work seam. The reveal coordinator is the -/// canonical owner of login/portal lifetime; the streaming scheduler only -/// reserves capacity for the exact generation and destination it is given. +/// graphical host for Runtime's canonical login/portal lifetime; the +/// streaming scheduler only reserves capacity for the exact generation and +/// destination it is given. /// internal interface IWorldRevealStreamingScheduler { @@ -26,23 +30,23 @@ internal interface IWorldRevealRenderResourceScheduler } /// -/// Owns one login/portal reveal lifetime across readiness and lifecycle -/// diagnostics. This keeps the destination barrier and its observations on a -/// single seam so callers cannot begin, prepare, or evaluate one owner while -/// forgetting the other. +/// Graphical-host adapter for one canonical Runtime login/portal reveal +/// lifetime. App owns render/collision readiness and destination resource +/// reservations; Runtime owns generation, destination, materialization, +/// simulation availability, completion, and cancellation. /// internal sealed class WorldRevealCoordinator { + private readonly RuntimeWorldTransitState _transit; private readonly WorldRevealReadinessBarrier _readiness; - private readonly WorldRevealLifecycleTelemetry _lifecycle; private readonly WorldGenerationQuiescence? _quiescence; private readonly IWorldRevealStreamingScheduler? _streaming; private readonly IWorldRevealRenderResourceScheduler? _renderResources; - private long _activeGeneration; - private bool _worldSimulationReleased; + private long _reservationGeneration; private bool _worldViewportReleased; public WorldRevealCoordinator( + RuntimeWorldTransitState transit, Func isRenderNeighborhoodReady, Func isSpawnCellReady, Func isTerrainNeighborhoodReady, @@ -50,11 +54,11 @@ internal sealed class WorldRevealCoordinator Action prepareCompositeTextures, Action invalidateCompositeTextures, Func isSpawnClaimUnhydratable, - Action? log = null, WorldGenerationQuiescence? quiescence = null, IWorldRevealStreamingScheduler? streaming = null, IWorldRevealRenderResourceScheduler? renderResources = null) { + _transit = transit ?? throw new ArgumentNullException(nameof(transit)); _readiness = new WorldRevealReadinessBarrier( isRenderNeighborhoodReady, isSpawnCellReady, @@ -63,27 +67,28 @@ internal sealed class WorldRevealCoordinator prepareCompositeTextures, invalidateCompositeTextures, isSpawnClaimUnhydratable); - _lifecycle = new WorldRevealLifecycleTelemetry(log); _quiescence = quiescence; _streaming = streaming; _renderResources = renderResources; } - public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot; - public int PortalMaterializationCount => _lifecycle.PortalMaterializationCount; - public bool WaitCueShown => _lifecycle.WaitCueShown; + public RuntimePortalSnapshot Snapshot => _transit.Snapshot; + public int PortalMaterializationCount => + _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) throw new ArgumentOutOfRangeException(nameof(destinationCell)); + WorldGenerationQuiescenceEdge quiescenceEdge = + _quiescence?.CaptureBegin() ?? default; _readiness.Begin(); - long generation = _lifecycle.Begin(kind); - _activeGeneration = generation; - _worldSimulationReleased = false; + long generation = _transit.BeginReveal(kind, destinationCell); + _quiescence?.CommitBegin(quiescenceEdge); + _reservationGeneration = generation; _worldViewportReleased = false; - _quiescence?.Begin(generation); _streaming?.BeginDestinationReservation( generation, destinationCell, @@ -105,7 +110,20 @@ internal sealed class WorldRevealCoordinator public WorldRevealReadinessSnapshot Evaluate(uint 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; } @@ -122,15 +140,30 @@ internal sealed class WorldRevealCoordinator /// public void ObserveMaterialized() { - _lifecycle.ObserveMaterialized(); - ReleaseWorldSimulation(); + RuntimePortalSnapshot snapshot = _transit.Snapshot; + if (snapshot.Generation == 0) + return; + + bool wasAvailable = _transit.IsWorldSimulationAvailable; + _transit.AcknowledgeMaterialized( + snapshot.Generation, + snapshot.DestinationCell); + ObserveSimulationRelease(wasAvailable); } - public void ObserveWorldViewportVisible() => - _lifecycle.ObserveWorldViewportVisible(); + public void ObserveWorldViewportVisible() + { + long generation = _transit.Snapshot.Generation; + if (generation != 0) + _transit.AcknowledgeWorldViewportVisible(generation); + } - public bool ObserveWait(TimeSpan elapsed) => - _lifecycle.ObserveWait(elapsed); + public bool ObserveWait(TimeSpan elapsed) + { + long generation = _transit.Snapshot.Generation; + return generation != 0 + && _transit.ObserveWait(generation, elapsed); + } /// /// Releases the destination streaming/resource reservation at retail's @@ -140,7 +173,7 @@ internal sealed class WorldRevealCoordinator /// public void RevealWorldViewport() { - long generation = _activeGeneration; + long generation = _reservationGeneration; if (generation == 0 || _worldViewportReleased) return; @@ -151,36 +184,53 @@ internal sealed class WorldRevealCoordinator public void Complete() { - _lifecycle.Complete(); - EndLifetime(); + long generation = _transit.Snapshot.Generation; + if (generation == 0) + return; + + bool wasAvailable = _transit.IsWorldSimulationAvailable; + _transit.Complete(generation); + ObserveSimulationRelease(wasAvailable); + EndHostLifetime(); } public void Cancel() { - _lifecycle.Cancel(); - EndLifetime(); - } - - private void EndLifetime() - { - long generation = _activeGeneration; + long generation = _transit.Snapshot.Generation; + if (generation == 0) + return; + + bool wasAvailable = _transit.IsWorldSimulationAvailable; + _transit.Cancel(generation); + 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) return; - ReleaseWorldSimulation(); RevealWorldViewport(); - _activeGeneration = 0; - _worldSimulationReleased = false; + _reservationGeneration = 0; _worldViewportReleased = false; } - private void ReleaseWorldSimulation() + private void ObserveSimulationRelease(bool wasAvailable) { - long generation = _activeGeneration; - if (generation == 0 || _worldSimulationReleased) - return; - - _quiescence?.End(generation); - _worldSimulationReleased = true; + if (!wasAvailable && _transit.IsWorldSimulationAvailable) + _quiescence?.ObserveReleased(); } } diff --git a/src/AcDream.App/Streaming/WorldRevealLifecycleTelemetry.cs b/src/AcDream.App/Streaming/WorldRevealLifecycleTelemetry.cs deleted file mode 100644 index d23b589a..00000000 --- a/src/AcDream.App/Streaming/WorldRevealLifecycleTelemetry.cs +++ /dev/null @@ -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; -} - -/// -/// 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. -/// -internal sealed class WorldRevealLifecycleTelemetry -{ - internal static readonly TimeSpan RetailWaitCueDelay = - TimeSpan.FromSeconds(5); - - private readonly Action _log; - private WorldRevealLifecycleSnapshot _snapshot; - private long _nextGeneration; - private int _portalMaterializationCount; - private bool _waitCueShown; - - public WorldRevealLifecycleTelemetry(Action? 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}"); - } -} diff --git a/src/AcDream.App/World/LiveEntityHydrationPorts.cs b/src/AcDream.App/World/LiveEntityHydrationPorts.cs index c555f2fe..0f11bb4f 100644 --- a/src/AcDream.App/World/LiveEntityHydrationPorts.cs +++ b/src/AcDream.App/World/LiveEntityHydrationPorts.cs @@ -5,6 +5,7 @@ using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.Core.Net; using AcDream.Core.Net.Messages; +using AcDream.Runtime; using AcDream.Runtime.Entities; namespace AcDream.App.World; @@ -237,7 +238,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC lbY, isSealedDungeon: _sealedDungeonCells.IsSealedDungeon( position.LandblockId)); - _worldReveal.Begin(WorldRevealKind.Login, position.LandblockId); + _worldReveal.Begin(RuntimePortalKind.Login, position.LandblockId); return new( true, diff --git a/src/AcDream.Runtime/GameRuntimeViews.cs b/src/AcDream.Runtime/GameRuntimeViews.cs index f974c1a3..52a008da 100644 --- a/src/AcDream.Runtime/GameRuntimeViews.cs +++ b/src/AcDream.Runtime/GameRuntimeViews.cs @@ -92,15 +92,59 @@ public enum RuntimePortalKind 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( long Generation, RuntimePortalKind Kind, - uint DestinationCell, - bool IsReady, - bool IsMaterialized, - bool IsCompleted, - bool IsCancelled, - bool IsWorldVisible); + RuntimeDestinationReadiness Readiness, + bool Materialized, + bool Completed, + bool Cancelled, + bool WorldViewportObserved, + 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 { diff --git a/src/AcDream.Runtime/World/RuntimeWorldTransitState.cs b/src/AcDream.Runtime/World/RuntimeWorldTransitState.cs new file mode 100644 index 00000000..9137711e --- /dev/null +++ b/src/AcDream.Runtime/World/RuntimeWorldTransitState.cs @@ -0,0 +1,385 @@ +using System.Globalization; + +namespace AcDream.Runtime.World; + +/// +/// 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. +/// +/// +/// Retail ordering follows SmartBox::TeleportPlayer @ 0x00453910, +/// SmartBox::UseTime @ 0x00455410, and +/// gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0. Destination +/// materialization resumes simulation before the later normal-world viewport +/// edge. +/// +public sealed class RuntimeWorldTransitState + : IRuntimePortalView +{ + public static readonly TimeSpan RetailWaitCueDelay = + TimeSpan.FromSeconds(5); + + private readonly Action _log; + private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle; + private long _nextGeneration; + + public RuntimeWorldTransitState(Action? 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; + } + + /// + /// Clears session-visible state while retaining the monotonically + /// increasing generation counter so a delayed prior-session host + /// acknowledgement cannot match a future generation. + /// + 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; +} diff --git a/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs b/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs index dd392978..b816d50a 100644 --- a/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs +++ b/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs @@ -5,6 +5,7 @@ using AcDream.App.Rendering.Residency; using AcDream.App.Rendering.Scene; using AcDream.App.Streaming; using AcDream.App.UI.Testing; +using AcDream.Runtime; using SixLabors.ImageSharp; namespace AcDream.App.Tests.Diagnostics; @@ -95,10 +96,11 @@ public sealed class WorldLifecycleAutomationControllerTests public void Checkpoint_WritesCanonicalRevealAndResourceSnapshot() { string directory = NewDirectory(); - var reveal = new WorldRevealLifecycleSnapshot( + var reveal = new RuntimePortalSnapshot( Generation: 7, - Kind: WorldRevealKind.Portal, - Readiness: new WorldRevealReadinessSnapshot( + Kind: RuntimePortalKind.Portal, + Readiness: new RuntimeDestinationReadiness( + Generation: 7, 0x8A020164u, IsIndoor: true, IsUnhydratable: false, @@ -110,7 +112,10 @@ public sealed class WorldLifecycleAutomationControllerTests Completed: true, Cancelled: false, WorldViewportObserved: true, - InvariantFailureCount: 0); + WorldSimulationAvailable: true, + InvariantFailureCount: 0, + WaitCueShown: false, + PortalMaterializationCount: 3); var resources = EmptyResources() with { LoadedLandblocks = 1, diff --git a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs index 07e96b10..cb18be03 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs @@ -6,6 +6,8 @@ using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.Core.Rendering; using AcDream.Core.World; +using AcDream.Runtime; +using AcDream.Runtime.World; namespace AcDream.App.Tests.Rendering; @@ -25,8 +27,9 @@ public sealed class WorldSceneRendererTests [Fact] public void QuiescedGeneration_PublishesEmptySelectionFrameAndSkipsEveryWorldOwner() { - var availability = new WorldGenerationAvailabilityState(); - availability.Begin(4); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); + transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u); var rig = new Rig( portalVisible: false, waitingForLogin: false, diff --git a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs index 1b720ba1..005aa289 100644 --- a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs +++ b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs @@ -65,7 +65,7 @@ public sealed class CurrentGameRuntimeAdapterTests }; harness.Objects.AddOrUpdate(item); harness.Chat.OnSystemMessage("runtime parity", 0x1Au); - harness.WorldReveal.Begin(WorldRevealKind.Portal, 0x12340001u); + harness.WorldReveal.Begin(RuntimePortalKind.Portal, 0x12340001u); WorldRevealReadinessSnapshot readiness = harness.WorldReveal.PrepareAndEvaluate(0x12340001u); Assert.True(readiness.IsReady); @@ -792,7 +792,9 @@ public sealed class CurrentGameRuntimeAdapterTests mouseLook: null, new NoopCombatInput()); Clock = new UpdateFrameClock(); + WorldTransit = new RuntimeWorldTransitState(); WorldReveal = new WorldRevealCoordinator( + WorldTransit, static (_, _) => true, static _ => true, static (_, _) => true, @@ -836,7 +838,7 @@ public sealed class CurrentGameRuntimeAdapterTests Actions, MovementState, Environment, - WorldReveal, + WorldTransit, Clock, selectionController); } @@ -861,6 +863,7 @@ public sealed class CurrentGameRuntimeAdapterTests public GameplayInputFrameController GameplayInput { get; } public RecordingCombatModeOperations CombatMode { get; } public UpdateFrameClock Clock { get; } + public RuntimeWorldTransitState WorldTransit { get; } public WorldRevealCoordinator WorldReveal { get; } public RecordingCommandRouting Commands { get; } public TestTransport Transport { get; } diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs new file mode 100644 index 00000000..2bb69031 --- /dev/null +++ b/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs @@ -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()); + } + + [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."); + } +} diff --git a/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs b/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs index 804700a0..698d4d08 100644 --- a/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs +++ b/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs @@ -5,7 +5,9 @@ using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; +using AcDream.Runtime; using AcDream.Runtime.Entities; +using AcDream.Runtime.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Streaming; @@ -617,8 +619,10 @@ public sealed class GpuWorldStateVisibilityTests const uint landblock = 0x0101FFFFu; const uint cell = 0x01010001u; const uint guid = 0x70000021u; - var availability = new WorldGenerationAvailabilityState(); - availability.Begin(7); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); + long generation = + transit.BeginReveal(RuntimePortalKind.Portal, cell); var state = new GpuWorldState(availability: availability); state.AddLandblock(new LoadedLandblock( landblock, @@ -644,7 +648,18 @@ public sealed class GpuWorldStateVisibilityTests Assert.True(record.IsSpatiallyVisible); 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(record.IsSpatiallyVisible); } @@ -688,8 +703,9 @@ public sealed class GpuWorldStateVisibilityTests [Fact] public void ReadinessNeighborhood_CopiesStaticAndLiveWhileWorldIsQuiesced() { - var availability = new WorldGenerationAvailabilityState(); - availability.Begin(7); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); + transit.BeginReveal(RuntimePortalKind.Portal, 0x10100001u); var centerStatic = Entity(1, 0u); var centerLive = Entity(2, 0x70000001u); var neighborStatic = Entity(3, 0u); diff --git a/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs b/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs index 8689d125..2567f210 100644 --- a/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs @@ -8,6 +8,8 @@ using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; +using AcDream.Runtime; +using AcDream.Runtime.World; namespace AcDream.App.Tests.Streaming; @@ -57,7 +59,7 @@ public sealed class LocalPlayerTeleportControllerTests (1L, 0x20210001u, WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius), Assert.Single(harness.Streaming.Reservations)); 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( destination, @@ -310,7 +312,7 @@ public sealed class LocalPlayerTeleportControllerTests harness.Controller.ResetSession(); Assert.False(harness.Controller.IsActive); - Assert.True(harness.Reveal.Snapshot.Cancelled); + Assert.Equal(RuntimePortalSnapshot.Idle, harness.Reveal.Snapshot); Assert.True(harness.Streaming.LastResetWasSessionEnding); var failed = new Harness(); @@ -665,6 +667,7 @@ public sealed class LocalPlayerTeleportControllerTests Session = new FakeSession(order); Presentation = new FakePresentation(order); Reveal = new WorldRevealCoordinator( + new RuntimeWorldTransitState(order.Add), isRenderNeighborhoodReady: (_, _) => worldReady, isSpawnCellReady: _ => worldReady, isTerrainNeighborhoodReady: (_, _) => worldReady, @@ -672,7 +675,6 @@ public sealed class LocalPlayerTeleportControllerTests prepareCompositeTextures: (_, _) => { }, invalidateCompositeTextures: () => { }, isSpawnClaimUnhydratable: _ => false, - log: order.Add, streaming: Streaming); Controller = new LocalPlayerTeleportController( Authority, diff --git a/tests/AcDream.App.Tests/Streaming/WorldGenerationQuiescenceTests.cs b/tests/AcDream.App.Tests/Streaming/WorldGenerationQuiescenceTests.cs index 7f55ecce..5df83081 100644 --- a/tests/AcDream.App.Tests/Streaming/WorldGenerationQuiescenceTests.cs +++ b/tests/AcDream.App.Tests/Streaming/WorldGenerationQuiescenceTests.cs @@ -3,7 +3,9 @@ using AcDream.App.Audio; using AcDream.App.Streaming; using AcDream.Core.Selection; using AcDream.Core.World; +using AcDream.Runtime; using AcDream.Runtime.Entities; +using AcDream.Runtime.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Streaming; @@ -11,12 +13,14 @@ namespace AcDream.App.Tests.Streaming; public sealed class WorldGenerationQuiescenceTests { private const uint LandblockId = 0x1010FFFFu; + private const uint DestinationCell = 0x10100001u; private const uint ServerGuid = 0x70000001u; [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); WorldEntity entity = Entity(); world.AddLandblock(new LoadedLandblock( @@ -32,10 +36,12 @@ public sealed class WorldGenerationQuiescenceTests Assert.Single(world.LandblockEntries); Assert.Single(world.LandblockBounds); - availability.Begin(17); + long generation = + transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell); world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby); Assert.False(availability.IsWorldAvailable); + Assert.Equal(generation, availability.QuiescedGeneration); Assert.False(world.IsLiveEntityVisible(key)); Assert.Empty(world.LandblockEntries); Assert.Empty(world.LandblockBounds); @@ -43,17 +49,25 @@ public sealed class WorldGenerationQuiescenceTests Assert.Same(entity, Assert.Single(world.Entities)); Assert.True(world.IsLoaded(LandblockId)); - Assert.False(availability.End(16)); + Assert.False(transit.Complete(generation + 1)); 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.Same(entity, Assert.Single(world.LandblockEntries).Entities.Single()); + Assert.Same( + entity, + Assert.Single(world.LandblockEntries).Entities.Single()); } [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); world.AddLandblock(new LoadedLandblock( LandblockId, @@ -65,23 +79,36 @@ public sealed class WorldGenerationQuiescenceTests selection.Select(ServerGuid, SelectionChangeSource.World); var audio = new RecordingAudioQuiescence(); var quiescence = new WorldGenerationQuiescence( - availability, selection, world, guid => guid == ServerGuid ? Key(entity) : null, audio); - quiescence.Begin(1); - quiescence.Begin(2); - quiescence.End(1); + WorldGenerationQuiescenceEdge firstEdge = + quiescence.CaptureBegin(); + 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.Equal(2, availability.QuiescedGeneration); + Assert.Equal(second, availability.QuiescedGeneration); Assert.Null(selection.SelectedObjectId); Assert.Equal(1, audio.SuspendCalls); 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.Equal(1, audio.ResumeCalls); @@ -91,22 +118,37 @@ public sealed class WorldGenerationQuiescenceTests public void Quiesce_PreservesNonWorldInventorySelection() { const uint inventoryGuid = 0x80000001u; - var availability = new WorldGenerationAvailabilityState(); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var selection = new SelectionState(); selection.Select(inventoryGuid, SelectionChangeSource.Inventory); var quiescence = new WorldGenerationQuiescence( - availability, selection, world, _ => null, audio: null); - quiescence.Begin(3); + WorldGenerationQuiescenceEdge edge = quiescence.CaptureBegin(); + transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell); + quiescence.CommitBegin(edge); 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() { Id = 1, diff --git a/tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs b/tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs index 95151ad9..f42e1e89 100644 --- a/tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs +++ b/tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs @@ -1,6 +1,8 @@ using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Selection; +using AcDream.Runtime; +using AcDream.Runtime.World; namespace AcDream.App.Tests.Streaming; @@ -17,21 +19,27 @@ public sealed class WorldRevealCoordinatorTests public WorldRevealCoordinator Build( List? logs = null, IWorldRevealStreamingScheduler? streaming = null, - IWorldRevealRenderResourceScheduler? renderResources = null) => new( - isRenderNeighborhoodReady: (_, _) => RenderReady, - isSpawnCellReady: _ => CollisionReady, - isTerrainNeighborhoodReady: (_, _) => CollisionReady, - areCompositeTexturesReady: () => CompositesReady, - prepareCompositeTextures: (_, _) => PrepareCount++, - invalidateCompositeTextures: () => - { - InvalidateCount++; - CompositesReady = false; - }, - isSpawnClaimUnhydratable: _ => false, - log: logs is null ? null : new Action(logs.Add), - streaming: streaming, - renderResources: renderResources); + IWorldRevealRenderResourceScheduler? renderResources = null, + RuntimeWorldTransitState? transit = null) + { + transit ??= new RuntimeWorldTransitState( + logs is null ? null : new Action(logs.Add)); + return new WorldRevealCoordinator( + transit, + isRenderNeighborhoodReady: (_, _) => RenderReady, + isSpawnCellReady: _ => CollisionReady, + isTerrainNeighborhoodReady: (_, _) => CollisionReady, + areCompositeTexturesReady: () => CompositesReady, + prepareCompositeTextures: (_, _) => PrepareCount++, + invalidateCompositeTextures: () => + { + InvalidateCount++; + CompositesReady = false; + }, + isSpawnClaimUnhydratable: _ => false, + streaming: streaming, + renderResources: renderResources); + } } [Fact] @@ -40,12 +48,12 @@ public sealed class WorldRevealCoordinatorTests var state = new State { CompositesReady = true }; 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, state.InvalidateCount); Assert.False(state.CompositesReady); - Assert.Equal(WorldRevealKind.Login, coordinator.Snapshot.Kind); + Assert.Equal(RuntimePortalKind.Login, coordinator.Snapshot.Kind); } [Fact] @@ -58,7 +66,7 @@ public sealed class WorldRevealCoordinatorTests CollisionReady = true, }; WorldRevealCoordinator coordinator = state.Build(); - coordinator.Begin(WorldRevealKind.Login, outdoorCell); + coordinator.Begin(RuntimePortalKind.Login, outdoorCell); WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell); state.CompositesReady = true; @@ -67,7 +75,8 @@ public sealed class WorldRevealCoordinatorTests Assert.False(first.IsReady); Assert.True(ready.IsReady); 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] @@ -82,7 +91,7 @@ public sealed class WorldRevealCoordinatorTests CompositesReady = true, }; WorldRevealCoordinator coordinator = state.Build(logs); - coordinator.Begin(WorldRevealKind.Portal, outdoorCell); + coordinator.Begin(RuntimePortalKind.Portal, outdoorCell); state.CompositesReady = true; Assert.True(coordinator.Evaluate(outdoorCell).IsReady); @@ -109,7 +118,7 @@ public sealed class WorldRevealCoordinatorTests CompositesReady = true, }; WorldRevealCoordinator coordinator = state.Build(); - coordinator.Begin(WorldRevealKind.Portal, 0x11340021u); + coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u); coordinator.Cancel(); coordinator.PrepareAndEvaluate(0x11340021u); @@ -118,23 +127,25 @@ public sealed class WorldRevealCoordinatorTests coordinator.Complete(); 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); } [Fact] public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens() { - var availability = new WorldGenerationAvailabilityState(); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var audio = new RecordingAudioQuiescence(); var quiescence = new WorldGenerationQuiescence( - availability, new SelectionState(), world, _ => null, audio); var coordinator = new WorldRevealCoordinator( + transit, isRenderNeighborhoodReady: (_, _) => true, isSpawnCellReady: _ => true, isTerrainNeighborhoodReady: (_, _) => true, @@ -144,12 +155,14 @@ public sealed class WorldRevealCoordinatorTests isSpawnClaimUnhydratable: _ => false, quiescence: quiescence); - Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login, 0x11340021u)); - Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal, 0x11340021u)); + Assert.Equal(1, coordinator.Begin(RuntimePortalKind.Login, 0x11340021u)); + Assert.Equal(2, coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u)); Assert.False(availability.IsWorldAvailable); Assert.Equal(2, availability.QuiescedGeneration); Assert.Equal(1, audio.SuspendCalls); + Assert.True(coordinator.Evaluate(0x11340021u).IsReady); + coordinator.ObserveMaterialized(); coordinator.Complete(); Assert.True(availability.IsWorldAvailable); @@ -159,17 +172,18 @@ public sealed class WorldRevealCoordinatorTests [Fact] public void Materialization_ReopensWorldBeforePortalViewportRelease() { - var availability = new WorldGenerationAvailabilityState(); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var audio = new RecordingAudioQuiescence(); var quiescence = new WorldGenerationQuiescence( - availability, new SelectionState(), world, _ => null, audio); var scheduler = new RecordingDestinationScheduler(); var coordinator = new WorldRevealCoordinator( + transit, isRenderNeighborhoodReady: (_, _) => true, isSpawnCellReady: _ => true, isTerrainNeighborhoodReady: (_, _) => true, @@ -180,10 +194,11 @@ public sealed class WorldRevealCoordinatorTests quiescence: quiescence, streaming: scheduler); long generation = coordinator.Begin( - WorldRevealKind.Portal, + RuntimePortalKind.Portal, 0x3032001Cu); Assert.False(availability.IsWorldAvailable); + Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady); coordinator.ObserveMaterialized(); @@ -211,16 +226,17 @@ public sealed class WorldRevealCoordinatorTests [Fact] public void TunnelContinue_TicksDestinationObjectsBehindPortalViewport() { - var availability = new WorldGenerationAvailabilityState(); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var quiescence = new WorldGenerationQuiescence( - availability, new SelectionState(), world, _ => null, audio: null); var scheduler = new RecordingDestinationScheduler(); var coordinator = new WorldRevealCoordinator( + transit, isRenderNeighborhoodReady: (_, _) => true, isSpawnCellReady: _ => true, isTerrainNeighborhoodReady: (_, _) => true, @@ -243,11 +259,12 @@ public sealed class WorldRevealCoordinatorTests () => calls.Add("spatial")), availability); - coordinator.Begin(WorldRevealKind.Portal, 0x3032001Cu); + coordinator.Begin(RuntimePortalKind.Portal, 0x3032001Cu); frame.Tick(1f / 60f); Assert.Equal(["network", "commands"], calls); calls.Clear(); + Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady); coordinator.ObserveMaterialized(); frame.Tick(1f / 60f); @@ -256,6 +273,43 @@ public sealed class WorldRevealCoordinatorTests 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] public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion() { @@ -264,10 +318,10 @@ public sealed class WorldRevealCoordinatorTests WorldRevealCoordinator coordinator = state.Build(streaming: scheduler); long first = coordinator.Begin( - WorldRevealKind.Portal, + RuntimePortalKind.Portal, 0x11340021u); long second = coordinator.Begin( - WorldRevealKind.Portal, + RuntimePortalKind.Portal, 0x20210123u); Assert.Equal( @@ -289,10 +343,10 @@ public sealed class WorldRevealCoordinatorTests state.Build(renderResources: resources); long first = coordinator.Begin( - WorldRevealKind.Portal, + RuntimePortalKind.Portal, 0x11340021u); long second = coordinator.Begin( - WorldRevealKind.Portal, + RuntimePortalKind.Portal, 0x20210123u); Assert.Equal([first, second], resources.Begins); diff --git a/tests/AcDream.App.Tests/Streaming/WorldRevealLifecycleTelemetryTests.cs b/tests/AcDream.App.Tests/Streaming/WorldRevealLifecycleTelemetryTests.cs deleted file mode 100644 index a318c720..00000000 --- a/tests/AcDream.App.Tests/Streaming/WorldRevealLifecycleTelemetryTests.cs +++ /dev/null @@ -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(); - 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(); - 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(); - 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); - } -} diff --git a/tests/AcDream.App.Tests/World/LiveEntityWorldOriginCoordinatorTests.cs b/tests/AcDream.App.Tests/World/LiveEntityWorldOriginCoordinatorTests.cs index eb4b7261..d8dc607a 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityWorldOriginCoordinatorTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityWorldOriginCoordinatorTests.cs @@ -2,6 +2,7 @@ using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; +using AcDream.Runtime.World; using AcDream.Core.World; namespace AcDream.App.Tests.World; @@ -80,6 +81,7 @@ public sealed class LiveEntityWorldOriginCoordinatorTests farRadius: 2); private static WorldRevealCoordinator Reveal() => new( + new RuntimeWorldTransitState(), (_, _) => true, _ => true, (_, _) => true, diff --git a/tests/AcDream.App.Tests/World/RecallTeleportAnimationTests.cs b/tests/AcDream.App.Tests/World/RecallTeleportAnimationTests.cs index c13f04e2..bbe02154 100644 --- a/tests/AcDream.App.Tests/World/RecallTeleportAnimationTests.cs +++ b/tests/AcDream.App.Tests/World/RecallTeleportAnimationTests.cs @@ -6,6 +6,8 @@ using AcDream.App.World; using AcDream.Content.Vfx; using AcDream.Core.Physics; using AcDream.Core.World; +using AcDream.Runtime; +using AcDream.Runtime.World; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; @@ -42,8 +44,9 @@ public sealed class RecallTeleportAnimationTests public void LiveFrame_WhileCellsAreBlocked_DrainsNetworkAndCommandsWithoutWorldTicks() { var calls = new List(); - var availability = new WorldGenerationAvailabilityState(); - availability.Begin(1); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); + transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u); var frame = new RetailLiveFrameCoordinator( new TestLiveObjectFramePhase(_ => calls.Add("objects")), new GpuWorldState(availability: availability), @@ -61,8 +64,9 @@ public sealed class RecallTeleportAnimationTests public void LiveFrame_WhileCellsAreBlocked_SynchronizesOnlyRenderProjection() { var calls = new List(); - var availability = new WorldGenerationAvailabilityState(); - availability.Begin(1); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); + transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u); var frame = new RetailLiveFrameCoordinator( new TestLiveObjectFramePhase(_ => calls.Add("objects")), new GpuWorldState(availability: availability), diff --git a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs index 4e186fcb..e39acb4f 100644 --- a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs +++ b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs @@ -3,6 +3,8 @@ using AcDream.App.Rendering; using AcDream.App.Streaming; using AcDream.App.Update; using AcDream.App.World; +using AcDream.Runtime; +using AcDream.Runtime.World; namespace AcDream.App.Tests.World; @@ -137,8 +139,10 @@ public sealed class UpdateFrameOrchestratorTests { var calls = new List(); var observed = new FrameObservations(); - var availability = new WorldGenerationAvailabilityState(); - availability.Begin(7); + var transit = new RuntimeWorldTransitState(); + var availability = new WorldGenerationAvailabilityState(transit); + long generation = + transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u); UpdateFrameOrchestrator frame = Create( calls, observed: observed, @@ -146,7 +150,20 @@ public sealed class UpdateFrameOrchestratorTests 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)); Assert.Equal([0.0, 0.0, 0.25], observed.PublishedTimes); diff --git a/tests/AcDream.Runtime.Tests/World/RuntimeWorldTransitStateTests.cs b/tests/AcDream.Runtime.Tests/World/RuntimeWorldTransitStateTests.cs new file mode 100644 index 00000000..26c34c93 --- /dev/null +++ b/tests/AcDream.Runtime.Tests/World/RuntimeWorldTransitStateTests.cs @@ -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 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( + 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); +}