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:
Erik 2026-07-11 09:46:32 +02:00
parent 3281e0acb4
commit dc1649c493
19 changed files with 479 additions and 28 deletions

View file

@ -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,

View 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);
}
}