using System.Net; using System.Numerics; using AcDream.App.Combat; using AcDream.App.Input; using AcDream.App.Interaction; using AcDream.App.Net; using AcDream.App.Runtime; using AcDream.App.Streaming; using AcDream.App.UI; using AcDream.App.Update; using AcDream.App.World; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Selection; using AcDream.Core.World; using AcDream.Runtime; using AcDream.Runtime.Session; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Input; namespace AcDream.App.Tests.Runtime; public sealed class CurrentGameRuntimeAdapterTests { [Fact] public void AdapterBorrowsCurrentOwnersAndCommandsExecuteAtPressTime() { using var harness = new Harness(); var trace = new RuntimeTraceRecorder(); using IDisposable subscription = harness.Runtime.Subscribe(trace); RuntimeGenerationToken initial = harness.Runtime.Generation; RuntimeSessionStartResult start = harness.Runtime.Session.Start(initial); Assert.Equal(RuntimeSessionStartStatus.Connected, start.Status); Assert.Equal(new RuntimeGenerationToken(1), start.Generation); Assert.Equal(Harness.PlayerGuid, harness.Identity.ServerGuid); Assert.Equal(RuntimeLifecycleState.InWorld, harness.Runtime.Lifecycle.State); LiveEntityRecord liveRecord = harness.Entities.RegisterAndMaterializeProjection(Spawn( Harness.TargetGuid, instance: 3, cell: 0x12340001u)); Assert.NotNull(liveRecord.WorldEntity); var item = new ClientObject { ObjectId = Harness.TargetGuid, Name = "Runtime fixture", StackSize = 4, Value = 25, ContainerId = Harness.PlayerGuid, ContainerSlot = 2, }; harness.Objects.AddOrUpdate(item); harness.Chat.OnSystemMessage("runtime parity", 0x1Au); harness.WorldReveal.Begin(WorldRevealKind.Portal, 0x12340001u); WorldRevealReadinessSnapshot readiness = harness.WorldReveal.PrepareAndEvaluate(0x12340001u); Assert.True(readiness.IsReady); harness.Clock.Advance(new UpdateFrameInput(0.125)); RuntimeGenerationToken generation = harness.Runtime.Generation; RuntimeCommandResult selection = harness.Runtime.Selection.Execute( generation, RuntimeSelectionCommand.SelectClosestHostile); RuntimeCommandResult movement = ((IGameRuntimeCommands)harness.Runtime).Movement.Execute( generation, RuntimeMovementCommand.ToggleRunLock); RuntimeCommandResult combat = harness.Runtime.Combat.Execute( generation, RuntimeCombatCommand.ToggleMode); RuntimeCommandResult chat = ((IGameRuntimeCommands)harness.Runtime).Chat.Execute( generation, new RuntimeChatCommand( RuntimeChatChannel.General, "hello runtime")); RuntimeCommandResult portal = ((IGameRuntimeCommands)harness.Runtime).Portal.Execute( generation, RuntimePortalCommand.RecallLifestone); Assert.True(selection.Accepted); Assert.True(movement.Accepted); Assert.True(combat.Accepted); Assert.True(chat.Accepted); Assert.True(portal.Accepted); Assert.Equal(Harness.TargetGuid, harness.Selection.SelectedObjectId); Assert.True(harness.MovementInput.AutoRunActive); Assert.Equal(1, harness.Combat.ToggleCount); Assert.Contains( harness.Commands.Published, static command => command is SendChatCmd { Channel: ChatChannelKind.General, Text: "hello runtime", }); Assert.Contains( harness.Commands.Published, static command => command is ExecuteClientCommandCmd { Command: ClientCommandId.LifestoneRecall, }); RuntimeStateCheckpoint checkpoint = harness.Runtime.CaptureCheckpoint(); Assert.Equal(1, checkpoint.EntityCount); Assert.Equal(1, checkpoint.InventoryObjectCount); Assert.Equal(1, checkpoint.ChatCount); Assert.Equal(1L, checkpoint.ChatRevision); Assert.Equal(1UL, checkpoint.FrameNumber); Assert.Equal(RuntimePortalKind.Portal, checkpoint.Portal.Kind); Assert.Equal(0x12340001u, checkpoint.Portal.DestinationCell); Assert.True(checkpoint.Portal.IsReady); Assert.True(harness.Runtime.Entities.TryGet( Harness.TargetGuid, out RuntimeEntitySnapshot entity)); Assert.Equal((ushort)3, entity.Identity.Incarnation); Assert.True(harness.Runtime.Inventory.TryGet( Harness.TargetGuid, out RuntimeInventoryItemSnapshot inventory)); Assert.Equal((ushort)3, inventory.Incarnation); Assert.Equal(4, inventory.StackSize); RuntimeTraceKind[] kinds = trace.Entries .Select(static entry => entry.Kind) .ToArray(); Assert.Equal(RuntimeTraceKind.Command, kinds[0]); Assert.Equal(RuntimeTraceKind.Lifecycle, kinds[1]); Assert.Contains(RuntimeTraceKind.Inventory, kinds); Assert.Contains(RuntimeTraceKind.Chat, kinds); Assert.Equal( [ RuntimeCommandDomain.Session, RuntimeCommandDomain.Selection, RuntimeCommandDomain.Movement, RuntimeCommandDomain.Combat, RuntimeCommandDomain.Chat, RuntimeCommandDomain.Portal, ], trace.Entries .Where(static entry => entry.Kind == RuntimeTraceKind.Command) .Select(static entry => (RuntimeCommandDomain)(entry.Code >> 16)) .ToArray()); } [Fact] public void GenerationGateRejectsStaleCommandsAndStopAcknowledgesTeardown() { using var harness = new Harness(); RuntimeSessionStartResult start = harness.Runtime.Session.Start(harness.Runtime.Generation); Assert.Equal(RuntimeSessionStartStatus.Connected, start.Status); RuntimeGenerationToken active = harness.Runtime.Generation; RuntimeTeardownAcknowledgement stopped = harness.Runtime.Session.Stop(active); RuntimeCommandResult stale = harness.Runtime.Combat.Execute( active, RuntimeCombatCommand.ToggleMode); Assert.True(stopped.IsComplete); Assert.Equal(active, stopped.RetiredGeneration); Assert.Equal(new RuntimeGenerationToken(2), stopped.CurrentGeneration); Assert.Equal(RuntimeCommandStatus.StaleGeneration, stale.Status); Assert.Equal(0, harness.Combat.ToggleCount); Assert.Equal(RuntimeLifecycleState.Stopped, harness.Runtime.Lifecycle.State); } private sealed class Harness : IDisposable { public const uint PlayerGuid = 0x50000002u; public const uint TargetGuid = 0x70000001u; private readonly ItemInteractionController _items; private readonly LiveSessionController _session; public Harness() { Identity = new LocalPlayerIdentityState(); Entities = new LiveEntityRuntime( new GpuWorldState(), new NoopEntityResources()); Objects = new ClientObjectTable(); Chat = new ChatLog(); Selection = new SelectionState(); MovementInput = new DispatcherMovementInputSource(); GameplayInput = new GameplayInputFrameController( dispatcher: null, MovementInput, mouseLook: null, new NoopCombatInput()); Combat = new RecordingCombatCommand(); Clock = new UpdateFrameClock(); WorldReveal = new WorldRevealCoordinator( static (_, _) => true, static _ => true, static (_, _) => true, static () => true, static (_, _) => { }, static () => { }, static _ => false); _items = new ItemInteractionController( Objects, () => PlayerGuid, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null); var query = new SelectionQuery(TargetGuid); var selectionController = new SelectionInteractionController( Selection, query, _items, new SelectionTransport(() => _session?.IsInWorld == true), new NoopInteractionMovement()); Options = LiveOptions(); Commands = new RecordingCommandRouting(); _session = new LiveSessionController(new SessionOperations()); Host = CreateHost(_session, Commands, Identity); Runtime = new CurrentGameRuntimeAdapter( _session, Host, Commands, Identity, Entities, Objects, Chat, new LocalPlayerControllerSlot(), WorldReveal, Clock, Selection, selectionController, GameplayInput, Combat); } public RuntimeOptions Options { get; } public LocalPlayerIdentityState Identity { get; } public LiveEntityRuntime Entities { get; } public ClientObjectTable Objects { get; } public ChatLog Chat { get; } public SelectionState Selection { get; } public DispatcherMovementInputSource MovementInput { get; } public GameplayInputFrameController GameplayInput { get; } public RecordingCombatCommand Combat { get; } public UpdateFrameClock Clock { get; } public WorldRevealCoordinator WorldReveal { get; } public RecordingCommandRouting Commands { get; } public LiveSessionHost Host { get; } public CurrentGameRuntimeAdapter Runtime { get; } public void Dispose() { Runtime.Dispose(); _session.Dispose(); _items.Dispose(); Entities.Clear(); } } private static LiveSessionHost CreateHost( LiveSessionController controller, RecordingCommandRouting commands, LocalPlayerIdentityState identity) { Action noop = static () => { }; var reset = new LiveSessionResetBindings { MouseCapture = noop, PlayerPresentation = noop, TeleportTransit = noop, SessionDialogs = noop, ChatCommandTargets = noop, SettingsCharacterContext = noop, EquippedChildren = noop, ExternalContainer = noop, InteractionAndSelection = noop, SelectionPresentation = noop, ObjectTable = noop, Spellbook = noop, MagicRuntime = noop, CombatAttack = noop, CombatState = noop, ItemMana = noop, LocalPlayer = noop, Friends = noop, Squelch = noop, TurbineChat = noop, ParticleVisibility = noop, InboundEventFifo = noop, LiveLiveness = noop, LiveRuntime = noop, RenderSceneProjection = noop, SessionIdentity = noop, RemoteTeleport = noop, NetworkEffects = noop, AnimationHookFrames = noop, LivePresentation = noop, RemoteMovementDiagnostics = noop, }; return new LiveSessionHost( controller, new LiveSessionHostBindings( new LiveSessionRoutingFactories( _ => new NoopEventRouting(), _ => commands), LiveSessionResetManifest.Create(reset).Execute, new LiveSessionSelectionBindings( id => identity.ServerGuid = id, _ => { }, _ => { }, _ => { }, _ => { }, () => { }), new LiveSessionEnteredWorldBindings( _ => { }, () => { }, () => { }, _ => { }, () => { }), (_, _, _) => { }, () => { }), new LiveSessionConnectOptions( true, "127.0.0.1", 9000, "user", "password")); } private static RuntimeOptions LiveOptions() { var environment = new Dictionary { ["ACDREAM_LIVE"] = "1", ["ACDREAM_TEST_HOST"] = "127.0.0.1", ["ACDREAM_TEST_PORT"] = "9000", ["ACDREAM_TEST_USER"] = "user", ["ACDREAM_TEST_PASS"] = "password", }; return RuntimeOptions.Parse("dat", environment.GetValueOrDefault); } private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, uint cell) { var position = new CreateObject.ServerPosition( cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: instance); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "runtime fixture", null, null, 0x09000001u, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class SessionOperations : ILiveSessionOperations { public IPEndPoint ResolveEndpoint(string host, int port) => new(IPAddress.Loopback, port); public WorldSession CreateSession(IPEndPoint endpoint) => new(endpoint, new TestTransport()); public void Connect(WorldSession session, string user, string password) { } public CharacterList.Parsed GetCharacters(WorldSession session) => new( 0u, [ new CharacterList.Character( 0x50000001u, "Unavailable", 30u), new CharacterList.Character( Harness.PlayerGuid, "Runtime", 0u), ], [], 11, "Runtime", true, true); public void EnterWorld(WorldSession session, int activeCharacterIndex) { } public void Tick(WorldSession session) { } public void DisposeSession(WorldSession session) => session.Dispose(); } private sealed class TestTransport : IWorldSessionTransport { public void Send(ReadOnlySpan datagram) { } public void Send(IPEndPoint remote, ReadOnlySpan datagram) { } public int Receive( Span destination, TimeSpan timeout, out IPEndPoint? from) { from = null; return -1; } public ValueTask ReceiveAsync( Memory destination, CancellationToken cancellationToken) => throw new OperationCanceledException(cancellationToken); public void Dispose() { } } private sealed class NoopEventRouting : ILiveSessionEventRouting { public void Attach() { } public void Dispose() { } } internal sealed class RecordingCommandRouting : ILiveSessionCommandRouting, ICommandBus { private bool _active; public List Published { get; } = []; public void Activate() => _active = true; public void Publish(T command) where T : notnull { if (_active) Published.Add(command); } public void Dispose() => _active = false; } internal sealed class RecordingCombatCommand : ILiveCombatModeCommand { public int ToggleCount { get; private set; } public void Toggle() => ToggleCount++; } private sealed class NoopCombatInput : ICombatInputFrameController { public void Tick() { } public void HandleMovementInput( InputAction action, ActivationType activation) { } public bool HandleInputAction( InputAction action, ActivationType activation) => false; } private sealed class SelectionQuery(uint target) : IWorldSelectionQuery { public uint? PickAtCursor(bool includeSelf) => target; public uint? PickAt(float mouseX, float mouseY, bool includeSelf) => target; public void BeginLightingPulse(uint serverGuid) { } public bool TryCaptureIdentity(uint serverGuid, out uint localEntityId) { localEntityId = 1u; return serverGuid == target; } public bool IsCurrent(uint serverGuid, uint localEntityId) => serverGuid == target && localEntityId == 1u; public string Describe(uint serverGuid) => "Runtime target"; public bool IsCreature(uint serverGuid) => serverGuid == target; public bool IsHostileMonster(uint serverGuid) => serverGuid == target; public ClosestCombatTarget? FindClosestHostileMonster() => new(target, DistanceSquared: 4f); public bool IsUseable(uint serverGuid) => serverGuid == target; public bool IsPickupable(uint serverGuid) => false; public bool TryGetApproach( uint serverGuid, out InteractionApproach approach) { approach = default; return false; } public Vector3? GetCombatCameraTargetPoint(uint serverGuid) => null; } private sealed class SelectionTransport(Func isInWorld) : ISelectionInteractionTransport { public bool IsInWorld => isInWorld(); public bool TrySendUse(uint serverGuid, out uint sequence) { sequence = 1u; return IsInWorld; } public bool TrySendPickup( uint itemGuid, uint destinationContainerId, int placement, out uint sequence) { sequence = 1u; return IsInWorld; } } private sealed class NoopInteractionMovement : IPlayerInteractionMovementSink { public bool BeginApproach( InteractionApproach approach, Action? armAfterCancel = null) => false; } private sealed class NoopEntityResources : ILiveEntityResourceLifecycle { public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { } } }