Clear retail AutoWield on the matching authoritative WieldObject instead of waiting for unrelated rollback bookkeeping to drain. This prevents a completed switch from consuming the next weapon activation locally, including crossbows. Co-Authored-By: Codex <codex@openai.com>
131 lines
5.4 KiB
Markdown
131 lines
5.4 KiB
Markdown
# Retail inventory weapon-switch pseudocode
|
|
|
|
## Scope
|
|
|
|
Inventory double-click of a weapon while another primary weapon is equipped,
|
|
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)`.
|
|
- `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)
|
|
|
|
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:
|
|
pending = (requestedItem = item.id, blockingItem = blocker.id)
|
|
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)
|
|
|
|
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
|
|
|
|
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`. 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.
|
|
|
|
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 the primary weapon-ready group plus retail's secondary
|
|
incompatible-shield and mismatched-ammo blockers. `CreateObject` now retains
|
|
the exact `AmmoType` needed for those decisions. Full `AutoWieldIsLegal`,
|
|
Aetheria, and dual-wield/off-hand rules remain tracked by divergence row
|
|
AP-108.
|