refactor(runtime): publish canonical entity object deltas
Issue stable Runtime identities at canonical registration, publish entity and inventory commits through one generation-stamped synchronous stream, and make graphical adapters borrow the same direct views and events as a no-window host. Preserve exact projection teardown and retail mutation order while removing App-side event reconstruction. Make the hard-recenter ordering fixture independent of the production two-millisecond frame budget so its injected-failure gate is deterministic. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
d3e96ff912
commit
ce3ac310d9
23 changed files with 2352 additions and 666 deletions
|
|
@ -80,7 +80,16 @@ public sealed class RuntimeEntityDirectory
|
|||
|
||||
var record = new RuntimeEntityRecord(snapshot);
|
||||
_activeByGuid.Add(snapshot.Guid, record);
|
||||
return record;
|
||||
try
|
||||
{
|
||||
ClaimLocalId(record);
|
||||
return record;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_activeByGuid.Remove(snapshot.Guid);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveActive(uint guid, out RuntimeEntityRecord? record) =>
|
||||
|
|
|
|||
271
src/AcDream.Runtime/Entities/RuntimeEntityObjectEventStream.cs
Normal file
271
src/AcDream.Runtime/Entities/RuntimeEntityObjectEventStream.cs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Runtime.Entities;
|
||||
|
||||
public interface IRuntimeEntityObjectObserver
|
||||
{
|
||||
void OnEntity(in RuntimeEntityDelta delta);
|
||||
|
||||
void OnInventory(in RuntimeInventoryDelta delta);
|
||||
}
|
||||
|
||||
public interface IRuntimeEntityObjectEventSource
|
||||
{
|
||||
IDisposable Subscribe(IRuntimeEntityObjectObserver observer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One synchronous, generation-stamped commit stream for the canonical entity
|
||||
/// directory and retained object table. It owns no queue and dispatches on the
|
||||
/// caller's update/network-drain thread.
|
||||
/// </summary>
|
||||
public sealed class RuntimeEntityObjectEventStream
|
||||
: IRuntimeEntityObjectEventSource,
|
||||
IDisposable
|
||||
{
|
||||
private readonly RuntimeEntityDirectory _entities;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly RuntimeEventSequencer _sequencer = new();
|
||||
private readonly object _observerGate = new();
|
||||
private IRuntimeEntityObjectObserver[] _observers = [];
|
||||
private Func<RuntimeGenerationToken> _generation = static () => default;
|
||||
private Func<ulong> _frameNumber = static () => 0UL;
|
||||
private bool _contextBound;
|
||||
private bool _disposed;
|
||||
|
||||
internal RuntimeEntityObjectEventStream(
|
||||
RuntimeEntityDirectory entities,
|
||||
ClientObjectTable objects)
|
||||
{
|
||||
_entities = entities
|
||||
?? throw new ArgumentNullException(nameof(entities));
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_objects.ObjectAdded += OnObjectAdded;
|
||||
_objects.ObjectUpdated += OnObjectUpdated;
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.ObjectRemovalClassified += OnObjectRemoved;
|
||||
_objects.Cleared += OnObjectsCleared;
|
||||
}
|
||||
|
||||
public ulong LastSequence => _sequencer.LastSequence;
|
||||
public int SubscriberCount => Volatile.Read(ref _observers).Length;
|
||||
public long DispatchFailureCount { get; private set; }
|
||||
public Exception? LastDispatchFailure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Binds the session-generation and Runtime-frame sources exactly once.
|
||||
/// Both are queried at commit time so reconnect/reset cannot stamp an event
|
||||
/// with a cached generation.
|
||||
/// </summary>
|
||||
public void BindContext(
|
||||
Func<RuntimeGenerationToken> generation,
|
||||
Func<ulong> frameNumber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(generation);
|
||||
ArgumentNullException.ThrowIfNull(frameNumber);
|
||||
lock (_observerGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_contextBound)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Runtime entity/object event context is already bound.");
|
||||
}
|
||||
|
||||
_generation = generation;
|
||||
_frameNumber = frameNumber;
|
||||
_contextBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Subscribe(IRuntimeEntityObjectObserver observer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(observer);
|
||||
lock (_observerGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
IRuntimeEntityObjectObserver[] current = _observers;
|
||||
if (Array.IndexOf(current, observer) >= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Runtime entity/object observer is already subscribed.");
|
||||
}
|
||||
|
||||
var replacement =
|
||||
new IRuntimeEntityObjectObserver[current.Length + 1];
|
||||
Array.Copy(current, replacement, current.Length);
|
||||
replacement[^1] = observer;
|
||||
Volatile.Write(ref _observers, replacement);
|
||||
}
|
||||
|
||||
return new ObserverSubscription(this, observer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates the next stamp for another domain on the same Runtime event
|
||||
/// surface. J4–J6 will move those remaining domain publishers into
|
||||
/// Runtime; until then the App adapter borrows this sequencer.
|
||||
/// </summary>
|
||||
public RuntimeEventStamp NextStamp()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(
|
||||
Volatile.Read(ref _disposed),
|
||||
this);
|
||||
return _sequencer.Next(_generation(), _frameNumber());
|
||||
}
|
||||
|
||||
internal void PublishEntity(
|
||||
RuntimeEntityChange change,
|
||||
RuntimeEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
var delta = new RuntimeEntityDelta(
|
||||
NextStamp(),
|
||||
change,
|
||||
RuntimeEntityObjectViews.Snapshot(record));
|
||||
IRuntimeEntityObjectObserver[] observers =
|
||||
Volatile.Read(ref _observers);
|
||||
foreach (IRuntimeEntityObjectObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnEntity(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
Volatile.Write(ref _observers, []);
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
_objects.ObjectRemovalClassified -= OnObjectRemoved;
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
_objects.ObjectUpdated -= OnObjectUpdated;
|
||||
_objects.ObjectAdded -= OnObjectAdded;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnObjectAdded(ClientObject item) =>
|
||||
PublishInventory(RuntimeInventoryChange.Added, item);
|
||||
|
||||
private void OnObjectUpdated(ClientObject item) =>
|
||||
PublishInventory(RuntimeInventoryChange.Updated, item);
|
||||
|
||||
private void OnObjectMoved(ClientObjectMove move)
|
||||
{
|
||||
ClientObject? item = move.Item ?? _objects.Get(move.ItemId);
|
||||
RuntimeInventoryItemSnapshot snapshot = item is null
|
||||
? new RuntimeInventoryItemSnapshot(
|
||||
move.ItemId,
|
||||
0,
|
||||
string.Empty,
|
||||
move.Current.ContainerId,
|
||||
move.Current.ContainerSlot,
|
||||
move.Current.WielderId,
|
||||
(uint)move.Current.EquipLocation,
|
||||
0,
|
||||
0)
|
||||
: RuntimeEntityObjectViews.Snapshot(item, _entities);
|
||||
PublishInventory(RuntimeInventoryChange.Moved, snapshot);
|
||||
}
|
||||
|
||||
private void OnObjectRemoved(ClientObjectRemoval removal) =>
|
||||
PublishInventory(
|
||||
RuntimeInventoryChange.Removed,
|
||||
RuntimeEntityObjectViews.Snapshot(
|
||||
removal.Object,
|
||||
_entities,
|
||||
removal.Generation));
|
||||
|
||||
private void OnObjectsCleared() =>
|
||||
PublishInventory(
|
||||
RuntimeInventoryChange.Cleared,
|
||||
default(RuntimeInventoryItemSnapshot));
|
||||
|
||||
private void PublishInventory(
|
||||
RuntimeInventoryChange change,
|
||||
ClientObject item) =>
|
||||
PublishInventory(
|
||||
change,
|
||||
RuntimeEntityObjectViews.Snapshot(item, _entities));
|
||||
|
||||
private void PublishInventory(
|
||||
RuntimeInventoryChange change,
|
||||
RuntimeInventoryItemSnapshot item)
|
||||
{
|
||||
var delta = new RuntimeInventoryDelta(
|
||||
NextStamp(),
|
||||
change,
|
||||
item);
|
||||
IRuntimeEntityObjectObserver[] observers =
|
||||
Volatile.Read(ref _observers);
|
||||
foreach (IRuntimeEntityObjectObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnInventory(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordDispatchFailure(Exception error)
|
||||
{
|
||||
DispatchFailureCount++;
|
||||
LastDispatchFailure = error;
|
||||
}
|
||||
|
||||
private void Unsubscribe(IRuntimeEntityObjectObserver observer)
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
IRuntimeEntityObjectObserver[] current = _observers;
|
||||
int index = Array.IndexOf(current, observer);
|
||||
if (index < 0)
|
||||
return;
|
||||
if (current.Length == 1)
|
||||
{
|
||||
Volatile.Write(ref _observers, []);
|
||||
return;
|
||||
}
|
||||
|
||||
var replacement =
|
||||
new IRuntimeEntityObjectObserver[current.Length - 1];
|
||||
if (index != 0)
|
||||
Array.Copy(current, 0, replacement, 0, index);
|
||||
if (index != current.Length - 1)
|
||||
{
|
||||
Array.Copy(
|
||||
current,
|
||||
index + 1,
|
||||
replacement,
|
||||
index,
|
||||
current.Length - index - 1);
|
||||
}
|
||||
Volatile.Write(ref _observers, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ObserverSubscription(
|
||||
RuntimeEntityObjectEventStream owner,
|
||||
IRuntimeEntityObjectObserver observer)
|
||||
: IDisposable
|
||||
{
|
||||
private RuntimeEntityObjectEventStream? _owner = owner;
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _owner, null)?.Unsubscribe(observer);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Entities;
|
||||
|
||||
public readonly record struct RuntimeEntityRegistrationResult(
|
||||
InboundCreateResult Inbound,
|
||||
RuntimeEntityRecord? Canonical,
|
||||
bool LogicalRegistrationCreated,
|
||||
bool ReplacedExistingGeneration,
|
||||
Exception? PriorGenerationCleanupFailure = null);
|
||||
|
||||
/// <summary>
|
||||
/// One exact DeleteObject acceptance issued by a
|
||||
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
|
||||
|
|
@ -14,15 +22,21 @@ public sealed class RuntimeEntityDeleteAcceptance
|
|||
{
|
||||
internal RuntimeEntityDeleteAcceptance(
|
||||
RuntimeEntityObjectLifetime owner,
|
||||
DeleteObject.Parsed delete)
|
||||
DeleteObject.Parsed delete,
|
||||
RuntimeEntityRecord? retiredCanonical,
|
||||
bool removeRetainedObject)
|
||||
{
|
||||
Owner = owner;
|
||||
Delete = delete;
|
||||
RetiredCanonical = retiredCanonical;
|
||||
RemoveRetainedObject = removeRetainedObject;
|
||||
}
|
||||
|
||||
internal RuntimeEntityObjectLifetime Owner { get; }
|
||||
internal bool Completed { get; set; }
|
||||
public DeleteObject.Parsed Delete { get; }
|
||||
public RuntimeEntityRecord? RetiredCanonical { get; }
|
||||
public bool RemoveRetainedObject { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -30,17 +44,253 @@ public sealed class RuntimeEntityDeleteAcceptance
|
|||
/// 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 sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||
{
|
||||
private bool _sessionClearInProgress;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeEntityObjectLifetime(
|
||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
||||
{
|
||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||
Objects = new ClientObjectTable();
|
||||
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
||||
EntityView = views.Entities;
|
||||
InventoryView = views.Inventory;
|
||||
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
||||
}
|
||||
|
||||
public RuntimeEntityDirectory Entities { get; }
|
||||
public ClientObjectTable Objects { get; }
|
||||
public IRuntimeEntityView EntityView { get; }
|
||||
public IRuntimeInventoryView InventoryView { get; }
|
||||
public RuntimeEntityObjectEventStream Events { get; }
|
||||
|
||||
public void BindEventContext(
|
||||
Func<RuntimeGenerationToken> generation,
|
||||
Func<ulong> frameNumber)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
Events.BindContext(generation, frameNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the presentation-free half of retail's CreateObject lifetime
|
||||
/// transaction. An attached graphical host may synchronously retire the
|
||||
/// displaced projection through <paramref name="retirePriorProjection"/>;
|
||||
/// a direct host omits it and Runtime retires canonical-only state.
|
||||
/// </summary>
|
||||
public RuntimeEntityRegistrationResult RegisterEntity(
|
||||
WorldSession.EntitySpawn incoming,
|
||||
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (_sessionClearInProgress)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A Runtime entity cannot register while its session lifetime is clearing.");
|
||||
}
|
||||
|
||||
InboundCreateResult result = Entities.AcceptCreate(incoming);
|
||||
if (result.Disposition
|
||||
is CreateObjectTimestampDisposition.StaleGeneration)
|
||||
{
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
result,
|
||||
Canonical: null,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: false);
|
||||
}
|
||||
|
||||
ulong sessionVersion = Entities.SessionLifetimeVersion;
|
||||
ulong operationVersion =
|
||||
Entities.AdvanceLifetimeMutation(incoming.Guid);
|
||||
|
||||
if (result.Disposition
|
||||
is CreateObjectTimestampDisposition.ExistingGeneration)
|
||||
{
|
||||
if (Entities.TryGetActive(
|
||||
incoming.Guid,
|
||||
out RuntimeEntityRecord retained))
|
||||
{
|
||||
Entities.RefreshSnapshot(
|
||||
retained,
|
||||
result.Snapshot,
|
||||
refreshPosition: true);
|
||||
Entities.AdvanceCreateAuthority(retained);
|
||||
PublishEntity(RuntimeEntityChange.Updated, retained);
|
||||
if (!IsCurrentOperation(
|
||||
incoming.Guid,
|
||||
retained,
|
||||
sessionVersion,
|
||||
operationVersion))
|
||||
{
|
||||
return SupersededRegistration(
|
||||
incoming.Guid,
|
||||
replacedExistingGeneration: false);
|
||||
}
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
result,
|
||||
retained,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: false);
|
||||
}
|
||||
|
||||
if (Entities.TryGetTeardown(
|
||||
incoming.Guid,
|
||||
result.Snapshot.InstanceSequence,
|
||||
out _))
|
||||
{
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
result,
|
||||
Canonical: null,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: false);
|
||||
}
|
||||
|
||||
RuntimeEntityRecord recovered = Entities.AddActive(result.Snapshot);
|
||||
PublishEntity(RuntimeEntityChange.Registered, recovered);
|
||||
if (!IsCurrentOperation(
|
||||
incoming.Guid,
|
||||
recovered,
|
||||
sessionVersion,
|
||||
operationVersion))
|
||||
{
|
||||
return SupersededRegistration(
|
||||
incoming.Guid,
|
||||
replacedExistingGeneration: false);
|
||||
}
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
result,
|
||||
recovered,
|
||||
LogicalRegistrationCreated: true,
|
||||
ReplacedExistingGeneration: false);
|
||||
}
|
||||
|
||||
bool replaced = Entities.RemoveActive(
|
||||
incoming.Guid,
|
||||
out RuntimeEntityRecord? prior);
|
||||
if (result.Disposition
|
||||
is CreateObjectTimestampDisposition.NewGeneration)
|
||||
{
|
||||
Entities.ParentAttachments.EndGeneration(
|
||||
incoming.Guid,
|
||||
result.Snapshot.InstanceSequence);
|
||||
}
|
||||
|
||||
Exception? cleanupFailure = null;
|
||||
if (prior is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
PublishEntity(RuntimeEntityChange.Deleted, prior);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
cleanupFailure = error;
|
||||
}
|
||||
|
||||
Exception? projectionFailure = retirePriorProjection is null
|
||||
? RetireCanonicalOnly(prior)
|
||||
: retirePriorProjection(prior);
|
||||
cleanupFailure = Combine(cleanupFailure, projectionFailure);
|
||||
}
|
||||
|
||||
if (Entities.SessionLifetimeVersion != sessionVersion
|
||||
|| Entities.CurrentLifetimeMutation(incoming.Guid)
|
||||
!= operationVersion)
|
||||
{
|
||||
if (cleanupFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its incoming replacement was superseded.",
|
||||
cleanupFailure);
|
||||
}
|
||||
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
SupersededCreateResult(),
|
||||
Entities.TryGetActive(
|
||||
incoming.Guid,
|
||||
out RuntimeEntityRecord current)
|
||||
? current
|
||||
: null,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: replaced);
|
||||
}
|
||||
|
||||
RuntimeEntityRecord canonical = Entities.AddActive(result.Snapshot);
|
||||
try
|
||||
{
|
||||
PublishEntity(RuntimeEntityChange.Registered, canonical);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (cleanupFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"Live entity 0x{incoming.Guid:X8} registered after prior cleanup and commit observers failed.",
|
||||
cleanupFailure,
|
||||
error);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
if (!IsCurrentOperation(
|
||||
incoming.Guid,
|
||||
canonical,
|
||||
sessionVersion,
|
||||
operationVersion))
|
||||
{
|
||||
if (cleanupFailure is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its committed replacement was superseded.",
|
||||
cleanupFailure);
|
||||
}
|
||||
|
||||
return SupersededRegistration(
|
||||
incoming.Guid,
|
||||
replaced);
|
||||
}
|
||||
|
||||
return new RuntimeEntityRegistrationResult(
|
||||
result,
|
||||
canonical,
|
||||
LogicalRegistrationCreated: true,
|
||||
ReplacedExistingGeneration: replaced,
|
||||
cleanupFailure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes a displaced incarnation that never crossed the graphical
|
||||
/// projection acquisition edge.
|
||||
/// </summary>
|
||||
public Exception? RetireCanonicalOnly(RuntimeEntityRecord canonical)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
try
|
||||
{
|
||||
Entities.RetainTeardown(canonical);
|
||||
try
|
||||
{
|
||||
Entities.SetPhysicsHost(canonical, null);
|
||||
Entities.SetPhysicsBody(canonical, null);
|
||||
Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false);
|
||||
Entities.SetHasPartArray(canonical, false);
|
||||
Entities.ReleaseLocalId(canonical);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Entities.ReleaseTeardown(canonical);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the retained-object half of an accepted CreateObject while the
|
||||
|
|
@ -54,6 +304,7 @@ public sealed class RuntimeEntityObjectLifetime
|
|||
WorldSession.EntitySpawn spawn,
|
||||
bool replaceGeneration)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (canonical.ServerGuid != spawn.Guid
|
||||
|| canonical.Incarnation != spawn.InstanceSequence)
|
||||
|
|
@ -75,24 +326,423 @@ public sealed class RuntimeEntityObjectLifetime
|
|||
&& IsCurrent();
|
||||
}
|
||||
|
||||
public bool TryApplyObjDesc(
|
||||
ObjDescEvent.Parsed update,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyObjDesc(update, out accepted);
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return applied;
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvanceObjDescAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool TryApplyPickup(
|
||||
PickupEvent.Parsed update,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyPickup(update, out accepted);
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return applied;
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvancePositionAuthority(canonical);
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool TryApplyCreateParent(
|
||||
CreateParentUpdate update,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyCreateParent(update, out accepted);
|
||||
return CommitPositionChannelUpdate(
|
||||
applied,
|
||||
update.ChildGuid,
|
||||
accepted,
|
||||
acknowledgeProjection);
|
||||
}
|
||||
|
||||
public bool TryApplyParent(
|
||||
ParentEvent.Parsed update,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyParent(update, out accepted);
|
||||
return CommitPositionChannelUpdate(
|
||||
applied,
|
||||
update.ChildGuid,
|
||||
accepted,
|
||||
acknowledgeProjection);
|
||||
}
|
||||
|
||||
public bool TryCommitParent(
|
||||
ParentAttachmentRelation relation,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool committed = Entities.TryCommitParent(
|
||||
relation.ChildGuid,
|
||||
relation.ParentGuid,
|
||||
relation.ParentLocation,
|
||||
relation.PlacementId,
|
||||
relation.ChildPositionSequence,
|
||||
out accepted);
|
||||
if (!committed
|
||||
|| !Entities.TryGetActive(
|
||||
relation.ChildGuid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return committed;
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool CommitAcceptedParentCellless(
|
||||
RuntimeEntityRecord canonical,
|
||||
ulong positionAuthorityVersion,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!Entities.IsCurrent(canonical)
|
||||
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical)
|
||||
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
||||
return Entities.IsCurrent(canonical)
|
||||
&& canonical.PositionAuthorityVersion == positionAuthorityVersion;
|
||||
}
|
||||
|
||||
public bool TryApplyMotion(
|
||||
WorldSession.EntityMotionUpdate update,
|
||||
bool retainPayload,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyMotion(
|
||||
update,
|
||||
retainPayload,
|
||||
out accepted,
|
||||
out timestamps);
|
||||
if (Entities.TryGetSnapshot(
|
||||
update.Guid,
|
||||
out WorldSession.EntitySpawn snapshot)
|
||||
&& Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
Entities.RefreshSnapshot(canonical, snapshot);
|
||||
if (applied && retainPayload)
|
||||
Entities.AdvanceMovementAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (applied && Entities.IsCurrent(canonical))
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
if (applied && !Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyVector(
|
||||
VectorUpdate.Parsed update,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyVector(update, out accepted);
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return applied;
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvanceVectorAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool TryApplyState(
|
||||
SetState.Parsed update,
|
||||
Action<RuntimeEntityRecord, RetailPhysicsStateTransition>?
|
||||
acknowledgeProjection,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out RetailPhysicsStateTransition transition)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool applied = Entities.TryApplyState(update, out accepted);
|
||||
transition = default;
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return applied;
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
transition = Entities.ApplyRawPhysicsState(
|
||||
canonical,
|
||||
update.PhysicsState);
|
||||
acknowledgeProjection?.Invoke(canonical, transition);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(
|
||||
transition.HiddenTransition
|
||||
is RetailHiddenTransition.BecameHidden
|
||||
? RuntimeEntityChange.Hidden
|
||||
: RuntimeEntityChange.Updated,
|
||||
canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool TryApplyPosition(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
bool isLocalPlayer,
|
||||
System.Numerics.Quaternion? forcePositionRotation,
|
||||
System.Numerics.Vector3? currentLocalVelocity,
|
||||
bool projectionRequiresTeleportHook,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
bool hadCanonical = Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord beforeCanonical);
|
||||
uint beforeCell = beforeCanonical?.FullCellId ?? 0u;
|
||||
bool wasCellless = hadCanonical && beforeCell == 0u;
|
||||
bool known = Entities.TryApplyPosition(
|
||||
update,
|
||||
isLocalPlayer,
|
||||
forcePositionRotation,
|
||||
currentLocalVelocity,
|
||||
out disposition,
|
||||
out accepted,
|
||||
out timestamps);
|
||||
if (!known
|
||||
|| !Entities.TryGetSnapshot(
|
||||
update.Guid,
|
||||
out WorldSession.EntitySpawn snapshot)
|
||||
|| !Entities.TryGetActive(
|
||||
update.Guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return known;
|
||||
}
|
||||
|
||||
bool acceptedPosition =
|
||||
disposition is not PositionTimestampDisposition.Rejected;
|
||||
if (disposition is PositionTimestampDisposition.Apply)
|
||||
{
|
||||
timestamps = timestamps with
|
||||
{
|
||||
TeleportHookRequired =
|
||||
timestamps.TeleportAdvanced
|
||||
|| wasCellless
|
||||
|| projectionRequiresTeleportHook,
|
||||
};
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(
|
||||
canonical,
|
||||
snapshot,
|
||||
refreshPosition: acceptedPosition);
|
||||
if (acceptedPosition
|
||||
&& ReferenceEquals(canonical, beforeCanonical))
|
||||
{
|
||||
Entities.AdvancePositionAuthority(canonical);
|
||||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||
}
|
||||
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
if (acceptedPosition)
|
||||
{
|
||||
PublishEntity(
|
||||
beforeCell != canonical.FullCellId
|
||||
? RuntimeEntityChange.Rebucketed
|
||||
: RuntimeEntityChange.Updated,
|
||||
canonical);
|
||||
}
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool CommitRebucket(
|
||||
RuntimeEntityRecord canonical,
|
||||
uint fullCellId,
|
||||
uint canonicalLandblockId,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
|
||||
uint previous = canonical.FullCellId;
|
||||
Entities.SetFullCell(
|
||||
canonical,
|
||||
fullCellId,
|
||||
canonicalLandblockId);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
if (previous != fullCellId)
|
||||
PublishEntity(RuntimeEntityChange.Rebucketed, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool CommitWithdrawal(
|
||||
RuntimeEntityRecord canonical,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool CommitChildNoDraw(
|
||||
RuntimeEntityRecord canonical,
|
||||
bool noDraw,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
|
||||
Entities.SetChildNoDraw(canonical, noDraw);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
public bool RetireAfterProjectionAcquisitionFailure(
|
||||
RuntimeEntityRecord canonical)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!Entities.RemoveActive(canonical))
|
||||
return false;
|
||||
|
||||
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
|
||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Accepts and retires the exact canonical DeleteObject generation without
|
||||
/// yet publishing the retained-object removal. This split lets a graphical
|
||||
/// host tear down the exact retired projection before completing the
|
||||
/// retained-object mutation while Runtime remains the only freshness,
|
||||
/// identity, and canonical-lifetime authority.
|
||||
/// </summary>
|
||||
public bool TryAcceptDelete(
|
||||
DeleteObject.Parsed delete,
|
||||
bool isLocalPlayer,
|
||||
bool removeRetainedObject,
|
||||
out RuntimeEntityDeleteAcceptance acceptance)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (!Entities.TryDelete(delete, isLocalPlayer))
|
||||
{
|
||||
acceptance = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
acceptance = new RuntimeEntityDeleteAcceptance(this, delete);
|
||||
Entities.AdvanceLifetimeMutation(delete.Guid);
|
||||
Entities.ParentAttachments.DeleteGeneration(
|
||||
delete.Guid,
|
||||
delete.InstanceSequence);
|
||||
|
||||
RuntimeEntityRecord? retiredCanonical = null;
|
||||
if (Entities.TryGetActive(
|
||||
delete.Guid,
|
||||
out RuntimeEntityRecord active)
|
||||
&& active.Incarnation == delete.InstanceSequence
|
||||
&& Entities.RemoveActive(active))
|
||||
{
|
||||
retiredCanonical = active;
|
||||
PublishEntity(
|
||||
removeRetainedObject
|
||||
? RuntimeEntityChange.Deleted
|
||||
: RuntimeEntityChange.Withdrawn,
|
||||
active);
|
||||
}
|
||||
|
||||
acceptance = new RuntimeEntityDeleteAcceptance(
|
||||
this,
|
||||
delete,
|
||||
retiredCanonical,
|
||||
removeRetainedObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +754,7 @@ public sealed class RuntimeEntityObjectLifetime
|
|||
public void CompleteAcceptedDelete(
|
||||
RuntimeEntityDeleteAcceptance acceptance)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(acceptance);
|
||||
if (!ReferenceEquals(acceptance.Owner, this))
|
||||
{
|
||||
|
|
@ -117,19 +768,132 @@ public sealed class RuntimeEntityObjectLifetime
|
|||
}
|
||||
|
||||
acceptance.Completed = true;
|
||||
ObjectTableWiring.ApplyEntityDelete(Objects, acceptance.Delete);
|
||||
if (acceptance.RemoveRetainedObject)
|
||||
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) =>
|
||||
public void ApplyAcceptedDormantDelete(DeleteObject.Parsed delete)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
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();
|
||||
public void ClearObjects()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
Objects.Clear();
|
||||
}
|
||||
|
||||
public IReadOnlyList<RuntimeEntityRecord> BeginSessionClear()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (_sessionClearInProgress)
|
||||
return Array.Empty<RuntimeEntityRecord>();
|
||||
|
||||
_sessionClearInProgress = true;
|
||||
Entities.BeginSessionClear();
|
||||
RuntimeEntityRecord[] retired = Entities.ActiveRecords.ToArray();
|
||||
foreach (RuntimeEntityRecord canonical in retired)
|
||||
{
|
||||
if (!Entities.RemoveActive(canonical))
|
||||
continue;
|
||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||
}
|
||||
return retired;
|
||||
}
|
||||
|
||||
public bool CompleteSessionClearIfConverged()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (!_sessionClearInProgress
|
||||
|| !Entities.CompleteSessionClearIfConverged())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_sessionClearInProgress = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
Events.Dispose();
|
||||
}
|
||||
|
||||
private bool CommitPositionChannelUpdate(
|
||||
bool applied,
|
||||
uint guid,
|
||||
WorldSession.EntitySpawn accepted,
|
||||
Action<RuntimeEntityRecord>? acknowledgeProjection)
|
||||
{
|
||||
if (!applied
|
||||
|| !Entities.TryGetActive(
|
||||
guid,
|
||||
out RuntimeEntityRecord canonical))
|
||||
{
|
||||
return applied;
|
||||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvancePositionAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
}
|
||||
|
||||
private void PublishEntity(
|
||||
RuntimeEntityChange change,
|
||||
RuntimeEntityRecord canonical) =>
|
||||
Events.PublishEntity(change, canonical);
|
||||
|
||||
private void EnsureNotDisposed() =>
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
private static Exception? Combine(
|
||||
Exception? first,
|
||||
Exception? second) =>
|
||||
first is null
|
||||
? second
|
||||
: second is null
|
||||
? first
|
||||
: new AggregateException(first, second);
|
||||
|
||||
private static InboundCreateResult SupersededCreateResult() => new(
|
||||
CreateObjectTimestampDisposition.StaleGeneration,
|
||||
default,
|
||||
null,
|
||||
default);
|
||||
|
||||
private bool IsCurrentOperation(
|
||||
uint guid,
|
||||
RuntimeEntityRecord canonical,
|
||||
ulong sessionVersion,
|
||||
ulong operationVersion) =>
|
||||
Entities.SessionLifetimeVersion == sessionVersion
|
||||
&& Entities.CurrentLifetimeMutation(guid) == operationVersion
|
||||
&& Entities.IsCurrent(canonical);
|
||||
|
||||
private RuntimeEntityRegistrationResult SupersededRegistration(
|
||||
uint guid,
|
||||
bool replacedExistingGeneration) =>
|
||||
new(
|
||||
SupersededCreateResult(),
|
||||
Entities.TryGetActive(guid, out RuntimeEntityRecord current)
|
||||
? current
|
||||
: null,
|
||||
LogicalRegistrationCreated: false,
|
||||
ReplacedExistingGeneration: replacedExistingGeneration);
|
||||
}
|
||||
|
|
|
|||
151
src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs
Normal file
151
src/AcDream.Runtime/Entities/RuntimeEntityObjectViews.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Runtime.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free borrowed views over the canonical entity/object lifetime.
|
||||
/// These views contain no App projection or backend state.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeEntityObjectViews
|
||||
{
|
||||
public RuntimeEntityObjectViews(
|
||||
RuntimeEntityDirectory entities,
|
||||
ClientObjectTable objects)
|
||||
{
|
||||
Entities = new EntityView(
|
||||
entities ?? throw new ArgumentNullException(nameof(entities)));
|
||||
Inventory = new InventoryView(
|
||||
entities,
|
||||
objects ?? throw new ArgumentNullException(nameof(objects)));
|
||||
}
|
||||
|
||||
public IRuntimeEntityView Entities { get; }
|
||||
public IRuntimeInventoryView Inventory { get; }
|
||||
|
||||
internal static RuntimeEntitySnapshot Snapshot(RuntimeEntityRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
uint localEntityId = record.LocalEntityId
|
||||
?? throw new InvalidOperationException(
|
||||
$"Canonical entity 0x{record.ServerGuid:X8}/{record.Incarnation} has no Runtime identity.");
|
||||
return new RuntimeEntitySnapshot(
|
||||
new RuntimeEntityIdentity(
|
||||
record.ServerGuid,
|
||||
localEntityId,
|
||||
record.Incarnation),
|
||||
record.FullCellId,
|
||||
(uint)record.FinalPhysicsState,
|
||||
ConvertPosition(record.Snapshot.Position));
|
||||
}
|
||||
|
||||
private static Position? ConvertPosition(
|
||||
CreateObject.ServerPosition? position) =>
|
||||
position is not { } value
|
||||
? null
|
||||
: new Position(
|
||||
value.LandblockId,
|
||||
new Vector3(
|
||||
value.PositionX,
|
||||
value.PositionY,
|
||||
value.PositionZ),
|
||||
new Quaternion(
|
||||
value.RotationX,
|
||||
value.RotationY,
|
||||
value.RotationZ,
|
||||
value.RotationW));
|
||||
|
||||
internal static RuntimeInventoryItemSnapshot Snapshot(
|
||||
ClientObject item,
|
||||
RuntimeEntityDirectory entities,
|
||||
ushort? exactGeneration = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
ushort incarnation = exactGeneration
|
||||
?? (entities.TryGetActive(
|
||||
item.ObjectId,
|
||||
out RuntimeEntityRecord canonical)
|
||||
? canonical.Incarnation
|
||||
: (ushort)0);
|
||||
return new RuntimeInventoryItemSnapshot(
|
||||
item.ObjectId,
|
||||
incarnation,
|
||||
item.Name,
|
||||
item.ContainerId,
|
||||
item.ContainerSlot,
|
||||
item.WielderId,
|
||||
(uint)item.CurrentlyEquippedLocation,
|
||||
item.StackSize,
|
||||
item.Value);
|
||||
}
|
||||
|
||||
private sealed class EntityView(RuntimeEntityDirectory owner)
|
||||
: IRuntimeEntityView
|
||||
{
|
||||
public int Count => owner.Count;
|
||||
|
||||
public bool TryGet(
|
||||
uint serverGuid,
|
||||
out RuntimeEntitySnapshot entity)
|
||||
{
|
||||
if (owner.TryGetActive(
|
||||
serverGuid,
|
||||
out RuntimeEntityRecord record))
|
||||
{
|
||||
entity = Snapshot(record);
|
||||
return true;
|
||||
}
|
||||
|
||||
entity = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Visit(IRuntimeEntityVisitor visitor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(visitor);
|
||||
foreach (RuntimeEntityRecord record in owner.ActiveRecords)
|
||||
{
|
||||
RuntimeEntitySnapshot entity = Snapshot(record);
|
||||
visitor.Visit(in entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InventoryView(
|
||||
RuntimeEntityDirectory entities,
|
||||
ClientObjectTable owner)
|
||||
: IRuntimeInventoryView
|
||||
{
|
||||
public int ObjectCount => owner.ObjectCount;
|
||||
public int ContainerCount => owner.ContainerCount;
|
||||
|
||||
public bool TryGet(
|
||||
uint objectId,
|
||||
out RuntimeInventoryItemSnapshot item)
|
||||
{
|
||||
ClientObject? found = owner.Get(objectId);
|
||||
if (found is not null)
|
||||
{
|
||||
item = Snapshot(found, entities);
|
||||
return true;
|
||||
}
|
||||
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Visit(IRuntimeInventoryVisitor visitor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(visitor);
|
||||
foreach (ClientObject item in owner.Objects)
|
||||
{
|
||||
RuntimeInventoryItemSnapshot snapshot = Snapshot(
|
||||
item,
|
||||
entities);
|
||||
visitor.Visit(in snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -225,12 +225,26 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
|||
/// <summary>Monotonic sequence owner for one runtime instance.</summary>
|
||||
public sealed class RuntimeEventSequencer
|
||||
{
|
||||
private RuntimeGenerationToken _generation;
|
||||
private bool _hasGeneration;
|
||||
private ulong _sequence;
|
||||
|
||||
public RuntimeEventStamp Next(
|
||||
RuntimeGenerationToken generation,
|
||||
ulong frameNumber) =>
|
||||
new(generation, checked(++_sequence), frameNumber);
|
||||
ulong frameNumber)
|
||||
{
|
||||
if (!_hasGeneration || generation != _generation)
|
||||
{
|
||||
_generation = generation;
|
||||
_sequence = 0;
|
||||
_hasGeneration = true;
|
||||
}
|
||||
|
||||
return new RuntimeEventStamp(
|
||||
generation,
|
||||
checked(++_sequence),
|
||||
frameNumber);
|
||||
}
|
||||
|
||||
public ulong LastSequence => _sequence;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ public readonly record struct RuntimeEntitySnapshot(
|
|||
RuntimeEntityIdentity Identity,
|
||||
uint CellId,
|
||||
uint PhysicsState,
|
||||
Position? Position,
|
||||
bool IsMaterialized,
|
||||
bool IsSpatiallyVisible,
|
||||
bool IsHydrated);
|
||||
Position? Position);
|
||||
|
||||
public interface IRuntimeEntityVisitor
|
||||
{
|
||||
|
|
@ -25,8 +22,6 @@ public interface IRuntimeEntityView
|
|||
{
|
||||
int Count { get; }
|
||||
|
||||
int MaterializedCount { get; }
|
||||
|
||||
bool TryGet(uint serverGuid, out RuntimeEntitySnapshot entity);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue