fix(inventory): sequence secondary wield blockers

Preserve retail AmmoType from CreateObject and port BlocksUseOfShield so bow, caster, and two-handed switches remove incompatible shields, while mismatched missile ammo is also returned to the pack. Each blocker remains server-confirmed before AutoWield re-enters.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-12 23:12:07 +02:00
parent 17b5712d53
commit 815ce0dec4
14 changed files with 206 additions and 16 deletions

View file

@ -19,6 +19,9 @@ internal sealed class AutoWieldController : IDisposable
| EquipMask.Held
| EquipMask.TwoHanded;
private const byte CombatUseMissile = 0x02;
private const byte CombatUseTwoHanded = 0x05;
private static readonly EquipMask[] AutoEquipOrder =
{
EquipMask.HeadWear,
@ -105,6 +108,20 @@ internal sealed class AutoWieldController : IDisposable
if (blocker is not null)
return BeginWeaponReplacement(item.ObjectId, blocker.ObjectId);
// Retail checks secondary blockers only after the primary weapon
// slot is vacant, then re-enters AutoWield after each confirmed
// move. ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0.
if (BlocksUseOfShield(item)
&& GetEquippedObjectAtLocation(
EquipMask.Shield, priority: 0, item.ObjectId) is { } shield)
return BeginWeaponReplacement(item.ObjectId, shield.ObjectId);
if (item.AmmoType is > 0
&& GetEquippedObjectAtLocation(
EquipMask.MissileAmmo, priority: 0, item.ObjectId) is { } ammo
&& ammo.AmmoType != item.AmmoType)
return BeginWeaponReplacement(item.ObjectId, ammo.ObjectId);
return SendWield(item, weaponLocation);
}
@ -270,6 +287,14 @@ internal sealed class AutoWieldController : IDisposable
&& (item.WielderId == player || item.ContainerId == player);
}
internal static bool BlocksUseOfShield(ClientObject item)
{
byte combatUse = item.CombatUse ?? 0;
return combatUse == CombatUseTwoHanded
|| (combatUse == CombatUseMissile && item.AmmoType is > 0)
|| item.Type.HasFlag(ItemType.Caster);
}
public void Dispose()
{
if (_disposed)