Skip to content

Commit e793f80

Browse files
committed
feat(spell): add minimap tracking enumeration
Vanilla 1.12 only stores the single active tracking spell (VAR_ACTIVE_TRACKING_SPELL, 0xBC6378) and never enumerates the tracking abilities the player knows. Add GetNumTrackingTypes/GetTrackingInfo/ SetTracking, which rebuild that list by scanning the player spellbook for spells whose EffectApplyAuraName is Track Creatures/Resources/Stealthed (44/45/151) -- the same discriminator the engine's own active-tracker updater (FUN_004E4170) uses. This reproduces pfUI's hardcoded tracking table with no table to maintain and auto-covers server-custom trackers (e.g. Turtle's Find Trees, 52917). - GetTrackingInfo returns name, texture, active, category ("spell"), spellID; active is read from VAR_ACTIVE_TRACKING_SPELL. - SetTracking selects a tracker by tail-calling the engine's Script_CastSpell(slot, "spell"). - Add offsets VAR_ACTIVE_TRACKING_SPELL and FUN_SCRIPT_CAST_SPELL (0x4B42F0), both with RE-provenance comments. - Document under docs/API.md (Tracking section). Verified in-game on a Hunter (8 trackers) and a Dwarf racial (1): active flips on SetTracking, texture matches native GetTrackingTexture, out-of-range returns nil.
1 parent ded767b commit e793f80

3 files changed

Lines changed: 266 additions & 0 deletions

File tree

docs/API.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,11 @@ build instructions.
584584
- [`GetTotemDuration(slot)`](#gettotemdurationslot)
585585
- [`TargetTotem(slot)`](#targettotemslot)
586586

587+
- [Tracking](#tracking)
588+
- [`GetNumTrackingTypes()`](#getnumtrackingtypes)
589+
- [`GetTrackingInfo(index)`](#gettrackinginfoindex)
590+
- [`SetTracking(index)`](#settrackingindex)
591+
587592
- [TradeSkillUI](#tradeskillui)
588593
- [`C_TradeSkillUI.GetTradeSkillListLink()`](#c_tradeskilluigettradeskilllistlink)
589594
- [`C_TradeSkillUI.GetCraftListLink()`](#c_tradeskilluigetcraftlistlink)
@@ -14141,6 +14146,62 @@ Looks up the totem creature's live GUID (object-manager scan for the
1414114146
player-owned creature of the slot's tracked entry) and sets the target
1414214147
through the same engine path `TargetUnit` uses (`CMSG_SET_SELECTION`).
1414314148

14149+
## Tracking
14150+
14151+
Vanilla 1.12 stores only the tracking spell that is active now. It does
14152+
not keep a list of the tracking spells you know. These globals add that
14153+
list. A minimap addon can then build a tracking menu without a hardcoded
14154+
spell table.
14155+
14156+
A tracking spell is a spell in your spellbook that finds creatures,
14157+
resources, or hidden units. This is the same test the engine uses to find
14158+
the active tracker. The list covers Find Herbs, Find Minerals, Find
14159+
Treasure, the Hunter Track spells, Sense Undead, Sense Demons, and Track
14160+
Humanoids. It also covers server-added trackers, such as Turtle's Find
14161+
Trees. You do not have to maintain a spell list.
14162+
14163+
### `GetNumTrackingTypes()`
14164+
14165+
Returns the number of tracking spells in your spellbook.
14166+
14167+
```lua
14168+
GetNumTrackingTypes() -- for example, 8 for a Hunter
14169+
```
14170+
14171+
### `GetTrackingInfo(index)`
14172+
14173+
`index` starts at 1 and follows spellbook order. The function returns
14174+
these five values, or `nil` if `index` is out of range:
14175+
14176+
| # | Value | Type | Notes |
14177+
|---|-------|------|-------|
14178+
| 1 | `name` | string | Localized spell name. |
14179+
| 2 | `texture` | string | Icon path. Give it to `texture:SetTexture(...)`. `nil` if the icon is missing. |
14180+
| 3 | `active` | boolean | `true` when this tracker is the one now in effect. |
14181+
| 4 | `category` | string | Always `"spell"`. Vanilla has no non-spell trackers. |
14182+
| 5 | `spellID` | number | The tracking spell's ID. This is a ClassicAPI extra. The retail 5th value is `nested`, which has no meaning here. |
14183+
14184+
```lua
14185+
local name, texture, active, category, spellID = GetTrackingInfo(1)
14186+
```
14187+
14188+
### `SetTracking(index)`
14189+
14190+
Turns on the tracking spell at `index`. In vanilla, you select a tracker
14191+
when you cast its spell. This function casts it. For an out-of-range
14192+
index, it does nothing. To turn tracking off, use the built-in
14193+
`CancelTrackingBuff()`.
14194+
14195+
```lua
14196+
for i = 1, GetNumTrackingTypes() do
14197+
local name = GetTrackingInfo(i)
14198+
if name == "Find Herbs" then
14199+
SetTracking(i)
14200+
break
14201+
end
14202+
end
14203+
```
14204+
1414414205
## TradeSkillUI
1414514206

1414614207
Backports the 2.x+ **trade-skill list link** — the shareable

src/Offsets.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4109,6 +4109,15 @@ enum Offsets {
41094109
VAR_PET_SPELLBOOK = 0x00B6F098,
41104110
SPELLBOOK_MAX_SLOTS = 0x400,
41114111

4112+
// Active minimap-tracking spell ID (0 = none). Vanilla tracks exactly
4113+
// one active tracker as a plain int here, derived from the player's
4114+
// active Track Creatures/Resources/Stealthed aura by `FUN_004E4170`
4115+
// (an aura-descriptor observer registered by `FUN_004E4100`). Read by
4116+
// `Script_GetTrackingTexture` (`0x004E4A20`), `Script_CancelTrackingBuff`
4117+
// (`0x004E4A80`), and `GameTooltip:SetTrackingSpell` (`0x00532C50`);
4118+
// `Spell::Tracking::GetTrackingInfo` reads it to report `active`.
4119+
VAR_ACTIVE_TRACKING_SPELL = 0x00BC6378,
4120+
41124121
// Player-spell-knowledge bitmap — `[VAR_PLAYER_SPELL_BITMAP]` is a
41134122
// pointer to a dword bitmap covering all spellIDs the player has
41144123
// learned, including talent passives, racials, profession recipes,
@@ -4379,6 +4388,15 @@ enum Offsets {
43794388
// results). Callable directly from C++ — push args on stack, call.
43804389
FUN_SCRIPT_CAST_SPELL_BY_NAME = 0x004B4AB0,
43814390

4391+
// `Script_CastSpell` — engine's Lua wrapper for the slot-based
4392+
// `CastSpell(spellbookSlot, bookType)` global. `int __fastcall(void *L)`.
4393+
// Parses (slot, bookType) from stack[1]/[2] via `FUN_004B3EC0`, then
4394+
// dispatches through `FUN_SPELL_CAST_DISPATCH` with the current target
4395+
// (or self). Raises "Invalid spell slot in CastSpell" for a bad slot.
4396+
// Callable from C++ by preparing the stack and tail-calling it (the
4397+
// `Spell::Tracking::SetTracking` "select = cast" path does this).
4398+
FUN_SCRIPT_CAST_SPELL = 0x004B42F0,
4399+
43824400
// Inner spell-cast dispatcher — `__fastcall(slot, bookType, targetGuidLo,
43834401
// targetGuidHi)`. `Script_CastSpellByName` calls this after resolving
43844402
// the spell name to a spellbook slot. For ground-target spells, the

src/spell/Tracking.cpp

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 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 General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
#include "Game.h"
15+
#include "Offsets.h"
16+
#include "dbc/Lookup.h"
17+
#include "spell/Lookup.h"
18+
19+
#include <cstdint>
20+
21+
// `GetNumTrackingTypes()` / `GetTrackingInfo(index)` / `SetTracking(index)` —
22+
// backport of the minimap-tracking enumeration the modern client exposes.
23+
//
24+
// Vanilla 1.12 never enumerates the tracking abilities the player KNOWS — it
25+
// only stores the single currently-active tracker as a plain spellID in
26+
// `VAR_ACTIVE_TRACKING_SPELL`, surfaced through `GetTrackingTexture()` /
27+
// `CancelTrackingBuff()` / `GameTooltip:SetTrackingSpell()`. 3.3.5's
28+
// `GetTrackingInfo` iterates a spellbook-derived list of tracking spells and
29+
// reports name/texture/active/category per entry; we rebuild that list here.
30+
//
31+
// A tracking spell is identified exactly the way the engine's own active-
32+
// tracker updater `FUN_004E4170` identifies one: a Spell.dbc record with an
33+
// `EffectApplyAuraName` of Track Creatures (44), Track Resources (45), or
34+
// Track Stealthed (151). That effect-based test reproduces pfUI's hardcoded
35+
// spellID/icon table (Find Herbs/Minerals/Treasure/Trees, every Hunter
36+
// Track*, Sense Undead/Demons, Track Humanoids) with no table to maintain,
37+
// and auto-covers server-custom trackers such as Turtle's Find Trees (52917).
38+
namespace Spell::Tracking {
39+
40+
// Tracking aura effect types — SPELL_AURA_TRACK_* ApplyAura names, the same
41+
// values `FUN_004E4170` scans a spell's three effects for.
42+
static constexpr int32_t AURA_TRACK_CREATURES = 44;
43+
static constexpr int32_t AURA_TRACK_RESOURCES = 45;
44+
static constexpr int32_t AURA_TRACK_STEALTHED = 151;
45+
46+
// True iff `spellID`'s record has any of the three Track* effects.
47+
static bool IsTrackingSpell(int spellID) {
48+
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
49+
if (record == nullptr)
50+
return false;
51+
auto *aura = Game::Ptr<const int32_t>(
52+
record, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
53+
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
54+
if (aura[i] == AURA_TRACK_CREATURES || aura[i] == AURA_TRACK_RESOURCES ||
55+
aura[i] == AURA_TRACK_STEALTHED)
56+
return true;
57+
}
58+
return false;
59+
}
60+
61+
// Walks the player spellbook in slot order counting tracking spells. If
62+
// `wantNth >= 1` and an Nth tracking spell exists, writes its spellID and
63+
// 1-based spellbook slot to `*outSpellID` / `*outSlot`. Returns the total
64+
// number of tracking spells found (so callers detect out-of-range by
65+
// comparing the return against `wantNth`).
66+
//
67+
// The spellbook array is zero-padded past the populated count (see
68+
// `Spell::Lookup::FindSpellbookSlot`), so a 0 entry is just an empty slot —
69+
// skip it and keep scanning the full range rather than break, matching the
70+
// existing spellbook walkers.
71+
static int EnumerateTracking(int wantNth, int *outSpellID, int *outSlot) {
72+
auto *book = reinterpret_cast<const int *>(
73+
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELLBOOK));
74+
int found = 0;
75+
for (int i = 0; i < Offsets::SPELLBOOK_MAX_SLOTS; ++i) {
76+
const int spellID = book[i];
77+
if (spellID <= 0 || !IsTrackingSpell(spellID))
78+
continue;
79+
++found;
80+
if (found == wantNth) {
81+
if (outSpellID != nullptr)
82+
*outSpellID = spellID;
83+
if (outSlot != nullptr)
84+
*outSlot = i + 1;
85+
}
86+
}
87+
return found;
88+
}
89+
90+
// `GetNumTrackingTypes()` -> number of tracking spells in the player's book.
91+
static int __fastcall Script_GetNumTrackingTypes(void *L) {
92+
Game::Lua::PushNumber(L,
93+
static_cast<double>(EnumerateTracking(0, nullptr, nullptr)));
94+
return 1;
95+
}
96+
97+
// `GetTrackingInfo(index)` -> name, texture, active, category, spellID.
98+
// - index is 1-based into the tracking list (spellbook order).
99+
// - texture is the SpellIcon.dbc path (nil if the icon is missing).
100+
// - active is a boolean: the tracker currently in effect.
101+
// - category is always "spell" — vanilla has no non-spell ("other")
102+
// trackers, unlike 3.3.5's class-masked table.
103+
// - spellID is a ClassicAPI extension (the modern 5th return is `nested`,
104+
// which has no meaning here); handy since selecting/among trackers is
105+
// spellID-driven.
106+
// Returns nil for an out-of-range index.
107+
static int __fastcall Script_GetTrackingInfo(void *L) {
108+
if (!Game::Lua::IsNumber(L, 1)) {
109+
Game::Lua::Error(L, "Usage: GetTrackingInfo(index)");
110+
return 0;
111+
}
112+
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
113+
if (index < 1)
114+
return 0;
115+
116+
int spellID = 0;
117+
int slot = 0;
118+
if (EnumerateTracking(index, &spellID, &slot) < index)
119+
return 0; // index past the number of tracking types -> nil
120+
121+
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
122+
const int locale = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
123+
const char *name =
124+
Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
125+
126+
const char *texture = nullptr;
127+
const int iconID = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_ICON_ID);
128+
if (auto *iconRec = DBC::Record(Offsets::VAR_SPELL_ICON_RECORDS,
129+
Offsets::VAR_SPELL_ICON_COUNT,
130+
static_cast<uint32_t>(iconID))) {
131+
texture = Game::Read<const char *>(iconRec, Offsets::OFF_SPELLICON_PATH);
132+
}
133+
134+
const bool active =
135+
Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_ACTIVE_TRACKING_SPELL)) ==
136+
spellID;
137+
138+
Game::Lua::PushString(L, name); // 1. name (PushString handles NULL -> nil)
139+
Game::Lua::PushString(L, texture); // 2. texture (icon path) or nil
140+
Game::Lua::PushBool(L, active); // 3. active
141+
Game::Lua::PushString(L, "spell"); // 4. category
142+
Game::Lua::PushNumber(L, static_cast<double>(spellID)); // 5. spellID (extension)
143+
return 5;
144+
}
145+
146+
// `SetTracking(index)` — selects (activates) the tracking spell at `index`.
147+
// Vanilla has no "set tracking" primitive: choosing a tracker IS casting its
148+
// spell (the tracking dropdown does exactly this). So we resolve the entry's
149+
// spellbook slot and tail-call the engine's own `Script_CastSpell(slot,
150+
// "spell")` — reusing its slot bounds-check, target resolution, and error
151+
// handling rather than reconstructing the cast path. Returns nil for an
152+
// out-of-range index.
153+
static int __fastcall Script_SetTracking(void *L) {
154+
if (!Game::Lua::IsNumber(L, 1)) {
155+
Game::Lua::Error(L, "Usage: SetTracking(index)");
156+
return 0;
157+
}
158+
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
159+
if (index < 1)
160+
return 0;
161+
162+
int spellID = 0;
163+
int slot = 0;
164+
if (EnumerateTracking(index, &spellID, &slot) < index)
165+
return 0;
166+
167+
// Rebuild the stack as CastSpell(slot, "spell") and hand off to the
168+
// engine handler. `slot` is 1-based, matching what a Lua `CastSpell`
169+
// caller passes.
170+
Game::Lua::SetTop(L, 0);
171+
Game::Lua::PushNumber(L, static_cast<double>(slot));
172+
Game::Lua::PushString(L, "spell");
173+
using CastSpell_t = int(__fastcall *)(void *);
174+
auto castSpell = reinterpret_cast<CastSpell_t>(Offsets::FUN_SCRIPT_CAST_SPELL);
175+
return castSpell(L);
176+
}
177+
178+
static void RegisterLuaFunctions() {
179+
Game::Lua::RegisterGlobalFunction("GetNumTrackingTypes",
180+
&Script_GetNumTrackingTypes);
181+
Game::Lua::RegisterGlobalFunction("GetTrackingInfo", &Script_GetTrackingInfo);
182+
Game::Lua::RegisterGlobalFunction("SetTracking", &Script_SetTracking);
183+
}
184+
185+
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
186+
187+
} // namespace Spell::Tracking

0 commit comments

Comments
 (0)