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