feat(ui): finish retail toolbar controls

Discover all seven panel launchers from their DAT panel-id attributes, route mounted panels through event-driven retained window state, and ghost unavailable panels. Port Use and Examine selection/target behavior with exact Appraise dispatch and retail cursor modes.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 12:11:53 +02:00
parent 21fefce0e0
commit d3d1c895a0
19 changed files with 478 additions and 72 deletions

View file

@ -20,6 +20,18 @@ public class ToolbarControllerTests
private static readonly uint[] CombatIds = { 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
private const uint CharacterButtonId = 0x10000199u;
private const uint InventoryButtonId = 0x100001B1u;
private const uint UseButtonId = 0x1000019Du;
private const uint ExamineButtonId = 0x100001A5u;
private static readonly (uint ElementId, uint PanelId)[] PanelButtons =
{
(0x10000197u, 12u),
(0x10000198u, 13u),
(CharacterButtonId, RetailPanelCatalog.Character),
(0x1000055Au, 25u),
(0x1000019Au, 16u),
(0x1000019Bu, 10u),
(InventoryButtonId, RetailPanelCatalog.Inventory),
};
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots,
Dictionary<uint, UiElement> indicators) FakeToolbar()
@ -37,8 +49,10 @@ public class ToolbarControllerTests
AddButton(id);
indicators[id] = dict[id];
}
AddButton(CharacterButtonId);
AddButton(InventoryButtonId);
foreach (var (elementId, panelId) in PanelButtons)
AddButton(elementId, panelId);
AddButton(UseButtonId);
AddButton(ExamineButtonId);
return (new ImportedLayout(root, dict), slots, indicators);
void AddSlot(uint id)
@ -47,7 +61,7 @@ public class ToolbarControllerTests
dict[id] = list; slots[id] = list; root.AddChild(list);
}
void AddButton(uint id)
void AddButton(uint id, uint? panelId = null)
{
var info = new ElementInfo
{
@ -59,6 +73,25 @@ public class ToolbarControllerTests
};
info.StateMedia["Normal"] = (0x1u, 1);
info.StateMedia["Highlight"] = (0x2u, 1);
info.StateMedia["Ghosted"] = (0x3u, 1);
if (panelId is { } value)
{
info.States[UiStateInfo.DirectStateId] = new UiStateInfo
{
Id = UiStateInfo.DirectStateId,
Properties = new UiPropertyBag
{
Values =
{
[0x10000029u] = new UiPropertyValue
{
Kind = UiPropertyKind.Enum,
UnsignedValue = value,
},
},
},
};
}
var button = new UiButton(info, _ => (0u, 0, 0)) { Width = 32, Height = 32 };
dict[id] = button;
root.AddChild(button);
@ -140,25 +173,30 @@ public class ToolbarControllerTests
}
[Fact]
public void WindowToggleButtons_clickCallbacks_fire()
public void PanelButtons_bindByDatPanelId_andGhostUnavailablePanels()
{
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
int inventoryClicks = 0;
int characterClicks = 0;
var toggles = new List<uint>();
var ctrl = ToolbarController.Bind(layout, repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
ctrl.BindWindowToggles(
toggleInventory: () => inventoryClicks++,
toggleCharacter: () => characterClicks++);
ctrl.BindPanelButtons(
panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character,
toggles.Add);
((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
((UiButton)layout.FindElement(CharacterButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
Assert.Equal(1, inventoryClicks);
Assert.Equal(1, characterClicks);
Assert.Equal(new[] { RetailPanelCatalog.Inventory, RetailPanelCatalog.Character }, toggles);
foreach (var (elementId, panelId) in PanelButtons)
{
var button = (UiButton)layout.FindElement(elementId)!;
Assert.Equal(
panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character,
button.Enabled);
}
}
[Fact]
@ -195,9 +233,9 @@ public class ToolbarControllerTests
iconIds: (_,_,_,_,_) => 0u,
useItem: _ => { },
itemInteraction: interaction);
ctrl.BindWindowToggles(
toggleInventory: () => inventoryClicks++,
toggleCharacter: () => { });
ctrl.BindPanelButtons(
panelId => panelId == RetailPanelCatalog.Inventory,
_ => inventoryClicks++);
interaction.ActivateItem(kit);
((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
@ -271,19 +309,82 @@ public class ToolbarControllerTests
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
ctrl.SetInventoryOpen(true);
ctrl.SetCharacterOpen(true);
ctrl.BindPanelButtons(_ => true, _ => { });
ctrl.SetPanelOpen(RetailPanelCatalog.Inventory, true);
ctrl.SetPanelOpen(RetailPanelCatalog.Character, true);
Assert.Equal("Highlight", inventoryButton.ActiveState);
Assert.Equal("Highlight", characterButton.ActiveState);
ctrl.SetInventoryOpen(false);
ctrl.SetCharacterOpen(false);
ctrl.SetPanelOpen(RetailPanelCatalog.Inventory, false);
ctrl.SetPanelOpen(RetailPanelCatalog.Character, false);
Assert.Equal("Normal", inventoryButton.ActiveState);
Assert.Equal("Normal", characterButton.ActiveState);
}
[Fact]
public void RetailFixture_panelButtonsExposeExactDatPanelIds()
{
ImportedLayout layout = FixtureLoader.LoadToolbar();
foreach (var (elementId, panelId) in PanelButtons)
{
var button = Assert.IsType<UiButton>(layout.FindElement(elementId));
Assert.True(button.TryGetEnumAttribute(0x10000029u, out uint actual));
Assert.Equal(panelId, actual);
}
}
[Fact]
public void UseAndExamineButtons_dispatchSelectedObjectPolicy()
{
const uint player = 0x50000001u;
const uint pack = 0x50000002u;
const uint item = 0x50000003u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container });
repo.MoveItem(pack, player, 0);
repo.AddOrUpdate(new ClientObject
{
ObjectId = item,
Type = ItemType.Misc,
Useability = ItemUseability.Contained,
});
repo.MoveItem(item, pack, 0);
var uses = new List<uint>();
var examines = new List<uint>();
uint selected = item;
var interaction = new ItemInteractionController(
repo,
() => player,
sendUse: uses.Add,
sendUseWithTarget: null,
sendWield: null,
sendDrop: null,
sendExamine: examines.Add);
ToolbarController.Bind(
layout,
repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: (_, _, _, _, _) => 0u,
useItem: _ => { },
itemInteraction: interaction,
selectedObjectId: () => selected);
((UiButton)layout.FindElement(UseButtonId)!).OnClick!.Invoke();
((UiButton)layout.FindElement(ExamineButtonId)!).OnClick!.Invoke();
Assert.Equal(new[] { item }, uses);
Assert.Equal(new[] { item }, examines);
selected = 0;
((UiButton)layout.FindElement(ExamineButtonId)!).OnClick!.Invoke();
Assert.Equal(InteractionModeKind.Examine, interaction.InteractionState.Current.Kind);
}
// ── C1: combat-mode indicator tests ─────────────────────────────────────
[Fact]