mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 21:01:29 +00:00
When dynamic zones are cached on zone boot each dz requests member statuses from world separately. This causes a lot of network traffic between world and booted zones when there are a lot of active dzs. This changes it to make a single request to world on zone boot and a single bulk reply back.
1677 lines
49 KiB
C++
1677 lines
49 KiB
C++
/**
|
|
* EQEmulator: Everquest Server Emulator
|
|
* Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY except by those people which sell it, which
|
|
* are required to give you total support for your newly bought product;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*
|
|
*/
|
|
|
|
#include "dynamic_zone.h"
|
|
#include "client.h"
|
|
#include "expedition_request.h"
|
|
#include "string_ids.h"
|
|
#include "worldserver.h"
|
|
#include "../common/repositories/character_expedition_lockouts_repository.h"
|
|
#include "../common/repositories/dynamic_zone_lockouts_repository.h"
|
|
#include <cereal/types/utility.hpp>
|
|
|
|
extern WorldServer worldserver;
|
|
|
|
// various expeditions use these strings when locking
|
|
constexpr char LockClose[] = "Your expedition is nearing its close. You cannot bring any additional people into your expedition at this time.";
|
|
constexpr char LockBegin[] = "The trial has begun. You cannot bring any additional people into your expedition at this time.";
|
|
|
|
DynamicZone::DynamicZone(uint32_t zone_id, uint32_t version, uint32_t duration, DynamicZoneType type)
|
|
{
|
|
m_zone_id = zone_id;
|
|
m_zone_version = version;
|
|
m_duration = std::chrono::seconds(duration);
|
|
m_type = type;
|
|
}
|
|
|
|
Database& DynamicZone::GetDatabase()
|
|
{
|
|
return database;
|
|
}
|
|
|
|
bool DynamicZone::SendServerPacket(ServerPacket* packet)
|
|
{
|
|
return worldserver.SendPacket(packet);
|
|
}
|
|
|
|
uint16_t DynamicZone::GetCurrentInstanceID() const
|
|
{
|
|
return zone ? static_cast<uint16_t>(zone->GetInstanceID()) : 0;
|
|
}
|
|
|
|
uint16_t DynamicZone::GetCurrentZoneID() const
|
|
{
|
|
return zone ? static_cast<uint16_t>(zone->GetZoneID()) : 0;
|
|
}
|
|
|
|
DynamicZone* DynamicZone::TryCreate(Client& client, DynamicZone& dzinfo, bool silent)
|
|
{
|
|
// only expedition types are currently created in zone
|
|
if (!zone || dzinfo.GetID() != 0 || !dzinfo.IsExpedition())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// request parses leader, members list, and lockouts while validating
|
|
ExpeditionRequest request(dzinfo, client, silent);
|
|
if (!request.Validate())
|
|
{
|
|
LogExpeditions("[{}] request by [{}] denied", dzinfo.GetName(), client.GetName());
|
|
return nullptr;
|
|
}
|
|
|
|
dzinfo.SetLeader({ request.GetLeaderID(), request.GetLeaderName(), DynamicZoneMemberStatus::Online });
|
|
|
|
// this creates a new dz instance and saves it to both db and cache
|
|
uint32_t dz_id = dzinfo.Create();
|
|
if (dz_id == 0)
|
|
{
|
|
// live uses this message when trying to enter an instance that isn't ready
|
|
// for now we can use it as a client error message if instance creation fails
|
|
client.MessageString(Chat::Red, DZ_PREVENT_ENTERING);
|
|
LogDynamicZones("Failed to create dynamic zone for zone [{}]", dzinfo.GetZoneID());
|
|
return nullptr;
|
|
}
|
|
|
|
auto [it, ok] = zone->dynamic_zone_cache.try_emplace(dz_id, std::make_unique<DynamicZone>(dzinfo));
|
|
|
|
DynamicZone* dz = it->second.get();
|
|
dz->SaveMembers(request.GetMembers());
|
|
dz->SaveLockouts(request.GetLockouts());
|
|
|
|
dz->SendLeaderMessage(request.GetLeaderClient(), Chat::System, DZ_AVAILABLE, { dz->GetName() });
|
|
if (dz->GetMemberCount() < request.GetMembers().size())
|
|
{
|
|
dz->SendLeaderMessage(request.GetLeaderClient(), Chat::System, fmt::format(DzNotAllAdded,
|
|
request.IsRaid() ? "raid" : "group", "expedition", dz->GetMaxPlayers(), request.GetMembers().size()));
|
|
}
|
|
|
|
// world must be notified before we request async member updates
|
|
auto pack = dz->CreateServerPacket(zone->GetZoneID(), zone->GetInstanceID());
|
|
worldserver.SendPacket(pack.get());
|
|
|
|
dz->UpdateMembers();
|
|
|
|
LogDynamicZones("Created new dz [{}] for zone [{}]", dz_id, dz->GetZoneID());
|
|
|
|
return dz;
|
|
}
|
|
|
|
void DynamicZone::CacheNewDynamicZone(ServerPacket* pack)
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzCreate_Struct*>(pack->pBuffer);
|
|
|
|
// caching new dz created in world or another zone (has member statuses set by world)
|
|
auto [it, ok] = zone->dynamic_zone_cache.try_emplace(buf->dz_id, std::make_unique<DynamicZone>());
|
|
it->second->Unserialize({ buf->cereal_data, buf->cereal_size });
|
|
it->second->UpdateMembers();
|
|
|
|
LogDynamicZones("Cached new dynamic zone [{}]", buf->dz_id);
|
|
}
|
|
|
|
void DynamicZone::CacheAllFromDatabase()
|
|
{
|
|
if (!zone)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BenchTimer bench;
|
|
|
|
auto dzs = DynamicZonesRepository::AllWithInstanceNotExpired(database);
|
|
auto members = DynamicZoneMembersRepository::AllWithNames(database);
|
|
auto lockouts = DynamicZoneLockoutsRepository::All(database);
|
|
|
|
zone->dynamic_zone_cache.clear();
|
|
zone->dynamic_zone_cache.reserve(dzs.size());
|
|
|
|
for (auto& entry : dzs)
|
|
{
|
|
uint32_t dz_id = entry.id;
|
|
auto dz = std::make_unique<DynamicZone>(std::move(entry));
|
|
|
|
for (auto& member : members)
|
|
{
|
|
if (member.dynamic_zone_id == dz_id)
|
|
{
|
|
dz->AddMemberFromRepositoryResult(std::move(member));
|
|
}
|
|
}
|
|
|
|
for (auto& lockout : lockouts)
|
|
{
|
|
if (lockout.dynamic_zone_id == dz->GetID())
|
|
{
|
|
dz->m_lockouts.emplace_back(dz->GetName(), std::move(lockout));
|
|
}
|
|
}
|
|
|
|
zone->dynamic_zone_cache.emplace(dz_id, std::move(dz));
|
|
}
|
|
|
|
if (!zone->dynamic_zone_cache.empty())
|
|
{
|
|
RequestMemberStatuses();
|
|
}
|
|
|
|
LogInfo("Loaded [{}] dynamic zone(s)", Strings::Commify(zone->dynamic_zone_cache.size()));
|
|
LogDynamicZones("Caching [{}] dynamic zone(s) took [{}s]", zone->dynamic_zone_cache.size(), bench.elapsed());
|
|
}
|
|
|
|
void DynamicZone::RequestMemberStatuses()
|
|
{
|
|
ServerPacket pack(ServerOP_DzGetBulkMemberStatuses, sizeof(ServerDzCerealData_Struct));
|
|
auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack.pBuffer);
|
|
buf->zone_id = static_cast<uint16_t>(zone->GetZoneID());
|
|
buf->inst_id = static_cast<uint16_t>(zone->GetInstanceID());
|
|
|
|
worldserver.SendPacket(&pack);
|
|
}
|
|
|
|
template <typename T>
|
|
DynamicZone* FindDynamicZone(T pred)
|
|
{
|
|
if (zone)
|
|
{
|
|
for (const auto& [id_, dz] : zone->dynamic_zone_cache)
|
|
{
|
|
if (pred(*dz.get()))
|
|
{
|
|
return dz.get();
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
DynamicZone* DynamicZone::FindDynamicZoneByID(uint32_t dz_id, DynamicZoneType type)
|
|
{
|
|
if (zone)
|
|
{
|
|
auto it = zone->dynamic_zone_cache.find(dz_id);
|
|
if (it != zone->dynamic_zone_cache.end() && (type == DynamicZoneType::None || it->second->GetType() == type))
|
|
{
|
|
return it->second.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
DynamicZone* DynamicZone::FindExpeditionByCharacter(uint32_t char_id)
|
|
{
|
|
return FindDynamicZone([&](const DynamicZone& dz) { return dz.IsExpedition() && dz.HasMember(char_id); });
|
|
}
|
|
|
|
DynamicZone* DynamicZone::FindExpeditionByZone(uint32_t zone_id, uint32_t instance_id)
|
|
{
|
|
return FindDynamicZone([&](const DynamicZone& dz) { return dz.IsExpedition() && dz.IsSameDz(zone_id, instance_id); });
|
|
}
|
|
|
|
void DynamicZone::StartAllClientRemovalTimers()
|
|
{
|
|
for (const auto& client_iter : entity_list.GetClientList())
|
|
{
|
|
if (client_iter.second)
|
|
{
|
|
client_iter.second->SetDzRemovalTimer(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DynamicZone::IsCurrentZoneDz() const
|
|
{
|
|
return zone && zone->GetInstanceID() != 0 && zone->GetInstanceID() == GetInstanceID();
|
|
}
|
|
|
|
void DynamicZone::SetSecondsRemaining(uint32_t seconds_remaining)
|
|
{
|
|
// async
|
|
constexpr uint32_t pack_size = sizeof(ServerDzSetDuration_Struct);
|
|
auto pack = std::make_unique<ServerPacket>(ServerOP_DzSetSecondsRemaining, pack_size);
|
|
auto buf = reinterpret_cast<ServerDzSetDuration_Struct*>(pack->pBuffer);
|
|
buf->dz_id = GetID();
|
|
buf->seconds = seconds_remaining;
|
|
worldserver.SendPacket(pack.get());
|
|
}
|
|
|
|
void DynamicZone::SetUpdatedDuration(uint32_t new_duration)
|
|
{
|
|
// preserves original start time, just modifies duration and expire time
|
|
m_duration = std::chrono::seconds(new_duration);
|
|
m_expire_time = m_start_time + m_duration;
|
|
|
|
LogDynamicZones("Updated dz [{}] zone [{}]:[{}] seconds remaining: [{}]",
|
|
m_id, m_zone_id, m_instance_id, GetSecondsRemaining());
|
|
|
|
if (zone && IsCurrentZoneDz())
|
|
{
|
|
zone->SetInstanceTimer(GetSecondsRemaining());
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendClientInvite(Client* client, const std::string& inviter, const std::string& swap_name)
|
|
{
|
|
if (!client)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LogExpeditions("Invite [{}] to [{}] by [{}] swap [{}]", client->GetName(), GetID(), inviter, swap_name);
|
|
|
|
client->SetPendingDzInvite({ GetID(), inviter, swap_name });
|
|
client->MessageString(Chat::System, DZ_INVITED, GetLeaderName().c_str(), GetName().c_str());
|
|
|
|
// live (as of March 11 2020 patch) warns for lockouts added during current
|
|
// expedition that client would receive upon entering (sent in invite packet)
|
|
std::string events;
|
|
for (const auto& lockout : m_lockouts)
|
|
{
|
|
// live doesn't issue a warning for the dz's replay timer
|
|
if (!lockout.IsReplay() && !lockout.IsExpired() && lockout.IsUUID(GetUUID()) &&
|
|
!client->HasDzLockout(GetName(), lockout.Event()))
|
|
{
|
|
auto time = lockout.GetTimeRemainingStrs();
|
|
events += fmt::format("\n{} - {}D:{}H:{}M", lockout.Event(), time.days, time.hours, time.mins);
|
|
}
|
|
}
|
|
|
|
if (!events.empty())
|
|
{
|
|
client->SendColoredText(Chat::System, fmt::format(
|
|
"Warning! You will be given replay timers for the following events if you enter {}:{}", GetName(), events));
|
|
}
|
|
|
|
auto outapp = CreateInvitePacket(inviter, swap_name).release();
|
|
client->FastQueuePacket(&outapp);
|
|
}
|
|
|
|
bool DynamicZone::ConfirmLeaderCommand(Client* client)
|
|
{
|
|
if (!client)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!GetLeader().IsValid())
|
|
{
|
|
client->MessageString(Chat::Red, DZ_NO_LEADER_INFO); // unconfirmed message
|
|
return false;
|
|
}
|
|
|
|
if (GetLeaderID() != client->CharacterID())
|
|
{
|
|
client->MessageString(Chat::System, DZ_NOT_LEADER, GetLeaderName().c_str());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void DynamicZone::SendLeaderMessage(Client* leader, uint16_t type, const std::string& msg)
|
|
{
|
|
Client::SendCrossZoneMessage(leader, GetLeaderName(), type, msg);
|
|
}
|
|
|
|
void DynamicZone::SendLeaderMessage(Client* leader, uint16_t type, uint32_t str_id, std::initializer_list<std::string> args)
|
|
{
|
|
Client::SendCrossZoneMessageString(leader, GetLeaderName(), type, str_id, args);
|
|
}
|
|
|
|
bool DynamicZone::ProcessAddConflicts(Client* leader, Client* client, bool swapping)
|
|
{
|
|
if (!client) // a null leader client is handled by SendLeaderMessage fallback
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool has_conflict = false;
|
|
|
|
if (IsCurrentZoneDz())
|
|
{
|
|
SendLeaderMessage(leader, Chat::Red, DZADD_LEAVE_ZONE, { client->GetName() });
|
|
has_conflict = true;
|
|
}
|
|
|
|
auto dz_id = client->GetExpeditionID();
|
|
if (dz_id)
|
|
{
|
|
int string_id = dz_id == GetID() ? DZADD_ALREADY_PART : DZADD_ALREADY_OTHER;
|
|
SendLeaderMessage(leader, Chat::Red, string_id, { client->GetName() });
|
|
has_conflict = true;
|
|
}
|
|
|
|
// check any extra event lockouts for this expedition that the client has and expedition doesn't
|
|
auto lockouts = client->GetDzLockouts(GetName());
|
|
for (const auto& lockout : lockouts)
|
|
{
|
|
// client with a replay lockout is allowed only if the replay timer was from this expedition
|
|
if (lockout.IsReplay() && lockout.UUID() != m_uuid)
|
|
{
|
|
has_conflict = true;
|
|
|
|
auto time = lockout.GetTimeRemainingStrs();
|
|
SendLeaderMessage(leader, Chat::Red, DZADD_REPLAY_TIMER, { client->GetName(), time.days, time.hours, time.mins });
|
|
}
|
|
else if (!lockout.IsReplay() && !HasLockout(lockout.Event()))
|
|
{
|
|
has_conflict = true;
|
|
|
|
auto time = lockout.GetTimeRemainingStrs();
|
|
SendLeaderMessage(leader, Chat::Red, DZADD_EVENT_TIMER, { client->GetName(), lockout.Event(), time.days, time.hours, time.mins });
|
|
}
|
|
}
|
|
|
|
// member swapping integrity is handled by invite response
|
|
if (!swapping)
|
|
{
|
|
auto member_count = GetDatabaseMemberCount();
|
|
if (member_count == 0)
|
|
{
|
|
has_conflict = true;
|
|
}
|
|
else if (member_count >= GetMaxPlayers())
|
|
{
|
|
SendLeaderMessage(leader, Chat::Red, DZADD_EXCEED_MAX, { fmt::format_int(GetMaxPlayers()).str() });
|
|
has_conflict = true;
|
|
}
|
|
}
|
|
|
|
auto invite_id = client->GetPendingDzInviteID();
|
|
if (invite_id)
|
|
{
|
|
int string_id = invite_id == GetID() ? DZADD_PENDING : DZADD_PENDING_OTHER;
|
|
SendLeaderMessage(leader, Chat::Red, string_id, { client->GetName() });
|
|
has_conflict = true;
|
|
}
|
|
|
|
return has_conflict;
|
|
}
|
|
|
|
void DynamicZone::TryAddClient(Client* client, const std::string& inviter, const std::string& swap_name, Client* leader)
|
|
{
|
|
if (!client)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LogExpeditions("Adding [{}] to [{}] by [{}] swap [{}]", client->GetName(), GetID(), inviter, swap_name);
|
|
|
|
// null leader client handled by ProcessAddConflicts/SendLeaderMessage fallbacks
|
|
if (!leader)
|
|
{
|
|
leader = entity_list.GetClientByName(inviter.c_str());
|
|
}
|
|
|
|
bool has_conflicts = ProcessAddConflicts(leader, client, !swap_name.empty());
|
|
if (!has_conflicts)
|
|
{
|
|
// live uses the original unsanitized input string in invite messages
|
|
uint32_t string_id = swap_name.empty() ? DZADD_INVITE : DZSWAP_INVITE;
|
|
SendLeaderMessage(leader, Chat::Yellow, string_id, { client->GetName() });
|
|
SendClientInvite(client, inviter, swap_name);
|
|
}
|
|
else if (swap_name.empty()) // swap command doesn't result in this message
|
|
{
|
|
SendLeaderMessage(leader, Chat::Red, DZADD_INVITE_FAIL, { client->GetName() });
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzAddPlayer(Client* client, const std::string& add_name, const std::string& swap_name)
|
|
{
|
|
if (!client || !ConfirmLeaderCommand(client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool invite_failed = false;
|
|
|
|
if (IsLocked())
|
|
{
|
|
client->MessageString(Chat::Red, DZADD_NOT_ALLOWING);
|
|
invite_failed = true;
|
|
}
|
|
else if (add_name.empty())
|
|
{
|
|
client->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_name.c_str());
|
|
invite_failed = true;
|
|
}
|
|
else
|
|
{
|
|
auto member_data = GetMemberData(add_name);
|
|
if (member_data.IsValid())
|
|
{
|
|
// live prioritizes offline message before already a member message
|
|
if (member_data.status == DynamicZoneMemberStatus::Offline)
|
|
{
|
|
client->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_name.c_str());
|
|
}
|
|
else
|
|
{
|
|
client->MessageString(Chat::Red, DZADD_ALREADY_PART, add_name.c_str());
|
|
}
|
|
invite_failed = true;
|
|
}
|
|
}
|
|
|
|
if (invite_failed)
|
|
{
|
|
client->MessageString(Chat::Red, DZADD_INVITE_FAIL, FormatName(add_name).c_str());
|
|
return;
|
|
}
|
|
|
|
if (Client* add_client = entity_list.GetClientByName(add_name.c_str()))
|
|
{
|
|
// client is online in this zone
|
|
TryAddClient(add_client, client->GetName(), swap_name, client);
|
|
}
|
|
else
|
|
{
|
|
// forward to world to check if client is online and perform cross-zone invite
|
|
SendWorldPlayerInvite(client->GetName(), swap_name, FormatName(add_name));
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzAddPlayerContinue(std::string inviter, std::string add_name, std::string swap_name)
|
|
{
|
|
// continuing expedition invite from leader in another zone
|
|
if (Client* add_client = entity_list.GetClientByName(add_name.c_str()))
|
|
{
|
|
TryAddClient(add_client, inviter, swap_name);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzInviteResponse(Client* client, bool accepted, const std::string& swap_name)
|
|
{
|
|
if (!client)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LogExpeditions("[{}] accepted invite [{}] swap [{}]", client->GetName(), accepted, swap_name);
|
|
|
|
// a null leader client is handled by SendLeaderMessage fallbacks
|
|
// note current leader receives invite reply messages (if leader changed)
|
|
Client* leader = entity_list.GetClientByCharID(GetLeaderID());
|
|
|
|
if (!accepted)
|
|
{
|
|
SendLeaderMessage(leader, Chat::Red, DZ_INVITE_DECLINED, { client->GetName() });
|
|
return;
|
|
}
|
|
|
|
bool is_swap = !swap_name.empty();
|
|
bool has_conflicts = IsLocked();
|
|
|
|
if (IsLocked())
|
|
{
|
|
SendLeaderMessage(leader, Chat::Red, DZADD_NOT_ALLOWING);
|
|
}
|
|
else
|
|
{
|
|
has_conflicts = ProcessAddConflicts(leader, client, is_swap);
|
|
}
|
|
|
|
// error if swapping and character was already removed before the accept
|
|
if (is_swap)
|
|
{
|
|
auto swap_member = GetMemberData(swap_name);
|
|
if (!swap_member.IsValid() || !HasDatabaseMember(swap_member.id))
|
|
{
|
|
has_conflicts = true;
|
|
}
|
|
}
|
|
|
|
if (has_conflicts)
|
|
{
|
|
SendLeaderMessage(leader, Chat::Red, DZ_INVITE_ERROR, { client->GetName() });
|
|
}
|
|
else
|
|
{
|
|
SendLeaderMessage(leader, Chat::Yellow, DZ_INVITE_ACCEPTED, { client->GetName() });
|
|
|
|
// replay timers are optionally added to new members on join with fresh expire time
|
|
if (m_add_replay)
|
|
{
|
|
auto it = std::ranges::find_if(m_lockouts, [&](const auto& l) { return l.IsReplay(); });
|
|
if (it != m_lockouts.end() && it->IsUUID(GetUUID()) && !client->HasDzLockout(GetName(), DzLockout::ReplayTimer))
|
|
{
|
|
DzLockout replay_timer = *it; // copy
|
|
replay_timer.Reset();
|
|
client->AddDzLockout(replay_timer, true);
|
|
}
|
|
}
|
|
|
|
DynamicZoneMember add_member(client->CharacterID(), client->GetName(), DynamicZoneMemberStatus::Online);
|
|
|
|
bool success = is_swap ? SwapMember(add_member, swap_name) : AddMember(add_member);
|
|
if (success)
|
|
{
|
|
SendLeaderMessage(leader, Chat::Yellow, DZ_ADDED, { client->GetName(), GetName() });
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzMakeLeader(Client* client, std::string leader_name)
|
|
{
|
|
if (!client || !ConfirmLeaderCommand(client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (leader_name.empty())
|
|
{
|
|
client->MessageString(Chat::Red, DZ_LEADER_OFFLINE, leader_name.c_str());
|
|
return;
|
|
}
|
|
|
|
// leader can only be changed by world
|
|
SendWorldMakeLeaderRequest(client->CharacterID(), FormatName(leader_name));
|
|
}
|
|
|
|
void DynamicZone::DzRemovePlayer(Client* client, std::string name)
|
|
{
|
|
if (!client || !ConfirmLeaderCommand(client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// live only seems to enforce min_players for requesting expeditions, no need to check here
|
|
// note: on live members removed when inside a dz instance remain "temporary"
|
|
// members for kick timer duration and still receive lockouts across zones (unimplemented)
|
|
bool removed = RemoveMember(name);
|
|
if (!removed)
|
|
{
|
|
client->MessageString(Chat::Red, DZ_NOT_MEMBER, FormatName(name).c_str());
|
|
}
|
|
else
|
|
{
|
|
client->MessageString(Chat::Yellow, DZ_REMOVED, FormatName(name).c_str(), m_name.c_str());
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzQuit(Client* client)
|
|
{
|
|
if (client)
|
|
{
|
|
RemoveMember(client->GetName());
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzSwapPlayer(Client* client, std::string rem_name, std::string add_name)
|
|
{
|
|
if (!client || !ConfirmLeaderCommand(client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (rem_name.empty() || !HasMember(rem_name))
|
|
{
|
|
client->MessageString(Chat::Red, DZSWAP_CANNOT_REMOVE, FormatName(rem_name).c_str());
|
|
return;
|
|
}
|
|
|
|
DzAddPlayer(client, add_name, rem_name);
|
|
}
|
|
|
|
void DynamicZone::DzPlayerList(Client* client)
|
|
{
|
|
if (client)
|
|
{
|
|
client->MessageString(Chat::Yellow, DZ_LEADER, GetLeaderName().c_str());
|
|
|
|
std::vector<std::string> names;
|
|
for (const auto& member : m_members)
|
|
{
|
|
names.push_back(member.name);
|
|
}
|
|
|
|
client->MessageString(Chat::Yellow, DZ_MEMBERS, fmt::format("{}", fmt::join(names, ", ")).c_str());
|
|
}
|
|
}
|
|
|
|
void DynamicZone::DzKickPlayers(Client* client)
|
|
{
|
|
if (!client || !ConfirmLeaderCommand(client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveAllMembers();
|
|
client->MessageString(Chat::Red, DZ_REMOVED, "Everyone", m_name.c_str());
|
|
}
|
|
|
|
void DynamicZone::HandleWorldMessage(ServerPacket* pack)
|
|
{
|
|
switch (pack->opcode)
|
|
{
|
|
case ServerOP_DzCreated:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzCreate_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->origin_zone_id, buf->origin_instance_id))
|
|
{
|
|
DynamicZone::CacheNewDynamicZone(pack);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzDeleted:
|
|
{
|
|
// sent by world when it deletes an expired or empty dz
|
|
// any system that held a reference to the dz should have already been notified
|
|
auto buf = reinterpret_cast<ServerDzID_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (zone && dz)
|
|
{
|
|
LogDynamicZonesDetail("Deleting dynamic zone [{}] from zone cache", buf->dz_id);
|
|
dz->SendUpdatesToZoneMembers(true, true); // members silently removed
|
|
zone->dynamic_zone_cache.erase(buf->dz_id);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzAddPlayer:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzCommand_Struct*>(pack->pBuffer);
|
|
if (buf->is_char_online)
|
|
{
|
|
if (auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
|
|
{
|
|
dz->DzAddPlayerContinue(buf->requester_name, buf->target_name, buf->remove_name);
|
|
}
|
|
}
|
|
else if (Client* leader = entity_list.GetClientByName(buf->requester_name))
|
|
{
|
|
std::string target_name = FormatName(buf->target_name);
|
|
leader->MessageString(Chat::Red, DZADD_NOT_ONLINE, target_name.c_str());
|
|
leader->MessageString(Chat::Red, DZADD_INVITE_FAIL, target_name.c_str());
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzMakeLeader:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzCommandMakeLeader_Struct*>(pack->pBuffer);
|
|
auto old_leader = entity_list.GetClientByCharID(buf->requester_id);
|
|
|
|
// success flag is set by world to indicate new leader set to an online member
|
|
if (old_leader && buf->is_success)
|
|
{
|
|
old_leader->MessageString(Chat::Yellow, DZ_LEADER_NAME, buf->new_leader_name);
|
|
}
|
|
else if (old_leader)
|
|
{
|
|
uint32_t str_id = buf->is_online ? DZ_NOT_MEMBER : DZ_LEADER_OFFLINE;
|
|
old_leader->MessageString(Chat::Red, str_id, buf->new_leader_name);
|
|
}
|
|
|
|
if (buf->is_success && !RuleB(Expedition, AlwaysNotifyNewLeaderOnChange))
|
|
{
|
|
if (auto new_leader = entity_list.GetClientByName(buf->new_leader_name))
|
|
{
|
|
new_leader->MessageString(Chat::Yellow, DZ_LEADER_YOU);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzAddRemoveMember:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzMember_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
auto status = static_cast<DynamicZoneMemberStatus>(buf->character_status);
|
|
dz->ProcessMemberAddRemove({ buf->character_id, buf->character_name, status }, buf->removed);
|
|
}
|
|
}
|
|
|
|
if (zone && zone->IsZone(buf->dz_zone_id, buf->dz_instance_id))
|
|
{
|
|
// cache independent redundancy to kick removed members from dz's instance
|
|
Client* client = entity_list.GetClientByCharID(buf->character_id);
|
|
if (client)
|
|
{
|
|
client->SetDzRemovalTimer(buf->removed);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzSwapMembers:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzMemberSwap_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
auto status = static_cast<DynamicZoneMemberStatus>(buf->add_character_status);
|
|
dz->ProcessMemberAddRemove({ buf->remove_character_id, buf->remove_character_name }, true);
|
|
dz->ProcessMemberAddRemove({ buf->add_character_id, buf->add_character_name, status }, false);
|
|
}
|
|
}
|
|
|
|
if (zone && zone->IsZone(buf->dz_zone_id, buf->dz_instance_id))
|
|
{
|
|
// cache independent redundancy to kick removed members from dz's instance
|
|
Client* removed_client = entity_list.GetClientByCharID(buf->remove_character_id);
|
|
if (removed_client)
|
|
{
|
|
removed_client->SetDzRemovalTimer(true);
|
|
}
|
|
|
|
Client* added_client = entity_list.GetClientByCharID(buf->add_character_id);
|
|
if (added_client)
|
|
{
|
|
added_client->SetDzRemovalTimer(false);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzRemoveAllMembers:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzID_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
dz->ProcessRemoveAllMembers();
|
|
}
|
|
}
|
|
|
|
if (zone && zone->IsZone(buf->dz_zone_id, buf->dz_instance_id))
|
|
{
|
|
// cache independent redundancy to kick removed members from dz's instance
|
|
DynamicZone::StartAllClientRemovalTimers();
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzDurationUpdate:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzSetDuration_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
dz->SetUpdatedDuration(buf->seconds);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzSetCompass:
|
|
case ServerOP_DzSetSafeReturn:
|
|
case ServerOP_DzSetZoneIn:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzLocation_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
if (pack->opcode == ServerOP_DzSetCompass)
|
|
{
|
|
dz->SetCompass(buf->zone_id, buf->x, buf->y, buf->z, false);
|
|
}
|
|
else if (pack->opcode == ServerOP_DzSetSafeReturn)
|
|
{
|
|
dz->SetSafeReturn(buf->zone_id, buf->x, buf->y, buf->z, buf->heading, false);
|
|
}
|
|
else if (pack->opcode == ServerOP_DzSetZoneIn)
|
|
{
|
|
dz->SetZoneInLocation(buf->x, buf->y, buf->z, buf->heading, false);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzSetSwitchID:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzSwitchID_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
dz->ProcessSetSwitchID(buf->dz_switch_id);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzGetMemberStatuses:
|
|
{
|
|
// reply from world for online member statuses request for async zone member updates
|
|
auto buf = reinterpret_cast<ServerDzMemberStatuses_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
for (uint32_t i = 0; i < buf->count; ++i)
|
|
{
|
|
auto status = static_cast<DynamicZoneMemberStatus>(buf->entries[i].online_status);
|
|
dz->SetInternalMemberStatus(buf->entries[i].character_id, status);
|
|
}
|
|
dz->m_has_member_statuses = true;
|
|
dz->SendUpdatesToZoneMembers(false, true);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzGetBulkMemberStatuses:
|
|
{
|
|
if (zone)
|
|
{
|
|
std::vector<std::pair<uint32_t, std::vector<DynamicZoneMember>>> dzs;
|
|
dzs.reserve(zone->dynamic_zone_cache.size());
|
|
|
|
auto buf = reinterpret_cast<ServerDzCerealData_Struct*>(pack->pBuffer);
|
|
EQ::Util::MemoryStreamReader ss(buf->cereal_data, buf->cereal_size);
|
|
{
|
|
cereal::BinaryInputArchive archive(ss);
|
|
archive(dzs);
|
|
}
|
|
|
|
for (const auto& [dz_id, members] : dzs)
|
|
{
|
|
if (auto dz = DynamicZone::FindDynamicZoneByID(dz_id))
|
|
{
|
|
for (const auto& member : members)
|
|
{
|
|
dz->SetInternalMemberStatus(member.id, member.status);
|
|
}
|
|
dz->m_has_member_statuses = true;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzUpdateMemberStatus:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzMemberStatus_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
auto status = static_cast<DynamicZoneMemberStatus>(buf->status);
|
|
dz->ProcessMemberStatusChange(buf->character_id, status);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzLeaderChanged:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzLeaderID_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
dz->ProcessLeaderChanged(buf->leader_id);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzExpireWarning:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzExpireWarning_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
dz->SendMembersExpireWarning(buf->minutes_remaining);
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzMovePC:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzMovePC_Struct*>(pack->pBuffer);
|
|
auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id);
|
|
if (dz)
|
|
{
|
|
Client* client = entity_list.GetClientByCharID(buf->character_id);
|
|
if (client)
|
|
{
|
|
dz->MovePCInto(client, false);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzLock:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzLock_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
if (auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
|
|
{
|
|
dz->SetLocked(buf->lock, false, static_cast<DzLockMsg>(buf->lock_msg), buf->color);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzReplayOnJoin:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzBool_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
if (auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
|
|
{
|
|
dz->SetReplayOnJoin(buf->enabled);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzLockout:
|
|
case ServerOP_DzLockoutDuration:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzLockout_Struct*>(pack->pBuffer);
|
|
if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id))
|
|
{
|
|
if (DynamicZone* dz = DynamicZone::FindDynamicZoneByID(buf->dz_id))
|
|
{
|
|
DzLockout lockout{ dz->GetUUID(), dz->GetName(), buf->event_name, buf->expire_time, buf->duration };
|
|
if (pack->opcode == ServerOP_DzLockout)
|
|
{
|
|
dz->HandleLockoutUpdate(lockout, buf->remove, buf->members_only);
|
|
}
|
|
else if (pack->opcode == ServerOP_DzLockoutDuration)
|
|
{
|
|
dz->HandleLockoutDuration(lockout, buf->seconds, buf->members_only, false);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ServerOP_DzCharacterLockout:
|
|
{
|
|
auto buf = reinterpret_cast<ServerDzCharacterLockout_Struct*>(pack->pBuffer);
|
|
if (Client* client = entity_list.GetClientByCharID(buf->char_id))
|
|
{
|
|
if (!buf->remove)
|
|
{
|
|
client->AddDzLockout(DzLockout{ buf->uuid, buf->expedition, buf->event, buf->expire_time, buf->duration });
|
|
}
|
|
else if (buf->event[0] != '\0')
|
|
{
|
|
client->RemoveDzLockout(buf->expedition, buf->event);
|
|
}
|
|
else
|
|
{
|
|
client->RemoveDzLockouts(buf->expedition);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateExpireWarningPacket(uint32_t minutes_remaining)
|
|
{
|
|
uint32_t outsize = sizeof(ExpeditionExpireWarning);
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzExpeditionEndsWarning, outsize);
|
|
auto buf = reinterpret_cast<ExpeditionExpireWarning*>(outapp->pBuffer);
|
|
buf->minutes_remaining = minutes_remaining;
|
|
return outapp;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateInfoPacket(bool clear)
|
|
{
|
|
constexpr uint32_t outsize = sizeof(DynamicZoneInfo_Struct);
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzExpeditionInfo, outsize);
|
|
if (!clear)
|
|
{
|
|
auto info = reinterpret_cast<DynamicZoneInfo_Struct*>(outapp->pBuffer);
|
|
info->assigned = true;
|
|
strn0cpy(info->dz_name, m_name.c_str(), sizeof(info->dz_name));
|
|
strn0cpy(info->leader_name, m_leader.name.c_str(), sizeof(info->leader_name));
|
|
info->max_players = m_max_players;
|
|
}
|
|
return outapp;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateMemberListPacket(bool clear)
|
|
{
|
|
uint32_t member_count = clear ? 0 : static_cast<uint32_t>(m_members.size());
|
|
uint32_t member_entries_size = sizeof(DynamicZoneMemberEntry_Struct) * member_count;
|
|
uint32_t outsize = sizeof(DynamicZoneMemberList_Struct) + member_entries_size;
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzMemberList, outsize);
|
|
auto buf = reinterpret_cast<DynamicZoneMemberList_Struct*>(outapp->pBuffer);
|
|
|
|
buf->member_count = member_count;
|
|
|
|
if (!clear)
|
|
{
|
|
for (auto i = 0; i < m_members.size(); ++i)
|
|
{
|
|
strn0cpy(buf->members[i].name, m_members[i].name.c_str(), sizeof(buf->members[i].name));
|
|
buf->members[i].online_status = static_cast<uint8_t>(m_members[i].status);
|
|
}
|
|
}
|
|
|
|
return outapp;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateMemberNamePacket(const std::string& name, bool remove)
|
|
{
|
|
constexpr uint32_t outsize = sizeof(DynamicZoneMemberListName_Struct);
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzMemberListName, outsize);
|
|
auto buf = reinterpret_cast<DynamicZoneMemberListName_Struct*>(outapp->pBuffer);
|
|
buf->add_name = !remove;
|
|
strn0cpy(buf->name, name.c_str(), sizeof(buf->name));
|
|
return outapp;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateMemberStatusPacket(const std::string& name, DynamicZoneMemberStatus status)
|
|
{
|
|
// member list status uses member list struct with a single entry
|
|
constexpr uint32_t outsize = sizeof(DynamicZoneMemberList_Struct) + sizeof(DynamicZoneMemberEntry_Struct);
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzMemberListStatus, outsize);
|
|
auto buf = reinterpret_cast<DynamicZoneMemberList_Struct*>(outapp->pBuffer);
|
|
buf->member_count = 1;
|
|
|
|
auto entry = static_cast<DynamicZoneMemberEntry_Struct*>(buf->members);
|
|
strn0cpy(entry->name, name.c_str(), sizeof(entry->name));
|
|
entry->online_status = static_cast<uint8_t>(status);
|
|
|
|
return outapp;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateLeaderNamePacket()
|
|
{
|
|
constexpr uint32_t outsize = sizeof(DynamicZoneLeaderName_Struct);
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzSetLeaderName, outsize);
|
|
auto buf = reinterpret_cast<DynamicZoneLeaderName_Struct*>(outapp->pBuffer);
|
|
strn0cpy(buf->leader_name, m_leader.name.c_str(), sizeof(buf->leader_name));
|
|
return outapp;
|
|
}
|
|
|
|
void DynamicZone::ProcessCompassChange(const DynamicZoneLocation& location)
|
|
{
|
|
DynamicZoneBase::ProcessCompassChange(location);
|
|
SendCompassUpdateToZoneMembers();
|
|
}
|
|
|
|
void DynamicZone::SendCompassUpdateToZoneMembers()
|
|
{
|
|
for (const auto& member : m_members)
|
|
{
|
|
Client* member_client = entity_list.GetClientByCharID(member.id);
|
|
if (member_client)
|
|
{
|
|
member_client->SendDzCompassUpdate();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::ProcessSetSwitchID(int dz_switch_id)
|
|
{
|
|
DynamicZoneBase::ProcessSetSwitchID(dz_switch_id);
|
|
SendCompassUpdateToZoneMembers();
|
|
}
|
|
|
|
void DynamicZone::SendLeaderNameToZoneMembers()
|
|
{
|
|
auto outapp_leader = CreateLeaderNamePacket();
|
|
|
|
for (const auto& member : m_members)
|
|
{
|
|
Client* member_client = entity_list.GetClientByCharID(member.id);
|
|
if (member_client)
|
|
{
|
|
member_client->QueuePacket(outapp_leader.get());
|
|
|
|
if (member.id == m_leader.id && RuleB(Expedition, AlwaysNotifyNewLeaderOnChange))
|
|
{
|
|
member_client->MessageString(Chat::Yellow, DZ_LEADER_YOU);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendMembersExpireWarning(uint32_t minutes)
|
|
{
|
|
// expeditions warn members in all zones not just the dz
|
|
auto outapp = CreateExpireWarningPacket(minutes);
|
|
for (const auto& member : GetMembers())
|
|
{
|
|
Client* client = entity_list.GetClientByCharID(member.id);
|
|
if (client)
|
|
{
|
|
client->QueuePacket(outapp.get());
|
|
|
|
// live doesn't actually send the chat message with it
|
|
client->MessageString(Chat::Yellow, DZ_MINUTES_REMAIN, fmt::format_int(minutes).c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendMemberListToZoneMembers()
|
|
{
|
|
auto outapp_members = CreateMemberListPacket(false);
|
|
|
|
for (const auto& member : m_members)
|
|
{
|
|
Client* member_client = entity_list.GetClientByCharID(member.id);
|
|
if (member_client)
|
|
{
|
|
member_client->QueuePacket(outapp_members.get());
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendMemberNameToZoneMembers(const std::string& char_name, bool remove)
|
|
{
|
|
auto outapp = CreateMemberNamePacket(char_name, remove);
|
|
|
|
for (const auto& member : m_members)
|
|
{
|
|
Client* member_client = entity_list.GetClientByCharID(member.id);
|
|
if (member_client)
|
|
{
|
|
member_client->QueuePacket(outapp.get());
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendMemberStatusToZoneMembers(const DynamicZoneMember& update)
|
|
{
|
|
auto outapp = CreateMemberStatusPacket(update.name, update.status);
|
|
|
|
for (const auto& member : m_members)
|
|
{
|
|
Client* member_client = entity_list.GetClientByCharID(member.id);
|
|
if (member_client)
|
|
{
|
|
member_client->QueuePacket(outapp.get());
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendClientWindowUpdate(Client* client)
|
|
{
|
|
if (client)
|
|
{
|
|
client->QueuePacket(CreateInfoPacket().get());
|
|
client->QueuePacket(CreateMemberListPacket().get());
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SendUpdatesToZoneMembers(bool removing_all, bool silent)
|
|
{
|
|
// performs a full update on all members (usually for dz creation or removing all)
|
|
if (!HasMembers())
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> outapp_info = nullptr;
|
|
std::unique_ptr<EQApplicationPacket> outapp_members = nullptr;
|
|
|
|
// only expeditions use the dz window. on live the window is filled by non
|
|
// expeditions when first created but never kept updated. that behavior could
|
|
// be replicated in the future by flagging this as a creation update
|
|
if (m_type == DynamicZoneType::Expedition)
|
|
{
|
|
// clearing info also clears member list, no need to send both when removing
|
|
outapp_info = CreateInfoPacket(removing_all);
|
|
outapp_members = removing_all ? nullptr : CreateMemberListPacket();
|
|
}
|
|
|
|
for (const auto& member : GetMembers())
|
|
{
|
|
Client* client = entity_list.GetClientByCharID(member.id);
|
|
if (client)
|
|
{
|
|
if (removing_all) {
|
|
client->RemoveDynamicZoneID(GetID());
|
|
} else {
|
|
client->AddDynamicZoneID(GetID());
|
|
}
|
|
|
|
client->SendDzCompassUpdate();
|
|
|
|
if (outapp_info)
|
|
{
|
|
client->QueuePacket(outapp_info.get());
|
|
}
|
|
|
|
if (outapp_members)
|
|
{
|
|
client->QueuePacket(outapp_members.get());
|
|
}
|
|
|
|
if (m_type == DynamicZoneType::Expedition && removing_all && !silent)
|
|
{
|
|
client->MessageString(Chat::Yellow, DZ_REMOVED, client->GetCleanName(), GetName().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::ProcessMemberAddRemove(const DynamicZoneMember& member, bool removed)
|
|
{
|
|
DynamicZoneBase::ProcessMemberAddRemove(member, removed);
|
|
|
|
// the affected client always gets a full compass update. for expeditions
|
|
// client also gets window info update and all members get a member list update
|
|
Client* client = entity_list.GetClientByCharID(member.id);
|
|
if (client)
|
|
{
|
|
if (!removed) {
|
|
client->AddDynamicZoneID(GetID());
|
|
} else {
|
|
client->RemoveDynamicZoneID(GetID());
|
|
}
|
|
|
|
client->SendDzCompassUpdate();
|
|
|
|
if (m_type == DynamicZoneType::Expedition)
|
|
{
|
|
// sending clear info also clears member list for removed members
|
|
client->QueuePacket(CreateInfoPacket(removed).get());
|
|
client->MessageString(Chat::Yellow, removed ? DZ_REMOVED : DZ_ADDED, client->GetCleanName(), GetName().c_str());
|
|
}
|
|
}
|
|
|
|
if (m_type == DynamicZoneType::Expedition)
|
|
{
|
|
// send full list when adding (MemberListName adds with "unknown" status)
|
|
if (!removed) {
|
|
SendMemberListToZoneMembers();
|
|
} else {
|
|
SendMemberNameToZoneMembers(member.name, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::ProcessRemoveAllMembers()
|
|
{
|
|
SendUpdatesToZoneMembers(true, false);
|
|
DynamicZoneBase::ProcessRemoveAllMembers();
|
|
}
|
|
|
|
void DynamicZone::UpdateMembers()
|
|
{
|
|
// gets member statuses from world and performs zone member updates on reply
|
|
// if we've already received member statuses we can just update immediately
|
|
if (m_has_member_statuses)
|
|
{
|
|
SendUpdatesToZoneMembers();
|
|
return;
|
|
}
|
|
|
|
constexpr uint32_t pack_size = sizeof(ServerDzID_Struct);
|
|
auto pack = std::make_unique<ServerPacket>(ServerOP_DzGetMemberStatuses, pack_size);
|
|
auto buf = reinterpret_cast<ServerDzID_Struct*>(pack->pBuffer);
|
|
buf->dz_id = GetID();
|
|
buf->sender_zone_id = zone ? zone->GetZoneID() : 0;
|
|
buf->sender_instance_id = zone ? zone->GetInstanceID() : 0;
|
|
worldserver.SendPacket(pack.get());
|
|
}
|
|
|
|
bool DynamicZone::ProcessMemberStatusChange(uint32_t character_id, DynamicZoneMemberStatus status)
|
|
{
|
|
bool changed = DynamicZoneBase::ProcessMemberStatusChange(character_id, status);
|
|
|
|
if (changed && m_type == DynamicZoneType::Expedition)
|
|
{
|
|
auto member = GetMemberData(character_id);
|
|
if (member.IsValid())
|
|
{
|
|
SendMemberStatusToZoneMembers(member);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void DynamicZone::ProcessLeaderChanged(uint32_t new_leader_id)
|
|
{
|
|
auto new_leader = GetMemberData(new_leader_id);
|
|
if (!new_leader.IsValid())
|
|
{
|
|
LogDynamicZones("Processed invalid new leader id [{}] for dz [{}]", new_leader_id, m_id);
|
|
return;
|
|
}
|
|
|
|
LogDynamicZones("Replaced [{}] leader [{}] with [{}]", m_id, GetLeaderName(), new_leader.name);
|
|
|
|
SetLeader(new_leader);
|
|
if (GetType() == DynamicZoneType::Expedition)
|
|
{
|
|
SendLeaderNameToZoneMembers();
|
|
}
|
|
}
|
|
|
|
bool DynamicZone::CanClientLootCorpse(Client* client, uint32_t npc_type_id, uint32_t entity_id)
|
|
{
|
|
// non-members of a dz cannot loot corpses inside the dz
|
|
if (!HasMember(client->CharacterID()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// expeditions may prevent looting based on client's lockouts
|
|
if (GetType() == DynamicZoneType::Expedition)
|
|
{
|
|
// entity id takes priority, falls back to checking by npc type if not set
|
|
std::string event = GetLootEvent(entity_id, DzLootEvent::Type::Entity);
|
|
if (event.empty())
|
|
{
|
|
event = GetLootEvent(npc_type_id, DzLootEvent::Type::NpcType);
|
|
}
|
|
|
|
if (!event.empty())
|
|
{
|
|
auto lockout = client->GetDzLockout(GetName(), event);
|
|
if (!lockout || lockout->UUID() != m_uuid)
|
|
{
|
|
// client lockout not received in this expedition, prevent looting
|
|
LogExpeditions("Character [{}] denied looting npc [{}] for [{}]", client->CharacterID(), npc_type_id, event);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void DynamicZone::MovePCInto(Client* client, bool world_verify) const
|
|
{
|
|
if (!world_verify)
|
|
{
|
|
DynamicZoneLocation zonein = GetZoneInLocation();
|
|
ZoneMode zone_mode = HasZoneInLocation() ? ZoneMode::ZoneSolicited : ZoneMode::ZoneToSafeCoords;
|
|
client->MovePC(GetZoneID(), GetInstanceID(), zonein.x, zonein.y, zonein.z, zonein.heading, 0, zone_mode);
|
|
}
|
|
else
|
|
{
|
|
ServerPacket pack(ServerOP_DzMovePC, sizeof(ServerDzMovePC_Struct));
|
|
auto buf = reinterpret_cast<ServerDzMovePC_Struct*>(pack.pBuffer);
|
|
buf->dz_id = GetID();
|
|
buf->sender_zone_id = static_cast<uint16_t>(zone->GetZoneID());
|
|
buf->sender_instance_id = static_cast<uint16_t>(zone->GetInstanceID());
|
|
buf->character_id = client->CharacterID();
|
|
worldserver.SendPacket(&pack);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SetLocked(bool lock, bool update_db, DzLockMsg lock_msg, uint32_t color)
|
|
{
|
|
DynamicZoneBase::SetLocked(lock, update_db, lock_msg, color);
|
|
|
|
if (m_is_locked && lock_msg != DzLockMsg::None && IsCurrentZoneDz())
|
|
{
|
|
auto msg = lock_msg == DzLockMsg::Close ? LockClose : LockBegin;
|
|
for (const auto& it : entity_list.GetClientList())
|
|
{
|
|
if (it.second)
|
|
{
|
|
it.second->Message(color, msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SaveLockouts(const std::vector<DzLockout>& lockouts)
|
|
{
|
|
m_lockouts = lockouts;
|
|
DynamicZoneLockoutsRepository::InsertLockouts(database, m_id, m_lockouts);
|
|
}
|
|
|
|
static void SendWorldCharacterLockout(uint32_t char_id, const DzLockout& lockout, bool remove)
|
|
{
|
|
uint32_t pack_size = sizeof(ServerDzCharacterLockout_Struct);
|
|
ServerPacket pack(ServerOP_DzCharacterLockout, pack_size);
|
|
auto buf = reinterpret_cast<ServerDzCharacterLockout_Struct*>(pack.pBuffer);
|
|
buf->remove = remove;
|
|
buf->char_id = char_id;
|
|
buf->expire_time = lockout.GetExpireTime();
|
|
buf->duration = lockout.GetDuration();
|
|
strn0cpy(buf->uuid, lockout.UUID().c_str(), sizeof(buf->uuid));
|
|
strn0cpy(buf->expedition, lockout.DzName().c_str(), sizeof(buf->expedition));
|
|
strn0cpy(buf->event, lockout.Event().c_str(), sizeof(buf->event));
|
|
worldserver.SendPacket(&pack);
|
|
}
|
|
|
|
void DynamicZone::AddCharacterLockout(
|
|
uint32_t char_id, const std::string& expedition, const std::string& event, uint32_t seconds, const std::string& uuid)
|
|
{
|
|
if (char_id)
|
|
{
|
|
auto lockout = DzLockout::Create(expedition, event, seconds, uuid);
|
|
CharacterExpeditionLockoutsRepository::InsertLockouts(database, char_id, { lockout });
|
|
SendWorldCharacterLockout(char_id, lockout, false);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::AddCharacterLockout(
|
|
const std::string& char_name, const std::string& expedition, const std::string& event, uint32_t seconds, const std::string& uuid)
|
|
{
|
|
if (!char_name.empty())
|
|
{
|
|
uint32_t char_id = database.GetCharacterID(char_name);
|
|
AddCharacterLockout(char_id, expedition, event, seconds, uuid);
|
|
}
|
|
}
|
|
|
|
bool DynamicZone::HasCharacterLockout(uint32_t char_id, const std::string& expedition, const std::string& event)
|
|
{
|
|
auto lockouts = GetCharacterLockouts(char_id);
|
|
return std::any_of(lockouts.begin(), lockouts.end(),
|
|
[&](const auto& l) { return !l.IsExpired() && l.IsSame(expedition, event); });
|
|
}
|
|
|
|
bool DynamicZone::HasCharacterLockout(const std::string& char_name, const std::string& expedition, const std::string& event)
|
|
{
|
|
if (!char_name.empty())
|
|
{
|
|
return HasCharacterLockout(database.GetCharacterID(char_name), expedition, event);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void DynamicZone::RemoveCharacterLockouts(uint32_t char_id, const std::string& expedition, const std::string& event)
|
|
{
|
|
if (char_id)
|
|
{
|
|
std::string where = fmt::format("character_id = {}", char_id);
|
|
if (!event.empty())
|
|
{
|
|
where += fmt::format(" AND expedition_name = '{}' AND event_name = '{}'", Strings::Escape(expedition), Strings::Escape(event));
|
|
}
|
|
else if (!expedition.empty())
|
|
{
|
|
where += fmt::format(" AND expedition_name = '{}'", Strings::Escape(expedition));
|
|
}
|
|
CharacterExpeditionLockoutsRepository::DeleteWhere(database, where);
|
|
|
|
DzLockout lockout{{}, expedition, event, 0, 0};
|
|
SendWorldCharacterLockout(char_id, lockout, true);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::RemoveCharacterLockouts(const std::string& char_name, const std::string& expedition, const std::string& event)
|
|
{
|
|
if (!char_name.empty())
|
|
{
|
|
uint32_t char_id = database.GetCharacterID(char_name);
|
|
RemoveCharacterLockouts(char_id, expedition, event);
|
|
}
|
|
}
|
|
|
|
std::vector<DzLockout> DynamicZone::GetCharacterLockouts(uint32_t char_id)
|
|
{
|
|
std::vector<DzLockout> lockouts;
|
|
if (char_id == 0)
|
|
{
|
|
return lockouts;
|
|
}
|
|
|
|
auto client = entity_list.GetClientByCharID(char_id);
|
|
if (client)
|
|
{
|
|
lockouts = client->GetDzLockouts();
|
|
}
|
|
else
|
|
{
|
|
lockouts = CharacterExpeditionLockoutsRepository::GetLockouts(database, char_id);
|
|
}
|
|
|
|
return lockouts;
|
|
}
|
|
|
|
void DynamicZone::AddClientsLockout(const DzLockout& lockout)
|
|
{
|
|
std::vector<uint32_t> char_ids;
|
|
for (const auto& it : entity_list.GetClientList())
|
|
{
|
|
char_ids.push_back(it.second->CharacterID());
|
|
it.second->AddDzLockout(lockout);
|
|
}
|
|
|
|
if (!char_ids.empty())
|
|
{
|
|
CharacterExpeditionLockoutsRepository::InsertLockout(database, char_ids, lockout);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::HandleLockoutUpdate(const DzLockout& lockout, bool remove, bool members_only)
|
|
{
|
|
DynamicZoneBase::HandleLockoutUpdate(lockout, remove, members_only);
|
|
|
|
std::vector<uint32_t> char_ids;
|
|
for (const auto& it : entity_list.GetClientList())
|
|
{
|
|
Client* client = it.second;
|
|
if (std::ranges::any_of(m_members, [&](const auto& m) { return m.id == client->CharacterID(); }))
|
|
{
|
|
if (!remove)
|
|
{
|
|
client->AddDzLockout(lockout);
|
|
}
|
|
else
|
|
{
|
|
client->RemoveDzLockout(GetName(), lockout.Event());
|
|
}
|
|
}
|
|
else if (!remove && IsCurrentZoneDz()) // non-member client inside the dz
|
|
{
|
|
// all clients inside the dz instance receive added lockouts to avoid exploits
|
|
// where members quit the expedition but haven't been kicked from zone yet
|
|
char_ids.push_back(client->CharacterID());
|
|
client->AddDzLockout(lockout);
|
|
}
|
|
}
|
|
|
|
if (!char_ids.empty())
|
|
{
|
|
CharacterExpeditionLockoutsRepository::InsertLockout(database, char_ids, lockout);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::HandleLockoutDuration(const DzLockout& lockout, int seconds, bool members_only, bool insert_db)
|
|
{
|
|
DynamicZoneBase::HandleLockoutDuration(lockout, seconds, members_only, insert_db);
|
|
|
|
std::vector<uint32_t> char_ids;
|
|
for (const auto& it : entity_list.GetClientList())
|
|
{
|
|
Client* client = it.second;
|
|
if (std::ranges::any_of(m_members, [&](const auto& m) { return m.id == client->CharacterID(); }))
|
|
{
|
|
client->AddDzLockoutDuration(lockout, seconds, m_uuid);
|
|
}
|
|
else if (IsCurrentZoneDz()) // non-member client inside the dz
|
|
{
|
|
char_ids.push_back(client->CharacterID());
|
|
client->AddDzLockoutDuration(lockout, seconds, m_uuid);
|
|
}
|
|
}
|
|
|
|
if (!char_ids.empty()) // always update db for non-members in dz (call may be from another zone)
|
|
{
|
|
int secs = static_cast<int>(seconds * RuleR(Expedition, LockoutDurationMultiplier));
|
|
CharacterExpeditionLockoutsRepository::AddLockoutDuration(database, char_ids, lockout, secs);
|
|
}
|
|
}
|
|
|
|
void DynamicZone::SetLootEvent(uint32_t id, const std::string& event, DzLootEvent::Type type)
|
|
{
|
|
if (id != 0 && IsCurrentZoneDz())
|
|
{
|
|
LogExpeditions("Setting loot event [{}] for id [{}] type [{}]", event, id, static_cast<int>(type));
|
|
auto it = std::ranges::find_if(m_loot_events, [&](const auto& le) { return le.id == id && le.type == type; });
|
|
if (it != m_loot_events.end())
|
|
{
|
|
it->event = event;
|
|
}
|
|
else
|
|
{
|
|
m_loot_events.push_back({id, event, type});
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string DynamicZone::GetLootEvent(uint32_t id, DzLootEvent::Type type) const
|
|
{
|
|
std::string event_name;
|
|
if (id != 0 && IsCurrentZoneDz())
|
|
{
|
|
auto it = std::ranges::find_if(m_loot_events, [&](const auto& le) { return le.id == id && le.type == type; });
|
|
if (it != m_loot_events.end())
|
|
{
|
|
event_name = it->event;
|
|
}
|
|
}
|
|
return event_name;
|
|
}
|
|
|
|
std::unique_ptr<EQApplicationPacket> DynamicZone::CreateInvitePacket(const std::string& inviter, const std::string& swap_name)
|
|
{
|
|
uint32_t outsize = sizeof(ExpeditionInvite_Struct);
|
|
auto outapp = std::make_unique<EQApplicationPacket>(OP_DzExpeditionInvite, outsize);
|
|
auto outbuf = reinterpret_cast<ExpeditionInvite_Struct*>(outapp->pBuffer);
|
|
strn0cpy(outbuf->inviter_name, inviter.c_str(), sizeof(outbuf->inviter_name));
|
|
strn0cpy(outbuf->expedition_name, GetName().c_str(), sizeof(outbuf->expedition_name));
|
|
strn0cpy(outbuf->swap_name, swap_name.c_str(), sizeof(outbuf->swap_name));
|
|
outbuf->swapping = !swap_name.empty();
|
|
outbuf->dz_zone_id = GetZoneID();
|
|
outbuf->dz_instance_id = GetInstanceID();
|
|
return outapp;
|
|
}
|
|
|
|
void DynamicZone::SendWorldPendingInvite(const ExpeditionInvite& invite, const std::string& add_name)
|
|
{
|
|
LogExpeditions("[{}] saving invite from [{}] to [{}]", add_name, invite.inviter_name, invite.dz_id);
|
|
SendWorldPlayerInvite(invite.inviter_name, invite.swap_name, add_name, true);
|
|
}
|
|
|
|
void DynamicZone::SendWorldPlayerInvite(const std::string& inviter, const std::string& swap_name, const std::string& add_name, bool pending)
|
|
{
|
|
auto opcode = pending ? ServerOP_DzSaveInvite : ServerOP_DzAddPlayer;
|
|
ServerPacket pack(opcode, static_cast<uint32_t>(sizeof(ServerDzCommand_Struct)));
|
|
auto buf = reinterpret_cast<ServerDzCommand_Struct*>(pack.pBuffer);
|
|
buf->dz_id = GetID();
|
|
buf->is_char_online = false;
|
|
strn0cpy(buf->requester_name, inviter.c_str(), sizeof(buf->requester_name));
|
|
strn0cpy(buf->target_name, add_name.c_str(), sizeof(buf->target_name));
|
|
strn0cpy(buf->remove_name, swap_name.c_str(), sizeof(buf->remove_name));
|
|
worldserver.SendPacket(&pack);
|
|
}
|
|
|
|
void DynamicZone::SendWorldMakeLeaderRequest(uint32_t char_id, const std::string& leader_name)
|
|
{
|
|
ServerPacket pack(ServerOP_DzMakeLeader, static_cast<uint32_t>(sizeof(ServerDzCommandMakeLeader_Struct)));
|
|
auto buf = reinterpret_cast<ServerDzCommandMakeLeader_Struct*>(pack.pBuffer);
|
|
buf->dz_id = GetID();
|
|
buf->requester_id = char_id;
|
|
strn0cpy(buf->new_leader_name, leader_name.c_str(), sizeof(buf->new_leader_name));
|
|
worldserver.SendPacket(&pack);
|
|
}
|