using System.Buffers.Binary; using System.Net; using System.Numerics; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Headless.Configuration; using AcDream.Headless.Credentials; using AcDream.Headless.Diagnostics; using AcDream.Headless.Hosting; using AcDream.Headless.Platform; using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; using AcDream.Runtime.World; namespace AcDream.Headless.Tests; public sealed class HeadlessSessionHostTests { [Fact] public void SingleSessionStartsReconnectsAndConvergesWithoutPresentation() { var operations = new FixtureSessionOperations(); using var diagnosticsOutput = new StringWriter(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(diagnosticsOutput), operations); RuntimeSessionStartResult first = host.Start(); ulong firstGeneration = host.Runtime.Generation.Value; host.Tick(0.015d); RuntimeSessionStartResult second = host.Reconnect(); Assert.Equal(RuntimeSessionStartStatus.Connected, first.Status); Assert.Equal(RuntimeSessionStartStatus.Connected, second.Status); Assert.Equal(0x50000002u, first.CharacterId); Assert.Equal("Headless", host.ActiveCharacterName); Assert.True(host.Runtime.Session.IsInWorld); Assert.True(host.Runtime.Generation.Value > firstGeneration); Assert.Equal(2, operations.CreatedSessionCount); Assert.Equal(1, operations.DisposedSessionCount); host.Dispose(); Assert.Equal(2, operations.DisposedSessionCount); Assert.True(host.Runtime.CaptureOwnership().IsConverged); Assert.True(credential.IsDisposed); string diagnostics = diagnosticsOutput.ToString(); Assert.Contains("\"state\":\"start-result\"", diagnostics); Assert.DoesNotContain("password", diagnostics); Assert.DoesNotContain("AcDream.App", diagnostics); } [Fact] public async Task ProcessHostRunsUntilCancellationAndReturnsStableExitCode() { var configuration = new HeadlessConfiguration { Version = 1, Sessions = [ Descriptor( HeadlessCredentialProviderKind.StandardInput, "stdin-bot"), ], }; HeadlessPathSet paths = HeadlessPathSet.Resolve( new HeadlessPathOverrides()); using var diagnostics = new StringWriter(); var operations = new FixtureSessionOperations(); using var host = new HeadlessProcessHost( configuration, paths, new System.IO.StringReader( "process-password" + Environment.NewLine), diagnostics, operations); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); HeadlessExitCode result = await host.RunAsync(cancellation.Token); Assert.Equal(HeadlessExitCode.Success, result); Assert.True(host.Session.Runtime.Session.IsInWorld); Assert.DoesNotContain( "process-password", diagnostics.ToString()); } [Fact] public async Task DirectCredentialsOverrideSingleConfiguredSession() { var configuration = new HeadlessConfiguration { Version = 1, Sessions = [Descriptor()], }; HeadlessPathSet paths = HeadlessPathSet.Resolve( new HeadlessPathOverrides()); using var diagnostics = new StringWriter(); var operations = new FixtureSessionOperations(); using var host = new HeadlessProcessHost( configuration, paths, TextReader.Null, diagnostics, operations, directCredentials: new HeadlessDirectCredentials( "direct-account", "direct-secret")); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); HeadlessExitCode result = await host.RunAsync(cancellation.Token); Assert.Equal(HeadlessExitCode.Success, result); Assert.Equal("direct-account", operations.LastUser); Assert.Equal("direct-secret", operations.LastPassword); Assert.DoesNotContain( "direct-secret", diagnostics.ToString()); } [Fact] public void DirectFrameUsesSharedRetailOrderAndMovementCadence() { var operations = new FixtureSessionOperations(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); using var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(TextWriter.Null), operations); Assert.Equal( RuntimeSessionStartStatus.Connected, host.Start().Status); HydrateGroundedPlayer(host.Runtime); var sent = new List<(byte[] Body, double Time)>(); var trace = new RuntimeTraceRecorder(); using IDisposable subscription = host.Runtime.Subscribe(trace); operations.Sessions[^1].GameActionCapture = body => sent.Add(( body, host.Runtime.Clock.SimulationTimeSeconds)); RuntimeCommandResult intent = host.Commands.Movement.SetIntent( host.Runtime.Generation, new MovementInput(Forward: true, Run: true)); host.Tick(0.015d); Assert.True(intent.Accepted); Assert.Contains( trace.Entries, static entry => entry.Kind == RuntimeTraceKind.Movement); Assert.Equal( [ MoveToState.MoveToStateAction, AutonomousPosition.AutonomousPositionAction, ], sent.Select(static entry => ActionOpcode(entry.Body)).ToArray()); sent.Clear(); for (int index = 0; index < 70; index++) host.Tick(0.015d); (byte[] Body, double Time)[] positions = sent .Where(static entry => ActionOpcode(entry.Body) == AutonomousPosition.AutonomousPositionAction) .ToArray(); Assert.InRange(positions.Length, 1, 2); Assert.True(positions[^1].Time >= 1d); if (positions.Length == 2) { Assert.True( positions[1].Time - positions[0].Time >= 0.99d); } Assert.DoesNotContain( sent, entry => ActionOpcode(entry.Body) == MoveToState.MoveToStateAction); } [Fact] public void TeardownRetriesOnlyTheUnfinishedSuffix() { var operations = new FixtureSessionOperations(); var writer = new FailOnceTextWriter(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(writer), operations); Assert.Equal( RuntimeSessionStartStatus.Connected, host.Start().Status); writer.FailNextWrite = true; Assert.Throws(host.Dispose); Assert.Equal(1, operations.DisposedSessionCount); host.Dispose(); Assert.Equal(1, operations.DisposedSessionCount); Assert.True(host.Runtime.CaptureOwnership().IsConverged); } [Fact] public void WorldProjectionHydratesCanonicalMovementAndTeleportState() { var operations = new FixtureSessionOperations(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); using var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(TextWriter.Null), operations); GameRuntime runtime = host.Runtime; const uint player = 0x50000002u; runtime.PlayerIdentity.ServerGuid = player; AddFlatLandblock(runtime.EntityObjects.Physics.Engine); RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(player)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var collision = new FixtureCollisionNeighborhood(); var projection = new HeadlessSessionWorldProjection( runtime, collision); projection.ProjectSpawn(record, isLocalPlayer: true); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SetPosition( new Vector3(48f, 49f, 50f), 0xA9B40001u); projection.ProjectPosition( record, isLocalPlayer: true, PositionTimestampDisposition.Apply); Assert.Same(controller, runtime.MovementOwner.Controller); Assert.Equal(record.LocalEntityId, controller.LocalEntityId); Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position); Assert.Equal( 0xA9B40000u, controller.CellId & 0xFFFF0000u); Assert.True((controller.CellId & 0xFFFFu) < 0x0100u); projection.BeginTeleport(); Assert.Equal(PlayerState.PortalSpace, controller.State); RuntimeDestinationReadiness readiness = projection.PrepareDestination( revealGeneration: 7, new RuntimeTeleportDestination( player, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 1, ForcePositionSequence: 0, new Position( 0xA9B40001u, new Vector3(96f, 97f, 50f), Quaternion.Identity))); Assert.True(readiness.IsCollisionReady); Assert.False(readiness.IsUnhydratable); Assert.Equal(PlayerState.InWorld, controller.State); Assert.Equal(3, collision.CenterCount); Assert.Equal(0xA9B40001u, collision.LastCell); } [Fact] public void WorldProjectionIgnoresNormalEchoButBlipsForcePosition() { var operations = new FixtureSessionOperations(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); using var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(TextWriter.Null), operations); GameRuntime runtime = host.Runtime; const uint player = 0x50000003u; runtime.PlayerIdentity.ServerGuid = player; AddFlatLandblock(runtime.EntityObjects.Physics.Engine); RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(player)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var collision = new FixtureCollisionNeighborhood(); var projection = new HeadlessSessionWorldProjection( runtime, collision); projection.ProjectSpawn(record, isLocalPlayer: true); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SetPosition( new Vector3(48f, 49f, 50f), 0xA9B40001u); projection.ProjectPosition( record, isLocalPlayer: true, PositionTimestampDisposition.Apply); Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position); var force = new WorldSession.EntityPositionUpdate( player, record.Snapshot.Position!.Value with { PositionX = 72f, PositionY = 73f, }, Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 0, ForcePositionSequence: 1); Assert.True(runtime.EntityObjects.TryApplyPosition( force, isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: controller.BodyVelocity, projectionRequiresTeleportHook: false, acknowledgeProjection: null, out PositionTimestampDisposition disposition, out _, out _)); Assert.Equal( PositionTimestampDisposition.ForcePosition, disposition); projection.ProjectPosition( record, isLocalPlayer: true, disposition); Assert.Equal(new Vector3(72f, 73f, 50f), controller.Position); Assert.Equal(2, collision.CenterCount); } private static HeadlessSessionDescriptor Descriptor( HeadlessCredentialProviderKind provider = HeadlessCredentialProviderKind.Environment, string credentialReference = "BOT_PASSWORD") => new() { Id = "bot", Endpoint = new HeadlessEndpointDescriptor { Host = "127.0.0.1", Port = 9000, }, Account = "account", Character = new HeadlessCharacterSelector { Name = "headless", }, Policy = new HeadlessBotPolicyDescriptor { Id = "idle", }, Credential = new HeadlessCredentialReference { Provider = provider, Reference = credentialReference, }, }; private static void HydrateGroundedPlayer(GameRuntime runtime) { const uint player = 0x50000002u; PhysicsEngine engine = runtime.EntityObjects.Physics.Engine; AddFlatLandblock(engine); RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(player)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var controller = new PlayerMovementController(engine); controller.SetPosition( new Vector3(96f, 97f, 50f), 0xA9B40001u, new Vector3(96f, 97f, 50f)); runtime.MovementOwner.Controller = controller; } private static void AddFlatLandblock(PhysicsEngine engine) { var heights = new byte[81]; Array.Fill(heights, (byte)50); var heightTable = new float[256]; for (int index = 0; index < heightTable.Length; index++) heightTable[index] = index; engine.AddLandblock( 0xA9B4FFFFu, new TerrainSurface(heights, heightTable), [], [], worldOffsetX: 0f, worldOffsetY: 0f); } private static WorldSession.EntitySpawn Spawn(uint guid) { var position = new CreateObject.ServerPosition( 0xA9B40001u, 96f, 97f, 50f, 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: 1); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: null, 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, [], [], [], null, null, "Headless", null, null, null, PhysicsState: physics.RawState, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private static uint ActionOpcode(byte[] body) => BinaryPrimitives.ReadUInt32LittleEndian( body.AsSpan(8, sizeof(uint))); private sealed class FixtureSessionOperations : ILiveSessionOperations { public List Sessions { get; } = []; public int CreatedSessionCount { get; private set; } public int DisposedSessionCount { get; private set; } public string? LastUser { get; private set; } public string? LastPassword { get; private set; } public IPEndPoint ResolveEndpoint(string host, int port) => new(IPAddress.Loopback, port); public WorldSession CreateSession(IPEndPoint endpoint) { CreatedSessionCount++; var session = new WorldSession(endpoint); Sessions.Add(session); return session; } public void Connect( WorldSession session, string user, string password) { LastUser = user; LastPassword = password; } public CharacterList.Parsed GetCharacters( WorldSession session) => new( 0u, [ new CharacterList.Character( 0x50000001u, "Other", 0u), new CharacterList.Character( 0x50000002u, "Headless", 0u), ], [], 11, "account", true, true); public void EnterWorld( WorldSession session, int activeCharacterIndex) { } public void Tick(WorldSession session) { } public void DisposeSession(WorldSession session) { DisposedSessionCount++; session.Dispose(); } } private sealed class FailOnceTextWriter : StringWriter { public bool FailNextWrite { get; set; } public override void WriteLine(string? value) { if (FailNextWrite) { FailNextWrite = false; throw new IOException("fixture write failure"); } base.WriteLine(value); } } private sealed class FixtureCollisionNeighborhood : IHeadlessCollisionNeighborhood { public int CenterCount { get; private set; } public uint LastCell { get; private set; } public void CenterOn(uint fullCellId) { CenterCount++; LastCell = fullCellId; } public bool IsReady(uint fullCellId) => fullCellId == LastCell; } }