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,45 @@
namespace AcDream.Runtime;
public readonly record struct RuntimeFrameTime(
ulong FrameNumber,
double DeltaSeconds,
double SimulationTimeSeconds);
public interface IGameRuntimeClock
{
ulong FrameNumber { get; }
double SimulationTimeSeconds { get; }
}
/// <summary>
/// Instance-scoped simulation clock shared by graphical and future headless
/// hosts. It owns no wall-clock/global state.
/// </summary>
public sealed class GameRuntimeClock : IGameRuntimeClock
{
public ulong FrameNumber { get; private set; }
public double SimulationTimeSeconds { get; private set; }
public RuntimeFrameTime Advance(
double hostDeltaSeconds,
bool advanceSimulationTime = true)
{
double deltaSeconds = NormalizeDeltaSeconds(hostDeltaSeconds);
FrameNumber = checked(FrameNumber + 1UL);
if (advanceSimulationTime)
SimulationTimeSeconds += deltaSeconds;
return new RuntimeFrameTime(
FrameNumber,
deltaSeconds,
SimulationTimeSeconds);
}
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
double.IsFinite(deltaSeconds)
&& deltaSeconds > 0.0
&& deltaSeconds <= float.MaxValue
? deltaSeconds
: 0.0;
}

View file

@ -0,0 +1,151 @@
namespace AcDream.Runtime;
public enum RuntimeCommandStatus
{
Accepted,
Inactive,
StaleGeneration,
Unsupported,
Rejected,
}
public readonly record struct RuntimeCommandResult(
RuntimeCommandStatus Status,
RuntimeGenerationToken Generation,
uint ResultObjectId = 0u)
{
public bool Accepted => Status == RuntimeCommandStatus.Accepted;
}
public enum RuntimeSessionStartStatus
{
Disabled,
MissingCredentials,
NoCharacters,
Connected,
Deferred,
Failed,
Inactive,
StaleGeneration,
}
public readonly record struct RuntimeSessionStartResult(
RuntimeSessionStartStatus Status,
RuntimeGenerationToken Generation,
uint CharacterId = 0u,
string CharacterName = "",
Exception? Error = null);
public enum RuntimeSelectionCommand
{
SelectClosestHostile,
SelectPrevious,
ExamineSelected,
UseSelected,
PickUpSelected,
}
public enum RuntimeCombatCommand
{
ToggleMode,
}
public enum RuntimeMovementCommand
{
ToggleRunLock,
Stop,
Ready,
Sit,
Crouch,
Sleep,
}
public enum RuntimeChatChannel
{
Say,
Tell,
Fellowship,
Allegiance,
Vassals,
Patron,
Monarch,
CoVassals,
General,
Trade,
LookingForGroup,
Roleplay,
Society,
Olthoi,
}
public readonly record struct RuntimeChatCommand(
RuntimeChatChannel Channel,
string Text,
string? TargetName = null);
public enum RuntimePortalCommand
{
RecallLifestone,
RecallMarketplace,
RecallHouse,
RecallMansion,
}
public interface IRuntimeSessionCommands
{
RuntimeSessionStartResult Start(RuntimeGenerationToken expectedGeneration);
RuntimeSessionStartResult Reconnect(RuntimeGenerationToken expectedGeneration);
RuntimeTeardownAcknowledgement Stop(RuntimeGenerationToken expectedGeneration);
}
public interface IRuntimeSelectionCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeSelectionCommand command);
}
public interface IRuntimeCombatCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeCombatCommand command);
}
public interface IRuntimeMovementCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeMovementCommand command);
}
public interface IRuntimeChatCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeChatCommand command);
}
public interface IRuntimePortalCommands
{
RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimePortalCommand command);
}
public interface IGameRuntimeCommands
{
IRuntimeSessionCommands Session { get; }
IRuntimeSelectionCommands Selection { get; }
IRuntimeCombatCommands Combat { get; }
IRuntimeMovementCommands Movement { get; }
IRuntimeChatCommands Chat { get; }
IRuntimePortalCommands Portal { get; }
}

View file

@ -0,0 +1,236 @@
namespace AcDream.Runtime;
public readonly record struct RuntimeEventStamp(
RuntimeGenerationToken Generation,
ulong Sequence,
ulong FrameNumber);
public enum RuntimeCommandDomain
{
Session,
Selection,
Combat,
Movement,
Chat,
Portal,
}
public readonly record struct RuntimeLifecycleDelta(
RuntimeEventStamp Stamp,
RuntimeLifecycleState Previous,
RuntimeLifecycleState Current);
public readonly record struct RuntimeCommandDelta(
RuntimeEventStamp Stamp,
RuntimeCommandDomain Domain,
int Operation,
RuntimeCommandStatus Status,
uint PrimaryObjectId = 0u,
string? Text = null);
public enum RuntimeEntityChange
{
Registered,
Updated,
Rebucketed,
Hidden,
Withdrawn,
Deleted,
}
public readonly record struct RuntimeEntityDelta(
RuntimeEventStamp Stamp,
RuntimeEntityChange Change,
RuntimeEntitySnapshot Entity);
public enum RuntimeInventoryChange
{
Added,
Updated,
Moved,
Removed,
Cleared,
}
public readonly record struct RuntimeInventoryDelta(
RuntimeEventStamp Stamp,
RuntimeInventoryChange Change,
RuntimeInventoryItemSnapshot Item);
public readonly record struct RuntimeChatEntry(
long Revision,
uint SenderGuid,
int Kind,
string Sender,
string Text,
string ChannelName);
public readonly record struct RuntimeChatDelta(
RuntimeEventStamp Stamp,
RuntimeChatEntry Entry);
public readonly record struct RuntimeMovementDelta(
RuntimeEventStamp Stamp,
RuntimeMovementSnapshot Movement);
public readonly record struct RuntimePortalDelta(
RuntimeEventStamp Stamp,
RuntimePortalSnapshot Portal);
public interface IRuntimeEventObserver
{
void OnLifecycle(in RuntimeLifecycleDelta delta);
void OnCommand(in RuntimeCommandDelta delta);
void OnEntity(in RuntimeEntityDelta delta);
void OnInventory(in RuntimeInventoryDelta delta);
void OnChat(in RuntimeChatDelta delta);
void OnMovement(in RuntimeMovementDelta delta);
void OnPortal(in RuntimePortalDelta delta);
}
public interface IRuntimeEventSource
{
IDisposable Subscribe(IRuntimeEventObserver observer);
}
public enum RuntimeTraceKind
{
Lifecycle,
Command,
Entity,
Inventory,
Chat,
Movement,
Portal,
Checkpoint,
}
/// <summary>
/// Stable normalized trace row used to compare a direct host with an adapter
/// host without retaining either owner's object graph.
/// </summary>
public readonly record struct RuntimeTraceEntry(
RuntimeEventStamp Stamp,
RuntimeTraceKind Kind,
int Code,
long Value,
uint PrimaryObjectId,
uint SecondaryObjectId,
string Text);
public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
{
private readonly List<RuntimeTraceEntry> _entries = [];
public IReadOnlyList<RuntimeTraceEntry> Entries => _entries;
public void AddCheckpoint(
RuntimeEventStamp stamp,
in RuntimeStateCheckpoint checkpoint)
{
_entries.Add(new RuntimeTraceEntry(
stamp,
RuntimeTraceKind.Checkpoint,
(int)checkpoint.Lifecycle,
checkpoint.ChatRevision,
(uint)checkpoint.EntityCount,
(uint)checkpoint.InventoryObjectCount,
string.Create(
System.Globalization.CultureInfo.InvariantCulture,
$"frame={checkpoint.FrameNumber};materialized={checkpoint.MaterializedEntityCount};" +
$"containers={checkpoint.InventoryContainerCount};chat={checkpoint.ChatCount};" +
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
}
public void OnLifecycle(in RuntimeLifecycleDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Lifecycle,
(int)delta.Current,
(int)delta.Previous,
0u,
0u,
string.Empty));
public void OnCommand(in RuntimeCommandDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Command,
((int)delta.Domain << 16) | (delta.Operation & 0xFFFF),
(int)delta.Status,
delta.PrimaryObjectId,
0u,
delta.Text ?? string.Empty));
public void OnEntity(in RuntimeEntityDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Entity,
(int)delta.Change,
delta.Entity.Identity.Incarnation,
delta.Entity.Identity.ServerGuid,
delta.Entity.CellId,
string.Empty));
public void OnInventory(in RuntimeInventoryDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Inventory,
(int)delta.Change,
delta.Item.StackSize,
delta.Item.ObjectId,
delta.Item.ContainerId,
delta.Item.Name ?? string.Empty));
public void OnChat(in RuntimeChatDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Chat,
delta.Entry.Kind,
delta.Entry.Revision,
delta.Entry.SenderGuid,
0u,
$"{delta.Entry.ChannelName}|{delta.Entry.Sender}|{delta.Entry.Text}"));
public void OnMovement(in RuntimeMovementDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Movement,
delta.Movement.IsAirborne ? 1 : 0,
BitConverter.DoubleToInt64Bits(delta.Movement.SimulationTimeSeconds),
delta.Movement.LocalEntityId,
delta.Movement.Position.ObjCellId,
string.Empty));
public void OnPortal(in RuntimePortalDelta delta) =>
_entries.Add(new RuntimeTraceEntry(
delta.Stamp,
RuntimeTraceKind.Portal,
(int)delta.Portal.Kind,
delta.Portal.Generation,
delta.Portal.DestinationCell,
0u,
$"{delta.Portal.IsReady}:{delta.Portal.IsMaterialized}:" +
$"{delta.Portal.IsCompleted}:{delta.Portal.IsCancelled}:" +
$"{delta.Portal.IsWorldVisible}"));
}
/// <summary>Monotonic sequence owner for one runtime instance.</summary>
public sealed class RuntimeEventSequencer
{
private ulong _sequence;
public RuntimeEventStamp Next(
RuntimeGenerationToken generation,
ulong frameNumber) =>
new(generation, checked(++_sequence), frameNumber);
public ulong LastSequence => _sequence;
}

View file

@ -0,0 +1,142 @@
using AcDream.Core.Physics;
namespace AcDream.Runtime;
public readonly record struct RuntimeEntityIdentity(
uint ServerGuid,
uint LocalEntityId,
ushort Incarnation);
public readonly record struct RuntimeEntitySnapshot(
RuntimeEntityIdentity Identity,
uint CellId,
uint PhysicsState,
Position? Position,
bool IsMaterialized,
bool IsSpatiallyVisible,
bool IsHydrated);
public interface IRuntimeEntityVisitor
{
void Visit(in RuntimeEntitySnapshot entity);
}
public interface IRuntimeEntityView
{
int Count { get; }
int MaterializedCount { get; }
bool TryGet(uint serverGuid, out RuntimeEntitySnapshot entity);
/// <summary>
/// Visits the canonical owner synchronously without copying its mutable
/// collection. The visitor must not retain references or mutate runtime
/// ownership during the call.
/// </summary>
void Visit(IRuntimeEntityVisitor visitor);
}
public readonly record struct RuntimeInventoryItemSnapshot(
uint ObjectId,
ushort Incarnation,
string Name,
uint ContainerId,
int ContainerSlot,
uint WielderId,
uint EquipLocation,
int StackSize,
int Value);
public interface IRuntimeInventoryVisitor
{
void Visit(in RuntimeInventoryItemSnapshot item);
}
public interface IRuntimeInventoryView
{
int ObjectCount { get; }
int ContainerCount { get; }
bool TryGet(uint objectId, out RuntimeInventoryItemSnapshot item);
/// <inheritdoc cref="IRuntimeEntityView.Visit"/>
void Visit(IRuntimeInventoryVisitor visitor);
}
public interface IRuntimeChatView
{
long Revision { get; }
int Count { get; }
}
public readonly record struct RuntimeMovementSnapshot(
bool HasController,
uint LocalEntityId,
Position Position,
System.Numerics.Vector3 Velocity,
bool IsAirborne,
double SimulationTimeSeconds);
public interface IRuntimeMovementView
{
RuntimeMovementSnapshot Snapshot { get; }
}
public enum RuntimePortalKind
{
None,
Login,
Portal,
}
public readonly record struct RuntimePortalSnapshot(
long Generation,
RuntimePortalKind Kind,
uint DestinationCell,
bool IsReady,
bool IsMaterialized,
bool IsCompleted,
bool IsCancelled,
bool IsWorldVisible);
public interface IRuntimePortalView
{
RuntimePortalSnapshot Snapshot { get; }
}
public readonly record struct RuntimeStateCheckpoint(
RuntimeGenerationToken Generation,
RuntimeLifecycleState Lifecycle,
ulong FrameNumber,
int EntityCount,
int MaterializedEntityCount,
int InventoryObjectCount,
int InventoryContainerCount,
long ChatRevision,
int ChatCount,
RuntimeMovementSnapshot Movement,
RuntimePortalSnapshot Portal);
public interface IGameRuntimeView
{
RuntimeGenerationToken Generation { get; }
RuntimeLifecycleSnapshot Lifecycle { get; }
IGameRuntimeClock Clock { get; }
IRuntimeEntityView Entities { get; }
IRuntimeInventoryView Inventory { get; }
IRuntimeChatView Chat { get; }
IRuntimeMovementView Movement { get; }
IRuntimePortalView Portal { get; }
RuntimeStateCheckpoint CaptureCheckpoint();
}

View file

@ -0,0 +1,62 @@
namespace AcDream.Runtime;
/// <summary>
/// Identifies one exact client-session generation. Default is the
/// pre-session generation and remains a valid compare token.
/// </summary>
public readonly record struct RuntimeGenerationToken(ulong Value)
{
public static RuntimeGenerationToken Initial => default;
public RuntimeGenerationToken Next() => new(checked(Value + 1UL));
public override string ToString() => Value.ToString(
System.Globalization.CultureInfo.InvariantCulture);
}
public enum RuntimeLifecycleState
{
Constructed,
Stopped,
Starting,
InWorld,
Stopping,
Faulted,
Disposed,
}
public readonly record struct RuntimeLifecycleSnapshot(
RuntimeGenerationToken Generation,
RuntimeLifecycleState State,
uint PlayerGuid,
bool HasTransport);
[Flags]
public enum RuntimeTeardownStage
{
None = 0,
CommandsInert = 1 << 0,
InboundDetached = 1 << 1,
TransportDisposed = 1 << 2,
HostReset = 1 << 3,
Complete = CommandsInert | InboundDetached | TransportDisposed | HostReset,
}
/// <summary>
/// A generation-scoped acknowledgement. A caller may retry only the missing
/// suffix; completed teardown stages never replay.
/// </summary>
public readonly record struct RuntimeTeardownAcknowledgement(
RuntimeGenerationToken RetiredGeneration,
RuntimeGenerationToken CurrentGeneration,
RuntimeCommandStatus Status,
RuntimeTeardownStage CompletedStages,
Exception? Error = null)
{
public bool IsComplete =>
Status == RuntimeCommandStatus.Accepted
&&
(CompletedStages & RuntimeTeardownStage.Complete)
== RuntimeTeardownStage.Complete
&& Error is null;
}