From 5ef8b5371d8990f0380acd939e71cd711289d429 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Jul 2026 05:54:46 +0200 Subject: [PATCH] refactor(runtime): own canonical entity and object lifetime Introduce one presentation-free RuntimeEntityObjectLifetime for the exact entity directory and ClientObjectTable. Make GameWindow, graphical projections, retained UI, interaction, session routing, create/delete integration, and reset borrow that owner while preserving synchronous retail ordering, dormant retention, and retry semantics. Co-authored-by: Codex --- .../LivePresentationComposition.cs | 12 +- .../Composition/SessionPlayerComposition.cs | 17 +- .../Net/LiveSessionRuntimeFactory.cs | 11 +- src/AcDream.App/Rendering/GameWindow.cs | 9 +- .../World/LiveEntityDeletionController.cs | 14 +- .../World/LiveEntityHydrationController.cs | 18 +- src/AcDream.App/World/LiveEntityRuntime.cs | 34 ++- src/AcDream.Core.Net/ObjectTableWiring.cs | 3 +- .../Entities/RuntimeEntityObjectLifetime.cs | 135 ++++++++++ .../Combat/CombatAttackTargetSourceTests.cs | 2 +- .../LivePresentationCompositionTests.cs | 2 +- .../Interaction/WorldSelectionQueryTests.cs | 2 +- .../LiveEntityRuntimeFixture.cs | 50 ++++ .../Net/LiveSessionResetPlanTests.cs | 2 +- .../LiveEntityInboundAuthorityGateTests.cs | 2 +- .../LiveEntityOrdinaryPhysicsUpdaterTests.cs | 4 +- .../Physics/ProjectileControllerTests.cs | 2 +- .../Physics/RemotePhysicsUpdaterTests.cs | 4 +- .../Physics/RemoteTeleportControllerTests.cs | 12 +- .../EquippedChildProjectionWithdrawalTests.cs | 2 +- .../Rendering/LiveAppearanceAnimationTests.cs | 4 +- .../LiveEntityAnimationPresenterTests.cs | 2 +- .../LiveEntityAnimationSchedulerTests.cs | 4 +- ...veEntityCreateSupersessionRecoveryTests.cs | 2 +- .../LiveRenderProjectionJournalTests.cs | 2 +- .../Vfx/EntityEffectControllerTests.cs | 2 +- .../Vfx/LiveEntityLightControllerTests.cs | 2 +- .../Runtime/CurrentGameRuntimeAdapterTests.cs | 2 +- .../Streaming/GpuWorldStateVisibilityTests.cs | 4 +- .../LocalPlayerTeleportControllerTests.cs | 4 +- .../StreamingFrameControllerTests.cs | 4 +- ...iveEntityRuntimeComponentLifecycleTests.cs | 4 +- .../LiveEntityHydrationControllerTests.cs | 10 +- .../World/LiveEntityLifecycleStressTests.cs | 4 +- .../LiveEntityPhysicsHostOwnershipTests.cs | 4 +- .../LiveEntityPresentationControllerTests.cs | 6 +- ...tityProjectionWithdrawalControllerTests.cs | 2 +- .../World/LiveEntityRuntimeTests.cs | 152 +++++------ .../World/RuntimeEntityOwnershipTests.cs | 80 +++++- .../RuntimeEntityObjectLifetimeTests.cs | 251 ++++++++++++++++++ 40 files changed, 712 insertions(+), 170 deletions(-) create mode 100644 src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs create mode 100644 tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs create mode 100644 tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index 1264f61c..d48db564 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -25,6 +25,7 @@ using AcDream.Core.Rendering; using AcDream.Core.Selection; using AcDream.Core.Vfx; using AcDream.Core.World; +using AcDream.Runtime.Entities; using DatReaderWriter; using DatReaderWriter.DBObjs; using Silk.NET.OpenGL; @@ -44,7 +45,7 @@ internal sealed record LivePresentationDependencies( WorldGameState WorldGameState, WorldEvents WorldEvents, SelectionState Selection, - ClientObjectTable Objects, + RuntimeEntityObjectLifetime EntityObjects, LiveEntityRuntimeSlot RuntimeSlot, DeferredLiveEntityMotionRuntimeBindings MotionBindings, DeferredEntityEffectAdvanceSource EffectAdvance, @@ -377,7 +378,8 @@ internal sealed class LivePresentationCompositionPhase worldState, new CompositeLiveEntityResourceLifecycle( [.. resourceOwners]), - componentLifecycle); + componentLifecycle, + d.EntityObjects); var liveRuntimeLease = scope.Own( "canonical live-entity runtime", liveEntities, @@ -487,7 +489,7 @@ internal sealed class LivePresentationCompositionPhase () => new EquippedChildRenderController( content.Dats, d.DatLock, - d.Objects, + d.EntityObjects.Objects, liveEntities, d.EffectPoses, parentAcceptance.TryAccept, @@ -675,7 +677,7 @@ internal sealed class LivePresentationCompositionPhase static value => value.Dispose()); var selectionQuery = new WorldSelectionQuery( liveEntities, - d.Objects, + d.EntityObjects.Objects, selectionScene, () => d.PlayerIdentity.ServerGuid, interaction.LateBindings.SelectionCamera.Snapshot, @@ -699,7 +701,7 @@ internal sealed class LivePresentationCompositionPhase } }); var radarSnapshotProvider = new RadarSnapshotProvider( - d.Objects, + d.EntityObjects.Objects, liveEntities, () => liveEntities.Snapshots, playerGuid: () => d.PlayerIdentity.ServerGuid, diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 925b890b..4e3fb595 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -26,6 +26,7 @@ using AcDream.Core.Player; using AcDream.Core.Social; using AcDream.Core.Spells; using AcDream.Core.World; +using AcDream.Runtime.Entities; using AcDream.Runtime.Session; using Silk.NET.Windowing; @@ -47,7 +48,7 @@ internal sealed record SessionPlayerDependencies( WorldGameState WorldGameState, WorldEvents WorldEvents, SelectionState Selection, - ClientObjectTable Objects, + RuntimeEntityObjectLifetime EntityObjects, EntityClassificationCache ClassificationCache, LiveEntityRuntimeSlot RuntimeSlot, LiveEntityAnimationRuntimeView AnimatedEntities, @@ -475,14 +476,14 @@ internal sealed class SessionPlayerCompositionPhase var dormantLiveEntities = new DormantLiveEntityStore(); var deletion = new LiveEntityDeletionController( live.LiveEntities, - d.Objects, + d.EntityObjects, teardown, d.PlayerIdentity, dormantLiveEntities, d.Options.DumpLiveSpawns ? d.Log : null); var hydration = new LiveEntityHydrationController( live.LiveEntities, - d.Objects, + d.EntityObjects, d.DatLock, projectionMaterializer, new LiveEntityRelationshipProjection(live.EquippedChildren), @@ -505,7 +506,7 @@ internal sealed class SessionPlayerCompositionPhase var networkUpdates = new LiveEntityNetworkUpdateController( live.LiveEntities, - d.Objects, + d.EntityObjects.Objects, hydration, live.EntityEffects, live.Presentation, @@ -574,7 +575,7 @@ internal sealed class SessionPlayerCompositionPhase new CombatAttackTargetSource( d.Selection, live.LiveEntities, - d.Objects, + d.EntityObjects.Objects, d.PlayerIdentity), d.Settings, d.PlayerController, @@ -809,7 +810,7 @@ internal sealed class SessionPlayerCompositionPhase d.Shortcuts, d.DesiredComponents), new LiveSessionDomainRuntime( - d.Objects, + d.EntityObjects, d.LocalPlayer, d.Spellbook, d.Combat, @@ -873,7 +874,7 @@ internal sealed class SessionPlayerCompositionPhase var combatCommand = new LiveCombatModeCommandController( new LiveSessionCombatModeAuthority(sessionHost), new LocalPlayerCombatEquipmentSource( - d.Objects, + d.EntityObjects.Objects, d.PlayerIdentity), d.Combat, new ItemInteractionCombatModeIntentSink( @@ -890,7 +891,7 @@ internal sealed class SessionPlayerCompositionPhase liveSessionCommands, d.PlayerIdentity, live.LiveEntities, - d.Objects, + d.EntityObjects.Objects, d.Chat, d.PlayerController, worldReveal, diff --git a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs index c0bbd214..86ab9055 100644 --- a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs +++ b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs @@ -23,6 +23,7 @@ using AcDream.Core.Social; using AcDream.Core.Spells; using AcDream.Core.World; using AcDream.Content; +using AcDream.Runtime.Entities; using AcDream.Runtime.Session; using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Vitals; @@ -41,7 +42,7 @@ internal sealed record LiveSessionPlayerRuntime( DesiredComponentSnapshotState DesiredComponents); internal sealed record LiveSessionDomainRuntime( - ClientObjectTable Objects, + RuntimeEntityObjectLifetime EntityObjects, LocalPlayerState LocalPlayer, Spellbook Spellbook, CombatState Combat, @@ -173,7 +174,7 @@ internal sealed class LiveSessionRuntimeFactory ExternalContainer = () => _domain.ExternalContainers.Reset(), InteractionAndSelection = _interaction.SelectionInteractions.ResetSession, SelectionPresentation = _world.SelectionScene.Reset, - ObjectTable = _domain.Objects.Clear, + ObjectTable = _domain.EntityObjects.ClearObjects, Spellbook = _domain.Spellbook.Clear, MagicRuntime = () => _ui.Magic?.Reset(), CombatAttack = _interaction.CombatAttack.ResetSession, @@ -260,7 +261,7 @@ internal sealed class LiveSessionRuntimeFactory } private LiveInventorySessionBindings CreateInventoryBindings() => new( - _domain.Objects, + _domain.EntityObjects.Objects, _domain.LocalPlayer, PlayerGuid: () => _player.Identity.ServerGuid, OnShortcuts: list => _player.Shortcuts.Items = list, @@ -333,7 +334,7 @@ internal sealed class LiveSessionRuntimeFactory ShowSystemMessage: text => _domain.Chat.OnSystemMessage(text, 0u), ShowWeenieError: code => _domain.Chat.OnWeenieError(code, null), PlayerPublicWeenieBitfield: () => - _domain.Objects.Get(_player.Identity.ServerGuid)? + _domain.EntityObjects.Objects.Get(_player.Identity.ServerGuid)? .PublicWeenieBitfield, ClientVersion: () => typeof(LiveSessionRuntimeFactory).Assembly @@ -351,7 +352,7 @@ internal sealed class LiveSessionRuntimeFactory SaveAutoUi: () => _ui.RetailUi?.SaveLayout(), LoadAutoUi: () => _ui.RetailUi?.RestoreLayout(), IsAway: () => - _domain.Objects.Get(_player.Identity.ServerGuid)? + _domain.EntityObjects.Objects.Get(_player.Identity.ServerGuid)? .Properties.GetBool(0x6Eu) == true, SetAway: session.SendSetAfkMode, SetAwayMessage: session.SendSetAfkMessage, diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d392e5bd..ea445e0c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -6,6 +6,7 @@ using AcDream.App.Rendering.Wb; using AcDream.App.Settings; using AcDream.App.World; using AcDream.Content; +using AcDream.Runtime.Entities; using AcDream.Runtime.Session; using DatReaderWriter; using Silk.NET.Input; @@ -340,7 +341,7 @@ public sealed class GameWindow : // installs that immutable table once DatCollection opens in OnLoad. public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata; public readonly AcDream.Core.Spells.Spellbook SpellBook = null!; - public readonly AcDream.Core.Items.ClientObjectTable Objects = new(); + private readonly RuntimeEntityObjectLifetime _runtimeEntityObjects = new(); /// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source). private readonly ShortcutSnapshotState _shortcutSnapshots = new(); public IReadOnlyList Shortcuts @@ -1260,7 +1261,7 @@ public sealed class GameWindow : _combatAttackOperations, _selection, _externalContainers, - Objects, + _runtimeEntityObjects.Objects, contentEffectsAudio.MagicCatalog, SpellBook, Chat, @@ -1314,7 +1315,7 @@ public sealed class GameWindow : _worldGameState, _worldEvents, _selection, - Objects, + _runtimeEntityObjects, _liveEntityRuntimeSlot, _liveEntityMotionBindings, _entityEffectAdvance, @@ -1372,7 +1373,7 @@ public sealed class GameWindow : _worldGameState, _worldEvents, _selection, - Objects, + _runtimeEntityObjects, _classificationCache, _liveEntityRuntimeSlot, _animatedEntities, diff --git a/src/AcDream.App/World/LiveEntityDeletionController.cs b/src/AcDream.App/World/LiveEntityDeletionController.cs index 3dfebffa..bebeecfd 100644 --- a/src/AcDream.App/World/LiveEntityDeletionController.cs +++ b/src/AcDream.App/World/LiveEntityDeletionController.cs @@ -1,7 +1,7 @@ using AcDream.App.Input; -using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; +using AcDream.Runtime.Entities; namespace AcDream.App.World; @@ -17,7 +17,7 @@ internal interface ILiveEntityPruneSink internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink { private readonly LiveEntityRuntime _runtime; - private readonly ClientObjectTable _objects; + private readonly RuntimeEntityObjectLifetime _entityObjects; private readonly ILiveEntityTeardownCoordinator _teardown; private readonly ILocalPlayerIdentitySource _identity; private readonly DormantLiveEntityStore _dormant; @@ -25,14 +25,15 @@ internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink public LiveEntityDeletionController( LiveEntityRuntime runtime, - ClientObjectTable objects, + RuntimeEntityObjectLifetime entityObjects, ILiveEntityTeardownCoordinator teardown, ILocalPlayerIdentitySource identity, DormantLiveEntityStore? dormant = null, Action? diagnostic = null) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); - _objects = objects ?? throw new ArgumentNullException(nameof(objects)); + _entityObjects = entityObjects + ?? throw new ArgumentNullException(nameof(entityObjects)); _teardown = teardown ?? throw new ArgumentNullException(nameof(teardown)); _identity = identity ?? throw new ArgumentNullException(nameof(identity)); _dormant = dormant ?? new DormantLiveEntityStore(); @@ -56,11 +57,10 @@ internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink bool removed = _runtime.UnregisterLiveEntity( delete, isLocalPlayer: false, - beforeTeardown: () => - ObjectTableWiring.ApplyEntityDelete(_objects, delete)); + removeRetainedObject: true); bool removedDormant = _dormant.RemoveExact(delete); if (!removed && removedDormant) - ObjectTableWiring.ApplyEntityDelete(_objects, delete); + _entityObjects.ApplyAcceptedDormantDelete(delete); if (removed) { _dormant.RemoveExact(delete); diff --git a/src/AcDream.App/World/LiveEntityHydrationController.cs b/src/AcDream.App/World/LiveEntityHydrationController.cs index 2ea70f61..299d9b7b 100644 --- a/src/AcDream.App/World/LiveEntityHydrationController.cs +++ b/src/AcDream.App/World/LiveEntityHydrationController.cs @@ -1,6 +1,5 @@ using AcDream.App.Rendering; using AcDream.App.Input; -using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.World; @@ -169,7 +168,7 @@ internal interface ILiveEntityWorldOriginCoordinator internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoadedSink { private readonly LiveEntityRuntime _runtime; - private readonly ClientObjectTable _objects; + private readonly RuntimeEntityObjectLifetime _entityObjects; private readonly object _datLock; private readonly ILiveEntityProjectionMaterializer _materializer; private readonly ILiveEntityRelationshipProjection _relationships; @@ -193,7 +192,7 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded public LiveEntityHydrationController( LiveEntityRuntime runtime, - ClientObjectTable objects, + RuntimeEntityObjectLifetime entityObjects, object datLock, ILiveEntityProjectionMaterializer materializer, ILiveEntityRelationshipProjection relationships, @@ -207,7 +206,8 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded Action? diagnostic = null) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); - _objects = objects ?? throw new ArgumentNullException(nameof(objects)); + _entityObjects = entityObjects + ?? throw new ArgumentNullException(nameof(entityObjects)); _datLock = datLock ?? throw new ArgumentNullException(nameof(datLock)); _materializer = materializer ?? throw new ArgumentNullException(nameof(materializer)); _relationships = relationships ?? throw new ArgumentNullException(nameof(relationships)); @@ -285,15 +285,13 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded if (_runtime.IsCurrentCreateIntegration( canonical, createIntegrationVersion) - && ObjectTableWiring.ApplyEntitySpawn( - _objects, + && _entityObjects.ApplyAcceptedSpawn( + canonical, + createIntegrationVersion, spawn, replaceGeneration: result.Disposition is AcDream.Core.Physics.CreateObjectTimestampDisposition.NewGeneration - || dormantDisposition is DormantCreateDisposition.NewGeneration, - accepting: () => _runtime.IsCurrentCreateIntegration( - canonical, - createIntegrationVersion)) + || dormantDisposition is DormantCreateDisposition.NewGeneration) && _runtime.IsCurrentCreateIntegration( canonical, createIntegrationVersion)) diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs index c3a5e183..4da5f3fc 100644 --- a/src/AcDream.App/World/LiveEntityRuntime.cs +++ b/src/AcDream.App/World/LiveEntityRuntime.cs @@ -448,6 +448,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource private readonly GpuWorldState _spatial; private readonly ILiveEntityResourceLifecycle _resources; private readonly ILiveEntityRuntimeComponentLifecycle _runtimeComponentLifecycle; + private readonly RuntimeEntityObjectLifetime _entityObjects; private readonly RuntimeEntityDirectory _directory; private readonly LiveEntityProjectionStore _projections; // Logical components survive retail's 25-second leave-visibility lifetime. @@ -471,12 +472,12 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource public LiveEntityRuntime( GpuWorldState spatial, ILiveEntityResourceLifecycle resources, - uint firstLocalEntityId = FirstLiveEntityId) + RuntimeEntityObjectLifetime entityObjects) : this( spatial, resources, NullLiveEntityRuntimeComponentLifecycle.Instance, - firstLocalEntityId) + entityObjects) { } @@ -484,12 +485,12 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource GpuWorldState spatial, ILiveEntityResourceLifecycle resources, Action tearDownRuntimeComponents, - uint firstLocalEntityId = FirstLiveEntityId) + RuntimeEntityObjectLifetime entityObjects) : this( spatial, resources, new DelegateLiveEntityRuntimeComponentLifecycle(tearDownRuntimeComponents), - firstLocalEntityId) + entityObjects) { } @@ -497,13 +498,15 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource GpuWorldState spatial, ILiveEntityResourceLifecycle resources, ILiveEntityRuntimeComponentLifecycle? runtimeComponentLifecycle, - uint firstLocalEntityId = FirstLiveEntityId) + RuntimeEntityObjectLifetime entityObjects) { _spatial = spatial ?? throw new ArgumentNullException(nameof(spatial)); _resources = resources ?? throw new ArgumentNullException(nameof(resources)); _runtimeComponentLifecycle = runtimeComponentLifecycle ?? throw new ArgumentNullException(nameof(runtimeComponentLifecycle)); - _directory = new RuntimeEntityDirectory(firstLocalEntityId); + _entityObjects = entityObjects + ?? throw new ArgumentNullException(nameof(entityObjects)); + _directory = _entityObjects.Entities; _projections = new LiveEntityProjectionStore(_directory); _spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged; } @@ -1187,6 +1190,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource public bool UnregisterLiveEntity( DeleteObject.Parsed delete, bool isLocalPlayer, + bool removeRetainedObject = false, Action? beforeTeardown = null) { if (_isRegisteringResources) @@ -1208,9 +1212,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource && (record.DeleteAcceptedForTeardown || _isClearing || _sessionClearPendingFinalization); + RuntimeEntityDeleteAcceptance? acceptedDelete = null; if (!retryingAcceptedDelete) { - if (!_directory.TryDelete(delete, isLocalPlayer)) + if (!_entityObjects.TryAcceptDelete( + delete, + isLocalPlayer, + out acceptedDelete)) return false; AdvanceLifetimeMutation(delete.Guid); ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence); @@ -1258,6 +1266,18 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource { if (!retryingAcceptedDelete) { + if (removeRetainedObject) + { + try + { + _entityObjects.CompleteAcceptedDelete(acceptedDelete!); + } + catch (Exception error) + { + (failures ??= new List()).Add(error); + } + } + try { beforeTeardown?.Invoke(); diff --git a/src/AcDream.Core.Net/ObjectTableWiring.cs b/src/AcDream.Core.Net/ObjectTableWiring.cs index b1522c93..40373211 100644 --- a/src/AcDream.Core.Net/ObjectTableWiring.cs +++ b/src/AcDream.Core.Net/ObjectTableWiring.cs @@ -10,7 +10,8 @@ namespace AcDream.Core.Net; /// 64-bit qualities (experience), SetStackSize (0x0197) = stack count, /// InventoryRemoveObject (0x0024) = inventory-view removal. /// CreateObject/DeleteObject application remains exposed as pure helpers so -/// the App live-entity owner can freshness-gate the shared projections first. +/// the Runtime entity/object lifetime owner can freshness-gate the canonical +/// incarnation before either graphical or retained-object projections mutate. /// Retail: ACCObjectMaint::CreateObject / DeleteObject (the weenie_object_table side). /// public static class ObjectTableWiring diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs new file mode 100644 index 00000000..825f51d4 --- /dev/null +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs @@ -0,0 +1,135 @@ +using AcDream.Core.Items; +using AcDream.Core.Net; +using AcDream.Core.Net.Messages; + +namespace AcDream.Runtime.Entities; + +/// +/// One exact DeleteObject acceptance issued by a +/// . The graphical host retires the +/// active identity before returning this token to the Runtime owner for the +/// retained-object mutation, preserving retail callback order. +/// +public sealed class RuntimeEntityDeleteAcceptance +{ + internal RuntimeEntityDeleteAcceptance( + RuntimeEntityObjectLifetime owner, + DeleteObject.Parsed delete) + { + Owner = owner; + Delete = delete; + } + + internal RuntimeEntityObjectLifetime Owner { get; } + internal bool Completed { get; set; } + public DeleteObject.Parsed Delete { get; } +} + +/// +/// Presentation-free ownership root for one session's canonical entity and +/// retained object lifetimes. Graphical and direct hosts borrow these exact +/// instances; they never allocate a second directory or object table. +/// +public sealed class RuntimeEntityObjectLifetime +{ + public RuntimeEntityObjectLifetime( + uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId) + { + Entities = new RuntimeEntityDirectory(firstLocalEntityId); + Objects = new ClientObjectTable(); + } + + public RuntimeEntityDirectory Entities { get; } + public ClientObjectTable Objects { get; } + + /// + /// Applies the retained-object half of an accepted CreateObject while the + /// exact canonical incarnation and its integration version remain current. + /// Object-table callbacks are synchronous and may re-enter entity lifetime, + /// so the acceptance predicate is checked before, during, and after apply. + /// + public bool ApplyAcceptedSpawn( + RuntimeEntityRecord canonical, + ulong expectedCreateIntegrationVersion, + WorldSession.EntitySpawn spawn, + bool replaceGeneration) + { + ArgumentNullException.ThrowIfNull(canonical); + if (canonical.ServerGuid != spawn.Guid + || canonical.Incarnation != spawn.InstanceSequence) + { + return false; + } + + bool IsCurrent() => + Entities.IsCurrent(canonical) + && canonical.CreateIntegrationVersion + == expectedCreateIntegrationVersion; + + return IsCurrent() + && ObjectTableWiring.ApplyEntitySpawn( + Objects, + spawn, + replaceGeneration, + IsCurrent) + && IsCurrent(); + } + + /// + /// Accepts the exact DeleteObject generation without yet publishing the + /// retained-object removal. This split lets the graphical host retire the + /// active identity first, matching the established reentrant callback + /// order, while Runtime remains the only freshness authority. + /// + public bool TryAcceptDelete( + DeleteObject.Parsed delete, + bool isLocalPlayer, + out RuntimeEntityDeleteAcceptance acceptance) + { + if (!Entities.TryDelete(delete, isLocalPlayer)) + { + acceptance = null!; + return false; + } + + acceptance = new RuntimeEntityDeleteAcceptance(this, delete); + return true; + } + + /// + /// Completes a previously accepted delete exactly once. Marking the token + /// complete before synchronous callbacks preserves the current retry rule: + /// a throwing observer does not replay an already-committed removal. + /// + public void CompleteAcceptedDelete( + RuntimeEntityDeleteAcceptance acceptance) + { + ArgumentNullException.ThrowIfNull(acceptance); + if (!ReferenceEquals(acceptance.Owner, this)) + { + throw new InvalidOperationException( + "A delete acceptance belongs to a different Runtime lifetime."); + } + if (acceptance.Completed) + { + throw new InvalidOperationException( + "A delete acceptance has already been completed."); + } + + acceptance.Completed = true; + ObjectTableWiring.ApplyEntityDelete(Objects, acceptance.Delete); + } + + /// + /// Applies a delete accepted by the dormant exact-incarnation owner after + /// the active Runtime directory correctly reports no live record. + /// + public void ApplyAcceptedDormantDelete(DeleteObject.Parsed delete) => + ObjectTableWiring.ApplyEntityDelete(Objects, delete); + + /// + /// Clears retained object state at the Runtime-owned reset stage. App + /// projection teardown remains a later acknowledged stage. + /// + public void ClearObjects() => Objects.Clear(); +} diff --git a/tests/AcDream.App.Tests/Combat/CombatAttackTargetSourceTests.cs b/tests/AcDream.App.Tests/Combat/CombatAttackTargetSourceTests.cs index 80c3b5ea..d211e9a3 100644 --- a/tests/AcDream.App.Tests/Combat/CombatAttackTargetSourceTests.cs +++ b/tests/AcDream.App.Tests/Combat/CombatAttackTargetSourceTests.cs @@ -97,7 +97,7 @@ public sealed class CombatAttackTargetSourceTests 0x0101_FFFFu, new LandBlock(), Array.Empty())); - Runtime = new LiveEntityRuntime(spatial, new Resources()); + Runtime = LiveEntityRuntimeFixture.Create(spatial, new Resources()); var identity = new LocalPlayerIdentityState { ServerGuid = Player }; Targets = new CombatAttackTargetSource( Selection, diff --git a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs index 974e7463..71e25652 100644 --- a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs @@ -150,7 +150,7 @@ public sealed class LivePresentationCompositionTests Assert.Contains("d.PortalTunnelFallback.AcquirePrepared(", phase); } - private static LiveEntityRuntime Runtime() => new( + private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(static _ => { }, static _ => { })); diff --git a/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs b/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs index cf12a08c..e535f187 100644 --- a/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs +++ b/tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs @@ -50,7 +50,7 @@ public sealed class WorldSelectionQueryTests 0x0101_FFFFu, new LandBlock(), Array.Empty())); - Runtime = new LiveEntityRuntime(spatial, new Resources()); + Runtime = LiveEntityRuntimeFixture.Create(spatial, new Resources()); Scene = new RetailSelectionScene(new GeometrySource(Mesh())); Query = new WorldSelectionQuery( Runtime, diff --git a/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs b/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs new file mode 100644 index 00000000..0031c108 --- /dev/null +++ b/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs @@ -0,0 +1,50 @@ +using AcDream.App.Streaming; +using AcDream.App.World; +using AcDream.Runtime.Entities; + +namespace AcDream.App.Tests; + +/// +/// Explicit isolated lifetime-group seam for App projection tests. Production +/// composition supplies its session-owned group from GameWindow; focused tests +/// use a fresh group per runtime so identity never leaks between fixtures. +/// +internal static class LiveEntityRuntimeFixture +{ + public static LiveEntityRuntime Create( + GpuWorldState spatial, + ILiveEntityResourceLifecycle resources, + uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId) + { + var lifetime = new RuntimeEntityObjectLifetime(firstLocalEntityId); + return new LiveEntityRuntime(spatial, resources, lifetime); + } + + public static LiveEntityRuntime Create( + GpuWorldState spatial, + ILiveEntityResourceLifecycle resources, + Action tearDownRuntimeComponents, + uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId) + { + var lifetime = new RuntimeEntityObjectLifetime(firstLocalEntityId); + return new LiveEntityRuntime( + spatial, + resources, + tearDownRuntimeComponents, + lifetime); + } + + public static LiveEntityRuntime Create( + GpuWorldState spatial, + ILiveEntityResourceLifecycle resources, + ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle, + uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId) + { + var lifetime = new RuntimeEntityObjectLifetime(firstLocalEntityId); + return new LiveEntityRuntime( + spatial, + resources, + runtimeComponentLifecycle, + lifetime); + } +} diff --git a/tests/AcDream.App.Tests/Net/LiveSessionResetPlanTests.cs b/tests/AcDream.App.Tests/Net/LiveSessionResetPlanTests.cs index 38b24dd1..4e2cf531 100644 --- a/tests/AcDream.App.Tests/Net/LiveSessionResetPlanTests.cs +++ b/tests/AcDream.App.Tests/Net/LiveSessionResetPlanTests.cs @@ -210,7 +210,7 @@ public sealed class LiveSessionResetPlanTests landblock, new LandBlock(), new List { staticEntity })); - var live = new LiveEntityRuntime(spatial, new FailingOnceResources()); + var live = LiveEntityRuntimeFixture.Create(spatial, new FailingOnceResources()); WorldSession.EntitySpawn spawn = Spawn(playerA, 1, 1, 0x01010001u); live.RegisterLiveEntity(spawn); live.MaterializeLiveEntity( diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityInboundAuthorityGateTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityInboundAuthorityGateTests.cs index 431d285a..bd233ab7 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityInboundAuthorityGateTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityInboundAuthorityGateTests.cs @@ -315,7 +315,7 @@ public sealed class LiveEntityInboundAuthorityGateTests accepted.VelocityAuthorityVersion)); } - private static LiveEntityRuntime Runtime() => new( + private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); diff --git a/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs b/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs index 29b11900..e878ab06 100644 --- a/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs +++ b/tests/AcDream.App.Tests/Physics/LiveEntityOrdinaryPhysicsUpdaterTests.cs @@ -23,7 +23,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu)); - var live = new LiveEntityRuntime( + var live = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); LiveEntityRecord record = live.RegisterAndMaterializeProjection( @@ -90,7 +90,7 @@ public sealed class LiveEntityOrdinaryPhysicsUpdaterTests { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); - var live = new LiveEntityRuntime( + var live = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); LiveEntityRecord record = live.RegisterAndMaterializeProjection( diff --git a/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs b/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs index ae1e64d8..a49444bb 100644 --- a/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs +++ b/tests/AcDream.App.Tests/Physics/ProjectileControllerTests.cs @@ -1457,7 +1457,7 @@ public sealed class ProjectileControllerTests { if (loadInitialLandblock) Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - Live = new LiveEntityRuntime(Spatial, Resources); + Live = LiveEntityRuntimeFixture.Create(Spatial, Resources); Engine = physicsEngine ?? new PhysicsEngine(); Origin.Recenter(1, 1); Controller = new ProjectileController( diff --git a/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs b/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs index 735a95f2..e96d5da3 100644 --- a/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs +++ b/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs @@ -692,7 +692,7 @@ public sealed class RemotePhysicsUpdaterTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - var live = new LiveEntityRuntime( + var live = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); BindHiddenRemote(live, firstGuid); @@ -840,7 +840,7 @@ public sealed class RemotePhysicsUpdaterTests 0xA9B4FFFFu, new LandBlock(), Array.Empty())); - Live = new LiveEntityRuntime( + Live = LiveEntityRuntimeFixture.Create( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), record => diff --git a/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs b/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs index 0c1e0062..a7a467f2 100644 --- a/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs +++ b/tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs @@ -87,7 +87,7 @@ public sealed class RemoteTeleportControllerTests const uint destinationCell = 0xAAB40001u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); - var live = new LiveEntityRuntime(spatial, new Resources()); + var live = LiveEntityRuntimeFixture.Create(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, @@ -228,7 +228,7 @@ public sealed class RemoteTeleportControllerTests const uint sourceCell = 0xA9B40039u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); - var live = new LiveEntityRuntime(spatial, new Resources()); + var live = LiveEntityRuntimeFixture.Create(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, @@ -319,7 +319,7 @@ public sealed class RemoteTeleportControllerTests Vector3 sourcePosition = new(191f, 10f, 50f); var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); - var live = new LiveEntityRuntime(spatial, new Resources()); + var live = LiveEntityRuntimeFixture.Create(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, @@ -1101,7 +1101,7 @@ public sealed class RemoteTeleportControllerTests ])); var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); - var live = new LiveEntityRuntime(spatial, resources); + var live = LiveEntityRuntimeFixture.Create(spatial, resources); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, @@ -1179,7 +1179,7 @@ public sealed class RemoteTeleportControllerTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0xA9B4FFFFu)); spatial.AddLandblock(EmptyLandblock(0xAAB4FFFFu)); - var live = new LiveEntityRuntime(spatial, new Resources()); + var live = LiveEntityRuntimeFixture.Create(spatial, new Resources()); live.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = live.MaterializeLiveEntity( guid, @@ -1383,7 +1383,7 @@ public sealed class RemoteTeleportControllerTests internal RollbackFixture(bool celllessSource = false) { Spatial.AddLandblock(EmptyLandblock(SourceLandblock)); - Live = new LiveEntityRuntime(Spatial, new Resources()); + Live = LiveEntityRuntimeFixture.Create(Spatial, new Resources()); Live.RegisterLiveEntity(Spawn(Guid, SourceCell)); Entity = Live.MaterializeLiveEntity( Guid, diff --git a/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs b/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs index 2f550002..f5318006 100644 --- a/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs @@ -1161,7 +1161,7 @@ public sealed class EquippedChildProjectionWithdrawalTests (Cell & 0xFFFF0000u) | 0xFFFFu, new LandBlock(), Array.Empty())); - Live = new LiveEntityRuntime( + Live = LiveEntityRuntimeFixture.Create( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), _lifecycle); diff --git a/tests/AcDream.App.Tests/Rendering/LiveAppearanceAnimationTests.cs b/tests/AcDream.App.Tests/Rendering/LiveAppearanceAnimationTests.cs index b1938e82..29039d38 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveAppearanceAnimationTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveAppearanceAnimationTests.cs @@ -17,7 +17,7 @@ public sealed class LiveAppearanceAnimationTests public void Capture_UsesCanonicalRecordEntityAndAnimationOwner() { const uint guid = 0x70000002u; - var runtime = new AcDream.App.World.LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( new AcDream.App.Streaming.GpuWorldState(), new AcDream.App.World.DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); var spawn = new AcDream.Core.Net.WorldSession.EntitySpawn( @@ -118,7 +118,7 @@ public sealed class LiveAppearanceAnimationTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); WorldSession.EntitySpawn first = Spawn(guid, cell, instance: 1); diff --git a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs index 97abac62..89478c1d 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationPresenterTests.cs @@ -440,7 +440,7 @@ public sealed class LiveEntityAnimationPresenterTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - var live = new LiveEntityRuntime( + var live = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); return Add(live, guid, partCount, scale, withSequencer); diff --git a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs index ad94b17f..3e76f176 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveEntityAnimationSchedulerTests.cs @@ -679,7 +679,7 @@ public sealed class LiveEntityAnimationSchedulerTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - var live = new LiveEntityRuntime( + var live = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); LiveEntityRecord record = live.RegisterAndMaterializeProjection( @@ -800,7 +800,7 @@ public sealed class LiveEntityAnimationSchedulerTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - var live = new LiveEntityRuntime( + var live = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); LiveEntityRecord record = live.RegisterAndMaterializeProjection( diff --git a/tests/AcDream.App.Tests/Rendering/LiveEntityCreateSupersessionRecoveryTests.cs b/tests/AcDream.App.Tests/Rendering/LiveEntityCreateSupersessionRecoveryTests.cs index 3c3bbf7f..0a9f6753 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveEntityCreateSupersessionRecoveryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveEntityCreateSupersessionRecoveryTests.cs @@ -286,7 +286,7 @@ public sealed class LiveEntityCreateSupersessionRecoveryTests 0x0101FFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); - return new LiveEntityRuntime( + return LiveEntityRuntimeFixture.Create( spatial, resources, new DelegateLiveEntityRuntimeComponentLifecycle(_ => { })); diff --git a/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs b/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs index 260381de..be45f841 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs @@ -357,7 +357,7 @@ public sealed class LiveRenderProjectionJournalTests var resources = new DelegateLiveEntityResourceLifecycle( static _ => { }, entity => sink?.OnResourceUnregister(entity)); - var runtime = new LiveEntityRuntime(state, resources); + var runtime = LiveEntityRuntimeFixture.Create(state, resources); var journal = new RenderProjectionJournal( RenderSceneGeneration.FromRaw(1)); var projections = new LiveRenderProjectionJournal( diff --git a/tests/AcDream.App.Tests/Rendering/Vfx/EntityEffectControllerTests.cs b/tests/AcDream.App.Tests/Rendering/Vfx/EntityEffectControllerTests.cs index f9ce6571..32ed7f8b 100644 --- a/tests/AcDream.App.Tests/Rendering/Vfx/EntityEffectControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Vfx/EntityEffectControllerTests.cs @@ -55,7 +55,7 @@ public sealed class EntityEffectControllerTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - Runtime = new LiveEntityRuntime( + Runtime = LiveEntityRuntimeFixture.Create( Spatial, new NoopResources(), record => _controller?.OnLiveEntityUnregistered(record)); diff --git a/tests/AcDream.App.Tests/Rendering/Vfx/LiveEntityLightControllerTests.cs b/tests/AcDream.App.Tests/Rendering/Vfx/LiveEntityLightControllerTests.cs index c76039fa..3cf160e3 100644 --- a/tests/AcDream.App.Tests/Rendering/Vfx/LiveEntityLightControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Vfx/LiveEntityLightControllerTests.cs @@ -151,7 +151,7 @@ public sealed class LiveEntityLightControllerTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - Runtime = new LiveEntityRuntime(Spatial, new NoopResources(), _ => { }); + Runtime = LiveEntityRuntimeFixture.Create(Spatial, new NoopResources(), _ => { }); Sink = new LightingHookSink(Lights, Poses); Controller = new LiveEntityLightController( Runtime, diff --git a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs index 7f68d24c..7090c14f 100644 --- a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs +++ b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs @@ -185,7 +185,7 @@ public sealed class CurrentGameRuntimeAdapterTests public Harness() { Identity = new LocalPlayerIdentityState(); - Entities = new LiveEntityRuntime( + Entities = LiveEntityRuntimeFixture.Create( new GpuWorldState(), new NoopEntityResources()); Objects = new ClientObjectTable(); diff --git a/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs b/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs index a965ef1a..804700a0 100644 --- a/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs +++ b/tests/AcDream.App.Tests/Streaming/GpuWorldStateVisibilityTests.cs @@ -579,7 +579,7 @@ public sealed class GpuWorldStateVisibilityTests const uint landblock = 0x0101FFFFu; const uint cell = 0x01010001u; var state = new GpuWorldState(); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( state, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); WorldSession.EntitySpawn first = Spawn(0x70000011u, cell); @@ -624,7 +624,7 @@ public sealed class GpuWorldStateVisibilityTests landblock, new LandBlock(), Array.Empty())); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( state, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); WorldSession.EntitySpawn spawn = Spawn(guid, cell); diff --git a/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs b/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs index ea6f5f67..2ef2dffb 100644 --- a/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs @@ -396,7 +396,7 @@ public sealed class LocalPlayerTeleportControllerTests 0x2021FFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); - var runtime = new LiveEntityRuntime(world, new NullResources()); + var runtime = LiveEntityRuntimeFixture.Create(world, new NullResources()); runtime.RegisterLiveEntity(Spawn(guid, cell)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, @@ -479,7 +479,7 @@ public sealed class LocalPlayerTeleportControllerTests destinationCell & 0xFFFF0000u | 0xFFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); - var runtime = new LiveEntityRuntime(world, new NullResources()); + var runtime = LiveEntityRuntimeFixture.Create(world, new NullResources()); runtime.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, diff --git a/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs b/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs index 9df94774..2e608a5f 100644 --- a/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs +++ b/tests/AcDream.App.Tests/Streaming/StreamingFrameControllerTests.cs @@ -506,7 +506,9 @@ public sealed class StreamingFrameControllerTests } private static LiveEntityRuntime Runtime(GpuWorldState state) => - new(state, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); + LiveEntityRuntimeFixture.Create( + state, + new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); private static LoadedLandblock EmptyLandblock(uint landblock) => new(landblock, new LandBlock(), Array.Empty()); diff --git a/tests/AcDream.App.Tests/World/DeferredLiveEntityRuntimeComponentLifecycleTests.cs b/tests/AcDream.App.Tests/World/DeferredLiveEntityRuntimeComponentLifecycleTests.cs index 43dff17c..55a3e554 100644 --- a/tests/AcDream.App.Tests/World/DeferredLiveEntityRuntimeComponentLifecycleTests.cs +++ b/tests/AcDream.App.Tests/World/DeferredLiveEntityRuntimeComponentLifecycleTests.cs @@ -110,7 +110,7 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests { const uint guid = 0x70000002u; var bridge = new DeferredLiveEntityRuntimeComponentLifecycle(); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), bridge); @@ -136,7 +136,7 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests private static LiveEntityRecord CreateRecord() { - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); return runtime.RegisterAndMaterializeProjection( diff --git a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs index 4fdf7ada..60eab374 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs @@ -1828,7 +1828,8 @@ public sealed class LiveEntityHydrationControllerTests private sealed class Fixture : IDisposable { public readonly List Operations = []; - public readonly ClientObjectTable Objects = new(); + public readonly RuntimeEntityObjectLifetime EntityObjects = new(); + public ClientObjectTable Objects => EntityObjects.Objects; public readonly RecordingResources Resources; public readonly LiveEntityRuntime Runtime; public readonly RecordingMaterializer Materializer; @@ -1861,7 +1862,8 @@ public sealed class LiveEntityHydrationControllerTests Runtime = new LiveEntityRuntime( spatial, Resources, - Teardown); + Teardown, + EntityObjects); Materializer = new RecordingMaterializer(Runtime, Operations); Relationships = new RecordingRelationships(Operations); Ready = new RecordingReadyPublisher(Operations); @@ -1887,13 +1889,13 @@ public sealed class LiveEntityHydrationControllerTests }; var deletion = new LiveEntityDeletionController( Runtime, - Objects, + EntityObjects, Teardown, identity, Dormant); Controller = new LiveEntityHydrationController( Runtime, - Objects, + EntityObjects, new object(), Materializer, Relationships, diff --git a/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs b/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs index 53e070e2..0edaa3a9 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs @@ -328,7 +328,7 @@ public sealed class LiveEntityLifecycleStressTests randomUnit: () => 0.5, canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true); - Runtime = new LiveEntityRuntime( + Runtime = LiveEntityRuntimeFixture.Create( Spatial, new DelegateLiveEntityResourceLifecycle( entity => @@ -489,7 +489,7 @@ public sealed class LiveEntityLifecycleStressTests EntityEffectController? effects = null; LiveEntityPresentationController? presentation = null; RemoteTeleportController? teleport = null; - Runtime = new LiveEntityRuntime( + Runtime = LiveEntityRuntimeFixture.Create( Spatial, new DelegateLiveEntityResourceLifecycle( entity => diff --git a/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs b/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs index fa5fca3d..1a86dda0 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityPhysicsHostOwnershipTests.cs @@ -630,10 +630,10 @@ public sealed class LiveEntityPhysicsHostOwnershipTests private static LiveEntityRuntime Runtime(Action? teardown = null) => teardown is null - ? new LiveEntityRuntime( + ? LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })) - : new LiveEntityRuntime( + : LiveEntityRuntimeFixture.Create( new GpuWorldState(), new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), teardown); diff --git a/tests/AcDream.App.Tests/World/LiveEntityPresentationControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityPresentationControllerTests.cs index 671aa756..366babc1 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityPresentationControllerTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityPresentationControllerTests.cs @@ -426,7 +426,7 @@ public sealed class LiveEntityPresentationControllerTests const uint pendingCell = 0x02020001u; var spatial = new GpuWorldState(); var shadows = new ShadowObjectRegistry(); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); runtime.RegisterLiveEntity(Fixture.Spawn( @@ -487,7 +487,7 @@ public sealed class LiveEntityPresentationControllerTests const uint pendingCell = 0x02020001u; var spatial = new GpuWorldState(); var shadows = new ShadowObjectRegistry(); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); runtime.RegisterLiveEntity(Fixture.Spawn( @@ -667,7 +667,7 @@ public sealed class LiveEntityPresentationControllerTests 0x0101FFFFu, new LandBlock(), Array.Empty())); - Runtime = new LiveEntityRuntime( + Runtime = LiveEntityRuntimeFixture.Create( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); WorldSession.EntitySpawn spawn = Spawn(initialState); diff --git a/tests/AcDream.App.Tests/World/LiveEntityProjectionWithdrawalControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityProjectionWithdrawalControllerTests.cs index 91e92651..5912b8b0 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityProjectionWithdrawalControllerTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityProjectionWithdrawalControllerTests.cs @@ -196,7 +196,7 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests new LandBlock(), Array.Empty())); var lifecycle = new DeferredLiveEntityRuntimeComponentLifecycle(); - Live = new LiveEntityRuntime( + Live = LiveEntityRuntimeFixture.Create( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), lifecycle); diff --git a/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs b/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs index e49d1cb4..f524c43f 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs @@ -22,7 +22,7 @@ public sealed class LiveEntityRuntimeTests public void RuntimeSlot_BindsExactlyOnceWithoutOwningEntityState() { var spatial = new GpuWorldState(); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); var slot = new LiveEntityRuntimeSlot(); Assert.Null(slot.Current); @@ -176,7 +176,7 @@ public sealed class LiveEntityRuntimeTests spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000001u, 1, 1, 0x01010001u); LiveEntityRegistrationResult registered = runtime.RegisterLiveEntity(spawn); @@ -213,7 +213,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000071u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn( guid, instance: 1, @@ -325,7 +325,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000074u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn( guid, instance: 1, @@ -417,7 +417,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000070u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -476,7 +476,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000071u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, @@ -513,7 +513,7 @@ public sealed class LiveEntityRuntimeTests const uint secondGuid = 0x70000074u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); foreach (uint guid in new[] { firstGuid, secondGuid }) { runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); @@ -545,7 +545,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000075u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, @@ -580,7 +580,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000072u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity oldEntity = runtime.MaterializeLiveEntity( guid, @@ -617,7 +617,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000073u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -672,7 +672,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 6, 1, 0x01010001u); LiveEntityRegistrationResult registration = runtime.RegisterLiveEntity(spawn); @@ -702,7 +702,7 @@ public sealed class LiveEntityRuntimeTests { const uint parentGuid = 0x70000020u; const uint childGuid = 0x70000021u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(parentGuid, 9, 1, 0x01010001u)); runtime.ParentAttachments.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 2, 9, 5)); @@ -725,7 +725,7 @@ public sealed class LiveEntityRuntimeTests { const uint parentGuid = 0x70000022u; const uint childGuid = 0x70000023u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(childGuid, 3, 4, 0x01010001u)); runtime.ParentAttachments.AcceptCreateObjectRelation(new ParentAttachmentRelation( parentGuid, childGuid, 1, 2, 9, 4)); @@ -743,7 +743,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn first = Spawn(0x70000002u, 4, 10, 0x01010001u); runtime.RegisterLiveEntity(first); WorldEntity original = runtime.MaterializeLiveEntity( @@ -771,7 +771,7 @@ public sealed class LiveEntityRuntimeTests spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); int runtimeTearDowns = 0; - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( spatial, resources, _ => runtimeTearDowns++); @@ -803,7 +803,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000004u, 2, 3, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -833,7 +833,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); const uint guid = 0x70000005u; runtime.RegisterLiveEntity(Spawn(guid, 3, 1, 0x01010001u)); WorldEntity first = runtime.MaterializeLiveEntity( @@ -863,7 +863,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000006u, 3, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); @@ -900,7 +900,7 @@ public sealed class LiveEntityRuntimeTests { var spatial = new GpuWorldState(); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000007u, 1, 1, 0x01020001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -929,7 +929,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000025u; var spatial = new GpuWorldState(); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 2, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -983,7 +983,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 4, 10, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -1063,7 +1063,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000027u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( @@ -1104,7 +1104,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); LiveEntityRecord record = RegisterProjection(runtime, spawn); RetailObjectQuantumClock clock = record.ObjectClock; @@ -1143,7 +1143,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000049u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn( guid, 1, @@ -1166,7 +1166,7 @@ public sealed class LiveEntityRuntimeTests const uint staticGuid = 0x70000051u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); LiveEntityRecord ordinary = RegisterProjection( runtime, @@ -1204,7 +1204,7 @@ public sealed class LiveEntityRuntimeTests const uint staticGuid = 0x70000054u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); LiveEntityRecord ordinary = RegisterProjection( runtime, @@ -1279,7 +1279,7 @@ public sealed class LiveEntityRuntimeTests { const uint parentGuid = 0x50000010u; const uint childGuid = 0x70000055u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(parentGuid, 1, 1, 0x01010001u)); LiveEntityRecord child = RegisterProjection( runtime, @@ -1314,7 +1314,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000048u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn firstSpawn = Spawn(guid, 1, 1, 0x01010001u); LiveEntityRecord first = RegisterProjection(runtime, firstSpawn); RetailObjectQuantumClock firstClock = first.ObjectClock; @@ -1341,7 +1341,7 @@ public sealed class LiveEntityRuntimeTests { const uint stateBeforeBindGuid = 0x70000037u; const uint bindBeforeStateGuid = 0x70000038u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u)); runtime.RegisterLiveEntity(Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u)); @@ -1381,7 +1381,7 @@ public sealed class LiveEntityRuntimeTests public void ParentDrivenChildNoDrawKeepsBoundBodyInCanonicalState() { const uint guid = 0x70000039u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -1401,7 +1401,7 @@ public sealed class LiveEntityRuntimeTests public void InitiallyVisibleCreate_DoesNotManufactureUnHideTransition() { const uint guid = 0x70000040u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); LiveEntityRecord record = RegisterProjection( runtime, @@ -1418,7 +1418,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000041u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn( guid, 1, @@ -1448,7 +1448,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000042u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, @@ -1486,7 +1486,7 @@ public sealed class LiveEntityRuntimeTests public void PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp() { const uint guid = 0x70000043u; - var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); Assert.True(runtime.TryApplyPickup( @@ -1524,7 +1524,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000044u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( @@ -1563,7 +1563,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000024u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -1593,7 +1593,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000009u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( @@ -1620,7 +1620,7 @@ public sealed class LiveEntityRuntimeTests spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); int runtimeTearDowns = 0; - var runtime = new LiveEntityRuntime(spatial, resources, _ => runtimeTearDowns++); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources, _ => runtimeTearDowns++); WorldSession.EntitySpawn spawn = Spawn(0x70000008u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( @@ -1645,7 +1645,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x7000000Au, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); @@ -1671,7 +1671,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterAndRollbackOnceResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); Assert.Throws(() => runtime.MaterializeLiveEntity( @@ -1709,7 +1709,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterAndRollbackOnceResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); Assert.Throws(() => runtime.MaterializeLiveEntity( guid, @@ -1736,7 +1736,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterAndRollbackOnceResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); Assert.Throws(() => runtime.MaterializeLiveEntity( @@ -1764,7 +1764,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingUnregisterResources(0x7000000Bu); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); foreach (uint guid in new[] { 0x7000000Bu, 0x7000000Cu }) { WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); @@ -1792,7 +1792,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingOnceUnregisterResources(guid); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); @@ -1819,7 +1819,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingOnceUnregisterResources(guid); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); @@ -1852,7 +1852,7 @@ public sealed class LiveEntityRuntimeTests if (!adapter.OnRemove(entity)) throw new InvalidOperationException("presentation owner was not registered"); }); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.ProjectionVisibilityChanged += (record, visible) => { if (record.WorldEntity is { } entity) @@ -1887,7 +1887,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingUnregisterResources(guid); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn first = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(first); runtime.MaterializeLiveEntity( @@ -1918,7 +1918,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010022u)); runtime.MaterializeLiveEntity(guid, 0x01010022u, id => Entity(id, guid)); @@ -1940,7 +1940,7 @@ public sealed class LiveEntityRuntimeTests spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity attached = runtime.MaterializeLiveEntity( guid, @@ -1965,7 +1965,7 @@ public sealed class LiveEntityRuntimeTests { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime( + var runtime = LiveEntityRuntimeFixture.Create( spatial, new RecordingResources(), firstLocalEntityId: LiveEntityRuntime.LastLiveEntityId); @@ -1984,7 +1984,7 @@ public sealed class LiveEntityRuntimeTests Assert.NotEqual(0u, secondEntity.Id); Assert.True(runtime.TryGetLocalEntityId(second.Guid, out uint resolved)); Assert.Equal(secondEntity.Id, resolved); - Assert.Throws(() => new LiveEntityRuntime( + Assert.Throws(() => LiveEntityRuntimeFixture.Create( new GpuWorldState(), new RecordingResources(), firstLocalEntityId: uint.MaxValue)); @@ -1996,7 +1996,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x7000002Au; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -2022,7 +2022,7 @@ public sealed class LiveEntityRuntimeTests public void SessionClear_ForgetsPersistentClassificationWithoutMaterializedRecord() { var spatial = new GpuWorldState(); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); spatial.MarkPersistent(0x7000002Bu); runtime.Clear(); @@ -2037,7 +2037,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x7000002Du; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -2067,7 +2067,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000040u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, @@ -2096,7 +2096,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000041u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -2127,7 +2127,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000044u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); bool replaced = false; @@ -2161,7 +2161,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000045u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity original = runtime.MaterializeLiveEntity( guid, @@ -2200,7 +2200,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x70000046u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); bool reprojected = false; @@ -2228,7 +2228,7 @@ public sealed class LiveEntityRuntimeTests { const uint guid = 0x70000075u; var spatial = new GpuWorldState(); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity pending = runtime.MaterializeLiveEntity( guid, @@ -2277,7 +2277,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingOnceUnregisterResources(guid); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity? replacementEntity = null; bool replaced = false; @@ -2318,7 +2318,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0303FFFFu)); - var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); + var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity original = runtime.MaterializeLiveEntity( guid, @@ -2352,7 +2352,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x7000002Cu; var spatial = new GpuWorldState(); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -2394,7 +2394,7 @@ public sealed class LiveEntityRuntimeTests const uint guid = 0x7000002Eu; var spatial = new GpuWorldState(); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, @@ -2434,7 +2434,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity old = runtime.MaterializeLiveEntity( guid, @@ -2477,7 +2477,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => @@ -2508,7 +2508,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => @@ -2538,7 +2538,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => @@ -2568,7 +2568,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => @@ -2600,7 +2600,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => @@ -2632,7 +2632,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => @@ -2660,7 +2660,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); resources.OnRegister = _ => runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); @@ -2686,7 +2686,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); resources.OnRegister = _ => runtime.Clear(); @@ -2711,7 +2711,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(firstGuid, 1, 1, 0x01010001u)); runtime.RegisterLiveEntity(Spawn(secondGuid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(firstGuid, 0x01010001u, id => Entity(id, firstGuid)); @@ -2744,7 +2744,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); runtime.RegisterLiveEntity(Spawn(oldGuid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(oldGuid, 0x01010001u, id => Entity(id, oldGuid)); resources.OnUnregister = _ => @@ -2785,7 +2785,7 @@ public sealed class LiveEntityRuntimeTests var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); - var runtime = new LiveEntityRuntime(spatial, resources); + var runtime = LiveEntityRuntimeFixture.Create(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x7000000Du, 4, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( diff --git a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs index 24b68fd7..4c136bda 100644 --- a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs +++ b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs @@ -13,8 +13,19 @@ namespace AcDream.App.Tests.World; public sealed class RuntimeEntityOwnershipTests { [Fact] - public void LiveEntityRuntime_ComposesCanonicalRuntimeDirectory() + public void LiveEntityRuntime_BorrowsCanonicalRuntimeDirectory() { + var lifetime = new RuntimeEntityObjectLifetime(); + var runtime = new LiveEntityRuntime( + new GpuWorldState(), + new DelegateLiveEntityResourceLifecycle( + static _ => { }, + static _ => { }), + lifetime); + FieldInfo owner = typeof(LiveEntityRuntime).GetField( + "_entityObjects", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Missing Runtime lifetime root."); FieldInfo directory = typeof(LiveEntityRuntime).GetField( "_directory", BindingFlags.Instance | BindingFlags.NonPublic) @@ -25,9 +36,62 @@ public sealed class RuntimeEntityOwnershipTests ?? throw new InvalidOperationException("Missing App projection sidecar store."); Assert.Equal(typeof(RuntimeEntityDirectory), directory.FieldType); + Assert.Same(lifetime, owner.GetValue(runtime)); + Assert.Same(lifetime.Entities, directory.GetValue(runtime)); Assert.Equal(typeof(LiveEntityProjectionStore), sidecars.FieldType); } + [Fact] + public void ProductionComposition_HasOneRuntimeEntityObjectConstructionRoot() + { + string root = FindRepositoryRoot(); + string appRoot = Path.Combine(root, "src", "AcDream.App"); + string runtimeRoot = Path.Combine(root, "src", "AcDream.Runtime"); + string[] appSources = Directory.GetFiles( + appRoot, + "*.cs", + SearchOption.AllDirectories); + string[] runtimeSources = Directory.GetFiles( + runtimeRoot, + "*.cs", + SearchOption.AllDirectories); + + string[] appObjectAllocators = appSources + .Where(path => !path.Contains( + $"{Path.DirectorySeparatorChar}Studio{Path.DirectorySeparatorChar}", + StringComparison.OrdinalIgnoreCase)) + .Where(path => File.ReadAllText(path).Contains( + "new ClientObjectTable", + StringComparison.Ordinal)) + .ToArray(); + string[] appDirectoryAllocators = appSources + .Where(path => File.ReadAllText(path).Contains( + "new RuntimeEntityDirectory", + StringComparison.Ordinal)) + .ToArray(); + string[] runtimeObjectAllocators = runtimeSources + .Where(path => File.ReadAllText(path).Contains( + "new ClientObjectTable", + StringComparison.Ordinal)) + .Select(path => Path.GetFileName(path)!) + .ToArray(); + string[] runtimeDirectoryAllocators = runtimeSources + .Where(path => File.ReadAllText(path).Contains( + "new RuntimeEntityDirectory", + StringComparison.Ordinal)) + .Select(path => Path.GetFileName(path)!) + .ToArray(); + + Assert.Empty(appObjectAllocators); + Assert.Empty(appDirectoryAllocators); + Assert.Equal( + ["RuntimeEntityObjectLifetime.cs"], + runtimeObjectAllocators); + Assert.Equal( + ["RuntimeEntityObjectLifetime.cs"], + runtimeDirectoryAllocators); + } + [Fact] public void AppProjectionOwners_UseExactRuntimeKeysAndNoGuidDictionary() { @@ -212,4 +276,18 @@ public sealed class RuntimeEntityOwnershipTests field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>) && field.FieldType.GetGenericArguments()[0] == keyType; + + private static string FindRepositoryRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + return directory.FullName; + directory = directory.Parent; + } + + throw new DirectoryNotFoundException( + $"Could not find AcDream.slnx above {AppContext.BaseDirectory}."); + } } diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs new file mode 100644 index 00000000..86309e33 --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeEntityObjectLifetimeTests.cs @@ -0,0 +1,251 @@ +using AcDream.Core.Net; +using AcDream.Core.Net.Messages; +using AcDream.Core.Physics; +using AcDream.Runtime.Entities; + +namespace AcDream.Runtime.Tests.Entities; + +public sealed class RuntimeEntityObjectLifetimeTests +{ + [Fact] + public void Owner_ConstructsOneExactDirectoryAndObjectTablePerInstance() + { + var first = new RuntimeEntityObjectLifetime(); + var second = new RuntimeEntityObjectLifetime(); + + Assert.NotSame(first.Entities, second.Entities); + Assert.NotSame(first.Objects, second.Objects); + Assert.Same(first.Entities, first.Entities); + Assert.Same(first.Objects, first.Objects); + } + + [Fact] + public void Owner_ConstructionLoadsNoPresentationOrBackendAssembly() + { + _ = new RuntimeEntityObjectLifetime(); + + string[] loaded = AppDomain.CurrentDomain + .GetAssemblies() + .Select(assembly => assembly.GetName().Name ?? string.Empty) + .ToArray(); + Assert.DoesNotContain(loaded, name => + name.StartsWith("AcDream.App", StringComparison.Ordinal) + || name.StartsWith("AcDream.UI.", StringComparison.Ordinal) + || name.StartsWith("Silk.NET", StringComparison.Ordinal) + || name.StartsWith("OpenAL", StringComparison.Ordinal) + || name.StartsWith("ImGui", StringComparison.Ordinal)); + } + + [Fact] + public void SeparateOwners_IsolateConflictingGuidsAndContents() + { + const uint guid = 0x70000001u; + var first = new RuntimeEntityObjectLifetime(); + var second = new RuntimeEntityObjectLifetime(); + RuntimeEntityRecord firstRecord = Accept(first, Spawn(guid, 1, "first")); + RuntimeEntityRecord secondRecord = Accept(second, Spawn(guid, 9, "second")); + + Assert.True(first.ApplyAcceptedSpawn( + firstRecord, + firstRecord.CreateIntegrationVersion, + firstRecord.Snapshot, + replaceGeneration: false)); + Assert.True(second.ApplyAcceptedSpawn( + secondRecord, + secondRecord.CreateIntegrationVersion, + secondRecord.Snapshot, + replaceGeneration: false)); + + Assert.Equal("first", first.Objects.Get(guid)!.Name); + Assert.Equal("second", second.Objects.Get(guid)!.Name); + Assert.Equal((ushort)1, firstRecord.Incarnation); + Assert.Equal((ushort)9, secondRecord.Incarnation); + } + + [Fact] + public void AcceptedSpawn_RejectsWrongOrSupersededCanonicalIncarnation() + { + var lifetime = new RuntimeEntityObjectLifetime(); + WorldSession.EntitySpawn spawn = Spawn(0x70000002u, 3, "current"); + RuntimeEntityRecord canonical = Accept(lifetime, spawn); + var wrong = new RuntimeEntityObjectLifetime(); + RuntimeEntityRecord wrongCanonical = Accept(wrong, spawn); + + Assert.False(lifetime.ApplyAcceptedSpawn( + wrongCanonical, + wrongCanonical.CreateIntegrationVersion, + spawn, + replaceGeneration: false)); + + ulong expectedVersion = canonical.CreateIntegrationVersion; + lifetime.Entities.AdvanceCreateAuthority(canonical); + Assert.False(lifetime.ApplyAcceptedSpawn( + canonical, + expectedVersion, + spawn, + replaceGeneration: false)); + Assert.Null(lifetime.Objects.Get(spawn.Guid)); + } + + [Fact] + public void AcceptedSpawn_RevalidatesAfterSynchronousObjectCallbacks() + { + var lifetime = new RuntimeEntityObjectLifetime(); + WorldSession.EntitySpawn spawn = Spawn(0x70000003u, 4, "reentrant"); + RuntimeEntityRecord canonical = Accept(lifetime, spawn); + ulong expectedVersion = canonical.CreateIntegrationVersion; + lifetime.Objects.ObjectAdded += _ => + lifetime.Entities.AdvanceCreateAuthority(canonical); + + Assert.False(lifetime.ApplyAcceptedSpawn( + canonical, + expectedVersion, + spawn, + replaceGeneration: false)); + Assert.NotNull(lifetime.Objects.Get(spawn.Guid)); + } + + [Fact] + public void AcceptedDelete_RemovesOnlyMatchingLogicalGeneration() + { + var lifetime = new RuntimeEntityObjectLifetime(); + WorldSession.EntitySpawn spawn = Spawn(0x70000004u, 6, "retained"); + RuntimeEntityRecord canonical = Accept(lifetime, spawn); + Assert.True(lifetime.ApplyAcceptedSpawn( + canonical, + canonical.CreateIntegrationVersion, + spawn, + replaceGeneration: true)); + + Assert.False(lifetime.TryAcceptDelete( + new DeleteObject.Parsed(spawn.Guid, 7), + isLocalPlayer: false, + out _)); + Assert.NotNull(lifetime.Objects.Get(spawn.Guid)); + + Assert.True(lifetime.TryAcceptDelete( + new DeleteObject.Parsed(spawn.Guid, 6), + isLocalPlayer: false, + out RuntimeEntityDeleteAcceptance accepted)); + lifetime.CompleteAcceptedDelete(accepted); + Assert.Null(lifetime.Objects.Get(spawn.Guid)); + } + + [Fact] + public void DeleteAcceptance_CannotCrossOwnersOrReplay() + { + var owner = new RuntimeEntityObjectLifetime(); + var other = new RuntimeEntityObjectLifetime(); + WorldSession.EntitySpawn spawn = Spawn(0x70000006u, 8, "token"); + RuntimeEntityRecord canonical = Accept(owner, spawn); + Assert.True(owner.ApplyAcceptedSpawn( + canonical, + canonical.CreateIntegrationVersion, + spawn, + replaceGeneration: true)); + Assert.True(owner.TryAcceptDelete( + new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence), + isLocalPlayer: false, + out RuntimeEntityDeleteAcceptance acceptance)); + + Assert.Throws( + () => other.CompleteAcceptedDelete(acceptance)); + owner.CompleteAcceptedDelete(acceptance); + Assert.Throws( + () => owner.CompleteAcceptedDelete(acceptance)); + Assert.Null(owner.Objects.Get(spawn.Guid)); + } + + [Fact] + public void ClearObjects_DoesNotResetCanonicalEntityIdentity() + { + var lifetime = new RuntimeEntityObjectLifetime(); + WorldSession.EntitySpawn spawn = Spawn(0x70000005u, 2, "clear"); + RuntimeEntityRecord canonical = Accept(lifetime, spawn); + Assert.True(lifetime.ApplyAcceptedSpawn( + canonical, + canonical.CreateIntegrationVersion, + spawn, + replaceGeneration: false)); + + lifetime.ClearObjects(); + + Assert.Null(lifetime.Objects.Get(spawn.Guid)); + Assert.True(lifetime.Entities.TryGetActive(spawn.Guid, out var retained)); + Assert.Same(canonical, retained); + } + + private static RuntimeEntityRecord Accept( + RuntimeEntityObjectLifetime lifetime, + WorldSession.EntitySpawn spawn) + { + InboundCreateResult accepted = lifetime.Entities.AcceptCreate(spawn); + return lifetime.Entities.AddActive(accepted.Snapshot); + } + + private static WorldSession.EntitySpawn Spawn( + uint guid, + ushort instance, + string name) + { + var position = new CreateObject.ServerPosition( + 0x0101FFFFu, + 10f, + 20f, + 5f, + 1f, + 0f, + 0f, + 0f); + var timestamps = new PhysicsTimestamps( + Position: 1, + Movement: 1, + State: 1, + Vector: 1, + Teleport: 0, + ServerControlledMove: 1, + ForcePosition: 0, + ObjDesc: 1, + Instance: instance); + var physics = new PhysicsSpawnData( + RawState: 0x408u, + Position: position, + Movement: null, + AnimationFrame: null, + SetupTableId: 0x02000001u, + MotionTableId: 0x09000001u, + SoundTableId: null, + PhysicsScriptTableId: null, + Parent: null, + Children: null, + Scale: null, + Friction: null, + Elasticity: null, + Translucency: null, + Velocity: null, + Acceleration: null, + AngularVelocity: null, + DefaultScriptType: null, + DefaultScriptIntensity: null, + Timestamps: timestamps); + return new WorldSession.EntitySpawn( + guid, + position, + 0x02000001u, + Array.Empty(), + Array.Empty(), + Array.Empty(), + null, + null, + name, + null, + null, + 0x09000001u, + PhysicsState: 0x408u, + InstanceSequence: instance, + MovementSequence: 1, + ServerControlSequence: 1, + PositionSequence: 1, + Physics: physics); + } +}