From 4b6ce1c19edae8a4db9d473ffa1790900858da04 Mon Sep 17 00:00:00 2001 From: Akkadius Date: Tue, 28 Mar 2017 01:30:42 -0500 Subject: [PATCH] [Performance] Reworked how client to NPC aggro checks are made - Before when reverse aggro checks were done (client to NPC), checks would happen every 750 millseconds where a client would check an entire entity list with distance calcs and other checks for aggro, with many clients in a zone and many NPC's this would add a lot of unecessary overhead. A temporary adjustment on 3/25 was made and upped the check to 6 seconds. - Now, there is a new methodology to scanning. The client will build a cache list of NPC's within close range as defined in new rule: RULE_INT(Range, ClientNPCScan, 300) and will also get any NPC that has an aggro range beyond that defined range to use in the frequent checks for aggro, the result is far less overhead - Client scanning changes when moving versus not moving, the client will scan aggro every 500 milliseconds while moving, and 3000 millseconds aggro check when not moving, with a 6000ms re-fetch for close NPC's - A demo of these changes can be found here: https://youtu.be/aGroiwLSTVU --- changelog.txt | 17 +++- common/ruletypes.h | 1 + zone/aggro.cpp | 2 +- zone/client.cpp | 7 +- zone/client.h | 6 +- zone/client_packet.cpp | 28 +++++ zone/client_process.cpp | 219 ++++++++++++++++++++++------------------ zone/entity.cpp | 15 +++ zone/entity.h | 1 + 9 files changed, 191 insertions(+), 105 deletions(-) diff --git a/changelog.txt b/changelog.txt index b2c71a440..841cc96b1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,21 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 03/27/2017 == +Akkadius: [Performance] Reworked how client to NPC aggro checks are made + - Before when reverse aggro checks were done (client to NPC), checks would happen every 750 millseconds where a client would + check an entire entity list with distance calcs and other checks for aggro, with many clients in a zone and many NPC's this would + add a lot of unecessary overhead. A temporary adjustment on 3/25 was made and upped the check to 6 seconds. + - Now, there is a new methodology to scanning. The client will build a cache list of NPC's within close range as defined in new rule: + RULE_INT(Range, ClientNPCScan, 300) and will also get any NPC that has an aggro range beyond that defined range to use in + the frequent checks for aggro, the result is far less overhead + - Client scanning changes when moving versus not moving, the client will scan aggro every 500 milliseconds while moving, and + 3000 millseconds aggro check when not moving, with a 6000ms re-fetch for close NPC's + - A demo of these changes can be found here: + https://youtu.be/aGroiwLSTVU + == 03/25/2017 == -Akkadius: Reduced CPU footprint in non-combat zones doing constant checks for combat related activities -Akkadius: Reduced CPU footprint in cases where a client is checking for aggro excessively every 750 millseconds. This has +Akkadius: [Performance] Reduced CPU footprint in non-combat zones doing constant checks for combat related activities +Akkadius: [Performance] Reduced CPU footprint in cases where a client is checking for aggro excessively every 750 millseconds. This has been adjusted to 6 seconds per new rule RULE_INT(Aggro, ClientAggroCheckInterval) - When zones have many players, with many NPC's, this adds up quickly diff --git a/common/ruletypes.h b/common/ruletypes.h index a6653da8e..71dcaafbf 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -559,6 +559,7 @@ RULE_INT(Range, SpellMessages, 75) RULE_INT(Range, SongMessages, 75) RULE_INT(Range, MobPositionUpdates, 600) RULE_INT(Range, CriticalDamage, 80) +RULE_INT(Range, ClientNPCScan, 300) RULE_CATEGORY_END() diff --git a/zone/aggro.cpp b/zone/aggro.cpp index bf74f4b4d..34d2b9056 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -284,7 +284,7 @@ bool Mob::CheckWillAggro(Mob *mob) { if(( t1 > iAggroRange) || ( t2 > iAggroRange) || ( t3 > iAggroRange) - ||(mob->IsInvisible(this)) + || (mob->IsInvisible(this)) || (mob->IsClient() && (!mob->CastToClient()->Connected() || mob->CastToClient()->IsLD() diff --git a/zone/client.cpp b/zone/client.cpp index 2170d8ba9..fb9d6a5d9 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -135,7 +135,7 @@ Client::Client(EQStreamInterface* ieqs) forget_timer(0), autosave_timer(RuleI(Character, AutosaveIntervalS) * 1000), #ifdef REVERSE_AGGRO - scanarea_timer(RuleI(Aggro, ClientAggroCheckInterval) * 1000), + client_scan_npc_aggro_timer(RuleI(Aggro, ClientAggroCheckInterval) * 1000), #endif tribute_timer(Tribute_duration), proximity_timer(ClientProximity_interval), @@ -160,7 +160,8 @@ Client::Client(EQStreamInterface* ieqs) m_AutoAttackPosition(0.0f, 0.0f, 0.0f, 0.0f), m_AutoAttackTargetLocation(0.0f, 0.0f, 0.0f), last_region_type(RegionTypeUnsupported), - m_dirtyautohaters(false) + m_dirtyautohaters(false), + npc_close_scan_timer(6000) { for(int cf=0; cf < _FilterCount; cf++) ClientFilters[cf] = FilterShow; @@ -358,6 +359,8 @@ Client::~Client() { m_tradeskill_object = nullptr; } + close_npcs.clear(); + if(IsDueling() && GetDuelTarget() != 0) { Entity* entity = entity_list.GetID(GetDuelTarget()); if(entity != nullptr && entity->IsClient()) { diff --git a/zone/client.h b/zone/client.h index 6dc4d6e38..6e6e9e127 100644 --- a/zone/client.h +++ b/zone/client.h @@ -221,6 +221,9 @@ public: Client(EQStreamInterface * ieqs); ~Client(); + std::unordered_map close_npcs; + bool is_client_moving; + //abstract virtual function implementations required by base abstract class virtual bool Death(Mob* killerMob, int32 damage, uint16 spell_id, EQEmu::skills::SkillType attack_skill); virtual void Damage(Mob* from, int32 damage, uint16 spell_id, EQEmu::skills::SkillType attack_skill, bool avoidable = true, int8 buffslot = -1, bool iBuffTic = false, eSpecialAttacks special = eSpecialAttacks::None); @@ -1458,7 +1461,7 @@ private: Timer forget_timer; // our 2 min everybody forgets you timer Timer autosave_timer; #ifdef REVERSE_AGGRO - Timer scanarea_timer; + Timer client_scan_npc_aggro_timer; #endif Timer tribute_timer; @@ -1477,6 +1480,7 @@ private: Timer helm_toggle_timer; Timer light_update_timer; Timer aggro_meter_timer; + Timer npc_close_scan_timer; glm::vec3 m_Proximity; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 1616ac687..f8ff9eddd 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -4580,6 +4580,34 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) rewind_timer.Start(30000, true); } + + /* Handle client aggro scanning timers NPCs */ + is_client_moving = (ppu->y_pos == m_Position.y && ppu->x_pos == m_Position.x) ? false : true; + + if (is_client_moving) { + Log.Out(Logs::Detail, Logs::Normal, "ClientUpdate: Client is moving - scan timer is: %u", client_scan_npc_aggro_timer.GetDuration()); + if (client_scan_npc_aggro_timer.GetDuration() > 1000) { + + npc_close_scan_timer.Disable(); + npc_close_scan_timer.Start(500); + + client_scan_npc_aggro_timer.Disable(); + client_scan_npc_aggro_timer.Start(500); + + } + } + else { + Log.Out(Logs::Detail, Logs::Normal, "ClientUpdate: Client is NOT moving - scan timer is: %u", client_scan_npc_aggro_timer.GetDuration()); + if (client_scan_npc_aggro_timer.GetDuration() < 1000) { + + npc_close_scan_timer.Disable(); + npc_close_scan_timer.Start(6000); + + client_scan_npc_aggro_timer.Disable(); + client_scan_npc_aggro_timer.Start(3000); + } + } + // Outgoing client packet float tmpheading = EQ19toFloat(ppu->heading); /* The clients send an update at best every 1.3 seconds diff --git a/zone/client_process.cpp b/zone/client_process.cpp index b2a010542..1ad41ccf6 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -63,7 +63,7 @@ extern EntityList entity_list; bool Client::Process() { bool ret = true; - if(Connected() || IsLD()) + if (Connected() || IsLD()) { // try to send all packets that weren't sent before if (!IsLD() && zoneinpacket_timer.Check()) @@ -71,58 +71,58 @@ bool Client::Process() { SendAllPackets(); } - if(adventure_request_timer) + if (adventure_request_timer) { - if(adventure_request_timer->Check()) + if (adventure_request_timer->Check()) { safe_delete(adventure_request_timer); } } - if(adventure_create_timer) + if (adventure_create_timer) { - if(adventure_create_timer->Check()) + if (adventure_create_timer->Check()) { safe_delete(adventure_create_timer); } } - if(adventure_leave_timer) + if (adventure_leave_timer) { - if(adventure_leave_timer->Check()) + if (adventure_leave_timer->Check()) { safe_delete(adventure_leave_timer); } } - if(adventure_door_timer) + if (adventure_door_timer) { - if(adventure_door_timer->Check()) + if (adventure_door_timer->Check()) { safe_delete(adventure_door_timer); } } - if(adventure_stats_timer) + if (adventure_stats_timer) { - if(adventure_stats_timer->Check()) + if (adventure_stats_timer->Check()) { safe_delete(adventure_stats_timer); } } - if(adventure_leaderboard_timer) + if (adventure_leaderboard_timer) { - if(adventure_leaderboard_timer->Check()) + if (adventure_leaderboard_timer->Check()) { safe_delete(adventure_leaderboard_timer); } } - if(dead) + if (dead) { SetHP(-100); - if(RespawnFromHoverTimer.Check()) + if (RespawnFromHoverTimer.Check()) HandleRespawnFromHover(0); } @@ -131,13 +131,13 @@ bool Client::Process() { // SendHPUpdate calls hpupdate_timer.Start so it can delay this timer, so lets not reset with the check // since the function will anyways - if(hpupdate_timer.Check(false)) + if (hpupdate_timer.Check(false)) SendHPUpdate(); - if(mana_timer.Check()) + if (mana_timer.Check()) SendManaUpdatePacket(); - if(dead && dead_timer.Check()) { + if (dead && dead_timer.Check()) { database.MoveCharacterToZone(GetName(), database.GetZoneName(m_pp.binds[0].zoneId)); m_pp.zone_id = m_pp.binds[0].zoneId; @@ -150,7 +150,7 @@ bool Client::Process() { Group *mygroup = GetGroup(); if (mygroup) { - entity_list.MessageGroup(this,true,15,"%s died.", GetName()); + entity_list.MessageGroup(this, true, 15, "%s died.", GetName()); mygroup->MemberZoned(this); } Raid *myraid = entity_list.GetRaidByClient(this); @@ -161,34 +161,29 @@ bool Client::Process() { return(false); } - if(charm_update_timer.Check()) - { + if (charm_update_timer.Check()) { CalcItemScale(); } - if(TaskPeriodic_Timer.Check() && taskstate) + if (TaskPeriodic_Timer.Check() && taskstate) taskstate->TaskPeriodicChecks(this); - if(linkdead_timer.Check()) - { + if (linkdead_timer.Check()) { LeaveGroup(); Save(); - if (GetMerc()) - { + if (GetMerc()) { GetMerc()->Save(); GetMerc()->Depop(); } Raid *myraid = entity_list.GetRaidByClient(this); - if (myraid) - { + if (myraid) { myraid->MemberZoned(this); } return false; //delete client } - if (camp_timer.Check()) - { + if (camp_timer.Check()) { LeaveGroup(); Save(); if (GetMerc()) @@ -202,8 +197,7 @@ bool Client::Process() { if (IsStunned() && stunned_timer.Check()) Mob::UnStun(); - if(!m_CheatDetectMoved) - { + if (!m_CheatDetectMoved) { m_TimeSinceLastPositionCheck = Timer::GetCurrentTime(); } @@ -211,32 +205,32 @@ bool Client::Process() { //NOTE: this is kinda a heavy-handed check to make sure the mob still exists before //doing the next pulse on them... Mob *song_target = nullptr; - if(bardsong_target_id == GetID()) { + if (bardsong_target_id == GetID()) { song_target = this; - } else { + } + else { song_target = entity_list.GetMob(bardsong_target_id); } if (song_target == nullptr) { InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); - } else { - if(!ApplyNextBardPulse(bardsong, song_target, bardsong_slot)) + } + else { + if (!ApplyNextBardPulse(bardsong, song_target, bardsong_slot)) InterruptSpell(SONG_ENDS_ABRUPTLY, 0x121, bardsong); //SpellFinished(bardsong, bardsong_target, bardsong_slot, spells[bardsong].mana); } } - if(GetMerc()) - { + if (GetMerc()) { UpdateMercTimer(); } - if(GetMercInfo().MercTemplateID != 0 && GetMercInfo().IsSuspended) - { + if (GetMercInfo().MercTemplateID != 0 && GetMercInfo().IsSuspended) { CheckMercSuspendTimer(); } - if(IsAIControlled()) + if (IsAIControlled()) AI_Process(); // Don't reset the bindwound timer so we can check it in BindWound as well. @@ -244,31 +238,49 @@ bool Client::Process() { BindWound(bindwound_target, false); } - if(KarmaUpdateTimer) - { - if(KarmaUpdateTimer->Check(false)) - { + if (KarmaUpdateTimer) { + if (KarmaUpdateTimer->Check(false)) { KarmaUpdateTimer->Start(RuleI(Chat, KarmaUpdateIntervalMS)); database.UpdateKarma(AccountID(), ++TotalKarma); } } - if(qGlobals) - { - if(qglobal_purge_timer.Check()) - { + if (qGlobals) { + if (qglobal_purge_timer.Check()) { qGlobals->PurgeExpiredGlobals(); } } - if(light_update_timer.Check()) { + if (light_update_timer.Check()) { UpdateEquipmentLight(); - if(UpdateActiveLight()) { + if (UpdateActiveLight()) { SendAppearancePacket(AT_Light, GetActiveLightType()); } } + + /* Build a close range list of NPC's */ + if (npc_close_scan_timer.Check()) { + + close_npcs.clear(); + + std::list npc_list; + entity_list.GetNPCList(npc_list); + + float scan_range = RuleI(Range, ClientNPCScan); + for (auto itr = npc_list.begin(); itr != npc_list.end(); ++itr) { + NPC* npc = *itr; + float distance = DistanceNoZ(m_Position, npc->GetPosition()); + if(distance <= scan_range) { + close_npcs.insert(std::pair(npc, distance)); + } + else if (npc->GetAggroRange() > scan_range) { + close_npcs.insert(std::pair(npc, distance)); + } + } + } + bool may_use_attacks = false; /* Things which prevent us from attacking: @@ -279,35 +291,35 @@ bool Client::Process() { - being stunned or mezzed - having used a ranged weapon recently */ - if(auto_attack) { - if(!IsAIControlled() && !dead + if (auto_attack) { + if (!IsAIControlled() && !dead && !(spellend_timer.Enabled() && casting_spell_id && !IsBardSong(casting_spell_id)) && !IsStunned() && !IsFeared() && !IsMezzed() && GetAppearance() != eaDead && !IsMeleeDisabled() ) may_use_attacks = true; - if(may_use_attacks && ranged_timer.Enabled()) { + if (may_use_attacks && ranged_timer.Enabled()) { //if the range timer is enabled, we need to consider it - if(!ranged_timer.Check(false)) { + if (!ranged_timer.Check(false)) { //the ranged timer has not elapsed, cannot attack. may_use_attacks = false; } } } - if(AutoFireEnabled()){ + if (AutoFireEnabled()) { EQEmu::ItemInstance *ranged = GetInv().GetItem(EQEmu::inventory::slotRange); - if(ranged) + if (ranged) { - if (ranged->GetItem() && ranged->GetItem()->ItemType == EQEmu::item::ItemTypeBow){ - if(ranged_timer.Check(false)){ - if(GetTarget() && (GetTarget()->IsNPC() || GetTarget()->IsClient())){ - if(GetTarget()->InFrontMob(this, GetTarget()->GetX(), GetTarget()->GetY())){ - if(CheckLosFN(GetTarget())){ + if (ranged->GetItem() && ranged->GetItem()->ItemType == EQEmu::item::ItemTypeBow) { + if (ranged_timer.Check(false)) { + if (GetTarget() && (GetTarget()->IsNPC() || GetTarget()->IsClient())) { + if (GetTarget()->InFrontMob(this, GetTarget()->GetX(), GetTarget()->GetY())) { + if (CheckLosFN(GetTarget())) { //client has built in los check, but auto fire does not.. done last. RangedAttack(GetTarget()); - if (CheckDoubleRangedAttack()) - RangedAttack(GetTarget(), true); + if (CheckDoubleRangedAttack()) + RangedAttack(GetTarget(), true); } else ranged_timer.Start(); @@ -319,11 +331,11 @@ bool Client::Process() { ranged_timer.Start(); } } - else if (ranged->GetItem() && (ranged->GetItem()->ItemType == EQEmu::item::ItemTypeLargeThrowing || ranged->GetItem()->ItemType == EQEmu::item::ItemTypeSmallThrowing)){ - if(ranged_timer.Check(false)){ - if(GetTarget() && (GetTarget()->IsNPC() || GetTarget()->IsClient())){ - if(GetTarget()->InFrontMob(this, GetTarget()->GetX(), GetTarget()->GetY())){ - if(CheckLosFN(GetTarget())){ + else if (ranged->GetItem() && (ranged->GetItem()->ItemType == EQEmu::item::ItemTypeLargeThrowing || ranged->GetItem()->ItemType == EQEmu::item::ItemTypeSmallThrowing)) { + if (ranged_timer.Check(false)) { + if (GetTarget() && (GetTarget()->IsNPC() || GetTarget()->IsClient())) { + if (GetTarget()->InFrontMob(this, GetTarget()->GetX(), GetTarget()->GetY())) { + if (CheckLosFN(GetTarget())) { //client has built in los check, but auto fire does not.. done last. ThrowingAttack(GetTarget()); } @@ -345,9 +357,9 @@ bool Client::Process() { { //check if change //only check on primary attack.. sorry offhand you gotta wait! - if(aa_los_them_mob) + if (aa_los_them_mob) { - if(auto_attack_target != aa_los_them_mob || + if (auto_attack_target != aa_los_them_mob || m_AutoAttackPosition.x != GetX() || m_AutoAttackPosition.y != GetY() || m_AutoAttackPosition.z != GetZ() || @@ -379,11 +391,11 @@ bool Client::Process() { if (!CombatRange(auto_attack_target)) { - Message_StringID(MT_TooFarAway,TARGET_TOO_FAR); + Message_StringID(MT_TooFarAway, TARGET_TOO_FAR); } else if (auto_attack_target == this) { - Message_StringID(MT_TooFarAway,TRY_ATTACKING_SOMEONE); + Message_StringID(MT_TooFarAway, TRY_ATTACKING_SOMEONE); } else if (!los_status || !los_status_facing) { @@ -412,23 +424,23 @@ bool Client::Process() { } } - if(auto_attack && may_use_attacks && auto_attack_target != nullptr + if (auto_attack && may_use_attacks && auto_attack_target != nullptr && CanThisClassDualWield() && attack_dw_timer.Check()) { // Range check - if(!CombatRange(auto_attack_target)) { + if (!CombatRange(auto_attack_target)) { // this is a duplicate message don't use it. //Message_StringID(MT_TooFarAway,TARGET_TOO_FAR); } // Don't attack yourself - else if(auto_attack_target == this) { + else if (auto_attack_target == this) { //Message_StringID(MT_TooFarAway,TRY_ATTACKING_SOMEONE); } else if (!los_status || !los_status_facing) { //you can't see your target } - else if(auto_attack_target->GetHP() > -10) { + else if (auto_attack_target->GetHP() > -10) { CheckIncreaseSkill(EQEmu::skills::SkillDualWield, auto_attack_target, -10); if (CheckDualWield()) { EQEmu::ItemInstance *wpn = GetInv().GetItem(EQEmu::inventory::slotSecondary); @@ -442,7 +454,7 @@ bool Client::Process() { if (position_timer.Check()) { if (IsAIControlled()) { - if(!IsMoving()) + if (!IsMoving()) { animation = 0; m_Delta = glm::vec4(0.0f, 0.0f, 0.0f, m_Delta.w); @@ -464,25 +476,25 @@ bool Client::Process() { } } - if(HasVirus()) { - if(viral_timer.Check()) { + if (HasVirus()) { + if (viral_timer.Check()) { viral_timer_counter++; - for(int i = 0; i < MAX_SPELL_TRIGGER*2; i+=2) { - if(viral_spells[i]) { - if(viral_timer_counter % spells[viral_spells[i]].viral_timer == 0) { - SpreadVirus(viral_spells[i], viral_spells[i+1]); + for (int i = 0; i < MAX_SPELL_TRIGGER * 2; i += 2) { + if (viral_spells[i]) { + if (viral_timer_counter % spells[viral_spells[i]].viral_timer == 0) { + SpreadVirus(viral_spells[i], viral_spells[i + 1]); } } } } - if(viral_timer_counter > 999) + if (viral_timer_counter > 999) viral_timer_counter = 0; } ProjectileAttack(); - if(spellbonuses.GravityEffect == 1) { - if(gravity_timer.Check()) + if (spellbonuses.GravityEffect == 1) { + if (gravity_timer.Check()) DoGravityEffect(); } @@ -514,7 +526,7 @@ bool Client::Process() { } SpellProcess(); - if (endupkeep_timer.Check() && !dead){ + if (endupkeep_timer.Check() && !dead) { DoEnduranceUpkeep(); } @@ -530,7 +542,7 @@ bool Client::Process() { BuffProcess(); DoStaminaUpdate(); - if(tribute_timer.Check()) { + if (tribute_timer.Check()) { ToggleTribute(true); //re-activate the tribute. } @@ -542,18 +554,18 @@ bool Client::Process() { Save(0); } - if(m_pp.intoxication > 0) + if (m_pp.intoxication > 0) { --m_pp.intoxication; CalcBonuses(); } - if(ItemTickTimer.Check()) + if (ItemTickTimer.Check()) { TickItemCheck(); } - if(ItemQuestTimer.Check()) + if (ItemQuestTimer.Check()) { ItemTimerCheck(); } @@ -592,8 +604,8 @@ bool Client::Process() { } return false; } - else if(!linkdead_timer.Enabled()){ - linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); + else if (!linkdead_timer.Enabled()) { + linkdead_timer.Start(RuleI(Zone, ClientLinkdeadMS)); client_state = CLIENT_LINKDEAD; AI_Start(CLIENT_LD_TIMEOUT); SendAppearancePacket(AT_Linkdead, 1); @@ -603,10 +615,10 @@ bool Client::Process() { /************ Get all packets from packet manager out queue and process them ************/ EQApplicationPacket *app = nullptr; - if(!eqs->CheckState(CLOSING)) + if (!eqs->CheckState(CLOSING)) { - while(ret && (app = (EQApplicationPacket *)eqs->PopPacket())) { - if(app) + while (ret && (app = (EQApplicationPacket *)eqs->PopPacket())) { + if (app) ret = HandlePacket(app); safe_delete(app); } @@ -616,8 +628,17 @@ bool Client::Process() { //At this point, we are still connected, everything important has taken //place, now check to see if anybody wants to aggro us. // only if client is not feigned - if(zone->CanDoCombat() && ret && !GetFeigned() && scanarea_timer.Check()) { - entity_list.CheckClientAggro(this); + if (zone->CanDoCombat() && ret && !GetFeigned() && client_scan_npc_aggro_timer.Check()) { + int npc_scan_count = 0; + for (auto it = close_npcs.begin(); it != close_npcs.end(); ++it) { + NPC *npc = it->first; + + if (npc->CheckWillAggro(this) && !npc->CheckAggro(this)) { + npc->AddToHateList(this, 25); + } + npc_scan_count++; + } + Log.Out(Logs::General, Logs::Aggro, "Checking Reverse Aggro (client->npc) scanned_npcs (%i)", npc_scan_count); } #endif diff --git a/zone/entity.cpp b/zone/entity.cpp index 5f20dffc7..ee9c975ed 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2288,10 +2288,15 @@ bool EntityList::RemoveNPC(uint16 delete_id) { auto it = npc_list.find(delete_id); if (it != npc_list.end()) { + NPC *npc = it->second; // make sure its proximity is removed RemoveProximity(delete_id); + // remove from client close lists + RemoveNPCFromClientCloseLists(npc); // remove from the list npc_list.erase(it); + + // remove from limit list if needed if (npc_limit_list.count(delete_id)) npc_limit_list.erase(delete_id); @@ -2300,6 +2305,16 @@ bool EntityList::RemoveNPC(uint16 delete_id) return false; } +bool EntityList::RemoveNPCFromClientCloseLists(NPC *npc) +{ + auto it = client_list.begin(); + while (it != client_list.end()) { + it->second->close_npcs.erase(npc); + ++it; + } + return false; +} + bool EntityList::RemoveMerc(uint16 delete_id) { auto it = merc_list.find(delete_id); diff --git a/zone/entity.h b/zone/entity.h index 0ee029940..b9b85a133 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -279,6 +279,7 @@ public: bool RemoveTrap(uint16 delete_id); bool RemoveObject(uint16 delete_id); bool RemoveProximity(uint16 delete_npc_id); + bool RemoveNPCFromClientCloseLists(NPC *npc); void RemoveAllMobs(); void RemoveAllClients(); void RemoveAllNPCs();