[Performance] Auto Idle / AFK (#4903)

* [Performance] AFK Client Packet Filtering

* Player feedback

* Update client_packet.cpp

* Fixes

* Streamline updates to SetAFK

* Decouple idling and AFK and manual AFK

* Reset clock timer when we take AFK or idle off

* Exclude bard songs in non combat zones from resetting timer

* GM exclusion adjustments
This commit is contained in:
Chris Miles 2025-05-22 13:08:32 -05:00 committed by GitHub
parent 53cc2de459
commit 567d46c3d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 273 additions and 22 deletions

View File

@ -233,6 +233,12 @@ RULE_BOOL(Character, SneakAlwaysSucceedOver100, false, "When sneak skill is over
RULE_INT(Character, BandolierSwapDelay, 0, "Bandolier swap delay in milliseconds, default is 0") RULE_INT(Character, BandolierSwapDelay, 0, "Bandolier swap delay in milliseconds, default is 0")
RULE_BOOL(Character, EnableHackedFastCampForGM, false, "Enables hacked fast camp for GM clients, if the GM doesn't have a hacked client they'll camp like normal") RULE_BOOL(Character, EnableHackedFastCampForGM, false, "Enables hacked fast camp for GM clients, if the GM doesn't have a hacked client they'll camp like normal")
RULE_BOOL(Character, AlwaysAllowNameChange, false, "Enable this option to allow /changename to work without enabling a name change via scripts.") RULE_BOOL(Character, AlwaysAllowNameChange, false, "Enable this option to allow /changename to work without enabling a name change via scripts.")
RULE_BOOL(Character, EnableAutoAFK, true, "Enable or disable the auto AFK feature, cuts down on packet spam")
RULE_BOOL(Character, AutoIdleFilterPackets, true, "Enable or disable filtering packets when auto AFK is enabled, heavily cuts down on packet spam in zones with lots of players")
RULE_INT(Character, SecondsBeforeIdleCombatZone, 600, "Seconds before a player is considered idle in combat zones (600 = 10 minutes)")
RULE_INT(Character, SecondsBeforeIdleNonCombatZone, 60, "Seconds before a player is considered idle in non-combat zones (60 = 1 minute)")
RULE_INT(Character, SecondsBeforeAFKCombatZone, 1800, "Seconds before a player is considered AFK in combat zones (1800 = 30 minutes)")
RULE_INT(Character, SecondsBeforeAFKNonCombatZone, 600, "Seconds before a player is considered AFK in non-combat zones (600 = 10 minutes)")
RULE_CATEGORY_END() RULE_CATEGORY_END()
RULE_CATEGORY(Mercs) RULE_CATEGORY(Mercs)

View File

@ -9,7 +9,7 @@
Aura::Aura(NPCType *type_data, Mob *owner, AuraRecord &record) Aura::Aura(NPCType *type_data, Mob *owner, AuraRecord &record)
: NPC(type_data, 0, owner->GetPosition(), GravityBehavior::Flying), spell_id(record.spell_id), : NPC(type_data, 0, owner->GetPosition(), GravityBehavior::Flying), spell_id(record.spell_id),
distance(record.distance), distance(record.distance),
remove_timer(record.duration), movement_timer(100), process_timer(1000), aura_id(-1) remove_timer(record.duration), movement_timer(1000), process_timer(1000), aura_id(-1)
{ {
GiveNPCTypeData(type_data); // we will delete this later on GiveNPCTypeData(type_data); // we will delete this later on
m_owner = owner->GetID(); m_owner = owner->GetID();

View File

@ -226,8 +226,8 @@ Client::Client() : Mob(
last_reported_endurance_percent = 0; last_reported_endurance_percent = 0;
last_reported_mana_percent = 0; last_reported_mana_percent = 0;
gm_hide_me = false; gm_hide_me = false;
AFK = false; m_is_afk = false;
LFG = false; LFG = false;
LFGFromLevel = 0; LFGFromLevel = 0;
LFGToLevel = 0; LFGToLevel = 0;
LFGMatchFilter = false; LFGMatchFilter = false;
@ -536,8 +536,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
last_reported_endurance_percent = 0; last_reported_endurance_percent = 0;
last_reported_mana_percent = 0; last_reported_mana_percent = 0;
gm_hide_me = false; gm_hide_me = false;
AFK = false; m_is_afk = false;
LFG = false; LFG = false;
LFGFromLevel = 0; LFGFromLevel = 0;
LFGToLevel = 0; LFGToLevel = 0;
LFGMatchFilter = false; LFGMatchFilter = false;
@ -1175,6 +1175,10 @@ void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CO
return; return;
} }
if (RuleB(Character, AutoIdleFilterPackets) && m_is_idle && IsFilteredAFKPacket(app)) {
return;
}
if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) {
AddPacket(app, ack_req); AddPacket(app, ack_req);
return; return;
@ -2528,7 +2532,7 @@ void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho)
Mob::FillSpawnStruct(ns, ForWho); Mob::FillSpawnStruct(ns, ForWho);
// Populate client-specific spawn information // Populate client-specific spawn information
ns->spawn.afk = AFK; ns->spawn.afk = m_is_afk;
ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live
ns->spawn.anon = m_pp.anon; ns->spawn.anon = m_pp.anon;
ns->spawn.gm = GetGM() ? 1 : 0; ns->spawn.gm = GetGM() ? 1 : 0;
@ -10839,15 +10843,37 @@ void Client::SetAnon(uint8 anon_flag) {
safe_delete(outapp); safe_delete(outapp);
} }
void Client::SetAFK(uint8 afk_flag) { void Client::SetAFK(uint8 afk_flag)
AFK = afk_flag; {
auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); if (!afk_flag) {
SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; ResetAFKTimer();
spawn_appearance->spawn_id = GetID(); }
spawn_appearance->type = AppearanceType::AFK;
spawn_appearance->parameter = afk_flag; bool changed_afk_state = (m_is_afk && !afk_flag) || (!m_is_afk && afk_flag);
entity_list.QueueClients(this, outapp);
safe_delete(outapp); if (!changed_afk_state) {
return;
}
// set messaging based on the state
std::string you_are = "You are no longer AFK.";
if (!m_is_afk && afk_flag) {
you_are = "You are now AFK.";
}
// set the state
m_is_afk = afk_flag;
// inform of state change
Message(Chat::Yellow, you_are.c_str());
// send the spawn appearance packet to all clients
static EQApplicationPacket p(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct));
auto *s = (SpawnAppearance_Struct *) p.pBuffer;
s->spawn_id = GetID();
s->type = AppearanceType::AFK;
s->parameter = afk_flag;
entity_list.QueueClients(this, &p);
} }
void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) {
@ -12713,7 +12739,7 @@ void Client::SendTopLevelInventory()
} }
} }
void Client::CheckSendBulkNpcPositions() void Client::CheckSendBulkNpcPositions(bool force)
{ {
float distance_moved = DistanceNoZ(m_last_position_before_bulk_update, GetPosition()); float distance_moved = DistanceNoZ(m_last_position_before_bulk_update, GetPosition());
float update_range = RuleI(Range, MobCloseScanDistance); float update_range = RuleI(Range, MobCloseScanDistance);
@ -12724,7 +12750,7 @@ void Client::CheckSendBulkNpcPositions()
int updated_count = 0; int updated_count = 0;
int skipped_count = 0; int skipped_count = 0;
if (is_ready_to_update) { if (is_ready_to_update || force) {
auto &mob_movement_manager = MobMovementManager::Get(); auto &mob_movement_manager = MobMovementManager::Get();
for (auto &e: entity_list.GetMobList()) { for (auto &e: entity_list.GetMobList()) {

View File

@ -501,9 +501,19 @@ public:
void Kick(const std::string &reason); void Kick(const std::string &reason);
void WorldKick(); void WorldKick();
inline uint8 GetAnon() const { return m_pp.anon; } inline uint8 GetAnon() const { return m_pp.anon; }
inline uint8 GetAFK() const { return AFK; } inline uint8 GetAFK() const { return m_is_afk; }
void SetAnon(uint8 anon_flag); void SetAnon(uint8 anon_flag);
inline Client* ResetAFKTimer() {
if (!RuleB(Character, EnableAutoAFK)) {
return this;
}
m_afk_reset = true;
m_last_moved = std::chrono::steady_clock::now();
return this;
};
void SetAFK(uint8 afk_flag); void SetAFK(uint8 afk_flag);
inline bool IsIdle() { return m_is_idle; }
inline PlayerProfile_Struct& GetPP() { return m_pp; } inline PlayerProfile_Struct& GetPP() { return m_pp; }
inline ExtendedProfile_Struct& GetEPP() { return m_epp; } inline ExtendedProfile_Struct& GetEPP() { return m_epp; }
inline EQ::InventoryProfile& GetInv() { return m_inv; } inline EQ::InventoryProfile& GetInv() { return m_inv; }
@ -2062,7 +2072,8 @@ private:
uint8 LFGToLevel; uint8 LFGToLevel;
bool LFGMatchFilter; bool LFGMatchFilter;
char LFGComments[64]; char LFGComments[64];
bool AFK; bool m_is_afk = false;
bool m_is_manual_afk = false;
bool auto_attack; bool auto_attack;
bool auto_fire; bool auto_fire;
bool runmode; bool runmode;
@ -2216,7 +2227,12 @@ private:
glm::vec4 m_last_position_before_bulk_update; glm::vec4 m_last_position_before_bulk_update;
Timer m_client_bulk_npc_pos_update_timer; Timer m_client_bulk_npc_pos_update_timer;
Timer m_position_update_timer; Timer m_position_update_timer;
void CheckSendBulkNpcPositions(); void CheckSendBulkNpcPositions(bool force = false);
// afk
bool m_is_idle = false;
bool m_afk_reset = false; // used to trigger next-tic afk reset
std::chrono::steady_clock::time_point m_last_moved = std::chrono::steady_clock::now();
void BulkSendInventoryItems(); void BulkSendInventoryItems();
@ -2410,6 +2426,9 @@ public:
const std::string &GetMailKey() const; const std::string &GetMailKey() const;
void ShowZoneShardMenu(); void ShowZoneShardMenu();
void Handle_OP_ChangePetName(const EQApplicationPacket *app); void Handle_OP_ChangePetName(const EQApplicationPacket *app);
bool IsFilteredAFKPacket(const EQApplicationPacket *p);
void CheckAutoIdleAFK(PlayerPositionUpdateClient_Struct *p);
void SyncWorldPositionsToClient(bool ignore_idle = false);
}; };
#endif #endif

View File

@ -4383,6 +4383,14 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app)
m_TargetRing = glm::vec3(castspell->x_pos, castspell->y_pos, castspell->z_pos); m_TargetRing = glm::vec3(castspell->x_pos, castspell->y_pos, castspell->z_pos);
if (castspell->spell_id && IsValidSpell(castspell->spell_id)) {
bool is_non_combat_zone = !zone->CanDoCombat() || zone->BuffTimersSuspended();
bool is_excluded_reset = is_non_combat_zone && IsBardSong(castspell->spell_id);
if (!is_excluded_reset) {
ResetAFKTimer();
}
}
LogSpells("OP CastSpell: slot [{}] spell [{}] target [{}] inv [{}]", castspell->slot, castspell->spell_id, castspell->target_id, (unsigned long)castspell->inventoryslot); LogSpells("OP CastSpell: slot [{}] spell [{}] target [{}] inv [{}]", castspell->slot, castspell->spell_id, castspell->target_id, (unsigned long)castspell->inventoryslot);
CastingSlot slot = static_cast<CastingSlot>(castspell->slot); CastingSlot slot = static_cast<CastingSlot>(castspell->slot);
@ -4566,6 +4574,12 @@ void Client::Handle_OP_ChannelMessage(const EQApplicationPacket *app)
return; return;
} }
// reject automatic AFK messages from resetting /afk
std::string message = cm->message;
if (!Strings::Contains(message, "Sorry, I am A.F.K.")) {
ResetAFKTimer();
}
if (IsAIControlled() && !GetGM()) { if (IsAIControlled() && !GetGM()) {
Message(Chat::Red, "You try to speak but can't move your mouth!"); Message(Chat::Red, "You try to speak but can't move your mouth!");
return; return;
@ -4992,6 +5006,10 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) {
SetMoving(!(cy == m_Position.y && cx == m_Position.x)); SetMoving(!(cy == m_Position.y && cx == m_Position.x));
if (RuleB(Character, EnableAutoAFK)) {
CheckAutoIdleAFK(ppu);
}
CheckClientToNpcAggroTimer(); CheckClientToNpcAggroTimer();
if (m_mob_check_moving_timer.Check()) { if (m_mob_check_moving_timer.Check()) {
@ -10792,6 +10810,8 @@ void Client::Handle_OP_MoveItem(const EQApplicationPacket *app)
return; return;
} }
ResetAFKTimer();
BenchTimer bench; BenchTimer bench;
MoveItem_Struct* mi = (MoveItem_Struct*) app->pBuffer; MoveItem_Struct* mi = (MoveItem_Struct*) app->pBuffer;
@ -14732,7 +14752,9 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app)
} }
else if (sa->type == AppearanceType::AFK) { else if (sa->type == AppearanceType::AFK) {
if (afk_toggle_timer.Check()) { if (afk_toggle_timer.Check()) {
AFK = (sa->parameter == 1); m_is_afk = (sa->parameter == 1);
m_is_manual_afk = (sa->parameter == 1);
ResetAFKTimer();
entity_list.QueueClients(this, app, true); entity_list.QueueClients(this, app, true);
} }
} }
@ -15551,6 +15573,14 @@ void Client::Handle_OP_TradeRequest(const EQApplicationPacket *app)
// Pass trade request on to recipient // Pass trade request on to recipient
if (tradee && tradee->IsClient()) { if (tradee && tradee->IsClient()) {
// if we are idling we need to sync client positions otherwise clients will not be aware of each other
if (m_is_idle) {
SyncWorldPositionsToClient(true);
}
if (tradee->CastToClient()->IsIdle()) {
tradee->CastToClient()->SyncWorldPositionsToClient(true);
}
tradee->CastToClient()->QueuePacket(app); tradee->CastToClient()->QueuePacket(app);
} }
else if (tradee && (tradee->IsNPC() || tradee->IsBot())) { else if (tradee && (tradee->IsNPC() || tradee->IsBot())) {
@ -15580,6 +15610,14 @@ void Client::Handle_OP_TradeRequestAck(const EQApplicationPacket *app)
Mob* tradee = entity_list.GetMob(msg->to_mob_id); Mob* tradee = entity_list.GetMob(msg->to_mob_id);
if (tradee && tradee->IsClient()) { if (tradee && tradee->IsClient()) {
// if we are idling we need to sync client positions otherwise clients will not be aware of each other
if (m_is_idle) {
SyncWorldPositionsToClient(true);
}
if (tradee->CastToClient()->IsIdle()) {
tradee->CastToClient()->SyncWorldPositionsToClient(true);
}
trade->Start(msg->to_mob_id); trade->Start(msg->to_mob_id);
tradee->CastToClient()->QueuePacket(app); tradee->CastToClient()->QueuePacket(app);
} }
@ -17166,3 +17204,153 @@ void Client::Handle_OP_EvolveItem(const EQApplicationPacket *app)
} }
} }
} }
bool Client::IsFilteredAFKPacket(const EQApplicationPacket *p)
{
if (p->GetOpcode() == OP_ClientUpdate) {
return true;
}
return false;
}
void Client::CheckAutoIdleAFK(PlayerPositionUpdateClient_Struct *p)
{
if (!RuleB(Character, EnableAutoAFK)) {
return;
}
bool is_non_combat_zone = !zone->CanDoCombat() || zone->BuffTimersSuspended();
int seconds_before_afk =
is_non_combat_zone ?
RuleI(Character, SecondsBeforeAFKNonCombatZone) :
RuleI(Character, SecondsBeforeAFKCombatZone);
int seconds_before_idle =
is_non_combat_zone ?
RuleI(Character, SecondsBeforeIdleNonCombatZone) :
RuleI(Character, SecondsBeforeIdleCombatZone);
// seconds_before_idle can't be greater than seconds_before_afk
if (seconds_before_idle > seconds_before_afk) {
seconds_before_idle = seconds_before_afk;
}
bool has_moved =
m_Position.x != p->x_pos ||
m_Position.y != p->y_pos ||
m_Position.z != p->z_pos ||
m_Position.w != EQ12toFloat(p->heading);
bool triggered_reset = m_afk_reset;
bool was_idle = m_is_idle;
bool is_idle_or_afk = m_is_idle || m_is_afk;
if (!has_moved && (!m_is_idle || !m_is_afk)) {
auto now = std::chrono::steady_clock::now();
auto since_last_moved = now - m_last_moved;
if (!m_is_manual_afk && !m_is_afk && since_last_moved > std::chrono::seconds(seconds_before_afk)) {
bool is_client_excluded_from_afk = (IsBuyer() || IsTrader() || GetGM());
if (is_client_excluded_from_afk) {
return;
}
LogInfo(
"Client [{}] has been AFK for [{}] seconds",
GetCleanName(),
std::chrono::duration_cast<std::chrono::seconds>(since_last_moved).count()
);
SetAFK(true);
return;
}
else if (!m_is_idle && since_last_moved > std::chrono::seconds(seconds_before_idle)) {
bool is_client_excluded_from_idle = GetGM() && !is_non_combat_zone;
if (is_client_excluded_from_idle) {
return;
}
LogInfo(
"Client [{}] has been idle for [{}] seconds",
GetCleanName(),
std::chrono::duration_cast<std::chrono::seconds>(since_last_moved).count()
);
m_is_idle = true;
Message(Chat::Yellow, "You are now idle. Updates will be sent to you less frequently.");
return;
}
}
// if we triggered a reset, but didn't move, we are still idling but not AFK
if (triggered_reset && was_idle) {
m_is_idle = true;
}
// if we moved or triggered reset through other actions, we are no longer AFK.
// we could trigger resetting AFK status through actions like message, cast, attack etc but still by idle until we move
if (!m_is_manual_afk && (has_moved || triggered_reset) && m_is_afk) {
LogInfo("AFK [{}] is no longer idle, syncing positions", GetCleanName());
SetAFK(false);
ResetAFKTimer();
}
// we could be not AFK and idle at the same time
if (has_moved && m_is_idle) {
LogInfo("Idle [{}] is no longer idle, syncing positions", GetCleanName());
m_is_idle = false;
Message(Chat::Yellow, "You are no longer idle.");
SyncWorldPositionsToClient();
ResetAFKTimer();
}
m_afk_reset = false;
}
void Client::SyncWorldPositionsToClient(bool ignore_idle)
{
// if we are idle currently, we need to force updates (which bypasses idle status) and reset idle status
bool reset_idle = false;
if (ignore_idle && m_is_idle) {
m_is_idle = false;
reset_idle = true;
}
LogInfo("Syncing positions for client [{}]", GetCleanName());
CheckSendBulkNpcPositions(true);
static EQApplicationPacket cu(OP_ClientUpdate, sizeof(PlayerPositionUpdateServer_Struct));
for (auto &e: entity_list.GetClientList()) {
auto c = e.second;
// skip if not in range
if (Distance(c->GetPosition(), GetPosition()) > RuleI(Range, ClientPositionUpdates)) {
continue;
}
// skip self
if (c == this) {
continue;
}
auto *spu = (PlayerPositionUpdateServer_Struct *) cu.pBuffer;
memset(spu, 0x00, sizeof(PlayerPositionUpdateServer_Struct));
spu->spawn_id = c->GetID();
spu->x_pos = FloatToEQ19(c->GetX());
spu->y_pos = FloatToEQ19(c->GetY());
spu->z_pos = FloatToEQ19(c->GetZ());
spu->heading = FloatToEQ12(c->GetHeading());
spu->delta_x = FloatToEQ13(0);
spu->delta_y = FloatToEQ13(0);
spu->delta_z = FloatToEQ13(0);
spu->delta_heading = FloatToEQ10(0);
spu->animation = 0;
QueuePacket(&cu);
}
if (ignore_idle && reset_idle) {
m_is_idle = false;
}
}

View File

@ -536,6 +536,10 @@ bool Client::Process() {
DoEnduranceRegen(); DoEnduranceRegen();
BuffProcess(); BuffProcess();
if (auto_attack) {
ResetAFKTimer();
}
if (tribute_timer.Check()) { if (tribute_timer.Check()) {
ToggleTribute(true); //re-activate the tribute. ToggleTribute(true); //re-activate the tribute.
} }

View File

@ -2646,7 +2646,7 @@ void Mob::SendStatsWindow(Client* c, bool use_window)
Chat::White, Chat::White,
fmt::format( fmt::format(
" AFK: {} LFG: {} Anon: {} PVP: {} GM: {} Fly Mode: {} ({}) GM Speed: {} Hide Me: {} Invulnerability: {} LD: {} Client Version: {} Tells Off: {}", " AFK: {} LFG: {} Anon: {} PVP: {} GM: {} Fly Mode: {} ({}) GM Speed: {} Hide Me: {} Invulnerability: {} LD: {} Client Version: {} Tells Off: {}",
CastToClient()->AFK ? "Yes" : "No", CastToClient()->m_is_afk ? "Yes" : "No",
CastToClient()->LFG ? "Yes" : "No", CastToClient()->LFG ? "Yes" : "No",
CastToClient()->GetAnon() ? "Yes" : "No", CastToClient()->GetAnon() ? "Yes" : "No",
CastToClient()->GetPVP() ? "Yes" : "No", CastToClient()->GetPVP() ? "Yes" : "No",

View File

@ -839,6 +839,10 @@ void MobMovementManager::SendCommandToClients(
continue; continue;
} }
if (c->IsIdle()) {
continue;
}
_impl->Stats.TotalSent++; _impl->Stats.TotalSent++;
if (anim != 0) { if (anim != 0) {
@ -879,6 +883,10 @@ void MobMovementManager::SendCommandToClients(
continue; continue;
} }
if (c->IsIdle()) {
continue;
}
float distance = c->CalculateDistance(mob->GetX(), mob->GetY(), mob->GetZ()); float distance = c->CalculateDistance(mob->GetX(), mob->GetY(), mob->GetZ());
bool match = false; bool match = false;