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}");
+ }
}