fix(ui): port retail partial stack transfers

Route the selected stack quantity through retail GetObjectSplitSize semantics, send exact split-to-container and split-to-ground actions, and keep the original object in place until the server publishes the newly guided stack. Cover selection scoping, wire bytes, container placement, and world drops with conformance tests.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:34:13 +02:00
parent 6005c51c4d
commit ea72c395c9
14 changed files with 280 additions and 19 deletions

View file

@ -58,6 +58,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
private readonly Action<uint>? _sendUse;
private readonly Action<uint>? _sendNoLongerViewing;
private readonly Action<uint, uint, int>? _sendPutItemInContainer; // (item, container, placement)
private readonly Action<uint, uint, uint, uint>? _sendStackableSplitToContainer;
private readonly Action<uint, uint, uint>? _sendStackableMerge;
private readonly Action<uint, uint>? _notifyMergeAttempt;
private readonly ItemInteractionController? _itemInteraction;
@ -83,6 +84,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Action<uint>? sendUse,
Action<uint>? sendNoLongerViewing,
Action<uint, uint, int>? sendPutItemInContainer,
Action<uint, uint, uint, uint>? sendStackableSplitToContainer,
Action<uint, uint, uint>? sendStackableMerge,
Action<uint, uint>? notifyMergeAttempt,
ItemInteractionController? itemInteraction,
@ -97,6 +99,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_sendUse = sendUse;
_sendNoLongerViewing = sendNoLongerViewing;
_sendPutItemInContainer = sendPutItemInContainer;
_sendStackableSplitToContainer = sendStackableSplitToContainer;
_sendStackableMerge = sendStackableMerge;
_notifyMergeAttempt = notifyMergeAttempt;
_itemInteraction = itemInteraction;
@ -206,6 +209,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Action<uint>? sendUse = null,
Action<uint>? sendNoLongerViewing = null,
Action<uint, uint, int>? sendPutItemInContainer = null,
Action<uint, uint, uint, uint>? sendStackableSplitToContainer = null,
Action<uint, uint, uint>? sendStackableMerge = null,
Action<uint, uint>? notifyMergeAttempt = null,
ItemInteractionController? itemInteraction = null,
@ -215,7 +219,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
ownerName, datFont,
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
sendUse, sendNoLongerViewing, sendPutItemInContainer,
sendStackableMerge, notifyMergeAttempt, itemInteraction,
sendStackableSplitToContainer, sendStackableMerge,
notifyMergeAttempt, itemInteraction,
onClose, stackSplitQuantity);
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
@ -417,8 +422,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
return ItemDragAcceptance.Reject;
}
/// <summary>Perform the move: resolve (container, placement) from the target, optimistically move
/// locally (instant), and send PutItemInContainer. Retail: HandleDropRelease + ItemList_InsertItem.</summary>
/// <summary>Resolve the destination and either split or move the stack. A partial split waits
/// for the server-created object's guid; a whole move remains optimistic. Retail:
/// <c>ItemHolder::AttemptToPlaceInContainer @ 0x00588140</c>.</summary>
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
// UIElement_ItemList::HandleDropRelease @ 0x004E4790 applies the same
@ -456,6 +462,22 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
else return;
if (container == item) return; // never into itself
if (_objects.Get(item) is { } source)
{
uint fullStack = (uint)Math.Max(source.StackSize, 1);
uint splitSize = _stackSplitQuantity?.GetObjectSplitSize(
item, _selection.SelectedObjectId ?? 0u, fullStack) ?? fullStack;
if (splitSize < fullStack)
{
// UIAttemptSplitToContainer leaves the source stack where it is. ACE will
// publish the reduced source plus a newly-guided destination stack.
_sendStackableSplitToContainer?.Invoke(
item, container, (uint)placement, splitSize);
return;
}
}
_objects.MoveItemOptimistic(item, container, placement); // instant local move
_sendPutItemInContainer?.Invoke(item, container, placement);
}