diff --git a/common/ruletypes.h b/common/ruletypes.h index 401e55099..4edda9277 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -736,9 +736,6 @@ RULE_CATEGORY_END() RULE_CATEGORY(Expedition) RULE_INT(Expedition, MinStatusToBypassPlayerCountRequirements, 80, "Minimum GM status to bypass minimum player requirements for Expedition creation") -RULE_BOOL(Expedition, EmptyDzShutdownEnabled, true, "Enable early instance shutdown after last member of expedition removed") -RULE_INT(Expedition, EmptyDzShutdownDelaySeconds, 1500, "Seconds to set dynamic zone instance expiration if early shutdown enabled") -RULE_INT(Expedition, WorldExpeditionProcessRateMS, 6000, "Timer interval (milliseconds) that world checks expedition states") RULE_BOOL(Expedition, AlwaysNotifyNewLeaderOnChange, false, "Always notify clients when made expedition leader. If false (live-like) new leaders are only notified when made leader via /dzmakeleader") RULE_REAL(Expedition, LockoutDurationMultiplier, 1.0, "Multiplies lockout duration by this value when new lockouts are added") RULE_BOOL(Expedition, EnableInDynamicZoneStatus, false, "Enables the 'In Dynamic Zone' member status in expedition window. If false (live-like) players inside the dynamic zone will show as 'Online'") @@ -747,6 +744,9 @@ RULE_CATEGORY_END() RULE_CATEGORY(DynamicZone) RULE_INT(DynamicZone, ClientRemovalDelayMS, 60000, "Delay (milliseconds) until a client is teleported out of dynamic zone after being removed as member") +RULE_BOOL(DynamicZone, EmptyShutdownEnabled, true, "Enable early instance shutdown for dynamic zones that have no members") +RULE_INT(DynamicZone, EmptyShutdownDelaySeconds, 1500, "Seconds to set dynamic zone instance expiration if early shutdown enabled") +RULE_INT(DynamicZone, WorldProcessRate, 6000, "Timer interval (milliseconds) that systems check their dynamic zone states") RULE_CATEGORY_END() #undef RULE_CATEGORY diff --git a/world/dynamic_zone.cpp b/world/dynamic_zone.cpp index 47e70d00a..252559c29 100644 --- a/world/dynamic_zone.cpp +++ b/world/dynamic_zone.cpp @@ -35,6 +35,30 @@ DynamicZone* DynamicZone::FindDynamicZoneByID(uint32_t dz_id) return nullptr; } +DynamicZoneStatus DynamicZone::Process(bool force_expire) +{ + DynamicZoneStatus status = DynamicZoneStatus::Normal; + + if (force_expire || IsExpired()) + { + status = DynamicZoneStatus::Expired; + + auto dz_zoneserver = zoneserver_list.FindByInstanceID(GetInstanceID()); + if (!dz_zoneserver || dz_zoneserver->NumPlayers() == 0) // no clients inside dz + { + status = DynamicZoneStatus::ExpiredEmpty; + + if (force_expire && !m_is_pending_early_shutdown && RuleB(DynamicZone, EmptyShutdownEnabled)) + { + SetSecondsRemaining(RuleI(DynamicZone, EmptyShutdownDelaySeconds)); + m_is_pending_early_shutdown = true; + } + } + } + + return status; +} + void DynamicZone::SetSecondsRemaining(uint32_t seconds_remaining) { auto now = std::chrono::system_clock::now(); diff --git a/world/dynamic_zone.h b/world/dynamic_zone.h index c95512260..84455d7eb 100644 --- a/world/dynamic_zone.h +++ b/world/dynamic_zone.h @@ -6,6 +6,14 @@ class ServerPacket; +enum class DynamicZoneStatus +{ + Unknown = 0, + Normal, + Expired, + ExpiredEmpty, +}; + class DynamicZone { public: @@ -23,6 +31,7 @@ public: std::chrono::system_clock::duration GetRemainingDuration() const { return m_expire_time - std::chrono::system_clock::now(); } + DynamicZoneStatus Process(bool force_expire); bool IsExpired() const { return m_expire_time < std::chrono::system_clock::now(); } void SetSecondsRemaining(uint32_t seconds_remaining); @@ -33,6 +42,7 @@ private: uint32_t m_instance_id = 0; uint32_t m_zone_id = 0; uint32_t m_zone_version = 0; + bool m_is_pending_early_shutdown = false; DynamicZoneType m_type{ DynamicZoneType::None }; std::chrono::seconds m_duration; std::chrono::time_point m_start_time; diff --git a/world/expedition.cpp b/world/expedition.cpp index d983a82d0..2937a20bc 100644 --- a/world/expedition.cpp +++ b/world/expedition.cpp @@ -161,3 +161,21 @@ void Expedition::CheckLeader() ChooseNewLeader(); } } + +bool Expedition::Process() +{ + // returns true if expedition needs to be deleted from world cache and db + // expedition is not deleted until its dz has no clients to prevent exploits + auto status = m_dynamic_zone.Process(IsEmpty()); // force expire if no members + if (status == DynamicZoneStatus::ExpiredEmpty) + { + LogExpeditions("Expedition [{}] expired or empty, notifying zones and deleting", GetID()); + SendZonesExpeditionDeleted(); + return true; + } + + CheckExpireWarning(); + CheckLeader(); + + return false; +} diff --git a/world/expedition.h b/world/expedition.h index 952abc1a0..2f48739df 100644 --- a/world/expedition.h +++ b/world/expedition.h @@ -43,19 +43,17 @@ public: uint32_t GetID() const { return m_expedition_id; } bool HasMember(uint32_t character_id); bool IsEmpty() const { return m_member_ids.empty(); } - bool IsPendingDelete() const { return m_pending_delete; } bool IsValid() const { return m_expedition_id != 0; } + bool Process(); void SendZonesExpeditionDeleted(); void SendZonesExpireWarning(uint32_t minutes_remaining); bool SetNewLeader(uint32_t new_leader_id); - void SetPendingDelete(bool pending) { m_pending_delete = pending; } private: void SendZonesLeaderChanged(); uint32_t m_expedition_id = 0; uint32_t m_leader_id = 0; - bool m_pending_delete = false; bool m_choose_leader_needed = false; Timer m_choose_leader_cooldown_timer; Timer m_warning_cooldown_timer; diff --git a/world/expedition_state.cpp b/world/expedition_state.cpp index c1cb0fc8c..815ef6c58 100644 --- a/world/expedition_state.cpp +++ b/world/expedition_state.cpp @@ -21,13 +21,9 @@ #include "expedition_state.h" #include "expedition.h" #include "expedition_database.h" -#include "zonelist.h" -#include "zoneserver.h" #include "../common/eqemu_logsys.h" #include -extern ZSList zoneserver_list; - ExpeditionState expedition_state; Expedition* ExpeditionState::GetExpedition(uint32_t expedition_id) @@ -117,36 +113,11 @@ void ExpeditionState::Process() for (auto it = m_expeditions.begin(); it != m_expeditions.end();) { - bool is_deleted = false; - - if (it->IsEmpty() || it->GetDynamicZone().IsExpired()) + bool is_deleted = it->Process(); + if (is_deleted) { - // don't delete expedition until its dz instance is empty. this prevents - // an exploit where all members leave expedition and complete an event - // before being kicked from removal timer. the lockout could never be - // applied because the zone expedition cache was already invalidated. - auto dz_zoneserver = zoneserver_list.FindByInstanceID(it->GetDynamicZone().GetInstanceID()); - if (!dz_zoneserver || dz_zoneserver->NumPlayers() == 0) - { - LogExpeditions("Expedition [{}] expired or empty, notifying zones and deleting", it->GetID()); - expedition_ids.emplace_back(it->GetID()); - it->SendZonesExpeditionDeleted(); - is_deleted = true; - } - - if (it->IsEmpty() && !it->IsPendingDelete() && RuleB(Expedition, EmptyDzShutdownEnabled)) - { - it->GetDynamicZone().SetSecondsRemaining(RuleI(Expedition, EmptyDzShutdownDelaySeconds)); - } - - it->SetPendingDelete(true); + expedition_ids.emplace_back(it->GetID()); } - else - { - it->CheckExpireWarning(); - it->CheckLeader(); - } - it = is_deleted ? m_expeditions.erase(it) : it + 1; } diff --git a/world/expedition_state.h b/world/expedition_state.h index 58b733989..d7fb27d09 100644 --- a/world/expedition_state.h +++ b/world/expedition_state.h @@ -44,7 +44,7 @@ public: private: std::vector m_expeditions; - Timer m_process_throttle_timer{static_cast(RuleI(Expedition, WorldExpeditionProcessRateMS))}; + Timer m_process_throttle_timer{static_cast(RuleI(DynamicZone, WorldProcessRate))}; }; #endif