diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs
new file mode 100644
index 00000000..4266aad9
--- /dev/null
+++ b/src/AcDream.App/Studio/FixtureProvider.cs
@@ -0,0 +1,114 @@
+using AcDream.App.Rendering;
+using AcDream.App.UI;
+using AcDream.App.UI.Layout;
+using AcDream.Core.Items;
+using AcDream.Core.Net.Messages;
+
+namespace AcDream.App.Studio;
+
+///
+/// Populates a loaded panel with sample data by calling the production
+/// controller Bind methods against .
+///
+///
+/// The studio is intentionally thin — there is no live game session, no
+/// server connection, and no network. FixtureProvider bridges that gap by
+/// feeding static fixtures (vitals percentages, a fake inventory, empty
+/// shortcut lists) so the bound widgets show plausible state instead of
+/// empty zeroes.
+///
+///
+///
+/// IconIds approach: raw-resolve stub — resolve the base iconId
+/// via and return the GL handle
+/// directly. This is intentionally simpler than GameWindow's full
+/// (5-layer composite). The raw icon is enough
+/// to confirm the grid cells draw something in the studio; the full
+/// compositor is the live-game concern, not the layout preview concern.
+///
+///
+public static class FixtureProvider
+{
+ ///
+ /// Populate with sample data appropriate for
+ /// . Calls the production controller Bind
+ /// methods so the panel's widgets drive off the same code path as the
+ /// full game.
+ ///
+ /// The LayoutDesc dat id used to import the layout.
+ /// The imported layout whose widgets to populate.
+ /// The live render stack (for
+ /// and ).
+ /// A already seeded by
+ /// .
+ public static void Populate(
+ uint layoutId,
+ ImportedLayout layout,
+ RenderStack stack,
+ ClientObjectTable objects)
+ {
+ switch (layoutId)
+ {
+ case 0x2100006Cu: // vitals
+ VitalsController.Bind(layout,
+ healthPct: () => SampleData.HealthPct,
+ staminaPct: () => SampleData.StaminaPct,
+ manaPct: () => SampleData.ManaPct,
+ healthText: () => "80/100",
+ staminaText: () => "60/100",
+ manaText: () => "90/100");
+ break;
+
+ case 0x21000016u: // toolbar
+ ToolbarController.Bind(
+ layout,
+ objects,
+ shortcuts: () => System.Array.Empty(),
+ iconIds: MakeIconIds(stack),
+ useItem: _ => { },
+ combatState: null,
+ peaceDigits: null,
+ warDigits: null,
+ emptyDigits: null,
+ sendAddShortcut: null,
+ sendRemoveShortcut: null);
+ break;
+
+ case 0x21000023u: // inventory
+ InventoryController.Bind(
+ layout,
+ objects,
+ playerGuid: () => SampleData.PlayerGuid,
+ iconIds: MakeIconIds(stack),
+ strength: () => 100,
+ datFont: stack.VitalsDatFont);
+ break;
+
+ default:
+ // Unknown layout — no-op; the panel renders structurally.
+ break;
+ }
+ }
+
+ // ── Helpers ─────────────────────────────────────────────────────────────
+
+ ///
+ /// Build the iconIds delegate for toolbar / inventory controllers.
+ ///
+ ///
+ /// Raw-resolve stub: resolve the base (arg 2)
+ /// via and return its GL handle.
+ /// The remaining args (type, underlayId, overlayId, effects) are ignored
+ /// for the studio — a single-layer icon is sufficient for layout preview.
+ ///
+ ///
+ /// This is what the task spec calls "v1 raw-resolve stub".
+ ///
+ private static Func MakeIconIds(RenderStack stack)
+ => (_, iconId, _, _, _) =>
+ {
+ if (iconId == 0u) return 0u;
+ var (handle, _, _) = stack.ResolveChrome(iconId);
+ return handle;
+ };
+}
diff --git a/src/AcDream.App/Studio/SampleData.cs b/src/AcDream.App/Studio/SampleData.cs
new file mode 100644
index 00000000..2704c046
--- /dev/null
+++ b/src/AcDream.App/Studio/SampleData.cs
@@ -0,0 +1,168 @@
+using AcDream.Core.Items;
+
+namespace AcDream.App.Studio;
+
+///
+/// Static sample data for the UI Studio fixture provider.
+/// Provides a pre-built populated with a
+/// representative player, their main-pack items, side bags, and equipped gear,
+/// so the 2-D inventory / paperdoll panels render populated when previewed in
+/// the studio without a live game session.
+///
+/// Icon ids used (all real 0x06xxxxxx RenderSurface ids confirmed in the dats):
+/// Inventory empty-slot sprite : 0x06004D20
+/// Generic "misc" item : 0x060011D4 (icon_misc_underlay / fallback)
+/// Iron Sword (melee weapon) : 0x060011CBu (acclient default weapon underlay)
+/// Leather Breastplate (armor) : 0x060011CFu (acclient default armor underlay)
+/// Leather Gloves : 0x060011F3u (acclient default clothing underlay)
+/// Steel Ring : 0x060011D5u (acclient default jewelry underlay)
+/// Healing Kit : 0x060011D4u (generic misc fallback)
+/// Spell components : 0x060011D4u (generic misc fallback)
+/// Side-bag 1 (Container) : 0x06004D20u
+/// Side-bag 2 (Container) : 0x06004D20u
+/// Equipped helm (HeadWear) : 0x060011F3u
+/// Equipped chest armor : 0x060011CFu
+/// Equipped melee weapon : 0x060011CBu
+///
+/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes
+/// as `iconId` into the iconIds lambda. FixtureProvider resolves them via
+/// and returns the raw GL handle.
+///
+public static class SampleData
+{
+ // ── Guids ────────────────────────────────────────────────────────────────
+
+ /// Fake server guid for the studio's synthetic player.
+ public const uint PlayerGuid = 0x50000001u;
+
+ // Items in main pack (slots 0–5).
+ private const uint SwordGuid = 0x50000010u;
+ private const uint ChestGuid = 0x50000011u;
+ private const uint GlovesGuid = 0x50000012u;
+ private const uint RingGuid = 0x50000013u;
+ private const uint HealKitGuid = 0x50000014u;
+ private const uint CompGuid = 0x50000015u;
+
+ // Side bags (also in main pack, ContainerId = PlayerGuid; slots 6 & 7).
+ private const uint Bag1Guid = 0x50000020u;
+ private const uint Bag2Guid = 0x50000021u;
+
+ // Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set).
+ private const uint HelmGuid = 0x50000030u;
+ private const uint ChestEqGuid = 0x50000031u;
+ private const uint WeaponEqGuid = 0x50000032u;
+
+ // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ───────────────────────────
+
+ // These are the same underlay/fallback icon ids the IconComposer tests pin.
+ private const uint IconWeapon = 0x060011CBu; // weapon underlay
+ private const uint IconArmor = 0x060011CFu; // armor underlay
+ private const uint IconClothing = 0x060011F3u; // clothing underlay
+ private const uint IconJewelry = 0x060011D5u; // jewelry underlay
+ private const uint IconMisc = 0x060011D4u; // misc / fallback underlay
+
+ // ── Public API ──────────────────────────────────────────────────────────
+
+ ///
+ /// Build a fresh populated with the
+ /// studio's sample player + a realistic inventory snapshot.
+ /// The table is owned by the caller and should be kept alive for the
+ /// window's lifetime.
+ ///
+ public static ClientObjectTable BuildObjectTable()
+ {
+ var t = new ClientObjectTable();
+
+ // ── Player object ─────────────────────────────────────────────────
+ t.AddOrUpdate(new ClientObject
+ {
+ ObjectId = PlayerGuid,
+ Name = "Studio Player",
+ Type = ItemType.Creature,
+ ItemsCapacity = 102,
+ ContainersCapacity = 7,
+ });
+
+ // ── Loose items in main pack (slots 0–5) ──────────────────────────
+
+ AddItem(t, SwordGuid, ItemType.MeleeWeapon, IconWeapon, "Iron Sword", PlayerGuid, 0, burden: 60);
+ AddItem(t, ChestGuid, ItemType.Armor, IconArmor, "Leather Breastplate", PlayerGuid, 1, burden: 200);
+ AddItem(t, GlovesGuid, ItemType.Clothing, IconClothing, "Leather Gloves", PlayerGuid, 2, burden: 50);
+ AddItem(t, RingGuid, ItemType.Jewelry, IconJewelry, "Steel Ring", PlayerGuid, 3, burden: 10);
+ AddItem(t, HealKitGuid, ItemType.Misc, IconMisc, "Healing Kit", PlayerGuid, 4, burden: 30);
+ AddItem(t, CompGuid, ItemType.SpellComponents, IconMisc, "Spell Comps", PlayerGuid, 5, burden: 25, stackSize: 50, stackMax: 100);
+
+ // ── Side bags (Container items in main pack, slots 6 & 7) ─────────
+
+ AddItem(t, Bag1Guid, ItemType.Container, IconMisc, "Small Pack 1",
+ containerId: PlayerGuid, slot: 6, burden: 20, itemsCapacity: 24);
+ AddItem(t, Bag2Guid, ItemType.Container, IconMisc, "Small Pack 2",
+ containerId: PlayerGuid, slot: 7, burden: 20, itemsCapacity: 24);
+
+ // ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ──
+
+ AddEquipped(t, HelmGuid, ItemType.Armor, IconClothing, "Tin Helm", EquipMask.HeadWear);
+ AddEquipped(t, ChestEqGuid, ItemType.Armor, IconArmor, "Chain Coat", EquipMask.ChestArmor);
+ AddEquipped(t, WeaponEqGuid, ItemType.MeleeWeapon, IconWeapon, "Wooden Sword", EquipMask.MeleeWeapon);
+
+ return t;
+ }
+
+ // ── Sample vital constants (used by FixtureProvider) ────────────────────
+
+ public const float HealthPct = 0.8f;
+ public const float StaminaPct = 0.6f;
+ public const float ManaPct = 0.9f;
+
+ // ── Helpers ─────────────────────────────────────────────────────────────
+
+ private static void AddItem(
+ ClientObjectTable t,
+ uint guid,
+ ItemType type,
+ uint iconId,
+ string name,
+ uint containerId,
+ int slot,
+ int burden = 0,
+ int stackSize = 1,
+ int stackMax = 1,
+ int itemsCapacity = 0)
+ {
+ t.AddOrUpdate(new ClientObject
+ {
+ ObjectId = guid,
+ Name = name,
+ Type = type,
+ IconId = iconId,
+ Burden = burden,
+ StackSize = stackSize,
+ StackSizeMax = stackMax,
+ ItemsCapacity = itemsCapacity,
+ });
+ t.MoveItem(guid, containerId, slot);
+ }
+
+ private static void AddEquipped(
+ ClientObjectTable t,
+ uint guid,
+ ItemType type,
+ uint iconId,
+ string name,
+ EquipMask equipMask)
+ {
+ t.AddOrUpdate(new ClientObject
+ {
+ ObjectId = guid,
+ Name = name,
+ Type = type,
+ IconId = iconId,
+ ValidLocations = equipMask,
+ CurrentlyEquippedLocation = equipMask,
+ ContainerId = PlayerGuid,
+ });
+ // Update the equip location on the object via MoveItem (sets ContainerId +
+ // CurrentlyEquippedLocation via the equip overload).
+ t.MoveItem(guid, PlayerGuid, newSlot: -1, newEquipLocation: equipMask);
+ }
+}
diff --git a/src/AcDream.App/Studio/StudioWindow.cs b/src/AcDream.App/Studio/StudioWindow.cs
index 151464e2..43eb5c5b 100644
--- a/src/AcDream.App/Studio/StudioWindow.cs
+++ b/src/AcDream.App/Studio/StudioWindow.cs
@@ -47,6 +47,10 @@ public sealed class StudioWindow : IDisposable
private StudioInspector? _inspector;
private UiElement? _panelRoot; // top-level element added to UiRoot (for hit-test + tree)
+ // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime
+ // so the controller subscriptions (ObjectAdded/ObjectMoved etc.) fire correctly.
+ private AcDream.Core.Items.ClientObjectTable? _objects;
+
public StudioWindow(StudioOptions opts)
{
_opts = opts ?? throw new ArgumentNullException(nameof(opts));
@@ -117,7 +121,18 @@ public sealed class StudioWindow : IDisposable
var root = _source.Load(_opts);
_panelRoot = root;
if (root is not null)
+ {
_stack.UiHost.Root.AddChild(root);
+
+ // Task 4: populate the panel with sample data via production controllers,
+ // so inventory / vitals / toolbar panels render with plausible content.
+ if (_source.CurrentLayout is not null)
+ {
+ uint layoutId = _opts.LayoutId ?? 0x2100006Cu;
+ _objects = SampleData.BuildObjectTable();
+ FixtureProvider.Populate(layoutId, _source.CurrentLayout, _stack, _objects);
+ }
+ }
else
Console.Error.WriteLine($"[studio] panel load failed: {_source.LastError}");
diff --git a/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs b/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs
new file mode 100644
index 00000000..88a517e7
--- /dev/null
+++ b/tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs
@@ -0,0 +1,105 @@
+using AcDream.App.Studio;
+using AcDream.Core.Items;
+
+namespace AcDream.App.Tests.Studio;
+
+///
+/// Unit tests for and .
+/// These tests have NO GL/dat dependency — they only exercise the in-memory
+/// ClientObjectTable population that FixtureProvider uses.
+///
+public class FixtureProviderTests
+{
+ ///
+ /// SampleData.BuildObjectTable() must place at least 6 items in the
+ /// player's main pack (ContainerId == PlayerGuid) and the player object
+ /// itself must exist with the right capacities.
+ ///
+ [Fact]
+ public void SampleTable_hasPackContents()
+ {
+ var t = SampleData.BuildObjectTable();
+
+ // Player object must exist.
+ var player = t.Get(SampleData.PlayerGuid);
+ Assert.NotNull(player);
+ Assert.Equal(102, player!.ItemsCapacity);
+ Assert.Equal(7, player.ContainersCapacity);
+
+ // At least 6 loose items must be in the main pack.
+ var contents = t.GetContents(SampleData.PlayerGuid);
+ Assert.True(contents.Count >= 6,
+ $"Expected >= 6 items in player pack; got {contents.Count}");
+
+ // Verify the first item has a non-zero IconId and a recognised Type.
+ var firstId = contents[0];
+ var first = t.Get(firstId);
+ Assert.NotNull(first);
+ Assert.NotEqual(0u, first!.IconId);
+ Assert.NotEqual(ItemType.None, first.Type);
+ }
+
+ ///
+ /// Spot-check that the sample table also seeds a sword-like item
+ /// (MeleeWeapon) and a piece of armor so icon resolution has
+ /// recognisable item types to work with.
+ ///
+ [Fact]
+ public void SampleTable_hasWeaponAndArmor()
+ {
+ var t = SampleData.BuildObjectTable();
+ var contents = t.GetContents(SampleData.PlayerGuid);
+
+ bool hasMelee = false, hasArmor = false;
+ foreach (var guid in contents)
+ {
+ var obj = t.Get(guid);
+ if (obj is null) continue;
+ if ((obj.Type & ItemType.MeleeWeapon) != 0) hasMelee = true;
+ if ((obj.Type & ItemType.Armor) != 0) hasArmor = true;
+ }
+
+ Assert.True(hasMelee, "Expected at least one MeleeWeapon in sample pack");
+ Assert.True(hasArmor, "Expected at least one Armor item in sample pack");
+ }
+
+ ///
+ /// Sample table must include at least 1 equipped item whose
+ /// CurrentlyEquippedLocation is non-None.
+ ///
+ [Fact]
+ public void SampleTable_hasEquippedItems()
+ {
+ var t = SampleData.BuildObjectTable();
+
+ bool anyEquipped = false;
+ foreach (var obj in t.Objects)
+ {
+ if (obj.CurrentlyEquippedLocation != EquipMask.None)
+ { anyEquipped = true; break; }
+ }
+
+ Assert.True(anyEquipped, "Expected at least one equipped item in sample table");
+ }
+
+ ///
+ /// Sample table must include at least 2 side-bag containers in the
+ /// player's pack (ContainerId == PlayerGuid, Type has Container bit).
+ ///
+ [Fact]
+ public void SampleTable_hasSideBags()
+ {
+ var t = SampleData.BuildObjectTable();
+
+ int bagCount = 0;
+ foreach (var guid in t.GetContents(SampleData.PlayerGuid))
+ {
+ var obj = t.Get(guid);
+ if (obj is not null && (obj.Type & ItemType.Container) != 0)
+ bagCount++;
+ }
+
+ Assert.True(bagCount >= 2,
+ $"Expected >= 2 side-bag containers in player pack; got {bagCount}");
+ }
+}