diff --git a/EVENTS.md b/EVENTS.md index 588ad51..e3398bf 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -145,6 +145,22 @@ for _, eventName in ipairs({"BUFF_ADDED_SELF", "BUFF_REMOVED_SELF", "DEBUFF_ADDE end ``` +#### AURA_CAST_ON_SELF and AURA_CAST_ON_OTHER +Fire when a spell cast applies an aura. "Self" covers casts that land on the active player (including cases where the active player is the caster with no explicit target); "Other" covers all other targets. + +These events are gated behind the `NP_EnableAuraCastEvents` CVar (default 0). Set it to `1` to enable. + +Parameters: +1. int spellId +2. string casterGuid - caster guid like "0xF5300000000000A5" +3. string targetGuid - target guid like "0xF5300000000000A5" +4. int effect - aura-applying effect id (event fires once for each qualifying effect in the spell) +5. int effectAuraName - corresponding entry from EffectApplyAuraName +6. int effectAmplitude - EffectAmplitude entry for the selected aura effect +7. int effectMiscValue - EffectMiscValue entry for the selected aura effect +8. int durationMs - spell duration in milliseconds (after modifiers) +9. int auraCapStatus - bitfield: 1 = buff bar full, 2 = debuff bar full (3 means both) + #### UNIT_DIED Fires when a unit death is recorded in the combat log. @@ -156,4 +172,4 @@ Example: frame:RegisterEvent("UNIT_DIED", function(guid) DEFAULT_CHAT_FRAME:AddMessage("Unit died: " .. guid) end) -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index d09ff09..868d036 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,14 @@ This includes functions for: - Spell/item/unit information (GetItemStats, GetSpellRec, GetUnitData, etc.) - Spell casting and queuing (QueueSpellByName, QueueScript, etc.) - Cast information (GetCastInfo, GetCurrentCastingInfo) -- Cooldown tracking (GetSpellIdCooldown, GetItemIdCooldown) +- Cooldown tracking (GetSpellIdCooldown, GetItemIdCooldown), including item metadata on cooldown detail tables +- Inventory helpers (GetTrinketCooldown, GetTrinkets) - Spell lookups and utilities +Cooldown detail tables now also expose `itemId`, `itemHasActiveSpell`, and `itemActiveSpellId` alongside the existing per-category timing data. + +Use `GetTrinkets([copy])` to enumerate equipped trinkets and bagged trinkets (backpack/bags 1-4) with `itemId`, `trinketName`, `bagIndex` (nil when equipped), and 1-based `slotIndex`. It reuses cached tables by default; pass `1` (or any truthy value) to force a fresh copy. + ## Custom Events For complete documentation of all custom events added by Nampower, see **[EVENTS.md](EVENTS.md)**. @@ -137,6 +142,7 @@ Available events: - SPELL_CAST_EVENT - Fires when you cast a spell with additional information - SPELL_DAMAGE_EVENT_SELF and SPELL_DAMAGE_EVENT_OTHER - Combat damage events - Buff/Debuff Events (BUFF_ADDED_SELF, BUFF_REMOVED_SELF, etc.) +- AURA_CAST_ON_SELF and AURA_CAST_ON_OTHER - Aura application events (fires once per aura effect; "Self" fires when the aura lands on the active player, including self-cast with no explicit target; includes aura metadata + amplitude/misc + aura cap bitfield for buff/debuff slots); set `NP_EnableAuraCastEvents=1` to enable - UNIT_DIED - Fires when a unit dies ## Bug Reporting diff --git a/SCRIPTS.md b/SCRIPTS.md index 4353334..73c57ba 100644 --- a/SCRIPTS.md +++ b/SCRIPTS.md @@ -35,6 +35,7 @@ The following functions use reusable table references: - **`GetItemStatsField(itemId, fieldName, [copy])`** - Returns individual item field value - **`GetUnitField(unitToken, fieldName, [copy])`** - Returns individual unit field value - **`GetSpellRecField(spellId, fieldName, [copy])`** - Returns individual spell field value +- **`GetTrinkets([copy])`** - Returns trinket list table **Important:** When using these functions without the `copy` parameter, **immediately copy or extract** any values you need to store for later use. Do not store references to the returned tables themselves. Alternatively, pass `1` as the `copy` parameter to get an independent table that is safe to store. @@ -201,6 +202,55 @@ if slot then end ``` +#### UseItemIdOrName(itemIdOrName, [target]) +Uses the first matching item found in the player's inventory (including equipped items) by item ID or name. + +**Parameters:** +- `itemIdOrName` (number|string): Item ID or item name (case-insensitive) +- `target` (optional, string|number): Unit token (e.g. `"target"`, `"player"`) or GUID + - If omitted, uses `LockedTargetGuid` if set; otherwise falls back to the active player GUID. + +**Returns:** +- `1` if the item was found and `CGItem_C::Use(...)` returned non-zero +- `0` if the item was not found or use failed + +**Examples:** +```lua +-- Use Hearthstone +UseItemIdOrName("Hearthstone") + +-- Use a healing potion on yourself (if the item requires a target) +UseItemIdOrName(13446, "player") +``` + +#### UseTrinket(slot|itemIdOrName, [target]) +Uses a trinket from the equipped trinket slots (13 and 14 only). + +**Parameters:** +- `slot|itemIdOrName` (number|string): + - `1` or `13` => use first trinket slot + - `2` or `14` => use second trinket slot + - Any other number => treat as item ID to find in trinket slots + - String => item name (case-insensitive) to find in trinket slots +- `target` (optional, string|number): Unit token or GUID. If omitted, uses `LockedTargetGuid` if set; otherwise falls back to active player GUID. + +**Returns:** +- `1` if the trinket was found and `CGItem_C::Use(...)` returned non-zero +- `0` if the trinket was found but use returned zero +- `-1` if no matching trinket was found in slots 13/14 + +**Examples:** +```lua +-- Use first trinket slot +UseTrinket(1) +-- Use second trinket slot on current target +UseTrinket(2, "target") +-- Use by item id if present in either trinket slot +UseTrinket(18406) +-- Use by name +UseTrinket("Royal Seal of Eldre'Thalas") +``` + #### GetEquippedItems(unitToken) Returns a table reference containing all equipped items for the specified unit. @@ -662,6 +712,9 @@ A Lua table reference with the following fields: - `isOnCooldown` (number): 1 if any cooldown is active, 0 otherwise - `cooldownRemainingMs` (number): Maximum remaining time across all cooldown types in milliseconds +- `itemId` (number): Item ID tied to the cooldown (0 if none) +- `itemHasActiveSpell` (number): 1 if the item has an on-use spell, 0 otherwise +- `itemActiveSpellId` (number): Spell ID of the active item spell (0 if none) **Individual Spell Cooldown:** - `individualStartS` (number): When the individual spell cooldown started (seconds, WoW time) @@ -734,6 +787,52 @@ else end ``` +#### GetTrinketCooldown(slot|itemIdOrName) +Returns cooldown information for the equipped trinket(s) in slots 13 or 14. Accepts slot shortcuts or item identifiers. + +**Parameters:** +- `slot|itemIdOrName` (number|string): + - `1` or `13` => first trinket slot + - `2` or `14` => second trinket slot + - Any other number => treat as item ID to match against trinket slots + - String => item name (case-insensitive) to match against trinket slots + +**Returns:** +- If no matching trinket is equipped in slots 13/14: returns `-1` +- Otherwise: a cooldown detail table with the same structure as `GetSpellIdCooldown` / `GetItemIdCooldown` + +**Example:** +```lua +-- Get cooldown for first trinket slot +local cd = GetTrinketCooldown(1) +if cd ~= -1 and cd.isOnCooldown == 0 then + print("Trinket ready") +end + +-- Check by name +local cd = GetTrinketCooldown("Royal Seal of Eldre'Thalas") +if cd ~= -1 then + print("Remaining: " .. cd.cooldownRemainingMs .. "ms") +end +``` + +#### GetTrinkets([copy]) +Returns a table of trinkets from equipped trinket slots and carried bags. + +**Parameters:** +- `[copy]` (number|boolean, optional): Pass `1` (or any truthy value) to force creation of a fresh Lua table. By default the function reuses an internal table and entry tables for performance. + +**Returns:** +A Lua table where each entry contains: +- `itemId` (number) +- `trinketName` (string, `"Unknown"` if no name available) +- `bagIndex` (number|nil): `nil` when equipped; `0` for backpack; `1-4` for equipped bags +- `slotIndex` (number): Lua 1-based slot within the container (or 1/2 for equipped trinket slots) + +**Notes:** +- Scans only equipped trinket slots and bags 0-4 (backpack + equipped bags). Does not scan bank or keyring. +- Reuses cached Lua tables unless `copyTable` is truthy; prefer copies if you will mutate the returned tables. + #### GetSpellIdForName(spellName) Returns: @@ -791,4 +890,4 @@ should print 90 for atiesh ``` #### ChannelStopCastingNextTick() -Will stop channeling early on the next tick if you have queue channeling spells enabled and try to cast a spell before the next tick (didn't know how to cancel channels without casting another spell). Uses your ChannelLatencyReductionPercentage to determine when to stop the channel. \ No newline at end of file +Will stop channeling early on the next tick if you have queue channeling spells enabled and try to cast a spell before the next tick (didn't know how to cancel channels without casting another spell). Uses your ChannelLatencyReductionPercentage to determine when to stop the channel. diff --git a/nampower/cooldown_scripts.cpp b/nampower/cooldown_scripts.cpp index ec03028..b4a4ea9 100644 --- a/nampower/cooldown_scripts.cpp +++ b/nampower/cooldown_scripts.cpp @@ -5,6 +5,8 @@ #include "items.hpp" #include "offsets.hpp" +#include + namespace Nampower { // Reusable table reference to reduce memory allocations static int cooldownDetailTableRef = LUA_REFNIL; @@ -13,6 +15,10 @@ namespace Nampower { bool isOnCooldown = false; uint32_t cooldownRemainingMs = 0; + bool itemHasActiveSpell = false; + uint32_t itemId = 0; + uint32_t itemActiveSpellId = 0; + uint32_t individualStartMs = 0; uint32_t individualDurationMs = 0; uint32_t individualRemainingMs = 0; @@ -31,13 +37,6 @@ namespace Nampower { bool isOnGcdCategoryCooldown = false; }; - - inline void PushTableInt(uintptr_t *luaState, char *key, int value) { - lua_pushstring(luaState, key); - lua_pushnumber(luaState, value); - lua_settable(luaState, -3); - } - game::SpellHistoryEntry *GetSpellHistoryHead() { auto const spellHistoryAddr = static_cast(Offsets::SpellHistories); @@ -67,16 +66,25 @@ namespace Nampower { return detail; } + detail.itemId = itemId; + + // Set base durations from spell record so we know what they are even if not on cooldown + detail.individualDurationMs = spellRec->RecoveryTime; + detail.categoryDurationMs = spellRec->CategoryRecoveryTime; + detail.gcdCategoryDurationMs = spellRec->StartRecoveryTime; + auto const category = itemCategoryOverride ? itemCategoryOverride : spellRec->Category; auto const startRecoveryCategory = spellRec->StartRecoveryCategory; + detail.categoryId = category; + detail.gcdCategoryId = startRecoveryCategory; + auto const now = static_cast(GetWowTimeMs() & 0xFFFFFFFF); // get bottom 32 bits of time in ms int entryCount = 0; for (auto entry = GetSpellHistoryHead(); entry && ((reinterpret_cast(entry) & 0x1) == 0); entry = reinterpret_cast(entry->field4_0x4)) { - // Individual spell cooldown if (entry->spellID == spellId && entry->itemID == itemId && (entry->recoveryTime != 0 || entry->onHold)) { @@ -107,7 +115,8 @@ namespace Nampower { } // GCD category cooldown (startRecoveryCategory) - if (entry->startRecoveryCategory == startRecoveryCategory && entry->startRecoveryTime != 0 && startRecoveryCategory != 0) { + if (entry->startRecoveryCategory == startRecoveryCategory && entry->startRecoveryTime != 0 && + startRecoveryCategory != 0) { auto start = entry->onHold ? now : entry->recoveryStart; auto end = entry->onHold ? uint64_t(now) + entry->startRecoveryTime @@ -124,7 +133,8 @@ namespace Nampower { } // Calculate overall cooldown status - detail.isOnCooldown = detail.isOnIndividualCooldown || detail.isOnCategoryCooldown || detail.isOnGcdCategoryCooldown; + detail.isOnCooldown = detail.isOnIndividualCooldown || detail.isOnCategoryCooldown || detail. + isOnGcdCategoryCooldown; // Find the maximum remaining cooldown detail.cooldownRemainingMs = detail.individualRemainingMs; @@ -135,12 +145,34 @@ namespace Nampower { detail.cooldownRemainingMs = detail.gcdCategoryRemainingMs; } + if (itemId > 0) { + // get info on active spell for item + auto *itemStats = GetItemStats(itemId); + if (itemStats) { + for (int i = 0; i < 5; ++i) { + if (itemStats->m_spellCooldown[i] > 0 || itemStats->m_spellCategoryCooldown[i] > 0) { + detail.itemHasActiveSpell = true; + detail.itemActiveSpellId = itemStats->m_spellID[i]; + detail.individualDurationMs = itemStats->m_spellCooldown[i]; + detail.categoryDurationMs = itemStats->m_spellCategoryCooldown[i]; + return detail; + } + } + // didn't have active spell, clear any spell gcd info for passive effects + detail.gcdCategoryDurationMs = 0; + detail.gcdCategoryId = 0; + } + } + return detail; } void PushCooldownDetailTable(uintptr_t *luaState, const CooldownDetail &detail) { static char isOnCooldownKey[] = "isOnCooldown"; static char cooldownRemainingMsKey[] = "cooldownRemainingMs"; + static char itemIdKey[] = "itemId"; + static char itemHasActiveSpellKey[] = "itemHasActiveSpell"; + static char itemActiveSpellIdKey[] = "itemActiveSpellId"; static char individualStartSKey[] = "individualStartS"; static char individualDurationMsKey[] = "individualDurationMs"; @@ -167,35 +199,38 @@ namespace Nampower { lua_rawgeti(luaState, LUA_REGISTRYINDEX, cooldownDetailTableRef); // Overall cooldown status - PushTableInt(luaState, isOnCooldownKey, detail.isOnCooldown ? 1 : 0); + PushTableValue(luaState, isOnCooldownKey, detail.isOnCooldown ? 1 : 0); PushTableValue(luaState, cooldownRemainingMsKey, detail.cooldownRemainingMs); + PushTableValue(luaState, itemIdKey, detail.itemId); + PushTableValue(luaState, itemHasActiveSpellKey, detail.itemHasActiveSpell ? 1 : 0); + PushTableValue(luaState, itemActiveSpellIdKey, detail.itemActiveSpellId); // Individual cooldown PushTableValue(luaState, individualStartSKey, detail.individualStartMs / 1000.0); PushTableValue(luaState, individualDurationMsKey, detail.individualDurationMs); PushTableValue(luaState, individualRemainingMsKey, detail.individualRemainingMs); - PushTableInt(luaState, isOnIndividualCooldownKey, detail.isOnIndividualCooldown ? 1 : 0); + PushTableValue(luaState, isOnIndividualCooldownKey, detail.isOnIndividualCooldown ? 1 : 0); // Category cooldown PushTableValue(luaState, categoryIdKey, detail.categoryId); PushTableValue(luaState, categoryStartSKey, detail.categoryStartMs / 1000.0); PushTableValue(luaState, categoryDurationMsKey, detail.categoryDurationMs); PushTableValue(luaState, categoryRemainingMsKey, detail.categoryRemainingMs); - PushTableInt(luaState, isOnCategoryCooldownKey, detail.isOnCategoryCooldown ? 1 : 0); + PushTableValue(luaState, isOnCategoryCooldownKey, detail.isOnCategoryCooldown ? 1 : 0); // GCD category cooldown PushTableValue(luaState, gcdCategoryIdKey, detail.gcdCategoryId); PushTableValue(luaState, gcdCategoryStartSKey, detail.gcdCategoryStartMs / 1000.0); PushTableValue(luaState, gcdCategoryDurationMsKey, detail.gcdCategoryDurationMs); PushTableValue(luaState, gcdCategoryRemainingMsKey, detail.gcdCategoryRemainingMs); - PushTableInt(luaState, isOnGcdCategoryCooldownKey, detail.isOnGcdCategoryCooldown ? 1 : 0); + PushTableValue(luaState, isOnGcdCategoryCooldownKey, detail.isOnGcdCategoryCooldown ? 1 : 0); } CooldownDetail GetItemCooldownDetail(uint32_t itemId) { auto *itemStats = GetItemStats(itemId); - CooldownDetail best{}; - uint32_t bestRemaining = 0; + CooldownDetail activeCooldownDetail{}; + uint32_t longestCooldownMs = 0; if (itemStats) { for (int i = 0; i < 5; ++i) { @@ -206,22 +241,17 @@ namespace Nampower { auto const categoryOverride = static_cast(itemStats->m_spellCategory[i]); auto const detail = GetCooldownFromSpellHistory(spellId, itemId, categoryOverride); - if (!detail.isOnCooldown) { - continue; - } - if (detail.cooldownRemainingMs >= bestRemaining) { - best = detail; - bestRemaining = detail.cooldownRemainingMs; + if (detail.cooldownRemainingMs >= longestCooldownMs) { + activeCooldownDetail = detail; + longestCooldownMs = detail.cooldownRemainingMs; } } + } else { + activeCooldownDetail = GetCooldownFromSpellHistory(0, itemId, 0); } - if (!best.isOnCooldown) { - best = GetCooldownFromSpellHistory(0, itemId, 0); - } - - return best; + return activeCooldownDetail; } uint32_t Script_GetSpellIdCooldown(uintptr_t *luaState) { @@ -266,4 +296,84 @@ namespace Nampower { PushCooldownDetailTable(luaState, cooldown); return 1; } + + static bool TrinketMatches(uint32_t itemId, uint32_t searchItemId, const char *searchItemName) { + if (searchItemId != 0) { + return itemId == searchItemId; + } + + if (searchItemName) { + auto itemStats = GetItemStats(itemId); + if (itemStats && itemStats->m_displayName[0]) { + return _stricmp(itemStats->m_displayName[0], searchItemName) == 0; + } + } + + return false; + } + + uint32_t Script_GetTrinketCooldown(uintptr_t *luaState) { + luaState = GetLuaStatePtr(); // pcall leads to corrupted lua state pointer on added scripts, not sure why + + if (!lua_isnumber(luaState, 1) && !lua_isstring(luaState, 1)) { + lua_error(luaState, "Usage: GetTrinketCooldown(slot|itemIdOrName)"); + return 0; + } + + uint32_t trinketInvSlot = 0; + uint32_t searchItemId = 0; + const char *searchItemName = nullptr; + + if (lua_isnumber(luaState, 1)) { + auto param = static_cast(lua_tonumber(luaState, 1)); + if (param == 1 || param == 13) { + trinketInvSlot = 12; + } else if (param == 2 || param == 14) { + trinketInvSlot = 13; + } else { + searchItemId = param; + } + } else { + searchItemName = lua_tostring(luaState, 1); + } + + auto const getBagItem = reinterpret_cast(Offsets::CGBag_C_GetItemAtSlot); + auto playerGuid = game::ClntObjMgrGetActivePlayerGuid(); + auto playerUnit = game::GetObjectPtr(playerGuid); + if (!playerUnit) { + lua_pushnumber(luaState, -1); + return 1; + } + + auto inventory = game::GetPlayerInventoryPtr(playerUnit); + + game::CGItem_C *item = nullptr; + + if (trinketInvSlot == 12 || trinketInvSlot == 13) { + item = getBagItem(inventory, trinketInvSlot); + } else { + for (uint32_t invSlot: {12u, 13u}) { + auto candidate = getBagItem(inventory, invSlot); + if (candidate && TrinketMatches(game::GetItemId(candidate), searchItemId, searchItemName)) { + item = candidate; + break; + } + } + } + + // if looking for trinket by name and not equipped, search bags to try to find item id + if (!item && searchItemName != nullptr) { + // look for item name in bags + auto result = FindPlayerItem(0, searchItemName); + if (result.found()) { + searchItemId = game::GetItemId(result.item); + } + } + + uint32_t itemId = (item) ? game::GetItemId(item) : searchItemId; + auto const cooldown = GetItemCooldownDetail(itemId); + + PushCooldownDetailTable(luaState, cooldown); + return 1; + } } diff --git a/nampower/cooldown_scripts.hpp b/nampower/cooldown_scripts.hpp index 92116af..24e71ba 100644 --- a/nampower/cooldown_scripts.hpp +++ b/nampower/cooldown_scripts.hpp @@ -9,4 +9,5 @@ namespace Nampower { uint32_t Script_GetSpellIdCooldown(uintptr_t *luaState); uint32_t Script_GetItemIdCooldown(uintptr_t *luaState); + uint32_t Script_GetTrinketCooldown(uintptr_t *luaState); } diff --git a/nampower/game.hpp b/nampower/game.hpp index 5ce9429..79b2b26 100644 --- a/nampower/game.hpp +++ b/nampower/game.hpp @@ -34,6 +34,98 @@ namespace game { #pragma pack(push, 1) + enum ItemClass + { + ITEM_CLASS_CONSUMABLE = 0, + ITEM_CLASS_CONTAINER = 1, + ITEM_CLASS_WEAPON = 2, + ITEM_CLASS_GEM = 3, + ITEM_CLASS_ARMOR = 4, + ITEM_CLASS_REAGENT = 5, + ITEM_CLASS_PROJECTILE = 6, + ITEM_CLASS_TRADE_GOODS = 7, + ITEM_CLASS_GENERIC = 8, + ITEM_CLASS_RECIPE = 9, + ITEM_CLASS_MONEY = 10, + ITEM_CLASS_QUIVER = 11, + ITEM_CLASS_QUEST = 12, + ITEM_CLASS_KEY = 13, + ITEM_CLASS_PERMANENT = 14, + ITEM_CLASS_JUNK = 15 + }; + + enum ItemSubclassWeapon + { + ITEM_SUBCLASS_WEAPON_AXE = 0, + ITEM_SUBCLASS_WEAPON_AXE2 = 1, + ITEM_SUBCLASS_WEAPON_BOW = 2, + ITEM_SUBCLASS_WEAPON_GUN = 3, + ITEM_SUBCLASS_WEAPON_MACE = 4, + ITEM_SUBCLASS_WEAPON_MACE2 = 5, + ITEM_SUBCLASS_WEAPON_POLEARM = 6, + ITEM_SUBCLASS_WEAPON_SWORD = 7, + ITEM_SUBCLASS_WEAPON_SWORD2 = 8, + ITEM_SUBCLASS_WEAPON_obsolete = 9, + ITEM_SUBCLASS_WEAPON_STAFF = 10, + ITEM_SUBCLASS_WEAPON_EXOTIC = 11, + ITEM_SUBCLASS_WEAPON_EXOTIC2 = 12, + ITEM_SUBCLASS_WEAPON_FIST = 13, + ITEM_SUBCLASS_WEAPON_MISC = 14, + ITEM_SUBCLASS_WEAPON_DAGGER = 15, + ITEM_SUBCLASS_WEAPON_THROWN = 16, + ITEM_SUBCLASS_WEAPON_SPEAR = 17, + ITEM_SUBCLASS_WEAPON_CROSSBOW = 18, + ITEM_SUBCLASS_WEAPON_WAND = 19, + ITEM_SUBCLASS_WEAPON_FISHING_POLE = 20 + }; + + enum ItemSubclassArmor + { + ITEM_SUBCLASS_ARMOR_MISC = 0, + ITEM_SUBCLASS_ARMOR_CLOTH = 1, + ITEM_SUBCLASS_ARMOR_LEATHER = 2, + ITEM_SUBCLASS_ARMOR_MAIL = 3, + ITEM_SUBCLASS_ARMOR_PLATE = 4, + ITEM_SUBCLASS_ARMOR_BUCKLER = 5, + ITEM_SUBCLASS_ARMOR_SHIELD = 6, + ITEM_SUBCLASS_ARMOR_LIBRAM = 7, + ITEM_SUBCLASS_ARMOR_IDOL = 8, + ITEM_SUBCLASS_ARMOR_TOTEM = 9 + }; + + enum InventoryType + { + INVTYPE_NON_EQUIP = 0, + INVTYPE_HEAD = 1, + INVTYPE_NECK = 2, + INVTYPE_SHOULDERS = 3, + INVTYPE_BODY = 4, + INVTYPE_CHEST = 5, + INVTYPE_WAIST = 6, + INVTYPE_LEGS = 7, + INVTYPE_FEET = 8, + INVTYPE_WRISTS = 9, + INVTYPE_HANDS = 10, + INVTYPE_FINGER = 11, + INVTYPE_TRINKET = 12, + INVTYPE_WEAPON = 13, + INVTYPE_SHIELD = 14, + INVTYPE_RANGED = 15, + INVTYPE_CLOAK = 16, + INVTYPE_2HWEAPON = 17, + INVTYPE_BAG = 18, + INVTYPE_TABARD = 19, + INVTYPE_ROBE = 20, + INVTYPE_WEAPONMAINHAND = 21, + INVTYPE_WEAPONOFFHAND = 22, + INVTYPE_HOLDABLE = 23, + INVTYPE_AMMO = 24, + INVTYPE_THROWN = 25, + INVTYPE_RANGEDRIGHT = 26, + INVTYPE_QUIVER = 27, + INVTYPE_RELIC = 28 + }; + struct SpellRec { unsigned int Id; unsigned int School; @@ -345,7 +437,7 @@ namespace game { }; struct __declspec(align(4)) ItemStats_C { - int m_class; + ItemClass m_class; int m_subclass; char *m_displayName[4]; int m_displayInfoID; @@ -353,7 +445,7 @@ namespace game { ItemStatsFlags m_flags; int m_buyPrice; int m_sellPrice; - int m_inventoryType; + InventoryType m_inventoryType; int m_allowableClass; int m_allowableRace; int m_itemLevel; @@ -1335,6 +1427,9 @@ namespace game { BUFF_REMOVED_OTHER = 558, UNIT_DIED = 559, + + AURA_CAST_ON_SELF = 560, + AURA_CAST_ON_OTHER = 561, }; enum TypeMask { diff --git a/nampower/item_scripts.cpp b/nampower/item_scripts.cpp index 1c8f440..e5f894b 100644 --- a/nampower/item_scripts.cpp +++ b/nampower/item_scripts.cpp @@ -5,21 +5,21 @@ #include "logging.hpp" #include "offsets.hpp" -#include -#include #include -#include +#include namespace Nampower { - // Local cache for item name lookups - static std::unordered_map itemNameToIdCache; - // Reusable table references to reduce memory allocations static int basicItemInfoTableRef = LUA_REFNIL; static int itemInfoTableRef = LUA_REFNIL; static int equippedItemsTableRef = LUA_REFNIL; static int bagItemsTableRef = LUA_REFNIL; static int bagTableRef = LUA_REFNIL; + static int trinketsTableRef = LUA_REFNIL; + static constexpr uint32_t MAX_TRINKET_ENTRY_TABLES = 100; + static std::array trinketEntryTableRefs{}; + static bool trinketEntryRefsInitialized = false; + static uint32_t lastTrinketCount = 0; // String keys used when pushing item data to Lua static char itemIdKey[] = "itemId"; @@ -32,62 +32,17 @@ namespace Nampower { static char maxDurabilityKey[] = "maxDurability"; static char tempEnchantmentTimeLeftMsKey[] = "tempEnchantmentTimeLeftMs"; static char tempEnchantmentChargesKey[] = "tempEnchantmentCharges"; - - std::string ToLowerCase(const char *str) { - std::string result; - if (!str) return result; - - while (*str) { - result += static_cast(tolower(*str)); - ++str; - } - return result; - } - - uint32_t GetItemIdFromCache(const char *itemName) { - if (!itemName) return 0; - - std::string lowerName = ToLowerCase(itemName); - auto it = itemNameToIdCache.find(lowerName); - if (it != itemNameToIdCache.end()) { - return it->second; - } - return 0; - } - - void CacheItemNameToId(const char *itemName, uint32_t itemId) { - if (!itemName || itemId == 0) return; - - std::string lowerName = ToLowerCase(itemName); - itemNameToIdCache[lowerName] = itemId; - } - - bool DoesItemMatch(uint32_t itemId, uint32_t searchItemId, const char *searchItemName) { - if (searchItemId != 0) { - return itemId == searchItemId; - } - - if (searchItemName) { - auto itemStats = GetItemStats(itemId); - if (itemStats && itemStats->m_displayName[0]) { - bool matches = _stricmp(itemStats->m_displayName[0], searchItemName) == 0; - if (matches) { - CacheItemNameToId(searchItemName, itemId); - } - return matches; - } - } - - return false; - } + static char bagIndexKey[] = "bagIndex"; + static char slotIndexKey[] = "slotIndex"; + static char trinketNameKey[] = "trinketName"; void PushItemFoundResult(uintptr_t *luaState, int32_t bagIndex, uint32_t slot) { uint32_t adjustedSlot = slot; if (bagIndex == 0) { adjustedSlot = slot - 0x17; // subtract 23 - } else if (bagIndex == -1) { + } else if (bagIndex == BANK_BAG_INDEX) { adjustedSlot = slot - 0x27; // subtract 39 - } else if (bagIndex == -2) { + } else if (bagIndex == KEYRING_BAG_INDEX) { adjustedSlot = slot - 0x51; // subtract 81 } @@ -95,13 +50,6 @@ namespace Nampower { lua_pushnumber(luaState, static_cast(adjustedSlot + 1)); // lua is 1 indexed } - uintptr_t *GetBagPtrFromContainer(uintptr_t *containerPtr) { - auto vftable = game::GetObjectVFTable(containerPtr); - using GetBagPtrT = uintptr_t * (__thiscall *)(uintptr_t *); - auto getBagPtrFunc = reinterpret_cast(vftable[4]); - return getBagPtrFunc(containerPtr); - } - void CreateBasicItemInfoTable(uintptr_t *luaState, game::CGItem *cgItem) { if (!cgItem) { lua_pushnil(luaState); @@ -200,29 +148,263 @@ namespace Nampower { return 0; } + auto result = FindPlayerItem(searchItemId, searchItemName); + if (!result.found()) { + lua_pushnil(luaState); + lua_pushnil(luaState); + return 2; + } + + if (result.bagIndex == EQUIPPED_BAG_INDEX) { + lua_pushnil(luaState); + lua_pushnumber(luaState, static_cast(result.slot + 1)); + return 2; + } + + PushItemFoundResult(luaState, result.bagIndex, result.slot); + return 2; + } + + uint32_t Script_UseItemIdOrName(uintptr_t *luaState) { + luaState = GetLuaStatePtr(); + + if (!lua_isnumber(luaState, 1) && !lua_isstring(luaState, 1)) { + lua_error(luaState, "Usage: UseItemIdOrName(itemIdOrName, [target])"); + return 0; + } + + uint32_t searchItemId = 0; + const char *searchItemName = nullptr; + + if (lua_isnumber(luaState, 1)) { + searchItemId = static_cast(lua_tonumber(luaState, 1)); + } else { + searchItemName = lua_tostring(luaState, 1); + uint32_t cachedItemId = GetItemIdFromCache(searchItemName); + if (cachedItemId != 0) { + searchItemId = cachedItemId; + searchItemName = nullptr; + } + } + + uint64_t targetGuid = 0; + if (lua_gettop(luaState) >= 2) { + if (!lua_isnumber(luaState, 2) && !lua_isstring(luaState, 2)) { + lua_error(luaState, "Usage: UseItemIdOrName(itemIdOrName, [target])"); + return 0; + } + + targetGuid = GetUnitGuidFromLuaParam(luaState, 2); + if (targetGuid == 0) { + lua_error(luaState, "Unable to determine target guid"); + return 0; + } + } else { + targetGuid = *reinterpret_cast(Offsets::LockedTargetGuid); + if (targetGuid == 0) { + targetGuid = game::ClntObjMgrGetActivePlayerGuid(); + } + } + + auto itemSearchResult = FindPlayerItem(searchItemId, searchItemName); + if (!itemSearchResult.found()) { + lua_pushnumber(luaState, 0); + return 1; + } + + using CGItem_C_UseT = uint32_t(__thiscall *)(game::CGItem_C *this_ptr, uint64_t *targetGuid, int useBindConfirm); + auto const useItem = reinterpret_cast(Offsets::CGItem_C_Use); + + auto const result = useItem(itemSearchResult.item, &targetGuid, 0); + lua_pushnumber(luaState, result); + return 1; + } + + uint32_t Script_UseTrinket(uintptr_t *luaState) { + luaState = GetLuaStatePtr(); + + if (!lua_isnumber(luaState, 1) && !lua_isstring(luaState, 1)) { + lua_error(luaState, "Usage: UseTrinket(slot|itemIdOrName, [target])"); + return 0; + } + + uint32_t trinketInvSlot = 0; + uint32_t searchItemId = 0; + const char *searchItemName = nullptr; + + if (lua_isnumber(luaState, 1)) { + auto param = static_cast(lua_tonumber(luaState, 1)); + if (param == 1 || param == 13) { + trinketInvSlot = 12; + } else if (param == 2 || param == 14) { + trinketInvSlot = 13; + } else { + searchItemId = param; + } + } else { + searchItemName = lua_tostring(luaState, 1); + uint32_t cachedItemId = GetItemIdFromCache(searchItemName); + if (cachedItemId != 0) { + searchItemId = cachedItemId; + searchItemName = nullptr; + } + } + + uint64_t targetGuid = 0; + if (lua_gettop(luaState) >= 2) { + if (!lua_isnumber(luaState, 2) && !lua_isstring(luaState, 2)) { + lua_error(luaState, "Usage: UseTrinket(slot|itemIdOrName, [target])"); + return 0; + } + + targetGuid = GetUnitGuidFromLuaParam(luaState, 2); + if (targetGuid == 0) { + lua_error(luaState, "Unable to determine target guid"); + return 0; + } + } else { + targetGuid = *reinterpret_cast(Offsets::LockedTargetGuid); + if (targetGuid == 0) { + targetGuid = game::ClntObjMgrGetActivePlayerGuid(); + } + } + auto const getBagItem = reinterpret_cast(Offsets::CGBag_C_GetItemAtSlot); - auto const getContainerGuid = reinterpret_cast(Offsets::GetContainerGuid); + auto playerGuid = game::ClntObjMgrGetActivePlayerGuid(); + auto playerUnit = game::GetObjectPtr(playerGuid); + if (!playerUnit) { + lua_pushnumber(luaState, -1); + return 1; + } auto inventory = game::GetPlayerInventoryPtr(playerUnit); - for (uint32_t slot = 0; slot <= 18; slot++) { - auto item = getBagItem(inventory, slot); - if (item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName)) { + game::CGItem_C *item = nullptr; + + if (trinketInvSlot == 12 || trinketInvSlot == 13) { + item = getBagItem(inventory, trinketInvSlot); + } else { + for (uint32_t slot : {12u, 13u}) { + auto candidate = getBagItem(inventory, slot); + if (candidate && DoesItemMatch(game::GetItemId(candidate), searchItemId, searchItemName)) { + item = candidate; + break; + } + } + } + + if (!item) { + lua_pushnumber(luaState, -1); + return 1; + } + + using CGItem_C_UseT = uint32_t(__thiscall *)(game::CGItem_C *this_ptr, uint64_t *targetGuid, int useBindConfirm); + auto const useItem = reinterpret_cast(Offsets::CGItem_C_Use); + + auto const result = useItem(item, &targetGuid, 0); + lua_pushnumber(luaState, result); + return 1; + } + + uint32_t Script_GetTrinkets(uintptr_t *luaState) { + luaState = GetLuaStatePtr(); + + auto const getBagItem = reinterpret_cast(Offsets::CGBag_C_GetItemAtSlot); + auto const getContainerGuid = reinterpret_cast(Offsets::GetContainerGuid); + + auto playerGuid = game::ClntObjMgrGetActivePlayerGuid(); + auto playerUnit = game::GetObjectPtr(playerGuid); + + bool copyTable = false; + if (lua_isnumber(luaState, 1)) { + copyTable = static_cast(lua_tonumber(luaState, 1)) != 0; + } + + if (!trinketEntryRefsInitialized) { + trinketEntryTableRefs.fill(LUA_REFNIL); + trinketEntryRefsInitialized = true; + } + + if (!copyTable && trinketsTableRef == LUA_REFNIL) { + lua_newtable(luaState); + trinketsTableRef = luaL_ref(luaState, LUA_REGISTRYINDEX); + } + + if (copyTable) { + lua_newtable(luaState); + } else { + lua_rawgeti(luaState, LUA_REGISTRYINDEX, trinketsTableRef); + for (uint32_t i = 1; i <= lastTrinketCount; ++i) { + lua_pushnumber(luaState, static_cast(i)); lua_pushnil(luaState); - lua_pushnumber(luaState, static_cast(slot+1)); // lua is 1 indexed - return 2; + lua_settable(luaState, -3); } } - for (uint32_t slot = 23; slot <= 38; slot++) { + if (!playerUnit) { + return 1; + } + + auto inventory = game::GetPlayerInventoryPtr(playerUnit); + uint32_t luaIndex = 1; + + auto pushTrinket = [&](int32_t bagIndex, uint32_t slot, game::CGItem_C *item) { + if (!item) return; + + auto itemId = game::GetItemId(item); + auto itemStats = GetItemStats(itemId); + if (!itemStats || itemStats->m_inventoryType != game::INVTYPE_TRINKET) { + return; + } + + uint32_t luaSlot = slot + 1; + if (bagIndex == 0) { + luaSlot = slot - 0x17 + 1; // backpack absolute slots 23-38 + } else if (bagIndex == BANK_BAG_INDEX) { + luaSlot = slot - 0x27 + 1; // bank absolute slots 39-62 + } else if (bagIndex == KEYRING_BAG_INDEX) { + luaSlot = slot - 0x51 + 1; // keyring absolute slots 81-96 + } + + lua_pushnumber(luaState, static_cast(luaIndex++)); + if (copyTable || luaIndex - 2 >= MAX_TRINKET_ENTRY_TABLES) { + lua_newtable(luaState); + } else { + auto &entryRef = trinketEntryTableRefs[luaIndex - 2]; + if (entryRef == LUA_REFNIL) { + lua_newtable(luaState); + entryRef = luaL_ref(luaState, LUA_REGISTRYINDEX); + } + lua_rawgeti(luaState, LUA_REGISTRYINDEX, entryRef); + } + PushTableValue(luaState, itemIdKey, itemId); + if (bagIndex == EQUIPPED_BAG_INDEX) { + lua_pushstring(luaState, bagIndexKey); + lua_pushnil(luaState); + lua_settable(luaState, -3); + } else { + PushTableValue(luaState, bagIndexKey, bagIndex); + } + PushTableValue(luaState, slotIndexKey, luaSlot); + const char *trinketName = itemStats->m_displayName[0] ? itemStats->m_displayName[0] : "Unknown"; + PushTableValue(luaState, trinketNameKey, trinketName); + lua_settable(luaState, -3); + }; + + // Equipped trinket slots (0-based slots 12 and 13) + for (uint32_t slot : {12u, 13u}) { auto item = getBagItem(inventory, slot); - if (item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName)) { - PushItemFoundResult(luaState, 0, slot); - return 2; - } + pushTrinket(EQUIPPED_BAG_INDEX, slot, item); } - for (int32_t bagIndex = 1; bagIndex <= 4; bagIndex++) { + // Backpack (bagIndex 0, absolute slots 23-38) + for (uint32_t slot = 23; slot <= 38; ++slot) { + auto item = getBagItem(inventory, slot); + pushTrinket(0, slot, item); + } + + // Equipped bags 1-4 + for (int32_t bagIndex = 1; bagIndex <= 4; ++bagIndex) { uint64_t containerGuid = getContainerGuid(bagIndex - 1); // bagIndex 1-4 maps to container 0-3 if (containerGuid == 0) continue; @@ -233,58 +415,17 @@ namespace Nampower { if (!bagPtr) continue; auto bagSize = *bagPtr; - for (uint32_t slot = 0; slot < bagSize; slot++) { + for (uint32_t slot = 0; slot < bagSize; ++slot) { auto item = getBagItem(bagPtr, slot); - if (item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName)) { - PushItemFoundResult(luaState, bagIndex, slot); - return 2; - } + pushTrinket(bagIndex, slot, item); } } - uint64_t bankGuid = *reinterpret_cast(Offsets::BankGuid); - - if (bankGuid > 0) { - for (uint32_t slot = 39; slot <= 62; slot++) { - auto item = getBagItem(inventory, slot); - if (item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName)) { - PushItemFoundResult(luaState, -1, slot); - return 2; - } - } - - for (int32_t bagIndex = 5; bagIndex <= 9; bagIndex++) { - uint64_t containerGuid = getContainerGuid(bagIndex - 1); // bagIndex 5-9 maps to container 4-8 - if (containerGuid == 0) continue; - - auto containerPtr = game::ClntObjMgrObjectPtr(game::TYPEMASK_CONTAINER, containerGuid); - if (!containerPtr) continue; - - auto bagPtr = GetBagPtrFromContainer(containerPtr); - if (!bagPtr) continue; - - auto bagSize = *bagPtr; - for (uint32_t slot = 0; slot < bagSize; slot++) { - auto item = getBagItem(bagPtr, slot); - if (item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName)) { - PushItemFoundResult(luaState, bagIndex, slot); - return 2; - } - } - } + if (!copyTable) { + lastTrinketCount = luaIndex - 1; } - for (uint32_t slot = 81; slot <= 96; slot++) { - auto item = getBagItem(inventory, slot); - if (item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName)) { - PushItemFoundResult(luaState, -2, slot); - return 2; - } - } - - lua_pushnil(luaState); - lua_pushnil(luaState); - return 2; + return 1; } uint32_t Script_GetEquippedItems(uintptr_t *luaState) { diff --git a/nampower/item_scripts.hpp b/nampower/item_scripts.hpp index a9d1dbc..87f6f4b 100644 --- a/nampower/item_scripts.hpp +++ b/nampower/item_scripts.hpp @@ -8,6 +8,9 @@ namespace Nampower { uint32_t Script_FindPlayerItemSlot(uintptr_t *luaState); + uint32_t Script_UseItemIdOrName(uintptr_t *luaState); + uint32_t Script_UseTrinket(uintptr_t *luaState); + uint32_t Script_GetTrinkets(uintptr_t *luaState); uint32_t Script_GetEquippedItems(uintptr_t *luaState); uint32_t Script_GetEquippedItem(uintptr_t *luaState); uint32_t Script_GetBagItem(uintptr_t *luaState); diff --git a/nampower/items.cpp b/nampower/items.cpp index 64dd436..a4f235c 100644 --- a/nampower/items.cpp +++ b/nampower/items.cpp @@ -7,6 +7,8 @@ #include "logging.hpp" #include "helper.hpp" +#include +#include #include #include #include @@ -19,6 +21,9 @@ namespace Nampower { // Global dictionary to store itemId -> ItemStats_C mappings static std::unordered_map itemStatsCache; + // Local cache for item name lookups + static std::unordered_map itemNameToIdCache; + // Track pending async loads (can have multiple at once) static std::unordered_set pendingItemIds; @@ -31,6 +36,169 @@ namespace Nampower { auto const getRow = reinterpret_cast(Offsets::DBCache_ItemCacheDBGetRow); auto const itemCache = reinterpret_cast(Offsets::ItemDBCache); + std::string ToLowerCase(const char *str) { + std::string result; + if (!str) return result; + + while (*str) { + result += static_cast(tolower(*str)); + ++str; + } + return result; + } + + uint32_t GetItemIdFromCache(const char *itemName) { + if (!itemName) return 0; + + std::string lowerName = ToLowerCase(itemName); + auto it = itemNameToIdCache.find(lowerName); + if (it != itemNameToIdCache.end()) { + return it->second; + } + return 0; + } + + void CacheItemNameToId(const char *itemName, uint32_t itemId) { + if (!itemName || itemId == 0) return; + + std::string lowerName = ToLowerCase(itemName); + itemNameToIdCache[lowerName] = itemId; + } + + bool DoesItemMatch(uint32_t itemId, uint32_t searchItemId, const char *searchItemName) { + if (searchItemId != 0) { + return itemId == searchItemId; + } + + if (searchItemName) { + auto itemStats = GetItemStats(itemId); + if (itemStats && itemStats->m_displayName[0]) { + bool matches = _stricmp(itemStats->m_displayName[0], searchItemName) == 0; + if (matches) { + CacheItemNameToId(searchItemName, itemId); + } + return matches; + } + } + + return false; + } + + uintptr_t *GetBagPtrFromContainer(uintptr_t *containerPtr) { + auto vftable = game::GetObjectVFTable(containerPtr); + using GetBagPtrT = uintptr_t * (__thiscall *)(uintptr_t *); + auto getBagPtrFunc = reinterpret_cast(vftable[4]); + return getBagPtrFunc(containerPtr); + } + + PlayerItemSearchResult FindPlayerItem(uint32_t searchItemId, const char *searchItemName) { + PlayerItemSearchResult result{}; + + auto const getBagItem = reinterpret_cast(Offsets::CGBag_C_GetItemAtSlot); + auto const getContainerGuid = reinterpret_cast(Offsets::GetContainerGuid); + + auto playerGuid = game::ClntObjMgrGetActivePlayerGuid(); + auto playerUnit = game::GetObjectPtr(playerGuid); + if (!playerUnit) { + return result; + } + + auto inventory = game::GetPlayerInventoryPtr(playerUnit); + + auto matchesItem = [&](game::CGItem_C *item) { + return item && DoesItemMatch(game::GetItemId(item), searchItemId, searchItemName); + }; + + for (uint32_t slot = 0; slot <= 18; slot++) { + auto item = getBagItem(inventory, slot); + if (matchesItem(item)) { + result.item = item; + result.bagIndex = EQUIPPED_BAG_INDEX; + result.slot = slot; + return result; + } + } + + for (uint32_t slot = 23; slot <= 38; slot++) { + auto item = getBagItem(inventory, slot); + if (matchesItem(item)) { + result.item = item; + result.bagIndex = 0; + result.slot = slot; + return result; + } + } + + for (int32_t bagIndex = 1; bagIndex <= 4; bagIndex++) { + uint64_t containerGuid = getContainerGuid(bagIndex - 1); + if (containerGuid == 0) continue; + + auto containerPtr = game::ClntObjMgrObjectPtr(game::TYPEMASK_CONTAINER, containerGuid); + if (!containerPtr) continue; + + auto bagPtr = GetBagPtrFromContainer(containerPtr); + if (!bagPtr) continue; + + auto bagSize = *bagPtr; + for (uint32_t slot = 0; slot < bagSize; slot++) { + auto item = getBagItem(bagPtr, slot); + if (matchesItem(item)) { + result.item = item; + result.bagIndex = bagIndex; + result.slot = slot; + return result; + } + } + } + + uint64_t bankGuid = *reinterpret_cast(Offsets::BankGuid); + if (bankGuid > 0) { + for (uint32_t slot = 39; slot <= 62; slot++) { + auto item = getBagItem(inventory, slot); + if (matchesItem(item)) { + result.item = item; + result.bagIndex = BANK_BAG_INDEX; + result.slot = slot; + return result; + } + } + + for (int32_t bagIndex = 5; bagIndex <= 9; bagIndex++) { + uint64_t containerGuid = getContainerGuid(bagIndex - 1); // bagIndex 5-9 maps to container 4-8 + if (containerGuid == 0) continue; + + auto containerPtr = game::ClntObjMgrObjectPtr(game::TYPEMASK_CONTAINER, containerGuid); + if (!containerPtr) continue; + + auto bagPtr = GetBagPtrFromContainer(containerPtr); + if (!bagPtr) continue; + + auto bagSize = *bagPtr; + for (uint32_t slot = 0; slot < bagSize; slot++) { + auto item = getBagItem(bagPtr, slot); + if (matchesItem(item)) { + result.item = item; + result.bagIndex = bagIndex; + result.slot = slot; + return result; + } + } + } + } + + for (uint32_t slot = 81; slot <= 96; slot++) { + auto item = getBagItem(inventory, slot); + if (matchesItem(item)) { + result.item = item; + result.bagIndex = KEYRING_BAG_INDEX; + result.slot = slot; + return result; + } + } + + return result; + } + std::string escapeJsonString(const char *str) { if (!str) return "null"; diff --git a/nampower/items.hpp b/nampower/items.hpp index bae42d4..ecc8f7e 100644 --- a/nampower/items.hpp +++ b/nampower/items.hpp @@ -11,9 +11,26 @@ namespace Nampower { using DBCache_ItemCacheDBGetRowT = uint32_t * (__thiscall *)(void *this_ptr, uint32_t itemId, uint64_t *guid, TooltipItemStatsCallbackT callback, uintptr_t *userData, bool requestIfMissing); + constexpr int32_t EQUIPPED_BAG_INDEX = -3; + constexpr int32_t BANK_BAG_INDEX = -1; + constexpr int32_t KEYRING_BAG_INDEX = -2; + + struct PlayerItemSearchResult { + game::CGItem_C *item = nullptr; + int32_t bagIndex = 0; // -3 equipped, -1 bank main slots, -2 keyring, 0 backpack, 1-9 bag indices + uint32_t slot = 0; + + bool found() const { return item != nullptr; } + }; + void ExportAllItems(); bool LoadItem(uint32_t itemId); // Returns true if item needs async load (caller should wait) bool ProcessItemExport(); // Process one item export per frame, returns true if still exporting game::ItemStats_C* GetItemStats(uint32_t itemId); + uint32_t GetItemIdFromCache(const char *itemName); + void CacheItemNameToId(const char *itemName, uint32_t itemId); + bool DoesItemMatch(uint32_t itemId, uint32_t searchItemId, const char *searchItemName); + PlayerItemSearchResult FindPlayerItem(uint32_t searchItemId, const char *searchItemName); + uintptr_t *GetBagPtrFromContainer(uintptr_t *containerPtr); } diff --git a/nampower/main.cpp b/nampower/main.cpp index 4816548..8d8accb 100644 --- a/nampower/main.cpp +++ b/nampower/main.cpp @@ -631,6 +631,9 @@ namespace Nampower { } else if (strcmp(cvar, "NP_SpamProtectionEnabled") == 0) { gUserSettings.spamProtectionEnabled = atoi(value) != 0; DEBUG_LOG("Set NP_SpamProtectionEnabled to " << gUserSettings.spamProtectionEnabled); + } else if (strcmp(cvar, "NP_EnableAuraCastEvents") == 0) { + gUserSettings.enableAuraCastEvents = atoi(value) != 0; + DEBUG_LOG("Set NP_EnableAuraCastEvents to " << gUserSettings.enableAuraCastEvents); } else if (strcmp(cvar, "NP_MinBufferTimeMs") == 0) { gUserSettings.minBufferTimeMs = atoi(value); DEBUG_LOG("Set NP_MinBufferTimeMs and current buffer to " << gUserSettings.minBufferTimeMs); @@ -771,6 +774,7 @@ namespace Nampower { gUserSettings.quickcastOnDoubleCast = false; gUserSettings.spamProtectionEnabled = true; + gUserSettings.enableAuraCastEvents = false; gUserSettings.minBufferTimeMs = 55; // time in ms to buffer cast to minimize server failure gUserSettings.nonGcdBufferTimeMs = 100; // time in ms to buffer non-GCD spells to minimize server failure @@ -954,6 +958,16 @@ namespace Nampower { 0, // unk2 0); // unk3 + char NP_EnableAuraCastEvents[] = "NP_EnableAuraCastEvents"; + CVarRegister(NP_EnableAuraCastEvents, // name + nullptr, // help + 0, // unk1 + gUserSettings.enableAuraCastEvents ? defaultTrue : defaultFalse, // default value address + nullptr, // callback + 1, // category + 0, // unk2 + 0); // unk3 + char NP_OnSwingBufferCooldownMs[] = "NP_OnSwingBufferCooldownMs"; CVarRegister(NP_OnSwingBufferCooldownMs, // name nullptr, // help @@ -1079,6 +1093,7 @@ namespace Nampower { loadUserVar("NP_DoubleCastToEndChannelEarly"); loadUserVar("NP_SpamProtectionEnabled"); + loadUserVar("NP_EnableAuraCastEvents"); loadUserVar("NP_MinBufferTimeMs"); loadUserVar("NP_NonGcdBufferTimeMs"); @@ -1221,6 +1236,12 @@ namespace Nampower { char UNIT_DIED[] = "UNIT_DIED"; addCustomEvent(game::UNIT_DIED, UNIT_DIED); + + char AURA_CAST_ON_SELF[] = "AURA_CAST_ON_SELF"; + addCustomEvent(game::AURA_CAST_ON_SELF, AURA_CAST_ON_SELF); + + char AURA_CAST_ON_OTHER[] = "AURA_CAST_ON_OTHER"; + addCustomEvent(game::AURA_CAST_ON_OTHER, AURA_CAST_ON_OTHER); } void FrameScript_CreateEventsHook(hadesmem::PatchDetourBase *detour, int param_1, uint32_t maxEventId) { @@ -1334,6 +1355,19 @@ namespace Nampower { char getItemIdCooldown[] = "GetItemIdCooldown"; RegisterLuaFunction(getItemIdCooldown, reinterpret_cast(Script_GetItemIdCooldown)); + + char getTrinketCooldown[] = "GetTrinketCooldown"; + RegisterLuaFunction(getTrinketCooldown, reinterpret_cast(Script_GetTrinketCooldown)); + + // 2.19 additions + char useItemIdOrName[] = "UseItemIdOrName"; + RegisterLuaFunction(useItemIdOrName, reinterpret_cast(Script_UseItemIdOrName)); + + char useTrinket[] = "UseTrinket"; + RegisterLuaFunction(useTrinket, reinterpret_cast(Script_UseTrinket)); + + char getTrinkets[] = "GetTrinkets"; + RegisterLuaFunction(getTrinkets, reinterpret_cast(Script_GetTrinkets)); } std::once_flag loadFlag; diff --git a/nampower/main.hpp b/nampower/main.hpp index f27fa06..c3eb2b1 100644 --- a/nampower/main.hpp +++ b/nampower/main.hpp @@ -30,7 +30,7 @@ namespace Nampower { constexpr uint32_t BUFFER_DECREASE_FREQUENCY = 10000; // time in ms between changes to lower buffer constexpr uint32_t MAJOR_VERSION = 2; - constexpr uint32_t MINOR_VERSION = 18; + constexpr uint32_t MINOR_VERSION = 19; constexpr uint32_t PATCH_VERSION = 0; constexpr int32_t LUA_REGISTRYINDEX = -10000; @@ -102,6 +102,7 @@ namespace Nampower { using CGPlayer_C_OnAttackIconPressedT = int (__fastcall *)(uintptr_t *this_ptr, void *dummy_edx, uint64_t guid); using CGActionBar_UseActionT = void (__fastcall *)(uint32_t param_1, int param_2, int param_3); + using CGCharacterInfo_UseItemT = void (__fastcall *)(uintptr_t *this_ptr, void *dummy_edx, uint32_t itemSlot, uint64_t *targetGuid); using GetSpellSlotAndTypeT = uint32_t (__fastcall *)(const char *, uint32_t *); using GetSpellSlotFromLuaT = uint32_t (__fastcall *)(int param_1, uint32_t *slot, uint32_t *type); diff --git a/nampower/offsets.hpp b/nampower/offsets.hpp index 8bfe993..48fd834 100644 --- a/nampower/offsets.hpp +++ b/nampower/offsets.hpp @@ -140,6 +140,8 @@ enum class Offsets : std::uint32_t { Script_SetCVar = 0x00488C10, Script_RunScript = 0x0048B980, Script_SpellStopCasting = 0x006E6E80, + Script_UseInventoryItem = 0x004c8de0, + Script_UseContainerItem = 0x004fa0e0, SStrDupA = 0X0064A620, @@ -197,6 +199,10 @@ enum class Offsets : std::uint32_t { CGPlayer_C_OnAttackIconPressed = 0X006131A0, + CGCharacterInfo_UseItem = 0x004c7970, + + CGItem_C_Use = 0x005D8D00, + DBCache_ItemCacheDBGetRow = 0x0055BA30, InvalidFunctionPtrCheck = 0x0042A320, diff --git a/nampower/spellcast.cpp b/nampower/spellcast.cpp index ffdf75b..0a603c7 100644 --- a/nampower/spellcast.cpp +++ b/nampower/spellcast.cpp @@ -855,6 +855,117 @@ namespace Nampower { return ret; } + bool doesSpellApplyAura(const game::SpellRec *spell) { + for (unsigned int i : spell->Effect) { + switch (i) { + case game::SPELL_EFFECT_APPLY_AURA: + case game::SPELL_EFFECT_APPLY_AREA_AURA_PARTY: + case game::SPELL_EFFECT_APPLY_AREA_AURA_RAID: + case game::SPELL_EFFECT_APPLY_AREA_AURA_FRIEND: + case game::SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case game::SPELL_EFFECT_APPLY_AREA_AURA_PET: + return true; + default: + break; + } + } + + return false; + } + + void TriggerAuraCastEvent(const game::SpellRec *spell, + uint64_t casterGuid, + uint64_t targetGuid, + uint64_t activePlayerGuid, + bool castByActivePlayer) { + auto eventToTrigger = game::AURA_CAST_ON_OTHER; + if (targetGuid == activePlayerGuid) { + eventToTrigger = game::AURA_CAST_ON_SELF; + } + + // ignore modifiers if not cast by active player + auto duration = game::GetSpellDuration(spell, !castByActivePlayer); + + auto *targetUnit = game::ClntObjMgrObjectPtr( + static_cast(game::TYPEMASK_PLAYER | game::TYPEMASK_UNIT), targetGuid); + + bool targetIsBuffCapped = false; + bool targetIsDebuffCapped = false; + + if (targetUnit) { + auto *unitFields = *reinterpret_cast(targetUnit + 68); + if (unitFields) { + targetIsBuffCapped = true; + for (int i = 31; i >= 0; --i) { + if (unitFields->aura[i] == 0) { + targetIsBuffCapped = false; + break; + } + } + + targetIsDebuffCapped = true; + for (int i = 47; i >= 32; --i) { + if (unitFields->aura[i] == 0) { + targetIsDebuffCapped = false; + break; + } + } + } + } + + char *casterGuidStr = ConvertGuidToString(casterGuid); + char *targetGuidStr = ConvertGuidToString(targetGuid); + char format[] = "%d%s%s%d%d%d%d%d%d"; + + for (int i = 0; i < 3; ++i) { + auto const effectType = spell->Effect[i]; + switch (effectType) { + case game::SPELL_EFFECT_APPLY_AURA: + case game::SPELL_EFFECT_APPLY_AREA_AURA_PARTY: + case game::SPELL_EFFECT_APPLY_AREA_AURA_RAID: + case game::SPELL_EFFECT_APPLY_AREA_AURA_FRIEND: + case game::SPELL_EFFECT_APPLY_AREA_AURA_ENEMY: + case game::SPELL_EFFECT_APPLY_AREA_AURA_PET: { + auto auraName = spell->EffectApplyAuraName[i]; + auto effectAmplitude = spell->EffectAmplitude[i]; + auto effectMiscValue = spell->EffectMiscValue[i]; + + auto auraCapStatus = static_cast(targetIsBuffCapped) | + (static_cast(targetIsDebuffCapped) << 1); + + ((int (__cdecl *)(int eventCode, + char *fmt, + uint32_t spellIdParam, + char *casterGuidStrParam, + char *targetGuidStrParam, + uint32_t effectParam, + uint32_t auraNameParam, + uint32_t effectAmplitudeParam, + uint32_t effectMiscValueParam, + uint32_t durationParam, + uint32_t auraCapStatusParam)) Offsets::SignalEventParam)( + eventToTrigger, + format, + spell->Id, + casterGuidStr, + targetGuidStr, + effectType, + auraName, + effectAmplitude, + effectMiscValue, + duration, + auraCapStatus); + break; + } + default: + break; + } + } + + delete[] casterGuidStr; + delete[] targetGuidStr; + } + void SpellGoHook(hadesmem::PatchDetourBase *detour, uint64_t *casterGUID, uint64_t *targetGUID, uint32_t spellId, @@ -862,14 +973,15 @@ namespace Nampower { auto const spellGo = detour->GetTrampolineT(); spellGo(casterGUID, targetGUID, spellId, spellData); - auto const castByActivePlayer = game::ClntObjMgrGetActivePlayerGuid() == *casterGUID; + auto const activePlayerGuid = game::ClntObjMgrGetActivePlayerGuid(); + auto const castByActivePlayer = activePlayerGuid == *casterGUID; + auto const spell = game::GetSpellInfo(spellId); if (castByActivePlayer) { auto const currentTime = GetTime(); // only care about our own casts if (!gCastData.channeling) { // check if spell is on swing - auto const spell = game::GetSpellInfo(spellId); if (spell->Attributes & game::SPELL_ATTR_ON_NEXT_SWING_1) { gLastCastData.onSwingStartTimeMs = currentTime; @@ -890,6 +1002,11 @@ namespace Nampower { } } } + + if (gUserSettings.enableAuraCastEvents && doesSpellApplyAura(spell)) { + auto targetGuidVal = targetGUID ? *targetGUID : *casterGUID; + TriggerAuraCastEvent(spell, *casterGUID, targetGuidVal, activePlayerGuid, castByActivePlayer); + } } void diff --git a/nampower/types.h b/nampower/types.h index 4e07d4e..9edd65d 100644 --- a/nampower/types.h +++ b/nampower/types.h @@ -28,6 +28,7 @@ struct UserSettings { bool quickcastOnDoubleCast; bool spamProtectionEnabled; + bool enableAuraCastEvents; uint32_t spellQueueWindowMs; uint32_t onSwingBufferCooldownMs;