fix(items): preserve wand stance and corpse use order

Keep active-combat AutoWield intent through ACE's pre-wield transition and queued trailing peace notice, while allowing explicit combat input to supersede it.

Queue one-shot item interactions until the frame's movement edge has been serialized, so a movement release cannot cancel ACE's server-side corpse approach callback.

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

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 19:32:01 +02:00
parent 0392c6d721
commit e6dd8bf6fa
10 changed files with 303 additions and 22 deletions

View file

@ -516,15 +516,15 @@ 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
**2026-07-17 active-switch follow-up:** Connected primary-weapon replacement
traces exposed ACE's queued old-stance transition and trailing `NonCombat`.
`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`. It recognizes both a
pre-wield transition followed by `ready -> NonCombat` and a wholly post-wield
`NonCombat -> ready -> NonCombat` tail, then sends the normal mode request from
the trailing notice after the queued callback that caused it. An explicit user
combat request cancels settlement. This keeps the correct missile or magic 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.
@ -2319,6 +2319,15 @@ 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.
**2026-07-17 input-order follow-up:** A second connected trace showed the
remaining far-R failure as `UseDone(0)` with no `ViewContents`. Silk delivered
R before the object phase had serialized a preceding movement-key release, so
ACE received `Use` and then `MoveToState`; ACE correctly cancelled the
Use-created MoveTo callback while acdream's local MoveTo kept walking. The new
`OutboundInteractionQueue` drains keyboard Use, world double-click, and pickup
immediately after the same frame's movement output and before inbound dispatch.
The flow still sends exactly one request and relies on ACE's arrival callback.
**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,7 +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` |
| AP-118 | An AutoWield transaction begun in active combat preserves the ready mode implied by the requested weapon. After authoritative `WieldObject`, a mode that settled without a blocker transition clears immediately; local ACE's observed pre-wield transition plus `ready -> NonCombat`, or post-wield `NonCombat -> ready -> NonCombat`, causes one normal `ChangeCombatMode` request from the trailing notice. Explicit user combat input cancels settlement. 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 primary-weapon 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 explicit combat request, replacement, or 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

@ -116,11 +116,20 @@ on server-confirmed WieldObject(item):
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:
if the first notice is the retained ready mode
and no authoritative transition occurred during blocker removal:
clear the retained mode; the server already settled correctly
else if notices form NonCombat -> retained ready mode -> NonCombat:
else if blocker removal already emitted a non-ready transition
and notices finish retained ready mode -> NonCombat:
clear the retained mode
request that ready mode from ACE's trailing NonCombat notice
else if post-wield notices form NonCombat -> retained ready mode -> NonCombat:
clear the retained mode
request that ready mode from ACE's trailing NonCombat notice
on explicit user combat-mode request:
clear retained AutoWield mode intent
the user's newer request wins
on PrivateUpdatePropertyInt(property = CombatMode, value):
if value is exactly NonCombat, Melee, Missile, or Magic:
@ -145,12 +154,17 @@ 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
that emitted no blocker transition and immediately reports the desired mode
needs no request. Local ACE can begin its old-weapon transition before
`WieldObject`, report the desired mode for the new weapon, and then publish a
queued trailing `NonCombat`; it can also emit the complete
`NonCombat -> desired mode -> NonCombat` tail after `WieldObject`. 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-
transaction begun in peace sends no mode request. An explicit user combat-mode
request cancels retained settlement so the compatibility owner cannot fight a
deliberate switch to Peace. This narrow server-
compatibility adaptation is recorded as AP-118.
The request is not complete when `GetAndWieldItem` is merely sent. Retail

View file

@ -30,6 +30,7 @@ Named Sept-2013 retail symbols:
- `ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0`
- `ItemHolder::AttemptToPlaceInContainer @ 0x00588140`
- `ItemHolder::UseObject @ 0x00588A80`
- `CommandInterpreter::SendMovementEvent @ 0x006B4680`
The exact client dispatch for server event `CloseGroundContainer (0x0052)` is
at `0x0055B187` in `acclient_2013_pseudo_c.txt`.
@ -40,6 +41,10 @@ Cross-references:
`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.
- ACE `GameActionMoveToState.Handle` confirms that a movement state received
after `Use` cancels the Use-created `CreateMoveToChain`. A cancelled chain
still reaches `TryUseItem(success:false)` and produces successful `UseDone`
without calling `Container.Open`, explaining the approach-with-no-loot trace.
- 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
@ -71,6 +76,13 @@ thumb, and arrow sprites from LayoutDesc `0x2100003E`.
## Pseudocode
```text
OnInputFrame():
sample movement-key levels
serialize any movement press/release edge as MoveToState
drain later one-shot Use/PickUp inputs in original FIFO order
// A preceding movement release must reach ACE before Use. Sending it
// afterward cancels ACE's server-side approach callback.
UseWorldObject(objectId):
decision = ItemHolder policy
send Use(objectId)
@ -210,6 +222,11 @@ DragOwnedItemToExternalContainer(itemId, placement, splitAmount):
- `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.
- `OutboundInteractionQueue` owns the input-frame ordering seam. Keyboard Use,
world double-click, and keyboard pickup are captured by the Silk callback,
then drained immediately after `LocalPlayerOutboundController` serializes
that frame's movement edge and before inbound network dispatch. It sends one
request; it is not an arrival retry.
- `GameWindow` only creates the lifecycle/UI bindings and supplies network
delegates. No external-container feature body lives there.