using System; using System.Numerics; using AcDream.Core.Items; namespace AcDream.App.UI.Layout; /// /// Binds the imported gmInventoryUI tree (LayoutDesc 0x21000023) and populates it from /// . The acdream analogue of retail /// gmInventoryUI/gmBackpackUI/gm3DItemsUI ::PostInit (named-retail decomp 176236/176596/176728). /// Read-only: no container switching / drag / wield-drop wire (later sub-phases). /// public sealed class InventoryController { // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds). public const uint ContentsGridId = 0x100001C6u; // gm3DItemsUI m_itemList ("Contents of Backpack") public const uint ContainerListId = 0x100001CAu; // gmBackpackUI m_containerList (side-bag selector) public const uint TopContainerId = 0x100001C9u; // gmBackpackUI m_topContainer (main-pack cell) public const uint BurdenMeterId = 0x100001D9u; // gmBackpackUI m_burdenMeter (vertical) public const uint BurdenTextId = 0x100001D8u; // gmBackpackUI m_burdenText ("%d%%") public const uint BurdenCaptionId = 0x100001D7u; // "Burden" public const uint ContentsCaptionId = 0x100001C5u; // "Contents of Backpack" // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037). private const int ContentsColumns = 6; private const float CellPx = 32f; private readonly ClientObjectTable _objects; private readonly Func _playerGuid; private readonly Func _iconIds; private readonly Func _strength; private readonly UiItemList? _contentsGrid; private readonly UiItemList? _containerList; private readonly UiItemList? _topContainer; private readonly UiMeter? _burdenMeter; private float _burdenFill; private int _burdenPercent; private static readonly Vector4 CaptionColor = new(1f, 1f, 1f, 1f); // ACE PropertyInt ids read by retail InqLoad (decomp 0x0058f130: InqInt(5)/InqInt(0xe6)). private const uint EncumbranceValProperty = 5u; // total carried burden private const uint EncumbranceAugProperty = 0xE6u; // carry-capacity augmentation private InventoryController( ImportedLayout layout, ClientObjectTable objects, Func playerGuid, Func iconIds, Func strength, UiDatFont? datFont) { _objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _strength = strength; _contentsGrid = layout.FindElement(ContentsGridId) as UiItemList; _containerList = layout.FindElement(ContainerListId) as UiItemList; _topContainer = layout.FindElement(TopContainerId) as UiItemList; if (_contentsGrid is not null) { _contentsGrid.Columns = ContentsColumns; _contentsGrid.CellWidth = CellPx; _contentsGrid.CellHeight = CellPx; } if (_containerList is not null) { _containerList.Columns = 1; _containerList.CellWidth = CellPx; _containerList.CellHeight = CellPx; } // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). _burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter; if (_burdenMeter is not null) { _burdenMeter.Vertical = true; // 11x58 vertical bar _burdenMeter.FillFromBottom = true; // retail direction 4 (visual-confirmed) _burdenMeter.Fill = () => _burdenFill; } // Captions: drive each host UiText directly with the known string (the caption // elements resolve to UiText). "Contents of Backpack" + "%d%%" are procedural in retail // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.) AttachCaption(layout.FindElement(BurdenCaptionId), () => "Burden", datFont); AttachCaption(layout.FindElement(ContentsCaptionId), () => "Contents of Backpack", datFont); AttachCaption(layout.FindElement(BurdenTextId), () => _burdenPercent + "%", datFont); // Rebuild on any change to the player's possessions. _objects.ObjectAdded += OnObjectChanged; _objects.ObjectMoved += OnObjectMoved; _objects.ObjectRemoved += OnObjectChanged; _objects.ObjectUpdated += OnObjectChanged; Populate(); } public static InventoryController Bind( ImportedLayout layout, ClientObjectTable objects, Func playerGuid, Func iconIds, Func strength, UiDatFont? datFont) => new InventoryController(layout, objects, playerGuid, iconIds, strength, datFont); private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); } private void OnObjectMoved(ClientObject o, uint from, uint to) { if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); } /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted. private bool Concerns(ClientObject o) { uint p = _playerGuid(); return o.ContainerId == p || o.WielderId == p || (o.ContainerId != 0 && _objects.Get(o.ContainerId)?.ContainerId == p); // 2-deep } /// Retail gm*UI display refresh: partition the player's direct contents into loose /// items (the "Contents of Backpack" grid) and side bags (the pack-selector list). The /// container index () IS retail's per-container /// item list — items enter it via the CreateObject (Ingest) / server-move (MoveItem) flows /// that call Reindex, slot-sorted. public void Populate() { uint p = _playerGuid(); var contents = _objects.GetContents(p); _contentsGrid?.Flush(); _containerList?.Flush(); foreach (var guid in contents) { var item = _objects.Get(guid); if (item is null) continue; // Equipped items belong on the paperdoll (Sub-phase C), never in the pack grid or // the side-bag list. A mid-session self-wield routes an item through // MoveItem(item, WielderGuid=player), so it transiently lands in GetContents(player); // retail's gm3DItemsUI shows pack contents only, gated on the wielded location. if (item.CurrentlyEquippedLocation != EquipMask.None) continue; bool isBag = item.Type.HasFlag(ItemType.Container) || item.ItemsCapacity > 0; var list = isBag ? _containerList : _contentsGrid; if (list is null) continue; uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve }; cell.SetItem(guid, tex); list.AddItem(cell); } // Main-pack cell: the player's own container. Icon = placeholder until a backpack // RenderSurface DID is pinned (spec §3 / divergence row). Bind the guid so a future // click can select it. if (_topContainer is not null) { _topContainer.Flush(); var main = new UiItemSlot { SpriteResolve = _topContainer.SpriteResolve }; main.SetItem(p, 0u); _topContainer.AddItem(main); } RefreshBurden(); } private void AttachCaption(UiElement? host, Func text, UiDatFont? datFont) { if (host is null) return; // The caption elements (0x100001D7 "Burden", 0x100001C5 "Contents of Backpack", // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live). // Drive the host UiText DIRECTLY: it is already in the paint tree and renders, whereas // a nested child UiText did not paint. Set it to a static centered single-line label. if (host is UiText t) { t.Centered = true; t.DatFont = datFont; t.ClickThrough = true; t.AcceptsFocus = false; t.IsEditControl = false; t.CapturesPointerDrag = false; t.LinesProvider = () => { var s = text(); return string.IsNullOrEmpty(s) ? Array.Empty() : new[] { new UiText.Line(s, CaptionColor) }; }; return; } // Fallback (non-UiText host): attach a centered label child. var label = new UiText { Left = 0f, Top = 0f, Width = host.Width, Height = host.Height, Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, Centered = true, DatFont = datFont, ClickThrough = true, AcceptsFocus = false, IsEditControl = false, CapturesPointerDrag = false, LinesProvider = () => { var s = text(); return string.IsNullOrEmpty(s) ? Array.Empty() : new[] { new UiText.Line(s, CaptionColor) }; }, }; host.AddChild(label); } /// Recompute the burden fill + percent. Port of CACQualities::InqLoad /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden: /// player wire EncumbranceVal (PropertyInt 5) if present, else the carried-Burden sum. private void RefreshBurden() { uint p = _playerGuid(); int str = _strength() ?? 10; // InqAttribute default 0xa int aug = _objects.Get(p)?.Properties.GetInt(EncumbranceAugProperty) ?? 0; int capacity = BurdenMath.EncumbranceCapacity(str, aug); int? wire = _objects.Get(p)?.Properties.Ints.TryGetValue(EncumbranceValProperty, out var ev) == true ? ev : (int?)null; int burden = wire ?? _objects.SumCarriedBurden(p); float load = BurdenMath.LoadRatio(capacity, burden); _burdenFill = BurdenMath.LoadToFill(load); _burdenPercent = BurdenMath.LoadToPercent(load); } /// Detach event handlers (idempotent). public void Dispose() { _objects.ObjectAdded -= OnObjectChanged; _objects.ObjectMoved -= OnObjectMoved; _objects.ObjectRemoved -= OnObjectChanged; _objects.ObjectUpdated -= OnObjectChanged; } }