diff --git a/docs/CC-IMMUNITY-DR.md b/docs/CC-IMMUNITY-DR.md new file mode 100644 index 0000000..423898e --- /dev/null +++ b/docs/CC-IMMUNITY-DR.md @@ -0,0 +1,188 @@ +# CC, Learned Immunity and Diminishing Returns + +How SuperCleveRoidMacros classifies crowd control, what it learns about NPC +immunity, and why diminishing returns only ever acts as a brake on that +learning. Source citations are into `C:\Git\vmangos` and `C:\Git\nampower`. + +## Three identities, never conflated + +```text +ccType Exact DBC mechanic. Backs [cc:*] and CC landing verification. + Conditionals.lua, CleveRoids.CCMechanics + +immunityType Exact DBC mechanic, recorded against an NPC name when a CC + spell comes back IMMUNE. + Utility.lua, MECHANIC_TO_IMMUNITY_TYPE / GetSpellImmunityType + +DR type The real Vanilla diminishing-return pool. Internal only. + Utility.lua, GetSpellImmunityDRType +``` + +The first two are the same taxonomy used for different purposes; the third is +a separate axis and is never exposed to macros. Folding any pair together is +what produced the bugs this design replaces: a target that resisted Gouge got +written down as stun-immune, and every later Kidney Shot was suppressed +against a target that had never resisted one. + +## `[cc:*]` follows the DBC mechanic exactly + +One conditional, one mechanic. Several names may alias a single mechanic, but +no name spans two: + +```text +[cc:stun] -> mechanic 12 only +[cc:freeze] -> mechanic 13 only +[cc:knockout] -> mechanic 14 only +[cc:sap] -> mechanic 30 only +[cc:daze] -> mechanic 27 only +``` + +Bare `[cc]` and `[cc:any]` still mean "any loss-of-control effect" and are +driven by `CleveRoids.CCTypesLossOfControl`, which is deliberately independent +of the per-mechanic table. An aggregate does not require redefining the parts. + +Current aliases: + +```text +slow -> snare +disoriented -> disorient +grip -> fumble (mechanic 6 is DBC Fumble) +incap / incapacitate / incapacitated -> knockout (immunity input only) +``` + +Aliases exist so published macros keep working. They resolve to the canonical +mechanic name before anything is stored, so SavedVariables only ever hold +canonical keys. + +## Vanilla DBC mechanic taxonomy + +The identities SCRM names. Where a mechanic is named at all, it is named as +itself: + +| DBC mechanic | ID | Canonical `ccType` | +|---|---:|---| +| Charm | 1 | `charm` | +| Disoriented | 2 | `disorient` | +| Disarm | 3 | `disarm` | +| Distract | 4 | `distract` | +| Fear | 5 | `fear` | +| Fumble | 6 | `fumble` | +| Root | 7 | `root` | +| Pacify | 8 | `pacify` | +| Silence | 9 | `silence` | +| Sleep | 10 | `sleep` | +| Snare | 11 | `snare` | +| Stun | 12 | `stun` | +| Freeze | 13 | `freeze` | +| Knockout | 14 | `knockout` | +| Bleed | 15 | `bleed` | +| Polymorph | 17 | `polymorph` | +| Banish | 18 | `banish` | +| Shackle | 20 | `shackle` | +| Turn | 23 | `turn` | +| Horror | 24 | `horror` | +| Interrupt | 26 | `interrupt` | +| Daze | 27 | `daze` | +| Sapped | 30 | `sap` | + +Mechanics 16, 19, 21, 22, 25, 28 and 29 (Bandage, Shield, Mount, Persuade, +Invulnerability, Discovery, Immune Shield) are real DBC values but are not +crowd control, so they get no conditional. + +## Diminishing returns + +DR matters here for exactly one reason: a target at DR level 4 returns +`IMMUNE`, and that must not be recorded as permanent immunity. Nothing else in +the addon consumes DR state, and there is no player-facing DR feature. + +### Only three pools can diminish a creature + +`GetDiminishingReturnsGroupType` in +`vmangos/src/game/Spells/SpellEntry.h:52-79` returns `DRTYPE_ALL` for exactly +three groups: + +```text +DIMINISHING_CONTROL_STUN +DIMINISHING_TRIGGER_STUN +DIMINISHING_KIDNEYSHOT +``` + +Every other staged group — sleep, both roots, fear, Warlock fear, charm, +polymorph, silence, disarm, Death Coil, freeze, banish, knockout — is +`DRTYPE_PLAYER`, and `Unit.cpp:7881` applies those only when the victim is a +player. So on an NPC target they can never generate a DR immunity, and they +must never hold back immunity learning. + +This is why `GetSpellImmunityDRType` returns nil for everything that is not +mechanic 12. It is not an approximation; it is the complete `DRTYPE_ALL` set. + +### Classifying a stun into its pool + +`SpellEntry::GetDiminishingReturnsGroup(bool triggered)` +(`vmangos/src/game/Spells/SpellEntry.cpp:281-432`) resolves the pool, and the +`triggered` argument means the runtime fact of whether an aura triggered the +cast — this is not a pure DBC property. SCRM reconstructs it from nampower +`SPELL_CAST_EVENT` correlation, which only fires for client-initiated casts: +a spell ID with a recent `CleveRoids.pendingCasts` entry was cast deliberately, +one without reached `SPELL_GO`/`SPELL_MISS` as a proc. + +```text +Kidney Shot -> stun_kidneyshot +Charge / Intercept stun -> stun_control +other mechanic-12, client cast -> stun_control +other mechanic-12, proc -> stun_trigger +``` + +Kidney Shot is matched on rogue family bit 21 (`0x00200000`, +`SpellClassMask.h:228`) rather than a bare ID list, so custom ranks that keep +their DBC family data still classify correctly. Charge (7922) and Intercept +(20253, 20614, 20615) are internally triggered but explicitly returned as +controlled stun by `SpellEntry.cpp:389-399`; they are the only such exceptions +in 1.12. + +### Pools that exist but do not apply + +Recorded so nobody re-adds them to the safeguard. Knockout and Sap are +distinct mechanics that share `DIMINISHING_KNOCKOUT` +(`SpellEntry.cpp:424-425`), and Horror maps to `DIMINISHING_DEATHCOIL` +(`SpellEntry.cpp:428-429`). Both are `DRTYPE_PLAYER`. `DIMINISHING_LIMITONLY` +is a PvP duration cap rather than a staged pool, and `DIMINISHING_NONE` is not +a pool at all. + +### The safeguard itself + +`recentCCHits[targetGUID][drType]` counts landed CC per DR pool, so the three +stun pools keep separate histories. A hit more than `DR_RESET_WINDOW` (20s) +after the previous one in that pool restarts the count at 1, matching the DR +decay the skip check uses. Without that reset the counter only ever climbed, +so three stuns on a target permanently disqualified it from ever teaching the +addon anything again. + +An `IMMUNE` result is treated as DR, and discarded, only while the pool holds +three or more hits inside the window. Otherwise it is learned. + +## nampower `SPELL_MISS` arguments + +`TriggerSpellMissEvent` (`nampower/spellevents.cpp:888-917`) signals both +`SPELL_MISS_SELF` and `SPELL_MISS_OTHER` with the same layout: + +```text +arg1 = casterGuid arg2 = targetGuid arg3 = spellId arg4 = missInfo +``` + +`SPELL_MISS_SELF` carries `casterGuid` too, even though it is by definition +the player — the event is chosen by comparing the caster to the active player +GUID, not by changing the payload. Reading `arg1` as the spell ID hands a GUID +string to the immunity recorder and silently loses every miss. + +## Known limitation: mouseover casts + +`ProcessSpellMissSelf` refuses to learn permanent immunity when it cannot +resolve a queryable unit, because the temporary-immunity-buff check needs one +— an NPC under Divine Shield must not be recorded as permanently immune. A CC +cast at a mouseover that is not the current target can therefore return +`IMMUNE` without being learned. + +This is the safeguard working as designed, not a known bug. Do not loosen it +without a reproduction showing a real failure, since the failure mode on the +other side is permanent bad data in `CleveRoids_ImmunityData`.