using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; using Xunit; namespace AcDream.App.Tests.UI.Layout; public class InventoryControllerTests { private const uint Player = 0x50000001u; // UiElement is abstract — a concrete, parameterless stand-in for the root + caption hosts // (the real captions are Type-0 → UiDatElement; in tests we only need a host that holds children). private sealed class TestElement : UiElement { } // Element ids (spec §1). private const uint ContentsGrid = 0x100001C6u; private const uint ContainerList = 0x100001CAu; private const uint TopContainer = 0x100001C9u; private const uint BurdenMeter = 0x100001D9u; private const uint BurdenText = 0x100001D8u; private const uint BurdenCaption = 0x100001D7u; private const uint ContentsCaption = 0x100001C5u; private static (ImportedLayout layout, UiItemList grid, UiItemList containers, UiItemList top, UiMeter meter, UiElement burdenText, UiElement burdenCap, UiElement contentsCap) BuildLayout() { var grid = new UiItemList { Width = 192, Height = 96 }; var containers = new UiItemList { Width = 36, Height = 252 }; var top = new UiItemList { Width = 36, Height = 36 }; var meter = new UiMeter { Width = 11, Height = 58 }; var burdenText = new TestElement { Width = 36, Height = 15 }; var burdenCap = new TestElement { Width = 36, Height = 15 }; var contentsCap = new TestElement { Width = 192, Height = 15 }; var root = new TestElement { Width = 300, Height = 362 }; root.AddChild(grid); root.AddChild(containers); root.AddChild(top); root.AddChild(meter); root.AddChild(burdenText); root.AddChild(burdenCap); root.AddChild(contentsCap); var byId = new Dictionary { [ContentsGrid] = grid, [ContainerList] = containers, [TopContainer] = top, [BurdenMeter] = meter, [BurdenText] = burdenText, [BurdenCaption] = burdenCap, [ContentsCaption] = contentsCap, }; return (new ImportedLayout(root, byId), grid, containers, top, meter, burdenText, burdenCap, contentsCap); } private static InventoryController Bind(ImportedLayout layout, ClientObjectTable objects, int? strength = 100) => InventoryController.Bind(layout, objects, () => Player, iconIds: (_, _, _, _, _) => 0u, // no real GL upload in tests strength: () => strength, datFont: null); // Place an object in a container at a slot via the faithful production path: // AddOrUpdate registers it, MoveItem sets ContainerId+ContainerSlot and calls Reindex // (the same machinery server-driven moves use), so GetContents returns it slot-ordered. private static void SeedContained(ClientObjectTable t, uint guid, uint container, int slot, int burden = 0, ItemType type = ItemType.None) { t.AddOrUpdate(new ClientObject { ObjectId = guid, Burden = burden, Type = type }); t.MoveItem(guid, container, slot); } [Fact] public void Populate_fills_contents_grid_with_loose_items_only() { var (layout, grid, containers, _, _, _, _, _) = BuildLayout(); var objects = new ClientObjectTable(); // Seed via the indexed path: AddOrUpdate registers the object, MoveItem places it in // the container at a slot (the faithful "server moved item into container" flow that // calls Reindex). GetContents — retail's per-container list — then returns them ordered. SeedContained(objects, 0xA, Player, slot: 0, burden: 10); // loose SeedContained(objects, 0xB, Player, slot: 1, burden: 20); // loose SeedContained(objects, 0xC, Player, slot: 2, type: ItemType.Container); // side bag Bind(layout, objects); Assert.Equal(2, grid.GetNumUIItems()); // 2 loose Assert.Equal(0xAu, grid.GetItem(0)!.ItemId); Assert.Equal(0xBu, grid.GetItem(1)!.ItemId); Assert.Equal(1, containers.GetNumUIItems()); // 1 side bag Assert.Equal(0xCu, containers.GetItem(0)!.ItemId); } [Fact] public void Equipped_items_are_excluded_from_the_grid_and_selector() { var (layout, grid, containers, _, _, _, _, _) = BuildLayout(); var objects = new ClientObjectTable(); SeedContained(objects, 0xA, Player, slot: 0); // loose pack item // A self-wielded item: MoveItem with an equip location indexes it under the player // (the live wield path), but it must NOT show in the pack grid or the selector. objects.AddOrUpdate(new ClientObject { ObjectId = 0xD }); objects.MoveItem(0xD, Player, newSlot: 1, newEquipLocation: EquipMask.MeleeWeapon); Bind(layout, objects); Assert.Equal(1, grid.GetNumUIItems()); // only the loose item Assert.Equal(0xAu, grid.GetItem(0)!.ItemId); Assert.Equal(0, containers.GetNumUIItems()); // equipped item is not a side bag either } [Fact] public void Grid_is_six_columns_thirtytwo_px() { var (layout, grid, _, _, _, _, _, _) = BuildLayout(); Bind(layout, new ClientObjectTable()); Assert.Equal(6, grid.Columns); Assert.Equal(32f, grid.CellWidth); Assert.Equal(32f, grid.CellHeight); } [Fact] public void ObjectAdded_for_player_item_rebuilds_grid() { var (layout, grid, _, _, _, _, _, _) = BuildLayout(); var objects = new ClientObjectTable(); Bind(layout, objects); Assert.Equal(0, grid.GetNumUIItems()); objects.Ingest(new WeenieData(0xA, "Sword", ItemType.MeleeWeapon, 1, 0, 0, 0, 0, null, null, null, 5, Player, null, null, null, null, null, null, null, null, null)); Assert.Equal(1, grid.GetNumUIItems()); Assert.Equal(0xAu, grid.GetItem(0)!.ItemId); } [Fact] public void Burden_meter_fill_and_percent_from_load() { var (layout, _, _, _, meter, burdenText, _, _) = BuildLayout(); var objects = new ClientObjectTable(); // Str 100 → capacity 15000. Carried 7500 → load 0.5 → fill 0.1667, "50%". objects.AddOrUpdate(new ClientObject { ObjectId = 0xA, ContainerId = Player, Burden = 7500 }); Bind(layout, objects, strength: 100); Assert.Equal(0.16667f, meter.Fill() ?? -1f, 3); Assert.True(meter.Vertical); // The % text caption child reads "50%". Assert.Contains("50%", CaptionText(burdenText)); } [Fact] public void Captions_render_known_strings() { var (layout, _, _, _, _, _, burdenCap, contentsCap) = BuildLayout(); Bind(layout, new ClientObjectTable()); Assert.Contains("Burden", CaptionText(burdenCap)); Assert.Contains("Contents of Backpack", CaptionText(contentsCap)); } // Reads the text of the UiText caption child attached by the controller. private static string CaptionText(UiElement host) { foreach (var c in host.Children) if (c is UiText t) { var lines = t.LinesProvider(); if (lines.Count > 0) return lines[0].Text; } return ""; } }