add spell start/go/failed/delayed/channel events

This commit is contained in:
avitasia
2026-01-14 18:45:58 -08:00
parent f85e0c0156
commit a4963e01dc
14 changed files with 11914 additions and 11429 deletions
+81 -1
View File
@@ -7,6 +7,11 @@ For custom Lua functions, see [SCRIPTS.md](SCRIPTS.md). For general usage inform
## Table of Contents
- [SPELL_QUEUE_EVENT](#spell_queue_event)
- [SPELL_CAST_EVENT](#spell_cast_event)
- [SPELL_START_SELF and SPELL_START_OTHER](#spell_start_self-and-spell_start_other)
- [SPELL_GO_SELF and SPELL_GO_OTHER](#spell_go_self-and-spell_go_other)
- [SPELL_FAILED_SELF and SPELL_FAILED_OTHER](#spell_failed_self-and-spell_failed_other)
- [SPELL_DELAYED_SELF and SPELL_DELAYED_OTHER](#spell_delayed_self-and-spell_delayed_other)
- [SPELL_CHANNEL_START and SPELL_CHANNEL_UPDATE](#spell_channel_start-and-spell_channel_update)
- [SPELL_DAMAGE_EVENT_SELF and SPELL_DAMAGE_EVENT_OTHER](#spell_damage_event_self-and-spell_damage_event_other)
- [Buff/Debuff Events](#buffdebuff-events)
- [AURA_CAST_ON_SELF and AURA_CAST_ON_OTHER](#aura_cast_on_self-and-aura_cast_on_other)
@@ -57,7 +62,7 @@ NampowerSettings:RegisterEvent("SPELL_QUEUE_EVENT", spellQueueEvent)
```
### SPELL_CAST_EVENT
Event you can register in game to get updates when you cast spells with some additional information. This will only fire for spells you (and certain pets) initiated.
Event you can register in game to get updates when you cast spells with some additional information. This will only fire for spells you (and certain pets) initiated. Triggered when you start casting a spell in the client before it is sent to the server.
The event is `SPELL_CAST_EVENT` and has 5 parameters:
1. int success - 1 if cast succeeded, 0 if failed
@@ -91,6 +96,81 @@ Cursive:RegisterEvent("SPELL_CAST_EVENT", function(success, spellId, castType, t
end);
```
### SPELL_START_SELF and SPELL_START_OTHER
Fire when a spell start packet is received. "Self" fires when the active player is the caster, "Other" fires for any other caster. Triggered by the server to notify that a spell with a cast time has begun.
These events are gated behind the `NP_EnableSpellStartEvents` CVar (default 0). Set it to `1` to enable.
Parameters:
1. int itemId - the id of the item that triggered the spell, 0 if it wasn't triggered by an item
2. int spellId
3. string casterGuid - caster guid like "0xF5300000000000A5"
4. string targetGuid - target guid like "0xF5300000000000A5" or "0x0000000000000000" if none
5. int castFlags
6. int castTime - cast time in milliseconds
### SPELL_GO_SELF and SPELL_GO_OTHER
Fire when a spell go packet is received. "Self" fires when the active player is the caster, "Other" fires for any other caster. Triggered by the server to indicate a spell completed casting.
These events are gated behind the `NP_EnableSpellGoEvents` CVar (default 0). Set it to `1` to enable.
Parameters:
1. int itemId - the id of the item that triggered the spell, 0 if it wasn't triggered by an item
2. int spellId
3. string casterGuid - caster guid like "0xF5300000000000A5"
4. string targetGuid - target guid like "0xF5300000000000A5" or "0x0000000000000000" if none
5. int castFlags
6. int numTargetsHit
7. int numTargetsMissed
#### CastFlags (bitmask)
```
CAST_FLAG_NONE = 0 (0x00000000)
CAST_FLAG_HIDDEN_COMBATLOG = 1 (0x00000001)
CAST_FLAG_UNKNOWN2 = 2 (0x00000002)
CAST_FLAG_UNKNOWN3 = 4 (0x00000004)
CAST_FLAG_UNKNOWN4 = 8 (0x00000008)
CAST_FLAG_UNKNOWN5 = 16 (0x00000010)
CAST_FLAG_AMMO = 32 (0x00000020)
CAST_FLAG_UNKNOWN7 = 64 (0x00000040)
CAST_FLAG_UNKNOWN8 = 128 (0x00000080)
CAST_FLAG_UNKNOWN9 = 256 (0x00000100)
```
### SPELL_FAILED_SELF and SPELL_FAILED_OTHER
Fire when a spell failure is reported. "Self" is fired from the client spell failure hook, "Other" is fired from the server handler (only includes caster guid and spell id).
Parameters for `SPELL_FAILED_SELF`:
1. int spellId
2. int spellResult - SpellCastResult enum value
3. int failedByServer - 1 if failed by server, 0 otherwise
Parameters for `SPELL_FAILED_OTHER`:
1. string casterGuid - caster guid like "0xF5300000000000A5"
2. int spellId
### SPELL_DELAYED_SELF and SPELL_DELAYED_OTHER
Fire when a spell is delayed, generally due to taking damage. "Self" fires when the active player is affected, "Other" for other players.
Parameters:
1. string casterGuid - caster guid like "0xF5300000000000A5"
2. int delayMs - delay in milliseconds
### SPELL_CHANNEL_START and SPELL_CHANNEL_UPDATE
Fire when the active player starts or updates a channeled spell. These are self-only events.
Channel target guid is read from `ChannelTargetGuid = 0xC4D980` and included in these events.
Parameters for `SPELL_CHANNEL_START`:
1. int spellId
2. string targetGuid - target guid like "0xF5300000000000A5" or "0x0000000000000000" if none
3. int durationMs - channel duration in milliseconds
Parameters for `SPELL_CHANNEL_UPDATE`:
1. int spellId
2. string targetGuid - target guid like "0xF5300000000000A5" or "0x0000000000000000" if none
3. int remainingMs - remaining channel time in milliseconds
### SPELL_DAMAGE_EVENT_SELF and SPELL_DAMAGE_EVENT_OTHER
New events you can register in game to get updates whenever spell damage occurs. SPELL_DAMAGE_EVENT_SELF will only trigger for damage you deal, while SPELL_DAMAGE_EVENT_OTHER will only trigger for damage dealt by others.
+7 -1
View File
@@ -99,6 +99,10 @@ SET NP_TargetingQueueWindowMs "1000"
- `NP_EnableAutoAttackEvents` - Whether to enable AUTO_ATTACK_SELF and AUTO_ATTACK_OTHER events. 0 to disable, 1 to enable. Default is 0.
- `NP_EnableSpellStartEvents` - Whether to enable SPELL_START_SELF and SPELL_START_OTHER events. 0 to disable, 1 to enable. Default is 0.
- `NP_EnableSpellGoEvents` - Whether to enable SPELL_GO_SELF and SPELL_GO_OTHER events. 0 to disable, 1 to enable. Default is 0.
- `NP_ChannelLatencyReductionPercentage` - The percentage of your latency to subtract from the end of a channel duration to optimize cast time while hopefully not losing any ticks (more explanation below). Default is 75.
- `NP_NameplateDistance` - The distance in yards to display nameplates. Defaults to whatever was set by the game or vanilla tweaks.
@@ -145,7 +149,9 @@ For complete documentation of all custom events added by Nampower, see **[EVENTS
Available events:
- SPELL_QUEUE_EVENT - Fires when spells are queued or dequeued
- SPELL_CAST_EVENT - Fires when you cast a spell with additional information
- SPELL_CAST_EVENT - Fires when you cast a spell with additional information. Triggered when you start casting a spell in the client before it is sent to the server.
- SPELL_START_SELF and SPELL_START_OTHER - Triggered by the server to notify that a spell with a cast time has begun.
- SPELL_GO_SELF and SPELL_GO_OTHER - Triggered by the server to indicate a spell completed casting.
- SPELL_DAMAGE_EVENT_SELF and SPELL_DAMAGE_EVENT_OTHER - Combat damage events
- Buff/Debuff Events (BUFF_ADDED_SELF, BUFF_REMOVED_SELF, etc.)
- AURA_CAST_ON_SELF and AURA_CAST_ON_OTHER - Aura application events (fires once per aura effect per target; "Self" fires when the aura lands on the active player, including self-cast with no explicit target; includes aura metadata + amplitude/misc + aura cap bitfield for buff/debuff slots). Handles single-target and multi-target (AOE) spells. Some auras don't have spell effects and won't trigger this; use BUFF/DEBUFF gains for those. Set `NP_EnableAuraCastEvents=1` to enable. See EVENTS.md for details on multi-target behavior and known limitations.
+11116 -11089
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -71,7 +71,7 @@ namespace Nampower {
}
}
char format[] = "%s%d%d%d%d";
static char format[] = "%s%d%d%d%d";
char *guidStr = new char[21]; // 2 for 0x prefix, 18 for the number, and 1 for '\0'
if (isSelf) {
std::snprintf(guidStr, 21, "0x%016llX", static_cast<unsigned long long>(playerGuid));
@@ -134,7 +134,7 @@ namespace Nampower {
unitCombatLogUnitDead(guid);
// Trigger UNIT_DIED event
char format[] = "%s";
static char format[] = "%s";
char *guidStr = new char[21]; // 2 for 0x prefix, 18 for the number, and 1 for '\0'
std::snprintf(guidStr, 21, "0x%016llX", static_cast<unsigned long long>(guid));
+70
View File
@@ -1067,6 +1067,61 @@ namespace game {
MAX_SPELL_TARGETS
};
enum SpellMissInfo
{
SPELL_MISS_NONE = 0,
SPELL_MISS_MISS = 1,
SPELL_MISS_RESIST = 2,
SPELL_MISS_DODGE = 3,
SPELL_MISS_PARRY = 4,
SPELL_MISS_BLOCK = 5,
SPELL_MISS_EVADE = 6,
SPELL_MISS_IMMUNE = 7,
SPELL_MISS_IMMUNE2 = 8,
SPELL_MISS_DEFLECT = 9,
SPELL_MISS_ABSORB = 10,
SPELL_MISS_REFLECT = 11
};
enum SpellHitType
{
SPELL_HIT_TYPE_CRIT_DEBUG = 0x01,
SPELL_HIT_TYPE_CRIT = 0x02,
SPELL_HIT_TYPE_HIT_DEBUG = 0x04,
SPELL_HIT_TYPE_SPLIT = 0x08,
SPELL_HIT_TYPE_VICTIM_IS_ATTACKER = 0x10,
SPELL_HIT_TYPE_ATTACK_TABLE_DEBUG = 0x20
};
enum SpellDmgClass
{
SPELL_DAMAGE_CLASS_NONE = 0,
SPELL_DAMAGE_CLASS_MAGIC = 1,
SPELL_DAMAGE_CLASS_MELEE = 2,
SPELL_DAMAGE_CLASS_RANGED = 3
};
enum SpellPreventionType
{
SPELL_PREVENTION_TYPE_NONE = 0,
SPELL_PREVENTION_TYPE_SILENCE = 1,
SPELL_PREVENTION_TYPE_PACIFY = 2
};
enum SpellCastFlags
{
CAST_FLAG_NONE = 0x00000000,
CAST_FLAG_HIDDEN_COMBATLOG = 0x00000001, // hide in combat log?
CAST_FLAG_UNKNOWN2 = 0x00000002,
CAST_FLAG_UNKNOWN3 = 0x00000004,
CAST_FLAG_UNKNOWN4 = 0x00000008,
CAST_FLAG_UNKNOWN5 = 0x00000010,
CAST_FLAG_AMMO = 0x00000020, // Projectiles visual
CAST_FLAG_UNKNOWN7 = 0x00000040, // !0x41 mask used to call CGTradeSkillInfo::DoRecast
CAST_FLAG_UNKNOWN8 = 0x00000080,
CAST_FLAG_UNKNOWN9 = 0x00000100,
};
// SpellEntry::Targets
enum SpellCastTargetFlags {
TARGET_FLAG_SELF = 0x00000000,
@@ -1478,6 +1533,21 @@ namespace game {
AUTO_ATTACK_SELF = 562,
AUTO_ATTACK_OTHER = 563,
SPELL_START_SELF = 564,
SPELL_START_OTHER = 565,
SPELL_GO_SELF = 566,
SPELL_GO_OTHER = 567,
SPELL_FAILED_SELF = 568,
SPELL_FAILED_OTHER = 569,
SPELL_DELAYED_SELF = 570,
SPELL_DELAYED_OTHER = 571,
SPELL_CHANNEL_START = 572,
SPELL_CHANNEL_UPDATE = 573,
};
enum TypeMask {
+58
View File
@@ -677,6 +677,12 @@ namespace Nampower {
} else if (strcmp(cvar, "NP_EnableAutoAttackEvents") == 0) {
gUserSettings.enableAutoAttackEvents = atoi(value) != 0;
DEBUG_LOG("Set NP_EnableAutoAttackEvents to " << gUserSettings.enableAutoAttackEvents);
} else if (strcmp(cvar, "NP_EnableSpellStartEvents") == 0) {
gUserSettings.enableSpellStartEvents = atoi(value) != 0;
DEBUG_LOG("Set NP_EnableSpellStartEvents to " << gUserSettings.enableSpellStartEvents);
} else if (strcmp(cvar, "NP_EnableSpellGoEvents") == 0) {
gUserSettings.enableSpellGoEvents = atoi(value) != 0;
DEBUG_LOG("Set NP_EnableSpellGoEvents to " << gUserSettings.enableSpellGoEvents);
} else if (strcmp(cvar, "NP_PreventMountingWhenBuffCapped") == 0) {
gUserSettings.preventMountingWhenBuffCapped = atoi(value) != 0;
DEBUG_LOG("Set NP_PreventMountingWhenBuffCapped to " << gUserSettings.preventMountingWhenBuffCapped);
@@ -1001,6 +1007,26 @@ namespace Nampower {
0, // unk2
0); // unk3
char NP_EnableSpellStartEvents[] = "NP_EnableSpellStartEvents";
CVarRegister(NP_EnableSpellStartEvents, // name
nullptr, // help
0, // unk1
gUserSettings.enableSpellStartEvents ? defaultTrue : defaultFalse, // default value address
nullptr, // callback
1, // category
0, // unk2
0); // unk3
char NP_EnableSpellGoEvents[] = "NP_EnableSpellGoEvents";
CVarRegister(NP_EnableSpellGoEvents, // name
nullptr, // help
0, // unk1
gUserSettings.enableSpellGoEvents ? defaultTrue : defaultFalse, // default value address
nullptr, // callback
1, // category
0, // unk2
0); // unk3
char NP_PreventMountingWhenBuffCapped[] = "NP_PreventMountingWhenBuffCapped";
CVarRegister(NP_PreventMountingWhenBuffCapped, // name
nullptr, // help
@@ -1138,6 +1164,8 @@ namespace Nampower {
loadUserVar("NP_SpamProtectionEnabled");
loadUserVar("NP_EnableAuraCastEvents");
loadUserVar("NP_EnableAutoAttackEvents");
loadUserVar("NP_EnableSpellStartEvents");
loadUserVar("NP_EnableSpellGoEvents");
loadUserVar("NP_PreventMountingWhenBuffCapped");
loadUserVar("NP_MinBufferTimeMs");
@@ -1388,6 +1416,36 @@ namespace Nampower {
char AUTO_ATTACK_OTHER[] = "AUTO_ATTACK_OTHER";
addCustomEvent(game::AUTO_ATTACK_OTHER, AUTO_ATTACK_OTHER);
char SPELL_START_SELF[] = "SPELL_START_SELF";
addCustomEvent(game::SPELL_START_SELF, SPELL_START_SELF);
char SPELL_START_OTHER[] = "SPELL_START_OTHER";
addCustomEvent(game::SPELL_START_OTHER, SPELL_START_OTHER);
char SPELL_GO_SELF[] = "SPELL_GO_SELF";
addCustomEvent(game::SPELL_GO_SELF, SPELL_GO_SELF);
char SPELL_GO_OTHER[] = "SPELL_GO_OTHER";
addCustomEvent(game::SPELL_GO_OTHER, SPELL_GO_OTHER);
char SPELL_FAILED_SELF[] = "SPELL_FAILED_SELF";
addCustomEvent(game::SPELL_FAILED_SELF, SPELL_FAILED_SELF);
char SPELL_FAILED_OTHER[] = "SPELL_FAILED_OTHER";
addCustomEvent(game::SPELL_FAILED_OTHER, SPELL_FAILED_OTHER);
char SPELL_DELAYED_SELF[] = "SPELL_DELAYED_SELF";
addCustomEvent(game::SPELL_DELAYED_SELF, SPELL_DELAYED_SELF);
char SPELL_DELAYED_OTHER[] = "SPELL_DELAYED_OTHER";
addCustomEvent(game::SPELL_DELAYED_OTHER, SPELL_DELAYED_OTHER);
char SPELL_CHANNEL_START[] = "SPELL_CHANNEL_START";
addCustomEvent(game::SPELL_CHANNEL_START, SPELL_CHANNEL_START);
char SPELL_CHANNEL_UPDATE[] = "SPELL_CHANNEL_UPDATE";
addCustomEvent(game::SPELL_CHANNEL_UPDATE, SPELL_CHANNEL_UPDATE);
}
void FrameScript_CreateEventsHook(hadesmem::PatchDetourBase *detour, int param_1, uint32_t maxEventId) {
+2 -2
View File
@@ -35,7 +35,7 @@ namespace Nampower {
constexpr uint32_t DISENCHANT_QUALITY_PURPLE = 0x04; // Epic
constexpr uint32_t MAJOR_VERSION = 2;
constexpr uint32_t MINOR_VERSION = 24;
constexpr uint32_t MINOR_VERSION = 25;
constexpr uint32_t PATCH_VERSION = 0;
constexpr int32_t LUA_REGISTRYINDEX = -10000;
@@ -92,7 +92,7 @@ namespace Nampower {
using EndSceneT = int (__fastcall *)(uintptr_t *unk);
using OnSpriteRightClickT = int (__fastcall *)(uint64_t objectGUID);
using CGGameUI_TargetT = void (__stdcall *)(uint64_t objectGUID);
using Spell_C_SpellFailedT = void (__fastcall *)(uint32_t, game::SpellCastResult, int, int, char unk3);
using Spell_C_SpellFailedT = void (__fastcall *)(uint32_t, game::SpellCastResult, int, int, bool);
using Spell_C_GetAutoRepeatingSpellT = int (__cdecl *)();
using SpellGoT = void (__fastcall *)(uint64_t *, uint64_t *, uint32_t, CDataStore *);
using Spell_C_HandleSpriteClickT = bool (__fastcall *)(game::CSpriteClickEvent *event);
+1 -1
View File
@@ -44,7 +44,7 @@ enum class Offsets : std::uint32_t {
SpellDb = 0xC0D780,
CursorMode = 0xBE2C4C,
CursorType = 0X00BE2C2C,
SpellIsTargeting = 0xCECAC0,
SpellNeedTargets = 0xCECAC0,
CastingItemIdPtr = 0X00CECAB0,
CastingSpellId = 0xCECA88,
AutoRepeatingSpellId = 0xCEAC30,
+13 -254
View File
@@ -9,8 +9,6 @@
namespace Nampower {
auto const APPLY_BUFFER_TO_GCD = false; // gcd issue seems fixed for now
constexpr int MAX_ALLOWED_SPELL_TARGETS = 100;
void SetReleaseAction(uint32_t input) {
uint32_t activeControl = *reinterpret_cast<uint32_t *>(Offsets::CGInputControlGetActive);
@@ -274,7 +272,7 @@ namespace Nampower {
gCastData.targetingSpellId = 0;
gCastData.numRetries = gLastNormalCastParams.numRetries;
Spell_C_CastSpellHook(castSpellDetour, gLastNormalCastParams.casterUnit, gLastNormalCastParams.spellId,
gLastNormalCastParams.item, gLastNormalCastParams.guid);
gLastNormalCastParams.item, gLastNormalCastParams.guid);
gLastCastData.wasQueued = true;
} else {
DEBUG_LOG("Ignoring queued cast, no spell id");
@@ -283,9 +281,9 @@ namespace Nampower {
TriggerSpellQueuedEvent(NORMAL_QUEUE_POPPED, gLastNormalCastParams.spellId);
gCastData.normalSpellQueued = false;
gCastData.castingQueuedSpell = false;
gCastData.targetingSpellQueued = false;
gCastData.targetingSpellId = 0;
gCastData.numRetries = 0;
gCastData.targetingSpellQueued = false;
gCastData.targetingSpellId = 0;
gCastData.numRetries = 0;
}
}
@@ -329,7 +327,7 @@ namespace Nampower {
void
TriggerSpellCastEvent(bool result, uint32_t spellId, CastType castType, std::uint64_t guid, uint32_t itemId) {
char format[] = "%d%d%d%s%d";
static char format[] = "%d%d%d%s%d";
char *guidStr = new char[21]; // 2 for 0x prefix, 18 for the number, and 1 for '\0'
std::snprintf(guidStr, 21, "0x%016llX", static_cast<unsigned long long>(guid));
@@ -848,8 +846,11 @@ namespace Nampower {
// 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) {
if (*reinterpret_cast<int *>(Offsets::SpellIsTargeting) == 0 && !gCastData.pendingOnSwingCast &&
!IsSpellOnCooldown(spellId)) {
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);
@@ -872,7 +873,9 @@ namespace Nampower {
DEBUG_LOG("Retry cast after cancel still failed");
}
} else {
DEBUG_LOG("Initial cast failed but not canceling spell cast");
DEBUG_LOG("Initial cast failed but not canceling spell cast. inTargetingMode ="
<< inTargetingMode << " pendingOnSwingCast =" << gCastData.pendingOnSwingCast
<< " spellIsOnCooldown =" << spellIsOnCooldown);
}
}
@@ -881,250 +884,6 @@ namespace Nampower {
return ret;
}
bool doesSpellApplyAura(const game::SpellRec *spell) {
if (!spell) {
return false;
}
for (unsigned int i: spell->Effect) {
switch (i) {
case game::SPELL_EFFECT_APPLY_AURA:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PARTY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_RAID:
case game::SPELL_EFFECT_APPLY_AREA_AURA_FRIEND:
case game::SPELL_EFFECT_APPLY_AREA_AURA_ENEMY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PET:
return true;
default:
break;
}
}
return false;
}
void triggerAuraCastEventOnTarget(const game::SpellRec *spell,
int spellEffectIndex,
char *casterGuidStr,
char *targetGuidStr, // if provided use this instead of creating one
uint64_t targetGuid,
uint64_t activePlayerGuid,
uint32_t effectType,
uint32_t duration) {
static char format[] = "%d%s%s%d%d%d%d%d%d";
auto eventToTrigger = game::AURA_CAST_ON_OTHER;
if (targetGuid == activePlayerGuid) {
eventToTrigger = game::AURA_CAST_ON_SELF;
}
auto auraName = spell->EffectApplyAuraName[spellEffectIndex];
auto effectAmplitude = spell->EffectAmplitude[spellEffectIndex];
auto effectMiscValue = spell->EffectMiscValue[spellEffectIndex];
auto *targetUnit = game::ClntObjMgrObjectPtr(
static_cast<game::TypeMask>(game::TYPEMASK_PLAYER | game::TYPEMASK_UNIT), targetGuid);
bool targetIsBuffCapped = game::UnitIsBuffCapped(targetUnit);
bool targetIsDebuffCapped = game::UnitIsDebuffCapped(targetUnit);
bool createdGuidStr = false;
if (!targetGuidStr) {
if (targetGuid > 0) {
targetGuidStr = ConvertGuidToString(targetGuid);
createdGuidStr = true;
}
}
auto auraCapStatus = static_cast<uint32_t>(targetIsBuffCapped) |
(static_cast<uint32_t>(targetIsDebuffCapped) << 1);
reinterpret_cast<int (__cdecl *)(int eventCode,
char *fmt,
uint32_t spellIdParam,
char *casterGuidStrParam,
char *targetGuidStrParam,
uint32_t effectParam,
uint32_t auraNameParam,
uint32_t effectAmplitudeParam,
uint32_t effectMiscValueParam,
uint32_t durationParam,
uint32_t auraCapStatusParam)>(Offsets::SignalEventParam)(
eventToTrigger,
format,
spell->Id,
casterGuidStr,
targetGuidStr,
effectType,
auraName,
effectAmplitude,
effectMiscValue,
duration,
auraCapStatus);
if (createdGuidStr) {
delete [] targetGuidStr;
}
}
void TriggerAuraCastEvent(const game::SpellRec *spell,
uint64_t casterGuid,
uint64_t targetGuids[MAX_ALLOWED_SPELL_TARGETS],
uint8_t numTargets,
uint64_t activePlayerGuid,
bool castByActivePlayer) {
// ignore modifiers if not cast by active player
auto duration = game::GetSpellDuration(spell, !castByActivePlayer);
char *casterGuidStr = ConvertGuidToString(casterGuid);
for (int i = 0; i < 3; ++i) {
auto const effectType = spell->Effect[i];
switch (effectType) {
case game::SPELL_EFFECT_APPLY_AURA:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PARTY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_RAID:
case game::SPELL_EFFECT_APPLY_AREA_AURA_FRIEND:
case game::SPELL_EFFECT_APPLY_AREA_AURA_ENEMY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PET: {
auto const implicitTargetA = spell->EffectImplicitTargetA[i];
auto const implicitTargetB = spell->EffectImplicitTargetB[i];
if (implicitTargetA == game::TARGET_UNIT_CASTER) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, casterGuidStr, casterGuid,
activePlayerGuid,
effectType, duration);
break;
}
// handle no targets
if (numTargets == 0 || implicitTargetA == game::TARGET_NONE) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, 0, activePlayerGuid,
effectType, duration);
} else if ((implicitTargetA >= game::TARGET_UNIT_ENEMY_NEAR_CASTER && implicitTargetA <=
game::TARGET_UNIT_ENEMY) ||
(implicitTargetA >= game::TARGET_PLAYER_NYI && implicitTargetA <=
game::TARGET_PLAYER_FRIEND_NYI) ||
implicitTargetA == game::TARGET_UNIT_FRIEND ||
implicitTargetA == game::TARGET_UNIT ||
implicitTargetA == game::TARGET_UNIT_CASTER_MASTER) {
// trigger on first target
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, targetGuids[0], activePlayerGuid,
effectType, duration);
} else if (implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_SRC_LOC ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DEST_LOC ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_24 ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DYNOBJ_LOC ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_WITHIN_CASTER_RANGE ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_54 ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_SRC_LOC ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DEST_LOC ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_24 ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DYNOBJ_LOC ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_WITHIN_CASTER_RANGE ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_54) {
// trigger on all targets except caster
for (int t = 0; t < numTargets; ++t) {
if (targetGuids[t] != casterGuid) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, targetGuids[t],
activePlayerGuid,
effectType, duration);
}
}
} else {
// trigger on all targets (probably some exceptions but good enough for now)
for (int t = 0; t < numTargets; ++t) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, targetGuids[t],
activePlayerGuid,
effectType, duration);
}
}
break;
}
default:
break;
}
}
delete[] casterGuidStr;
}
void
SpellGoHook(hadesmem::PatchDetourBase *detour, uint64_t *itemGUID, uint64_t *casterGUID,
uint32_t spellId,
CDataStore *spellData) {
auto const rpos = spellData->m_read;
int16_t castFlags;
spellData->Get(castFlags);
uint8_t numTargets;
spellData->Get(numTargets);
static uint64_t targetGuids[MAX_ALLOWED_SPELL_TARGETS]; // don't allocate new array each time
// each target corresponds to a spell effect, should never have more than 3
for (int i = 0; i < numTargets && i < MAX_ALLOWED_SPELL_TARGETS; ++i) {
targetGuids[i] = 0;
spellData->Get(targetGuids[i]);
}
// reset read pointer
spellData->m_read = rpos;
auto const spellGo = detour->GetTrampolineT<SpellGoT>();
// if this wasn't triggered by an item, itemGUID will just be the casterGUID duplicated
spellGo(itemGUID, casterGUID, spellId, spellData);
auto const activePlayerGuid = game::ClntObjMgrGetActivePlayerGuid();
auto const castByActivePlayer = activePlayerGuid == *casterGUID;
auto const spell = game::GetSpellInfo(spellId);
if (!spell || spell->Id <= 0) {
DEBUG_LOG("Unable to determine spell information in SpellGo for " << spellId);
return;
}
if (castByActivePlayer) {
auto const currentTime = GetTime();
// only care about our own casts
if (!gCastData.channeling) {
// check if spell is on swing
if (spell->Attributes & game::SPELL_ATTR_ON_NEXT_SWING_1) {
gLastCastData.onSwingStartTimeMs = currentTime;
gCastData.pendingOnSwingCast = false;
if (gCastData.onSwingQueued) {
DEBUG_LOG("On swing spell " << game::GetSpellName(spellId) <<
" resolved, casting queued on swing spell "
<< game::GetSpellName(gLastOnSwingCastParams.spellId));
TriggerSpellQueuedEvent(ON_SWING_QUEUE_POPPED, gLastOnSwingCastParams.spellId);
Spell_C_CastSpellHook(castSpellDetour, gLastOnSwingCastParams.casterUnit,
gLastOnSwingCastParams.spellId,
gLastOnSwingCastParams.item, gLastOnSwingCastParams.guid);
gCastData.onSwingQueued = false;
}
}
}
}
if (gUserSettings.enableAuraCastEvents && doesSpellApplyAura(spell)) {
// not enough available args for itemId currently
// uint32_t itemId = 0;
// if (itemGUID && itemGUID != casterGUID) {
// auto *item = reinterpret_cast<game::CGItem_C *>(game::ClntObjMgrObjectPtr(
// game::TYPEMASK_ITEM, *itemGUID));
// if (item) {
// itemId = game::GetItemId(item);
// }
// }
TriggerAuraCastEvent(spell, *casterGUID, targetGuids, numTargets, activePlayerGuid, castByActivePlayer);
}
}
void
CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer,
game::SpellCastResult reason) {
+1 -1
View File
@@ -30,7 +30,7 @@ namespace Nampower {
CancelSpellHook(hadesmem::PatchDetourBase *detour, bool failed, bool notifyServer, game::SpellCastResult reason);
void SpellGoHook(hadesmem::PatchDetourBase *detour, uint64_t *itemGUID, uint64_t *casterGUID, uint32_t spellId,
CDataStore *spellData);
CDataStore *packet);
bool Spell_C_TargetSpellHook(hadesmem::PatchDetourBase *detour,
uint32_t *player,
+52 -2
View File
@@ -6,8 +6,52 @@
#include "spellcast.hpp"
#include "logging.hpp"
#include "offsets.hpp"
#include "helper.hpp"
namespace Nampower {
void TriggerSpellChannelStartEvent(uint32_t spellId, uint32_t duration) {
static char format[] = "%d%s%d";
auto playerGuid = game::ClntObjMgrGetActivePlayerGuid();
auto playerUnit = game::GetObjectPtr(playerGuid);
auto unitTargetGuid = game::UnitGetTargetGuid(playerUnit); // channel target guid gets updated later
char *targetGuidStr = ConvertGuidToString(unitTargetGuid);
((int (__cdecl *)(int eventCode,
char *fmt,
uint32_t spellIdParam,
char *targetGuidParam,
uint32_t durationParam)) Offsets::SignalEventParam)(
game::SPELL_CHANNEL_START,
format,
spellId,
targetGuidStr,
duration);
delete[] targetGuidStr;
}
void TriggerSpellChannelUpdateEvent(uint32_t spellId, uint32_t channelRemainingTime) {
static char format[] = "%d%s%d";
auto channelTargetGuid = *reinterpret_cast<uint64_t *>(Offsets::ChannelTargetGuid);
char *targetGuidStr = ConvertGuidToString(channelTargetGuid);
((int (__cdecl *)(int eventCode,
char *fmt,
uint32_t spellIdParam,
char *targetGuidParam,
uint32_t remainingParam)) Offsets::SignalEventParam)(
game::SPELL_CHANNEL_UPDATE,
format,
spellId,
targetGuidStr,
channelRemainingTime);
delete[] targetGuidStr;
}
int SpellChannelStartHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet) {
auto const rpos = packet->m_read;
@@ -96,7 +140,11 @@ namespace Nampower {
}
auto const spellChannelStartHandler = detour->GetTrampolineT<PacketHandlerT>();
return spellChannelStartHandler(opCode, packet);
auto ret = spellChannelStartHandler(opCode, packet);
TriggerSpellChannelStartEvent(spellId, duration);
return ret;
}
// this gets called on damage and when channel ends with the end time of the channel
@@ -108,6 +156,8 @@ namespace Nampower {
packet->m_read = rpos;
TriggerSpellChannelUpdateEvent(gCastData.channelSpellId, channelRemainingTime);
if (channelRemainingTime <= 0) {
DEBUG_LOG("Channel done: " << game::GetSpellName(gCastData.channelSpellId)
<< " elapsed " << (GetTime() - gLastCastData.channelStartTimeMs)
@@ -122,4 +172,4 @@ namespace Nampower {
auto const spellChannelUpdateHandler = detour->GetTrampolineT<PacketHandlerT>();
return spellChannelUpdateHandler(opCode, packet);
}
}
}
+508 -75
View File
@@ -11,6 +11,7 @@
namespace Nampower {
uint32_t lastCastResultTimeMs;
constexpr int MAX_ALLOWED_SPELL_TARGETS = 100;
void SignalEventHook(hadesmem::PatchDetourBase *detour, game::Events eventId) {
auto const signalEvent = detour->GetTrampolineT<SignalEventT>();
@@ -43,10 +44,46 @@ namespace Nampower {
return spellTargetUnit(luaState);
}
void TriggerSpellFailedSelfEvent(uint32_t spellId,
game::SpellCastResult spellResult,
bool failedByServer) {
static char format[] = "%d%d%d";
((int (__cdecl *)(int eventCode,
char *fmt,
uint32_t spellIdParam,
uint32_t spellResultParam,
uint32_t failedByServerParam)) Offsets::SignalEventParam)(
game::SPELL_FAILED_SELF,
format,
spellId,
static_cast<uint32_t>(spellResult),
failedByServer ? 1 : 0);
}
void TriggerSpellFailedOtherEvent(uint64_t casterGuid,
uint32_t spellId) {
static char format[] = "%s%d";
char *casterGuidStr = ConvertGuidToString(casterGuid);
((int (__cdecl *)(int eventCode,
char *fmt,
char *casterGuidParam,
uint32_t spellIdParam)) Offsets::SignalEventParam)(
game::SPELL_FAILED_OTHER,
format,
casterGuidStr,
spellId);
delete[] casterGuidStr;
}
void Spell_C_SpellFailedHook(hadesmem::PatchDetourBase *detour, uint32_t spellId,
game::SpellCastResult spellResult, int unk1, int unk2, char unk3) {
game::SpellCastResult spellResult, int unk1, int unk2, bool failedByServer) {
auto const spellFailed = detour->GetTrampolineT<Spell_C_SpellFailedT>();
spellFailed(spellId, spellResult, unk1, unk2, unk3);
spellFailed(spellId, spellResult, unk1, unk2, failedByServer);
// ignore fake failure (used by Unleashed Potential and not sure what else)
if (spellResult == game::SpellCastResult::SPELL_FAILED_DONT_REPORT) {
@@ -65,10 +102,12 @@ namespace Nampower {
spellId == 51934 ||
spellId == 51935 ||
spellId == 51936)
) {
) {
return;
}
TriggerSpellFailedSelfEvent(spellId, spellResult, failedByServer);
ResetCastFlags();
if (spellId == gCastData.onSwingSpellId) {
@@ -78,7 +117,7 @@ namespace Nampower {
if ((spellResult == game::SpellCastResult::SPELL_FAILED_NOT_READY ||
spellResult == game::SpellCastResult::SPELL_FAILED_ITEM_NOT_READY ||
spellResult == game::SpellCastResult::SPELL_FAILED_SPELL_IN_PROGRESS)
) {
) {
auto const currentTime = GetTime();
if (spellResult == game::SpellCastResult::SPELL_FAILED_NOT_READY) {
@@ -99,8 +138,8 @@ namespace Nampower {
if (castParams->castType == CastType::NON_GCD ||
castParams->castType == CastType::TARGETING_NON_GCD) {
DEBUG_LOG("Non gcd spell " << game::GetSpellName(spellId) << " is still on cooldown "
<< spellCooldown
<< " queuing retry");
<< spellCooldown
<< " queuing retry");
gCastData.cooldownNonGcdSpellQueued = true;
gCastData.cooldownNonGcdEndMs = spellCooldown + currentTime;
@@ -108,8 +147,8 @@ namespace Nampower {
gNonGcdCastQueue.push(*castParams, gUserSettings.replaceMatchingNonGcdCategory);
} else {
DEBUG_LOG("Spell " << game::GetSpellName(spellId) << " is still on cooldown "
<< spellCooldown
<< " queuing retry");
<< spellCooldown
<< " queuing retry");
gCastData.cooldownNormalSpellQueued = true;
gCastData.cooldownNormalEndMs = spellCooldown + currentTime;
@@ -142,8 +181,8 @@ namespace Nampower {
if (!gUserSettings.retryServerRejectedSpells) {
DEBUG_LOG("Cast failed for " << game::GetSpellName(spellId)
<< " code " << int(spellResult)
<< " not queuing retry due to retryServerRejectedSpells=false");
<< " code " << int(spellResult)
<< " not queuing retry due to retryServerRejectedSpells=false");
return;
}
gLastErrorTimeMs = currentTime;
@@ -162,22 +201,21 @@ namespace Nampower {
if (castParams->castType == CastType::NON_GCD ||
castParams->castType == CastType::TARGETING_NON_GCD) {
// see if we have a recent successful cast of this spell
auto successfulCastParams = gCastHistory.findNewestSuccessfulSpellId(spellId);
if (successfulCastParams && successfulCastParams->castStartTimeMs > currentTime - 1000) {
// we found a recent successful cast of this spell, ignore the failure that was the result
// of spamming the cast
DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " "
<< " non gcd code " << int(spellResult)
<< ", but a recent cast succeeded, not retrying");
<< " non gcd code " << int(spellResult)
<< ", but a recent cast succeeded, not retrying");
return;
}
DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " "
<< " non gcd code " << int(spellResult)
<< ", retry " << castParams->numRetries
<< " result " << castParams->castResult);
<< " non gcd code " << int(spellResult)
<< ", retry " << castParams->numRetries
<< " result " << castParams->castResult);
gCastData.delayEndMs = currentTime + gBufferTimeMs; // retry after buffer delay
TriggerSpellQueuedEvent(QueueEvents::NON_GCD_QUEUED, spellId);
gCastData.nonGcdSpellQueued = true;
@@ -186,13 +224,13 @@ namespace Nampower {
// if gcd is active, do nothing
if (gCastData.gcdEndMs > currentTime) {
DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " "
<< " code " << int(spellResult)
<< ", gcd active not retrying");
<< " code " << int(spellResult)
<< ", gcd active not retrying");
} else {
DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " "
<< " code " << int(spellResult)
<< ", retry " << castParams->numRetries
<< " result " << castParams->castResult);
<< " code " << int(spellResult)
<< ", retry " << castParams->numRetries
<< " result " << castParams->castResult);
TriggerSpellQueuedEvent(QueueEvents::NORMAL_QUEUED, spellId);
gCastData.normalSpellQueued = true;
@@ -201,13 +239,13 @@ namespace Nampower {
}
} else {
DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " "
<< " code " << int(spellResult)
<< ", not retrying as it has already been retried 3 times");
<< " code " << int(spellResult)
<< ", not retrying as it has already been retried 3 times");
}
} else {
DEBUG_LOG("Cast failed for #" << lastCastId << " " << game::GetSpellName(spellId) << " "
<< " code " << int(spellResult)
<< ", no recent cast params found in history");
<< " code " << int(spellResult)
<< ", no recent cast params found in history");
}
@@ -223,10 +261,10 @@ namespace Nampower {
}
} else if (gCastData.normalSpellQueued) {
DEBUG_LOG("Cast failed for " << game::GetSpellName(spellId)
<< " spell queued + ignored code " << int(spellResult));
<< " spell queued + ignored code " << int(spellResult));
} else {
DEBUG_LOG("Cast failed for " << game::GetSpellName(spellId)
<< " ignored code " << int(spellResult));
<< " ignored code " << int(spellResult));
}
}
@@ -248,19 +286,43 @@ namespace Nampower {
packet->m_read = rpos;
DEBUG_LOG("Spell cooldown opcode:" << opCode << " for " << game::GetSpellName(spellId) << " cooldown "
<< cooldown);
<< cooldown);
}
return spellCooldownHandler(opCode, packet);
}
void TriggerSpellDelayedEvent(uint64_t casterGuid,
uint32_t delayMs) {
static char format[] = "%s%d";
char *casterGuidStr = ConvertGuidToString(casterGuid);
auto event = game::SPELL_DELAYED_OTHER;
if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) {
event = game::SPELL_DELAYED_SELF;
}
((int (__cdecl *)(int eventCode,
char *fmt,
char *casterGuidParam,
uint32_t delayParam)) Offsets::SignalEventParam)(
event,
format,
casterGuidStr,
delayMs);
delete[] casterGuidStr;
}
void Spell_C_CooldownEventTriggeredHook(hadesmem::PatchDetourBase *detour,
uint32_t spellId,
uint64_t *targetGUID,
int param_3,
int clearCooldowns) {
DEBUG_LOG("Cooldown event triggered for " << game::GetSpellName(spellId) << " " <<
targetGUID << " " << param_3 << " " << clearCooldowns);
targetGUID << " " << param_3 << " " << clearCooldowns);
auto const cooldownEventTriggered = detour->GetTrampolineT<Spell_C_CooldownEventTriggeredT>();
cooldownEventTriggered(spellId, targetGUID, param_3, clearCooldowns);
@@ -279,6 +341,8 @@ namespace Nampower {
packet->m_read = rpos;
TriggerSpellDelayedEvent(guid, delay);
auto const activePlayer = game::ClntObjMgrGetActivePlayerGuid();
if (guid == activePlayer) {
@@ -291,8 +355,8 @@ namespace Nampower {
auto lastCastParams = gCastHistory.peek();
if (lastCastParams != nullptr) {
DEBUG_LOG("Spell delayed by " << delay << " for cast #" << lastCastParams->castId << " "
<< game::GetSpellName(lastCastParams->spellId)
<< " cast end " << gCastData.castEndMs);
<< game::GetSpellName(lastCastParams->spellId)
<< " cast end " << gCastData.castEndMs);
}
}
}
@@ -373,19 +437,19 @@ namespace Nampower {
if (gLastServerSpellDelayMs > 0) {
DEBUG_LOG("Cast result for #" << matchingCastId << " "
<< game::GetSpellName(spellId)
<< "(" << spellId << ")"
<< " status " << int(status)
<< " result " << int(spellCastResult) << " latency " << currentLatency
<< " server delay " << gLastServerSpellDelayMs
<< " since last cast result " << currentTime - lastCastResultTimeMs);
<< game::GetSpellName(spellId)
<< "(" << spellId << ")"
<< " status " << int(status)
<< " result " << int(spellCastResult) << " latency " << currentLatency
<< " server delay " << gLastServerSpellDelayMs
<< " since last cast result " << currentTime - lastCastResultTimeMs);
} else {
DEBUG_LOG("Cast result for #" << matchingCastId << " "
<< game::GetSpellName(spellId)
<< "(" << spellId << ")"
<< " status " << int(status)
<< " result " << int(spellCastResult) << " latency " << currentLatency
<< " since last cast result " << currentTime - lastCastResultTimeMs);
<< game::GetSpellName(spellId)
<< "(" << spellId << ")"
<< " status " << int(status)
<< " result " << int(spellCastResult) << " latency " << currentLatency
<< " since last cast result " << currentTime - lastCastResultTimeMs);
}
lastCastResultTimeMs = currentTime;
@@ -408,9 +472,358 @@ namespace Nampower {
DEBUG_LOG("Spell failed opcode:" << opCode << " for " << game::GetSpellName(spellId) << " guid " << guid);
TriggerSpellFailedOtherEvent(guid, spellId);
return spellFailedHandler(opCode, packet);
}
bool doesSpellApplyAura(const game::SpellRec *spell) {
if (!spell) {
return false;
}
for (unsigned int i: spell->Effect) {
switch (i) {
case game::SPELL_EFFECT_APPLY_AURA:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PARTY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_RAID:
case game::SPELL_EFFECT_APPLY_AREA_AURA_FRIEND:
case game::SPELL_EFFECT_APPLY_AREA_AURA_ENEMY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PET:
return true;
default:
break;
}
}
return false;
}
void triggerAuraCastEventOnTarget(const game::SpellRec *spell,
int spellEffectIndex,
char *casterGuidStr,
char *targetGuidStr, // if provided use this instead of creating one
uint64_t targetGuid,
uint64_t activePlayerGuid,
uint32_t effectType,
uint32_t duration) {
static char format[] = "%d%s%s%d%d%d%d%d%d";
auto eventToTrigger = game::AURA_CAST_ON_OTHER;
if (targetGuid == activePlayerGuid) {
eventToTrigger = game::AURA_CAST_ON_SELF;
}
auto auraName = spell->EffectApplyAuraName[spellEffectIndex];
auto effectAmplitude = spell->EffectAmplitude[spellEffectIndex];
auto effectMiscValue = spell->EffectMiscValue[spellEffectIndex];
auto *targetUnit = game::ClntObjMgrObjectPtr(
static_cast<game::TypeMask>(game::TYPEMASK_PLAYER | game::TYPEMASK_UNIT), targetGuid);
bool targetIsBuffCapped = game::UnitIsBuffCapped(targetUnit);
bool targetIsDebuffCapped = game::UnitIsDebuffCapped(targetUnit);
bool createdGuidStr = false;
if (!targetGuidStr) {
if (targetGuid > 0) {
targetGuidStr = ConvertGuidToString(targetGuid);
createdGuidStr = true;
}
}
auto auraCapStatus = static_cast<uint32_t>(targetIsBuffCapped) |
(static_cast<uint32_t>(targetIsDebuffCapped) << 1);
reinterpret_cast<int (__cdecl *)(int eventCode,
char *fmt,
uint32_t spellIdParam,
char *casterGuidStrParam,
char *targetGuidStrParam,
uint32_t effectParam,
uint32_t auraNameParam,
uint32_t effectAmplitudeParam,
uint32_t effectMiscValueParam,
uint32_t durationParam,
uint32_t auraCapStatusParam)>(Offsets::SignalEventParam)(
eventToTrigger,
format,
spell->Id,
casterGuidStr,
targetGuidStr,
effectType,
auraName,
effectAmplitude,
effectMiscValue,
duration,
auraCapStatus);
if (createdGuidStr) {
delete [] targetGuidStr;
}
}
void TriggerAuraCastEvent(const game::SpellRec *spell,
uint64_t casterGuid,
uint64_t targetGuids[MAX_ALLOWED_SPELL_TARGETS],
uint8_t numTargets,
uint64_t activePlayerGuid,
bool castByActivePlayer) {
// ignore modifiers if not cast by active player
auto duration = game::GetSpellDuration(spell, !castByActivePlayer);
char *casterGuidStr = ConvertGuidToString(casterGuid);
for (int i = 0; i < 3; ++i) {
auto const effectType = spell->Effect[i];
switch (effectType) {
case game::SPELL_EFFECT_APPLY_AURA:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PARTY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_RAID:
case game::SPELL_EFFECT_APPLY_AREA_AURA_FRIEND:
case game::SPELL_EFFECT_APPLY_AREA_AURA_ENEMY:
case game::SPELL_EFFECT_APPLY_AREA_AURA_PET: {
auto const implicitTargetA = spell->EffectImplicitTargetA[i];
auto const implicitTargetB = spell->EffectImplicitTargetB[i];
if (implicitTargetA == game::TARGET_UNIT_CASTER) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, casterGuidStr, casterGuid,
activePlayerGuid,
effectType, duration);
break;
}
// handle no targets
if (numTargets == 0 || implicitTargetA == game::TARGET_NONE) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, 0, activePlayerGuid,
effectType, duration);
} else if ((implicitTargetA >= game::TARGET_UNIT_ENEMY_NEAR_CASTER && implicitTargetA <=
game::TARGET_UNIT_ENEMY) ||
(implicitTargetA >= game::TARGET_PLAYER_NYI && implicitTargetA <=
game::TARGET_PLAYER_FRIEND_NYI) ||
implicitTargetA == game::TARGET_UNIT_FRIEND ||
implicitTargetA == game::TARGET_UNIT ||
implicitTargetA == game::TARGET_UNIT_CASTER_MASTER) {
// trigger on first target
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, targetGuids[0], activePlayerGuid,
effectType, duration);
} else if (implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_SRC_LOC ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DEST_LOC ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_24 ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DYNOBJ_LOC ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_WITHIN_CASTER_RANGE ||
implicitTargetA == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_54 ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_SRC_LOC ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DEST_LOC ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_24 ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_AOE_AT_DYNOBJ_LOC ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_WITHIN_CASTER_RANGE ||
implicitTargetB == game::TARGET_ENUM_UNITS_ENEMY_IN_CONE_54) {
// trigger on all targets except caster
for (int t = 0; t < numTargets; ++t) {
if (targetGuids[t] != casterGuid) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, targetGuids[t],
activePlayerGuid,
effectType, duration);
}
}
} else {
// trigger on all targets (probably some exceptions but good enough for now)
for (int t = 0; t < numTargets; ++t) {
triggerAuraCastEventOnTarget(spell, i, casterGuidStr, nullptr, targetGuids[t],
activePlayerGuid,
effectType, duration);
}
}
break;
}
default:
break;
}
}
delete[] casterGuidStr;
}
void TriggerSpellStartEvent(uint32_t itemId,
uint32_t spellId,
uint64_t casterGuid,
uint64_t targetGuid,
uint16_t castFlags,
uint32_t castTime) {
static char format[] = "%d%d%s%s%d%d";
char *casterGuidStr = ConvertGuidToString(casterGuid);
char *targetGuidStr = ConvertGuidToString(targetGuid);
auto event = game::SPELL_START_OTHER;
if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) {
event = game::SPELL_START_SELF;
}
((int (__cdecl *)(int eventCode,
char *fmt,
uint32_t itemIdParam,
uint32_t spellIdParam,
char *casterGuidParam,
char *targetGuidParam,
uint32_t castFlagsParam,
uint32_t castTimeParam)) Offsets::SignalEventParam)(
event,
format,
itemId,
spellId,
casterGuidStr,
targetGuidStr,
castFlags,
castTime);
delete[] casterGuidStr;
delete[] targetGuidStr;
}
void TriggerSpellGoEvent(uint32_t itemId,
uint32_t spellId,
const uint64_t *casterGuid,
uint64_t targetGuid,
uint16_t castFlags,
uint8_t numTargetsHit,
uint8_t numTargetsMissed) {
static char format[] = "%d%d%s%s%d%d%d";
uint64_t casterGuidValue = casterGuid ? *casterGuid : 0;
char *casterGuidStr = ConvertGuidToString(casterGuidValue);
char *targetGuidStr = ConvertGuidToString(targetGuid);
auto event = game::SPELL_GO_OTHER;
if (casterGuidValue == game::ClntObjMgrGetActivePlayerGuid()) {
event = game::SPELL_GO_SELF;
}
((int (__cdecl *)(int eventCode,
char *fmt,
uint32_t itemIdParam,
uint32_t spellIdParam,
char *casterGuidParam,
char *targetGuidParam,
uint32_t castFlagsParam,
uint32_t numTargetsHitParam,
uint32_t numTargetsMissedParam)) Offsets::SignalEventParam)(
event,
format,
itemId,
spellId,
casterGuidStr,
targetGuidStr,
castFlags,
numTargetsHit,
numTargetsMissed);
delete[] casterGuidStr;
delete[] targetGuidStr;
}
void SpellGoHook(hadesmem::PatchDetourBase *detour, uint64_t *itemGUID, uint64_t *casterGUID,
uint32_t spellId, CDataStore *packet) {
auto const rpos = packet->m_read;
int16_t castFlags;
packet->Get(castFlags);
uint8_t numTargetsHit;
packet->Get(numTargetsHit);
static uint64_t targetHitGuids[MAX_ALLOWED_SPELL_TARGETS]; // don't allocate new array each time
// each target corresponds to a spell effect, should never have more than 3
for (int i = 0; i < numTargetsHit && i < MAX_ALLOWED_SPELL_TARGETS; ++i) {
targetHitGuids[i] = 0;
packet->Get(targetHitGuids[i]);
}
uint8_t numTargetsMissed;
packet->Get(numTargetsMissed);
for (int i = 0; i < numTargetsMissed; ++i) {
uint64_t missedTargetGuid;
packet->Get(missedTargetGuid); // not worth storing missed targets
uint8_t missCondition;
packet->Get(missCondition);
// Reflect entries include an extra result byte in SpellGoTargets.
if (missCondition == game::SPELL_MISS_REFLECT) {
uint8_t reflectResult;
packet->Get(reflectResult);
}
}
int16_t spellCastTargetMask;
packet->Get(spellCastTargetMask);
uint64_t unitTargetGUID = 0;
if (spellCastTargetMask & (game::TARGET_FLAG_UNIT | game::TARGET_FLAG_UNK2))
packet->GetPackedGuid(unitTargetGUID);
uint32_t itemId = 0;
uint64_t casterGuidValue = casterGUID ? *casterGUID : 0;
if (itemGUID && *itemGUID != 0 && *itemGUID != casterGuidValue) {
auto *item = reinterpret_cast<game::CGItem_C *>(game::ClntObjMgrObjectPtr(
game::TYPEMASK_ITEM, *itemGUID));
if (item) {
itemId = game::GetItemId(item);
}
}
if (gUserSettings.enableSpellGoEvents) {
TriggerSpellGoEvent(itemId, spellId, casterGUID, unitTargetGUID, castFlags,
numTargetsHit, numTargetsMissed);
}
// reset read pointer
packet->m_read = rpos;
auto const spellGo = detour->GetTrampolineT<SpellGoT>();
// if this wasn't triggered by an item, itemGUID will just be the casterGUID duplicated
spellGo(itemGUID, casterGUID, spellId, packet);
auto const activePlayerGuid = game::ClntObjMgrGetActivePlayerGuid();
auto const castByActivePlayer = activePlayerGuid == casterGuidValue;
auto const spell = game::GetSpellInfo(spellId);
if (!spell || spell->Id <= 0) {
DEBUG_LOG("Unable to determine spell information in SpellGo for " << spellId);
return;
}
if (castByActivePlayer) {
auto const currentTime = GetTime();
// only care about our own casts
if (!gCastData.channeling) {
// check if spell is on swing
if (spell->Attributes & game::SPELL_ATTR_ON_NEXT_SWING_1) {
gLastCastData.onSwingStartTimeMs = currentTime;
gCastData.pendingOnSwingCast = false;
if (gCastData.onSwingQueued) {
DEBUG_LOG("On swing spell " << game::GetSpellName(spellId) <<
" resolved, casting queued on swing spell "
<< game::GetSpellName(gLastOnSwingCastParams.spellId));
TriggerSpellQueuedEvent(ON_SWING_QUEUE_POPPED, gLastOnSwingCastParams.spellId);
Spell_C_CastSpellHook(castSpellDetour, gLastOnSwingCastParams.casterUnit,
gLastOnSwingCastParams.spellId,
gLastOnSwingCastParams.item, gLastOnSwingCastParams.guid);
gCastData.onSwingQueued = false;
}
}
}
}
if (gUserSettings.enableAuraCastEvents && doesSpellApplyAura(spell)) {
TriggerAuraCastEvent(spell, *casterGUID, targetHitGuids, numTargetsHit, activePlayerGuid,
castByActivePlayer);
}
}
int SpellStartHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t unk, uint32_t opCode, uint32_t unk2,
CDataStore *packet) {
auto const spellStartHandler = detour->GetTrampolineT<FastCallPacketHandlerT>();
@@ -427,16 +840,36 @@ namespace Nampower {
uint64_t casterGuid;
packet->GetPackedGuid(casterGuid);
uint32_t spellId;
packet->Get(spellId);
uint16_t castFlags;
packet->Get(castFlags);
uint32_t castTime;
packet->Get(castTime);
int16_t spellCastTargetMask;
packet->Get(spellCastTargetMask);
uint64_t unitTargetGUID = 0;
if (spellCastTargetMask & (game::TARGET_FLAG_UNIT | game::TARGET_FLAG_UNK2))
packet->GetPackedGuid(unitTargetGUID);
uint32_t itemId = 0;
if (itemGuid != 0 && itemGuid != casterGuid) {
auto *item = reinterpret_cast<game::CGItem_C *>(game::ClntObjMgrObjectPtr(
game::TYPEMASK_ITEM, itemGuid));
if (item) {
itemId = game::GetItemId(item);
}
}
if (gUserSettings.enableSpellStartEvents) {
TriggerSpellStartEvent(itemId, spellId, casterGuid, unitTargetGUID, castFlags, castTime);
}
if (casterGuid == game::ClntObjMgrGetActivePlayerGuid()) {
uint32_t spellId;
packet->Get(spellId);
uint16_t castFlags;
packet->Get(castFlags);
uint32_t castTime;
packet->Get(castTime);
bool isAutoRepeat = false;
auto spellInfo = game::GetSpellInfo(spellId);
@@ -460,8 +893,8 @@ namespace Nampower {
castParams->castTimeMs = castTime;
DEBUG_LOG("Server cast time for " << game::GetSpellName(spellId) << " increased by "
<< castTimeDifference << "ms. Updated cast end time to "
<< gCastData.castEndMs);
<< castTimeDifference << "ms. Updated cast end time to "
<< gCastData.castEndMs);
}
} else if (!isAutoRepeat && castParams->castTimeMs > castTime) {
// server cast time decreased
@@ -470,23 +903,23 @@ namespace Nampower {
if (castTimeDifference > 5) {
auto gcdTime = GetGcdOrCooldownForSpell(spellId);
if (gcdTime > 1500) {
gcdTime = 1500; // items with spells on gcd will return their item gcd, make sure not to use that
gcdTime = 1500;
// items with spells on gcd will return their item gcd, make sure not to use that
}
if (gcdTime > castTime) {
DEBUG_LOG("Server cast time " << castTime << " was reduced below gcd of " << gcdTime
<< " using gcd instead");
<< " using gcd instead");
castTime = gcdTime;
gCastData.castEndMs = castParams->castStartTimeMs + gcdTime;
} else {
gCastData.castEndMs = castParams->castStartTimeMs + castTime + gBufferTimeMs;
}
castParams->castTimeMs = castTime;
DEBUG_LOG("Server cast time for " << game::GetSpellName(spellId) << " decreased by "
<< castTimeDifference << "ms. Updated cast end time to "
<< gCastData.castEndMs);
<< castTimeDifference << "ms. Updated cast end time to "
<< gCastData.castEndMs);
}
}
@@ -535,7 +968,7 @@ namespace Nampower {
int32_t resist,
uint32_t auraType,
uint32_t hitInfo) {
char format[] = "%s%s%d%d%s%d%d%s";
static char format[] = "%s%s%d%d%s%d%d%s";
char *targetGuidStr = ConvertGuidToString(targetGuid);
@@ -575,16 +1008,16 @@ namespace Nampower {
uint32_t hitInfo,
uint32_t spellSchool,
const char *effectAuraStr)) Offsets::SignalEventParam)(
event,
format,
targetGuidStr,
casterGuidStr,
spellId,
amount,
mitigationStream.str().c_str(),
hitInfo,
spellSchool,
effectsAuraTypeStream.str().c_str());
event,
format,
targetGuidStr,
casterGuidStr,
spellId,
amount,
mitigationStream.str().c_str(),
hitInfo,
spellSchool,
effectsAuraTypeStream.str().c_str());
delete[] targetGuidStr;
delete[] casterGuidStr;
@@ -615,7 +1048,7 @@ namespace Nampower {
switch (auraType) {
case 3: // SPELL_AURA_PERIODIC_DAMAGE
case 89: // SPELL_AURA_PERIODIC_DAMAGE_PERCENT
packet->Get(amount); // damage amount
packet->Get(amount); // damage amount
uint32_t spellSchool;
packet->Get(spellSchool);
@@ -632,7 +1065,7 @@ namespace Nampower {
break;
case 8: // SPELL_AURA_PERIODIC_HEAL
case 20: // SPELL_AURA_OBS_MOD_HEALTH
packet->Get(amount); // heal amount
packet->Get(amount); // heal amount
// No custom event for these for now
break;
@@ -640,16 +1073,16 @@ namespace Nampower {
case 24: // SPELL_AURA_PERIODIC_ENERGIZE
packet->Get(powerType);
packet->Get(amount); // power type amount
packet->Get(amount); // power type amount
// No custom event for these for now
break;
case 64: // SPELL_AURA_PERIODIC_MANA_LEECH
packet->Get(powerType);
packet->Get(amount); // power type amount
packet->Get(amount); // power type amount
uint32_t multiplier;
packet->Get(multiplier); // gain multiplier
packet->Get(multiplier); // gain multiplier
// No custom event for these for now
break;
@@ -726,7 +1159,7 @@ namespace Nampower {
DEBUG_LOG("Play spell visual id " << spellVisualKitIndex << " for guid " << targetGuid);
// return playSpellVisualHandler(opCode, packet);
// return playSpellVisualHandler(opCode, packet);
return 1;
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Nampower {
uint32_t Script_SpellTargetUnitHook(hadesmem::PatchDetourBase *detour, uintptr_t *luaState);
void Spell_C_SpellFailedHook(hadesmem::PatchDetourBase *detour, uint32_t spellId,
game::SpellCastResult spellResult, int unk1, int unk2, char unk3);
game::SpellCastResult spellResult, int unk1, int unk2, bool failedByServer);
int CastResultHandlerHook(hadesmem::PatchDetourBase *detour, uint32_t *opCode, CDataStore *packet);
+2
View File
@@ -30,6 +30,8 @@ struct UserSettings {
bool spamProtectionEnabled = true;
bool enableAuraCastEvents = false;
bool enableAutoAttackEvents = false;
bool enableSpellStartEvents = false;
bool enableSpellGoEvents = false;
bool preventMountingWhenBuffCapped = true;
uint32_t spellQueueWindowMs = 500;