# 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 @ 0x004CBAD0` - `gmExternalContainerUI::SetGroundObject @ 0x004CBBD0` - `gmExternalContainerUI::CloseCurrentContainer @ 0x004CBCB0` - `gmExternalContainerUI::PostInit @ 0x004CBDE0` - `gmExternalContainerUI::OnVisibilityChanged @ 0x004CBF50` - `gmExternalContainerUI::RecvNotice_SetGroundObject @ 0x004CBFD0` - `gmExternalContainerUI::OnItemListDragOver @ 0x004CC0C0` - `UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` - `ClientUISystem::SetGroundObject @ 0x00564510` - `ClientUISystem::OnViewContents @ 0x00565870` - `ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680` - `ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0` - `ItemHolder::AttemptToPlaceInContainer @ 0x00588140` - `ItemHolder::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`, and `Container.FinishClose` confirm the authoritative packet order and that ACE can send `ViewContents` for the root followed by its nested containers. - holtburger protocol `inventory/events.rs` and `inventory/actions.rs` confirm the exact wire shapes. Its world inventory handler independently models open container contents as temporary preview retention and retires that preview on `CloseGroundContainer`. ## 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 ```text 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 - `ExternalContainerState` in Core owns only expected/current external-root identity and transition ordering. It filters ACE's nested `ViewContents` packets so they cannot replace the visible root. - `ExternalContainerLifecycleController` in App observes those transitions, retires the replaced root's temporary preview tree, then owns the replacement-only `NoLongerViewingContents` network side effect. The wire lifetime therefore does not depend on whether retained UI is mounted. - `ClientObjectTable` remains the canonical object and ordered-content projection owner. - `ExternalContainerController` owns the authored strip, range/close behavior, nested-list selection, selection, and drag/drop presentation. - `ItemInteractionController` remains the only interpreter of retail item-use and placement policy. It gains explicit adapters for `SetGroundObject` and `PlaceInContainer`; panels do not reimplement those decisions. - `GameWindow` only 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.