feat(ui): port retail selected-stack quantity

Bind the authored stack count entry and horizontal slider to one Core split-quantity owner, preserve retail count-first naming and exact 1000-step rounding, refresh on stack changes, and consume the selected amount during merges. Conformance covers the production DAT fixture and retained pointer/focus paths.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:07:25 +02:00
parent dc1649c493
commit a20e5c68c7
19 changed files with 595 additions and 51 deletions

View file

@ -61,6 +61,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
private readonly Action<uint, uint, uint>? _sendStackableMerge;
private readonly Action<uint, uint>? _notifyMergeAttempt;
private readonly ItemInteractionController? _itemInteraction;
private readonly StackSplitQuantityState? _stackSplitQuantity;
private bool _disposed;
// ACE PropertyInt ids read by retail InqLoad (decomp 0x0058f130: InqInt(5)/InqInt(0xe6)).
@ -85,7 +86,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Action<uint, uint, uint>? sendStackableMerge,
Action<uint, uint>? notifyMergeAttempt,
ItemInteractionController? itemInteraction,
Action? onClose)
Action? onClose,
StackSplitQuantityState? stackSplitQuantity)
{
_objects = objects;
_playerGuid = playerGuid;
@ -98,6 +100,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_sendStackableMerge = sendStackableMerge;
_notifyMergeAttempt = notifyMergeAttempt;
_itemInteraction = itemInteraction;
_stackSplitQuantity = stackSplitQuantity;
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
WindowChromeController.BindCloseButton(layout, onClose);
@ -206,13 +209,14 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Action<uint, uint, uint>? sendStackableMerge = null,
Action<uint, uint>? notifyMergeAttempt = null,
ItemInteractionController? itemInteraction = null,
Action? onClose = null)
Action? onClose = null,
StackSplitQuantityState? stackSplitQuantity = null)
=> new InventoryController(layout, objects, playerGuid, iconIds, strength, selection,
ownerName, datFont,
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
sendUse, sendNoLongerViewing, sendPutItemInContainer,
sendStackableMerge, notifyMergeAttempt, itemInteraction,
onClose);
onClose, stackSplitQuantity);
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
private void OnObjectRemoved(ClientObject o)
@ -463,6 +467,10 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|| _objects.Get(targetId) is not { } target)
return false;
int requestedAmount = _selection.SelectedObjectId == sourceId
&& _stackSplitQuantity is not null
? (int)Math.Min(_stackSplitQuantity.Value, int.MaxValue)
: Math.Max(1, source.StackSize);
StackMergePlan? plan = StackMergePlanner.Plan(
new StackMergeItem(
source.ObjectId,
@ -477,7 +485,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
target.StackSizeMax,
target.TradeState),
_itemInteraction?.CanMakeInventoryRequest ?? true,
requestedAmount: Math.Max(1, source.StackSize));
requestedAmount);
if (plan is not { } merge)
return false;