Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and OP3's per-page OptionPage model: the General Options header + two DualHash-linked opacity sliders (Option_DefaultOpacity_Property 0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on drag through RetailWindowOpacityController, defaults read from the installed DAT's DBProperties collection at DID 0x78000001 via ChatOptionsDatDefaults), and the five per-window text-filter blocks (main window 12 rows minus Gameplay, four floaties 13 rows each — the byte-verified authored order cross-checked against the raw gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not just the research doc's own table) writing AcDream.Core.Chat. ChatWindowState directly, the same state CH6's chat windows already read. AP-195 retired: ported both halves left open at the OP2 re-review — the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by the block-level P0x10000082/P0x10000083 sprites now threaded through ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail (UiCheckboxBitfield64.Height grows with its stacked row content; the enclosing ListBox reflows around the block's FINAL height via the new UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking rather than a third stacking path). AP-187 broadened to cover the main window's own filter (previously only the four floaties) and the new live-editing write path. The main chat window's filter (retail window id 8, ChatWindowState id 0) gains its own settings.json persistence (ChatSettings. ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields; opacity persistence is now wired on every live slider change, not only through the old dev-scaffold Settings panel. Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all 19 committed layout fixtures — purely additive, confirmed against the live installed DAT (0x10000520's own 0x82/0x83 properties resolve to 0x06004D17/0x06004D19 exactly as AP-195 documented). Conformance: FilterRows/FilterBlocks pinned against the byte-verified authored order and ChatWindowState's own default constants; the AP-195 LED swap and self-sizing behavior; the DAT opacity-default extraction against the live installed DAT; live filter/opacity writes reaching ChatWindowState/RetailWindowOpacityController; OnShown re-seed and Reset/Defaults ghosting per the OP4 binding-pattern discipline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
607 lines
26 KiB
C#
607 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.Chat;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Campaign OP slice OP5 (2026-08-11) conformance + behavior tests for
|
|
/// <see cref="ChatOptionsPageController"/> — the same hermetic pattern
|
|
/// <c>CharacterOptionsPageControllerTests</c> established: pure-data conformance
|
|
/// against the byte-verified research doc/decomp tables, then behavioral tests
|
|
/// against the committed <c>options_panel_2100006E_1000018D.json</c> +
|
|
/// <c>options_2100002B.json</c> fixtures — no live DAT access.
|
|
/// </summary>
|
|
public sealed class ChatOptionsPageControllerTests
|
|
{
|
|
// ── Pure data conformance ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void FilterRows_Has13EntriesInByteVerifiedAuthoredOrder()
|
|
{
|
|
// docs/research/2026-08-10-options-panel-structure.md §5.2, cross-checked
|
|
// against the raw gmChatOptionsUI::InitOptions / AddCheckboxBitfield64Option
|
|
// pseudo-C AddChild call sequence (this class's own doc trace).
|
|
(ulong Mask, string Key)[] expected =
|
|
{
|
|
(0x0000000083912021ul, "ID_ChatOption_TextFilter_Gameplay"),
|
|
(0x0000000000600040ul, "ID_ChatOption_TextFilter_Combat"),
|
|
(0x0000000000020080ul, "ID_ChatOption_TextFilter_Magic"),
|
|
(0x0000000000001004ul, "ID_ChatOption_TextFilter_AreaSpeech"),
|
|
(0x0000000000000018ul, "ID_ChatOption_TextFilter_Tells"),
|
|
(0x0000000000040C00ul, "ID_ChatOption_TextFilter_Allegience"),
|
|
(0x0000000000080000ul, "ID_ChatOption_TextFilter_Fellowship"),
|
|
(0x0000000008000000ul, "ID_ChatOption_TextFilter_General"),
|
|
(0x0000000010000000ul, "ID_ChatOption_TextFilter_Trade"),
|
|
(0x0000000020000000ul, "ID_ChatOption_TextFilter_LFG"),
|
|
(0x0000000040000000ul, "ID_ChatOption_TextFilter_Roleplay"),
|
|
(0x0000000100000000ul, "ID_ChatOption_TextFilter_Society"),
|
|
(0x0000000004000000ul, "ID_ChatOption_TextFilter_Error"),
|
|
};
|
|
|
|
Assert.Equal(13, ChatOptionsPageController.FilterRows.Length);
|
|
for (int i = 0; i < expected.Length; i++)
|
|
{
|
|
Assert.Equal(expected[i].Mask, ChatOptionsPageController.FilterRows[i].Mask);
|
|
Assert.Equal(expected[i].Key, ChatOptionsPageController.FilterRows[i].RetailLabelKey);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FilterRows_EveryMaskIsPairwiseDistinct_AndNonZero()
|
|
{
|
|
var masks = ChatOptionsPageController.FilterRows.Select(static r => r.Mask).ToList();
|
|
Assert.Equal(masks.Count, masks.Distinct().Count());
|
|
Assert.All(masks, m => Assert.NotEqual(0ul, m));
|
|
}
|
|
|
|
[Fact]
|
|
public void FilterBlocks_Has5EntriesInAuthoredOrder_WithRetailWindowIds()
|
|
{
|
|
// Research doc §5: main(8), floaty1(2), floaty2(3), floaty3(4), floaty4(5) —
|
|
// authored top-to-bottom order in gmChatOptionsUI::InitOptions.
|
|
(int RetailId, int CompactId, string HeaderKey, ulong Default, bool IncludesGameplay)[] expected =
|
|
{
|
|
(8, ChatWindowState.MainWindowId, "ID_ChatOption_MainChatWindow_Section",
|
|
0xFBFFFFFFul, false),
|
|
(2, 1, "ID_ChatOption_FloatyChatWindow1_Section", 0x0000101Cul, true),
|
|
(3, 2, "ID_ChatOption_FloatyChatWindow2_Section", 0x00040C00ul, true),
|
|
(4, 3, "ID_ChatOption_FloatyChatWindow3_Section", 0x00080000ul, true),
|
|
(5, 4, "ID_ChatOption_FloatyChatWindow4_Section", 0x78000000ul, true),
|
|
};
|
|
|
|
Assert.Equal(5, ChatOptionsPageController.FilterBlocks.Length);
|
|
for (int i = 0; i < expected.Length; i++)
|
|
{
|
|
ChatOptionsPageController.FilterBlockSpec spec = ChatOptionsPageController.FilterBlocks[i];
|
|
Assert.Equal(expected[i].RetailId, spec.RetailWindowId);
|
|
Assert.Equal(expected[i].CompactId, spec.CompactWindowId);
|
|
Assert.Equal(expected[i].HeaderKey, spec.HeaderKey);
|
|
Assert.Equal(expected[i].Default, spec.DefaultFilter);
|
|
Assert.Equal(expected[i].IncludesGameplay, spec.IncludesGameplayRow);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FilterBlocks_DefaultsMatchChatWindowStateNamedConstants()
|
|
{
|
|
// These MUST be the exact same source (ChatWindowState's own constants),
|
|
// not two independently-typed literal tables that could drift apart.
|
|
Assert.Equal(ChatWindowState.MainWindowDefaultFilter, ChatOptionsPageController.FilterBlocks[0].DefaultFilter);
|
|
Assert.Equal(ChatWindowState.Floaty1DefaultFilter, ChatOptionsPageController.FilterBlocks[1].DefaultFilter);
|
|
Assert.Equal(ChatWindowState.Floaty2DefaultFilter, ChatOptionsPageController.FilterBlocks[2].DefaultFilter);
|
|
Assert.Equal(ChatWindowState.Floaty3DefaultFilter, ChatOptionsPageController.FilterBlocks[3].DefaultFilter);
|
|
Assert.Equal(ChatWindowState.Floaty4DefaultFilter, ChatOptionsPageController.FilterBlocks[4].DefaultFilter);
|
|
}
|
|
|
|
// ── Row/header string-hash conformance ───────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData("ID_ChatOption_TextFilter_Gameplay")]
|
|
[InlineData("ID_ChatOption_TextFilter_Combat")]
|
|
[InlineData("ID_ChatOption_TextFilter_Magic")]
|
|
[InlineData("ID_ChatOption_TextFilter_AreaSpeech")]
|
|
[InlineData("ID_ChatOption_TextFilter_Tells")]
|
|
[InlineData("ID_ChatOption_TextFilter_Allegience")]
|
|
[InlineData("ID_ChatOption_TextFilter_Fellowship")]
|
|
[InlineData("ID_ChatOption_TextFilter_General")]
|
|
[InlineData("ID_ChatOption_TextFilter_Trade")]
|
|
[InlineData("ID_ChatOption_TextFilter_LFG")]
|
|
[InlineData("ID_ChatOption_TextFilter_Roleplay")]
|
|
[InlineData("ID_ChatOption_TextFilter_Society")]
|
|
[InlineData("ID_ChatOption_TextFilter_Error")]
|
|
public void EveryFilterRowKey_IsAuthoredOnTheFilterRowsTable(string key)
|
|
{
|
|
Assert.Contains(ChatOptionsPageController.FilterRows, r => r.RetailLabelKey == key);
|
|
}
|
|
|
|
// ── Behavioral: built against the committed fixtures ─────────────────────
|
|
|
|
private static (uint, int, int) NoTex(uint _) => (0, 0, 0);
|
|
|
|
private static ElementInfo? Find(ElementInfo n, uint id)
|
|
{
|
|
if (n.Id == id) return n;
|
|
foreach (ElementInfo c in n.Children)
|
|
{
|
|
ElementInfo? f = Find(c, id);
|
|
if (f is not null) return f;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>The same "resolve a row template from the standalone 0x2100002B
|
|
/// fixture" resolver <c>CharacterOptionsPageControllerTests</c> uses.</summary>
|
|
private static Func<uint, uint, UiElement?> MakeTemplateResolver()
|
|
{
|
|
ElementInfo panelRoot = FixtureLoader.LoadOptionsPanelInfos();
|
|
return (layoutId, elementId) =>
|
|
{
|
|
if (layoutId != 0x2100002Bu) return null;
|
|
ElementInfo? templateInfo = Find(panelRoot, elementId);
|
|
return templateInfo is null ? null : LayoutImporter.Build(templateInfo, NoTex, null).Root;
|
|
};
|
|
}
|
|
|
|
private sealed class FakeBindings
|
|
{
|
|
public float DefaultOpacity = 0.5f;
|
|
public float ActiveOpacity = 1.0f;
|
|
public float DefaultOpacityDatDefault = 0.5f;
|
|
public float ActiveOpacityDatDefault = 1.0f;
|
|
public List<float> DefaultOpacitySets { get; } = new();
|
|
public List<float> ActiveOpacitySets { get; } = new();
|
|
|
|
public Dictionary<int, ulong> Filters { get; } = new()
|
|
{
|
|
[ChatWindowState.MainWindowId] = ChatWindowState.MainWindowDefaultFilter,
|
|
[1] = ChatWindowState.Floaty1DefaultFilter,
|
|
[2] = ChatWindowState.Floaty2DefaultFilter,
|
|
[3] = ChatWindowState.Floaty3DefaultFilter,
|
|
[4] = ChatWindowState.Floaty4DefaultFilter,
|
|
};
|
|
public List<(int WindowId, ulong Value)> FilterSets { get; } = new();
|
|
|
|
public ChatOptionsPageController.Bindings ToBindings() => new(
|
|
CurrentDefaultOpacity: () => DefaultOpacity,
|
|
CurrentActiveOpacity: () => ActiveOpacity,
|
|
SetDefaultOpacity: value =>
|
|
{
|
|
(DefaultOpacity, ActiveOpacity) = ChatOpacityLinkFor(ActiveOpacity, value, isDefault: true);
|
|
DefaultOpacitySets.Add(value);
|
|
},
|
|
SetActiveOpacity: value =>
|
|
{
|
|
(DefaultOpacity, ActiveOpacity) = ChatOpacityLinkFor(DefaultOpacity, value, isDefault: false);
|
|
ActiveOpacitySets.Add(value);
|
|
},
|
|
DefaultOpacityDatDefault: DefaultOpacityDatDefault,
|
|
ActiveOpacityDatDefault: ActiveOpacityDatDefault,
|
|
CurrentFilter: windowId => Filters[windowId],
|
|
SetFilter: (windowId, value) =>
|
|
{
|
|
Filters[windowId] = value;
|
|
FilterSets.Add((windowId, value));
|
|
});
|
|
|
|
// Mirrors AcDream.UI.Abstractions.Panels.Settings.ChatOpacityLink's own
|
|
// math exactly (the fake stands in for RetailWindowOpacityController,
|
|
// which itself just calls that link) — kept local so this test file has
|
|
// no cross-project dependency for a two-line clamp/drag rule.
|
|
private static (float Default, float Active) ChatOpacityLinkFor(
|
|
float other, float value, bool isDefault)
|
|
{
|
|
value = Math.Clamp(value, 0f, 1f);
|
|
if (isDefault)
|
|
return (value, other < value ? value : other);
|
|
return (other > value ? value : other, value);
|
|
}
|
|
}
|
|
|
|
private static (OptionsPanelController Panel, FakeBindings Bindings, bool Bound) BindReal(
|
|
Func<uint, uint, string?>? resolveString = null)
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
|
|
var fakeBindings = new FakeBindings();
|
|
bool bound = ChatOptionsPageController.Bind(
|
|
layout,
|
|
controller.ChatPage,
|
|
MakeTemplateResolver(),
|
|
resolveString ?? ((_, _) => null),
|
|
fakeBindings.ToBindings());
|
|
|
|
return (controller, fakeBindings, bound);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_Succeeds_AndRegistersExactlySevenRows()
|
|
{
|
|
// 2 opacity sliders + 5 filter blocks = 7 IOptionRow registrations
|
|
// (each block is ONE composite row, not one row per checkbox).
|
|
(OptionsPanelController controller, _, bool bound) = BindReal();
|
|
|
|
Assert.True(bound);
|
|
Assert.Equal(7, controller.ChatPage.Rows.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_MainWindowBlock_Has12Rows_FloatyBlocks_Have13Rows()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
var fakeBindings = new FakeBindings();
|
|
bool bound = ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
Assert.True(bound);
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
|
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
|
|
|
|
Assert.Equal(5, blocks.Count);
|
|
Assert.Equal(12, blocks[0].Rows.Count); // main window — no Gameplay row
|
|
for (int i = 1; i < 5; i++)
|
|
Assert.Equal(13, blocks[i].Rows.Count); // the four floaties
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_FilterBlocks_ResolveTheAP195LedSprites()
|
|
{
|
|
// AP-195 end-to-end: the regenerated fixture now carries dat properties
|
|
// 0x10000082/0x10000083 on template consumer 0x10000520
|
|
// (0x06004D17/0x06004D19), and DatWidgetFactory threads them into the
|
|
// constructed UiCheckboxBitfield64 — every one of the five blocks shares
|
|
// the SAME authored template, so every block resolves the same sprites.
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
var fakeBindings = new FakeBindings();
|
|
ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
|
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
|
|
|
|
Assert.Equal(5, blocks.Count);
|
|
Assert.All(blocks, b =>
|
|
{
|
|
Assert.Equal(0x06004D17u, b.CheckedLedSprite);
|
|
Assert.Equal(0x06004D19u, b.UncheckedLedSprite);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_FilterBlocks_SelfSizeToFitTheirRows()
|
|
{
|
|
// AP-195's self-sizing tail — the block grows past its authored 272x100
|
|
// extent to fit N rows (13 floaty rows * 20px template height > 100).
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
var fakeBindings = new FakeBindings();
|
|
ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
|
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
|
|
|
|
Assert.All(blocks, b => Assert.True(b.Height > 100f, $"block Height={b.Height}"));
|
|
Assert.Equal(272f, blocks[0].Width); // width (GetWidth()) untouched
|
|
}
|
|
|
|
private static List<UiCheckboxBitfield64> CollectBlocks(UiElement root)
|
|
{
|
|
var found = new List<UiCheckboxBitfield64>();
|
|
Walk(root, found);
|
|
return found;
|
|
|
|
static void Walk(UiElement node, List<UiCheckboxBitfield64> acc)
|
|
{
|
|
if (node is UiCheckboxBitfield64 block) acc.Add(block);
|
|
foreach (UiElement child in node.Children) Walk(child, acc);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_SeedsSlidersFromCurrentOpacity_NotTheDatDefault()
|
|
{
|
|
var fakeBindings = new FakeBindings { DefaultOpacity = 0.25f, ActiveOpacity = 0.75f };
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
bool bound = ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
Assert.True(bound);
|
|
|
|
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
|
|
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
|
|
Assert.Equal(0.25f, defaultRow.Current);
|
|
Assert.Equal(0.75f, activeRow.Current);
|
|
// Never wrote back just from seeding.
|
|
Assert.Empty(fakeBindings.DefaultOpacitySets);
|
|
Assert.Empty(fakeBindings.ActiveOpacitySets);
|
|
}
|
|
|
|
[Fact]
|
|
public void DraggingDefaultSlider_AppliesLive_AndDragsActiveUp_NeverClamping()
|
|
{
|
|
var fakeBindings = new FakeBindings { DefaultOpacity = 0.3f, ActiveOpacity = 0.3f };
|
|
(OptionsPanelController controller, FakeBindings bindings, bool bound) =
|
|
BindRealWith(fakeBindings);
|
|
Assert.True(bound);
|
|
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
|
|
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
|
|
|
|
// Raise Default ABOVE the current Active value — retail's link drags
|
|
// Active UP to match, never clamps Default down.
|
|
defaultRow.SetCurrentValue(0.9f);
|
|
|
|
Assert.Equal(0.9f, bindings.DefaultOpacity);
|
|
Assert.Equal(0.9f, bindings.ActiveOpacity); // dragged up
|
|
Assert.Equal(0.9f, defaultRow.Current);
|
|
Assert.Equal(0.9f, activeRow.Current); // the OTHER row's own value moved too
|
|
Assert.Single(bindings.DefaultOpacitySets);
|
|
// Live-apply: Changed reflects immediately (retail's Apply(1) shape).
|
|
Assert.True(controller.ChatPage.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void DraggingActiveSliderBelowDefault_DragsDefaultDown()
|
|
{
|
|
var fakeBindings = new FakeBindings { DefaultOpacity = 0.6f, ActiveOpacity = 0.6f };
|
|
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
|
|
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
|
|
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
|
|
|
|
activeRow.SetCurrentValue(0.1f);
|
|
|
|
Assert.Equal(0.1f, bindings.ActiveOpacity);
|
|
Assert.Equal(0.1f, bindings.DefaultOpacity); // dragged down
|
|
Assert.Equal(0.1f, defaultRow.Current);
|
|
Assert.Equal(0.1f, activeRow.Current);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnShown_ReSeedsBothSliders_FromLiveOpacity()
|
|
{
|
|
var fakeBindings = new FakeBindings { DefaultOpacity = 0.5f, ActiveOpacity = 1.0f };
|
|
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
|
|
|
|
bindings.DefaultOpacity = 0.2f;
|
|
bindings.ActiveOpacity = 0.9f;
|
|
|
|
controller.ChatPage.OnShown();
|
|
|
|
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
|
|
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
|
|
Assert.Equal(0.2f, defaultRow.Current);
|
|
Assert.Equal(0.2f, defaultRow.Saved);
|
|
Assert.Equal(0.9f, activeRow.Current);
|
|
Assert.Equal(0.9f, activeRow.Saved);
|
|
Assert.False(controller.ChatPage.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Defaults_RestoresDatExtractedSliderDefaults()
|
|
{
|
|
var fakeBindings = new FakeBindings
|
|
{
|
|
DefaultOpacity = 0.1f,
|
|
ActiveOpacity = 0.1f,
|
|
DefaultOpacityDatDefault = 0.5f,
|
|
ActiveOpacityDatDefault = 1.0f,
|
|
};
|
|
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
|
|
|
|
controller.ChatPage.Defaults();
|
|
|
|
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
|
|
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
|
|
Assert.Equal(0.5f, defaultRow.Current);
|
|
Assert.Equal(1.0f, activeRow.Current);
|
|
Assert.Equal(0.5f, bindings.DefaultOpacity);
|
|
Assert.Equal(1.0f, bindings.ActiveOpacity);
|
|
}
|
|
|
|
[Fact]
|
|
public void CheckingAFilterRow_PublishesSetFilter_ForItsOwnCompactWindowId()
|
|
{
|
|
var fakeBindings = new FakeBindings();
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
bool bound = ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
Assert.True(bound);
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
|
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
|
|
UiCheckboxBitfield64 floaty1Block = blocks[1]; // compact window id 1
|
|
|
|
UiCheckboxBitfield64.Row row = floaty1Block.Rows[0]; // Gameplay (floaty has it)
|
|
row.Toggle.OnClick!.Invoke();
|
|
|
|
var (windowId, value) = Assert.Single(fakeBindings.FilterSets);
|
|
Assert.Equal(1, windowId);
|
|
Assert.Equal(fakeBindings.Filters[1], value);
|
|
}
|
|
|
|
[Fact]
|
|
public void MainWindowBlock_HasNoGameplayRow()
|
|
{
|
|
var fakeBindings = new FakeBindings();
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
|
UiCheckboxBitfield64 mainBlock = CollectBlocks(listBox)[0];
|
|
|
|
Assert.All(mainBlock.Rows, r => Assert.NotEqual("Gameplay", r.Label));
|
|
Assert.DoesNotContain(mainBlock.Rows, r => r.LowMask == 0x83912021ul);
|
|
}
|
|
|
|
[Fact]
|
|
public void OnShown_ReSeedsFilterBlock_FromLiveChatWindowState()
|
|
{
|
|
var fakeBindings = new FakeBindings();
|
|
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
|
|
|
|
// Simulate the filter having changed behind the panel's back (e.g. a
|
|
// future second write path) between mount and the next tab-show.
|
|
bindings.Filters[ChatWindowState.MainWindowId] = 0x1ul;
|
|
|
|
controller.ChatPage.OnShown();
|
|
|
|
var mainRow = Assert.IsType<BitfieldOptionRow>(controller.ChatPage.Rows[2]); // 2 sliders + main block
|
|
Assert.Equal(0x1ul, mainRow.Current);
|
|
Assert.Equal(0x1ul, mainRow.Saved);
|
|
Assert.False(controller.ChatPage.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_RevertsAFilterBlock_ToItsSavedBaseline()
|
|
{
|
|
var fakeBindings = new FakeBindings();
|
|
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
|
|
var mainRow = Assert.IsType<BitfieldOptionRow>(controller.ChatPage.Rows[2]);
|
|
ulong initial = mainRow.Current;
|
|
bindings.FilterSets.Clear();
|
|
|
|
mainRow.SetCurrentValue(0x0ul);
|
|
Assert.True(controller.ChatPage.Changed);
|
|
|
|
controller.ChatPage.Reset();
|
|
|
|
Assert.Equal(initial, mainRow.Current);
|
|
Assert.Contains(bindings.FilterSets, s => s.WindowId == ChatWindowState.MainWindowId && s.Value == initial);
|
|
}
|
|
|
|
[Fact]
|
|
public void LabelResolutionFailure_LeavesRowsBuilt_NeverInventsEnglish()
|
|
{
|
|
(OptionsPanelController controller, _, bool bound) = BindReal(resolveString: (_, _) => null);
|
|
|
|
Assert.True(bound);
|
|
Assert.Equal(7, controller.ChatPage.Rows.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_MissingListBox_ReturnsFalse_AndDoesNotThrow()
|
|
{
|
|
var emptyRoot = new ElementInfo { Id = 0, Type = 3 };
|
|
ImportedLayout emptyLayout = LayoutImporter.Build(emptyRoot, NoTex, null);
|
|
var page = new OptionPage();
|
|
|
|
bool bound = ChatOptionsPageController.Bind(
|
|
emptyLayout, page, MakeTemplateResolver(), (_, _) => null,
|
|
new FakeBindings().ToBindings());
|
|
|
|
Assert.False(bound);
|
|
Assert.Empty(page.Rows);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScrollbarLinkage_ModelPointsAtTheChatListBoxScroll()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
var fakeBindings = new FakeBindings();
|
|
ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
|
var scrollbar = Assert.IsType<UiScrollbar>(
|
|
layout.FindElement(ChatOptionsPageController.ScrollbarElementId));
|
|
|
|
Assert.Same(listBox.Scroll, scrollbar.Model);
|
|
}
|
|
|
|
private static (OptionsPanelController Panel, FakeBindings Bindings, bool Bound) BindRealWith(
|
|
FakeBindings fakeBindings)
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
|
OptionsPanelController controller = OptionsPanelController.Bind(
|
|
layout,
|
|
new OptionsPanelController.Callbacks(
|
|
Toggle: () => { },
|
|
RequestExitToCharacterSelection: () => { },
|
|
ExitGame: () => { },
|
|
UseMouseTurningSettings: () => { },
|
|
DisplaySystemMessage: _ => { }))!;
|
|
bool bound = ChatOptionsPageController.Bind(
|
|
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
|
|
fakeBindings.ToBindings());
|
|
return (controller, fakeBindings, bound);
|
|
}
|
|
}
|