Move pre-window loading, startup application, live settings mutation, toon context, quality reapply, and SettingsVM loans behind one RuntimeSettingsController. Preserve retail command behavior, ordered target publication, draft semantics, and retryable failure convergence while removing duplicate GameWindow state and feature bodies. Co-authored-by: Codex <codex@openai.com>
255 lines
8.3 KiB
C#
255 lines
8.3 KiB
C#
using AcDream.App.Audio;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.UI;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
using AcDream.UI.Abstractions.Settings;
|
|
using Silk.NET.Maths;
|
|
using Silk.NET.Windowing;
|
|
|
|
namespace AcDream.App.Settings;
|
|
|
|
internal interface IRuntimeDisplayWindowTarget
|
|
{
|
|
void Apply(DisplaySettings display);
|
|
}
|
|
|
|
internal interface IRuntimeQualityApplicationTarget
|
|
{
|
|
void SetAlphaToCoverage(bool enabled);
|
|
|
|
void SetAnisotropic(int level);
|
|
|
|
void PublishRenderRange(int nearRadius, int farRadius);
|
|
|
|
void ReconfigureStreamingRadii(int nearRadius, int farRadius);
|
|
|
|
void SetCompletionBudget(int maxCompletionsPerFrame);
|
|
}
|
|
|
|
internal interface IRuntimeUiLockTarget
|
|
{
|
|
void Apply(bool locked);
|
|
}
|
|
|
|
internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarget
|
|
{
|
|
private readonly IWindow _window;
|
|
|
|
public SilkRuntimeDisplayWindowTarget(IWindow window)
|
|
{
|
|
_window = window ?? throw new ArgumentNullException(nameof(window));
|
|
}
|
|
|
|
public void Apply(DisplaySettings display)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(display);
|
|
if (TryParseResolution(display.Resolution, out int width, out int height)
|
|
&& (_window.Size.X != width || _window.Size.Y != height))
|
|
{
|
|
_window.Size = new Vector2D<int>(width, height);
|
|
}
|
|
|
|
WindowState desired = display.Fullscreen
|
|
? WindowState.Fullscreen
|
|
: WindowState.Normal;
|
|
if (_window.WindowState != desired)
|
|
_window.WindowState = desired;
|
|
}
|
|
|
|
internal static bool TryParseResolution(
|
|
string spec,
|
|
out int width,
|
|
out int height)
|
|
{
|
|
width = height = 0;
|
|
if (string.IsNullOrWhiteSpace(spec))
|
|
return false;
|
|
string[] parts = spec.Split('x', 2);
|
|
return parts.Length == 2
|
|
&& int.TryParse(parts[0], out width)
|
|
&& int.TryParse(parts[1], out height)
|
|
&& width > 0
|
|
&& height > 0;
|
|
}
|
|
}
|
|
|
|
internal sealed class RuntimeSettingsStartupTargets : IRuntimeSettingsStartupTarget
|
|
{
|
|
private readonly IRuntimeDisplayWindowTarget _displayWindow;
|
|
private readonly DisplayFramePacingController _pacing;
|
|
private readonly CameraController _cameras;
|
|
private readonly OpenAlAudioEngine? _audio;
|
|
|
|
public RuntimeSettingsStartupTargets(
|
|
IRuntimeDisplayWindowTarget displayWindow,
|
|
DisplayFramePacingController pacing,
|
|
CameraController cameras,
|
|
OpenAlAudioEngine? audio)
|
|
{
|
|
_displayWindow = displayWindow
|
|
?? throw new ArgumentNullException(nameof(displayWindow));
|
|
_pacing = pacing ?? throw new ArgumentNullException(nameof(pacing));
|
|
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
|
|
_audio = audio;
|
|
}
|
|
|
|
public void ApplyDisplay(DisplaySettings display)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(display);
|
|
_pacing.RefreshActiveMonitor();
|
|
_pacing.ApplyPreference(display.VSync);
|
|
_displayWindow.Apply(display);
|
|
ApplyFieldOfView(_cameras, display.FieldOfView);
|
|
}
|
|
|
|
public void ApplyAudio(AudioSettings audio) => ApplyAudio(_audio, audio);
|
|
|
|
internal static void ApplyFieldOfView(
|
|
CameraController cameras,
|
|
float degrees)
|
|
{
|
|
float radians = degrees * (MathF.PI / 180f);
|
|
cameras.Orbit.FovY = radians;
|
|
cameras.Fly.FovY = radians;
|
|
if (cameras.Chase is not null)
|
|
cameras.Chase.FovY = radians;
|
|
}
|
|
|
|
internal static void ApplyAudio(
|
|
OpenAlAudioEngine? engine,
|
|
AudioSettings audio)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(audio);
|
|
if (engine is not { IsAvailable: true })
|
|
return;
|
|
engine.MasterVolume = audio.Master;
|
|
engine.MusicVolume = audio.Music;
|
|
engine.SfxVolume = audio.Sfx;
|
|
engine.AmbientVolume = audio.Ambient;
|
|
}
|
|
}
|
|
|
|
internal sealed class RuntimeQualityApplicationTarget
|
|
: IRuntimeQualityApplicationTarget
|
|
{
|
|
private readonly WbDrawDispatcher _dispatcher;
|
|
private readonly TerrainAtlas _terrainAtlas;
|
|
private readonly StreamingController _streaming;
|
|
private readonly WorldRenderRangeState _renderRange;
|
|
|
|
public RuntimeQualityApplicationTarget(
|
|
WbDrawDispatcher dispatcher,
|
|
TerrainAtlas terrainAtlas,
|
|
StreamingController streaming,
|
|
WorldRenderRangeState renderRange)
|
|
{
|
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
|
_terrainAtlas = terrainAtlas ?? throw new ArgumentNullException(nameof(terrainAtlas));
|
|
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
|
_renderRange = renderRange ?? throw new ArgumentNullException(nameof(renderRange));
|
|
}
|
|
|
|
public void SetAlphaToCoverage(bool enabled) =>
|
|
_dispatcher.AlphaToCoverage = enabled;
|
|
|
|
public void SetAnisotropic(int level) => _terrainAtlas.SetAnisotropic(level);
|
|
|
|
public void PublishRenderRange(int nearRadius, int farRadius)
|
|
{
|
|
_renderRange.NearRadius = nearRadius;
|
|
_renderRange.FarRadius = farRadius;
|
|
}
|
|
|
|
public void ReconfigureStreamingRadii(int nearRadius, int farRadius) =>
|
|
_streaming.ReconfigureRadii(nearRadius, farRadius);
|
|
|
|
public void SetCompletionBudget(int maxCompletionsPerFrame) =>
|
|
_streaming.MaxCompletionsPerFrame = maxCompletionsPerFrame;
|
|
}
|
|
|
|
internal sealed class RuntimeUiLockTarget(UiRoot root) : IRuntimeUiLockTarget
|
|
{
|
|
private readonly UiRoot _root = root ?? throw new ArgumentNullException(nameof(root));
|
|
|
|
public void Apply(bool locked) => _root.UiLocked = locked;
|
|
}
|
|
|
|
internal sealed class NullRuntimeUiLockTarget : IRuntimeUiLockTarget
|
|
{
|
|
public static NullRuntimeUiLockTarget Instance { get; } = new();
|
|
|
|
private NullRuntimeUiLockTarget()
|
|
{
|
|
}
|
|
|
|
public void Apply(bool locked)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Complete late-bound target for changes made after startup. Construction and
|
|
/// binding are inert; only an explicit controller command mutates borrowers.
|
|
/// </summary>
|
|
internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|
{
|
|
private readonly IRuntimeDisplayWindowTarget _displayWindow;
|
|
private readonly IRuntimeQualityApplicationTarget _quality;
|
|
private readonly IRuntimeUiLockTarget _uiLock;
|
|
private readonly Action<string> _log;
|
|
|
|
public RuntimeSettingsTargets(
|
|
IRuntimeDisplayWindowTarget displayWindow,
|
|
WbDrawDispatcher dispatcher,
|
|
TerrainAtlas terrainAtlas,
|
|
StreamingController streaming,
|
|
WorldRenderRangeState renderRange,
|
|
UiRoot? uiRoot,
|
|
Action<string>? log = null)
|
|
: this(
|
|
displayWindow,
|
|
new RuntimeQualityApplicationTarget(
|
|
dispatcher,
|
|
terrainAtlas,
|
|
streaming,
|
|
renderRange),
|
|
uiRoot is null
|
|
? NullRuntimeUiLockTarget.Instance
|
|
: new RuntimeUiLockTarget(uiRoot),
|
|
log)
|
|
{
|
|
}
|
|
|
|
internal RuntimeSettingsTargets(
|
|
IRuntimeDisplayWindowTarget displayWindow,
|
|
IRuntimeQualityApplicationTarget quality,
|
|
IRuntimeUiLockTarget uiLock,
|
|
Action<string>? log = null)
|
|
{
|
|
_displayWindow = displayWindow
|
|
?? throw new ArgumentNullException(nameof(displayWindow));
|
|
_quality = quality ?? throw new ArgumentNullException(nameof(quality));
|
|
_uiLock = uiLock ?? throw new ArgumentNullException(nameof(uiLock));
|
|
_log = log ?? Console.WriteLine;
|
|
}
|
|
|
|
public void ApplyDisplayWindowState(DisplaySettings display) =>
|
|
_displayWindow.Apply(display);
|
|
|
|
public void ApplyQuality(QualitySettings quality)
|
|
{
|
|
_quality.SetAlphaToCoverage(quality.AlphaToCoverage);
|
|
_quality.SetAnisotropic(quality.AnisotropicLevel);
|
|
_quality.PublishRenderRange(quality.NearRadius, quality.FarRadius);
|
|
_quality.ReconfigureStreamingRadii(quality.NearRadius, quality.FarRadius);
|
|
_quality.SetCompletionBudget(quality.MaxCompletionsPerFrame);
|
|
_log(
|
|
$"[QUALITY] Streaming reconciled: nearRadius={quality.NearRadius}, " +
|
|
$"farRadius={quality.FarRadius}, " +
|
|
$"maxCompletions={quality.MaxCompletionsPerFrame}");
|
|
}
|
|
|
|
public void ApplyUiLock(bool locked) => _uiLock.Apply(locked);
|
|
}
|