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>
191 lines
8.8 KiB
Markdown
191 lines
8.8 KiB
Markdown
# Retail inventory weapon-switch pseudocode
|
|
|
|
## Scope
|
|
|
|
Inventory double-click of a weapon while another primary weapon is equipped,
|
|
or dragging an item onto a specific paperdoll slot, in both peace and combat
|
|
modes. The UI request sequence is shared; combat-mode animation remains
|
|
server-authoritative.
|
|
|
|
## Retail oracle
|
|
|
|
- `ItemHolder::DetermineUseResult @ 0x00588460` classifies an owned combat-use
|
|
item as `WieldRight`.
|
|
- `CPlayerSystem::UsingItem @ 0x00562F70` dispatches that result to
|
|
`AutoWield(item, SLOT_SIDE_RIGHT, 0, 1, 0, 1)`.
|
|
- `CPlayerSystem::AutoWield @ 0x00560A60` treats
|
|
`0x03500000` (`MeleeWeapon | MissileWeapon | Held | TwoHanded`) as one
|
|
weapon-ready group. If occupied, it records the requested item, sends the
|
|
blocking weapon to the player container, and does not wield the requested
|
|
item yet.
|
|
- `CPlayerSystem::RecvNotice_ServerSaysMoveItem @ 0x00563260` retries
|
|
`AutoWield` only after the server confirms the blocking item moved.
|
|
- `ACCWeenieObject::UIAttemptWield @ 0x0058D590` sends
|
|
`GetAndWieldItem(item, equipMask)`.
|
|
- `gmPaperDollUI::HandleDropRelease @ 0x004A4D80` routes an accepted physical
|
|
item through `AcceptDragObject @ 0x004A3B10`, which calls `AutoWield` (or
|
|
`AutoWear`) with the paperdoll's resolved target side. It does not send a
|
|
direct `GetAndWieldItem` while a blocker remains equipped.
|
|
- `ClientCombatSystem::OnQualityChanged @ 0x0056C1B0` reads player
|
|
PropertyInt 40 (`CombatMode`) and applies it through `SetCombatMode`.
|
|
|
|
Addresses are from `docs/research/named-retail/symbols.json`; pseudo-C is in
|
|
`docs/research/named-retail/acclient_2013_pseudo_c.txt`.
|
|
|
|
## Cross-reference
|
|
|
|
- ACE `Player_Inventory.cs` says the client normally sends the conflicting
|
|
item's dequip before `GetAndWieldItem`. `TryShuffleStance` changes a player
|
|
who is already in combat to Missile, Magic, or Melee after the new item is
|
|
equipped; it leaves a player in peace mode in peace.
|
|
- ACE `Creature_Combat.cs::SwitchCombatStyles` performs the visible
|
|
old-stance -> peace -> new-stance motion chain and sends
|
|
`PrivateUpdatePropertyInt(CombatMode, ...)`.
|
|
- holtburger independently confirms the action/event shapes:
|
|
`GetAndWieldItem { item_guid, equip_mask }`, `WieldObject`, and
|
|
PropertyInt 40 as `CombatMode`.
|
|
|
|
## Faithful pseudocode
|
|
|
|
```text
|
|
on inventory item double-click(item):
|
|
decision = DetermineUseResult(item)
|
|
if decision is WieldRight or WieldLeft or AutoSort:
|
|
auto_wield(item)
|
|
|
|
on paperdoll drop(item, target slot):
|
|
resolvedMask = resolve item.ValidLocations against the authored slot side
|
|
auto_wield(item, resolvedMask)
|
|
|
|
auto_wield(item):
|
|
if another auto-wield transaction is waiting for the server:
|
|
consume the request without sending another inventory action
|
|
|
|
if item has no valid equip location or is already equipped:
|
|
return false
|
|
|
|
if item.validLocations intersects WEAPON_READY_MASK:
|
|
desiredMask = item.validLocations & WEAPON_READY_MASK
|
|
blocker = player's equipped item intersecting WEAPON_READY_MASK
|
|
|
|
if blocker exists:
|
|
print system text "Moving <blocker name> to your backpack"
|
|
pending = (requestedItem = item.id,
|
|
blockingItem = blocker.id,
|
|
resolvedMask = desiredMask)
|
|
send PutItemInContainer(blocker.id, player.id, placement = 0)
|
|
return true
|
|
|
|
if BlocksUseOfShield(item) and a shield is equipped:
|
|
begin the same confirmed blocker transaction for the shield
|
|
|
|
if item.ammoType is not AMMO_NONE
|
|
and equipped ammo has a different ammoType:
|
|
begin the same confirmed blocker transaction for the ammo
|
|
|
|
send_wield(item, desiredMask)
|
|
return true
|
|
|
|
use the existing retail slot/auto-wear selection rules
|
|
send_wield(item, selectedMask)
|
|
|
|
on server-confirmed item move(blocker):
|
|
if blocker matches pending.blockingItem
|
|
and blocker is now loose in the player's main pack:
|
|
requested = pending.requestedItem
|
|
clear pending before re-entry
|
|
auto_wield(requested, pending.resolvedMask)
|
|
|
|
on server inventory-request failure(item, error):
|
|
if item matches pending.blockingItem or pending.requestedItem:
|
|
clear pending
|
|
|
|
send_wield(item, mask):
|
|
pending = (requestedItem = item.id, blockingItem = none)
|
|
update the retained inventory/paperdoll projection optimistically
|
|
send GetAndWieldItem(item.id, mask)
|
|
|
|
on server-confirmed WieldObject(item):
|
|
reconcile the item's optimistic rollback snapshot separately
|
|
if item matches pending.requestedItem
|
|
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
|
|
and no authoritative transition occurred during blocker removal:
|
|
clear the retained mode; the server already settled correctly
|
|
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:
|
|
CombatState.SetCombatMode(value)
|
|
```
|
|
|
|
## Animation ownership
|
|
|
|
The client must not synthesize a local bow-to-sword animation. In peace mode,
|
|
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
|
|
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. 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
|
|
`UIAttemptWield` records `prevRequest = IR_WIELD` and leaves the item waiting
|
|
until `ServerSaysMoveItem` reconciles it. acdream therefore keeps AutoWield
|
|
busy through the final authoritative `WieldObject`; allowing another switch
|
|
during the optimistic interval makes ACE reject otherwise valid items. Retail
|
|
`ACCWeenieObject::ServerSaysMoveItem @ 0x0058DBB0` clears the matching request
|
|
directly; that UI completion is intentionally independent of the object's
|
|
separate optimistic rollback counter.
|
|
|
|
## Known remaining retail surface
|
|
|
|
This port covers inventory activation and paperdoll drops through the same
|
|
primary weapon-ready transaction, plus retail's secondary incompatible-shield
|
|
and mismatched-ammo blockers. `CreateObject` retains the exact `AmmoType`
|
|
needed for those decisions, and paperdoll target-side intent survives every
|
|
authoritative blocker retry. Full `AutoWieldIsLegal` and dual-wield/off-hand
|
|
rules remain tracked by divergence row AP-108. Retail also appends
|
|
`" - cannot unwield the %s"` when `AttemptToPlaceInContainer` rejects
|
|
synchronously. ACDream's current send seam is void and ACE reports that
|
|
rejection asynchronously, so the successful status line is exact while that
|
|
failure suffix remains tracked instead of being fabricated at the wrong
|
|
transaction boundary.
|