eqemu-server/zone/exp.cpp
Mitch Freeman 91f5932c6d
[Feature] Add RoF2 Guild features (#3699)
* [Feature] Add additional Guild Features

This adds the following guild features and design pattern
- the existing guild system was used
- guild features are based on RoF2 within source with translaters used to converted between client differences
- backward compatible with Ti and UF, and allows for mixed client servers
- Guild Back for Ti and UF is based on RoF2 Permissions for banking if Guild Leader does not use Ti/UF
- Guild Ranks and Permissions are enabled.
- Guild Tributes are enabled.
- Event logging via rules for donating tribute items and plat
- Rules to limit Guild Tributes based on max level of server
- Rewrote guild communications to client using specific opcodes
-- Server no longer sends a guild member list on each zone
-- Guild window is updated when a member levels, rank changes, zone changes, banker/alt status using individual opcodes
-- When a member is removed or added to a guild, a single opcode is sent to each guild member
-- This reduces network traffic considerably

Known issues:
- Visual bug only. Guild Tributes window will display a 0 for level if tribute is above max level rule setting.
- Visual bug only. Guild Mgmt Window will not display an online member if the player has 'show offline' unchecked and a guild member zones within the Notes/Tribute tab.  This is resolved by selecting and de-selecting the 'Show Offline' checkbox.

* Updated RoF2 Guild Comms

Updated RoF2 Guild Comms
Update RoF2 Opcodes
Rewrote RoF2 Guild Communications using specific opcodes.
Added database changes - they are irreversible

* Formatting

* Update base_guild_members_repository.h

* Format GuildInfo

* Format GuildAction enum

* Formatting in clientlist

* quantity vs quantity

* desc vs description

* Format structs

* Inline struct values

* Formatting

* Formatting

* Formatting fixes

* Formatting items

* Formatting

* Formatting

* struct formatting updates

* Updated formatting

* Updated
- std:string items
- naming conventions
- magic numbers

* Repo refactors
Other formatting updates

* Remove test guild commands

* Updated #guild info command

* Add new repo methods for Neckolla ReplaceOne and ReplaceMany

* Fix guild_tributes repo

* Update database_update_manifest.cpp

* Phase 1 of final testing with RoF2 -> RoF2.
Next phase will be inter compatibility review

* Remove #guild testing commands

* Fix uf translator error
Rewrite LoadGuilds

* Use extended repository

* FIx guild window on member add

* LoadGuild Changes

* Update guild_base.cpp

* Few small fixes for display issue with UF

* Update guild_base.cpp

* Update guild_members_repository.h

* Update zoneserver.cpp

* Update guild.cpp

* Update entity.h

* Switch formatting

* Formatting

* Update worldserver.cpp

* Switch formatting

* Formatting switch statement

* Update guild.cpp

* Formatting in guild_base

* We don't need to validate m_db everywhere

* More formatting / spacing issues

* Switch format

* Update guild_base.cpp

* Fix an UF issue displaying incorrect guildtag as <>

* Updated several constants, fixed a few issues with Ti/UF and guild tributes not being removed or sent when a member is removed/disbands from a guild.

* Formatting and logging updates

* Fix for Loadguilds and permissions after repo updates.

* Cleanup unnecessary m_db checks

* Updated logging to use player_event_logs

* Updated to use the single opcodes for guild traffic for Ti/UF/RoF2.  Several enhancements for guild functionality for more reusable code and readability.

* Update to fix Demote Self and guild invites declining when option set to not accept guild invites

* Potential fix for guild notes/tribute display issues when client has 'Show Offline' unchecked.

* Updates to fox recent master changes

Updates to fix recent master changes

* Updates in response to comments

* Further Updates in response to comments

* Comment updates and refactor for SendAppearance functions

* Comment updates

* Update client spawn process for show guild name

Add show guild tag to default spawn process

* Update to use zone spawn packets for RoF2
Removed several unused functions as a result
Updated MemberRankUpdate to properly update guild_show on rank change.
Updated OP_GuildURLAndChannel opcode for UF/RoF2

* Cleanup of world changes
Created function for repetitive zonelist sendpackets to only booted zones
Re-Inserted accidental delete of scanclosemobs

* Fixes

* Further world cleanup

* Fix a few test guild bank cases for backward compat
Removed a duplicate db call
Fixed a fallthrough issue

* Update guild_mgr.cpp

* Cleanup

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2024-02-10 03:27:58 -06:00

1308 lines
37 KiB
C++

/* EQEMu: Everquest Server Emulator
Copyright (C) 2001-2003 EQEMu Development Team (http://eqemulator.net)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY except by those people which sell it, which
are required to give you total support for your newly bought product;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "../common/global_define.h"
#include "../common/features.h"
#include "../common/rulesys.h"
#include "../common/strings.h"
#include "client.h"
#include "data_bucket.h"
#include "groups.h"
#include "mob.h"
#include "raids.h"
#include "queryserv.h"
#include "quest_parser_collection.h"
#include "lua_parser.h"
#include "string_ids.h"
#include "../common/data_verification.h"
#include "bot.h"
#include "../common/events/player_event_logs.h"
#include "worldserver.h"
extern WorldServer worldserver;
extern QueryServ* QServ;
static uint64 ScaleAAXPBasedOnCurrentAATotal(int earnedAA, uint64 add_aaxp)
{
float baseModifier = RuleR(AA, ModernAAScalingStartPercent);
int aaMinimum = RuleI(AA, ModernAAScalingAAMinimum);
int aaLimit = RuleI(AA, ModernAAScalingAALimit);
// Are we within the scaling window?
if (earnedAA >= aaLimit || earnedAA < aaMinimum)
{
LogDebug("Not within AA scaling window");
// At or past the limit. We're done.
return add_aaxp;
}
// We're not at the limit yet. How close are we?
int remainingAA = aaLimit - earnedAA;
// We might not always be X - 0
int scaleRange = aaLimit - aaMinimum;
// Normalize and get the effectiveness based on the range and the character's
// current spent AA.
float normalizedScale = (float)remainingAA / scaleRange;
// Scale.
uint64 totalWithExpMod = add_aaxp * (baseModifier / 100) * normalizedScale;
// Are we so close to the scale limit that we're earning more XP without scaling? This
// will happen when we get very close to the limit. In this case, just grant the unscaled
// amount.
if (totalWithExpMod < add_aaxp)
{
return add_aaxp;
}
Log(Logs::Detail,
Logs::None,
"Total before the modifier %d :: NewTotal %d :: ScaleRange: %d, SpentAA: %d, RemainingAA: %d, normalizedScale: %0.3f",
add_aaxp, totalWithExpMod, scaleRange, earnedAA, remainingAA, normalizedScale);
return totalWithExpMod;
}
static uint32 MaxBankedGroupLeadershipPoints(int Level)
{
if(Level < 35)
return 4;
if(Level < 51)
return 6;
return 8;
}
static uint32 MaxBankedRaidLeadershipPoints(int Level)
{
if(Level < 45)
return 6;
if(Level < 55)
return 8;
return 10;
}
uint64 Client::CalcEXP(uint8 consider_level, bool ignore_modifiers) {
uint64 in_add_exp = EXP_FORMULA;
if (XPRate != 0) {
in_add_exp = static_cast<uint64>(in_add_exp * (static_cast<float>(XPRate) / 100.0f));
}
if (!ignore_modifiers) {
auto total_modifier = 1.0f;
auto zone_modifier = 1.0f;
if (RuleR(Character, ExpMultiplier) >= 0) {
total_modifier *= RuleR(Character, ExpMultiplier);
}
if (zone->newzone_data.zone_exp_multiplier >= 0) {
zone_modifier *= zone->newzone_data.zone_exp_multiplier;
}
if (RuleB(Character, UseRaceClassExpBonuses)) {
if (
GetClass() == Class::Warrior ||
GetClass() == Class::Rogue ||
GetBaseRace() == HALFLING
) {
total_modifier *= 1.05;
}
}
if (zone->IsHotzone()) {
total_modifier += RuleR(Zone, HotZoneBonus);
}
in_add_exp = uint64(float(in_add_exp) * total_modifier * zone_modifier);
}
if (RuleB(Character,UseXPConScaling)) {
if (consider_level != 0xFF) {
switch (consider_level) {
case CON_GRAY:
in_add_exp = 0;
return 0;
case CON_GREEN:
in_add_exp = in_add_exp * RuleI(Character, GreenModifier) / 100;
break;
case CON_LIGHTBLUE:
in_add_exp = in_add_exp * RuleI(Character, LightBlueModifier) / 100;
break;
case CON_BLUE:
in_add_exp = in_add_exp * RuleI(Character, BlueModifier) / 100;
break;
case CON_WHITE:
in_add_exp = in_add_exp * RuleI(Character, WhiteModifier) / 100;
break;
case CON_YELLOW:
in_add_exp = in_add_exp * RuleI(Character, YellowModifier) / 100;
break;
case CON_RED:
in_add_exp = in_add_exp * RuleI(Character, RedModifier) / 100;
break;
}
}
}
if (!ignore_modifiers) {
if (RuleB(Zone, LevelBasedEXPMods)) {
if (zone->level_exp_mod[GetLevel()].ExpMod) {
in_add_exp *= zone->level_exp_mod[GetLevel()].ExpMod;
}
}
if (RuleR(Character, FinalExpMultiplier) >= 0) {
in_add_exp *= RuleR(Character, FinalExpMultiplier);
}
if (RuleB(Character, EnableCharacterEXPMods)) {
in_add_exp *= zone->GetEXPModifier(this);
}
}
return in_add_exp;
}
uint64 Client::GetExperienceForKill(Mob *against)
{
#ifdef LUA_EQEMU
uint64 lua_ret = 0;
bool ignoreDefault = false;
lua_ret = LuaParser::Instance()->GetExperienceForKill(this, against, ignoreDefault);
if (ignoreDefault) {
return lua_ret;
}
#endif
if (against && against->IsNPC()) {
uint32 level = (uint32)against->GetLevel();
uint64 ret = EXP_FORMULA;
auto mod = against->GetKillExpMod();
if(mod >= 0) {
ret *= mod;
ret /= 100;
}
return ret;
}
return 0;
}
float static GetConLevelModifierPercent(uint8 conlevel)
{
switch (conlevel)
{
case CON_GREEN:
return (float)RuleI(Character, GreenModifier) / 100;
break;
case CON_LIGHTBLUE:
return (float)RuleI(Character, LightBlueModifier) / 100;
break;
case CON_BLUE:
return (float)RuleI(Character, BlueModifier) / 100;
break;
case CON_WHITE:
return (float)RuleI(Character, WhiteModifier) / 100;
break;
case CON_YELLOW:
return (float)RuleI(Character, YellowModifier) / 100;
break;
case CON_RED:
return (float)RuleI(Character, RedModifier) / 100;
break;
default:
return 0;
}
}
void Client::CalculateNormalizedAAExp(uint64 &add_aaxp, uint8 conlevel, bool resexp)
{
// Functionally this is the same as having the case in the switch, but this is
// cleaner to read.
if (CON_GRAY == conlevel || resexp)
{
add_aaxp = 0;
return;
}
// For this, we ignore the provided value of add_aaxp because it doesn't
// apply. XP per AA is normalized such that there are X white con kills
// per AA.
uint32 whiteConKillsPerAA = RuleI(AA, NormalizedAANumberOfWhiteConPerAA);
uint32 xpPerAA = RuleI(AA, ExpPerPoint);
float colorModifier = GetConLevelModifierPercent(conlevel);
float percentToAAXp = (float)m_epp.perAA / 100;
// Normalize the amount of AA XP we earned for this kill.
add_aaxp = percentToAAXp * (xpPerAA / (whiteConKillsPerAA / colorModifier));
}
void Client::CalculateStandardAAExp(uint64 &add_aaxp, uint8 conlevel, bool resexp)
{
if (!resexp)
{
//if XP scaling is based on the con of a monster, do that now.
if (RuleB(Character, UseXPConScaling))
{
if (conlevel != 0xFF && !resexp)
{
add_aaxp *= GetConLevelModifierPercent(conlevel);
}
}
} //end !resexp
float aatotalmod = 1.0;
if (zone->newzone_data.zone_exp_multiplier >= 0) {
aatotalmod *= zone->newzone_data.zone_exp_multiplier;
}
// Shouldn't race not affect AA XP?
if (RuleB(Character, UseRaceClassExpBonuses))
{
if (GetBaseRace() == HALFLING) {
aatotalmod *= 1.05;
}
if (GetClass() == Class::Rogue || GetClass() == Class::Warrior) {
aatotalmod *= 1.05;
}
}
// why wasn't this here? Where should it be?
if (zone->IsHotzone())
{
aatotalmod += RuleR(Zone, HotZoneBonus);
}
if (RuleB(Zone, LevelBasedEXPMods)) {
if (zone->level_exp_mod[GetLevel()].ExpMod) {
add_aaxp *= zone->level_exp_mod[GetLevel()].AAExpMod;
}
}
if (RuleR(Character, FinalExpMultiplier) >= 0) {
add_aaxp *= RuleR(Character, FinalExpMultiplier);
}
if (RuleB(Character, EnableCharacterEXPMods)) {
add_aaxp *= zone->GetAAEXPModifier(this);
}
add_aaxp = (uint64)(RuleR(Character, AAExpMultiplier) * add_aaxp * aatotalmod);
}
void Client::CalculateLeadershipExp(uint64 &add_exp, uint8 conlevel)
{
if (IsLeadershipEXPOn() && (conlevel == CON_BLUE || conlevel == CON_WHITE || conlevel == CON_YELLOW || conlevel == CON_RED))
{
add_exp = static_cast<uint64>(static_cast<float>(add_exp) * 0.8f);
if (GetGroup())
{
if (m_pp.group_leadership_points < MaxBankedGroupLeadershipPoints(GetLevel())
&& RuleI(Character, KillsPerGroupLeadershipAA) > 0)
{
uint64 exp = GROUP_EXP_PER_POINT / RuleI(Character, KillsPerGroupLeadershipAA);
Client *mentoree = GetGroup()->GetMentoree();
if (GetGroup()->GetMentorPercent() && mentoree &&
mentoree->GetGroupPoints() < MaxBankedGroupLeadershipPoints(mentoree->GetLevel()))
{
uint64 mentor_exp = exp * (GetGroup()->GetMentorPercent() / 100.0f);
exp -= mentor_exp;
mentoree->AddLeadershipEXP(mentor_exp, 0); // ends up rounded down
mentoree->MessageString(Chat::LeaderShip, GAIN_GROUP_LEADERSHIP_EXP);
}
if (exp > 0)
{
// possible if you mentor 100% to the other client
AddLeadershipEXP(exp, 0); // ends up rounded up if mentored, no idea how live actually does it
MessageString(Chat::LeaderShip, GAIN_GROUP_LEADERSHIP_EXP);
}
}
else
{
MessageString(Chat::LeaderShip, MAX_GROUP_LEADERSHIP_POINTS);
}
}
else
{
Raid *raid = GetRaid();
// Raid leaders CAN NOT gain group AA XP, other group leaders can though!
if (raid->IsLeader(this))
{
if (m_pp.raid_leadership_points < MaxBankedRaidLeadershipPoints(GetLevel())
&& RuleI(Character, KillsPerRaidLeadershipAA) > 0)
{
AddLeadershipEXP(0, RAID_EXP_PER_POINT / RuleI(Character, KillsPerRaidLeadershipAA));
MessageString(Chat::LeaderShip, GAIN_RAID_LEADERSHIP_EXP);
}
else
{
MessageString(Chat::LeaderShip, MAX_RAID_LEADERSHIP_POINTS);
}
}
else
{
if (m_pp.group_leadership_points < MaxBankedGroupLeadershipPoints(GetLevel())
&& RuleI(Character, KillsPerGroupLeadershipAA) > 0)
{
uint32 group_id = raid->GetGroup(this);
uint64 exp = GROUP_EXP_PER_POINT / RuleI(Character, KillsPerGroupLeadershipAA);
Client *mentoree = raid->GetMentoree(group_id);
if (raid->GetMentorPercent(group_id) && mentoree &&
mentoree->GetGroupPoints() < MaxBankedGroupLeadershipPoints(mentoree->GetLevel()))
{
uint64 mentor_exp = exp * (raid->GetMentorPercent(group_id) / 100.0f);
exp -= mentor_exp;
mentoree->AddLeadershipEXP(mentor_exp, 0);
mentoree->MessageString(Chat::LeaderShip, GAIN_GROUP_LEADERSHIP_EXP);
}
if (exp > 0)
{
AddLeadershipEXP(exp, 0);
MessageString(Chat::LeaderShip, GAIN_GROUP_LEADERSHIP_EXP);
}
}
else
{
MessageString(Chat::LeaderShip, MAX_GROUP_LEADERSHIP_POINTS);
}
}
}
}
}
void Client::CalculateExp(uint64 in_add_exp, uint64 &add_exp, uint64 &add_aaxp, uint8 conlevel, bool resexp)
{
add_exp = in_add_exp;
if (!resexp && (XPRate != 0))
{
add_exp = static_cast<uint64>(in_add_exp * (static_cast<float>(XPRate) / 100.0f));
}
// Make sure it was initialized.
add_aaxp = 0;
if (!resexp)
{
//figure out how much of this goes to AAs
add_aaxp = add_exp * m_epp.perAA / 100;
//take that amount away from regular exp
add_exp -= add_aaxp;
float totalmod = 1.0;
float zemmod = 1.0;
//get modifiers
if (RuleR(Character, ExpMultiplier) >= 0) {
totalmod *= RuleR(Character, ExpMultiplier);
}
//add the zone exp modifier.
if (zone->newzone_data.zone_exp_multiplier >= 0) {
zemmod *= zone->newzone_data.zone_exp_multiplier;
}
if (RuleB(Character, UseRaceClassExpBonuses))
{
if (GetBaseRace() == HALFLING) {
totalmod *= 1.05;
}
if (GetClass() == Class::Rogue || GetClass() == Class::Warrior) {
totalmod *= 1.05;
}
}
//add hotzone modifier if one has been set.
if (zone->IsHotzone())
{
totalmod += RuleR(Zone, HotZoneBonus);
}
add_exp = uint64(float(add_exp) * totalmod * zemmod);
//if XP scaling is based on the con of a monster, do that now.
if (RuleB(Character, UseXPConScaling))
{
if (conlevel != 0xFF && !resexp)
{
add_exp = add_exp * GetConLevelModifierPercent(conlevel);
}
}
// Calculate any changes to leadership experience.
CalculateLeadershipExp(add_exp, conlevel);
} //end !resexp
if (RuleB(Zone, LevelBasedEXPMods)) {
if (zone->level_exp_mod[GetLevel()].ExpMod) {
add_exp *= zone->level_exp_mod[GetLevel()].ExpMod;
}
}
if (RuleR(Character, FinalExpMultiplier) >= 0) {
add_exp *= RuleR(Character, FinalExpMultiplier);
}
if (RuleB(Character, EnableCharacterEXPMods)) {
add_exp *= zone->GetEXPModifier(this);
}
//Enforce Percent XP Cap per kill, if rule is enabled
int kill_percent_xp_cap = RuleI(Character, ExperiencePercentCapPerKill);
if (kill_percent_xp_cap >= 0) {
auto experience_for_level = (GetEXPForLevel(GetLevel() + 1) - GetEXPForLevel(GetLevel()));
float exp_percent = (float)((float)add_exp / (float)(GetEXPForLevel(GetLevel() + 1) - GetEXPForLevel(GetLevel()))) * (float)100; //EXP needed for level
if (exp_percent > kill_percent_xp_cap) {
add_exp = GetEXP() + static_cast<uint64>(std::floor(experience_for_level * (kill_percent_xp_cap / 100.0f)));
return;
}
}
add_exp = GetEXP() + add_exp;
}
void Client::AddEXP(uint64 in_add_exp, uint8 conlevel, bool resexp) {
if (!IsEXPEnabled()) {
return;
}
EVENT_ITEM_ScriptStopReturn();
uint64 exp = 0;
uint64 aaexp = 0;
if (m_epp.perAA < 0 || m_epp.perAA > 100) {
m_epp.perAA = 0; // stop exploit with sanity check
}
// Calculate regular XP
CalculateExp(in_add_exp, exp, aaexp, conlevel, resexp);
// Calculate regular AA XP
if (!RuleB(AA, NormalizedAAEnabled))
{
CalculateStandardAAExp(aaexp, conlevel, resexp);
}
else
{
CalculateNormalizedAAExp(aaexp, conlevel, resexp);
}
// Are we also doing linear AA acceleration?
if (RuleB(AA, ModernAAScalingEnabled) && aaexp > 0)
{
aaexp = ScaleAAXPBasedOnCurrentAATotal(GetAAPoints(), aaexp);
}
// Get current AA XP total
uint32 had_aaexp = GetAAXP();
// Add it to the XP we just earned.
aaexp += had_aaexp;
// Make sure our new total (existing + just earned) isn't lower than the
// existing total. If it is, we overflowed the bounds of uint32 and wrapped.
// Reset to the existing total.
if (aaexp < had_aaexp)
{
aaexp = had_aaexp; //watch for wrap
}
// Check for Unused AA Cap. If at or above cap, set AAs to cap, set aaexp to 0 and set aa percentage to 0.
// Doing this here means potentially one kill wasted worth of experience, but easiest to put it here than to rewrite this function.
int aa_cap = RuleI(AA, UnusedAAPointCap);
if (aa_cap >= 0 && aaexp > 0) {
if (m_pp.aapoints == aa_cap) {
MessageString(Chat::Red, AA_CAP);
aaexp = 0;
m_epp.perAA = 0;
} else if (m_pp.aapoints > aa_cap) {
MessageString(Chat::Red, OVER_AA_CAP, fmt::format_int(aa_cap).c_str(), fmt::format_int(aa_cap).c_str());
m_pp.aapoints = aa_cap;
aaexp = 0;
m_epp.perAA = 0;
}
}
// AA Sanity Checking for players who set aa exp and deleveled below allowed aa level.
if (GetLevel() <= 50 && m_epp.perAA > 0) {
Message(Chat::Yellow, "You are below the level allowed to gain AA Experience. AA Experience set to 0%");
aaexp = 0;
m_epp.perAA = 0;
}
// Now update our character's normal and AA xp
SetEXP(exp, aaexp, resexp);
}
void Client::SetEXP(uint64 set_exp, uint64 set_aaxp, bool isrezzexp) {
LogDebug("Attempting to Set Exp for [{}] (XP: [{}], AAXP: [{}], Rez: [{}])", GetCleanName(), set_exp, set_aaxp, isrezzexp ? "true" : "false");
auto max_AAXP = GetRequiredAAExperience();
if (max_AAXP == 0 || GetEXPForLevel(GetLevel()) == 0xFFFFFFFF) {
Message(Chat::Red, "Error in Client::SetEXP. EXP not set.");
return; // Must be invalid class/race
}
uint32 i = 0;
uint32 membercount = 0;
if(GetGroup())
{
for (i = 0; i < MAX_GROUP_MEMBERS; i++) {
if (GetGroup()->members[i] != nullptr) {
membercount++;
}
}
}
uint64 current_exp = GetEXP();
uint64 current_aa_exp = GetAAXP();
uint64 total_current_exp = current_exp + current_aa_exp;
uint64 total_add_exp = set_exp + set_aaxp;
if (total_add_exp > total_current_exp) {
uint64 exp_gained = set_exp - current_exp;
uint64 aa_exp_gained = set_aaxp - current_aa_exp;
float exp_percent = (float)((float)exp_gained / (float)(GetEXPForLevel(GetLevel() + 1) - GetEXPForLevel(GetLevel())))*(float)100; //EXP needed for level
float aa_exp_percent = (float)((float)aa_exp_gained / (float)(RuleI(AA, ExpPerPoint)))*(float)100; //AAEXP needed for level
std::string exp_amount_message = "";
if (RuleI(Character, ShowExpValues) >= 1) {
if (exp_gained > 0 && aa_exp_gained > 0) {
exp_amount_message = fmt::format("({}) ({})", exp_gained, aa_exp_gained);
}
else if (exp_gained > 0) {
exp_amount_message = fmt::format("({})", exp_gained);
}
else {
exp_amount_message = fmt::format("({}) AA", aa_exp_gained);
}
}
std::string exp_percent_message = "";
if (RuleI(Character, ShowExpValues) >= 2) {
if (exp_gained > 0 && aa_exp_gained > 0) exp_percent_message = StringFormat("(%.3f%%, %.3f%%AA)", exp_percent, aa_exp_percent);
else if (exp_gained > 0) exp_percent_message = StringFormat("(%.3f%%)", exp_percent);
else exp_percent_message = StringFormat("(%.3f%%AA)", aa_exp_percent);
}
if (isrezzexp) {
if (RuleI(Character, ShowExpValues) > 0)
Message(Chat::Experience, "You regain %s experience from resurrection. %s", exp_amount_message.c_str(), exp_percent_message.c_str());
else MessageString(Chat::Experience, REZ_REGAIN);
} else {
if (membercount > 1) {
if (RuleI(Character, ShowExpValues) > 0)
Message(Chat::Experience, "You have gained %s party experience! %s", exp_amount_message.c_str(), exp_percent_message.c_str());
else MessageString(Chat::Experience, GAIN_GROUPXP);
}
else if (IsRaidGrouped()) {
if (RuleI(Character, ShowExpValues) > 0)
Message(Chat::Experience, "You have gained %s raid experience! %s", exp_amount_message.c_str(), exp_percent_message.c_str());
else MessageString(Chat::Experience, GAIN_RAIDEXP);
}
else {
if (RuleI(Character, ShowExpValues) > 0)
Message(Chat::Experience, "You have gained %s experience! %s", exp_amount_message.c_str(), exp_percent_message.c_str());
else MessageString(Chat::Experience, GAIN_XP);
}
}
}
else if(total_add_exp < total_current_exp){ //only loss message if you lose exp, no message if you gained/lost nothing.
uint64 exp_lost = current_exp - set_exp;
float exp_percent = (float)((float)exp_lost / (float)(GetEXPForLevel(GetLevel() + 1) - GetEXPForLevel(GetLevel())))*(float)100;
if (RuleI(Character, ShowExpValues) == 1 && exp_lost > 0) Message(Chat::Yellow, "You have lost %i experience.", exp_lost);
else if (RuleI(Character, ShowExpValues) == 2 && exp_lost > 0) Message(Chat::Yellow, "You have lost %i experience. (%.3f%%)", exp_lost, exp_percent);
else Message(Chat::Yellow, "You have lost experience.");
}
//check_level represents the level we should be when we have
//this ammount of exp (once these loops complete)
uint16 check_level = GetLevel()+1;
//see if we gained any levels
bool level_increase = true;
int8 level_count = 0;
while (set_exp >= GetEXPForLevel(check_level)) {
check_level++;
if (check_level > 127) { //hard level cap
check_level = 127;
break;
}
level_count++;
if(GetMercenaryID())
UpdateMercLevel();
}
//see if we lost any levels
while (set_exp < GetEXPForLevel(check_level-1)) {
check_level--;
if (check_level < 2) { //hard level minimum
check_level = 2;
break;
}
level_increase = false;
if(GetMercenaryID())
UpdateMercLevel();
}
check_level--;
//see if we gained any AAs
if (set_aaxp >= max_AAXP) {
/*
Note: AA exp is stored differently than normal exp.
Exp points are only stored in m_pp.expAA until you
gain a full AA point, once you gain it, a point is
added to m_pp.aapoints and the ammount needed to gain
that point is subtracted from m_pp.expAA
then, once they spend an AA point, it is subtracted from
m_pp.aapoints. In theory it then goes into m_pp.aapoints_spent,
but im not sure if we have that in the right spot.
*/
//record how many points we have
uint32 last_unspentAA = m_pp.aapoints;
//figure out how many AA points we get from the exp were setting
m_pp.aapoints = set_aaxp / max_AAXP;
LogDebug("Calculating additional AA Points from AAXP for [{}]: [{}] / [{}] = [{}] points", GetCleanName(), set_aaxp, max_AAXP, (float)set_aaxp / (float)max_AAXP);
//get remainder exp points, set in PP below
set_aaxp = set_aaxp - (max_AAXP * m_pp.aapoints);
//add in how many points we had
m_pp.aapoints += last_unspentAA;
//figure out how many points were actually gained
uint32 gained = (m_pp.aapoints - last_unspentAA);
//Message(Chat::Yellow, "You have gained %d skill points!!", m_pp.aapoints - last_unspentAA);
char val1[20] = { 0 };
char val2[20] = { 0 };
if (gained == 1 && m_pp.aapoints == 1)
MessageString(Chat::Experience, GAIN_SINGLE_AA_SINGLE_AA, ConvertArray(m_pp.aapoints, val1)); //You have gained an ability point! You now have %1 ability point.
else if (gained == 1 && m_pp.aapoints > 1)
MessageString(Chat::Experience, GAIN_SINGLE_AA_MULTI_AA, ConvertArray(m_pp.aapoints, val1)); //You have gained an ability point! You now have %1 ability points.
else
MessageString(Chat::Experience, GAIN_MULTI_AA_MULTI_AA, ConvertArray(gained, val1), ConvertArray(m_pp.aapoints, val2)); //You have gained %1 ability point(s)! You now have %2 ability point(s).
if (RuleB(AA, SoundForAAEarned)) {
SendSound();
}
if (parse->PlayerHasQuestSub(EVENT_AA_GAIN)) {
parse->EventPlayer(EVENT_AA_GAIN, this, std::to_string(gained), 0);
}
RecordPlayerEventLog(PlayerEvent::AA_GAIN, PlayerEvent::AAGainedEvent{gained});
/* QS: PlayerLogAARate */
if (RuleB(QueryServ, PlayerLogAARate)){
int add_points = (m_pp.aapoints - last_unspentAA);
std::string query = StringFormat("INSERT INTO `qs_player_aa_rate_hourly` (char_id, aa_count, hour_time) VALUES (%i, %i, UNIX_TIMESTAMP() - MOD(UNIX_TIMESTAMP(), 3600)) ON DUPLICATE KEY UPDATE `aa_count` = `aa_count` + %i", CharacterID(), add_points, add_points);
QServ->SendQuery(query.c_str());
}
//Message(Chat::Yellow, "You now have %d skill points available to spend.", m_pp.aapoints);
}
uint8 maxlevel = RuleI(Character, MaxExpLevel) + 1;
if(maxlevel <= 1)
maxlevel = RuleI(Character, MaxLevel) + 1;
if(check_level > maxlevel) {
check_level = maxlevel;
if(RuleB(Character, KeepLevelOverMax)) {
set_exp = GetEXPForLevel(GetLevel()+1);
}
else {
set_exp = GetEXPForLevel(maxlevel);
}
}
auto client_max_level = GetClientMaxLevel();
if (client_max_level) {
if (GetLevel() >= client_max_level) {
auto exp_needed = GetEXPForLevel(client_max_level);
if (set_exp > exp_needed) {
set_exp = exp_needed;
}
}
}
if ((GetLevel() != check_level) && !(check_level >= maxlevel)) {
char val1[20]={0};
if (level_increase)
{
if (level_count == 1)
MessageString(Chat::Experience, GAIN_LEVEL, ConvertArray(check_level, val1));
else
Message(Chat::Yellow, "Welcome to level %i!", check_level);
if (check_level == RuleI(Character, DeathItemLossLevel) &&
m_ClientVersionBit & EQ::versions::maskUFAndEarlier)
MessageString(Chat::Yellow, CORPSE_ITEM_LOST);
if (check_level == RuleI(Character, DeathExpLossLevel))
MessageString(Chat::Yellow, CORPSE_EXP_LOST);
}
uint8 myoldlevel = GetLevel();
SetLevel(check_level);
if (RuleB(Bots, Enabled) && RuleB(Bots, BotLevelsWithOwner)) {
// hack way of doing this..but, least invasive... (same criteria as gain level for sendlvlapp)
Bot::LevelBotWithClient(this, GetLevel(), (myoldlevel == check_level - 1));
}
}
//If were at max level then stop gaining experience if we make it to the cap
if(GetLevel() == maxlevel - 1){
uint32 expneeded = GetEXPForLevel(maxlevel);
if(set_exp > expneeded) {
set_exp = expneeded;
}
}
if (parse->PlayerHasQuestSub(EVENT_EXP_GAIN) && m_pp.exp != set_exp) {
parse->EventPlayer(EVENT_EXP_GAIN, this, std::to_string(set_exp - m_pp.exp), 0);
}
if (parse->PlayerHasQuestSub(EVENT_AA_EXP_GAIN) && m_pp.expAA != set_aaxp) {
parse->EventPlayer(EVENT_AA_EXP_GAIN, this, std::to_string(set_aaxp - m_pp.expAA), 0);
}
//set the client's EXP and AAEXP
m_pp.exp = set_exp;
m_pp.expAA = set_aaxp;
if (GetLevel() < 51) {
m_epp.perAA = 0; // turn off aa exp if they drop below 51
} else {
SendAlternateAdvancementStats(); //otherwise, send them an AA update
}
//send the expdata in any case so the xp bar isnt stuck after leveling
uint32 tmpxp1 = GetEXPForLevel(GetLevel()+1);
uint32 tmpxp2 = GetEXPForLevel(GetLevel());
// Quag: crash bug fix... Divide by zero when tmpxp1 and 2 equalled each other, most likely the error case from GetEXPForLevel() (invalid class, etc)
if (tmpxp1 != tmpxp2 && tmpxp1 != 0xFFFFFFFF && tmpxp2 != 0xFFFFFFFF) {
auto outapp = new EQApplicationPacket(OP_ExpUpdate, sizeof(ExpUpdate_Struct));
ExpUpdate_Struct* eu = (ExpUpdate_Struct*)outapp->pBuffer;
float tmpxp = (float) ( (float) set_exp-tmpxp2 ) / ( (float) tmpxp1-tmpxp2 );
eu->exp = (uint32)(330.0f * tmpxp);
FastQueuePacket(&outapp);
}
if (admin >= AccountStatus::GMAdmin && GetGM()) {
char val1[20]={0};
char val2[20]={0};
char val3[20]={0};
MessageString(Chat::Experience, GM_GAINXP, ConvertArray(set_aaxp,val1),ConvertArray(set_exp,val2),ConvertArray(GetEXPForLevel(GetLevel()+1),val3)); //[GM] You have gained %1 AXP and %2 EXP (%3).
}
}
void Client::SetLevel(uint8 set_level, bool command)
{
if (GetEXPForLevel(set_level) == 0xFFFFFFFF) {
LogError("GetEXPForLevel([{}]) = 0xFFFFFFFF", set_level);
return;
}
auto outapp = new EQApplicationPacket(OP_LevelUpdate, sizeof(LevelUpdate_Struct));
auto* lu = (LevelUpdate_Struct *) outapp->pBuffer;
lu->level = set_level;
if (m_pp.level2 != 0) {
lu->level_old = m_pp.level2;
} else {
lu->level_old = level;
}
level = set_level;
if (IsRaidGrouped()) {
Raid *r = GetRaid();
if (r) {
r->UpdateLevel(GetName(), set_level);
}
}
if (set_level > m_pp.level2) {
if (m_pp.level2 == 0) {
m_pp.points += 5;
} else {
m_pp.points += (5 * (set_level - m_pp.level2));
}
m_pp.level2 = set_level;
}
if (set_level > m_pp.level) {
int levels_gained = (set_level - m_pp.level);
if (parse->PlayerHasQuestSub(EVENT_LEVEL_UP)) {
parse->EventPlayer(EVENT_LEVEL_UP, this, std::to_string(levels_gained), 0);
}
if (player_event_logs.IsEventEnabled(PlayerEvent::LEVEL_GAIN)) {
auto e = PlayerEvent::LevelGainedEvent{
.from_level = m_pp.level,
.to_level = set_level,
.levels_gained = levels_gained
};
RecordPlayerEventLog(PlayerEvent::LEVEL_GAIN, e);
}
if (RuleB(QueryServ, PlayerLogLevels)) {
const auto event_desc = fmt::format(
"Leveled UP :: to Level:{} from Level:{} in zoneid:{} instid:{}",
set_level,
m_pp.level,
GetZoneID(),
GetInstanceID()
);
QServ->PlayerLogEvent(Player_Log_Levels, CharacterID(), event_desc);
}
} else if (set_level < m_pp.level) {
int levels_lost = (m_pp.level - set_level);
if (parse->PlayerHasQuestSub(EVENT_LEVEL_DOWN)) {
parse->EventPlayer(EVENT_LEVEL_DOWN, this, std::to_string(levels_lost), 0);
}
if (player_event_logs.IsEventEnabled(PlayerEvent::LEVEL_LOSS)) {
auto e = PlayerEvent::LevelLostEvent{
.from_level = m_pp.level,
.to_level = set_level,
.levels_lost = levels_lost
};
RecordPlayerEventLog(PlayerEvent::LEVEL_LOSS, e);
}
if (RuleB(QueryServ, PlayerLogLevels)) {
const auto event_desc = fmt::format(
"Leveled DOWN :: to Level:{} from Level:{} in zoneid:{} instid:{}",
set_level,
m_pp.level,
GetZoneID(),
GetInstanceID()
);
QServ->PlayerLogEvent(Player_Log_Levels, CharacterID(), event_desc);
}
}
m_pp.level = set_level;
if (command) {
m_pp.exp = GetEXPForLevel(set_level);
Message(Chat::Yellow, fmt::format("Welcome to level {}!", set_level).c_str());
lu->exp = 0;
AutoGrantAAPoints();
} else {
const auto temporary_xp = (
static_cast<float>(m_pp.exp - GetEXPForLevel(GetLevel())) /
static_cast<float>(GetEXPForLevel(GetLevel() + 1) - GetEXPForLevel(GetLevel()))
);
lu->exp = static_cast<uint32>(330.0f * temporary_xp);
}
QueuePacket(outapp);
safe_delete(outapp);
SendAppearancePacket(AppearanceType::WhoLevel, set_level); // who level change
LogInfo("Setting Level for [{}] to [{}]", GetName(), set_level);
CalcBonuses();
if (!RuleB(Character, HealOnLevel)) {
const auto max_hp = CalcMaxHP();
if (GetHP() > max_hp) {
SetHP(max_hp);
}
} else {
SetHP(CalcMaxHP()); // Why not, lets give them a free heal
}
if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) {
SetPVP(true);
}
if (IsInAGuild()) {
guild_mgr.SendToWorldMemberLevelUpdate(GuildID(), GetLevel(), std::string(GetCleanName()));
DoGuildTributeUpdate();
}
DoTributeUpdate();
SendHPUpdate();
SetMana(CalcMaxMana());
UpdateWho();
UpdateMercLevel();
Save();
}
// Note: The client calculates exp separately, we cant change this function
// Add: You can set the values you want now, client will be always sync :) - Merkur
uint32 Client::GetEXPForLevel(uint16 check_level)
{
#ifdef LUA_EQEMU
uint32 lua_ret = 0;
bool ignoreDefault = false;
lua_ret = LuaParser::Instance()->GetEXPForLevel(this, check_level, ignoreDefault);
if (ignoreDefault) {
return lua_ret;
}
#endif
uint16 check_levelm1 = check_level-1;
float mod;
if (check_level < 31)
mod = 1.0;
else if (check_level < 36)
mod = 1.1;
else if (check_level < 41)
mod = 1.2;
else if (check_level < 46)
mod = 1.3;
else if (check_level < 52)
mod = 1.4;
else if (check_level < 53)
mod = 1.5;
else if (check_level < 54)
mod = 1.6;
else if (check_level < 55)
mod = 1.7;
else if (check_level < 56)
mod = 1.9;
else if (check_level < 57)
mod = 2.1;
else if (check_level < 58)
mod = 2.3;
else if (check_level < 59)
mod = 2.5;
else if (check_level < 60)
mod = 2.7;
else if (check_level < 61)
mod = 3.0;
else
mod = 3.1;
float base = (check_levelm1)*(check_levelm1)*(check_levelm1);
mod *= 1000;
uint32 finalxp = uint32(base * mod);
if(RuleB(Character,UseOldRaceExpPenalties))
{
float racemod = 1.0;
if(GetBaseRace() == TROLL || GetBaseRace() == IKSAR) {
racemod = 1.2;
} else if(GetBaseRace() == OGRE) {
racemod = 1.15;
} else if(GetBaseRace() == BARBARIAN) {
racemod = 1.05;
} else if(GetBaseRace() == HALFLING) {
racemod = 0.95;
}
finalxp = uint64(finalxp * racemod);
}
if(RuleB(Character,UseOldClassExpPenalties))
{
float classmod = 1.0;
if(GetClass() == Class::Paladin || GetClass() == Class::ShadowKnight || GetClass() == Class::Ranger || GetClass() == Class::Bard) {
classmod = 1.4;
} else if(GetClass() == Class::Monk) {
classmod = 1.2;
} else if(GetClass() == Class::Wizard || GetClass() == Class::Enchanter || GetClass() == Class::Magician || GetClass() == Class::Necromancer) {
classmod = 1.1;
} else if(GetClass() == Class::Rogue) {
classmod = 0.91;
} else if(GetClass() == Class::Warrior) {
classmod = 0.9;
}
finalxp = uint64(finalxp * classmod);
}
return finalxp;
}
void Client::AddLevelBasedExp(uint8 exp_percentage, uint8 max_level, bool ignore_mods)
{
uint64 award;
uint64 xp_for_level;
if (exp_percentage > 100)
{
exp_percentage = 100;
}
if (!max_level || GetLevel() < max_level)
{
max_level = GetLevel();
}
xp_for_level = GetEXPForLevel(max_level + 1) - GetEXPForLevel(max_level);
award = xp_for_level * exp_percentage / 100;
if (RuleB(Zone, LevelBasedEXPMods) && !ignore_mods) {
if (zone->level_exp_mod[GetLevel()].ExpMod) {
award *= zone->level_exp_mod[GetLevel()].ExpMod;
}
}
if (RuleR(Character, FinalExpMultiplier) >= 0) {
award *= RuleR(Character, FinalExpMultiplier);
}
uint64 newexp = GetEXP() + award;
SetEXP(newexp, GetAAXP());
}
void Group::SplitExp(const uint64 exp, Mob* other) {
if (other->CastToNPC()->MerchantType != 0) {
return;
}
if (other->GetOwner() && other->GetOwner()->IsClient()) {
return;
}
const auto member_count = GroupCount();
if (!member_count) {
return;
}
auto group_experience = exp;
const auto highest_level = GetHighestLevel();
auto group_modifier = 1.0f;
if (RuleB(Character, EnableGroupMemberEXPModifier)) {
if (EQ::ValueWithin(member_count, 2, 5)) {
group_modifier = 1 + RuleR(Character, GroupMemberEXPModifier) * (member_count - 1); // 2 = 1.2x, 3 = 1.4x, 4 = 1.6x, 5 = 1.8x
} else if (member_count == 6) {
group_modifier = RuleR(Character, FullGroupEXPModifier);
}
}
if (EQ::ValueWithin(member_count, 2, 6)) {
if (RuleB(Character, EnableGroupEXPModifier)) {
group_experience += static_cast<uint64>(
static_cast<float>(exp) *
group_modifier *
RuleR(Character, GroupExpMultiplier)
);
} else {
group_experience += static_cast<uint64>(
static_cast<float>(exp) *
group_modifier
);
}
}
const uint8 consider_level = Mob::GetLevelCon(highest_level, other->GetLevel());
if (consider_level == CON_GRAY) {
return;
}
for (const auto& m : members) {
if (m && m->IsClient()) {
const int32 diff = m->GetLevel() - highest_level;
int32 max_diff = -(m->GetLevel() * 15 / 10 - m->GetLevel());
if (max_diff > -5) {
max_diff = -5;
}
if (diff >= max_diff) {
const uint64 tmp = (m->GetLevel() + 3) * (m->GetLevel() + 3) * 75 * 35 / 10;
const uint64 tmp2 = group_experience / member_count;
m->CastToClient()->AddEXP(tmp < tmp2 ? tmp : tmp2, consider_level);
}
}
}
}
void Raid::SplitExp(const uint64 exp, Mob* other) {
if (other->CastToNPC()->MerchantType != 0) {
return;
}
if (other->GetOwner() && other->GetOwner()->IsClient()) {
return;
}
const auto member_count = RaidCount();
if (!member_count) {
return;
}
auto raid_experience = exp;
const auto highest_level = GetHighestLevel();
if (RuleB(Character, EnableRaidEXPModifier)) {
raid_experience = static_cast<uint64>(static_cast<float>(raid_experience) * (1.0f - RuleR(Character, RaidExpMultiplier)));
}
raid_experience = static_cast<uint64>(static_cast<float>(raid_experience) * RuleR(Character, FinalRaidExpMultiplier));
const auto consider_level = Mob::GetLevelCon(highest_level, other->GetLevel());
if (consider_level == CON_GRAY) {
return;
}
uint32 member_modifier = 1;
if (RuleB(Character, EnableRaidMemberEXPModifier)) {
member_modifier = member_count;
}
for (const auto& m : members) {
if (m.member && !m.is_bot) {
const int32 diff = m.member->GetLevel() - highest_level;
int32 max_diff = -(m.member->GetLevel() * 15 / 10 - m.member->GetLevel());
if (max_diff > -5) {
max_diff = -5;
}
if (diff >= max_diff) {
const uint64 tmp = (m.member->GetLevel() + 3) * (m.member->GetLevel() + 3) * 75 * 35 / 10;
const uint64 tmp2 = (raid_experience / member_modifier) + 1;
m.member->AddEXP(tmp < tmp2 ? tmp : tmp2, consider_level);
}
}
}
}
void Client::SetLeadershipEXP(uint64 group_exp, uint64 raid_exp) {
while(group_exp >= GROUP_EXP_PER_POINT) {
group_exp -= GROUP_EXP_PER_POINT;
m_pp.group_leadership_points++;
MessageString(Chat::LeaderShip, GAIN_GROUP_LEADERSHIP_POINT);
}
while(raid_exp >= RAID_EXP_PER_POINT) {
raid_exp -= RAID_EXP_PER_POINT;
m_pp.raid_leadership_points++;
MessageString(Chat::LeaderShip, GAIN_RAID_LEADERSHIP_POINT);
}
m_pp.group_leadership_exp = group_exp;
m_pp.raid_leadership_exp = raid_exp;
SendLeadershipEXPUpdate();
}
void Client::AddLeadershipEXP(uint64 group_exp, uint64 raid_exp) {
SetLeadershipEXP(GetGroupEXP() + group_exp, GetRaidEXP() + raid_exp);
}
void Client::SendLeadershipEXPUpdate() {
auto outapp = new EQApplicationPacket(OP_LeadershipExpUpdate, sizeof(LeadershipExpUpdate_Struct));
LeadershipExpUpdate_Struct* eu = (LeadershipExpUpdate_Struct *) outapp->pBuffer;
eu->group_leadership_exp = m_pp.group_leadership_exp;
eu->group_leadership_points = m_pp.group_leadership_points;
eu->raid_leadership_exp = m_pp.raid_leadership_exp;
eu->raid_leadership_points = m_pp.raid_leadership_points;
FastQueuePacket(&outapp);
}
uint8 Client::GetCharMaxLevelFromQGlobal() {
auto char_cache = GetQGlobals();
std::list<QGlobal> global_map;
const uint32 zone_id = zone ? zone->GetZoneID() : 0;
if (char_cache) {
QGlobalCache::Combine(global_map, char_cache->GetBucket(), 0, CharacterID(), zone_id);
}
for (const auto& global : global_map) {
if (global.name == "CharMaxLevel") {
if (Strings::IsNumber(global.value)) {
return static_cast<uint8>(Strings::ToUnsignedInt(global.value));
}
}
}
return 0;
}
uint8 Client::GetCharMaxLevelFromBucket()
{
DataBucketKey k = GetScopedBucketKeys();
k.key = "CharMaxLevel";
auto b = DataBucket::GetData(k);
if (!b.value.empty()) {
if (Strings::IsNumber(b.value)) {
return static_cast<uint8>(Strings::ToUnsignedInt(b.value));
}
}
return 0;
}
uint32 Client::GetRequiredAAExperience() {
#ifdef LUA_EQEMU
uint32 lua_ret = 0;
bool ignoreDefault = false;
lua_ret = LuaParser::Instance()->GetRequiredAAExperience(this, ignoreDefault);
if (ignoreDefault) {
return lua_ret;
}
#endif
return RuleI(AA, ExpPerPoint);
}