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

2 comments on commit e45316a

@hgarner17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the animation of casting my spell does not happen until the spell is completed casting.. then it shows the spell being launched. Is there a way to enable the 'wind up' for casting the spell while still using this set up. The game play feels so great but I wish i could still see my elf wind up and throw the wrath spell. currently the elf stands there with arms to their side until spell completes. Is this just the cost of doing business? or is there a way to show the wind up before the launch of the spell animation? thanks! great add on btw

@namreeb
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the animation of casting my spell does not happen until the spell is completed casting.. then it shows the spell being launched. Is there a way to enable the 'wind up' for casting the spell while still using this set up. The game play feels so great but I wish i could still see my elf wind up and throw the wrath spell. currently the elf stands there with arms to their side until spell completes. Is this just the cost of doing business? or is there a way to show the wind up before the launch of the spell animation? thanks! great add on btw

Please open an issue rather than commenting on a commit. Include any information you can about how to reproduce the issue. Thanks.

Please sign in to comment.