mirror of
https://github.com/brues-code/nampower.git
synced 2026-09-28 01:36:03 +00:00
97ba0ff2f4
add CastSpellNoQueue default to current target in SPELL_CAST_EVENT
923 lines
42 KiB
C++
923 lines
42 KiB
C++
//
|
|
// Created by pmacc on 9/21/2024.
|
|
//
|
|
|
|
#include "spellcast.hpp"
|
|
#include "helper.hpp"
|
|
#include "offsets.hpp"
|
|
#include "logging.hpp"
|
|
|
|
namespace Nampower {
|
|
auto const APPLY_BUFFER_TO_GCD = false; // gcd issue seems fixed for now
|
|
void SetReleaseAction(uint32_t input) {
|
|
uint32_t activeControl = *reinterpret_cast<uint32_t *>(Offsets::CGInputControlGetActive);
|
|
|
|
typedef void (__thiscall *SetReleaseActionT)(uint32_t, uint32_t);
|
|
auto SetReleaseAction = reinterpret_cast<SetReleaseActionT>(Offsets::CGInputControlSetReleaseAction);
|
|
SetReleaseAction(activeControl, input);
|
|
}
|
|
|
|
void SetControlBit(uint32_t input) {
|
|
uint32_t activeControl = *reinterpret_cast<uint32_t *>(Offsets::CGInputControlGetActive);
|
|
auto *LastHardwareAction = reinterpret_cast<uintptr_t *>(Offsets::LastHardwareAction);
|
|
|
|
typedef void (__thiscall *SetControlBitT)(uint32_t, uint32_t, uint32_t, uintptr_t *, int);
|
|
auto SetControlBit = reinterpret_cast<SetControlBitT>(Offsets::CGInputControlSetControlBit);
|
|
SetControlBit(activeControl, 2, input, LastHardwareAction, 0);
|
|
}
|
|
|
|
void CameraOrSelectOrMoveStart() {
|
|
SetReleaseAction(1);
|
|
SetControlBit(1);
|
|
}
|
|
|
|
void CameraOrSelectOrMoveStop() {
|
|
SetControlBit(0);
|
|
}
|
|
|
|
void EnableSpellTargeting(const game::SpellRec *spell) {
|
|
// set s_needtargets 00cecac0
|
|
auto s_needtargets = reinterpret_cast<uint32_t *>(Offsets::SpellNeedsTargets);
|
|
*s_needtargets = spell->Targets;
|
|
|
|
auto const CursorSetCursorMode = reinterpret_cast<void (__fastcall *)(uint32_t)>(Offsets::CursorSetCursorMode);
|
|
CursorSetCursorMode(2); // CAST_CURSOR
|
|
auto const CursorModelSetSequence = reinterpret_cast<void (__fastcall *)(uint32_t)>(
|
|
Offsets::CursorModelSetSequence);
|
|
CursorModelSetSequence(2); // CAST_CURSOR
|
|
}
|
|
|
|
uint32_t Spell_C_HandleTerrainClickHook(hadesmem::PatchDetourBase *detour, game::CTerrainClickEvent *event) {
|
|
if (!gCastData.targetingSpellQueued) {
|
|
auto const handleTerrainClick = detour->GetTrampolineT<Spell_C_HandleTerrainClickT>();
|
|
return handleTerrainClick(event);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void CGWorldFrame_OnLayerTrackTerrainHook(hadesmem::PatchDetourBase *detour, void *thisptr, int dummy_edx,
|
|
int param_1) {
|
|
auto originalSpellId = *reinterpret_cast<uint32_t *>(Offsets::VisualSpellId);
|
|
auto originalCasterGuid = *reinterpret_cast<uint64_t *>(Offsets::CasterGuid);
|
|
|
|
if (gCastData.targetingSpellQueued) {
|
|
// switch out visual spellid to the queued spell so that we get the correct radius/range
|
|
*reinterpret_cast<uint32_t *>(Offsets::VisualSpellId) = gCastData.targetingSpellId;
|
|
// set casterguid to active player so range checks work correctly
|
|
*reinterpret_cast<uint64_t *>(Offsets::CasterGuid) = game::ClntObjMgrGetActivePlayerGuid();
|
|
}
|
|
|
|
auto const onLayerTrackTerrain = detour->GetTrampolineT<CGWorldFrame_OnLayerTrackTerrainT>();
|
|
onLayerTrackTerrain(thisptr, dummy_edx, param_1);
|
|
|
|
if (gCastData.targetingSpellQueued) {
|
|
// switch back to originalSpellId
|
|
*reinterpret_cast<uint32_t *>(Offsets::VisualSpellId) = originalSpellId;
|
|
// restore original caster guid
|
|
*reinterpret_cast<uint64_t *>(Offsets::CasterGuid) = originalCasterGuid;
|
|
}
|
|
}
|
|
|
|
float Spell_C_GetSpellRadiusHook(hadesmem::PatchDetourBase *detour) {
|
|
auto originalSpellId = *reinterpret_cast<uint32_t *>(Offsets::VisualSpellId);
|
|
if (gCastData.targetingSpellQueued) {
|
|
// switch out visual spellid to the queued spell so that we get the correct radius
|
|
*reinterpret_cast<uint32_t *>(Offsets::VisualSpellId) = gCastData.targetingSpellId;
|
|
}
|
|
|
|
auto const getSpellRadius = detour->GetTrampolineT<Spell_C_GetSpellRadiusT>();
|
|
auto radius = getSpellRadius();
|
|
|
|
return radius;
|
|
}
|
|
|
|
void TriggerQuickcast() {
|
|
// store the current target
|
|
auto const targetGuid = game::GetCurrentTargetGuid();
|
|
|
|
CameraOrSelectOrMoveStart();
|
|
CameraOrSelectOrMoveStop();
|
|
|
|
// check if target changed
|
|
if (targetGuid != game::GetCurrentTargetGuid()) {
|
|
DEBUG_LOG("Target changed during quick cast, restoring previous target " << targetGuid);
|
|
auto const targetUnit = reinterpret_cast<CGGameUI_TargetT>(Offsets::CGGameUI_Target);
|
|
targetUnit(targetGuid);
|
|
}
|
|
}
|
|
|
|
bool Spell_C_TargetSpellHook(hadesmem::PatchDetourBase *detour,
|
|
uint32_t *player,
|
|
uint32_t *spellId,
|
|
uint32_t unk3,
|
|
float unk4) {
|
|
auto const spellTarget = detour->GetTrampolineT<Spell_C_TargetSpellT>();
|
|
auto result = spellTarget(player, spellId, unk3, unk4);
|
|
|
|
if (!result) {
|
|
auto const spellName = game::GetSpellName(*spellId);
|
|
auto const spell = game::GetSpellInfo(*spellId);
|
|
|
|
if (spell->Targets == game::SpellCastTargetFlags::TARGET_FLAG_DEST_LOCATION &&
|
|
spell->Effect[0] != game::SPELL_EFFECT_SUMMON_GUARDIAN) {
|
|
// if quickcast is on instantly trigger all casts
|
|
// otherwise if this is a queued cast, trigger it instant cast
|
|
if (gUserSettings.quickcastTargetingSpells ||
|
|
(gUserSettings.queueTargetingSpells && gCastData.castingQueuedSpell)) {
|
|
DEBUG_LOG("Quickcasting terrain spell " << spellName
|
|
<< " quickcast: "
|
|
<< gUserSettings.quickcastTargetingSpells
|
|
<< " queuetrigger: " << gCastData.castingQueuedSpell);
|
|
TriggerQuickcast();
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
uint32_t GetChannelBaseDuration(const game::SpellRec *spell) {
|
|
auto const duration = game::GetDurationObject(spell->DurationIndex);
|
|
if (duration == nullptr) {
|
|
DEBUG_LOG("GetChannelBaseDuration: Duration object is null for spell " << game::GetSpellName(spell->Id));
|
|
return 0;
|
|
}
|
|
return duration->m_Duration;
|
|
}
|
|
|
|
void BeginCast(uint32_t castTime, const game::SpellRec *spell, const game::SpellCast *cast) {
|
|
if (cast != nullptr && cast->itemTarget == 0 && cast->caster != game::ClntObjMgrGetActivePlayerGuid()) {
|
|
DEBUG_LOG("Ignoring non active player begin cast of spell " << game::GetSpellName(cast->spellId) << " "
|
|
<< cast->spellId);
|
|
return;
|
|
}
|
|
|
|
auto currentTime = GetTime();
|
|
|
|
gLastCastData.castTimeMs = castTime;
|
|
|
|
gCastData.channeling = SpellIsChanneling(spell);
|
|
gLastCastData.wasQueued = false; // reset the last spell queued flag
|
|
|
|
if (gCastData.channeling) {
|
|
gCastData.channelDuration = GetChannelBaseDuration(spell);
|
|
gCastData.channelEndMs = currentTime + gCastData.channelDuration;
|
|
}
|
|
|
|
auto const spellOnGcd = SpellIsOnGcd(spell);
|
|
gLastCastData.wasOnGcd = spellOnGcd;
|
|
|
|
auto lastCastParams = gCastHistory.peek();
|
|
uint64_t lastCastId = 0;
|
|
if (lastCastParams != nullptr) {
|
|
gLastCastData.wasItem = lastCastParams->item != nullptr;
|
|
lastCastParams->castResult = CastResult::WAITING_FOR_SERVER;
|
|
lastCastId = lastCastParams->castId;
|
|
}
|
|
|
|
auto bufferMs = GetServerDelayMs();
|
|
// Reset the server delay
|
|
gLastServerSpellDelayMs = 0;
|
|
|
|
if (spellOnGcd) {
|
|
auto gcdTime = GetGcdOrCooldownForSpell(spell->Id);
|
|
if (gcdTime > 1500) {
|
|
gcdTime = 1500; // items with spells on gcd will return their item gcd, make sure not to use that
|
|
}
|
|
|
|
if (!APPLY_BUFFER_TO_GCD) {
|
|
if (castTime < gcdTime - 50) {
|
|
bufferMs = 0; // no longer need to buffer spells with cast time 50ms < gcd
|
|
} else if (castTime < gcdTime) {
|
|
auto const diff = gcdTime - castTime;
|
|
if (bufferMs > diff) {
|
|
bufferMs -= diff; // subtract the difference from the buffer
|
|
} else {
|
|
bufferMs = 0; // if the buffer is less than the difference, set it to 0
|
|
}
|
|
}
|
|
}
|
|
|
|
gCastData.gcdEndMs = currentTime + gcdTime + bufferMs;
|
|
DEBUG_LOG("BeginCast #" << lastCastId
|
|
<< " " << game::GetSpellName(spell->Id)
|
|
<< "(" << spell->Id << ")"
|
|
<< " cast time: " << castTime
|
|
<< " buffer: " << bufferMs
|
|
<< " Gcd: " << gcdTime
|
|
<< " latency: " << GetLatencyMs()
|
|
<< " time since last cast " << currentTime - gLastCastData.startTimeMs);
|
|
} else {
|
|
gCastData.delayEndMs = currentTime +
|
|
gUserSettings.nonGcdBufferTimeMs;
|
|
// set small "cast time" to avoid attempting next spell too fast
|
|
DEBUG_LOG("BeginCast #" << lastCastId
|
|
<< " " << game::GetSpellName(spell->Id)
|
|
<< "(" << spell->Id << ")"
|
|
<< " cast time: " << castTime
|
|
<< " buffer: " << bufferMs
|
|
<< " NO Gcd"
|
|
<< " latency: " << GetLatencyMs()
|
|
<< " time since last cast " << currentTime - gLastCastData.startTimeMs);
|
|
}
|
|
|
|
gCastData.castEndMs = castTime ? currentTime + castTime + bufferMs : 0;
|
|
gCastData.castSpellId = castTime ? spell->Id : 0;
|
|
gCastData.bufferMs = bufferMs;
|
|
|
|
// check if we can lower buffers
|
|
if (currentTime - gLastBufferDecreaseTimeMs > BUFFER_DECREASE_FREQUENCY) {
|
|
if (gBufferTimeMs > gUserSettings.minBufferTimeMs) {
|
|
gBufferTimeMs -= DYNAMIC_BUFFER_INCREMENT;
|
|
DEBUG_LOG("Decreasing default buffer to " << gBufferTimeMs);
|
|
}
|
|
|
|
gLastBufferDecreaseTimeMs = currentTime; // update the last error time to prevent lowering buffer too often
|
|
}
|
|
|
|
gLastCastData.startTimeMs = currentTime;
|
|
}
|
|
|
|
void CastQueuedNonGcdSpell() {
|
|
if (gCastData.nonGcdSpellQueued) {
|
|
auto nonGcdCastParams = gNonGcdCastQueue.pop();
|
|
if (nonGcdCastParams.spellId > 0) {
|
|
DEBUG_LOG("Triggering queued non gcd cast of " << game::GetSpellName(nonGcdCastParams.spellId));
|
|
|
|
gCastData.castingQueuedSpell = true;
|
|
gCastData.targetingSpellQueued = false; // make sure this is off so SendCast is allowed
|
|
gCastData.targetingSpellId = 0;
|
|
gCastData.numRetries = nonGcdCastParams.numRetries;
|
|
Spell_C_CastSpellHook(castSpellDetour, nonGcdCastParams.casterUnit, nonGcdCastParams.spellId,
|
|
nonGcdCastParams.item, nonGcdCastParams.guid);
|
|
gLastCastData.wasQueued = true;
|
|
} else {
|
|
DEBUG_LOG("Ignoring queued non gcd cast, no spell id");
|
|
gLastCastData.wasQueued = false;
|
|
}
|
|
TriggerSpellQueuedEvent(NON_GCD_QUEUE_POPPED, nonGcdCastParams.spellId);
|
|
gCastData.nonGcdSpellQueued = !gNonGcdCastQueue.isEmpty();
|
|
gCastData.castingQueuedSpell = false;
|
|
gCastData.targetingSpellQueued = false;
|
|
gCastData.targetingSpellId = 0;
|
|
gCastData.numRetries = 0;
|
|
}
|
|
}
|
|
|
|
void CastQueuedNormalSpell() {
|
|
if (gCastData.normalSpellQueued) {
|
|
if (gLastNormalCastParams.spellId > 0) {
|
|
DEBUG_LOG("Triggering queued cast of " << game::GetSpellName(gLastNormalCastParams.spellId));
|
|
gCastData.castingQueuedSpell = true;
|
|
gCastData.targetingSpellQueued = false; // make sure this is off so SendCast is allowed
|
|
gCastData.targetingSpellId = 0;
|
|
gCastData.numRetries = gLastNormalCastParams.numRetries;
|
|
Spell_C_CastSpellHook(castSpellDetour, gLastNormalCastParams.casterUnit, gLastNormalCastParams.spellId,
|
|
gLastNormalCastParams.item, gLastNormalCastParams.guid);
|
|
gLastCastData.wasQueued = true;
|
|
} else {
|
|
DEBUG_LOG("Ignoring queued cast, no spell id");
|
|
gLastCastData.wasQueued = false;
|
|
}
|
|
TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId);
|
|
gCastData.normalSpellQueued = false;
|
|
gCastData.castingQueuedSpell = false;
|
|
gCastData.targetingSpellQueued = false;
|
|
gCastData.targetingSpellId = 0;
|
|
gCastData.numRetries = 0;
|
|
}
|
|
}
|
|
|
|
void CastQueuedSpells() {
|
|
if (gCastData.nonGcdSpellQueued) {
|
|
CastQueuedNonGcdSpell();
|
|
} else {
|
|
CastQueuedNormalSpell();
|
|
}
|
|
}
|
|
|
|
void SaveCastParams(CastSpellParams *params,
|
|
uint32_t *casterUnit,
|
|
uint32_t spellId,
|
|
game::CGItem_C *item,
|
|
std::uint64_t guid,
|
|
uint32_t gcDCategory,
|
|
uint32_t castTimeMs,
|
|
uint32_t castStartTimeMs,
|
|
CastType castType,
|
|
uint32_t numRetries) {
|
|
params->casterUnit = casterUnit;
|
|
params->spellId = spellId;
|
|
params->item = item;
|
|
params->guid = guid;
|
|
|
|
params->gcdCategory = gcDCategory;
|
|
params->castTimeMs = castTimeMs;
|
|
params->castStartTimeMs = castStartTimeMs;
|
|
params->castType = castType;
|
|
params->numRetries = numRetries;
|
|
}
|
|
|
|
void TriggerSpellQueuedEvent(QueueEvents queueEventCode, uint32_t spellId) {
|
|
((int (__cdecl *)(int, char *, uint32_t, uint32_t)) Offsets::SignalEventParam)(
|
|
game::SPELL_QUEUE_EVENT, // SPELL_QUEUE_EVENT event we are adding
|
|
(char *) Offsets::IntIntParamFormat,
|
|
queueEventCode,
|
|
spellId);
|
|
}
|
|
|
|
void
|
|
TriggerSpellCastEvent(bool result, uint32_t spellId, CastType castType, std::uint64_t guid, uint32_t itemId) {
|
|
static char format[] = "%d%d%d%s%d";
|
|
|
|
if (!guid) {
|
|
// default to current target
|
|
guid = game::GetCurrentTargetGuid();
|
|
}
|
|
char *guidStr = ConvertGuidToString(guid);
|
|
|
|
((int (__cdecl *)(int, char *, uint32_t, uint32_t, uint32_t, char *, uint32_t)) Offsets::SignalEventParam)(
|
|
game::SPELL_CAST_EVENT, // SPELL_CAST_EVENT event we are adding
|
|
format,
|
|
result,
|
|
spellId,
|
|
castType,
|
|
guidStr,
|
|
itemId);
|
|
|
|
FreeGuidString(guidStr);
|
|
}
|
|
|
|
void clearCastingSpell() {
|
|
// clearing current casting spell id if needed
|
|
// this prevents client from failing to cast spells without a casttime
|
|
// due to not receiving spell result yet
|
|
auto const castingSpellId = reinterpret_cast<uint32_t *>(Offsets::CastingSpellId);
|
|
if (*castingSpellId > 0) {
|
|
*castingSpellId = 0;
|
|
}
|
|
}
|
|
|
|
void CGSpellBook_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t spellSlot, int bookType,
|
|
uint64_t target) {
|
|
auto const cgSpellBookCastSpell = detour->GetTrampolineT<CGSpellBook_CastSpellT>();
|
|
|
|
// check for double cast to trigger quickcast on targeting spells
|
|
if (gUserSettings.quickcastOnDoubleCast) {
|
|
auto currentTime = GetTime();
|
|
|
|
uint32_t spellId = 0;
|
|
if (bookType == 0) {
|
|
spellId = *reinterpret_cast<uint32_t *>(
|
|
static_cast<uint32_t>(Offsets::CGSpellBook_mKnownSpells) + spellSlot * 4);
|
|
} else if (bookType == 1) {
|
|
spellId = *reinterpret_cast<uint32_t *>(
|
|
static_cast<uint32_t>(Offsets::CGSpellBook_mKnownPetSpells) + spellSlot * 4);
|
|
} else {
|
|
// call original
|
|
cgSpellBookCastSpell(spellSlot, bookType, target);
|
|
return;
|
|
}
|
|
|
|
if (IsTargetingTerrainSpell() && spellId > 0) {
|
|
if (gCastData.channeling || EffectiveCastEndMs() >= currentTime) {
|
|
// if we are already casting block action as that will interrupt
|
|
return;
|
|
}
|
|
|
|
// check if aoe spell targeting is active
|
|
if (gLastCastData.attemptSpellId == spellId) {
|
|
auto spell = game::GetSpellInfo(spellId);
|
|
auto spellName = game::GetSpellName(spellId);
|
|
|
|
// don't mess with summon guardian
|
|
if (spell->Effect[0] != game::SPELL_EFFECT_SUMMON_GUARDIAN) {
|
|
DEBUG_LOG(
|
|
"CastSpell double cast detected for targeting spell " << spellName <<
|
|
", triggering quickcast");
|
|
TriggerQuickcast();
|
|
return; // don't cast again
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cgSpellBookCastSpell(spellSlot, bookType, target);
|
|
}
|
|
|
|
void CGActionBar_UseActionHook(hadesmem::PatchDetourBase *detour, uint32_t actionSlot, int param_2, int param_3) {
|
|
if (-1 > actionSlot && actionSlot < 120) {
|
|
uint32_t refValue = 0;
|
|
|
|
typedef uint32_t (__fastcall *GetSpellIdFromActionT)(uint32_t actionSlot, uint32_t *refParam);
|
|
auto getSpellIdFromAction = reinterpret_cast<GetSpellIdFromActionT>(Offsets::GetSpellIdFromAction);
|
|
|
|
uint32_t spellId = getSpellIdFromAction(actionSlot, &refValue);
|
|
if (spellId > 0) {
|
|
// check for double cast to trigger quickcast on targeting spells
|
|
if (gUserSettings.quickcastOnDoubleCast) {
|
|
auto currentTime = GetTime();
|
|
|
|
if (IsTargetingTerrainSpell()) {
|
|
if (gCastData.channeling || EffectiveCastEndMs() >= currentTime) {
|
|
// if we are already casting block action as that will interrupt
|
|
return;
|
|
}
|
|
|
|
// check if aoe spell targeting is active
|
|
if (gLastCastData.attemptSpellId == spellId) {
|
|
auto spell = game::GetSpellInfo(spellId);
|
|
auto spellName = game::GetSpellName(spellId);
|
|
|
|
// don't mess with summon guardian
|
|
if (spell->Effect[0] != game::SPELL_EFFECT_SUMMON_GUARDIAN) {
|
|
DEBUG_LOG(
|
|
"Use action double cast detected for targeting spell " << spellName <<
|
|
", triggering quickcast");
|
|
TriggerQuickcast();
|
|
return; // don't cast again
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto const useAction = detour->GetTrampolineT<CGActionBar_UseActionT>();
|
|
return useAction(actionSlot, param_2, param_3);
|
|
}
|
|
|
|
bool
|
|
Spell_C_CastSpellHook(hadesmem::PatchDetourBase *detour, uint32_t *casterUnit, uint32_t spellId,
|
|
game::CGItem_C *item,
|
|
std::uint64_t guid) {
|
|
// save the detour to allow quickly calling this hook
|
|
castSpellDetour = detour;
|
|
|
|
// 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;
|
|
|
|
if (item == nullptr && casterUnit != playerUnit) {
|
|
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<Spell_C_CastSpellT>();
|
|
return castSpell(casterUnit, spellId, item, guid);
|
|
}
|
|
|
|
auto const spell = game::GetSpellInfo(spellId);
|
|
auto const spellIsOnSwing = SpellIsOnSwing(spell);
|
|
auto const spellName = game::GetSpellName(spellId);
|
|
auto currentTime = GetTime();
|
|
auto const castTime = game::GetCastTime(casterUnit, spellId);
|
|
gCastData.attemptedCastTimeMs = castTime;
|
|
|
|
auto const spellOnGcd = SpellIsOnGcd(spell);
|
|
auto const spellIsChanneling = SpellIsChanneling(spell);
|
|
auto const spellIsTargeting = SpellIsTargeting(spell);
|
|
auto const isSpecialSpell = SpellIsAttackTradeskillOrEnchant(spell);
|
|
|
|
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");
|
|
|
|
static char message[] = "Preventing mounting due to buff cap (breaks dismount)";
|
|
((int (__cdecl *)(int, char *, char *)) Offsets::SignalEventParam)(
|
|
game::UI_ERROR_MESSAGE,
|
|
(char *) Offsets::StringParamFormat,
|
|
message);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto const currentTargetGuid = game::GetCurrentTargetGuid();
|
|
auto const guidForHistory = (guid != 0) ? guid : currentTargetGuid;
|
|
|
|
if (spellIsChanneling) {
|
|
auto casterGuid = game::UnitGetGuid(casterUnit);
|
|
if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) {
|
|
// check that locked target guid matches our unit target guid
|
|
auto unitTargetGuid = game::UnitGetTargetGuid(casterUnit);
|
|
if (unitTargetGuid != currentTargetGuid) {
|
|
DEBUG_LOG("Updating selection target to " << currentTargetGuid << " from " << unitTargetGuid);
|
|
SetSelectionTarget(currentTargetGuid);
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for double press to interrupt channeling early
|
|
if (gCastData.channeling && !gCastData.cancelChannelNextTick &&
|
|
gCastData.numRetries == 0 &&
|
|
gUserSettings.doubleCastToEndChannelEarly &&
|
|
gCastData.channelStartMs > 0) {
|
|
// wait 500ms after the start of a channel before allowing double cast to end it early
|
|
if (currentTime - gCastData.channelStartMs > 500) {
|
|
// check if same spell is being cast again within 350ms
|
|
if (gLastCastData.attemptSpellId == spellId && currentTime - gLastCastData.attemptTimeMs < 350) {
|
|
DEBUG_LOG("Double cast detected for " << spellName << ", ending channel early");
|
|
gCastData.cancelChannelNextTick = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
gLastCastData.attemptTimeMs = currentTime;
|
|
gLastCastData.attemptSpellId = spellId;
|
|
|
|
uint32_t itemId = 0;
|
|
if (item) {
|
|
itemId = game::GetItemId(reinterpret_cast<game::CGItem_C *>(item));
|
|
}
|
|
|
|
DEBUG_LOG("Attempt cast " << spellName << " itemId " << itemId << " on guid " << guid << " target "
|
|
<< currentTargetGuid
|
|
<< ", time since last cast " << currentTime - gLastCastData.startTimeMs);
|
|
|
|
// clear cooldown queue if we are casting a spell
|
|
if (spellOnGcd && gCastData.cooldownNormalSpellQueued) {
|
|
gCastData.cooldownNormalSpellQueued = false;
|
|
TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId);
|
|
} else if (gCastData.cooldownNonGcdSpellQueued) {
|
|
gCastData.cooldownNonGcdSpellQueued = false;
|
|
// pop the params
|
|
auto nonGcdParams = gNonGcdCastQueue.pop();
|
|
TriggerSpellQueuedEvent(NON_GCD_QUEUE_POPPED, nonGcdParams.spellId);
|
|
}
|
|
|
|
// on swing spells are independent of cast bar / gcd, handle them separately
|
|
if (spellIsOnSwing) {
|
|
SaveCastParams(&gLastOnSwingCastParams, casterUnit, spellId, item, guid, spell->StartRecoveryCategory,
|
|
castTime,
|
|
currentTime, ON_SWING, 0);
|
|
|
|
gCastHistory.pushFront({
|
|
gNextCastId, casterUnit, spellId, item, guidForHistory,
|
|
spell->StartRecoveryCategory,
|
|
castTime,
|
|
currentTime,
|
|
ON_SWING,
|
|
gCastData.numRetries,
|
|
CastResult::WAITING_FOR_CAST
|
|
});
|
|
gNextCastId++;
|
|
|
|
// try to cast the spell
|
|
auto const castSpell = detour->GetTrampolineT<Spell_C_CastSpellT>();
|
|
auto ret = castSpell(casterUnit, spellId, item, guid);
|
|
|
|
TriggerSpellCastEvent(ret, spellId, ON_SWING, guid, itemId);
|
|
|
|
if (ret) {
|
|
gCastData.pendingOnSwingCast = true;
|
|
gCastData.onSwingSpellId = spellId;
|
|
}
|
|
|
|
if (!ret && gUserSettings.queueOnSwingSpells && !gNoQueueCast) {
|
|
// if not in cooldown window
|
|
if (currentTime - gLastCastData.onSwingStartTimeMs > gUserSettings.onSwingBufferCooldownMs) {
|
|
DEBUG_LOG("Queuing on swing spell " << spellName);
|
|
TriggerSpellQueuedEvent(ON_SWING_QUEUED, spellId);
|
|
gCastData.onSwingQueued = true;
|
|
}
|
|
} else {
|
|
gLastCastData.onSwingStartTimeMs = GetTime();
|
|
DEBUG_LOG("Successful on swing spell " << spellName);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
auto const castSpell = detour->GetTrampolineT<Spell_C_CastSpellT>();
|
|
|
|
auto effectiveCastEndMs = EffectiveCastEndMs();
|
|
auto remainingEffectiveCastTime = (effectiveCastEndMs > currentTime) ? effectiveCastEndMs - currentTime : 0;
|
|
auto remainingGcd = (gCastData.gcdEndMs > currentTime) ? gCastData.gcdEndMs - currentTime : 0;
|
|
|
|
auto remainingCD = (remainingEffectiveCastTime > remainingGcd) ? remainingEffectiveCastTime : remainingGcd;
|
|
|
|
auto inSpellQueueWindow = InSpellQueueWindow(remainingEffectiveCastTime, remainingGcd, spellIsTargeting);
|
|
|
|
// don't queue trade skills or enchants
|
|
if (isSpecialSpell) {
|
|
inSpellQueueWindow = false;
|
|
}
|
|
|
|
if (spellOnGcd) {
|
|
auto castType = NORMAL;
|
|
if (spellIsTargeting) {
|
|
castType = TARGETING;
|
|
} else if (spellIsChanneling) {
|
|
castType = CHANNEL;
|
|
}
|
|
|
|
SaveCastParams(&gLastNormalCastParams, casterUnit, spellId, item, guid,
|
|
spell->StartRecoveryCategory,
|
|
castTime,
|
|
currentTime, castType, 0);
|
|
}
|
|
|
|
// skip queueing if gNoQueueCast is set
|
|
// skip queueing if spellIsChanneling and gUserSettings.queueChannelingSpells is false
|
|
if (!gNoQueueCast && (!spellIsChanneling || gUserSettings.queueChannelingSpells)) {
|
|
if (spellIsTargeting) {
|
|
if (gUserSettings.queueTargetingSpells) {
|
|
if (castTime > 0 && inSpellQueueWindow) {
|
|
if (gUserSettings.queueCastTimeSpells) {
|
|
// call EnableSpellTargeting to set s_needTargets and trigger the targeting indicator
|
|
EnableSpellTargeting(spell);
|
|
|
|
DEBUG_LOG(
|
|
"Queuing targeting for after cast/gcd: " << remainingCD << "ms " << spellName);
|
|
TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId);
|
|
gCastData.normalSpellQueued = true;
|
|
gCastData.targetingSpellQueued = true;
|
|
gCastData.targetingSpellId = spellId;
|
|
return false;
|
|
}
|
|
} else if (inSpellQueueWindow) {
|
|
if (gUserSettings.queueInstantSpells) {
|
|
if (spellOnGcd) {
|
|
// call EnableSpellTargeting to set s_needTargets and trigger the targeting indicator
|
|
EnableSpellTargeting(spell);
|
|
|
|
DEBUG_LOG(
|
|
"Queuing instant cast targeting for after cast/gcd: " << remainingCD << "ms "
|
|
<< spellName);
|
|
TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId);
|
|
gCastData.normalSpellQueued = true;
|
|
gCastData.targetingSpellQueued = true;
|
|
gCastData.targetingSpellId = spellId;
|
|
return false;
|
|
} else if (remainingEffectiveCastTime > 0) {
|
|
auto castParams = gNonGcdCastQueue.findSpellId(spellId);
|
|
if (castParams) {
|
|
DEBUG_LOG("Updating instant cast non GCD targeting params for " << spellName);
|
|
castParams->guid = guid;
|
|
return false;
|
|
} else {
|
|
// call EnableSpellTargeting to set s_needTargets and trigger the targeting indicator
|
|
EnableSpellTargeting(spell);
|
|
|
|
DEBUG_LOG("Queuing instant cast non GCD targeting for after cast/gcd: "
|
|
<< remainingEffectiveCastTime << "ms " << spellName
|
|
<< " gcd category "
|
|
<< spell->StartRecoveryCategory);
|
|
|
|
gNonGcdCastQueue.push({
|
|
0, casterUnit, spellId, item, guid,
|
|
spell->StartRecoveryCategory,
|
|
castTime,
|
|
0,
|
|
::NON_GCD,
|
|
false
|
|
}, gUserSettings.replaceMatchingNonGcdCategory);
|
|
TriggerSpellQueuedEvent(NON_GCD_QUEUED, spellId);
|
|
gCastData.nonGcdSpellQueued = true;
|
|
gCastData.targetingSpellQueued = true;
|
|
gCastData.targetingSpellId = spellId;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (castTime > 0 && inSpellQueueWindow) {
|
|
if (gUserSettings.queueCastTimeSpells) {
|
|
if (spellOnGcd) {
|
|
DEBUG_LOG("Queuing for after cast/gcd: " << remainingCD << "ms " << spellName);
|
|
TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId);
|
|
gCastData.normalSpellQueued = true;
|
|
return false;
|
|
} else if (remainingEffectiveCastTime > 0) {
|
|
auto castParams = gNonGcdCastQueue.findSpellId(spellId);
|
|
if (castParams) {
|
|
DEBUG_LOG("Updating non GCD params for " << spellName);
|
|
castParams->guid = guid;
|
|
return false;
|
|
} else {
|
|
DEBUG_LOG("Queuing non GCD for after cast/gcd: "
|
|
<< remainingEffectiveCastTime << "ms " << spellName << " gcd category "
|
|
<< spell->StartRecoveryCategory);
|
|
|
|
gNonGcdCastQueue.push({
|
|
0, casterUnit, spellId, item, guid,
|
|
spell->StartRecoveryCategory,
|
|
castTime,
|
|
0,
|
|
::NON_GCD,
|
|
false
|
|
}, gUserSettings.replaceMatchingNonGcdCategory);
|
|
TriggerSpellQueuedEvent(NON_GCD_QUEUED, spellId);
|
|
gCastData.nonGcdSpellQueued = true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} else if (inSpellQueueWindow) {
|
|
if ((spellIsChanneling && gUserSettings.queueChannelingSpells) ||
|
|
(!spellIsChanneling && gUserSettings.queueInstantSpells)) {
|
|
auto desc = "instant cast";
|
|
if (spellIsChanneling) {
|
|
desc = "channeling";
|
|
}
|
|
|
|
if (spellOnGcd) {
|
|
DEBUG_LOG(
|
|
"Queuing " << desc << " for after cast/gcd: " << remainingCD << "ms " << spellName);
|
|
TriggerSpellQueuedEvent(NORMAL_QUEUED, spellId);
|
|
gCastData.normalSpellQueued = true;
|
|
return false;
|
|
} else if (remainingEffectiveCastTime > 0) {
|
|
auto castParams = gNonGcdCastQueue.findSpellId(spellId);
|
|
if (castParams) {
|
|
DEBUG_LOG("Updating " << desc << " non GCD params for " << spellName);
|
|
castParams->guid = guid;
|
|
return false;
|
|
} else {
|
|
DEBUG_LOG("Queuing " << desc << " non GCD for after cast/gcd: "
|
|
<< remainingEffectiveCastTime << "ms " << spellName << " gcd category "
|
|
<< spell->StartRecoveryCategory);
|
|
|
|
gNonGcdCastQueue.push({
|
|
0, casterUnit, spellId, item, guid,
|
|
spell->StartRecoveryCategory,
|
|
castTime,
|
|
0,
|
|
::NON_GCD,
|
|
false
|
|
}, gUserSettings.replaceMatchingNonGcdCategory);
|
|
TriggerSpellQueuedEvent(NON_GCD_QUEUED, spellId);
|
|
gCastData.nonGcdSpellQueued = true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isSpecialSpell) {
|
|
// is there a cast? (ignore for on swing spells)
|
|
if (remainingEffectiveCastTime) {
|
|
DEBUG_LOG("Cast or delay active " << remainingEffectiveCastTime << "ms remaining");
|
|
return false;
|
|
} else {
|
|
gCastData.castEndMs = 0;
|
|
gCastData.castSpellId = 0;
|
|
}
|
|
|
|
// is there a Gcd?
|
|
if (spellOnGcd && remainingGcd) {
|
|
DEBUG_LOG("Gcd active " << remainingGcd << "ms remaining");
|
|
return false;
|
|
} else {
|
|
gCastData.gcdEndMs = 0;
|
|
}
|
|
}
|
|
|
|
// prevent casting instant cast spells and spells with SPELL_ATTR_DISABLED_WHILE_ACTIVE
|
|
// if cast in the last second and still waiting for server result or succeeded
|
|
// otherwise can break cooldown in the client and cause unnecessary errors
|
|
if (castTime == 0 || spell->Attributes & game::SPELL_ATTR_DISABLED_WHILE_ACTIVE) {
|
|
// check if spam protection is enabled
|
|
if (gUserSettings.spamProtectionEnabled) {
|
|
auto castParams = gCastHistory.findNewestWaitingForServerSpellId(spellId);
|
|
if (castParams &&
|
|
currentTime - castParams->castStartTimeMs < 500) {
|
|
DEBUG_LOG("Ignoring " << spellName
|
|
<< " cast still waiting for server result for the same spell");
|
|
return false;
|
|
} else {
|
|
castParams = gCastHistory.findNewestSuccessfulSpellId(spellId);
|
|
if (castParams &&
|
|
castParams->guid == guid &&
|
|
currentTime - castParams->castStartTimeMs < 500) {
|
|
DEBUG_LOG("Ignoring " << spellName
|
|
<< " cast recently succeeded for the same spell and target");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// try clearing current casting spell id if
|
|
// not using tradeskill or enchant
|
|
// no on swing spell queued (will interrupt them)
|
|
if (!isSpecialSpell && !gCastData.pendingOnSwingCast) {
|
|
clearCastingSpell();
|
|
}
|
|
|
|
// add to cast history
|
|
auto castType = CastType::NORMAL;
|
|
if (spellIsChanneling) {
|
|
castType = CastType::CHANNEL;
|
|
} else if (spellIsTargeting) {
|
|
if (spellOnGcd) {
|
|
castType = CastType::TARGETING;
|
|
} else {
|
|
castType = CastType::TARGETING_NON_GCD;
|
|
}
|
|
} else if (!spellOnGcd) {
|
|
castType = CastType::NON_GCD;
|
|
}
|
|
|
|
gCastHistory.pushFront({
|
|
gNextCastId, casterUnit, spellId, item, guidForHistory,
|
|
spell->StartRecoveryCategory,
|
|
castTime,
|
|
currentTime,
|
|
castType,
|
|
gCastData.numRetries,
|
|
CastResult::WAITING_FOR_CAST
|
|
});
|
|
gNextCastId++;
|
|
|
|
auto ret = castSpell(casterUnit, spellId, item, guid);
|
|
|
|
// if this is a trade skill or item enchant, do nothing further
|
|
if (isSpecialSpell) {
|
|
TriggerSpellCastEvent(ret, spellId, castType, guid, itemId);
|
|
return ret;
|
|
}
|
|
|
|
// 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 && !gCastData.ignoreCancelDueToCooldown) {
|
|
auto inTargetingMode = *reinterpret_cast<int *>(Offsets::SpellNeedTargets);
|
|
auto spellIsOnCooldown = IsSpellOnCooldown(spellId);
|
|
if (inTargetingMode == 0 &&
|
|
!gCastData.pendingOnSwingCast &&
|
|
!spellIsOnCooldown) {
|
|
DEBUG_LOG("Canceling spell cast due to previous spell having cast time of "
|
|
<< gLastCastData.castTimeMs);
|
|
|
|
//JT: Suggest replacing CancelSpell with InterruptSpell (the API called when moving during casting).
|
|
// The address of InterruptSpell needs to be dug out. It could possibly fix the sometimes broken animations.
|
|
gCastData.cancellingSpell = true;
|
|
|
|
auto const cancelSpell = reinterpret_cast<CancelSpellT>(Offsets::CancelSpell);
|
|
cancelSpell(false, false, game::SPELL_FAILED_ERROR);
|
|
|
|
gCastData.cancellingSpell = false;
|
|
|
|
clearCastingSpell();
|
|
|
|
// try again now that cast bar is gone
|
|
ret = castSpell(casterUnit, spellId, item, guid);
|
|
|
|
auto const cursorMode = *reinterpret_cast<int *>(Offsets::CursorMode);
|
|
if (!ret && !(spell->Attributes & game::SPELL_ATTR_RANGED) && cursorMode != 2) {
|
|
DEBUG_LOG("Retry cast after cancel still failed");
|
|
}
|
|
} else {
|
|
DEBUG_LOG("Initial cast failed but not canceling spell cast. inTargetingMode ="
|
|
<< inTargetingMode << " pendingOnSwingCast =" << gCastData.pendingOnSwingCast
|
|
<< " spellIsOnCooldown =" << spellIsOnCooldown);
|
|
}
|
|
}
|
|
|
|
TriggerSpellCastEvent(ret, spellId, castType, guid, itemId);
|
|
|
|
return ret;
|
|
}
|
|
|
|
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();
|
|
} else if (failed) {
|
|
DEBUG_LOG("Cancel spell cast failed:" << failed <<
|
|
" notifyServer:" << notifyServer << " reason:" << int(reason));
|
|
}
|
|
|
|
auto const cancelSpell = detour->GetTrampolineT<CancelSpellT>();
|
|
return cancelSpell(failed, notifyServer, reason);
|
|
}
|
|
|
|
void SendCastHook(hadesmem::PatchDetourBase *detour, game::SpellCast *cast, char unk) {
|
|
auto const sendCast = detour->GetTrampolineT<SendCastT>();
|
|
sendCast(cast, unk);
|
|
|
|
auto const spell = game::GetSpellInfo(cast->spellId);
|
|
BeginCast(gCastData.attemptedCastTimeMs, spell, cast);
|
|
}
|
|
}
|