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 <codex@openai.com>
This commit is contained in:
parent
d9bf4c4960
commit
5ef8b5371d
40 changed files with 712 additions and 170 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<LiveEntityAnimationState> 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
||||
private readonly ShortcutSnapshotState _shortcutSnapshots = new();
|
||||
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> 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,
|
||||
|
|
|
|||
|
|
@ -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<string>? 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);
|
||||
|
|
|
|||
|
|
@ -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<string>? 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))
|
||||
|
|
|
|||
|
|
@ -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<LiveEntityRecord> 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<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
beforeTeardown?.Invoke();
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
/// </summary>
|
||||
public static class ObjectTableWiring
|
||||
|
|
|
|||
135
src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
Normal file
135
src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.Runtime.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One exact DeleteObject acceptance issued by a
|
||||
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
|
||||
/// active identity before returning this token to the Runtime owner for the
|
||||
/// retained-object mutation, preserving retail callback order.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a delete accepted by the dormant exact-incarnation owner after
|
||||
/// the active Runtime directory correctly reports no live record.
|
||||
/// </summary>
|
||||
public void ApplyAcceptedDormantDelete(DeleteObject.Parsed delete) =>
|
||||
ObjectTableWiring.ApplyEntityDelete(Objects, delete);
|
||||
|
||||
/// <summary>
|
||||
/// Clears retained object state at the Runtime-owned reset stage. App
|
||||
/// projection teardown remains a later acknowledged stage.
|
||||
/// </summary>
|
||||
public void ClearObjects() => Objects.Clear();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue