Second attempt at V4a afterceec3bc4was reverted at9aaf97e7for losing world multisampling and a 334-file scope explosion. This lands the same functional slice with a much smaller footprint and the two structural fixes the revert postmortem (docs/plans/2026-07-27-vulkan-campaign.md SS7.1) called for. What moved onto the RHI: - TextRenderer: the ui_text shader now compiles through IGpuDevice.CreatePipeline (one IGpuPipeline, replacing the old hand-rolled Shader class); its three fence-buffered per-flight VBOs are gone in favour of a per-IGpuFrame ring allocation per draw bucket; its 1x1 white fill texture is created via IGpuDevice.CreateTexture and registered into the device's texture table. Flush keeps TextRenderGlStateScope and the manual GL disable block verbatim (TextRendererFailureSafetyTests pins their literal presence) alongside the new pipeline bind - both target the identical final GL state, so this is redundant, not contradictory. Sprite/font texture binding stays classic (glActiveTexture/glBindTexture) because DrawSprite receives arbitrary externally-owned GL texture names from dozens of UI call sites outside this slice's scope; IGpuPassEncoder has no verb for that, by design (every other RHI consumer samples through the bindless texture table). - BitmapFont: the stb-baked R8 atlas is created/uploaded through IGpuDevice.CreateTexture; TextureId stays a raw GL name extracted from the IGpuTexture, since its only consumer is TextRenderer's classic path above. - DebugLineRenderer: the debug_line shader compiles through IGpuDevice.CreatePipeline (LineList topology, depth disabled); Flush ring- allocates its vertex data and draws through IGpuPassEncoder. uView/uProjection don't fit the shared GpuPushConstants block (one combined VP matrix) so they are set directly on the pipeline's compiled program, mirroring TextRenderer. - TextureCache: GetOrUploadRenderSurface and the public UploadRgba8(byte[],...) wrapper now create IGpuTexture+GpuTextureSlot internally, extracting the raw GL name for their unchanged uint return type - DrawSprite's signature and its 16 call sites across the UI are untouched. The world-material path (GetOrUpload, the raw layer-array upload) is untouched. - UiViewport: TextureHandle (uint) -> TextureSlot (GpuTextureSlot), resolved back to a raw GL name via TextRenderer.ResolveExternalTextureSlot at draw time. Its texture is produced by PaperdollViewportRenderer/ PrivateEntityViewportRenderer, both still raw GL until V4g, so RetailPaperdollFrameView/RetailCreatureAppraisalFrameView register it through the pre-approved GlGpuDevice.RegisterExternalColorTexture transitional seam (campaign doc SS7.1's final paragraph) instead of inventing anything broader. The two revert-postmortem fixes, both in Gpu/Gl (never in the pinned Gpu/ contract): - GlGpuDevice.BeginPass now resets the render-state cache unconditionally on every pass, not only a clearing one. The first attempt's crash came from exactly this gap: a raw-GL renderer running between two RHI passes changes GL program/blend/depth/cull state the cache never observes, so a later BindPipeline skipped re-issuing glUseProgram and the following push-constant upload threw GL_INVALID_OPERATION. - GlGpuPassEncoder now captures ambient GL capability state (program, VAO, array buffer, texture0 binding, depth test/write/func, blend enable+func, cull enable+mode, front face, alpha-to-coverage, multisample) on construction and restores it on Dispose, generalizing what TextRenderGlStateScope already did for TextRenderer specifically to every RHI pass - this is what stops DebugLineRenderer's pipeline bind (which has no scope of its own) from leaking state into the next raw-GL renderer. Both are marked transitional, deleted at V4h once nothing raw-GL remains. Frame lifecycle (additive, per the task's own description of this piece): new GpuDeviceFrameLifetime wraps IGpuDevice.BeginFrame()/IGpuFrame.End() and exposes the open frame via ICurrentGpuFrameSource. RenderFrameOrchestrator's IRenderFrameLifetime now routes through this wrapper instead of calling GpuFrameFlightController directly - GlGpuDevice.BeginFrame already calls straight through to that same controller, so the fence/slot-rotation contract is unchanged; the wrapper only additionally yields the IGpuFrame ported renderers need. No clears moved, no framebuffer binding changed, frame-graph phase order is untouched. The two now-dead per-slot TextRenderer.BeginFrame(int) calls in RuntimeRenderFrameBeginResources are removed. The UI Studio (RenderBootstrap/StudioWindow) gets its own independent RHI device+lifetime, mirroring the production composition. Real bug found and fixed while exercising this for the first time: both BitmapFont and TextureCache's nearest-filter override called TexParameter AFTER RegisterTexture, which made the bindless handle resident - GL_ARB_ bindless_texture forbids modifying a texture's parameters once its handle is resident, so this threw GL_INVALID_OPERATION building the retained UI's own TextRenderer. Fixed by moving both TexParameter blocks before RegisterTexture. Scope note: touches 25 files (24 modified + this commit's one new file), not the ~10 the brief estimated, because the frame-lifecycle wiring and the viewport escape hatch (both explicitly asked for) ripple through five composition files and two frame presenters that thread IGpuDevice/ ICurrentGpuFrameSource to construction sites. No file outside that necessary set was touched: no visibility sweep beyond the specific constructors/ properties whose new parameter types are internal (TextRenderer/BitmapFont/ DebugLineRenderer/UiHost's constructors, TextureCache's otherwise-orphaned convenience overload, UiViewport.TextureSlot), no world-mesh/terrain/particle/ sky file touched, no test deleted or weakened - three source-text conformance tests (TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork, GlTextureOwnershipTests' TextRenderer.cs check, and RenderFrameResourceControllerTests' frame-order check) were replaced with equivalent assertions against the new construction/wiring shape, since their pinned invariant was specifically the old raw-GL shape this slice legitimately replaces. Gates: - dotnet build -c Release: 0 warnings, 0 errors. - dotnet test tests/AcDream.App.Tests -c Release: 3,843 passed / 3 skipped - exactly the baseline. Complete solution: 8,906 passed / 5 skipped across all nine test projects. - Offline pixel gate (tools/run-offline-pixel-gate.ps1, parenta97e04aevs this commit): 26 differing pixels of 563,200 compared (fraction 4.62e-05), pass against the 0.001/563-pixel threshold. Verified against a same-commit control (two captures at this commit differ by 20 pixels) rather than accepted at face value - the two numbers are in the same band, confirming this is normal animated-content/frame-pacing noise and not the systematic silhouette-edge loss (1,791 pixels, 224x higher) the first attempt's revert diagnosed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
524 lines
19 KiB
C#
524 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,
|
|
_dispatcher,
|
|
camera,
|
|
null);
|
|
Platform = new GameWindowPlatformResult<GL, IInputContext>(null!, 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<GL, 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.");
|
|
}
|
|
}
|