acdream/src/AcDream.Runtime/GameRuntimeEvents.cs
Erik ce3ac310d9 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>
2026-07-26 06:42:13 +02:00

250 lines
6.9 KiB
C#

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 RuntimeGenerationToken _generation;
private bool _hasGeneration;
private ulong _sequence;
public RuntimeEventStamp Next(
RuntimeGenerationToken generation,
ulong frameNumber)
{
if (!_hasGeneration || generation != _generation)
{
_generation = generation;
_sequence = 0;
_hasGeneration = true;
}
return new RuntimeEventStamp(
generation,
checked(++_sequence),
frameNumber);
}
public ulong LastSequence => _sequence;
}