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
|
|
@ -17,6 +17,7 @@ using AcDream.Core.Physics;
|
|||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
|
@ -174,6 +175,172 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Assert.Equal(RuntimeLifecycleState.Stopped, harness.Runtime.Lifecycle.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectAndGraphicalHosts_ProduceIdenticalEntityObjectTrace()
|
||||
{
|
||||
WorldSession.EntitySpawn spawn =
|
||||
Spawn(Harness.TargetGuid, instance: 7, cell: 0x12340001u);
|
||||
var direct = new RuntimeEntityObjectLifetime();
|
||||
direct.BindEventContext(static () => default, static () => 0UL);
|
||||
var directTrace = new EntityObjectTrace();
|
||||
using IDisposable directSubscription =
|
||||
direct.Events.Subscribe(directTrace);
|
||||
|
||||
RuntimeEntityRegistrationResult directRegistration =
|
||||
direct.RegisterEntity(spawn);
|
||||
RuntimeEntityRecord directCanonical =
|
||||
Assert.IsType<RuntimeEntityRecord>(directRegistration.Canonical);
|
||||
direct.Objects.AddOrUpdate(Object(spawn));
|
||||
direct.Objects.MoveItem(
|
||||
spawn.Guid,
|
||||
Harness.PlayerGuid,
|
||||
newSlot: 3);
|
||||
var vector = new VectorUpdate.Parsed(
|
||||
spawn.Guid,
|
||||
new Vector3(1f, 2f, 3f),
|
||||
new Vector3(0f, 0f, 0.25f),
|
||||
spawn.InstanceSequence,
|
||||
VectorSequence: 2);
|
||||
Assert.True(direct.TryApplyVector(
|
||||
vector,
|
||||
acknowledgeProjection: null,
|
||||
out _));
|
||||
var state = new SetState.Parsed(
|
||||
spawn.Guid,
|
||||
(uint)(PhysicsStateFlags.ReportCollisions
|
||||
| PhysicsStateFlags.Hidden),
|
||||
spawn.InstanceSequence,
|
||||
StateSequence: 2);
|
||||
Assert.True(direct.TryApplyState(
|
||||
state,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _));
|
||||
WorldSession.EntityPositionUpdate position =
|
||||
Position(spawn.Guid, spawn.InstanceSequence);
|
||||
Assert.True(direct.TryApplyPosition(
|
||||
position,
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
projectionRequiresTeleportHook: false,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(direct.TryApplyPickup(
|
||||
new PickupEvent.Parsed(
|
||||
spawn.Guid,
|
||||
spawn.InstanceSequence,
|
||||
PositionSequence: 3),
|
||||
acknowledgeProjection: null,
|
||||
out _));
|
||||
Assert.True(direct.TryAcceptDelete(
|
||||
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
|
||||
isLocalPlayer: false,
|
||||
removeRetainedObject: true,
|
||||
out RuntimeEntityDeleteAcceptance directDelete));
|
||||
direct.CompleteAcceptedDelete(directDelete);
|
||||
Assert.Null(direct.RetireCanonicalOnly(directCanonical));
|
||||
|
||||
using var graphical = new Harness();
|
||||
var graphicalTrace = new EntityObjectTrace();
|
||||
using IDisposable graphicalSubscription =
|
||||
graphical.EntityObjects.Events.Subscribe(graphicalTrace);
|
||||
_ = graphical.Entities.RegisterAndMaterializeProjection(spawn);
|
||||
graphical.Objects.AddOrUpdate(Object(spawn));
|
||||
graphical.Objects.MoveItem(
|
||||
spawn.Guid,
|
||||
Harness.PlayerGuid,
|
||||
newSlot: 3);
|
||||
Assert.True(graphical.Entities.TryApplyVector(vector, out _));
|
||||
Assert.True(graphical.Entities.TryApplyState(state, out _, out _));
|
||||
Assert.True(graphical.Entities.TryApplyPosition(
|
||||
position,
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
out _,
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(graphical.Entities.TryApplyPickup(
|
||||
new PickupEvent.Parsed(
|
||||
spawn.Guid,
|
||||
spawn.InstanceSequence,
|
||||
PositionSequence: 3),
|
||||
out _));
|
||||
Assert.True(graphical.Entities.UnregisterLiveEntity(
|
||||
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
|
||||
isLocalPlayer: false,
|
||||
removeRetainedObject: true));
|
||||
|
||||
Assert.Equal(directTrace.Entries, graphicalTrace.Entries);
|
||||
Assert.Equal(0, direct.Entities.Count);
|
||||
Assert.Equal(0, direct.Objects.ObjectCount);
|
||||
Assert.Equal(0, graphical.EntityObjects.Entities.Count);
|
||||
Assert.Equal(0, graphical.Objects.ObjectCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalObserverFailure_DoesNotStarveLaterObserverOrOwner()
|
||||
{
|
||||
using var harness = new Harness();
|
||||
var throwing = new ThrowingEntityObserver();
|
||||
var recording = new RuntimeTraceRecorder();
|
||||
IDisposable first = harness.Runtime.Subscribe(throwing);
|
||||
IDisposable second = harness.Runtime.Subscribe(recording);
|
||||
|
||||
LiveEntityRecord record =
|
||||
harness.Entities.RegisterAndMaterializeProjection(Spawn(
|
||||
Harness.TargetGuid,
|
||||
instance: 4,
|
||||
cell: 0x12340001u));
|
||||
|
||||
Assert.True(harness.EntityObjects.Entities.IsCurrent(record.Canonical));
|
||||
Assert.Contains(
|
||||
recording.Entries,
|
||||
entry => entry.Kind == RuntimeTraceKind.Entity
|
||||
&& entry.PrimaryObjectId == Harness.TargetGuid);
|
||||
Assert.Equal(1, harness.EntityObjects.Events.SubscriberCount);
|
||||
Assert.Equal(0, harness.EntityObjects.Events.DispatchFailureCount);
|
||||
|
||||
first.Dispose();
|
||||
second.Dispose();
|
||||
Assert.Equal(0, harness.EntityObjects.Events.SubscriberCount);
|
||||
}
|
||||
|
||||
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
|
||||
{
|
||||
ObjectId = spawn.Guid,
|
||||
Name = spawn.Name ?? string.Empty,
|
||||
StackSize = 4,
|
||||
Value = 25,
|
||||
ContainerId = Harness.PlayerGuid,
|
||||
ContainerSlot = 2,
|
||||
};
|
||||
|
||||
private static WorldSession.EntityPositionUpdate Position(
|
||||
uint guid,
|
||||
ushort instance) =>
|
||||
new(
|
||||
guid,
|
||||
new CreateObject.ServerPosition(
|
||||
0x12350001u,
|
||||
30f,
|
||||
40f,
|
||||
8f,
|
||||
1f,
|
||||
0f,
|
||||
0f,
|
||||
0f),
|
||||
Velocity: new Vector3(1f, 2f, 3f),
|
||||
PlacementId: null,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: instance,
|
||||
PositionSequence: 2,
|
||||
TeleportSequence: 0,
|
||||
ForcePositionSequence: 0);
|
||||
|
||||
private sealed class Harness : IDisposable
|
||||
{
|
||||
public const uint PlayerGuid = 0x50000002u;
|
||||
|
|
@ -185,10 +352,12 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
public Harness()
|
||||
{
|
||||
Identity = new LocalPlayerIdentityState();
|
||||
Entities = LiveEntityRuntimeFixture.Create(
|
||||
EntityObjects = new RuntimeEntityObjectLifetime();
|
||||
Entities = new LiveEntityRuntime(
|
||||
new GpuWorldState(),
|
||||
new NoopEntityResources());
|
||||
Objects = new ClientObjectTable();
|
||||
new NoopEntityResources(),
|
||||
EntityObjects);
|
||||
Objects = EntityObjects.Objects;
|
||||
Chat = new ChatLog();
|
||||
Selection = new SelectionState();
|
||||
MovementInput = new DispatcherMovementInputSource();
|
||||
|
|
@ -233,7 +402,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Commands,
|
||||
Identity,
|
||||
Entities,
|
||||
Objects,
|
||||
EntityObjects,
|
||||
Chat,
|
||||
new LocalPlayerControllerSlot(),
|
||||
WorldReveal,
|
||||
|
|
@ -246,6 +415,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
|
||||
public RuntimeOptions Options { get; }
|
||||
public LocalPlayerIdentityState Identity { get; }
|
||||
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||
public LiveEntityRuntime Entities { get; }
|
||||
public ClientObjectTable Objects { get; }
|
||||
public ChatLog Chat { get; }
|
||||
|
|
@ -541,6 +711,64 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
ActivationType activation) => false;
|
||||
}
|
||||
|
||||
private sealed class EntityObjectTrace : IRuntimeEntityObjectObserver
|
||||
{
|
||||
public List<string> Entries { get; } = [];
|
||||
|
||||
public void OnEntity(in RuntimeEntityDelta delta) =>
|
||||
Entries.Add(
|
||||
$"E:{delta.Stamp.Sequence}:{delta.Change}:"
|
||||
+ $"{delta.Entity.Identity.ServerGuid:X8}:"
|
||||
+ $"{delta.Entity.Identity.LocalEntityId}:"
|
||||
+ $"{delta.Entity.Identity.Incarnation}:"
|
||||
+ $"{delta.Entity.CellId:X8}:"
|
||||
+ $"{delta.Entity.PhysicsState:X8}:"
|
||||
+ $"{delta.Entity.Position?.ObjCellId:X8}:"
|
||||
+ $"{delta.Entity.Position?.Frame.Origin.X}:"
|
||||
+ $"{delta.Entity.Position?.Frame.Origin.Y}:"
|
||||
+ $"{delta.Entity.Position?.Frame.Origin.Z}");
|
||||
|
||||
public void OnInventory(in RuntimeInventoryDelta delta) =>
|
||||
Entries.Add(
|
||||
$"I:{delta.Stamp.Sequence}:{delta.Change}:"
|
||||
+ $"{delta.Item.ObjectId:X8}:"
|
||||
+ $"{delta.Item.Incarnation}:"
|
||||
+ $"{delta.Item.ContainerId:X8}:"
|
||||
+ $"{delta.Item.ContainerSlot}:"
|
||||
+ $"{delta.Item.StackSize}:"
|
||||
+ $"{delta.Item.Value}");
|
||||
}
|
||||
|
||||
private sealed class ThrowingEntityObserver : IRuntimeEventObserver
|
||||
{
|
||||
public void OnLifecycle(in RuntimeLifecycleDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCommand(in RuntimeCommandDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnEntity(in RuntimeEntityDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
|
||||
public void OnInventory(in RuntimeInventoryDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnChat(in RuntimeChatDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnMovement(in RuntimeMovementDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnPortal(in RuntimePortalDelta delta)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SelectionQuery(uint target) : IWorldSelectionQuery
|
||||
{
|
||||
public uint? PickAtCursor(bool includeSelf) => target;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue