using System.Numerics; using System.Reflection; using AcDream.App.Composition; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; namespace AcDream.App.Tests.Composition; public sealed class HostInputCameraCompositionTests { [Fact] public void ProductionPhasePreservesTheCompleteHostInputCameraOrder() { using var fixture = new Fixture(); HostInputCameraResult result = fixture.Phase().Compose(fixture.Platform); Assert.Equal( Enum.GetValues(), fixture.Points); Assert.Same(fixture.Publication.GpuFrames, result.GpuFrameFlights); Assert.Same(fixture.Publication.Keyboard, result.KeyboardSource); Assert.Same(fixture.Publication.Mouse, result.MouseSource); Assert.Same(fixture.Publication.Dispatcher, result.InputDispatcher); Assert.Same(fixture.Publication.Camera, result.CameraController); Assert.Same(fixture.Publication.Pointer, result.CameraPointerInput); Assert.Equal(1280f / 720f, fixture.ViewportAspect.Aspect, precision: 5); Assert.Equal((1280, 720), fixture.Factory.Viewport.Size); } [Theory] [MemberData(nameof(FaultPointValues))] public void FailureAtEveryProductionBoundaryStopsTheExactSuffix( int faultPointValue) { var faultPoint = (HostInputCameraCompositionPoint)faultPointValue; using var fixture = new Fixture(faultPoint); Assert.Throws(() => fixture.Phase().Compose(fixture.Platform)); HostInputCameraCompositionPoint[] expected = Enum.GetValues() .TakeWhile(point => point <= faultPoint) .ToArray(); Assert.Equal(expected, fixture.Points); fixture.Publication.AssertPublishedThrough(faultPoint); } public static TheoryData FaultPointValues() { var data = new TheoryData(); foreach (HostInputCameraCompositionPoint point in Enum.GetValues()) { data.Add((int)point); } return data; } [Fact] public void GameWindowUsesTheExactPlatformPreludeAndPhaseOneType() { string source = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); Assert.Contains("GameWindowPlatformAcquisition.Acquire(", source, StringComparison.Ordinal); Assert.Contains("new HostInputCameraCompositionPhase(", source, StringComparison.Ordinal); Assert.DoesNotContain("_gl = GL.GetApi(_window!)", source, StringComparison.Ordinal); Assert.DoesNotContain("_input = _window!.CreateInput()", source, StringComparison.Ordinal); } private sealed class Fixture : IDisposable { private readonly HostInputCameraCompositionPoint? _failure; public Fixture(HostInputCameraCompositionPoint? failure = null) { _failure = failure; IKeyboard keyboard = DispatchProxy.Create(); IMouse mouse = DispatchProxy.Create(); Input = new InputContext(keyboard, mouse); Platform = new GameWindowPlatformResult(null!, Input); ViewportAspect = new ViewportAspectState(); Framebuffer = new FramebufferResizeController(ViewportAspect); Capture = new CaptureSource(); Movement = new DispatcherMovementInputSource(Capture); CameraInput = new DispatcherCameraInputSource(); PlayerMode = new LocalPlayerModeState(); Chase = new ChaseCameraInputState(); Pointer = new PointerPositionState(); Factory = new Factory(); Publication = new Publication(); } public List Points { get; } = []; public InputContext Input { get; } public GameWindowPlatformResult Platform { get; } public ViewportAspectState ViewportAspect { get; } public FramebufferResizeController Framebuffer { get; } public CaptureSource Capture { get; } public DispatcherMovementInputSource Movement { get; } public DispatcherCameraInputSource CameraInput { get; } public LocalPlayerModeState PlayerMode { get; } public ChaseCameraInputState Chase { get; } public PointerPositionState Pointer { get; } public Factory Factory { get; } public Publication Publication { get; } public HostInputCameraCompositionPhase Phase() => new( new HostInputCameraDependencies( Framebuffer, new Vector2D(1280, 720), new HostQuiescenceGate(), Capture, KeyBindings.RetailDefaults(), Movement, CameraInput, PlayerMode, Chase, Pointer, new DiagnosticLog()), Publication, Factory, point => { Points.Add(point); if (_failure == point) throw new InvalidOperationException($"fault at {point}"); }); public void Dispose() => Publication.Dispose(); } private sealed class Publication : IGameWindowHostInputCameraPublication, IDisposable { public GpuFrameFlightController? GpuFrames { get; private set; } public SilkKeyboardSource? Keyboard { get; private set; } public SilkMouseSource? Mouse { get; private set; } public IMouseLookCursor? Cursor { get; private set; } public InputDispatcher? Dispatcher { get; private set; } public CameraController? Camera { get; private set; } public CameraPointerInputController? Pointer { get; private set; } public void PublishGpuFrameFlights(GpuFrameFlightController value) => GpuFrames = PublishOnce(GpuFrames, value); public void PublishKeyboardSource(SilkKeyboardSource value) => Keyboard = PublishOnce(Keyboard, value); public void PublishMouseSource(SilkMouseSource value) => Mouse = PublishOnce(Mouse, value); public void PublishMouseLookCursor(IMouseLookCursor value) => Cursor = PublishOnce(Cursor, value); public void PublishInputDispatcher(InputDispatcher value) => Dispatcher = PublishOnce(Dispatcher, value); public void PublishCameraController(CameraController value) => Camera = PublishOnce(Camera, value); public void PublishCameraPointerInput(CameraPointerInputController value) => Pointer = PublishOnce(Pointer, value); public void AssertPublishedThrough(HostInputCameraCompositionPoint point) { Assert.Equal( point >= HostInputCameraCompositionPoint.GpuFrameFlightsPublished, GpuFrames is not null); Assert.Equal( point >= HostInputCameraCompositionPoint.KeyboardPublished, Keyboard is not null); Assert.Equal( point >= HostInputCameraCompositionPoint.MousePublished, Mouse is not null); Assert.Equal( point >= HostInputCameraCompositionPoint.MouseLookCursorPublished, Cursor is not null); Assert.Equal( point >= HostInputCameraCompositionPoint.DispatcherPublished, Dispatcher is not null); Assert.Equal( point >= HostInputCameraCompositionPoint.CameraPublished, Camera is not null); Assert.Equal( point >= HostInputCameraCompositionPoint.CameraPointerPublished, Pointer is not null); } public void Dispose() { Pointer?.Dispose(); Pointer = null; Dispatcher?.Dispose(); Dispatcher = null; Mouse?.Dispose(); Mouse = null; Keyboard?.Dispose(); Keyboard = null; GpuFrames?.Dispose(); GpuFrames = null; } private static T PublishOnce(T? current, T value) where T : class { if (current is not null) throw new InvalidOperationException("duplicate publication"); return value; } } private sealed class Factory : IHostInputCameraCompositionFactory { public ViewportTarget Viewport { get; } = new(); private readonly KeyboardSurface _keyboard = new(); private readonly MouseSurface _mouse = new(); private readonly RawPointerSurface _rawPointer = new(); public IFramebufferViewportTarget CreateViewportTarget(GL gl) => Viewport; public GpuFrameFlightController CreateGpuFrameFlights(GL gl) => new(new FenceApi()); public WorldRenderDiagnostics CreateWorldRenderDiagnostics( GL gl, IRenderFrameDiagnosticLog log) => new(new GlStateReader(), log); public SilkKeyboardSource CreateKeyboardSource( IKeyboard keyboard, HostQuiescenceGate quiescence) => SilkKeyboardSource.CreateDetached(_keyboard, quiescence); public SilkMouseSource CreateMouseSource( IMouse mouse, IInputCaptureSource capture, IKeyboardSource? keyboard, HostQuiescenceGate quiescence) => SilkMouseSource.CreateDetached(_mouse, capture, keyboard, quiescence); public IMouseLookCursor CreateMouseLookCursor(IMouse mouse) => new Cursor(); public InputDispatcher CreateInputDispatcher( IKeyboardSource keyboard, IMouseSource mouse, KeyBindings bindings) => InputDispatcher.CreateDetached(keyboard, mouse, bindings); public CameraController CreateCameraController() => new(new OrbitCamera(), new FlyCamera()); public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) => new CameraTarget(camera); public CameraPointerInputController CreateCameraPointerInput( IReadOnlyList mice, HostQuiescenceGate quiescence, IInputCaptureSource capture, LocalPlayerModeState playerMode, CameraController camera, ChaseCameraInputState chase, IMouseSource mouse, PointerPositionState pointer) => new( [_rawPointer], new CursorModeTarget(), quiescence, capture, playerMode, camera, chase, mouse, pointer, new Clock()); } private sealed class InputContext(IKeyboard keyboard, IMouse mouse) : IInputContext { public nint Handle => 1; public IReadOnlyList Gamepads { get; } = []; public IReadOnlyList Joysticks { get; } = []; public IReadOnlyList Keyboards { get; } = [keyboard]; public IReadOnlyList Mice { get; } = [mouse]; public IReadOnlyList OtherDevices { get; } = []; public event Action? ConnectionChanged { add { } remove { } } public void Dispose() { } } public class NullDeviceProxy : DispatchProxy { protected override object? Invoke( MethodInfo? targetMethod, object?[]? args) { Type returnType = targetMethod?.ReturnType ?? typeof(void); if (returnType == typeof(void)) return null; if (returnType == typeof(string)) return string.Empty; return returnType.IsValueType ? Activator.CreateInstance(returnType) : null; } } private sealed class KeyboardSurface : IKeyboardEventSurface { public void AddKeyDown(Action callback) { } public void RemoveKeyDown(Action callback) { } public void AddKeyUp(Action callback) { } public void RemoveKeyUp(Action callback) { } public bool IsKeyPressed(Key key) => false; } private sealed class MouseSurface : IMouseEventSurface { public void AddMouseDown(Action callback) { } public void RemoveMouseDown(Action callback) { } public void AddMouseUp(Action callback) { } public void RemoveMouseUp(Action callback) { } public void AddMouseMove(Action callback) { } public void RemoveMouseMove(Action callback) { } public void AddScroll(Action callback) { } public void RemoveScroll(Action callback) { } public bool IsButtonPressed(MouseButton button) => false; } private sealed class RawPointerSurface : IRawPointerSurface { public void AddMouseMove(Action callback) { } public void RemoveMouseMove(Action callback) { } } private sealed class CursorModeTarget : IPointerCursorModeTarget { public CursorMode CursorMode { get; set; } } private sealed class Cursor : IMouseLookCursor { public bool HasSavedMode { get; private set; } public void Hide() => HasSavedMode = true; public void Restore() => HasSavedMode = false; } private sealed class Clock : IInputMonotonicClock { public float NowSeconds => 0; } private sealed class CaptureSource : IInputCaptureSource { public bool WantCaptureMouse => false; public bool WantCaptureKeyboard => false; public bool DevToolsWantCaptureKeyboard => false; } private sealed class ViewportTarget : IFramebufferViewportTarget { public (int Width, int Height) Size { get; private set; } public void ResizeViewport(int width, int height) => Size = (width, height); } private sealed class CameraTarget(CameraController camera) : IFramebufferCameraTarget { public void SetAspect(float aspect) => camera.SetAspect(aspect); } private sealed class FenceApi : IGpuFenceApi { public nint Insert() => 1; public GpuFenceWaitResult Wait( nint fence, bool flushCommands, ulong timeoutNanoseconds) => GpuFenceWaitResult.Signaled; public void Delete(nint fence) { } } private sealed class GlStateReader : IRenderGlStateReader { public RenderGlStateSnapshot CaptureState() => default; public RenderGlScissorSnapshot CaptureScissor() => default; } private sealed class DiagnosticLog : IRenderFrameDiagnosticLog { public void WriteLine(string message) { } } private static string FindRepoRoot() { DirectoryInfo? directory = new(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) return directory.FullName; directory = directory.Parent; } throw new DirectoryNotFoundException("Could not find AcDream.slnx."); } }