Skip to content

Commit b17aeda

Browse files
committed
Add aura caster + non-player expiration to C_UnitAuras
Populate AuraData sourceUnit/sourceGUID and non-player expirationTime by co-hooking SMSG_SPELL_GO (FUN_SPELL_GO) and caching (targetGuid, spellId) -> caster + expiration. The vanilla unit aura array stores only spell IDs; the SpellGo packet is the only place the client sees an aura's caster and a server-authoritative duration. We parse the engine packet ourselves, so this is self-sufficient (no nampower dependency) -- when nampower is loaded we simply co-hook the same site. sourceGUID is a ClassicAPI extension (raw "0x..." string), set whenever a caster is known including when sourceUnit is nil; stable across the session, unlike the volatile nameplate token, and usable as a unit token under SuperWoW. Also consolidate the local-player-GUID read into Unit::Identity::PlayerGuid() (was duplicated inline in four files) and expose Unit::Identity GuidForToken / TokenFromGUID for reuse.
1 parent 23d2fe4 commit b17aeda

11 files changed

Lines changed: 492 additions & 79 deletions

File tree

docs/API.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9271,20 +9271,48 @@ selects.
92719271
| `isHelpful` | boolean | true for slot < 32 |
92729272
| `isHarmful` | boolean | true for slot >= 32 |
92739273
| `duration` | number | base applied duration in seconds, looked up via `Spell.dbc → SpellDuration.dbc` with level scaling. Talent / glyph duration extensions (Improved PW:F, etc.) aren't reflected here — those are baked into `expirationTime` on the caster's side. Returns 0 for spells flagged "no duration" (passives, paladin auras, infinite buffs) |
9274-
| `expirationTime` | number | populated for `unit == "player"` (read from the engine's player-buff table at `0x00BC6040`; same data `GetPlayerBuffTimeLeft` returns). Always `0` for all other units — vanilla server only broadcasts cast/duration for the local player's own auras. `expirationTime - GetTime()` gives the true remaining time (including any talent extensions) |
9274+
| `expirationTime` | number | for `unit == "player"`, read from the engine's player-buff table at `0x00BC6040` (same data `GetPlayerBuffTimeLeft` returns). For any other unit, taken from the `Aura::Source` cache (cast time + duration captured from `SMSG_SPELL_GO`; see below). `0` when neither source has it. `expirationTime - GetTime()` gives the true remaining time |
9275+
| `sourceUnit` | string | unit token of the caster (`"player"`, `"raid7"`, `"nameplate1"`, …), resolved from the `Aura::Source` cache. `nil` if the cast wasn't observed or the caster maps to no current token |
9276+
| `sourceGUID` | string | caster's `"0x…"` GUID string from the same cache. **ClassicAPI extension — not a retail `AuraData` field.** Set whenever a caster is known, including when `sourceUnit` is `nil` (caster left token range). Stable for the session, unlike the volatile nameplate token; doubles as a unit token under SuperWoW. `nil` on a cache miss |
92759277
| `charges` / `maxCharges` | number | always `0` — vanilla has stacks, not charges |
92769278
| `timeMod` | number | always `1` — vanilla has no haste-affected auras |
92779279
| `isStealable`, `isBossAura`, `isFromPlayerOrPlayerPet`, `isNameplateOnly`, `nameplateShowAll`, `nameplateShowPersonal`, `canApplyAura`, `shouldConsolidate`, `isRaid` | boolean | always `false` — modern UI concepts vanilla doesn't have |
9278-
| `sourceUnit`, `auraInstanceID`, `points` | (absent) | omitted from the table — Lua read yields nil, matching modern semantics for "field doesn't apply" |
9280+
| `auraInstanceID`, `points` | (absent) | omitted from the table — Lua read yields nil, matching modern semantics for "field doesn't apply" |
92799281

92809282
### Filter parsing
92819283

92829284
The optional `filter` string is a pipe-separated set of upper-case
92839285
tokens, matching modern syntax (`"HELPFUL"`, `"HARMFUL"`,
92849286
`"HELPFUL|PLAYER"`, etc.). Only `HELPFUL` (default) and `HARMFUL`
92859287
are honored on vanilla; other tokens (`PLAYER` / `RAID` /
9286-
`CANCELABLE` / `INCLUDE_NAME_PLATE_ONLY`) are accepted but no-op —
9287-
they'd need source-GUID tracking or systems vanilla doesn't have.
9288+
`CANCELABLE` / `INCLUDE_NAME_PLATE_ONLY`) are accepted but no-op.
9289+
(`PLAYER` is now technically derivable — `sourceGUID` vs. the player
9290+
GUID — but filtering on it isn't wired in yet; `RAID` / nameplate
9291+
flags still need systems vanilla doesn't have.)
9292+
9293+
### Caster & timing (`Aura::Source`)
9294+
9295+
`sourceUnit`, `sourceGUID`, and non-player `expirationTime` come from a
9296+
client-side cache that vanilla itself can't provide: the unit aura array
9297+
stores only spell IDs — never the caster, and no cast/expiration timing for
9298+
anyone but the local player. The one place the client sees an aura's caster
9299+
+ a server-authoritative duration is the `SMSG_SPELL_GO` packet at cast
9300+
time. `Aura::Source` co-hooks the engine's `SpellGo` handler
9301+
(`FUN_SPELL_GO`, `0x006E7A70`), parses the hit-target list, and caches
9302+
`(targetGuid, spellId) → { casterGuid, expirationMs }`. `Push` then keys
9303+
into it by the unit's GUID + the aura's spell ID.
9304+
9305+
Implications:
9306+
9307+
- **Best-effort.** Only auras applied *after* login (whose cast we
9308+
observed) carry caster/expiration; auras already up when you logged in,
9309+
or applied by mechanisms that don't emit `SMSG_SPELL_GO`, leave the
9310+
defaults (`sourceUnit`/`sourceGUID` nil, non-player `expirationTime` 0).
9311+
- **No nampower dependency.** We parse the engine packet ourselves. nampower
9312+
(which hooks the same function) need not be loaded; when it is, we simply
9313+
co-hook the site.
9314+
- Entries are evicted once their timed aura elapses, so the cache stays
9315+
bounded; infinite-duration auras persist until overwritten under load.
92889316

92899317
### `C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])`
92909318

src/Offsets.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,19 @@ enum Offsets {
682682
OFF_SPELLDURATION_PER_LEVEL_MS = 0x08,
683683
OFF_SPELLDURATION_MAX_MS = 0x0C,
684684

685+
// `SMSG_SPELL_GO` inner handler — `void __fastcall(uint64_t *itemGUID,
686+
// uint64_t *casterGUID, uint32_t spellId, CDataStore *packet)`. The
687+
// engine has already decoded item/caster/spell into args; the packet
688+
// read cursor sits at the post-header body (castFlags, hit/missed
689+
// target lists, target mask). This is the ONLY place the client sees
690+
// an aura's *caster* — the unit aura array stores spell IDs but never
691+
// who applied them. `Aura::Source` co-hooks this (alongside nampower,
692+
// which detours the same function) to cache `(targetGuid, spellId) →
693+
// casterGuid` for `C_UnitAuras` sourceUnit + non-player expiration.
694+
// Packet layout mirrored from nampower's SpellGoHook (verified on the
695+
// same Octo/Turtle 1.12.1 client). Per-cast frequency, not per-frame.
696+
FUN_SPELL_GO = 0x006E7A70,
697+
685698
// CGPlayer-side sub-struct, allocated for any player-controlled
686699
// unit (local self, target, party, raid, inspect targets — all of
687700
// them). Holds player-specific data that's *not* in the broadcast

src/aura/Data.cpp

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
#include "Game.h"
1717
#include "Offsets.h"
18+
#include "aura/Source.h"
1819
#include "dbc/Lookup.h"
20+
#include "guid/Guid.h"
21+
#include "unit/Identity.h"
1922

2023
#include <cstdint>
2124
#include <cstring>
@@ -38,6 +41,20 @@ const uint8_t *Descriptor(const uint8_t *unit) {
3841
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
3942
}
4043

44+
// The unit's 64-bit GUID, read through the pointer at `unit + 0x08` to an
45+
// instance block whose first 8 bytes are the GUID (verified in
46+
// `Script_GetInventoryItemLink`; see `OFF_UNIT_GUID_PTR`). Used to key into
47+
// the `Aura::Source` caster/expiration cache. 0 if unresolved.
48+
uint64_t UnitGuid(const uint8_t *unit) {
49+
if (unit == nullptr)
50+
return 0;
51+
const uint8_t *block = *reinterpret_cast<const uint8_t *const *>(
52+
unit + Offsets::OFF_UNIT_GUID_PTR);
53+
if (block == nullptr)
54+
return 0;
55+
return *reinterpret_cast<const uint64_t *>(block);
56+
}
57+
4158
const uint8_t *SpellRecord(uint32_t spellID) {
4259
return DBC::Record(Offsets::VAR_SPELL_RECORDS,
4360
Offsets::VAR_SPELL_RECORD_COUNT, spellID);
@@ -374,8 +391,44 @@ void Push(void *L, const uint8_t *unit, int slot) {
374391
if (entry != nullptr)
375392
expirationTime = PlayerBuffExpirationSeconds(entry);
376393
}
394+
395+
// Caster + non-player expiration from the SMSG_SPELL_GO cache
396+
// (`Aura::Source`), captured at cast time. The vanilla descriptor has
397+
// neither. `sourceUnit` applies to any unit; `expirationTime` only
398+
// fills in here when the player-buff table above didn't — that table is
399+
// authoritative for the player's own auras, the cache covers everyone
400+
// else. Both are best-effort: a miss (aura predates login, or the cast
401+
// wasn't observed) leaves the modern-truthful defaults untouched.
402+
const char *sourceUnit = nullptr;
403+
const char *sourceGUID = nullptr;
404+
char tokenBuf[32];
405+
char guidBuf[Guid::STRING_SIZE];
406+
if (spellID != 0) {
407+
uint64_t casterGuid = 0;
408+
uint32_t expMs = 0;
409+
if (Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs)) {
410+
if (expirationTime == 0.0 && expMs != 0)
411+
expirationTime = static_cast<double>(expMs) * 0.001;
412+
if (casterGuid != 0) {
413+
// `sourceUnit` is a token (nil when the caster maps to no
414+
// current token); `sourceGUID` is the raw "0x..." GUID and
415+
// is set whenever we have a caster — a ClassicAPI extension
416+
// (not in retail AuraData) that survives the caster leaving
417+
// token range and doubles as a unit token under SuperWoW.
418+
sourceUnit = Unit::Identity::TokenFromGUID(
419+
casterGuid, tokenBuf, sizeof tokenBuf);
420+
sourceGUID = Guid::FormatAsString(casterGuid, guidBuf,
421+
sizeof guidBuf);
422+
}
423+
}
424+
}
425+
377426
Game::Lua::SetFieldNumber(L, "duration", duration);
378427
Game::Lua::SetFieldNumber(L, "expirationTime", expirationTime);
428+
if (sourceUnit != nullptr)
429+
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
430+
if (sourceGUID != nullptr)
431+
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
379432
Game::Lua::SetFieldNumber(L, "charges", 0);
380433
Game::Lua::SetFieldNumber(L, "maxCharges", 0);
381434
Game::Lua::SetFieldNumber(L, "timeMod", 1);
@@ -391,10 +444,11 @@ void Push(void *L, const uint8_t *unit, int slot) {
391444
Game::Lua::SetFieldBool(L, "shouldConsolidate", false);
392445
Game::Lua::SetFieldBool(L, "isRaid", false);
393446

394-
// `sourceUnit`, `auraInstanceID`, `points` deliberately omitted
395-
// — modern returns nil for those when they don't apply, and
396-
// Lua reading a missing key yields nil, so the table doesn't
397-
// need an explicit entry.
447+
// `sourceUnit` / `sourceGUID` are set above when the `Aura::Source`
448+
// cache has a caster for this aura; left unset (→ nil) on a miss.
449+
// `auraInstanceID` and `points` are deliberately omitted — modern
450+
// returns nil for those when they don't apply, and Lua reading a
451+
// missing key yields nil, so the table doesn't need an explicit entry.
398452
}
399453

400454
} // namespace Aura::Data

src/aura/Data.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,18 @@ const char *DispelName(uint32_t dispelTypeID);
8585
//
8686
// Populates these fields with real data:
8787
// name, icon, applications, spellId, dispelName,
88-
// isHelpful, isHarmful, timeMod
88+
// isHelpful, isHarmful, timeMod, duration
89+
//
90+
// And these from the `Aura::Source` SMSG_SPELL_GO cache when available
91+
// (else their inapplicable default):
92+
// expirationTime (player: engine buff table; others: cache; 0 on miss),
93+
// sourceUnit (caster token from the cache; nil on miss),
94+
// sourceGUID (caster "0x..." GUID string; nil on miss — a ClassicAPI
95+
// extension, not in retail AuraData)
8996
//
9097
// And these with vanilla-truthful defaults (matches modern's
9198
// "field present but inapplicable" semantics):
92-
// duration=0, expirationTime=0, charges=0, maxCharges=0,
93-
// sourceUnit=nil, auraInstanceID=nil, points=nil,
99+
// charges=0, maxCharges=0, auraInstanceID=nil, points=nil,
94100
// isStealable=false, isBossAura=false, isFromPlayerOrPlayerPet=false,
95101
// isNameplateOnly=false, nameplateShowAll=false,
96102
// nameplateShowPersonal=false, canApplyAura=false,

0 commit comments

Comments
 (0)