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:
Erik 2026-07-14 07:47:03 +02:00
parent 8a5d77f7f4
commit 8dd996053d
24 changed files with 2449 additions and 631 deletions

View file

@ -1,5 +1,5 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.Meshing;
using AcDream.Core.Net;
@ -24,13 +24,10 @@ public sealed class EquippedChildRenderController : IDisposable
private readonly DatCollection _dats;
private readonly object _datLock;
private readonly ClientObjectTable _objects;
private readonly GpuWorldState _worldState;
private readonly Func<uint, WorldEntity?> _resolveEntity;
private readonly Func<uint, WorldSession.EntitySpawn?> _resolveSpawn;
private readonly LiveEntityRuntime _liveEntities;
private readonly Func<ParentEvent.Parsed, bool> _acceptParent;
private readonly Func<uint> _nextEntityId;
private readonly ParentAttachmentState _relations = new();
private ParentAttachmentState Relations => _liveEntities.ParentAttachments;
private readonly Dictionary<uint, AttachedChild> _attachedByChild = new();
public IEnumerable<uint> AttachedEntityIds
@ -46,20 +43,14 @@ public sealed class EquippedChildRenderController : IDisposable
DatCollection dats,
object datLock,
ClientObjectTable objects,
GpuWorldState worldState,
Func<uint, WorldEntity?> resolveEntity,
Func<uint, WorldSession.EntitySpawn?> resolveSpawn,
Func<ParentEvent.Parsed, bool> acceptParent,
Func<uint> nextEntityId)
LiveEntityRuntime liveEntities,
Func<ParentEvent.Parsed, bool> acceptParent)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_resolveEntity = resolveEntity ?? throw new ArgumentNullException(nameof(resolveEntity));
_resolveSpawn = resolveSpawn ?? throw new ArgumentNullException(nameof(resolveSpawn));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_acceptParent = acceptParent ?? throw new ArgumentNullException(nameof(acceptParent));
_nextEntityId = nextEntityId ?? throw new ArgumentNullException(nameof(nextEntityId));
_objects.ObjectMoved += OnObjectMoved;
_objects.MoveRolledBack += OnMoveRolledBack;
@ -79,7 +70,7 @@ public sealed class EquippedChildRenderController : IDisposable
&& spawn.ParentLocation is { } parentLocation
&& spawn.PlacementId is { } placementId)
{
_relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
Relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
parentGuid,
spawn.Guid,
parentLocation,
@ -93,7 +84,7 @@ public sealed class EquippedChildRenderController : IDisposable
// ParentEvent can precede the parent's CreateObject. Revisit every
// child waiting specifically on the object that just arrived.
IReadOnlyList<uint> waiting = _relations.ChildrenWaitingForParent(spawn.Guid);
IReadOnlyList<uint> waiting = Relations.ChildrenWaitingForParent(spawn.Guid);
for (int i = 0; i < waiting.Count; i++)
{
ResolveRelations(waiting[i]);
@ -112,7 +103,7 @@ public sealed class EquippedChildRenderController : IDisposable
{
lock (_datLock)
{
IReadOnlyList<uint> waiting = _relations.ChildrenWaitingForParent(guid);
IReadOnlyList<uint> waiting = Relations.ChildrenWaitingForParent(guid);
for (int i = 0; i < waiting.Count; i++)
{
ResolveRelations(waiting[i]);
@ -130,7 +121,7 @@ public sealed class EquippedChildRenderController : IDisposable
{
lock (_datLock)
{
_relations.Enqueue(update);
Relations.Enqueue(update);
ResolveRelations(update.ChildGuid);
TryRealize(update.ChildGuid);
}
@ -139,13 +130,11 @@ public sealed class EquippedChildRenderController : IDisposable
public void OnGenerationReplaced(uint guid, ushort replacementGeneration)
{
TearDownObjectProjections(guid);
_relations.EndGeneration(guid, replacementGeneration);
}
public void OnGenerationDeleted(uint guid, ushort deletedGeneration)
{
TearDownObjectProjections(guid);
_relations.DeleteGeneration(guid, deletedGeneration);
}
private void OnObjectRemovalClassified(ClientObjectRemoval removal)
@ -160,7 +149,7 @@ public sealed class EquippedChildRenderController : IDisposable
// same-generation world/parent update can still replay them. Logical
// delete/replacement are driven directly by the accepted inbound
// lifecycle and never depend on this table.
_relations.EndChildProjection(guid);
Relations.EndChildProjection(guid);
}
/// <summary>
@ -172,7 +161,7 @@ public sealed class EquippedChildRenderController : IDisposable
public void OnChildBecameUnparented(uint childGuid)
{
Remove(childGuid);
_relations.EndChildProjection(childGuid);
Relations.EndChildProjection(childGuid);
}
/// <summary>Recompose every child after the parent's animation tick.</summary>
@ -180,8 +169,7 @@ public sealed class EquippedChildRenderController : IDisposable
{
foreach (AttachedChild child in _attachedByChild.Values)
{
WorldEntity? parent = _resolveEntity(child.ParentGuid);
if (parent is null)
if (!_liveEntities.TryGetWorldEntity(child.ParentGuid, out WorldEntity parent))
continue;
if (!EquippedChildAttachment.TryCompose(
@ -199,22 +187,23 @@ public sealed class EquippedChildRenderController : IDisposable
child.Entity.SetPosition(parent.Position);
child.Entity.Rotation = parent.Rotation;
child.Entity.ParentCellId = parent.ParentCellId;
if (parent.ParentCellId is { } parentCellId)
_liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId);
}
}
private void TryRealize(uint childGuid)
{
if (!_relations.TryGetProjection(childGuid, out ParentAttachmentRelation pending))
if (!Relations.TryGetProjection(childGuid, out ParentAttachmentRelation pending))
return;
WorldEntity? parentEntity = _resolveEntity(pending.ParentGuid);
WorldSession.EntitySpawn? parentSpawn = _resolveSpawn(pending.ParentGuid);
WorldSession.EntitySpawn? childSpawn = _resolveSpawn(childGuid);
if (parentEntity is null || parentSpawn is null || childSpawn is null)
if (!_liveEntities.TryGetWorldEntity(pending.ParentGuid, out WorldEntity parentEntity)
|| !_liveEntities.TryGetSnapshot(pending.ParentGuid, out WorldSession.EntitySpawn parentSpawn)
|| !_liveEntities.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn childSpawn))
return;
if (parentSpawn.Value.SetupTableId is not { } parentSetupId
|| childSpawn.Value.SetupTableId is not { } childSetupId
if (parentSpawn.SetupTableId is not { } parentSetupId
|| childSpawn.SetupTableId is not { } childSetupId
|| parentEntity.ParentCellId is not { } parentCellId)
return;
@ -225,8 +214,8 @@ public sealed class EquippedChildRenderController : IDisposable
var parentLocation = (ParentLocation)pending.ParentLocation;
var placement = (Placement)pending.PlacementId;
IReadOnlyList<MeshRef> template = BuildPartTemplate(childSetup, childSpawn.Value);
float scale = childSpawn.Value.ObjScale is { } objScale && objScale > 0f
IReadOnlyList<MeshRef> template = BuildPartTemplate(childSetup, childSpawn);
float scale = childSpawn.ObjScale is { } objScale && objScale > 0f
? objScale
: 1.0f;
@ -242,19 +231,32 @@ public sealed class EquippedChildRenderController : IDisposable
return;
Remove(childGuid);
var entity = new WorldEntity
{
Id = _nextEntityId(),
ServerGuid = childGuid,
SourceGfxObjOrSetupId = childSetupId,
Position = parentEntity.Position,
Rotation = parentEntity.Rotation,
MeshRefs = parts,
PaletteOverride = BuildPaletteOverride(childSpawn.Value),
ParentCellId = parentCellId,
};
_worldState.AppendLiveEntity(parentCellId, entity);
WorldEntity? entity = _liveEntities.MaterializeLiveEntity(
childGuid,
parentCellId,
localId => new WorldEntity
{
Id = localId,
ServerGuid = childGuid,
SourceGfxObjOrSetupId = childSetupId,
Position = parentEntity.Position,
Rotation = parentEntity.Rotation,
MeshRefs = parts,
PaletteOverride = BuildPaletteOverride(childSpawn),
ParentCellId = parentCellId,
},
LiveEntityProjectionKind.Attached);
if (entity is null)
return;
entity.SetPosition(parentEntity.Position);
entity.Rotation = parentEntity.Rotation;
entity.ParentCellId = parentCellId;
entity.ApplyAppearance(
parts,
BuildPaletteOverride(childSpawn),
childSpawn.AnimPartChanges is { Count: > 0 } changes
? changes.Select(change => new PartOverride(change.PartIndex, change.NewModelId)).ToArray()
: Array.Empty<PartOverride>());
_attachedByChild[childGuid] = new AttachedChild(
pending.ParentGuid,
childGuid,
@ -268,15 +270,17 @@ public sealed class EquippedChildRenderController : IDisposable
Console.WriteLine(
$"equipment: attached child=0x{childGuid:X8} parent=0x{pending.ParentGuid:X8} " +
$"location={parentLocation} placement={placement}");
_relations.MarkProjected(childGuid);
Relations.MarkProjected(childGuid);
}
private void ResolveRelations(uint childGuid)
{
_relations.Resolve(
Relations.Resolve(
childGuid,
guid => _resolveSpawn(guid) is not null,
guid => _resolveSpawn(guid)?.InstanceSequence,
guid => _liveEntities.TryGetSnapshot(guid, out _),
guid => _liveEntities.TryGetSnapshot(guid, out WorldSession.EntitySpawn spawn)
? spawn.InstanceSequence
: null,
_acceptParent);
}
@ -370,7 +374,7 @@ public sealed class EquippedChildRenderController : IDisposable
// A rejected unwield restores the equipped location without a fresh
// wire ParentEvent; reinstall the last accepted relationship only for
// this explicit rollback signal.
if (_relations.RestoreLastAccepted(item.ObjectId))
if (Relations.RestoreLastAccepted(item.ObjectId))
{
lock (_datLock)
TryRealize(item.ObjectId);
@ -381,7 +385,7 @@ public sealed class EquippedChildRenderController : IDisposable
{
if (!_attachedByChild.Remove(childGuid))
return;
_worldState.RemoveEntityByServerGuid(childGuid);
_liveEntities.WithdrawLiveEntityProjection(childGuid);
}
private void TearDownObjectProjections(uint guid)
@ -401,7 +405,7 @@ public sealed class EquippedChildRenderController : IDisposable
uint[] attached = _attachedByChild.Keys.ToArray();
for (int i = 0; i < attached.Length; i++)
Remove(attached[i]);
_relations.Clear();
Relations.Clear();
}
public void Dispose()