Core/Units: Fix stats from items being incorrectly affected by SPELL_AURA_MOD_PERCENT_STAT (#31936)
This commit is contained in:
@@ -2445,7 +2445,7 @@ void Player::InitStatsForLevel(bool reapplyMods)
|
||||
//set create powers
|
||||
SetCreateMana(basemana);
|
||||
|
||||
SetArmor(int32(m_createStats[STAT_AGILITY] * 2), 0);
|
||||
SetArmor(int32(GetCreateStat(STAT_AGILITY) * 2), 0);
|
||||
|
||||
InitStatBuffMods();
|
||||
|
||||
@@ -2504,7 +2504,7 @@ void Player::InitStatsForLevel(bool reapplyMods)
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::DodgePercentage), 0.0f);
|
||||
|
||||
// set armor (resistance 0) to original value (create_agility*2)
|
||||
SetArmor(int32(m_createStats[STAT_AGILITY] * 2), 0);
|
||||
SetArmor(int32(GetCreateStat(STAT_AGILITY) * 2), 0);
|
||||
SetBonusResistanceMod(SPELL_SCHOOL_NORMAL, 0);
|
||||
// set other resistance to original value (0)
|
||||
for (uint8 i = SPELL_SCHOOL_HOLY; i < MAX_SPELL_SCHOOL; ++i)
|
||||
@@ -8077,23 +8077,23 @@ void Player::_ApplyItemBonuses(Item* item, uint8 slot, bool apply)
|
||||
HandleStatFlatModifier(UNIT_MOD_HEALTH, BASE_VALUE, float(val), apply);
|
||||
break;
|
||||
case ITEM_MOD_AGILITY: // modify agility
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_AGILITY);
|
||||
break;
|
||||
case ITEM_MOD_STRENGTH: //modify strength
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_STRENGTH);
|
||||
break;
|
||||
case ITEM_MOD_INTELLECT: //modify intellect
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_INTELLECT);
|
||||
break;
|
||||
case ITEM_MOD_SPIRIT: //modify spirit
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_SPIRIT, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_SPIRIT, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_SPIRIT);
|
||||
break;
|
||||
case ITEM_MOD_STAMINA: //modify stamina
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STAMINA, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STAMINA, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_STAMINA);
|
||||
break;
|
||||
case ITEM_MOD_DEFENSE_SKILL_RATING:
|
||||
@@ -8257,28 +8257,28 @@ void Player::_ApplyItemBonuses(Item* item, uint8 slot, bool apply)
|
||||
ApplyRatingMod(CR_STURDINESS, int32(val), apply);
|
||||
break;
|
||||
case ITEM_MOD_AGI_STR_INT:
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, TOTAL_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, TOTAL_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_AGILITY);
|
||||
UpdateStatBuffMod(STAT_STRENGTH);
|
||||
UpdateStatBuffMod(STAT_INTELLECT);
|
||||
break;
|
||||
case ITEM_MOD_AGI_STR:
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, TOTAL_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_AGILITY);
|
||||
UpdateStatBuffMod(STAT_STRENGTH);
|
||||
break;
|
||||
case ITEM_MOD_AGI_INT:
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_AGILITY, TOTAL_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_AGILITY);
|
||||
UpdateStatBuffMod(STAT_INTELLECT);
|
||||
break;
|
||||
case ITEM_MOD_STR_INT:
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, BASE_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_STRENGTH, TOTAL_VALUE, float(val), apply);
|
||||
HandleStatFlatModifier(UNIT_MOD_STAT_INTELLECT, TOTAL_VALUE, float(val), apply);
|
||||
UpdateStatBuffMod(STAT_STRENGTH);
|
||||
UpdateStatBuffMod(STAT_INTELLECT);
|
||||
break;
|
||||
|
||||
@@ -106,10 +106,11 @@ int32 Unit::GetCreatePowerValue(Powers power) const
|
||||
|
||||
bool Player::UpdateStats(Stats stat)
|
||||
{
|
||||
// value = ((base_value * base_pct) + total_value) * total_pct
|
||||
// value = (base_value * base_pct) * total_pct + total_value * total_pct
|
||||
float value = GetTotalStatValue(stat);
|
||||
|
||||
SetStat(stat, int32(value));
|
||||
UpdateStatBuffMod(stat);
|
||||
|
||||
if (stat == STAT_STAMINA || stat == STAT_INTELLECT || stat == STAT_STRENGTH)
|
||||
{
|
||||
@@ -201,6 +202,7 @@ bool Player::UpdateAllStats()
|
||||
{
|
||||
float value = GetTotalStatValue(Stats(i));
|
||||
SetStat(Stats(i), int32(value));
|
||||
UpdateStatBuffMod(Stats(i));
|
||||
}
|
||||
|
||||
UpdateArmor();
|
||||
|
||||
@@ -348,7 +348,6 @@ Unit::Unit(bool isWorldObject) :
|
||||
for (uint8 i = 0; i < UNIT_MOD_END; ++i)
|
||||
{
|
||||
m_auraFlatModifiersGroup[i][BASE_VALUE] = 0.0f;
|
||||
m_auraFlatModifiersGroup[i][BASE_PCT_EXCLUDE_CREATE] = 100.0f;
|
||||
m_auraFlatModifiersGroup[i][TOTAL_VALUE] = 0.0f;
|
||||
m_auraPctModifiersGroup[i][BASE_PCT] = 1.0f;
|
||||
m_auraPctModifiersGroup[i][TOTAL_PCT] = 1.0f;
|
||||
@@ -369,7 +368,6 @@ Unit::Unit(bool isWorldObject) :
|
||||
m_weaponDamage[i][MAXDAMAGE] = BASE_MAXDAMAGE;
|
||||
}
|
||||
|
||||
m_createStats = { };
|
||||
m_floatStatPosBuff = { };
|
||||
m_floatStatNegBuff = { };
|
||||
|
||||
@@ -5341,30 +5339,18 @@ void Unit::UpdateStatBuffMod(Stats stat)
|
||||
{
|
||||
float modPos = 0.0f;
|
||||
float modNeg = 0.0f;
|
||||
float factor = 0.0f;
|
||||
|
||||
UnitMods const unitMod = static_cast<UnitMods>(UNIT_MOD_STAT_START + AsUnderlyingType(stat));
|
||||
|
||||
// includes value from items and enchantments
|
||||
float modValue = GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
if (modValue > 0.f)
|
||||
modPos += modValue;
|
||||
else
|
||||
modNeg += modValue;
|
||||
|
||||
if (IsGuardian())
|
||||
{
|
||||
modValue = static_cast<Guardian*>(this)->GetBonusStatFromOwner(stat);
|
||||
float modValue = static_cast<Guardian*>(this)->GetBonusStatFromOwner(stat);
|
||||
if (modValue > 0.f)
|
||||
modPos += modValue;
|
||||
else
|
||||
modNeg += modValue;
|
||||
}
|
||||
|
||||
// SPELL_AURA_MOD_STAT_BONUS_PCT only affects BASE_VALUE
|
||||
modPos = CalculatePct(modPos, std::max(GetFlatModifierValue(unitMod, BASE_PCT_EXCLUDE_CREATE), -100.0f));
|
||||
modNeg = CalculatePct(modNeg, std::max(GetFlatModifierValue(unitMod, BASE_PCT_EXCLUDE_CREATE), -100.0f));
|
||||
|
||||
modPos += GetTotalAuraModifier(SPELL_AURA_MOD_STAT, [stat](AuraEffect const* aurEff) -> bool
|
||||
{
|
||||
if ((aurEff->GetMiscValue() < 0 || aurEff->GetMiscValue() == stat) && aurEff->GetAmount() > 0)
|
||||
@@ -5379,25 +5365,35 @@ void Unit::UpdateStatBuffMod(Stats stat)
|
||||
return false;
|
||||
});
|
||||
|
||||
factor = GetTotalAuraMultiplier(SPELL_AURA_MOD_PERCENT_STAT, [stat](AuraEffect const* aurEff) -> bool
|
||||
{
|
||||
if (aurEff->GetMiscValue() == -1 || aurEff->GetMiscValue() == stat)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
float baseValue = GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
baseValue *= GetPctModifierValue(unitMod, BASE_PCT);
|
||||
baseValue *= GetPctModifierValue(unitMod, TOTAL_PCT);
|
||||
float baseModFromPct = baseValue - GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
|
||||
factor *= GetTotalAuraMultiplier(SPELL_AURA_MOD_TOTAL_STAT_PERCENTAGE, [stat](AuraEffect const* aurEff) -> bool
|
||||
{
|
||||
if (aurEff->GetMiscValue() == -1 || aurEff->GetMiscValue() == stat)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
float totalValue = GetFlatModifierValue(unitMod, TOTAL_VALUE);
|
||||
AddPct(totalValue, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_STAT_BONUS_PCT, stat));
|
||||
float totalModFromPct = totalValue * GetPctModifierValue(unitMod, TOTAL_PCT) - totalValue;
|
||||
float modsFromPct = baseModFromPct + totalModFromPct;
|
||||
|
||||
modPos *= factor;
|
||||
modNeg *= factor;
|
||||
// recalculate stat bonuses not applied by auras
|
||||
float nonAuraMod = totalValue;
|
||||
nonAuraMod -= modPos; // remove positive auras
|
||||
nonAuraMod -= modNeg; // remove negative auras
|
||||
|
||||
m_floatStatPosBuff[stat] = modPos;
|
||||
m_floatStatNegBuff[stat] = modNeg;
|
||||
// add item stat bonuses to positive
|
||||
if (nonAuraMod > 0.f)
|
||||
modPos += nonAuraMod;
|
||||
else
|
||||
modNeg += nonAuraMod;
|
||||
|
||||
// add pct mods
|
||||
if (modsFromPct > 0.f)
|
||||
modPos += modsFromPct;
|
||||
else
|
||||
modNeg += modsFromPct;
|
||||
|
||||
m_floatStatPosBuff[stat] = std::round(modPos);
|
||||
m_floatStatNegBuff[stat] = std::round(modNeg);
|
||||
|
||||
UpdateStatBuffModForClient(stat);
|
||||
}
|
||||
@@ -5408,6 +5404,18 @@ void Unit::UpdateStatBuffModForClient(Stats stat)
|
||||
SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::StatNegBuff, stat), int32(m_floatStatNegBuff[stat]));
|
||||
}
|
||||
|
||||
void Unit::SetCreateStat(Stats stat, float val)
|
||||
{
|
||||
UnitMods const unitMod = static_cast<UnitMods>(UNIT_MOD_STAT_START + AsUnderlyingType(stat));
|
||||
HandleStatFlatModifier(unitMod, BASE_VALUE, val, true);
|
||||
}
|
||||
|
||||
float Unit::GetCreateStat(Stats stat) const
|
||||
{
|
||||
UnitMods const unitMod = static_cast<UnitMods>(UNIT_MOD_STAT_START + AsUnderlyingType(stat));
|
||||
return GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
}
|
||||
|
||||
void Unit::_RegisterDynObject(DynamicObject* dynObj)
|
||||
{
|
||||
m_dynObj.push_back(dynObj);
|
||||
@@ -9891,7 +9899,6 @@ void Unit::HandleStatFlatModifier(UnitMods unitMod, UnitModifierFlatType modifie
|
||||
switch (modifierType)
|
||||
{
|
||||
case BASE_VALUE:
|
||||
case BASE_PCT_EXCLUDE_CREATE:
|
||||
case TOTAL_VALUE:
|
||||
m_auraFlatModifiersGroup[unitMod][modifierType] += apply ? amount : -amount;
|
||||
break;
|
||||
@@ -10146,18 +10153,18 @@ void Unit::UpdateAllDamagePctDoneMods()
|
||||
|
||||
float Unit::GetTotalStatValue(Stats stat) const
|
||||
{
|
||||
float createStat = GetCreateStat(stat); // retrieved early to workaround a GCC false positive warning about out of bounds array access (conversion to UnitMods confuses it)
|
||||
|
||||
UnitMods unitMod = UnitMods(UNIT_MOD_STAT_START + AsUnderlyingType(stat));
|
||||
|
||||
// value = ((base_value * base_pct) + total_value) * total_pct
|
||||
float value = CalculatePct(GetFlatModifierValue(unitMod, BASE_VALUE), std::max(GetFlatModifierValue(unitMod, BASE_PCT_EXCLUDE_CREATE), -100.0f));
|
||||
value += createStat;
|
||||
value *= GetPctModifierValue(unitMod, BASE_PCT);
|
||||
value += GetFlatModifierValue(unitMod, TOTAL_VALUE);
|
||||
value *= GetPctModifierValue(unitMod, TOTAL_PCT);
|
||||
// value = (base_value * base_pct) * total_pct + total_value * total_pct
|
||||
float baseValue = GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
baseValue *= GetPctModifierValue(unitMod, BASE_PCT);
|
||||
baseValue *= GetPctModifierValue(unitMod, TOTAL_PCT);
|
||||
|
||||
return value;
|
||||
float totalValue = GetFlatModifierValue(unitMod, TOTAL_VALUE);
|
||||
AddPct(totalValue, GetTotalAuraModifierByMiscValue(SPELL_AURA_MOD_STAT_BONUS_PCT, stat));
|
||||
totalValue *= GetPctModifierValue(unitMod, TOTAL_PCT);
|
||||
|
||||
return baseValue + totalValue;
|
||||
}
|
||||
|
||||
float Unit::GetTotalAuraModValue(UnitMods unitMod) const
|
||||
@@ -10168,7 +10175,7 @@ float Unit::GetTotalAuraModValue(UnitMods unitMod) const
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float value = CalculatePct(GetFlatModifierValue(unitMod, BASE_VALUE), std::max(GetFlatModifierValue(unitMod, BASE_PCT_EXCLUDE_CREATE), -100.0f));
|
||||
float value = GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
value *= GetPctModifierValue(unitMod, BASE_PCT);
|
||||
value += GetFlatModifierValue(unitMod, TOTAL_VALUE);
|
||||
value *= GetPctModifierValue(unitMod, TOTAL_PCT);
|
||||
@@ -10220,7 +10227,7 @@ void Unit::UpdateResistances(uint32 school)
|
||||
{
|
||||
UnitMods unitMod = UnitMods(UNIT_MOD_RESISTANCE_START + school);
|
||||
|
||||
float value = CalculatePct(GetFlatModifierValue(unitMod, BASE_VALUE), std::max(GetFlatModifierValue(unitMod, BASE_PCT_EXCLUDE_CREATE), -100.0f));
|
||||
float value = GetFlatModifierValue(unitMod, BASE_VALUE);
|
||||
value *= GetPctModifierValue(unitMod, BASE_PCT);
|
||||
|
||||
float baseValue = value;
|
||||
|
||||
@@ -159,17 +159,16 @@ typedef std::unordered_multimap<uint32 /*type*/, uint32 /*spellId*/> SpellImmune
|
||||
|
||||
enum UnitModifierFlatType
|
||||
{
|
||||
BASE_VALUE = 0,
|
||||
BASE_PCT_EXCLUDE_CREATE = 1, // percent modifier affecting all stat values from auras and gear but not player base for level
|
||||
TOTAL_VALUE = 2,
|
||||
MODIFIER_TYPE_FLAT_END = 3
|
||||
BASE_VALUE = 0, // flat value containing only fixed create stats, e.g. player_classlevelstats
|
||||
TOTAL_VALUE = 1, // flat value containing flat modfiers from gear and auras
|
||||
MODIFIER_TYPE_FLAT_END
|
||||
};
|
||||
|
||||
enum UnitModifierPctType
|
||||
{
|
||||
BASE_PCT = 0,
|
||||
TOTAL_PCT = 1,
|
||||
MODIFIER_TYPE_PCT_END = 2
|
||||
BASE_PCT = 0, // percent modifier affecting only BASE_VALUE
|
||||
TOTAL_PCT = 1, // percent modifier affecting BASE_VALUE and TOTAL_VALUE
|
||||
MODIFIER_TYPE_PCT_END
|
||||
};
|
||||
|
||||
enum WeaponDamageRange
|
||||
@@ -1441,7 +1440,7 @@ class TC_GAME_API Unit : public WorldObject
|
||||
void UpdateStatBuffMod(Stats stat);
|
||||
void UpdateStatBuffModForClient(Stats stat);
|
||||
void SetStatSupportBuff(Stats stat, int32 val) { SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::StatSupportBuff, stat), val); }
|
||||
void SetCreateStat(Stats stat, float val) { m_createStats[stat] = val; }
|
||||
void SetCreateStat(Stats stat, float val);
|
||||
void SetCreateHealth(uint32 val) { SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::BaseHealth), val); }
|
||||
uint32 GetCreateHealth() const { return m_unitData->BaseHealth; }
|
||||
void SetCreateMana(uint32 val) { SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::BaseMana), val); }
|
||||
@@ -1449,7 +1448,7 @@ class TC_GAME_API Unit : public WorldObject
|
||||
virtual int32 GetCreatePowerValue(Powers power) const;
|
||||
float GetPosStat(Stats stat) const { return m_unitData->StatPosBuff[stat]; }
|
||||
float GetNegStat(Stats stat) const { return m_unitData->StatNegBuff[stat]; }
|
||||
float GetCreateStat(Stats stat) const { return m_createStats[stat]; }
|
||||
float GetCreateStat(Stats stat) const;
|
||||
|
||||
uint32 GetChannelSpellId() const { return m_unitData->ChannelData->SpellID; }
|
||||
void SetChannelSpellId(uint32 channelSpellId)
|
||||
@@ -1954,7 +1953,6 @@ class TC_GAME_API Unit : public WorldObject
|
||||
|
||||
bool m_ControlledByPlayer;
|
||||
|
||||
std::array<float, MAX_STATS> m_createStats;
|
||||
std::array<float, MAX_STATS> m_floatStatPosBuff;
|
||||
std::array<float, MAX_STATS> m_floatStatNegBuff;
|
||||
|
||||
|
||||
@@ -4137,31 +4137,20 @@ void AuraEffect::HandleModBonusArmorPercent(AuraApplication const* aurApp, uint8
|
||||
aurApp->GetTarget()->UpdateArmor();
|
||||
}
|
||||
|
||||
void AuraEffect::HandleModStatBonusPercent(AuraApplication const* aurApp, uint8 mode, bool apply) const
|
||||
void AuraEffect::HandleModStatBonusPercent(AuraApplication const* aurApp, uint8 mode, bool /*apply*/) const
|
||||
{
|
||||
if (!(mode & (AURA_EFFECT_HANDLE_CHANGE_AMOUNT_MASK | AURA_EFFECT_HANDLE_STAT)))
|
||||
return;
|
||||
|
||||
Unit* target = aurApp->GetTarget();
|
||||
|
||||
if (GetMiscValue() < -1 || GetMiscValue() > 4)
|
||||
if (GetMiscValue() < 0 || GetMiscValue() >= MAX_STATS)
|
||||
{
|
||||
TC_LOG_ERROR("spells", "WARNING: Misc Value for SPELL_AURA_MOD_STAT_BONUS_PCT not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
// only players have base stats
|
||||
if (target->GetTypeId() != TYPEID_PLAYER)
|
||||
return;
|
||||
|
||||
for (int32 i = STAT_STRENGTH; i < MAX_STATS; ++i)
|
||||
{
|
||||
if (GetMiscValue() == i || GetMiscValue() == -1)
|
||||
{
|
||||
target->HandleStatFlatModifier(UnitMods(UNIT_MOD_STAT_START + i), BASE_PCT_EXCLUDE_CREATE, float(GetAmount()), apply);
|
||||
target->UpdateStatBuffMod(Stats(i));
|
||||
}
|
||||
}
|
||||
target->UpdateStats(Stats(GetMiscValue()));
|
||||
}
|
||||
|
||||
void AuraEffect::HandleOverrideSpellPowerByAttackPower(AuraApplication const* aurApp, uint8 mode, bool apply) const
|
||||
|
||||
Reference in New Issue
Block a user