feat(runtime): define borrowed views commands and ordered events

Establish the J1 presentation-independent contract with instance-scoped clocks and generations, immutable borrowed views, typed generation-gated commands, normalized ordered diagnostics, and teardown acknowledgements. Route graphical startup plus press-time selection, movement, and combat through focused App adapters over the exact existing owners without adding a queue or mirrored world.

Validated by the Release solution build, 13 Runtime tests, 3,838 App tests with three existing skips, and the complete 8,424-test Release suite with five existing skips.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 19:08:42 +02:00
parent afebbe3eca
commit 854d9e9cd1
21 changed files with 2594 additions and 44 deletions

View file

@ -0,0 +1,271 @@
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Chat;
using AcDream.Core.Items;
using AcDream.Runtime;
namespace AcDream.App.Runtime;
/// <summary>
/// Read-only borrowed projections over the current App owners. Enumeration is
/// synchronous and allocation-free; no mutable collection is copied or owned.
/// </summary>
internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
{
private readonly LiveSessionController _session;
private readonly LocalPlayerIdentityState _playerIdentity;
private readonly LiveEntityRuntime _entities;
private readonly ClientObjectTable _objects;
private readonly ChatLog _chat;
private readonly IGameRuntimeClock _clock;
private readonly EntityView _entityView;
private readonly InventoryView _inventoryView;
private readonly ChatView _chatView;
private readonly MovementView _movementView;
private readonly PortalView _portalView;
private bool _active = true;
public CurrentGameRuntimeViewAdapter(
LiveSessionController session,
LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
ClientObjectTable objects,
ChatLog chat,
ILocalPlayerControllerSource playerController,
WorldRevealCoordinator worldReveal,
IGameRuntimeClock clock)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_playerIdentity = playerIdentity
?? throw new ArgumentNullException(nameof(playerIdentity));
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
_chat = chat ?? throw new ArgumentNullException(nameof(chat));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_entityView = new EntityView(_entities);
_inventoryView = new InventoryView(_objects, _entities);
_chatView = new ChatView(_chat);
_movementView = new MovementView(
playerController
?? throw new ArgumentNullException(nameof(playerController)),
_clock);
_portalView = new PortalView(
worldReveal ?? throw new ArgumentNullException(nameof(worldReveal)));
}
internal bool IsActive => _active && !_session.IsDisposalComplete;
public RuntimeGenerationToken Generation =>
new(_session.SessionGeneration);
public RuntimeLifecycleSnapshot Lifecycle
{
get
{
RuntimeLifecycleState state;
if (!IsActive)
state = RuntimeLifecycleState.Disposed;
else if (_session.IsInWorld)
state = RuntimeLifecycleState.InWorld;
else if (_session.CurrentSession is not null)
state = RuntimeLifecycleState.Starting;
else if (_session.SessionGeneration == 0UL)
state = RuntimeLifecycleState.Constructed;
else
state = RuntimeLifecycleState.Stopped;
return new RuntimeLifecycleSnapshot(
Generation,
state,
_playerIdentity.ServerGuid,
_session.CurrentSession is not null);
}
}
public IGameRuntimeClock Clock => _clock;
public IRuntimeEntityView Entities => _entityView;
public IRuntimeInventoryView Inventory => _inventoryView;
public IRuntimeChatView Chat => _chatView;
public IRuntimeMovementView Movement => _movementView;
public IRuntimePortalView Portal => _portalView;
public RuntimeStateCheckpoint CaptureCheckpoint() =>
new(
Generation,
Lifecycle.State,
_clock.FrameNumber,
_entities.Count,
_entities.MaterializedCount,
_objects.ObjectCount,
_objects.ContainerCount,
_chat.Revision,
_chat.Count,
_movementView.Snapshot,
_portalView.Snapshot);
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;
public int Count => owner.Count;
}
private sealed class MovementView(
ILocalPlayerControllerSource owner,
IGameRuntimeClock clock)
: IRuntimeMovementView
{
public RuntimeMovementSnapshot Snapshot
{
get
{
PlayerMovementController? controller = owner.Controller;
return controller is null
? new RuntimeMovementSnapshot(
false,
0u,
default,
default,
false,
clock.SimulationTimeSeconds)
: new RuntimeMovementSnapshot(
true,
controller.LocalEntityId,
controller.CellPosition,
controller.BodyVelocity,
controller.IsAirborne,
clock.SimulationTimeSeconds);
}
}
}
private sealed class PortalView(WorldRevealCoordinator owner)
: IRuntimePortalView
{
public RuntimePortalSnapshot Snapshot
{
get
{
WorldRevealLifecycleSnapshot snapshot = owner.Snapshot;
RuntimePortalKind kind = snapshot.Generation == 0
? RuntimePortalKind.None
: snapshot.Kind switch
{
WorldRevealKind.Login => RuntimePortalKind.Login,
WorldRevealKind.Portal => RuntimePortalKind.Portal,
_ => RuntimePortalKind.None,
};
return new RuntimePortalSnapshot(
snapshot.Generation,
kind,
snapshot.Readiness.DestinationCell,
snapshot.IsReady,
snapshot.Materialized,
snapshot.Completed,
snapshot.Cancelled,
snapshot.WorldViewportObserved);
}
}
}
}