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
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue