Compare commits

...
1 Commits
Author SHA1 Message Date
Brues b81f672ad7 fix(aura): classify auras by polarity flag, not slot range
The server seats buffs in slots 0..31 and debuffs in 32..47 only while it
can: once the 16 debuff slots are full, _AddSpellAuraHolder parks further
debuffs in free BUFF slots and sets UNIT_FLAG_AURAS_VISIBLE so the client
still renders them (tortoise-wow, verified). Classifying by slot range
therefore reported those as buffs -- absent from HARMFUL, and wrong in
isHelpful / isHarmful. Confirmed live on a 20-aura target: 18 harmful, two
of them sitting in buff slots, where the old code returned 16.

Every visible aura carries its real polarity in the per-slot flag nibble
(SpellAuraHolder::SetAuraFlag writes 0x04 positive / 0x08 negative), so
HELPFUL / HARMFUL now select on that instead. All the walkers -- by index,
by spell ID, by name, GetUnitAuras, GetAuraSlots -- share one order helper,
SlotInFilterOrder, which visits the polarity's home range first and the
other range after, so every index is unchanged whenever nothing has
spilled. Aura::Source's KindForSlot classifies the same way, keeping the
descriptor-drop fallback consistent with the query paths. The out-of-range
group array carries spell IDs with no flags, so that path stays
range-based -- the seating range is the only polarity it has.

GameTooltip:SetUnitAura now bridges the two index spaces. The engine's
SetUnitBuff / SetUnitDebuff index their own slot RANGE, which diverges from
the polarity index space as soon as a debuff spills, so the method resolves
(index, filter) to an absolute slot and hands the engine the index that
slot holds within its own range, picking the method by where the slot
lives. It counts with the engine's TOOLTIP visibility predicate
(FUN_GAMETOOLTIP_AURA_VISIBLE = 0x00534DF0), not the UnitBuff one
(0x00519860): the latter adds a per-player immunity-bitmap check off record
+0x2B0, so the two index spaces are not interchangeable. Both decompiled to
confirm the walks being mirrored.

The aura tooltip builder only ever receives spellId / level / stacks, so a
debuff parked in a buff slot renders identically through SetUnitBuff.

Note this is deliberately MORE than the engine does: stock UnitBuff /
UnitDebuff split by slot range and show a spilled debuff as a buff. Retail's
contract is the aura's actual polarity, and the descriptor already carries
it, so we report that.
2026-09-04 03:58:14 -05:00
7 changed files with 234 additions and 87 deletions
+25 -17
View File
@@ -5484,10 +5484,13 @@ go to a different resolver and won't match).
### `GameTooltip:SetUnitAura(unit, index, [filter])`
Unified-aura method. The engine splits this into `SetUnitBuff` and
`SetUnitDebuff`; we dispatch to the right one based on the `filter`
string (`"HARMFUL"` → `SetUnitDebuff`, anything else → `SetUnitBuff`).
`filter` defaults to helpful when omitted.
Unified-aura method. `index` is in the same `HELPFUL` / `HARMFUL` index
space as `C_UnitAuras.GetAuraDataByIndex`, so an index you got from
`C_UnitAuras` always opens the matching tooltip. That includes a debuff
that the server parked in a buff slot. The tooltip itself comes from the
engine's own `SetUnitBuff` or `SetUnitDebuff`. `filter` defaults to
helpful when omitted. Only its `HELPFUL` or `HARMFUL` token is read, so
pass an index from the same plain list.
```lua
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
@@ -5497,9 +5500,9 @@ GameTooltip:SetUnitAura("player", 1) -- defaults to HELPFUL
GameTooltip:Show()
```
Pure dispatcher — no engine changes; the underlying logic is whatever
the engine's `SetUnitBuff` / `SetUnitDebuff` already does. Just lets you use
the unified call shape (which most aura libraries backport from)
The engine methods index their own slot ranges, which differ from the
`C_UnitAuras` index space once a debuff sits in a buff slot. This method
translates between the two, so you never need to
without conditionally splitting on filter.
### `GameTooltip:SetSpellByID(spellID)`
@@ -15269,10 +15272,14 @@ Backport of the `C_UnitAuras` namespace. Returns
Reads primarily off the unit's `m_objectFields` descriptor — same
data source `UnitBuff` / `UnitDebuff` use. The descriptor has 48 aura
slots total: 32 helpful (buffs) at indices 0..31, 16 harmful
(debuffs) at indices 32..47. Functions in this namespace take a
1-based Lua index that translates onto whichever range the filter
selects.
slots. The server seats buffs in slots 0 to 31 and debuffs in slots 32
to 47 while it can. When the 16 debuff slots are full, it puts further
debuffs into free buff slots. Each slot also carries a flag that records
the aura's real polarity, and `HELPFUL` / `HARMFUL` select on that flag.
So a debuff that sits in a buff slot is still a debuff here. The native
`UnitBuff` / `UnitDebuff` split by slot range and report such a debuff as
a buff. Indices are stable in the normal case: a `HARMFUL` walk visits
slots 32 to 47 first, then any spilled debuffs in 0 to 31.
When a party/raid member has **no live unit object at all** (a
different map, far out of range), there is no descriptor to read — but
@@ -15289,8 +15296,8 @@ these functions surface them, exactly as the built-in `UnitBuff` /
| `applications` | number | stack count (engine stores `stacks-1`, we display `+1`) |
| `spellId` | number | spell ID from the descriptor's aura array |
| `dispelName` | string | `"Magic"` / `"Curse"` / `"Disease"` / `"Poison"` (from `SpellDispelType.dbc`), or `""` if non-dispellable |
| `isHelpful` | boolean | true for slot < 32 |
| `isHarmful` | boolean | true for slot >= 32 |
| `isHelpful` | boolean | true when the slot's polarity flag marks the aura positive |
| `isHarmful` | boolean | true when the slot's polarity flag marks the aura negative. Read from the flag, not the slot number, so a debuff parked in a buff slot reads harmful |
| `duration` | number | applied duration in seconds. When the aura's cast was observed (in the `Aura::Source` cache), this is the caster-modified duration — talent/glyph extensions like Improved Shadow Word: Pain included — so it stays consistent with `expirationTime` (`remaining ≤ duration`). On a cache miss it falls back to the base `Spell.dbc → SpellDuration.dbc` value with level scaling. Returns 0 for spells flagged "no duration" (passives, paladin auras, infinite buffs) |
| `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 |
| `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 |
@@ -15307,8 +15314,8 @@ The optional `filter` string is a pipe-separated set of upper-case
tokens (`"HELPFUL"`, `"HARMFUL"`,
`"HELPFUL|PLAYER"`, etc.). Honored:
- **`HELPFUL`** (default) / **`HARMFUL`** — pick the buff or debuff range.
In `GetUnitAuras`, supplying neither returns both ranges.
- **`HELPFUL`** (default) / **`HARMFUL`** — pick buffs or debuffs by each
aura's polarity flag. In `GetUnitAuras`, supplying neither returns both.
- **`PLAYER`** — restrict to auras the local player cast, via the
`Aura::Source` caster cache (`sourceGUID == ` player GUID). Combines with
the range tokens (`"HARMFUL|PLAYER"` = your debuffs only). Because the
@@ -15414,8 +15421,9 @@ end
separated by `|` or spaces, each token optionally negated with a
leading `!`. This build honors:
- `HELPFUL` / `HARMFUL` — buffs / debuffs. With neither token the
query returns both (the indexed getter defaults to helpful).
- `HELPFUL` / `HARMFUL` — buffs / debuffs, by each aura's polarity flag
rather than its slot number. With neither token the query returns both
(the indexed getter defaults to helpful).
- `PLAYER` / `!PLAYER` — only auras the local player cast, or only
auras the player did not cast. Caster data comes from casts this
session, so an aura present before you saw it cast has no caster and
+19
View File
@@ -1497,6 +1497,16 @@ enum Offsets {
UNIT_AURA_DEBUFF_COUNT = 16, // slot range 32..47 (harmful)
UNIT_AURA_TOTAL = 48,
UNIT_AURA_VISIBLE_MASK = 0x0E, // nibble mask used by the engine's visibility gate
// Per-slot flag nibble bits as the server writes them (tortoise-wow
// SpellAuraHolder::SetAuraFlag, verified): a positive aura gets HELPFUL
// (+ CANCELABLE unless SPELL_ATTR_CANT_CANCEL), a negative aura gets
// HARMFUL. This is the aura's real polarity. The slot range is only where
// the server preferred to seat it — once the 16 debuff slots are full it
// parks further debuffs in 0..31 (and sets UNIT_FLAG_AURAS_VISIBLE so the
// client renders them), so a slot number alone misreads those.
UNIT_AURA_FLAG_CANCELABLE = 0x01,
UNIT_AURA_FLAG_HELPFUL = 0x04,
UNIT_AURA_FLAG_HARMFUL = 0x08,
// PLAYER_FIELD_MOD_DAMAGE_DONE_POS/NEG — the player's per-school spell
// damage bonus. Offsets from the engine's own UpdateField name/index
@@ -1528,6 +1538,15 @@ enum Offsets {
// `+0x2B0`. Both `Script_UnitBuff` and `Script_UnitDebuff` call
// it to filter their aura iteration; we call it the same way.
FUN_SPELL_IS_VISIBLE_AURA = 0x00519860,
// The TOOLTIP variant of the gate: what `Script_GameTooltip_SetUnitBuff`
// (0x00534AC0) / `SetUnitDebuff` (0x00534E30) apply per slot while
// turning a Lua index into a slot. `__fastcall(spellRecord) -> bool`:
// Attributes & 0x80 (HIDDEN_CLIENTSIDE) clear, AttributesEx & 0x10000000
// (NO_AURA_ICON) clear, and no EffectApplyAuraName in {44, 45, 151}
// (the tracking auras). Verified by decompile; differs from the UnitBuff
// gate above, so an index into one space is not an index into the other
// — `GameTooltip:SetUnitAura` counts with THIS one when it translates.
FUN_GAMETOOLTIP_AURA_VISIBLE = 0x00534DF0,
// Player-only aura timing tables. Unlike UNIT_FIELD_AURA (which is
// populated for any unit but has no timing info), these are the
+10 -12
View File
@@ -21,7 +21,9 @@
// tokens separated by `|` and/or whitespace, each optionally negated with a
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
// Honored: `HELPFUL` / `HARMFUL` (descriptor slot ranges), `PLAYER` /
// Honored: `HELPFUL` / `HARMFUL` (the aura's flag-nibble polarity — see
// Data.h; NOT the slot range, which misreads debuffs spilled into buff
// slots), `PLAYER` /
// `!PLAYER` (caster == / != the local player, from the Aura::Source cache),
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
@@ -472,19 +474,15 @@ int __fastcall Script_UnitAuraBySlot(void *L) {
return PushAuraBySlot(L, Data::Emit::Positional);
}
// Iterates one slot range and pushes AuraData tables into `outer` at
// sequential keys starting from `nextKey`. Updates `nextKey` so a
// follow-up call can append to the same outer table.
// Iterates the auras of one polarity (in `SlotInFilterOrder`, the same order
// the by-index getters use) and pushes AuraData tables into `outer` at
// sequential keys starting from `nextKey`. Updates `nextKey` so a follow-up
// call can append to the same outer table.
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
Data::Filter filter, int &nextKey, Data::Match match) {
const int start = (filter == Data::Filter::Harmful)
? Offsets::UNIT_AURA_BUFF_COUNT
: 0;
const int end = (filter == Data::Filter::Harmful)
? Offsets::UNIT_AURA_TOTAL
: Offsets::UNIT_AURA_BUFF_COUNT;
for (int slot = start; slot < end; ++slot) {
if (!Data::SlotMatches(unit, slot, match))
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
const int slot = Data::SlotInFilterOrder(filter, i);
if (!Data::SlotMatchesFilter(unit, slot, filter, match))
continue;
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
Data::Push(L, unit, slot);
+75 -29
View File
@@ -272,6 +272,49 @@ bool IsSlotPopulated(const uint8_t *unit, int slot) {
return IsVisible(SpellRecord(spellID));
}
bool IsSlotHarmful(const uint8_t *unit, int slot) {
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
return false;
auto *desc = Descriptor(unit);
if (desc == nullptr)
return false;
const uint8_t byte = *(desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS + slot / 2);
const uint8_t nibble = (byte >> ((slot & 1) * 4)) & 0xF;
return (nibble & Offsets::UNIT_AURA_FLAG_HARMFUL) != 0;
}
bool IsSlotTooltipVisible(const uint8_t *unit, int slot) {
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
return false;
auto *desc = Descriptor(unit);
if (desc == nullptr)
return false;
const int spellID = *reinterpret_cast<const int *>(
desc + Offsets::OFF_UNIT_FIELD_AURA + slot * 4);
if (spellID <= 0) // the engine's own `0 < id` test (signed)
return false;
const uint8_t byte = *(desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS + slot / 2);
const uint8_t nibble = (byte >> ((slot & 1) * 4)) & 0xF;
if ((nibble & Offsets::UNIT_AURA_VISIBLE_MASK) == 0)
return false;
const uint8_t *rec = SpellRecord(static_cast<uint32_t>(spellID));
if (rec == nullptr)
return false;
using Fn = char(__fastcall *)(const uint8_t *);
auto fn = reinterpret_cast<Fn>(
static_cast<uintptr_t>(Offsets::FUN_GAMETOOLTIP_AURA_VISIBLE));
return fn(rec) != 0;
}
int SlotInFilterOrder(Filter filter, int i) {
// Harmful: 32..47 first, then 0..31 (where the server parks debuffs once
// the 16 are full). Helpful: 0..31 then 32..47 — positive auras never
// spill downward in practice, so that tail is a no-op safety net.
if (filter == Filter::Harmful)
return (i + Offsets::UNIT_AURA_BUFF_COUNT) % Offsets::UNIT_AURA_TOTAL;
return i;
}
bool IsPlayerCast(const uint8_t *unit, int slot) {
const uint32_t spellID = ReadSpellID(unit, slot);
if (spellID == 0)
@@ -342,6 +385,15 @@ bool SlotMatches(const uint8_t *unit, int slot, const Match &match) {
return IsSlotPopulated(unit, slot) && PopulatedSlotMatches(unit, slot, match);
}
bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
const Match &match) {
if (!IsSlotPopulated(unit, slot))
return false;
if (IsSlotHarmful(unit, slot) != (filter == Filter::Harmful))
return false;
return PopulatedSlotMatches(unit, slot, match);
}
// Group-array analog of IsPlayerCast: the member has no descriptor, so there is
// no slot to attribute by and the cache is consulted by (guid, spellID) alone.
// A miss counts as "not the player" (same as IsPlayerCast).
@@ -355,15 +407,10 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
Match match) {
if (unit == nullptr || oneBasedIndex < 1)
return -1;
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 matches = 0;
for (int slot = start; slot < end; ++slot) {
if (!SlotMatches(unit, slot, match))
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
const int slot = SlotInFilterOrder(filter, i);
if (!SlotMatchesFilter(unit, slot, filter, match))
continue;
if (++matches == oneBasedIndex)
return slot;
@@ -375,17 +422,15 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
const Filter *filter, Match match) {
if (unit == nullptr || spellID == 0)
return -1;
const int start = (filter != nullptr && *filter == Filter::Harmful)
? Offsets::UNIT_AURA_BUFF_COUNT
: 0;
const int end = (filter != nullptr && *filter == Filter::Helpful)
? Offsets::UNIT_AURA_BUFF_COUNT
: Offsets::UNIT_AURA_TOTAL;
for (int slot = start; slot < end; ++slot) {
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
const int slot = (filter != nullptr) ? SlotInFilterOrder(*filter, i) : i;
if (!IsSlotPopulated(unit, slot))
continue;
if (ReadSpellID(unit, slot) != spellID)
continue;
if (filter != nullptr &&
IsSlotHarmful(unit, slot) != (*filter == Filter::Harmful))
continue;
if (!PopulatedSlotMatches(unit, slot, match))
continue;
return slot;
@@ -397,18 +442,16 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
const Filter *filter, Match match) {
if (unit == nullptr || spellName == nullptr || *spellName == '\0')
return -1;
const int start = (filter != nullptr && *filter == Filter::Harmful)
? Offsets::UNIT_AURA_BUFF_COUNT
: 0;
const int end = (filter != nullptr && *filter == Filter::Helpful)
? Offsets::UNIT_AURA_BUFF_COUNT
: Offsets::UNIT_AURA_TOTAL;
for (int slot = start; slot < end; ++slot) {
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
const int slot = (filter != nullptr) ? SlotInFilterOrder(*filter, i) : i;
if (!IsSlotPopulated(unit, slot))
continue;
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
if (name == nullptr || std::strcmp(name, spellName) != 0)
continue;
if (filter != nullptr &&
IsSlotHarmful(unit, slot) != (*filter == Filter::Harmful))
continue;
if (!PopulatedSlotMatches(unit, slot, match))
continue;
return slot;
@@ -670,7 +713,7 @@ static Attribution CachedAttribution(const Aura::Source::CachedAura &c) {
void Push(void *L, const uint8_t *unit, int slot, Emit emit) {
const uint32_t spellID = ReadSpellID(unit, slot);
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
const bool isHelpful = !IsSlotHarmful(unit, slot); // nibble, not slot range
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
PushEnriched(L, UnitGuid(unit), spellID, isHelpful, ReadStacks(unit, slot),
unitLevel, unit != nullptr && unit == LocalPlayer(), slot,
@@ -828,8 +871,10 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
namespace {
// Absolute slot range [start, end) for a filter, matching the descriptor
// convention (helpful 0..31, harmful 32..47).
// Absolute slot range [start, end) for a filter in the GROUP ARRAY (helpful
// 0..31, harmful 32..47). The array carries spell IDs only — no flag nibble —
// so the seating range is the only polarity the out-of-range path has. (The
// descriptor path reads the nibble instead; see SlotInFilterOrder.)
void GroupRange(Filter filter, int &start, int &end) {
start = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_BUFF_COUNT : 0;
end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
@@ -1125,11 +1170,10 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
int *out, int cap) {
int n = 0;
int start, end;
GroupRange(filter, start, end); // same [start, end) for the descriptor
if (unit != nullptr) {
for (int slot = start; slot < end && n < cap; ++slot) {
if (SlotMatches(unit, slot, match))
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL && n < cap; ++i) {
const int slot = SlotInFilterOrder(filter, i);
if (SlotMatchesFilter(unit, slot, filter, match))
out[n++] = slot;
}
// The cache fallback yields only while the descriptor shows no visible
@@ -1152,6 +1196,8 @@ int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
if (arr == nullptr)
return 0;
Aura::Source::ObserveGroupAuras(guid, arr);
int start, end;
GroupRange(filter, start, end);
for (int slot = start; slot < end && n < cap; ++slot) {
if (GroupSlotMatches(guid, arr, slot, match))
out[n++] = OPAQUE_GROUP + slot;
+43 -14
View File
@@ -20,19 +20,27 @@
// `unit + OFF_CGUNIT_OBJECT_FIELDS`; the layout is documented in
// `Offsets.h` under `OFF_UNIT_FIELD_AURA*`.
//
// Slot indexing convention used here matches the engine's:
// Slots are absolute 0..47. The server SEATS positive auras in 0..31 and
// negative ones in 32..47, but that is a preference, not a rule: once the 16
// debuff slots are full it parks further debuffs in 0..31 (and sets
// UNIT_FLAG_AURAS_VISIBLE so the client renders them). An aura's real polarity
// is the per-slot flag nibble (UNIT_AURA_FLAG_HELPFUL / _HARMFUL), which the
// server writes for every visible aura — that is what `Filter` selects on
// here. The engine's own UnitBuff/UnitDebuff split by slot range and so report
// a spilled debuff as a buff; that is a 1.12 UI limitation we deliberately do
// not mirror (retail's contract is the aura's actual polarity). A polarity
// walk visits its home range first, then the other range, so indices stay
// exactly what they were in the common no-spill case.
//
// slot 0..31 → buffs (helpful)
// slot 32..47 → debuffs (harmful)
//
// Callers that take a 1-based Lua index into "buffs" or "debuffs"
// translate to the absolute 0..47 slot before calling in.
// Callers that take a 1-based Lua index into "buffs" or "debuffs" translate
// to the absolute 0..47 slot before calling in.
namespace Aura::Data {
// Filter for slot iteration. The engine's `UnitBuff` / `UnitDebuff`
// pick one direction; modern `C_UnitAuras.GetAuraDataByIndex` defaults
// to helpful when no filter is specified.
// Filter for slot iteration: the aura's polarity per its flag nibble (see the
// file header). The engine's `UnitBuff` / `UnitDebuff` pick one direction;
// modern `C_UnitAuras.GetAuraDataByIndex` defaults to helpful when no filter
// is specified.
enum class Filter { Helpful, Harmful };
// How a resolved aura is emitted onto the Lua stack by the single-aura push
@@ -85,11 +93,32 @@ uint32_t ReadSpellID(const uint8_t *unit, int slot);
// to decide whether to surface an aura through Lua.
bool IsSlotPopulated(const uint8_t *unit, int slot);
// Finds the absolute slot of the `oneBasedIndex`-th populated aura
// matching `filter`. Returns -1 if no such aura. Walks
// 0..UNIT_AURA_BUFF_COUNT-1 for Helpful, UNIT_AURA_BUFF_COUNT..
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
// the local player cast (per `IsPlayerCast`) are counted/returned.
// The aura's polarity from its flag nibble: true iff the slot carries
// UNIT_AURA_FLAG_HARMFUL. False for an empty slot or a null unit. This, not
// the slot range, is what `Filter` selects on (see the file header).
bool IsSlotHarmful(const uint8_t *unit, int slot);
// The engine's TOOLTIP visibility gate for a slot: occupied, visible nibble,
// and the spell passes `FUN_GAMETOOLTIP_AURA_VISIBLE` — exactly what
// `SetUnitBuff` / `SetUnitDebuff` count while turning an index into a slot.
// Differs from `IsSlotPopulated` (the UnitBuff gate), so use THIS to compute
// the index the engine's tooltip methods expect for a given slot.
bool IsSlotTooltipVisible(const uint8_t *unit, int slot);
// The i-th slot (0..UNIT_AURA_TOTAL-1) in the order a `filter` walk visits the
// descriptor: the polarity's home range first (helpful 0..31, harmful 32..47),
// then the other range, where spilled debuffs live. Every enumeration that
// hands out indices or slot lists uses this so they agree on order.
int SlotInFilterOrder(Filter filter, int i);
// `SlotMatches` plus the polarity test: populated, of `filter`'s polarity, and
// passing `match`.
bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
const Match &match);
// Finds the absolute slot of the `oneBasedIndex`-th populated aura of
// `filter`'s polarity that passes `match`. Returns -1 if no such aura. Walks
// all 48 slots in `SlotInFilterOrder`.
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
Match match = {});
+9 -5
View File
@@ -1515,10 +1515,14 @@ const Net::PacketDispatch::AutoSubscribe _spellGoSub{&SpellGoSub};
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
// 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;
// Classify by the slot's flag nibble (UNIT_AURA_FLAG_HARMFUL), not its range:
// the server parks debuffs in buff slots once the 16 are full, and the nibble
// is what records their real polarity. The descriptor write pass has already
// landed the flags when the application hooks fire (see Aura::Data).
int8_t KindForSlot(const void *unit, int slot) {
return Aura::Data::IsSlotHarmful(static_cast<const uint8_t *>(unit), slot)
? KIND_HARMFUL
: KIND_HELPFUL;
}
// Stamp expiration for an aura that just landed/refreshed in `slot` on `unit`.
@@ -1574,7 +1578,7 @@ void StampApplication(void *unit, uint32_t spellId, int slot) {
}
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
StoreFromApplication(unitGuid, spellId, caster, expirationMs, durationMs,
slot, KindForSlot(slot));
slot, KindForSlot(unit, slot));
}
// Bump the player-stat-inputs signal when an aura change hits the LOCAL
+53 -10
View File
@@ -13,6 +13,7 @@
#include "Game.h"
#include "Offsets.h"
#include "aura/Data.h"
#include "guid/Guid.h"
#include "object/Resolve.h"
@@ -20,13 +21,38 @@
namespace Unit::Tooltip {
// The 1-based index the engine's `SetUnitBuff` / `SetUnitDebuff` would need to
// land on `slot`: one plus the slots before it in ITS range (0..31 or 32..47)
// that pass the engine's tooltip visibility gate — the exact walk
// `FUN_00534AC0` / `FUN_00534E30` perform (verified by decompile).
static int EngineIndexForSlot(const uint8_t *unit, int slot) {
const int home =
slot < Offsets::UNIT_AURA_BUFF_COUNT ? 0 : Offsets::UNIT_AURA_BUFF_COUNT;
int index = 1;
for (int s = home; s < slot; ++s) {
if (Aura::Data::IsSlotTooltipVisible(unit, s))
++index;
}
return index;
}
// `GameTooltip:SetUnitAura(unit, index, [filter])` — modern unified-aura
// method. 1.12 splits this into `SetUnitBuff` (slot 32) and
// `SetUnitDebuff` (slot 33); we dispatch to the right one based on the
// `filter` string ("HELPFUL" → SetUnitBuff, "HARMFUL" → SetUnitDebuff).
// method. 1.12 has `SetUnitBuff` (slot 32) and `SetUnitDebuff` (slot 33), each
// indexing its own slot RANGE (0..31 / 32..47) under the engine's tooltip
// gate. `C_UnitAuras` indexes by the aura's polarity nibble instead (see
// Aura::Data), so the two index spaces diverge once the server has parked a
// debuff in a buff slot — and an index an addon got from `C_UnitAuras` must
// still open the right tooltip. So: resolve (index, filter) to the absolute
// slot in OUR space, then hand the engine the index THAT slot has within its
// own range under ITS gate, choosing SetUnitBuff or SetUnitDebuff by where the
// slot lives. The aura tooltip builder only ever receives spellId / level /
// stacks, so a debuff in a buff slot renders identically through SetUnitBuff.
//
// `filter` defaults to "HELPFUL" when omitted, matching modern
// behavior. Anything other than a HARMFUL match goes to SetUnitBuff.
// `filter` defaults to "HELPFUL" when omitted, matching modern behavior; only
// its HELPFUL/HARMFUL half is read (a PLAYER-filtered index is not an index
// into the plain list — pass an index from the same plain list, as FrameXML
// does). A unit with no live CGUnit (out-of-range groupmate) passes straight
// through: the engine reads the group array by range there, exactly as we do.
static int __fastcall Script_GameTooltipSetUnitAura(void *L) {
// Args: self (table), unit (string), index (number), filter (optional string)
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
@@ -51,14 +77,31 @@ static int __fastcall Script_GameTooltipSetUnitAura(void *L) {
}
}
// Drop the filter arg before calling — the existing
// SetUnitBuff/SetUnitDebuff take (self, unit, index) only.
Game::Lua::SetTop(L, 3);
int index = static_cast<int>(Game::Lua::ToNumber(L, 3));
bool useDebuffMethod = isHarmful;
const auto *unit = static_cast<const uint8_t *>(
Game::ResolveUnitToken(Game::Lua::ToString(L, 2)));
if (unit != nullptr) {
const int slot = Aura::Data::FindNthSlot(
unit, index,
isHarmful ? Aura::Data::Filter::Harmful : Aura::Data::Filter::Helpful);
if (slot >= 0) {
index = EngineIndexForSlot(unit, slot);
useDebuffMethod = slot >= Offsets::UNIT_AURA_BUFF_COUNT;
}
// slot < 0: nothing at that index in our space; the raw index goes
// through and the engine's own walk shows nothing either.
}
// Rebuild the args as (self, unit, engineIndex) — the engine methods take
// exactly those three; the filter string is consumed here.
Game::Lua::SetTop(L, 2);
Game::Lua::PushNumber(L, static_cast<double>(index));
using Script_t = int(__fastcall *)(void *L);
auto fn = reinterpret_cast<Script_t>(
isHarmful ? Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_DEBUFF
: Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_BUFF);
useDebuffMethod ? Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_DEBUFF
: Offsets::FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_BUFF);
return fn(L);
}