Skip to content

Commit 94e77ef

Browse files
committed
feat(unit): UnitCreatedBySpell + consolidate the unit-token resolver
Add `UnitCreatedBySpell(unit)` — the spell that summoned a unit (a totem's totem-drop spell, a pet/guardian's summon spell), read from the unit's UNIT_CREATED_BY_SPELL descriptor field (byte +0x230). It is the same field the engine's unit-title builder consults to pick the Pet/Minion/Guardian/ Creation title, and it is broadcast, so it resolves for any unit in range. This is the summoning spell, distinct from the (server-only) spell a totem casts. Also add a single `Game::ResolveUnitToken(token)` helper that wraps the engine token->CGUnit* resolver, and route unit/Pet.cpp through it plus the Game::Read<T> field accessors — removing the hand-rolled ResolveUnitToken_t typedef and raw reinterpret_casts from this module. The name was already referenced in Offsets.h and CLAUDE.md but had never been implemented.
1 parent 1458d60 commit 94e77ef

5 files changed

Lines changed: 94 additions & 6 deletions

File tree

docs/API.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ build instructions.
621621
- [`UnitIsPet(unit)`](#unitispetunit)
622622
- [`UnitIsOtherPlayersPet(unit)`](#unitisotherplayerspetunit)
623623
- [`UnitOwnerGUID(unit)`](#unitownerguidunit)
624+
- [`UnitCreatedBySpell(unit)`](#unitcreatedbyspellunit)
624625
- [`UnitStandState(unit)`](#unitstandstateunit)
625626
- [`UnitInRange(unit)`](#unitinrangeunit)
626627
- [`UnitDistanceSquared(unit)`](#unitdistancesquaredunit)
@@ -14876,6 +14877,26 @@ if owner == UnitGUID("player") then ... end -- is it my minion?
1487614877
Reads the same owner field the pet predicates use (`CharmedBy`, else
1487714878
`CreatedBy` in the unit's `m_objectFields`).
1487814879

14880+
### `UnitCreatedBySpell(unit)`
14881+
14882+
Returns the spell ID that summoned `unit`: the totem-drop spell for a totem,
14883+
or the summon spell for a pet or guardian. Returns `nil` for an unresolved
14884+
unit, and for a unit that no spell summoned (players and world creatures).
14885+
14886+
This is the summoning spell, not a spell that the unit casts later. The spell
14887+
that a totem casts stays on the server and never reaches the client. The
14888+
client receives this value for every summoned unit, so it works for any unit
14889+
in range, not only your own summons. Use `GetSpellInfo` to get a readable
14890+
name.
14891+
14892+
A ClassicAPI extension, not a stock WoW global.
14893+
14894+
```lua
14895+
UnitCreatedBySpell("pet") -- your pet's summon spell id
14896+
local id = UnitCreatedBySpell("target") -- target a totem → its totem spell id
14897+
if id then print(GetSpellInfo(id)) end -- prints the name, like "Searing Totem"
14898+
```
14899+
1487914900
### `UnitStandState(unit)`
1488014901

1488114902
Returns the unit's standstate as an integer, matching the modern

src/Game.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717

1818
namespace Game {
1919

20+
void *ResolveUnitToken(const char *token) {
21+
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
22+
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
23+
return fn(token);
24+
}
25+
2026
namespace Lua {
2127
// Each entry binds a typed function pointer in `Game::Lua::` to the
2228
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned

src/Game.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ inline T &Ref(uintptr_t address) {
5757
return *reinterpret_cast<T *>(address);
5858
}
5959

60+
// --- Engine primitives ------------------------------------------------------
61+
62+
// Resolve a unit token (`"player"`, `"target"`, `"party3"`, `"nameplate1"`,
63+
// …) to its live `CGUnit_C *`, through the engine's own resolver
64+
// (`FUN_RESOLVE_UNIT_TOKEN`). Returns null for a valid token that currently
65+
// has no unit (e.g. `"target"` with nothing targeted). NOTE: the engine
66+
// RAISES a Lua error for a string that isn't a valid token form (an arbitrary
67+
// name like `"Bob"`), so only pass real unit tokens — the same contract every
68+
// stock `Unit*` global carries. Centralizes the
69+
// `reinterpret_cast<…>(FUN_RESOLVE_UNIT_TOKEN)` idiom that was copy-pasted
70+
// across a dozen modules.
71+
void *ResolveUnitToken(const char *token);
72+
6073
using FrameScript_Initialize_t = bool(__fastcall *)();
6174
using LoadScriptFunctions_t = void(__fastcall *)();
6275

src/Offsets.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,19 @@ enum Offsets {
10891089
OFF_UNIT_FIELD_CHARMEDBY = 0x10,
10901090
OFF_UNIT_FIELD_CREATEDBY = 0x20,
10911091

1092+
// UNIT_CREATED_BY_SPELL — the spell that summoned this unit (descriptor
1093+
// byte +0x230, field index 0x8C). Set for every spell-summoned unit:
1094+
// totems (the shaman's totem-drop spell), pets, guardians, wild summons.
1095+
// Verified two ways: the unit-title builder `FUN_0052FD30` reads
1096+
// `[desc+0x230]` as the summoning spell (then switches on its Spell.dbc
1097+
// Effect[0] to pick "Guardian"/"Creation"), and the server stamps it
1098+
// with `m_spellInfo->Id` (tortoise-wow `EffectSummonTotem`/
1099+
// `EffectSummonGuardian`/`EffectSummon`). A broadcast field, so it's
1100+
// readable for any unit in range. `Unit::Pet` exposes it as
1101+
// `UnitCreatedBySpell`. NB: this is the SUMMONING spell — distinct from
1102+
// the spell a totem casts (that's server-side only, never broadcast).
1103+
OFF_UNIT_FIELD_CREATED_BY_SPELL = 0x230,
1104+
10921105
// Pet-vs-minion discriminator for an owned unit: `int __fastcall(unit)`.
10931106
// `0x00605570` is really the engine's **creature-type resolver** — it's
10941107
// `Script_UnitCreatureType`'s (`0x0051A280`) inner helper and returns the

src/unit/Pet.cpp

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ namespace Unit::Pet {
5656

5757
namespace {
5858

59-
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
6059
// `int __fastcall(unit)` → 1 for a controllable pet, non-1 otherwise.
6160
// The engine's pet-vs-minion title discriminator. See
6261
// FUN_UNIT_PET_MINION_CLASS.
@@ -68,30 +67,39 @@ const uint8_t *ResolveUnit(void *L) {
6867
const char *token = Game::Lua::ToString(L, 1);
6968
if (token == nullptr)
7069
return nullptr;
71-
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
72-
return static_cast<const uint8_t *>(resolve(token));
70+
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
7371
}
7472

7573
// The unit's owner/controller GUID: CharmedBy, else CreatedBy (the two
7674
// fields the engine itself uses for ownership). 0 for players, world
7775
// creatures, and anything unowned.
7876
uint64_t OwnerGuid(const uint8_t *unit) {
79-
auto *desc = *reinterpret_cast<const uint8_t *const *>(
80-
unit + Offsets::OFF_UNIT_DESCRIPTOR);
77+
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
8178
if (desc == nullptr)
8279
return 0;
8380
const int owners[] = {
8481
Offsets::OFF_UNIT_FIELD_CHARMEDBY,
8582
Offsets::OFF_UNIT_FIELD_CREATEDBY,
8683
};
8784
for (int off : owners) {
88-
const uint64_t g = *reinterpret_cast<const uint64_t *>(desc + off);
85+
const uint64_t g = Game::Read<uint64_t>(desc, off);
8986
if (g != 0)
9087
return g;
9188
}
9289
return 0;
9390
}
9491

92+
// The spell that summoned this unit (its UNIT_CREATED_BY_SPELL descriptor
93+
// field): the totem-drop spell for a totem, the summon spell for a
94+
// pet/guardian, etc. 0 for a unit not summoned by a spell (players, world
95+
// creatures) or an object with no descriptor.
96+
uint32_t CreatedBySpell(const uint8_t *unit) {
97+
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
98+
if (desc == nullptr)
99+
return 0;
100+
return Game::Read<uint32_t>(desc, Offsets::OFF_UNIT_FIELD_CREATED_BY_SPELL);
101+
}
102+
95103
// `UnitIsMinion(unit)` — true iff the unit is a player's minion (pet,
96104
// guardian, totem, or charmed creature): player-controlled but not itself
97105
// a player. NPCs, world creatures, and players return false. A bad or
@@ -184,6 +192,31 @@ int __fastcall Script_UnitOwnerGUID(void *L) {
184192
return 1;
185193
}
186194

195+
// `UnitCreatedBySpell(unit)` — the spell ID that summoned this unit, read
196+
// from its UNIT_CREATED_BY_SPELL field: the shaman totem-drop spell for a
197+
// totem, the summon spell for a pet/guardian, and so on. This is the same
198+
// field the engine's unit-title builder consults to pick the
199+
// "Pet"/"Minion"/"Guardian"/"Creation" title. It is a broadcast descriptor
200+
// field, so it works for any unit in range, not just your own summons.
201+
// Returns nil for an unresolved unit and for anything not summoned by a
202+
// spell (players, world creatures). Note this is the *summoning* spell —
203+
// the spell a totem casts is server-side only and never reaches the client.
204+
// A ClassicAPI extension — not a stock WoW global.
205+
int __fastcall Script_UnitCreatedBySpell(void *L) {
206+
const uint8_t *unit = ResolveUnit(L);
207+
if (unit == nullptr) {
208+
Game::Lua::PushNil(L);
209+
return 1;
210+
}
211+
const uint32_t spellId = CreatedBySpell(unit);
212+
if (spellId == 0) {
213+
Game::Lua::PushNil(L);
214+
return 1;
215+
}
216+
Game::Lua::PushNumber(L, static_cast<double>(spellId));
217+
return 1;
218+
}
219+
187220
} // namespace
188221

189222
static void RegisterLuaFunctions() {
@@ -192,6 +225,8 @@ static void RegisterLuaFunctions() {
192225
Game::Lua::RegisterGlobalFunction("UnitIsOtherPlayersPet",
193226
&Script_UnitIsOtherPlayersPet);
194227
Game::Lua::RegisterGlobalFunction("UnitOwnerGUID", &Script_UnitOwnerGUID);
228+
Game::Lua::RegisterGlobalFunction("UnitCreatedBySpell",
229+
&Script_UnitCreatedBySpell);
195230
}
196231

197232
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};

0 commit comments

Comments
 (0)