feat(ui): port retail component book rows

Instantiate gmSpellComponentUI category and component entries from LayoutDesc 0x21000033 instead of synthesizing catalog rows. Preserve localized category titles, authored textures and geometry, live icon/count/desired bindings, selected-object synchronization, and retail's inclusive 0..5000 desired-level contract.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 17:38:24 +02:00
parent 15bb61e05d
commit a1175e6aac
13 changed files with 2395 additions and 60 deletions

View file

@ -2,6 +2,7 @@ using AcDream.App.Spells;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
using AcDream.Core.Selection;
using AcDream.Core.Spells;
using System.Numerics;
@ -238,6 +239,130 @@ public sealed class SpellbookWindowControllerTests
Assert.Empty(VisibleSpellIds(list));
}
[Fact]
public void Components_UseAuthoredCategoryAndItemTemplates_WithRetailCountsAndSelection()
{
ImportedLayout layout = FixtureLoader.LoadSpellbook();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject
{
ObjectId = 10u,
WeenieClassId = 100u,
Name = "Copper Scarab",
Type = ItemType.SpellComponents,
IconId = 0x06000100u,
StackSize = 2,
ContainerId = 1u,
IsComponentPack = true,
});
objects.AddOrUpdate(new ClientObject
{
ObjectId = 11u,
WeenieClassId = 101u,
Name = "Diamond Scarab",
Type = ItemType.SpellComponents,
IconId = 0x06000101u,
StackSize = 1,
ContainerId = 1u,
IsComponentPack = true,
});
objects.AddOrUpdate(new ClientObject
{
ObjectId = 12u,
WeenieClassId = 200u,
Name = "Amaranth",
Type = ItemType.SpellComponents,
IconId = 0x06000200u,
StackSize = 3,
ContainerId = 1u,
IsComponentPack = true,
});
var components = new Dictionary<uint, SpellComponentDescriptor>
{
[100u] = new(100u, "Copper Scarab", 0u, 0x06000100u),
[101u] = new(101u, "Diamond Scarab", 0u, 0x06000101u),
[200u] = new(200u, "Amaranth", 1u, 0x06000200u),
};
Spellbook book = CreateSpellbook();
book.SetDesiredComponent(100u, 7u);
var selected = new List<uint>();
var desired = new List<(uint Component, uint Amount)>();
var selection = new SelectionState();
using SpellbookWindowController controller = Bind(
layout,
book,
objects: objects,
components: components,
selection: selection,
selectObject: objectId =>
{
selected.Add(objectId);
selection.Select(objectId, SelectionChangeSource.Inventory);
},
setDesiredComponent: (component, amount) => desired.Add((component, amount)),
resolveComponentIcon: icon => icon + 0x1000u)!;
controller.ShowPage(SpellbookWindowPage.Components);
UiItemList list = Assert.IsType<UiItemList>(
layout.FindElement(SpellbookWindowController.ComponentListId)?.Children.Single());
Assert.Equal(5, list.GetNumUIItems());
Assert.Same(
list.Scroll,
Assert.IsType<UiScrollbar>(
layout.FindElement(SpellbookWindowController.ComponentScrollbarId)).Model);
UiTemplateListSlot scarabs = Assert.IsType<UiTemplateListSlot>(list.GetItem(0));
UiText scarabTitle = Assert.IsType<UiText>(scarabs.Content.Root);
Assert.Equal("SCARABS", Assert.Single(scarabTitle.LinesProvider()).Text);
Assert.Equal(0x06001392u, scarabTitle.BackgroundSprite);
UiTemplateListSlot copper = Assert.IsType<UiTemplateListSlot>(list.GetItem(1));
UiDatElement copperRoot = Assert.IsType<UiDatElement>(copper.Content.Root);
Assert.Equal((0x06005E21u, 1), copperRoot.ActiveMedia());
Assert.Equal("Copper Scarab", Assert.Single(
Assert.IsType<UiText>(copper.Content.FindElement(
ComponentBookTemplateFactory.NameId)).LinesProvider()).Text);
Assert.Equal("2", Assert.Single(
Assert.IsType<UiText>(copper.Content.FindElement(
ComponentBookTemplateFactory.OwnedCountId)).LinesProvider()).Text);
UiField desiredField = Assert.IsType<UiField>(copper.Content.FindElement(
ComponentBookTemplateFactory.DesiredCountId));
Assert.Equal((224f, 15f, 60f, 15f),
(desiredField.Left, desiredField.Top, desiredField.Width, desiredField.Height));
Assert.True(desiredField.RightAligned);
Assert.Equal("7", desiredField.Text);
UiElement iconHost = Assert.IsAssignableFrom<UiElement>(copper.Content.FindElement(
ComponentBookTemplateFactory.IconId));
Assert.Equal(
0x06001100u,
Assert.IsType<UiTextureElement>(Assert.Single(iconHost.Children)).Texture);
copper.OnEvent(new UiEvent(0u, copper, UiEventType.Click));
Assert.Equal([10u], selected);
Assert.True(copper.Selected);
Assert.Equal(UiButtonStateMachine.Highlight, copperRoot.ActiveRetailStateId);
Assert.Equal((0x06005E22u, 1), copperRoot.ActiveMedia());
desiredField.SetText("21");
desiredField.OnSubmit!(desiredField.Text);
Assert.Equal([(100u, 21u)], desired);
desiredField.SetText("5001");
desiredField.OnSubmit!(desiredField.Text);
Assert.Equal("21", desiredField.Text);
Assert.Equal([(100u, 21u)], desired);
UiTemplateListSlot herbs = Assert.IsType<UiTemplateListSlot>(list.GetItem(3));
Assert.Equal("HERBS", Assert.Single(
Assert.IsType<UiText>(herbs.Content.Root).LinesProvider()).Text);
UiTemplateListSlot amaranth = Assert.IsType<UiTemplateListSlot>(list.GetItem(4));
selection.Select(12u, SelectionChangeSource.Radar);
Assert.True(amaranth.Selected);
Assert.False(copper.Selected);
selection.Select(0xDEADBEEFu, SelectionChangeSource.World);
Assert.False(amaranth.Selected);
}
private static SpellbookWindowController? Bind(
ImportedLayout layout,
Spellbook spellbook,
@ -245,23 +370,35 @@ public sealed class SpellbookWindowControllerTests
Action<uint>? sendFilter = null,
Action<uint>? removeSpell = null,
Action<string, Action<bool>>? showConfirmation = null,
Action? close = null)
Action? close = null,
ClientObjectTable? objects = null,
IReadOnlyDictionary<uint, SpellComponentDescriptor>? components = null,
SelectionState? selection = null,
Action<uint>? selectObject = null,
Action<uint, uint>? setDesiredComponent = null,
Func<uint, uint>? resolveComponentIcon = null)
=> SpellbookWindowController.Bind(
layout,
spellbook,
new ClientObjectTable(),
objects ?? new ClientObjectTable(),
() => 1u,
new Dictionary<uint, SpellComponentDescriptor>(),
components ?? new Dictionary<uint, SpellComponentDescriptor>(),
selection ?? new SelectionState(),
spellId => spellId + 1000u,
iconId => iconId,
resolveComponentIcon ?? (iconId => iconId),
spellId => spellId == 103u ? 8 : 1,
_ => { },
selectObject ?? (_ => { }),
addFavorite ?? (_ => { }),
sendFilter ?? (_ => { }),
removeSpell ?? (_ => { }),
showConfirmation ?? ((_, _) => { }),
(_, _) => { },
setDesiredComponent ?? ((_, _) => { }),
close ?? (() => { }),
new ComponentBookTemplateFactory(
FixtureLoader.LoadComponentCategoryTemplateInfos(),
FixtureLoader.LoadComponentRowTemplateInfos(),
_ => (0u, 0, 0),
defaultFont: null),
RowStyle,
rowFont: null);