add logging and disenchant all ability

This commit is contained in:
avitasia
2025-12-26 13:04:16 -08:00
parent 960239c4b7
commit bd9049f8bc
15 changed files with 10031 additions and 9664 deletions
+63
View File
@@ -11,6 +11,7 @@ For installation, configuration, and general usage information, see the main [RE
- [Spell Casting and Queuing](#spell-casting-and-queuing)
- [Cast Information](#cast-information)
- [Cooldown Information](#cooldown-information)
- [Utility Functions](#utility-functions)
---
## Performance Optimization - Table References
@@ -892,3 +893,65 @@ 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.
---
### Utility Functions
#### DisenchantAll(itemIdOrName) or DisenchantAll(quality)
Automatically disenchants items in your inventory. Can disenchant a specific item by ID/name, or all weapons and armor of a specified quality.
**⚠️ WARNING ⚠️**
**THIS FUNCTION WILL AUTOMATICALLY DISENCHANT ITEMS WITHOUT CONFIRMATION!**
- **Use at your own risk** - there is no undo for disenchanting
- Only disenchants items from **player inventory bags (0-4)** - does NOT touch bank items
- Make sure you have the Disenchant spell and the items are disenchantable before using
- **Always double-check your bags** before running this command
**Parameters:**
**Mode 1: Disenchant by Item ID or Name**
- `itemIdOrName` (number|string): Item ID (number) or item name (string)
- Disenchants all copies of the specified item found in your bags
- Works on any disenchantable item type (weapons, armor, etc.)
**Mode 2: Disenchant by Quality** *(weapons and armor only)*
- `quality` (string): Must be either:
- `"greens"` - Disenchants all uncommon (green) quality weapons and armor
- `"blues"` - Disenchants all rare (blue) quality weapons and armor
- Only affects **weapons** (class 2) and **armor** (class 4)
**Returns:**
- `1` if the first disenchant succeeded
- `0` if no matching items were found or the disenchant failed
**Behavior:**
- Searches player inventory bags (0-4) only - **bank items are protected**
- Finds the first matching item in your inventory
- Casts Disenchant spell on that item
- Automatically continues disenchanting matching items every 4 seconds
- Stops when no more matching items are found or an error occurs
**Examples:**
```lua
-- Disenchant all green weapons and armor in your bags
DisenchantAll("greens")
-- Disenchant all blue weapons and armor in your bags
DisenchantAll("blues")
-- Disenchant a specific item by ID
DisenchantAll(12345)
-- Disenchant a specific item by name
DisenchantAll("Glowing Brightwood Staff")
```
**Important Notes:**
- The function runs continuously until all matching items are disenchanted
- Items must be in your inventory bags (0-4), not in bank, keyring, or equipped
- When using quality mode ("greens"/"blues"), only weapons and armor are affected
- When using item ID/name mode, any disenchantable item can be targeted
- Make sure you have enough bag space for the disenchanting materials
- The function will stop if you run out of matching items or if the disenchant spell fails
- **REVIEW YOUR BAGS CAREFULLY BEFORE USE** - disenchanting cannot be undone!
+9687 -9650
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -96,7 +96,7 @@ namespace game {
const SpellRec *GetSpellInfo(uint32_t spellId) {
auto const spellDb = reinterpret_cast<WowClientDB<SpellRec> *>(Offsets::SpellDb);
if (spellId > spellDb->m_maxId)
if (spellId > spellDb->m_maxId || spellId <= 0)
return nullptr;
return spellDb->m_recordsById[spellId];
+11
View File
@@ -336,6 +336,17 @@ namespace game {
int charges;
};
enum ItemQualities
{
ITEM_QUALITY_POOR = 0, // GREY
ITEM_QUALITY_NORMAL = 1, // WHITE
ITEM_QUALITY_UNCOMMON = 2, // GREEN
ITEM_QUALITY_RARE = 3, // BLUE
ITEM_QUALITY_EPIC = 4, // PURPLE
ITEM_QUALITY_LEGENDARY = 5, // ORANGE
ITEM_QUALITY_ARTIFACT = 6 // LIGHT YELLOW
};
struct CGObject {
void *vftable;
uintptr_t *m_data;
+1 -1
View File
@@ -24,7 +24,7 @@ namespace Nampower {
lua_tonumberT lua_tonumber = reinterpret_cast<lua_tonumberT>(Offsets::lua_tonumber);
lua_pushnumberT lua_pushnumber = reinterpret_cast<lua_pushnumberT>(Offsets::lua_pushnumber);
lua_pushstringT lua_pushstring = reinterpret_cast<lua_pushstringT>(Offsets::lua_pushstring);
lua_pushbooleanT lua_pushboolean = reinterpret_cast<lua_pushbooleanT>(Offsets::lua_pushboolean);
// lua_pushbooleanT lua_pushboolean = reinterpret_cast<lua_pushbooleanT>(Offsets::lua_pushboolean); doesn't seem to work properly
lua_pushnilT lua_pushnil = reinterpret_cast<lua_pushnilT>(Offsets::lua_pushnil);
lua_newtableT lua_newtable = reinterpret_cast<lua_newtableT>(Offsets::lua_newtable);
lua_settableT lua_settable = reinterpret_cast<lua_settableT>(Offsets::lua_settable);
+85
View File
@@ -217,6 +217,91 @@ namespace Nampower {
return result;
}
PlayerItemSearchResult FindPlayerDisenchantItem(int32_t quality) {
PlayerItemSearchResult result{};
auto const getBagItem = reinterpret_cast<CGBag_C_GetItemAtSlotT>(Offsets::CGBag_C_GetItemAtSlot);
auto const getContainerGuid = reinterpret_cast<GetContainerGuidT>(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) {
if (!item) {
return false;
}
uint32_t itemId = game::GetItemId(item);
auto *itemStats = GetItemStats(itemId);
if (!itemStats) {
return false;
}
// Check if item is weapon or armor
if (itemStats->m_class != game::ITEM_CLASS_WEAPON &&
itemStats->m_class != game::ITEM_CLASS_ARMOR) {
return false;
}
if (itemStats->m_quality != quality) {
return false;
}
return true;
};
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;
}
}
}
// don't search bank
return result;
}
std::string escapeJsonString(const char *str) {
if (!str) return "null";
+1
View File
@@ -100,6 +100,7 @@ namespace Nampower {
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);
PlayerItemSearchResult FindPlayerDisenchantItem(int32_t quality);
uintptr_t *GetBagPtrFromContainer(uintptr_t *containerPtr);
}
+21 -2
View File
@@ -64,6 +64,10 @@ namespace Nampower {
uint32_t gBufferTimeMs; // adjusts dynamically depending on errors
uint32_t gDisenchantItemId = 0;
int32_t gDisenchantQuality = -1; // -1 = unset
uint32_t gNextDisenchantTimeMs = 0;
bool gForceQueueCast;
bool gNoQueueCast;
bool gQueuesProcessed;
@@ -97,7 +101,7 @@ namespace Nampower {
std::unique_ptr<hadesmem::PatchDetour<SetCVarT> > gSetCVarDetour;
std::unique_ptr<hadesmem::PatchDetour<CGSpellBook_CastSpellT> > gCGSpellBook_CastSpellDetour;
std::unique_ptr<hadesmem::PatchDetour<CastSpellT> > gCastDetour;
std::unique_ptr<hadesmem::PatchDetour<Spell_C_CastSpellT> > gCastDetour;
std::unique_ptr<hadesmem::PatchDetour<SendCastT> > gSendCastDetour;
std::unique_ptr<hadesmem::PatchDetour<CancelSpellT> > gCancelSpellDetour;
std::unique_ptr<hadesmem::PatchDetour<SignalEventT> > gSignalEventDetour;
@@ -520,6 +524,18 @@ namespace Nampower {
// script ran, stop processing
return true;
}
// Check for disenchant all
if (gDisenchantItemId != 0 || gDisenchantQuality != -1) {
auto currentTime = GetTime();
// Try to disenchant when timer has elapsed
if (gNextDisenchantTimeMs <= currentTime) {
if (TryDisenchant()) {
return true;
}
}
}
}
gQueuesProcessed = true;
@@ -1121,7 +1137,7 @@ namespace Nampower {
process, Offsets::CGSpellBook_CastSpell, &CGSpellBook_CastSpellHook);
gCGActionBar_UseActionDetour = createHook<CGActionBar_UseActionT>(
process, Offsets::CGActionBar_UseAction, &CGActionBar_UseActionHook);
gCastDetour = createHook<CastSpellT>(process, Offsets::Spell_C_CastSpell, &Spell_C_CastSpellHook);
gCastDetour = createHook<Spell_C_CastSpellT>(process, Offsets::Spell_C_CastSpell, &Spell_C_CastSpellHook);
gSendCastDetour = createHook<SendCastT>(process, Offsets::SendCast, &SendCastHook);
gCancelSpellDetour = createHook<CancelSpellT>(process, Offsets::CancelSpell, &CancelSpellHook);
gCastResultHandlerDetour = createHook<PacketHandlerT>(process, Offsets::CastResultHandler,
@@ -1373,6 +1389,9 @@ namespace Nampower {
char getTrinkets[] = "GetTrinkets";
RegisterLuaFunction(getTrinkets, reinterpret_cast<uintptr_t *>(Script_GetTrinkets));
char disenchantAll[] = "DisenchantAll";
RegisterLuaFunction(disenchantAll, reinterpret_cast<uintptr_t *>(Script_DisenchantAll));
}
void load() {
+7 -3
View File
@@ -30,8 +30,8 @@ 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 = 20;
constexpr uint32_t PATCH_VERSION = 1;
constexpr uint32_t MINOR_VERSION = 21;
constexpr uint32_t PATCH_VERSION = 0;
constexpr int32_t LUA_REGISTRYINDEX = -10000;
constexpr int32_t LUA_GLOBALSINDEX = -10001;
@@ -42,6 +42,10 @@ namespace Nampower {
extern uint32_t gBufferTimeMs; // adjusts dynamically depending on errors
extern uint32_t gDisenchantItemId;
extern int32_t gDisenchantQuality;
extern uint32_t gNextDisenchantTimeMs;
extern bool gForceQueueCast;
extern bool gNoQueueCast;
@@ -70,7 +74,7 @@ namespace Nampower {
using RangeCheckSelectedT = bool (__fastcall *)(uintptr_t *playerUnit, const game::SpellRec *,
std::uint64_t targetGuid, char ignoreErrors);
using CGSpellBook_CastSpellT = void (__fastcall *)(uint32_t spellSlot, int bookType, uint64_t target);
using CastSpellT = bool (__fastcall *)(uintptr_t *playerUnit, uint32_t spellId, uintptr_t *item,
using Spell_C_CastSpellT = bool (__fastcall *)(uintptr_t *playerUnit, uint32_t spellId, game::CGItem_C *item,
std::uint64_t targetGuid);
using SendCastT = void (__fastcall *)(game::SpellCast *, char unk);
using CancelSpellT = void (__fastcall *)(bool, bool, game::SpellCastResult);
+123
View File
@@ -611,4 +611,127 @@ namespace Nampower {
return 0;
}
bool TryDisenchant() {
DEBUG_LOG("Trying disenchant with quality filter " << gDisenchantQuality << " itemId " << gDisenchantItemId);
if (gDisenchantQuality < 0 && gDisenchantItemId == 0) {
return false;
}
// Find the item in player inventory
PlayerItemSearchResult itemSearchResult;
if (gDisenchantItemId != 0) {
// Item-specific disenchanting
itemSearchResult = FindPlayerItem(gDisenchantItemId, nullptr);
} else {
// Quality-based disenchanting (weapons and armor only)
itemSearchResult = FindPlayerDisenchantItem(gDisenchantQuality);
}
if (!itemSearchResult.found()) {
// Item not found, stop disenchanting
DEBUG_LOG("No disenchantable item found, stopping disenchant");
gDisenchantItemId = 0;
gDisenchantQuality = -1;
gNextDisenchantTimeMs = 0;
return false;
}
// Get the item GUID
auto item = itemSearchResult.item;
if (!item || !item->object.m_obj) {
// Invalid item, stop disenchanting
DEBUG_LOG("Invalid item, stopping disenchant");
gDisenchantItemId = 0;
gDisenchantQuality = -1;
gNextDisenchantTimeMs = 0;
return false;
}
uint64_t itemGuid = item->object.m_obj->m_guid;
// Get player unit for casting
auto playerGuid = game::ClntObjMgrGetActivePlayerGuid();
auto playerUnit = game::GetObjectPtr(playerGuid);
if (!playerUnit) {
// No player unit, stop disenchanting
DEBUG_LOG("No player unit, stopping disenchant");
gDisenchantItemId = 0;
gDisenchantQuality = -1;
gNextDisenchantTimeMs = 0;
return false;
}
// Set timer for next disenchant attempt (5 seconds to allow time for inventory to update)
auto currentTime = GetTime();
gNextDisenchantTimeMs = currentTime + 5000;
// Cast Disenchant spell (13262) on the item
auto const castSpell = reinterpret_cast<Spell_C_CastSpellT>(Offsets::Spell_C_CastSpell);
bool success = castSpell(playerUnit, 13262, nullptr, itemGuid);
if (!success) {
// Cast failed, stop disenchanting
DEBUG_LOG("Cast failed, stopping disenchant");
gDisenchantItemId = 0;
gDisenchantQuality = -1;
gNextDisenchantTimeMs = 0;
return false;
}
return true;
}
uint32_t Script_DisenchantAll(uintptr_t *luaState) {
luaState = GetLuaStatePtr();
if (!lua_isnumber(luaState, 1) && !lua_isstring(luaState, 1)) {
lua_error(luaState, "Usage: DisenchantAll(itemIdOrName) or DisenchantAll(quality)\n"
"quality: \"greens\" for uncommon, \"blues\" for rare\n"
"itemIdOrName: item ID (number) or item name (string)");
return 0;
}
// Reset both modes
gDisenchantItemId = 0;
gDisenchantQuality = -1;
// Check number first (lua_isstring will also match numbers)
if (lua_isnumber(luaState, 1)) {
// It's an item ID
gDisenchantItemId = static_cast<uint32_t>(lua_tonumber(luaState, 1));
} else if (lua_isstring(luaState, 1)) {
// It's a string - check if it's quality-based or item name
const char *param = lua_tostring(luaState, 1);
if (_stricmp(param, "greens") == 0) {
gDisenchantQuality = game::ITEM_QUALITY_UNCOMMON; // 2
} else if (_stricmp(param, "blues") == 0) {
gDisenchantQuality = game::ITEM_QUALITY_RARE; // 3
} else {
// It's an item name
uint32_t cachedItemId = GetItemIdFromCache(param);
if (cachedItemId != 0) {
gDisenchantItemId = cachedItemId;
} else {
// Try to find the item to get its ID
auto itemSearchResult = FindPlayerItem(0, param);
if (itemSearchResult.found()) {
gDisenchantItemId = game::GetItemId(itemSearchResult.item);
// Cache the item name for future lookups
CacheItemNameToId(param, gDisenchantItemId);
} else {
// Item not found
lua_pushnumber(luaState, 0);
return 1;
}
}
}
}
// Try the first disenchant
bool success = TryDisenchant();
lua_pushnumber(luaState, success ? 1 : 0);
return 1;
}
}
+4
View File
@@ -30,5 +30,9 @@ namespace Nampower {
uint32_t Script_StartItemExport(uintptr_t *luaState);
bool TryDisenchant();
uint32_t Script_DisenchantAll(uintptr_t *luaState);
bool RunQueuedScript(int priority);
}
+17 -5
View File
@@ -299,7 +299,7 @@ namespace Nampower {
void SaveCastParams(CastSpellParams *params,
uint32_t *casterUnit,
uint32_t spellId,
uintptr_t *item,
game::CGItem_C *item,
std::uint64_t guid,
uint32_t gcDCategory,
uint32_t castTimeMs,
@@ -444,7 +444,7 @@ namespace Nampower {
bool
Spell_C_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t *casterUnit, uint32_t spellId,
uintptr_t *item,
game::CGItem_C *item,
std::uint64_t guid) {
// save the detour to allow quickly calling this hook
castSpellDetour = detour;
@@ -459,7 +459,7 @@ namespace Nampower {
DEBUG_LOG("Ignoring non active player cast of spell " << game::GetSpellName(spellId) << " " << spellId);
// just call original function if caster is not the active player
// happens with Doomguard rain of fire
auto const castSpell = detour->GetTrampolineT<CastSpellT>();
auto const castSpell = detour->GetTrampolineT<Spell_C_CastSpellT>();
return castSpell(casterUnit, spellId, item, guid);
}
@@ -478,6 +478,7 @@ namespace Nampower {
if (casterUnit == playerUnit && gUserSettings.preventMountingWhenBuffCapped) {
// Check for mount spell when player is buff capped
if (SpellIsMounting(spell)) {
// check if they are not already mounted
if (playerUnit && game::UnitIsBuffCapped(playerUnit) && game::UnitGetMountDisplayId(playerUnit) == 0) {
DEBUG_LOG("Blocking mount spell " << spellName << " due to buff cap");
@@ -562,7 +563,7 @@ namespace Nampower {
gNextCastId++;
// try to cast the spell
auto const castSpell = detour->GetTrampolineT<CastSpellT>();
auto const castSpell = detour->GetTrampolineT<Spell_C_CastSpellT>();
auto ret = castSpell(casterUnit, spellId, item, guid);
TriggerSpellCastEvent(ret, spellId, ON_SWING, guid, itemId);
@@ -587,7 +588,7 @@ namespace Nampower {
return ret;
}
auto const castSpell = detour->GetTrampolineT<CastSpellT>();
auto const castSpell = detour->GetTrampolineT<Spell_C_CastSpellT>();
auto effectiveCastEndMs = EffectiveCastEndMs();
auto remainingEffectiveCastTime = (effectiveCastEndMs > currentTime) ? effectiveCastEndMs - currentTime : 0;
@@ -877,6 +878,10 @@ namespace Nampower {
}
bool doesSpellApplyAura(const game::SpellRec *spell) {
if (!spell) {
return false;
}
for (unsigned int i: spell->Effect) {
switch (i) {
case game::SPELL_EFFECT_APPLY_AURA:
@@ -977,6 +982,11 @@ namespace Nampower {
auto const castByActivePlayer = activePlayerGuid == *casterGUID;
auto const spell = game::GetSpellInfo(spellId);
if (!spell || spell->Id <= 0) {
DEBUG_LOG("Unable to determine spell information in SpellGo for " << spellId);
return;
}
if (castByActivePlayer) {
auto const currentTime = GetTime();
// only care about our own casts
@@ -1006,6 +1016,8 @@ namespace Nampower {
if (gUserSettings.enableAuraCastEvents && doesSpellApplyAura(spell)) {
auto targetGuidVal = targetGUID ? *targetGUID : *casterGUID;
DEBUG_LOG("Triggering aura cast event for " << game::GetSpellName(spell->Id)
<< " (" << spell->Id << ") caster: " << *casterGUID << " target: " << targetGuidVal);
TriggerAuraCastEvent(spell, *casterGUID, targetGuidVal, activePlayerGuid, castByActivePlayer);
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ namespace Nampower {
void CGActionBar_UseActionHook(hadesmem::PatchDetourBase *detour, uint32_t actionSlot, int param_2, int param_3);
bool Spell_C_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t *casterUnit, uint32_t spellId, uintptr_t *item,
bool Spell_C_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t *casterUnit, uint32_t spellId, game::CGItem_C *item,
std::uint64_t guid);
void
+8
View File
@@ -52,6 +52,14 @@ namespace Nampower {
return;
}
// Check for Disenchant (13262) failure and stop the disenchant loop
if (spellId == 13262) {
DEBUG_LOG("Disenchant spell failed with result " << int(spellResult) << ", stopping disenchant loop");
gDisenchantItemId = 0;
gDisenchantQuality = -1;
gNextDisenchantTimeMs = 0;
}
// ignore SPELL_FAILED_CANT_DO_THAT_YET for arcane surge gets sent all the time after success
if (spellResult == game::SpellCastResult::SPELL_FAILED_CANT_DO_THAT_YET &&
(spellId == 51933 ||
+1 -1
View File
@@ -76,7 +76,7 @@ struct CastSpellParams {
uint32_t *casterUnit;
uint32_t spellId;
uintptr_t *item;
game::CGItem_C *item;
uint64_t guid;
/* *********************** */