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

@ -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.