Skip to content

Commit

Permalink
Adjusted castbar management logic to hopefully resolve some race cond…
Browse files Browse the repository at this point in the history
…itions
  • Loading branch information
namreeb committed Dec 1, 2018
1 parent c38b7f7 commit e45316a
Showing 1 changed file with 84 additions and 41 deletions.
125 changes: 84 additions & 41 deletions nampower/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

#include <cstdint>
#include <memory>
#include <atomic>

#ifdef _DEBUG
#include <sstream>
Expand All @@ -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<bool> 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<bool> gNotifyServer;

// true when we are in the middle of an attempt to cast a spell
static std::atomic<bool> gCasting;

static game::SpellFailedReason gCancelReason;

using CastSpellT = bool(__fastcall *)(void *, int, void *, std::uint64_t);
Expand All @@ -71,7 +80,7 @@ std::unique_ptr<hadesmem::PatchRaw> 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
Expand All @@ -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<decltype(signalEvent)>(Offsets::SignalEventParam);
signalEvent(game::Events::SPELLCAST_START, "%s%d", game::GetSpellName(spellId), castTime);
if (castTime)
{
void(*signalEvent)(std::uint32_t, const char *, ...) = reinterpret_cast<decltype(signalEvent)>(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)
Expand All @@ -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<CastSpellT>();
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<CancelSpellT>(Offsets::CancelSpell);
gCancelling = true;

auto const cancelSpell = reinterpret_cast<CancelSpellT>(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<int *>(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<CancelSpellT>();
auto const ret = cancelSpell(failed, notifyServer, reason);

gCancelling = false;

return ret;
}

Expand All @@ -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<SignalEventT>();
signalEvent(eventId);
Expand Down Expand Up @@ -254,7 +296,8 @@ extern "C" __declspec(dllexport) DWORD Load()
#endif

gCancelling = false;
gCancelFromClient = false;
gNotifyServer = false;
gCasting = false;

const hadesmem::Process process(::GetCurrentProcessId());

Expand Down

0 comments on commit e45316a

Please sign in to comment.