From 9ed9d8dbd9bd0476162405bbc4ddbb1916d21a2b Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 25 Jun 2026 15:59:13 +0200 Subject: [PATCH] =?UTF-8?q?fix(studio):=20Task=204=20=E2=80=94=20bind=20Pa?= =?UTF-8?q?perdollController=20+=20wire=20empty-slot=20sprites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FixtureProvider.Populate for 0x21000023 was binding only InventoryController and omitting PaperdollController, so the ~21 equip slots rendered empty even though sample equipped items existed in the table. Also, the three per-list empty-slot sprites were not being resolved from the dat (contentsEmpty / sideBagEmpty / mainPackEmpty all defaulted to 0), so the slot cells had no background art. Fixes: - FixtureProvider.Populate gains a DatCollection dats param (mirrors the GameWindow.OnLoad lookup at line 2233-2235). The 0x21000023 case now resolves all three empty sprites via ItemListCellTemplate.ResolveEmptySprite and passes them to InventoryController.Bind. - PaperdollController.Bind is now called after InventoryController in the same 0x21000023 case, matching GameWindow:2257-2265 (same layout subtree, contentsEmpty as the equip-slot placeholder, sendWield: null since there is no live session in the studio). - StudioWindow.OnLoad passes _dats! to the updated Populate signature. SampleData.AddEquipped was already correct: MoveItem(guid, PlayerGuid, -1, equipMask) at ClientObjectTable.cs:134 sets CurrentlyEquippedLocation = newEquipLocation and calls Reindex which places the item in _containerIndex[PlayerGuid]. No SampleData change needed. Tests: 2 new assertions in FixtureProviderTests — sideBags_matchInventoryControllerFilter (Type.HasFlag(Container) || ItemsCapacity > 0 filter must match both bags) and equippedItems_retainLocationAndAreInContents (equipped items have CurrentlyEquippedLocation != None AND appear in GetContents so the paperdoll controller can find them). 604 pass / 2 skip / 0 fail. Co-Authored-By: Claude Sonnet 4.6 --- src/AcDream.App/Studio/FixtureProvider.cs | 42 ++++++++++++-- src/AcDream.App/Studio/StudioWindow.cs | 2 +- .../Studio/FixtureProviderTests.cs | 55 +++++++++++++++++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs index 4266aad9..4a5f319a 100644 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ b/src/AcDream.App/Studio/FixtureProvider.cs @@ -3,6 +3,7 @@ using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; using AcDream.Core.Net.Messages; +using DatReaderWriter; namespace AcDream.App.Studio; @@ -41,11 +42,14 @@ public static class FixtureProvider /// and ). /// A already seeded by /// . + /// The live DAT collection used to resolve per-list empty-slot sprites + /// (same lookup GameWindow.OnLoad performs for the production binding). public static void Populate( uint layoutId, ImportedLayout layout, RenderStack stack, - ClientObjectTable objects) + ClientObjectTable objects, + DatCollection dats) { switch (layoutId) { @@ -74,15 +78,41 @@ public static class FixtureProvider sendRemoveShortcut: null); break; - case 0x21000023u: // inventory + case 0x21000023u: // inventory + paperdoll + { + // Resolve the per-list empty-slot art from the dat cell template, matching the + // exact lookup GameWindow.OnLoad performs (UIElement_ItemList::InternalCreateItem + // 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty). + uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u); + uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu); + uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u); + + var iconIds = MakeIconIds(stack); + InventoryController.Bind( layout, objects, - playerGuid: () => SampleData.PlayerGuid, - iconIds: MakeIconIds(stack), - strength: () => 100, - datFont: stack.VitalsDatFont); + playerGuid: () => SampleData.PlayerGuid, + iconIds: iconIds, + strength: () => 100, + datFont: stack.VitalsDatFont, + contentsEmptySprite: contentsEmpty, + sideBagEmptySprite: sideBagEmpty, + mainPackEmptySprite: mainPackEmpty); + + // Bind the paperdoll equip slots (same imported subtree as the inventory). + // Mirrors GameWindow:2257-2265: PaperdollController.Bind with contentsEmpty as + // emptySlotSprite (each slot shows the same square-frame placeholder as the grid). + PaperdollController.Bind( + layout, + objects, + playerGuid: () => SampleData.PlayerGuid, + iconIds: iconIds, + sendWield: null, // no live session in the studio + emptySlotSprite: contentsEmpty, + datFont: stack.VitalsDatFont); break; + } default: // Unknown layout — no-op; the panel renders structurally. diff --git a/src/AcDream.App/Studio/StudioWindow.cs b/src/AcDream.App/Studio/StudioWindow.cs index 43eb5c5b..4212f1e1 100644 --- a/src/AcDream.App/Studio/StudioWindow.cs +++ b/src/AcDream.App/Studio/StudioWindow.cs @@ -130,7 +130,7 @@ public sealed class StudioWindow : IDisposable { uint layoutId = _opts.LayoutId ?? 0x2100006Cu; _objects = SampleData.BuildObjectTable(); - FixtureProvider.Populate(layoutId, _source.CurrentLayout, _stack, _objects); + FixtureProvider.Populate(layoutId, _source.CurrentLayout, _stack, _objects, _dats); } } else diff --git a/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs b/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs index 88a517e7..32794876 100644 --- a/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs +++ b/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs @@ -102,4 +102,59 @@ public class FixtureProviderTests Assert.True(bagCount >= 2, $"Expected >= 2 side-bag containers in player pack; got {bagCount}"); } + + /// + /// Side bags must match the InventoryController filter: ItemType.Container OR ItemsCapacity > 0. + /// This ensures they appear in the side-bag column even if the exact Type flag changes. + /// + [Fact] + public void SampleTable_sideBags_matchInventoryControllerFilter() + { + var t = SampleData.BuildObjectTable(); + + int bagCount = 0; + foreach (var guid in t.GetContents(SampleData.PlayerGuid)) + { + var obj = t.Get(guid); + if (obj is null) continue; + // InventoryController.Populate line ~203: Type.HasFlag(Container) OR ItemsCapacity > 0 + bool isBag = obj.Type.HasFlag(ItemType.Container) || obj.ItemsCapacity > 0; + if (isBag) bagCount++; + } + + Assert.True(bagCount >= 2, + $"Expected >= 2 items matching the side-bag filter; got {bagCount}"); + } + + /// + /// Equipped items must BOTH (a) retain CurrentlyEquippedLocation != None after + /// seeding AND (b) appear in GetContents(PlayerGuid) so PaperdollController.Populate + /// can find them. These are the two conditions PaperdollController checks on every + /// equipped item (PaperdollController.Populate: ContainerId == playerGuid AND + /// CurrentlyEquippedLocation != None). + /// + [Fact] + public void SampleTable_equippedItems_retainLocationAndAreInContents() + { + var t = SampleData.BuildObjectTable(); + var contents = new System.Collections.Generic.HashSet(t.GetContents(SampleData.PlayerGuid)); + + int equippedCount = 0; + foreach (var obj in t.Objects) + { + if (obj.CurrentlyEquippedLocation == EquipMask.None) continue; + equippedCount++; + + // Must appear in GetContents so the controller can pick them up. + Assert.True(contents.Contains(obj.ObjectId), + $"Equipped item 0x{obj.ObjectId:X8} ('{obj.Name}', loc={obj.CurrentlyEquippedLocation}) " + + $"is NOT in GetContents(PlayerGuid). PaperdollController will miss it."); + + // The equip location must survive the MoveItem call. + Assert.NotEqual(EquipMask.None, obj.CurrentlyEquippedLocation); + } + + Assert.True(equippedCount >= 3, + $"Expected >= 3 equipped items in sample table; got {equippedCount}"); + } }