acdream/src/AcDream.App/World/LiveEntityDeletionController.cs
Erik 5ef8b5371d refactor(runtime): own canonical entity and object lifetime
Introduce one presentation-free RuntimeEntityObjectLifetime for the exact entity directory and ClientObjectTable. Make GameWindow, graphical projections, retained UI, interaction, session routing, create/delete integration, and reset borrow that owner while preserving synchronous retail ordering, dormant retention, and retry semantics.

Co-authored-by: Codex <codex@openai.com>
2026-07-26 05:54:46 +02:00

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;
}
}