fix(items): complete retail corpse looting feedback
Route corpse Use through the shared ItemHolder policy so Stuck corpses open instead of being sent as pickup requests. Restore the framed, horizontally resizable external-container strip and use a compact initial width. Port retail's target-list pending item projection so loot is marked in the chosen inventory slot without changing canonical ownership before the server confirms. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
20ce67b625
commit
d51a0fc825
13 changed files with 275 additions and 102 deletions
|
|
@ -21,6 +21,10 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
|
||||
private const float ItemCellSize = 32f;
|
||||
private const float ContainerCellSize = 36f;
|
||||
// User-directed retained default: the raw 800-pixel LayoutDesc remains
|
||||
// horizontally reachable by resize/persistence but opens less wide.
|
||||
internal const float DefaultContentWidth = 700f;
|
||||
internal const float MinimumContentWidth = 160f;
|
||||
|
||||
private readonly ExternalContainerState _state;
|
||||
private readonly ClientObjectTable _objects;
|
||||
|
|
@ -144,6 +148,32 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
contentsEmptySprite,
|
||||
containerEmptySprite);
|
||||
|
||||
/// <summary>
|
||||
/// Retail's root authors only the tiled center surface; the common floating-window
|
||||
/// bevel surrounds it. Its Left/Right edge rules stretch every horizontal list and
|
||||
/// pin the close button, so this strip resizes on X only.
|
||||
/// </summary>
|
||||
internal static RetailWindowFrame.Options CreateWindowOptions(UiElement root)
|
||||
=> new()
|
||||
{
|
||||
WindowName = WindowNames.ExternalContainer,
|
||||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = root.Left,
|
||||
Top = root.Top,
|
||||
ContentWidth = Math.Min(root.Width, DefaultContentWidth),
|
||||
ContentHeight = root.Height,
|
||||
MinWidth = MinimumContentWidth + 2f * RetailChromeSprites.Border,
|
||||
Visible = false,
|
||||
Draggable = true,
|
||||
Resizable = true,
|
||||
ResizeX = true,
|
||||
ResizeY = false,
|
||||
ResizableEdges = ResizeEdges.Left | ResizeEdges.Right,
|
||||
ConstrainDragToParent = true,
|
||||
ConstrainResizeToParent = true,
|
||||
DrawChromeCenter = false,
|
||||
};
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
uint root = _state.CurrentContainerId;
|
||||
|
|
|
|||
|
|
@ -63,8 +63,14 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
private readonly Action<uint, uint>? _notifyMergeAttempt;
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private readonly StackSplitQuantityState? _stackSplitQuantity;
|
||||
private PendingListPlacement? _pendingListPlacement;
|
||||
private bool _disposed;
|
||||
|
||||
private readonly record struct PendingListPlacement(
|
||||
uint ItemId,
|
||||
uint ContainerId,
|
||||
int Placement);
|
||||
|
||||
// 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
|
||||
|
|
@ -167,6 +173,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
// Rebuild on any change to the player's possessions.
|
||||
_objects.ObjectAdded += OnObjectChanged;
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.MoveRequestFailed += OnMoveRequestFailed;
|
||||
_objects.ContainerContentsReplaced += OnContainerContentsReplaced;
|
||||
_objects.ObjectRemoved += OnObjectRemoved;
|
||||
_objects.ObjectUpdated += OnObjectChanged;
|
||||
|
|
@ -225,9 +232,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
notifyMergeAttempt, itemInteraction,
|
||||
onClose, stackSplitQuantity);
|
||||
|
||||
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
|
||||
private void OnObjectChanged(ClientObject o)
|
||||
{
|
||||
if (Concerns(o) || _pendingListPlacement?.ItemId == o.ObjectId)
|
||||
Populate();
|
||||
}
|
||||
private void OnObjectRemoved(ClientObject o)
|
||||
{
|
||||
if (_pendingListPlacement?.ItemId == o.ObjectId)
|
||||
_pendingListPlacement = null;
|
||||
if (_selection.SelectedObjectId == o.ObjectId)
|
||||
{
|
||||
_selection.Clear(
|
||||
|
|
@ -238,8 +251,12 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
}
|
||||
private void OnObjectMoved(ClientObjectMove move)
|
||||
{
|
||||
bool resolvedPending = _pendingListPlacement?.ItemId == move.ItemId;
|
||||
if (resolvedPending)
|
||||
_pendingListPlacement = null;
|
||||
uint player = _playerGuid();
|
||||
if ((move.Item is { } item && Concerns(item))
|
||||
if (resolvedPending
|
||||
|| (move.Item is { } item && Concerns(item))
|
||||
|| move.Previous.ContainerId == player
|
||||
|| move.Current.ContainerId == player
|
||||
|| move.Previous.WielderId == player
|
||||
|
|
@ -253,7 +270,18 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
}
|
||||
private void OnInteractionStateChanged() => ApplyIndicators();
|
||||
private void OnSelectionChanged(SelectionTransition _) => ApplyIndicators();
|
||||
private void OnObjectsCleared() => Populate();
|
||||
private void OnMoveRequestFailed(MoveRequestFailure failure)
|
||||
{
|
||||
if (_pendingListPlacement?.ItemId != failure.ItemId)
|
||||
return;
|
||||
_pendingListPlacement = null;
|
||||
Populate();
|
||||
}
|
||||
private void OnObjectsCleared()
|
||||
{
|
||||
_pendingListPlacement = null;
|
||||
Populate();
|
||||
}
|
||||
|
||||
/// <summary>True if the object is in (or wielded by) the player — i.e. a rebuild is warranted.</summary>
|
||||
private bool Concerns(ClientObject o)
|
||||
|
|
@ -291,12 +319,32 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
// Contents grid: the OPEN container's loose items. (Bags live in the column; a side bag has
|
||||
// no sub-bags, so the isBag skip is a no-op when a bag is open.)
|
||||
var visibleContents = new List<uint>();
|
||||
foreach (var guid in _objects.GetContents(open))
|
||||
{
|
||||
var item = _objects.Get(guid);
|
||||
if (item is null || item.CurrentlyEquippedLocation != EquipMask.None) continue;
|
||||
bool isBag = IsBag(item);
|
||||
if (!isBag) AddCell(_contentsGrid, guid, isContainer: false);
|
||||
if (!isBag) visibleContents.Add(guid);
|
||||
}
|
||||
|
||||
PendingListPlacement? pending = _pendingListPlacement;
|
||||
if (pending is { } projection
|
||||
&& projection.ContainerId == open
|
||||
&& !visibleContents.Contains(projection.ItemId)
|
||||
&& _objects.Get(projection.ItemId) is { } pendingItem
|
||||
&& !IsBag(pendingItem))
|
||||
{
|
||||
int index = Math.Clamp(projection.Placement, 0, visibleContents.Count);
|
||||
visibleContents.Insert(index, projection.ItemId);
|
||||
}
|
||||
|
||||
foreach (uint guid in visibleContents)
|
||||
{
|
||||
bool waiting = pending is { } waitingProjection
|
||||
&& waitingProjection.ContainerId == open
|
||||
&& waitingProjection.ItemId == guid;
|
||||
AddCell(_contentsGrid, guid, isContainer: false, waiting);
|
||||
}
|
||||
|
||||
// Pad the grid to the OPEN container's capacity (main pack 102 / side bag 24).
|
||||
|
|
@ -365,7 +413,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
/// <summary>Add a populated cell wired to its click role: container cell → open+select,
|
||||
/// item cell → select-only.</summary>
|
||||
private void AddCell(UiItemList? list, uint guid, bool isContainer)
|
||||
private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false)
|
||||
{
|
||||
if (list is null) return;
|
||||
var item = _objects.Get(guid);
|
||||
|
|
@ -376,6 +424,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
|
||||
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
|
||||
cell.SetItem(guid, tex, dragIconTexture: dragTex);
|
||||
cell.SetWaitingState(waiting);
|
||||
cell.SlotIndex = list.GetNumUIItems(); // index it will occupy (== its slot in a packed list)
|
||||
ConfigureDropFeedback(list, cell);
|
||||
cell.DoubleClicked = () => _itemInteraction?.ActivateItem(guid);
|
||||
|
|
@ -456,6 +505,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
return ItemDragAcceptance.None;
|
||||
if (payload.ObjId == 0)
|
||||
return ItemDragAcceptance.Reject;
|
||||
if (payload.SourceKind == ItemDragSource.Ground
|
||||
&& _pendingListPlacement is not null)
|
||||
return ItemDragAcceptance.Reject;
|
||||
if (targetList == _contentsGrid)
|
||||
return ItemDragAcceptance.Accept;
|
||||
if (targetList == _containerList || targetList == _topContainer)
|
||||
|
|
@ -525,12 +577,22 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
}
|
||||
}
|
||||
|
||||
// External-container contents are a temporary ViewContents projection.
|
||||
// Retail UIAttemptPutInContainer leaves them untouched until the server's
|
||||
// authoritative move response; only rearrangements within owned inventory
|
||||
// use the existing optimistic projection.
|
||||
if (payload.SourceKind != ItemDragSource.Ground)
|
||||
// External-container contents retain canonical ownership while the request
|
||||
// is in flight, but retail immediately inserts an m_pendingItem copy into
|
||||
// the chosen destination slot and ghosts it. The server move/failure notice
|
||||
// resolves that visual projection. UIElement_ItemList::HandleDropRelease
|
||||
// @ 0x004E4790; ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680.
|
||||
if (payload.SourceKind == ItemDragSource.Ground)
|
||||
{
|
||||
if (_pendingListPlacement is not null)
|
||||
return;
|
||||
_pendingListPlacement = new PendingListPlacement(item, container, placement);
|
||||
Populate();
|
||||
}
|
||||
else
|
||||
{
|
||||
_objects.MoveItemOptimistic(item, container, placement);
|
||||
}
|
||||
_sendPutItemInContainer?.Invoke(item, container, placement);
|
||||
}
|
||||
|
||||
|
|
@ -716,6 +778,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_disposed = true;
|
||||
_objects.ObjectAdded -= OnObjectChanged;
|
||||
_objects.ObjectMoved -= OnObjectMoved;
|
||||
_objects.MoveRequestFailed -= OnMoveRequestFailed;
|
||||
_objects.ContainerContentsReplaced -= OnContainerContentsReplaced;
|
||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||
_objects.ObjectUpdated -= OnObjectChanged;
|
||||
|
|
|
|||
|
|
@ -1640,20 +1640,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Host.Root,
|
||||
root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.ExternalContainer,
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Left = root.Left,
|
||||
Top = root.Top,
|
||||
ContentWidth = root.Width,
|
||||
ContentHeight = root.Height,
|
||||
Visible = false,
|
||||
Draggable = false,
|
||||
Resizable = false,
|
||||
ResizeX = false,
|
||||
ResizeY = false,
|
||||
});
|
||||
ExternalContainerController.CreateWindowOptions(root));
|
||||
|
||||
uint contentsEmpty;
|
||||
uint containerEmpty;
|
||||
|
|
|
|||
|
|
@ -144,9 +144,17 @@ public class UiItemSlot : UiElement
|
|||
// ItemList_BeginDrag ghosts physical lists, but explicitly excludes shortcut lists
|
||||
// (along with vendor/salvage lists, which acdream does not model as ItemDragSource).
|
||||
// Keep the source's full cell icon in place and reveal the authored grey mesh over it.
|
||||
_waiting = active && ItemId != 0 && SourceKind != ItemDragSource.ShortcutBar;
|
||||
SetWaitingState(active && SourceKind != ItemDragSource.ShortcutBar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply retail's <c>m_elem_Icon_Ghosted</c> waiting presentation independently
|
||||
/// of pointer ownership. Destination ItemLists use this for their temporary
|
||||
/// <c>m_pendingItem</c> inserted by <c>HandleDropRelease @ 0x004E4790</c>.
|
||||
/// </summary>
|
||||
internal void SetWaitingState(bool waiting)
|
||||
=> _waiting = waiting && ItemId != 0;
|
||||
|
||||
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
|
||||
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
|
||||
/// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue