Compare commits

...
1 Commits
Author SHA1 Message Date
Brues 939b52ae69 Recover dropped auras in C_UnitAuras; evict on removal
The 1.12 engine drops a unit's UNIT_FIELD_AURA slots when it loses sync with
the unit (rogue stealth, party range fluctuation) even though the auras are
still active server-side, so C_UnitAuras.GetAuraDataByIndex /
GetBuffDataByIndex returned nil for auras that are still up.

Fall back to the Aura::Source cache (caster/timing captured from
SMSG_SPELL_GO at cast time, which survives descriptor clears) when the
descriptor has no slot:

- Aura::Source: entries gain a helpful/harmful kind, recorded from the
  descriptor slot at application time (OnAuraAdded/OnAuraStacksChanged);
  SpellGo-only entries stay unknown and are excluded. New Enumerate() returns
  non-expired entries of a given kind for a unit.
- Aura::Data: a shared BuildTable feeds both the descriptor Push and a new
  cache-fed path; PushNthCacheFallback / AppendCacheFallbacks append cache
  entries after the descriptor ones, deduped against populated slots.
- Aura::Api: the index path and GetUnitAuras consult the fallback.

To keep the fallback from resurrecting auras that are genuinely gone
(replaced by a higher rank, dispelled, fell off early), co-hook OnAuraRemoved
(FUN_00612320) and evict the cache entry. A rogue stealthing does NOT route
through OnAuraRemoved (object-visibility drop, not per-aura removal), so
eviction and stealth-recovery coexist: real removals evict, visibility drops
don't.

Add FUN_ON_AURA_REMOVED offset.
Fixes #5
2026-06-28 13:47:26 -05:00
6 changed files with 339 additions and 96 deletions
+12
View File
@@ -733,6 +733,18 @@ enum Offsets {
// re-stamp expiration so stacking-DoT refreshes are picked up.
FUN_ON_AURA_STACKS_CHANGED = 0x00612450,
// `CGUnit_C::OnAuraRemoved` — same ABI as `OnAuraAdded`
// (`__fastcall(unit /*ecx*/, edx_unused, slot, spellId)`; the disassembly
// of the diff dispatcher `FUN_00604d00` at the call site `0x00604dc4`
// shows `ECX = unit`, stack `[slot, spellId]`). The sibling of
// `OnAuraAdded`: `FUN_00604d00` fires this when a descriptor aura slot
// goes from occupied to empty — a genuine removal (fell off, dispelled,
// or replaced by a higher rank, which fires Removed(old)+Added(new)).
// `Aura::Source` co-hooks it to EVICT the cached entry so the
// `GetAuraDataByIndex` fallback can't resurrect a superseded/dispelled
// aura that's still inside its computed duration window.
FUN_ON_AURA_REMOVED = 0x00612320,
// CGPlayer-side sub-struct, allocated for any player-controlled
// unit (local self, target, party, raid, inspect targets — all of
// them). Holds player-specific data that's *not* in the broadcast
+10 -3
View File
@@ -92,11 +92,16 @@ int PushAuraByIndex(void *L, const char *unitToken, int index,
Data::Filter filter, bool playerOnly = false) {
const uint8_t *unit = ResolveUnit(unitToken);
const int slot = Data::FindNthSlot(unit, index, filter, playerOnly);
if (slot < 0) {
Game::Lua::PushNil(L);
if (slot >= 0) {
Data::Push(L, unit, slot);
return 1;
}
Data::Push(L, unit, slot);
// Descriptor exhausted — an aura the engine dropped from the slot array
// (rogue stealth, party range fluctuation) may still be live in the
// Aura::Source cache. Surface it after the descriptor entries.
if (Data::PushNthCacheFallback(L, unit, index, filter, playerOnly))
return 1;
Game::Lua::PushNil(L);
return 1;
}
@@ -218,6 +223,8 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
Data::Push(L, unit, slot);
Game::Lua::SetTable(L, outerIdx);
}
// Append auras the descriptor dropped but Aura::Source still has live.
Data::AppendCacheFallbacks(L, unit, filter, playerOnly, outerIdx, nextKey);
}
int __fastcall Script_GetUnitAuras(void *L) {
+187 -83
View File
@@ -350,10 +350,13 @@ const char *DispelName(uint32_t dispelTypeID) {
return (name == nullptr || *name == '\0') ? nullptr : name;
}
void Push(void *L, const uint8_t *unit, int slot) {
const uint32_t spellID = ReadSpellID(unit, slot);
// Writes the full modern AuraData table on top of the Lua stack from
// already-resolved values. Shared by the descriptor path (`Push`) and the
// cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table).
static void BuildTable(void *L, uint32_t spellID, int applications,
bool isHelpful, double duration, double expirationTime,
uint64_t casterGuid) {
const uint8_t *spellRecord = SpellRecord(spellID);
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
uint32_t dispelTypeID = 0;
if (spellRecord != nullptr) {
@@ -370,87 +373,32 @@ void Push(void *L, const uint8_t *unit, int slot) {
Game::Lua::SetFieldString(L, "name", name);
Game::Lua::SetFieldString(L, "icon", icon);
Game::Lua::SetFieldNumber(L, "applications",
static_cast<double>(ReadStacks(unit, slot)));
static_cast<double>(applications));
Game::Lua::SetFieldNumber(L, "spellId", static_cast<double>(spellID));
Game::Lua::SetFieldString(L, "dispelName", dispel);
Game::Lua::SetFieldBool(L, "isHelpful", isHelpful);
Game::Lua::SetFieldBool(L, "isHarmful", !isHelpful);
// Timing fields. Vanilla doesn't broadcast cast time / duration
// for ANY unit's auras except the local player's — for everyone
// else (target, party, raid, mouseover) `expirationTime` stays 0
// and addons have to track expiry themselves via aura-add events.
// For the player we read the engine's `VAR_PLAYER_BUFF_TABLE`
// (same data `GetPlayerBuffTimeLeft` returns), which gives us a
// real expiration timestamp.
//
// `duration` comes from Spell.dbc → SpellDuration.dbc with
// level scaling. That's the *base* value; talent / glyph
// duration-extension modifiers (Improved PW:F, etc.) are
// already baked into the expiration timestamp on the
// caster's side, so `expirationTime - GetTime()` reflects the
// true remaining time even when `duration` doesn't include
// the talent boost. We populate `duration` for any unit (the
// base value is identical regardless of who's affected); only
// `expirationTime` is player-gated.
double duration = 0.0;
double expirationTime = 0.0;
const uint8_t *player = LocalPlayer();
if (spellID != 0) {
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
duration = SpellBaseDurationSeconds(spellID, unitLevel);
}
if (unit != nullptr && unit == player) {
const uint8_t *entry = FindPlayerBuffEntry(spellID);
if (entry != nullptr)
expirationTime = PlayerBuffExpirationSeconds(entry);
}
// Caster + non-player expiration from the SMSG_SPELL_GO cache
// (`Aura::Source`), captured at cast time. The vanilla descriptor has
// neither. `sourceUnit` applies to any unit; `expirationTime` only
// fills in here when the player-buff table above didn't — that table is
// authoritative for the player's own auras, the cache covers everyone
// else. Both are best-effort: a miss (aura predates login, or the cast
// wasn't observed) leaves the modern-truthful defaults untouched.
const char *sourceUnit = nullptr;
const char *sourceGUID = nullptr;
char tokenBuf[32];
char guidBuf[Guid::STRING_SIZE];
if (spellID != 0) {
uint64_t casterGuid = 0;
uint32_t expMs = 0;
uint32_t durMs = 0;
if (Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs,
&durMs)) {
if (expirationTime == 0.0 && expMs != 0)
expirationTime = static_cast<double>(expMs) * 0.001;
// Prefer the applied (caster-modified) duration so `duration`
// stays consistent with `expirationTime` — otherwise a talented
// DoT (e.g. Improved Shadow Word: Pain) shows remaining time
// exceeding the base `duration`.
if (durMs != 0)
duration = static_cast<double>(durMs) * 0.001;
if (casterGuid != 0) {
// `sourceUnit` is a token (nil when the caster maps to no
// current token); `sourceGUID` is the raw "0x..." GUID and
// is set whenever we have a caster — a ClassicAPI extension
// (not in retail AuraData) that survives the caster leaving
// token range and doubles as a unit token under SuperWoW.
sourceUnit = Unit::Identity::TokenFromGUID(
casterGuid, tokenBuf, sizeof tokenBuf);
sourceGUID = Guid::FormatAsString(casterGuid, guidBuf,
sizeof guidBuf);
}
}
}
Game::Lua::SetFieldNumber(L, "duration", duration);
Game::Lua::SetFieldNumber(L, "expirationTime", expirationTime);
if (sourceUnit != nullptr)
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
if (sourceGUID != nullptr)
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
if (casterGuid != 0) {
// `sourceUnit` is a token (nil when the caster maps to no current
// token); `sourceGUID` is the raw "0x..." GUID and is set whenever we
// have a caster — a ClassicAPI extension (not in retail AuraData) that
// survives the caster leaving token range and doubles as a unit token
// under SuperWoW.
char tokenBuf[32];
char guidBuf[Guid::STRING_SIZE];
const char *sourceUnit = Unit::Identity::TokenFromGUID(
casterGuid, tokenBuf, sizeof tokenBuf);
const char *sourceGUID =
Guid::FormatAsString(casterGuid, guidBuf, sizeof guidBuf);
if (sourceUnit != nullptr)
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
if (sourceGUID != nullptr)
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
}
Game::Lua::SetFieldNumber(L, "charges", 0);
Game::Lua::SetFieldNumber(L, "maxCharges", 0);
Game::Lua::SetFieldNumber(L, "timeMod", 1);
@@ -466,11 +414,167 @@ void Push(void *L, const uint8_t *unit, int slot) {
Game::Lua::SetFieldBool(L, "shouldConsolidate", false);
Game::Lua::SetFieldBool(L, "isRaid", false);
// `sourceUnit` / `sourceGUID` are set above when the `Aura::Source`
// cache has a caster for this aura; left unset (→ nil) on a miss.
// `auraInstanceID` and `points` are deliberately omitted — modern
// returns nil for those when they don't apply, and Lua reading a
// missing key yields nil, so the table doesn't need an explicit entry.
// `auraInstanceID` and `points` are deliberately omitted — modern returns
// nil for those when they don't apply, and Lua reading a missing key
// yields nil, so the table doesn't need an explicit entry.
}
void Push(void *L, const uint8_t *unit, int slot) {
const uint32_t spellID = ReadSpellID(unit, slot);
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
// Timing fields. Vanilla doesn't broadcast cast time / duration for ANY
// unit's auras except the local player's — for everyone else (target,
// party, raid, mouseover) `expirationTime` stays 0 unless the
// `Aura::Source` cache observed the cast. For the player we read the
// engine's `VAR_PLAYER_BUFF_TABLE` (same data `GetPlayerBuffTimeLeft`
// returns), which gives a real expiration timestamp.
//
// `duration` comes from Spell.dbc → SpellDuration.dbc with level scaling
// (the *base* value); talent / glyph duration-extension modifiers are
// already baked into the expiration timestamp on the caster's side, so
// `expirationTime - GetTime()` reflects true remaining time even when the
// base `duration` doesn't include the talent boost.
double duration = 0.0;
double expirationTime = 0.0;
uint64_t casterGuid = 0;
const uint8_t *player = LocalPlayer();
if (spellID != 0) {
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
duration = SpellBaseDurationSeconds(spellID, unitLevel);
}
if (unit != nullptr && unit == player) {
const uint8_t *entry = FindPlayerBuffEntry(spellID);
if (entry != nullptr)
expirationTime = PlayerBuffExpirationSeconds(entry);
}
// Caster + non-player expiration from the SMSG_SPELL_GO cache, captured at
// cast time (the descriptor has neither). The player-buff table above is
// authoritative for the player's own expiration; the cache fills it for
// everyone else. Prefer the applied (caster-modified) duration so it stays
// consistent with `expirationTime` — otherwise a talented DoT (Improved
// Shadow Word: Pain) would show remaining time exceeding the base.
if (spellID != 0) {
uint64_t c = 0;
uint32_t expMs = 0;
uint32_t durMs = 0;
if (Aura::Source::Get(UnitGuid(unit), spellID, &c, &expMs, &durMs)) {
if (expirationTime == 0.0 && expMs != 0)
expirationTime = static_cast<double>(expMs) * 0.001;
if (durMs != 0)
duration = static_cast<double>(durMs) * 0.001;
casterGuid = c;
}
}
BuildTable(L, spellID, ReadStacks(unit, slot), isHelpful, duration,
expirationTime, casterGuid);
}
namespace {
// Builds an AuraData table from an `Aura::Source` cache entry — the fallback
// when the descriptor has dropped the aura's slot. Stack count isn't broadcast
// in SMSG_SPELL_GO, so `applications` defaults to 1. `duration` uses the
// applied (caster-modified) ms when known, else the Spell.dbc base.
void PushFromCache(void *L, const uint8_t *unit,
const Aura::Source::CachedAura &c, bool isHelpful) {
double duration = 0.0;
if (c.durationMs != 0) {
duration = static_cast<double>(c.durationMs) * 0.001;
} else {
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
duration = SpellBaseDurationSeconds(c.spellId, unitLevel);
}
const double expirationTime =
(c.expirationMs != 0) ? static_cast<double>(c.expirationMs) * 0.001 : 0.0;
BuildTable(L, c.spellId, 1, isHelpful, duration, expirationTime,
c.casterGuid);
}
// Counts populated descriptor slots matching `filter` (and `playerOnly`).
int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
if (unit == nullptr)
return 0;
const int start = (filter == Filter::Harmful)
? Offsets::UNIT_AURA_BUFF_COUNT
: 0;
const int end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
: Offsets::UNIT_AURA_BUFF_COUNT;
int n = 0;
for (int slot = start; slot < end; ++slot) {
if (!IsSlotPopulated(unit, slot))
continue;
if (playerOnly && !IsPlayerCast(unit, slot))
continue;
++n;
}
return n;
}
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
// `filter`/`playerOnly`: it must not already be in a populated descriptor slot
// (no double-listing the same live aura) and, when player-filtered, must have
// been cast by us. Superseded / dispelled auras are kept out of the cache by
// `Aura::Source`'s removal eviction, not filtered here.
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
Filter filter, bool playerOnly) {
if (FindSlotBySpellID(unit, c.spellId, &filter, false) >= 0)
return false; // still in the descriptor — returned via the slot path
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
return false;
return true;
}
} // namespace
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
Filter filter, bool playerOnly) {
if (unit == nullptr)
return false;
// The descriptor held `descCount` matches; the caller already found fewer
// than `oneBasedIndex` there, so the cache supplies index
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
const int target = oneBasedIndex - CountSlots(unit, filter, playerOnly);
if (target < 1)
return false;
const uint64_t guid = UnitGuid(unit);
if (guid == 0)
return false;
Aura::Source::CachedAura buf[kFallbackMax];
const int n = Aura::Source::Enumerate(
guid, filter == Filter::Harmful, buf, kFallbackMax);
int seen = 0;
for (int i = 0; i < n; ++i) {
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
continue;
if (++seen == target) {
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
return true;
}
}
return false;
}
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
bool playerOnly, int outerIdx, int &nextKey) {
if (unit == nullptr)
return;
const uint64_t guid = UnitGuid(unit);
if (guid == 0)
return;
Aura::Source::CachedAura buf[kFallbackMax];
const int n = Aura::Source::Enumerate(
guid, filter == Filter::Harmful, buf, kFallbackMax);
for (int i = 0; i < n; ++i) {
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
continue;
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
Game::Lua::SetTable(L, outerIdx);
}
}
} // namespace Aura::Data
+20
View File
@@ -112,4 +112,24 @@ const char *DispelName(uint32_t dispelTypeID);
// Net stack effect: +1 (the table).
void Push(void *L, const uint8_t *unit, int slot);
// Fallback for the index path when the descriptor has dropped an aura's slot
// (rogue stealth, party range fluctuation) but it's still active server-side.
// `oneBasedIndex` is the same index the caller passed to `FindNthSlot`; this
// resolves it against the `Aura::Source` cache *after* the descriptor matches
// (so cache entries append after descriptor ones, preserving existing index
// order). On a hit it pushes the AuraData table and returns true; on a miss it
// pushes nothing and returns false (caller pushes nil). Only entries whose
// helpful/harmful classification matches `filter` and that aren't already in a
// populated descriptor slot are considered.
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
Filter filter, bool playerOnly = false);
// Appends every eligible `Aura::Source` cache fallback for `unit` matching
// `filter` into the array table at `outerIdx`, continuing from `nextKey`
// (updated in place). The bulk-enumeration analog of `PushNthCacheFallback`,
// for `GetUnitAuras`. Skips entries already present in a populated descriptor
// slot so it never double-lists.
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
bool playerOnly, int outerIdx, int &nextKey);
} // namespace Aura::Data
+90 -10
View File
@@ -69,12 +69,19 @@ uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
// ---- Cache ---------------------------------------------------------------
// Helpful/harmful classification, recorded from the aura's descriptor slot
// when it's applied (the only place the buff/debuff split is known). SpellGo
// gives no slot, so casts we only saw there stay Unknown until/unless an
// application hook fires for the same aura.
enum Kind : int8_t { KIND_UNKNOWN = -1, KIND_HELPFUL = 0, KIND_HARMFUL = 1 };
struct Entry {
uint64_t targetGuid;
uint64_t casterGuid;
uint32_t spellId;
uint32_t expirationMs; // 0 = infinite / unknown duration
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
int8_t kind; // Kind; descriptor-slot-derived, KIND_UNKNOWN if only seen via SpellGo
bool used;
};
@@ -88,13 +95,19 @@ int g_writeCursor = 0;
// would replace talented timing with the unmodified base), so it skips
// entries that already carry a caster.
void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
uint32_t expirationMs, uint32_t durationMs, bool fromCast) {
uint32_t expirationMs, uint32_t durationMs, bool fromCast,
int8_t kind) {
if (targetGuid == 0 || spellId == 0)
return;
// Update an existing entry for this exact aura instance.
for (auto &e : g_cache) {
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
// Learn the classification whenever a slot-derived kind arrives —
// independent of caster/timing ownership, and never downgrade a
// known kind back to unknown.
if (kind != KIND_UNKNOWN)
e.kind = kind;
if (!fromCast && e.casterGuid != 0)
return; // SpellGo owns this entry; keep its caster + timing
if (casterGuid != 0)
@@ -108,13 +121,32 @@ void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
const uint32_t now = NowMs();
for (auto &e : g_cache) {
if (!e.used || (e.expirationMs != 0 && now >= e.expirationMs)) {
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, true};
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
kind, true};
return;
}
}
Entry &slot = g_cache[g_writeCursor];
g_writeCursor = (g_writeCursor + 1) % kCacheSize;
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, true};
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, kind,
true};
}
// Evict the entry for an aura the engine reports gone. Keyed by (target,
// spell) like the rest of the cache. Without this, the GetAuraDataByIndex
// fallback would keep surfacing a dropped aura until its computed expiry —
// e.g. a Rank 1 buff replaced by Rank 2 (engine drops Rank 1 from the
// descriptor) would show as a phantom second aura, or a dispelled buff would
// linger.
void Evict(uint64_t targetGuid, uint32_t spellId) {
if (targetGuid == 0 || spellId == 0)
return;
for (auto &e : g_cache) {
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
e.used = false;
return;
}
}
}
// Drop entries whose timed aura has elapsed so the table doesn't fill with
@@ -176,11 +208,13 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
if (numTargets == 0) {
// No explicit hit list (self-cast with caster-implicit target).
Store(caster, spellId, caster, expirationMs, durationMs, true);
Store(caster, spellId, caster, expirationMs, durationMs, true,
KIND_UNKNOWN);
return;
}
for (int i = 0; i < numTargets; ++i)
Store(targets[i], spellId, caster, expirationMs, durationMs, true);
Store(targets[i], spellId, caster, expirationMs, durationMs, true,
KIND_UNKNOWN);
}
const Game::HookAutoRegister _hook{Offsets::FUN_SPELL_GO,
@@ -194,7 +228,7 @@ const Game::HookAutoRegister _hook{Offsets::FUN_SPELL_GO,
// paths, so it stamps timing only with `fromCast=false` — Store skips any
// entry SpellGo already owns, so a directly-cast aura keeps its talented
// timing. Base (unmodified) duration is the best estimate without a caster.
void StampApplication(void *unit, uint32_t spellId) {
void StampApplication(void *unit, uint32_t spellId, int8_t kind) {
if (spellId == 0)
return;
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
@@ -206,7 +240,13 @@ void StampApplication(void *unit, uint32_t spellId) {
const uint32_t durationMs = SpellDurationMs(rec, /*casterIsPlayer*/ false);
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
Store(unitGuid, spellId, /*casterGuid*/ 0, expirationMs, durationMs,
/*fromCast*/ false);
/*fromCast*/ false, kind);
}
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
int8_t KindForSlot(int slot) {
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
}
// OnAuraAdded — a new aura occupies a slot (gives the spellId directly).
@@ -217,7 +257,7 @@ OnAuraAdded_t g_origOnAuraAdded = nullptr;
void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
uint32_t spellId) {
g_origOnAuraAdded(unit, edx, slot, spellId);
StampApplication(unit, spellId);
StampApplication(unit, spellId, KindForSlot(static_cast<int>(slot)));
}
const Game::HookAutoRegister _hookAuraAdded{
@@ -234,8 +274,10 @@ OnAuraStacksChanged_t g_origOnAuraStacksChanged = nullptr;
void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
uint8_t stackCount) {
g_origOnAuraStacksChanged(unit, edx, slot, stackCount);
StampApplication(unit, Aura::Data::ReadSpellID(
static_cast<const uint8_t *>(unit), slot));
StampApplication(
unit,
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot),
KindForSlot(slot));
}
const Game::HookAutoRegister _hookAuraStacks{
@@ -243,6 +285,26 @@ const Game::HookAutoRegister _hookAuraStacks{
reinterpret_cast<void *>(&OnAuraStacksChanged_h),
reinterpret_cast<void **>(&g_origOnAuraStacksChanged)};
// OnAuraRemoved — a descriptor aura slot went empty: the aura fell off, was
// dispelled, or was replaced by a higher rank (the diff dispatcher fires
// Removed(old) + Added(new)). Evict the cache entry so the fallback stops
// surfacing it. Same ABI as OnAuraAdded (unit in ecx, slot + spellId on the
// stack); `slot` is unused here — we evict by spellId.
using OnAuraRemoved_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
uint32_t spellId);
OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
void __fastcall OnAuraRemoved_h(void *unit, void *edx, uint32_t slot,
uint32_t spellId) {
g_origOnAuraRemoved(unit, edx, slot, spellId);
(void)slot;
Evict(Unit::Identity::GuidForObject(unit), spellId);
}
const Game::HookAutoRegister _hookAuraRemoved{
Offsets::FUN_ON_AURA_REMOVED, reinterpret_cast<void *>(&OnAuraRemoved_h),
reinterpret_cast<void **>(&g_origOnAuraRemoved)};
} // namespace
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
@@ -260,4 +322,22 @@ bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
return false;
}
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
if (unitGuid == 0 || out == nullptr || maxOut <= 0)
return 0;
const int8_t want = harmful ? KIND_HARMFUL : KIND_HELPFUL;
const uint32_t now = NowMs();
int n = 0;
for (const auto &e : g_cache) {
if (n >= maxOut)
break;
if (!e.used || e.targetGuid != unitGuid || e.kind != want)
continue;
if (e.expirationMs != 0 && now >= e.expirationMs)
continue; // expired (infinite-duration entries pass)
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs};
}
return n;
}
} // namespace Aura::Source
+20
View File
@@ -42,4 +42,24 @@ namespace Aura::Source {
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
uint32_t *outExpirationMs, uint32_t *outDurationMs);
// One cached aura, as returned by `Enumerate`.
struct CachedAura {
uint32_t spellId;
uint64_t casterGuid; // 0 = caster unknown (application hook, no SpellGo)
uint32_t expirationMs; // 0 = infinite / unknown
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
};
// Fills `out` with up to `maxOut` cached, non-expired auras on `unitGuid`
// whose helpful/harmful classification matches `harmful`. Returns the count
// written. The classification is recorded from the aura's descriptor slot at
// application time (`OnAuraAdded`/`OnAuraStacksChanged`); entries we only ever
// saw via `SMSG_SPELL_GO` have no slot and so an unknown classification —
// those are excluded (we can't tell which range they belong to).
//
// This is the fallback source for `Aura::Data` when the unit descriptor has
// dropped an aura's slot (rogue stealth, party range fluctuation) even though
// the aura is still active server-side: the cache survives descriptor clears.
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut);
} // namespace Aura::Source