From e45316a3bb6dc439c9c665a6d00adbef64d23192 Mon Sep 17 00:00:00 2001 From: namreeb Date: Fri, 30 Nov 2018 22:23:31 -1000 Subject: [PATCH] Adjusted castbar management logic to hopefully resolve some race conditions --- nampower/main.cpp | 125 +++++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/nampower/main.cpp b/nampower/main.cpp index 218ee9c..90aa3d8 100644 --- a/nampower/main.cpp +++ b/nampower/main.cpp @@ -37,6 +37,7 @@ #include #include +#include #ifdef _DEBUG #include @@ -52,8 +53,16 @@ static DWORD gCooldown; static DWORD gLastCast; #endif -static bool gCancelling; -static bool gCancelFromClient; +// true when we are simulating a server-based spell cancel to reset the cast bar +static std::atomic gCancelling; + +// true when the current spell cancellation requires that we notify the server +// (a client side cancellation of a non-instant cast spell) +static std::atomic gNotifyServer; + +// true when we are in the middle of an attempt to cast a spell +static std::atomic gCasting; + static game::SpellFailedReason gCancelReason; using CastSpellT = bool(__fastcall *)(void *, int, void *, std::uint64_t); @@ -71,7 +80,7 @@ std::unique_ptr gCastbarPatch; void BeginCast(DWORD currentTime, std::uint32_t castTime, int spellId) { - gCooldown = currentTime + castTime; + gCooldown = castTime ? currentTime + castTime : 0; #ifdef _DEBUG // don't bother building the string if nobody will see it @@ -83,16 +92,17 @@ void BeginCast(DWORD currentTime, std::uint32_t castTime, int spellId) if (gLastCast) str << " elapsed: " << (currentTime - gLastCast); - str << std::endl; - ::OutputDebugStringA(str.str().c_str()); } gLastCast = currentTime; #endif - void(*signalEvent)(std::uint32_t, const char *, ...) = reinterpret_cast(Offsets::SignalEventParam); - signalEvent(game::Events::SPELLCAST_START, "%s%d", game::GetSpellName(spellId), castTime); + if (castTime) + { + void(*signalEvent)(std::uint32_t, const char *, ...) = reinterpret_cast(Offsets::SignalEventParam); + signalEvent(game::Events::SPELLCAST_START, "%s%d", game::GetSpellName(spellId), castTime); + } } bool CastSpellHook(hadesmem::PatchDetourBase *detour, void *unit, int spellId, void *item, std::uint64_t guid) @@ -109,52 +119,65 @@ bool CastSpellHook(hadesmem::PatchDetourBase *detour, void *unit, int spellId, v gCooldown = 0; } + gCasting = true; + auto const spell = game::GetSpellInfo(spellId); auto const castSpell = detour->GetTrampolineT(); auto ret = castSpell(unit, spellId, item, guid); + auto const castTime = game::GetCastTime(unit, spellId); + // if this is a trade skill or item enchant, do nothing further if (spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_TRADE_SKILL || spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_ENCHANT_ITEM || spell->Effect[0] == game::SpellEffects::SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY) return ret; - // haven't gotten spell result yet, probably due to latency. simulate a cancel to clear the cast bar - if (!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 + if (!ret && castTime) { - auto const cancelSpell = reinterpret_cast(Offsets::CancelSpell); + gCancelling = true; + auto const cancelSpell = reinterpret_cast(Offsets::CancelSpell); cancelSpell(false, false, game::SpellFailedReason::SPELL_FAILED_ERROR); + gCancelling = false; + // try again... ret = castSpell(unit, spellId, item, guid); } auto const cursorMode = *reinterpret_cast(Offsets::CursorMode); - if (ret) - { - auto const castTime = game::GetCastTime(unit, spellId); + if (ret && !!spell && !(spell->Attributes & game::SPELL_ATTR_RANGED) && cursorMode != 2) + BeginCast(currentTime, castTime, spellId); - if (!!spell && castTime > 0 && !(spell->Attributes & game::SPELL_ATTR_RANGED) && cursorMode != 2) - BeginCast(currentTime, castTime, spellId); - } + gCasting = false; return ret; } int CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer, game::SpellFailedReason reason) { - gCancelling = true; - gCancelFromClient = notifyServer; + gNotifyServer = notifyServer; gCancelReason = reason; +#ifdef _DEBUG + if (::IsDebuggerPresent()) + { + std::stringstream str; + str << "Cancel spell. " << " failed: " << failed << " notifyServer: " << notifyServer + << " reason: " << reason << " cancelling: " << gCancelling << "\n" << std::endl; + + ::OutputDebugStringA(str.str().c_str()); + } +#endif + auto const cancelSpell = detour->GetTrampolineT(); auto const ret = cancelSpell(failed, notifyServer, reason); - gCancelling = false; - return ret; } @@ -179,36 +202,55 @@ void SignalEventHook(hadesmem::PatchDetourBase *detour, game::Events eventId) { auto const currentTime = ::GetTickCount(); - // if we are not in the process of cancelling a spell at all then no intervention is necessary on our part - if (gCancelling) + if (!gCasting && (eventId == game::Events::SPELLCAST_STOP || eventId == game::Events::SPELLCAST_FAILED)) { #ifdef _DEBUG if (::IsDebuggerPresent()) { - if (eventId == game::Events::SPELLCAST_STOP || - eventId == game::Events::SPELLCAST_FAILED) - { - std::stringstream str; + std::stringstream str; - str << "Event " << (eventId == game::Events::SPELLCAST_STOP ? "SPELLCAST_STOP" : "SPELLCAST_FAILED") - << " at time " << currentTime << " gLastCast = " << gLastCast << " gCooldown = " << gCooldown << std::endl; + str << "Event " << (eventId == game::Events::SPELLCAST_STOP ? "SPELLCAST_STOP" : "SPELLCAST_FAILED") + << " at time " << currentTime << " gLastCast = " << gLastCast << " gCooldown = " << gCooldown << std::endl; - ::OutputDebugStringA(str.str().c_str()); - } + ::OutputDebugStringA(str.str().c_str()); } #endif + } - if (eventId == game::Events::SPELLCAST_STOP || eventId == game::Events::SPELLCAST_FAILED) - { - // if the current cast is cancelled (from the client for any reason or immediately by the server), reset our own - // cooldown to allow another one. this can come from the server for an instant cast (i.e. Presence of Mind) - if (gCancelFromClient) - gCooldown = 0; - // prevent the result of a previous cast from stopping the current castbar - else if (currentTime <= gCooldown) - return; - } + // SPELLCAST_STOP means the cast started and then stopped. it may or may not have completed. + // this can happen by one of two conditions: + // 1) it is caused by us in CastSpellHook() because the player is spamming + // casts and we have not yet received result of the last one (or by the + // client through some other means) + // 2) the player is casting slowly enough (or possibly not at all) such that + // the result of the last cast arrives before our next attempt to cast + // for scenario #1 we want to allow the event as it will reset the cast bar + // on the last attempt. + // for scenario #2, we have already reset the cast bar, and we do not want to + // do it again because we may already be casting the next spell. + + if (eventId == game::Events::SPELLCAST_STOP) + { + // if this is from the client, we don't care about anything else. immediately stop + // the cast bar and reset our internal cooldown. + if (gNotifyServer) + gCooldown = 0; + + // if this is from the server but it is happening too early, it is for one of two reasons. + // 1) it is for the last cast, in which case we can ignore it + // 2) it is for our current cast and the server decided to cast sooner than we expected + // this can happen from mage 8/8 t2 proc or presence of mind + else if (!gCasting && !gCancelling && currentTime <= gCooldown) + return; } + // SPELLCAST_FAILED means the attempt was rejected by either the client or the server, + // depending on the value of gNotifyServer. if it was rejected by the server, this + // could mean that our latency has decreased since the previous cast. when that happens + // the server perceives too little time as having passed to allow another cast. i dont + // think there is anything we can do about this except to honor the servers request to + // abort the cast. reset our cooldown and allow + else if (eventId == game::Events::SPELLCAST_FAILED && !gNotifyServer) + gCooldown = 0; auto const signalEvent = detour->GetTrampolineT(); signalEvent(eventId); @@ -254,7 +296,8 @@ extern "C" __declspec(dllexport) DWORD Load() #endif gCancelling = false; - gCancelFromClient = false; + gNotifyServer = false; + gCasting = false; const hadesmem::Process process(::GetCurrentProcessId());