EquipItem slot validation off by one — EquipmentSlot↔ItemEquipSlot ordinal cast mismatch #793

Open
opened 2026-07-22 06:59:52 +00:00 by spikerj · 0 comments
Owner

Found by the coverage wave (spikersoft-backend PR #454) while testing BaseZone.ProcessEquipItemCommand.

PlayerEntity.EquipItem (SpikerSoft.Common IGameEntity.cs ~line 800) validates with:

if (definition.EquipSlot != (ItemEquipSlot)(int)equipSlot && definition.EquipSlot != ItemEquipSlot.None)
    return (false, null);

But the enums are misaligned by one:

  • EquipmentSlot (IGameCommand.cs): Weapon = 0, Armor = 1, HpPotion = 2, MpPotion = 3, ...
  • ItemEquipSlot (ItemDefinition.cs): None = 0, Weapon = 1, Armor = 2, HpPotion = 3, MpPotion = 4

So the cast maps EquipmentSlot.Weapon (0)ItemEquipSlot.None (0), and every item equips only through the command slot one BELOW its real one:

  • EquipItemCommand { EquipmentSlot = Weapon } with a weapon (e.g. wooden_staff) → rejected
  • EquipItemCommand { EquipmentSlot = Armor } with that same weapon → accepted (equipment dict then stores the weapon under Armor)

Impact: clients sending the semantically correct slot can never equip anything; whatever works today works by accident of the off-by-one. Fix by mapping explicitly (switch) or aligning the enums — note EquipmentSlot.Armor is documented "Legacy - maps to Torso", so an explicit mapping function is probably the safe fix.

Until fixed, BaseZoneItemCommandTests pins the current off-by-one behavior as characterization tests referencing this issue — they will fail when the mapping is corrected (then flip the expectations).

Found by the coverage wave (spikersoft-backend PR #454) while testing `BaseZone.ProcessEquipItemCommand`. `PlayerEntity.EquipItem` (SpikerSoft.Common `IGameEntity.cs` ~line 800) validates with: ```csharp if (definition.EquipSlot != (ItemEquipSlot)(int)equipSlot && definition.EquipSlot != ItemEquipSlot.None) return (false, null); ``` But the enums are misaligned by one: - `EquipmentSlot` (IGameCommand.cs): `Weapon = 0, Armor = 1, HpPotion = 2, MpPotion = 3, ...` - `ItemEquipSlot` (ItemDefinition.cs): `None = 0, Weapon = 1, Armor = 2, HpPotion = 3, MpPotion = 4` So the cast maps `EquipmentSlot.Weapon (0)` → `ItemEquipSlot.None (0)`, and every item equips only through the command slot one BELOW its real one: - `EquipItemCommand { EquipmentSlot = Weapon }` with a weapon (e.g. `wooden_staff`) → **rejected** - `EquipItemCommand { EquipmentSlot = Armor }` with that same weapon → **accepted** (equipment dict then stores the weapon under `Armor`) Impact: clients sending the semantically correct slot can never equip anything; whatever works today works by accident of the off-by-one. Fix by mapping explicitly (switch) or aligning the enums — note `EquipmentSlot.Armor` is documented "Legacy - maps to Torso", so an explicit mapping function is probably the safe fix. Until fixed, `BaseZoneItemCommandTests` pins the current off-by-one behavior as characterization tests referencing this issue — they will fail when the mapping is corrected (then flip the expectations).
Sign in to join this conversation.