Files
ClassicAPI/src/action/Info.cpp
T
Brues d9102bae3b Relicense source headers from LGPL to GPL v3
The repo was internally inconsistent: the LICENSE file is GPL v3 but every
src/ header carried the LGPL v3 notice and the README said "LGPL v3 or
later". LGPL was also the wrong fit — this is an injected mod plus a
bundled addon, not a library other code links against, so LGPL's linking
exception buys nothing.

It's also required: src/voicechat/Tts.cpp is ported from awesome_wotlk's
VoiceChat.cpp, which is GPL v3, and a derivative of GPL code cannot be
relicensed under more permissive terms. Dependencies stay compatible —
MinHook (BSD-2) and the nampower-mirrored packet shapes (BSD) are
GPL-compatible, and SAPI is covered by the system-library exception.

Swept "GNU Lesser General Public License" -> "GNU General Public License"
across all 257 src/ headers and changed the README license line to match
the GPL v3 LICENSE file. Comment-only; no build impact.
2026-06-29 16:38:16 -05:00

155 lines
5.8 KiB
C++

// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetActionInfo(slot)` — modern-WoW-style action descriptor lookup.
// Returns `actionType, id, subType` for the slot, or nil if empty.
//
// The vanilla 1.12.1 engine stores action descriptors as packed
// uint32s in `VAR_ACTION_TABLE` (`0x00BC6980`, 120 slots). Top 4 bits
// are the type tag; the `0x40000000` tag is ambiguous between
// macros and bag-items and is disambiguated by checking the per-
// character macro-slot map at `VAR_MACRO_SLOT_MAP` (36 entries).
//
// empty slot: nil
// spell action: "spell", spellID, "spell"
// macro: "macro", macroIndex (1-based)
// bag item: "item", nil (see note below)
// item-by-id: "item", itemID
//
// The subType is always "spell" for spell-type entries — the engine
// helper hardcodes pet=0 for entries on this table. Pet-bar actions
// live in a separate table; that's `PetHasActionBar` + the pet-action
// helpers, not this function.
//
// The macro discriminator is the same trick SuperWoWhook uses in its
// modified `GetActionText`: walk the 36-entry macro-slot map and check
// whether `entry & 0xBFFFFFFF` matches one of those macro IDs. If yes,
// the slot holds a macro; if no, it's a bag-item.
//
// **Bag-item itemID note.** For real bag items (top-4 bits = `0x4`
// and the payload isn't in the macro-slot map), the engine stores a
// bag-instance key, not the itemID. Resolving to itemID requires
// walking the per-character action-item hash table at `[0x00BDCC54]`
// and reading a struct field whose offset we haven't fully derived
// yet. For now we report the action *type* ("item") but return nil
// for the id. Callers that need it can fall back to
// `GetActionTexture` + Lua-side item lookups.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Action::Info {
namespace {
using SlotToSpell_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outPetFlag);
uint32_t ReadEntry(int slot0) {
auto *base = reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE));
return base[slot0];
}
// Returns 1-based macro slot index if `key` matches any entry in the
// macro-slot map, or 0 if no match. `key` is the action entry with the
// `0x40000000` type bit stripped (i.e. `entry & 0xBFFFFFFF`).
int FindMacroSlot(uint32_t key) {
auto *map = reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
if (map[i] == key)
return i + 1;
}
return 0;
}
int __fastcall Script_GetActionInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: GetActionInfo(slot)");
return 0;
}
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS) {
Game::Lua::PushNil(L);
return 1;
}
const uint32_t entry = ReadEntry(slot0);
if (entry == 0) {
Game::Lua::PushNil(L);
return 1;
}
const uint32_t type = entry & Offsets::ACTION_TYPE_MASK;
if (type == Offsets::ACTION_TYPE_SPELL) {
// For spell entries the engine helper just returns the entry as
// spellID; we still call through it so the petFlag behavior
// stays in sync with what other action readers see (the helper
// also handles the case where the slot is somehow invalid).
auto fn = reinterpret_cast<SlotToSpell_t>(
static_cast<uintptr_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL));
uint32_t petFlag = 0;
const uint32_t spellID = fn(static_cast<uint32_t>(slot0), &petFlag);
if (spellID == 0) {
Game::Lua::PushNil(L);
return 1;
}
(void)petFlag; // hardcoded 0 in the engine helper for this table
Game::Lua::PushString(L, "spell");
Game::Lua::PushNumber(L, static_cast<double>(spellID));
Game::Lua::PushString(L, "spell");
return 3;
}
if (type == Offsets::ACTION_TYPE_BAG_OR_MACRO) {
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
const int macroSlot = FindMacroSlot(key);
if (macroSlot > 0) {
Game::Lua::PushString(L, "macro");
Game::Lua::PushNumber(L, static_cast<double>(macroSlot));
return 2;
}
// Not a macro — it's a bag item. We know the type but extracting
// the itemID from the bag-instance hash needs more reversing;
// see the file-level comment.
Game::Lua::PushString(L, "item");
Game::Lua::PushNil(L);
return 2;
}
if (type == Offsets::ACTION_TYPE_ITEM_BY_ID) {
const uint32_t itemID = entry & Offsets::ACTION_PAYLOAD_MASK_ITEM_BY_ID;
Game::Lua::PushString(L, "item");
Game::Lua::PushNumber(L, static_cast<double>(itemID));
return 2;
}
// Unknown type tag — surface as nil so callers don't get garbage.
Game::Lua::PushNil(L);
return 1;
}
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetActionInfo", &Script_GetActionInfo);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Action::Info