feat(ui): complete retail item drop branches
Give retained buttons a reusable item-drop seam, wire the toolbar backpack target, and port retail stack-merge legality, capacity clamping, wire dispatch, destination selection, and immediate shortcut rekey notice. Record the live ACE merge gate and keep split quantity under AP-101. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
3281e0acb4
commit
dc1649c493
19 changed files with 479 additions and 28 deletions
|
|
@ -2124,7 +2124,10 @@ public sealed class GameWindow : IDisposable
|
|||
Combat.GetHealthPercent,
|
||||
Combat.HasHealth,
|
||||
guid => (uint)(Objects.Get(guid)?.StackSize ?? 0),
|
||||
guid => _liveSession?.SendQueryHealth(guid)),
|
||||
guid => _liveSession?.SendQueryHealth(guid),
|
||||
() => _playerServerGuid,
|
||||
(item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement)),
|
||||
Character: new AcDream.App.UI.CharacterRuntimeBindings(_characterSheetProvider),
|
||||
Inventory: new AcDream.App.UI.InventoryRuntimeBindings(
|
||||
Objects,
|
||||
|
|
@ -2137,6 +2140,8 @@ public sealed class GameWindow : IDisposable
|
|||
guid => _liveSession?.SendNoLongerViewingContents(guid),
|
||||
(item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
||||
(source, target, amount) =>
|
||||
_liveSession?.SendStackableMerge(source, target, amount),
|
||||
(item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||
_itemInteractionController,
|
||||
_selection),
|
||||
|
|
|
|||
|
|
@ -127,6 +127,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
public int BusyCount => _busyCount;
|
||||
|
||||
public bool CanMakeInventoryRequest => _readyForInventoryRequest() && _busyCount == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Raised for retail confirmation/auxiliary actions whose retained panel is
|
||||
/// owned outside this controller (PK/NPK altar, volatile rare, trade, salvage).
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ 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>? _sendStackableMerge;
|
||||
private readonly Action<uint, uint>? _notifyMergeAttempt;
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -80,6 +82,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint>? sendUse,
|
||||
Action<uint>? sendNoLongerViewing,
|
||||
Action<uint, uint, int>? sendPutItemInContainer,
|
||||
Action<uint, uint, uint>? sendStackableMerge,
|
||||
Action<uint, uint>? notifyMergeAttempt,
|
||||
ItemInteractionController? itemInteraction,
|
||||
Action? onClose)
|
||||
{
|
||||
|
|
@ -91,6 +95,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_sendUse = sendUse;
|
||||
_sendNoLongerViewing = sendNoLongerViewing;
|
||||
_sendPutItemInContainer = sendPutItemInContainer;
|
||||
_sendStackableMerge = sendStackableMerge;
|
||||
_notifyMergeAttempt = notifyMergeAttempt;
|
||||
_itemInteraction = itemInteraction;
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
|
||||
|
|
@ -197,12 +203,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint>? sendUse = null,
|
||||
Action<uint>? sendNoLongerViewing = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
Action<uint, uint, uint>? sendStackableMerge = null,
|
||||
Action<uint, uint>? notifyMergeAttempt = null,
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Action? onClose = null)
|
||||
=> new InventoryController(layout, objects, playerGuid, iconIds, strength, selection,
|
||||
ownerName, datFont,
|
||||
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
|
||||
sendUse, sendNoLongerViewing, sendPutItemInContainer, itemInteraction,
|
||||
sendUse, sendNoLongerViewing, sendPutItemInContainer,
|
||||
sendStackableMerge, notifyMergeAttempt, itemInteraction,
|
||||
onClose);
|
||||
|
||||
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
|
||||
|
|
@ -417,6 +426,14 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
uint item = payload.ObjId;
|
||||
if (item == 0) return;
|
||||
|
||||
// UIElement_ItemList::AcceptDragObject tries ItemHolder::AttemptMerge first
|
||||
// when a physical item is released on an occupied inventory cell. A legal
|
||||
// merge consumes the drop; an illegal merge falls through to insert/move.
|
||||
if (targetList == _contentsGrid
|
||||
&& targetCell.ItemId != 0
|
||||
&& TryMergeStacks(item, targetCell.ItemId))
|
||||
return;
|
||||
|
||||
uint container; int placement;
|
||||
if (targetList == _contentsGrid)
|
||||
{
|
||||
|
|
@ -439,6 +456,39 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_sendPutItemInContainer?.Invoke(item, container, placement);
|
||||
}
|
||||
|
||||
private bool TryMergeStacks(uint sourceId, uint targetId)
|
||||
{
|
||||
if (_sendStackableMerge is null
|
||||
|| _objects.Get(sourceId) is not { } source
|
||||
|| _objects.Get(targetId) is not { } target)
|
||||
return false;
|
||||
|
||||
StackMergePlan? plan = StackMergePlanner.Plan(
|
||||
new StackMergeItem(
|
||||
source.ObjectId,
|
||||
source.WeenieClassId,
|
||||
source.StackSize,
|
||||
source.StackSizeMax,
|
||||
source.TradeState),
|
||||
new StackMergeItem(
|
||||
target.ObjectId,
|
||||
target.WeenieClassId,
|
||||
target.StackSize,
|
||||
target.StackSizeMax,
|
||||
target.TradeState),
|
||||
_itemInteraction?.CanMakeInventoryRequest ?? true,
|
||||
requestedAmount: Math.Max(1, source.StackSize));
|
||||
if (plan is not { } merge)
|
||||
return false;
|
||||
|
||||
_sendStackableMerge(merge.SourceObjectId, merge.TargetObjectId, merge.Amount);
|
||||
// ItemHolder::AttemptMerge broadcasts this immediately after UIAttemptMerge;
|
||||
// it is an attempt notice, not a later server-confirm event.
|
||||
_notifyMergeAttempt?.Invoke(merge.SourceObjectId, merge.TargetObjectId);
|
||||
_selection.Select(merge.TargetObjectId, SelectionChangeSource.Inventory);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>True only when we KNOW the container is full (capacity known + contents indexed). A
|
||||
/// closed bag (unknown count) returns false → advisory accept; the server is authoritative.</summary>
|
||||
private bool IsContainerFull(uint container)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private readonly Action<uint>? _selectItem;
|
||||
private readonly Func<uint>? _playerGuid;
|
||||
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
|
||||
private bool _disposed;
|
||||
|
||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||
|
|
@ -100,7 +102,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Action<ShortcutEntry>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null,
|
||||
Action? toggleCombat = null,
|
||||
Action<uint>? selectItem = null)
|
||||
Action<uint>? selectItem = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_combatState = combatState;
|
||||
|
|
@ -114,6 +118,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_sendAddShortcut = sendAddShortcut;
|
||||
_sendRemoveShortcut = sendRemoveShortcut;
|
||||
_selectItem = selectItem;
|
||||
_playerGuid = playerGuid;
|
||||
_sendPutItemInContainer = sendPutItemInContainer;
|
||||
|
||||
for (int i = 0; i < SlotIds.Length; i++)
|
||||
{
|
||||
|
|
@ -145,6 +151,15 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
|
||||
_characterButton = layout.FindElement(CharacterButtonId) as UiButton;
|
||||
_inventoryButton = layout.FindElement(InventoryButtonId) as UiButton;
|
||||
if (_inventoryButton is not null)
|
||||
{
|
||||
// gmToolbarUI::HandleInventoryButtonDragOver @ 0x004BD180 uses the
|
||||
// authored child overlay's Accept state (0x10000046 = 0x060011F7).
|
||||
// UiButton owns the retained drop-target seam; the panel owns policy.
|
||||
_inventoryButton.ItemDragAcceptSprite = 0x060011F7u;
|
||||
_inventoryButton.OnItemDragOver = InventoryButtonDragOver;
|
||||
_inventoryButton.OnItemDrop = HandleInventoryButtonDrop;
|
||||
}
|
||||
|
||||
// Hide target-object meters + stack slider (gmToolbarUI::PostInit).
|
||||
foreach (var id in HiddenIds)
|
||||
|
|
@ -237,11 +252,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Action<ShortcutEntry>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null,
|
||||
Action? toggleCombat = null,
|
||||
Action<uint>? selectItem = null)
|
||||
Action<uint>? selectItem = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
||||
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem);
|
||||
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
|
||||
playerGuid, sendPutItemInContainer);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
|
@ -392,6 +410,24 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
};
|
||||
}
|
||||
|
||||
private static ItemDragAcceptance InventoryButtonDragOver(ItemDragPayload payload)
|
||||
=> payload.ObjId != 0 && payload.SourceKind != ItemDragSource.ShortcutBar
|
||||
? ItemDragAcceptance.Accept
|
||||
: ItemDragAcceptance.None;
|
||||
|
||||
private void HandleInventoryButtonDrop(ItemDragPayload payload)
|
||||
{
|
||||
// gmToolbarUI::HandleDropRelease @ 0x004BE7C0 inventory-button branch:
|
||||
// only fresh physical items are placed into the player's main inventory.
|
||||
// A shortcut alias remains neutral and its remove-on-lift result stands.
|
||||
if (payload.ObjId == 0 || payload.SourceKind == ItemDragSource.ShortcutBar)
|
||||
return;
|
||||
uint player = _playerGuid?.Invoke() ?? 0u;
|
||||
if (player == 0 || _repo.Get(payload.ObjId) is null)
|
||||
return;
|
||||
_sendPutItemInContainer?.Invoke(payload.ObjId, player, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>gmToolbarUI::UseShortcut @ 0x004BD350</c>. Target mode
|
||||
/// is one-shot and precedes both normal use and selection.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ public sealed record ToolbarRuntimeBindings(
|
|||
Func<uint, float> HealthPercent,
|
||||
Func<uint, bool> HasHealth,
|
||||
Func<uint, uint> StackSize,
|
||||
Action<uint> SendQueryHealth);
|
||||
Action<uint> SendQueryHealth,
|
||||
Func<uint> PlayerGuid,
|
||||
Action<uint, uint, int>? SendPutItemInContainer);
|
||||
|
||||
public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
||||
|
||||
|
|
@ -65,6 +67,7 @@ public sealed record InventoryRuntimeBindings(
|
|||
Action<uint>? SendUse,
|
||||
Action<uint>? SendNoLongerViewing,
|
||||
Action<uint, uint, int>? SendPutItemInContainer,
|
||||
Action<uint, uint, uint>? SendStackableMerge,
|
||||
Action<uint, uint>? SendWield,
|
||||
ItemInteractionController ItemInteraction,
|
||||
SelectionState Selection);
|
||||
|
|
@ -365,7 +368,9 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
||||
peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut,
|
||||
toggleCombat: b.ToggleCombat,
|
||||
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar));
|
||||
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
||||
playerGuid: b.PlayerGuid,
|
||||
sendPutItemInContainer: b.SendPutItemInContainer);
|
||||
ToolbarInputController = new ToolbarInputController(ToolbarController, b.Selection);
|
||||
SelectedObjectController = Layout.SelectedObjectController.Bind(
|
||||
layout,
|
||||
|
|
@ -583,11 +588,15 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
}
|
||||
|
||||
InventoryRuntimeBindings b = _bindings.Inventory;
|
||||
Action<uint, uint>? notifyMergeAttempt = ToolbarController is null
|
||||
? null
|
||||
: ToolbarController.ReplaceFullyMergedShortcut;
|
||||
InventoryController inventory = InventoryController.Bind(
|
||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Strength, b.Selection,
|
||||
_bindings.Assets.DefaultFont, _bindings.Character.Provider.CharacterName,
|
||||
contents, sideBag, mainPack, b.SendUse, b.SendNoLongerViewing,
|
||||
b.SendPutItemInContainer, b.ItemInteraction,
|
||||
b.SendPutItemInContainer, b.SendStackableMerge,
|
||||
notifyMergeAttempt, b.ItemInteraction,
|
||||
() => CloseWindow(WindowNames.Inventory));
|
||||
PaperdollController paperdoll = PaperdollController.Bind(
|
||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,19 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
/// <summary>Optional click handler. Wired by the controller (e.g. chat Submit, ToggleMaximize).</summary>
|
||||
public Action? OnClick { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional item-drop target callbacks. Retail uses ordinary buttons as drop surfaces
|
||||
/// in a few panels (notably gmToolbarUI's inventory button), so this belongs on the
|
||||
/// retained button rather than in UiRoot or a panel-specific hit-test workaround.
|
||||
/// </summary>
|
||||
public Func<ItemDragPayload, ItemDragAcceptance>? OnItemDragOver { get; set; }
|
||||
public Action<ItemDragPayload>? OnItemDrop { get; set; }
|
||||
public uint ItemDragAcceptSprite { get; set; }
|
||||
public uint ItemDragRejectSprite { get; set; }
|
||||
private ItemDragAcceptance _itemDragAcceptance;
|
||||
|
||||
internal ItemDragAcceptance ItemDragAcceptanceForTest => _itemDragAcceptance;
|
||||
|
||||
/// <summary>The dat element id from <see cref="ElementInfo.Id"/>.</summary>
|
||||
public uint ElementId => _info.Id;
|
||||
|
||||
|
|
@ -224,6 +237,19 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
float ty = (Height - lf.LineHeight) * 0.5f;
|
||||
ctx.DrawStringDat(lf, label, tx, ty, LabelColor);
|
||||
}
|
||||
|
||||
uint dragSprite = _itemDragAcceptance switch
|
||||
{
|
||||
ItemDragAcceptance.Accept => ItemDragAcceptSprite,
|
||||
ItemDragAcceptance.Reject => ItemDragRejectSprite,
|
||||
_ => 0u,
|
||||
};
|
||||
if (dragSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = _resolve(dragSprite);
|
||||
if (tex != 0)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
|
|
@ -276,6 +302,19 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
}
|
||||
OnClick?.Invoke();
|
||||
return OnClick is not null;
|
||||
case UiEventType.DragEnter:
|
||||
_itemDragAcceptance = e.Payload is ItemDragPayload payload
|
||||
? OnItemDragOver?.Invoke(payload) ?? ItemDragAcceptance.None
|
||||
: ItemDragAcceptance.None;
|
||||
return OnItemDragOver is not null;
|
||||
case UiEventType.DragOver:
|
||||
_itemDragAcceptance = ItemDragAcceptance.None;
|
||||
return OnItemDragOver is not null;
|
||||
case UiEventType.DropReleased:
|
||||
_itemDragAcceptance = ItemDragAcceptance.None;
|
||||
if (e.Payload is ItemDragPayload dropped)
|
||||
OnItemDrop?.Invoke(dropped);
|
||||
return OnItemDrop is not null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1336,6 +1336,13 @@ public sealed class WorldSession : IDisposable
|
|||
SendGameAction(InteractRequests.BuildPickUp(seq, itemGuid, containerGuid, placement));
|
||||
}
|
||||
|
||||
/// <summary>Send StackableMerge (0x0054) with retail's already-clamped transfer amount.</summary>
|
||||
public void SendStackableMerge(uint sourceGuid, uint targetGuid, uint amount)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(InventoryActions.BuildStackableMerge(seq, sourceGuid, targetGuid, amount));
|
||||
}
|
||||
|
||||
/// <summary>Send retail QueryHealth (0x01BF). Server replies UpdateHealth (0x01C0).</summary>
|
||||
/// <remarks>
|
||||
/// Retail anchor: <c>CM_Combat::Event_QueryHealth</c> / <c>gmToolbarUI::HandleSelectionChanged:198635</c>
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ public static class ShortcutDropPlanner
|
|||
|
||||
/// <summary>
|
||||
/// Plan the toolbar side of <c>RecvNotice_FullMergingItem @ 0x004BE9B0</c>.
|
||||
/// The normal retail invariant is one object shortcut; all matching records are
|
||||
/// rekeyed here so a malformed duplicate cannot retain a destroyed stack id.
|
||||
/// <c>CreateShortcutToItem</c> removes an existing destination shortcut before
|
||||
/// placing it into the source shortcut's former slot.
|
||||
/// </summary>
|
||||
public static ShortcutMutation[] PlanFullStackMerge(
|
||||
IReadOnlyList<ShortcutEntry?> slots,
|
||||
|
|
@ -104,15 +104,21 @@ public static class ShortcutDropPlanner
|
|||
if (slots.Count != ShortcutStore.SlotCount || oldObjectId == 0 || newObjectId == 0)
|
||||
return Array.Empty<ShortcutMutation>();
|
||||
|
||||
var mutations = new List<ShortcutMutation>();
|
||||
var state = new ShortcutEntry?[ShortcutStore.SlotCount];
|
||||
for (int slot = 0; slot < slots.Count; slot++)
|
||||
{
|
||||
if (VisibleEntry(slots[slot]) is not { } entry || entry.ObjectId != oldObjectId)
|
||||
continue;
|
||||
|
||||
mutations.Add(ShortcutMutation.Remove(slot));
|
||||
mutations.Add(ShortcutMutation.Add(entry with { ObjectId = newObjectId }));
|
||||
state[slot] = slots[slot];
|
||||
}
|
||||
|
||||
var mutations = new List<ShortcutMutation>(3);
|
||||
int sourceSlot = FindFirstVisibleObject(state, oldObjectId);
|
||||
if (sourceSlot < 0)
|
||||
return Array.Empty<ShortcutMutation>();
|
||||
|
||||
ShortcutEntry sourceEntry = state[sourceSlot]!.Value;
|
||||
RemoveVisibleAt(state, sourceSlot, mutations);
|
||||
RemoveFirstVisibleObject(state, newObjectId, mutations);
|
||||
Add(state, sourceEntry with { ObjectId = newObjectId }, mutations);
|
||||
return mutations.ToArray();
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +155,14 @@ public static class ShortcutDropPlanner
|
|||
}
|
||||
}
|
||||
|
||||
private static int FindFirstVisibleObject(ShortcutEntry?[] state, uint objectId)
|
||||
{
|
||||
for (int slot = 0; slot < state.Length; slot++)
|
||||
if (VisibleEntry(state[slot]) is { } entry && entry.ObjectId == objectId)
|
||||
return slot;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
ShortcutEntry?[] state,
|
||||
ShortcutEntry entry,
|
||||
|
|
|
|||
49
src/AcDream.Core/Items/StackMergePlanner.cs
Normal file
49
src/AcDream.Core/Items/StackMergePlanner.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.Core.Items;
|
||||
|
||||
public readonly record struct StackMergeItem(
|
||||
uint ObjectId,
|
||||
uint WeenieClassId,
|
||||
int StackSize,
|
||||
int MaxStackSize,
|
||||
int TradeState);
|
||||
|
||||
public readonly record struct StackMergePlan(uint SourceObjectId, uint TargetObjectId, uint Amount);
|
||||
|
||||
/// <summary>
|
||||
/// Pure port of <c>ItemHolder::IsMergeAttemptLegal @ 0x00586F30</c> and the
|
||||
/// transfer-size branch in <c>ItemHolder::AttemptMerge @ 0x005878F0</c>.
|
||||
/// </summary>
|
||||
public static class StackMergePlanner
|
||||
{
|
||||
public static StackMergePlan? Plan(
|
||||
in StackMergeItem source,
|
||||
in StackMergeItem target,
|
||||
bool readyForInventoryRequest,
|
||||
int requestedAmount)
|
||||
{
|
||||
if (!readyForInventoryRequest
|
||||
|| source.ObjectId == 0
|
||||
|| target.ObjectId == 0
|
||||
|| source.ObjectId == target.ObjectId
|
||||
|| source.MaxStackSize <= 1
|
||||
|| target.MaxStackSize <= 1
|
||||
|| source.TradeState == 1
|
||||
|| target.TradeState == 1
|
||||
|| source.WeenieClassId != target.WeenieClassId)
|
||||
return null;
|
||||
|
||||
int targetSize = Math.Max(1, target.StackSize);
|
||||
int available = target.MaxStackSize - targetSize;
|
||||
if (available <= 0)
|
||||
return null;
|
||||
|
||||
int sourceSize = Math.Max(1, source.StackSize);
|
||||
int desired = requestedAmount > 0 ? Math.Min(requestedAmount, sourceSize) : sourceSize;
|
||||
uint transfer = (uint)Math.Min(desired, available);
|
||||
return transfer == 0
|
||||
? null
|
||||
: new StackMergePlan(source.ObjectId, target.ObjectId, transfer);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue