Skip to content

core/player: re-evaluate corruption penalties without re-applying them - #81

Draft
svenbledt wants to merge 2 commits into
Hextv:mainfrom
svenbledt:pr/corruption-sync
Draft

core/player: re-evaluate corruption penalties without re-applying them#81
svenbledt wants to merge 2 commits into
Hextv:mainfrom
svenbledt:pr/corruption-sync

Conversation

@svenbledt

Copy link
Copy Markdown
Collaborator

Changes Proposed:

This PR proposes changes to:

  • Core (units, players, creatures, game systems).
  • Scripts — one new debug command, .debug corruption.
  • Database (SAI, creatures, etc).

Player::UpdateCorruption keeps the CorruptionEffects.db2 threshold penalties in step with the
player's effective corruption. It had three defects, each of which left the penalties out of step.

It re-applied live auras. The loop ended in an unconditional CastSpell(this, aura, true), so
every corruption rating change re-cast penalties that were already present — restarting their
durations and resetting their proc state. It now casts only what is missing and lets an already
applied aura recalculate its own amounts in place.

It was never re-evaluated on an area change. Each CorruptionEffects row carries a
PlayerConditionID, but the sync ran only from UpdateRating(CR_CORRUPTION | CR_CORRUPTION_RESISTANCE) — a gear or rating-aura change. A player who walked into an area where the
condition stopped matching kept the penalty indefinitely; one who walked out never regained it.
UpdateArea now schedules a sync.

It was never re-evaluated on resurrect. RemoveAllAurasOnDeath strips the penalties, and neither
a rating change nor an area transition necessarily follows, so reviving where you fell left the
player uncorrupted until they happened to cross an area border. That is why a corpse run appeared to
fix it and .revive did not.

Why the sync is coalesced rather than run at each mutation site

Syncing at every mutation site is wrong in the other direction. A bulk item rebuild strips every
item's corruption before re-applying it, so a per-mutation sync walks the total down to zero and
tears down each tier's aura on the way back up. The sync is instead coalesced behind a dirty flag and
runs once per Player::Update tick.

The two item-level rebuilds that did not bracket themselves with SetCanModifyStats now do, and
UpdateItemLevelAreaBasedScaling settles its derived stats afterwards — without that, the max health
it scales the player to was one rebuild stale.

GetEffectiveCorruption is split out of UpdateCorruption so callers that need the value do not
repeat the rating subtraction.

.debug corruption

When a penalty fails to apply there are three gates that could have rejected it — the threshold, the
PlayerConditionID, or the row being flagged disabled — and no way to tell which. The alternative is
equipping corrupted gear and inferring the answer from what happens.

The command prints the selected player's effective corruption alongside the two ratings it derives
from, then every CorruptionEffects row with its threshold, aura, condition, the verdict on that
player, and whether the aura is actually applied. A server whose client data has no
CorruptionEffects rows at all reports that explicitly, since it is otherwise indistinguishable from
a core fault. RBAC-gated behind RBAC_PERM_COMMAND_DEBUG like the rest of the table.

AI-assisted Pull Requests

  • AI tools were used. Claude Code, model Claude Opus 5. Used for tracing the three missed
    re-evaluation paths and drafting. Every line has been reviewed and is defensible by the author.

Issues Addressed:

  • Closes nothing tracked.

SOURCE:

The changes have been validated through:

  • Live research (checked on live servers, e.g Classic WotLK, Retail, etc.)
  • Sniffs (remember to share them with the open source community!)
  • Video evidence, knowledge databases or other public sources.
  • The changes promoted by this pull request come partially or entirely from another project (cherry-pick).

The behaviour being matched is described by the client data itself: CorruptionEffects.db2 carries
MinCorruption, an Aura and a PlayerConditionID per row, which is what the sync is made to
honour. Retail's own behaviour — penalties appearing and disappearing as corruption crosses a
threshold, and being suppressed in certain areas — is public knowledge from 8.3.

Tests Performed:

  • Tested in-game by the author.
  • Tested in-game by other community members/someone else other than the author.
  • This pull request requires further testing and may have edge cases to be tested.

Built clean on Windows / VS 2022 x64 RelWithDebInfo, and verified in-game: penalties apply and remove
as corruption crosses each threshold, survive a gear swap, and return on resurrect.

Not covered by that test: the area-condition path. The re-evaluation on UpdateArea was reasoned
from the PlayerConditionID column and has not been exercised against an area where a corruption
condition actually changes state.

How to Test the Changes:

  • This pull request requires further testing. Steps below.
  1. .debug corruption on yourself with no corrupted gear — every row should report below-threshold.
  2. Equip corrupted gear until you cross a threshold and re-run it. The row should flip to applied,
    and the penalty aura should be on you.
  3. Swap another piece of gear without changing the total. On main the penalty's duration restarts;
    on this branch it does not.
  4. Die and .revive in place. On main the penalties stay gone until you cross an area border; on
    this branch they return on the next tick.
  5. Regression: this touches Player::Update and two item-level rebuild paths. Confirm max health and
    derived stats are correct after an item-level scaling change (a scaled instance, a timewalking
    zone).

Known Issues and TODO List:

  • The area-condition re-evaluation is untested in-game (see above).
  • The sync runs once per Player::Update tick when dirty. That is one extra
    CorruptionEffects walk per affected player per tick; it has not been profiled under load.

This is one of six PRs splitting a previously oversized branch apart. It is independent of the
other five and can merge on its own; the corruption feature PR stacks on top of it.

Player::UpdateCorruption had three problems, all of which left the
CorruptionEffects.db2 threshold penalties out of step with the player.

It re-applied live auras. The loop ended in an unconditional
CastSpell(this, aura, true), so every corruption rating change re-cast
penalties that were already present, restarting their durations and
resetting their proc state. It now casts only what is missing and lets
an aura that is already applied recalculate its own amounts in place.

It was never re-evaluated on an area change. Each CorruptionEffects row
carries a PlayerConditionID, and the sync ran only from
UpdateRating(CR_CORRUPTION | CR_CORRUPTION_RESISTANCE) - a gear or
rating-aura change. A player who walked into an area where the condition
stopped matching kept the penalty indefinitely, and one who walked out
never regained it. UpdateArea now schedules a sync.

It was never re-evaluated on resurrect. RemoveAllAurasOnDeath strips the
penalties and neither a rating change nor an area transition follows, so
reviving where you fell left the player uncorrupted until they happened
to cross an area border. That is why a corpse run appeared to fix it and
.revive did not.

Syncing at each mutation site is wrong in the other direction: a bulk
item rebuild strips every item's corruption before re-applying it, so a
per-mutation sync walks the total down to zero and tears down each
tier's aura on the way. The sync is now coalesced behind a dirty flag
and runs once per Player::Update tick. The two item-level rebuilds that
did not bracket themselves with SetCanModifyStats now do, and
UpdateItemLevelAreaBasedScaling settles its derived stats afterwards -
without that the max health it scales the player to was one rebuild
stale.

GetEffectiveCorruption is split out of UpdateCorruption so callers that
need the value do not repeat the rating subtraction.

Verified in-game: penalties apply and remove as corruption crosses each
threshold, survive a gear swap, and return on resurrect.
Corruption penalties are driven entirely by CorruptionEffects.db2, and
when one fails to apply there is no way to see which of the three gates
rejected it - the threshold, the PlayerConditionID, or the row being
flagged disabled. The only alternative is equipping corrupted gear and
inferring the answer from what happens.

.debug corruption prints the selected player's effective corruption
alongside the two ratings it derives from, then every CorruptionEffects
row with its threshold, aura, condition, the verdict on that player and
whether the aura is actually applied. A server whose client data has no
CorruptionEffects rows at all reports that explicitly, since it is
otherwise indistinguishable from a core fault.

RBAC-gated behind RBAC_PERM_COMMAND_DEBUG like the rest of the table.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants