refactor(runtime): own canonical entity and object lifetime

Introduce one presentation-free RuntimeEntityObjectLifetime for the exact entity directory and ClientObjectTable. Make GameWindow, graphical projections, retained UI, interaction, session routing, create/delete integration, and reset borrow that owner while preserving synchronous retail ordering, dormant retention, and retry semantics.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 05:54:46 +02:00
parent d9bf4c4960
commit 5ef8b5371d
40 changed files with 712 additions and 170 deletions

View file

@ -0,0 +1,135 @@
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.Runtime.Entities;
/// <summary>
/// One exact DeleteObject acceptance issued by a
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
/// active identity before returning this token to the Runtime owner for the
/// retained-object mutation, preserving retail callback order.
/// </summary>
public sealed class RuntimeEntityDeleteAcceptance
{
internal RuntimeEntityDeleteAcceptance(
RuntimeEntityObjectLifetime owner,
DeleteObject.Parsed delete)
{
Owner = owner;
Delete = delete;
}
internal RuntimeEntityObjectLifetime Owner { get; }
internal bool Completed { get; set; }
public DeleteObject.Parsed Delete { get; }
}
/// <summary>
/// Presentation-free ownership root for one session's canonical entity and
/// retained object lifetimes. Graphical and direct hosts borrow these exact
/// instances; they never allocate a second directory or object table.
/// </summary>
public sealed class RuntimeEntityObjectLifetime
{
public RuntimeEntityObjectLifetime(
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
Objects = new ClientObjectTable();
}
public RuntimeEntityDirectory Entities { get; }
public ClientObjectTable Objects { get; }
/// <summary>
/// Applies the retained-object half of an accepted CreateObject while the
/// exact canonical incarnation and its integration version remain current.
/// Object-table callbacks are synchronous and may re-enter entity lifetime,
/// so the acceptance predicate is checked before, during, and after apply.
/// </summary>
public bool ApplyAcceptedSpawn(
RuntimeEntityRecord canonical,
ulong expectedCreateIntegrationVersion,
WorldSession.EntitySpawn spawn,
bool replaceGeneration)
{
ArgumentNullException.ThrowIfNull(canonical);
if (canonical.ServerGuid != spawn.Guid
|| canonical.Incarnation != spawn.InstanceSequence)
{
return false;
}
bool IsCurrent() =>
Entities.IsCurrent(canonical)
&& canonical.CreateIntegrationVersion
== expectedCreateIntegrationVersion;
return IsCurrent()
&& ObjectTableWiring.ApplyEntitySpawn(
Objects,
spawn,
replaceGeneration,
IsCurrent)
&& IsCurrent();
}
/// <summary>
/// Accepts the exact DeleteObject generation without yet publishing the
/// retained-object removal. This split lets the graphical host retire the
/// active identity first, matching the established reentrant callback
/// order, while Runtime remains the only freshness authority.
/// </summary>
public bool TryAcceptDelete(
DeleteObject.Parsed delete,
bool isLocalPlayer,
out RuntimeEntityDeleteAcceptance acceptance)
{
if (!Entities.TryDelete(delete, isLocalPlayer))
{
acceptance = null!;
return false;
}
acceptance = new RuntimeEntityDeleteAcceptance(this, delete);
return true;
}
/// <summary>
/// Completes a previously accepted delete exactly once. Marking the token
/// complete before synchronous callbacks preserves the current retry rule:
/// a throwing observer does not replay an already-committed removal.
/// </summary>
public void CompleteAcceptedDelete(
RuntimeEntityDeleteAcceptance acceptance)
{
ArgumentNullException.ThrowIfNull(acceptance);
if (!ReferenceEquals(acceptance.Owner, this))
{
throw new InvalidOperationException(
"A delete acceptance belongs to a different Runtime lifetime.");
}
if (acceptance.Completed)
{
throw new InvalidOperationException(
"A delete acceptance has already been completed.");
}
acceptance.Completed = true;
ObjectTableWiring.ApplyEntityDelete(Objects, acceptance.Delete);
}
/// <summary>
/// Applies a delete accepted by the dormant exact-incarnation owner after
/// the active Runtime directory correctly reports no live record.
/// </summary>
public void ApplyAcceptedDormantDelete(DeleteObject.Parsed delete) =>
ObjectTableWiring.ApplyEntityDelete(Objects, delete);
/// <summary>
/// Clears retained object state at the Runtime-owned reset stage. App
/// projection teardown remains a later acknowledged stage.
/// </summary>
public void ClearObjects() => Objects.Clear();
}