Add the ClientUISystem ground-object lifecycle, authoritative root and nested ViewContents projections, replacement and close semantics, and the DAT-authored gmExternalContainerUI strip for chests and corpses. Route double-click loot and full or partial drag transfers through the shared retail item policy without optimistic external ownership. Remove the incorrect NoLongerViewingContents behavior from owned side packs and retire AP-106/#196. Release build succeeds and all 5,875 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
6.8 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.
| 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)
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:
send PutItemInContainer(itemId, player/openOwnedBag, placement)
do not mutate canonical ownership before the server response
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.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.