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, } /// /// Stable normalized trace row used to compare a direct host with an adapter /// host without retaining either owner's object graph. /// 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 _entries = []; public IReadOnlyList 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}")); } /// Monotonic sequence owner for one runtime instance. 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; }