improvements to disenchant all and cast retry logic

This commit is contained in:
avitasia
2025-12-29 13:35:51 -08:00
parent 84520d1c20
commit 0368dd69b2
12 changed files with 9895 additions and 9787 deletions
+9758 -9735
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -9,6 +9,7 @@ set(SOURCE_FILES
cdatastore.hpp
cdatastore.cpp
castqueue.h
cooldown.hpp
game.hpp
game.cpp
main.hpp
+37
View File
@@ -0,0 +1,37 @@
//
// Cooldown-related types and functions
//
#pragma once
#include <cstdint>
namespace Nampower {
struct CooldownDetail {
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;
bool isOnIndividualCooldown = false;
uint32_t categoryId = 0;
uint32_t categoryStartMs = 0;
uint32_t categoryDurationMs = 0;
uint32_t categoryRemainingMs = 0;
bool isOnCategoryCooldown = false;
uint32_t gcdCategoryId = 0;
uint32_t gcdCategoryStartMs = 0;
uint32_t gcdCategoryDurationMs = 0;
uint32_t gcdCategoryRemainingMs = 0;
bool isOnGcdCategoryCooldown = false;
};
CooldownDetail GetItemCooldownDetail(uint32_t itemId);
}
+2 -26
View File
@@ -8,36 +8,12 @@
#include <cstring>
#include "cooldown.hpp"
namespace Nampower {
// Reusable table reference to reduce memory allocations
static int cooldownDetailTableRef = LUA_REFNIL;
struct CooldownDetail {
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;
bool isOnIndividualCooldown = false;
uint32_t categoryId = 0;
uint32_t categoryStartMs = 0;
uint32_t categoryDurationMs = 0;
uint32_t categoryRemainingMs = 0;
bool isOnCategoryCooldown = false;
uint32_t gcdCategoryId = 0;
uint32_t gcdCategoryStartMs = 0;
uint32_t gcdCategoryDurationMs = 0;
uint32_t gcdCategoryRemainingMs = 0;
bool isOnGcdCategoryCooldown = false;
};
game::SpellHistoryEntry *GetSpellHistoryHead() {
auto const spellHistoryAddr = static_cast<uintptr_t>(Offsets::SpellHistories);
+13 -11
View File
@@ -217,7 +217,7 @@ namespace Nampower {
return result;
}
PlayerItemSearchResult FindPlayerDisenchantItem(int32_t quality, bool includeSoulbound) {
PlayerItemSearchResult FindPlayerDisenchantItem(uint32_t qualityBitmask, bool includeSoulbound) {
PlayerItemSearchResult result{};
auto const getBagItem = reinterpret_cast<CGBag_C_GetItemAtSlotT>(Offsets::CGBag_C_GetItemAtSlot);
@@ -261,22 +261,24 @@ namespace Nampower {
return false;
}
if (itemStats->m_quality != quality) {
// Check if item quality matches the bitmask
uint32_t qualityBit = 0;
if (itemStats->m_quality == game::ITEM_QUALITY_UNCOMMON) {
qualityBit = 0x01; // Green
} else if (itemStats->m_quality == game::ITEM_QUALITY_RARE) {
qualityBit = 0x02; // Blue
} else if (itemStats->m_quality == game::ITEM_QUALITY_EPIC) {
qualityBit = 0x04; // Purple
}
if ((qualityBitmask & qualityBit) == 0) {
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;
}
}
// Skip equipped gear slots (0-18) - only search bags
for (uint32_t slot = 23; slot <= 38; slot++) {
auto item = getBagItem(inventory, slot);
+1 -1
View File
@@ -101,7 +101,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, bool includeSoulbound = false);
PlayerItemSearchResult FindPlayerDisenchantItem(uint32_t qualityBitmask, bool includeSoulbound = false);
uintptr_t *GetBagPtrFromContainer(uintptr_t *containerPtr);
}
+3 -3
View File
@@ -65,7 +65,7 @@ namespace Nampower {
uint32_t gBufferTimeMs; // adjusts dynamically depending on errors
uint32_t gDisenchantItemId = 0;
int32_t gDisenchantQuality = -1; // -1 = unset
uint32_t gDisenchantQuality = 0; // 0 = unset
bool gDisenchantIncludeSoulbound = false;
uint32_t gNextDisenchantTimeMs = 0;
@@ -373,7 +373,7 @@ namespace Nampower {
void ResetDisenchantState() {
gDisenchantItemId = 0;
gDisenchantQuality = -1;
gDisenchantQuality = 0;
gDisenchantIncludeSoulbound = false;
gNextDisenchantTimeMs = 0;
}
@@ -534,7 +534,7 @@ namespace Nampower {
}
// Check for disenchant all
if (gDisenchantItemId != 0 || gDisenchantQuality != -1) {
if (gDisenchantItemId != 0 || gDisenchantQuality != 0) {
auto currentTime = GetTime();
// Try to disenchant when timer has elapsed
+7 -2
View File
@@ -29,8 +29,13 @@ namespace Nampower {
constexpr uint32_t BUFFER_INCREASE_FREQUENCY = 5000; // time in ms between changes to raise buffer
constexpr uint32_t BUFFER_DECREASE_FREQUENCY = 10000; // time in ms between changes to lower buffer
// Disenchant quality bitmasks
constexpr uint32_t DISENCHANT_QUALITY_GREEN = 0x01; // Uncommon
constexpr uint32_t DISENCHANT_QUALITY_BLUE = 0x02; // Rare
constexpr uint32_t DISENCHANT_QUALITY_PURPLE = 0x04; // Epic
constexpr uint32_t MAJOR_VERSION = 2;
constexpr uint32_t MINOR_VERSION = 22;
constexpr uint32_t MINOR_VERSION = 23;
constexpr uint32_t PATCH_VERSION = 0;
constexpr int32_t LUA_REGISTRYINDEX = -10000;
@@ -43,7 +48,7 @@ namespace Nampower {
extern uint32_t gBufferTimeMs; // adjusts dynamically depending on errors
extern uint32_t gDisenchantItemId;
extern int32_t gDisenchantQuality;
extern uint32_t gDisenchantQuality;
extern bool gDisenchantIncludeSoulbound;
extern uint32_t gNextDisenchantTimeMs;
+42 -6
View File
@@ -613,7 +613,7 @@ namespace Nampower {
bool TryDisenchant() {
DEBUG_LOG("Trying disenchant with quality filter " << gDisenchantQuality << " itemId " << gDisenchantItemId);
if (gDisenchantQuality < 0 && gDisenchantItemId == 0) {
if (gDisenchantQuality == 0 && gDisenchantItemId == 0) {
ResetDisenchantState();
return false;
}
@@ -631,6 +631,7 @@ namespace Nampower {
if (!itemSearchResult.found()) {
// Item not found, stop disenchanting
DEBUG_LOG("No disenchantable item found, stopping disenchant");
LuaCall("DEFAULT_CHAT_FRAME:AddMessage(\"No more items to disenchant.\")");
ResetDisenchantState();
return false;
}
@@ -656,6 +657,14 @@ namespace Nampower {
return false;
}
// Display which item is being disenchanted
char luaCode[256];
snprintf(luaCode, sizeof(luaCode),
"local itemLink = GetContainerItemLink(%d, %d); "
"if itemLink then DEFAULT_CHAT_FRAME:AddMessage(\"Disenchanting \" .. itemLink .. \" move during cast to cancel.\") end",
itemSearchResult.bagIndex, itemSearchResult.slot + 1); // Lua slots are 1-indexed
LuaCall(luaCode);
// Set timer for next disenchant attempt (5 seconds to allow time for inventory to update)
auto currentTime = GetTime();
gNextDisenchantTimeMs = currentTime + 5000;
@@ -667,6 +676,7 @@ namespace Nampower {
if (!success) {
// Cast failed, stop disenchanting
DEBUG_LOG("Cast failed, stopping disenchant");
LuaCall("DEFAULT_CHAT_FRAME:AddMessage(\"Disenchant interrupted or failed.\")");
ResetDisenchantState();
return false;
}
@@ -679,7 +689,7 @@ namespace Nampower {
if (!lua_isnumber(luaState, 1) && !lua_isstring(luaState, 1)) {
lua_error(luaState, "Usage: DisenchantAll(itemIdOrName, [includeSoulbound]) or DisenchantAll(quality, [includeSoulbound])\n"
"quality: \"greens\" for uncommon, \"blues\" for rare\n"
"quality: \"greens\", \"blues\", \"purples\", or combinations like \"greens|blues\" or \"greens|blues|purples\"\n"
"itemIdOrName: item ID (number) or item name (string)\n"
"includeSoulbound: optional number - pass 1 to include soulbound items (defaults to 0)");
return 0;
@@ -711,10 +721,36 @@ namespace Nampower {
} 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
// Parse quality combinations (e.g., "greens", "blues|purples", "greens|blues|purples")
bool isQualityString = false;
uint32_t qualityBitmask = 0;
// Make a mutable copy of the string for tokenizing
std::string paramStr(param);
size_t start = 0;
size_t end = 0;
while (end != std::string::npos) {
end = paramStr.find('|', start);
std::string token = paramStr.substr(start, (end == std::string::npos) ? std::string::npos : end - start);
if (_stricmp(token.c_str(), "greens") == 0) {
qualityBitmask |= DISENCHANT_QUALITY_GREEN;
isQualityString = true;
} else if (_stricmp(token.c_str(), "blues") == 0) {
qualityBitmask |= DISENCHANT_QUALITY_BLUE;
isQualityString = true;
} else if (_stricmp(token.c_str(), "purples") == 0) {
qualityBitmask |= DISENCHANT_QUALITY_PURPLE;
isQualityString = true;
}
start = (end == std::string::npos) ? std::string::npos : end + 1;
}
if (isQualityString) {
gDisenchantQuality = qualityBitmask;
} else {
// It's an item name
uint32_t cachedItemId = GetItemIdFromCache(param);
+12 -3
View File
@@ -452,6 +452,9 @@ namespace Nampower {
// it is possible when spamming to attempt to cast before queues are processed
processQueues();
// clear flag to avoid canceling spell cast due to cooldown
gCastData.ignoreCancelDueToCooldown = false;
auto playerGuid = game::ClntObjMgrGetActivePlayerGuid();
auto playerUnit = (playerGuid > 0) ? game::GetObjectPtr(playerGuid) : nullptr;
@@ -843,7 +846,7 @@ namespace Nampower {
// haven't gotten spell result from the previous cast yet, probably due to latency.
// simulate a cancel to clear the cast bar but only when there should be a cast time
// mining/herbing have cast time but aren't on Gcd, don't cancel them
if (!ret && gLastCastData.castTimeMs > 0 && gLastCastData.wasOnGcd) {
if (!ret && gLastCastData.castTimeMs > 0 && gLastCastData.wasOnGcd && !gCastData.ignoreCancelDueToCooldown) {
if (*reinterpret_cast<int *>(Offsets::SpellIsTargeting) == 0 && !gCastData.pendingOnSwingCast &&
!IsSpellOnCooldown(spellId)) {
DEBUG_LOG("Canceling spell cast due to previous spell having cast time of "
@@ -1016,8 +1019,6 @@ 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);
}
}
@@ -1025,6 +1026,14 @@ namespace Nampower {
void
CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer,
game::SpellCastResult reason) {
// avoid canceling spell cast if it failed due to cooldown
if (gCastData.ignoreCancelDueToCooldown &&
(reason == game::SpellCastResult::SPELL_FAILED_NOT_READY ||
reason == game::SpellCastResult::SPELL_FAILED_ITEM_NOT_READY)) {
DEBUG_LOG("Ignoring cancel spell cast due to cooldown, reason:" << int(reason));
return;
}
// triggered by us, reset the cast bar
if (notifyServer) {
ResetCastFlags();
+18
View File
@@ -7,6 +7,7 @@
#include "logging.hpp"
#include "spellcast.hpp"
#include "helper.hpp"
#include "cooldown.hpp"
namespace Nampower {
uint32_t lastCastResultTimeMs;
@@ -88,6 +89,8 @@ namespace Nampower {
if (castParams) {
// mark as failed so it can be cast again
castParams->castResult = CastResult::SERVER_FAILURE;
// set flag to avoid canceling spell cast
gCastData.ignoreCancelDueToCooldown = true;
}
// check if we should do cooldown queuing
@@ -120,6 +123,21 @@ namespace Nampower {
return;
}
}
} else if (spellResult == game::SpellCastResult::SPELL_FAILED_ITEM_NOT_READY) {
// check if item is just on cooldown still
auto castParams = gCastHistory.findSpellId(spellId);
if (castParams && castParams->item) {
auto const itemId = game::GetItemId(castParams->item);
auto const itemCooldown = GetItemCooldownDetail(itemId);
if (itemCooldown.cooldownRemainingMs > 0) {
// mark as failed so it can be cast again
castParams->castResult = CastResult::SERVER_FAILURE;
// set flag to avoid canceling spell cast
gCastData.ignoreCancelDueToCooldown = true;
DEBUG_LOG("Item " << itemId << " is still on cooldown " << itemCooldown.cooldownRemainingMs);
return;
}
}
}
if (!gUserSettings.retryServerRejectedSpells) {
+1
View File
@@ -133,6 +133,7 @@ struct CastData {
uint32_t numRetries;
bool cancellingSpell;
bool ignoreCancelDueToCooldown;
bool channeling;
bool cancelChannelNextTick;