show cooldown, proc chance, and ICD info in item tooltips

This commit is contained in:
avitasia
2026-04-01 12:53:24 -07:00
parent aea37f99e3
commit a41fb80abc
13 changed files with 15366 additions and 14723 deletions
+2
View File
@@ -111,6 +111,8 @@ SET NP_TargetingQueueWindowMs "1000"
- `NP_EnableSpellEnergizeEvents` - Whether to enable SPELL_ENERGIZE_BY_SELF, SPELL_ENERGIZE_BY_OTHER, and SPELL_ENERGIZE_ON_SELF events. 0 to disable, 1 to enable. Default is 0.
- `NP_EnableEnhancedTooltips` - Whether to enable nampower's tooltip additions such as spell proc chance, ICD, and item cooldown text. 0 to disable, 1 to enable. Default is 1.
- `NP_EnableUnitEventsPet` - Whether to fire unit state events for the `pet` and `partypet1``partypet4` unit tokens. 0 to disable, 1 to enable. Default is 1.
- `NP_EnableUnitEventsParty` - Whether to fire unit state events for the `party1``party4` unit tokens. 0 to disable, 1 to enable. Default is 1.
+15035 -14713
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -55,6 +55,8 @@ set(SOURCE_FILES
pet.cpp
unit.hpp
unit.cpp
tooltip.hpp
tooltip.cpp
)
add_library(${DLL_NAME} SHARED ${SOURCE_FILES})
+1
View File
@@ -33,5 +33,6 @@ namespace Nampower {
bool isOnGcdCategoryCooldown = false;
};
CooldownDetail GetItemSpellCooldownDetail(uint32_t itemId, uint32_t spellId);
CooldownDetail GetItemCooldownDetail(uint32_t itemId);
}
+44 -5
View File
@@ -14,6 +14,10 @@ namespace Nampower {
// Reusable table reference to reduce memory allocations
static int cooldownDetailTableRef = LUA_REFNIL;
static uint32_t GetNonNegativeItemCooldownMs(int32_t durationMs) {
return durationMs > 0 ? static_cast<uint32_t>(durationMs) : 0;
}
game::SpellHistoryEntry *GetSpellHistoryHead() {
auto const spellHistoryAddr = static_cast<uintptr_t>(Offsets::SpellHistories);
@@ -127,11 +131,13 @@ namespace Nampower {
auto *itemStats = GetItemStats(itemId);
if (itemStats) {
for (int i = 0; i < 5; ++i) {
if (itemStats->m_spellCooldown[i] > 0 || itemStats->m_spellCategoryCooldown[i] > 0) {
auto const spellCooldownMs = GetNonNegativeItemCooldownMs(itemStats->m_spellCooldown[i]);
auto const spellCategoryCooldownMs = GetNonNegativeItemCooldownMs(itemStats->m_spellCategoryCooldown[i]);
if (spellCooldownMs > 0 || spellCategoryCooldownMs > 0) {
detail.itemHasActiveSpell = true;
detail.itemActiveSpellId = itemStats->m_spellID[i];
detail.individualDurationMs = itemStats->m_spellCooldown[i];
detail.categoryDurationMs = itemStats->m_spellCategoryCooldown[i];
detail.individualDurationMs = spellCooldownMs;
detail.categoryDurationMs = spellCategoryCooldownMs;
return detail;
}
}
@@ -199,6 +205,40 @@ namespace Nampower {
PushTableValue(luaState, isOnGcdCategoryCooldownKey, detail.isOnGcdCategoryCooldown ? 1 : 0);
}
CooldownDetail GetItemSpellCooldownDetail(uint32_t itemId, uint32_t spellId) {
auto *itemStats = GetItemStats(itemId);
if (!itemStats || spellId == 0) {
return CooldownDetail{};
}
for (int i = 0; i < 5; ++i) {
if (itemStats->m_spellID[i] != spellId) {
continue;
}
auto const categoryOverride = static_cast<uint32_t>(itemStats->m_spellCategory[i]);
auto detail = GetCooldownFromSpellHistory(spellId, itemId, categoryOverride);
auto const spellCooldownMs = GetNonNegativeItemCooldownMs(itemStats->m_spellCooldown[i]);
auto const spellCategoryCooldownMs = GetNonNegativeItemCooldownMs(itemStats->m_spellCategoryCooldown[i]);
if (spellCooldownMs > 0 || spellCategoryCooldownMs > 0) {
detail.itemHasActiveSpell = true;
detail.itemActiveSpellId = spellId;
detail.individualDurationMs = spellCooldownMs;
detail.categoryDurationMs = spellCategoryCooldownMs;
} else {
// Passive/on-equip effects can share a spell record but don't expose an item-use cooldown.
detail.gcdCategoryDurationMs = 0;
detail.gcdCategoryId = 0;
}
return detail;
}
return CooldownDetail{};
}
CooldownDetail GetItemCooldownDetail(uint32_t itemId) {
auto *itemStats = GetItemStats(itemId);
@@ -212,8 +252,7 @@ namespace Nampower {
continue;
}
auto const categoryOverride = static_cast<uint32_t>(itemStats->m_spellCategory[i]);
auto const detail = GetCooldownFromSpellHistory(spellId, itemId, categoryOverride);
auto const detail = GetItemSpellCooldownDetail(itemId, spellId);
if (detail.cooldownRemainingMs >= longestCooldownMs) {
activeCooldownDetail = detail;
+1 -1
View File
@@ -66,7 +66,7 @@ namespace Nampower {
{"maxDamage", offsetof(game::ItemStats_C, m_maxDamage), FieldType::FLOAT, 5},
{"damageType", offsetof(game::ItemStats_C, m_damageType), FieldType::INT32, 5},
{"resistances", offsetof(game::ItemStats_C, m_resistances), FieldType::INT32, 7},
{"spellID", offsetof(game::ItemStats_C, m_spellID), FieldType::INT32, 5},
{"spellID", offsetof(game::ItemStats_C, m_spellID), FieldType::UINT32, 5},
{"spellTrigger", offsetof(game::ItemStats_C, m_spellTrigger), FieldType::INT32, 5},
{"spellCharges", offsetof(game::ItemStats_C, m_spellCharges), FieldType::INT32, 5},
{"spellCooldown", offsetof(game::ItemStats_C, m_spellCooldown), FieldType::INT32, 5},
+16 -1
View File
@@ -497,7 +497,7 @@ namespace game {
int m_delay;
int m_ammoType;
int m_rangedModRange;
int m_spellID[5];
uint32_t m_spellID[5];
int m_spellTrigger[5];
int m_spellCharges[5];
int m_spellCooldown[5];
@@ -1013,6 +1013,21 @@ namespace game {
// Each caster has his own aura slot, instead of replacing others
};
enum ItemSpelltriggerType
{
ITEM_SPELLTRIGGER_ON_USE = 0, // use after equip cooldown
ITEM_SPELLTRIGGER_ON_EQUIP = 1,
ITEM_SPELLTRIGGER_CHANCE_ON_HIT = 2,
ITEM_SPELLTRIGGER_SOULSTONE = 4,
/*
* ItemSpelltriggerType 5 might have changed on 2.4.3/3.0.3: Such auras
* will be applied on item pickup and removed on item loss - maybe on the
* other hand the item is destroyed if the aura is removed ("removed on
* death" of spell 57348 makes me think so)
*/
ITEM_SPELLTRIGGER_ON_NO_DELAY_USE = 5, // no equip cooldown
};
enum SpellTarget {
TARGET_NONE = 0,
TARGET_UNIT_CASTER = 1,
+21
View File
@@ -49,6 +49,7 @@
#include "autoattack.hpp"
#include "unit.hpp"
#include "pet.hpp"
#include "tooltip.hpp"
#include <cstdint>
#include <memory>
@@ -175,6 +176,8 @@ namespace Nampower {
std::unique_ptr<hadesmem::PatchDetour<CGUnit_C_HandleEnvironmentDamageT> > gCGUnit_C_HandleEnvironmentDamageDetour;
std::unique_ptr<hadesmem::PatchDetour<UnitCombatLogDamageShieldT> > gUnitCombatLogDamageShieldDetour;
std::unique_ptr<hadesmem::PatchDetour<LoadScriptFunctionsT> > gGlueLoadScriptFunctionsDetour;
std::unique_ptr<hadesmem::PatchDetour<CGTooltip_SetItemT> > gCGTooltip_SetItemDetour;
std::unique_ptr<hadesmem::PatchDetour<SpellParserParseTextT> > gSpellParserParseTextDetour;
// Flags for one-time initialization
std::once_flag loadFlag;
@@ -743,6 +746,9 @@ namespace Nampower {
} else if (strcmp(cvar, "NP_PreventMountingWhenBuffCapped") == 0) {
gUserSettings.preventMountingWhenBuffCapped = atoi(value) != 0;
DEBUG_LOG("Set NP_PreventMountingWhenBuffCapped to " << gUserSettings.preventMountingWhenBuffCapped);
} else if (strcmp(cvar, "NP_EnableEnhancedTooltips") == 0) {
gUserSettings.enableEnhancedTooltips = atoi(value) != 0;
DEBUG_LOG("Set NP_EnableEnhancedTooltips to " << gUserSettings.enableEnhancedTooltips);
} else if (strcmp(cvar, "NP_MinBufferTimeMs") == 0) {
gUserSettings.minBufferTimeMs = atoi(value);
DEBUG_LOG("Set NP_MinBufferTimeMs and current buffer to " << gUserSettings.minBufferTimeMs);
@@ -1164,6 +1170,16 @@ namespace Nampower {
0, // unk2
0); // unk3
char NP_EnableEnhancedTooltips[] = "NP_EnableEnhancedTooltips";
CVarRegister(NP_EnableEnhancedTooltips, // name
nullptr, // help
0, // unk1
gUserSettings.enableEnhancedTooltips ? defaultTrue : defaultFalse, // default value address
nullptr, // callback
1, // category
0, // unk2
0); // unk3
char NP_OnSwingBufferCooldownMs[] = "NP_OnSwingBufferCooldownMs";
CVarRegister(NP_OnSwingBufferCooldownMs, // name
nullptr, // help
@@ -1385,6 +1401,7 @@ namespace Nampower {
loadUserVar("NP_EnableSpellHealEvents");
loadUserVar("NP_EnableSpellEnergizeEvents");
loadUserVar("NP_PreventMountingWhenBuffCapped");
loadUserVar("NP_EnableEnhancedTooltips");
loadUserVar("NP_MinBufferTimeMs");
loadUserVar("NP_NonGcdBufferTimeMs");
@@ -1602,6 +1619,10 @@ namespace Nampower {
process, Offsets::CGUnit_C_HandleEnvironmentDamage, &CGUnit_C_HandleEnvironmentDamageHook);
gUnitCombatLogDamageShieldDetour = createHook<UnitCombatLogDamageShieldT>(
process, Offsets::UnitCombatLogDamageShield, &UnitCombatLogDamageShieldHook);
gCGTooltip_SetItemDetour = createHook<CGTooltip_SetItemT>(
process, Offsets::CGTooltip_SetItem, &CGTooltip_SetItemHook);
gSpellParserParseTextDetour = createHook<SpellParserParseTextT>(
process, Offsets::SpellParserParseText, &SpellParserParseTextHook);
gLoadScriptFunctionsDetour = createHook<LoadScriptFunctionsT>(process, Offsets::Player_LoadScriptFunctions,
&Player_LoadScriptFunctionsHook);
gGlueLoadScriptFunctionsDetour = createHook<LoadScriptFunctionsT>(process, Offsets::Glue_LoadScriptFunctions,
+2 -2
View File
@@ -26,8 +26,8 @@ namespace Nampower {
constexpr uint32_t DISENCHANT_QUALITY_PURPLE = 0x04; // Epic
constexpr uint32_t MAJOR_VERSION = 4;
constexpr uint32_t MINOR_VERSION = 2;
constexpr uint32_t PATCH_VERSION = 2;
constexpr uint32_t MINOR_VERSION = 3;
constexpr uint32_t PATCH_VERSION = 0;
constexpr int32_t LUA_REGISTRYINDEX = -10000;
constexpr int32_t LUA_GLOBALSINDEX = -10001;
+2
View File
@@ -289,6 +289,8 @@ enum class Offsets : std::uint32_t {
TogglePetSlotAutocast = 0x004bcbb0,
CGPetInfo_GetPetSpellAction = 0x004bd190,
CGPetInfo_SendPetAction = 0x004bd1d0,
CGTooltip_SetItem = 0x0052B650,
SpellParserParseText = 0x005075F0,
PetActionBarSlots = 0x00b71438,
PetReactionMode = 0x00b71468,
ActivePetGuid = 0x00b714a0,
+187
View File
@@ -0,0 +1,187 @@
#include "tooltip.hpp"
#include "cooldown.hpp"
#include "items.hpp"
#include <cstdio>
#include <cstring>
#include <unordered_map>
namespace Nampower {
static const std::unordered_map<uint32_t, uint32_t> gKnownSpellIcdSeconds = {
{12322, 3}, // Unbridled Wrath
{18137, 3}, // Shadowguard
{28780, 1}, // The Eye of the Dead
{44096, 2}, // Ancient Accord Passive
{48003, 1}, // Burning Hatred
{51061, 18}, // Uncontained Magic Passive
{51272, 3}, // Wisdom of the Mak'aru Passive
{52864, 1}, // Midnight Comet
{52987, 8}, // Command Whelp Passive
{52998, 5}, // Decayed Heart Passive
};
uint32_t gActiveTooltipItemId = 0;
static bool ItemHasOnUseSpellId(uint32_t itemId, uint32_t spellId) {
auto const *itemStats = GetItemStats(itemId);
if (!itemStats || spellId == 0) {
return false;
}
for (int i = 0; i < 5; ++i) {
auto const spellCooldownMs = itemStats->m_spellCooldown[i] > 0 ? itemStats->m_spellCooldown[i] : 0;
auto const spellCategoryCooldownMs = itemStats->m_spellCategoryCooldown[i] > 0
? itemStats->m_spellCategoryCooldown[i]
: 0;
auto const spellTrigger = itemStats->m_spellTrigger[i];
if (static_cast<uint32_t>(itemStats->m_spellID[i]) == spellId &&
spellTrigger != game::ITEM_SPELLTRIGGER_ON_EQUIP &&
spellTrigger != game::ITEM_SPELLTRIGGER_CHANCE_ON_HIT &&
(spellCooldownMs > 0 || spellCategoryCooldownMs > 0)) {
return true;
}
}
return false;
}
int CGTooltip_SetItemHook(hadesmem::PatchDetourBase *detour,
void *thisptr,
void *dummy_edx,
uint32_t itemId,
int **param_2,
int **param_3,
int param_4,
int param_5,
int param_6,
int param_7,
uint32_t param_8,
int ****param_9) {
auto const cgTooltipSetItem = detour->GetTrampolineT<CGTooltip_SetItemT>();
gActiveTooltipItemId = itemId;
auto const result = cgTooltipSetItem(thisptr, dummy_edx, itemId, param_2, param_3, param_4, param_5, param_6,
param_7, param_8, param_9);
gActiveTooltipItemId = 0;
return result;
}
static uint32_t GetTooltipItemCooldownDurationMs(uint32_t itemId, uint32_t spellId) {
auto const detail = GetItemSpellCooldownDetail(itemId, spellId);
auto durationMs = detail.individualDurationMs;
if (detail.categoryDurationMs > durationMs) {
durationMs = detail.categoryDurationMs;
}
return durationMs;
}
static uint32_t GetKnownSpellIcdDurationMs(uint32_t spellId) {
auto const it = gKnownSpellIcdSeconds.find(spellId);
if (it == gKnownSpellIcdSeconds.end()) {
return 0;
}
return it->second * 1000;
}
static bool AppendTooltipText(char *buffer, size_t bufferSize, const char *text) {
if (!buffer || !text || bufferSize == 0) {
return false;
}
auto const currentLength = std::strlen(buffer);
auto const textLength = std::strlen(text);
auto const maxLength = bufferSize - 1;
if (currentLength >= maxLength || textLength > (maxLength - currentLength)) {
return false;
}
std::strncat(buffer, text, textLength);
return true;
}
static bool AppendTooltipDurationText(char *buffer,
size_t bufferSize,
const char *label,
uint32_t durationMs) {
if (durationMs == 0) {
return false;
}
char text[32] = "";
if (durationMs >= 60000) {
std::snprintf(text, sizeof(text), " %s: %.1f min", label, durationMs / 60000.0f);
} else {
std::snprintf(text, sizeof(text), " %s: %u sec", label, static_cast<uint32_t>((durationMs + 500) / 1000));
}
return AppendTooltipText(buffer, bufferSize, text);
}
static bool AppendTooltipIcdText(char *buffer, size_t bufferSize, uint32_t durationMs) {
if (durationMs == 0) {
return false;
}
char text[24] = "";
if (durationMs >= 60000) {
std::snprintf(text, sizeof(text), " ICD: %.1fm", durationMs / 60000.0f);
} else {
std::snprintf(text, sizeof(text), " ICD: %us", static_cast<uint32_t>((durationMs + 500) / 1000));
}
return AppendTooltipText(buffer, bufferSize, text);
}
static bool AppendTooltipProcChanceText(char *buffer, size_t bufferSize, uint32_t procChance) {
if (!buffer || procChance == 0 || procChance >= 100) {
return false;
}
char procChanceText[16] = "";
int procChanceTextLength = std::snprintf(procChanceText, sizeof(procChanceText), "%u%%", procChance);
if (procChanceTextLength <= 0 || std::strstr(buffer, procChanceText) != nullptr) {
return false;
}
char chanceText[24] = "";
std::snprintf(chanceText, sizeof(chanceText), " Chance: %u%%", procChance);
return AppendTooltipText(buffer, bufferSize, chanceText);
}
bool SpellParserParseTextHook(hadesmem::PatchDetourBase *detour,
game::SpellRec *spellRec,
char *buffer,
int bufferSize,
float param_4,
float param_5,
float param_6,
float param_7,
float param_8) {
auto const spellParserParseText = detour->GetTrampolineT<SpellParserParseTextT>();
auto const result = spellParserParseText(spellRec, buffer, bufferSize, param_4, param_5, param_6, param_7,
param_8);
if (!result || !gUserSettings.enableEnhancedTooltips || !spellRec || !buffer || bufferSize <= 0) {
return result;
}
// Add item cooldown if on use spell
if (gActiveTooltipItemId != 0 && ItemHasOnUseSpellId(gActiveTooltipItemId, spellRec->Id)) {
AppendTooltipDurationText(buffer,
static_cast<size_t>(bufferSize),
"CD",
GetTooltipItemCooldownDurationMs(gActiveTooltipItemId, spellRec->Id));
}
// Add ICD if available
AppendTooltipIcdText(buffer, static_cast<size_t>(bufferSize), GetKnownSpellIcdDurationMs(spellRec->Id));
// Add proc chance from dbc if available
AppendTooltipProcChanceText(buffer, static_cast<size_t>(bufferSize), spellRec->procChance);
return result;
}
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "main.hpp"
namespace Nampower {
extern uint32_t gActiveTooltipItemId;
using SpellParserParseTextT = bool (__fastcall *)(game::SpellRec *spellRec,
char *buffer,
int bufferSize,
float param_4,
float param_5,
float param_6,
float param_7,
float param_8);
bool SpellParserParseTextHook(hadesmem::PatchDetourBase *detour,
game::SpellRec *spellRec,
char *buffer,
int bufferSize,
float param_4,
float param_5,
float param_6,
float param_7,
float param_8);
using CGTooltip_SetItemT = int (__fastcall *)(void *thisptr,
void *dummy_edx,
uint32_t itemId,
int **param_2,
int **param_3,
int param_4,
int param_5,
int param_6,
int param_7,
uint32_t param_8,
int ****param_9);
int CGTooltip_SetItemHook(hadesmem::PatchDetourBase *detour,
void *thisptr,
void *dummy_edx,
uint32_t itemId,
int **param_2,
int **param_3,
int param_4,
int param_5,
int param_6,
int param_7,
uint32_t param_8,
int ****param_9);
}
+2 -1
View File
@@ -44,6 +44,7 @@ struct UserSettings {
bool enableSpellHealEvents = false;
bool enableSpellEnergizeEvents = false;
bool preventMountingWhenBuffCapped = true;
bool enableEnhancedTooltips = true;
uint32_t spellQueueWindowMs = 500;
uint32_t onSwingBufferCooldownMs = 500;
@@ -172,4 +173,4 @@ struct SpellSlotCacheEntry {
uint32_t type;
std::string cachedNameLower;
std::string cachedRankLower;
};
};