merge: integrate main (D.2b paperdoll/inventory UI line) into the physics/collision branch

Brings the 134 D.2b UI commits onto the physics/collision development line so
main can fast-forward. Today's #149 (BSP-less static collision) + #150 (open
doors fully passable) + the full collision/streaming/dense-town-FPS arc meet
the paperdoll/inventory work.

# Conflicts:
#	docs/ISSUES.md
#	docs/architecture/retail-divergence-register.md
This commit is contained in:
Erik 2026-06-25 12:57:46 +02:00
commit 01594b4cfd
95 changed files with 17201 additions and 167 deletions

View file

@ -663,6 +663,10 @@ public sealed class GameWindow : IDisposable
private AcDream.App.UI.Layout.ToolbarController? _toolbarController;
// Phase D.5.3a — selected-object strip controller (name, overlay state, health meter).
private AcDream.App.UI.Layout.SelectedObjectController? _selectedObjectController;
// Phase D.2b-B — inventory controller (backpack grid + pack-selector + burden meter).
private AcDream.App.UI.Layout.InventoryController? _inventoryController;
// Phase D.2b Sub-phase C — paperdoll equip-slot controller (wield drag handler).
private AcDream.App.UI.Layout.PaperdollController? _paperdollController;
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
// Phase I.2: ImGui debug panel ViewModel. Lives for as long as
@ -2059,7 +2063,9 @@ public sealed class GameWindow : IDisposable
combatState: Combat,
peaceDigits: toolbarPeaceDigits,
warDigits: toolbarWarDigits,
emptyDigits: toolbarEmptyDigits);
emptyDigits: toolbarEmptyDigits,
sendAddShortcut: (i, g) => _liveSession?.SendAddShortcut(i, g),
sendRemoveShortcut: i => _liveSession?.SendRemoveShortcut(i));
// Phase D.5.3a — selected-object strip (name, overlay state, health meter).
// Analogue of retail gmToolbarUI::HandleSelectionChanged
@ -2083,7 +2089,7 @@ public sealed class GameWindow : IDisposable
// thickness on every side, giving an outer window of 310×132.
const int toolbarBorder = AcDream.App.UI.RetailChromeSprites.Border;
float toolbarContentW = 300f, toolbarContentH = toolbarRoot.Height;
var toolbarFrame = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
var toolbarFrame = new AcDream.App.UI.UiCollapsibleFrame(ResolveChrome)
{
Left = 10, Top = 300,
Width = toolbarContentW + 2 * toolbarBorder,
@ -2096,14 +2102,59 @@ public sealed class GameWindow : IDisposable
toolbarRoot.Top = toolbarBorder;
toolbarRoot.Width = toolbarContentW;
toolbarRoot.Height = toolbarContentH;
// Anchor content to all four edges so it reflows if the frame is resized.
// Anchor content to Left|Top|Right only — drop Bottom so the dat content
// keeps its full height when the frame collapses; row 2 hides via Visible,
// not reflow. (Width is fixed so the horizontal anchors are inert but harmless.)
toolbarRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom;
// The frame is the draggable window; the content itself is not.
toolbarRoot.ClickThrough = false;
| AcDream.App.UI.AnchorEdges.Right;
// The frame is the draggable window; the content itself is not. ClickThrough so the
// content panel never CLAIMS a hit — its behavioral children (slots, indicators) are
// hit children-first, and its empty areas fall through to the frame (move). Critically,
// when collapsed the row-2 band is hidden, so below the collapsed frame the content has
// no visible/hit children and ClickThrough lets clicks fall through (no phantom
// window-drag from where row 2 used to be). Same pattern as the chat content panel.
toolbarRoot.ClickThrough = true;
toolbarRoot.Draggable = false;
toolbarRoot.Resizable = false;
toolbarFrame.AddChild(toolbarRoot);
// Collapse-to-one-row: the frame's bottom edge snaps between a one-row (row 2 hidden)
// and two-row height. CollapsedHeight is computed from the layout (just above row 2),
// so there's no magic constant. Bottom-edge only; default expanded.
// The full row-2 band (all at content-y 90, toolbar dump 0x21000016): a 6px left
// edge-piece (0x100006B6) + the 9 slots (0x100006B7..BF) + an 8px right edge-piece
// (0x100006C0). Hide ALL 11 when collapsed — hiding only the 9 slots leaves the two
// edge-pieces drawing as black pillars below the bar.
uint[] row2Ids =
{
0x100006B6u,
0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu,
0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu,
0x100006C0u,
};
var toolbarRow2 = new System.Collections.Generic.List<AcDream.App.UI.UiElement>();
float minRow2Top = float.MaxValue;
foreach (var id in row2Ids)
if (toolbarLayout.FindElement(id) is { } e2)
{
toolbarRow2.Add(e2);
if (e2.Top < minRow2Top) minRow2Top = e2.Top;
}
if (toolbarRow2.Count > 0)
{
float expandedH = toolbarContentH + 2 * toolbarBorder; // today's full height
float collapsedH = minRow2Top + 2 * toolbarBorder; // just above row 2
toolbarFrame.CollapsedHeight = collapsedH;
toolbarFrame.ExpandedHeight = expandedH;
toolbarFrame.SecondRow = toolbarRow2;
toolbarFrame.Resizable = true;
toolbarFrame.ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom; // bottom edge only
toolbarFrame.MinHeight = collapsedH;
toolbarFrame.MaxHeight = expandedH;
// Height stays expandedH (already set at construction) = default expanded.
Console.WriteLine($"[D.2b] toolbar collapse: collapsed={collapsedH:0} expanded={expandedH:0} (row2Top={minRow2Top:0}).");
}
_uiHost.Root.AddChild(toolbarFrame);
Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016).");
@ -2131,6 +2182,125 @@ public sealed class GameWindow : IDisposable
}
}
}
// Phase D.2b-B — the real inventory window from LayoutDesc 0x21000023 (gmInventoryUI),
// via the LayoutImporter. The sub-window mount pulls in the nested paperdoll/backpack/
// 3D-items panels (Type-0 leaves inheriting BaseLayoutId 0x21000024/22/21). Starts
// HIDDEN; F12 toggles it via the window manager. Position is the dat's own (X=500,Y=138).
AcDream.App.UI.Layout.ImportedLayout? invLayout;
lock (_datLock)
invLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
_dats!, 0x21000023u, ResolveChrome, vitalsDatFont);
if (invLayout is not null)
{
var inventoryRoot = invLayout.Root;
// Wrap the dat content in the universal 8-piece beveled window chrome — the
// SAME UiNineSlicePanel the vitals/chat/toolbar windows use. The gmInventoryUI
// LayoutDesc (0x21000023) is 300×362; the frame adds one border thickness on
// every side. The frame is the draggable window; the dat content sits inside it.
const int invBorder = AcDream.App.UI.RetailChromeSprites.Border;
float invContentW = inventoryRoot.Width, invContentH = inventoryRoot.Height;
var inventoryFrame = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
{
Left = inventoryRoot.Left, Top = inventoryRoot.Top, // keep the dat window position
Width = invContentW + 2 * invBorder,
Height = invContentH + 2 * invBorder,
Opacity = 1.0f,
Visible = false, // F12 toggles it
Anchors = AcDream.App.UI.AnchorEdges.None, // user-positioned
Draggable = true,
};
// Content offset by the border so it sits inside the chrome.
inventoryRoot.Left = invBorder; inventoryRoot.Top = invBorder;
inventoryRoot.Width = invContentW; inventoryRoot.Height = invContentH;
// Content fills the frame vertically (Left|Top|Bottom = fixed width + height tracks
// the frame) so a vertical resize grows the inner content.
inventoryRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Bottom;
inventoryRoot.Draggable = false;
inventoryFrame.AddChild(inventoryRoot);
// Vertical-only resize: drag the BOTTOM edge to expand and see more of the pack.
// ResizeX=false + ResizableEdges=Bottom blocks horizontal resize (user request).
// MinHeight = the dat default (no shrink below it); MaxHeight = toward full screen.
inventoryFrame.Resizable = true;
inventoryFrame.ResizeX = false;
inventoryFrame.ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom;
inventoryFrame.MinHeight = invContentH + 2 * invBorder;
inventoryFrame.MaxHeight = 560f;
// Stretch the contents region with the window: the gm3DItemsUI sub-window + its grid
// + scrollbar + the full-window backdrop grow vertically (Left|Top|Bottom); the
// paperdoll + side-bag column stay pinned at the top (Left|Top). The grid's ViewHeight
// grows → more rows + the scrollbar thumb ratio (view/content) reflects it.
void StretchV(uint id)
{
if (invLayout!.FindElement(id) is { } el)
el.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
| AcDream.App.UI.AnchorEdges.Bottom;
}
void PinTopLeft(uint id)
{
if (invLayout!.FindElement(id) is { } el)
el.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top;
}
StretchV(0x100001D0u); // full-window backdrop
StretchV(0x100001CFu); // gm3DItemsUI sub-window (contents-grid container)
StretchV(0x100001C6u); // contents grid (UiItemList)
StretchV(0x100001C7u); // contents scrollbar
PinTopLeft(0x100001CDu); // paperdoll (fixed at top)
PinTopLeft(0x100001CEu); // side-bag column (fixed at top)
_uiHost.Root.AddChild(inventoryFrame);
_uiHost.RegisterWindow(AcDream.App.UI.WindowNames.Inventory, inventoryFrame);
// Phase D.2b-B — populate the inventory from ClientObjectTable: the
// "Contents of Backpack" grid + the pack-selector strip + the burden meter +
// captions. Analogue of gmInventoryUI/gmBackpackUI/gm3DItemsUI::PostInit.
// Resolve each inventory list's empty-slot art from the dat cell template
// (retail UIElement_ItemList::InternalCreateItem 0x004e3570: attr 0x1000000e ->
// catalog 0x21000037 prototype's ItemSlot_Empty). The contents grid lives in
// gm3DItemsUI (0x21000021); the side-bag + main-pack in gmBackpackUI (0x21000022).
uint contentsEmpty, sideBagEmpty, mainPackEmpty;
lock (_datLock)
{
contentsEmpty = AcDream.App.UI.Layout.ItemListCellTemplate.ResolveEmptySprite(_dats!, 0x21000021u, 0x100001C6u);
sideBagEmpty = AcDream.App.UI.Layout.ItemListCellTemplate.ResolveEmptySprite(_dats!, 0x21000022u, 0x100001CAu);
mainPackEmpty = AcDream.App.UI.Layout.ItemListCellTemplate.ResolveEmptySprite(_dats!, 0x21000022u, 0x100001C9u);
}
Console.WriteLine($"[D.2b empty-slot] contents=0x{contentsEmpty:X8} sideBag=0x{sideBagEmpty:X8} mainPack=0x{mainPackEmpty:X8}");
_inventoryController = AcDream.App.UI.Layout.InventoryController.Bind(
invLayout, Objects,
playerGuid: () => _playerServerGuid,
iconIds: (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects),
strength: () => LocalPlayer.GetAttribute(
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength) is { } sa
? (int?)sa.Current : null,
datFont: vitalsDatFont,
contentsEmptySprite: contentsEmpty,
sideBagEmptySprite: sideBagEmpty,
mainPackEmptySprite: mainPackEmpty,
sendUse: g => _liveSession?.SendUse(g),
sendNoLongerViewing: g => _liveSession?.SendNoLongerViewingContents(g),
sendPutItemInContainer: (item, container, placement) =>
_liveSession?.SendPutItemInContainer(item, container, placement));
// Slice 1: bind the paperdoll equip slots (same imported subtree) — show equipped gear +
// wield-on-drop. Unwield is handled by InventoryController (drag an equipped item to the grid).
_paperdollController = AcDream.App.UI.Layout.PaperdollController.Bind(
invLayout, Objects,
playerGuid: () => _playerServerGuid,
iconIds: (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects),
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
// Empty equip slots show a visible frame (same square as the inventory grid) so every
// slot position is seen + usable; the live 3D character (the doll) is Slice 2.
emptySlotSprite: contentsEmpty);
Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023).");
}
else Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found.");
}
// Phase N.4+N.5 — WB rendering pipeline foundation. The modern path is
@ -2433,7 +2603,7 @@ public sealed class GameWindow : IDisposable
// D.5.4: ingest CreateObject into the object table (upsert) and wire Delete +
// UiEffects live update. Wire BEFORE EntitySpawned += OnLiveEntitySpawned so
// the table is populated before the render handler runs.
AcDream.Core.Net.ObjectTableWiring.Wire(session, Objects);
AcDream.Core.Net.ObjectTableWiring.Wire(session, Objects, () => _playerServerGuid);
_liveSession.EntitySpawned += OnLiveEntitySpawned;
_liveSession.EntityDeleted += OnLiveEntityDeleted;
_liveSession.MotionUpdated += OnLiveMotionUpdated;
@ -2531,7 +2701,8 @@ public sealed class GameWindow : IDisposable
Console.WriteLine($"player: applied server skills run={_lastSeenRunSkill} jump={_lastSeenJumpSkill}");
}
},
onShortcuts: list => Shortcuts = list);
onShortcuts: list => Shortcuts = list,
playerGuid: () => _playerServerGuid);
// Phase I.7: subscribe to CombatState events and emit
// retail-faithful "You hit X for Y damage" chat lines into
@ -11709,6 +11880,13 @@ public sealed class GameWindow : IDisposable
switch (action)
{
case AcDream.UI.Abstractions.Input.InputAction.ToggleInventoryPanel:
// Retail F12 (rebindable). Gated upstream by WantsKeyboard, so it
// does not fire while the chat input holds focus. Null _uiHost =
// retail UI off (ACDREAM_RETAIL_UI unset) → no-op.
_uiHost?.ToggleWindow(AcDream.App.UI.WindowNames.Inventory);
break;
case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleDebugPanel:
foreach (var panel in EnumerateDebugPanel())
{