/** * 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.h" #include "string_ids.h" #include "worldserver.h" #include "../common/eqemu_logsys.h" extern WorldServer worldserver; 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() { return zone ? static_cast(zone->GetInstanceID()) : 0; } uint16_t DynamicZone::GetCurrentZoneID() { return zone ? static_cast(zone->GetZoneID()) : 0; } DynamicZone* DynamicZone::CreateNew(DynamicZone& dz_request, const std::vector& members) { if (!zone || dz_request.GetID() != 0) { return nullptr; } // this creates a new dz instance and saves it to both db and cache uint32_t dz_id = dz_request.Create(); if (dz_id == 0) { LogDynamicZones("Failed to create dynamic zone for zone [{}]", dz_request.GetZoneID()); return nullptr; } auto dz = std::make_unique(dz_request); if (!members.empty()) { dz->SaveMembers(members); } LogDynamicZones("Created new dz [{}] for zone [{}]", dz_id, dz_request.GetZoneID()); // world must be notified before we request async member updates auto pack = dz->CreateServerDzCreatePacket(zone->GetZoneID(), zone->GetInstanceID()); worldserver.SendPacket(pack.get()); auto inserted = zone->dynamic_zone_cache.emplace(dz_id, std::move(dz)); // expeditions invoke their own updates after installing client update callbacks if (inserted.first->second->GetType() != DynamicZoneType::Expedition) { inserted.first->second->DoAsyncZoneMemberUpdates(); } return inserted.first->second.get(); } 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 dz = std::make_unique(); dz->LoadSerializedDzPacket(buf->cereal_data, buf->cereal_size); uint32_t dz_id = dz->GetID(); auto inserted = zone->dynamic_zone_cache.emplace(dz_id, std::move(dz)); // expeditions invoke their own updates after installing client update callbacks if (inserted.first->second->GetType() != DynamicZoneType::Expedition) { inserted.first->second->DoAsyncZoneMemberUpdates(); } LogDynamicZones("Cached new dynamic zone [{}]", dz_id); } void DynamicZone::CacheAllFromDatabase() { if (!zone) { return; } BenchTimer bench; auto dynamic_zones = DynamicZonesRepository::AllWithInstanceNotExpired(database); auto dynamic_zone_members = DynamicZoneMembersRepository::GetAllWithNames(database); zone->dynamic_zone_cache.clear(); zone->dynamic_zone_cache.reserve(dynamic_zones.size()); for (auto& entry : dynamic_zones) { uint32_t dz_id = entry.id; auto dz = std::make_unique(std::move(entry)); for (auto& member : dynamic_zone_members) { if (member.dynamic_zone_id == dz_id) { dz->AddMemberFromRepositoryResult(std::move(member)); } } zone->dynamic_zone_cache.emplace(dz_id, std::move(dz)); } 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()); } DynamicZone* DynamicZone::FindDynamicZoneByID(uint32_t dz_id) { if (!zone) { return nullptr; } auto dz = zone->dynamic_zone_cache.find(dz_id); if (dz != zone->dynamic_zone_cache.end()) { return dz->second.get(); } return nullptr; } void DynamicZone::RegisterOnClientAddRemove(std::function on_client_addremove) { m_on_client_addremove = std::move(on_client_addremove); } void DynamicZone::StartAllClientRemovalTimers() { for (const auto& client_iter : entity_list.GetClientList()) { if (client_iter.second) { client_iter.second->SetDzRemovalTimer(true); } } } bool DynamicZone::IsCurrentZoneDzInstance() 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 && IsCurrentZoneDzInstance()) { zone->SetInstanceTimer(GetSecondsRemaining()); } } 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) { dz->SendUpdatesToZoneMembers(true, true); // members silently removed // manually handle expeditions to remove any references before the dz is deleted if (dz->GetType() == DynamicZoneType::Expedition) { auto expedition = Expedition::FindCachedExpeditionByDynamicZoneID(dz->GetID()); if (expedition) { LogExpeditionsDetail("Deleting expedition [{}] from zone cache", expedition->GetID()); zone->expedition_cache.erase(expedition->GetID()); } } LogDynamicZonesDetail("Deleting dynamic zone [{}] from zone cache", buf->dz_id); zone->dynamic_zone_cache.erase(buf->dz_id); } 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_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; } } } 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::CreateMemberListNamePacket( const std::string& name, bool remove_name) { 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_name; strn0cpy(buf->name, name.c_str(), sizeof(buf->name)); return outapp; } std::unique_ptr DynamicZone::CreateMemberListStatusPacket( 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, DZMAKELEADER_YOU); } } } } void DynamicZone::SendMembersExpireWarning(uint32_t minutes_remaining) { // expeditions warn members in all zones not just the dz auto outapp = CreateExpireWarningPacket(minutes_remaining); for (const auto& member : GetMembers()) { Client* member_client = entity_list.GetClientByCharID(member.id); if (member_client) { member_client->QueuePacket(outapp.get()); // live doesn't actually send the chat message with it member_client->MessageString(Chat::Yellow, EXPEDITION_MIN_REMAIN, fmt::format_int(minutes_remaining).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::SendMemberListNameToZoneMembers(const std::string& char_name, bool remove) { auto outapp_member_name = CreateMemberListNamePacket(char_name, remove); for (const auto& member : m_members) { Client* member_client = entity_list.GetClientByCharID(member.id); if (member_client) { member_client->QueuePacket(outapp_member_name.get()); } } } void DynamicZone::SendMemberListStatusToZoneMembers(const DynamicZoneMember& update_member) { auto outapp_member_status = CreateMemberListStatusPacket(update_member.name, update_member.status); for (const auto& member : m_members) { Client* member_client = entity_list.GetClientByCharID(member.id); if (member_client) { member_client->QueuePacket(outapp_member_status.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()); } // callback to the dz system so it can perform any messages or set client data if (m_on_client_addremove) { m_on_client_addremove(client, removing_all, silent); } } } } 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()); } if (m_on_client_addremove) { m_on_client_addremove(client, removed, false); } } if (m_type == DynamicZoneType::Expedition) { // send full list when adding (MemberListName adds with "unknown" status) if (!removed) { SendMemberListToZoneMembers(); } else { SendMemberListNameToZoneMembers(member.name, true); } } } void DynamicZone::ProcessRemoveAllMembers(bool silent) { SendUpdatesToZoneMembers(true, silent); DynamicZoneBase::ProcessRemoveAllMembers(silent); } void DynamicZone::DoAsyncZoneMemberUpdates() { // 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 member_id, DynamicZoneMemberStatus status) { bool changed = DynamicZoneBase::ProcessMemberStatusChange(member_id, status); if (changed && m_type == DynamicZoneType::Expedition) { auto member = GetMemberData(member_id); if (member.IsValid()) { SendMemberListStatusToZoneMembers(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) { auto expedition = Expedition::FindCachedExpeditionByZoneInstance(zone->GetZoneID(), zone->GetInstanceID()); if (expedition && !expedition->CanClientLootCorpse(client, npc_type_id, entity_id)) { 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); } }