fix(items): finish corpse pickup and combat switching

Port retail's first-slot ShowPendingInPlayer path for double-click loot and carry the current owned-container destination through deferred pickup. Retire the previous ground-container view as soon as a replacement is requested so its range close cannot cancel ACE's active MoveTo chain.

Preserve active-combat weapon intent across ACE's authoritative wand-to-missile stance tail while leaving peace-mode switches unchanged.

Release build succeeds and all 5,885 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 18:35:29 +02:00
parent d51a0fc825
commit 0392c6d721
17 changed files with 631 additions and 81 deletions

View file

@ -516,6 +516,18 @@ optimistic pre-confirm projection instead of treating equipment as backpack
contents. The connected gate completed NonCombat → Missile → NonCombat →
Missile, including a Jump press, without a server rejection.
**2026-07-17 active-switch follow-up:** A connected Magic wand -> missile
weapon trace exposed ACE's queued `Magic -> Melee -> NonCombat -> Missile ->
NonCombat` tail. `AutoWieldController` now retains the desired ready mode
through every confirmed blocker move and, only for a transaction that began
in active combat, arms settlement on authoritative `WieldObject`. An already-
correct first mode notice clears it; ACE's observed `NonCombat -> Missile ->
NonCombat` sequence sends the normal mode request from its trailing notice,
after the queued callback that caused that notice.
This keeps the missile bar present while retaining
ACE ownership of every state and motion; peace-mode switching is unchanged.
The narrow ACE compatibility difference is registered as AP-118.
**Research:** `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`
**Acceptance:** Relaunch with the crossbow already equipped. The combat button
@ -2294,6 +2306,19 @@ R5/R6 touches the action list. **Where:**
**Research:** `docs/research/2026-07-17-retail-external-container-looting-pseudocode.md`.
**2026-07-17 interaction-routing follow-up:** The production binding routes
keyboard R through the same application Use adapter as retained double-click,
preserving approach-then-send completion. It also supplies the live external
root to `DetermineUseResult`, so double-clicking a corpse child requests
loot-to-pack rather than ordinary item use.
**2026-07-17 pending/replacement follow-up:** Double-click loot now follows
`ShowPendingInPlayer`: it inserts a waiting projection in slot 1 of the current
owned pack and sends that same destination/placement on the wire. A new corpse
request now retires the previous ground-object presentation immediately, as
retail `SetGroundObject` does. This removes the stale range watcher whose close
Use canceled ACE's active MoveTo chain before the new corpse could open.
**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; corpse Use never attempts to pick up the corpse; a full-stack loot drop immediately shows a ghosted pending copy at the chosen destination slot and confirmation/failure clears it; canonical transfers remain server-authoritative. Focused Core, Core.Net, and App tests pin each path; visual acceptance remains the normal user gate.
---

View file

@ -215,6 +215,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-115 | The DAT-authored portal-space viewport and its animation `SoundTweakedHook` are live, but the separate `ClientUISystem` enter/exit sound enums and centered repeating `"In Portal Space - Please Wait..."` display string are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; teleport event switch in `GameWindow.cs` | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
| AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
| AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, an immediately correct mode clears settlement; local ACE's observed `NonCombat -> ready mode -> NonCombat` tail causes one normal `ChangeCombatMode` request from the trailing notice. Retail's client does not need this extra request against the retail server. | `src/AcDream.App/UI/AutoWieldController.cs`; production binding in `GameWindow.cs` | Local ACE queues a trailing NonCombat callback during wand-to-bow replacement and rejects an earlier request while the shuffle is busy; responding to the authoritative notice that completes that exact sequence orders the ordinary request after it without suppressing any server state | A non-ACE server that emits a different intermediate sequence can retain the settlement until a later replacement/logout clears it; peace-mode equips send none | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0`; ACE `Player_Inventory.TryShuffleStance` / `TryDequipObjectWithNetworking` |
## 4. Temporary stopgap (TS) — 33 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)

View file

@ -111,6 +111,17 @@ on server-confirmed WieldObject(item):
and pending.blockingItem is none:
clear pending
if the transaction began in an active combat mode:
// ACE compatibility boundary; see below.
retain the ready mode implied by the confirmed equip location
on combat-mode notices after confirmed WieldObject:
if the first notice is the retained ready mode:
clear the retained mode; the server already settled correctly
else if notices form NonCombat -> retained ready mode -> NonCombat:
clear the retained mode
request that ready mode from ACE's trailing NonCombat notice
on PrivateUpdatePropertyInt(property = CombatMode, value):
if value is exactly NonCombat, Melee, Missile, or Magic:
CombatState.SetCombatMode(value)
@ -119,11 +130,28 @@ on PrivateUpdatePropertyInt(property = CombatMode, value):
## Animation ownership
The client must not synthesize a local bow-to-sword animation. In peace mode,
ACE leaves `CombatMode` as `NonCombat`. In combat mode, ACE selects the new
weapon's stance and sends the authoritative motion stream plus PropertyInt 40.
The inventory controller only sequences the two wire requests and waits for
the old weapon's confirmed move; the existing motion and equipped-child
render paths consume the server events.
ACE leaves `CombatMode` as `NonCombat`. The inventory controller sequences the
two wire requests and waits for the old weapon's confirmed move; the existing
motion and equipped-child render paths consume the server events.
### ACE active-combat compatibility boundary
A connected 2026-07-17 trace of Magic wand -> missile weapon showed ACE emit
`Magic -> Melee -> NonCombat -> Missile -> NonCombat`. This is a consequence of
ACE `Player_Inventory.TryShuffleStance` plus its queued dequip callbacks: the
server does select Missile for the new weapon, but a later queued callback
publishes NonCombat after the authoritative `WieldObject`. Retail's client does
not need a second request against the retail server.
acdream therefore preserves the user's desired ready mode only when AutoWield
begins in active combat. Authoritative `WieldObject` arms settlement. A server
whose first subsequent notice is already the desired mode needs no request.
Local ACE instead emits `NonCombat -> desired mode -> NonCombat`; the client
sends one normal `ChangeCombatMode` request from that trailing notice,
necessarily after the queued callback that caused it. All resulting state and animation remain server-
authoritative. No update is ignored, delayed, or locally fabricated, and a
transaction begun in peace sends no mode request. This narrow server-
compatibility adaptation is recorded as AP-118.
The request is not complete when `GetAndWieldItem` is merely sent. Retail
`UIAttemptWield` records `prevRequest = IR_WIELD` and leaves the item waiting

View file

@ -22,6 +22,10 @@ Named Sept-2013 retail symbols:
- `UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`
- `ClientUISystem::SetGroundObject @ 0x00564510`
- `ClientUISystem::OnViewContents @ 0x00565870`
- `CPlayerSystem::PlaceInBackpack @ 0x0055D8C0`
- `gmInventoryUI::RecvNotice_ShowPendingInPlayer @ 0x004A6B50`
- `gmInventoryUI::RecvNotice_EndPendingInPlayer @ 0x004A68B0`
- `UIElement_ItemList::ItemList_InsertItem @ 0x004E3F40`
- `ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680`
- `ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0`
- `ItemHolder::AttemptToPlaceInContainer @ 0x00588140`
@ -69,9 +73,22 @@ thumb, and arrow sprites from LayoutDesc `0x2100003E`.
```text
UseWorldObject(objectId):
decision = ItemHolder policy
if decision is SetGroundObject:
expectedExternalRoot = objectId
send Use(objectId)
if decision is SetGroundObject:
SetGroundObject(objectId, notifyServer = true)
SetGroundObject(newRoot, notifyServer):
if currentExternalRoot == newRoot:
return
oldRoot = currentExternalRoot
if oldRoot != 0:
retire oldRoot's temporary contents tree
publish SetGroundObject(0) to the external panel
if notifyServer:
send NoLongerViewingContents(oldRoot)
currentExternalRoot = newRoot
expectedExternalRoot = newRoot
// The panel remains empty until ViewContents for newRoot is accepted.
UseCorpse(corpseId):
// A corpse is a TYPE_CONTAINER carrying BF_STUCK | BF_CORPSE.
@ -86,11 +103,6 @@ OnViewContents(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)
@ -138,6 +150,14 @@ OnExternalItemDoubleClick(itemId):
ItemHolder.UseObject(itemId)
// Because item.ContainerId is currentExternalRoot, retail policy emits
// PlaceInBackpack; the server remains authoritative for the move.
destination = current open owned pack
destinationList.RemoveTrailingEmptyItem()
destinationList.AddEmptySlot(at index 0)
destinationList.AddItem(itemId)
destinationList.pendingItem.SetWaitingState(true)
send PutItemInContainer(itemId, destination, placement = 0)
// The pending item therefore reserves the FIRST slot and shifts the
// existing packed list to the right until the server confirms or rejects.
DragExternalItemToOwnedInventory(itemId, placement, splitAmount):
if splitAmount < full stack:
@ -171,8 +191,10 @@ DragOwnedItemToExternalContainer(itemId, placement, splitAmount):
## 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.
identity and transition ordering. A replacement request retires current
presentation immediately, matching `SetGroundObject`; accepted
`ViewContents` later opens the requested root. 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