diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 6b7c58841..cf6f4ca28 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -25,6 +25,9 @@ N(OP_AdventureRequest), N(OP_AdventureStatsReply), N(OP_AdventureStatsRequest), N(OP_AdventureUpdate), +N(OP_AggroMeterLockTarget), +N(OP_AggroMeterTargetInfo), +N(OP_AggroMeterUpdate), N(OP_AltCurrency), N(OP_AltCurrencyMerchantReply), N(OP_AltCurrencyMerchantRequest), diff --git a/common/ruletypes.h b/common/ruletypes.h index f972034f0..a0bf01496 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -114,6 +114,7 @@ RULE_BOOL(Character, CheckCursorEmptyWhenLooting, true) // If true, a player can RULE_BOOL(Character, MaintainIntoxicationAcrossZones, true) // If true, alcohol effects are maintained across zoning and logging out/in. RULE_BOOL(Character, EnableDiscoveredItems, true) // If enabled, it enables EVENT_DISCOVER_ITEM and also saves character names and timestamps for the first time an item is discovered. RULE_BOOL(Character, EnableXTargetting, true) // Enable Extended Targetting Window, for users with UF and later clients. +RULE_BOOL(Character, EnableAggroMeter, true) // Enable Aggro Meter, for users with RoF and later clients. RULE_BOOL(Character, KeepLevelOverMax, false) // Don't delevel a character that has somehow gone over the level cap RULE_INT(Character, FoodLossPerUpdate, 35) // How much food/water you lose per stamina update RULE_INT(Character, BaseInstrumentSoftCap, 36) // Softcap for instrument mods, 36 commonly referred to as "3.6" as well. diff --git a/utils/patches/patch_RoF.conf b/utils/patches/patch_RoF.conf index aefcd8dbc..ad54b6341 100644 --- a/utils/patches/patch_RoF.conf +++ b/utils/patches/patch_RoF.conf @@ -359,6 +359,9 @@ OP_OpenContainer=0x654f OP_Marquee=0x288a OP_Fling=0x6b8e OP_CancelSneakHide=0x265f +OP_AggroMeterLockTarget=0x70b7 +OP_AggroMeterTargetInfo=0x18fe +OP_AggroMeterUpdate=0x75aa OP_DzQuit=0x5fc8 OP_DzListTimers=0x67b9 diff --git a/utils/patches/patch_RoF2.conf b/utils/patches/patch_RoF2.conf index 20111e850..9f588d18f 100644 --- a/utils/patches/patch_RoF2.conf +++ b/utils/patches/patch_RoF2.conf @@ -360,6 +360,9 @@ OP_ItemRecastDelay=0x15a9 OP_ResetAA=0x1669 OP_Fling=0x6f80 OP_CancelSneakHide=0x0927 +OP_AggroMeterLockTarget=0x1643 +OP_AggroMeterTargetInfo=0x16bc +OP_AggroMeterUpdate=0x1781 # Expeditions OP_DzAddPlayer=0x4701 diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 7bc96c3db..deadd079e 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -4,6 +4,7 @@ SET(zone_sources aa.cpp aa_ability.cpp aggro.cpp + aggromanager.cpp attack.cpp beacon.cpp bonuses.cpp @@ -132,6 +133,7 @@ SET(zone_sources SET(zone_headers aa.h aa_ability.h + aggromanager.h basic_functions.h beacon.h bot.h diff --git a/zone/aggromanager.cpp b/zone/aggromanager.cpp new file mode 100644 index 000000000..8dd532c74 --- /dev/null +++ b/zone/aggromanager.cpp @@ -0,0 +1,11 @@ +#include "aggromanager.h" + +AggroMeter::AggroMeter() : lock_id(0), target_id(0), secondary_id(0), lock_changed(false) +{ + for (int i = 0; i < AT_Max; ++i) { + data[i].type = i; + data[i].pct = 0; + } +} + + diff --git a/zone/aggromanager.h b/zone/aggromanager.h new file mode 100644 index 000000000..b140b07d7 --- /dev/null +++ b/zone/aggromanager.h @@ -0,0 +1,80 @@ +#ifndef AGGROMANAGER_H +#define AGGROMANAGER_H + +#include "../common/types.h" +#include +#include + +class AggroMeter +{ +public: + enum AggroTypes { + AT_Player, + AT_Secondary, + AT_Group1, + AT_Group2, + AT_Group3, + AT_Group4, + AT_Group5, + AT_XTarget1, + AT_XTarget2, + AT_XTarget3, + AT_XTarget4, + AT_XTarget5, + AT_XTarget6, + AT_XTarget7, + AT_XTarget8, + AT_XTarget9, + AT_XTarget10, + AT_XTarget11, + AT_XTarget12, + AT_XTarget13, + AT_XTarget14, + AT_XTarget15, + AT_XTarget16, + AT_XTarget17, + AT_XTarget18, + AT_XTarget19, + AT_XTarget20, + AT_Max + }; + +private: + struct AggroData { + int16 type; + int16 pct; + }; + + AggroData data[AT_Max]; + int lock_id; // we set this + int target_id; // current target or if PC targeted, their Target + // so secondary depends on if we have aggro or not + // When we are the current target, this will be the 2nd person on list + // When we are not tanking, this will be the current tank + int secondary_id; + + // so we need some easy way to detect the client changing but still delaying the packet + bool lock_changed; +public: + AggroMeter(); + ~AggroMeter() {} + + inline void set_lock_id(int in) { lock_id = in; lock_changed = true; } + inline bool update_lock() { bool ret = lock_changed; lock_changed = false; return ret; } + inline void set_target_id(int in) { target_id = in; } + inline void set_secondary_id(int in) { secondary_id = in; } + // returns true when changed + inline bool set_pct(AggroTypes t, int pct) { assert(t >= AT_Player && t < AT_Max); if (data[t].pct == pct) return false; data[t].pct = pct; return true; } + + inline int get_lock_id() const { return lock_id; } + inline int get_target_id() const { return target_id; } + inline int get_secondary_id() const { return secondary_id; } + inline int get_pct(AggroTypes t) const { assert(t >= AT_Player && t < AT_Max); return data[t].pct; } + // the ID of the spawn for player entry depends on lock_id + inline int get_player_aggro_id() const { return lock_id ? lock_id : target_id; } + // fuck it, lets just use a buffer the size of the largest to work with + const inline size_t max_packet_size() const { return sizeof(uint8) + sizeof(uint32) + sizeof(uint8) + (sizeof(uint8) + sizeof(uint16)) * AT_Max; } +}; + + +#endif /* !AGGROMANAGER_H */ diff --git a/zone/client.cpp b/zone/client.cpp index a3239eeb1..9e02195c0 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -154,6 +154,7 @@ Client::Client(EQStreamInterface* ieqs) afk_toggle_timer(250), helm_toggle_timer(250), light_update_timer(600), + aggro_meter_timer(1000), m_Proximity(FLT_MAX, FLT_MAX, FLT_MAX), //arbitrary large number m_ZoneSummonLocation(-2.0f,-2.0f,-2.0f), m_AutoAttackPosition(0.0f, 0.0f, 0.0f, 0.0f), @@ -8771,3 +8772,167 @@ void Client::CheckRegionTypeChanges() else if (GetPVP()) SetPVP(false, false); } + +void Client::ProcessAggroMeter() +{ + if (!AggroMeterAvailable()) + return; + + // we need to decide if we need to send OP_AggroMeterTargetInfo now + // This packet sends the current lock target ID and the current target ID + // target ID will be either our target or our target of target when we're targeting a PC + bool send_targetinfo = false; + auto cur_tar = GetTarget(); + + // probably should have PVP rules ... + if (cur_tar && cur_tar != this) { + if (cur_tar->IsNPC() && !cur_tar->IsPetOwnerClient() && cur_tar->GetID() != m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(cur_tar->GetID()); + send_targetinfo = true; + } else if ((cur_tar->IsPetOwnerClient() || cur_tar->IsClient()) && cur_tar->GetTarget() && cur_tar->GetTarget()->GetID() != m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(cur_tar->GetTarget()->GetID()); + send_targetinfo = true; + } + } else if (m_aggrometer.get_target_id()) { + m_aggrometer.set_target_id(0); + send_targetinfo = true; + } + + if (m_aggrometer.update_lock()) + send_targetinfo = true; + + if (send_targetinfo) { + auto app = new EQApplicationPacket(OP_AggroMeterTargetInfo, sizeof(uint32) * 2); + app->WriteUInt32(m_aggrometer.get_lock_id()); + app->WriteUInt32(m_aggrometer.get_target_id()); + FastQueuePacket(&app); + } + + // we could just calculate how big the packet would need to be ... but it's easier this way :P should be 87 bytes + auto app = new EQApplicationPacket(OP_AggroMeterUpdate, m_aggrometer.max_packet_size()); + + cur_tar = entity_list.GetMob(m_aggrometer.get_target_id()); + + // first we must check the secondary + // TODO: lock target should affect secondary as well + bool send = false; + Mob *secondary = nullptr; + bool has_aggro = false; + if (cur_tar) { + if (cur_tar->GetTarget() == this) {// we got aggro + secondary = cur_tar->GetSecondaryHate(this); + has_aggro = true; + } else { + secondary = cur_tar->GetTarget(); + } + } + + if (secondary && secondary->GetID() != m_aggrometer.get_secondary_id()) { + m_aggrometer.set_secondary_id(secondary->GetID()); + app->WriteUInt8(1); + app->WriteUInt32(m_aggrometer.get_secondary_id()); + send = true; + } else if (!secondary && m_aggrometer.get_secondary_id()) { + m_aggrometer.set_secondary_id(0); + app->WriteUInt8(1); + app->WriteUInt32(0); + send = true; + } else { // might not need to send in this case + app->WriteUInt8(0); + } + + auto count_offset = app->GetWritePosition(); + app->WriteUInt8(0); + + int count = 0; + auto add_entry = [&app, &count, this](AggroMeter::AggroTypes i) { + count++; + app->WriteUInt8(i); + app->WriteUInt16(m_aggrometer.get_pct(i)); + }; + // TODO: Player entry should either be lock or yourself, ignoring lock for now + // player, secondary, and group depend on your target/lock + if (cur_tar) { + if (m_aggrometer.set_pct(AggroMeter::AT_Player, cur_tar->GetHateRatio(cur_tar->GetTarget(), this))) + add_entry(AggroMeter::AT_Player); + + if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, has_aggro ? cur_tar->GetHateRatio(this, secondary) : secondary ? 100 : 0)) + add_entry(AggroMeter::AT_Secondary); + + // fuuuuuuuuuuuuuuuuuuuuuuuucckkkkkkkkkkkkkkk raids + if (IsRaidGrouped()) { + auto raid = GetRaid(); + if (raid) { + auto gid = raid->GetGroup(this); + if (gid < 12) { + int at_id = AggroMeter::AT_Group1; + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) { + if (raid->members[i].member && raid->members[i].member != this && raid->members[i].GroupNumber == gid) { + if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), raid->members[i].member))) + add_entry(static_cast(at_id)); + at_id++; + if (at_id > AggroMeter::AT_Group5) + break; + } + } + } + } + } else if (IsGrouped()) { + auto group = GetGroup(); + if (group) { + int at_id = AggroMeter::AT_Group1; + for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { + if (group->members[i] && group->members[i] != this) { + if (m_aggrometer.set_pct(static_cast(at_id), cur_tar->GetHateRatio(cur_tar->GetTarget(), group->members[i]))) + add_entry(static_cast(at_id)); + at_id++; + } + } + } + } + } else { // we might need to clear out some data now + if (m_aggrometer.set_pct(AggroMeter::AT_Player, 0)) + add_entry(AggroMeter::AT_Player); + if (m_aggrometer.set_pct(AggroMeter::AT_Secondary, 0)) + add_entry(AggroMeter::AT_Secondary); + if (m_aggrometer.set_pct(AggroMeter::AT_Group1, 0)) + add_entry(AggroMeter::AT_Group1); + if (m_aggrometer.set_pct(AggroMeter::AT_Group2, 0)) + add_entry(AggroMeter::AT_Group2); + if (m_aggrometer.set_pct(AggroMeter::AT_Group3, 0)) + add_entry(AggroMeter::AT_Group3); + if (m_aggrometer.set_pct(AggroMeter::AT_Group4, 0)) + add_entry(AggroMeter::AT_Group5); + if (m_aggrometer.set_pct(AggroMeter::AT_Group5, 0)) + add_entry(AggroMeter::AT_Group5); + } + + // now to go over our xtargets + // if the entry is an NPC it's our hate relative to the NPCs current tank + // if it's a PC, it's their hate relative to our current target + for (int i = 0; i < GetMaxXTargets(); ++i) { + if (XTargets[i].ID) { + auto mob = entity_list.GetMob(XTargets[i].ID); + if (mob) { + int ratio = 0; + if (mob->IsNPC()) + ratio = mob->GetHateRatio(mob->GetTarget(), this); + else if (cur_tar) + ratio = cur_tar->GetHateRatio(cur_tar->GetTarget(), mob); + if (m_aggrometer.set_pct(static_cast(AggroMeter::AT_XTarget1 + i), ratio)) + add_entry(static_cast(AggroMeter::AT_XTarget1 + i)); + } + } + } + + if (send || count) { + app->size = app->GetWritePosition(); // this should be safe, although not recommended + // but this way we can have a smaller buffer created for the packet dispatched to the client w/o resizing this one + app->SetWritePosition(count_offset); + app->WriteUInt8(count); + FastQueuePacket(&app); + } else { + safe_delete(app); + } +} + diff --git a/zone/client.h b/zone/client.h index ae6c2ef30..6dc4d6e38 100644 --- a/zone/client.h +++ b/zone/client.h @@ -49,6 +49,7 @@ namespace EQEmu #include "../common/guilds.h" //#include "../common/item_data.h" #include "xtargetautohaters.h" +#include "aggromanager.h" #include "common.h" #include "merc.h" @@ -1133,6 +1134,11 @@ public: bool GroupFollow(Client* inviter); inline bool GetRunMode() const { return runmode; } + inline bool AggroMeterAvailable() const { return ((m_ClientVersionBit & EQEmu::versions::bit_RoF2AndLater)) && RuleB(Character, EnableAggroMeter); } // RoF untested + inline void SetAggroMeterLock(int in) { m_aggrometer.set_lock_id(in); } + + void ProcessAggroMeter(); // builds packet and sends + void InitializeMercInfo(); bool CheckCanSpawnMerc(uint32 template_id); bool CheckCanHireMerc(Mob* merchant, uint32 template_id); @@ -1470,6 +1476,7 @@ private: Timer afk_toggle_timer; Timer helm_toggle_timer; Timer light_update_timer; + Timer aggro_meter_timer; glm::vec3 m_Proximity; @@ -1560,6 +1567,8 @@ private: XTargetAutoHaters m_autohatermgr; XTargetAutoHaters *m_activeautohatermgr; + AggroMeter m_aggrometer; + Timer ItemTickTimer; Timer ItemQuestTimer; std::map accountflags; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 721421b9e..535e5e98e 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -120,6 +120,7 @@ void MapOpcodes() ConnectedOpcodes[OP_AdventureMerchantSell] = &Client::Handle_OP_AdventureMerchantSell; ConnectedOpcodes[OP_AdventureRequest] = &Client::Handle_OP_AdventureRequest; ConnectedOpcodes[OP_AdventureStatsRequest] = &Client::Handle_OP_AdventureStatsRequest; + ConnectedOpcodes[OP_AggroMeterLockTarget] = &Client::Handle_OP_AggroMeterLockTarget; ConnectedOpcodes[OP_AltCurrencyMerchantRequest] = &Client::Handle_OP_AltCurrencyMerchantRequest; ConnectedOpcodes[OP_AltCurrencyPurchase] = &Client::Handle_OP_AltCurrencyPurchase; ConnectedOpcodes[OP_AltCurrencyReclaim] = &Client::Handle_OP_AltCurrencyReclaim; @@ -2400,6 +2401,17 @@ void Client::Handle_OP_AdventureStatsRequest(const EQApplicationPacket *app) FastQueuePacket(&outapp); } +void Client::Handle_OP_AggroMeterLockTarget(const EQApplicationPacket *app) +{ + if (app->size < sizeof(uint32)) { + Log.Out(Logs::General, Logs::Error, "Handle_OP_AggroMeterLockTarget had a packet that was too small."); + return; + } + + SetAggroMeterLock(app->ReadUInt32(0)); + ProcessAggroMeter(); +} + void Client::Handle_OP_AltCurrencyMerchantRequest(const EQApplicationPacket *app) { VERIFY_PACKET_LENGTH(OP_AltCurrencyMerchantRequest, app, uint32); diff --git a/zone/client_packet.h b/zone/client_packet.h index 2635724dd..76a26e04e 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -32,6 +32,7 @@ void Handle_OP_AdventureMerchantSell(const EQApplicationPacket *app); void Handle_OP_AdventureRequest(const EQApplicationPacket *app); void Handle_OP_AdventureStatsRequest(const EQApplicationPacket *app); + void Handle_OP_AggroMeterLockTarget(const EQApplicationPacket *app); void Handle_OP_AltCurrencyMerchantRequest(const EQApplicationPacket *app); void Handle_OP_AltCurrencyPurchase(const EQApplicationPacket *app); void Handle_OP_AltCurrencyReclaim(const EQApplicationPacket *app); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index b97f7b22e..2bfa8a055 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -684,7 +684,8 @@ bool Client::Process() { if (client_state == CLIENT_CONNECTED) { if (m_dirtyautohaters) ProcessXTargetAutoHaters(); - // aggro meter stuff should live here + if (aggro_meter_timer.Check()) + ProcessAggroMeter(); } return ret; diff --git a/zone/hate_list.cpp b/zone/hate_list.cpp index 34519b69f..225ff39ff 100644 --- a/zone/hate_list.cpp +++ b/zone/hate_list.cpp @@ -23,6 +23,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "raids.h" #include "../common/rulesys.h" +#include "../common/data_verification.h" #include "hate_list.h" #include "quest_parser_collection.h" @@ -277,7 +278,24 @@ int HateList::GetSummonedPetCountOnHateList(Mob *hater) { return pet_count; } -Mob *HateList::GetEntWithMostHateOnList(Mob *center) +int HateList::GetHateRatio(Mob *top, Mob *other) +{ + auto other_entry = Find(other); + + if (!other_entry || other_entry->stored_hate_amount < 1) + return 0; + + auto top_entry = Find(top); + + if (!top_entry || top_entry->stored_hate_amount < 1) + return 999; // shouldn't happen if you call it right :P + + return EQEmu::Clamp(static_cast((other_entry->stored_hate_amount * 100) / top_entry->stored_hate_amount), 1, 999); +} + +// skip is used to ignore a certain mob on the list +// Currently used for getting 2nd on list for aggro meter +Mob *HateList::GetEntWithMostHateOnList(Mob *center, Mob *skip) { // hack fix for zone shutdown crashes on some servers if (!zone->IsLoaded()) @@ -310,6 +328,11 @@ Mob *HateList::GetEntWithMostHateOnList(Mob *center) continue; } + if (cur->entity_on_hatelist == skip) { + ++iterator; + continue; + } + auto hateEntryPosition = glm::vec3(cur->entity_on_hatelist->GetX(), cur->entity_on_hatelist->GetY(), cur->entity_on_hatelist->GetZ()); if (center->IsNPC() && center->CastToNPC()->IsUnderwaterOnly() && zone->HasWaterMap()) { if (!zone->watermap->InLiquid(hateEntryPosition)) { @@ -436,6 +459,11 @@ Mob *HateList::GetEntWithMostHateOnList(Mob *center) while (iterator != list.end()) { struct_HateList *cur = (*iterator); + if (cur->entity_on_hatelist == skip) { + ++iterator; + continue; + } + if (center->IsNPC() && center->CastToNPC()->IsUnderwaterOnly() && zone->HasWaterMap()) { if(!zone->watermap->InLiquid(glm::vec3(cur->entity_on_hatelist->GetPosition()))) { skipped_count++; diff --git a/zone/hate_list.h b/zone/hate_list.h index dda8cb21d..f0e2b7618 100644 --- a/zone/hate_list.h +++ b/zone/hate_list.h @@ -41,9 +41,9 @@ public: Mob *GetClosestEntOnHateList(Mob *hater); Mob *GetDamageTopOnHateList(Mob *hater); - Mob *GetEntWithMostHateOnList(Mob *center); + Mob *GetEntWithMostHateOnList(Mob *center, Mob *skip = nullptr); Mob *GetRandomEntOnHateList(); - Mob* GetEntWithMostHateOnList(); + Mob *GetEntWithMostHateOnList(); bool IsEntOnHateList(Mob *mob); bool IsHateListEmpty(); @@ -51,6 +51,7 @@ public: int AreaRampage(Mob *caster, Mob *target, int count, ExtraAttackOptions *opts); int GetSummonedPetCountOnHateList(Mob *hater); + int GetHateRatio(Mob *top, Mob *other); int32 GetEntHateAmount(Mob *ent, bool in_damage = false); @@ -73,4 +74,5 @@ private: Mob *hate_owner; }; -#endif \ No newline at end of file +#endif + diff --git a/zone/mob.h b/zone/mob.h index fe76f949d..42672f046 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -539,7 +539,9 @@ public: void DoubleAggro(Mob *other) { uint32 in_hate = GetHateAmount(other); SetHateAmountOnEnt(other, (in_hate ? in_hate * 2 : 1)); } uint32 GetHateAmount(Mob* tmob, bool is_dam = false) { return hate_list.GetEntHateAmount(tmob,is_dam);} uint32 GetDamageAmount(Mob* tmob) { return hate_list.GetEntHateAmount(tmob, true);} + int GetHateRatio(Mob *first, Mob *with) { return hate_list.GetHateRatio(first, with); } Mob* GetHateTop() { return hate_list.GetEntWithMostHateOnList(this);} + Mob* GetSecondaryHate(Mob *skip) { return hate_list.GetEntWithMostHateOnList(this, skip); } Mob* GetHateDamageTop(Mob* other) { return hate_list.GetDamageTopOnHateList(other);} Mob* GetHateRandom() { return hate_list.GetRandomEntOnHateList();} Mob* GetHateMost() { return hate_list.GetEntWithMostHateOnList();}