fix(core): correct DamageType's rotated bits and ItemType's shifted craft ladder

Two enums disagreed with the retail client, and both disagreements were the
quiet kind - nothing read the wrong members, so nothing was visibly broken. They
were traps armed for the first person to write a comparison against them.

DamageType had its four drain/restore bits rotated. acdream assigned
Nether/Mana/Health/Stamina to 0x80/0x100/0x200/0x400; retail's DAMAGE_TYPE
(acclient.h:3788) assigns Health/Stamina/Mana/Nether. The ACE weenie corpus
attests retail's order independently - 0x100 Stamina, 0x200 Mana, 0x400 Nether -
and so does the vendored client-side enum catalog. Tellingly, both of acdream's
live damage-type name tables, CombatChatTranslator.FormatDamageType (ported from
holtburger) and ItemAppraisalTextFormatter.TryDamageTypeName, already used
retail's order reading the raw wire uint directly. The enum was the only thing in
the tree that was wrong. Retail's BASE_DAMAGE_TYPE (0x10000000) was also missing;
CombatChatTranslator already knew about it.

ItemType had two separate problems. The craft ladder was shifted one bit:
CraftAlchemyIntermediate sat on 0x02000000, which retail leaves unused, and an
invented CraftCookingIntermediate occupied 0x04000000, which is retail's real
alchemy-intermediate bit. The weenie corpus attests 0x04000000 as
Craft_Alchemy_Intermediate 235 times and contains no cooking-intermediate at all -
there is no such item type. Separately, the composite masks were recomputed
locally from the bits above them instead of transcribed, which is exactly how the
ladder drifted in the first place. That made Weapon (retail 0x101, melee|missile)
an exact alias of WeaponOrCaster (0x8101), and left Item at 0x830F where retail's
TYPE_ITEM is 0x2DFBEF - a mask two orders of magnitude broader. The composites are
now transcribed as literals with retail's value, not derived, and the five
retail-only masks acdream never had (portal/lockable magic targets, the
enchantable and redirectable targets, and the two vendor masks) come along.

Note for the reader wondering why the campaign trusted retail over the catalog
here: on CraftFletchingBase the catalog is the one that is wrong (it says
0x02000000; retail and acdream both say 0x01000000). No single oracle was assumed
correct - retail's header decided, with the weenie corpus as the tiebreak.

Behavior: no production code reads any changed member. The only reference in the
tree is a test that wants a nonzero HookItemTypes and does not care which. So no
branch changes and no wire behavior moves - but the values did change, which is
why this is a fix commit and not a data commit. No divergence-register row: these
were unintentional errors, now retired, not deviations we chose.

RetailEnumConformanceTests pins both enums to the acclient.h tables, asserts
acdream declares nothing retail does not, and calls out the two specific traps -
that 0x02000000 stays unclaimed, and that Weapon and WeaponOrCaster are no longer
the same value.

Core tests 3,726 -> 3,785.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-29 01:21:10 +02:00
parent 251dd68a92
commit f3e95a3ebd
3 changed files with 196 additions and 13 deletions

View file

@ -10,9 +10,17 @@ namespace AcDream.Core.Items;
/// <summary>
/// AC's <c>ItemType</c> is a 32-bit flags enum — a single dat weenie can
/// assert multiple type bits. From <c>ACE.Entity.Enum.ItemType</c>
/// cross-checked against the decompile paperdoll tooltip dispatcher.
/// Full bit list in the research doc §1.
/// assert multiple type bits. Verbatim retail <c>ITEM_TYPE</c>
/// (<c>docs/research/named-retail/acclient.h:3300</c>).
///
/// <para>The craft ladder used to be shifted: <c>CraftAlchemyIntermediate</c> sat on
/// 0x02000000 (retail leaves that bit unused and puts alchemy-intermediate on
/// 0x04000000), and an invented <c>CraftCookingIntermediate</c> occupied the real
/// alchemy-intermediate bit. The ACE weenie corpus attests 0x04000000 =
/// Craft_Alchemy_Intermediate 235 times and contains no cooking-intermediate at all.
/// The composite masks were likewise recomputed locally rather than transcribed, which
/// made <c>Weapon</c> an alias of <c>WeaponOrCaster</c> and left <c>Item</c> two orders
/// of magnitude narrower than retail's. Full bit list in the research doc §1.</para>
/// </summary>
[Flags]
public enum ItemType : uint
@ -43,17 +51,27 @@ public enum ItemType : uint
CraftCookingBase = 0x00400000,
CraftAlchemyBase = 0x00800000,
CraftFletchingBase = 0x01000000,
CraftAlchemyIntermediate= 0x02000000,
CraftCookingIntermediate= 0x04000000,
// 0x02000000 is deliberately unused in retail's ladder.
CraftAlchemyIntermediate= 0x04000000,
CraftFletchingIntermediate = 0x08000000,
LifeStone = 0x10000000,
TinkeringTool = 0x20000000,
TinkeringMaterial = 0x40000000,
Gameboard = 0x80000000u,
Vestements = Armor | Clothing,
Weapon = MeleeWeapon | MissileWeapon | Caster,
WeaponOrCaster = Weapon,
Item = Weapon | Armor | Clothing | Jewelry | Container,
// Composite masks, transcribed verbatim rather than recomputed from the bits
// above — retail's TYPE_ITEM in particular is far broader than any obvious
// union, and deriving these locally is how the craft ladder drifted.
Vestements = 0x00000006, // TYPE_VESTEMENTS
Weapon = 0x00000101, // TYPE_WEAPON: melee | missile, no caster
WeaponOrCaster = 0x00008101, // TYPE_WEAPON_OR_CASTER
LockableMagicTarget = 0x00000280, // TYPE_LOCKABLE_MAGIC_TARGET
RedirectableItemEnchantmentTarget = 0x00008107,
PortalMagicTarget = 0x10010000, // TYPE_PORTAL_MAGIC_TARGET
ItemEnchantableTarget = 0x00088B8F, // TYPE_ITEM_ENCHANTABLE_TARGET
Item = 0x002DFBEF, // TYPE_ITEM
VendorShopkeep = 0x480467A7, // TYPE_VENDOR_SHOPKEEP
VendorGrocer = 0x00446220, // TYPE_VENDOR_GROCER
}
/// <summary>