mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
939b52ae69 |
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user