Document the CC, immunity and DR design

Records why the three identities are separate, the DBC mechanic table each
[cc:type] now follows, and which DR pools the immunity safeguard may act on.

Every vMaNGOS and nampower claim behind the preceding four commits is cited
to the source line that establishes it, so the next person to touch this can
check the premises instead of re-deriving them: the DRTYPE_ALL set is three
stun pools and nothing else, Charge and Intercept are the only triggered
stuns forced into the controlled pool, and SPELL_MISS puts the spell ID in
arg3.

Also notes the mouseover learning gap as designed behavior rather than a bug,
since the failure on the other side is permanent bad SavedVariables data.
This commit is contained in:
Brues
2026-09-15 08:08:51 -05:00
parent 4474a607f7
commit 37ca7cef40
+188
View File
@@ -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`.