acdream/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs
Erik d13d63d0a5 fix #389 review round: settings v3 FOV migration + live apply; AD-90
Dual-lens Opus review of 7e0c1303 (reports committed under
docs/research/). The law, gate, and vertical application are CONFIRMED
at instruction-byte level against the PDB-paired acclient.exe (the BN
text FPU-elides this whole area); the fix round addresses the findings:

- Blast MUST-FIX 1: real schema migration instead of a hand-edited dev
  file. SettingsStore v2->v3: a pre-v3 display.fieldOfView was the
  applied vertical FOV in degrees; v3 means retail's m_fGameFOV.
  LoadDisplay migrates on read - the untouched old default 60 maps to
  the retail default 90; a deliberate other value preserves its visible
  16:9 framing (x (16/9 - 0.1)), clamped to the registered [10,160];
  the next save stamps v3 and migration never reruns. The dev
  settings.json hand-edit was reverted so the migration owns it.
- Blast MUST-FIX 2 / mechanism M2: the Field of View now applies LIVE on
  Save (retail: Render::GRPCallback_OnRenderPreferenceChanged @0x0054d999
  -> SmartBox::SetDefaultFov). RuntimeSettingsTargets gains the camera
  graph and applies through ApplyDisplayWindowState - the update-phase
  seam, deliberately NOT the render-phase preview path (the review's
  WATCH-3 cull-vs-raster landmine).
- Mechanism M1 -> register row AD-90: retail's divisor aspect runs
  through the Render.AspectRatio preference (ComputeAspectForViewport
  @0x0054f150, (w/h) x pref x 0.75) - exactly raw w/h at the registered
  default, which is what acdream assumes; retail's NaN-through-the-gate
  quirk (M3) is folded into the same row as deliberately not reproduced.
- Docs: RetailFieldOfView now cites the decisive vertical proof
  (D3DXMatrixPerspectiveFovLH fovy slot @0x0059ab71), the unconditional
  SmartBox::RenderNormalMode site, and M4's exact horizontal numbers
  (89.0/83.9/80.6 deg); the Config FOV row comment updated to LIVE.
- Blast WATCH 4 disposition: the 15 replay-harness PI/3 constants stay -
  they are CAPTURE-TIME camera parameters for recorded fixtures, not
  production framing; changing them would invalidate the replays.

Tests: +6 SettingsStore migration facts, +1 live-apply fact.
App suite 4,962/3 skips; UI.Abstractions 922.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:27:15 +02:00

634 lines
23 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.IO;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.UI.Abstractions.Tests.Panels.Settings;
/// <summary>
/// L.0: <see cref="SettingsStore"/> reads / writes <c>settings.json</c>.
/// Tests use a temp-file path so they don't touch the user's
/// %LOCALAPPDATA% file.
/// </summary>
public sealed class SettingsStoreTests : System.IDisposable
{
private readonly string _tempPath;
public SettingsStoreTests()
{
// Unique per-test file under the system temp dir so parallel test
// runners don't clobber each other.
_tempPath = Path.Combine(
Path.GetTempPath(),
$"acdream-settings-test-{System.Guid.NewGuid():N}.json");
}
public void Dispose()
{
if (File.Exists(_tempPath)) File.Delete(_tempPath);
}
[Fact]
public void LoadDisplay_returns_defaults_when_file_is_missing()
{
var store = new SettingsStore(_tempPath);
var loaded = store.LoadDisplay();
Assert.Equal(DisplaySettings.Default, loaded);
}
[Fact]
public void SaveDisplay_then_LoadDisplay_round_trips_all_fields()
{
var store = new SettingsStore(_tempPath);
var original = new DisplaySettings(
Resolution: "2560x1440",
Fullscreen: true,
VSync: false,
FieldOfView: 100f,
Gamma: 1.4f,
ShowFps: true,
Quality: AcDream.UI.Abstractions.Settings.QualityPreset.Ultra,
ParticleRange: ParticleRange.Extended);
store.SaveDisplay(original);
var loaded = store.LoadDisplay();
Assert.Equal(original, loaded);
}
[Fact]
public void LoadDisplay_falls_back_to_defaults_when_file_is_corrupt()
{
File.WriteAllText(_tempPath, "{ this is not valid json");
var store = new SettingsStore(_tempPath);
var loaded = store.LoadDisplay();
Assert.Equal(DisplaySettings.Default, loaded);
}
[Fact]
public void LoadDisplay_falls_back_per_field_when_keys_missing()
{
// Partial file — only resolution set; everything else should
// pick up DisplaySettings.Default values.
File.WriteAllText(_tempPath, """
{
"version": 1,
"display": { "resolution": "1366x768" }
}
""");
var store = new SettingsStore(_tempPath);
var loaded = store.LoadDisplay();
Assert.Equal("1366x768", loaded.Resolution);
Assert.Equal(DisplaySettings.Default.Fullscreen, loaded.Fullscreen);
Assert.Equal(DisplaySettings.Default.VSync, loaded.VSync);
Assert.Equal(DisplaySettings.Default.FieldOfView, loaded.FieldOfView);
Assert.Equal(ParticleRange.Extended, loaded.ParticleRange);
}
// ── #389 schema-v3 fieldOfView migration ────────────────────────────
// Pre-v3 the stored number was the applied vertical FOV in degrees;
// v3 makes it retail's m_fGameFOV. LoadDisplay migrates pre-v3 files
// on read (blast-review MUST-FIX 1).
[Theory]
// The untouched old default (acdream's invented 60) → the retail
// registered default 90.
[InlineData(60f, 90f)]
// A deliberate old choice preserves its visible 16:9 framing:
// gameFOV = vFOV × (16/9 0.1). 45 × 1.67778 = 75.5.
[InlineData(45f, 75.5f)]
// Clamped into the slider's registered [10,160]: 120 × 1.67778 = 201.3.
[InlineData(120f, 160f)]
public void LoadDisplay_migrates_pre_v3_fieldOfView(float stored, float expected)
{
File.WriteAllText(_tempPath, $$"""
{
"version": 2,
"display": { "fieldOfView": {{stored}} }
}
""");
var store = new SettingsStore(_tempPath);
Assert.Equal(expected, store.LoadDisplay().FieldOfView, precision: 1);
}
[Fact]
public void LoadDisplay_does_not_migrate_a_v3_fieldOfView()
{
// A post-migration 60 is a deliberate gameFOV — it must survive.
File.WriteAllText(_tempPath, """
{
"version": 3,
"display": { "fieldOfView": 60 }
}
""");
var store = new SettingsStore(_tempPath);
Assert.Equal(60f, store.LoadDisplay().FieldOfView);
}
[Fact]
public void LoadDisplay_absent_fieldOfView_needs_no_migration()
{
// The per-field fallback is DisplaySettings.Default (already v3
// semantics, 90) — migrating it would corrupt the default.
File.WriteAllText(_tempPath, """
{
"version": 2,
"display": { "resolution": "1920x1080" }
}
""");
var store = new SettingsStore(_tempPath);
Assert.Equal(90f, store.LoadDisplay().FieldOfView);
}
[Fact]
public void SaveDisplay_stamps_v3_so_the_migration_runs_once()
{
File.WriteAllText(_tempPath, """
{
"version": 2,
"display": { "fieldOfView": 60 }
}
""");
var store = new SettingsStore(_tempPath);
DisplaySettings migrated = store.LoadDisplay(); // 60 → 90
store.SaveDisplay(migrated); // stamps version 3
// A second load must NOT re-migrate the already-migrated 90.
Assert.Equal(90f, store.LoadDisplay().FieldOfView);
}
[Fact]
public void LoadDisplay_invalid_particle_range_falls_back_to_default()
{
File.WriteAllText(_tempPath, """
{
"version": 2,
"display": { "particleRange": "Unlimited" }
}
""");
var loaded = new SettingsStore(_tempPath).LoadDisplay();
Assert.Equal(ParticleRange.Extended, loaded.ParticleRange);
}
[Fact]
public void LoadDisplay_numeric_undefined_particle_range_falls_back_to_default()
{
File.WriteAllText(_tempPath, """
{
"version": 2,
"display": { "particleRange": "999" }
}
""");
var loaded = new SettingsStore(_tempPath).LoadDisplay();
Assert.Equal(ParticleRange.Extended, loaded.ParticleRange);
}
[Fact]
public void SaveDisplay_preserves_unknown_top_level_keys()
{
// Forward-compat: a newer client may have written sections we
// don't know about (audio, gameplay). Saving display must not
// delete those, otherwise running an older client would silently
// drop the user's other-tab preferences.
File.WriteAllText(_tempPath, """
{
"version": 1,
"display": { "resolution": "1280x720" },
"audio": { "master": 0.5, "music": 0.7 }
}
""");
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "1920x1080" });
var raw = File.ReadAllText(_tempPath);
Assert.Contains("\"audio\"", raw);
Assert.Contains("\"master\"", raw);
Assert.Contains("0.5", raw);
// And the new display value did get written.
Assert.Contains("1920x1080", raw);
}
// -- Audio section round-trip ----------------------------------------
[Fact]
public void LoadAudio_returns_defaults_when_file_is_missing()
{
var store = new SettingsStore(_tempPath);
Assert.Equal(AudioSettings.Default, store.LoadAudio());
}
[Fact]
public void SaveAudio_then_LoadAudio_round_trips_all_fields()
{
var store = new SettingsStore(_tempPath);
var original = new AudioSettings(Master: 0.3f, Sfx: 0.9f, Ambient: 0.6f);
store.SaveAudio(original);
var loaded = store.LoadAudio();
Assert.Equal(original, loaded);
}
[Fact]
public void LoadAudio_falls_back_per_field_when_keys_missing()
{
File.WriteAllText(_tempPath, """
{
"version": 1,
"audio": { "master": 0.25 }
}
""");
var store = new SettingsStore(_tempPath);
var loaded = store.LoadAudio();
Assert.Equal(0.25f, loaded.Master);
Assert.Equal(AudioSettings.Default.Sfx, loaded.Sfx);
Assert.Equal(AudioSettings.Default.Ambient, loaded.Ambient);
}
[Fact]
public void SaveAudio_preserves_display_section()
{
// Save display first, then audio — display values must survive.
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" });
store.SaveAudio(AudioSettings.Default with { Master = 0.4f });
Assert.Equal("2560x1440", store.LoadDisplay().Resolution);
Assert.Equal(0.4f, store.LoadAudio().Master);
}
[Fact]
public void SaveDisplay_after_SaveAudio_preserves_audio_section()
{
// Reverse order — audio must survive a subsequent display save.
var store = new SettingsStore(_tempPath);
store.SaveAudio(AudioSettings.Default with { Ambient = 0.1f });
store.SaveDisplay(DisplaySettings.Default with { ShowFps = true });
Assert.Equal(0.1f, store.LoadAudio().Ambient);
Assert.True(store.LoadDisplay().ShowFps);
}
// -- Gameplay section: OP9 migration honesty ---------------------------
//
// Campaign OP slice OP9 (docs/plans/2026-08-10-options-panel-campaign.md
// §OP9) retired the Gameplay tab's client-local GameplaySettings record
// and Load/SaveGameplay outright — all 13 fields were already re-pointed
// to the server-bit seam (RuntimeCharacterOptionsState) at OP4. An
// EXISTING settings.json written by a pre-OP9 build can still carry a
// "gameplay" top-level block; these two tests pin that SettingsStore
// tolerates it (no Load* method reads it, so it neither throws nor is
// silently dropped) rather than requiring a migration step.
[Fact]
public void LeftoverGameplaySection_FromAnOlderSettingsJson_DoesNotBreakOtherLoads()
{
File.WriteAllText(_tempPath, """
{
"version": 2,
"display": { "resolution": "1366x768" },
"gameplay": { "lockUI": true, "toggleRun": false }
}
""");
var store = new SettingsStore(_tempPath);
Assert.Equal("1366x768", store.LoadDisplay().Resolution);
Assert.Equal(AudioSettings.Default, store.LoadAudio());
Assert.Equal(ChatSettings.Default, store.LoadChat());
}
[Fact]
public void LeftoverGameplaySection_SurvivesAnUnrelatedSave()
{
// The generic SaveSection round-trip preservation (raw-JSON-text
// carry-forward of unknown top-level keys, exercised elsewhere by
// SaveDisplay_preserves_unknown_top_level_keys) is exactly what
// keeps an orphaned "gameplay" block alive forever once nothing
// reads or writes it — this pins that mechanism for the specific
// section OP9 just retired.
File.WriteAllText(_tempPath, """
{
"version": 2,
"gameplay": { "lockUI": true, "showHelm": false }
}
""");
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "1920x1080" });
var raw = File.ReadAllText(_tempPath);
Assert.Contains("\"gameplay\"", raw);
Assert.Contains("\"lockUI\": true", raw);
Assert.Contains("\"showHelm\": false", raw);
Assert.Contains("1920x1080", raw);
}
// -- Chat section round-trip ------------------------------------------
[Fact]
public void LoadChat_returns_defaults_when_file_is_missing()
{
var store = new SettingsStore(_tempPath);
Assert.Equal(ChatSettings.Default, store.LoadChat());
}
[Fact]
public void SaveChat_then_LoadChat_round_trips_all_fields()
{
var store = new SettingsStore(_tempPath);
var original = new ChatSettings(
HearGeneralChat: false,
HearTradeChat: false,
HearLFGChat: false,
HearRoleplayChat: true,
HearSocietyChat: true,
AppearOffline: true,
ShowTimestamps: false,
FilterProfanity: false,
FontSize: 16f);
store.SaveChat(original);
Assert.Equal(original, store.LoadChat());
}
// -- Campaign CH slice CH6b: floating chat window filter round-trip ---
[Fact]
public void LoadChat_returns_retail_PostInit_filter_defaults_when_file_is_missing()
{
var store = new SettingsStore(_tempPath);
ChatSettings loaded = store.LoadChat();
Assert.Equal(0x0000101Cu, loaded.ChatWindow1Filter);
Assert.Equal(0x00040C00u, loaded.ChatWindow2Filter);
Assert.Equal(0x00080000u, loaded.ChatWindow3Filter);
Assert.Equal(0x78000000u, loaded.ChatWindow4Filter);
}
[Fact]
public void SaveChat_then_LoadChat_round_trips_floating_window_filters()
{
var store = new SettingsStore(_tempPath);
var original = ChatSettings.Default with
{
ChatWindow1Filter = 0x1u,
ChatWindow2Filter = 0xFFFFFFFFu,
ChatWindow3Filter = 0x8000000000000000u, // exercises the high dword (Society/reserved bits)
ChatWindow4Filter = 0ul,
};
store.SaveChat(original);
ChatSettings loaded = store.LoadChat();
Assert.Equal(original.ChatWindow1Filter, loaded.ChatWindow1Filter);
Assert.Equal(original.ChatWindow2Filter, loaded.ChatWindow2Filter);
Assert.Equal(original.ChatWindow3Filter, loaded.ChatWindow3Filter);
Assert.Equal(original.ChatWindow4Filter, loaded.ChatWindow4Filter);
}
// -- Campaign OP slice OP5: main chat window filter round-trip ---------
[Fact]
public void LoadChat_returns_retail_main_window_filter_default_when_file_is_missing()
{
var store = new SettingsStore(_tempPath);
ChatSettings loaded = store.LoadChat();
Assert.Equal(0xFBFFFFFFu, loaded.ChatWindowMainFilter);
}
[Fact]
public void SaveChat_then_LoadChat_round_trips_main_window_filter()
{
var store = new SettingsStore(_tempPath);
var original = ChatSettings.Default with { ChatWindowMainFilter = 0x1ul };
store.SaveChat(original);
ChatSettings loaded = store.LoadChat();
Assert.Equal(original.ChatWindowMainFilter, loaded.ChatWindowMainFilter);
}
// -- Campaign CH slice CH6c: window opacity round-trip -----------------
[Fact]
public void LoadChat_returns_acdream_opaque_opacity_defaults_when_file_is_missing()
{
// CH6c review fix (2026-08-10): the pre-fix default was retail's base
// ChatInterface value (0.5/1.0), which faded every registered window
// to 50% opacity out of the box. acdream now ships gmMainChatUI's own
// 1.0/1.0 override (0x004CD0F0) as the shared global default — see
// ChatSettings.DefaultOpacity and register row AP-190.
var store = new SettingsStore(_tempPath);
ChatSettings loaded = store.LoadChat();
Assert.Equal(1.0f, loaded.DefaultOpacity);
Assert.Equal(1.0f, loaded.ActiveOpacity);
}
[Fact]
public void SaveChat_then_LoadChat_round_trips_opacity()
{
var store = new SettingsStore(_tempPath);
var original = ChatSettings.Default with
{
DefaultOpacity = 0.2f,
ActiveOpacity = 0.8f,
};
store.SaveChat(original);
ChatSettings loaded = store.LoadChat();
Assert.Equal(original.DefaultOpacity, loaded.DefaultOpacity);
Assert.Equal(original.ActiveOpacity, loaded.ActiveOpacity);
}
[Fact]
public void All_three_sections_coexist_in_one_settings_json()
{
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" });
store.SaveAudio(AudioSettings.Default with { Master = 0.5f });
store.SaveChat(ChatSettings.Default with { HearTradeChat = false, FontSize = 14f });
Assert.Equal("2560x1440", store.LoadDisplay().Resolution);
Assert.Equal(0.5f, store.LoadAudio().Master);
Assert.False(store.LoadChat().HearTradeChat);
Assert.Equal(14f, store.LoadChat().FontSize);
}
// -- Character section round-trip (per-toon) --------------------------
[Fact]
public void LoadCharacter_returns_defaults_when_file_is_missing()
{
var store = new SettingsStore(_tempPath);
Assert.Equal(CharacterSettings.Default, store.LoadCharacter("default"));
}
[Fact]
public void LoadCharacter_returns_defaults_when_toonKey_not_in_file()
{
// File exists with a different toon's data; asking for "+Acdream"
// returns defaults rather than the other toon's data.
var store = new SettingsStore(_tempPath);
store.SaveCharacter("Bob", CharacterSettings.Default with { AutoAttack = true });
var loaded = store.LoadCharacter("+Acdream");
Assert.Equal(CharacterSettings.Default, loaded);
}
[Fact]
public void SaveCharacter_then_LoadCharacter_round_trips_all_fields()
{
var store = new SettingsStore(_tempPath);
var original = new CharacterSettings(
DefaultChatChannel: "Allegiance",
AutoAttack: true,
ConfirmSalvage: false,
ShowPickupMessages: false);
store.SaveCharacter("+Acdream", original);
Assert.Equal(original, store.LoadCharacter("+Acdream"));
}
[Fact]
public void SaveCharacter_preserves_other_toons_within_character_section()
{
// Two different toons, each with distinct settings — saving one
// must not clobber the other.
var store = new SettingsStore(_tempPath);
var alice = CharacterSettings.Default with { DefaultChatChannel = "Allegiance" };
var bob = CharacterSettings.Default with { DefaultChatChannel = "Fellowship", AutoAttack = true };
store.SaveCharacter("Alice", alice);
store.SaveCharacter("Bob", bob);
Assert.Equal(alice, store.LoadCharacter("Alice"));
Assert.Equal(bob, store.LoadCharacter("Bob"));
}
[Fact]
public void SaveCharacter_preserves_other_top_level_sections()
{
// Display/audio survive when SaveCharacter writes its nested map.
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" });
store.SaveAudio(AudioSettings.Default with { Master = 0.4f });
store.SaveCharacter("+Acdream", CharacterSettings.Default with { AutoAttack = true });
Assert.Equal("2560x1440", store.LoadDisplay().Resolution);
Assert.Equal(0.4f, store.LoadAudio().Master);
Assert.True(store.LoadCharacter("+Acdream").AutoAttack);
}
[Fact]
public void All_four_sections_coexist_in_one_settings_json()
{
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" });
store.SaveAudio(AudioSettings.Default with { Master = 0.5f });
store.SaveChat(ChatSettings.Default with { HearTradeChat = false });
store.SaveCharacter("+Acdream",
CharacterSettings.Default with { DefaultChatChannel = "Fellowship" });
Assert.Equal("2560x1440", store.LoadDisplay().Resolution);
Assert.Equal(0.5f, store.LoadAudio().Master);
Assert.False(store.LoadChat().HearTradeChat);
Assert.Equal("Fellowship", store.LoadCharacter("+Acdream").DefaultChatChannel);
}
[Fact]
public void WindowPositions_RoundTripPerCharacterAndPreserveSettings()
{
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" });
store.SaveWindowPosition("Alice", "radar", new UiWindowPosition(321.5f, 18f));
store.SaveWindowPosition("Bob", "radar", new UiWindowPosition(44f, 55f));
Assert.Equal(new UiWindowPosition(321.5f, 18f),
store.LoadWindowPosition("Alice", "radar"));
Assert.Equal(new UiWindowPosition(44f, 55f),
store.LoadWindowPosition("Bob", "radar"));
Assert.Null(store.LoadWindowPosition("Alice", "inventory"));
Assert.Equal("2560x1440", store.LoadDisplay().Resolution);
}
[Fact]
public void WindowLayouts_RoundTripPerCharacterResolutionAndPreserveSettings()
{
var store = new SettingsStore(_tempPath);
store.SaveDisplay(DisplaySettings.Default with { Resolution = "2560x1440" });
var alice1080 = new UiWindowLayout(10f, 20f, 500f, 300f, true, false, true);
var alice1440 = new UiWindowLayout(30f, 40f, 650f, 420f, false, true, false);
var bob1080 = new UiWindowLayout(50f, 60f, 310f, 132f, true, false, false);
store.SaveWindowLayout("Alice", "1920x1080", "chat", alice1080);
store.SaveWindowLayout("Alice", "2560x1440", "chat", alice1440);
store.SaveWindowLayout("Bob", "1920x1080", "toolbar", bob1080);
Assert.Equal(alice1080, store.LoadWindowLayout("Alice", "1920x1080", "chat", default));
Assert.Equal(alice1440, store.LoadWindowLayout("Alice", "2560x1440", "chat", default));
Assert.Equal(bob1080, store.LoadWindowLayout("Bob", "1920x1080", "toolbar", default));
Assert.Null(store.LoadWindowLayout("Alice", "1920x1080", "inventory", default));
Assert.Equal("2560x1440", store.LoadDisplay().Resolution);
}
[Fact]
public void WindowLayouts_MigrateLegacyRadarPositionOverCurrentDefaults()
{
var store = new SettingsStore(_tempPath);
store.SaveWindowPosition("Alice", "radar", new UiWindowPosition(321.5f, 18f));
var current = new UiWindowLayout(10f, 10f, 160f, 180f, true, false, false);
UiWindowLayout migrated = Assert.IsType<UiWindowLayout>(
store.LoadWindowLayout("Alice", "1920x1080", "radar", current));
Assert.Equal(321.5f, migrated.X);
Assert.Equal(18f, migrated.Y);
Assert.Equal(current.Width, migrated.Width);
Assert.Equal(current.Height, migrated.Height);
Assert.Equal(current.Visible, migrated.Visible);
}
[Fact]
public void WindowLayouts_FallBackToNearestSavedResolution()
{
var store = new SettingsStore(_tempPath);
var hd = new UiWindowLayout(100f, 110f, 400f, 250f, true, false, false);
var uhd = new UiWindowLayout(300f, 310f, 600f, 450f, true, false, false);
store.SaveWindowLayout("Alice", "1280x720", "chat", hd);
store.SaveWindowLayout("Alice", "3840x2160", "chat", uhd);
Assert.Equal(hd, store.LoadWindowLayout("Alice", "1600x900", "chat", default));
Assert.Equal(uhd, store.LoadWindowLayout("Alice", "3200x1800", "chat", default));
}
[Fact]
public void NamedWindowLayouts_RoundTripIndependentlyOfCharacterAndResolution()
{
var store = new SettingsStore(_tempPath);
var hunting = new UiWindowLayout(10f, 20f, 500f, 250f, true, false, true);
var crafting = new UiWindowLayout(30f, 40f, 300f, 400f, false, true, false);
store.SaveNamedWindowLayout("hunting", "chat", hunting);
store.SaveNamedWindowLayout("crafting", "chat", crafting);
Assert.Equal(hunting, store.LoadNamedWindowLayout("hunting", "chat", default));
Assert.Equal(crafting, store.LoadNamedWindowLayout("crafting", "chat", default));
Assert.Null(store.LoadNamedWindowLayout("missing", "chat", default));
}
}