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.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<string> Log,
Action<string>? 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,

View file

@ -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);

View file

@ -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<WorldRevealLifecycleSnapshot> _getReveal;
private readonly Func<RuntimePortalSnapshot> _getReveal;
private readonly Func<int> _getPortalMaterializationCount;
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
_captureResources;
@ -189,7 +190,7 @@ internal sealed class WorldLifecycleAutomationController :
private bool _disposed;
public WorldLifecycleAutomationController(
Func<WorldRevealLifecycleSnapshot> getReveal,
Func<RuntimePortalSnapshot> getReveal,
Func<int> getPortalMaterializationCount,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
FrameScreenshotController screenshots,

View file

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

View file

@ -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,

View file

@ -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);
}
}
}
}

View file

@ -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;

View file

@ -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
}
/// <summary>
/// Single-writer state behind <see cref="IWorldGenerationAvailability"/>.
/// 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.
/// </summary>
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);
/// <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>
/// 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.
/// </summary>
internal sealed class WorldGenerationQuiescence
{
private readonly WorldGenerationAvailabilityState _availability;
private readonly SelectionState _selection;
private readonly GpuWorldState _world;
private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey;
private readonly IWorldAudioQuiescence? _audio;
private bool _effectsQuiesced;
public WorldGenerationQuiescence(
WorldGenerationAvailabilityState availability,
SelectionState selection,
GpuWorldState world,
Func<uint, RuntimeEntityKey?> 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)
/// <summary>
/// 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.
/// </summary>
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();
}
}

View file

@ -1,9 +1,13 @@
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming;
/// <summary>
/// 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.
/// </summary>
internal interface IWorldRevealStreamingScheduler
{
@ -26,23 +30,23 @@ internal interface IWorldRevealRenderResourceScheduler
}
/// <summary>
/// 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.
/// </summary>
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<uint, int, bool> isRenderNeighborhoodReady,
Func<uint, bool> isSpawnCellReady,
Func<uint, int, bool> isTerrainNeighborhoodReady,
@ -50,11 +54,11 @@ internal sealed class WorldRevealCoordinator
Action<uint, int> prepareCompositeTextures,
Action invalidateCompositeTextures,
Func<uint, bool> isSpawnClaimUnhydratable,
Action<string>? 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
/// </remarks>
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);
}
/// <summary>
/// Releases the destination streaming/resource reservation at retail's
@ -140,7 +173,7 @@ internal sealed class WorldRevealCoordinator
/// </summary>
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();
}
}

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.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,

View file

@ -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
{

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.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,

View file

@ -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,

View file

@ -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; }

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.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);

View file

@ -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,

View file

@ -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,

View file

@ -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<string>? 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<string>(logs.Add),
streaming: streaming,
renderResources: renderResources);
IWorldRevealRenderResourceScheduler? renderResources = null,
RuntimeWorldTransitState? transit = null)
{
transit ??= new RuntimeWorldTransitState(
logs is null ? null : new Action<string>(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);

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.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,

View file

@ -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<string>();
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<string>();
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),

View file

@ -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<string>();
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);

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);
}