/** * 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 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(zone->GetInstanceID()) : 0; } uint16_t DynamicZone::GetCurrentZoneID() const { return zone ? static_cast(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(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(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()); 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(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(pack.pBuffer); buf->zone_id = static_cast(zone->GetZoneID()); buf->inst_id = static_cast(zone->GetInstanceID()); worldserver.SendPacket(&pack); } template 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(ServerOP_DzSetSecondsRemaining, pack_size); auto buf = reinterpret_cast(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 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 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(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(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(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(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(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(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(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(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(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(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(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(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(pack->pBuffer); auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id); if (dz) { for (uint32_t i = 0; i < buf->count; ++i) { auto status = static_cast(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>> dzs; dzs.reserve(zone->dynamic_zone_cache.size()); auto buf = reinterpret_cast(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(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(buf->status); dz->ProcessMemberStatusChange(buf->character_id, status); } } break; } case ServerOP_DzLeaderChanged: { auto buf = reinterpret_cast(pack->pBuffer); auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id); if (dz) { dz->ProcessLeaderChanged(buf->leader_id); } break; } case ServerOP_DzExpireWarning: { auto buf = reinterpret_cast(pack->pBuffer); auto dz = DynamicZone::FindDynamicZoneByID(buf->dz_id); if (dz) { dz->SendMembersExpireWarning(buf->minutes_remaining); } break; } case ServerOP_DzMovePC: { auto buf = reinterpret_cast(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(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(buf->lock_msg), buf->color); } } break; } case ServerOP_DzReplayOnJoin: { auto buf = reinterpret_cast(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(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(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 DynamicZone::CreateExpireWarningPacket(uint32_t minutes_remaining) { uint32_t outsize = sizeof(ExpeditionExpireWarning); auto outapp = std::make_unique(OP_DzExpeditionEndsWarning, outsize); auto buf = reinterpret_cast(outapp->pBuffer); buf->minutes_remaining = minutes_remaining; return outapp; } std::unique_ptr DynamicZone::CreateInfoPacket(bool clear) { constexpr uint32_t outsize = sizeof(DynamicZoneInfo_Struct); auto outapp = std::make_unique(OP_DzExpeditionInfo, outsize); if (!clear) { auto info = reinterpret_cast(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 DynamicZone::CreateMemberListPacket(bool clear) { uint32_t member_count = clear ? 0 : static_cast(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(OP_DzMemberList, outsize); auto buf = reinterpret_cast(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(m_members[i].status); } } return outapp; } std::unique_ptr DynamicZone::CreateMemberNamePacket(const std::string& name, bool remove) { constexpr uint32_t outsize = sizeof(DynamicZoneMemberListName_Struct); auto outapp = std::make_unique(OP_DzMemberListName, outsize); auto buf = reinterpret_cast(outapp->pBuffer); buf->add_name = !remove; strn0cpy(buf->name, name.c_str(), sizeof(buf->name)); return outapp; } std::unique_ptr 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(OP_DzMemberListStatus, outsize); auto buf = reinterpret_cast(outapp->pBuffer); buf->member_count = 1; auto entry = static_cast(buf->members); strn0cpy(entry->name, name.c_str(), sizeof(entry->name)); entry->online_status = static_cast(status); return outapp; } std::unique_ptr DynamicZone::CreateLeaderNamePacket() { constexpr uint32_t outsize = sizeof(DynamicZoneLeaderName_Struct); auto outapp = std::make_unique(OP_DzSetLeaderName, outsize); auto buf = reinterpret_cast(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 outapp_info = nullptr; std::unique_ptr 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(ServerOP_DzGetMemberStatuses, pack_size); auto buf = reinterpret_cast(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(pack.pBuffer); buf->dz_id = GetID(); buf->sender_zone_id = static_cast(zone->GetZoneID()); buf->sender_instance_id = static_cast(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& 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(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 DynamicZone::GetCharacterLockouts(uint32_t char_id) { std::vector 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 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 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 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(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(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 DynamicZone::CreateInvitePacket(const std::string& inviter, const std::string& swap_name) { uint32_t outsize = sizeof(ExpeditionInvite_Struct); auto outapp = std::make_unique(OP_DzExpeditionInvite, outsize); auto outbuf = reinterpret_cast(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(sizeof(ServerDzCommand_Struct))); auto buf = reinterpret_cast(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(sizeof(ServerDzCommandMakeLeader_Struct))); auto buf = reinterpret_cast(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); }