using System.Net; using AcDream.Core.Net; using AcDream.Core.Net.Messages; 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.Session; namespace AcDream.Headless.Tests; public sealed class HeadlessProcessSchedulerTests { [Fact] public void AbsoluteDeadlinesTickSessionsInStableElapsedTime() { var time = new ManualTimeProvider(); var operations = new FixtureSessionOperations(); using HeadlessSessionHost first = CreateSession("first", time, operations); using HeadlessSessionHost second = CreateSession("second", time, operations); Assert.Equal( RuntimeSessionStartStatus.Connected, first.Start().Status); Assert.Equal( RuntimeSessionStartStatus.Connected, second.Start().Status); var scheduler = new HeadlessProcessScheduler( [first, second], time); Assert.False(scheduler.DispatchDue(time.GetTimestamp())); time.Advance(TimeSpan.FromMilliseconds(15)); Assert.True(scheduler.DispatchDue(time.GetTimestamp())); Assert.Equal(1UL, first.Runtime.Clock.FrameNumber); Assert.Equal(1UL, second.Runtime.Clock.FrameNumber); Assert.Equal( 0.015d, first.Runtime.Clock.SimulationTimeSeconds, precision: 9); HeadlessSchedulerSnapshot snapshot = scheduler.CaptureSnapshot(); Assert.Equal(2, snapshot.TurnCount); Assert.Equal(0, snapshot.CatchUpCollapseCount); } [Fact] public void LongPauseUsesBoundedCatchUpAndPreservesElapsedTime() { var time = new ManualTimeProvider(); var operations = new FixtureSessionOperations(); using HeadlessSessionHost session = CreateSession("catch-up", time, operations); Assert.Equal( RuntimeSessionStartStatus.Connected, session.Start().Status); var scheduler = new HeadlessProcessScheduler( [session], time, maximumCatchUpTurns: 4); time.Advance(TimeSpan.FromSeconds(1)); Assert.True(scheduler.DispatchDue(time.GetTimestamp())); Assert.Equal(4UL, session.Runtime.Clock.FrameNumber); Assert.Equal( 1d, session.Runtime.Clock.SimulationTimeSeconds, precision: 9); Assert.Equal( 1, scheduler.CaptureSnapshot().CatchUpCollapseCount); } [Fact] public void ReconnectQuiescenceIsAMonotonicDeadlineNotABlockingWait() { var time = new ManualTimeProvider(); var operations = new FixtureSessionOperations(); using HeadlessSessionHost session = CreateSession( "reconnect", time, operations, TimeSpan.FromMilliseconds(2500)); Assert.Equal( RuntimeSessionStartStatus.Connected, session.Start().Status); var scheduler = new HeadlessProcessScheduler( [session], time); RuntimeSessionStartResult reconnect = session.Reconnect(); Assert.Equal( RuntimeSessionStartStatus.Deferred, reconnect.Status); Assert.True(session.IsReconnectPending); Assert.Equal(1, operations.CreatedSessionCount); Assert.Equal(1, operations.DisposedSessionCount); time.Advance(TimeSpan.FromMilliseconds(2499)); Assert.False(scheduler.DispatchDue(time.GetTimestamp())); Assert.Equal(1, operations.CreatedSessionCount); time.Advance(TimeSpan.FromMilliseconds(1)); Assert.True(scheduler.DispatchDue(time.GetTimestamp())); Assert.False(session.IsReconnectPending); Assert.Equal(2, operations.CreatedSessionCount); Assert.True(session.Runtime.Session.IsInWorld); } [Fact] public async Task ProcessHostRunsMultipleSessionsOnOneScheduler() { var configuration = new HeadlessConfiguration { Version = 1, Sessions = [ Descriptor( "first", "first-stdin"), Descriptor( "second", "second-stdin"), ], }; var operations = new FixtureSessionOperations(); using var diagnostics = new StringWriter(); using var host = new HeadlessProcessHost( configuration, HeadlessPathSet.Resolve(new HeadlessPathOverrides()), new StringReader( "first-password" + Environment.NewLine + "second-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.Equal(2, host.Sessions.Count); Assert.All( host.Sessions, session => Assert.True(session.Runtime.Session.IsInWorld)); Assert.Equal(2, operations.CreatedSessionCount); Assert.DoesNotContain( "password", diagnostics.ToString(), StringComparison.Ordinal); } private static HeadlessSessionHost CreateSession( string id, TimeProvider time, ILiveSessionOperations operations, TimeSpan? reconnectQuiescence = null) { var credential = new HeadlessCredentialSecret( $"{id}-credential", "password"); try { return new HeadlessSessionHost( Descriptor(id, $"{id}-credential"), credential, new HeadlessDiagnosticWriter(TextWriter.Null), operations, time, reconnectQuiescence); } catch { credential.Dispose(); throw; } } private static HeadlessSessionDescriptor Descriptor( string id, string credentialReference) => new() { Id = id, Endpoint = new HeadlessEndpointDescriptor { Host = "127.0.0.1", Port = 9000, }, Account = $"{id}-account", Character = new HeadlessCharacterSelector { Index = 0, }, Policy = new HeadlessBotPolicyDescriptor { Id = "idle", }, Credential = new HeadlessCredentialReference { Provider = HeadlessCredentialProviderKind.StandardInput, Reference = credentialReference, }, }; private sealed class ManualTimeProvider : TimeProvider { private long _timestamp; public override long TimestampFrequency => TimeSpan.TicksPerSecond; public override long GetTimestamp() => _timestamp; internal void Advance(TimeSpan duration) => _timestamp = checked(_timestamp + duration.Ticks); } private sealed class FixtureSessionOperations : ILiveSessionOperations { public int CreatedSessionCount { get; private set; } public int DisposedSessionCount { get; private set; } public IPEndPoint ResolveEndpoint(string host, int port) => new(IPAddress.Loopback, port); public WorldSession CreateSession(IPEndPoint endpoint) { CreatedSessionCount++; return new WorldSession(endpoint); } public void Connect( WorldSession session, string user, string password) { } public CharacterList.Parsed GetCharacters( WorldSession session) => new( 0u, [ new CharacterList.Character( 0x50000001u, "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(); } } }