refactor(runtime): publish canonical entity object deltas

Issue stable Runtime identities at canonical registration, publish entity and inventory commits through one generation-stamped synchronous stream, and make graphical adapters borrow the same direct views and events as a no-window host. Preserve exact projection teardown and retail mutation order while removing App-side event reconstruction.

Make the hard-recenter ordering fixture independent of the production two-millisecond frame budget so its injected-failure gate is deterministic.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 06:42:13 +02:00
parent d3e96ff912
commit ce3ac310d9
23 changed files with 2352 additions and 666 deletions

View file

@ -5,9 +5,9 @@ using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Chat;
using AcDream.Core.Items;
using AcDream.Core.Selection;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Session;
using AcDream.UI.Abstractions;
@ -35,7 +35,7 @@ internal sealed class CurrentGameRuntimeAdapter
ICommandBus commands,
LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
ClientObjectTable objects,
RuntimeEntityObjectLifetime entityObjects,
ChatLog chat,
ILocalPlayerControllerSource playerController,
WorldRevealCoordinator worldReveal,
@ -49,17 +49,18 @@ internal sealed class CurrentGameRuntimeAdapter
session,
playerIdentity,
entities,
objects,
entityObjects,
chat,
playerController,
worldReveal,
clock);
entityObjects.BindEventContext(
() => _view.Generation,
() => clock.FrameNumber);
_events = new CurrentGameRuntimeEventAdapter(
_view,
entities,
objects,
chat,
clock);
entityObjects,
chat);
_commands = new CurrentGameRuntimeCommandAdapter(
session,
sessionHost,

View file

@ -1,8 +1,6 @@
using AcDream.App.World;
using AcDream.Core.Chat;
using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
namespace AcDream.App.Runtime;
@ -26,31 +24,30 @@ internal interface ICurrentGameRuntimeEventSink
internal sealed class CurrentGameRuntimeEventAdapter
: IRuntimeEventSource,
ICurrentGameRuntimeEventSink,
IRuntimeEntityObjectObserver,
IDisposable
{
private readonly CurrentGameRuntimeViewAdapter _view;
private readonly LiveEntityRuntime _entities;
private readonly ClientObjectTable _objects;
private readonly RuntimeEntityObjectLifetime _entityObjects;
private readonly ChatLog _chat;
private readonly IGameRuntimeClock _clock;
private readonly RuntimeEventSequencer _events = new();
private readonly object _observerGate = new();
private IRuntimeEventObserver[] _observers = [];
private IDisposable? _entityObjectSubscription;
private bool _ownerEventsAttached;
private bool _disposed;
internal long DispatchFailureCount { get; private set; }
internal Exception? LastDispatchFailure { get; private set; }
public CurrentGameRuntimeEventAdapter(
CurrentGameRuntimeViewAdapter view,
LiveEntityRuntime entities,
ClientObjectTable objects,
ChatLog chat,
IGameRuntimeClock clock)
RuntimeEntityObjectLifetime entityObjects,
ChatLog chat)
{
_view = view ?? throw new ArgumentNullException(nameof(view));
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_entityObjects = entityObjects
?? throw new ArgumentNullException(nameof(entityObjects));
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
}
public IDisposable Subscribe(IRuntimeEventObserver observer)
@ -106,7 +103,16 @@ internal sealed class CurrentGameRuntimeEventAdapter
primaryObjectId,
text);
foreach (IRuntimeEventObserver observer in observers)
observer.OnCommand(in delta);
{
try
{
observer.OnCommand(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
}
public void EmitLifecycle(RuntimeLifecycleState previous)
@ -123,7 +129,16 @@ internal sealed class CurrentGameRuntimeEventAdapter
previous,
current);
foreach (IRuntimeEventObserver observer in observers)
observer.OnLifecycle(in delta);
{
try
{
observer.OnLifecycle(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
}
private void Unsubscribe(IRuntimeEventObserver observer)
@ -159,12 +174,7 @@ internal sealed class CurrentGameRuntimeEventAdapter
private void AttachOwnerEvents()
{
_entities.ProjectionVisibilityChanged += OnEntityVisibilityChanged;
_objects.ObjectAdded += OnObjectAdded;
_objects.ObjectUpdated += OnObjectUpdated;
_objects.ObjectMoved += OnObjectMoved;
_objects.ObjectRemovalClassified += OnObjectRemoved;
_objects.Cleared += OnObjectsCleared;
_entityObjectSubscription = _entityObjects.Events.Subscribe(this);
_chat.EntryAppended += OnChatEntry;
_ownerEventsAttached = true;
}
@ -174,79 +184,43 @@ internal sealed class CurrentGameRuntimeEventAdapter
if (!_ownerEventsAttached)
return;
_chat.EntryAppended -= OnChatEntry;
_objects.Cleared -= OnObjectsCleared;
_objects.ObjectRemovalClassified -= OnObjectRemoved;
_objects.ObjectMoved -= OnObjectMoved;
_objects.ObjectUpdated -= OnObjectUpdated;
_objects.ObjectAdded -= OnObjectAdded;
_entities.ProjectionVisibilityChanged -= OnEntityVisibilityChanged;
_entityObjectSubscription?.Dispose();
_entityObjectSubscription = null;
_ownerEventsAttached = false;
}
private void OnEntityVisibilityChanged(LiveEntityRecord record, bool visible)
public void OnEntity(in RuntimeEntityDelta delta)
{
if (!TryObservers(out IRuntimeEventObserver[] observers))
return;
RuntimeEntityChange change = visible
? RuntimeEntityChange.Updated
: (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
? RuntimeEntityChange.Hidden
: RuntimeEntityChange.Withdrawn;
var delta = new RuntimeEntityDelta(
NextStamp(),
change,
CurrentGameRuntimeViewAdapter.Snapshot(record));
foreach (IRuntimeEventObserver observer in observers)
observer.OnEntity(in delta);
{
try
{
observer.OnEntity(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
}
private void OnObjectAdded(ClientObject item) =>
EmitInventory(RuntimeInventoryChange.Added, item);
private void OnObjectUpdated(ClientObject item) =>
EmitInventory(RuntimeInventoryChange.Updated, item);
private void OnObjectMoved(ClientObjectMove move)
{
ClientObject? item = move.Item ?? _objects.Get(move.ItemId);
if (item is not null)
EmitInventory(RuntimeInventoryChange.Moved, item);
}
private void OnObjectRemoved(ClientObjectRemoval removal) =>
EmitInventory(
RuntimeInventoryChange.Removed,
removal.Object,
removal.Generation);
private void OnObjectsCleared()
public void OnInventory(in RuntimeInventoryDelta delta)
{
if (!TryObservers(out IRuntimeEventObserver[] observers))
return;
var delta = new RuntimeInventoryDelta(
NextStamp(),
RuntimeInventoryChange.Cleared,
default);
foreach (IRuntimeEventObserver observer in observers)
observer.OnInventory(in delta);
}
private void EmitInventory(
RuntimeInventoryChange change,
ClientObject item,
ushort? exactGeneration = null)
{
if (!TryObservers(out IRuntimeEventObserver[] observers))
return;
var delta = new RuntimeInventoryDelta(
NextStamp(),
change,
CurrentGameRuntimeViewAdapter.Snapshot(
item,
_entities,
exactGeneration));
foreach (IRuntimeEventObserver observer in observers)
observer.OnInventory(in delta);
{
try
{
observer.OnInventory(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
}
private void OnChatEntry(ChatEntry entry)
@ -263,11 +237,20 @@ internal sealed class CurrentGameRuntimeEventAdapter
entry.Text,
entry.ChannelName));
foreach (IRuntimeEventObserver observer in observers)
observer.OnChat(in delta);
{
try
{
observer.OnChat(in delta);
}
catch (Exception error)
{
RecordDispatchFailure(error);
}
}
}
private RuntimeEventStamp NextStamp() =>
_events.Next(_view.Generation, _clock.FrameNumber);
_entityObjects.Events.NextStamp();
private bool TryObservers(out IRuntimeEventObserver[] observers)
{
@ -275,6 +258,12 @@ internal sealed class CurrentGameRuntimeEventAdapter
return observers.Length != 0;
}
private void RecordDispatchFailure(Exception error)
{
DispatchFailureCount++;
LastDispatchFailure = error;
}
private sealed class ObserverSubscription(
CurrentGameRuntimeEventAdapter owner,
IRuntimeEventObserver observer)

View file

@ -5,6 +5,7 @@ using AcDream.App.World;
using AcDream.Core.Chat;
using AcDream.Core.Items;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Session;
namespace AcDream.App.Runtime;
@ -21,8 +22,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
private readonly ClientObjectTable _objects;
private readonly ChatLog _chat;
private readonly IGameRuntimeClock _clock;
private readonly EntityView _entityView;
private readonly InventoryView _inventoryView;
private readonly IRuntimeEntityView _entityView;
private readonly IRuntimeInventoryView _inventoryView;
private readonly ChatView _chatView;
private readonly MovementView _movementView;
private readonly PortalView _portalView;
@ -32,7 +33,7 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
LiveSessionController session,
LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
ClientObjectTable objects,
RuntimeEntityObjectLifetime entityObjects,
ChatLog chat,
ILocalPlayerControllerSource playerController,
WorldRevealCoordinator worldReveal,
@ -42,12 +43,13 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
_playerIdentity = playerIdentity
?? throw new ArgumentNullException(nameof(playerIdentity));
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
ArgumentNullException.ThrowIfNull(entityObjects);
_objects = entityObjects.Objects;
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_entityView = new EntityView(_entities);
_inventoryView = new InventoryView(_objects, _entities);
_entityView = entityObjects.EntityView;
_inventoryView = entityObjects.InventoryView;
_chatView = new ChatView(_chat);
_movementView = new MovementView(
playerController
@ -108,103 +110,6 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
internal void Deactivate() => _active = false;
internal static RuntimeEntitySnapshot Snapshot(LiveEntityRecord record) =>
new(
new RuntimeEntityIdentity(
record.ServerGuid,
record.LocalEntityId ?? 0u,
record.Generation),
record.FullCellId,
(uint)record.FinalPhysicsState,
record.PhysicsBody?.CellPosition,
record.LocalEntityId.HasValue,
record.IsSpatiallyVisible,
record.InitialHydrationCompleted);
internal static RuntimeInventoryItemSnapshot Snapshot(
ClientObject item,
LiveEntityRuntime entities,
ushort? exactGeneration = null)
{
ushort generation = exactGeneration
?? (entities.TryGetRecord(item.ObjectId, out LiveEntityRecord record)
? record.Generation
: (ushort)0);
return new RuntimeInventoryItemSnapshot(
item.ObjectId,
generation,
item.Name,
item.ContainerId,
item.ContainerSlot,
item.WielderId,
(uint)item.CurrentlyEquippedLocation,
item.StackSize,
item.Value);
}
private sealed class EntityView(LiveEntityRuntime owner)
: IRuntimeEntityView
{
public int Count => owner.Count;
public int MaterializedCount => owner.MaterializedCount;
public bool TryGet(uint serverGuid, out RuntimeEntitySnapshot entity)
{
if (owner.TryGetRecord(serverGuid, out LiveEntityRecord record))
{
entity = Snapshot(record);
return true;
}
entity = default;
return false;
}
public void Visit(IRuntimeEntityVisitor visitor)
{
ArgumentNullException.ThrowIfNull(visitor);
foreach (LiveEntityRecord record in owner.Records)
{
RuntimeEntitySnapshot entity = Snapshot(record);
visitor.Visit(in entity);
}
}
}
private sealed class InventoryView(
ClientObjectTable owner,
LiveEntityRuntime entities)
: IRuntimeInventoryView
{
public int ObjectCount => owner.ObjectCount;
public int ContainerCount => owner.ContainerCount;
public bool TryGet(
uint objectId,
out RuntimeInventoryItemSnapshot item)
{
ClientObject? found = owner.Get(objectId);
if (found is not null)
{
item = Snapshot(found, entities);
return true;
}
item = default;
return false;
}
public void Visit(IRuntimeInventoryVisitor visitor)
{
ArgumentNullException.ThrowIfNull(visitor);
foreach (ClientObject item in owner.Objects)
{
RuntimeInventoryItemSnapshot snapshot = Snapshot(item, entities);
visitor.Visit(in snapshot);
}
}
}
private sealed class ChatView(ChatLog owner) : IRuntimeChatView
{
public long Revision => owner.Revision;