From f7ef99b58a222303eaaa3462bea2cc320183e15a Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Tue, 20 Oct 2026 23:56:14 -0300 Subject: [PATCH 01/13] rebuild plan --- docs/secret-values-options.md | 483 ++++++++++++++++++++++++++++++++++ docs/spell-data-generation.md | 108 ++++++++ 2 files changed, 591 insertions(+) create mode 100644 docs/secret-values-options.md create mode 100644 docs/spell-data-generation.md diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md new file mode 100644 index 0000000..ae26c99 --- /dev/null +++ b/docs/secret-values-options.md @@ -0,0 +1,483 @@ +# Secret values: options for Retail and Classic + +Analysis only. No code was changed. As of 2026-10-19. + +- Sources: `../refs/wow-ui-source` live **12.1.5.70448** (2026-10-16), the `forever` (1.60.1), + `classic` (5.5.4), `classic_titan` (3.80.2), `classic_anniversary` (2.5.6) and `classic_era` (1.15.9) + worktrees, `../refs/ElvUI`, and warcraft.wiki.gg + [Secret values](https://warcraft.wiki.gg/wiki/Secret_values) (last edited 2026-09-23, change log up to + 12.1.5). +- Unless stated otherwise, paths are relative to `../refs/wow-ui-source/Interface/AddOns/`, and + `APIDoc/` means `Blizzard_APIDocumentationGenerated/`. +- This builds on [cooldown-manager.md](cooldown-manager.md) (CDM, AuraContainer, ElvUI) and doesn't repeat + it. That doc was written against 12.1.0. Client facts are in [compatibility.md](compatibility.md). +- Evidence tags: **VERIFIED** means read in Blizzard source or API docs (path given). **UNVERIFIED** means + an assumption that needs an in-game check (see [Experiments](#experiments)). + +## Summary + +1. On Retail and Classic Forever, phase 2 can't work in combat or in encounters. Phase 2 lists every aura + and then decides in Lua by spellId, name, caster, dispel type, stacks or expiration. All of those fields + are secret, and secrets can't be compared, used as table keys, counted with `#` or used in arithmetic. +2. Four things still work. Server-side filter strings (`HARMFUL|PLAYER`, `!` negation, and the new + `BIG_DEFENSIVE`, `EXTERNAL_DEFENSIVE`, `CROWD_CONTROL`, `IMPORTANT`, `RAID_PLAYER_DISPELLABLE`, + `DISPELLABLE`). Blizzard's secure `CustomAuraContainerTemplate` (filters, spell-ID include/exclude where + allowed, expiration sort). Never-secret spells queried one at a time. Duration objects and curves, for + display only. +3. Retail blocks spell-ID filtering of **debuffs on the player or a friendly unit** by design. An include + list then shows nothing, and an exclude list is ignored. A better boss-debuff table can't help on Retail; + it only helps Classic. +4. Decision: **two aura implementations, loaded per client family** with `[Family]` in the TOC. On + Mainline (Retail and Forever), use AuraContainer only (no `UNIT_AURA` path): filter strings and flags, + plus curated `includeSpellIDs`/`excludeSpellIDs` where ID filtering is allowed. On Classic, keep + today's implementation unchanged. Buttons, layout, `data/` and cooldowns stay shared. +5. These features don't survive on Retail: stack thresholds, numeric priority sort, the blacklist on + debuff frames, filtering by name, the `sourceUnit == nil` checks, LibDispel's flash, and Filger's own + timer text. +6. Classic: same branch and same TOC. Only its aura file differs, and that file is today's code. Don't + freeze a branch. + +## Progress + +Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments](#experiments). + +- [ ] **Phase 0: fixes that work today** + - [ ] Experiment 1: restriction state on every client + - [ ] Experiment 2: `GetAuraDuration` and `#` on secret instance IDs (Retail, in combat) + - [ ] Experiment 3: never-secret coverage of `data/`, Stagger + - [ ] Read `cdInfo.isEnabled` (`Filger.lua:648`) + - [ ] Refresh only the event's `spellID` in `SPELL_UPDATE_COOLDOWN` (nil → all; override/base IDs; + items and slots still refreshed) + - [ ] Secret auras: `GetAuraDuration` + `SetCooldownFromDurationObject`, no `duration > 0` compare + (needs Experiment 2) +- [ ] **Phase 1: family split, no behavior change** + - [ ] Move `aura_proto` and aura frame definitions to `core/Classic/`; add `core\[Family]\...` to the TOC + - [ ] `core/Mainline/` starts as a copy + - [ ] Experiment 9: `[Family]` resolves on every client, Forever included +- [ ] **Phase 2: Mainline container for PLAYER_AURAS and PLAYER_BUFFS** + - [ ] Experiment 4: container test addon + - [ ] Experiment 5: filter strings on Retail and each Classic client + - [ ] PLAYER_AURAS: `HELPFUL|PLAYER` + `includeSpellIDs` + - [ ] PLAYER_BUFFS: `HELPFUL|!PLAYER` + `excludeSpellIDs` +- [ ] **Phase 3: remaining Mainline frames** + - [ ] Experiment 6: private aura anchors + - [ ] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS + - [ ] BOSS_DEBUFFS (`isBossAura`, private-aura anchors if Experiment 6 passes) + - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss, Stagger groups + - [ ] Delete the copied event path from `core/Mainline/` +- [ ] **Other experiments** + - [ ] Experiment 7: cooldown edges (items, slots, secret cast `spellID`) + - [ ] Experiment 8: tooltip methods on each Classic client +- [ ] **Later iteration:** spell data ([spell-data-generation.md](spell-data-generation.md)) + +## Facts + +### Where Filger reads aura and cooldown data and decides on it + +| Location | APIs | Data read → decision | Client path | +|---|---|---|---| +| `Filger.lua:32-54` `OnEnter` | `GameTooltip:SetUnitAura`, `SetUnit{Buff,Debuff}ByAuraInstanceID`, `SetSpellByID` | `auraIndex` / `auraInstanceID` → tooltip | Index on Vanilla, instance ID elsewhere | +| `Filger.lua:72-92` `OnUpdate` | `GetTime` | `expirationTime - GetTime()`, `<= 5` → red text, hide on expiry | All | +| `Filger.lua:184-208` `UpdateColor` | `UnitCanAssist`, `UnitCanAttack` | `isHarmful/isHelpful`, `Filger.colors[dispelName]` (table key), `isPlayerAura` → desaturate, `isDispelable/isStealable` → flash | All | +| `Filger.lua:218-272` `Update` | `Cooldown:SetCooldown`, `SetTexture`, `SetText` | `name` check, `duration > 0`, `expirationTime - duration`, `applications > 1` | All | +| `Filger.lua:281-295` `SortAuras` | — | Compares `priority`, `expirationTime`, `auraInstanceID` | All | +| `Filger.lua:308-310` `FilterAura` | — | `applications >= stackThreshold` | All | +| `Filger.lua:312-324` `GetAuraIndex` | `C_UnitAuras.GetAuraDataByIndex` | `auraInstanceID ==` → tooltip index | Vanilla | +| `Filger.lua:337-370` `ProcessData` | `UnitIsUnit`, `UnitIsOwnerOrControllerOfUnit`, LibDispel | `sourceUnit` (or `isFromPlayerOrPlayerPet` when restricted), `spellId`/`dispelName` → LibDispel, `self.spells[spellId]`, `blacklist[spellId]`, `blacklist[name]` | All | +| `Filger.lua:374-391` `ScanSpellAuras` | `C_Secrets.ShouldSpellAuraBeSecret`, `C_UnitAuras.GetUnitAuraBySpellID` | `isHarmful ==`, `isFromPlayerOrPlayerPet` | Retail/Forever while restricted | +| `Filger.lua:393-498` `UpdateAuras` | `C_Secrets.ShouldAurasBeSecret`, `GetAuraSlots`, `GetAuraDataBySlot`, `GetAuraDataByAuraInstanceID`, `IsAuraFilteredOutByInstanceID`, `UNIT_AURA` | `updateInfo` fields, `self.all[auraInstanceID]` keys | All (restricted branch on Retail/Forever) | +| `Filger.lua:601-613` `FilterCooldown` | — | `enabled`, `isKnown`, `casted`, `duration > 1.5`, `start > 0` | All | +| `Filger.lua:620-625` `SortCooldowns` | — | Compares `expirationTime` | All | +| `Filger.lua:627-683` `ProcessCooldownData` | `C_Secrets.ShouldSpellCooldownBeSecret`, `Filger.GetSpellCooldown`, `C_Spell.GetSpellCooldownDuration`, `GetInventoryItemCooldown`, `C_Container.GetItemCooldown`, `C_SpellBook.IsSpellKnown` | `isActive and not isOnGCD` → duration object; `start/duration` otherwise; reads `cdInfo.enabled`, but the field is `isEnabled` (bug, see `core/utils.lua:61-72`) | Secret branch on Retail/Forever | +| `Filger.lua:749-752` `UNIT_SPELLCAST_SUCCEEDED` | `issecretvalue` | `casted[spellId]` key, skipped when secret | All | +| `config.lua:92-94` PLAYER_BUFFS | — | `not isPlayerAura` | All | +| `config.lua:108-128` TARGET_DEBUFFS | `UnitCanAttack` | `isPlayerAura`; sort on `priority`, `expirationTime`, `isPlayerAura` | All | +| `config.lua:155-157` FOCUS_DEBUFFS | — | `isBossAura`, `sourceUnit == nil`, `isFromPlayerOrPlayerPet` | All | +| `config.lua:170-172` BOSS_DEBUFFS (disabled) | — | `isBossAura`, `sourceUnit == nil` | All | +| `core/data.lua:58-111` | `Filger.GetSpellInfo` | Validates the data tables at load, out of combat; safe | All | + +### Secret-value rules (Retail 12.1.5) + +**Conditions** + +- `SecretWhenUnitAuraRestricted`: secret "when combat, encounter, challenge mode, or PvP match addon + restrictions are in effect. Individual spells may be flagged as never or always secret, which takes + priority". `SecretWhenCooldownsRestricted` has the same wording. `SecretWhenAurasRestricted` (used by + `UNIT_AURA`) has no per-spell override. **VERIFIED** `APIDoc/SecretPredicatesDocumentation.lua`. +- Restrictions depend on combat/encounter/M+/PvP state, not on the unit type. The cast payload is the + exception: `SecretWhenUnitSpellCastRestricted` is secret "if the unit … is not the player or their pet". + **VERIFIED** (same file). +- The wiki says dungeons and raids are restricted maps even out of combat (`SecretOnRestrictedMaps`). None + of the aura or cooldown APIs Filger calls carry that predicate. **VERIFIED** (grep of + `UnitAuraDocumentation.lua`, `SpellDocumentation.lua`). + +**What a secret allows** (wiki, **VERIFIED**) + +- Not allowed in tainted code: arithmetic, comparison, use as a table key, indexing, `#`, calling. Any of + these raises an immediate Lua error. +- Allowed: storing a secret in a variable or table, passing it to functions, concatenating it, and testing + it as a boolean when it isn't a boolean (the type isn't secret). + +**Aura APIs** (`APIDoc/UnitAuraDocumentation.lua`, **VERIFIED**) + +| API | Flag | Meaning for Filger | +|---|---|---| +| `GetAuraDataBySlot`, `GetAuraDataByIndex`, `GetAuraDataByAuraInstanceID`, `GetBuffDataByIndex`, `GetDebuffDataByIndex` | `SecretWhenUnitAuraRestricted` | Every field is secret while restricted (except never-secret spells) | +| `GetUnitAuras` | `ConditionalSecretContents` | Same; one can be sorted server-side (`sortRule`) | +| `GetUnitAuraBySpellID`, `GetPlayerAuraBySpellID`, `GetAuraDataBySpellName` | `RequiresNonSecretAura` | Full readable data for never-secret spells; **nothing** (no error) otherwise | +| `GetAuraSlots`, `GetUnitAuraInstanceIDs`, `IsAuraFilteredOutByInstanceID`, `GetAuraDuration` | No `SecretWhen…`; `SecretArguments = "AllowedWhenUntainted"` | The calls themselves aren't secret, but they take an instance ID, which is secret (next row) | +| `auraInstanceID` | Secret: "Aura instance IDs are secret" (`Blizzard_CooldownViewer/CooldownViewerSecure.lua:4`); `UNIT_AURA_BLOCKED` `auraInstanceID SecretValue = true` | It can't be a table key, which breaks `self.all[auraInstanceID]` | +| `GetAuraApplicationDisplayCount`, `GetAuraDispelTypeColor(curve)`, `GetAuraBaseDuration`, `GetAuraCasterGUID` | `SecretWhenUnitAuraRestricted` | Usable for display only (`SetText`, `SetVertexColor`) | +| `UNIT_AURA` | `SecretWhenAurasRestricted` | `updateInfo` is secret, so incremental updates are impossible; Filger already falls back (`Filger.lua:396-398`) | +| `AuraIsBigDefensive(spellID)`, `AuraIsPrivate(spellID)` | `AllowedWhenTainted` | Accept a secret spellID; whether they return a secret is **UNVERIFIED** | + +- Whether a tainted caller can pass a **secret** instance ID into the `AllowedWhenUntainted` functions is + **UNVERIFIED**. The flag suggests it can't. But ElvUI calls `GetAuraDuration(button.unit, + button.auraInstanceID)` (`../refs/ElvUI/ElvUI/Game/Shared/Modules/Auras/Auras.lua:388`), probably with IDs + handed over by the container. This is Experiment 2. + +**Secrecy predictions** (`APIDoc/SecretPredicateAPIDocumentation.lua`, +`APIDoc/SecretWrapperConstantsDocumentation.lua`, **VERIFIED**) + +- They return plain booleans: `C_Secrets.HasSecretRestrictions`, `ShouldAurasBeSecret`, + `ShouldCooldownsBeSecret`, `ShouldSpellAuraBeSecret(spellID)`, `ShouldSpellCooldownBeSecret(spellID)`, + `ShouldUnitAuraIndexBeSecret`, `ShouldTotemSlotBeSecret`. +- `GetSpellAuraSecrecy`, `GetSpellCooldownSecrecy` and `GetSpellCastSecrecy` return `Enum.SecrecyLevel` + (`NeverSecret`, `AlwaysSecret`, `ContextuallySecret`). This is the only "whitelist" there is, and it is + per spell, set by Blizzard, and not published as a list. Which spells are `NeverSecret` is **UNVERIFIED** + (Experiment 3). The source mentions Exhaustion and Sated as examples + (`Blizzard_AuraContainer/Blizzard_AuraContainerUtil.lua:61-63`). + +**Cooldowns** (**VERIFIED**) + +- `C_Spell.GetSpellCooldown` is `SecretWhenCooldownsRestricted`. In `SpellCooldownInfo`, `isEnabled`, + `isActive` and `isOnGCD` are `NeverSecret`; `startTime`, `duration` and `modRate` aren't + (`APIDoc/SpellSharedDocumentation.lua:19-31`). +- `C_Spell.GetSpellCooldownDuration` returns a duration object. See cooldown-manager.md §4 for charges and + cast counts. +- `UNIT_SPELLCAST_SUCCEEDED` `spellID` is secret for non-player/pet units and for flagged spells + (`APIDoc/UnitDocumentation.lua:4699-4709`). +- Item and slot cooldowns have no secret flag in the docs. Behavior in game is **UNVERIFIED**. + +**What can still be done with a secret** (display only) + +- Duration objects: only `Copy` and `HasSecretValues` are `ReturnsNeverSecret`. `GetRemainingDuration`, + `HasExpired`, `IsActive` and `Evaluate*(curve)` all follow the underlying data, so they are secret when it + is (`APIDoc/LuaDurationObjectAPIDocumentation.lua`, **VERIFIED**). +- Sinks for duration objects: `Cooldown:SetCooldownFromDurationObject`, `StatusBar:SetTimerDuration`, + `C_DurationUtil.CreateDurationTextBinding`. +- `C_CurveUtil.CreateCurve` and `CreateColorCurve` map a secret onto alpha, color or desaturation + (`APIDoc/CurveUtilDocumentation.lua`, `DurationUtilDocumentation.lua`, **VERIFIED**). +- Setters that accept secrets from addons: `SetText`, `SetTexture`, `SetVertexColor`, `SetAlpha`, + `SetDesaturated`, `StatusBar:SetValue` (cooldown-manager.md §4, **VERIFIED** there). +- `Cooldown:SetCooldown` **rejects** secrets, so `Filger.lua:245` errors on a secret aura. The cooldown + path (`Filger.lua:564-566`) already avoids it. + +**Server-side filters** (**VERIFIED**) + +- Filter strings, from `Blizzard_FrameXMLUtil/AuraUtil.lua:270-288`: `HELPFUL`, `HARMFUL`, `PLAYER`, + `RAID`, `CANCELABLE`, `INCLUDE_NAME_PLATE_ONLY`, `MAW`, `EXTERNAL_DEFENSIVE`, `CROWD_CONTROL`, + `RAID_IN_COMBAT`, `RAID_PLAYER_DISPELLABLE`, `BIG_DEFENSIVE`, `IMPORTANT`, `DISPELLABLE`. + - A leading `!` negates a component (`!PLAYER`). `NOT_CANCELABLE` was removed; use `!CANCELABLE`. The + `config.lua:21` comment is outdated on Retail. + - The server applies them, so they work on secret auras. +- Server sort: `Enum.UnitAuraSortRule` `Default`, `BigDefensive`, `Expiration`, `ExpirationOnly`, `Name`, + `NameOnly` (`APIDoc/UnitAuraSharedDocumentation.lua`). +- `CustomAuraContainerTemplate` candidate filters, checked in secure code + (`Blizzard_AuraContainer/Blizzard_AuraContainerUtil.lua:89-170`): + - Spell and dispel lists: `includeSpellIDs`, `excludeSpellIDs`, `includeDispelTypes`, + `excludeDispelTypes`. + - Source and type flags: `canApplyAura`, `isBossAura`, `isBossOrRoleAura`, `isFromPlayerOrPlayerPet`, + `isRoleAura`, `isPriorityAura`, `isStealable`. + - Other: `maxDuration`, `nameplateShowAll`, `nameplateShowPersonal`, `processedAuraType`. +- Spell-ID filters are gated by `CanApplyIdentityCandidateFilters` + (`Blizzard_AuraContainerUtil.lua:52-87`): + +| Aura | ID filter allowed? | If not allowed | +|---|---|---| +| Spell is `NeverSecret` | Always | — | +| Helpful, on player / group member / their pet | Yes | — | +| Harmful, on a unit the player can assist (player, friendly target) | **No** | `includeSpellIDs` → aura **rejected**; `excludeSpellIDs` → ignored, aura **passes** (`:106-111`) | +| Helpful, on a unit the player can't assist (hostile target) | **No** | Same | +| Harmful on hostile; helpful on friendly | Yes | — | + + The source says this restriction is deliberate: "it shouldn't be possible to filter out debuffs on + player targets … if encounter-specific debuffs are applied to the player". cooldown-manager.md §6 says + every aura passes when the gate refuses. That is only true for exclude lists. + +**Other exposed systems** + +- Aura containers: `Blizzard_AuraContainer` frames are `DenyTaintedAccessWhenAurasAreSecret` + (`Blizzard_AuraContainerShared.lua:108`). The buttons offer `AddDispelTypeTexture` + (`Blizzard_CustomAuraButton.lua:112`) and `SetDurationText` (`:178`). **VERIFIED**. Whether Filger can + create, anchor and style them without taint is **UNVERIFIED** (ElvUI does). +- Cooldown Manager: addons can read the static catalog; the live state is secret. See cooldown-manager.md + §4–§8. +- Private auras (many boss mechanics) are never returned by `C_UnitAuras`. Blizzard shows them through + `C_UnitAuras.AddPrivateAuraAnchor(args)`. Whether a third-party addon may call it in a restricted context + is **UNVERIFIED**. + +### Classic clients + +| Client | Secret flags on aura APIs | Modern aura/duration API present | CDM loads | AuraContainer | Filter strings | +|---|---|---|---|---|---| +| Retail 12.1.5 (`standard`) | Yes (15 functions) | Yes | Yes | Yes | Full list incl. `IMPORTANT`, `DISPELLABLE`, `!` | +| Forever 1.60.1 (`camelot`) | Yes (15) | Yes | Yes (`standard, camelot`) | Yes | Same as Retail (**UNVERIFIED** line by line) | +| MoP 5.5.4 | 0 | Yes | No (`standard` only) | No | Up to `BIG_DEFENSIVE`; keeps `NOT_CANCELABLE`; no `IMPORTANT`/`DISPELLABLE` | +| Titan WotLK 3.80.2 | 0 | Yes | No | No | Same as MoP (**VERIFIED** for MoP and Era; Titan/TBC assumed identical, **UNVERIFIED**) | +| TBC 2.5.6 | 0 | Yes | No | No | See above | +| Era 1.15.9 | 0 | Yes | No | No | Same as MoP | +| Cata | No worktree | **UNVERIFIED** | — | — | — | + +- **VERIFIED** by grep in each worktree: the `classic*` API docs list the same functions as Retail. + - Auras: `C_UnitAuras.GetAuraSlots`, `GetAuraDataBySlot`, `GetUnitAuraInstanceIDs`, `GetAuraDuration`, + `GetUnitAuraBySpellID`. + - Secrets and display: `C_Secrets.*`, `SetCooldownFromDurationObject`, `GetSpellCooldownDuration`, + `CreateColorCurve`. + - What differs is that the secret flags are missing and the Blizzard addons that load. +- Whether the Classic servers fill the new flags (`BIG_DEFENSIVE`, `EXTERNAL_DEFENSIVE`, …) is + **UNVERIFIED**. +- What Filger must branch on: + - Secret mode: `C_Secrets.HasSecretRestrictions()`, which exists everywhere and is documented to be false + when the build has no restrictions. + - Whether `CustomAuraContainerTemplate` exists. + - The Vanilla tooltip by index (`Filger.lua:37-39`, `365-367`). + - The filter-string vocabulary (`!CANCELABLE` vs `NOT_CANCELABLE`, the Retail-only `IMPORTANT` and + `DISPELLABLE`). + - Whether `GameTooltip:SetUnit{Buff,Debuff}ByAuraInstanceID` exists. It's a widget method that isn't in + the API docs; already guarded at `Filger.lua:40`; **UNVERIFIED** per client. + +## Options + +Each option is a different design, not a variant of another one. Every option keeps cooldowns list-driven +(spell IDs are Filger's own and are queried directly), plus `isActive`/`isOnGCD` and duration objects. + +### A. Secret-safe filters: only what the server can decide + +- **What:** each aura frame becomes an AuraContainer built from filter strings and boolean candidate + filters. Nothing reads aura fields in Lua. Proposed mapping (Filger frame → server filter): + - PLAYER_DEBUFFS → `HARMFUL` (all debuffs; Stagger only if never-secret) + - PLAYER_AURAS → `HELPFUL|PLAYER` + `includeSpellIDs` (allowed) + - PLAYER_BUFFS → `HELPFUL|!PLAYER` + `excludeSpellIDs` (blacklist allowed) + - TARGET_DEBUFFS hostile → `HARMFUL|PLAYER`; friendly → `HARMFUL|!PLAYER` (no ID filters) + - TARGET_AURAS → `HELPFUL`; on hostile units, `isStealable` or `IMPORTANT` + - FOCUS_DEBUFFS / BOSS_DEBUFFS → `HARMFUL` + `isBossAura = true` +- **Avoids secrets:** the server and Blizzard secure code do every comparison. +- **Works on:** Retail and Forever. Classic doesn't need it (no container there). +- **Screen:** cleaner than plain phase 2 on player buffs and own DoTs. More cluttered on player debuffs and + hostile-target buffs. +- **Lost:** stack thresholds, priority, the blacklist on debuff frames, name filters, `sourceUnit == nil`, + the LibDispel flash, Filger's own timer text. +- **Maintenance:** low. The filter strings live in `config.lua`. +- **Effort:** M. **Risk:** medium. The container is new in 12.1, and its API and the identity gate can + change. +- **Verify out of combat:** the container builds and the filter strings return the expected auras on a + dummy (Experiments 4, 5). The identity gate only bites in combat (Experiment 4). +- **Evidence:** filters and the gate are VERIFIED. That Filger can use the container is UNVERIFIED. + +### B. Curated lists plus coarse filters (all inside the container) + +- **What:** A, plus a small exact list where ID filtering is allowed. Both live in container groups. On + Mainline, B doesn't mix the container with a `UNIT_AURA` path: in combat that path sees only secrets, + and out of combat the container already works. + - Each class's defensives and procs on the player: `includeSpellIDs`, sorted by `Expiration`. + - The player's own debuffs on a hostile target. + - Priority tiers become separate aura groups ordered by `layout.layoutIndex` (UNVERIFIED that the order + holds). +- **Avoids secrets:** the list is handed to the container; Filger never compares it. +- **Works on:** both. On Classic the same `data/` list feeds today's Lua filter, priorities and stack + thresholds included. +- **Screen:** close to phase 1 on the player and target frames. Phase-2 style on the debuff frames. +- **Lost:** as in A, but the curated frames keep exactness. +- **Maintenance:** a handful of spells per class, and no boss-debuff list on Retail. +- **Effort:** M. **Risk:** the same as A. +- **Verify:** Experiments 3 and 4. +- **Evidence:** VERIFIED design; usability is UNVERIFIED. + +### C, D. Spell data + +Moved to [spell-data-generation.md](spell-data-generation.md) (learn mode, generated data). A later +iteration, after Filger works on every game version. Until then `data/` stays unchanged. + +### E. Lean on Blizzard's frames + +- **What:** skin the CDM viewers and anchor Filger next to them. Show boss mechanics through private-aura + anchors. Blizzard's buff frames are the fallback for everything else. +- **Avoids secrets:** Blizzard does all the logic. +- **Works on:** Retail and Forever. +- **Cost:** the tracked set lives in `/cdm` (per character, outside `data/`), which goes against "decisions + in code". Edit Mode owns position. There's no focus support, and the CDM shows only auras the player + cast. +- **Effort:** S (skin). **Risk:** high coupling to Blizzard internals. +- **Evidence:** cooldown-manager.md §7–§10 (VERIFIED). Private-aura anchors from addons are UNVERIFIED. + +### F. Architecture: split vs one path + +- **F1 (chosen): split the aura implementation per client family** with the TOC `[Family]` token. The + families match the secret boundary exactly: + + | `[Family]` folder | Clients | Secrets | Container | Aura implementation | + |---|---|---|---|---| + | `Mainline` | Retail (`standard`), Forever (`camelot`) | yes | yes | AuraContainer only | + | `Classic` | Era, TBC, Titan, MoP (and Cata) | no | no | Today's `UNIT_AURA` + `GetAuraSlots` + Lua filters | + + - `[Game]` would need identical copies in `Standard/` and `Camelot/`, and again in every Classic folder. + `[Family]` is too coarse for spell data (compatibility.md), but it is the right grain for the + implementation. Blizzard uses the same pattern (`Blizzard_AuraContainer.toc`: + `[Family]\Blizzard_AuraButtonTooltip.xml`, folders `Classic/` and `Mainline/`, **VERIFIED**). + - For: + - No runtime client branches. + - The Classic file is today's code, so there's no regression risk there. + - The Mainline file can drop `ScanSpellAuras`, `self.restricted` and the `issecretvalue` guards in the + aura path. + - Against: + - The look must be kept identical across two files. + - A client that changes family behavior later (Classic gaining secrets) means moving a file instead of + flipping a check. +- **F2 (rejected): one file**, with `Spawn` choosing the source via + `C_Secrets.HasSecretRestrictions() and CustomAuraContainerTemplate`. Once Mainline is container-only, + the two paths share no aura logic, so the branch would only select between two unrelated + implementations inside one file. +- **What's split and what's shared:** + + | Per family (`core\[Family]\...`) | Shared (untagged) | + |---|---| + | Aura element: container groups vs today's `aura_proto` | `Filger:CreateButton` / button styling | + | Aura frame definitions: filter strings and groups vs `FilterAura`/`SortAuras` closures | Layout (`SetPosition`), `Spawn` skeleton | + | Aura tooltips | `data/`, colors, fonts, commands, `config.lua` general settings | + | | Cooldowns: the secret branch is per spell via `ShouldSpellCooldownBeSecret`, non-secret on Classic | + +- TOC sketch (file names illustrative): + + ```text + core\init.xml + config.lua + core\[Family]\auras.lua + Filger.lua + ``` + + Every family folder must contain every listed file, as with `[Game]`. +- Guard: the Mainline file should warn once at load if `CustomAuraContainerTemplate` is missing, instead + of silently showing nothing. + +### G. Drop what can't survive + +- **What:** in secret mode, hide the frames that depend on "show everything except" logic + (PLAYER_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS), or let them fall back to raw filter strings. Keep player + auras, own DoTs and cooldowns. +- **Works on:** Retail and Forever. Classic is unchanged. +- **Screen:** cleaner but less informative. Debuffs on the player must come from Blizzard's frames. +- **Effort:** S. **Risk:** low. +- **Evidence:** VERIFIED. + +## Comparison + +| Option | Clients | Screen | Lost vs phase 1 / phase 2 | Maintenance | Effort | Risk | Evidence | +|---|---|---|---|---|---|---|---| +| A. Secret-safe filters | Retail, Forever | Mixed: clean on own buffs/DoTs, cluttered on debuffs | Stacks, priority, debuff blacklist, name filters, dispel flash, own timer | Low | M | Medium (new container) | Rules VERIFIED; use UNVERIFIED | +| B. Curated lists + coarse filters | All (Classic via current logic) | Near phase 1 on player/target; phase 2 on debuffs | As A, minus exactness on curated frames | Low–medium (short lists) | M | Medium | As A | +| E. Blizzard frames | Retail, Forever | Blizzard's look | Units, data in code, position | Lowest code, user-side `/cdm` | S | High coupling | Mostly VERIFIED | +| F1. Family split (chosen) | All | — | — | Two aura files, one `data/` | Part of A/B | Low; `[Family]` on Forever unverified | TOC pattern VERIFIED in Blizzard source | +| F2. One path + feature check (rejected) | All | — | — | One file, two unrelated paths | Part of A/B | Low | VERIFIED APIs | +| G. Drop features | Retail, Forever | Cleaner, less info | Debuff frames | Lowest | S | Low | VERIFIED | + +## Recommendation + +**Decided: F1 family split.** + +- **Mainline (Retail, Forever):** AuraContainer only. Option A's filter strings and flags, plus B's + curated `includeSpellIDs`/`excludeSpellIDs` on the frames where ID filtering is allowed: PLAYER_AURAS + (class list), PLAYER_BUFFS (blacklist), own DoTs on a hostile target. No `UNIT_AURA` path. +- **Classic:** today's implementation, unchanged. +- **Shared:** buttons, layout, `data/`, cooldowns. +- E isn't recommended: it moves the decisions out of code. Spell data (C, D) is a later iteration + ([spell-data-generation.md](spell-data-generation.md)). + +### Migration (each phase ships on its own) + +1. **Phase 0: fixes that work today.** + - Read `cdInfo.isEnabled` (`Filger.lua:648`). + - On secret auras, use `C_UnitAuras.GetAuraDuration` + `SetCooldownFromDurationObject` instead of + `SetCooldown` (`Filger.lua:244-245`), provided Experiment 2 passes. + - Refresh only the event's `spellID` in `SPELL_UPDATE_COOLDOWN`. +2. **Phase 1: the split, with no behavior change.** + - Move `aura_proto` and the aura frame definitions into `core/Classic/` and add `core\[Family]\...` to + the TOC. + - Start `core/Mainline/` as a copy, so Retail behaves as it does today. + - Check in game that `[Family]` resolves on every client, Forever included. +3. **Phase 2: Mainline container for PLAYER_AURAS and PLAYER_BUFFS.** PLAYER_AURAS uses + `HELPFUL|PLAYER` + `includeSpellIDs`; PLAYER_BUFFS uses `HELPFUL|!PLAYER` + `excludeSpellIDs`. Both + frames are allowed to filter by ID. +4. **Phase 3: the remaining Mainline frames.** + - TARGET_DEBUFFS (`HARMFUL|PLAYER` on hostile units), TARGET_AURAS, FOCUS_DEBUFFS. + - BOSS_DEBUFFS (`isBossAura`; plus private-aura anchors if Experiment 6 passes). + - PLAYER_DEBUFFS shows only important debuffs, one group each: `HARMFUL|RAID_PLAYER_DISPELLABLE`, + `HARMFUL|CROWD_CONTROL`, `isBossAura`, and Stagger by ID if it is never-secret (Experiment 3). + - Then delete the copied event path from `core/Mainline/`. + +### Impossible in secret mode on Retail and Forever (drop or replace) + +| Feature | Status | Replacement | +|---|---|---| +| `stackThreshold` (`Filger.lua:309`) | Impossible | None | +| Numeric `priority` sort (`Filger.lua:282`, `config.lua:115`) | Impossible | Approximate with aura groups (UNVERIFIED) | +| Blacklist/whitelist by ID on debuffs on the player or a friendly target, and on buffs on a hostile target | Blocked by design | Never-secret spells only | +| Blacklist by name (`"Well Fed"`, `"Food"`, …) | Impossible | Exclude by spell ID where allowed | +| `sourceUnit == nil` (`config.lua:156`, `171`) | Impossible | `isBossAura` | +| LibDispel type/dispelable lookups, dispel flash (`Filger.lua:347-348`, `200-206`) | Impossible | `RAID_PLAYER_DISPELLABLE`/`DISPELLABLE` group + `AddDispelTypeTexture` / `GetAuraDispelTypeColor` (UNVERIFIED) | +| Dispel-colored border via `Filger.colors[dispelName]` (`Filger.lua:187`) | Impossible as a table key | `GetAuraDispelTypeColor(curve)` → `SetBackdropBorderColor` (UNVERIFIED that the backdrop accepts secrets) | +| Own timer text and red under 5 s (`Filger.lua:72-92`) | Impossible | Duration text binding + color curve | +| Sorting auras by expiration in Lua | Impossible | Container `Expiration` sort | +| Sorting cooldowns by remaining time while secret (`Filger.lua:620`) | Impossible | Fixed list order | +| Incremental `UNIT_AURA` bookkeeping keyed by `auraInstanceID` | Impossible | Container handles updates | + +### Classic + +Same branch, same TOC, its own aura file (`core/Classic/`), which is today's code. Classic has no +secret flags, so today's logic keeps working there, including the boss-debuff and blacklist data. +Filter-string differences (`NOT_CANCELABLE`) stay inside each family's frame definitions. The Vanilla +tooltip-by-index branch stays inside the Classic file. + +## Experiments + +Run each one on Retail, and on the Classic clients where noted. Unless stated otherwise, run them **in +combat** (target dummy, then a dungeon boss). + +1. **Restriction state.** + `/run print(C_Secrets.HasSecretRestrictions(), C_Secrets.ShouldAurasBeSecret(), C_Secrets.ShouldCooldownsBeSecret())` + on every client: out of combat, in combat in the open world, in a dungeon out of combat, and in a boss + fight. Pass: Classic prints `false false false` everywhere. +2. **Instance IDs from tainted code.** + `/run local t=C_UnitAuras.GetUnitAuraInstanceIDs("player","HELPFUL"); local id=t[1]; print(issecretvalue(id), pcall(function() return #t end), pcall(C_UnitAuras.GetAuraDuration,"player",id))`. + Settles whether `GetAuraDuration` and the `#` count work on secret IDs. +3. **Never-secret coverage** (out of combat is fine). A dev command loops over `Filger.spells`, + `Filger.blacklist` and `Filger.cooldowns` and counts `C_Secrets.GetSpellAuraSecrecy(id) == Enum.SecrecyLevel.NeverSecret`. + Also check Stagger: `/run for _,id in ipairs({124273,124274,124275}) do print(id, C_Secrets.GetSpellAuraSecrecy(id)) end`. +4. **Container test addon** (about 30 lines, `CreateFrame("AuraContainer", nil, UIParent, "CustomAuraContainerTemplate")`): + - one group `HELPFUL|PLAYER` with `includeSpellIDs` + - one group `HARMFUL` on `player` with `includeSpellIDs` (expected empty in combat) and with + `excludeSpellIDs` (expected unfiltered) + - `SetUnit("focus")` + - Pass: no taint errors, and the filters behave as the gate table says. +5. **Filter strings.** `/run print(#{C_UnitAuras.GetAuraSlots("player","HELPFUL|!PLAYER")}-1)`, then the + same for `HELPFUL|BIG_DEFENSIVE`, `HARMFUL|RAID_PLAYER_DISPELLABLE` and `HELPFUL|!CANCELABLE`. Run it + with a known defensive up, on Retail and each Classic client. Out of combat is fine. +6. **Private aura anchors.** In a test addon, call `C_UnitAuras.AddPrivateAuraAnchor{ unitToken="player", + auraIndex=1, parent=frame, showCooldownFrame=true, iconInfo={ iconAnchor={point="CENTER", + relativeTo=frame, relativePoint="CENTER", offsetX=0, offsetY=0}, iconWidth=40, iconHeight=40 } }`. It + needs a dungeon boss that applies a private aura. Pass: no error, and the icon shows. +7. **Cooldown edges.** In combat, print `issecretvalue` for `GetInventoryItemCooldown("player",13)` and + `C_Container.GetItemCooldown()`. Log which of the player's own spells arrive with a secret + `spellID` in `UNIT_SPELLCAST_SUCCEEDED`. +8. **Tooltips.** `/run print(GameTooltip.SetUnitBuffByAuraInstanceID ~= nil, GameTooltip.SetUnitAura ~= nil)` + on each Classic client. +9. **`[Family]` loading.** Add a one-line `print` to each `core//` stub. `/reload` on every + client: Retail and Forever must print Mainline, the rest Classic, and exactly one line must print per + client. Out of combat is fine. + +## Open questions for you + +1. ~~PLAYER_DEBUFFS on Retail~~ Decided: important debuffs only (dispellable, crowd control, boss, + Stagger); not every DoT. See Phase 3. +2. ~~Spell data (`FilgerData` dump, Classic boss data, `data/Cata/`)~~ Moved to + [spell-data-generation.md](spell-data-generation.md). diff --git a/docs/spell-data-generation.md b/docs/spell-data-generation.md new file mode 100644 index 0000000..a5bfe5a --- /dev/null +++ b/docs/spell-data-generation.md @@ -0,0 +1,108 @@ +# Spell data + +Plan only. No code was changed. As of 2026-10-21. + +- Split out of [secret-values-options.md](secret-values-options.md) (its former options C and D). Client + facts are in [compatibility.md](compatibility.md). +- Unless stated otherwise, paths are relative to `../refs/wow-ui-source/Interface/AddOns/`, and + `APIDoc/` means `Blizzard_APIDocumentationGenerated/`. +- Evidence tags: **VERIFIED** means read in Blizzard source or API docs (path given). **UNVERIFIED** means + an assumption that needs an in-game check. + +## When + +A later iteration, on its own branch, after Filger works on every game version +([secret-values-options.md](secret-values-options.md)). Until then `data/` stays unchanged. + +## Decisions + +- Nothing in `data/` is dropped or trimmed. Old content stays playable (Classic raids of any expansion, + legacy raids on Retail), so the boss and blacklist data still matter. +- Classic boss-debuff data: keep the existing data. +- `data/Cata/` and `Filger.isCata`: keep both. + +## Goal + +Maintain the spell tables in `data/` (boss debuffs, class lists, blacklist) with less hand work. Any tool +is maintainer-only: nothing runs for players and nothing becomes configurable. + +## Where it helps + +- **Retail and Forever:** class lists only. Spell-ID filtering of debuffs on the player or a friendly unit + is blocked by design (identity gate, see secret-values-options.md), so a boss-debuff list can't take + effect there in restricted content. Boss mechanics come from `isBossAura`, `RAID_PLAYER_DISPELLABLE` and + `CROWD_CONTROL` instead. Whether legacy raids on Retail run under addon restrictions is **UNVERIFIED**. +- **Classic:** boss debuffs and class lists, for raids of any expansion. + +## Sources + +### D1. In-game Encounter Journal dump + +1. A dev-only `/filger` command walks the journal out of combat: instance → encounter → section. +2. For each section, `C_EncounterJournal.GetSectionInfo(sectionID)` returns `spellID`, `title`, + `headerType`, … (`APIDoc/EncounterJournalDocumentation.lua:382-397`, **VERIFIED**). Keep sections with a + `spellID`, plus instance, boss and title. +3. Get the output out of the game: print for copy/paste, or write to `FilgerData` (declared, unused) and + read it from `WTF/` after logout. Using `FilgerData` this way is dev-only and temporary, not player + configuration. +4. A maintainer script turns the output into `data//bosses.lua`, or merges it into the existing file. + +Facts: + +- `GetSectionInfo` is in the API docs of every worktree: live, `classic`, `classic_titan`, + `classic_anniversary`, `classic_era`. Each also ships `Blizzard_EncounterJournal`. **VERIFIED** by grep. +- Whether the Classic journals contain their own raids and abilities is **UNVERIFIED**. +- Whether the section `spellID` is the debuff ID or the ID of the cast that applies it is **UNVERIFIED**. + They often differ, so the output needs review by hand. +- The journal has no class spells. D1 can't produce class lists. +- No third-party license involved; the data comes from the client. + +### D2. Build-time DB2 export + +- wago.tools CSV for `JournalEncounterSection`, `SpellEffect`, `SpellMisc`, processed in the maintainer's + environment, never at runtime. +- `SpellEffect` can link a cast to the aura it applies, which addresses D1's cast-vs-debuff problem + (**UNVERIFIED**). +- The data belongs to Blizzard. Bulk use and attribution under wago.tools' terms are **UNVERIFIED**; check + before relying on it. + +### D3. Community data + +- BigWigs, LittleWigs, DBM boss modules. +- Their licenses vary and are **UNVERIFIED** here. Read each LICENSE before taking anything. +- Take IDs only, never code, and credit the source. + +### C. Learn or log mode + +- **What:** while not restricted, record the spellIds of the auras seen. Promote the useful ones into the + whitelist or blacklist. +- **Avoids secrets:** reads happen only while `C_Secrets.ShouldAurasBeSecret()` is false. +- **The catch:** on Retail, encounter and M+ debuffs mostly exist only **while restricted**, so they can't + be learned, and ID lists on player debuffs are ignored anyway (identity gate). There it only helps class + buffs, which a curated list covers already. +- **Persistence:** it needs `FilgerData`. Kept as a dev-only command whose output the maintainer copies + into `data/`, it doesn't add configuration. As a player-facing feature it breaks the no-configuration + rule. +- **Works on:** all clients. Classic never restricts, so learning works there in combat too. +- **Evidence:** VERIFIED reasoning from the predicates; whether boss auras show up out of combat is + UNVERIFIED. + +## Comparison + +| Source | Boss data | Class lists | Runs | License | Effort | +|---|---|---|---|---|---| +| D1. EJ dump | Yes (needs review) | No | In game, out of combat | None | M | +| D2. DB2 export | Yes | Possible (**UNVERIFIED**) | Offline | wago.tools terms **UNVERIFIED** | M | +| D3. Community | Yes, curated | No | Offline | Per project, **UNVERIFIED** | S–M | +| C. Learn mode | Classic only; Retail can't see them | Yes, from what the maintainer plays | In game | None | S–M | + +Maintenance for D1–D3: one run per patch. Risk: low (offline or dev-only). + +## Recommendation + +D1 first (no license question); D2 if class lists outgrow hand maintenance. C only as a dev aid on +Classic. + +## Open questions + +1. If D1 or C is built: print for copy/paste, or temporary `FilgerData`? From fa285d5be83ac4c84a5eb82dc71967b785d65426 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Wed, 21 Oct 2026 23:09:47 -0300 Subject: [PATCH 02/13] fix cooldown state and secret timer text, record phase 0 results - read cdInfo.isEnabled (cdInfo.enabled does not exist, so spells were always enabled) - SPELL_UPDATE_COOLDOWN refreshes only the event's spell, the shown rows, and slot/item rows; nil spellID still refreshes everything - secret cooldowns: text binding on button.Time with a seconds formatter and a color curve (red under 5s), replacing Blizzard countdown numbers - docs: phase 0 experiment results (restriction state, aura access while tainted, never-secret coverage, cooldown event payload) --- Filger.lua | 73 ++++++++++++++++++++++++++--------- docs/secret-values-options.md | 65 +++++++++++++++++++++++++------ 2 files changed, 109 insertions(+), 29 deletions(-) diff --git a/Filger.lua b/Filger.lua index 5be6de4..88799b8 100644 --- a/Filger.lua +++ b/Filger.lua @@ -17,6 +17,10 @@ local GetItemCooldown = C_Container.GetItemCooldown local GetInventoryItemCooldown = _G.GetInventoryItemCooldown local C_Secrets = _G.C_Secrets local issecretvalue = _G.issecretvalue +local C_StringUtil = _G.C_StringUtil +local C_CurveUtil = _G.C_CurveUtil +local C_DurationUtil = _G.C_DurationUtil +local Enum = _G.Enum -------------------------------------------------- -- Filger @@ -29,6 +33,19 @@ local isInit = false do local button_proto = {} + -- secret durations: text and color like OnUpdate ("2m", "45s", "4.3"; red under 5s) + local timeFormatter = C_StringUtil.CreateSecondsFormatter() + timeFormatter:SetDefaultAbbreviation(Enum.SecondsFormatterAbbreviation.OneLetter) + timeFormatter:SetStripIntervalWhitespace(Enum.SecondsFormatterIntervalWhitespace.StripIgnoreLocale) + timeFormatter:SetDesiredUnitCount(1) + timeFormatter:SetCanRoundUpLastUnit(true) + timeFormatter:SetMillisecondsThreshold(5) + + local timeColorCurve = C_CurveUtil.CreateColorCurve() + timeColorCurve:SetType(Enum.LuaCurveType.Step) + timeColorCurve:AddPoint(0, CreateColor(0.99, 0.31, 0.31)) + timeColorCurve:AddPoint(5, CreateColor(0.84, 0.75, 0.65)) + function button_proto:OnEnter() if (GameTooltip:IsForbidden() and not self:IsVisible()) then return end @@ -53,14 +70,14 @@ do end end - -- secret durations can't drive the OnUpdate timer text, Blizzard countdown numbers are used instead + -- secret durations can't drive the OnUpdate timer text, the duration text binding is used instead function button_proto:SetSecret(isSecret) if self.isSecret == isSecret then return end self.isSecret = isSecret - self.Cooldown:SetHideCountdownNumbers(not isSecret) + self.TimeBinding:SetEnabled(isSecret) + self.Time:SetText("") if isSecret then self:SetScript("OnUpdate", nil) - self.Time:SetText("") end end @@ -142,6 +159,15 @@ do time:SetTextColor(0.84, 0.75, 0.65) button.Time = time + local timeBinding = C_DurationUtil.CreateDurationTextBinding() + timeBinding:SetFontString(time) + timeBinding:SetFormatter(timeFormatter) + timeBinding:SetTextColorCurve(timeColorCurve, Enum.DurationTextBindingProperty.RemainingDuration) + timeBinding:SetExpiredText("") + timeBinding:SetZeroDurationText("") + timeBinding:SetEnabled(false) + button.TimeBinding = timeBinding + local animation = button:CreateAnimationGroup() animation:SetLooping("BOUNCE") button.Animation = animation @@ -563,6 +589,7 @@ do if button.Cooldown then if data.durationObject then button.Cooldown:SetCooldownFromDurationObject(data.durationObject) + button.TimeBinding:SetDuration(data.durationObject) button.Cooldown:Show() elseif data.duration and data.duration > 0 then button.Cooldown:SetCooldown(data.expirationTime - data.duration, data.duration, data.timeMod) @@ -645,7 +672,7 @@ do start = cdInfo.startTime duration = cdInfo.duration end - enabled = cdInfo.enabled + enabled = cdInfo.isEnabled and 1 or 0 end elseif data.slotId then index = "SLOT_" .. data.slotId @@ -682,19 +709,28 @@ do } end - function cooldown_proto:UpdateCooldowns(event, unit) + function cooldown_proto:UpdateCooldowns(event, unit, spellID, baseSpellID) local changed = true - self.all = table.wipe(self.all or {}) - self.active = table.wipe(self.active or {}) + self.active = self.active or {} + if not spellID then + table.wipe(self.active) + end for _, row in next, self.spells do - local data = self:ProcessCooldownData(unit, row) - self.all[data.index] = data - - if self:FilterCooldown(data) then - self.active[data.index] = true - elseif data.spellId then - self.casted[data.spellId] = nil + -- no event fires when a cooldown ends, so shown rows are re-checked on every event; + -- slots and items are always re-checked because their events carry the item's spell ID + if not spellID or not row.spellId or self.active[row] + or row.spellId == spellID or row.spellId == baseSpellID then + local data = self:ProcessCooldownData(unit, row) + + if self:FilterCooldown(data) then + self.active[row] = data + else + self.active[row] = nil + if data.spellId then + self.casted[data.spellId] = nil + end + end end end @@ -703,8 +739,8 @@ do -- and repopulate them, multiple table.remove calls are insanely slow self.sorted = table.wipe(self.sorted or {}) - for index, _ in next, self.active do - table.insert(self.sorted, self.all[index]) + for _, data in next, self.active do + table.insert(self.sorted, data) end table.sort(self.sorted, self.SortCooldowns) @@ -741,8 +777,9 @@ do self:UpdateCooldowns("ForceUpdate", self.unit, nil) end - function cooldown_proto:SPELL_UPDATE_COOLDOWN() - self:UpdateCooldowns("SPELL_UPDATE_COOLDOWN", self.unit) + -- spellID is nil when every cooldown should be updated + function cooldown_proto:SPELL_UPDATE_COOLDOWN(spellID, baseSpellID) + self:UpdateCooldowns("SPELL_UPDATE_COOLDOWN", self.unit, spellID, baseSpellID) end -- register which spell the player cast diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index ae26c99..528d7f1 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -42,14 +42,21 @@ Analysis only. No code was changed. As of 2026-10-19. Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments](#experiments). - [ ] **Phase 0: fixes that work today** - - [ ] Experiment 1: restriction state on every client - - [ ] Experiment 2: `GetAuraDuration` and `#` on secret instance IDs (Retail, in combat) - - [ ] Experiment 3: never-secret coverage of `data/`, Stagger - - [ ] Read `cdInfo.isEnabled` (`Filger.lua:648`) - - [ ] Refresh only the event's `spellID` in `SPELL_UPDATE_COOLDOWN` (nil → all; override/base IDs; - items and slots still refreshed) - - [ ] Secret auras: `GetAuraDuration` + `SetCooldownFromDurationObject`, no `duration > 0` compare - (needs Experiment 2) + - [x] Experiment 1: restriction state on every client (Forever and Titan not tested) + - [x] Experiment 2: `GetAuraDuration` and `#` on secret instance IDs (Retail, in combat): **fails**, + tainted code can't call any `RequiresUnitAuraAccess` API while auras are secret + - [x] Experiment 3: never-secret coverage of `data/`, Stagger (spells 1/100, blacklist 17/313, + cooldowns 0/28, Stagger contextually secret) + - [x] Read `cdInfo.isEnabled` (`Filger.lua:648`) + - [x] Refresh only the event's `spellID` in `SPELL_UPDATE_COOLDOWN` (nil → all; override/base IDs; + items and slots still refreshed). Shown rows are also re-checked on every event: logged on + Retail and MoP (2026-10-21), the event fires when a cooldown starts, never when it ends. + `spellID` was never secret in combat, and `baseSpellID` was always nil. + - [ ] ~~Secret auras: `GetAuraDuration` + `SetCooldownFromDurationObject`, no `duration > 0` compare~~ + Blocked by Experiment 2: no instance ID is reachable from tainted code in combat + - [x] Secret cooldown timer text: a `C_DurationUtil` duration text binding on `button.Time`, with a + seconds formatter (`2m`, `45s`, `4.3`) and a step color curve (red under 5 s). Verified on Retail + in combat (2026-10-21). Blizzard countdown numbers stay hidden. - [ ] **Phase 1: family split, no behavior change** - [ ] Move `aura_proto` and aura frame definitions to `core/Classic/`; add `core\[Family]\...` to the TOC - [ ] `core/Mainline/` starts as a copy @@ -63,7 +70,7 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - [ ] Experiment 6: private aura anchors - [ ] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS - [ ] BOSS_DEBUFFS (`isBossAura`, private-aura anchors if Experiment 6 passes) - - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss, Stagger groups + - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss groups (Stagger dropped, Experiment 3) - [ ] Delete the copied event path from `core/Mainline/` - [ ] **Other experiments** - [ ] Experiment 7: cooldown edges (items, slots, secret cast `spellID`) @@ -412,7 +419,8 @@ iteration, after Filger works on every game version. Until then `data/` stays un - TARGET_DEBUFFS (`HARMFUL|PLAYER` on hostile units), TARGET_AURAS, FOCUS_DEBUFFS. - BOSS_DEBUFFS (`isBossAura`; plus private-aura anchors if Experiment 6 passes). - PLAYER_DEBUFFS shows only important debuffs, one group each: `HARMFUL|RAID_PLAYER_DISPELLABLE`, - `HARMFUL|CROWD_CONTROL`, `isBossAura`, and Stagger by ID if it is never-secret (Experiment 3). + `HARMFUL|CROWD_CONTROL`, `isBossAura`. ~~Stagger by ID~~: not possible, Stagger is contextually + secret (Experiment 3). - Then delete the copied event path from `core/Mainline/`. ### Impossible in secret mode on Retail and Forever (drop or replace) @@ -426,7 +434,7 @@ iteration, after Filger works on every game version. Until then `data/` stays un | `sourceUnit == nil` (`config.lua:156`, `171`) | Impossible | `isBossAura` | | LibDispel type/dispelable lookups, dispel flash (`Filger.lua:347-348`, `200-206`) | Impossible | `RAID_PLAYER_DISPELLABLE`/`DISPELLABLE` group + `AddDispelTypeTexture` / `GetAuraDispelTypeColor` (UNVERIFIED) | | Dispel-colored border via `Filger.colors[dispelName]` (`Filger.lua:187`) | Impossible as a table key | `GetAuraDispelTypeColor(curve)` → `SetBackdropBorderColor` (UNVERIFIED that the backdrop accepts secrets) | -| Own timer text and red under 5 s (`Filger.lua:72-92`) | Impossible | Duration text binding + color curve | +| Own timer text and red under 5 s (`Filger.lua:72-92`) | Impossible | Duration text binding + color curve (works for cooldowns, verified on Retail) | | Sorting auras by expiration in Lua | Impossible | Container `Expiration` sort | | Sorting cooldowns by remaining time while secret (`Filger.lua:620`) | Impossible | Fixed list order | | Incremental `UNIT_AURA` bookkeeping keyed by `auraInstanceID` | Impossible | Container handles updates | @@ -447,12 +455,47 @@ combat** (target dummy, then a dungeon boss). `/run print(C_Secrets.HasSecretRestrictions(), C_Secrets.ShouldAurasBeSecret(), C_Secrets.ShouldCooldownsBeSecret())` on every client: out of combat, in combat in the open world, in a dungeon out of combat, and in a boss fight. Pass: Classic prints `false false false` everywhere. + - **Result (2026-10-21):** + - Retail: `true false false` out of combat (open world and dungeon); `true true true` in combat + (dummy and boss). Restrictions follow combat state; a dungeon alone doesn't restrict these APIs. + `HasSecretRestrictions()` is always `true`, so it tells which client this is, not whether data + is secret right now. + - Era, TBC, MoP: `false false false` (open world, in and out of combat). Dungeon steps skipped, + since `HasSecretRestrictions()` is false on these clients. + - Forever, Titan: not tested (no client). 2. **Instance IDs from tainted code.** `/run local t=C_UnitAuras.GetUnitAuraInstanceIDs("player","HELPFUL"); local id=t[1]; print(issecretvalue(id), pcall(function() return #t end), pcall(C_UnitAuras.GetAuraDuration,"player",id))`. Settles whether `GetAuraDuration` and the `#` count work on secret IDs. + - **Result (2026-10-21, Retail, in a dungeon):** + - Out of combat: `false true true LuaDurationObject: …`. IDs aren't secret, and `GetAuraDuration` + works. The middle `pcall` printed only `true` because a call that isn't last in an argument list + keeps only its first return value. That's a flaw in the snippet, not a finding. + - In combat: the first call already errors with `GetUnitAuraInstanceIDs(): Auras cannot be accessed + when secret while tainted by '*** ForceTaint_Strong ***'`. `RequiresUnitAuraAccess` is a + `Precondition` with `FailureMode = "Error"` (`APIDoc/SecretPredicatesDocumentation.lua:48`). It + is set on 18 `C_UnitAuras` functions, including `GetAuraSlots`, `GetAuraDataBySlot`, + `GetAuraDataByAuraInstanceID`, `GetUnitAuraInstanceIDs`, `GetUnitAuras`, `GetAuraDuration`, + `IsAuraFilteredOutByInstanceID`, `GetAuraDispelTypeColor` and `GetAuraApplicationDisplayCount`. + So the problem isn't that instance IDs are secret: tainted code can't reach any instance ID at + all. + - Consequence: in combat on Retail, the only aura data an addon can read itself comes from the + `RequiresNonSecretAura` lookups (`GetPlayerAuraBySpellID`, `GetUnitAuraBySpellID`, …) for + never-secret spells. Everything else needs the AuraContainer. + - Caveat: `/run` is tainted with `ForceTaint_Strong`. Addon code is tainted too, so the result + should be the same for Filger, but that is **UNVERIFIED** from an addon file. 3. **Never-secret coverage** (out of combat is fine). A dev command loops over `Filger.spells`, `Filger.blacklist` and `Filger.cooldowns` and counts `C_Secrets.GetSpellAuraSecrecy(id) == Enum.SecrecyLevel.NeverSecret`. Also check Stagger: `/run for _,id in ipairs({124273,124274,124275}) do print(id, C_Secrets.GetSpellAuraSecrecy(id)) end`. + - **Result (2026-10-21, Retail, `/run` loops; cooldowns checked with `GetSpellCooldownSecrecy`):** + - `Filger.spells` (all classes): 1/100 never-secret (344179). In combat, `ScanSpellAuras` + finds almost nothing. + - `Filger.blacklist` (numeric keys): 17/313 never-secret: 80354 404464 404468 462854 390435 + 381753 1126 206151 2823 3408 6673 1459 57723 57724 369459 264689 21562. Only these can be + ID-filtered on debuffs on the player. + - `Filger.cooldowns`: 0/28. Every cooldown is contextually secret, so the duration-object + path is required in combat. + - Stagger 124273/124274/124275: `2` (`ContextuallySecret`). Stagger is a harmful aura on the + player, so the identity gate blocks it. A Stagger group by ID is impossible on Retail. 4. **Container test addon** (about 30 lines, `CreateFrame("AuraContainer", nil, UIParent, "CustomAuraContainerTemplate")`): - one group `HELPFUL|PLAYER` with `includeSpellIDs` - one group `HARMFUL` on `player` with `includeSpellIDs` (expected empty in combat) and with From fc357c8004d1d202a3515b29e63783fb399cc04a Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Wed, 21 Oct 2026 23:55:05 -0300 Subject: [PATCH 03/13] split aura implementation per client family, record phase 1 results - move aura_proto and the aura frame definitions to core/[Family]/auras.lua (Classic and Mainline start as identical copies), exposed as ns.aura_proto and merged into Filger.config.frames - shared layout numbers move to Filger.config.layout - create the cooldown duration text binding on Mainline only: Classic bindings have no SetTextColorCurve (crashed on MoP) --- Filger.lua | 384 +------------------------- Filger.toc | 1 + config.lua | 140 +--------- core/Classic/auras.lua | 506 ++++++++++++++++++++++++++++++++++ core/Mainline/auras.lua | 506 ++++++++++++++++++++++++++++++++++ docs/secret-values-options.md | 16 +- 6 files changed, 1046 insertions(+), 507 deletions(-) create mode 100644 core/Classic/auras.lua create mode 100644 core/Mainline/auras.lua diff --git a/Filger.lua b/Filger.lua index 88799b8..74d5351 100644 --- a/Filger.lua +++ b/Filger.lua @@ -1,15 +1,8 @@ local _, ns = ... local Filger = ns.Filger -local LibDispel = LibStub("LibDispel") -assert(LibDispel, "Filger requires LibDispel") - -local blacklist = ns.Filger.blacklist or {} - -- Blizzard local CreateFrame = _G.CreateFrame -local UnitIsUnit = _G.UnitIsUnit -local UnitIsOwnerOrControllerOfUnit = _G.UnitIsOwnerOrControllerOfUnit local IsSpellKnown = C_SpellBook.IsSpellKnown local GetInventoryItemLink = _G.GetInventoryItemLink local GetItemInfo = C_Item.GetItemInfo @@ -74,7 +67,9 @@ do function button_proto:SetSecret(isSecret) if self.isSecret == isSecret then return end self.isSecret = isSecret - self.TimeBinding:SetEnabled(isSecret) + if self.TimeBinding then + self.TimeBinding:SetEnabled(isSecret) + end self.Time:SetText("") if isSecret then self:SetScript("OnUpdate", nil) @@ -159,14 +154,17 @@ do time:SetTextColor(0.84, 0.75, 0.65) button.Time = time - local timeBinding = C_DurationUtil.CreateDurationTextBinding() - timeBinding:SetFontString(time) - timeBinding:SetFormatter(timeFormatter) - timeBinding:SetTextColorCurve(timeColorCurve, Enum.DurationTextBindingProperty.RemainingDuration) - timeBinding:SetExpiredText("") - timeBinding:SetZeroDurationText("") - timeBinding:SetEnabled(false) - button.TimeBinding = timeBinding + -- only secret cooldowns (Mainline) need it; Classic bindings have no SetTextColorCurve + if Filger.isMainline then + local timeBinding = C_DurationUtil.CreateDurationTextBinding() + timeBinding:SetFontString(time) + timeBinding:SetFormatter(timeFormatter) + timeBinding:SetTextColorCurve(timeColorCurve, Enum.DurationTextBindingProperty.RemainingDuration) + timeBinding:SetExpiredText("") + timeBinding:SetZeroDurationText("") + timeBinding:SetEnabled(false) + button.TimeBinding = timeBinding + end local animation = button:CreateAnimationGroup() animation:SetLooping("BOUNCE") @@ -188,358 +186,6 @@ do end end --------------------------------------------------- --- AURAS --------------------------------------------------- -local aura_proto = { - createdButtons = 0, - anchoredButtons = 0, - visibleButtons = 0 -} - -do - --[[ Callback: button:UpdateColor(button, unit, data) - Called after the aura button has been updated. - - * self - the widget holding the aura buttons - * button - the updated aura button (Button) - * unit - the unit on which the aura is cast (string) - * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - * index - the actual position of the aura button (number) - ]]-- - function aura_proto:UpdateColor(button, unit, data) - if button.Backdrop then - if (data.isHarmful and UnitCanAssist("player", unit)) or (data.isHelpful and UnitCanAttack(unit, "player") and UnitCanAttack("player", unit)) then - local color = Filger.colors[data.dispelName] - button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) - else - local color = Filger.config.general.backdrop.color - button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) - end - end - - if button.Icon and self.desaturated == true then - button.Icon:SetDesaturated(data.isHarmful and not data.isPlayerAura and not UnitIsUnit(unit, "player")) - end - - if button.Animation then - if data.isDispelable or data.isStealable then - button.Animation:Play() - button.Animation.playing = true - else - button.Animation:Stop() - button.Animation.playing = false - end - end - end - - --[[ Callback: aura_proto:Update(unit, data, index) - Update the aura button and its components. - - * self - the widget holding the aura buttons - * unit - the unit on which the aura is cast (string) - * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - * index - the actual position of the aura button (number) - ]]-- - function aura_proto:Update(unit, data, index) - if not data.name then return end - - local button = self[index] - if not button then - button = Filger:CreateButton(self, index) - - table.insert(self, button) - - self.createdButtons = self.createdButtons + 1 - end - - -- tooltips - button.unit = unit - button.filter = self.filter - button.auraInstanceID = data.auraInstanceID - button.auraIndex = data.auraIndex - button.isHarmful = data.isHarmful - - -- timers - button.name = data.name - button.spellId = data.spellId - button.duration = data.duration or 0 - button.expirationTime = data.expirationTime or GetTime() - - if button.Cooldown then - if data.duration and data.duration > 0 then - button.Cooldown:SetCooldown(data.expirationTime - data.duration, data.duration, data.timeMod) - button.Cooldown:Show() - else - button.Cooldown:Hide() - end - end - - if button.Icon then - if data.icon then - button.Icon:SetTexture(data.icon) - button.Icon:Show() - else - button.Icon:Hide() - end - end - - if button.Count then - button.Count:SetText((data.applications and data.applications > 1) and data.applications or "") - end - - if data.duration and data.duration > 0 then - button:SetScript("OnUpdate", button.OnUpdate) - end - - button:Show() - - self:UpdateColor(button, unit, data) - end - - --[[ Override: aura_proto.SortAuras(a, b) - Defines a custom sorting algorithm for ordering the auras. - - Defaults to [AuraUtil.DefaultAuraCompare](https://github.com/Gethe/wow-ui-source/search?q=DefaultAuraCompare). - - Overridden by the more specific SortAuras overrides if they are defined. - --]] - aura_proto.SortAuras = function(a, b) - if (a.priority ~= b.priority) then - return a.priority > b.priority - end - - if (a.expirationTime ~= b.expirationTime) then - return a.expirationTime < b.expirationTime - end - - -- if (a.canApplyAura ~= b.canApplyAura) then - -- return a.canApplyAura - -- end - - return a.auraInstanceID < b.auraInstanceID - end - - --[[ Override: aura_proto:FilterAura(unit, data) - Defines a custom filter that controls if the aura button should be shown. - - * self - the widget holding the aura buttons - * unit - the unit for which the update has been triggered (string) - * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - - ## Returns - - * show - indicates whether the aura button should be shown (boolean) - --]] - function aura_proto:FilterAura(unit, data) - return data.enabled and data.applications >= data.stackThreshold - end - - function aura_proto:GetAuraIndex(unit, auraInstanceID, filter) - local index = 1 - repeat - local data = C_UnitAuras.GetAuraDataByIndex(unit, index, filter) - if not data then - return nil - elseif data.auraInstanceID == auraInstanceID then - return index - end - index = index + 1 - until (not data) - return nil - end - - --[[ Override: aura_proto:ProcessData(unit, data) - Add some custom value sto the UnitAuraInfo. - - * self - the widget holding the aura buttons - * unit - the unit for which the update has been triggered (string) - * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - - ## Returns - - * data - a custom 'UnitAuraInfo' object (table) - --]] - function aura_proto:ProcessData(unit, data) - if not data then return end - - if self.restricted then - -- unit comparisons may be secret while restricted - data.isPlayerAura = data.isFromPlayerOrPlayerPet - else - data.isPlayerAura = data.sourceUnit and (UnitIsUnit("player", data.sourceUnit) or UnitIsOwnerOrControllerOfUnit("player", data.sourceUnit)) - end - - data.dispelName = LibDispel:GetDispelType(data.spellId, data.dispelName) - data.isDispelable = LibDispel:IsDispelable(unit, data.spellId, data.dispelName, data.isHarmful) - - local spell = self.spells[data.spellId] - if spell then - data.enabled = spell.enabled or false - data.priority = spell.priority or 0 - data.stackThreshold = spell.stackThreshold or 0 - else - data.enabled = true - data.priority = 0 - data.stackThreshold = 0 - end - - if blacklist[data.spellId] or blacklist[data.name] then - data.enabled = false - end - - if Filger.isVanilla then - data.auraIndex = self:GetAuraIndex(unit, data.auraInstanceID, self.filter) - end - - return data - end - - -- Retail: while auras are secret, tainted code can't list them; only spells whose aura - -- isn't secret can be looked up one by one, so only listed spells are shown. - function aura_proto:ScanSpellAuras(unit) - local isHarmful = self.filter:find("HARMFUL") ~= nil - local isPlayer = self.filter:find("PLAYER") ~= nil - - for spellId in next, self.spells do - if not C_Secrets.ShouldSpellAuraBeSecret(spellId) then - local data = C_UnitAuras.GetUnitAuraBySpellID(unit, spellId) - if data and data.isHarmful == isHarmful and (not isPlayer or data.isFromPlayerOrPlayerPet) then - data = self:ProcessData(unit, data) - self.all[data.auraInstanceID] = data - - if self:FilterAura(unit, data) then - self.actives[data.auraInstanceID] = true - end - end - end - end - end - - function aura_proto:UpdateAuras(event, unit, updateInfo) - if (self.unit ~= unit) then return end - - local isRestricted = C_Secrets and C_Secrets.ShouldAurasBeSecret() - -- updateInfo is secret while restricted; cached auras are stale right after it - local isFullUpdate = isRestricted or self.restricted or not updateInfo or updateInfo.isFullUpdate - self.restricted = isRestricted - - local changed = false - - if isFullUpdate then - self.all = table.wipe(self.all or {}) - self.actives = table.wipe(self.actives or {}) - changed = true - - if isRestricted then - self:ScanSpellAuras(unit) - else - local slots = { C_UnitAuras.GetAuraSlots(unit, self.filter) } - for i = 2, #slots do -- #1 return is continuationToken, we don't care about it - local data = self:ProcessData(unit, C_UnitAuras.GetAuraDataBySlot(unit, slots[i])) - self.all[data.auraInstanceID] = data - - if self:FilterAura(unit, data) then - self.actives[data.auraInstanceID] = true - end - end - end - else - if updateInfo.addedAuras then - for _, data in next, updateInfo.addedAuras do - if not C_UnitAuras.IsAuraFilteredOutByInstanceID(unit, data.auraInstanceID, self.filter) then - self.all[data.auraInstanceID] = self:ProcessData(unit, data) - if self:FilterAura(unit, data) then - self.actives[data.auraInstanceID] = true - changed = true - end - end - end - end - - if updateInfo.updatedAuraInstanceIDs then - for _, auraInstanceID in next, updateInfo.updatedAuraInstanceIDs do - local data = self.all[auraInstanceID] - if data then - self.all[auraInstanceID] = self:ProcessData(unit, C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID)) - if self:FilterAura(unit, data) then - self.actives[auraInstanceID] = true - changed = true - end - end - end - end - - if updateInfo.removedAuraInstanceIDs then - for _, auraInstanceID in next, updateInfo.removedAuraInstanceIDs do - if self.all[auraInstanceID] then - self.all[auraInstanceID] = nil - end - - if self.actives[auraInstanceID] then - self.actives[auraInstanceID] = nil - changed = true - end - end - end - end - - if changed then - -- instead of removing auras one by one, just wipe the tables entirely - -- and repopulate them, multiple table.remove calls are insanely slow - self.sorted = table.wipe(self.sorted or {}) - - for auraInstanceID, _ in next, self.actives do - table.insert(self.sorted, self.all[auraInstanceID]) - end - - table.sort(self.sorted, self.SortAuras) - - local numVisible = math.min(self.limit, #self.sorted) - - for i = 1, numVisible do - self:Update(unit, self.sorted[i], i) - end - - local visibleChanged = true - - if (numVisible ~= self.visibleButtons) then - self.visibleButtons = numVisible - visibleChanged = true - end - - for i = numVisible + 1, #self do - self[i]:Hide() - end - - if (visibleChanged or self.createdButtons > self.anchoredButtons) then - if visibleChanged then - self:SetPosition(1, numVisible) - else - self:SetPosition(self.anchoredButtons + 1, self.createdButtons) - self.anchoredButtons = self.createdButtons - end - end - end - end - - function aura_proto:ForceUpdate() - self:UpdateAuras("ForceUpdate", self.unit, nil) - end - - function aura_proto:UNIT_AURA(unit, updateInfo) - self:UpdateAuras("UNIT_AURA", unit, updateInfo) - end - - function aura_proto:PLAYER_TARGET_CHANGED(...) - self:ForceUpdate() - end - - function aura_proto:PLAYER_FOCUS_CHANGED(...) - self:ForceUpdate() - end -end - -------------------------------------------------- -- COOLDOWNS -------------------------------------------------- @@ -872,7 +518,7 @@ do element:RegisterEvent("SPELL_UPDATE_COOLDOWN") element:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", self.unit) else - element = Mixin(element, aura_proto) + element = Mixin(element, ns.aura_proto) -- overrider element functions with configurations element.SortAuras = data.SortAuras or element.SortAuras diff --git a/Filger.toc b/Filger.toc index 6013deb..8d32ae1 100644 --- a/Filger.toc +++ b/Filger.toc @@ -23,4 +23,5 @@ data\shared\blacklist.lua data\shared\cooldowns.lua core\init.xml config.lua +core\[Family]\auras.lua Filger.lua diff --git a/config.lua b/config.lua index c17308e..ff902aa 100644 --- a/config.lua +++ b/config.lua @@ -3,10 +3,7 @@ local Filger = ns.Filger ns.data.Finalize() -local class = Filger.class local cooldowns = Filger.cooldowns or {} -local spells = Filger.spells or {} -local all = Filger.all or {} ------------------------------------------------------------ -- Config @@ -28,10 +25,6 @@ local all = Filger.all or {} * hidePlayer -- ]] -local size = 35 -local spacing = 5 -local xOffset, yOffset = 239, 39 - Filger.config = { ["general"] = { ["backdrop"] = { @@ -44,133 +37,14 @@ Filger.config = { ["well_fed"] = false, -- enabled filter to hide well fed buff (too many spells with this name) ["gametooltip"] = true, -- enabled tooltips }, + -- shared by the aura frames in core//auras.lua + ["layout"] = { + size = 35, + spacing = 5, + xOffset = 239, + yOffset = 39 + }, ["frames"] = { - -- LEFT - ["PLAYER_DEBUFFS"] = { - enabled = true, - unit = "player", - filter = "HARMFUL", - anchor = { "RIGHT", UIParent, "CENTER", -xOffset, yOffset }, - limit = 8, - size = size, - spacing = spacing, - initialAnchor = "BOTTOMRIGHT", - ["growth-x"] = "LEFT", - ["growth-y"] = "DOWN", - spells = { - [124275] = { enabled = true, priority = 3 }, -- Light Stagger - [124274] = { enabled = true, priority = 3 }, -- Moderate Stagger - [124273] = { enabled = true, priority = 3 } -- Heavy Stagger - } - }, - ["PLAYER_AURAS"] = { - enabled = true, - unit = "player", - filter = "PLAYER|HELPFUL", - anchor = { "RIGHT", UIParent, "CENTER", -xOffset, 0 }, - limit = 8, - size = size, - spacing = spacing, - initialAnchor = "BOTTOMRIGHT", - ["growth-x"] = "LEFT", - ["growth-y"] = "DOWN", - spells = Mixin(spells[class] or {}, spells["ALL"] or {}) - }, - ["PLAYER_BUFFS"] = { - enabled = true, - unit = "player", - filter = "HELPFUL", - caster = "all", - hidePlayer = true, - anchor = { "RIGHT", UIParent, "CENTER", -xOffset, -yOffset }, - limit = 8, - size = size, - spacing = spacing, - initialAnchor = "BOTTOMRIGHT", - ["growth-x"] = "LEFT", - ["growth-y"] = "DOWN", - FilterAura = function(element, unit, data) - return data.enabled and not data.isPlayerAura - end - }, - ["TARGET_DEBUFFS"] = { - enabled = true, - unit = "target", - filter = "HARMFUL", - anchor = { "LEFT", UIParent, "CENTER", xOffset, yOffset }, - limit = 8, - size = size, - spacing = spacing, - initialAnchor = "BOTTOMLEFT", - ["growth-x"] = "RIGHT", - ["growth-y"] = "DOWN", - spells = all, - FilterAura = function(element, unit, data) - if UnitCanAttack(unit, "player") then - return data.enabled and data.isPlayerAura - end - return data.enabled and not data.isPlayerAura - end, - SortAuras = function(a, b) - if (a.priority ~= b.priority) then - return a.priority < b.priority - end - - if (a.expirationTime ~= b.expirationTime) then - return a.expirationTime < b.expirationTime - end - - if (a.isPlayerAura ~= b.isPlayerAura) then - return a.isPlayerAura - end - - return a.auraInstanceID < b.auraInstanceID - end - }, - ["TARGET_AURAS"] = { - enabled = true, - unit = "target", - filter = "HELPFUL", - anchor = { "LEFT", UIParent, "CENTER", xOffset, 0 }, - limit = 8, - size = size, - spacing = spacing, - initialAnchor = "BOTTOMLEFT", - ["growth-x"] = "RIGHT", - ["growth-y"] = "DOWN", - spells = all - }, - ["FOCUS_DEBUFFS"] = { - enabled = true, - unit = "focus", - filter = "HARMFUL", - anchor = { "LEFT", UIParent, "CENTER", xOffset, -yOffset }, - limit = 8, - size = size, - spacing = spacing, - initialAnchor = "BOTTOMLEFT", - ["growth-x"] = "RIGHT", - ["growth-y"] = "DOWN", - desaturated = false, - FilterAura = function(element, unit, data) - return data.enabled and data.isBossAura or (data.sourceUnit == nil) or not data.isFromPlayerOrPlayerPet - end - }, - ["BOSS_DEBUFFS"] = { - enabled = false, - unit = "player", - filter = "HARMFUL", - anchor = { "CENTER", UIParent, "CENTER", 0, 160 }, - limit = 1, - size = math.floor(1.5 * size), - spacing = spacing, - initialAnchor = "BOTTOMLEFT", - ["growth-x"] = "RIGHT", - ["growth-y"] = "DOWN", - FilterAura = function(element, unit, data) - return data.enabled and data.isBossAura or (data.sourceUnit == nil) - end - }, ["PLAYER_COOLDOWNS"] = { enabled = true, unit = "player", diff --git a/core/Classic/auras.lua b/core/Classic/auras.lua new file mode 100644 index 0000000..2365356 --- /dev/null +++ b/core/Classic/auras.lua @@ -0,0 +1,506 @@ +local _, ns = ... +local Filger = ns.Filger + +local LibDispel = LibStub("LibDispel") +assert(LibDispel, "Filger requires LibDispel") + +local blacklist = ns.Filger.blacklist or {} + +-- Blizzard +local UnitIsUnit = _G.UnitIsUnit +local UnitIsOwnerOrControllerOfUnit = _G.UnitIsOwnerOrControllerOfUnit +local C_Secrets = _G.C_Secrets + +-------------------------------------------------- +-- AURAS +-------------------------------------------------- +local aura_proto = { + createdButtons = 0, + anchoredButtons = 0, + visibleButtons = 0 +} + +do + --[[ Callback: button:UpdateColor(button, unit, data) + Called after the aura button has been updated. + + * self - the widget holding the aura buttons + * button - the updated aura button (Button) + * unit - the unit on which the aura is cast (string) + * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + * index - the actual position of the aura button (number) + ]]-- + function aura_proto:UpdateColor(button, unit, data) + if button.Backdrop then + if (data.isHarmful and UnitCanAssist("player", unit)) or (data.isHelpful and UnitCanAttack(unit, "player") and UnitCanAttack("player", unit)) then + local color = Filger.colors[data.dispelName] + button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) + else + local color = Filger.config.general.backdrop.color + button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) + end + end + + if button.Icon and self.desaturated == true then + button.Icon:SetDesaturated(data.isHarmful and not data.isPlayerAura and not UnitIsUnit(unit, "player")) + end + + if button.Animation then + if data.isDispelable or data.isStealable then + button.Animation:Play() + button.Animation.playing = true + else + button.Animation:Stop() + button.Animation.playing = false + end + end + end + + --[[ Callback: aura_proto:Update(unit, data, index) + Update the aura button and its components. + + * self - the widget holding the aura buttons + * unit - the unit on which the aura is cast (string) + * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + * index - the actual position of the aura button (number) + ]]-- + function aura_proto:Update(unit, data, index) + if not data.name then return end + + local button = self[index] + if not button then + button = Filger:CreateButton(self, index) + + table.insert(self, button) + + self.createdButtons = self.createdButtons + 1 + end + + -- tooltips + button.unit = unit + button.filter = self.filter + button.auraInstanceID = data.auraInstanceID + button.auraIndex = data.auraIndex + button.isHarmful = data.isHarmful + + -- timers + button.name = data.name + button.spellId = data.spellId + button.duration = data.duration or 0 + button.expirationTime = data.expirationTime or GetTime() + + if button.Cooldown then + if data.duration and data.duration > 0 then + button.Cooldown:SetCooldown(data.expirationTime - data.duration, data.duration, data.timeMod) + button.Cooldown:Show() + else + button.Cooldown:Hide() + end + end + + if button.Icon then + if data.icon then + button.Icon:SetTexture(data.icon) + button.Icon:Show() + else + button.Icon:Hide() + end + end + + if button.Count then + button.Count:SetText((data.applications and data.applications > 1) and data.applications or "") + end + + if data.duration and data.duration > 0 then + button:SetScript("OnUpdate", button.OnUpdate) + end + + button:Show() + + self:UpdateColor(button, unit, data) + end + + --[[ Override: aura_proto.SortAuras(a, b) + Defines a custom sorting algorithm for ordering the auras. + + Defaults to [AuraUtil.DefaultAuraCompare](https://github.com/Gethe/wow-ui-source/search?q=DefaultAuraCompare). + + Overridden by the more specific SortAuras overrides if they are defined. + --]] + aura_proto.SortAuras = function(a, b) + if (a.priority ~= b.priority) then + return a.priority > b.priority + end + + if (a.expirationTime ~= b.expirationTime) then + return a.expirationTime < b.expirationTime + end + + -- if (a.canApplyAura ~= b.canApplyAura) then + -- return a.canApplyAura + -- end + + return a.auraInstanceID < b.auraInstanceID + end + + --[[ Override: aura_proto:FilterAura(unit, data) + Defines a custom filter that controls if the aura button should be shown. + + * self - the widget holding the aura buttons + * unit - the unit for which the update has been triggered (string) + * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + + ## Returns + + * show - indicates whether the aura button should be shown (boolean) + --]] + function aura_proto:FilterAura(unit, data) + return data.enabled and data.applications >= data.stackThreshold + end + + function aura_proto:GetAuraIndex(unit, auraInstanceID, filter) + local index = 1 + repeat + local data = C_UnitAuras.GetAuraDataByIndex(unit, index, filter) + if not data then + return nil + elseif data.auraInstanceID == auraInstanceID then + return index + end + index = index + 1 + until (not data) + return nil + end + + --[[ Override: aura_proto:ProcessData(unit, data) + Add some custom value sto the UnitAuraInfo. + + * self - the widget holding the aura buttons + * unit - the unit for which the update has been triggered (string) + * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + + ## Returns + + * data - a custom 'UnitAuraInfo' object (table) + --]] + function aura_proto:ProcessData(unit, data) + if not data then return end + + if self.restricted then + -- unit comparisons may be secret while restricted + data.isPlayerAura = data.isFromPlayerOrPlayerPet + else + data.isPlayerAura = data.sourceUnit and (UnitIsUnit("player", data.sourceUnit) or UnitIsOwnerOrControllerOfUnit("player", data.sourceUnit)) + end + + data.dispelName = LibDispel:GetDispelType(data.spellId, data.dispelName) + data.isDispelable = LibDispel:IsDispelable(unit, data.spellId, data.dispelName, data.isHarmful) + + local spell = self.spells[data.spellId] + if spell then + data.enabled = spell.enabled or false + data.priority = spell.priority or 0 + data.stackThreshold = spell.stackThreshold or 0 + else + data.enabled = true + data.priority = 0 + data.stackThreshold = 0 + end + + if blacklist[data.spellId] or blacklist[data.name] then + data.enabled = false + end + + if Filger.isVanilla then + data.auraIndex = self:GetAuraIndex(unit, data.auraInstanceID, self.filter) + end + + return data + end + + -- Retail: while auras are secret, tainted code can't list them; only spells whose aura + -- isn't secret can be looked up one by one, so only listed spells are shown. + function aura_proto:ScanSpellAuras(unit) + local isHarmful = self.filter:find("HARMFUL") ~= nil + local isPlayer = self.filter:find("PLAYER") ~= nil + + for spellId in next, self.spells do + if not C_Secrets.ShouldSpellAuraBeSecret(spellId) then + local data = C_UnitAuras.GetUnitAuraBySpellID(unit, spellId) + if data and data.isHarmful == isHarmful and (not isPlayer or data.isFromPlayerOrPlayerPet) then + data = self:ProcessData(unit, data) + self.all[data.auraInstanceID] = data + + if self:FilterAura(unit, data) then + self.actives[data.auraInstanceID] = true + end + end + end + end + end + + function aura_proto:UpdateAuras(event, unit, updateInfo) + if (self.unit ~= unit) then return end + + local isRestricted = C_Secrets and C_Secrets.ShouldAurasBeSecret() + -- updateInfo is secret while restricted; cached auras are stale right after it + local isFullUpdate = isRestricted or self.restricted or not updateInfo or updateInfo.isFullUpdate + self.restricted = isRestricted + + local changed = false + + if isFullUpdate then + self.all = table.wipe(self.all or {}) + self.actives = table.wipe(self.actives or {}) + changed = true + + if isRestricted then + self:ScanSpellAuras(unit) + else + local slots = { C_UnitAuras.GetAuraSlots(unit, self.filter) } + for i = 2, #slots do -- #1 return is continuationToken, we don't care about it + local data = self:ProcessData(unit, C_UnitAuras.GetAuraDataBySlot(unit, slots[i])) + self.all[data.auraInstanceID] = data + + if self:FilterAura(unit, data) then + self.actives[data.auraInstanceID] = true + end + end + end + else + if updateInfo.addedAuras then + for _, data in next, updateInfo.addedAuras do + if not C_UnitAuras.IsAuraFilteredOutByInstanceID(unit, data.auraInstanceID, self.filter) then + self.all[data.auraInstanceID] = self:ProcessData(unit, data) + if self:FilterAura(unit, data) then + self.actives[data.auraInstanceID] = true + changed = true + end + end + end + end + + if updateInfo.updatedAuraInstanceIDs then + for _, auraInstanceID in next, updateInfo.updatedAuraInstanceIDs do + local data = self.all[auraInstanceID] + if data then + self.all[auraInstanceID] = self:ProcessData(unit, C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID)) + if self:FilterAura(unit, data) then + self.actives[auraInstanceID] = true + changed = true + end + end + end + end + + if updateInfo.removedAuraInstanceIDs then + for _, auraInstanceID in next, updateInfo.removedAuraInstanceIDs do + if self.all[auraInstanceID] then + self.all[auraInstanceID] = nil + end + + if self.actives[auraInstanceID] then + self.actives[auraInstanceID] = nil + changed = true + end + end + end + end + + if changed then + -- instead of removing auras one by one, just wipe the tables entirely + -- and repopulate them, multiple table.remove calls are insanely slow + self.sorted = table.wipe(self.sorted or {}) + + for auraInstanceID, _ in next, self.actives do + table.insert(self.sorted, self.all[auraInstanceID]) + end + + table.sort(self.sorted, self.SortAuras) + + local numVisible = math.min(self.limit, #self.sorted) + + for i = 1, numVisible do + self:Update(unit, self.sorted[i], i) + end + + local visibleChanged = true + + if (numVisible ~= self.visibleButtons) then + self.visibleButtons = numVisible + visibleChanged = true + end + + for i = numVisible + 1, #self do + self[i]:Hide() + end + + if (visibleChanged or self.createdButtons > self.anchoredButtons) then + if visibleChanged then + self:SetPosition(1, numVisible) + else + self:SetPosition(self.anchoredButtons + 1, self.createdButtons) + self.anchoredButtons = self.createdButtons + end + end + end + end + + function aura_proto:ForceUpdate() + self:UpdateAuras("ForceUpdate", self.unit, nil) + end + + function aura_proto:UNIT_AURA(unit, updateInfo) + self:UpdateAuras("UNIT_AURA", unit, updateInfo) + end + + function aura_proto:PLAYER_TARGET_CHANGED(...) + self:ForceUpdate() + end + + function aura_proto:PLAYER_FOCUS_CHANGED(...) + self:ForceUpdate() + end +end + +ns.aura_proto = aura_proto + +-------------------------------------------------- +-- FRAMES +-------------------------------------------------- +local class = Filger.class +local spells = Filger.spells or {} +local all = Filger.all or {} + +local layout = Filger.config.layout +local size, spacing = layout.size, layout.spacing +local xOffset, yOffset = layout.xOffset, layout.yOffset + +Mixin(Filger.config.frames, { + -- LEFT + ["PLAYER_DEBUFFS"] = { + enabled = true, + unit = "player", + filter = "HARMFUL", + anchor = { "RIGHT", UIParent, "CENTER", -xOffset, yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMRIGHT", + ["growth-x"] = "LEFT", + ["growth-y"] = "DOWN", + spells = { + [124275] = { enabled = true, priority = 3 }, -- Light Stagger + [124274] = { enabled = true, priority = 3 }, -- Moderate Stagger + [124273] = { enabled = true, priority = 3 } -- Heavy Stagger + } + }, + ["PLAYER_AURAS"] = { + enabled = true, + unit = "player", + filter = "PLAYER|HELPFUL", + anchor = { "RIGHT", UIParent, "CENTER", -xOffset, 0 }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMRIGHT", + ["growth-x"] = "LEFT", + ["growth-y"] = "DOWN", + spells = Mixin(spells[class] or {}, spells["ALL"] or {}) + }, + ["PLAYER_BUFFS"] = { + enabled = true, + unit = "player", + filter = "HELPFUL", + caster = "all", + hidePlayer = true, + anchor = { "RIGHT", UIParent, "CENTER", -xOffset, -yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMRIGHT", + ["growth-x"] = "LEFT", + ["growth-y"] = "DOWN", + FilterAura = function(element, unit, data) + return data.enabled and not data.isPlayerAura + end + }, + ["TARGET_DEBUFFS"] = { + enabled = true, + unit = "target", + filter = "HARMFUL", + anchor = { "LEFT", UIParent, "CENTER", xOffset, yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + spells = all, + FilterAura = function(element, unit, data) + if UnitCanAttack(unit, "player") then + return data.enabled and data.isPlayerAura + end + return data.enabled and not data.isPlayerAura + end, + SortAuras = function(a, b) + if (a.priority ~= b.priority) then + return a.priority < b.priority + end + + if (a.expirationTime ~= b.expirationTime) then + return a.expirationTime < b.expirationTime + end + + if (a.isPlayerAura ~= b.isPlayerAura) then + return a.isPlayerAura + end + + return a.auraInstanceID < b.auraInstanceID + end + }, + ["TARGET_AURAS"] = { + enabled = true, + unit = "target", + filter = "HELPFUL", + anchor = { "LEFT", UIParent, "CENTER", xOffset, 0 }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + spells = all + }, + ["FOCUS_DEBUFFS"] = { + enabled = true, + unit = "focus", + filter = "HARMFUL", + anchor = { "LEFT", UIParent, "CENTER", xOffset, -yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + desaturated = false, + FilterAura = function(element, unit, data) + return data.enabled and data.isBossAura or (data.sourceUnit == nil) or not data.isFromPlayerOrPlayerPet + end + }, + ["BOSS_DEBUFFS"] = { + enabled = false, + unit = "player", + filter = "HARMFUL", + anchor = { "CENTER", UIParent, "CENTER", 0, 160 }, + limit = 1, + size = math.floor(1.5 * size), + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + FilterAura = function(element, unit, data) + return data.enabled and data.isBossAura or (data.sourceUnit == nil) + end + }, +}) diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua new file mode 100644 index 0000000..2365356 --- /dev/null +++ b/core/Mainline/auras.lua @@ -0,0 +1,506 @@ +local _, ns = ... +local Filger = ns.Filger + +local LibDispel = LibStub("LibDispel") +assert(LibDispel, "Filger requires LibDispel") + +local blacklist = ns.Filger.blacklist or {} + +-- Blizzard +local UnitIsUnit = _G.UnitIsUnit +local UnitIsOwnerOrControllerOfUnit = _G.UnitIsOwnerOrControllerOfUnit +local C_Secrets = _G.C_Secrets + +-------------------------------------------------- +-- AURAS +-------------------------------------------------- +local aura_proto = { + createdButtons = 0, + anchoredButtons = 0, + visibleButtons = 0 +} + +do + --[[ Callback: button:UpdateColor(button, unit, data) + Called after the aura button has been updated. + + * self - the widget holding the aura buttons + * button - the updated aura button (Button) + * unit - the unit on which the aura is cast (string) + * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + * index - the actual position of the aura button (number) + ]]-- + function aura_proto:UpdateColor(button, unit, data) + if button.Backdrop then + if (data.isHarmful and UnitCanAssist("player", unit)) or (data.isHelpful and UnitCanAttack(unit, "player") and UnitCanAttack("player", unit)) then + local color = Filger.colors[data.dispelName] + button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) + else + local color = Filger.config.general.backdrop.color + button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) + end + end + + if button.Icon and self.desaturated == true then + button.Icon:SetDesaturated(data.isHarmful and not data.isPlayerAura and not UnitIsUnit(unit, "player")) + end + + if button.Animation then + if data.isDispelable or data.isStealable then + button.Animation:Play() + button.Animation.playing = true + else + button.Animation:Stop() + button.Animation.playing = false + end + end + end + + --[[ Callback: aura_proto:Update(unit, data, index) + Update the aura button and its components. + + * self - the widget holding the aura buttons + * unit - the unit on which the aura is cast (string) + * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + * index - the actual position of the aura button (number) + ]]-- + function aura_proto:Update(unit, data, index) + if not data.name then return end + + local button = self[index] + if not button then + button = Filger:CreateButton(self, index) + + table.insert(self, button) + + self.createdButtons = self.createdButtons + 1 + end + + -- tooltips + button.unit = unit + button.filter = self.filter + button.auraInstanceID = data.auraInstanceID + button.auraIndex = data.auraIndex + button.isHarmful = data.isHarmful + + -- timers + button.name = data.name + button.spellId = data.spellId + button.duration = data.duration or 0 + button.expirationTime = data.expirationTime or GetTime() + + if button.Cooldown then + if data.duration and data.duration > 0 then + button.Cooldown:SetCooldown(data.expirationTime - data.duration, data.duration, data.timeMod) + button.Cooldown:Show() + else + button.Cooldown:Hide() + end + end + + if button.Icon then + if data.icon then + button.Icon:SetTexture(data.icon) + button.Icon:Show() + else + button.Icon:Hide() + end + end + + if button.Count then + button.Count:SetText((data.applications and data.applications > 1) and data.applications or "") + end + + if data.duration and data.duration > 0 then + button:SetScript("OnUpdate", button.OnUpdate) + end + + button:Show() + + self:UpdateColor(button, unit, data) + end + + --[[ Override: aura_proto.SortAuras(a, b) + Defines a custom sorting algorithm for ordering the auras. + + Defaults to [AuraUtil.DefaultAuraCompare](https://github.com/Gethe/wow-ui-source/search?q=DefaultAuraCompare). + + Overridden by the more specific SortAuras overrides if they are defined. + --]] + aura_proto.SortAuras = function(a, b) + if (a.priority ~= b.priority) then + return a.priority > b.priority + end + + if (a.expirationTime ~= b.expirationTime) then + return a.expirationTime < b.expirationTime + end + + -- if (a.canApplyAura ~= b.canApplyAura) then + -- return a.canApplyAura + -- end + + return a.auraInstanceID < b.auraInstanceID + end + + --[[ Override: aura_proto:FilterAura(unit, data) + Defines a custom filter that controls if the aura button should be shown. + + * self - the widget holding the aura buttons + * unit - the unit for which the update has been triggered (string) + * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + + ## Returns + + * show - indicates whether the aura button should be shown (boolean) + --]] + function aura_proto:FilterAura(unit, data) + return data.enabled and data.applications >= data.stackThreshold + end + + function aura_proto:GetAuraIndex(unit, auraInstanceID, filter) + local index = 1 + repeat + local data = C_UnitAuras.GetAuraDataByIndex(unit, index, filter) + if not data then + return nil + elseif data.auraInstanceID == auraInstanceID then + return index + end + index = index + 1 + until (not data) + return nil + end + + --[[ Override: aura_proto:ProcessData(unit, data) + Add some custom value sto the UnitAuraInfo. + + * self - the widget holding the aura buttons + * unit - the unit for which the update has been triggered (string) + * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) + + ## Returns + + * data - a custom 'UnitAuraInfo' object (table) + --]] + function aura_proto:ProcessData(unit, data) + if not data then return end + + if self.restricted then + -- unit comparisons may be secret while restricted + data.isPlayerAura = data.isFromPlayerOrPlayerPet + else + data.isPlayerAura = data.sourceUnit and (UnitIsUnit("player", data.sourceUnit) or UnitIsOwnerOrControllerOfUnit("player", data.sourceUnit)) + end + + data.dispelName = LibDispel:GetDispelType(data.spellId, data.dispelName) + data.isDispelable = LibDispel:IsDispelable(unit, data.spellId, data.dispelName, data.isHarmful) + + local spell = self.spells[data.spellId] + if spell then + data.enabled = spell.enabled or false + data.priority = spell.priority or 0 + data.stackThreshold = spell.stackThreshold or 0 + else + data.enabled = true + data.priority = 0 + data.stackThreshold = 0 + end + + if blacklist[data.spellId] or blacklist[data.name] then + data.enabled = false + end + + if Filger.isVanilla then + data.auraIndex = self:GetAuraIndex(unit, data.auraInstanceID, self.filter) + end + + return data + end + + -- Retail: while auras are secret, tainted code can't list them; only spells whose aura + -- isn't secret can be looked up one by one, so only listed spells are shown. + function aura_proto:ScanSpellAuras(unit) + local isHarmful = self.filter:find("HARMFUL") ~= nil + local isPlayer = self.filter:find("PLAYER") ~= nil + + for spellId in next, self.spells do + if not C_Secrets.ShouldSpellAuraBeSecret(spellId) then + local data = C_UnitAuras.GetUnitAuraBySpellID(unit, spellId) + if data and data.isHarmful == isHarmful and (not isPlayer or data.isFromPlayerOrPlayerPet) then + data = self:ProcessData(unit, data) + self.all[data.auraInstanceID] = data + + if self:FilterAura(unit, data) then + self.actives[data.auraInstanceID] = true + end + end + end + end + end + + function aura_proto:UpdateAuras(event, unit, updateInfo) + if (self.unit ~= unit) then return end + + local isRestricted = C_Secrets and C_Secrets.ShouldAurasBeSecret() + -- updateInfo is secret while restricted; cached auras are stale right after it + local isFullUpdate = isRestricted or self.restricted or not updateInfo or updateInfo.isFullUpdate + self.restricted = isRestricted + + local changed = false + + if isFullUpdate then + self.all = table.wipe(self.all or {}) + self.actives = table.wipe(self.actives or {}) + changed = true + + if isRestricted then + self:ScanSpellAuras(unit) + else + local slots = { C_UnitAuras.GetAuraSlots(unit, self.filter) } + for i = 2, #slots do -- #1 return is continuationToken, we don't care about it + local data = self:ProcessData(unit, C_UnitAuras.GetAuraDataBySlot(unit, slots[i])) + self.all[data.auraInstanceID] = data + + if self:FilterAura(unit, data) then + self.actives[data.auraInstanceID] = true + end + end + end + else + if updateInfo.addedAuras then + for _, data in next, updateInfo.addedAuras do + if not C_UnitAuras.IsAuraFilteredOutByInstanceID(unit, data.auraInstanceID, self.filter) then + self.all[data.auraInstanceID] = self:ProcessData(unit, data) + if self:FilterAura(unit, data) then + self.actives[data.auraInstanceID] = true + changed = true + end + end + end + end + + if updateInfo.updatedAuraInstanceIDs then + for _, auraInstanceID in next, updateInfo.updatedAuraInstanceIDs do + local data = self.all[auraInstanceID] + if data then + self.all[auraInstanceID] = self:ProcessData(unit, C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID)) + if self:FilterAura(unit, data) then + self.actives[auraInstanceID] = true + changed = true + end + end + end + end + + if updateInfo.removedAuraInstanceIDs then + for _, auraInstanceID in next, updateInfo.removedAuraInstanceIDs do + if self.all[auraInstanceID] then + self.all[auraInstanceID] = nil + end + + if self.actives[auraInstanceID] then + self.actives[auraInstanceID] = nil + changed = true + end + end + end + end + + if changed then + -- instead of removing auras one by one, just wipe the tables entirely + -- and repopulate them, multiple table.remove calls are insanely slow + self.sorted = table.wipe(self.sorted or {}) + + for auraInstanceID, _ in next, self.actives do + table.insert(self.sorted, self.all[auraInstanceID]) + end + + table.sort(self.sorted, self.SortAuras) + + local numVisible = math.min(self.limit, #self.sorted) + + for i = 1, numVisible do + self:Update(unit, self.sorted[i], i) + end + + local visibleChanged = true + + if (numVisible ~= self.visibleButtons) then + self.visibleButtons = numVisible + visibleChanged = true + end + + for i = numVisible + 1, #self do + self[i]:Hide() + end + + if (visibleChanged or self.createdButtons > self.anchoredButtons) then + if visibleChanged then + self:SetPosition(1, numVisible) + else + self:SetPosition(self.anchoredButtons + 1, self.createdButtons) + self.anchoredButtons = self.createdButtons + end + end + end + end + + function aura_proto:ForceUpdate() + self:UpdateAuras("ForceUpdate", self.unit, nil) + end + + function aura_proto:UNIT_AURA(unit, updateInfo) + self:UpdateAuras("UNIT_AURA", unit, updateInfo) + end + + function aura_proto:PLAYER_TARGET_CHANGED(...) + self:ForceUpdate() + end + + function aura_proto:PLAYER_FOCUS_CHANGED(...) + self:ForceUpdate() + end +end + +ns.aura_proto = aura_proto + +-------------------------------------------------- +-- FRAMES +-------------------------------------------------- +local class = Filger.class +local spells = Filger.spells or {} +local all = Filger.all or {} + +local layout = Filger.config.layout +local size, spacing = layout.size, layout.spacing +local xOffset, yOffset = layout.xOffset, layout.yOffset + +Mixin(Filger.config.frames, { + -- LEFT + ["PLAYER_DEBUFFS"] = { + enabled = true, + unit = "player", + filter = "HARMFUL", + anchor = { "RIGHT", UIParent, "CENTER", -xOffset, yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMRIGHT", + ["growth-x"] = "LEFT", + ["growth-y"] = "DOWN", + spells = { + [124275] = { enabled = true, priority = 3 }, -- Light Stagger + [124274] = { enabled = true, priority = 3 }, -- Moderate Stagger + [124273] = { enabled = true, priority = 3 } -- Heavy Stagger + } + }, + ["PLAYER_AURAS"] = { + enabled = true, + unit = "player", + filter = "PLAYER|HELPFUL", + anchor = { "RIGHT", UIParent, "CENTER", -xOffset, 0 }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMRIGHT", + ["growth-x"] = "LEFT", + ["growth-y"] = "DOWN", + spells = Mixin(spells[class] or {}, spells["ALL"] or {}) + }, + ["PLAYER_BUFFS"] = { + enabled = true, + unit = "player", + filter = "HELPFUL", + caster = "all", + hidePlayer = true, + anchor = { "RIGHT", UIParent, "CENTER", -xOffset, -yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMRIGHT", + ["growth-x"] = "LEFT", + ["growth-y"] = "DOWN", + FilterAura = function(element, unit, data) + return data.enabled and not data.isPlayerAura + end + }, + ["TARGET_DEBUFFS"] = { + enabled = true, + unit = "target", + filter = "HARMFUL", + anchor = { "LEFT", UIParent, "CENTER", xOffset, yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + spells = all, + FilterAura = function(element, unit, data) + if UnitCanAttack(unit, "player") then + return data.enabled and data.isPlayerAura + end + return data.enabled and not data.isPlayerAura + end, + SortAuras = function(a, b) + if (a.priority ~= b.priority) then + return a.priority < b.priority + end + + if (a.expirationTime ~= b.expirationTime) then + return a.expirationTime < b.expirationTime + end + + if (a.isPlayerAura ~= b.isPlayerAura) then + return a.isPlayerAura + end + + return a.auraInstanceID < b.auraInstanceID + end + }, + ["TARGET_AURAS"] = { + enabled = true, + unit = "target", + filter = "HELPFUL", + anchor = { "LEFT", UIParent, "CENTER", xOffset, 0 }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + spells = all + }, + ["FOCUS_DEBUFFS"] = { + enabled = true, + unit = "focus", + filter = "HARMFUL", + anchor = { "LEFT", UIParent, "CENTER", xOffset, -yOffset }, + limit = 8, + size = size, + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + desaturated = false, + FilterAura = function(element, unit, data) + return data.enabled and data.isBossAura or (data.sourceUnit == nil) or not data.isFromPlayerOrPlayerPet + end + }, + ["BOSS_DEBUFFS"] = { + enabled = false, + unit = "player", + filter = "HARMFUL", + anchor = { "CENTER", UIParent, "CENTER", 0, 160 }, + limit = 1, + size = math.floor(1.5 * size), + spacing = spacing, + initialAnchor = "BOTTOMLEFT", + ["growth-x"] = "RIGHT", + ["growth-y"] = "DOWN", + FilterAura = function(element, unit, data) + return data.enabled and data.isBossAura or (data.sourceUnit == nil) + end + }, +}) diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 528d7f1..8f3cd26 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -56,11 +56,15 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] Blocked by Experiment 2: no instance ID is reachable from tainted code in combat - [x] Secret cooldown timer text: a `C_DurationUtil` duration text binding on `button.Time`, with a seconds formatter (`2m`, `45s`, `4.3`) and a step color curve (red under 5 s). Verified on Retail - in combat (2026-10-21). Blizzard countdown numbers stay hidden. -- [ ] **Phase 1: family split, no behavior change** - - [ ] Move `aura_proto` and aura frame definitions to `core/Classic/`; add `core\[Family]\...` to the TOC - - [ ] `core/Mainline/` starts as a copy - - [ ] Experiment 9: `[Family]` resolves on every client, Forever included + in combat (2026-10-21). Blizzard countdown numbers stay hidden. Created on Mainline only: Classic + bindings have no `SetTextColorCurve` (`APIDoc/DurationTextBindingObjectAPIDocumentation.lua`, + crashed on MoP 2026-10-21). +- [x] **Phase 1: family split, no behavior change** + - [x] Move `aura_proto` and aura frame definitions to `core/Classic/auras.lua` (`ns.aura_proto`, + frames added to `Filger.config.frames`); add `core\[Family]\auras.lua` to the TOC. Shared layout + numbers live in `Filger.config.layout` + - [x] `core/Mainline/auras.lua` starts as a copy + - [x] Experiment 9: `[Family]` resolves on every client (Forever and Titan not tested) - [ ] **Phase 2: Mainline container for PLAYER_AURAS and PLAYER_BUFFS** - [ ] Experiment 4: container test addon - [ ] Experiment 5: filter strings on Retail and each Classic client @@ -517,6 +521,8 @@ combat** (target dummy, then a dungeon boss). 9. **`[Family]` loading.** Add a one-line `print` to each `core//` stub. `/reload` on every client: Retail and Forever must print Mainline, the rest Classic, and exactly one line must print per client. Out of combat is fine. + - **Result (2026-10-21):** Retail prints `Mainline`; Era, TBC and MoP print `Classic`, one line + each. Forever and Titan not tested (no client). ## Open questions for you From 733762d02f21743198e54bc9247a2916d953ca07 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Thu, 22 Oct 2026 08:57:10 -0300 Subject: [PATCH 04/13] use aura containers for player auras and buffs on mainline, record phase 2 results - PLAYER_AURAS: HELPFUL|PLAYER + includeSpellIDs from the class spell list - split Filger:StyleButton out of CreateButton so container buttons share the style - secret-safe duration text options for container buttons - name frames from their config key (FilgerPlayerAuras, ...) instead of unit + index - /filger auras lists your auras with spell IDs; unknown command shows help - data: add Transcendence, Elevated Stagger, Mudborne; blacklist Flurry Charge, August Blessing --- Filger.lua | 70 ++++++++++++++------- core/Mainline/auras.lua | 114 +++++++++++++++++++++++++++++++--- core/commands.lua | 37 +++++++++-- data/Standard/blacklist.lua | 2 + data/Standard/spells.lua | 5 ++ docs/secret-values-options.md | 8 ++- 6 files changed, 198 insertions(+), 38 deletions(-) diff --git a/Filger.lua b/Filger.lua index 74d5351..c9998a9 100644 --- a/Filger.lua +++ b/Filger.lua @@ -39,6 +39,14 @@ do timeColorCurve:AddPoint(0, CreateColor(0.99, 0.31, 0.31)) timeColorCurve:AddPoint(5, CreateColor(0.84, 0.75, 0.65)) + -- aura container duration text (Mainline); Classic bindings have no SetTextColorCurve + if Filger.isMainline then + Filger.timeTextOptions = { + textFormatter = timeFormatter, + textColor = { curve = timeColorCurve, property = Enum.DurationTextBindingProperty.RemainingDuration } + } + end + function button_proto:OnEnter() if (GameTooltip:IsForbidden() and not self:IsVisible()) then return end @@ -103,25 +111,19 @@ do end end - --[[ Callback: button_proto:CreateButton(element, index) - Creates an aura/cooldown button at a given position. - - * self - the widget holding the aura buttons - * index - the position at which the aura button is to be created (number) - - ## Returns - * button - the button used to represent the aura (Button) + --[[ Filger:StyleButton(button) + Creates the button's visual regions (backdrop, cooldown, icon, count, time text). + Shared by Filger buttons and aura container buttons. --]] - function Filger:CreateButton(element, index) + function Filger:StyleButton(button) -- resources local font, fontSize, fontStyle = Filger.fonts.normal, 12, "OUTLINE" + -- aura container buttons are unnamed; "$parent" would create globals like "Icon" + local name = button:GetName() - local button = Mixin(CreateFrame("Frame", element:GetName() .. "Aura" .. index, element), button_proto) - button:SetWidth(element.size) - button:SetHeight(element.size) button:CreateBackdrop() - local cooldown = CreateFrame("Cooldown", "$parentCD", button, "CooldownFrameTemplate") + local cooldown = CreateFrame("Cooldown", name and name .. "CD", button, "CooldownFrameTemplate") cooldown:SetAllPoints() cooldown:SetReverse(true) cooldown:SetDrawEdge(false) @@ -130,29 +132,47 @@ do -- cooldown.noCooldownCount = true button.Cooldown = cooldown - local icon = button:CreateTexture("$parentIcon", "ARTWORK") + local icon = button:CreateTexture(name and name .. "Icon", "ARTWORK") icon:SetAllPoints() icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) button.Icon = icon - local overlay = CreateFrame("Frame", "$parentOverlay", button, nil) + local overlay = CreateFrame("Frame", name and name .. "Overlay", button, nil) overlay:SetAllPoints(button) overlay:SetFrameLevel(cooldown:GetFrameLevel() + 1) button.Overlay = overlay - local count = overlay:CreateFontString("$parentCount", "OVERLAY") + local count = overlay:CreateFontString(name and name .. "OverlayCount", "OVERLAY") count:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0) count:SetFont(font, fontSize, fontStyle) count:SetJustifyH("RIGHT") count:SetTextColor(0.84, 0.75, 0.65) button.Count = count - local time = overlay:CreateFontString("$parentTime", "OVERLAY") + local time = overlay:CreateFontString(name and name .. "OverlayTime", "OVERLAY") time:SetPoint("CENTER", button, "CENTER", 0, 0) time:SetFont(font, 20, fontStyle) time:SetJustifyH("CENTER") time:SetTextColor(0.84, 0.75, 0.65) button.Time = time + end + + --[[ Callback: button_proto:CreateButton(element, index) + Creates an aura/cooldown button at a given position. + + * self - the widget holding the aura buttons + * index - the position at which the aura button is to be created (number) + + ## Returns + * button - the button used to represent the aura (Button) + --]] + function Filger:CreateButton(element, index) + local button = Mixin(CreateFrame("Frame", element:GetName() .. "Aura" .. index, element), button_proto) + button:SetWidth(element.size) + button:SetHeight(element.size) + self:StyleButton(button) + + local time = button.Time -- only secret cooldowns (Mainline) need it; Classic bindings have no SetTextColorCurve if Filger.isMainline then @@ -478,10 +498,10 @@ do end end - function Filger:Spawn(data) - local index = #self - local unit = data.unit:gsub("^%l", string.upper):gsub("t(arget)", "T%1") - local element = Mixin(CreateFrame("Frame", self:GetName() .. unit .. index, self), element_proto) + function Filger:Spawn(key, data) + -- "PLAYER_AURAS" -> "FilgerPlayerAuras" + local name = key:lower():gsub("^%l", string.upper):gsub("_(%l)", string.upper) + local element = Mixin(CreateFrame("Frame", self:GetName() .. name, self), element_proto) -- configuration element.unit = data.unit or "player" @@ -517,6 +537,10 @@ do element:RegisterEvent("SPELL_UPDATE_COOLDOWN") element:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", self.unit) + elseif data.groups then + -- Mainline only: Blizzard's secure aura container filters and displays the auras + element = Mixin(element, ns.container_proto) + element:CreateContainer(data) else element = Mixin(element, ns.aura_proto) @@ -571,9 +595,9 @@ function Filger:PLAYER_LOGIN() -- self:RegisterEvent("PLAYER_ENTERING_WORLD") -- create panels based on spell list - for _, data in pairs(self.config.frames) do + for key, data in pairs(self.config.frames) do if (data.enabled) then - local element = self:Spawn(data) + local element = self:Spawn(key, data) element:ForceUpdate() table.insert(self, element) end diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua index 2365356..6375ab6 100644 --- a/core/Mainline/auras.lua +++ b/core/Mainline/auras.lua @@ -365,6 +365,85 @@ end ns.aura_proto = aura_proto +-------------------------------------------------- +-- AURA CONTAINERS +-------------------------------------------------- +-- Blizzard's secure container filters, sorts and displays auras that are secret to Filger. +local container_proto = {} + +do + local AnchorUtil = _G.AnchorUtil + local AuraContainerSortMethod = _G.AuraContainerSortMethod + local C_XMLUtil = _G.C_XMLUtil + + local TEMPLATE = "CustomAuraContainerTemplate" + + -- container buttons can only be styled here: once auras are secret, tainted code can't touch them + local function CreateInitializer(element) + local size = element.size + local tooltip = Filger.config.general.gametooltip + + return function(button) + button:SetSize(size, size) + Filger:StyleButton(button) + + button:SetIcon(button.Icon) + button:SetDurationCooldown(button.Cooldown) + button:SetApplicationCount(button.Count) + button:SetDurationText(button.Time, Filger.timeTextOptions) + + if tooltip then + button:SetTooltipAnchorPoint("ANCHOR_RIGHT") + else + button:SetMouseMotionEnabled(false) + end + end + end + + function container_proto:CreateContainer(data) + if not C_XMLUtil.GetTemplateInfo(TEMPLATE) then + Filger:warn(TEMPLATE .. " not found, " .. self:GetName() .. " is disabled.") + return + end + + local container = CreateFrame("AuraContainer", self:GetName() .. "Container", self, TEMPLATE) + container:SetPoint(self.initialAnchor, self, self.initialAnchor) + container:SetFlowLayoutAnchorPoint(self.initialAnchor) + container:SetFlowLayoutGrowthDirection( + (self["growth-x"] == "LEFT") and AnchorUtil.FlowDirection.Left or AnchorUtil.FlowDirection.Right, + (self["growth-y"] == "DOWN") and AnchorUtil.FlowDirection.Down or AnchorUtil.FlowDirection.Up) + container:SetUnit(self.unit) + + local initializeFrame = CreateInitializer(self) + local layout = { + elementSpacing = self.spacing, + lineSpacing = self.spacing, + elementWidth = self.size, + elementHeight = self.size + } + + for i, group in ipairs(data.groups) do + container:AddAuraGroup(self:GetName() .. "Group" .. i, group.filter, { + maxFrameCount = self.limit, + candidateFilters = group.candidateFilters, + sortMethod = AuraContainerSortMethod.ExpirationOnly, + layout = layout, + initializeFrame = initializeFrame + }) + end + + self.container = container + end + + function container_proto:ForceUpdate() + if self.container then + self.container:UpdateAllAuras() + end + end +end + +ns.container_proto = container_proto + -------------------------------------------------- -- FRAMES -------------------------------------------------- @@ -376,6 +455,17 @@ local layout = Filger.config.layout local size, spacing = layout.size, layout.spacing local xOffset, yOffset = layout.xOffset, layout.yOffset +-- container candidate filters take { [spellId] = true }; name keys can't be matched +local function ToSpellIDs(list) + local ids = {} + for spellId, value in next, list do + if type(spellId) == "number" and (value == true or (type(value) == "table" and value.enabled)) then + ids[spellId] = true + end + end + return ids +end + Mixin(Filger.config.frames, { -- LEFT ["PLAYER_DEBUFFS"] = { @@ -398,7 +488,6 @@ Mixin(Filger.config.frames, { ["PLAYER_AURAS"] = { enabled = true, unit = "player", - filter = "PLAYER|HELPFUL", anchor = { "RIGHT", UIParent, "CENTER", -xOffset, 0 }, limit = 8, size = size, @@ -406,14 +495,18 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMRIGHT", ["growth-x"] = "LEFT", ["growth-y"] = "DOWN", - spells = Mixin(spells[class] or {}, spells["ALL"] or {}) + groups = { + { + filter = "HELPFUL|PLAYER", + candidateFilters = { + includeSpellIDs = ToSpellIDs(Mixin(spells[class] or {}, spells["ALL"] or {})) + } + } + } }, ["PLAYER_BUFFS"] = { enabled = true, unit = "player", - filter = "HELPFUL", - caster = "all", - hidePlayer = true, anchor = { "RIGHT", UIParent, "CENTER", -xOffset, -yOffset }, limit = 8, size = size, @@ -421,9 +514,14 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMRIGHT", ["growth-x"] = "LEFT", ["growth-y"] = "DOWN", - FilterAura = function(element, unit, data) - return data.enabled and not data.isPlayerAura - end + groups = { + { + filter = "HELPFUL|!PLAYER", + candidateFilters = { + excludeSpellIDs = ToSpellIDs(blacklist) + } + } + } }, ["TARGET_DEBUFFS"] = { enabled = true, diff --git a/core/commands.lua b/core/commands.lua index 300e2a4..077c686 100644 --- a/core/commands.lua +++ b/core/commands.lua @@ -1,6 +1,9 @@ local _, ns = ... local Filger = ns.Filger +-- Blizzard +local C_Secrets = _G.C_Secrets + ------------------------------------------------------------ -- Slash Commands ------------------------------------------------------------ @@ -12,9 +15,9 @@ SlashCmdList["FILGER"] = function(cmd) local command, arg = string.split(" ", msg, 2) arg = arg and arg:gsub(" ", "") - if commands[command] then - commands[command].func(arg) - end + -- empty or unknown command: show help + local entry = commands[command] or commands["help"] + entry.func(arg) end function Filger:AddCommand(command, handler, description) @@ -32,4 +35,30 @@ local help = function() end end -Filger:AddCommand("help", help) +Filger:AddCommand("help", help, "list commands") + +-- lists the unit's own auras with their spell IDs, to fill data//spells.lua +-- usage: /filger auras player buffs (arg arrives without spaces: "playerbuffs") +local auras = function(arg) + local unit, kind = (arg or ""):match("^(%a-)(d?e?buffs)$") + if not unit or unit == "" or (kind ~= "buffs" and kind ~= "debuffs") then + print(Filger.title, "usage: /filger auras ") + return + end + + -- tainted code can't read secret auras (in combat on Retail), GetAuraSlots would error + if C_Secrets and C_Secrets.ShouldAurasBeSecret() then + print(Filger.title, "auras are secret right now, leave combat and try again.") + return + end + + local filter = ((kind == "buffs") and "HELPFUL" or "HARMFUL") .. "|PLAYER" + for _, slot in ipairs({ select(2, C_UnitAuras.GetAuraSlots(unit, filter)) }) do + local data = C_UnitAuras.GetAuraDataBySlot(unit, slot) + if data then + print(Filger.title, data.spellId, data.name) + end + end +end + +Filger:AddCommand("auras", auras, "list your auras on a unit with their spell IDs (/filger auras player buffs)") diff --git a/data/Standard/blacklist.lua b/data/Standard/blacklist.lua index bb41f11..690904c 100644 --- a/data/Standard/blacklist.lua +++ b/data/Standard/blacklist.lua @@ -34,6 +34,8 @@ do [166646] = true, -- Windwalking [129914] = true, -- Combat Wisdom [450380] = true, -- Chi Wave + [451021] = true, -- Flurry Charge + [454494] = true, -- August Blessing -- PRIEST [21562] = true, -- Power Word: Fortitude diff --git a/data/Standard/spells.lua b/data/Standard/spells.lua index 0766cf5..5d2941e 100644 --- a/data/Standard/spells.lua +++ b/data/Standard/spells.lua @@ -51,6 +51,9 @@ local data = { [384267] = CreateSpellPriority(3), -- Siphon Storm }, ["MONK"] = { + -- All + [101643] = CreateSpellPriority(1), -- Transcendence + -- Mistweaver [432180] = CreateSpellPriority(false), -- Dance of the Wind [388193] = CreateSpellPriority(10), -- Jadefire Stomp @@ -60,6 +63,7 @@ local data = { [122278] = CreateSpellPriority(1), -- Dampen Harm [122783] = CreateSpellPriority(1), -- Diffuse Magic [322507] = CreateSpellPriority(3), -- Celestial Brew + [1260619] = CreateSpellPriority(3), -- Elevated Stagger [325092] = CreateSpellPriority(10), -- Purified Chi }, ["PALADIN"] = { @@ -154,6 +158,7 @@ local data = { [457925] = CreateSpellPriority(false), -- Venomous Potential (Seal of the Poisoned Pact) [462513] = CreateSpellPriority(8, 1), -- Severed Strands (Spymaster's Web) [449947] = CreateSpellPriority(20), -- Realigning Nexus Convergence Divergence (Treacherous Transmitter) + [1219102] = CreateSpellPriority(8), -- Mudborne (trinket) -- Consumables [431932] = CreateSpellPriority(5), -- Tempered Potion diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 8f3cd26..de44931 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -65,11 +65,13 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] numbers live in `Filger.config.layout` - [x] `core/Mainline/auras.lua` starts as a copy - [x] Experiment 9: `[Family]` resolves on every client (Forever and Titan not tested) -- [ ] **Phase 2: Mainline container for PLAYER_AURAS and PLAYER_BUFFS** +- [x] **Phase 2: Mainline container for PLAYER_AURAS and PLAYER_BUFFS** - [ ] Experiment 4: container test addon - [ ] Experiment 5: filter strings on Retail and each Classic client - - [ ] PLAYER_AURAS: `HELPFUL|PLAYER` + `includeSpellIDs` - - [ ] PLAYER_BUFFS: `HELPFUL|!PLAYER` + `excludeSpellIDs` + - [x] PLAYER_AURAS: `HELPFUL|PLAYER` + `includeSpellIDs` (strict allowlist: only listed, enabled + numeric spell IDs are shown) + - [x] PLAYER_BUFFS: `HELPFUL|!PLAYER` + `excludeSpellIDs` + - [x] Frames are named from their `config.frames` key (`FilgerPlayerAuras`, ...) - [ ] **Phase 3: remaining Mainline frames** - [ ] Experiment 6: private aura anchors - [ ] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS From 35ef972e782a255084ec4eb6a92f366fa0a0f6e9 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Thu, 22 Oct 2026 09:39:14 -0300 Subject: [PATCH 05/13] record experiments 4 and 5 results, add /filger exp4 and exp5 - /filger exp4 ,: four CustomAuraContainerTemplate test containers for the spell-ID gate (include/exclude on player buffs/debuffs, focus debuffs) - /filger exp5: aura counts per filter string on the player - docs: Retail results for exp4 (ID filters refused on player debuffs, as the gate table says) and exp5 (`!`, BIG_DEFENSIVE, IMPORTANT work; NOT_CANCELABLE ignored); MoP exp5 (no `!` negation, BIG_DEFENSIVE and IMPORTANT ignored, NOT_CANCELABLE works) - data: comment out Premonition of Piety and Vulcanic Blaze --- core/development.lua | 92 ++++++++++++++++++++++++++++++++--- data/Standard/spells.lua | 4 +- docs/secret-values-options.md | 41 ++++++++++++++-- 3 files changed, 126 insertions(+), 11 deletions(-) diff --git a/core/development.lua b/core/development.lua index 9835a7e..d18d5f4 100644 --- a/core/development.lua +++ b/core/development.lua @@ -1,6 +1,86 @@ -local _, ns = ... -local Filger = ns.Filger - ------------------------------------------------------------- --- Development (write anything here) ------------------------------------------------------------- +local _, ns = ... +local Filger = ns.Filger + +------------------------------------------------------------ +-- Development (write anything here) +------------------------------------------------------------ + +-- Experiment 4 (docs/secret-values-options.md): spell-ID gate of CustomAuraContainerTemplate +-- usage: /filger exp4 , (out of combat, then enter combat and compare) +local exp4 = function(arg) + local buff, debuff = (arg or ""):match("^(%d+),(%d+)$") + if not buff then + print(Filger.title, "usage: /filger exp4 ,") + return + elseif not C_XMLUtil or not C_XMLUtil.GetTemplateInfo("CustomAuraContainerTemplate") then + print(Filger.title, "exp4: no CustomAuraContainerTemplate on this client") + return + elseif InCombatLockdown() then + print(Filger.title, "exp4: leave combat first") + return + elseif _G.FilgerExp4_T1 then + print(Filger.title, "exp4: already created, /reload to change IDs") + return + end + buff, debuff = tonumber(buff), tonumber(debuff) + + -- CustomAuraButtonTemplate has no regions; without an icon the button draws nothing + local function initializeFrame(button) + button:SetSize(32, 32) + local icon = button:CreateTexture(nil, "ARTWORK") + icon:SetAllPoints() + button:SetIcon(icon) + local count = button:CreateFontString(nil, "OVERLAY", "NumberFontNormal") + count:SetPoint("BOTTOMRIGHT") + button:SetApplicationCount(count) + end + + local function Make(name, unit, filter, candidateFilters, y) + local c = CreateFrame("AuraContainer", "FilgerExp4_" .. name, UIParent, "CustomAuraContainerTemplate") + c:SetPoint("LEFT", UIParent, "CENTER", 0, y) + c:SetUnit(unit) + c:AddAuraGroup(name, filter, { + candidateFilters = candidateFilters, + initializeFrame = initializeFrame, + layout = { elementWidth = 32, elementHeight = 32, elementSpacing = 2, lineSpacing = 2 } + }) + + local label = UIParent:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("RIGHT", UIParent, "CENTER", -10, y) + label:SetText(name .. " " .. unit .. " " .. filter) + end + + -- expected in combat (Retail): T1 buff, T2 empty, T3 debuff still shown, T4 no error + Make("T1", "player", "HELPFUL|PLAYER", { includeSpellIDs = { [buff] = true } }, 120) + Make("T2", "player", "HARMFUL", { includeSpellIDs = { [debuff] = true } }, 80) + Make("T3", "player", "HARMFUL", { excludeSpellIDs = { [debuff] = true } }, 40) + Make("T4", "focus", "HARMFUL", nil, 0) + print(Filger.title, "exp4: created T1-T4, buff", buff, "debuff", debuff) +end + +Filger:AddCommand("exp4", exp4, "experiment 4: aura container spell-ID gate (/filger exp4 ,)") + +-- Experiment 5: which filter strings each client accepts and fills +-- usage: /filger exp5 (out of combat; GetAuraSlots errors on secret auras) +local filters = { + -- complementary pairs must add up to HELPFUL, so they can be checked solo + "HELPFUL", "HARMFUL", "HELPFUL|PLAYER", "HELPFUL|!PLAYER", "HELPFUL|CANCELABLE", "HELPFUL|!CANCELABLE", + "HELPFUL|NOT_CANCELABLE", "HELPFUL|BIG_DEFENSIVE", "HELPFUL|IMPORTANT", "HARMFUL|RAID_PLAYER_DISPELLABLE", + "HARMFUL|DISPELLABLE" +} + +local exp5 = function() + if C_Secrets and C_Secrets.ShouldAurasBeSecret() then + print(Filger.title, "exp5: auras are secret right now, leave combat first") + return + end + + for _, filter in ipairs(filters) do + -- unknown tokens may error; the first return is a continuation token, not a slot + local ok, n = pcall(function() return select("#", C_UnitAuras.GetAuraSlots("player", filter)) - 1 end) + -- "||" so chat doesn't read "|R" / "|N" as escape codes + print(Filger.title, (filter:gsub("|", "||")), ok and n or ("error: " .. tostring(n))) + end +end + +Filger:AddCommand("exp5", exp5, "experiment 5: aura counts per filter string on the player") diff --git a/data/Standard/spells.lua b/data/Standard/spells.lua index 5d2941e..159e1de 100644 --- a/data/Standard/spells.lua +++ b/data/Standard/spells.lua @@ -103,7 +103,7 @@ local data = { [198069] = CreateSpellPriority(9), -- Power of the Dark Side [428933] = CreateSpellPriority(10), -- Premonition of Insight (-cooldown) - [428930] = CreateSpellPriority(10), -- Premonition of Piety (+ healing) + -- [428930] = CreateSpellPriority(10), -- Premonition of Piety (+ healing) [428934] = CreateSpellPriority(10), -- Premonition of Solace (single heal to shield) -- Holy @@ -135,7 +135,7 @@ local data = { [470532] = CreateSpellPriority(7), -- Arc Discharge [375986] = CreateSpellPriority(7), -- Primordial Wave [187878] = CreateSpellPriority(7), -- Crash Lightning - [470058] = CreateSpellPriority(1), -- Vulcanic Blaze + -- [470058] = CreateSpellPriority(1), -- Vulcanic Blaze [201900] = CreateSpellPriority(1), -- Hot Hand [384411] = CreateSpellPriority(1), -- Static Accumulation [455110] = CreateSpellPriority(1), -- Supercharge diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index de44931..e2f6c97 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -66,8 +66,10 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - [x] `core/Mainline/auras.lua` starts as a copy - [x] Experiment 9: `[Family]` resolves on every client (Forever and Titan not tested) - [x] **Phase 2: Mainline container for PLAYER_AURAS and PLAYER_BUFFS** - - [ ] Experiment 4: container test addon - - [ ] Experiment 5: filter strings on Retail and each Classic client + - [x] Experiment 4: container spell-ID gate (Retail, in combat): passes, `includeSpellIDs` refused + and `excludeSpellIDs` ignored on player debuffs, as the gate table says + - [x] Experiment 5: filter strings (Retail): `!` negation, `BIG_DEFENSIVE` and `IMPORTANT` work; + `NOT_CANCELABLE` silently ignored. Classic not needed: its aura file keeps Lua filtering - [x] PLAYER_AURAS: `HELPFUL|PLAYER` + `includeSpellIDs` (strict allowlist: only listed, enabled numeric spell IDs are shown) - [x] PLAYER_BUFFS: `HELPFUL|!PLAYER` + `excludeSpellIDs` @@ -236,7 +238,7 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] |---|---|---|---|---|---| | Retail 12.1.5 (`standard`) | Yes (15 functions) | Yes | Yes | Yes | Full list incl. `IMPORTANT`, `DISPELLABLE`, `!` | | Forever 1.60.1 (`camelot`) | Yes (15) | Yes | Yes (`standard, camelot`) | Yes | Same as Retail (**UNVERIFIED** line by line) | -| MoP 5.5.4 | 0 | Yes | No (`standard` only) | No | Up to `BIG_DEFENSIVE`; keeps `NOT_CANCELABLE`; no `IMPORTANT`/`DISPELLABLE` | +| MoP 5.5.4 | 0 | Yes | No (`standard` only) | No | Up to `BIG_DEFENSIVE` in `AuraUtil.lua`, but in game (Experiment 5): no `!` negation, `BIG_DEFENSIVE` ignored; keeps `NOT_CANCELABLE`; no `IMPORTANT`/`DISPELLABLE` | | Titan WotLK 3.80.2 | 0 | Yes | No | No | Same as MoP (**VERIFIED** for MoP and Era; Titan/TBC assumed identical, **UNVERIFIED**) | | TBC 2.5.6 | 0 | Yes | No | No | See above | | Era 1.15.9 | 0 | Yes | No | No | Same as MoP | @@ -508,9 +510,42 @@ combat** (target dummy, then a dungeon boss). `excludeSpellIDs` (expected unfiltered) - `SetUnit("focus")` - Pass: no taint errors, and the filters behave as the gate table says. + - **Result (2026-10-22, Retail, Brewmaster, target dummy):** `/filger exp4 101643,124275` + (`core/development.lua`), Transcendence as the buff, Light Stagger as the debuff. + - Out of combat: T1 shows Transcendence. T2/T3 not checked (Stagger falls off after combat). + - In combat: T1 Transcendence; T2 (`HARMFUL` + include Stagger) **empty**; T3 (`HARMFUL` + + exclude Stagger) **shows Light Stagger**; T4 (focus `HARMFUL`) shows Keg Smash and Mystic Touch. + No errors reported. Matches the gate table: ID filters on debuffs on the player are refused. + - `CustomAuraButtonTemplate` has no regions. Without an `initializeFrame` that calls `SetIcon`, + buttons draw nothing (first run showed an empty T1). 5. **Filter strings.** `/run print(#{C_UnitAuras.GetAuraSlots("player","HELPFUL|!PLAYER")}-1)`, then the same for `HELPFUL|BIG_DEFENSIVE`, `HARMFUL|RAID_PLAYER_DISPELLABLE` and `HELPFUL|!CANCELABLE`. Run it with a known defensive up, on Retail and each Classic client. Out of combat is fine. + - Now `/filger exp5` (`core/development.lua`). Unknown tokens don't error, they're ignored: a count + equal to the `HELPFUL`/`HARMFUL` baseline means the token is unknown or matches everything. + - **Result (2026-10-22, Retail, solo, out of combat):** Celestial Brew, Fortifying Brew, Tiger's + Lust and a trinket buff up; no debuffs. + - `HELPFUL` 17; `PLAYER` 17 + `!PLAYER` 0 = 17; `CANCELABLE` 17 + `!CANCELABLE` 0 = 17. Negation + works. + - `NOT_CANCELABLE` 17 (= baseline): removed on Retail and silently ignored, no error. + - `BIG_DEFENSIVE` 1, `IMPORTANT` 1: applied by the server (below baseline). Which buff each + matched wasn't recorded. + - `RAID_PLAYER_DISPELLABLE`, `DISPELLABLE` 0: inconclusive, `HARMFUL` was 0. + - Not tested: a buff from another caster, a dispellable debuff. + - **Result (2026-10-22, MoP, solo, out of combat):** no debuffs. + - `HELPFUL` 7; `PLAYER` 6 works. + - `!` negation is **not supported**: `!PLAYER` 7 and `!CANCELABLE` 7 equal the baseline, so the + negated token is ignored. Confirmed with a Paladin's Hand of Freedom on the player: `HELPFUL` 4, + `PLAYER` 2, `!PLAYER` 4 (should be 2). + - `CANCELABLE` 7 + `NOT_CANCELABLE` 0 = 7: `NOT_CANCELABLE` works on MoP. + - `BIG_DEFENSIVE` 7 and `IMPORTANT` 7 (= baseline): ignored. This contradicts the client table, + which lists `BIG_DEFENSIVE` for MoP from `AuraUtil.lua`; the token is known to the UI code but + the server doesn't apply it. + - Dispel filters 0: inconclusive. + - Classic auras use only `HELPFUL`, `HARMFUL` and `PLAYER|HELPFUL` (`core/Classic/auras.lua`), + so nothing breaks today. Classic filters must not use `!` or the new flags. + - Era and TBC skipped: the container filter strings are Mainline only, and Classic keeps its + Lua filtering. 6. **Private aura anchors.** In a test addon, call `C_UnitAuras.AddPrivateAuraAnchor{ unitToken="player", auraIndex=1, parent=frame, showCooldownFrame=true, iconInfo={ iconAnchor={point="CENTER", relativeTo=frame, relativePoint="CENTER", offsetX=0, offsetY=0}, iconWidth=40, iconHeight=40 } }`. It From a27196eb34912f2aa8680d7d195eeab35ef11c1b Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Thu, 22 Oct 2026 23:26:09 -0300 Subject: [PATCH 06/13] test private auras --- core/Mainline/auras.lua | 10 +++----- core/development.lua | 43 +++++++++++++++++++++++++++++++++++ docs/secret-values-options.md | 18 ++++++++++----- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua index 6375ab6..33b3a2c 100644 --- a/core/Mainline/auras.lua +++ b/core/Mainline/auras.lua @@ -8,7 +8,6 @@ local blacklist = ns.Filger.blacklist or {} -- Blizzard local UnitIsUnit = _G.UnitIsUnit -local UnitIsOwnerOrControllerOfUnit = _G.UnitIsOwnerOrControllerOfUnit local C_Secrets = _G.C_Secrets -------------------------------------------------- @@ -186,12 +185,9 @@ do function aura_proto:ProcessData(unit, data) if not data then return end - if self.restricted then - -- unit comparisons may be secret while restricted - data.isPlayerAura = data.isFromPlayerOrPlayerPet - else - data.isPlayerAura = data.sourceUnit and (UnitIsUnit("player", data.sourceUnit) or UnitIsOwnerOrControllerOfUnit("player", data.sourceUnit)) - end + -- UnitIsUnit/UnitIsOwnerOrControllerOfUnit on sourceUnit can be secret even out of combat + -- (NPC sources, restricted maps), so rely on the aura's own flag + data.isPlayerAura = data.isFromPlayerOrPlayerPet data.dispelName = LibDispel:GetDispelType(data.spellId, data.dispelName) data.isDispelable = LibDispel:IsDispelable(unit, data.spellId, data.dispelName, data.isHarmful) diff --git a/core/development.lua b/core/development.lua index d18d5f4..d9cb07a 100644 --- a/core/development.lua +++ b/core/development.lua @@ -84,3 +84,46 @@ local exp5 = function() end Filger:AddCommand("exp5", exp5, "experiment 5: aura counts per filter string on the player") + +-- Experiment 6: private aura anchors from addon code +-- usage: /filger exp6 (anywhere; the private aura icon draws inside the grey boxes) +local exp6 = function() + if not C_UnitAuras.AddPrivateAuraAnchor then + print(Filger.title, "exp6: no AddPrivateAuraAnchor on this client") + return + elseif _G.FilgerExp6 then + print(Filger.title, "exp6: already created") + return + end + + local frame = CreateFrame("Frame", "FilgerExp6", UIParent) + frame:SetSize(3 * 44, 44) + frame:SetPoint("CENTER", UIParent, "CENTER", 0, 200) + + -- one anchor per private aura slot on the player + for i = 1, 3 do + local slot = CreateFrame("Frame", nil, frame) + slot:SetSize(40, 40) + slot:SetPoint("LEFT", frame, "LEFT", (i - 1) * 44, 0) + local bg = slot:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetColorTexture(0.3, 0.3, 0.3, 0.5) + + local ok, anchorID = pcall(C_UnitAuras.AddPrivateAuraAnchor, { + unitToken = "player", + auraIndex = i, + parent = slot, + showCooldownFrame = true, + showCountdownNumbers = true, + iconInfo = { + iconAnchor = { point = "CENTER", relativeTo = slot, relativePoint = "CENTER", offsetX = 0, offsetY = 0 }, + iconWidth = 40, + iconHeight = 40 + } + }) + print(Filger.title, "exp6: slot", i, "aurasSecret", C_Secrets and C_Secrets.ShouldAurasBeSecret(), + ok and "anchorID" or "error", tostring(anchorID)) + end +end + +Filger:AddCommand("exp6", exp6, "experiment 6: private aura anchors on the player") diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index e2f6c97..496055f 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -75,9 +75,10 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - [x] PLAYER_BUFFS: `HELPFUL|!PLAYER` + `excludeSpellIDs` - [x] Frames are named from their `config.frames` key (`FilgerPlayerAuras`, ...) - [ ] **Phase 3: remaining Mainline frames** - - [ ] Experiment 6: private aura anchors + - [x] Experiment 6: private aura anchors: **pass**, addon code can add them in combat while auras + are secret - [ ] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS - - [ ] BOSS_DEBUFFS (`isBossAura`, private-aura anchors if Experiment 6 passes) + - [ ] BOSS_DEBUFFS (`isBossAura` plus private-aura anchors, Experiment 6) - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss groups (Stagger dropped, Experiment 3) - [ ] Delete the copied event path from `core/Mainline/` - [ ] **Other experiments** @@ -229,8 +230,8 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - Cooldown Manager: addons can read the static catalog; the live state is secret. See cooldown-manager.md §4–§8. - Private auras (many boss mechanics) are never returned by `C_UnitAuras`. Blizzard shows them through - `C_UnitAuras.AddPrivateAuraAnchor(args)`. Whether a third-party addon may call it in a restricted context - is **UNVERIFIED**. + `C_UnitAuras.AddPrivateAuraAnchor(args)`. Addon code can call it in combat while auras are secret: + **VERIFIED** (Experiment 6). ### Classic clients @@ -324,7 +325,7 @@ iteration, after Filger works on every game version. Until then `data/` stays un in code". Edit Mode owns position. There's no focus support, and the CDM shows only auras the player cast. - **Effort:** S (skin). **Risk:** high coupling to Blizzard internals. -- **Evidence:** cooldown-manager.md §7–§10 (VERIFIED). Private-aura anchors from addons are UNVERIFIED. +- **Evidence:** cooldown-manager.md §7–§10 (VERIFIED). Private-aura anchors from addons work (Experiment 6). ### F. Architecture: split vs one path @@ -425,7 +426,7 @@ iteration, after Filger works on every game version. Until then `data/` stays un frames are allowed to filter by ID. 4. **Phase 3: the remaining Mainline frames.** - TARGET_DEBUFFS (`HARMFUL|PLAYER` on hostile units), TARGET_AURAS, FOCUS_DEBUFFS. - - BOSS_DEBUFFS (`isBossAura`; plus private-aura anchors if Experiment 6 passes). + - BOSS_DEBUFFS (`isBossAura`; plus private-aura anchors, Experiment 6). - PLAYER_DEBUFFS shows only important debuffs, one group each: `HARMFUL|RAID_PLAYER_DISPELLABLE`, `HARMFUL|CROWD_CONTROL`, `isBossAura`. ~~Stagger by ID~~: not possible, Stagger is contextually secret (Experiment 3). @@ -550,6 +551,11 @@ combat** (target dummy, then a dungeon boss). auraIndex=1, parent=frame, showCooldownFrame=true, iconInfo={ iconAnchor={point="CENTER", relativeTo=frame, relativePoint="CENTER", offsetX=0, offsetY=0}, iconWidth=40, iconHeight=40 } }`. It needs a dungeon boss that applies a private aura. Pass: no error, and the icon shows. + - Now `/filger exp6` (`core/development.lua`): anchors for `auraIndex` 1–3 on the player. + - **Result (2026-10-22, Retail, Aberrus, Echo of Neltharion, solo):** run in combat: + `aurasSecret true`, anchor IDs 3133–3135, no error. Two different private debuffs (Volcanic Heart + among them) drew in the anchor slots. Blizzard's raid frames didn't show them (solo, so likely no + raid frames; not investigated). 7. **Cooldown edges.** In combat, print `issecretvalue` for `GetInventoryItemCooldown("player",13)` and `C_Container.GetItemCooldown()`. Log which of the player's own spells arrive with a secret `spellID` in `UNIT_SPELLCAST_SUCCEEDED`. From 2cb701ce8ae6257ebfc8f1985171387473bfc389 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Thu, 22 Oct 2026 23:57:46 -0300 Subject: [PATCH 07/13] use aura containers for target and focus frames on mainline --- core/Mainline/auras.lua | 93 ++++++++++++++++++++++------------- docs/secret-values-options.md | 5 +- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua index 33b3a2c..f974d10 100644 --- a/core/Mainline/auras.lua +++ b/core/Mainline/auras.lua @@ -419,22 +419,52 @@ do } for i, group in ipairs(data.groups) do - container:AddAuraGroup(self:GetName() .. "Group" .. i, group.filter, { + local key = self:GetName() .. "Group" .. i + container:AddAuraGroup(key, group.filter, { maxFrameCount = self.limit, candidateFilters = group.candidateFilters, sortMethod = AuraContainerSortMethod.ExpirationOnly, layout = layout, initializeFrame = initializeFrame }) + + -- groups with `hostile` are only enabled while the unit's reaction matches + if group.hostile ~= nil then + self.reactionGroups = self.reactionGroups or {} + self.reactionGroups[key] = group.hostile + end end self.container = container + + -- the container doesn't follow unit token changes by itself + if (self.unit == "target") then + self:RegisterEvent("PLAYER_TARGET_CHANGED") + elseif (self.unit == "focus") then + self:RegisterEvent("PLAYER_FOCUS_CHANGED") + end end function container_proto:ForceUpdate() - if self.container then - self.container:UpdateAllAuras() + local container = self.container + if not container then return end + + if self.reactionGroups then + local hostile = UnitCanAttack(self.unit, "player") + for key, groupHostile in next, self.reactionGroups do + container:SetAuraGroupEnabled(key, groupHostile == hostile) + end end + + container:UpdateAllAuras() + end + + function container_proto:PLAYER_TARGET_CHANGED() + self:ForceUpdate() + end + + function container_proto:PLAYER_FOCUS_CHANGED() + self:ForceUpdate() end end @@ -445,7 +475,6 @@ ns.container_proto = container_proto -------------------------------------------------- local class = Filger.class local spells = Filger.spells or {} -local all = Filger.all or {} local layout = Filger.config.layout local size, spacing = layout.size, layout.spacing @@ -522,7 +551,6 @@ Mixin(Filger.config.frames, { ["TARGET_DEBUFFS"] = { enabled = true, unit = "target", - filter = "HARMFUL", anchor = { "LEFT", UIParent, "CENTER", xOffset, yOffset }, limit = 8, size = size, @@ -530,33 +558,14 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMLEFT", ["growth-x"] = "RIGHT", ["growth-y"] = "DOWN", - spells = all, - FilterAura = function(element, unit, data) - if UnitCanAttack(unit, "player") then - return data.enabled and data.isPlayerAura - end - return data.enabled and not data.isPlayerAura - end, - SortAuras = function(a, b) - if (a.priority ~= b.priority) then - return a.priority < b.priority - end - - if (a.expirationTime ~= b.expirationTime) then - return a.expirationTime < b.expirationTime - end - - if (a.isPlayerAura ~= b.isPlayerAura) then - return a.isPlayerAura - end - - return a.auraInstanceID < b.auraInstanceID - end + groups = { + { filter = "HARMFUL|PLAYER", hostile = true }, + { filter = "HARMFUL|!PLAYER", hostile = false } + } }, ["TARGET_AURAS"] = { enabled = true, unit = "target", - filter = "HELPFUL", anchor = { "LEFT", UIParent, "CENTER", xOffset, 0 }, limit = 8, size = size, @@ -564,12 +573,24 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMLEFT", ["growth-x"] = "RIGHT", ["growth-y"] = "DOWN", - spells = all + groups = { + { + filter = "HELPFUL", + hostile = false, + candidateFilters = { + excludeSpellIDs = ToSpellIDs(blacklist) + } + }, + -- buffs on hostile units can't be filtered by spell ID; keep the groups to show + { filter = "HELPFUL|IMPORTANT", hostile = true }, + { filter = "HELPFUL|DISPELLABLE", hostile = true }, + -- { filter = "HELPFUL", hostile = true, candidateFilters = { isStealable = true } }, + -- { filter = "HELPFUL", hostile = true }, + } }, ["FOCUS_DEBUFFS"] = { enabled = true, unit = "focus", - filter = "HARMFUL", anchor = { "LEFT", UIParent, "CENTER", xOffset, -yOffset }, limit = 8, size = size, @@ -577,11 +598,13 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMLEFT", ["growth-x"] = "RIGHT", ["growth-y"] = "DOWN", - desaturated = false, - FilterAura = function(element, unit, data) - return data.enabled and data.isBossAura or (data.sourceUnit == nil) or not data.isFromPlayerOrPlayerPet - end - }, + groups = { + { + filter = "HARMFUL", + candidateFilters = { isBossAura = true } + } + } + }, ["BOSS_DEBUFFS"] = { enabled = false, unit = "player", diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 496055f..19ae82d 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -77,7 +77,10 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - [ ] **Phase 3: remaining Mainline frames** - [x] Experiment 6: private aura anchors: **pass**, addon code can add them in combat while auras are secret - - [ ] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS + - [x] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS: groups with `hostile` are enabled by + `UnitCanAttack` on target/focus change. TARGET_DEBUFFS `HARMFUL|PLAYER` (hostile) / + `HARMFUL|!PLAYER` (friendly); TARGET_AURAS `HELPFUL` + blacklist (friendly) / + `HELPFUL|IMPORTANT` + `HELPFUL|DISPELLABLE` (hostile); FOCUS_DEBUFFS `HARMFUL` + `isBossAura` - [ ] BOSS_DEBUFFS (`isBossAura` plus private-aura anchors, Experiment 6) - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss groups (Stagger dropped, Experiment 3) - [ ] Delete the copied event path from `core/Mainline/` From 73573937f6179deaae40e913a03e4c0d048c3370 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Fri, 23 Oct 2026 00:04:49 -0300 Subject: [PATCH 08/13] use aura container and private aura anchors for boss debuffs on mainline BOSS_DEBUFFS is now enabled on Mainline. It shows HARMFUL auras with isBossAura (limit 1) and adds two private aura anchors after the container slot. Classic is unchanged. --- core/Mainline/auras.lua | 33 ++++++++++++++++++++++++++++----- docs/secret-values-options.md | 3 ++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua index f974d10..1512100 100644 --- a/core/Mainline/auras.lua +++ b/core/Mainline/auras.lua @@ -435,6 +435,26 @@ do end end + -- private auras (boss mechanics) never reach C_UnitAuras; Blizzard draws them into these anchors + local growthx = (self["growth-x"] == "LEFT") and -1 or 1 + for i = 1, data.privateAuras or 0 do + local anchor = CreateFrame("Frame", nil, self) + anchor:SetSize(self.size, self.size) + anchor:SetPoint(self.initialAnchor, self, self.initialAnchor, (self.limit + i - 1) * (self.size + self.spacing) * growthx, 0) + C_UnitAuras.AddPrivateAuraAnchor({ + unitToken = self.unit, + auraIndex = i, + parent = anchor, + showCooldownFrame = true, + showCountdownNumbers = true, + iconInfo = { + iconAnchor = { point = "CENTER", relativeTo = anchor, relativePoint = "CENTER", offsetX = 0, offsetY = 0 }, + iconWidth = self.size, + iconHeight = self.size + } + }) + end + self.container = container -- the container doesn't follow unit token changes by itself @@ -606,9 +626,8 @@ Mixin(Filger.config.frames, { } }, ["BOSS_DEBUFFS"] = { - enabled = false, + enabled = true, unit = "player", - filter = "HARMFUL", anchor = { "CENTER", UIParent, "CENTER", 0, 160 }, limit = 1, size = math.floor(1.5 * size), @@ -616,8 +635,12 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMLEFT", ["growth-x"] = "RIGHT", ["growth-y"] = "DOWN", - FilterAura = function(element, unit, data) - return data.enabled and data.isBossAura or (data.sourceUnit == nil) - end + groups = { + { + filter = "HARMFUL", + candidateFilters = { isBossAura = true } + } + }, + privateAuras = 2 }, }) diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 19ae82d..538b62e 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -81,7 +81,8 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] `UnitCanAttack` on target/focus change. TARGET_DEBUFFS `HARMFUL|PLAYER` (hostile) / `HARMFUL|!PLAYER` (friendly); TARGET_AURAS `HELPFUL` + blacklist (friendly) / `HELPFUL|IMPORTANT` + `HELPFUL|DISPELLABLE` (hostile); FOCUS_DEBUFFS `HARMFUL` + `isBossAura` - - [ ] BOSS_DEBUFFS (`isBossAura` plus private-aura anchors, Experiment 6) + - [x] BOSS_DEBUFFS: enabled on Mainline. `HARMFUL` + `isBossAura` (limit 1), plus 2 private-aura + anchors (`privateAuras`) after the container slot - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss groups (Stagger dropped, Experiment 3) - [ ] Delete the copied event path from `core/Mainline/` - [ ] **Other experiments** From fd0d8ac2bd6a81a1f66a9d3089e8b1dea00e80e3 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Fri, 23 Oct 2026 00:28:10 -0300 Subject: [PATCH 09/13] use aura container for player debuffs on mainline Show only important debuffs, one group each: RAID_PLAYER_DISPELLABLE, CROWD_CONTROL and isBossAura. Stagger dropped (contextually secret, experiment 3). --- core/Mainline/auras.lua | 14 +++++++++----- docs/secret-values-options.md | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua index 1512100..a41541a 100644 --- a/core/Mainline/auras.lua +++ b/core/Mainline/auras.lua @@ -516,7 +516,6 @@ Mixin(Filger.config.frames, { ["PLAYER_DEBUFFS"] = { enabled = true, unit = "player", - filter = "HARMFUL", anchor = { "RIGHT", UIParent, "CENTER", -xOffset, yOffset }, limit = 8, size = size, @@ -524,10 +523,15 @@ Mixin(Filger.config.frames, { initialAnchor = "BOTTOMRIGHT", ["growth-x"] = "LEFT", ["growth-y"] = "DOWN", - spells = { - [124275] = { enabled = true, priority = 3 }, -- Light Stagger - [124274] = { enabled = true, priority = 3 }, -- Moderate Stagger - [124273] = { enabled = true, priority = 3 } -- Heavy Stagger + -- debuffs on the player can't be filtered by spell ID (Stagger is contextually secret); + -- only the important ones, one group each + groups = { + { filter = "HARMFUL|RAID_PLAYER_DISPELLABLE" }, + { filter = "HARMFUL|CROWD_CONTROL" }, + { + filter = "HARMFUL", + candidateFilters = { isBossAura = true } + } } }, ["PLAYER_AURAS"] = { diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 538b62e..199b56d 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -83,7 +83,8 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] `HELPFUL|IMPORTANT` + `HELPFUL|DISPELLABLE` (hostile); FOCUS_DEBUFFS `HARMFUL` + `isBossAura` - [x] BOSS_DEBUFFS: enabled on Mainline. `HARMFUL` + `isBossAura` (limit 1), plus 2 private-aura anchors (`privateAuras`) after the container slot - - [ ] PLAYER_DEBUFFS: dispellable, crowd control, boss groups (Stagger dropped, Experiment 3) + - [x] PLAYER_DEBUFFS: `HARMFUL|RAID_PLAYER_DISPELLABLE`, `HARMFUL|CROWD_CONTROL`, `HARMFUL` + + `isBossAura` groups (Stagger dropped, Experiment 3) - [ ] Delete the copied event path from `core/Mainline/` - [ ] **Other experiments** - [ ] Experiment 7: cooldown edges (items, slots, secret cast `spellID`) From 8e4d5382c1a07e2c247a7c88e0ddc52feb0f46f7 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Fri, 23 Oct 2026 06:24:55 -0300 Subject: [PATCH 10/13] remove unused aura event path from mainline All Mainline aura frames now use aura containers, so the copied aura_proto (UNIT_AURA path) and its LibDispel/UnitIsUnit/C_Secrets locals were dead code. --- core/Mainline/auras.lua | 358 ---------------------------------- docs/secret-values-options.md | 4 +- 2 files changed, 2 insertions(+), 360 deletions(-) diff --git a/core/Mainline/auras.lua b/core/Mainline/auras.lua index a41541a..86b81d1 100644 --- a/core/Mainline/auras.lua +++ b/core/Mainline/auras.lua @@ -1,366 +1,8 @@ local _, ns = ... local Filger = ns.Filger -local LibDispel = LibStub("LibDispel") -assert(LibDispel, "Filger requires LibDispel") - local blacklist = ns.Filger.blacklist or {} --- Blizzard -local UnitIsUnit = _G.UnitIsUnit -local C_Secrets = _G.C_Secrets - --------------------------------------------------- --- AURAS --------------------------------------------------- -local aura_proto = { - createdButtons = 0, - anchoredButtons = 0, - visibleButtons = 0 -} - -do - --[[ Callback: button:UpdateColor(button, unit, data) - Called after the aura button has been updated. - - * self - the widget holding the aura buttons - * button - the updated aura button (Button) - * unit - the unit on which the aura is cast (string) - * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - * index - the actual position of the aura button (number) - ]]-- - function aura_proto:UpdateColor(button, unit, data) - if button.Backdrop then - if (data.isHarmful and UnitCanAssist("player", unit)) or (data.isHelpful and UnitCanAttack(unit, "player") and UnitCanAttack("player", unit)) then - local color = Filger.colors[data.dispelName] - button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) - else - local color = Filger.config.general.backdrop.color - button.Backdrop:SetBackdropBorderColor(color.r, color.g, color.b, color.a or 1) - end - end - - if button.Icon and self.desaturated == true then - button.Icon:SetDesaturated(data.isHarmful and not data.isPlayerAura and not UnitIsUnit(unit, "player")) - end - - if button.Animation then - if data.isDispelable or data.isStealable then - button.Animation:Play() - button.Animation.playing = true - else - button.Animation:Stop() - button.Animation.playing = false - end - end - end - - --[[ Callback: aura_proto:Update(unit, data, index) - Update the aura button and its components. - - * self - the widget holding the aura buttons - * unit - the unit on which the aura is cast (string) - * data - the [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - * index - the actual position of the aura button (number) - ]]-- - function aura_proto:Update(unit, data, index) - if not data.name then return end - - local button = self[index] - if not button then - button = Filger:CreateButton(self, index) - - table.insert(self, button) - - self.createdButtons = self.createdButtons + 1 - end - - -- tooltips - button.unit = unit - button.filter = self.filter - button.auraInstanceID = data.auraInstanceID - button.auraIndex = data.auraIndex - button.isHarmful = data.isHarmful - - -- timers - button.name = data.name - button.spellId = data.spellId - button.duration = data.duration or 0 - button.expirationTime = data.expirationTime or GetTime() - - if button.Cooldown then - if data.duration and data.duration > 0 then - button.Cooldown:SetCooldown(data.expirationTime - data.duration, data.duration, data.timeMod) - button.Cooldown:Show() - else - button.Cooldown:Hide() - end - end - - if button.Icon then - if data.icon then - button.Icon:SetTexture(data.icon) - button.Icon:Show() - else - button.Icon:Hide() - end - end - - if button.Count then - button.Count:SetText((data.applications and data.applications > 1) and data.applications or "") - end - - if data.duration and data.duration > 0 then - button:SetScript("OnUpdate", button.OnUpdate) - end - - button:Show() - - self:UpdateColor(button, unit, data) - end - - --[[ Override: aura_proto.SortAuras(a, b) - Defines a custom sorting algorithm for ordering the auras. - - Defaults to [AuraUtil.DefaultAuraCompare](https://github.com/Gethe/wow-ui-source/search?q=DefaultAuraCompare). - - Overridden by the more specific SortAuras overrides if they are defined. - --]] - aura_proto.SortAuras = function(a, b) - if (a.priority ~= b.priority) then - return a.priority > b.priority - end - - if (a.expirationTime ~= b.expirationTime) then - return a.expirationTime < b.expirationTime - end - - -- if (a.canApplyAura ~= b.canApplyAura) then - -- return a.canApplyAura - -- end - - return a.auraInstanceID < b.auraInstanceID - end - - --[[ Override: aura_proto:FilterAura(unit, data) - Defines a custom filter that controls if the aura button should be shown. - - * self - the widget holding the aura buttons - * unit - the unit for which the update has been triggered (string) - * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - - ## Returns - - * show - indicates whether the aura button should be shown (boolean) - --]] - function aura_proto:FilterAura(unit, data) - return data.enabled and data.applications >= data.stackThreshold - end - - function aura_proto:GetAuraIndex(unit, auraInstanceID, filter) - local index = 1 - repeat - local data = C_UnitAuras.GetAuraDataByIndex(unit, index, filter) - if not data then - return nil - elseif data.auraInstanceID == auraInstanceID then - return index - end - index = index + 1 - until (not data) - return nil - end - - --[[ Override: aura_proto:ProcessData(unit, data) - Add some custom value sto the UnitAuraInfo. - - * self - the widget holding the aura buttons - * unit - the unit for which the update has been triggered (string) - * data - [UnitAuraInfo](https://wowpedia.fandom.com/wiki/Struct_UnitAuraInfo) object (table) - - ## Returns - - * data - a custom 'UnitAuraInfo' object (table) - --]] - function aura_proto:ProcessData(unit, data) - if not data then return end - - -- UnitIsUnit/UnitIsOwnerOrControllerOfUnit on sourceUnit can be secret even out of combat - -- (NPC sources, restricted maps), so rely on the aura's own flag - data.isPlayerAura = data.isFromPlayerOrPlayerPet - - data.dispelName = LibDispel:GetDispelType(data.spellId, data.dispelName) - data.isDispelable = LibDispel:IsDispelable(unit, data.spellId, data.dispelName, data.isHarmful) - - local spell = self.spells[data.spellId] - if spell then - data.enabled = spell.enabled or false - data.priority = spell.priority or 0 - data.stackThreshold = spell.stackThreshold or 0 - else - data.enabled = true - data.priority = 0 - data.stackThreshold = 0 - end - - if blacklist[data.spellId] or blacklist[data.name] then - data.enabled = false - end - - if Filger.isVanilla then - data.auraIndex = self:GetAuraIndex(unit, data.auraInstanceID, self.filter) - end - - return data - end - - -- Retail: while auras are secret, tainted code can't list them; only spells whose aura - -- isn't secret can be looked up one by one, so only listed spells are shown. - function aura_proto:ScanSpellAuras(unit) - local isHarmful = self.filter:find("HARMFUL") ~= nil - local isPlayer = self.filter:find("PLAYER") ~= nil - - for spellId in next, self.spells do - if not C_Secrets.ShouldSpellAuraBeSecret(spellId) then - local data = C_UnitAuras.GetUnitAuraBySpellID(unit, spellId) - if data and data.isHarmful == isHarmful and (not isPlayer or data.isFromPlayerOrPlayerPet) then - data = self:ProcessData(unit, data) - self.all[data.auraInstanceID] = data - - if self:FilterAura(unit, data) then - self.actives[data.auraInstanceID] = true - end - end - end - end - end - - function aura_proto:UpdateAuras(event, unit, updateInfo) - if (self.unit ~= unit) then return end - - local isRestricted = C_Secrets and C_Secrets.ShouldAurasBeSecret() - -- updateInfo is secret while restricted; cached auras are stale right after it - local isFullUpdate = isRestricted or self.restricted or not updateInfo or updateInfo.isFullUpdate - self.restricted = isRestricted - - local changed = false - - if isFullUpdate then - self.all = table.wipe(self.all or {}) - self.actives = table.wipe(self.actives or {}) - changed = true - - if isRestricted then - self:ScanSpellAuras(unit) - else - local slots = { C_UnitAuras.GetAuraSlots(unit, self.filter) } - for i = 2, #slots do -- #1 return is continuationToken, we don't care about it - local data = self:ProcessData(unit, C_UnitAuras.GetAuraDataBySlot(unit, slots[i])) - self.all[data.auraInstanceID] = data - - if self:FilterAura(unit, data) then - self.actives[data.auraInstanceID] = true - end - end - end - else - if updateInfo.addedAuras then - for _, data in next, updateInfo.addedAuras do - if not C_UnitAuras.IsAuraFilteredOutByInstanceID(unit, data.auraInstanceID, self.filter) then - self.all[data.auraInstanceID] = self:ProcessData(unit, data) - if self:FilterAura(unit, data) then - self.actives[data.auraInstanceID] = true - changed = true - end - end - end - end - - if updateInfo.updatedAuraInstanceIDs then - for _, auraInstanceID in next, updateInfo.updatedAuraInstanceIDs do - local data = self.all[auraInstanceID] - if data then - self.all[auraInstanceID] = self:ProcessData(unit, C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID)) - if self:FilterAura(unit, data) then - self.actives[auraInstanceID] = true - changed = true - end - end - end - end - - if updateInfo.removedAuraInstanceIDs then - for _, auraInstanceID in next, updateInfo.removedAuraInstanceIDs do - if self.all[auraInstanceID] then - self.all[auraInstanceID] = nil - end - - if self.actives[auraInstanceID] then - self.actives[auraInstanceID] = nil - changed = true - end - end - end - end - - if changed then - -- instead of removing auras one by one, just wipe the tables entirely - -- and repopulate them, multiple table.remove calls are insanely slow - self.sorted = table.wipe(self.sorted or {}) - - for auraInstanceID, _ in next, self.actives do - table.insert(self.sorted, self.all[auraInstanceID]) - end - - table.sort(self.sorted, self.SortAuras) - - local numVisible = math.min(self.limit, #self.sorted) - - for i = 1, numVisible do - self:Update(unit, self.sorted[i], i) - end - - local visibleChanged = true - - if (numVisible ~= self.visibleButtons) then - self.visibleButtons = numVisible - visibleChanged = true - end - - for i = numVisible + 1, #self do - self[i]:Hide() - end - - if (visibleChanged or self.createdButtons > self.anchoredButtons) then - if visibleChanged then - self:SetPosition(1, numVisible) - else - self:SetPosition(self.anchoredButtons + 1, self.createdButtons) - self.anchoredButtons = self.createdButtons - end - end - end - end - - function aura_proto:ForceUpdate() - self:UpdateAuras("ForceUpdate", self.unit, nil) - end - - function aura_proto:UNIT_AURA(unit, updateInfo) - self:UpdateAuras("UNIT_AURA", unit, updateInfo) - end - - function aura_proto:PLAYER_TARGET_CHANGED(...) - self:ForceUpdate() - end - - function aura_proto:PLAYER_FOCUS_CHANGED(...) - self:ForceUpdate() - end -end - -ns.aura_proto = aura_proto - -------------------------------------------------- -- AURA CONTAINERS -------------------------------------------------- diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 199b56d..edbf1f3 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -74,7 +74,7 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] numeric spell IDs are shown) - [x] PLAYER_BUFFS: `HELPFUL|!PLAYER` + `excludeSpellIDs` - [x] Frames are named from their `config.frames` key (`FilgerPlayerAuras`, ...) -- [ ] **Phase 3: remaining Mainline frames** +- [x] **Phase 3: remaining Mainline frames** - [x] Experiment 6: private aura anchors: **pass**, addon code can add them in combat while auras are secret - [x] TARGET_DEBUFFS, TARGET_AURAS, FOCUS_DEBUFFS: groups with `hostile` are enabled by @@ -85,7 +85,7 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] anchors (`privateAuras`) after the container slot - [x] PLAYER_DEBUFFS: `HARMFUL|RAID_PLAYER_DISPELLABLE`, `HARMFUL|CROWD_CONTROL`, `HARMFUL` + `isBossAura` groups (Stagger dropped, Experiment 3) - - [ ] Delete the copied event path from `core/Mainline/` + - [x] Delete the copied event path from `core/Mainline/` - [ ] **Other experiments** - [ ] Experiment 7: cooldown edges (items, slots, secret cast `spellID`) - [ ] Experiment 8: tooltip methods on each Classic client From 015eea83e3e69ebcc266aa82f9d69d7f1a322035 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Fri, 23 Oct 2026 07:55:35 -0300 Subject: [PATCH 11/13] remove classic tooltip index branch, record experiments 7 and 8 - /filger exp7 [itemID]: logs each player cast with its spellID and the start/duration of trinket slots 13/14 and the item, or secret - /filger exp8: prints which GameTooltip aura methods exist - remove GetAuraIndex and the SetUnitAura tooltip branch used on Era; SetUnit{Buff,Debuff}ByAuraInstanceID works on Retail, Era, TBC and MoP (checked in game), and Era no longer scans every aura on the unit for each aura it processes - docs: exp7 results (Retail, dummy and boss: no secret spellIDs, item and slot cooldowns readable in combat, a new item cooldown isn't readable yet at UNIT_SPELLCAST_SUCCEEDED, a trinket cooldown can shorten while it runs); exp8 results; add experiment 10 (item cooldown refresh events) --- Filger.lua | 6 +-- core/Classic/auras.lua | 19 ---------- core/development.lua | 54 ++++++++++++++++++++++++++ docs/secret-values-options.md | 71 +++++++++++++++++++++++++++++++---- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/Filger.lua b/Filger.lua index c9998a9..3aaba99 100644 --- a/Filger.lua +++ b/Filger.lua @@ -52,11 +52,7 @@ do GameTooltip:SetOwner(self, "ANCHOR_RIGHT") - if self.auraIndex then - -- classic aura tooltip - GameTooltip:SetUnitAura(self.unit, self.auraIndex, self.isHarmful and "HARMFUL" or "HELPFUL") - elseif self.auraInstanceID and GameTooltip.SetUnitDebuffByAuraInstanceID then - -- retail aura tooltip + if self.auraInstanceID and GameTooltip.SetUnitDebuffByAuraInstanceID then if self.isHarmful then GameTooltip:SetUnitDebuffByAuraInstanceID(self.unit, self.auraInstanceID) else diff --git a/core/Classic/auras.lua b/core/Classic/auras.lua index 2365356..7e6cce5 100644 --- a/core/Classic/auras.lua +++ b/core/Classic/auras.lua @@ -80,7 +80,6 @@ do button.unit = unit button.filter = self.filter button.auraInstanceID = data.auraInstanceID - button.auraIndex = data.auraIndex button.isHarmful = data.isHarmful -- timers @@ -158,20 +157,6 @@ do return data.enabled and data.applications >= data.stackThreshold end - function aura_proto:GetAuraIndex(unit, auraInstanceID, filter) - local index = 1 - repeat - local data = C_UnitAuras.GetAuraDataByIndex(unit, index, filter) - if not data then - return nil - elseif data.auraInstanceID == auraInstanceID then - return index - end - index = index + 1 - until (not data) - return nil - end - --[[ Override: aura_proto:ProcessData(unit, data) Add some custom value sto the UnitAuraInfo. @@ -211,10 +196,6 @@ do data.enabled = false end - if Filger.isVanilla then - data.auraIndex = self:GetAuraIndex(unit, data.auraInstanceID, self.filter) - end - return data end diff --git a/core/development.lua b/core/development.lua index d9cb07a..06f7859 100644 --- a/core/development.lua +++ b/core/development.lua @@ -127,3 +127,57 @@ local exp6 = function() end Filger:AddCommand("exp6", exp6, "experiment 6: private aura anchors on the player") + +-- Experiment 7: cooldown edges (item/slot cooldowns, secret cast spellID) +-- usage: /filger exp7 [itemID] toggles logging; enter combat and cast spells / use the item +-- on each player cast: is the spellID secret, are slot 13/14 and item cooldowns secret +local exp7Frame +-- prints start/duration when readable, so an active cooldown is visible +local function secrecy(...) + local ok, start, duration = pcall(...) + if not ok then return "error: " .. tostring(start) end + if start == nil then return "nil" end + if issecretvalue(start) or issecretvalue(duration) then + return "|cffff0000secret|r " .. tostring(issecretvalue(start)) .. "/" .. tostring(issecretvalue(duration)) + end + return format("%.1f/%.1f", start, duration) +end + +local exp7 = function(arg) + if not issecretvalue then + print(Filger.title, "exp7: no issecretvalue on this client") + return + elseif exp7Frame then + exp7Frame:UnregisterAllEvents() + exp7Frame = nil + print(Filger.title, "exp7: stopped") + return + end + local itemID = tonumber(arg) + + exp7Frame = CreateFrame("Frame") + exp7Frame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player") + exp7Frame:SetScript("OnEvent", function(_, _, _, _, spellID) + -- a secret spellID can't be printed or looked up; the cast order tells which spell it was + local id = issecretvalue(spellID) and "|cffff0000secret|r" or tostring(spellID) + print(Filger.title, "exp7:", format("%.1f", GetTime()), "combat", tostring(InCombatLockdown()), + "spellID", id, + "slot13", secrecy(GetInventoryItemCooldown, "player", 13), + "slot14", secrecy(GetInventoryItemCooldown, "player", 14), + "item", itemID and secrecy(C_Container.GetItemCooldown, itemID) or "-") + end) + print(Filger.title, "exp7: logging player casts (start/duration or secret), run again to stop") +end + +Filger:AddCommand("exp7", exp7, "experiment 7: cooldown edges, logs player casts (/filger exp7 [itemID])") + +-- Experiment 8: which aura tooltip methods each client has +-- usage: /filger exp8 (anywhere) +local exp8 = function() + for _, method in ipairs({ "SetUnitAura", "SetUnitBuff", "SetUnitDebuff", "SetUnitBuffByAuraInstanceID", + "SetUnitDebuffByAuraInstanceID" }) do + print(Filger.title, "exp8:", method, GameTooltip[method] ~= nil) + end +end + +Filger:AddCommand("exp8", exp8, "experiment 8: aura tooltip methods on GameTooltip") diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index edbf1f3..145bf2d 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -87,8 +87,12 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] `isBossAura` groups (Stagger dropped, Experiment 3) - [x] Delete the copied event path from `core/Mainline/` - [ ] **Other experiments** - - [ ] Experiment 7: cooldown edges (items, slots, secret cast `spellID`) - - [ ] Experiment 8: tooltip methods on each Classic client + - [x] Experiment 7: cooldown edges (Retail, dummy and boss): nothing secret; item cooldowns aren't + applied yet at `UNIT_SPELLCAST_SUCCEEDED` + - [x] Experiment 8: tooltip methods on each Classic client: all five exist on Retail, Era, TBC, MoP + (Forever and Titan not tested); index branch removed, instance-ID tooltips verified on all four + - [ ] Experiment 10: item and slot cooldowns refresh when they start and when they shorten + (`SPELL_UPDATE_COOLDOWN` only today; `BAG_UPDATE_COOLDOWN` may be needed) - [ ] **Later iteration:** spell data ([spell-data-generation.md](spell-data-generation.md)) ## Facts @@ -97,13 +101,12 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] | Location | APIs | Data read → decision | Client path | |---|---|---|---| -| `Filger.lua:32-54` `OnEnter` | `GameTooltip:SetUnitAura`, `SetUnit{Buff,Debuff}ByAuraInstanceID`, `SetSpellByID` | `auraIndex` / `auraInstanceID` → tooltip | Index on Vanilla, instance ID elsewhere | +| `Filger.lua:32-54` `OnEnter` | `SetUnit{Buff,Debuff}ByAuraInstanceID`, `SetSpellByID` | `auraInstanceID` → tooltip | All (index branch removed) | | `Filger.lua:72-92` `OnUpdate` | `GetTime` | `expirationTime - GetTime()`, `<= 5` → red text, hide on expiry | All | | `Filger.lua:184-208` `UpdateColor` | `UnitCanAssist`, `UnitCanAttack` | `isHarmful/isHelpful`, `Filger.colors[dispelName]` (table key), `isPlayerAura` → desaturate, `isDispelable/isStealable` → flash | All | | `Filger.lua:218-272` `Update` | `Cooldown:SetCooldown`, `SetTexture`, `SetText` | `name` check, `duration > 0`, `expirationTime - duration`, `applications > 1` | All | | `Filger.lua:281-295` `SortAuras` | — | Compares `priority`, `expirationTime`, `auraInstanceID` | All | | `Filger.lua:308-310` `FilterAura` | — | `applications >= stackThreshold` | All | -| `Filger.lua:312-324` `GetAuraIndex` | `C_UnitAuras.GetAuraDataByIndex` | `auraInstanceID ==` → tooltip index | Vanilla | | `Filger.lua:337-370` `ProcessData` | `UnitIsUnit`, `UnitIsOwnerOrControllerOfUnit`, LibDispel | `sourceUnit` (or `isFromPlayerOrPlayerPet` when restricted), `spellId`/`dispelName` → LibDispel, `self.spells[spellId]`, `blacklist[spellId]`, `blacklist[name]` | All | | `Filger.lua:374-391` `ScanSpellAuras` | `C_Secrets.ShouldSpellAuraBeSecret`, `C_UnitAuras.GetUnitAuraBySpellID` | `isHarmful ==`, `isFromPlayerOrPlayerPet` | Retail/Forever while restricted | | `Filger.lua:393-498` `UpdateAuras` | `C_Secrets.ShouldAurasBeSecret`, `GetAuraSlots`, `GetAuraDataBySlot`, `GetAuraDataByAuraInstanceID`, `IsAuraFilteredOutByInstanceID`, `UNIT_AURA` | `updateInfo` fields, `self.all[auraInstanceID]` keys | All (restricted branch on Retail/Forever) | @@ -262,11 +265,11 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - Secret mode: `C_Secrets.HasSecretRestrictions()`, which exists everywhere and is documented to be false when the build has no restrictions. - Whether `CustomAuraContainerTemplate` exists. - - The Vanilla tooltip by index (`Filger.lua:37-39`, `365-367`). + - ~~The Vanilla tooltip by index~~ Removed: the instance-ID tooltip works on Era (Experiment 8). - The filter-string vocabulary (`!CANCELABLE` vs `NOT_CANCELABLE`, the Retail-only `IMPORTANT` and `DISPELLABLE`). - Whether `GameTooltip:SetUnit{Buff,Debuff}ByAuraInstanceID` exists. It's a widget method that isn't in - the API docs; already guarded at `Filger.lua:40`; **UNVERIFIED** per client. + the API docs; already guarded at `Filger.lua:40`. Exists on Retail, Era, TBC and MoP (Experiment 8). ## Options @@ -457,8 +460,7 @@ iteration, after Filger works on every game version. Until then `data/` stays un Same branch, same TOC, its own aura file (`core/Classic/`), which is today's code. Classic has no secret flags, so today's logic keeps working there, including the boss-debuff and blacklist data. -Filter-string differences (`NOT_CANCELABLE`) stay inside each family's frame definitions. The Vanilla -tooltip-by-index branch stays inside the Classic file. +Filter-string differences (`NOT_CANCELABLE`) stay inside each family's frame definitions. ## Experiments @@ -564,13 +566,66 @@ combat** (target dummy, then a dungeon boss). 7. **Cooldown edges.** In combat, print `issecretvalue` for `GetInventoryItemCooldown("player",13)` and `C_Container.GetItemCooldown()`. Log which of the player's own spells arrive with a secret `spellID` in `UNIT_SPELLCAST_SUCCEEDED`. + - Now `/filger exp7 [itemID]` (`core/development.lua`): toggles a log line per player cast with + `spellID` (or `secret`) and start/duration (or `secret`) for slots 13, 14 and the item. + - **Result (2026-10-23, Retail, Brewmaster, target dummy):** `/filger exp7 211880`, 15 casts, 14 of + them in combat. + - `UNIT_SPELLCAST_SUCCEEDED` on `player`: no secret `spellID`, including procs and + triggered spells (1241059, 1221146, 1219102). The `issecretvalue` skip in + `cooldown_proto:UNIT_SPELLCAST_SUCCEEDED` never fired. + - `GetInventoryItemCooldown` slots 13/14 and `C_Container.GetItemCooldown(211880)`: start and + duration not secret in combat, unlike spell cooldowns (Experiment 3: 0/28 never-secret). Matches + the API docs, which list no secret returns for `GetItemCooldown`. That run printed only + `issecretvalue`, so it doesn't show whether a cooldown was active; the boss run below does. + - **Result (2026-10-23, Retail, Brewmaster, dungeon boss):** `/filger exp7 211878` (Algari Healing + Potion), values printed. Trinket in slot 14 and the potion used in combat. + - Nothing secret: every `spellID` readable, and slots 13/14 and the item return plain numbers + while their cooldowns run (slot 14 `…/120.0`, item `…/300.0`). + - Item ID 211878 showed the 300 s cooldown of a potion drunk in the previous run (start 35327.8, + cast of 431416 at 35326.9), so the potion ranks share one cooldown, or 211880 and 211878 were + the same rank there. Not separated. + - At `UNIT_SPELLCAST_SUCCEEDED` the new item cooldown isn't applied yet: the potion cast (431416) + read `item 0.0/0.0`, the next cast read start 35630.9 (before the event's `GetTime` 35631.1). + Same for the trinket: the 1221146/1219102 cast at 35494.8 read `slot14 0.0/0.0`, the next cast + read start 35494.6. 1221146/1219102 are likely the trinket's use (also seen on the dummy); + **UNVERIFIED**. + - The slot 14 start moved earlier while the cooldown ran (35494.6 → 35468.6) and the cooldown ended + early at about 35590. Something reduced the trinket cooldown; the cause wasn't checked. Item + cooldowns must be re-read on update events, not computed once from the first read. + - Consequence for Filger: item and slot cooldowns need no secret branch. Filger refreshes them only + on `SPELL_UPDATE_COOLDOWN` (`Filger.lua:538`); whether that fires for item cooldowns, or + `BAG_UPDATE_COOLDOWN` is needed, is **UNVERIFIED**. 8. **Tooltips.** `/run print(GameTooltip.SetUnitBuffByAuraInstanceID ~= nil, GameTooltip.SetUnitAura ~= nil)` on each Classic client. + - Now `/filger exp8` (`core/development.lua`): prints which `GameTooltip` aura methods exist. + - **Result (2026-10-23, Retail, Era, TBC, MoP):** `SetUnitAura`, `SetUnitBuff`, `SetUnitDebuff`, + `SetUnitBuffByAuraInstanceID` and `SetUnitDebuffByAuraInstanceID` all exist on every client. + Forever and Titan not tested. + - Rendering by instance ID (`/run`: `GetAuraDataByIndex("player",1,"HELPFUL")`, then + `SetUnitBuffByAuraInstanceID` with its `auraInstanceID`): + - Era: index 1 was Honorless Target (instance ID 2, Fortitude also up); the tooltip showed + Honorless Target. **Pass** for one buff. + - MoP: the tooltip showed Cyclonic Inspiration, the index-1 buff (the buff bar orders buffs + differently). The name wasn't printed, so the match is likely but not confirmed. + - After removing the index branch (2026-10-23): Filger buff and debuff tooltips show the right + aura on Retail, Era, TBC and MoP. + - Consequence: the Vanilla index branch (`Filger.isVanilla` → `GetAuraIndex` in + `core/Classic/auras.lua`, a full aura scan per processed aura) was removed (2026-10-23). Every + client now uses the instance-ID tooltip. 9. **`[Family]` loading.** Add a one-line `print` to each `core//` stub. `/reload` on every client: Retail and Forever must print Mainline, the rest Classic, and exactly one line must print per client. Out of combat is fine. - **Result (2026-10-21):** Retail prints `Mainline`; Era, TBC and MoP print `Classic`, one line each. Forever and Titan not tested (no client). +10. **Item cooldown refresh.** Filger refreshes item and slot cooldowns only on `SPELL_UPDATE_COOLDOWN` + (`Filger.lua:538`). Experiment 7 showed that an item cooldown starts just after + `UNIT_SPELLCAST_SUCCEEDED` and can shorten while it runs. Check which event reports each change: + - Search `../refs` for how Blizzard's UI refreshes item cooldowns (`BAG_UPDATE_COOLDOWN`, + `ACTIONBAR_UPDATE_COOLDOWN`). + - In game, on Retail and each Classic client: use a trinket and a potion in combat, and check that + Filger's icons start and follow the cooldown, including a cooldown that shortens. + - Pass: the icons stay correct with today's events. Otherwise register the missing event for item + and slot rows only. ## Open questions for you From 11d184a445075a08b83e045fca6ff64756644c90 Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Fri, 23 Oct 2026 09:04:46 -0300 Subject: [PATCH 12/13] add /filger exp10 for item cooldown refresh events - /filger exp10 [itemID]: logs slot 13/14 and item cooldown changes per event (SPELL_UPDATE_COOLDOWN, BAG_UPDATE_COOLDOWN, ACTIONBAR_UPDATE_COOLDOWN) - exp7/exp10 cooldown output also prints enabled; skip the secret check where issecretvalue doesn't exist (Classic) --- core/development.lua | 47 ++++++++++++++++++++-- docs/future-plan.md | 54 +++++++++++++++++++++++++ docs/secret-values-options.md | 75 +++++++++++++++++++++++++++++++++-- 3 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 docs/future-plan.md diff --git a/core/development.lua b/core/development.lua index 06f7859..44249a7 100644 --- a/core/development.lua +++ b/core/development.lua @@ -132,15 +132,15 @@ Filger:AddCommand("exp6", exp6, "experiment 6: private aura anchors on the playe -- usage: /filger exp7 [itemID] toggles logging; enter combat and cast spells / use the item -- on each player cast: is the spellID secret, are slot 13/14 and item cooldowns secret local exp7Frame --- prints start/duration when readable, so an active cooldown is visible +-- prints start/duration/enabled when readable, so an active cooldown is visible local function secrecy(...) - local ok, start, duration = pcall(...) + local ok, start, duration, enabled = pcall(...) if not ok then return "error: " .. tostring(start) end if start == nil then return "nil" end - if issecretvalue(start) or issecretvalue(duration) then + if issecretvalue and (issecretvalue(start) or issecretvalue(duration)) then return "|cffff0000secret|r " .. tostring(issecretvalue(start)) .. "/" .. tostring(issecretvalue(duration)) end - return format("%.1f/%.1f", start, duration) + return format("%.1f/%.1f/%s", start, duration, tostring(enabled)) end local exp7 = function(arg) @@ -181,3 +181,42 @@ local exp8 = function() end Filger:AddCommand("exp8", exp8, "experiment 8: aura tooltip methods on GameTooltip") + +-- Experiment 10: which event reports each item/slot cooldown change +-- usage: /filger exp10 [itemID] toggles logging; use a trinket and a potion, compare with Filger's icons +-- prints a line when an event sees slot 13/14/item values that differ from what that same event saw last +local exp10Frame +local exp10Events = { "SPELL_UPDATE_COOLDOWN", "BAG_UPDATE_COOLDOWN", "ACTIONBAR_UPDATE_COOLDOWN" } + +local exp10 = function(arg) + if exp10Frame then + exp10Frame:UnregisterAllEvents() + exp10Frame = nil + print(Filger.title, "exp10: stopped") + return + end + local itemID = tonumber(arg) + local last = {} + + exp10Frame = CreateFrame("Frame") + for _, event in ipairs(exp10Events) do + exp10Frame:RegisterEvent(event) + end + exp10Frame:SetScript("OnEvent", function(_, event, ...) + local slot13 = secrecy(GetInventoryItemCooldown, "player", 13) + local slot14 = secrecy(GetInventoryItemCooldown, "player", 14) + local item = itemID and secrecy(C_Container.GetItemCooldown, itemID) or "-" + local snapshot = slot13 .. " " .. slot14 .. " " .. item + if last[event] == snapshot then return end + last[event] = snapshot + -- SPELL_UPDATE_COOLDOWN payload: spellID, baseSpellID, category, startRecoveryCategory, itemID (Retail only) + local spellID, _, _, _, payloadItemID = ... + local payload = event == "SPELL_UPDATE_COOLDOWN" + and ("spellID " .. tostring(spellID) .. " itemID " .. tostring(payloadItemID)) or "" + print(Filger.title, "exp10:", format("%.1f", GetTime()), event, payload, + "slot13", slot13, "slot14", slot14, "item", item) + end) + print(Filger.title, "exp10: logging slot 13/14/item cooldown changes per event, run again to stop") +end + +Filger:AddCommand("exp10", exp10, "experiment 10: item cooldown refresh events (/filger exp10 [itemID])") diff --git a/docs/future-plan.md b/docs/future-plan.md new file mode 100644 index 0000000..2515238 --- /dev/null +++ b/docs/future-plan.md @@ -0,0 +1,54 @@ +# Future plan + +## Potion cooldown + +Show one icon for the shared potion cooldown, without listing potion item IDs. + +### Why not item IDs + +- Every potion in the bags goes on cooldown when one is used, so one icon per potion would duplicate + the same timer. +- Potion IDs change every patch and expansion, so a list in `data/` would need constant upkeep. + +### Today + +- `ProcessCooldownData` already has a `data.itemId` branch (`GetItemInfo`, `C_Container.GetItemCooldown`), + and tooltips handle `itemId` (`Filger.lua`, `button_proto` `OnEnter`). +- The branch is unreachable: `core/data.lua` has no item constructor, and `buildCooldowns` drops any + row that has no `slotId` or known `spellId`. +- Events are enough. On Retail and MoP, `SPELL_UPDATE_COOLDOWN` reported every item cooldown change + (Experiment 10, [secret-values-options.md](secret-values-options.md)). Item rows are always re-checked + because they have no `spellId`. + +### Idea + +One "potion" row, with no ID. The row resolves to whichever potion is in the bags: + +- Find it by item class, not by ID: `C_Item.GetItemInfoInstant(itemID)` returns `classID, subClassID`. + A potion is `Enum.ItemClass.Consumable` (0) with `Enum.ItemConsumableSubclass.Potion` (1). + `ItemConsumableSubclass` and `GetItemInfoInstant` exist in the generated docs of every worktree + (live, forever, classic, classic_titan, classic_anniversary, classic_era). +- Scan the bags (`C_Container.GetContainerNumSlots`, `C_Container.GetContainerItemID`) only when they + change (`BAG_UPDATE_DELAYED`), never on cooldown events. Cache the item ID found. +- Keep the last ID found after the last potion is drunk, so the running cooldown still shows. +- Read the cooldown with the existing `itemId` branch, which works the same for any potion that shares it. + +### Phases + +1. **Experiments** (in game, `/filger exp10 `, which now prints `enabled`): + - MoP: drink in combat, then leave combat. Does `enabled` read 0 in combat while `start` follows + `GetTime()`, and does the cooldown start when combat ends? + - Era and TBC: the same. + - Retail: does a healing potion share its cooldown with a combat potion? Healthstones? (**UNVERIFIED**) + - Era: are potions classID 0 / subClassID 1, or Generic (0)? (**UNVERIFIED**, check with + `/run print(C_Item.GetItemInfoInstant())`) +2. **Data:** `data.CreatePotionCooldown(enabled)` in `core/data.lua`, one row in + `data/shared/cooldowns.lua` `gear`, and accept it in `buildCooldowns`. +3. **Resolve:** a bag scan that sets the row's `itemId`, run at login and on `BAG_UPDATE_DELAYED`, + registered only by the cooldown element. +4. **Classic in combat:** if phase 1 confirms the pending cooldown, keep the icon hidden while + `enabled` is 0 (`FilterCooldown` already requires `data.enabled`). + +### Open questions + +- If the bags hold potions from different cooldown groups, which one wins? Unknown until phase 1. diff --git a/docs/secret-values-options.md b/docs/secret-values-options.md index 145bf2d..3328715 100644 --- a/docs/secret-values-options.md +++ b/docs/secret-values-options.md @@ -41,7 +41,7 @@ Analysis only. No code was changed. As of 2026-10-19. Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments](#experiments). -- [ ] **Phase 0: fixes that work today** +- [x] **Phase 0: fixes that work today** - [x] Experiment 1: restriction state on every client (Forever and Titan not tested) - [x] Experiment 2: `GetAuraDuration` and `#` on secret instance IDs (Retail, in combat): **fails**, tainted code can't call any `RequiresUnitAuraAccess` API while auras are secret @@ -86,13 +86,14 @@ Details in [Migration](#migration-each-phase-ships-on-its-own) and [Experiments] - [x] PLAYER_DEBUFFS: `HARMFUL|RAID_PLAYER_DISPELLABLE`, `HARMFUL|CROWD_CONTROL`, `HARMFUL` + `isBossAura` groups (Stagger dropped, Experiment 3) - [x] Delete the copied event path from `core/Mainline/` -- [ ] **Other experiments** +- [x] **Other experiments** - [x] Experiment 7: cooldown edges (Retail, dummy and boss): nothing secret; item cooldowns aren't applied yet at `UNIT_SPELLCAST_SUCCEEDED` - [x] Experiment 8: tooltip methods on each Classic client: all five exist on Retail, Era, TBC, MoP (Forever and Titan not tested); index branch removed, instance-ID tooltips verified on all four - - [ ] Experiment 10: item and slot cooldowns refresh when they start and when they shorten - (`SPELL_UPDATE_COOLDOWN` only today; `BAG_UPDATE_COOLDOWN` may be needed) + - [x] Experiment 10: item and slot cooldowns refresh when they start and when they shorten: + `SPELL_UPDATE_COOLDOWN` is enough on Retail (start, shortening, end) and MoP (start); + `BAG_UPDATE_COOLDOWN` isn't needed. Era, TBC and the rest of MoP not tested - [ ] **Later iteration:** spell data ([spell-data-generation.md](spell-data-generation.md)) ## Facts @@ -626,6 +627,72 @@ combat** (target dummy, then a dungeon boss). Filger's icons start and follow the cooldown, including a cooldown that shortens. - Pass: the icons stay correct with today's events. Otherwise register the missing event for item and slot rows only. + - Now `/filger exp10 [itemID]` (`core/development.lua`): toggles a log of `SPELL_UPDATE_COOLDOWN`, + `BAG_UPDATE_COOLDOWN` and `ACTIONBAR_UPDATE_COOLDOWN`. A line prints when an event sees slot + 13/14/item values that differ from what that same event saw last, so each event's first view of a + start, a shortening and an end shows up once. + - **Source (2026-10-23, `wow-ui-source` live, classic, classic_anniversary, classic_era, forever, + classic_titan):** + - `BAG_UPDATE_COOLDOWN` exists on every client, with no payload (`ContainerDocumentation.lua`). + - Blizzard refreshes item and slot cooldowns on `BAG_UPDATE_COOLDOWN`: equipped slots + (`PaperDollItemSlotButton_OnEvent`, Retail and Classic), bags (`ContainerFrame`), the bank, + the objective tracker's quest items and the Retail `CooldownViewer` (`OnBagUpdateCooldownEvent`). + Action buttons use `ACTIONBAR_UPDATE_COOLDOWN`. No Blizzard item or slot button uses + `SPELL_UPDATE_COOLDOWN`. + - Retail and Forever `SPELL_UPDATE_COOLDOWN` carry a fifth argument `itemID` ("the itemID associated + with the cast of the spell"); Classic clients don't. So on Retail an item use likely fires + `SPELL_UPDATE_COOLDOWN` too, but whether it fires again when an item cooldown shortens is + **UNVERIFIED**. + - **Result (2026-10-23, Retail, first run):** `/filger exp10 211878`, trinket in slot 14 and the potion + used. The client is inferred from spell 1221146 (seen on Retail in Experiment 7); combat state not + recorded. + - Both starts are already readable in the same frame on all three events, in the order + `ACTIONBAR_UPDATE_COOLDOWN`, `BAG_UPDATE_COOLDOWN`, `SPELL_UPDATE_COOLDOWN`: trinket at 37940.8 + (`slot14 37940.6/120.0`, `spellID 1221146`), potion at 37957.9 (`item 37957.7/300.0`, + `spellID 216521`). This confirms 1221146 is the trinket's use spell. + - The `itemID` argument was `nil` on both item uses, despite its documentation. + - So starts work with today's events: Filger re-checks slot and item rows on every + `SPELL_UPDATE_COOLDOWN` (`cooldown_proto:UpdateCooldowns`). + - Not covered: a shortening, the end of a cooldown (the log stopped before either), Filger's icons, + and the Classic clients. + - **Result (2026-10-23, Retail, same session continued):** + - **No event fires when a cooldown ends.** The trinket was due at 38060.6, and 0/0 first showed at + 38063.0 on an unrelated cast (`spellID 458503`). The potion was due at 38257.7 and showed 0/0 at + 38261.4 (`spellID 450552`). Filger doesn't depend on this: `button_proto:OnUpdate` hides the icon + at `expirationTime`. + - **Shortening is reported.** The trinket started at 38290.4, then its start moved 2 s earlier about + once a second (38288.4 … 38214.4) and it ended at 38334.0, roughly 76 s early. Every step showed + up in the same frame on all three events, with `SPELL_UPDATE_COOLDOWN` carrying a player spell + (1219102, 437965, 1263408, …). Filger re-reads slot rows on each of these events, so + `expirationTime` follows the shortening. Each line is a change, so this doesn't show whether the + shortening triggers the event or rides on the casts. + - `BAG_UPDATE_COOLDOWN` never reported a change that `SPELL_UPDATE_COOLDOWN` missed. At 38213.1 it + didn't fire while `SPELL_UPDATE_COOLDOWN` (`spellID nil`) and `ACTIONBAR_UPDATE_COOLDOWN` did, + with `GetItemCooldown(211878)` returning `nil`. Then 0/0 at 38213.7, then 37958.9/300 at 38214.3: + the start moved 1.2 s later. Cause not checked. + - The `itemID` argument stayed `nil` on every event. + - **Retail passes for starts, shortenings and ends with today's events.** Filger's trinket icon + followed the cooldown. The potion showed no icon: `data/` has slot rows only + (`data/shared/cooldowns.lua`: slots 2, 6, 13, 14), and there's no item-row constructor. + `buildCooldowns` (`core/data.lua`) would also drop an `itemId` row, since it keeps only rows + with `slotId` or a known `spellId`. The `data.itemId` branch in `ProcessCooldownData` is + unreachable today. Classic clients still to run. + - **Result (2026-10-23, MoP, Monk, in combat):** `/filger exp10 76097` (Master Healing Potion), trinket + in slot 14 and the potion used. + - Trinket start: `slot14 40321.4/90.0` readable at 40321.6 on all three events in the same frame + (`SPELL_UPDATE_COOLDOWN` `spellID 146395`). The `itemID` argument is `nil`, as the MoP docs say. + - **Potion in combat:** from 40326.1 (`spellID 105708`), `GetItemCooldown(76097)` returned + `/60.0`: the start moved forward with every read (40326.1 … 40330.7). This looks like + Classic's "potion cooldown starts when you leave combat". The log didn't show `enabled` and + stopped before combat ended, so it's **UNVERIFIED**. If it holds, Filger's `start + duration` + would show a full 60 s forever while in combat, unless `enabled` is 0 (`FilterCooldown` hides + it). Moot: potions aren't tracked (decided 2026-10-23; all potions share one cooldown and the + item IDs change every patch and expansion). + - `SPELL_UPDATE_COOLDOWN` saw every change. `BAG_UPDATE_COOLDOWN` skipped some (40326.8, 40327.6, + 40327.8, 40328.7) and fired 0.2 s after the others at 40272.4. `ACTIONBAR_UPDATE_COOLDOWN` fired + alone once (40326.2). + - Not covered: the trinket end, a shortening, the potion after leaving combat, Filger's trinket icon. + - `/filger exp10` now also prints `enabled` (third value) for slots and the item. ## Open questions for you From 93d34fa1c7a8de47760db8656556bd59d0f5b88c Mon Sep 17 00:00:00 2001 From: pedrozc90 Date: Fri, 23 Oct 2026 09:17:15 -0300 Subject: [PATCH 13/13] dedupe cooldown buttons by active override spell Avenging Wrath (31884) was listed three times for paladin, and in Protection Sentinel (389539) overrides it, so one cast showed four buttons. Remove the duplicate data entries, skip repeated spell IDs when building the cooldown list, and show one button per active spell (C_Spell.GetOverrideSpell, when available). --- Filger.lua | 10 +++++++++- core/data.lua | 6 +++++- data/Standard/cooldowns.lua | 4 +--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Filger.lua b/Filger.lua index 3aaba99..cdd22d0 100644 --- a/Filger.lua +++ b/Filger.lua @@ -8,6 +8,7 @@ local GetInventoryItemLink = _G.GetInventoryItemLink local GetItemInfo = C_Item.GetItemInfo local GetItemCooldown = C_Container.GetItemCooldown local GetInventoryItemCooldown = _G.GetInventoryItemCooldown +local GetOverrideSpell = C_Spell and C_Spell.GetOverrideSpell local C_Secrets = _G.C_Secrets local issecretvalue = _G.issecretvalue local C_StringUtil = _G.C_StringUtil @@ -358,6 +359,8 @@ do index = index, name = name, spellId = data.spellId, + -- a talent can replace one tracked spell with another (e.g. Sentinel overrides Avenging Wrath) + activeSpellId = data.spellId and GetOverrideSpell and GetOverrideSpell(data.spellId) or data.spellId, slotId = data.slotId, itemId = data.itemId, icon = icon, @@ -400,9 +403,14 @@ do -- instead of removing auras one by one, just wipe the tables entirely -- and repopulate them, multiple table.remove calls are insanely slow self.sorted = table.wipe(self.sorted or {}) + self.shownSpells = table.wipe(self.shownSpells or {}) for _, data in next, self.active do - table.insert(self.sorted, data) + local id = data.activeSpellId + if not id or not self.shownSpells[id] then + if id then self.shownSpells[id] = true end + table.insert(self.sorted, data) + end end table.sort(self.sorted, self.SortCooldowns) diff --git a/core/data.lua b/core/data.lua index b228288..9119765 100644 --- a/core/data.lua +++ b/core/data.lua @@ -94,10 +94,14 @@ end -- player class rows first, then "ALL" rows (racials, gear), each in source order local buildCooldowns = function(Filger) local cooldowns = {} + local seen = {} for _, key in ipairs({ Filger.class, "ALL" }) do for _, source in ipairs(cooldownSources) do for _, row in ipairs(source[key] or {}) do - if row.enabled then + if row.spellId and seen[row.spellId] then + Filger:warn("COOLDOWN", "Spell " .. row.spellId .. " is duplicated.") + elseif row.enabled then + if row.spellId then seen[row.spellId] = true end if row.slotId or Filger.GetSpellInfo(row.spellId) then table.insert(cooldowns, row) else diff --git a/data/Standard/cooldowns.lua b/data/Standard/cooldowns.lua index f2af806..13449c9 100644 --- a/data/Standard/cooldowns.lua +++ b/data/Standard/cooldowns.lua @@ -155,7 +155,7 @@ local data = { CreateSpellCooldown(1022), -- Blessing of Protection CreateSpellCooldown(1044), -- Blessing of Freedom CreateSpellCooldown(6940), -- Blessing of Sacrifice - CreateSpellCooldown(31884), -- Avenging Wrath (Holy) + CreateSpellCooldown(31884), -- Avenging Wrath CreateSpellCooldown(96231, false), -- Rebuke CreateSpellCooldown(115750), -- Blinding Light CreateSpellCooldown(375576), -- Divine Toll @@ -176,13 +176,11 @@ local data = { CreateSpellCooldown(204018), -- Blessing of Spellwarding CreateSpellCooldown(327193), -- Moment of Glory CreateSpellCooldown(378974), -- Bastion of Light - CreateSpellCooldown(31884), -- Sanctified Wrath CreateSpellCooldown(389539), -- Sentinel CreateSpellCooldown(387174), -- Eye of Tyr -- Retribution CreateSpellCooldown(403876), -- Divine Protection - CreateSpellCooldown(31884), -- Avenging Wrath: Might CreateSpellCooldown(255937, false), -- Wake of Ashes CreateSpellCooldown(343721), -- Final Reckoning CreateSpellCooldown(343527, false), -- Execution Sentence