diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 310e2239..1f0e29ab 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -121,38 +121,19 @@ public sealed class InventoryController } /// Retail gm*UI display refresh: partition the player's direct contents into loose - /// items (the "Contents of Backpack" grid) and side bags (the pack-selector list). - /// Uses the container index when available (items ingested via Ingest/MoveItem) or - /// falls back to a full scan — both paths are correct for the two production flows. + /// items (the "Contents of Backpack" grid) and side bags (the pack-selector list). The + /// container index () IS retail's per-container + /// item list — items enter it via the CreateObject (Ingest) / server-move (MoveItem) flows + /// that call Reindex, slot-sorted. public void Populate() { uint p = _playerGuid(); - - // GetContents returns indexed + slot-sorted items (the production path via Ingest). - // For AddOrUpdate-seeded items (tests + initial populate before indexing), we fall - // back to scanning all objects with ContainerId == p, sorted by slot. - var indexed = _objects.GetContents(p); - IEnumerable guids; - if (indexed.Count > 0) - { - guids = indexed; - } - else - { - var direct = new List<(int slot, uint id)>(); - foreach (var o in _objects.Objects) - if (o.ContainerId == p) - direct.Add((o.ContainerSlot, o.ObjectId)); - direct.Sort((a, b) => a.slot.CompareTo(b.slot)); - var ids = new List(direct.Count); - foreach (var (_, id) in direct) ids.Add(id); - guids = ids; - } + var contents = _objects.GetContents(p); _contentsGrid?.Flush(); _containerList?.Flush(); - foreach (var guid in guids) + foreach (var guid in contents) { var item = _objects.Get(guid); if (item is null) continue; diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 99d5b34b..14d6053e 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -54,17 +54,27 @@ public class InventoryControllerTests 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(); - objects.AddOrUpdate(new ClientObject { ObjectId = 0xA, ContainerId = Player, - ContainerSlot = 0, Burden = 10 }); // loose - objects.AddOrUpdate(new ClientObject { ObjectId = 0xB, ContainerId = Player, - ContainerSlot = 1, Burden = 20 }); // loose - objects.AddOrUpdate(new ClientObject { ObjectId = 0xC, ContainerId = Player, - ContainerSlot = 2, Type = ItemType.Container }); // side bag + // 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);