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:
parent
d51a0fc825
commit
0392c6d721
17 changed files with 631 additions and 81 deletions
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue