feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -14,6 +14,117 @@ public enum ParticleRange
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" —
@ -77,6 +188,16 @@ public sealed record DisplaySettings(
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

View file

@ -97,7 +97,10 @@ public sealed class SettingsStore
TextureFiltering: ReadInt (disp, "textureFiltering", d.TextureFiltering),
LandscapeDrawDistance: ReadInt (disp, "landscapeDrawDistance", d.LandscapeDrawDistance),
BuildingDetailTextures: ReadBool (disp, "buildingDetailTextures", d.BuildingDetailTextures),
MultiPassAlpha: ReadBool (disp, "multiPassAlpha", d.MultiPassAlpha));
MultiPassAlpha: ReadBool (disp, "multiPassAlpha", d.MultiPassAlpha))
{
RenderPack = ReadRenderPackSelection(disp, d.RenderPack),
};
}
catch (Exception ex)
{
@ -686,6 +689,7 @@ public sealed class SettingsStore
["multiPassAlpha"] = d.MultiPassAlpha,
["particleRange"] = d.ParticleRange.ToString(),
["quality"] = d.Quality.ToString(),
["renderPack"] = BuildRenderPackObject(d.RenderPack),
["resolution"] = d.Resolution,
["screenBrightness"] = d.ScreenBrightness,
["showFps"] = d.ShowFps,
@ -693,6 +697,21 @@ public sealed class SettingsStore
["vsync"] = d.VSync,
};
private static SortedDictionary<string, object?> BuildRenderPackObject(
RenderPackSelectionSettings selection)
{
var overrides = new SortedDictionary<string, object>(StringComparer.Ordinal);
foreach ((string key, string value) in selection.SettingOverrides)
overrides[key] = value;
return new SortedDictionary<string, object?>(StringComparer.Ordinal)
{
["packId"] = selection.PackId,
["packVersion"] = selection.PackVersion,
["presetId"] = selection.PresetId,
["settingOverrides"] = overrides,
};
}
private static SortedDictionary<string, object> BuildMiscObject(MiscSettings m)
=> new(StringComparer.Ordinal)
{
@ -777,6 +796,49 @@ public sealed class SettingsStore
File.WriteAllText(_path, sb.ToString());
}
private static RenderPackSelectionSettings ReadRenderPackSelection(
JsonElement display,
RenderPackSelectionSettings fallback)
{
if (!display.TryGetProperty("renderPack", out JsonElement value)
|| value.ValueKind != JsonValueKind.Object)
{
return fallback;
}
string packId = ReadString(value, "packId", string.Empty);
string presetId = ReadString(value, "presetId", string.Empty);
if (string.IsNullOrWhiteSpace(packId) || string.IsNullOrWhiteSpace(presetId))
return fallback;
string? version = null;
if (value.TryGetProperty("packVersion", out JsonElement versionElement)
&& versionElement.ValueKind == JsonValueKind.String)
{
version = versionElement.GetString();
}
var overrides = new List<KeyValuePair<string, string>>();
if (value.TryGetProperty("settingOverrides", out JsonElement overrideElement))
{
if (overrideElement.ValueKind != JsonValueKind.Object)
return fallback;
foreach (JsonProperty property in overrideElement.EnumerateObject())
{
if (property.Value.ValueKind != JsonValueKind.String
|| property.Value.GetString() is not { } settingValue)
{
return fallback;
}
overrides.Add(new KeyValuePair<string, string>(property.Name, settingValue));
}
}
return new RenderPackSelectionSettings(packId, version, presetId)
{
SettingOverrides = new RenderPackSettingOverrides(overrides),
};
}
private static string ReadString(JsonElement obj, string name, string fallback)
=> obj.TryGetProperty(name, out var el) && el.ValueKind == JsonValueKind.String
? (el.GetString() ?? fallback) : fallback;