refactor(world): separate live lifetime from spatial buckets
Introduce LiveEntityRuntime as the canonical owner of each accepted server-object incarnation, stable local identity, timestamped state, parent relations, runtime components, and exactly-once teardown. Split logical registration from rebucketing so pending landblocks, equipment attachment, pickup re-entry, and GUID replacement reuse the same entity and effect owners. Keep canonical materialized and visible target/radar views distinct, preserve retail leave_world versus exit_world semantics, gate root simulation while cell-less, and track transitional pre-Create F754 owners through delete and session reset. Remove stale-spawn rehydration and make GpuWorldState spatial-only for live objects. Add lifecycle, generation, pending, unload, attachment, event-publication, local-ID, rollback, and effect-cleanup coverage; update architecture, milestones, memory, and the divergence register. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
8a5d77f7f4
commit
8dd996053d
24 changed files with 2449 additions and 631 deletions
732
src/AcDream.App/World/LiveEntityRuntime.cs
Normal file
732
src/AcDream.App/World/LiveEntityRuntime.cs
Normal file
|
|
@ -0,0 +1,732 @@
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Animation state owned by a live object. The concrete App animation runtime
|
||||
/// implements this seam so identity storage does not depend on a render backend.
|
||||
/// </summary>
|
||||
public interface ILiveEntityAnimationRuntime
|
||||
{
|
||||
WorldEntity Entity { get; }
|
||||
}
|
||||
|
||||
/// <summary>Remote motion state owned by a live object.</summary>
|
||||
public interface ILiveEntityRemoteMotionRuntime
|
||||
{
|
||||
PhysicsBody Body { get; }
|
||||
}
|
||||
|
||||
/// <summary>Marker for the projectile runtime added by the projectile phase.</summary>
|
||||
public interface ILiveEntityProjectileRuntime { }
|
||||
|
||||
/// <summary>Marker for the DAT-driven effect profile added by the effect phase.</summary>
|
||||
public interface ILiveEntityEffectProfile { }
|
||||
|
||||
public enum LiveEntityProjectionKind
|
||||
{
|
||||
World,
|
||||
Attached,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
|
||||
/// Spatial bucketing is deliberately absent: registering or removing meshes,
|
||||
/// scripts, and effects is a logical-lifetime operation, not a landblock move.
|
||||
/// </summary>
|
||||
public interface ILiveEntityResourceLifecycle
|
||||
{
|
||||
void Register(WorldEntity entity);
|
||||
void Unregister(WorldEntity entity);
|
||||
}
|
||||
|
||||
/// <summary>Delegate adapter used by the App composition root.</summary>
|
||||
public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle
|
||||
{
|
||||
private readonly Action<WorldEntity> _register;
|
||||
private readonly Action<WorldEntity> _unregister;
|
||||
|
||||
public DelegateLiveEntityResourceLifecycle(
|
||||
Action<WorldEntity> register,
|
||||
Action<WorldEntity> unregister)
|
||||
{
|
||||
_register = register ?? throw new ArgumentNullException(nameof(register));
|
||||
_unregister = unregister ?? throw new ArgumentNullException(nameof(unregister));
|
||||
}
|
||||
|
||||
public void Register(WorldEntity entity) => _register(entity);
|
||||
public void Unregister(WorldEntity entity) => _unregister(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The one logical record for a server object incarnation. The record survives
|
||||
/// loaded/pending landblock movement and temporary loss of its render bucket.
|
||||
/// Only an accepted DeleteObject, session reset, or newer INSTANCE_TS ends it.
|
||||
/// </summary>
|
||||
public sealed class LiveEntityRecord
|
||||
{
|
||||
internal LiveEntityRecord(WorldSession.EntitySpawn snapshot)
|
||||
{
|
||||
ServerGuid = snapshot.Guid;
|
||||
Snapshot = snapshot;
|
||||
RefreshDerivedState();
|
||||
}
|
||||
|
||||
public uint ServerGuid { get; }
|
||||
public ushort Generation => Snapshot.InstanceSequence;
|
||||
public WorldSession.EntitySpawn Snapshot { get; internal set; }
|
||||
public WorldEntity? WorldEntity { get; internal set; }
|
||||
public uint? LocalEntityId => WorldEntity?.Id;
|
||||
public uint FullCellId { get; internal set; }
|
||||
public uint CanonicalLandblockId { get; internal set; }
|
||||
public uint RawPhysicsState { get; internal set; }
|
||||
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
|
||||
public PhysicsBody? PhysicsBody { get; internal set; }
|
||||
public ILiveEntityAnimationRuntime? AnimationRuntime { get; internal set; }
|
||||
public ILiveEntityRemoteMotionRuntime? RemoteMotionRuntime { get; internal set; }
|
||||
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
|
||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||
public bool ResourcesRegistered { get; internal set; }
|
||||
public bool IsSpatiallyProjected { get; internal set; }
|
||||
public bool IsSpatiallyVisible { get; internal set; }
|
||||
public bool WorldSpawnPublished { get; internal set; }
|
||||
public LiveEntityProjectionKind ProjectionKind { get; internal set; }
|
||||
|
||||
internal void RefreshDerivedState(bool refreshPosition = true)
|
||||
{
|
||||
if (refreshPosition && Snapshot.Position is { } position)
|
||||
{
|
||||
FullCellId = position.LandblockId;
|
||||
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
|
||||
}
|
||||
RawPhysicsState = Snapshot.Physics?.RawState
|
||||
?? Snapshot.PhysicsState
|
||||
?? 0u;
|
||||
FinalPhysicsState = (PhysicsStateFlags)RawPhysicsState;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct LiveEntityRegistrationResult(
|
||||
InboundCreateResult Inbound,
|
||||
LiveEntityRecord? Record,
|
||||
bool LogicalRegistrationCreated,
|
||||
bool ReplacedExistingGeneration,
|
||||
Exception? PriorGenerationCleanupFailure = null);
|
||||
|
||||
/// <summary>
|
||||
/// Update-thread owner of live server-object identity, accepted state, runtime
|
||||
/// components, and logical teardown. <see cref="GpuWorldState"/> is a spatial
|
||||
/// projection only; moving between loaded and pending buckets never replays a
|
||||
/// create-time renderer, script, animation, or effect action.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail keeps one object per accepted INSTANCE_TS: same-generation
|
||||
/// CreateObject branches mutate it, Pickup leaves world without destruction,
|
||||
/// and Delete/instance replacement end it. See SmartBox CreateObject/event
|
||||
/// pseudocode in <c>2026-07-13-retail-projectile-vfx-pseudocode.md</c>,
|
||||
/// <c>CPhysicsObj::set_description</c> (0x00514F40),
|
||||
/// <c>CPhysicsObj::change_cell</c> (0x00513390), and
|
||||
/// <c>CPhysicsObj::exit_world</c> (0x00514E60).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class LiveEntityRuntime
|
||||
{
|
||||
public const uint FirstLiveEntityId = 1_000_000u;
|
||||
public const uint LastLiveEntityId = 0x3FFF_FFFFu;
|
||||
|
||||
private readonly GpuWorldState _spatial;
|
||||
private readonly ILiveEntityResourceLifecycle _resources;
|
||||
private readonly Action<LiveEntityRecord>? _tearDownRuntimeComponents;
|
||||
private readonly InboundPhysicsStateController _inbound = new();
|
||||
private readonly Dictionary<uint, LiveEntityRecord> _recordsByGuid = new();
|
||||
private readonly Dictionary<uint, WorldEntity> _materializedWorldEntitiesByGuid = new();
|
||||
private readonly Dictionary<uint, WorldEntity> _visibleWorldEntitiesByGuid = new();
|
||||
private readonly Dictionary<uint, uint> _guidByLocalId = new();
|
||||
private readonly Dictionary<uint, ILiveEntityAnimationRuntime> _animationsByLocalId = new();
|
||||
private readonly Dictionary<uint, ILiveEntityRemoteMotionRuntime> _remoteMotionByGuid = new();
|
||||
private uint _nextLocalEntityId;
|
||||
|
||||
public LiveEntityRuntime(
|
||||
GpuWorldState spatial,
|
||||
ILiveEntityResourceLifecycle resources,
|
||||
Action<LiveEntityRecord>? tearDownRuntimeComponents = null,
|
||||
uint firstLocalEntityId = FirstLiveEntityId)
|
||||
{
|
||||
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
_tearDownRuntimeComponents = tearDownRuntimeComponents;
|
||||
if (firstLocalEntityId is < FirstLiveEntityId or > LastLiveEntityId)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(firstLocalEntityId),
|
||||
$"Live entity ids must stay in 0x{FirstLiveEntityId:X8}..0x{LastLiveEntityId:X8}.");
|
||||
_nextLocalEntityId = firstLocalEntityId;
|
||||
_spatial.LiveProjectionVisibilityChanged += OnSpatialVisibilityChanged;
|
||||
}
|
||||
|
||||
public int Count => _recordsByGuid.Count;
|
||||
public int MaterializedCount => _guidByLocalId.Count;
|
||||
public IReadOnlyCollection<LiveEntityRecord> Records => _recordsByGuid.Values;
|
||||
/// <summary>
|
||||
/// Every materialized top-level world projection, including an object
|
||||
/// parked in a pending (currently unloaded) spatial bucket. Network and
|
||||
/// physics mutation must use this stable view so streaming visibility can
|
||||
/// never masquerade as logical destruction.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<uint, WorldEntity> MaterializedWorldEntities =>
|
||||
_materializedWorldEntitiesByGuid;
|
||||
|
||||
/// <summary>
|
||||
/// Currently visible top-level world projections. Attached children and
|
||||
/// pending projections are excluded from radar, picking, status, and target
|
||||
/// candidate consumers.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<uint, WorldEntity> WorldEntities =>
|
||||
_visibleWorldEntitiesByGuid;
|
||||
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _inbound.Snapshots;
|
||||
public IReadOnlyDictionary<uint, ILiveEntityAnimationRuntime> AnimationRuntimes => _animationsByLocalId;
|
||||
public IReadOnlyDictionary<uint, ILiveEntityRemoteMotionRuntime> RemoteMotionRuntimes => _remoteMotionByGuid;
|
||||
public ParentAttachmentState ParentAttachments { get; } = new();
|
||||
|
||||
public LiveEntityRegistrationResult RegisterLiveEntity(WorldSession.EntitySpawn incoming)
|
||||
{
|
||||
InboundCreateResult result = _inbound.AcceptCreate(incoming);
|
||||
if (result.Disposition is CreateObjectTimestampDisposition.StaleGeneration)
|
||||
return new LiveEntityRegistrationResult(result, null, false, false);
|
||||
|
||||
if (result.Disposition is CreateObjectTimestampDisposition.ExistingGeneration)
|
||||
{
|
||||
if (_recordsByGuid.TryGetValue(incoming.Guid, out LiveEntityRecord? retained))
|
||||
{
|
||||
retained.Snapshot = result.Snapshot;
|
||||
retained.RefreshDerivedState();
|
||||
return new LiveEntityRegistrationResult(result, retained, false, false);
|
||||
}
|
||||
|
||||
// Defensive repair for a caller that cleared only the logical map.
|
||||
// Normal session teardown clears both owners together.
|
||||
var recovered = new LiveEntityRecord(result.Snapshot);
|
||||
_recordsByGuid.Add(incoming.Guid, recovered);
|
||||
return new LiveEntityRegistrationResult(result, recovered, true, false);
|
||||
}
|
||||
|
||||
bool replaced = _recordsByGuid.Remove(incoming.Guid, out LiveEntityRecord? old);
|
||||
Exception? tearDownFailure = null;
|
||||
if (old is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
TearDownRecord(old);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
tearDownFailure = error;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Disposition is CreateObjectTimestampDisposition.NewGeneration)
|
||||
ParentAttachments.EndGeneration(incoming.Guid, result.Snapshot.InstanceSequence);
|
||||
|
||||
var record = new LiveEntityRecord(result.Snapshot);
|
||||
_recordsByGuid.Add(incoming.Guid, record);
|
||||
return new LiveEntityRegistrationResult(
|
||||
result,
|
||||
record,
|
||||
true,
|
||||
replaced,
|
||||
tearDownFailure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes logical registration when a renderable projection can be
|
||||
/// hydrated. The factory is called at most once for the incarnation.
|
||||
/// </summary>
|
||||
public WorldEntity? MaterializeLiveEntity(
|
||||
uint serverGuid,
|
||||
uint fullCellId,
|
||||
Func<uint, WorldEntity> factory,
|
||||
LiveEntityProjectionKind projectionKind = LiveEntityProjectionKind.World)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
|
||||
return null;
|
||||
|
||||
if (record.WorldEntity is null)
|
||||
{
|
||||
uint localId = ReserveLocalEntityId();
|
||||
WorldEntity entity = factory(localId)
|
||||
?? throw new InvalidOperationException("A live-entity projection factory returned null.");
|
||||
if (entity.Id != localId)
|
||||
throw new InvalidOperationException(
|
||||
$"Live projection id 0x{entity.Id:X8} did not match reserved id 0x{localId:X8}.");
|
||||
if (entity.ServerGuid != serverGuid)
|
||||
throw new InvalidOperationException(
|
||||
$"Live projection guid 0x{entity.ServerGuid:X8} did not match record 0x{serverGuid:X8}.");
|
||||
|
||||
_guidByLocalId.Add(localId, serverGuid);
|
||||
record.WorldEntity = entity;
|
||||
try
|
||||
{
|
||||
_resources.Register(entity);
|
||||
record.ResourcesRegistered = true;
|
||||
}
|
||||
catch (Exception registrationError)
|
||||
{
|
||||
// Registration is an atomic logical-lifetime boundary. Give
|
||||
// composite owners a symmetric rollback opportunity, then
|
||||
// remove the identity even if cleanup itself fails.
|
||||
try
|
||||
{
|
||||
_resources.Unregister(entity);
|
||||
}
|
||||
catch (Exception rollbackError)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Live entity resource registration and rollback both failed.",
|
||||
registrationError,
|
||||
rollbackError);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_guidByLocalId.Remove(localId);
|
||||
record.WorldEntity = null;
|
||||
record.ResourcesRegistered = false;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
record.ProjectionKind = projectionKind;
|
||||
RebucketLiveEntity(serverGuid, fullCellId);
|
||||
return record.WorldEntity;
|
||||
}
|
||||
|
||||
/// <summary>Changes spatial buckets only. No logical resource callbacks run.</summary>
|
||||
public bool RebucketLiveEntity(uint serverGuid, uint spatialCellOrLandblockId)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
return false;
|
||||
|
||||
_spatial.RebucketLiveEntity(entity, spatialCellOrLandblockId);
|
||||
bool visible = _spatial.IsLiveEntityVisible(serverGuid);
|
||||
record.IsSpatiallyVisible = visible;
|
||||
if (record.ProjectionKind is LiveEntityProjectionKind.World)
|
||||
_materializedWorldEntitiesByGuid[serverGuid] = entity;
|
||||
else
|
||||
_materializedWorldEntitiesByGuid.Remove(serverGuid);
|
||||
if (visible && record.ProjectionKind is LiveEntityProjectionKind.World)
|
||||
_visibleWorldEntitiesByGuid[serverGuid] = entity;
|
||||
else
|
||||
_visibleWorldEntitiesByGuid.Remove(serverGuid);
|
||||
if ((spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu)
|
||||
record.FullCellId = spatialCellOrLandblockId;
|
||||
record.CanonicalLandblockId = spatialCellOrLandblockId == 0
|
||||
? 0u
|
||||
: (spatialCellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
record.IsSpatiallyProjected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes only the render-bucket reference. The logical record and every
|
||||
/// create-time resource remain alive for later projection/rebucketing.
|
||||
/// </summary>
|
||||
public bool WithdrawLiveEntityProjection(uint serverGuid)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is null)
|
||||
return false;
|
||||
|
||||
_spatial.RemoveLiveEntityProjection(serverGuid);
|
||||
_materializedWorldEntitiesByGuid.Remove(serverGuid);
|
||||
_visibleWorldEntitiesByGuid.Remove(serverGuid);
|
||||
record.IsSpatiallyProjected = false;
|
||||
record.IsSpatiallyVisible = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UnregisterLiveEntity(
|
||||
DeleteObject.Parsed delete,
|
||||
bool isLocalPlayer,
|
||||
Action? beforeTeardown = null)
|
||||
{
|
||||
if (!_inbound.TryDelete(delete, isLocalPlayer))
|
||||
return false;
|
||||
|
||||
ParentAttachments.DeleteGeneration(delete.Guid, delete.InstanceSequence);
|
||||
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
beforeTeardown?.Invoke();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
|
||||
if (_recordsByGuid.Remove(delete.Guid, out LiveEntityRecord? record))
|
||||
{
|
||||
try
|
||||
{
|
||||
TearDownRecord(record);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
$"Live entity 0x{delete.Guid:X8} deletion cleanup failed.",
|
||||
failures);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetRecord(uint serverGuid, out LiveEntityRecord record) =>
|
||||
_recordsByGuid.TryGetValue(serverGuid, out record!);
|
||||
|
||||
public bool TryGetWorldEntity(uint serverGuid, out WorldEntity entity)
|
||||
{
|
||||
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.WorldEntity is { } found)
|
||||
{
|
||||
entity = found;
|
||||
return true;
|
||||
}
|
||||
|
||||
entity = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsWorldEntity(uint serverGuid) =>
|
||||
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.WorldEntity is not null;
|
||||
|
||||
public bool TryGetServerGuid(uint localEntityId, out uint serverGuid) =>
|
||||
_guidByLocalId.TryGetValue(localEntityId, out serverGuid);
|
||||
|
||||
public bool TryGetLocalEntityId(uint serverGuid, out uint localEntityId)
|
||||
{
|
||||
if (_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.LocalEntityId is { } found)
|
||||
{
|
||||
localEntityId = found;
|
||||
return true;
|
||||
}
|
||||
|
||||
localEntityId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetAnimationRuntime(uint serverGuid, ILiveEntityAnimationRuntime runtime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
throw new InvalidOperationException($"Cannot bind animation before live entity 0x{serverGuid:X8} is materialized.");
|
||||
if (!ReferenceEquals(runtime.Entity, entity))
|
||||
throw new InvalidOperationException("Animation runtime belongs to a different WorldEntity.");
|
||||
|
||||
if (record.AnimationRuntime is not null)
|
||||
_animationsByLocalId.Remove(entity.Id);
|
||||
record.AnimationRuntime = runtime;
|
||||
_animationsByLocalId[entity.Id] = runtime;
|
||||
}
|
||||
|
||||
public bool TryGetAnimationRuntime(uint localEntityId, out ILiveEntityAnimationRuntime runtime) =>
|
||||
_animationsByLocalId.TryGetValue(localEntityId, out runtime!);
|
||||
|
||||
public bool ClearAnimationRuntime(uint serverGuid)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.AnimationRuntime is null)
|
||||
return false;
|
||||
if (record.LocalEntityId is { } localId)
|
||||
_animationsByLocalId.Remove(localId);
|
||||
record.AnimationRuntime = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetRemoteMotionRuntime(uint serverGuid, ILiveEntityRemoteMotionRuntime runtime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record))
|
||||
throw new InvalidOperationException($"Cannot bind remote motion before live entity 0x{serverGuid:X8} exists.");
|
||||
record.RemoteMotionRuntime = runtime;
|
||||
record.PhysicsBody = runtime.Body;
|
||||
_remoteMotionByGuid[serverGuid] = runtime;
|
||||
}
|
||||
|
||||
public bool TryGetRemoteMotionRuntime(uint serverGuid, out ILiveEntityRemoteMotionRuntime runtime) =>
|
||||
_remoteMotionByGuid.TryGetValue(serverGuid, out runtime!);
|
||||
|
||||
public bool ClearRemoteMotionRuntime(uint serverGuid)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.RemoteMotionRuntime is null)
|
||||
return false;
|
||||
_remoteMotionByGuid.Remove(serverGuid);
|
||||
record.RemoteMotionRuntime = null;
|
||||
record.PhysicsBody = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetSnapshot(uint guid, out WorldSession.EntitySpawn spawn) =>
|
||||
_inbound.TryGetSnapshot(guid, out spawn);
|
||||
|
||||
public bool TryApplyObjDesc(ObjDescEvent.Parsed update, out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
bool applied = _inbound.TryApplyObjDesc(update, out accepted);
|
||||
if (applied) RefreshRecord(update.Guid, accepted);
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyPickup(PickupEvent.Parsed update, out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
bool applied = _inbound.TryApplyPickup(update, out accepted);
|
||||
if (applied)
|
||||
{
|
||||
RefreshRecord(update.Guid, accepted);
|
||||
ClearWorldCell(update.Guid);
|
||||
ParentAttachments.EndChildProjection(update.Guid);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyCreateParent(CreateParentUpdate update, out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
bool applied = _inbound.TryApplyCreateParent(update, out accepted);
|
||||
if (applied)
|
||||
{
|
||||
RefreshRecord(update.ChildGuid, accepted);
|
||||
ClearWorldCell(update.ChildGuid);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyParent(ParentEvent.Parsed update, out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
bool applied = _inbound.TryApplyParent(update, out accepted);
|
||||
if (applied)
|
||||
{
|
||||
RefreshRecord(update.ChildGuid, accepted);
|
||||
ClearWorldCell(update.ChildGuid);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyMotion(
|
||||
WorldSession.EntityMotionUpdate update,
|
||||
bool retainPayload,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
bool applied = _inbound.TryApplyMotion(update, retainPayload, out accepted, out timestamps);
|
||||
if (_inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
|
||||
RefreshRecord(update.Guid, snapshot);
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyVector(VectorUpdate.Parsed update, out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
bool applied = _inbound.TryApplyVector(update, out accepted);
|
||||
if (applied) RefreshRecord(update.Guid, accepted);
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyState(SetState.Parsed update, out WorldSession.EntitySpawn accepted)
|
||||
{
|
||||
bool applied = _inbound.TryApplyState(update, out accepted);
|
||||
if (applied) RefreshRecord(update.Guid, accepted);
|
||||
return applied;
|
||||
}
|
||||
|
||||
public bool TryApplyPosition(
|
||||
WorldSession.EntityPositionUpdate update,
|
||||
bool isLocalPlayer,
|
||||
System.Numerics.Quaternion? forcePositionRotation,
|
||||
System.Numerics.Vector3? currentLocalVelocity,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
bool known = _inbound.TryApplyPosition(
|
||||
update,
|
||||
isLocalPlayer,
|
||||
forcePositionRotation,
|
||||
currentLocalVelocity,
|
||||
out disposition,
|
||||
out accepted,
|
||||
out timestamps);
|
||||
if (known && _inbound.TryGetSnapshot(update.Guid, out WorldSession.EntitySpawn snapshot))
|
||||
{
|
||||
bool acceptedPosition = disposition is not PositionTimestampDisposition.Rejected;
|
||||
RefreshRecord(update.Guid, snapshot, refreshPosition: acceptedPosition);
|
||||
if (acceptedPosition)
|
||||
{
|
||||
ParentAttachments.EndChildProjection(update.Guid);
|
||||
}
|
||||
}
|
||||
return known;
|
||||
}
|
||||
|
||||
public bool IsFreshTeleportStart(uint localPlayerGuid, ushort teleportSequence) =>
|
||||
_inbound.IsFreshTeleportStart(localPlayerGuid, teleportSequence);
|
||||
|
||||
/// <summary>
|
||||
/// Retail runs root movement/animation only while the object has world-cell
|
||||
/// membership. A pickup or parent transition keeps script/effect owners but
|
||||
/// clears the cell and withdraws the root projection until a fresh Position
|
||||
/// re-enters it.
|
||||
/// </summary>
|
||||
public bool ShouldAdvanceRootRuntime(uint serverGuid) =>
|
||||
_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
&& record.IsSpatiallyProjected
|
||||
&& record.FullCellId != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Claims the one logical top-level spawn notification for this object
|
||||
/// incarnation. Spatial re-entry must restore presentation without replaying
|
||||
/// plugin/event registration. An attached-only child claims nothing until it
|
||||
/// first becomes a top-level world projection.
|
||||
/// </summary>
|
||||
public bool TryMarkWorldSpawnPublished(uint serverGuid)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is null
|
||||
|| record.ProjectionKind is not LiveEntityProjectionKind.World
|
||||
|| record.WorldSpawnPublished)
|
||||
return false;
|
||||
record.WorldSpawnPublished = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
foreach (LiveEntityRecord record in _recordsByGuid.Values.ToArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
TearDownRecord(record);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
_recordsByGuid.Clear();
|
||||
_materializedWorldEntitiesByGuid.Clear();
|
||||
_visibleWorldEntitiesByGuid.Clear();
|
||||
_guidByLocalId.Clear();
|
||||
_animationsByLocalId.Clear();
|
||||
_remoteMotionByGuid.Clear();
|
||||
ParentAttachments.Clear();
|
||||
_inbound.Clear();
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException("One or more live entities failed session teardown.", failures);
|
||||
}
|
||||
|
||||
private void RefreshRecord(
|
||||
uint guid,
|
||||
WorldSession.EntitySpawn accepted,
|
||||
bool refreshPosition = false)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(guid, out LiveEntityRecord? record))
|
||||
return;
|
||||
record.Snapshot = accepted;
|
||||
record.RefreshDerivedState(refreshPosition);
|
||||
}
|
||||
|
||||
private void ClearWorldCell(uint guid)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(guid, out LiveEntityRecord? record))
|
||||
return;
|
||||
record.FullCellId = 0;
|
||||
record.CanonicalLandblockId = 0;
|
||||
}
|
||||
|
||||
private uint ReserveLocalEntityId()
|
||||
{
|
||||
uint start = _nextLocalEntityId;
|
||||
do
|
||||
{
|
||||
uint candidate = _nextLocalEntityId;
|
||||
_nextLocalEntityId = candidate == LastLiveEntityId
|
||||
? FirstLiveEntityId
|
||||
: candidate + 1u;
|
||||
if (!_guidByLocalId.ContainsKey(candidate))
|
||||
return candidate;
|
||||
}
|
||||
while (_nextLocalEntityId != start);
|
||||
|
||||
throw new InvalidOperationException("The live entity id namespace is exhausted.");
|
||||
}
|
||||
|
||||
private void OnSpatialVisibilityChanged(uint serverGuid, bool visible)
|
||||
{
|
||||
if (!_recordsByGuid.TryGetValue(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
return;
|
||||
|
||||
record.IsSpatiallyVisible = visible;
|
||||
if (visible && record.ProjectionKind is LiveEntityProjectionKind.World)
|
||||
_visibleWorldEntitiesByGuid[serverGuid] = entity;
|
||||
else
|
||||
_visibleWorldEntitiesByGuid.Remove(serverGuid);
|
||||
}
|
||||
|
||||
private void TearDownRecord(LiveEntityRecord record)
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
void RunCleanup(Action cleanup)
|
||||
{
|
||||
try
|
||||
{
|
||||
cleanup();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (_tearDownRuntimeComponents is not null)
|
||||
RunCleanup(() => _tearDownRuntimeComponents(record));
|
||||
|
||||
if (record.WorldEntity is { } entity)
|
||||
{
|
||||
RunCleanup(() => _spatial.RemoveLiveEntityProjection(record.ServerGuid));
|
||||
if (record.ResourcesRegistered)
|
||||
RunCleanup(() => _resources.Unregister(entity));
|
||||
_guidByLocalId.Remove(entity.Id);
|
||||
_materializedWorldEntitiesByGuid.Remove(record.ServerGuid);
|
||||
_visibleWorldEntitiesByGuid.Remove(record.ServerGuid);
|
||||
_animationsByLocalId.Remove(entity.Id);
|
||||
}
|
||||
|
||||
_remoteMotionByGuid.Remove(record.ServerGuid);
|
||||
record.AnimationRuntime = null;
|
||||
record.RemoteMotionRuntime = null;
|
||||
record.PhysicsBody = null;
|
||||
record.ProjectileRuntime = null;
|
||||
record.EffectProfile = null;
|
||||
record.ResourcesRegistered = false;
|
||||
record.IsSpatiallyProjected = false;
|
||||
record.IsSpatiallyVisible = false;
|
||||
record.WorldSpawnPublished = false;
|
||||
record.WorldEntity = null;
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
$"Live entity 0x{record.ServerGuid:X8} teardown failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
97
src/AcDream.App/World/LiveEntityRuntimeViews.cs
Normal file
97
src/AcDream.App/World/LiveEntityRuntimeViews.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Strongly typed, storage-free view over animation components owned by a
|
||||
/// <see cref="LiveEntityRuntime"/>. This keeps feature loops type-safe without
|
||||
/// introducing a second component dictionary.
|
||||
/// </summary>
|
||||
public sealed class LiveEntityAnimationRuntimeView<TAnimation>
|
||||
: IEnumerable<KeyValuePair<uint, TAnimation>>
|
||||
where TAnimation : class, ILiveEntityAnimationRuntime
|
||||
{
|
||||
private readonly Func<LiveEntityRuntime?> _runtime;
|
||||
|
||||
public LiveEntityAnimationRuntimeView(Func<LiveEntityRuntime?> runtime) =>
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
||||
public int Count => _runtime()?.AnimationRuntimes.Count ?? 0;
|
||||
public IEnumerable<uint> Keys =>
|
||||
_runtime()?.AnimationRuntimes.Keys ?? Array.Empty<uint>();
|
||||
|
||||
public TAnimation this[uint localEntityId]
|
||||
{
|
||||
set
|
||||
{
|
||||
LiveEntityRuntime runtime = _runtime()
|
||||
?? throw new InvalidOperationException("Live entity runtime is not initialized.");
|
||||
if (!runtime.TryGetServerGuid(localEntityId, out uint guid))
|
||||
throw new InvalidOperationException($"No live entity owns local id 0x{localEntityId:X8}.");
|
||||
runtime.SetAnimationRuntime(guid, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetValue(uint localEntityId, out TAnimation animation)
|
||||
{
|
||||
if (_runtime()?.TryGetAnimationRuntime(localEntityId, out var found) == true
|
||||
&& found is TAnimation typed)
|
||||
{
|
||||
animation = typed;
|
||||
return true;
|
||||
}
|
||||
|
||||
animation = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Remove(uint localEntityId)
|
||||
{
|
||||
LiveEntityRuntime? runtime = _runtime();
|
||||
return runtime is not null
|
||||
&& runtime.TryGetServerGuid(localEntityId, out uint guid)
|
||||
&& runtime.ClearAnimationRuntime(guid);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<uint, TAnimation>> GetEnumerator()
|
||||
{
|
||||
if (_runtime() is not { } runtime)
|
||||
yield break;
|
||||
foreach (var pair in runtime.AnimationRuntimes)
|
||||
if (pair.Value is TAnimation typed)
|
||||
yield return new KeyValuePair<uint, TAnimation>(pair.Key, typed);
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>Storage-free typed view over remote-motion components.</summary>
|
||||
public sealed class LiveEntityRemoteMotionRuntimeView<TRemoteMotion>
|
||||
where TRemoteMotion : class, ILiveEntityRemoteMotionRuntime
|
||||
{
|
||||
private readonly Func<LiveEntityRuntime?> _runtime;
|
||||
|
||||
public LiveEntityRemoteMotionRuntimeView(Func<LiveEntityRuntime?> runtime) =>
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
||||
public TRemoteMotion this[uint serverGuid]
|
||||
{
|
||||
set => (_runtime()
|
||||
?? throw new InvalidOperationException("Live entity runtime is not initialized."))
|
||||
.SetRemoteMotionRuntime(serverGuid, value);
|
||||
}
|
||||
|
||||
public bool TryGetValue(uint serverGuid, out TRemoteMotion motion)
|
||||
{
|
||||
if (_runtime()?.TryGetRemoteMotionRuntime(serverGuid, out var found) == true
|
||||
&& found is TRemoteMotion typed)
|
||||
{
|
||||
motion = typed;
|
||||
return true;
|
||||
}
|
||||
|
||||
motion = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Remove(uint serverGuid) =>
|
||||
_runtime()?.ClearRemoteMotionRuntime(serverGuid) == true;
|
||||
}
|
||||
288
src/AcDream.App/World/ParentAttachmentState.cs
Normal file
288
src/AcDream.App/World/ParentAttachmentState.cs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Update-thread state machine for parent relations that may arrive before
|
||||
/// either CreateObject. Unaccepted wire events remain in arrival order;
|
||||
/// rollback history contains only relations that passed the canonical retail
|
||||
/// timestamp gate.
|
||||
/// </summary>
|
||||
public sealed class ParentAttachmentState
|
||||
{
|
||||
private readonly Dictionary<uint, Queue<ParentAttachmentRelation>> _unresolvedByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _projectionByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
||||
|
||||
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
||||
{
|
||||
_projectionByChild[relation.ChildGuid] = relation;
|
||||
_lastAcceptedByChild[relation.ChildGuid] = relation;
|
||||
}
|
||||
|
||||
public void Enqueue(ParentEvent.Parsed update)
|
||||
{
|
||||
if (!_unresolvedByChild.TryGetValue(update.ChildGuid, out Queue<ParentAttachmentRelation>? queue))
|
||||
{
|
||||
queue = new Queue<ParentAttachmentRelation>();
|
||||
_unresolvedByChild.Add(update.ChildGuid, queue);
|
||||
}
|
||||
|
||||
queue.Enqueue(new ParentAttachmentRelation(
|
||||
update.ParentGuid,
|
||||
update.ChildGuid,
|
||||
update.ParentLocation,
|
||||
update.PlacementId,
|
||||
update.ParentInstanceSequence,
|
||||
update.ChildPositionSequence));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves every now-addressable relation for one child in wire order.
|
||||
/// A future parent generation remains queued; an older generation or a
|
||||
/// rejected child POSITION_TS is discarded. Every accepted relation
|
||||
/// supersedes the preceding render projection.
|
||||
/// </summary>
|
||||
public void Resolve(
|
||||
uint childGuid,
|
||||
Func<uint, bool> isObjectKnown,
|
||||
Func<uint, ushort?> resolveInstance,
|
||||
Func<ParentEvent.Parsed, bool> accept)
|
||||
{
|
||||
if (!_unresolvedByChild.TryGetValue(childGuid, out Queue<ParentAttachmentRelation>? queue))
|
||||
return;
|
||||
int candidateCount = queue.Count;
|
||||
for (int i = 0; i < candidateCount; i++)
|
||||
{
|
||||
ParentAttachmentRelation relation = queue.Dequeue();
|
||||
ushort? parentInstance = resolveInstance(relation.ParentGuid);
|
||||
if (parentInstance is null)
|
||||
{
|
||||
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parentInstance.Value != relation.ParentInstanceSequence)
|
||||
{
|
||||
// Current parent newer than the packet: discard the stale
|
||||
// relation. Packet newer than current: retain it until that
|
||||
// parent generation is constructed.
|
||||
if (PhysicsTimestampGate.IsNewer(
|
||||
relation.ParentInstanceSequence,
|
||||
parentInstance.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Parent });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObjectKnown(childGuid))
|
||||
{
|
||||
queue.Enqueue(relation with { WaitOwner = ParentAttachmentWaitOwner.Child });
|
||||
continue;
|
||||
}
|
||||
|
||||
var update = new ParentEvent.Parsed(
|
||||
relation.ParentGuid,
|
||||
relation.ChildGuid,
|
||||
relation.ParentLocation,
|
||||
relation.PlacementId,
|
||||
relation.ParentInstanceSequence,
|
||||
relation.ChildPositionSequence);
|
||||
if (!accept(update))
|
||||
continue;
|
||||
|
||||
_projectionByChild[childGuid] = relation;
|
||||
_lastAcceptedByChild[childGuid] = relation;
|
||||
}
|
||||
|
||||
if (queue.Count == 0)
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
public bool TryGetProjection(uint childGuid, out ParentAttachmentRelation relation) =>
|
||||
_projectionByChild.TryGetValue(childGuid, out relation);
|
||||
|
||||
public void MarkProjected(uint childGuid) => _projectionByChild.Remove(childGuid);
|
||||
|
||||
public bool RestoreLastAccepted(uint childGuid)
|
||||
{
|
||||
if (!_lastAcceptedByChild.TryGetValue(childGuid, out ParentAttachmentRelation relation))
|
||||
return false;
|
||||
_projectionByChild[childGuid] = relation;
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<uint> ChildrenWaitingForParent(uint parentGuid)
|
||||
{
|
||||
var result = new HashSet<uint>();
|
||||
foreach ((uint childGuid, ParentAttachmentRelation relation) in _projectionByChild)
|
||||
{
|
||||
if (relation.ParentGuid == parentGuid)
|
||||
result.Add(childGuid);
|
||||
}
|
||||
|
||||
foreach ((uint childGuid, Queue<ParentAttachmentRelation> queue) in _unresolvedByChild)
|
||||
{
|
||||
if (queue.Any(relation => relation.ParentGuid == parentGuid))
|
||||
result.Add(childGuid);
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public void RemoveObject(uint guid)
|
||||
{
|
||||
_projectionByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
_unresolvedByChild.Remove(guid);
|
||||
|
||||
RemoveParentReferences(_projectionByChild, guid);
|
||||
RemoveParentReferences(_lastAcceptedByChild, guid);
|
||||
|
||||
uint[] children = _unresolvedByChild.Keys.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
{
|
||||
uint childGuid = children[i];
|
||||
Queue<ParentAttachmentRelation> retained = new(
|
||||
_unresolvedByChild[childGuid].Where(relation => relation.ParentGuid != guid));
|
||||
if (retained.Count == 0)
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
else
|
||||
_unresolvedByChild[childGuid] = retained;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends one logical incarnation without discarding unresolved wire events
|
||||
/// that may explicitly address the replacement/future parent generation.
|
||||
/// </summary>
|
||||
public void EndGeneration(uint guid, ushort replacementGeneration)
|
||||
{
|
||||
FilterChildCandidates(
|
||||
guid,
|
||||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||
_projectionByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveParentReferences(_projectionByChild, guid);
|
||||
RemoveParentReferences(_lastAcceptedByChild, guid);
|
||||
FilterParentCandidates(
|
||||
guid,
|
||||
relation => relation.ParentInstanceSequence == replacementGeneration
|
||||
|| PhysicsTimestampGate.IsNewer(
|
||||
replacementGeneration,
|
||||
relation.ParentInstanceSequence));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes one exact incarnation. Child-addressed candidates die with the
|
||||
/// child; candidates addressed to a strictly newer parent incarnation
|
||||
/// survive for retail's post-Create blob replay.
|
||||
/// </summary>
|
||||
public void DeleteGeneration(uint guid, ushort deletedGeneration)
|
||||
{
|
||||
FilterChildCandidates(
|
||||
guid,
|
||||
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
|
||||
_projectionByChild.Remove(guid);
|
||||
_lastAcceptedByChild.Remove(guid);
|
||||
RemoveParentReferences(_projectionByChild, guid);
|
||||
RemoveParentReferences(_lastAcceptedByChild, guid);
|
||||
FilterParentCandidates(
|
||||
guid,
|
||||
relation => PhysicsTimestampGate.IsNewer(
|
||||
deletedGeneration,
|
||||
relation.ParentInstanceSequence));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears an accepted attachment projection after Pickup or a world
|
||||
/// Position without destroying fresher unresolved ParentEvents.
|
||||
/// </summary>
|
||||
public void EndChildProjection(uint childGuid)
|
||||
{
|
||||
_projectionByChild.Remove(childGuid);
|
||||
_lastAcceptedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
public void RemoveChild(uint childGuid)
|
||||
{
|
||||
_projectionByChild.Remove(childGuid);
|
||||
_lastAcceptedByChild.Remove(childGuid);
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_unresolvedByChild.Clear();
|
||||
_projectionByChild.Clear();
|
||||
_lastAcceptedByChild.Clear();
|
||||
}
|
||||
|
||||
private static void RemoveParentReferences(
|
||||
Dictionary<uint, ParentAttachmentRelation> relations,
|
||||
uint parentGuid)
|
||||
{
|
||||
uint[] children = relations
|
||||
.Where(pair => pair.Value.ParentGuid == parentGuid)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
relations.Remove(children[i]);
|
||||
}
|
||||
|
||||
private void FilterParentCandidates(
|
||||
uint parentGuid,
|
||||
Func<ParentAttachmentRelation, bool> retain)
|
||||
{
|
||||
uint[] children = _unresolvedByChild.Keys.ToArray();
|
||||
for (int i = 0; i < children.Length; i++)
|
||||
{
|
||||
uint childGuid = children[i];
|
||||
Queue<ParentAttachmentRelation> queue = _unresolvedByChild[childGuid];
|
||||
var retained = new Queue<ParentAttachmentRelation>(
|
||||
queue.Where(relation => relation.ParentGuid != parentGuid || retain(relation)));
|
||||
if (retained.Count == 0)
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
else
|
||||
_unresolvedByChild[childGuid] = retained;
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterChildCandidates(
|
||||
uint childGuid,
|
||||
Func<ParentAttachmentRelation, bool> retain)
|
||||
{
|
||||
if (!_unresolvedByChild.TryGetValue(
|
||||
childGuid,
|
||||
out Queue<ParentAttachmentRelation>? queue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var retained = new Queue<ParentAttachmentRelation>(queue.Where(retain));
|
||||
if (retained.Count == 0)
|
||||
_unresolvedByChild.Remove(childGuid);
|
||||
else
|
||||
_unresolvedByChild[childGuid] = retained;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct ParentAttachmentRelation(
|
||||
uint ParentGuid,
|
||||
uint ChildGuid,
|
||||
uint ParentLocation,
|
||||
uint PlacementId,
|
||||
ushort ParentInstanceSequence,
|
||||
ushort ChildPositionSequence,
|
||||
ParentAttachmentWaitOwner WaitOwner = ParentAttachmentWaitOwner.Unknown);
|
||||
|
||||
public enum ParentAttachmentWaitOwner
|
||||
{
|
||||
Unknown,
|
||||
Parent,
|
||||
Child,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue