using AcDream.App.Audio; using AcDream.App.Net; using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; using AcDream.App.Streaming; using AcDream.App.UI; using AcDream.UI.Abstractions; 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); } /// /// Campaign CH slice CH6c target seam for the Chat tab's transparency sliders, /// mirroring 's shape. /// internal interface IRuntimeChatOpacityTarget { void Apply(float defaultOpacity, float activeOpacity); } 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(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.SfxVolume = audio.Sfx; engine.AmbientVolume = audio.Ambient; } } internal sealed class RuntimeQualityApplicationTarget : IRuntimeQualityApplicationTarget { // Campaign V slice V6h: absent on a backend that composes no world // renderers. Alpha-to-coverage and anisotropy are properties of renderers // that do not exist there; render range and streaming radii still apply. 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; _terrainAtlas = terrainAtlas; _streaming = streaming ?? throw new ArgumentNullException(nameof(streaming)); _renderRange = renderRange ?? throw new ArgumentNullException(nameof(renderRange)); } public void SetAlphaToCoverage(bool enabled) { if (_dispatcher is not null) _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) { } } internal sealed class RuntimeChatOpacityTarget(RetailWindowOpacityController controller) : IRuntimeChatOpacityTarget { private readonly RetailWindowOpacityController _controller = controller ?? throw new ArgumentNullException(nameof(controller)); public void Apply(float defaultOpacity, float activeOpacity) => _controller.SetOpacity(defaultOpacity, activeOpacity); } internal sealed class NullRuntimeChatOpacityTarget : IRuntimeChatOpacityTarget { public static NullRuntimeChatOpacityTarget Instance { get; } = new(); private NullRuntimeChatOpacityTarget() { } public void Apply(float defaultOpacity, float activeOpacity) { } } /// /// Complete late-bound target for changes made after startup. Construction and /// binding are inert; only an explicit controller command mutates borrowers. /// internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets { private readonly IRuntimeDisplayWindowTarget _displayWindow; private readonly IRuntimeQualityApplicationTarget _quality; private readonly IRuntimeUiLockTarget _uiLock; private readonly IRuntimeChatOpacityTarget _chatOpacity; private readonly ICommandBus _commands; private readonly Action _log; public RuntimeSettingsTargets( IRuntimeDisplayWindowTarget displayWindow, WbDrawDispatcher? dispatcher, TerrainAtlas? terrainAtlas, StreamingController streaming, WorldRenderRangeState renderRange, UiRoot? uiRoot, ICommandBus commands, RetailWindowOpacityController? chatOpacity = null, Action? log = null) : this( displayWindow, new RuntimeQualityApplicationTarget( dispatcher, terrainAtlas, streaming, renderRange), uiRoot is null ? NullRuntimeUiLockTarget.Instance : new RuntimeUiLockTarget(uiRoot), commands, log, chatOpacity is null ? NullRuntimeChatOpacityTarget.Instance : new RuntimeChatOpacityTarget(chatOpacity)) { } internal RuntimeSettingsTargets( IRuntimeDisplayWindowTarget displayWindow, IRuntimeQualityApplicationTarget quality, IRuntimeUiLockTarget uiLock, ICommandBus commands, Action? log = null, IRuntimeChatOpacityTarget? chatOpacity = null) { _displayWindow = displayWindow ?? throw new ArgumentNullException(nameof(displayWindow)); _quality = quality ?? throw new ArgumentNullException(nameof(quality)); _uiLock = uiLock ?? throw new ArgumentNullException(nameof(uiLock)); _chatOpacity = chatOpacity ?? NullRuntimeChatOpacityTarget.Instance; _commands = commands ?? throw new ArgumentNullException(nameof(commands)); _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); /// /// CH3 (2026-08-09): publishes through the SAME /// generation-gated route every /// other outbound Settings/chat command uses — a no-op when no route is /// currently attached (disconnected / reconnecting), exactly like every /// other ICommandBus.Publish call site. N6 (CH3 Opus review, /// 2026-08-09): this silent drop is safe because /// RuntimeSettingsController.SaveChat already wrote the toggle to /// settings.json BEFORE calling here — the local preference is never /// lost — and the next successful connect's PlayerDescription re-runs /// SyncChatFromServerOptions, reconciling the draft/persisted /// snapshot back to whatever the server actually has (which may or may /// not match the dropped toggle, since the wire send never landed). /// public void SetSingleCharacterOption(uint optionId, bool value) => _commands.Publish(new SetSingleCharacterOptionRuntimeCmd(optionId, value)); public void SetChatOpacity(float defaultOpacity, float activeOpacity) => _chatOpacity.Apply(defaultOpacity, activeOpacity); }