244 lines
10 KiB
C#
244 lines
10 KiB
C#
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",
|
||
};
|
||
}
|