From 0bdc866c1575356f079e23e25a27c3799d7af599 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 25 Jun 2026 15:45:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(studio):=20FixtureProvider=20=E2=80=94=20s?= =?UTF-8?q?ample=20data=20populates=20the=202-D=20panels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 4 of the UI Studio plan: adds SampleData (static ClientObjectTable builder with a synthetic player, 6 loose items, 2 side bags, and 3 equipped pieces) and FixtureProvider (switches on layoutId and calls the production controller Bind methods — VitalsController, ToolbarController, InventoryController — so the studio previews panels with plausible data instead of empty widgets). Icon ids approach: raw-resolve stub — resolves the base iconId via RenderStack.ResolveChrome and returns the GL handle directly. This is v1 (single-layer icon); the full 5-layer IconComposer composite is a live-game concern, not a layout-preview concern. StudioWindow wires FixtureProvider.Populate after _source.Load; the ClientObjectTable is stored on _objects so controller event subscriptions (ObjectAdded/ObjectMoved) remain live for the window's lifetime. 4 new FixtureProviderTests (SampleTable_hasPackContents, SampleTable_hasWeaponAndArmor, SampleTable_hasEquippedItems, SampleTable_hasSideBags) — all pass. Full App suite: 602 passed / 2 skipped (pre-existing) / 0 failed. Build green. Co-Authored-By: Claude Sonnet 4.6 --- src/AcDream.App/Studio/FixtureProvider.cs | 114 ++++++++++++ src/AcDream.App/Studio/SampleData.cs | 168 ++++++++++++++++++ src/AcDream.App/Studio/StudioWindow.cs | 15 ++ .../Studio/FixtureProviderTests.cs | 105 +++++++++++ 4 files changed, 402 insertions(+) create mode 100644 src/AcDream.App/Studio/FixtureProvider.cs create mode 100644 src/AcDream.App/Studio/SampleData.cs create mode 100644 tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs 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}"); + } +}