ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.
The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.
Three seams, as §5.5.9 specified:
1. Platform acquisition — already generic — publishes GameWindowGraphics
instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
take their Vulkan arm when it is null; each branch names the slice that
removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
The default factory is chosen inside the phase from the platform result.
HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
backbuffer clear pass computing the same RenderFrameFoundation from the same
clock and weather owners, then private presentation over it.
§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.
VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.
One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.
Gates: strict GL offline pixel gate against 46d893f7 measures 1.24e-05 (7 of
563,200 pixels), inside the documented 15-23 px / 4.1e-05 band, so GL behaviour
did not move. App tests 4,075/3 skips; complete Release suite 9,138/5 skips.
One full Vulkan run with VK_LAYER_KHRONOS_validation: zero errors, zero
warnings. Both Vulkan runs converged the ownership ledger — no [shutdown]
diagnostic on either stream. The reduced probe harness presented 34,811
validation-clean frames.
No divergence-register row: GL is the shipping backend and the pixel gate proves
it unmoved; the Vulkan arm is not a retail deviation but a backend under
construction.
Next is V4t, the texture stack, which the world arm cannot be written without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
526 lines
19 KiB
C#
526 lines
19 KiB
C#
using System.Numerics;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using AcDream.App.Combat;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Diagnostics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Settings;
|
|
using AcDream.Core.Chat;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Core.Player;
|
|
using AcDream.Core.Spells;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
using AcDream.UI.Abstractions.Settings;
|
|
using AcDream.UI.ImGui;
|
|
using Silk.NET.Input;
|
|
using Silk.NET.Maths;
|
|
using Silk.NET.OpenGL;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Tests.Composition;
|
|
|
|
public sealed class SettingsDevToolsCompositionTests
|
|
{
|
|
[Fact]
|
|
public void DisabledFrontendAppliesSettingsAndAcquiresNothingOptional()
|
|
{
|
|
using var fixture = new Fixture(enabled: false);
|
|
|
|
SettingsDevToolsResult result = fixture.Compose();
|
|
|
|
Assert.Null(result.DevTools);
|
|
Assert.False(
|
|
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
|
|
Assert.Equal(1, fixture.Startup.DisplayCalls);
|
|
Assert.Equal(1, fixture.Startup.AudioCalls);
|
|
Assert.Equal(0, fixture.Factory.Calls);
|
|
Assert.Equal(
|
|
[SettingsDevToolsCompositionPoint.SettingsApplied],
|
|
fixture.Points);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnsupportedInputDisablesBeforeAcquiringFrontendResources()
|
|
{
|
|
using var fixture = new Fixture(inputSupported: false);
|
|
|
|
SettingsDevToolsResult result = fixture.Compose();
|
|
|
|
Assert.Null(result.DevTools);
|
|
Assert.Null(fixture.Factory.Input);
|
|
Assert.Null(fixture.Factory.Bootstrap);
|
|
Assert.Equal(1, fixture.Factory.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnabledFrontendPublishesOneCompleteOwnerAfterEveryEdge()
|
|
{
|
|
using var fixture = new Fixture();
|
|
|
|
SettingsDevToolsResult result = fixture.Compose();
|
|
|
|
Assert.Same(fixture.Publication.Owner, result.DevTools);
|
|
Assert.Equal(
|
|
Enum.GetValues<SettingsDevToolsCompositionPoint>(),
|
|
fixture.Points);
|
|
Assert.True(fixture.Factory.Input!.Activated);
|
|
Assert.Equal(4, fixture.Factory.Backend!.LayoutCalls);
|
|
Assert.Equal(1, fixture.Publication.PublishCalls);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OptionalFailurePoints))]
|
|
public void OptionalPrefixFailureDisablesOnlyAfterCompleteReverseCleanup(
|
|
int pointValue)
|
|
{
|
|
var point = (SettingsDevToolsCompositionPoint)pointValue;
|
|
using var fixture = new Fixture(failurePoint: point);
|
|
|
|
SettingsDevToolsResult result = fixture.Compose();
|
|
|
|
Assert.Null(result.DevTools);
|
|
Assert.Null(fixture.Publication.Owner);
|
|
Assert.Equal(
|
|
Enum.GetValues<SettingsDevToolsCompositionPoint>()
|
|
.TakeWhile(candidate => candidate <= point),
|
|
fixture.Points);
|
|
if (fixture.Factory.Input is { } input)
|
|
Assert.True(input.IsDisposalComplete);
|
|
if (fixture.Factory.Bootstrap is { } bootstrap)
|
|
Assert.Equal(1, bootstrap.DisposeCalls);
|
|
if (fixture.Factory.Backend is { } backend)
|
|
Assert.Equal(1, backend.DisposeCalls);
|
|
}
|
|
|
|
public static TheoryData<int> OptionalFailurePoints()
|
|
{
|
|
var data = new TheoryData<int>();
|
|
foreach (SettingsDevToolsCompositionPoint point in
|
|
Enum.GetValues<SettingsDevToolsCompositionPoint>())
|
|
{
|
|
if (point is SettingsDevToolsCompositionPoint.SettingsApplied
|
|
or SettingsDevToolsCompositionPoint.DevToolsPublished)
|
|
{
|
|
continue;
|
|
}
|
|
data.Add((int)point);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
[Fact]
|
|
public void FailureAfterPublicationPropagatesToLifetimeOwner()
|
|
{
|
|
using var fixture = new Fixture(
|
|
failurePoint: SettingsDevToolsCompositionPoint.DevToolsPublished);
|
|
|
|
Assert.Throws<InvalidOperationException>(fixture.Compose);
|
|
|
|
Assert.NotNull(fixture.Publication.Owner);
|
|
Assert.False(fixture.Publication.Owner!.IsDisposalComplete);
|
|
}
|
|
|
|
[Fact]
|
|
public void CleanupFailurePropagatesAndRetrySkipsCompletedOperations()
|
|
{
|
|
using var fixture = new Fixture(
|
|
failurePoint: SettingsDevToolsCompositionPoint.InitialLayoutApplied,
|
|
backendDisposeFailures: 1);
|
|
|
|
var failure = Assert.Throws<CompositionAcquisitionException>(fixture.Compose);
|
|
Assert.False(failure.IsCleanupComplete);
|
|
Assert.Equal(1, fixture.Factory.Backend!.DisposeCalls);
|
|
Assert.True(fixture.Factory.Input!.IsDisposalComplete);
|
|
|
|
failure.RetryCleanup();
|
|
|
|
Assert.True(failure.IsCleanupComplete);
|
|
Assert.Equal(2, fixture.Factory.Backend.DisposeCalls);
|
|
Assert.Equal(1, fixture.Factory.Input.DisposeCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void BootstrapConstructionWithoutCleanupOwnerAbortsHostStartup()
|
|
{
|
|
var bootstrapFailure = (ImGuiBootstrapperConstructionException)
|
|
Activator.CreateInstance(
|
|
typeof(ImGuiBootstrapperConstructionException),
|
|
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
binder: null,
|
|
args: [new InvalidOperationException("partial Silk construction")],
|
|
culture: null)!;
|
|
using var fixture = new Fixture(bootstrapFailure: bootstrapFailure);
|
|
|
|
Assert.Throws<ImGuiBootstrapperConstructionException>(fixture.Compose);
|
|
|
|
Assert.True(fixture.Factory.Input!.IsDisposalComplete);
|
|
Assert.Null(fixture.Publication.Owner);
|
|
Assert.DoesNotContain(
|
|
fixture.Logs,
|
|
message => message.Contains("devtools disabled", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowUsesProductionPhaseAndDoesNotRetainHostClosures()
|
|
{
|
|
string source = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
|
|
Assert.Contains("new SettingsDevToolsCompositionPhase(", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("new AcDream.UI.ImGui.ImGuiBootstrapper(", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("getPlayerPosition: () => GetDebugPlayerPosition()", source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_runtimeSettings.ApplyStartup(", source,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private sealed class Fixture : IDisposable
|
|
{
|
|
private readonly SettingsDevToolsCompositionPoint? _failurePoint;
|
|
private readonly InputDispatcher _dispatcher;
|
|
private readonly FrameProfiler _profiler = new();
|
|
|
|
public Fixture(
|
|
bool enabled = true,
|
|
SettingsDevToolsCompositionPoint? failurePoint = null,
|
|
int backendDisposeFailures = 0,
|
|
bool inputSupported = true,
|
|
Exception? bootstrapFailure = null)
|
|
{
|
|
_failurePoint = failurePoint;
|
|
Factory = new Factory(
|
|
backendDisposeFailures,
|
|
inputSupported,
|
|
bootstrapFailure);
|
|
Publication = new Publication();
|
|
Startup = new StartupTarget();
|
|
Settings = new RuntimeSettingsController(
|
|
new Storage(),
|
|
QualitySettings.From,
|
|
static _ => { });
|
|
_dispatcher = InputDispatcher.CreateDetached(
|
|
new KeyboardSource(),
|
|
new MouseSource(),
|
|
new KeyBindings());
|
|
_dispatcher.Attach();
|
|
var camera = new CameraController(new OrbitCamera(), new FlyCamera());
|
|
Host = new HostInputCameraResult(
|
|
null!,
|
|
null!,
|
|
null!,
|
|
null!,
|
|
null!,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
_dispatcher,
|
|
camera,
|
|
null);
|
|
Platform = new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.OpenGl, null!);
|
|
Content = (ContentEffectsAudioResult)RuntimeHelpers.GetUninitializedObject(
|
|
typeof(ContentEffectsAudioResult));
|
|
Dependencies = new SettingsDevToolsDependencies(
|
|
DispatchProxy.Create<IView, ViewProxy>(),
|
|
Settings,
|
|
Startup,
|
|
new HostQuiescenceGate(),
|
|
GameRuntimeTestFactory.Create(),
|
|
enabled
|
|
? new SettingsDevToolsOptionalDependencies(
|
|
new Facts(),
|
|
new KeyBindingTarget(),
|
|
new DeferredCanonicalWorldEntityCountSource(),
|
|
new DeferredRenderFrameDiagnosticsSource(),
|
|
new DeferredDevToolsPlayerModeCommands())
|
|
: null,
|
|
new RuntimeDiagnosticCommandSlot(),
|
|
new CombatFeedbackSlot(),
|
|
new KeyBindings(),
|
|
_profiler,
|
|
new FramebufferResizeController(new ViewportAspectState()),
|
|
Logs.Add);
|
|
}
|
|
|
|
public List<SettingsDevToolsCompositionPoint> Points { get; } = [];
|
|
public List<string> Logs { get; } = [];
|
|
public Factory Factory { get; }
|
|
public Publication Publication { get; }
|
|
public StartupTarget Startup { get; }
|
|
public RuntimeSettingsController Settings { get; }
|
|
public SettingsDevToolsDependencies Dependencies { get; }
|
|
public HostInputCameraResult Host { get; }
|
|
public GameWindowPlatformResult<GameWindowGraphics, IInputContext> Platform { get; }
|
|
public ContentEffectsAudioResult Content { get; }
|
|
|
|
public SettingsDevToolsResult Compose() =>
|
|
new SettingsDevToolsCompositionPhase(
|
|
Dependencies,
|
|
Publication,
|
|
Factory,
|
|
point =>
|
|
{
|
|
Points.Add(point);
|
|
if (point == _failurePoint)
|
|
throw new InvalidOperationException($"fault at {point}");
|
|
}).Compose(Platform, Host, Content);
|
|
|
|
public void Dispose()
|
|
{
|
|
Dependencies.Runtime.Dispose();
|
|
Publication.Owner?.Dispose();
|
|
_dispatcher.Dispose();
|
|
_profiler.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class Publication : IGameWindowSettingsDevToolsPublication
|
|
{
|
|
public DevToolsCompositionOwner? Owner { get; private set; }
|
|
public int PublishCalls { get; private set; }
|
|
|
|
public void PublishDevTools(DevToolsCompositionOwner value)
|
|
{
|
|
if (Owner is not null)
|
|
throw new InvalidOperationException("duplicate publication");
|
|
Owner = value;
|
|
PublishCalls++;
|
|
}
|
|
}
|
|
|
|
private sealed class Factory(
|
|
int backendDisposeFailures,
|
|
bool inputSupported,
|
|
Exception? bootstrapFailure)
|
|
: ISettingsDevToolsCompositionFactory
|
|
{
|
|
public int Calls { get; private set; }
|
|
public InputContext? Input { get; private set; }
|
|
public Bootstrap? Bootstrap { get; private set; }
|
|
public Backend? Backend { get; private set; }
|
|
|
|
public bool IsSupported(IInputContext input)
|
|
{
|
|
Calls++;
|
|
return inputSupported;
|
|
}
|
|
|
|
public IDevToolsInputContext CreateInputContext(
|
|
IInputContext input,
|
|
HostQuiescenceGate quiescence)
|
|
{
|
|
Calls++;
|
|
return Input = new InputContext();
|
|
}
|
|
|
|
public IImGuiBootstrapper CreateBootstrap(
|
|
GL gl,
|
|
IView window,
|
|
IInputContext input)
|
|
{
|
|
Calls++;
|
|
if (bootstrapFailure is not null)
|
|
throw bootstrapFailure;
|
|
return Bootstrap = new Bootstrap();
|
|
}
|
|
|
|
public ImGuiPanelHost CreatePanelHost()
|
|
{
|
|
Calls++;
|
|
return new ImGuiPanelHost();
|
|
}
|
|
|
|
public IDevToolsFrameBackend CreateBackend(
|
|
IImGuiBootstrapper bootstrap,
|
|
ImGuiPanelHost panels)
|
|
{
|
|
Calls++;
|
|
return Backend = new Backend(
|
|
(Bootstrap)bootstrap,
|
|
backendDisposeFailures);
|
|
}
|
|
}
|
|
|
|
private sealed class InputContext : IDevToolsInputContext
|
|
{
|
|
public bool Activated { get; private set; }
|
|
public int DisposeCalls { get; private set; }
|
|
public bool IsDisposalComplete { get; private set; }
|
|
public nint Handle => 0;
|
|
public IReadOnlyList<IGamepad> Gamepads { get; } = [];
|
|
public IReadOnlyList<IJoystick> Joysticks { get; } = [];
|
|
public IReadOnlyList<IKeyboard> Keyboards { get; } = [];
|
|
public IReadOnlyList<IMouse> Mice { get; } = [];
|
|
public IReadOnlyList<IInputDevice> OtherDevices { get; } = [];
|
|
#pragma warning disable CS0067
|
|
public event Action<IInputDevice, bool>? ConnectionChanged;
|
|
#pragma warning restore CS0067
|
|
public void Activate() => Activated = true;
|
|
public void Deactivate() => Activated = false;
|
|
public void Dispose()
|
|
{
|
|
DisposeCalls++;
|
|
Activated = false;
|
|
IsDisposalComplete = true;
|
|
}
|
|
}
|
|
|
|
private sealed class Bootstrap : IImGuiBootstrapper
|
|
{
|
|
public int DisposeCalls { get; private set; }
|
|
public void BeginFrame(float deltaSeconds) { }
|
|
public void Render() { }
|
|
public void AbortFrame() { }
|
|
public void Dispose() => DisposeCalls++;
|
|
}
|
|
|
|
private sealed class Backend(
|
|
Bootstrap bootstrap,
|
|
int remainingDisposeFailures) : IDevToolsFrameBackend
|
|
{
|
|
private int _remainingDisposeFailures = remainingDisposeFailures;
|
|
public int DisposeCalls { get; private set; }
|
|
public int LayoutCalls { get; private set; }
|
|
public void BeginFrame(float deltaSeconds) { }
|
|
public void AbortFrame() { }
|
|
public bool BeginMainMenuBar() => false;
|
|
public void EndMainMenuBar() { }
|
|
public bool BeginMenu(string label) => false;
|
|
public void EndMenu() { }
|
|
public bool MenuItem(string label, string? shortcut = null, bool selected = false) => false;
|
|
public void Separator() { }
|
|
public void RenderPanels(AcDream.UI.Abstractions.PanelContext context) { }
|
|
public void RenderDrawData() { }
|
|
public void SetWindowLayout(
|
|
string title,
|
|
Vector2 position,
|
|
Vector2 size,
|
|
DevToolsPanelLayoutCondition condition) => LayoutCalls++;
|
|
public void Dispose()
|
|
{
|
|
DisposeCalls++;
|
|
if (_remainingDisposeFailures-- > 0)
|
|
throw new InvalidOperationException("backend cleanup failed");
|
|
bootstrap.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class StartupTarget : IRuntimeSettingsStartupTarget
|
|
{
|
|
public int DisplayCalls { get; private set; }
|
|
public int AudioCalls { get; private set; }
|
|
public void ApplyDisplay(DisplaySettings display) => DisplayCalls++;
|
|
public void ApplyAudio(AudioSettings audio) => AudioCalls++;
|
|
}
|
|
|
|
private sealed class Storage : IRuntimeSettingsStorage
|
|
{
|
|
public SettingsStore? LayoutStore => null;
|
|
public string Location => "memory://settings";
|
|
public DisplaySettings LoadDisplay() => DisplaySettings.Default;
|
|
public AudioSettings LoadAudio() => AudioSettings.Default;
|
|
public GameplaySettings LoadGameplay() => GameplaySettings.Default;
|
|
public ChatSettings LoadChat() => ChatSettings.Default;
|
|
public CharacterSettings LoadCharacter(string toonKey) => CharacterSettings.Default;
|
|
public void SaveDisplay(DisplaySettings display) { }
|
|
public void SaveAudio(AudioSettings audio) { }
|
|
public void SaveGameplay(GameplaySettings gameplay) { }
|
|
public void SaveChat(ChatSettings chat) { }
|
|
public void SaveCharacter(string toonKey, CharacterSettings character) { }
|
|
}
|
|
|
|
private sealed class KeyBindingTarget : IRuntimeKeyBindingTarget
|
|
{
|
|
public void Apply(KeyBindings bindings) { }
|
|
}
|
|
|
|
private sealed class Facts : IDevToolsRuntimeFacts
|
|
{
|
|
public Vector3 PlayerPosition => default;
|
|
public float PlayerHeadingDegrees => 0;
|
|
public uint PlayerCellId => 0;
|
|
public bool PlayerOnGround => false;
|
|
public bool InPlayerMode => false;
|
|
public bool InFlyMode => false;
|
|
public float VerticalVelocity => 0;
|
|
public int EntityCount => 0;
|
|
public int AnimatedCount => 0;
|
|
public int VisibleLandblocks => 0;
|
|
public int TotalLandblocks => 0;
|
|
public int ShadowObjectCount => 0;
|
|
public float NearestObjectDistance => float.PositiveInfinity;
|
|
public string NearestObjectLabel => "-";
|
|
public bool Colliding => false;
|
|
public bool CollisionWireframesVisible => false;
|
|
public int StreamingRadius => 0;
|
|
public float MouseSensitivity => 1;
|
|
public float ChaseDistance => 0;
|
|
public bool RmbOrbitHeld => false;
|
|
public string HourName => "0";
|
|
public float DayFraction => 0;
|
|
public string Weather => "Clear";
|
|
public int ActiveLights => 0;
|
|
public int RegisteredLights => 0;
|
|
public int ParticleCount => 0;
|
|
public float Fps => 60;
|
|
public float FrameMilliseconds => 16.7f;
|
|
}
|
|
|
|
private sealed class KeyboardSource : IKeyboardSource
|
|
{
|
|
#pragma warning disable CS0067
|
|
public event Action<Key, ModifierMask>? KeyDown;
|
|
public event Action<Key, ModifierMask>? KeyUp;
|
|
#pragma warning restore CS0067
|
|
public bool IsHeld(Key key) => false;
|
|
public ModifierMask CurrentModifiers => ModifierMask.None;
|
|
}
|
|
|
|
private sealed class MouseSource : IMouseSource
|
|
{
|
|
#pragma warning disable CS0067
|
|
public event Action<MouseButton, ModifierMask>? MouseDown;
|
|
public event Action<MouseButton, ModifierMask>? MouseUp;
|
|
public event Action<float, float>? MouseMove;
|
|
public event Action<float>? Scroll;
|
|
#pragma warning restore CS0067
|
|
public bool IsHeld(MouseButton button) => false;
|
|
public bool WantCaptureMouse => false;
|
|
public bool WantCaptureKeyboard => false;
|
|
}
|
|
|
|
public class ViewProxy : DispatchProxy
|
|
{
|
|
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
|
{
|
|
if (targetMethod?.Name == "get_Size")
|
|
return new Vector2D<int>(1280, 720);
|
|
Type type = targetMethod?.ReturnType ?? typeof(void);
|
|
if (type == typeof(void))
|
|
return null;
|
|
return type.IsValueType ? Activator.CreateInstance(type) : null;
|
|
}
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|