@@ -350,10 +350,13 @@ const char *DispelName(uint32_t dispelTypeID) {
350350 return (name == nullptr || *name == ' \0 ' ) ? nullptr : name;
351351}
352352
353- void Push (void *L, const uint8_t *unit, int slot) {
354- const uint32_t spellID = ReadSpellID (unit, slot);
353+ // Writes the full modern AuraData table on top of the Lua stack from
354+ // already-resolved values. Shared by the descriptor path (`Push`) and the
355+ // cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table).
356+ static void BuildTable (void *L, uint32_t spellID, int applications,
357+ bool isHelpful, double duration, double expirationTime,
358+ uint64_t casterGuid) {
355359 const uint8_t *spellRecord = SpellRecord (spellID);
356- const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT ;
357360
358361 uint32_t dispelTypeID = 0 ;
359362 if (spellRecord != nullptr ) {
@@ -370,31 +373,71 @@ void Push(void *L, const uint8_t *unit, int slot) {
370373 Game::Lua::SetFieldString (L, " name" , name);
371374 Game::Lua::SetFieldString (L, " icon" , icon);
372375 Game::Lua::SetFieldNumber (L, " applications" ,
373- static_cast <double >(ReadStacks (unit, slot) ));
376+ static_cast <double >(applications ));
374377 Game::Lua::SetFieldNumber (L, " spellId" , static_cast <double >(spellID));
375378 Game::Lua::SetFieldString (L, " dispelName" , dispel);
376379 Game::Lua::SetFieldBool (L, " isHelpful" , isHelpful);
377380 Game::Lua::SetFieldBool (L, " isHarmful" , !isHelpful);
381+ Game::Lua::SetFieldNumber (L, " duration" , duration);
382+ Game::Lua::SetFieldNumber (L, " expirationTime" , expirationTime);
383+
384+ if (casterGuid != 0 ) {
385+ // `sourceUnit` is a token (nil when the caster maps to no current
386+ // token); `sourceGUID` is the raw "0x..." GUID and is set whenever we
387+ // have a caster — a ClassicAPI extension (not in retail AuraData) that
388+ // survives the caster leaving token range and doubles as a unit token
389+ // under SuperWoW.
390+ char tokenBuf[32 ];
391+ char guidBuf[Guid::STRING_SIZE ];
392+ const char *sourceUnit = Unit::Identity::TokenFromGUID (
393+ casterGuid, tokenBuf, sizeof tokenBuf);
394+ const char *sourceGUID =
395+ Guid::FormatAsString (casterGuid, guidBuf, sizeof guidBuf);
396+ if (sourceUnit != nullptr )
397+ Game::Lua::SetFieldString (L, " sourceUnit" , sourceUnit);
398+ if (sourceGUID != nullptr )
399+ Game::Lua::SetFieldString (L, " sourceGUID" , sourceGUID);
400+ }
401+
402+ Game::Lua::SetFieldNumber (L, " charges" , 0 );
403+ Game::Lua::SetFieldNumber (L, " maxCharges" , 0 );
404+ Game::Lua::SetFieldNumber (L, " timeMod" , 1 );
378405
379- // Timing fields. Vanilla doesn't broadcast cast time / duration
380- // for ANY unit's auras except the local player's — for everyone
381- // else (target, party, raid, mouseover) `expirationTime` stays 0
382- // and addons have to track expiry themselves via aura-add events.
383- // For the player we read the engine's `VAR_PLAYER_BUFF_TABLE`
384- // (same data `GetPlayerBuffTimeLeft` returns), which gives us a
385- // real expiration timestamp.
406+ // Boolean fields whose modern semantics don't apply in vanilla.
407+ Game::Lua::SetFieldBool (L, " isStealable" , false );
408+ Game::Lua::SetFieldBool (L, " isBossAura" , false );
409+ Game::Lua::SetFieldBool (L, " isFromPlayerOrPlayerPet" , false );
410+ Game::Lua::SetFieldBool (L, " isNameplateOnly" , false );
411+ Game::Lua::SetFieldBool (L, " nameplateShowAll" , false );
412+ Game::Lua::SetFieldBool (L, " nameplateShowPersonal" , false );
413+ Game::Lua::SetFieldBool (L, " canApplyAura" , false );
414+ Game::Lua::SetFieldBool (L, " shouldConsolidate" , false );
415+ Game::Lua::SetFieldBool (L, " isRaid" , false );
416+
417+ // `auraInstanceID` and `points` are deliberately omitted — modern returns
418+ // nil for those when they don't apply, and Lua reading a missing key
419+ // yields nil, so the table doesn't need an explicit entry.
420+ }
421+
422+ void Push (void *L, const uint8_t *unit, int slot) {
423+ const uint32_t spellID = ReadSpellID (unit, slot);
424+ const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT ;
425+
426+ // Timing fields. Vanilla doesn't broadcast cast time / duration for ANY
427+ // unit's auras except the local player's — for everyone else (target,
428+ // party, raid, mouseover) `expirationTime` stays 0 unless the
429+ // `Aura::Source` cache observed the cast. For the player we read the
430+ // engine's `VAR_PLAYER_BUFF_TABLE` (same data `GetPlayerBuffTimeLeft`
431+ // returns), which gives a real expiration timestamp.
386432 //
387- // `duration` comes from Spell.dbc → SpellDuration.dbc with
388- // level scaling. That's the *base* value; talent / glyph
389- // duration-extension modifiers (Improved PW:F, etc.) are
390- // already baked into the expiration timestamp on the
391- // caster's side, so `expirationTime - GetTime()` reflects the
392- // true remaining time even when `duration` doesn't include
393- // the talent boost. We populate `duration` for any unit (the
394- // base value is identical regardless of who's affected); only
395- // `expirationTime` is player-gated.
433+ // `duration` comes from Spell.dbc → SpellDuration.dbc with level scaling
434+ // (the *base* value); talent / glyph duration-extension modifiers are
435+ // already baked into the expiration timestamp on the caster's side, so
436+ // `expirationTime - GetTime()` reflects true remaining time even when the
437+ // base `duration` doesn't include the talent boost.
396438 double duration = 0.0 ;
397439 double expirationTime = 0.0 ;
440+ uint64_t casterGuid = 0 ;
398441 const uint8_t *player = LocalPlayer ();
399442 if (spellID != 0 ) {
400443 const int unitLevel = (unit != nullptr ) ? PlayerLevel (unit) : 0 ;
@@ -405,72 +448,133 @@ void Push(void *L, const uint8_t *unit, int slot) {
405448 if (entry != nullptr )
406449 expirationTime = PlayerBuffExpirationSeconds (entry);
407450 }
408-
409- // Caster + non-player expiration from the SMSG_SPELL_GO cache
410- // (`Aura::Source`), captured at cast time. The vanilla descriptor has
411- // neither. `sourceUnit` applies to any unit; `expirationTime` only
412- // fills in here when the player-buff table above didn't — that table is
413- // authoritative for the player's own auras, the cache covers everyone
414- // else. Both are best-effort: a miss (aura predates login, or the cast
415- // wasn't observed) leaves the modern-truthful defaults untouched.
416- const char *sourceUnit = nullptr ;
417- const char *sourceGUID = nullptr ;
418- char tokenBuf[32 ];
419- char guidBuf[Guid::STRING_SIZE ];
451+ // Caster + non-player expiration from the SMSG_SPELL_GO cache, captured at
452+ // cast time (the descriptor has neither). The player-buff table above is
453+ // authoritative for the player's own expiration; the cache fills it for
454+ // everyone else. Prefer the applied (caster-modified) duration so it stays
455+ // consistent with `expirationTime` — otherwise a talented DoT (Improved
456+ // Shadow Word: Pain) would show remaining time exceeding the base.
420457 if (spellID != 0 ) {
421- uint64_t casterGuid = 0 ;
458+ uint64_t c = 0 ;
422459 uint32_t expMs = 0 ;
423460 uint32_t durMs = 0 ;
424- if (Aura::Source::Get (UnitGuid (unit), spellID, &casterGuid, &expMs,
425- &durMs)) {
461+ if (Aura::Source::Get (UnitGuid (unit), spellID, &c, &expMs, &durMs)) {
426462 if (expirationTime == 0.0 && expMs != 0 )
427463 expirationTime = static_cast <double >(expMs) * 0.001 ;
428- // Prefer the applied (caster-modified) duration so `duration`
429- // stays consistent with `expirationTime` — otherwise a talented
430- // DoT (e.g. Improved Shadow Word: Pain) shows remaining time
431- // exceeding the base `duration`.
432464 if (durMs != 0 )
433465 duration = static_cast <double >(durMs) * 0.001 ;
434- if (casterGuid != 0 ) {
435- // `sourceUnit` is a token (nil when the caster maps to no
436- // current token); `sourceGUID` is the raw "0x..." GUID and
437- // is set whenever we have a caster — a ClassicAPI extension
438- // (not in retail AuraData) that survives the caster leaving
439- // token range and doubles as a unit token under SuperWoW.
440- sourceUnit = Unit::Identity::TokenFromGUID (
441- casterGuid, tokenBuf, sizeof tokenBuf);
442- sourceGUID = Guid::FormatAsString (casterGuid, guidBuf,
443- sizeof guidBuf);
444- }
466+ casterGuid = c;
445467 }
446468 }
447469
448- Game::Lua::SetFieldNumber (L, " duration" , duration);
449- Game::Lua::SetFieldNumber (L, " expirationTime" , expirationTime);
450- if (sourceUnit != nullptr )
451- Game::Lua::SetFieldString (L, " sourceUnit" , sourceUnit);
452- if (sourceGUID != nullptr )
453- Game::Lua::SetFieldString (L, " sourceGUID" , sourceGUID);
454- Game::Lua::SetFieldNumber (L, " charges" , 0 );
455- Game::Lua::SetFieldNumber (L, " maxCharges" , 0 );
456- Game::Lua::SetFieldNumber (L, " timeMod" , 1 );
470+ BuildTable (L, spellID, ReadStacks (unit, slot), isHelpful, duration,
471+ expirationTime, casterGuid);
472+ }
457473
458- // Boolean fields whose modern semantics don't apply in vanilla.
459- Game::Lua::SetFieldBool (L, " isStealable" , false );
460- Game::Lua::SetFieldBool (L, " isBossAura" , false );
461- Game::Lua::SetFieldBool (L, " isFromPlayerOrPlayerPet" , false );
462- Game::Lua::SetFieldBool (L, " isNameplateOnly" , false );
463- Game::Lua::SetFieldBool (L, " nameplateShowAll" , false );
464- Game::Lua::SetFieldBool (L, " nameplateShowPersonal" , false );
465- Game::Lua::SetFieldBool (L, " canApplyAura" , false );
466- Game::Lua::SetFieldBool (L, " shouldConsolidate" , false );
467- Game::Lua::SetFieldBool (L, " isRaid" , false );
474+ namespace {
475+
476+ // Builds an AuraData table from an `Aura::Source` cache entry — the fallback
477+ // when the descriptor has dropped the aura's slot. Stack count isn't broadcast
478+ // in SMSG_SPELL_GO, so `applications` defaults to 1. `duration` uses the
479+ // applied (caster-modified) ms when known, else the Spell.dbc base.
480+ void PushFromCache (void *L, const uint8_t *unit,
481+ const Aura::Source::CachedAura &c, bool isHelpful) {
482+ double duration = 0.0 ;
483+ if (c.durationMs != 0 ) {
484+ duration = static_cast <double >(c.durationMs ) * 0.001 ;
485+ } else {
486+ const int unitLevel = (unit != nullptr ) ? PlayerLevel (unit) : 0 ;
487+ duration = SpellBaseDurationSeconds (c.spellId , unitLevel);
488+ }
489+ const double expirationTime =
490+ (c.expirationMs != 0 ) ? static_cast <double >(c.expirationMs ) * 0.001 : 0.0 ;
491+ BuildTable (L, c.spellId , 1 , isHelpful, duration, expirationTime,
492+ c.casterGuid );
493+ }
468494
469- // `sourceUnit` / `sourceGUID` are set above when the `Aura::Source`
470- // cache has a caster for this aura; left unset (→ nil) on a miss.
471- // `auraInstanceID` and `points` are deliberately omitted — modern
472- // returns nil for those when they don't apply, and Lua reading a
473- // missing key yields nil, so the table doesn't need an explicit entry.
495+ // Counts populated descriptor slots matching `filter` (and `playerOnly`).
496+ int CountSlots (const uint8_t *unit, Filter filter, bool playerOnly) {
497+ if (unit == nullptr )
498+ return 0 ;
499+ const int start = (filter == Filter::Harmful)
500+ ? Offsets::UNIT_AURA_BUFF_COUNT
501+ : 0 ;
502+ const int end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
503+ : Offsets::UNIT_AURA_BUFF_COUNT ;
504+ int n = 0 ;
505+ for (int slot = start; slot < end; ++slot) {
506+ if (!IsSlotPopulated (unit, slot))
507+ continue ;
508+ if (playerOnly && !IsPlayerCast (unit, slot))
509+ continue ;
510+ ++n;
511+ }
512+ return n;
513+ }
514+
515+ constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL ;
516+
517+ // Whether cache entry `c` should be surfaced as a fallback for `unit` under
518+ // `filter`/`playerOnly`: it must not already be in a populated descriptor slot
519+ // (no double-listing the same live aura) and, when player-filtered, must have
520+ // been cast by us. Superseded / dispelled auras are kept out of the cache by
521+ // `Aura::Source`'s removal eviction, not filtered here.
522+ bool FallbackEligible (const uint8_t *unit, const Aura::Source::CachedAura &c,
523+ Filter filter, bool playerOnly) {
524+ if (FindSlotBySpellID (unit, c.spellId , &filter, false ) >= 0 )
525+ return false ; // still in the descriptor — returned via the slot path
526+ if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid ())
527+ return false ;
528+ return true ;
529+ }
530+
531+ } // namespace
532+
533+ bool PushNthCacheFallback (void *L, const uint8_t *unit, int oneBasedIndex,
534+ Filter filter, bool playerOnly) {
535+ if (unit == nullptr )
536+ return false ;
537+ // The descriptor held `descCount` matches; the caller already found fewer
538+ // than `oneBasedIndex` there, so the cache supplies index
539+ // `oneBasedIndex - descCount` (1-based) of the entries it didn't.
540+ const int target = oneBasedIndex - CountSlots (unit, filter, playerOnly);
541+ if (target < 1 )
542+ return false ;
543+ const uint64_t guid = UnitGuid (unit);
544+ if (guid == 0 )
545+ return false ;
546+ Aura::Source::CachedAura buf[kFallbackMax ];
547+ const int n = Aura::Source::Enumerate (
548+ guid, filter == Filter::Harmful, buf, kFallbackMax );
549+ int seen = 0 ;
550+ for (int i = 0 ; i < n; ++i) {
551+ if (!FallbackEligible (unit, buf[i], filter, playerOnly))
552+ continue ;
553+ if (++seen == target) {
554+ PushFromCache (L, unit, buf[i], filter == Filter::Helpful);
555+ return true ;
556+ }
557+ }
558+ return false ;
559+ }
560+
561+ void AppendCacheFallbacks (void *L, const uint8_t *unit, Filter filter,
562+ bool playerOnly, int outerIdx, int &nextKey) {
563+ if (unit == nullptr )
564+ return ;
565+ const uint64_t guid = UnitGuid (unit);
566+ if (guid == 0 )
567+ return ;
568+ Aura::Source::CachedAura buf[kFallbackMax ];
569+ const int n = Aura::Source::Enumerate (
570+ guid, filter == Filter::Harmful, buf, kFallbackMax );
571+ for (int i = 0 ; i < n; ++i) {
572+ if (!FallbackEligible (unit, buf[i], filter, playerOnly))
573+ continue ;
574+ Game::Lua::PushNumber (L, static_cast <double >(nextKey++));
575+ PushFromCache (L, unit, buf[i], filter == Filter::Helpful);
576+ Game::Lua::SetTable (L, outerIdx);
577+ }
474578}
475579
476580} // namespace Aura::Data
0 commit comments