Files
Brues 37ca7cef40 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.
2026-09-15 08:10:12 -05:00

7.2 KiB

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

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:

[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:

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:

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.

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:

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.