feat(studio): FixtureProvider — sample data populates the 2-D panels

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 15:45:11 +02:00
parent 101a35cc2d
commit 0bdc866c15
4 changed files with 402 additions and 0 deletions

View file

@ -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;
/// <summary>
/// Populates a loaded panel with sample data by calling the production
/// controller Bind methods against <see cref="SampleData"/>.
///
/// <para>
/// 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.
/// </para>
///
/// <para>
/// <b>IconIds approach:</b> raw-resolve stub — resolve the base <c>iconId</c>
/// via <see cref="RenderStack.ResolveChrome"/> and return the GL handle
/// directly. This is intentionally simpler than GameWindow's full
/// <see cref="IconComposer"/> (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.
/// </para>
/// </summary>
public static class FixtureProvider
{
/// <summary>
/// Populate <paramref name="layout"/> with sample data appropriate for
/// <paramref name="layoutId"/>. Calls the production controller Bind
/// methods so the panel's widgets drive off the same code path as the
/// full game.
/// </summary>
/// <param name="layoutId">The LayoutDesc dat id used to import the layout.</param>
/// <param name="layout">The imported layout whose widgets to populate.</param>
/// <param name="stack">The live render stack (for <see cref="RenderStack.ResolveChrome"/>
/// and <see cref="RenderStack.VitalsDatFont"/>).</param>
/// <param name="objects">A <see cref="ClientObjectTable"/> already seeded by
/// <see cref="SampleData.BuildObjectTable"/>.</param>
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<PlayerDescriptionParser.ShortcutEntry>(),
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 ─────────────────────────────────────────────────────────────
/// <summary>
/// Build the <c>iconIds</c> delegate for toolbar / inventory controllers.
///
/// <para>
/// Raw-resolve stub: resolve the base <paramref name="iconId"/> (arg 2)
/// via <see cref="RenderStack.ResolveChrome"/> 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.
/// </para>
///
/// <para>This is what the task spec calls "v1 raw-resolve stub".</para>
/// </summary>
private static Func<ItemType, uint, uint, uint, uint, uint> MakeIconIds(RenderStack stack)
=> (_, iconId, _, _, _) =>
{
if (iconId == 0u) return 0u;
var (handle, _, _) = stack.ResolveChrome(iconId);
return handle;
};
}