Skip to content

Commit 37e8b32

Browse files
committed
Add C_Spell.GetSpellMechanicByID(spellID) -> (mechanicID, name)
Reads the spell-level Mechanic field straight out of Spell.dbc (record +0x14) and resolves the name from SpellMechanic.dbc. Covers every spell the client knows (not just the player's spellbook), with no caching or network round-trip since the DBCs are resident from boot. Replaces hand-maintained spellID -> mechanic tables addons keep because vanilla exposes no Lua reader for the field (e.g. SuperCleveRoidMacros' CCSpellMechanics, used for [cc:stun] conditionals). Returns: nil for an invalid spellID; (0, nil) for a known spell with no mechanic; (mechanicID, name) otherwise. The name is always the enUS column (locale-independent) since it's a stable token consumers string-match against; nil if the mechanic has no record / no English name (numeric ID still returned). Mechanic offset (+0x14) verified two ways: schema anchor off the verified Attributes@+0x18, and FUN_006e9ca0 (mechanic-immunity check) reading spellRec[+0x14] as the spell's mechanic. SpellMechanic.dbc name at record +0x04 confirmed in-game: 118 -> (17,"polymorphed"), 339 -> (7,"rooted"), 133 -> (0,nil). The .dbc has 27 rows; only the enUS name column is populated.
1 parent bdc574d commit 37e8b32

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

src/Offsets.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,6 +2381,18 @@ enum Offsets {
23812381
VAR_SPELL_RECORDS = 0x00C0D788, // SpellRecord *records[spellID]
23822382
VAR_SPELL_RECORD_COUNT = 0x00C0D78C, // max spellID
23832383

2384+
// SpellMechanic.dbc — maps a SpellMechanic ID to its localized
2385+
// name. Standard 5-DWORD DBC instance at 0x00C0D7BC; records ptr at
2386+
// +0x08, count at +0x0C (see docs/DBCs.md). Records are 1-based
2387+
// (records[mechanicID]); the engine only ever uses the table for
2388+
// existence/bounds checks (FUN_006e9ca0, FUN_00612df0, FUN_006e8eb0
2389+
// case 0x8d all do `records[id] != 0`), never reading the name — so
2390+
// the name offset below follows the canonical 2-column
2391+
// (ID + localized Name) DBC schema rather than an engine-reader anchor.
2392+
VAR_SPELLMECHANIC_RECORDS = 0x00C0D7C4, // SpellMechanicRecord *records[mechanicID]
2393+
VAR_SPELLMECHANIC_COUNT = 0x00C0D7C8, // max mechanic ID
2394+
OFF_SPELLMECHANIC_NAME = 0x04, // char *name[9], locale-indexed
2395+
23842396
// Spell.dbc School field — 0-based integer at record +0x04.
23852397
// Verified empirically against Fireball (133) → School=2 (Fire)
23862398
// and Frostbolt (116) → School=4 (Frost) on Octo 1.12.1. Vanilla
@@ -3710,6 +3722,15 @@ enum Offsets {
37103722
OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME = 0x16C, // int32[3]
37113723
OFF_SPELL_RECORD_EFFECT_MISC_VALUE = 0x1A8, // int32[3]
37123724
SPELL_RECORD_EFFECT_COUNT = 3,
3725+
3726+
// Spell.dbc Mechanic field — the spell-level SpellMechanic ID (→
3727+
// SpellMechanic.dbc), 0 = none. Field 5 of the record
3728+
// (ID, School, Category, castUI, Dispel, Mechanic), which lands at
3729+
// +0x14 given Attributes (field 6) is verified at +0x18. Cross-
3730+
// confirmed by FUN_006e9ca0 (the mechanic-immunity check), which
3731+
// reads `spellRec[+0x14]` as the spell's mechanic when the per-
3732+
// effect EffectMiscValue is 0. Read by C_Spell.GetSpellMechanicByID.
3733+
OFF_SPELL_RECORD_MECHANIC = 0x14,
37133734
SPELL_AURA_MOD_SHAPESHIFT = 36,
37143735
SPELL_AURA_MOUNTED = 78,
37153736

src/spell/Mechanic.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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_Spell.GetSpellMechanicByID(spellID)` -> (mechanicID, localizedName)
15+
//
16+
// Reads the spell-level Mechanic field straight out of Spell.dbc, so it
17+
// covers *every* spell the client knows — not just the player's
18+
// spellbook — with no caching or network round-trip (Spell.dbc is fully
19+
// resident from boot). This replaces hand-maintained spellID -> mechanic
20+
// tables (e.g. SuperCleveRoidMacros' `CCSpellMechanics`) that addons keep
21+
// solely because vanilla exposes no Lua reader for the field.
22+
//
23+
// Returns:
24+
// - nothing (nil) for an invalid / out-of-range spell ID
25+
// - (0, nil) for a known spell that carries no mechanic
26+
// - (mechanicID, name) otherwise. `name` is the enUS SpellMechanic.dbc
27+
// name (always English, locale-independent — it's a stable token
28+
// consumers match against). nil only if the mechanic has no record /
29+
// no English name; the numeric ID is still returned in that case.
30+
//
31+
// The mechanic numbering is WoW's standard SpellMechanic enum
32+
// (1 = Charm, 5 = Fear, 7 = Root, 12 = Stun, 17 = Polymorph, ...);
33+
// addons map those IDs to their own names as needed.
34+
35+
#include "Game.h"
36+
#include "Offsets.h"
37+
#include "dbc/Lookup.h"
38+
#include "spell/Arg.h"
39+
#include "spell/Lookup.h"
40+
41+
#include <cstdint>
42+
43+
namespace Spell::Mechanic {
44+
45+
// SpellMechanic.dbc name — always the enUS (index 0) column, regardless
46+
// of client locale. The mechanic name is a stable token consumers key
47+
// off (`"rooted"`, `"stunned"`, …), so it must be identical on every
48+
// client; a localized value would break cross-locale string matching.
49+
// Vanilla only populates the enUS column of this table anyway. Returns
50+
// nullptr if the record is missing or the English slot is empty.
51+
static const char *MechanicName(uint32_t mechanicID) {
52+
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELLMECHANIC_RECORDS,
53+
Offsets::VAR_SPELLMECHANIC_COUNT, mechanicID);
54+
if (rec == nullptr)
55+
return nullptr;
56+
auto names = reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELLMECHANIC_NAME);
57+
const char *s = names[0]; // enUS
58+
return (s != nullptr && s[0] != '\0') ? s : nullptr;
59+
}
60+
61+
static int __fastcall Script_GetSpellMechanicByID(void *L) {
62+
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
63+
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
64+
if (record == nullptr)
65+
return 0; // nil for invalid / out-of-range spell IDs
66+
67+
const uint32_t mechanicID =
68+
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_SPELL_RECORD_MECHANIC);
69+
70+
Game::Lua::PushNumber(L, static_cast<double>(mechanicID));
71+
72+
if (mechanicID == 0) {
73+
Game::Lua::PushNil(L); // known spell, no mechanic
74+
return 2;
75+
}
76+
77+
// Locale name with enUS fallback (nullptr → nil if even English is
78+
// empty); the caller still gets the numeric ID either way.
79+
Game::Lua::PushString(L, MechanicName(mechanicID)); // pushstring(NULL) → pushnil
80+
return 2;
81+
}
82+
83+
static void RegisterLuaFunctions() {
84+
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellMechanicByID",
85+
&Script_GetSpellMechanicByID);
86+
}
87+
88+
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
89+
90+
} // namespace Spell::Mechanic

0 commit comments

Comments
 (0)