feat(ui): Campaign OP slice OP5 — the Chat tab
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>
This commit is contained in:
parent
4798302226
commit
e71e5a9614
52 changed files with 4540 additions and 40 deletions
|
|
@ -270,10 +270,11 @@ public class OptionsPanelLayoutConformanceTests
|
|||
/// containing ONLY the checkbox retail's own CreateChildren looks for
|
||||
/// (<see cref="UiCheckboxBitfield64.TemplateCheckboxElementId"/>) — proves the
|
||||
/// widget resolves and stamps that exact id rather than assuming row shape.</summary>
|
||||
private static UiCheckboxBitfield64 NewBitfieldWithStubTemplate()
|
||||
private static UiCheckboxBitfield64 NewBitfieldWithStubTemplate(
|
||||
uint checkedLedSprite = 0u, uint uncheckedLedSprite = 0u)
|
||||
{
|
||||
var templates = new[] { new UiTemplateListEntry(0x2100002Bu, 0x10000521u) };
|
||||
return new UiCheckboxBitfield64(templates)
|
||||
return new UiCheckboxBitfield64(templates, checkedLedSprite, uncheckedLedSprite)
|
||||
{
|
||||
Width = 272f,
|
||||
TemplateResolver = (layoutId, elementId) =>
|
||||
|
|
@ -359,6 +360,191 @@ public class OptionsPanelLayoutConformanceTests
|
|||
Assert.Empty(bitfield.Rows);
|
||||
}
|
||||
|
||||
// ── AP-195: LED media swap + self-sizing (Campaign OP slice OP5) ────────
|
||||
|
||||
private const uint CheckedLedDid = 0x06004D17u;
|
||||
private const uint UncheckedLedDid = 0x06004D19u;
|
||||
|
||||
[Fact]
|
||||
public void AddChild_AllMaskBitsSet_AppliesCheckedLedSprite()
|
||||
{
|
||||
var bitfield = NewBitfieldWithStubTemplate(CheckedLedDid, UncheckedLedDid);
|
||||
const ulong combatMask = 0x00000000_00600040ul; // two bits
|
||||
bitfield.SetDefaultValue(combatMask, 0ul); // BOTH bits already set
|
||||
|
||||
UiButton? row = bitfield.AddChild(combatMask, 0ul, "Combat");
|
||||
|
||||
Assert.NotNull(row);
|
||||
Assert.True(row!.Selected);
|
||||
Assert.Equal(CheckedLedDid, row.FaceFileOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChild_PartialMaskBitsSet_AppliesUncheckedLedSprite()
|
||||
{
|
||||
var bitfield = NewBitfieldWithStubTemplate(CheckedLedDid, UncheckedLedDid);
|
||||
const ulong combatMask = 0x00000000_00600040ul; // two bits
|
||||
const ulong onlyOneBitOfMask = 0x00000000_00000040ul;
|
||||
bitfield.SetDefaultValue(onlyOneBitOfMask, 0ul); // checked (any-set) but not all-set
|
||||
|
||||
UiButton? row = bitfield.AddChild(combatMask, 0ul, "Combat");
|
||||
|
||||
Assert.NotNull(row);
|
||||
Assert.True(row!.Selected); // any-set half unaffected by AP-195
|
||||
Assert.Equal(UncheckedLedDid, row.FaceFileOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChild_NoMaskBitsSet_LeavesLedOverrideUnset()
|
||||
{
|
||||
var bitfield = NewBitfieldWithStubTemplate(CheckedLedDid, UncheckedLedDid);
|
||||
bitfield.SetDefaultValue(0ul, 0ul);
|
||||
|
||||
UiButton? row = bitfield.AddChild(0x00000000_00600040ul, 0ul, "Combat");
|
||||
|
||||
Assert.NotNull(row);
|
||||
Assert.False(row!.Selected);
|
||||
Assert.Null(row.FaceFileOverride); // retail's own gate: swap only applies while checked
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleRow_TogglingOff_ClearsLedOverride()
|
||||
{
|
||||
var bitfield = NewBitfieldWithStubTemplate(CheckedLedDid, UncheckedLedDid);
|
||||
const ulong singleBitMask = 0x00000000_00000040ul;
|
||||
bitfield.SetDefaultValue(singleBitMask, 0ul); // fully set -> checked LED
|
||||
|
||||
UiButton row = bitfield.AddChild(singleBitMask, 0ul, "Combat")!;
|
||||
Assert.Equal(CheckedLedDid, row.FaceFileOverride);
|
||||
|
||||
row.OnClick!.Invoke(); // turns OFF (was fully set)
|
||||
|
||||
Assert.False(row.Selected);
|
||||
Assert.Null(row.FaceFileOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChild_MissingLedSprites_LeavesOverrideNull_EvenWhileChecked()
|
||||
{
|
||||
// Constructed WITHOUT checkedLedSprite/uncheckedLedSprite (0/0 default) —
|
||||
// e.g. an authoring gap or a fixture that predates AP-195's properties.
|
||||
var bitfield = NewBitfieldWithStubTemplate();
|
||||
const ulong mask = 0x1ul;
|
||||
bitfield.SetDefaultValue(mask, 0ul);
|
||||
|
||||
UiButton? row = bitfield.AddChild(mask, 0ul, "Row");
|
||||
|
||||
Assert.NotNull(row);
|
||||
Assert.True(row!.Selected);
|
||||
Assert.Null(row.FaceFileOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddChild_MultipleRows_SelfSizesHeightToStackedContent()
|
||||
{
|
||||
// AP-195's self-sizing tail: retail's ResizeTo(GetWidth(), CalculatePaperSize)
|
||||
// — the block grows to fit N rows instead of keeping its authored 272x100
|
||||
// extent. Each stub row template is 14px tall (NewBitfieldWithStubTemplate).
|
||||
var bitfield = NewBitfieldWithStubTemplate();
|
||||
bitfield.SetDefaultValue(0ul, 0ul);
|
||||
Assert.Equal(0f, bitfield.Height);
|
||||
|
||||
bitfield.AddChild(0x1ul, 0ul, "Row1");
|
||||
Assert.Equal(14f, bitfield.Height);
|
||||
|
||||
bitfield.AddChild(0x2ul, 0ul, "Row2");
|
||||
bitfield.AddChild(0x4ul, 0ul, "Row3");
|
||||
|
||||
Assert.Equal(42f, bitfield.Height); // 3 * 14
|
||||
Assert.Equal(272f, bitfield.Width); // Width (GetWidth()) is untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCurrentValue_PushesValueAndVisuals_WithoutFiringValueChanged()
|
||||
{
|
||||
var bitfield = NewBitfieldWithStubTemplate(CheckedLedDid, UncheckedLedDid);
|
||||
const ulong mask = 0x1ul;
|
||||
bitfield.SetDefaultValue(0ul, 0ul); // starts unset
|
||||
UiButton row = bitfield.AddChild(mask, 0ul, "Row")!;
|
||||
Assert.False(row.Selected);
|
||||
|
||||
bool valueChangedFired = false;
|
||||
bitfield.ValueChanged += (_, _) => valueChangedFired = true;
|
||||
|
||||
bitfield.SetCurrentValue(mask, 0ul); // the OnShown/Reset/Defaults re-seed path
|
||||
|
||||
Assert.Equal(mask, bitfield.CurrentLow);
|
||||
Assert.True(row.Selected);
|
||||
Assert.Equal(CheckedLedDid, row.FaceFileOverride);
|
||||
Assert.False(valueChangedFired); // a re-seed must never loop back as "user edited this"
|
||||
}
|
||||
|
||||
// ── UiTemplateListBox.AddPrebuiltRow (AP-195's "reuse the ListBox's own
|
||||
// stacking" disposition) ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AddPrebuiltRow_StacksBelowPreviousContent_UsingTheRowsFinalHeight()
|
||||
{
|
||||
ElementInfo panelRoot = FixtureLoader.LoadOptionsPanelInfos();
|
||||
ElementInfo characterListBoxInfo = Find(FixtureLoader.LoadOptionsCharacterInfos(), 0x100001FAu)!;
|
||||
var listBox = new UiTemplateListBox(
|
||||
characterListBoxInfo, NoTex,
|
||||
characterListBoxInfo.TemplateList, characterListBoxInfo.ScrollbarElementId)
|
||||
{
|
||||
TemplateResolver = (layoutId, elementId) =>
|
||||
{
|
||||
if (layoutId != 0x2100002Bu) return null;
|
||||
ElementInfo? templateInfo = Find(panelRoot, elementId);
|
||||
return templateInfo is null ? null : LayoutImporter.Build(templateInfo, NoTex, null).Root;
|
||||
},
|
||||
};
|
||||
|
||||
// A first ordinary row via the normal template path (header, 22px).
|
||||
UiElement? header = listBox.AddItemFromTemplateList(0);
|
||||
Assert.NotNull(header);
|
||||
Assert.Equal(22, listBox.ContentHeight);
|
||||
|
||||
// A pre-sized row built OUTSIDE the ListBox's own resolve step (the shape a
|
||||
// self-sizing UiCheckboxBitfield64 block uses) — its height is whatever the
|
||||
// caller already grew it to BEFORE calling AddPrebuiltRow.
|
||||
var prebuilt = new UiPanel { Width = 272f, Height = 260f };
|
||||
|
||||
UiElement returned = listBox.AddPrebuiltRow(prebuilt);
|
||||
|
||||
Assert.Same(prebuilt, returned);
|
||||
Assert.Equal(22f, prebuilt.Top); // stacked directly below the header row
|
||||
Assert.Equal(22 + 260, listBox.ContentHeight); // the FINAL 260px height, not a stale one
|
||||
Assert.Contains(prebuilt, listBox.Children.OfType<UiScrollablePanel>().Single().Children);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPrebuiltRow_SubsequentTemplateRow_StacksAfterThePrebuiltRowsFinalHeight()
|
||||
{
|
||||
ElementInfo characterListBoxInfo = Find(FixtureLoader.LoadOptionsCharacterInfos(), 0x100001FAu)!;
|
||||
var listBox = new UiTemplateListBox(
|
||||
characterListBoxInfo, NoTex,
|
||||
characterListBoxInfo.TemplateList, characterListBoxInfo.ScrollbarElementId)
|
||||
{
|
||||
TemplateResolver = (layoutId, elementId) =>
|
||||
{
|
||||
if (layoutId != 0x2100002Bu) return null;
|
||||
ElementInfo? templateInfo = Find(FixtureLoader.LoadOptionsPanelInfos(), elementId);
|
||||
return templateInfo is null ? null : LayoutImporter.Build(templateInfo, NoTex, null).Root;
|
||||
},
|
||||
};
|
||||
|
||||
var prebuilt = new UiPanel { Width = 272f, Height = 260f };
|
||||
listBox.AddPrebuiltRow(prebuilt);
|
||||
Assert.Equal(260, listBox.ContentHeight);
|
||||
|
||||
// A separator row added AFTER the prebuilt block must stack below its FINAL
|
||||
// 260px height, not the block's pre-build authored 100px extent — this is
|
||||
// exactly the reflow problem AddPrebuiltRow exists to avoid.
|
||||
UiElement? separator = listBox.AddItemFromTemplateList(1); // separator template
|
||||
Assert.NotNull(separator);
|
||||
Assert.Equal(260f, separator!.Top);
|
||||
}
|
||||
|
||||
// ── The template mechanism, exercised end-to-end ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue