Skip to content

Commit 17f44f9

Browse files
committed
Add C_Item.GetEnchantInfo(enchantID)
Resolves an item-enchantment ID (the enchantID from C_Item.GetWeaponEnchantInfo, and item permanent-enchant IDs) into its SpellItemEnchantment.dbc record: { enchantID, name, effects = {{type,amount,arg},...}, spellID } - name: localized display name ("Crusader", "Brilliant Mana Oil"). - effects: each active slot's ITEM_ENCHANTMENT_TYPE / amount / arg. - spellID: convenience for spell-type enchants (1/3/7) — the proc/ equip/use spellID, e.g. Crusader -> 20007 "Holy Strength", to chain into C_Spell.GetSpellDescription. Record layout verified by parsing the on-disk DBC against known records (Crusader 1900: type 1, arg 20007; Sharpened+3 id 13: type 2, amount 3; Reinforced Armor+8 id 15: type 4, amount 8): Type[3]@+0x04, Amount[3]@+0x10, EffectArg[3]@+0x28, Name[8]@+0x34. The engine's by-ID readers only touch name/flags/condition, so the effect columns couldn't be pinned from disassembly — the extracted dbc/ dump was the oracle. Verified in-game across all three example enchants. Placed in C_Item (not C_Spell): the id originates from C_Item.GetWeaponEnchantInfo and the concept is an item enchantment. Source-item is not derivable (no back-reference, no client reverse index).
1 parent 73d9010 commit 17f44f9

4 files changed

Lines changed: 227 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
3838
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
3939
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
4040
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
41-
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
41+
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
4242
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
4343
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
4444
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |

docs/API.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7432,6 +7432,70 @@ addons reading positions 4..8 by index still work.
74327432
Equivalent to the extension of `GetWeaponEnchantInfo` introduced
74337433
in 3.x.
74347434

7435+
### `C_Item.GetEnchantInfo(enchantID)`
7436+
7437+
Resolves an item-enchantment ID — the `enchantID` returned by
7438+
[`C_Item.GetWeaponEnchantInfo`](#c_itemgetweaponenchantinfo) for a
7439+
weapon's temporary enchant, and the same IDs item permanent enchants
7440+
use — into a table:
7441+
7442+
```lua
7443+
local info = C_Item.GetEnchantInfo(enchantID)
7444+
-- info.enchantID = <id>
7445+
-- info.name = "Crusader" -- localized display name
7446+
-- info.effects = { {type=1, amount=0, arg=20007} }
7447+
-- info.spellID = 20007 -- spell-type enchants only
7448+
```
7449+
7450+
```lua
7451+
-- A proc/equip enchant → chain its spellID into C_Spell:
7452+
local info = C_Item.GetEnchantInfo(1900) -- "Crusader"
7453+
if info.spellID then
7454+
print(C_Spell.GetSpellDescription(info.spellID)) -- the proc's text
7455+
end
7456+
```
7457+
7458+
`effects` is an array of the record's non-empty effect slots, each
7459+
`{ type, amount, arg }` where `type` is the standard
7460+
`ITEM_ENCHANTMENT_TYPE`:
7461+
7462+
| type | meaning | carries |
7463+
|------|---------|---------|
7464+
| 1 | combat-proc spell | `arg` = spellID |
7465+
| 2 | weapon damage | `amount` = +damage |
7466+
| 3 | equip spell / aura | `arg` = spellID |
7467+
| 4 | resistance / armor | `amount` = +value |
7468+
| 5 | stat | `arg` = stat index, `amount` = value |
7469+
| 6 | totem ||
7470+
| 7 | use spell | `arg` = spellID |
7471+
7472+
For spell types (1/3/7) `arg` is a spellID feedable into
7473+
[`C_Spell.GetSpellInfo`](#getspellinfospellid--getspellinfoslot-booktype) /
7474+
`GetSpellDescription`; the first such id is also surfaced at top level
7475+
as `spellID` for convenience (absent for non-spell enchants like
7476+
sharpening stones).
7477+
7478+
Returns `nil` for a non-numeric / non-positive id, an out-of-range
7479+
id, or a record with no name.
7480+
7481+
Reads `SpellItemEnchantment.dbc` (records `0x00C0D7D8`, count
7482+
`0x00C0D7DC`) — the 24-column table every enchant ID indexes:
7483+
`Type[3]@+0x04`, `Amount[3]@+0x10`, `EffectArg[3]@+0x28`,
7484+
`Name[8]@+0x34` (locale-indexed). The layout was verified by parsing
7485+
the on-disk DBC against known records (Crusader 1900 → type 1, arg
7486+
20007; Sharpened +3 → type 2, amount 3). Fully resident from boot, so
7487+
it answers for any enchant ID with no caching or round-trip.
7488+
7489+
Lives in `C_Item` (not `C_Spell`) because the id originates from
7490+
`C_Item.GetWeaponEnchantInfo` and the concept is an item enchantment —
7491+
`SpellItemEnchantment` is just the DBC's internal name (enchants are
7492+
*implemented* via spell effects).
7493+
7494+
> **Not derivable: the source item.** The enchant record holds no
7495+
> back-reference to the item that applied it, and vanilla has no
7496+
> client-side reverse index (enchantID → item). Finding it would need
7497+
> an external scraped DB (pfQuest/Questie-style).
7498+
74357499
### `IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`
74367500

74377501
Classify a spell as offensive (`IsHarmfulSpell`) or non-offensive

src/Offsets.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,6 +2428,29 @@ enum Offsets {
24282428
VAR_SPELLMECHANIC_COUNT = 0x00C0D7C8, // max mechanic ID
24292429
OFF_SPELLMECHANIC_NAME = 0x04, // char *name[9], locale-indexed
24302430

2431+
// SpellItemEnchantment.dbc — the table a weapon/item enchant ID
2432+
// (e.g. the enchantID from `C_Item.GetWeaponEnchantInfo`) indexes.
2433+
// Records-ptr at instance +0x08, count at +0x0C (instance 0x00C0D7D0).
2434+
// 24-DWORD records (recordSize 0x60). Layout fully verified by
2435+
// parsing the on-disk SpellItemEnchantment.dbc against known records
2436+
// (Crusader 1900: Type[0]=1, Arg[0]=20007 "Holy Strength";
2437+
// Sharpened+3 id 13: Type[0]=2, Amount[0]=3; Reinforced Armor+8 id 15:
2438+
// Type[0]=4, Amount[0]=8). OFF_..._NAME (+0x34) is cross-confirmed by
2439+
// two engine readers (FUN_00496170 / FUN_00495d60) that push
2440+
// `[rec + 0x34 + VAR_LOCALE_INDEX*4]` as the display name.
2441+
// Type[i] : ITEM_ENCHANTMENT_TYPE — 1=combat-proc spell,
2442+
// 2=damage, 3=equip spell, 4=resistance, 5=stat,
2443+
// 6=totem, 7=use spell. 0 = empty slot.
2444+
// Amount[i] : magnitude for damage/resist/stat types (min; vanilla
2445+
// min==max). 0 for spell types (value lives in the spell).
2446+
// Arg[i] : spellID for spell types (1/3/7); stat index for STAT.
2447+
VAR_SPELLITEMENCHANT_RECORDS = 0x00C0D7D8, // SpellItemEnchantmentRecord *records[enchantID]
2448+
VAR_SPELLITEMENCHANT_COUNT = 0x00C0D7DC, // max enchant ID
2449+
OFF_SPELLITEMENCHANT_TYPE = 0x04, // int Type[3]
2450+
OFF_SPELLITEMENCHANT_AMOUNT = 0x10, // int Amount[3] (EffectPointsMin)
2451+
OFF_SPELLITEMENCHANT_ARG = 0x28, // int EffectArg[3]
2452+
OFF_SPELLITEMENCHANT_NAME = 0x34, // char *name[8], locale-indexed
2453+
24312454
// Spell.dbc School field — 0-based integer at record +0x04.
24322455
// Verified empirically against Fireball (133) → School=2 (Fire)
24332456
// and Frostbolt (116) → School=4 (Frost) on Octo 1.12.1. Vanilla

src/item/EnchantInfo.cpp

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU Lesser General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU Lesser General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `C_Item.GetEnchantInfo(enchantID)` -> table | nil
15+
//
16+
// Resolves an item-enchantment ID — the kind `C_Item.GetWeaponEnchantInfo`
17+
// returns for a weapon's temporary enchant (poisons, oils, sharpening
18+
// stones, shaman imbues, …), and the same IDs item permanent enchants
19+
// use — into its `SpellItemEnchantment.dbc` record and returns a table:
20+
//
21+
// {
22+
// enchantID = <id>,
23+
// name = "<localized display name>", -- "Crusader", "+8 Spirit"
24+
// effects = { -- 1..3 active effect slots
25+
// { type = <ITEM_ENCHANTMENT_TYPE>, amount = <int>, arg = <int> },
26+
// ...
27+
// },
28+
// spellID = <int>, -- present only for spell-type enchants (proc/
29+
// -- equip/use): the EffectArg of the first such
30+
// -- effect, e.g. Crusader -> 20007 "Holy Strength".
31+
// }
32+
//
33+
// `effects[i].type` is the standard ITEM_ENCHANTMENT_TYPE:
34+
// 1 = combat-proc spell (arg = spellID)
35+
// 2 = weapon damage (amount = +dmg)
36+
// 3 = equip spell / aura (arg = spellID)
37+
// 4 = resistance / armor (amount = +value)
38+
// 5 = stat (arg = stat index, amount = value)
39+
// 6 = totem, 7 = use spell
40+
// For spell types (1/3/7) `arg` is a spellID you can feed straight into
41+
// `C_Spell.GetSpellInfo` / `GetSpellDescription`; `spellID` is surfaced
42+
// at top level as a convenience for that common case.
43+
//
44+
// Returns nil for a non-numeric / non-positive id, an out-of-range id,
45+
// or a record with no name. Lives in C_Item (not C_Spell) because the
46+
// id originates from `C_Item.GetWeaponEnchantInfo` and the concept is an
47+
// item enchantment — `SpellItemEnchantment` is just the DBC's internal
48+
// name (enchants are *implemented* via spell effects).
49+
//
50+
// Not derivable: the source item. The record holds no back-reference to
51+
// the item that applied the enchant, and vanilla has no client-side
52+
// reverse index (enchantID -> item).
53+
54+
#include "Game.h"
55+
#include "Offsets.h"
56+
#include "dbc/Lookup.h"
57+
58+
#include <cstdint>
59+
60+
namespace Item::EnchantInfo {
61+
62+
namespace {
63+
64+
constexpr int kNumEffects = 3;
65+
constexpr int kNumNameLocales = 8; // Name[8]; field after is the locale mask
66+
67+
// Spell-type enchants whose EffectArg is a spellID (proc / equip / use).
68+
bool IsSpellType(int type) { return type == 1 || type == 3 || type == 7; }
69+
70+
int __fastcall Script_C_Item_GetEnchantInfo(void *L) {
71+
if (!Game::Lua::IsNumber(L, 1))
72+
return 0; // nil
73+
const int enchantID = static_cast<int>(Game::Lua::ToNumber(L, 1));
74+
if (enchantID <= 0)
75+
return 0;
76+
77+
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
78+
Offsets::VAR_SPELLITEMENCHANT_COUNT,
79+
static_cast<uint32_t>(enchantID));
80+
if (rec == nullptr)
81+
return 0; // OOR id or unpopulated slot -> nil
82+
83+
// Localized display name (8-slot locale array at +0x34). Clamp the
84+
// locale index to the array bounds — index 8 would read the locale
85+
// mask column that follows. enUS (0) fallback for empty slots.
86+
auto names = reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELLITEMENCHANT_NAME);
87+
int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
88+
if (locale < 0 || locale >= kNumNameLocales)
89+
locale = 0;
90+
const char *name = names[locale];
91+
if (name == nullptr || name[0] == '\0')
92+
name = names[0]; // enUS fallback
93+
if (name == nullptr || name[0] == '\0')
94+
return 0; // unnamed record -> nil
95+
96+
auto type = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_TYPE);
97+
auto amount = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
98+
auto arg = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_ARG);
99+
100+
Game::Lua::NewTable(L);
101+
Game::Lua::SetFieldNumber(L, "enchantID", enchantID);
102+
Game::Lua::SetFieldString(L, "name", name);
103+
104+
// effects = { {type,amount,arg}, ... } — only non-empty slots.
105+
Game::Lua::PushString(L, "effects");
106+
Game::Lua::NewTable(L);
107+
int n = 0;
108+
int spellID = 0;
109+
for (int i = 0; i < kNumEffects; ++i) {
110+
if (type[i] == 0)
111+
continue;
112+
if (spellID == 0 && IsSpellType(type[i]) && arg[i] != 0)
113+
spellID = arg[i];
114+
++n;
115+
Game::Lua::PushNumber(L, n); // 1-based array index
116+
Game::Lua::NewTable(L);
117+
Game::Lua::SetFieldNumber(L, "type", type[i]);
118+
Game::Lua::SetFieldNumber(L, "amount", amount[i]);
119+
Game::Lua::SetFieldNumber(L, "arg", arg[i]);
120+
Game::Lua::SetTable(L, -3); // effects[n] = entry
121+
}
122+
Game::Lua::SetTable(L, -3); // result.effects = effects
123+
124+
if (spellID != 0)
125+
Game::Lua::SetFieldNumber(L, "spellID", spellID);
126+
127+
return 1;
128+
}
129+
130+
void RegisterLuaFunctions() {
131+
Game::Lua::RegisterTableFunction("C_Item", "GetEnchantInfo",
132+
&Script_C_Item_GetEnchantInfo);
133+
}
134+
135+
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
136+
137+
} // namespace
138+
139+
} // namespace Item::EnchantInfo

0 commit comments

Comments
 (0)