diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 78ac3a3a..90cb4454 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -1088,6 +1088,12 @@ void Player::Update(uint32 p_time) if (!IsInWorld()) return; + if (m_corruptionNeedsUpdate) + { + m_corruptionNeedsUpdate = false; + UpdateCorruption(); + } + // undelivered mail if (m_nextMailDelivereTime && m_nextMailDelivereTime <= time(nullptr)) { @@ -4635,6 +4641,11 @@ void Player::ResurrectPlayer(float restore_percent, bool applySickness) setDeathState(ALIVE); + // RemoveAllAurasOnDeath stripped the corruption penalties and nothing puts them back: + // the sync is driven by a rating change or an area transition, and resurrecting is + // neither. Reviving where you fell otherwise leaves the player uncorrupted. + ScheduleCorruptionUpdate(); + // add the flag to make sure opcode is always sent AddUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); SetWaterWalking(false); @@ -5526,7 +5537,11 @@ void Player::UpdateRating(CombatRating cr) break; case CR_CORRUPTION: case CR_CORRUPTION_RESISTANCE: - UpdateCorruption(); + // Bulk rebuilds strip every item's corruption before re-applying it, so syncing per + // mutation would walk the total down to zero and tear down each tier's aura on the + // way. Callers that clear this flag are responsible for one sync when they finish. + if (affectStats) + ScheduleCorruptionUpdate(); break; case CR_SPEED: case CR_RESILIENCE_PLAYER_DAMAGE: @@ -7689,6 +7704,11 @@ void Player::UpdateArea(uint32 newArea) if (garrison.second->IsAllowedArea(newArea)) garrison.second->Enter(); } + + // A corruption penalty can be gated on a PlayerConditionID that reads the player's + // location, and nothing else re-evaluates those conditions when only the area + // changes. Safe on every transition: the sync casts only what is missing. + ScheduleCorruptionUpdate(); } } @@ -30832,12 +30852,15 @@ void Player::CreateChallengeKey(Item* item) void Player::SetEffectiveLevelAndMaxItemLevel(uint32 effectiveLevel, uint32 maxItemLevel) { float healthPct = GetHealthPct(); + SetCanModifyStats(false); _RemoveAllItemMods(); SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::EffectiveLevel), effectiveLevel); SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::MaxItemLevel), maxItemLevel); _ApplyAllItemMods(); + SetCanModifyStats(true); + UpdateAverageItemLevel(); uint32 basemana = 0; @@ -30865,9 +30888,15 @@ void Player::UpdateItemLevelAreaBasedScaling() if (_usePvpItemLevels != pvpActivity) { float healthPct = GetHealthPct(); + SetCanModifyStats(false); _RemoveAllItemMods(); ActivatePvpItemLevels(pvpActivity); _ApplyAllItemMods(); + SetCanModifyStats(true); + // The bracket defers every derived stat, not only corruption, and neither item-mod + // pass recomputes on its own - so settle them all once here, as _ApplyAllStatBonuses + // does, leaving the max health scaled below freshly computed rather than stale. + UpdateAllStats(); SetHealth(CalculatePct(GetMaxHealth(), healthPct)); } // @todo other types of power scaling diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 1c19ed79..537d2bb8 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1993,7 +1993,12 @@ class TC_GAME_API Player : public Unit, public GridObject void UpdateLeechPercentage(); void UpdateSpellCritChance(); + float GetEffectiveCorruption() const; void UpdateCorruption(); + // Corruption penalties are synced once per tick rather than at each mutation site. + // A single item swap can move several corrupted pieces through several intermediate + // states; syncing each one would apply and remove the same penalty auras repeatedly. + void ScheduleCorruptionUpdate() { m_corruptionNeedsUpdate = true; } void UpdateArmorPenetration(int32 amount); void UpdateExpertise(WeaponAttackType attType); void ApplyManaRegenBonus(int32 amount, bool apply); @@ -3016,6 +3021,7 @@ class TC_GAME_API Player : public Unit, public GridObject bool m_canParry; bool m_canBlock; bool m_canTitanGrip; + bool m_corruptionNeedsUpdate = false; uint32 m_titanGripPenaltySpellId; uint8 m_swingErrorMsg; diff --git a/src/server/game/Entities/Unit/StatSystem.cpp b/src/server/game/Entities/Unit/StatSystem.cpp index c8d70d0e..14031668 100644 --- a/src/server/game/Entities/Unit/StatSystem.cpp +++ b/src/server/game/Entities/Unit/StatSystem.cpp @@ -732,9 +732,14 @@ void Player::UpdateSpellCritChance() SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::SpellCritPercentage), crit); } +float Player::GetEffectiveCorruption() const +{ + return GetRatingBonusValue(CR_CORRUPTION) - GetRatingBonusValue(CR_CORRUPTION_RESISTANCE); +} + void Player::UpdateCorruption() { - float effectiveCorruption = GetRatingBonusValue(CR_CORRUPTION) - GetRatingBonusValue(CR_CORRUPTION_RESISTANCE); + float const effectiveCorruption = GetEffectiveCorruption(); for (CorruptionEffectsEntry const* corruptionEffect : sCorruptionEffectsStore) { if ((CorruptionEffectsFlag(corruptionEffect->Flags) & CorruptionEffectsFlag::Disabled) != CorruptionEffectsFlag::None) @@ -755,7 +760,13 @@ void Player::UpdateCorruption() } } - CastSpell(this, corruptionEffect->Aura, true); + // Re-casting a penalty that is already applied restarts its duration and resets its + // proc state, and this runs on every rating change and area transition. Cast only + // what is missing; what is already there recomputes its magnitudes in place. + if (Aura* corruptionAura = GetAura(corruptionEffect->Aura)) + corruptionAura->RecalculateAmountOfEffects(); + else + CastSpell(this, corruptionEffect->Aura, true); } } diff --git a/src/server/scripts/Commands/cs_debug.cpp b/src/server/scripts/Commands/cs_debug.cpp index db540dba..6d1814da 100644 --- a/src/server/scripts/Commands/cs_debug.cpp +++ b/src/server/scripts/Commands/cs_debug.cpp @@ -117,6 +117,7 @@ class debug_commandscript : public CommandScript { "transportState", rbac::RBAC_PERM_COMMAND_DEBUG, false, &HandleDebugTransportStateCommand, "" }, { "worldstate" , rbac::RBAC_PERM_COMMAND_DEBUG, false, &HandleDebugWorldStateCommand, "" }, { "wsexpression" , rbac::RBAC_PERM_COMMAND_DEBUG, false, &HandleDebugWSExpressionCommand, "" }, + { "corruption", rbac::RBAC_PERM_COMMAND_DEBUG, false, &HandleDebugCorruptionCommand, "" }, }; static std::vector commandTable = { @@ -1429,6 +1430,48 @@ class debug_commandscript : public CommandScript return true; } + static bool HandleDebugCorruptionCommand(ChatHandler* handler, char const* /*args*/) + { + Player* target = handler->getSelectedPlayerOrSelf(); + if (!target) + { + handler->SendSysMessage(LANG_NO_CHAR_SELECTED); + handler->SetSentErrorMessage(true); + return false; + } + + float const effectiveCorruption = target->GetEffectiveCorruption(); + handler->PSendSysMessage("Corruption for %s: effective %.2f (corruption %.2f - resistance %.2f)", + target->GetName().c_str(), effectiveCorruption, + target->GetRatingBonusValue(CR_CORRUPTION), target->GetRatingBonusValue(CR_CORRUPTION_RESISTANCE)); + + uint32 rows = 0; + for (CorruptionEffectsEntry const* corruptionEffect : sCorruptionEffectsStore) + { + ++rows; + + char const* state; + if ((CorruptionEffectsFlag(corruptionEffect->Flags) & CorruptionEffectsFlag::Disabled) != CorruptionEffectsFlag::None) + state = "disabled"; + else if (effectiveCorruption < corruptionEffect->MinCorruption) + state = "below threshold"; + else if (PlayerConditionEntry const* playerCondition = sPlayerConditionStore.LookupEntry(corruptionEffect->PlayerConditionID)) + state = ConditionMgr::IsPlayerMeetingCondition(target, playerCondition) ? "qualifies" : "condition failed"; + else + state = "qualifies"; + + handler->PSendSysMessage(" row %u: min %.2f aura %d cond %d - %s, aura %s", + corruptionEffect->ID, corruptionEffect->MinCorruption, corruptionEffect->Aura, + corruptionEffect->PlayerConditionID, state, + target->HasAura(corruptionEffect->Aura) ? "APPLIED" : "absent"); + } + + if (!rows) + handler->SendSysMessage("CorruptionEffects.db2 holds no rows - no penalty can apply on this server. That is missing client data, not a core fault."); + + return true; + } + static bool HandleDebugMaxItemLevelCommand(ChatHandler* handler, char const* args) { CommandArgs commandArgs = CommandArgs(handler, args, { CommandArgs::ARG_UINT, CommandArgs::ARG_UINT });