refactor(app): compose platform input and camera startup
This commit is contained in:
parent
adb204560d
commit
1d51e35c14
4 changed files with 877 additions and 88 deletions
|
|
@ -0,0 +1,424 @@
|
|||
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<HostInputCameraCompositionPoint>(),
|
||||
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<InvalidOperationException>(() =>
|
||||
fixture.Phase().Compose(fixture.Platform));
|
||||
|
||||
HostInputCameraCompositionPoint[] expected =
|
||||
Enum.GetValues<HostInputCameraCompositionPoint>()
|
||||
.TakeWhile(point => point <= faultPoint)
|
||||
.ToArray();
|
||||
Assert.Equal(expected, fixture.Points);
|
||||
fixture.Publication.AssertPublishedThrough(faultPoint);
|
||||
}
|
||||
|
||||
public static TheoryData<int> FaultPointValues()
|
||||
{
|
||||
var data = new TheoryData<int>();
|
||||
foreach (HostInputCameraCompositionPoint point in
|
||||
Enum.GetValues<HostInputCameraCompositionPoint>())
|
||||
{
|
||||
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<IKeyboard, NullDeviceProxy>();
|
||||
IMouse mouse = DispatchProxy.Create<IMouse, NullDeviceProxy>();
|
||||
Input = new InputContext(keyboard, mouse);
|
||||
Platform = new GameWindowPlatformResult<GL, IInputContext>(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<HostInputCameraCompositionPoint> Points { get; } = [];
|
||||
public InputContext Input { get; }
|
||||
public GameWindowPlatformResult<GL, IInputContext> 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<int>(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>(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<IMouse> 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<IGamepad> Gamepads { get; } = [];
|
||||
public IReadOnlyList<IJoystick> Joysticks { get; } = [];
|
||||
public IReadOnlyList<IKeyboard> Keyboards { get; } = [keyboard];
|
||||
public IReadOnlyList<IMouse> Mice { get; } = [mouse];
|
||||
public IReadOnlyList<IInputDevice> OtherDevices { get; } = [];
|
||||
public event Action<IInputDevice, bool>? 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<Key> callback) { }
|
||||
public void RemoveKeyDown(Action<Key> callback) { }
|
||||
public void AddKeyUp(Action<Key> callback) { }
|
||||
public void RemoveKeyUp(Action<Key> callback) { }
|
||||
public bool IsKeyPressed(Key key) => false;
|
||||
}
|
||||
|
||||
private sealed class MouseSurface : IMouseEventSurface
|
||||
{
|
||||
public void AddMouseDown(Action<MouseButton> callback) { }
|
||||
public void RemoveMouseDown(Action<MouseButton> callback) { }
|
||||
public void AddMouseUp(Action<MouseButton> callback) { }
|
||||
public void RemoveMouseUp(Action<MouseButton> callback) { }
|
||||
public void AddMouseMove(Action<Vector2> callback) { }
|
||||
public void RemoveMouseMove(Action<Vector2> callback) { }
|
||||
public void AddScroll(Action<float> callback) { }
|
||||
public void RemoveScroll(Action<float> callback) { }
|
||||
public bool IsButtonPressed(MouseButton button) => false;
|
||||
}
|
||||
|
||||
private sealed class RawPointerSurface : IRawPointerSurface
|
||||
{
|
||||
public void AddMouseMove(Action<Vector2> callback) { }
|
||||
public void RemoveMouseMove(Action<Vector2> 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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue