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>
8.1 KiB
Retail external-container and looting pseudocode
Date: 2026-07-17
Scope: chests, corpses, and other world containers opened through Use; the
bottom-of-screen external-container strip; looting into the player's carried
inventory; returning carried items to the open external container; and the
external-view close/replacement lifetime. Owned backpack and side-pack browsing
is deliberately not part of this lifetime.
Retail oracle
Named Sept-2013 retail symbols:
gmExternalContainerUI::ListenToElementMessage @ 0x004CBAD0gmExternalContainerUI::SetGroundObject @ 0x004CBBD0gmExternalContainerUI::CloseCurrentContainer @ 0x004CBCB0gmExternalContainerUI::PostInit @ 0x004CBDE0gmExternalContainerUI::OnVisibilityChanged @ 0x004CBF50gmExternalContainerUI::RecvNotice_SetGroundObject @ 0x004CBFD0gmExternalContainerUI::OnItemListDragOver @ 0x004CC0C0UIElement_ItemList::ListenToElementMessage @ 0x004E4D50ClientUISystem::SetGroundObject @ 0x00564510ClientUISystem::OnViewContents @ 0x00565870ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0ItemHolder::AttemptToPlaceInContainer @ 0x00588140ItemHolder::UseObject @ 0x00588A80
The exact client dispatch for server event CloseGroundContainer (0x0052) is
at 0x0055B187 in acclient_2013_pseudo_c.txt.
Cross-references:
- ACE
GameEventViewContents,GameEventCloseGroundContainer,GameActionNoLongerViewingContents,Container.SendInventory, andContainer.FinishCloseconfirm the authoritative packet order and that ACE can sendViewContentsfor the root followed by its nested containers. - holtburger protocol
inventory/events.rsandinventory/actions.rsconfirm the exact wire shapes. Its world inventory handler independently models open container contents as temporary preview retention and retires that preview onCloseGroundContainer.
Authored UI
The strip is LayoutDesc 0x21000008, selected root 0x10000063
(0,400, 800 x 110) rather than a synthetic inventory window.
The root's own DirectState is only the tiled center surface 0x06004CC2; the
common floating-window nine-slice supplies the missing outer bevel. The authored
Left/Right edge rules stretch the two horizontal lists and scrollbar while
pinning the close button, so the mounted strip resizes horizontally only. The
modern retained mount starts at a compact 700-pixel content width and may be
resized from either side up to the viewport edge.
| Element | Purpose | Authored bounds |
|---|---|---|
0x10000064 |
current root container item list | 5,5,36,36 |
0x10000067 |
nested-container selector list | 67,5,680,36 |
0x10000068 |
close button | 770,1,25,23 |
0x1000006A |
current contents item list | 8,47,784,32 |
0x1000006B |
horizontal contents scrollbar | 8,81,784,16 |
The close button uses sprites 0x060011BD (normal) and 0x060011BC
(pressed). The horizontal scrollbar inherits the retail track, three-part
thumb, and arrow sprites from LayoutDesc 0x2100003E.
Pseudocode
UseWorldObject(objectId):
decision = ItemHolder policy
if decision is SetGroundObject:
expectedExternalRoot = objectId
send Use(objectId)
UseCorpse(corpseId):
// A corpse is a TYPE_CONTAINER carrying BF_STUCK | BF_CORPSE.
// BF_STUCK excludes DetermineUseResult's PlaceInBackpack branch.
classify through ItemHolder::UseObject
send Use(corpseId), never PutItemInContainer(corpseId)
OnViewContents(containerId, orderedEntries):
// Always update the projection: ACE sends nested-container snapshots too.
objectTable.ReplaceContents(containerId, orderedEntries)
if containerId != expectedExternalRoot:
return
old = currentExternalRoot
if old != 0 and old != containerId:
objectTable.StopViewingContentsTree(old)
send NoLongerViewingContents(old) exactly once
currentExternalRoot = containerId
expectedExternalRoot = containerId
externalPanel.SetGroundObject(containerId)
ExternalPanel.SetGroundObject(containerId):
unregister old range watch
clear root, nested-container, and contents lists
add root object to top list
set current local container to root
populate nested-container selector and current contents
if containerId == 0:
hide panel
else:
register range watch using root UseRadius
show panel
ExternalPanel.CloseCurrentContainer():
if currentExternalRoot == 0:
return
send Use(currentExternalRoot)
// Do not send NoLongerViewingContents here. ACE responds with 0x0052.
detach local lists and wait for authoritative close
OnCloseGroundContainer(containerId):
if containerId != currentExternalRoot:
return
objectTable.StopViewingContentsTree(containerId)
currentExternalRoot = 0
expectedExternalRoot = 0
externalPanel.SetGroundObject(0)
OnUseDone(error):
if error != 0 and expectedExternalRoot != currentExternalRoot:
expectedExternalRoot = currentExternalRoot
OnExternalRangeExit():
externalPanel.SetVisible(false)
ExternalPanel.CloseCurrentContainer()
OnExternalItemSingleClick(itemId):
first offer it to target mode
otherwise select the item
OnExternalItemDoubleClick(itemId):
ItemHolder.UseObject(itemId)
// Because item.ContainerId is currentExternalRoot, retail policy emits
// PlaceInBackpack; the server remains authoritative for the move.
DragExternalItemToOwnedInventory(itemId, placement, splitAmount):
if splitAmount < full stack:
send StackableSplitToContainer(itemId, player/openOwnedBag,
placement, splitAmount)
else:
destinationList.InsertItem(itemId, placement)
destinationList.pendingItem.SetWaitingState(true)
send PutItemInContainer(itemId, player/openOwnedBag, placement)
do not mutate canonical ownership before the server response
OnServerMoveItem(itemId):
if destinationList.pendingItem.itemId == itemId:
clear pendingItem
rebuild the list from authoritative placement
OnInventoryServerSaveFailed(itemId):
if destinationList.pendingItem.itemId == itemId:
remove the pending copy
leave the source ownership unchanged
DragOwnedItemToExternalContainer(itemId, placement, splitAmount):
if splitAmount < full stack:
send StackableSplitToContainer(itemId, currentExternalSubcontainer,
placement, splitAmount)
else:
send PutItemInContainer(itemId, currentExternalSubcontainer, placement)
do not mutate canonical ownership before the server response
Architectural mapping
ExternalContainerStatein Core owns only expected/current external-root identity and transition ordering. It filters ACE's nestedViewContentspackets so they cannot replace the visible root.ExternalContainerLifecycleControllerin App observes those transitions, retires the replaced root's temporary preview tree, then owns the replacement-onlyNoLongerViewingContentsnetwork side effect. The wire lifetime therefore does not depend on whether retained UI is mounted.ClientObjectTableremains the canonical object and ordered-content projection owner.ExternalContainerControllerowns the authored strip, range/close behavior, nested-list selection, selection, and drag/drop presentation.InventoryControllerowns retailUIElement_ItemList::m_pendingItem: a destination-only waiting projection at the exact chosen slot. It subscribes to authoritative move and failure notices and never rewrites external-item ownership while the request is in flight.ItemInteractionControllerremains the only interpreter of retail item-use and placement policy. It gains explicit adapters forSetGroundObjectandPlaceInContainer; panels do not reimplement those decisions.GameWindowonly creates the lifecycle/UI bindings and supplies network delegates. No external-container feature body lives there.
There is no new intended retail divergence. Completing this mechanism retires
AP-106/#196: NoLongerViewingContents belongs to replacement of an external
root, not switching between owned side packs.