diff --git a/nampower/game.cpp b/nampower/game.cpp index d4c2685..c347466 100644 --- a/nampower/game.cpp +++ b/nampower/game.cpp @@ -36,6 +36,13 @@ namespace game { +void *GetObjectPtr(std::uint64_t guid) +{ + void *(__stdcall *getObjectPtr)(std::uint64_t) = hadesmem::detail::AliasCast(Offsets::GetObjectPtr); + + return getObjectPtr(guid); +} + std::uint32_t GetCastTime(void *unit, int spellId) { auto const vmt = *reinterpret_cast(unit); @@ -68,7 +75,7 @@ const char *GetSpellName(int spellId) std::uint64_t ClntObjMgrGetActivePlayer() { - std::uint64_t (_cdecl *getActivePlayer)() = hadesmem::detail::AliasCast(Offsets::GetActivePlayer); + auto const getActivePlayer = hadesmem::detail::AliasCast(Offsets::GetActivePlayer); return getActivePlayer(); } diff --git a/nampower/game.hpp b/nampower/game.hpp index ae407f1..8f28a0c 100644 --- a/nampower/game.hpp +++ b/nampower/game.hpp @@ -138,141 +138,24 @@ struct WowClientDB int Unk4; int Unk5; }; + +struct SpellCast +{ + unsigned __int64 casterUnit; + unsigned __int64 caster; + int spellId; + unsigned __int16 targets; + unsigned __int16 unk; + unsigned __int64 unitTarget; + unsigned __int64 itemTarget; + unsigned int Fields2[18]; + char targetString[128]; +}; #pragma pack(pop) -enum SpellEffects +enum SpellFailedReason { - SPELL_EFFECT_NONE = 0x0, - SPELL_EFFECT_INSTAKILL = 0x1, - SPELL_EFFECT_SCHOOL_DAMAGE = 0x2, - SPELL_EFFECT_DUMMY = 0x3, - SPELL_EFFECT_PORTAL_TELEPORT = 0x4, - SPELL_EFFECT_TELEPORT_UNITS = 0x5, - SPELL_EFFECT_APPLY_AURA = 0x6, - SPELL_EFFECT_ENVIRONMENTAL_DAMAGE = 0x7, - SPELL_EFFECT_POWER_DRAIN = 0x8, - SPELL_EFFECT_HEALTH_LEECH = 0x9, - SPELL_EFFECT_HEAL = 0xA, - SPELL_EFFECT_BIND = 0xB, - SPELL_EFFECT_PORTAL = 0xC, - SPELL_EFFECT_RITUAL_BASE = 0xD, - SPELL_EFFECT_RITUAL_SPECIALIZE = 0xE, - SPELL_EFFECT_RITUAL_ACTIVATE_PORTAL = 0xF, - SPELL_EFFECT_QUEST_COMPLETE = 0x10, - SPELL_EFFECT_WEAPON_DAMAGE_NOSCHOOL = 0x11, - SPELL_EFFECT_RESURRECT = 0x12, - SPELL_EFFECT_ADD_EXTRA_ATTACKS = 0x13, - SPELL_EFFECT_DODGE = 0x14, - SPELL_EFFECT_EVADE = 0x15, - SPELL_EFFECT_PARRY = 0x16, - SPELL_EFFECT_BLOCK = 0x17, - SPELL_EFFECT_CREATE_ITEM = 0x18, - SPELL_EFFECT_WEAPON = 0x19, - SPELL_EFFECT_DEFENSE = 0x1A, - SPELL_EFFECT_PERSISTENT_AREA_AURA = 0x1B, - SPELL_EFFECT_SUMMON = 0x1C, - SPELL_EFFECT_LEAP = 0x1D, - SPELL_EFFECT_ENERGIZE = 0x1E, - SPELL_EFFECT_WEAPON_PERCENT_DAMAGE = 0x1F, - SPELL_EFFECT_TRIGGER_MISSILE = 0x20, - SPELL_EFFECT_OPEN_LOCK = 0x21, - SPELL_EFFECT_SUMMON_CHANGE_ITEM = 0x22, - SPELL_EFFECT_APPLY_AREA_AURA_PARTY = 0x23, - SPELL_EFFECT_LEARN_SPELL = 0x24, - SPELL_EFFECT_SPELL_DEFENSE = 0x25, - SPELL_EFFECT_DISPEL = 0x26, - SPELL_EFFECT_LANGUAGE = 0x27, - SPELL_EFFECT_DUAL_WIELD = 0x28, - SPELL_EFFECT_SUMMON_WILD = 0x29, - SPELL_EFFECT_SUMMON_GUARDIAN = 0x2A, - SPELL_EFFECT_TELEPORT_UNITS_FACE_CASTER = 0x2B, - SPELL_EFFECT_SKILL_STEP = 0x2C, - SPELL_EFFECT_ADD_HONOR = 0x2D, - SPELL_EFFECT_SPAWN = 0x2E, - SPELL_EFFECT_TRADE_SKILL = 0x2F, - SPELL_EFFECT_STEALTH = 0x30, - SPELL_EFFECT_DETECT = 0x31, - SPELL_EFFECT_TRANS_DOOR = 0x32, - SPELL_EFFECT_FORCE_CRITICAL_HIT = 0x33, - SPELL_EFFECT_GUARANTEE_HIT = 0x34, - SPELL_EFFECT_ENCHANT_ITEM = 0x35, - SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY = 0x36, - SPELL_EFFECT_TAMECREATURE = 0x37, - SPELL_EFFECT_SUMMON_PET = 0x38, - SPELL_EFFECT_LEARN_PET_SPELL = 0x39, - SPELL_EFFECT_WEAPON_DAMAGE = 0x3A, - SPELL_EFFECT_OPEN_LOCK_ITEM = 0x3B, - SPELL_EFFECT_PROFICIENCY = 0x3C, - SPELL_EFFECT_SEND_EVENT = 0x3D, - SPELL_EFFECT_POWER_BURN = 0x3E, - SPELL_EFFECT_THREAT = 0x3F, - SPELL_EFFECT_TRIGGER_SPELL = 0x40, - SPELL_EFFECT_HEALTH_FUNNEL = 0x41, - SPELL_EFFECT_POWER_FUNNEL = 0x42, - SPELL_EFFECT_HEAL_MAX_HEALTH = 0x43, - SPELL_EFFECT_INTERRUPT_CAST = 0x44, - SPELL_EFFECT_DISTRACT = 0x45, - SPELL_EFFECT_PULL = 0x46, - SPELL_EFFECT_PICKPOCKET = 0x47, - SPELL_EFFECT_ADD_FARSIGHT = 0x48, - SPELL_EFFECT_SUMMON_POSSESSED = 0x49, - SPELL_EFFECT_SUMMON_TOTEM = 0x4A, - SPELL_EFFECT_HEAL_MECHANICAL = 0x4B, - SPELL_EFFECT_SUMMON_OBJECT_WILD = 0x4C, - SPELL_EFFECT_SCRIPT_EFFECT = 0x4D, - SPELL_EFFECT_ATTACK = 0x4E, - SPELL_EFFECT_SANCTUARY = 0x4F, - SPELL_EFFECT_ADD_COMBO_POINTS = 0x50, - SPELL_EFFECT_CREATE_HOUSE = 0x51, - SPELL_EFFECT_BIND_SIGHT = 0x52, - SPELL_EFFECT_DUEL = 0x53, - SPELL_EFFECT_STUCK = 0x54, - SPELL_EFFECT_SUMMON_PLAYER = 0x55, - SPELL_EFFECT_ACTIVATE_OBJECT = 0x56, - SPELL_EFFECT_SUMMON_TOTEM_SLOT1 = 0x57, - SPELL_EFFECT_SUMMON_TOTEM_SLOT2 = 0x58, - SPELL_EFFECT_SUMMON_TOTEM_SLOT3 = 0x59, - SPELL_EFFECT_SUMMON_TOTEM_SLOT4 = 0x5A, - SPELL_EFFECT_THREAT_ALL = 0x5B, - SPELL_EFFECT_ENCHANT_HELD_ITEM = 0x5C, - SPELL_EFFECT_SUMMON_PHANTASM = 0x5D, - SPELL_EFFECT_SELF_RESURRECT = 0x5E, - SPELL_EFFECT_SKINNING = 0x5F, - SPELL_EFFECT_CHARGE = 0x60, - SPELL_EFFECT_SUMMON_CRITTER = 0x61, - SPELL_EFFECT_KNOCK_BACK = 0x62, - SPELL_EFFECT_DISENCHANT = 0x63, - SPELL_EFFECT_INEBRIATE = 0x64, - SPELL_EFFECT_FEED_PET = 0x65, - SPELL_EFFECT_DISMISS_PET = 0x66, - SPELL_EFFECT_REPUTATION = 0x67, - SPELL_EFFECT_SUMMON_OBJECT_SLOT1 = 0x68, - SPELL_EFFECT_SUMMON_OBJECT_SLOT2 = 0x69, - SPELL_EFFECT_SUMMON_OBJECT_SLOT3 = 0x6A, - SPELL_EFFECT_SUMMON_OBJECT_SLOT4 = 0x6B, - SPELL_EFFECT_DISPEL_MECHANIC = 0x6C, - SPELL_EFFECT_SUMMON_DEAD_PET = 0x6D, - SPELL_EFFECT_DESTROY_ALL_TOTEMS = 0x6E, - SPELL_EFFECT_DURABILITY_DAMAGE = 0x6F, - SPELL_EFFECT_SUMMON_DEMON = 0x70, - SPELL_EFFECT_RESURRECT_NEW = 0x71, - SPELL_EFFECT_ATTACK_ME = 0x72, - SPELL_EFFECT_DURABILITY_DAMAGE_PCT = 0x73, - SPELL_EFFECT_SKIN_PLAYER_CORPSE = 0x74, - SPELL_EFFECT_SPIRIT_HEAL = 0x75, - SPELL_EFFECT_SKILL = 0x76, - SPELL_EFFECT_APPLY_AREA_AURA_PET = 0x77, - SPELL_EFFECT_TELEPORT_GRAVEYARD = 0x78, - SPELL_EFFECT_NORMALIZED_WEAPON_DMG = 0x79, - SPELL_EFFECT_122 = 0x7A, - SPELL_EFFECT_SEND_TAXI = 0x7B, - SPELL_EFFECT_PLAYER_PULL = 0x7C, - SPELL_EFFECT_MODIFY_THREAT_PERCENT = 0x7D, - SPELL_EFFECT_126 = 0x7E, - SPELL_EFFECT_127 = 0x7F, - SPELL_EFFECT_APPLY_AREA_AURA_FRIEND = 0x80, - SPELL_EFFECT_APPLY_AREA_AURA_ENEMY = 0x81, - TOTAL_SPELL_EFFECTS = 0x82, + SPELL_FAILED_ERROR = 0x1C, }; enum SpellAttributes @@ -715,6 +598,7 @@ enum Events : std::uint32_t LOTTERY_ITEM_UPDAT = 0x224, }; +void *GetObjectPtr(std::uint64_t guid); std::uint32_t GetCastTime(void *unit, int spellId); const SpellRec *GetSpellInfo(int spellId); const char *GetSpellName(int spellId); diff --git a/nampower/main.cpp b/nampower/main.cpp index 69d0dec..8780a45 100644 --- a/nampower/main.cpp +++ b/nampower/main.cpp @@ -54,18 +54,47 @@ static DWORD gLastCast; static bool gCancelling; static bool gCancelFromClient; +static game::SpellFailedReason gCancelReason; using CastSpellT = bool(__fastcall *)(void *, int, void *, std::uint64_t); -using CancelSpellT = int(__fastcall *)(bool, bool, int); +using SendCastT = void(__fastcall *)(game::SpellCast *); +using CancelSpellT = int(__fastcall *)(bool, bool, game::SpellFailedReason); using SignalEventT = void(__fastcall *)(game::Events); using PacketHandlerT = int(__stdcall *)(int, game::CDataStore *); std::unique_ptr> gCastDetour; +std::unique_ptr> gSendCastDetour; std::unique_ptr> gCancelSpellDetour; std::unique_ptr> gSignalEventDetour; std::unique_ptr> gSpellDelayedDetour; std::unique_ptr gCastbarPatch; +void BeginCast(DWORD currentTime, std::uint32_t castTime, int spellId) +{ + gCooldown = currentTime + castTime; + +#ifdef _DEBUG + // don't bother building the string if nobody will see it + if (::IsDebuggerPresent()) + { + std::stringstream str; + str << "Casting " << game::GetSpellName(spellId) << " with cast time " << castTime << " at time " << currentTime; + + 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); +} + bool CastSpellHook(hadesmem::PatchDetourBase *detour, void *unit, int spellId, void *item, std::uint64_t guid) { auto const currentTime = ::GetTickCount(); @@ -85,61 +114,35 @@ bool CastSpellHook(hadesmem::PatchDetourBase *detour, void *unit, int spellId, v auto const castSpell = detour->GetTrampolineT(); auto ret = castSpell(unit, spellId, item, guid); - // 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) { auto const cancelSpell = reinterpret_cast(Offsets::CancelSpell); - cancelSpell(false, false, 28); + cancelSpell(false, false, game::SpellFailedReason::SPELL_FAILED_ERROR); // 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 (!!spell && castTime > 0 && !(spell->Attributes & game::SPELL_ATTR_RANGED)) - { - gCooldown = currentTime + castTime; - -#ifdef _DEBUG - // don't bother building the string if nobody will see it - if (::IsDebuggerPresent()) - { - std::stringstream str; - str << "Casting " << game::GetSpellName(spellId) << " with cast time " << castTime << " at time " << currentTime; - - 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 (!!spell && castTime > 0 && !(spell->Attributes & game::SPELL_ATTR_RANGED) && cursorMode != 2) + BeginCast(currentTime, castTime, spellId); } return ret; } -int CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer, int reason) +int CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer, game::SpellFailedReason reason) { gCancelling = true; gCancelFromClient = notifyServer; + gCancelReason = reason; auto const cancelSpell = detour->GetTrampolineT(); auto const ret = cancelSpell(failed, notifyServer, reason); @@ -149,6 +152,23 @@ int CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyS return ret; } +void SendCastHook(hadesmem::PatchDetourBase *detour, game::SpellCast *cast) +{ + auto const cursorMode = *reinterpret_cast(Offsets::CursorMode); + + // if we were waiting for a target, it means there is no cast bar yet. make one \o/ + if (cursorMode == 2) + { + auto const unit = game::GetObjectPtr(cast->caster); + auto const castTime = game::GetCastTime(unit, cast->spellId); + + BeginCast(::GetTickCount(), castTime, cast->spellId); + } + + auto const sendCast = detour->GetTrampolineT(); + sendCast(cast); +} + void SignalEventHook(hadesmem::PatchDetourBase *detour, game::Events eventId) { auto const currentTime = ::GetTickCount(); @@ -174,8 +194,9 @@ void SignalEventHook(hadesmem::PatchDetourBase *detour, game::Events eventId) if (eventId == game::Events::SPELLCAST_STOP || eventId == game::Events::SPELLCAST_FAILED) { - // if the client is cancelling a cast, reset our own cooldown to allow another one - if (gCancelFromClient) + // 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 || gCancelReason == game::SpellFailedReason::SPELL_FAILED_ERROR) gCooldown = 0; // prevent the result of a previous cast from stopping the current castbar else if (currentTime < gCooldown) @@ -241,6 +262,11 @@ extern "C" __declspec(dllexport) DWORD Load() gCancelSpellDetour = std::make_unique>(process, cancelSpellOrig, &CancelSpellHook); gCancelSpellDetour->Apply(); + // monitor for spell cast triggered after target (terrain, item, etc.) is selected + auto const sendCastOrig = hadesmem::detail::AliasCast(Offsets::SendCast); + gSendCastDetour = std::make_unique>(process, sendCastOrig, &SendCastHook); + gSendCastDetour->Apply(); + // this hook will alter cast bar behavior based on events from the game auto const signalEventOrig = hadesmem::detail::AliasCast(Offsets::SignalEvent); gSignalEventDetour = std::make_unique>(process, signalEventOrig, &SignalEventHook); diff --git a/nampower/offsets.hpp b/nampower/offsets.hpp index 4dd3c66..92aa622 100644 --- a/nampower/offsets.hpp +++ b/nampower/offsets.hpp @@ -33,9 +33,11 @@ enum class Offsets : std::uint32_t { + GetObjectPtr = 0x464870, GetActivePlayer = 0x468550, CastSpell = 0x6E4B60, CancelSpell = 0x6E4940, + SendCast = 0x6E54F0, CreateCastbar = 0x6E7A53, SpellDelayed = 0x6E74F0, SignalEvent = 0x703E50, @@ -43,4 +45,5 @@ enum class Offsets : std::uint32_t Language = 0xC0E080, SignalEventParam = 0x703F50, SpellDb = 0xC0D788, + CursorMode = 0xBE2C4C, };