|
| 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