feat(items): port retail external-container looting
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>
This commit is contained in:
parent
48a118db91
commit
20ce67b625
27 changed files with 1750 additions and 45 deletions
|
|
@ -2279,22 +2279,22 @@ R5/R6 touches the action list. **Where:**
|
|||
|
||||
---
|
||||
|
||||
## #196 — External-container lifecycle and retained window events are missing
|
||||
## #196 — External-container lifecycle and retained window events are missing — DONE 2026-07-17
|
||||
|
||||
**Status:** OPEN
|
||||
**Status:** CLOSED
|
||||
**Severity:** MEDIUM
|
||||
**Filed:** 2026-07-02
|
||||
**Component:** ui / net
|
||||
|
||||
**Description:** `UiRoot`'s window registry flips `Visible` with no lifecycle event, toolbar-button sync is manually pushed at each mutation site, and acdream has no retained external/ground-container window. The original issue incorrectly assigned `NoLongerViewingContents (0x0195)` to closing or navigating the owned inventory's side bags. Named-retail evidence instead ties it to `ClientUISystem.groundObject`: replacing or closing an external container sends `0x0195` exactly once. The owned `gmInventoryUI` side-bag path has no corresponding static send.
|
||||
**Description:** `UiRoot`'s window registry originally flipped `Visible` with no lifecycle event, toolbar-button sync was manually pushed at each mutation site, and acdream had no retained external/ground-container window. The original issue incorrectly assigned `NoLongerViewingContents (0x0195)` to closing or navigating the owned inventory's side bags. Named-retail evidence instead ties it to replacing `ClientUISystem.groundObject`; the authored close/range path sends `Use(root)` and waits for server event `CloseGroundContainer (0x0052)`. Owned `gmInventoryUI` packs never send `0x0195`.
|
||||
|
||||
**Root cause / status:** PARTIAL 2026-07-11. Typed retained-window visibility events and subscriber-driven toolbar state shipped in Wave 4.4d, replacing manual sync at mutation sites. The issue remains open for the separate external-container owner whose ground-object replacement/close path must emit one `0x0195`. Do not add that packet to owned side-bag navigation without a live packet trace.
|
||||
**Resolution:** Typed retained-window visibility and subscriber-driven toolbar state shipped in Wave 4.4d. The remaining external lifetime shipped 2026-07-17: Core `ExternalContainerState` owns expected/current root identity; App `ExternalContainerLifecycleController` emits exactly one `0x0195` only on root replacement; `GameEventWiring` accepts the expected root `ViewContents`, preserves nested snapshots, and retires the complete temporary projection tree on authoritative `0x0052`. `ExternalContainerController` mounts retail LayoutDesc `0x21000008`, including its horizontal lists/scrollbar, nested-container selection, exact close/range behavior, loot-to-pack, return-to-container, and partial-stack requests. Owned inventory no longer exposes any `0x0195` capability.
|
||||
|
||||
**Files:** `src/AcDream.App/UI/RetailWindowManager.cs`, `src/AcDream.App/UI/RetailUiRuntime.cs`; future external-container controller.
|
||||
**Files:** `src/AcDream.Core/Items/ExternalContainerState.cs`, `src/AcDream.App/World/ExternalContainerLifecycleController.cs`, `src/AcDream.App/UI/Layout/ExternalContainerController.cs`, `src/AcDream.Core.Net/GameEventWiring.cs`, `src/AcDream.Core/Items/ClientObjectTable.cs`.
|
||||
|
||||
**Research:** `docs/research/2026-07-02-ui-architecture-review.md`; correction oracle `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md`.
|
||||
**Research:** `docs/research/2026-07-17-retail-external-container-looting-pseudocode.md`.
|
||||
|
||||
**Acceptance:** replacing/closing an external container sends `NoLongerViewingContents` exactly once; owned main/side-pack navigation and closing send nothing absent contrary trace evidence; lifecycle subscribers replace manual toolbar sync calls; repeat hiding is idempotent.
|
||||
**Acceptance:** replacement sends `NoLongerViewingContents` once; authored close/range sends `Use` once and waits for `0x0052`; owned main/side-pack navigation sends neither; root/nested projections retire without deleting objects; looting and partial/full transfers remain server-authoritative. Focused Core, Core.Net, and App tests pin each path; visual acceptance remains the normal user gate.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,14 @@ radar, inventory, paperdoll, toolbar, use/examine consumers, and plugins;
|
|||
Temporary pointer modes are separate App orchestration in `InteractionState` and
|
||||
must never become a competing selection owner.
|
||||
|
||||
External-container state follows the same ownership rule. Core
|
||||
`ExternalContainerState` owns the expected/current `ClientUISystem::groundObject`
|
||||
identity, Core.Net applies authoritative `ViewContents` and
|
||||
`CloseGroundContainer` events, and App `ExternalContainerLifecycleController`
|
||||
owns the replacement-only `NoLongerViewingContents` side effect. The retained
|
||||
`ExternalContainerController` is a projection over that state; mounting or
|
||||
disabling the gameplay UI cannot change the session lifetime.
|
||||
|
||||
Every retained gameplay window has a typed `RetailWindowHandle` and at most one
|
||||
`IRetainedPanelController` lifecycle owner. Multi-controller windows use
|
||||
`RetainedPanelControllerGroup`; the manager disposes controllers exactly once in
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| ~~AP-101~~ | **RETIRED 2026-07-11 (Wave 4.4e)** — toolbar control coverage is complete: DAT panel launchers, combat, Use/Examine, selected-object strip, shortcuts, and exact thrown-weapon/separate-ammo count resolution on authored element `0x10000194`. | `src/AcDream.Core/Items/ToolbarAmmoPolicy.cs`; `src/AcDream.App/UI/Layout/ToolbarController.cs` | — | — | `gmToolbarUI::UpdateAmmoID @ 0x004BF210`; `UpdateAmmoNumber @ 0x004BE9E0` |
|
||||
| AP-104 | Vitals detail element `0x100004A9` and root `HideDetail`/`ShowDetail` transitions are not wired | `src/AcDream.App/UI/Layout/VitalsController.cs` | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00`; `PostInit @ 0x004BFCE0` |
|
||||
| AP-105 | **PARTIAL 2026-07-13** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`), and both chat backends share typed client-command routing plus one retained `ChatVM` for reply state. Retained chat still lacks complete tab/filter/unread, social availability, incoming squelch enforcement, and focus-opacity behavior. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, command ownership, and outer maximize geometry work; later chat work consolidates the remaining presentation/filter state | Tabs are no-ops, squelched lines can still render, contextual social actions are absent, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods |
|
||||
| AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
|
||||
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
|
||||
| AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **Primary replacement retired from this row 2026-07-14:** inventory activation and paperdoll drops share the confirmed blocker transaction, preserve explicit slot intent, relocate an already-worn item to an explicitly selected compatible slot, and emit retail's successful move-to-backpack status. **Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; primary weapon, incompatible shield, and mismatched ammo blockers sequence through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
|
||||
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ W1 plan: [`docs/superpowers/plans/2026-06-02-unified-cell-graph-stage1.md`](../s
|
|||
| B.4b | Outbound Use handler wiring + 4 bonus fixes (L.2g slices 1b+1c, double-click detection, DoubleClick gate fix). Shipped 2026-05-13 (branch `claude/compassionate-wilson-23ff99`, merge pending). Closes #57. Files #58 (door swing animation, M1-deferred). `WorldPicker.BuildRay` + `Pick` (ray-sphere entity pick with inside-sphere guard); `GameWindow.OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `_entitiesByServerGuid` reverse-lookup dict + ServerGuid→entity.Id translation in `OnLiveStateUpdated` (L.2g slice 1c — THE actual blocker); `InputDispatcher` double-click detection 500ms threshold (binding was dead code without it); `CollisionExemption.ShouldSkip` widened to ETHEREAL-alone (ACE Door.Open() sends `state=0x0001000C`, not `0x14`). M1 demo target "open the inn door" verified at Holtburg inn doorway. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4b-plan.md`](../superpowers/plans/2026-05-13-phase-b4b-plan.md). Handoff: [`docs/research/2026-05-13-b4b-shipped-handoff.md`](../research/2026-05-13-b4b-shipped-handoff.md). | Live ✓ |
|
||||
| B.4c | Door swing animation. Shipped 2026-05-13 (branch `claude/phase-b4c-door-anim`, merge pending). Closes #58. Files #61 (AnimationSequencer link→cycle boundary flash; low-severity polish) + #62 (PARTSDIAG null-guard; latent). Spawn-time `AnimationSequencer` registration for door entities in `GameWindow.OnLiveEntitySpawnedLocked`: initial cycle seeded from `spawn.PhysicsState` (Off for closed, On for open). Shared `IsDoorName` / `IsDoorSpawn` helpers. `[door-cycle]` diagnostic in `OnLiveMotionUpdated` (gated on `ACDREAM_PROBE_BUILDING`). Bonus stance-value fix: `NonCombat = 0x3D` not `0x01` (wrong value caused doors to render halfway underground via empty sequencer frames). Visual-verified 2026-05-13 at Holtburg inn doorway: swing-open + swing-close cycles both play. M1 demo target "open the inn door" now has full visual feedback. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4c-plan.md`](../superpowers/plans/2026-05-13-phase-b4c-plan.md). Handoff: [`docs/research/2026-05-13-b4c-shipped-handoff.md`](../research/2026-05-13-b4c-shipped-handoff.md). | Live ✓ |
|
||||
| B.5 | Ground-item pickup (F-key, close-range path). Shipped 2026-05-14 (branch `claude/phase-b5-pickup`, merge pending). Closes M1 demo target 4/4 *"pick up an item"*. New `InteractRequests.BuildPickUp(seq, itemGuid, containerGuid, placement)` builds the 24-byte `PutItemInContainer (0xF7B1/0x0019)` wire body verified against `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs`. New private `GameWindow.SendPickUp(uint itemGuid)` helper mirrors `SendUse`'s gate-on-InWorld pattern; `case InputAction.SelectionPickUp` in `OnInputAction` switch routes the F-key through `_selectedGuid`. **Bonus wire-handler fix (Task 2b):** ACE despawns picked-up items via `GameMessagePickupEvent (0xF74A)`, not the `GameMessageDeleteObject (0xF747)` we already handled — surfaced during visual testing (item kept rendering on ground after successful server-side pickup). New `PickupEvent.cs` parser + `WorldSession` dispatch branch adapt to `DeleteObject.Parsed` and reuse the existing `EntityDeleted → OnLiveEntityDeleted → RemoveLiveEntityByServerGuid` chain. Files #63 (server-initiated `MoveToObject` auto-walk not honored — out-of-range pickup / double-click fails server-side timeout) + #64 (local-player pickup animation does not render). Visual-verified 2026-05-14 at Holtburg: 3 successful close-range pickups (Pink Taper + Violet Tapers), item despawns locally as ACE acks. Plan: [`docs/superpowers/plans/2026-05-14-phase-b5-pickup.md`](../superpowers/plans/2026-05-14-phase-b5-pickup.md). Handoff: [`docs/research/2026-05-14-b5-shipped-handoff.md`](../research/2026-05-14-b5-shipped-handoff.md). | Live ✓ |
|
||||
| B.5+ external-container looting | 2026-07-17. Retail `ClientUISystem::groundObject` lifetime plus `gmExternalContainerUI` LayoutDesc `0x21000008`: authoritative root/nested `ViewContents`, root-replacement `NoLongerViewingContents`, authored close/range `Use` → `CloseGroundContainer`, temporary projection-tree teardown, horizontal item lists/scrollbar, nested-container browsing, double-click loot, drag return, and partial-stack requests. Owned side packs no longer send the external-only packet. Research: [`docs/research/2026-07-17-retail-external-container-looting-pseudocode.md`](../research/2026-07-17-retail-external-container-looting-pseudocode.md). Closes #196 and retires AP-106. | Tests ✓; live visual gate pending |
|
||||
| Indoor lighting + rendering — Phase 1 (diagnostics) | Five `[indoor-*]` probes wired through new `AcDream.Core.Rendering.RenderingDiagnostics` static class + DebugVM mirrors + DebugPanel checkboxes. `WbMeshAdapter` emits `[indoor-upload] requested/completed`; `WbDrawDispatcher` emits `[indoor-walk]`, `[indoor-lookup]`, `[indoor-xform]`, `[indoor-cull]` per cell entity. All rate-limited via per-cellId frame counter; lookup probe uses high-bit-tagged key namespace to avoid cross-probe suppression. Holtburg `ACDREAM_PROBE_INDOOR_ALL=1` capture identified 26/123 cells silently failing — confirmed H1 (WB swallowed exception). Spec: [`docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md`](../superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md). Plan: [`docs/superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md`](../superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md). Capture: [`docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md`](../research/2026-05-19-indoor-cell-rendering-probe-capture.md). | Tests ✓ |
|
||||
| Indoor lighting + rendering — Phase 2 (fix) | Three-component diagnostic-driven fix for missing-floor bug. Component 1: `WbMeshAdapter` captures the `Task<ObjectMeshData?>` from `PrepareMeshDataAsync` and attaches a `ContinueWith` for EnvCell ids — surfaces faulted-task exceptions + clean-null returns. Component 2: replaced `NullLogger<ObjectMeshManager>` with a Console-backed `ConsoleErrorLogger<T>` so WB's intentional `_logger.LogError(ex, ...)` at the swallow site at `ObjectMeshManager.cs:589` writes `[wb-error]` lines. **Root cause definitively identified in one capture: `ArgumentOutOfRangeException` from `DatReaderWriter.Setup.Unpack` at WB's `PrepareEnvCellMeshData` line 1223 — `TryGet<Setup>(stab.Id, ...)` was called blindly on every `envCell.StaticObjects` id without checking the Setup-prefix bit. GfxObj-typed stabs (0x01xxxxxx) caused mid-deserialization throws, bubbling up to PrepareMeshData's outer catch which silently returned null. Entire cell upload failed, room mesh never reached `_renderData`.** Component 3 fix: one-line type-check guard `(stab.Id & 0xFF000000u) == 0x02000000u && _dats.Portal.TryGet<Setup>(stab.Id, out var stabSetup)`. Committed to WB submodule on branch `acdream-fix-floor-rendering` at SHA `34460c4` — needs submodule pointer advance at merge time. **Verification: 0 [wb-error] (was 385), 0 NULL_RESULT (was 55), Holtburg 123/123 cells complete (was 97/123). User visually confirmed floors render in Holtburg Inn.** Surfaced 9 pre-existing indoor bugs (see-through floor, indoor collision, stairs, walls, click-thru, indoor lighting artifacts, atmospheric-lighting-on-stabs, slope terrain lighting) — all filed in `docs/ISSUES.md` for follow-up phases. Cause report: [`docs/research/2026-05-19-indoor-cell-rendering-cause.md`](../research/2026-05-19-indoor-cell-rendering-cause.md). Verification: [`docs/research/2026-05-19-indoor-cell-rendering-verification.md`](../research/2026-05-19-indoor-cell-rendering-verification.md). Plan: [`docs/superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md`](../superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md). | Live ✓ |
|
||||
| C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]` → `[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue