acdream/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs

244 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using AcDream.UI.Abstractions.Settings;
namespace AcDream.UI.Abstractions.Panels.Settings;
/// <summary>
/// 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.
/// </summary>
public enum ParticleRange
{
Retail = 0,
Extended = 1,
}
/// <summary>
/// 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.
/// </summary>
public sealed class RenderPackSettingOverrides :
IReadOnlyDictionary<string, string>,
IEquatable<RenderPackSettingOverrides>
{
private readonly SortedDictionary<string, string> _values;
public static RenderPackSettingOverrides Empty { get; } = new([]);
public RenderPackSettingOverrides(
IEnumerable<KeyValuePair<string, string>> values)
{
ArgumentNullException.ThrowIfNull(values);
_values = new SortedDictionary<string, string>(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<string> Keys => _values.Keys;
public IEnumerable<string> 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<KeyValuePair<string, string>> 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<string, string>(
_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();
}
}
/// <summary>
/// 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
/// <see cref="Retail"/> and retains the precise reason for diagnostics.
/// </summary>
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);
/// <summary>
/// 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.
/// </summary>
public RenderPackSettingOverrides SettingOverrides { get; init; } =
RenderPackSettingOverrides.Empty;
public bool IsRetail =>
string.Equals(PackId, RetailPackId, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Display-related preferences persisted to <c>settings.json</c>.
/// Originally documented as "no retail equivalent for FOV / vsync etc" —
/// Campaign OP slice OP6's Config-tab research corrected that: retail's
/// <c>gmClient::InitUIPreferences @0x004035b0</c> DOES register
/// <c>Render_FieldOfView</c> (<c>ID_Graphics_FieldOfView</c>, range
/// [10,160]), <c>Display_SyncToRefresh</c>, <c>Display_Resolution</c>, and
/// <c>Render_ScreenBrightness</c> (<see cref="ScreenBrightness"/> below,
/// range [-1,1]) as genuine <c>UserPreferences.ini</c> rows — they simply
/// had no acdream UI surface until OP6's Config tab. Resolution/
/// Fullscreen are LIVE on save (<c>RuntimeSettingsTargets.
/// ApplyDisplayWindowState</c> resizes the window immediately); VSync/FOV/
/// Gamma apply at the next launch only (<c>RuntimeSettingsController.
/// ApplyStartup</c>), matching this record's pre-existing behaviour — OP6
/// did not change when these three take effect, only how they're reached.
///
/// <para>
/// Records are immutable; mutation goes through <c>with</c>-expressions —
/// e.g. <c>RuntimeSettingsController.SaveDisplay</c>'s callers, or the
/// OP9-retired <c>SettingsVM.SetDisplay</c>'s draft assignment before that.
/// </para>
/// </summary>
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)
{
/// <summary>
/// Opt-in graphics enhancement selection. This is deliberately separate
/// from <see cref="Quality"/>, 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.
/// </summary>
public RenderPackSelectionSettings RenderPack { get; init; } =
RenderPackSelectionSettings.Retail;
/// <summary>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 <c>m_fGameFOV</c> in degrees — registered default 90, range
/// [10,160] (<c>gmClient::InitUIPreferences @0x004035b0</c>; #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.</summary>
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);
/// <summary>
/// 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 <c>AcDream.App.Rendering.DisplayModeCatalog</c>, 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
/// <c>800x600</c> as the authored Config-tab default
/// (<c>gmConfigUI::InitOptions SetDefaultValue(0x03200258)</c>,
/// <c>gmClient::Init @0x004047af</c>) — the curation and the desktop-mode
/// default that replaces it are a deliberate deviation carried in the
/// divergence register (see the #391 row).
/// </summary>
public static IReadOnlyList<string> AvailableResolutions { get; } = new[]
{
"1280x720",
"1366x768",
"1600x900",
"1920x1080",
"2560x1440",
"3840x2160",
};
}