From 567d46c3d6c6a04cdd1839b8b9374b985a424a92 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Thu, 22 May 2025 13:08:32 -0500 Subject: [PATCH] [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 --- common/ruletypes.h | 6 ++ zone/aura.cpp | 2 +- zone/client.cpp | 58 ++++++++--- zone/client.h | 25 ++++- zone/client_packet.cpp | 190 +++++++++++++++++++++++++++++++++- zone/client_process.cpp | 4 + zone/mob.cpp | 2 +- zone/mob_movement_manager.cpp | 8 ++ 8 files changed, 273 insertions(+), 22 deletions(-) diff --git a/common/ruletypes.h b/common/ruletypes.h index b523c5de4..cf6499cf0 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -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_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, 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(Mercs) diff --git a/zone/aura.cpp b/zone/aura.cpp index 178ca1700..a511b6082 100644 --- a/zone/aura.cpp +++ b/zone/aura.cpp @@ -9,7 +9,7 @@ Aura::Aura(NPCType *type_data, Mob *owner, AuraRecord &record) : NPC(type_data, 0, owner->GetPosition(), GravityBehavior::Flying), spell_id(record.spell_id), 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 m_owner = owner->GetID(); diff --git a/zone/client.cpp b/zone/client.cpp index 94c976788..683d182f3 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -226,8 +226,8 @@ Client::Client() : Mob( last_reported_endurance_percent = 0; last_reported_mana_percent = 0; gm_hide_me = false; - AFK = false; - LFG = false; + m_is_afk = false; + LFG = false; LFGFromLevel = 0; LFGToLevel = 0; LFGMatchFilter = false; @@ -536,8 +536,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob( last_reported_endurance_percent = 0; last_reported_mana_percent = 0; gm_hide_me = false; - AFK = false; - LFG = false; + m_is_afk = false; + LFG = false; LFGFromLevel = 0; LFGToLevel = 0; LFGMatchFilter = false; @@ -1175,6 +1175,10 @@ void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CO return; } + if (RuleB(Character, AutoIdleFilterPackets) && m_is_idle && IsFilteredAFKPacket(app)) { + return; + } + if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { AddPacket(app, ack_req); return; @@ -2528,7 +2532,7 @@ void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) Mob::FillSpawnStruct(ns, ForWho); // 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.anon = m_pp.anon; ns->spawn.gm = GetGM() ? 1 : 0; @@ -10839,15 +10843,37 @@ void Client::SetAnon(uint8 anon_flag) { safe_delete(outapp); } -void Client::SetAFK(uint8 afk_flag) { - AFK = afk_flag; - auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); - SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; - spawn_appearance->spawn_id = GetID(); - spawn_appearance->type = AppearanceType::AFK; - spawn_appearance->parameter = afk_flag; - entity_list.QueueClients(this, outapp); - safe_delete(outapp); +void Client::SetAFK(uint8 afk_flag) +{ + if (!afk_flag) { + ResetAFKTimer(); + } + + bool changed_afk_state = (m_is_afk && !afk_flag) || (!m_is_afk && afk_flag); + + 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) { @@ -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 update_range = RuleI(Range, MobCloseScanDistance); @@ -12724,7 +12750,7 @@ void Client::CheckSendBulkNpcPositions() int updated_count = 0; int skipped_count = 0; - if (is_ready_to_update) { + if (is_ready_to_update || force) { auto &mob_movement_manager = MobMovementManager::Get(); for (auto &e: entity_list.GetMobList()) { diff --git a/zone/client.h b/zone/client.h index e6832bf24..9f96ba235 100644 --- a/zone/client.h +++ b/zone/client.h @@ -501,9 +501,19 @@ public: void Kick(const std::string &reason); void WorldKick(); 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); + 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); + inline bool IsIdle() { return m_is_idle; } inline PlayerProfile_Struct& GetPP() { return m_pp; } inline ExtendedProfile_Struct& GetEPP() { return m_epp; } inline EQ::InventoryProfile& GetInv() { return m_inv; } @@ -2062,7 +2072,8 @@ private: uint8 LFGToLevel; bool LFGMatchFilter; char LFGComments[64]; - bool AFK; + bool m_is_afk = false; + bool m_is_manual_afk = false; bool auto_attack; bool auto_fire; bool runmode; @@ -2216,7 +2227,12 @@ private: glm::vec4 m_last_position_before_bulk_update; Timer m_client_bulk_npc_pos_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(); @@ -2410,6 +2426,9 @@ public: const std::string &GetMailKey() const; void ShowZoneShardMenu(); void Handle_OP_ChangePetName(const EQApplicationPacket *app); + bool IsFilteredAFKPacket(const EQApplicationPacket *p); + void CheckAutoIdleAFK(PlayerPositionUpdateClient_Struct *p); + void SyncWorldPositionsToClient(bool ignore_idle = false); }; #endif diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 5328da726..634e13334 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -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); + 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); CastingSlot slot = static_cast(castspell->slot); @@ -4566,6 +4574,12 @@ void Client::Handle_OP_ChannelMessage(const EQApplicationPacket *app) 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()) { Message(Chat::Red, "You try to speak but can't move your mouth!"); return; @@ -4992,6 +5006,10 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { SetMoving(!(cy == m_Position.y && cx == m_Position.x)); + if (RuleB(Character, EnableAutoAFK)) { + CheckAutoIdleAFK(ppu); + } + CheckClientToNpcAggroTimer(); if (m_mob_check_moving_timer.Check()) { @@ -10792,6 +10810,8 @@ void Client::Handle_OP_MoveItem(const EQApplicationPacket *app) return; } + ResetAFKTimer(); + BenchTimer bench; 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) { 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); } } @@ -15551,6 +15573,14 @@ void Client::Handle_OP_TradeRequest(const EQApplicationPacket *app) // Pass trade request on to recipient 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); } 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); 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); 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(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(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; + } +} diff --git a/zone/client_process.cpp b/zone/client_process.cpp index ec63581d6..04f724c98 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -536,6 +536,10 @@ bool Client::Process() { DoEnduranceRegen(); BuffProcess(); + if (auto_attack) { + ResetAFKTimer(); + } + if (tribute_timer.Check()) { ToggleTribute(true); //re-activate the tribute. } diff --git a/zone/mob.cpp b/zone/mob.cpp index 0a3baee13..e5a5eda05 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -2646,7 +2646,7 @@ void Mob::SendStatsWindow(Client* c, bool use_window) Chat::White, fmt::format( " 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()->GetAnon() ? "Yes" : "No", CastToClient()->GetPVP() ? "Yes" : "No", diff --git a/zone/mob_movement_manager.cpp b/zone/mob_movement_manager.cpp index bf73ba750..e82202df6 100644 --- a/zone/mob_movement_manager.cpp +++ b/zone/mob_movement_manager.cpp @@ -839,6 +839,10 @@ void MobMovementManager::SendCommandToClients( continue; } + if (c->IsIdle()) { + continue; + } + _impl->Stats.TotalSent++; if (anim != 0) { @@ -879,6 +883,10 @@ void MobMovementManager::SendCommandToClients( continue; } + if (c->IsIdle()) { + continue; + } + float distance = c->CalculateDistance(mob->GetX(), mob->GetY(), mob->GetZ()); bool match = false;