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",
};
}