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:
parent
d3e96ff912
commit
ce3ac310d9
23 changed files with 2352 additions and 666 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue