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
333
src/AcDream.App/Composition/HostInputCameraComposition.cs
Normal file
333
src/AcDream.App/Composition/HostInputCameraComposition.cs
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
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.Composition;
|
||||
|
||||
internal interface IGameWindowHostInputCameraPublication
|
||||
{
|
||||
void PublishGpuFrameFlights(GpuFrameFlightController value);
|
||||
void PublishKeyboardSource(SilkKeyboardSource value);
|
||||
void PublishMouseSource(SilkMouseSource value);
|
||||
void PublishMouseLookCursor(IMouseLookCursor value);
|
||||
void PublishInputDispatcher(InputDispatcher value);
|
||||
void PublishCameraController(CameraController value);
|
||||
void PublishCameraPointerInput(CameraPointerInputController value);
|
||||
}
|
||||
|
||||
internal sealed record HostInputCameraResult(
|
||||
GpuFrameFlightController GpuFrameFlights,
|
||||
WorldRenderDiagnostics WorldRenderDiagnostics,
|
||||
SilkKeyboardSource? KeyboardSource,
|
||||
SilkMouseSource? MouseSource,
|
||||
IMouseLookCursor? MouseLookCursor,
|
||||
InputDispatcher? InputDispatcher,
|
||||
CameraController CameraController,
|
||||
CameraPointerInputController? CameraPointerInput);
|
||||
|
||||
internal sealed record HostInputCameraDependencies(
|
||||
FramebufferResizeController FramebufferResize,
|
||||
Vector2D<int> InitialFramebufferSize,
|
||||
HostQuiescenceGate HostQuiescence,
|
||||
IInputCaptureSource InputCapture,
|
||||
KeyBindings KeyBindings,
|
||||
DispatcherMovementInputSource MovementInput,
|
||||
DispatcherCameraInputSource CameraInput,
|
||||
LocalPlayerModeState LocalPlayerMode,
|
||||
ChaseCameraInputState ChaseCameraInput,
|
||||
PointerPositionState PointerPosition,
|
||||
IRenderFrameDiagnosticLog RenderDiagnosticLog);
|
||||
|
||||
internal interface IHostInputCameraCompositionFactory
|
||||
{
|
||||
IFramebufferViewportTarget CreateViewportTarget(GL gl);
|
||||
GpuFrameFlightController CreateGpuFrameFlights(GL gl);
|
||||
WorldRenderDiagnostics CreateWorldRenderDiagnostics(
|
||||
GL gl,
|
||||
IRenderFrameDiagnosticLog log);
|
||||
SilkKeyboardSource CreateKeyboardSource(
|
||||
IKeyboard keyboard,
|
||||
HostQuiescenceGate quiescence);
|
||||
SilkMouseSource CreateMouseSource(
|
||||
IMouse mouse,
|
||||
IInputCaptureSource capture,
|
||||
IKeyboardSource? keyboard,
|
||||
HostQuiescenceGate quiescence);
|
||||
IMouseLookCursor CreateMouseLookCursor(IMouse mouse);
|
||||
InputDispatcher CreateInputDispatcher(
|
||||
IKeyboardSource keyboard,
|
||||
IMouseSource mouse,
|
||||
KeyBindings bindings);
|
||||
CameraController CreateCameraController();
|
||||
IFramebufferCameraTarget CreateCameraTarget(CameraController camera);
|
||||
CameraPointerInputController CreateCameraPointerInput(
|
||||
IReadOnlyList<IMouse> mice,
|
||||
HostQuiescenceGate quiescence,
|
||||
IInputCaptureSource capture,
|
||||
LocalPlayerModeState playerMode,
|
||||
CameraController camera,
|
||||
ChaseCameraInputState chase,
|
||||
IMouseSource mouse,
|
||||
PointerPositionState pointer);
|
||||
}
|
||||
|
||||
internal sealed class RetailHostInputCameraCompositionFactory
|
||||
: IHostInputCameraCompositionFactory
|
||||
{
|
||||
public IFramebufferViewportTarget CreateViewportTarget(GL gl) =>
|
||||
new SilkFramebufferViewportTarget(gl);
|
||||
|
||||
public GpuFrameFlightController CreateGpuFrameFlights(GL gl) => new(gl);
|
||||
|
||||
public WorldRenderDiagnostics CreateWorldRenderDiagnostics(
|
||||
GL gl,
|
||||
IRenderFrameDiagnosticLog log) =>
|
||||
new(new SilkRenderGlStateReader(gl), 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 SilkMouseLookCursor(mouse);
|
||||
|
||||
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 CameraFramebufferTarget(camera);
|
||||
|
||||
public CameraPointerInputController CreateCameraPointerInput(
|
||||
IReadOnlyList<IMouse> mice,
|
||||
HostQuiescenceGate quiescence,
|
||||
IInputCaptureSource capture,
|
||||
LocalPlayerModeState playerMode,
|
||||
CameraController camera,
|
||||
ChaseCameraInputState chase,
|
||||
IMouseSource mouse,
|
||||
PointerPositionState pointer) =>
|
||||
CameraPointerInputController.Create(
|
||||
mice,
|
||||
quiescence,
|
||||
capture,
|
||||
playerMode,
|
||||
camera,
|
||||
chase,
|
||||
mouse,
|
||||
pointer,
|
||||
new EnvironmentInputMonotonicClock());
|
||||
}
|
||||
|
||||
internal enum HostInputCameraCompositionPoint
|
||||
{
|
||||
ViewportBound,
|
||||
GpuFrameFlightsPublished,
|
||||
KeyboardPublished,
|
||||
KeyboardAttached,
|
||||
MousePublished,
|
||||
MouseAttached,
|
||||
MouseLookCursorPublished,
|
||||
DispatcherPublished,
|
||||
DispatcherAttached,
|
||||
MovementInputBound,
|
||||
CameraInputBound,
|
||||
CameraPublished,
|
||||
CameraTargetBound,
|
||||
InitialFramebufferApplied,
|
||||
CameraPointerPublished,
|
||||
CameraPointerAttached,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production Phase 1. Callback-bearing owners publish to the lifetime shell
|
||||
/// before attachment, so a side-effecting event accessor cannot leave an
|
||||
/// unreachable subscription after partial startup failure.
|
||||
/// </summary>
|
||||
internal sealed class HostInputCameraCompositionPhase :
|
||||
IHostInputCameraCompositionPhase<
|
||||
GameWindowPlatformResult<GL, IInputContext>,
|
||||
HostInputCameraResult>
|
||||
{
|
||||
private readonly HostInputCameraDependencies _dependencies;
|
||||
private readonly IGameWindowHostInputCameraPublication _publication;
|
||||
private readonly IHostInputCameraCompositionFactory _factory;
|
||||
private readonly Action<HostInputCameraCompositionPoint>? _faultInjection;
|
||||
|
||||
public HostInputCameraCompositionPhase(
|
||||
HostInputCameraDependencies dependencies,
|
||||
IGameWindowHostInputCameraPublication publication,
|
||||
IHostInputCameraCompositionFactory? factory = null,
|
||||
Action<HostInputCameraCompositionPoint>? faultInjection = null)
|
||||
{
|
||||
_dependencies = dependencies
|
||||
?? throw new ArgumentNullException(nameof(dependencies));
|
||||
_publication = publication
|
||||
?? throw new ArgumentNullException(nameof(publication));
|
||||
_factory = factory ?? new RetailHostInputCameraCompositionFactory();
|
||||
_faultInjection = faultInjection;
|
||||
}
|
||||
|
||||
public HostInputCameraResult Compose(
|
||||
GameWindowPlatformResult<GL, IInputContext> platform)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(platform);
|
||||
var scope = new CompositionAcquisitionScope();
|
||||
try
|
||||
{
|
||||
HostInputCameraResult result = ComposeCore(platform, scope);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
scope.RollbackAndThrow(failure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
private HostInputCameraResult ComposeCore(
|
||||
GameWindowPlatformResult<GL, IInputContext> platform,
|
||||
CompositionAcquisitionScope scope)
|
||||
{
|
||||
GL gl = platform.Graphics;
|
||||
IInputContext input = platform.Input;
|
||||
|
||||
_dependencies.FramebufferResize.BindViewport(
|
||||
_factory.CreateViewportTarget(gl));
|
||||
Fault(HostInputCameraCompositionPoint.ViewportBound);
|
||||
|
||||
GpuFrameFlightController gpuFrames = scope.Acquire(
|
||||
"GPU frame flights",
|
||||
() => _factory.CreateGpuFrameFlights(gl),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishGpuFrameFlights);
|
||||
Fault(HostInputCameraCompositionPoint.GpuFrameFlightsPublished);
|
||||
|
||||
WorldRenderDiagnostics diagnostics =
|
||||
_factory.CreateWorldRenderDiagnostics(
|
||||
gl,
|
||||
_dependencies.RenderDiagnosticLog);
|
||||
|
||||
IKeyboard? firstKeyboard = input.Keyboards.FirstOrDefault();
|
||||
IMouse? firstMouse = input.Mice.FirstOrDefault();
|
||||
SilkKeyboardSource? keyboard = null;
|
||||
SilkMouseSource? mouse = null;
|
||||
IMouseLookCursor? cursor = null;
|
||||
InputDispatcher? dispatcher = null;
|
||||
CameraPointerInputController? pointer = null;
|
||||
|
||||
if (firstKeyboard is not null)
|
||||
{
|
||||
keyboard = scope.Acquire(
|
||||
"keyboard source",
|
||||
() => _factory.CreateKeyboardSource(
|
||||
firstKeyboard,
|
||||
_dependencies.HostQuiescence),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishKeyboardSource);
|
||||
Fault(HostInputCameraCompositionPoint.KeyboardPublished);
|
||||
keyboard.Attach();
|
||||
Fault(HostInputCameraCompositionPoint.KeyboardAttached);
|
||||
}
|
||||
|
||||
if (firstMouse is not null)
|
||||
{
|
||||
mouse = scope.Acquire(
|
||||
"mouse source",
|
||||
() => _factory.CreateMouseSource(
|
||||
firstMouse,
|
||||
_dependencies.InputCapture,
|
||||
keyboard,
|
||||
_dependencies.HostQuiescence),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishMouseSource);
|
||||
Fault(HostInputCameraCompositionPoint.MousePublished);
|
||||
mouse.Attach();
|
||||
Fault(HostInputCameraCompositionPoint.MouseAttached);
|
||||
|
||||
cursor = _factory.CreateMouseLookCursor(firstMouse);
|
||||
_publication.PublishMouseLookCursor(cursor);
|
||||
Fault(HostInputCameraCompositionPoint.MouseLookCursorPublished);
|
||||
}
|
||||
|
||||
if (keyboard is not null && mouse is not null)
|
||||
{
|
||||
dispatcher = scope.Acquire(
|
||||
"input dispatcher",
|
||||
() => _factory.CreateInputDispatcher(
|
||||
keyboard,
|
||||
mouse,
|
||||
_dependencies.KeyBindings),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishInputDispatcher);
|
||||
Fault(HostInputCameraCompositionPoint.DispatcherPublished);
|
||||
dispatcher.Attach();
|
||||
Fault(HostInputCameraCompositionPoint.DispatcherAttached);
|
||||
|
||||
_dependencies.MovementInput.Bind(dispatcher);
|
||||
Fault(HostInputCameraCompositionPoint.MovementInputBound);
|
||||
_dependencies.CameraInput.Bind(dispatcher);
|
||||
Fault(HostInputCameraCompositionPoint.CameraInputBound);
|
||||
}
|
||||
|
||||
CameraController camera = _factory.CreateCameraController();
|
||||
_publication.PublishCameraController(camera);
|
||||
Fault(HostInputCameraCompositionPoint.CameraPublished);
|
||||
_dependencies.FramebufferResize.BindCamera(
|
||||
_factory.CreateCameraTarget(camera));
|
||||
Fault(HostInputCameraCompositionPoint.CameraTargetBound);
|
||||
_dependencies.FramebufferResize.Resize(
|
||||
_dependencies.InitialFramebufferSize);
|
||||
Fault(HostInputCameraCompositionPoint.InitialFramebufferApplied);
|
||||
|
||||
if (mouse is not null && firstMouse is not null)
|
||||
{
|
||||
pointer = scope.Acquire(
|
||||
"camera pointer input",
|
||||
() => _factory.CreateCameraPointerInput(
|
||||
input.Mice,
|
||||
_dependencies.HostQuiescence,
|
||||
_dependencies.InputCapture,
|
||||
_dependencies.LocalPlayerMode,
|
||||
camera,
|
||||
_dependencies.ChaseCameraInput,
|
||||
mouse,
|
||||
_dependencies.PointerPosition),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishCameraPointerInput);
|
||||
Fault(HostInputCameraCompositionPoint.CameraPointerPublished);
|
||||
pointer.AttachRaw();
|
||||
Fault(HostInputCameraCompositionPoint.CameraPointerAttached);
|
||||
}
|
||||
|
||||
return new HostInputCameraResult(
|
||||
gpuFrames,
|
||||
diagnostics,
|
||||
keyboard,
|
||||
mouse,
|
||||
cursor,
|
||||
dispatcher,
|
||||
camera,
|
||||
pointer);
|
||||
}
|
||||
|
||||
private void Fault(HostInputCameraCompositionPoint point) =>
|
||||
_faultInjection?.Invoke(point);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Plugins;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -11,7 +12,10 @@ using Silk.NET.Windowing;
|
|||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed class GameWindow : IDisposable
|
||||
public sealed class GameWindow :
|
||||
IDisposable,
|
||||
IGameWindowPlatformPublication<GL, IInputContext>,
|
||||
IGameWindowHostInputCameraPublication
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -653,56 +657,96 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
void IGameWindowPlatformPublication<GL, IInputContext>.PublishGraphics(
|
||||
GL graphics) =>
|
||||
PublishCompositionOwner(ref _gl, graphics, "graphics API");
|
||||
|
||||
void IGameWindowPlatformPublication<GL, IInputContext>.PublishInput(
|
||||
IInputContext input) =>
|
||||
PublishCompositionOwner(ref _input, input, "input context");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishGpuFrameFlights(
|
||||
GpuFrameFlightController value) =>
|
||||
PublishCompositionOwner(ref _gpuFrameFlights, value, "GPU frame flights");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
|
||||
AcDream.App.Input.SilkKeyboardSource value) =>
|
||||
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishMouseSource(
|
||||
AcDream.App.Input.SilkMouseSource value) =>
|
||||
PublishCompositionOwner(ref _mouseSource, value, "mouse source");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishMouseLookCursor(
|
||||
AcDream.App.Input.IMouseLookCursor value) =>
|
||||
PublishCompositionOwner(ref _mouseLookCursor, value, "mouse-look cursor");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishInputDispatcher(
|
||||
AcDream.UI.Abstractions.Input.InputDispatcher value) =>
|
||||
PublishCompositionOwner(ref _inputDispatcher, value, "input dispatcher");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishCameraController(
|
||||
CameraController value) =>
|
||||
PublishCompositionOwner(ref _cameraController, value, "camera controller");
|
||||
|
||||
void IGameWindowHostInputCameraPublication.PublishCameraPointerInput(
|
||||
AcDream.App.Input.CameraPointerInputController value) =>
|
||||
PublishCompositionOwner(ref _cameraPointerInput, value, "camera pointer input");
|
||||
|
||||
private static void PublishCompositionOwner<T>(
|
||||
ref T? destination,
|
||||
T value,
|
||||
string name)
|
||||
where T : class
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
if (destination is not null)
|
||||
throw new InvalidOperationException(
|
||||
$"The GameWindow composition shell already owns {name}.");
|
||||
destination = value;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_gl), nameof(_input))]
|
||||
private GameWindowPlatformResult<GL, IInputContext> AcquirePlatform()
|
||||
{
|
||||
GameWindowPlatformResult<GL, IInputContext> platform =
|
||||
GameWindowPlatformAcquisition.Acquire(
|
||||
() => GL.GetApi(_window!),
|
||||
static gl => gl.Dispose(),
|
||||
() => _window!.CreateInput(),
|
||||
static input => input.Dispose(),
|
||||
this);
|
||||
if (_gl is null || _input is null)
|
||||
throw new InvalidOperationException(
|
||||
"Platform acquisition returned without publishing both owners.");
|
||||
return platform;
|
||||
}
|
||||
|
||||
private void OnLoad()
|
||||
{
|
||||
// Task 7: wire the physics data cache into the engine so Transition can
|
||||
// run narrow-phase BSP tests during FindObjCollisions.
|
||||
_physicsEngine.DataCache = _physicsDataCache;
|
||||
|
||||
_gl = GL.GetApi(_window!);
|
||||
_framebufferResize.BindViewport(
|
||||
new SilkFramebufferViewportTarget(_gl));
|
||||
_gpuFrameFlights = new AcDream.App.Rendering.GpuFrameFlightController(_gl);
|
||||
var worldRenderDiagnostics = new AcDream.App.Rendering.WorldRenderDiagnostics(
|
||||
new AcDream.App.Rendering.SilkRenderGlStateReader(_gl),
|
||||
_renderDiagnosticLog);
|
||||
_input = _window!.CreateInput();
|
||||
GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();
|
||||
|
||||
// Phase K.1b — every keyboard/mouse handler routes through the
|
||||
// InputDispatcher. The legacy direct kb.KeyDown / mouse.MouseDown
|
||||
// switches are gone; subscribers below own all game-side reactions.
|
||||
// We KEEP a direct mouse.MouseMove handler because mouse-delta is
|
||||
// axis input, not chord input — but with explicit WantCaptureMouse
|
||||
// gating and the previous "_playerMouseDeltaX +=" line dropped so
|
||||
// mouse delta NEVER drives character yaw (regression-prevention
|
||||
// per K.1b plan §D).
|
||||
var firstKb = _input.Keyboards.FirstOrDefault();
|
||||
var firstMouse = _input.Mice.FirstOrDefault();
|
||||
if (firstKb is not null)
|
||||
{
|
||||
_kbSource = AcDream.App.Input.SilkKeyboardSource.CreateDetached(
|
||||
firstKb,
|
||||
_hostQuiescence);
|
||||
_kbSource.Attach();
|
||||
}
|
||||
if (firstMouse is not null)
|
||||
{
|
||||
_mouseSource = AcDream.App.Input.SilkMouseSource.CreateDetached(
|
||||
firstMouse,
|
||||
_inputCapture,
|
||||
_kbSource,
|
||||
_hostQuiescence);
|
||||
_mouseSource.Attach();
|
||||
_mouseLookCursor = new AcDream.App.Input.SilkMouseLookCursor(firstMouse);
|
||||
}
|
||||
if (_kbSource is not null && _mouseSource is not null)
|
||||
{
|
||||
_inputDispatcher = AcDream.UI.Abstractions.Input.InputDispatcher.CreateDetached(
|
||||
_kbSource, _mouseSource, _keyBindings);
|
||||
_inputDispatcher.Attach();
|
||||
_movementInput.Bind(_inputDispatcher);
|
||||
_cameraInput.Bind(_inputDispatcher);
|
||||
}
|
||||
HostInputCameraResult hostInputCamera =
|
||||
new HostInputCameraCompositionPhase(
|
||||
new HostInputCameraDependencies(
|
||||
_framebufferResize,
|
||||
_window!.FramebufferSize,
|
||||
_hostQuiescence,
|
||||
_inputCapture,
|
||||
_keyBindings,
|
||||
_movementInput,
|
||||
_cameraInput,
|
||||
_localPlayerMode,
|
||||
_chaseCameraInput,
|
||||
_pointerPosition,
|
||||
_renderDiagnosticLog),
|
||||
this).Compose(platform);
|
||||
WorldRenderDiagnostics worldRenderDiagnostics =
|
||||
hostInputCamera.WorldRenderDiagnostics;
|
||||
|
||||
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
|
@ -754,27 +798,6 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine("world-hud font: no system monospace font found");
|
||||
}
|
||||
|
||||
var orbit = new OrbitCamera();
|
||||
var fly = new FlyCamera();
|
||||
_cameraController = new CameraController(orbit, fly);
|
||||
_framebufferResize.BindCamera(
|
||||
new CameraFramebufferTarget(_cameraController));
|
||||
_framebufferResize.Resize(_window!.FramebufferSize);
|
||||
if (_mouseSource is not null && firstMouse is not null)
|
||||
{
|
||||
_cameraPointerInput = AcDream.App.Input.CameraPointerInputController.Create(
|
||||
_input.Mice,
|
||||
_hostQuiescence,
|
||||
_inputCapture,
|
||||
_localPlayerMode,
|
||||
_cameraController,
|
||||
_chaseCameraInput,
|
||||
_mouseSource,
|
||||
_pointerPosition,
|
||||
new AcDream.App.Input.EnvironmentInputMonotonicClock());
|
||||
_cameraPointerInput.AttachRaw();
|
||||
}
|
||||
|
||||
_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);
|
||||
_magicCatalog = AcDream.App.Spells.MagicCatalog.Load(_dats);
|
||||
SpellBook.InstallMetadata(_magicCatalog.SpellTable);
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -51,19 +51,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
|
||||
AssertAppearsInOrder(
|
||||
body,
|
||||
"_gl = GL.GetApi(_window!);",
|
||||
"_input = _window!.CreateInput();",
|
||||
"_kbSource = AcDream.App.Input.SilkKeyboardSource.CreateDetached(",
|
||||
"_kbSource.Attach();",
|
||||
"_mouseSource = AcDream.App.Input.SilkMouseSource.CreateDetached(",
|
||||
"_mouseSource.Attach();",
|
||||
"_inputDispatcher = AcDream.UI.Abstractions.Input.InputDispatcher.CreateDetached(",
|
||||
"_inputDispatcher.Attach();",
|
||||
"_movementInput.Bind(_inputDispatcher);",
|
||||
"_cameraInput.Bind(_inputDispatcher);",
|
||||
"_cameraController = new CameraController(orbit, fly);",
|
||||
"_cameraPointerInput = AcDream.App.Input.CameraPointerInputController.Create(",
|
||||
"_cameraPointerInput.AttachRaw();",
|
||||
"GameWindowPlatformResult<GL, IInputContext> platform = AcquirePlatform();",
|
||||
"new HostInputCameraCompositionPhase(",
|
||||
"this).Compose(platform);",
|
||||
"_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);",
|
||||
"_runtimeSettings.ApplyStartup(",
|
||||
"new RuntimeSettingsStartupTargets(",
|
||||
|
|
@ -86,15 +76,24 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"_gameplayInputActions.Attach();",
|
||||
"_liveSessionHost.Start(_options)");
|
||||
Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)"));
|
||||
Assert.Contains("if (firstKb is not null)", body, StringComparison.Ordinal);
|
||||
Assert.Contains("if (firstMouse is not null)", body, StringComparison.Ordinal);
|
||||
string phaseOne = Slice(
|
||||
File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"HostInputCameraComposition.cs")),
|
||||
"private HostInputCameraResult ComposeCore(",
|
||||
"private void Fault(");
|
||||
Assert.Contains("if (firstKeyboard is not null)", phaseOne, StringComparison.Ordinal);
|
||||
Assert.Contains("if (firstMouse is not null)", phaseOne, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"if (_kbSource is not null && _mouseSource is not null)",
|
||||
body,
|
||||
"if (keyboard is not null && mouse is not null)",
|
||||
phaseOne,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"if (firstKb is not null && firstMouse is not null)",
|
||||
body,
|
||||
"if (firstKeyboard is not null && firstMouse is not null)",
|
||||
phaseOne,
|
||||
StringComparison.Ordinal);
|
||||
int start = body.IndexOf("_liveSessionHost.Start(_options)", StringComparison.Ordinal);
|
||||
string postStart = body[start..];
|
||||
|
|
@ -199,11 +198,21 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
Assert.DoesNotContain("Viewport(", body, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("SetAspect(", body, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ResetLayout(", body, StringComparison.Ordinal);
|
||||
string phaseOne = Slice(
|
||||
File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Composition",
|
||||
"HostInputCameraComposition.cs")),
|
||||
"private HostInputCameraResult ComposeCore(",
|
||||
"private void Fault(");
|
||||
AssertAppearsInOrder(
|
||||
load,
|
||||
"_framebufferResize.BindViewport(",
|
||||
"_framebufferResize.BindCamera(",
|
||||
"_framebufferResize.Resize(_window!.FramebufferSize);");
|
||||
phaseOne,
|
||||
"_dependencies.FramebufferResize.BindViewport(",
|
||||
"_publication.PublishCameraController(camera)",
|
||||
"_dependencies.FramebufferResize.BindCamera(",
|
||||
"_dependencies.FramebufferResize.Resize(");
|
||||
Assert.DoesNotContain(
|
||||
"_viewportAspect.Update(_window",
|
||||
load,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue