using System.Collections.Generic; using AcDream.UI.Abstractions.Settings; namespace AcDream.UI.Abstractions.Panels.Settings; /// /// Particle visibility distance policy. Retail uses the GfxObj-authored /// distance exactly; Extended is the acdream default requested for longer /// distant-effect visibility, with the corresponding CPU cost. /// public enum ParticleRange { Retail = 0, Extended = 1, } /// /// Immutable, value-equal user overrides for one selected render pack. Keys /// are stable setting IDs and compare case-insensitively; values remain the /// declaration's invariant string representation until descriptor validation. /// public sealed class RenderPackSettingOverrides : IReadOnlyDictionary, IEquatable { private readonly SortedDictionary _values; public static RenderPackSettingOverrides Empty { get; } = new([]); public RenderPackSettingOverrides( IEnumerable> values) { ArgumentNullException.ThrowIfNull(values); _values = new SortedDictionary(StringComparer.OrdinalIgnoreCase); foreach ((string key, string value) in values) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); _values[key] = value; } } public int Count => _values.Count; public IEnumerable Keys => _values.Keys; public IEnumerable Values => _values.Values; public string this[string key] => _values[key]; public bool ContainsKey(string key) => _values.ContainsKey(key); public bool TryGetValue(string key, out string value) => _values.TryGetValue(key, out value!); public IEnumerator> GetEnumerator() => _values.GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); public RenderPackSettingOverrides Set(string settingId, string value) { ArgumentException.ThrowIfNullOrWhiteSpace(settingId); ArgumentNullException.ThrowIfNull(value); var next = new SortedDictionary( _values, StringComparer.OrdinalIgnoreCase) { [settingId] = value, }; return new RenderPackSettingOverrides(next); } public bool Equals(RenderPackSettingOverrides? other) => other is not null && _values.Count == other._values.Count && _values.All(pair => other._values.TryGetValue(pair.Key, out string? value) && string.Equals(pair.Value, value, StringComparison.Ordinal)); public override bool Equals(object? obj) => obj is RenderPackSettingOverrides other && Equals(other); public override int GetHashCode() { var hash = new HashCode(); foreach ((string key, string value) in _values) { hash.Add(key, StringComparer.OrdinalIgnoreCase); hash.Add(value, StringComparer.Ordinal); } return hash.ToHashCode(); } } /// /// Stable, user-authored selection of one optional render pack. Logical ids /// are persisted instead of menu indexes so discovery order can never select a /// different pack or preset after an install/update. The renderer normalizes a /// missing, malformed, unavailable, or incompatible selection back to /// and retains the precise reason for diagnostics. /// public sealed record RenderPackSelectionSettings( string PackId, string? PackVersion, string PresetId) { public const string RetailPackId = "retail"; public const string RetailPresetId = "off"; public static RenderPackSelectionSettings Retail { get; } = new( RetailPackId, PackVersion: null, RetailPresetId); /// /// User-authored values keyed by the selected pack's stable setting IDs. /// Empty by default so pre-render-pack settings files upgrade without a /// migration write. /// public RenderPackSettingOverrides SettingOverrides { get; init; } = RenderPackSettingOverrides.Empty; public bool IsRetail => string.Equals(PackId, RetailPackId, StringComparison.OrdinalIgnoreCase); } /// /// Display-related preferences persisted to settings.json. /// Originally documented as "no retail equivalent for FOV / vsync etc" — /// Campaign OP slice OP6's Config-tab research corrected that: retail's /// gmClient::InitUIPreferences @0x004035b0 DOES register /// Render_FieldOfView (ID_Graphics_FieldOfView, range /// [10,160]), Display_SyncToRefresh, Display_Resolution, and /// Render_ScreenBrightness ( below, /// range [-1,1]) as genuine UserPreferences.ini rows — they simply /// had no acdream UI surface until OP6's Config tab. Resolution/ /// Fullscreen are LIVE on save (RuntimeSettingsTargets. /// ApplyDisplayWindowState resizes the window immediately); VSync/FOV/ /// Gamma apply at the next launch only (RuntimeSettingsController. /// ApplyStartup), matching this record's pre-existing behaviour — OP6 /// did not change when these three take effect, only how they're reached. /// /// /// Records are immutable; mutation goes through with-expressions — /// e.g. RuntimeSettingsController.SaveDisplay's callers, or the /// OP9-retired SettingsVM.SetDisplay's draft assignment before that. /// /// public sealed record DisplaySettings( string Resolution, bool Fullscreen, bool VSync, float FieldOfView, float Gamma, bool ShowFps, QualityPreset Quality, ParticleRange ParticleRange, // Campaign OP slice OP6: the Config tab's "Graphics Options" + // "Rendering Quality Options" rows with no acdream renderer consumer — // the world renderer is Vulkan + one aggregate QualityPreset, not // per-feature knobs (register row, OP6). Persisted faithfully; every // default below is retail's own byte-verified // gmClient::InitUIPreferences / gmConfigUI::InitOptions literal. // // OP6 rework (2026-08-11, review S2): Screen Brightness gets its OWN // field — the rejected slice reused Gamma (a pre-existing multiplier, // default 1.0, legacy Settings-panel range [0.5, 2.0]), a genuinely // different unit system from retail's own Render_ScreenBrightness // range [-1, 1] / default 0 (AttachPreference @0x004043df, // SetPreferenceRange @0x004043f3). Overloading Gamma pinned the Config // row at the WRONG default (a fresh Gamma=1.0 normalizes to the // slider's maximum, not center) and made Defaults write a value (0) // outside the legacy slider's own range. Gamma itself is untouched — // still the pre-existing multiplier the legacy Settings panel drives. float ScreenBrightness = 0f, bool AutomaticDegrades = false, float GraphicsPerformance = 0f, float DegradeDistance = 50f, int LandscapeTextureDetail = 2, int EnvironmentTextureDetail = 1, int TextureFiltering = 1, // UNRESOLVED (OP6, cite in register row): retail's own // SetDefaultValue(8) does not index its 6-entry SetEnumChoices array // (VeryLow..Extreme) — reproduced faithfully as an opaque int, not // guessed into a clamped index. int LandscapeDrawDistance = 8, bool BuildingDetailTextures = true, bool MultiPassAlpha = false) { /// /// Opt-in graphics enhancement selection. This is deliberately separate /// from , which remains the authoritative retail /// renderer/streaming quality preset. Keeping the default here means an /// upgraded settings file that predates shader packs deserializes to the /// exact retail path without a migration write. /// public RenderPackSelectionSettings RenderPack { get; init; } = RenderPackSelectionSettings.Retail; /// Values used on first launch / when settings.json is absent. /// Geometry defaults preserve the pre-L.0 runtime state: Resolution /// matches the WindowOptions startup size (1280×720). FieldOfView is /// retail's m_fGameFOV in degrees — registered default 90, range /// [10,160] (gmClient::InitUIPreferences @0x004035b0; #389 /// corrected the pre-port 60, which encoded the old direct-vertical-FOV /// semantics this record no longer means). VSync defaults on so normal /// rendering is synchronized to the active monitor, while /// ShowFps matches retail's initially-hidden SmartBox FPS readout. public static DisplaySettings Default { get; } = new( Resolution: "1280x720", Fullscreen: false, VSync: true, FieldOfView: 90f, Gamma: 1.0f, ShowFps: false, Quality: QualityPreset.High, ParticleRange: ParticleRange.Extended); /// /// FALLBACK resolution presets — used only when the monitor's real mode /// list is unavailable (fixture/conformance callers, headless mounts). In /// production the Config dropdown is populated from the display's actual /// modes, curated to modern formats (#391, user-directed 2026-08-13: /// "we should only support modern resolutions. Not any old format") — /// see AcDream.App.Rendering.DisplayModeCatalog, whose curation /// filter this fallback list also passes through. Retail's own list was /// the adapter's full mode enumeration including 4:3 legacy modes, with /// 800x600 as the authored Config-tab default /// (gmConfigUI::InitOptions SetDefaultValue(0x03200258), /// gmClient::Init @0x004047af) — the curation and the desktop-mode /// default that replaces it are a deliberate deviation carried in the /// divergence register (see the #391 row). /// public static IReadOnlyList AvailableResolutions { get; } = new[] { "1280x720", "1366x768", "1600x900", "1920x1080", "2560x1440", "3840x2160", }; }