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>
102 lines
3.5 KiB
C#
102 lines
3.5 KiB
C#
using AcDream.App.Input;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Runtime.Entities;
|
|
|
|
namespace AcDream.App.World;
|
|
|
|
internal interface ILiveEntityPruneSink
|
|
{
|
|
bool Prune(LiveEntityPruneCandidate candidate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns authoritative and expiry-driven live-object deletion through one
|
|
/// generation-safe runtime transaction.
|
|
/// </summary>
|
|
internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink
|
|
{
|
|
private readonly LiveEntityRuntime _runtime;
|
|
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
|
private readonly ILiveEntityTeardownCoordinator _teardown;
|
|
private readonly ILocalPlayerIdentitySource _identity;
|
|
private readonly DormantLiveEntityStore _dormant;
|
|
private readonly Action<string>? _diagnostic;
|
|
|
|
public LiveEntityDeletionController(
|
|
LiveEntityRuntime runtime,
|
|
RuntimeEntityObjectLifetime entityObjects,
|
|
ILiveEntityTeardownCoordinator teardown,
|
|
ILocalPlayerIdentitySource identity,
|
|
DormantLiveEntityStore? dormant = null,
|
|
Action<string>? diagnostic = null)
|
|
{
|
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
|
_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();
|
|
_diagnostic = diagnostic;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>SmartBox::HandleDeleteObject @ 0x00451EA0</c> rejects the
|
|
/// player, requires the exact Instance timestamp, then performs one
|
|
/// symmetric logical teardown.
|
|
/// </summary>
|
|
public bool Delete(DeleteObject.Parsed delete)
|
|
{
|
|
if (delete.Guid == _identity.ServerGuid)
|
|
return false;
|
|
|
|
bool hasActiveRecord = _runtime.TryGetRecord(delete.Guid, out _);
|
|
if (!hasActiveRecord)
|
|
_teardown.ForgetUnknownOwner(delete.Guid);
|
|
|
|
bool removed = _runtime.UnregisterLiveEntity(
|
|
delete,
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true);
|
|
bool removedDormant = _dormant.RemoveExact(delete);
|
|
if (!removed && removedDormant)
|
|
_entityObjects.ApplyAcceptedDormantDelete(delete);
|
|
if (removed)
|
|
{
|
|
_dormant.RemoveExact(delete);
|
|
_diagnostic?.Invoke(
|
|
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
|
|
}
|
|
else if (removedDormant)
|
|
{
|
|
_diagnostic?.Invoke(
|
|
$"live: delete dormant guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
|
|
}
|
|
return removed || removedDormant;
|
|
}
|
|
|
|
public bool Prune(LiveEntityPruneCandidate candidate)
|
|
{
|
|
if (!_runtime.TryGetRecord(
|
|
candidate.Key,
|
|
out LiveEntityRecord record)
|
|
|| record.ServerGuid != candidate.ServerGuid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
WorldSession.EntitySpawn snapshot = record.Snapshot;
|
|
bool removed = _runtime.UnregisterLiveEntity(
|
|
new DeleteObject.Parsed(
|
|
candidate.ServerGuid,
|
|
candidate.Generation),
|
|
isLocalPlayer: false);
|
|
if (!removed)
|
|
return false;
|
|
|
|
_dormant.Retain(snapshot);
|
|
_diagnostic?.Invoke(
|
|
$"live: dormant guid=0x{candidate.ServerGuid:X8} instSeq={candidate.Generation}");
|
|
return true;
|
|
}
|
|
}
|