diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 80cf9851f..2ca06319a 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -118,6 +118,7 @@ namespace Logs { Merchants, ZonePoints, Loot, + Expeditions, MaxCategoryID /* Don't Remove this */ }; @@ -194,7 +195,8 @@ namespace Logs { "HotReload", "Merchants", "ZonePoints", - "Loot" + "Loot", + "Expeditions", }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 514f37e21..b10521eee 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -601,6 +601,21 @@ OutF(LogSys, Logs::Detail, Logs::Loot, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogExpeditions(message, ...) do {\ + if (LogSys.log_settings[Logs::Expeditions].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::Expeditions, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogExpeditionsModerate(message, ...) do {\ + if (LogSys.log_settings[Logs::Expeditions].is_category_enabled == 1)\ + OutF(LogSys, Logs::Moderate, Logs::Expeditions, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogExpeditionsDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::Expeditions].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::Expeditions, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.log_settings[log_category].is_category_enabled == 1)\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ @@ -952,6 +967,15 @@ #define LogZonePointsDetail(message, ...) do {\ } while (0) +#define LogExpeditions(message, ...) do {\ +} while (0) + +#define LogExpeditionsModerate(message, ...) do {\ +} while (0) + +#define LogExpeditionsDetail(message, ...) do {\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ } while (0) diff --git a/common/ruletypes.h b/common/ruletypes.h index e46439184..f1ea792de 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -785,6 +785,11 @@ RULE_BOOL(Instances, RecycleInstanceIds, true, "Setting whether free instance ID RULE_INT(Instances, GuildHallExpirationDays, 90, "Amount of days before a Guild Hall instance expires") RULE_CATEGORY_END() +RULE_CATEGORY(Expedition) +RULE_INT(Expedition, MinStatusToBypassPlayerCountRequirements, 80, "Minimum GM status to bypass minimum player requirements for Expedition creation") +RULE_BOOL(Expedition, UseDatabaseToVerifyLeaderCommands, false, "Use database instead of zone cache to verify Expedition leader for commands") +RULE_CATEGORY_END() + #undef RULE_CATEGORY #undef RULE_INT #undef RULE_REAL diff --git a/common/servertalk.h b/common/servertalk.h index 5ed3b3306..6efb20eb8 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -140,6 +140,18 @@ #define ServerOP_LFPUpdate 0x0213 #define ServerOP_LFPMatches 0x0214 #define ServerOP_ClientVersionSummary 0x0215 + +#define ServerOP_ExpeditionCreate 0x0400 +#define ServerOP_ExpeditionDeleted 0x0401 +#define ServerOP_ExpeditionLeaderChanged 0x0402 +#define ServerOP_ExpeditionLockout 0x0403 +#define ServerOP_ExpeditionMemberChange 0x0404 +#define ServerOP_ExpeditionMemberSwap 0x0405 +#define ServerOP_ExpeditionMemberStatus 0x0406 +#define ServerOP_ExpeditionGetOnlineMembers 0x0407 +#define ServerOP_ExpeditionDzAddPlayer 0x0408 +#define ServerOP_ExpeditionDzMakeLeader 0x0409 + #define ServerOP_LSInfo 0x1000 #define ServerOP_LSStatus 0x1001 #define ServerOP_LSClientAuthLeg 0x1002 @@ -257,6 +269,8 @@ #define ServerOP_CZTaskRemoveGroup 0x4560 #define ServerOP_CZTaskRemoveRaid 0x4561 #define ServerOP_CZTaskRemoveGuild 0x4562 +#define ServerOP_CZClientMessage 0x4563 +#define ServerOP_CZClientMessageString 0x4564 #define ServerOP_WWAssignTask 0x4750 #define ServerOP_WWCastSpell 0x4751 @@ -1958,6 +1972,87 @@ struct UCSServerStatus_Struct { }; }; +struct ServerCZClientMessage_Struct { + uint16 chat_type; + char character_name[64]; + uint32 message_size; + char message[1]; +}; + +struct ServerCZClientMessageString_Struct { + uint32 string_id; + uint16 chat_type; + char character_name[64]; + uint32 string_params_size; + char string_params[1]; // null delimited +}; + +struct ServerExpeditionID_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint32 sender_instance_id; +}; + +struct ServerExpeditionMemberChange_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 removed; // 0: added, 1: removed + uint32 char_id; + char char_name[64]; +}; + +struct ServerExpeditionMemberSwap_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint32 add_char_id; + uint32 remove_char_id; + char add_char_name[64]; + char remove_char_name[64]; +}; + +struct ServerExpeditionMemberStatus_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 status; // 0: unknown 1: Online 2: Offline 3: In Dynamic Zone 4: Link Dead + uint32 character_id; +}; + +struct ServerExpeditionCharacterEntry_Struct { + uint32 character_id; + uint32 character_zone_id; + uint16 character_instance_id; + uint8 character_online; // 0: offline 1: online +}; + +struct ServerExpeditionCharacters_Struct { + uint32 expedition_id; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint32 count; + ServerExpeditionCharacterEntry_Struct entries[0]; +}; + +struct ServerExpeditionLockout_Struct { + uint32 expedition_id; + uint64 expire_time; + uint32 duration; + uint32 sender_zone_id; + uint16 sender_instance_id; + uint8 remove; + char event_name[256]; +}; + +struct ServerDzCommand_Struct { + uint32 expedition_id; + uint8 is_char_online; // 0: target name is offline, 1: online + char requester_name[64]; + char target_name[64]; + char remove_name[64]; // used for swap command +}; + #pragma pack() #endif diff --git a/common/string_util.cpp b/common/string_util.cpp index 680c2822b..9190207ee 100644 --- a/common/string_util.cpp +++ b/common/string_util.cpp @@ -592,3 +592,14 @@ std::string numberToWords(unsigned long long int n) return res; } + +std::string FormatName(const std::string& char_name) +{ + std::string formatted(char_name); + if (!formatted.empty()) + { + std::transform(formatted.begin(), formatted.end(), formatted.begin(), ::tolower); + formatted[0] = ::toupper(formatted[0]); + } + return formatted; +} diff --git a/common/string_util.h b/common/string_util.h index 0d602e395..226e21645 100644 --- a/common/string_util.h +++ b/common/string_util.h @@ -206,6 +206,6 @@ void MakeLowerString(const char *source, char *target); void RemoveApostrophes(std::string &s); std::string convert2digit(int n, std::string suffix); std::string numberToWords(unsigned long long int n); - +std::string FormatName(const std::string& char_name); #endif diff --git a/world/CMakeLists.txt b/world/CMakeLists.txt index ab14ed603..a0830c6a2 100644 --- a/world/CMakeLists.txt +++ b/world/CMakeLists.txt @@ -9,6 +9,7 @@ SET(world_sources console.cpp eql_config.cpp eqemu_api_world_data_service.cpp + expedition.cpp launcher_link.cpp launcher_list.cpp lfplist.cpp @@ -39,6 +40,7 @@ SET(world_headers console.h eql_config.h eqemu_api_world_data_service.h + expedition.h launcher_link.h launcher_list.h lfplist.h diff --git a/world/expedition.cpp b/world/expedition.cpp new file mode 100644 index 000000000..e871ff25c --- /dev/null +++ b/world/expedition.cpp @@ -0,0 +1,136 @@ +/** + * 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 "expedition.h" +#include "clientlist.h" +#include "cliententry.h" +#include "zonelist.h" +#include "zoneserver.h" +#include "worlddb.h" +#include "../common/servertalk.h" +#include "../common/string_util.h" + +extern ClientList client_list; +extern ZSList zoneserver_list; + +void Expedition::PurgeEmptyExpeditions() +{ + std::string query = SQL( + DELETE expedition FROM expedition_details expedition + LEFT JOIN ( + SELECT expedition_id, COUNT(IF(is_current_member = TRUE, 1, NULL)) member_count + FROM expedition_members + GROUP BY expedition_id + ) AS expedition_members + ON expedition_members.expedition_id = expedition.id + WHERE expedition_members.expedition_id IS NULL OR expedition_members.member_count <= 0 + ); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to purge empty expeditions"); + } +} + +void Expedition::PurgeExpiredCharacterLockouts() +{ + std::string query = SQL( + DELETE FROM expedition_character_lockouts + WHERE expire_time <= NOW(); + ); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to purge expired lockouts"); + } +} + +void Expedition::AddPlayer(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + ClientListEntry* invited_cle = client_list.FindCharacter(buf->target_name); + if (invited_cle && invited_cle->Server()) + { + // continue in the add target's zone + buf->is_char_online = true; + invited_cle->Server()->SendPacket(pack); + } + else + { + // add target not online, return to inviter + ClientListEntry* inviter_cle = client_list.FindCharacter(buf->requester_name); + if (inviter_cle && inviter_cle->Server()) + { + inviter_cle->Server()->SendPacket(pack); + } + } +} + +void Expedition::MakeLeader(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + // notify requester (old leader) and new leader of the result + ZoneServer* new_leader_zs = nullptr; + ClientListEntry* new_leader_cle = client_list.FindCharacter(buf->target_name); + if (new_leader_cle && new_leader_cle->Server()) + { + buf->is_char_online = true; + new_leader_zs = new_leader_cle->Server(); + new_leader_zs->SendPacket(pack); + } + + // if old and new leader are in the same zone only send one message + ClientListEntry* requester_cle = client_list.FindCharacter(buf->requester_name); + if (requester_cle && requester_cle->Server() && requester_cle->Server() != new_leader_zs) + { + requester_cle->Server()->SendPacket(pack); + } +} + +void Expedition::GetOnlineMembers(ServerPacket* pack) +{ + auto buf = reinterpret_cast(pack->pBuffer); + + // not efficient but only requested during caching + char zone_name[64] = {0}; + std::vector all_clients; + all_clients.reserve(client_list.GetClientCount()); + client_list.GetClients(zone_name, all_clients); + + for (uint32_t i = 0; i < buf->count; ++i) + { + for (const auto& cle : all_clients) + { + if (cle && cle->CharID() == buf->entries[i].character_id) + { + buf->entries[i].character_zone_id = cle->zone(); + buf->entries[i].character_instance_id = cle->instance(); + buf->entries[i].character_online = true; + break; + } + } + } + + zoneserver_list.SendPacket(buf->sender_zone_id, buf->sender_instance_id, pack); +} diff --git a/world/expedition.h b/world/expedition.h new file mode 100644 index 000000000..bfd1250ed --- /dev/null +++ b/world/expedition.h @@ -0,0 +1,35 @@ +/** + * 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 + * + */ + +#ifndef WORLD_EXPEDITION_H +#define WORLD_EXPEDITION_H + +class ServerPacket; + +namespace Expedition +{ + void PurgeEmptyExpeditions(); + void PurgeExpiredCharacterLockouts(); + void AddPlayer(ServerPacket* pack); + void MakeLeader(ServerPacket* pack); + void GetOnlineMembers(ServerPacket* pack); +}; + +#endif diff --git a/world/main.cpp b/world/main.cpp index b94ecea4c..d7ac2d91e 100644 --- a/world/main.cpp +++ b/world/main.cpp @@ -88,6 +88,7 @@ union semun { #include "queryserv.h" #include "web_interface.h" #include "console.h" +#include "expedition.h" #include "../common/net/servertalk_server.h" #include "../zone/data_bucket.h" @@ -429,6 +430,10 @@ int main(int argc, char** argv) { Timer PurgeInstanceTimer(450000); PurgeInstanceTimer.Start(450000); + LogInfo("Purging expired expeditions"); + Expedition::PurgeEmptyExpeditions(); //database.PurgeExpiredExpeditions(); + Expedition::PurgeExpiredCharacterLockouts(); + LogInfo("Loading char create info"); content_db.LoadCharacterCreateAllocations(); content_db.LoadCharacterCreateCombos(); @@ -599,6 +604,8 @@ int main(int argc, char** argv) { if (PurgeInstanceTimer.Check()) { database.PurgeExpiredInstances(); database.PurgeAllDeletedDataBuckets(); + Expedition::PurgeEmptyExpeditions(); + Expedition::PurgeExpiredCharacterLockouts(); } if (EQTimeTimer.Check()) { diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 5e2268755..d39b3288d 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -36,6 +36,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "ucs.h" #include "queryserv.h" #include "world_store.h" +#include "expedition.h" extern ClientList client_list; extern GroupLFPList LFPGroupList; @@ -1355,6 +1356,44 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { cle->ProcessTellQueue(); break; } + case ServerOP_CZClientMessage: + { + auto buf = reinterpret_cast(pack->pBuffer); + client_list.SendPacket(buf->character_name, pack); + break; + } + case ServerOP_CZClientMessageString: + { + auto buf = reinterpret_cast(pack->pBuffer); + client_list.SendPacket(buf->character_name, pack); + break; + } + case ServerOP_ExpeditionCreate: + case ServerOP_ExpeditionDeleted: + case ServerOP_ExpeditionLeaderChanged: + case ServerOP_ExpeditionLockout: + case ServerOP_ExpeditionMemberChange: + case ServerOP_ExpeditionMemberSwap: + case ServerOP_ExpeditionMemberStatus: + { + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionGetOnlineMembers: + { + Expedition::GetOnlineMembers(pack); + break; + } + case ServerOP_ExpeditionDzAddPlayer: + { + Expedition::AddPlayer(pack); + break; + } + case ServerOP_ExpeditionDzMakeLeader: + { + Expedition::MakeLeader(pack); + break; + } default: { LogInfo("Unknown ServerOPcode from zone {:#04x}, size [{}]", pack->opcode, pack->size); diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index beacca565..81112445e 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -30,6 +30,10 @@ SET(zone_sources encounter.cpp entity.cpp exp.cpp + expedition.cpp + expedition_database.cpp + expedition_lockout_timer.cpp + expedition_request.cpp fastmath.cpp fearpath.cpp forage.cpp @@ -172,6 +176,10 @@ SET(zone_headers entity.h errmsg.h event_codes.h + expedition.h + expedition_database.h + expedition_lockout_timer.h + expedition_request.h fastmath.h forage.h global_loot_manager.h diff --git a/zone/client.cpp b/zone/client.cpp index 821c14570..5f513f4bb 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -40,6 +40,9 @@ extern volatile bool RunLoops; #include "../common/data_verification.h" #include "../common/profanity_manager.h" #include "data_bucket.h" +#include "expedition.h" +#include "expedition_database.h" +#include "expedition_lockout_timer.h" #include "position.h" #include "worldserver.h" #include "zonedb.h" @@ -3201,6 +3204,27 @@ void Client::MessageString(uint32 type, uint32 string_id, const char* message1, safe_delete(outapp); } +void Client::MessageString(const ServerCZClientMessageString_Struct* msg) +{ + if (msg) + { + if (msg->string_params_size == 0) + { + MessageString(msg->chat_type, msg->string_id); + } + else + { + uint32_t outsize = sizeof(FormattedMessage_Struct) + msg->string_params_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_FormattedMessage, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->string_id = msg->string_id; + outbuf->type = msg->chat_type; + memcpy(outbuf->message, msg->string_params, msg->string_params_size); + QueuePacket(outapp.get()); + } + } +} + // helper function, returns true if we should see the message bool Client::FilteredMessageCheck(Mob *sender, eqFilterType filter) { @@ -3397,6 +3421,13 @@ void Client::LinkDead() if(raid){ raid->MemberZoned(this); } + + Expedition* expedition = GetExpedition(); + if (expedition) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::LinkDead); + } + // save_timer.Start(2500); linkdead_timer.Start(RuleI(Zone,ClientLinkdeadMS)); SendAppearancePacket(AT_Linkdead, 1); @@ -6124,17 +6155,20 @@ void Client::CheckEmoteHail(Mob *target, const char* message) void Client::MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count) { + uint32 entry_size = sizeof(DynamicZoneCompassEntry_Struct) * count; + auto outapp = new EQApplicationPacket(OP_DzCompass, sizeof(DynamicZoneCompass_Struct) + entry_size); + auto outbuf = reinterpret_cast(outapp->pBuffer); - auto outapp = new EQApplicationPacket(OP_DzCompass, sizeof(ExpeditionInfo_Struct) + - sizeof(DynamicZoneCompassEntry_Struct) * count); - DynamicZoneCompass_Struct *ecs = (DynamicZoneCompass_Struct*)outapp->pBuffer; - //ecs->clientid = GetID(); - ecs->count = count; + outbuf->client_id = 0; + outbuf->count = count; if (count) { - ecs->entries[0].x = in_x; - ecs->entries[0].y = in_y; - ecs->entries[0].z = in_z; + outbuf->entries[0].dz_zone_id = 0; + outbuf->entries[0].dz_instance_id = 0; + outbuf->entries[0].dz_type = 0; + outbuf->entries[0].x = in_x; + outbuf->entries[0].y = in_y; + outbuf->entries[0].z = in_z; } FastQueuePacket(&outapp); @@ -9459,3 +9493,254 @@ void Client::ShowDevToolsMenu() void Client::SendChatLineBreak(uint16 color) { Message(color, "------------------------------------------------"); } + +void Client::SendCrossZoneMessage( + Client* client, const std::string& character_name, uint16_t chat_type, const std::string& message) +{ + // if client is null, falls back to sending a cross zone message by name + if (!client) + { + client = entity_list.GetClientByName(character_name.c_str()); + } + + if (client) + { + client->Message(chat_type, message.c_str()); + } + else if (message.size() > 0) + { + uint32_t msg_size = static_cast(message.size()) + 1; + uint32_t pack_size = sizeof(ServerCZClientMessage_Struct) + msg_size; + auto pack = std::unique_ptr(new ServerPacket(ServerOP_CZClientMessage, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->chat_type = chat_type; + strn0cpy(buf->character_name, character_name.c_str(), sizeof(buf->character_name)); + buf->message_size = msg_size; + strn0cpy(buf->message, message.c_str(), buf->message_size); + + worldserver.SendPacket(pack.get()); + } +} + +void Client::SendCrossZoneMessageString( + Client* client, const std::string& character_name, uint16_t chat_type, + uint32_t string_id, const std::initializer_list& parameters) +{ + // if client is null, falls back to sending a cross zone message by name + SerializeBuffer parameter_buffer; + for (const auto& parameter : parameters) + { + parameter_buffer.WriteString(parameter); + } + + uint32_t pack_size = sizeof(ServerCZClientMessageString_Struct) + static_cast(parameter_buffer.size()); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_CZClientMessageString, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->string_id = string_id; + buf->chat_type = chat_type; + strn0cpy(buf->character_name, character_name.c_str(), sizeof(buf->character_name)); + buf->string_params_size = static_cast(parameter_buffer.size()); + buf->string_params[0] = '\0'; + if (parameter_buffer.size()) { + memcpy(buf->string_params, parameter_buffer.buffer(), parameter_buffer.size()); + } + + if (!client) // double check client isn't in this zone + { + client = entity_list.GetClientByName(character_name.c_str()); + } + + if (client) + { + client->MessageString(buf); + } + else + { + worldserver.SendPacket(pack.get()); + } +} + +void Client::UpdateExpeditionInfoAndLockouts() +{ + // this is processed by client after entering a zone + // todo: live re-invites if client zoned with a pending invite window open + auto expedition = GetExpedition(); + if (expedition) + { + expedition->SendClientExpeditionInfo(this); + + // live only adds lockouts obtained during the active expedition to new + // members once they zone into the expedition's dynamic zone instance + if (zone && /*zone->GetInstanceID() && zone->GetInstanceID()*/zone->GetZoneID() == expedition->GetInstanceID()) + { + ExpeditionDatabase::AssignPendingLockouts(CharacterID(), expedition->GetName()); + expedition->SetMemberStatus(this, ExpeditionMemberStatus::InDynamicZone); + } + else + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::Online); + } + } + Expedition::LoadAllClientLockouts(this); +} + +Expedition* Client::CreateExpedition( + std::string name, uint32 min_players, uint32 max_players, bool has_replay_timer) +{ + return Expedition::TryCreate(this, name, min_players, max_players, has_replay_timer); +} + +Expedition* Client::GetExpedition() const +{ + if (zone && m_expedition_id) + { + auto expedition_cache_iter = zone->expedition_cache.find(m_expedition_id); + if (expedition_cache_iter != zone->expedition_cache.end()) + { + return expedition_cache_iter->second.get(); + } + } + return nullptr; +} + +std::vector Client::GetExpeditionLockouts(const std::string& expedition_name) +{ + std::vector lockouts; + for (const auto& lockout : m_expedition_lockouts) + { + if (lockout.GetExpeditionName() == expedition_name) + { + lockouts.emplace_back(lockout); + } + } + return lockouts; +} + +void Client::DzListTimers() +{ + // only lists player's current replay timer lockouts, not all event lockouts + bool found = false; + for (const auto& lockout : m_expedition_lockouts) + { + if (lockout.IsReplayTimer()) + { + found = true; + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + MessageString( + Chat::Yellow, DZLIST_REPLAY_TIMER, + time_remaining.days.c_str(), time_remaining.hours.c_str(), time_remaining.mins.c_str(), + lockout.GetExpeditionName().c_str() + ); + } + } + + if (!found) + { + MessageString(Chat::Yellow, EXPEDITION_NO_TIMERS); + } +} + +void Client::AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db) +{ + // todo: support for account based lockouts like live AoC expeditions + auto it = std::find_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& existing_lockout) { + return existing_lockout.IsSameLockout(lockout); + }); + + if (it != m_expedition_lockouts.end()) + { + it->SetExpireTime(lockout.GetExpireTime()); + } + else + { + m_expedition_lockouts.emplace_back(lockout); + } + + if (update_db) { // for quest api + ExpeditionDatabase::InsertCharacterLockouts(CharacterID(), { lockout }, true); + } +} + +void Client::AddNewExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, uint32_t seconds) +{ + auto expire_at = std::chrono::system_clock::now() + std::chrono::seconds(seconds); + auto expire_time = static_cast(std::chrono::system_clock::to_time_t(expire_at)); + ExpeditionLockoutTimer lockout{ expedition_name, event_name, expire_time, seconds }; + AddExpeditionLockout(lockout, true); + SendExpeditionLockoutTimers(); +} + +void Client::RemoveExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool update_db) +{ + m_expedition_lockouts.erase(std::remove_if(m_expedition_lockouts.begin(), m_expedition_lockouts.end(), + [&](const ExpeditionLockoutTimer& lockout) { + return lockout.IsSameLockout(expedition_name, event_name); + } + ), m_expedition_lockouts.end()); + + if (update_db) { // for quest api + ExpeditionDatabase::DeleteCharacterLockout(CharacterID(), expedition_name, event_name); + } +} + +const ExpeditionLockoutTimer* Client::GetExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) const +{ + for (const auto& expedition_lockout : m_expedition_lockouts) + { + if ((include_expired || expedition_lockout.GetSecondsRemaining() > 0) && + expedition_lockout.IsSameLockout(expedition_name, event_name)) + { + return &expedition_lockout; + } + } + return nullptr; +} + +bool Client::HasExpeditionLockout( + const std::string& expedition_name, const std::string& event_name, bool include_expired) +{ + return (GetExpeditionLockout(expedition_name, event_name, include_expired) != nullptr); +} + +void Client::SendExpeditionLockoutTimers() +{ + std::vector lockout_entries; + + // erases expired lockouts while building lockout timer list + for (auto it = m_expedition_lockouts.begin(); it != m_expedition_lockouts.end();) + { + auto seconds_remaining = it->GetSecondsRemaining(); + if (seconds_remaining <= 0) + { + it = m_expedition_lockouts.erase(it); + } + else + { + ExpeditionLockoutTimerEntry_Struct lockout; + strn0cpy(lockout.expedition_name, it->GetExpeditionName().c_str(), sizeof(lockout.expedition_name)); + lockout.seconds_remaining = seconds_remaining; + lockout.event_type = it->IsReplayTimer() ? Expedition::REPLAY_TIMER_ID : Expedition::EVENT_TIMER_ID; + strn0cpy(lockout.event_name, it->GetEventName().c_str(), sizeof(lockout.event_name)); + + lockout_entries.emplace_back(lockout); + ++it; + } + } + + uint32_t lockout_count = static_cast(lockout_entries.size()); + uint32_t lockout_entries_size = sizeof(ExpeditionLockoutTimerEntry_Struct) * lockout_count; + uint32_t outsize = sizeof(ExpeditionLockoutTimers_Struct) + lockout_entries_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionLockoutTimers, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + outbuf->client_id = 0; + outbuf->count = lockout_count; + if (!lockout_entries.empty()) + { + memcpy(outbuf->timers, lockout_entries.data(), lockout_entries_size); + } + QueuePacket(outapp.get()); +} diff --git a/zone/client.h b/zone/client.h index 13f7b81b6..b9e47b903 100644 --- a/zone/client.h +++ b/zone/client.h @@ -21,6 +21,8 @@ class Client; class EQApplicationPacket; class EQStream; +class Expedition; +class ExpeditionLockoutTimer; class Group; class NPC; class Object; @@ -283,6 +285,7 @@ public: uint8 SlotConvert(uint8 slot,bool bracer=false); void MessageString(uint32 type, uint32 string_id, uint32 distance = 0); void MessageString(uint32 type, uint32 string_id, const char* message,const char* message2=0,const char* message3=0,const char* message4=0,const char* message5=0,const char* message6=0,const char* message7=0,const char* message8=0,const char* message9=0, uint32 distance = 0); + void MessageString(const ServerCZClientMessageString_Struct* msg); bool FilteredMessageCheck(Mob *sender, eqFilterType filter); void FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, uint32 string_id); void FilteredMessageString(Mob *sender, uint32 type, eqFilterType filter, @@ -1104,6 +1107,31 @@ public: void MarkSingleCompassLoc(float in_x, float in_y, float in_z, uint8 count=1); + // cross zone client messaging helpers (null client argument will fallback to messaging by name) + static void SendCrossZoneMessage( + Client* client, const std::string& client_name, uint16_t chat_type, const std::string& message); + static void SendCrossZoneMessageString( + Client* client, const std::string& client_name, uint16_t chat_type, + uint32_t string_id, const std::initializer_list& parameters = {}); + + void AddExpeditionLockout(const ExpeditionLockoutTimer& lockout, bool update_db = false); + void AddNewExpeditionLockout(const std::string& expedition_name, const std::string& event_name, uint32_t duration); + Expedition* CreateExpedition(std::string name, uint32 min_players, uint32 max_players, bool has_replay_timer = false); + Expedition* GetExpedition() const; + uint32 GetExpeditionID() const { return m_expedition_id; } + const ExpeditionLockoutTimer* GetExpeditionLockout(const std::string& expedition_name, const std::string& event_name, bool include_expired = false) const; + const std::vector& GetExpeditionLockouts() const { return m_expedition_lockouts; }; + std::vector GetExpeditionLockouts(const std::string& expedition_name); + uint32 GetPendingExpeditionInviteID() const { return m_pending_expedition_invite_id; } + bool HasExpeditionLockout(const std::string& expedition_name, const std::string& event_name, bool include_expired = false); + bool IsInExpedition() const { return m_expedition_id != 0; } + void RemoveExpeditionLockout(const std::string& expedition_name, const std::string& event_name, bool update_db = false); + void SetPendingExpeditionInvite(uint32 id) { m_pending_expedition_invite_id = id; } + void SendExpeditionLockoutTimers(); + void SetExpeditionID(uint32 expedition_id) { m_expedition_id = expedition_id; }; + void UpdateExpeditionInfoAndLockouts(); + void DzListTimers(); + void CalcItemScale(); bool CalcItemScale(uint32 slot_x, uint32 slot_y); // behavior change: 'slot_y' is now [RANGE]_END and not [RANGE]_END + 1 void DoItemEnterZone(); @@ -1658,6 +1686,11 @@ private: int client_max_level; + uint32 m_expedition_id = 0; + uint32 m_pending_expedition_invite_id = 0; + Expedition* m_expedition = nullptr; + std::vector m_expedition_lockouts; + #ifdef BOTS public: diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index ed1ec0bdb..798a4c552 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -49,6 +49,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/zone_numbers.h" #include "data_bucket.h" #include "event_codes.h" +#include "expedition.h" +#include "expedition_database.h" #include "guild_mgr.h" #include "merc.h" #include "petitions.h" @@ -191,6 +193,15 @@ void MapOpcodes() ConnectedOpcodes[OP_DuelResponse2] = &Client::Handle_OP_DuelResponse2; ConnectedOpcodes[OP_DumpName] = &Client::Handle_OP_DumpName; ConnectedOpcodes[OP_Dye] = &Client::Handle_OP_Dye; + ConnectedOpcodes[OP_DzAddPlayer] = &Client::Handle_OP_DzAddPlayer; + ConnectedOpcodes[OP_DzChooseZoneReply] = &Client::Handle_OP_DzChooseZoneReply; + ConnectedOpcodes[OP_DzExpeditionInviteResponse] = &Client::Handle_OP_DzExpeditionInviteResponse; + ConnectedOpcodes[OP_DzListTimers] = &Client::Handle_OP_DzListTimers; + ConnectedOpcodes[OP_DzMakeLeader] = &Client::Handle_OP_DzMakeLeader; + ConnectedOpcodes[OP_DzPlayerList] = &Client::Handle_OP_DzPlayerList; + ConnectedOpcodes[OP_DzRemovePlayer] = &Client::Handle_OP_DzRemovePlayer; + ConnectedOpcodes[OP_DzSwapPlayer] = &Client::Handle_OP_DzSwapPlayer; + ConnectedOpcodes[OP_DzQuit] = &Client::Handle_OP_DzQuit; ConnectedOpcodes[OP_Emote] = &Client::Handle_OP_Emote; ConnectedOpcodes[OP_EndLootRequest] = &Client::Handle_OP_EndLootRequest; ConnectedOpcodes[OP_EnvDamage] = &Client::Handle_OP_EnvDamage; @@ -266,6 +277,7 @@ void MapOpcodes() ConnectedOpcodes[OP_ItemViewUnknown] = &Client::Handle_OP_Ignore; ConnectedOpcodes[OP_Jump] = &Client::Handle_OP_Jump; ConnectedOpcodes[OP_KeyRing] = &Client::Handle_OP_KeyRing; + ConnectedOpcodes[OP_KickPlayers] = &Client::Handle_OP_KickPlayers; ConnectedOpcodes[OP_LDoNButton] = &Client::Handle_OP_LDoNButton; ConnectedOpcodes[OP_LDoNDisarmTraps] = &Client::Handle_OP_LDoNDisarmTraps; ConnectedOpcodes[OP_LDoNInspect] = &Client::Handle_OP_LDoNInspect; @@ -885,6 +897,8 @@ void Client::CompleteConnect() guild_mgr.RequestOnlineGuildMembers(this->CharacterID(), this->GuildID()); } + UpdateExpeditionInfoAndLockouts(); + /** Request adventure info **/ auto pack = new ServerPacket(ServerOP_AdventureDataRequest, 64); strcpy((char*)pack->pBuffer, GetName()); @@ -1701,6 +1715,8 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) /* Task Packets */ LoadClientTaskState(); + m_expedition_id = ExpeditionDatabase::GetExpeditionIDFromCharacterID(CharacterID()); + /** * DevTools Load Settings */ @@ -5604,6 +5620,91 @@ void Client::Handle_OP_Dye(const EQApplicationPacket *app) return; } +void Client::Handle_OP_DzAddPlayer(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzAddPlayer(this, dzcmd->name); + } + else + { + // the only /dz command that sends an error message if no active expedition + Message(Chat::System, DZ_YOU_NOT_ASSIGNED); + } +} + +void Client::Handle_OP_DzChooseZoneReply(const EQApplicationPacket *app) +{ + // todo: implement + LogExpeditionsModerate("Handle_OP_DzChooseZoneReply"); + auto dzmsg = reinterpret_cast(app->pBuffer); +} + +void Client::Handle_OP_DzExpeditionInviteResponse(const EQApplicationPacket *app) +{ + auto expedition = Expedition::FindCachedExpeditionByID(m_pending_expedition_invite_id); + m_pending_expedition_invite_id = 0; + + if (expedition) + { + auto dzmsg = reinterpret_cast(app->pBuffer); + expedition->DzInviteResponse(this, dzmsg->accepted, dzmsg->swapping, dzmsg->swap_name); + } +} + +void Client::Handle_OP_DzListTimers(const EQApplicationPacket *app) +{ + DzListTimers(); +} + +void Client::Handle_OP_DzMakeLeader(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzMakeLeader(this, dzcmd->name); + } +} + +void Client::Handle_OP_DzPlayerList(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) { + expedition->DzPlayerList(this); + } +} + +void Client::Handle_OP_DzRemovePlayer(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzRemovePlayer(this, dzcmd->name); + } +} + +void Client::Handle_OP_DzSwapPlayer(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) + { + auto dzcmd = reinterpret_cast(app->pBuffer); + expedition->DzSwapPlayer(this, dzcmd->rem_player_name, dzcmd->add_player_name); + } +} + +void Client::Handle_OP_DzQuit(const EQApplicationPacket *app) +{ + auto expedition = GetExpedition(); + if (expedition) { + expedition->DzQuit(this); + } +} + void Client::Handle_OP_Emote(const EQApplicationPacket *app) { if (app->size != sizeof(Emote_Struct)) { @@ -8874,6 +8975,23 @@ void Client::Handle_OP_KeyRing(const EQApplicationPacket *app) KeyRingList(); } +void Client::Handle_OP_KickPlayers(const EQApplicationPacket *app) +{ + auto buf = reinterpret_cast(app->pBuffer); + if (buf->kick_expedition) + { + auto expedition = GetExpedition(); + if (expedition) + { + expedition->DzKickPlayers(this); + } + } + else if (buf->kick_task) + { + // todo: shared tasks + } +} + void Client::Handle_OP_LDoNButton(const EQApplicationPacket *app) { if (app->size < sizeof(bool)) diff --git a/zone/client_packet.h b/zone/client_packet.h index 9605dc8cf..3951a1761 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -101,6 +101,15 @@ void Handle_OP_DuelResponse2(const EQApplicationPacket *app); void Handle_OP_DumpName(const EQApplicationPacket *app); void Handle_OP_Dye(const EQApplicationPacket *app); + void Handle_OP_DzAddPlayer(const EQApplicationPacket *app); + void Handle_OP_DzChooseZoneReply(const EQApplicationPacket *app); + void Handle_OP_DzExpeditionInviteResponse(const EQApplicationPacket *app); + void Handle_OP_DzListTimers(const EQApplicationPacket *app); + void Handle_OP_DzMakeLeader(const EQApplicationPacket *app); + void Handle_OP_DzPlayerList(const EQApplicationPacket *app); + void Handle_OP_DzRemovePlayer(const EQApplicationPacket *app); + void Handle_OP_DzSwapPlayer(const EQApplicationPacket *app); + void Handle_OP_DzQuit(const EQApplicationPacket *app); void Handle_OP_Emote(const EQApplicationPacket *app); void Handle_OP_EndLootRequest(const EQApplicationPacket *app); void Handle_OP_EnvDamage(const EQApplicationPacket *app); @@ -174,6 +183,7 @@ void Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app); void Handle_OP_Jump(const EQApplicationPacket *app); void Handle_OP_KeyRing(const EQApplicationPacket *app); + void Handle_OP_KickPlayers(const EQApplicationPacket *app); void Handle_OP_LDoNButton(const EQApplicationPacket *app); void Handle_OP_LDoNDisarmTraps(const EQApplicationPacket *app); void Handle_OP_LDoNInspect(const EQApplicationPacket *app); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index ba62d470d..c898c249f 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -44,6 +44,7 @@ #include "../common/spdat.h" #include "../common/string_util.h" #include "event_codes.h" +#include "expedition.h" #include "guild_mgr.h" #include "map.h" #include "petitions.h" @@ -560,6 +561,12 @@ bool Client::Process() { client_state = CLIENT_LINKDEAD; AI_Start(CLIENT_LD_TIMEOUT); SendAppearancePacket(AT_Linkdead, 1); + + Expedition* expedition = GetExpedition(); + if (expedition) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::LinkDead); + } } } @@ -641,6 +648,11 @@ bool Client::Process() { myraid->MemberZoned(this); } } + Expedition* expedition = GetExpedition(); + if (expedition && !bZoning) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::Offline); + } OnDisconnect(false); return false; } @@ -682,6 +694,12 @@ void Client::OnDisconnect(bool hard_disconnect) { if (MyRaid) MyRaid->MemberZoned(this); + Expedition* expedition = GetExpedition(); + if (expedition) + { + expedition->SetMemberStatus(this, ExpeditionMemberStatus::Offline); + } + parse->EventPlayer(EVENT_DISCONNECT, this, "", 0); /* QS: PlayerLogConnectDisconnect */ diff --git a/zone/command.cpp b/zone/command.cpp index d56325725..8a67846e0 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -60,6 +60,7 @@ #include "data_bucket.h" #include "command.h" +#include "expedition.h" #include "guild_mgr.h" #include "map.h" #include "qglobals.h" @@ -198,6 +199,7 @@ int command_init(void) command_add("disarmtrap", "Analog for ldon disarm trap for the newer clients since we still don't have it working.", 80, command_disarmtrap) || command_add("distance", "- Reports the distance between you and your target.", 80, command_distance) || command_add("doanim", "[animnum] [type] - Send an EmoteAnim for you or your target", 50, command_doanim) || + command_add("dz", "Manage expeditions and dynamic zone instances", 80, command_dz) || command_add("editmassrespawn", "[name-search] [second-value] - Mass (Zone wide) NPC respawn timer editing command", 100, command_editmassrespawn) || command_add("emote", "['name'/'world'/'zone'] [type] [message] - Send an emote message", 80, command_emote) || command_add("emotesearch", "Searches NPC Emotes", 80, command_emotesearch) || @@ -6825,6 +6827,58 @@ void command_doanim(Client *c, const Seperator *sep) c->DoAnim(atoi(sep->arg[1]),atoi(sep->arg[2])); } +void command_dz(Client* c, const Seperator* sep) +{ + if (!c || !zone) { + return; + } + + if (strcasecmp(sep->arg[1], "cache") == 0) + { + if (strcasecmp(sep->arg[2], "list") == 0) + { + c->Message(Chat::White, "Total Active Expeditions: [%u]", static_cast(zone->expedition_cache.size())); + for (const auto& expedition : zone->expedition_cache) + { + c->Message( + Chat::White, "Expedition id: [%u]: leader: [%s] instance id: [%u] members: [%u]", + expedition.second->GetID(), + expedition.second->GetLeaderName().c_str(), + expedition.second->GetInstanceID(), + expedition.second->GetMemberCount() + ); + } + } + else if (strcasecmp(sep->arg[2], "reload") == 0) + { + Expedition::CacheAllFromDatabase(); + c->Message(Chat::White, "Reloaded [%u] expeditions to cache from database.", static_cast(zone->expedition_cache.size())); + } + } + else if (strcasecmp(sep->arg[1], "destroy") == 0) + { + if (sep->IsNumber(2)) + { + auto expedition_id = std::strtoul(sep->arg[2], nullptr, 10); + if (expedition_id) + { + auto expedition = Expedition::FindCachedExpeditionByID(expedition_id); + if (expedition) + { + expedition->RemoveAllMembers(); + } + } + } + } + else + { + c->Message(Chat::White, "#dz usage:"); + c->Message(Chat::White, "#dz cache list - list expeditions in current zone cache"); + c->Message(Chat::White, "#dz cache reload - reload zone cache from database"); + c->Message(Chat::White, "#dz destroy - destroy expedition globally (must be in cache)"); + } +} + void command_editmassrespawn(Client* c, const Seperator* sep) { if (strcasecmp(sep->arg[1], "usage") == 0) { diff --git a/zone/command.h b/zone/command.h index 87e01ae49..aebe8a697 100644 --- a/zone/command.h +++ b/zone/command.h @@ -92,6 +92,7 @@ void command_disablerecipe(Client *c, const Seperator *sep); void command_disarmtrap(Client *c, const Seperator *sep); void command_distance(Client *c, const Seperator *sep); void command_doanim(Client *c, const Seperator *sep); +void command_dz(Client *c, const Seperator *sep); void command_editmassrespawn(Client* c, const Seperator* sep); void command_emote(Client *c, const Seperator *sep); void command_emotesearch(Client* c, const Seperator *sep); diff --git a/zone/expedition.cpp b/zone/expedition.cpp new file mode 100644 index 000000000..95d01577e --- /dev/null +++ b/zone/expedition.cpp @@ -0,0 +1,1587 @@ +/** + * 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 "expedition.h" +#include "expedition_database.h" +#include "expedition_lockout_timer.h" +#include "expedition_request.h" +#include "client.h" +#include "groups.h" +#include "raids.h" +#include "string_ids.h" +#include "zonedb.h" +#include "worldserver.h" +#include "../common/eqemu_logsys.h" + +extern WorldServer worldserver; +extern Zone* zone; + +// message string 8271 (not in emu clients) +const char* const DZ_YOU_NOT_ASSIGNED = "You could not use this command because you are not currently assigned to a dynamic zone."; +// message string 9265 (not in emu clients) +const char* const EXPEDITION_OTHER_BELONGS = "{} attempted to create an expedition but {} already belongs to one."; +// lockout warnings were added to live in March 11 2020 patch +const char* const DZADD_INVITE_WARNING = "Warning! You will be given replay timers for the following events if you enter %s:"; +const char* const DZADD_INVITE_WARNING_TIMER = "%s - %sD:%sH:%sM"; +const char* const KICKPLAYERS_EVERYONE = "Everyone"; + +const uint32_t Expedition::REPLAY_TIMER_ID = std::numeric_limits::max(); +const uint32_t Expedition::EVENT_TIMER_ID = 1; + +Expedition::Expedition( + uint32_t id, std::string expedition_name, const ExpeditionMember& leader, + uint32_t min_players, uint32_t max_players, bool replay_timer +) : + m_id(id), + m_expedition_name(expedition_name), + m_leader(leader), + m_min_players(min_players), + m_max_players(max_players), + m_has_replay_timer(replay_timer) +{ +} + +Expedition* Expedition::TryCreate( + Client* requester, std::string name, uint32_t min_players, uint32_t max_players, bool replay_timer) +{ + if (!requester || !zone) + { + return nullptr; + } + + // request parses leader, members list, and lockouts while validating + ExpeditionRequest request(requester, name, min_players, max_players, replay_timer); + if (!request.Validate()) + { + LogExpeditionsModerate("Creation of [{}] by [{}] denied", name, requester->GetName()); + return nullptr; + } + + ExpeditionMember leader{ request.GetLeaderID(), request.GetLeaderName() }; + + // unique expedition ids are created from database via auto-increment column + auto expedition_id = ExpeditionDatabase::InsertExpedition( + name, leader.char_id, min_players, max_players, replay_timer + ); + + if (expedition_id) + { + auto expedition = std::make_unique( + expedition_id, name, leader, min_players, max_players, replay_timer + ); + + LogExpeditions( + "Created [{}] ({}) leader: [{}] minplayers: [{}] maxplayers: [{}]", + expedition->GetID(), + expedition->GetName(), + expedition->GetLeaderName(), + expedition->GetMinPlayers(), + expedition->GetMaxPlayers() + ); + + expedition->SaveMembers(request); + expedition->SaveLockouts(request); + expedition->SendUpdatesToZoneMembers(); + expedition->SendWorldExpeditionUpdate(); // cache in other zones + + Client* leader_client = request.GetLeaderClient(); + + Client::SendCrossZoneMessageString( + leader_client, leader.name, Chat::Yellow, EXPEDITION_AVAILABLE, { name } + ); + + auto inserted = zone->expedition_cache.emplace(expedition_id, std::move(expedition)); + return inserted.first->second.get(); + } + + return nullptr; +} + +void Expedition::CacheExpeditions(MySQLRequestResult& results) +{ + if (!results.Success() || !zone) + { + return; + } + + uint32_t last_expedition_id = 0; + for (auto row = results.begin(); row != results.end(); ++row) + { + auto expedition_id = strtoul(row[0], nullptr, 10); + if (expedition_id != last_expedition_id) + { + auto leader_id = static_cast(strtoul(row[3], nullptr, 10)); + ExpeditionMember leader{ leader_id, row[7] }; // id, name + + std::unique_ptr expedition = std::make_unique( + expedition_id, + row[2], // expedition name + leader, // expedition leader + strtoul(row[4], nullptr, 10), // min_players + strtoul(row[5], nullptr, 10), // max_players + (strtoul(row[6], nullptr, 10) != 0) // has_replay_timer + ); + + expedition->LoadMembers(); + expedition->SendUpdatesToZoneMembers(); + + // don't bother caching empty expeditions + if (expedition->GetMemberCount() > 0) + { + expedition->SendWorldGetOnlineMembers(); + zone->expedition_cache.emplace(expedition_id, std::move(expedition)); + } + } + + last_expedition_id = expedition_id; + + // optional lockouts from left join + if (row[8] && row[9] && row[10] && row[11]) + { + auto it = zone->expedition_cache.find(last_expedition_id); + if (it != zone->expedition_cache.end()) + { + it->second->AddInternalLockout(ExpeditionLockoutTimer{ + row[2], // expedition_name + row[8], // event_name + strtoull(row[9], nullptr, 10), // expire_time + static_cast(strtoul(row[10], nullptr, 10)), // original duration + (strtoul(row[11], nullptr, 10) != 0) // is_inherited + }); + } + } + } +} + +void Expedition::CacheFromDatabase(uint32_t expedition_id) +{ + if (zone) + { + auto start = std::chrono::steady_clock::now(); + + auto results = ExpeditionDatabase::LoadExpedition(expedition_id); + if (!results.Success()) + { + LogExpeditions("Failed to load Expedition [{}] for zone cache", expedition_id); + return; + } + + CacheExpeditions(results); + + auto end = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast>(end - start); + LogExpeditions("Caching new expedition [{}] took {}s", expedition_id, elapsed.count()); + } +} + +bool Expedition::CacheAllFromDatabase() +{ + if (!zone) + { + return false; + } + + auto start = std::chrono::steady_clock::now(); + + zone->expedition_cache.clear(); + + // load all active expeditions and members to current zone cache + auto results = ExpeditionDatabase::LoadAllExpeditions(); + if (!results.Success()) + { + LogExpeditions("Failed to load Expeditions for zone cache"); + return false; + } + + CacheExpeditions(results); + + auto end = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast>(end - start); + LogExpeditions("Caching [{}] expeditions took {}s", zone->expedition_cache.size(), elapsed.count()); + + return true; +} + +void Expedition::LoadAllClientLockouts(Client* client) +{ + if (!client) + { + return; + } + + auto results = ExpeditionDatabase::LoadCharacterLockouts(client->CharacterID()); + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + auto expire_time = strtoull(row[0], nullptr, 10); + auto original_duration = static_cast(strtoul(row[1], nullptr, 10)); + ExpeditionLockoutTimer lockout{ row[2], row[3], expire_time, original_duration }; + client->AddExpeditionLockout(lockout); + } + } + client->SendExpeditionLockoutTimers(); +} + +void Expedition::LoadMembers() +{ + m_members.clear(); + + auto results = ExpeditionDatabase::LoadExpeditionMembers(m_id); + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + auto character_id = strtoul(row[0], nullptr, 10); + bool is_current_member = strtoul(row[1], nullptr, 10); + AddInternalMember(row[2], character_id, is_current_member, true); + } + } +} + +void Expedition::SaveLockouts(ExpeditionRequest& request) +{ + m_lockouts = std::move(request).TakeLockouts(); + ExpeditionDatabase::InsertLockouts(m_id, m_lockouts); +} + +void Expedition::SaveMembers(ExpeditionRequest& request) +{ + m_members = std::move(request).TakeMembers(); + for (const auto& member : m_members) + { + m_member_id_history.emplace(member.char_id); + } + ExpeditionDatabase::InsertMembers(m_id, m_members); +} + +Expedition* Expedition::FindCachedExpeditionByCharacterID(uint32_t character_id) +{ + if (zone) + { + for (const auto& expedition : zone->expedition_cache) + { + if (expedition.second->HasMember(character_id)) + { + return expedition.second.get(); + } + } + } + return nullptr; +} + +Expedition* Expedition::FindCachedExpeditionByCharacterName(const std::string& char_name) +{ + if (zone) + { + for (const auto& expedition : zone->expedition_cache) + { + if (expedition.second->HasMember(char_name)) + { + return expedition.second.get(); + } + } + } + return nullptr; +} + +Expedition* Expedition::FindCachedExpeditionByID(uint32_t expedition_id) +{ + if (zone && expedition_id) + { + auto expedition_cache_iter = zone->expedition_cache.find(expedition_id); + if (expedition_cache_iter != zone->expedition_cache.end()) + { + return expedition_cache_iter->second.get(); + } + } + return nullptr; +} + +Expedition* Expedition::FindExpeditionByInstanceID(uint32_t instance_id) +{ + // ask database since it may have expired + auto expedition_id = ExpeditionDatabase::GetExpeditionIDFromInstanceID(instance_id); + return Expedition::FindCachedExpeditionByID(expedition_id); +} + +bool Expedition::HasLockout(const std::string& event_name) +{ + return (m_lockouts.find(event_name) != m_lockouts.end()); +} + +bool Expedition::HasReplayLockout() +{ + return (m_lockouts.find(DZ_REPLAY_TIMER_NAME) != m_lockouts.end()); +} + +bool Expedition::HasMember(uint32_t character_id) +{ + for (const auto& member : m_members) + { + if (member.char_id == character_id) + { + return true; + } + } + return false; +} + +bool Expedition::HasMember(const std::string& name) +{ + for (const auto& member : m_members) + { + if (strcasecmp(member.name.c_str(), name.c_str()) == 0) + { + return true; + } + } + return false; +} + +ExpeditionMember Expedition::GetMemberData(uint32_t character_id) +{ + ExpeditionMember member_data; + for (const auto& member : m_members) + { + if (member.char_id == character_id) + { + member_data = member; + break; + } + } + return member_data; +} + +ExpeditionMember Expedition::GetMemberData(const std::string& character_name) +{ + ExpeditionMember member_data; + for (const auto& member : m_members) + { + if (strcasecmp(member.name.c_str(), character_name.c_str()) == 0) + { + member_data = member; + break; + } + } + return member_data; +} + +void Expedition::AddReplayLockout(uint32_t seconds) +{ + AddLockout(DZ_REPLAY_TIMER_NAME, seconds); +} + +void Expedition::AddLockout(const std::string& event_name, uint32_t seconds) +{ + auto expire_at = std::chrono::system_clock::now() + std::chrono::seconds(seconds); + auto expire_time = static_cast(std::chrono::system_clock::to_time_t(expire_at)); + + // both expedition and current members get the lockout data, expirations updated on duplicates + ExpeditionLockoutTimer lockout{ m_expedition_name, event_name, expire_time, seconds }; + + ExpeditionDatabase::InsertLockout(m_id, lockout); + ExpeditionDatabase::InsertMembersLockout(m_members, lockout); + + ProcessLockoutUpdate(event_name, expire_time, seconds, false); + SendWorldLockoutUpdate(event_name, expire_time, seconds); +} + +void Expedition::RemoveLockout(const std::string& event_name) +{ + ExpeditionDatabase::DeleteLockout(m_id, event_name); + ExpeditionDatabase::DeleteMembersLockout(m_members, m_expedition_name, event_name); + + ProcessLockoutUpdate(event_name, 0, 0, true); + SendWorldLockoutUpdate(event_name, 0, 0, true); +} + +void Expedition::AddInternalLockout(ExpeditionLockoutTimer&& lockout_timer) +{ + m_lockouts.emplace(lockout_timer.GetEventName(), std::move(lockout_timer)); +} + +void Expedition::AddInternalMember( + const std::string& char_name, uint32_t character_id, bool is_current_member, bool offline) +{ + if (is_current_member) + { + auto it = std::find_if(m_members.begin(), m_members.end(), + [character_id](const ExpeditionMember& member) { + return member.char_id == character_id; + }); + + if (it == m_members.end()) + { + auto status = offline ? ExpeditionMemberStatus::Offline : ExpeditionMemberStatus::Online; + m_members.emplace_back(ExpeditionMember{character_id, char_name, status}); + } + } + + m_member_id_history.emplace(character_id); +} + +bool Expedition::AddMember(const std::string& add_char_name, uint32_t add_char_id) +{ + if (HasMember(add_char_id)) + { + return false; + } + + ExpeditionDatabase::InsertMember(m_id, add_char_id); + + ProcessMemberAdded(add_char_name, add_char_id); + SendWorldMemberChanged(add_char_name, add_char_id, false); + + return true; +} + +void Expedition::RemoveAllMembers() +{ + ExpeditionDatabase::DeleteAllMembers(m_id); + ExpeditionDatabase::DeleteExpedition(m_id); + + SendUpdatesToZoneMembers(true); + SendWorldExpeditionUpdate(true); +} + +bool Expedition::RemoveMember(const std::string& remove_char_name) +{ + auto member = GetMemberData(remove_char_name); + if (member.char_id == 0 || member.name.empty()) + { + return false; + } + + ExpeditionDatabase::UpdateMemberRemoved(m_id, member.char_id); + + ProcessMemberRemoved(member.name, member.char_id); + SendWorldMemberChanged(member.name, member.char_id, true); + + // live always sends a leader update but we can send only if leader changes + if (!m_members.empty() && member.char_id == m_leader.char_id) + { + ChooseNewLeader(); + } + + if (m_members.empty()) + { + // cache removal will occur in world message handler + ExpeditionDatabase::DeleteExpedition(m_id); + } + + return true; +} + +void Expedition::SwapMember(Client* add_client, const std::string& remove_char_name) +{ + if (!add_client || remove_char_name.empty()) + { + return; + } + + auto member = GetMemberData(remove_char_name); + if (member.char_id == 0 || member.name.empty()) + { + return; + } + + // make remove and add atomic to avoid racing with separate world messages + ExpeditionDatabase::UpdateMemberRemoved(m_id, member.char_id); + ExpeditionDatabase::InsertMember(m_id, add_client->CharacterID()); + + ProcessMemberRemoved(member.name, member.char_id); + ProcessMemberAdded(add_client->GetName(), add_client->CharacterID()); + SendWorldMemberSwapped(member.name, member.char_id, add_client->GetName(), add_client->CharacterID()); + + if (!m_members.empty() && member.char_id == m_leader.char_id) + { + ChooseNewLeader(); + } +} + +void Expedition::SetMemberStatus(Client* client, ExpeditionMemberStatus status) +{ + if (client) + { + UpdateMemberStatus(client->CharacterID(), status); + SendWorldMemberStatus(client->CharacterID(), status); + } +} + +void Expedition::UpdateMemberStatus(uint32_t update_member_id, ExpeditionMemberStatus status) +{ + auto member_data = GetMemberData(update_member_id); + if (member_data.char_id == 0 || member_data.name.empty()) + { + return; + } + + auto outapp_member_status = CreateMemberListStatusPacket(member_data.name, status); + + for (auto& member : m_members) + { + if (member.char_id == update_member_id) + { + member.status = status; + } + + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->QueuePacket(outapp_member_status.get()); + } + } +} + +bool Expedition::ChooseNewLeader() +{ + for (const auto& member : m_members) + { + if (member.char_id != m_leader.char_id) + { + LogExpeditionsModerate("Replacing leader [{}] with [{}]", m_leader.name, member.name); + SetNewLeader(member.char_id, member.name); + return true; + } + } + return false; +} + +void Expedition::SendClientExpeditionInvite( + Client* client, const std::string& inviter_name, const std::string& swap_remove_name) +{ + if (!client) + { + return; + } + + LogExpeditionsModerate( + "Sending expedition [{}] invite to player [{}] inviter [{}] swap name [{}]", + m_id, client->GetName(), inviter_name, swap_remove_name + ); + + client->SetPendingExpeditionInvite(m_id); + + client->MessageString( + Chat::System, EXPEDITION_ASKED_TO_JOIN, m_leader.name.c_str(), m_expedition_name.c_str() + ); + + // live (as of March 11 2020 patch) sends warnings for lockouts added + // during current expedition that client would receive on entering dz + bool warned = false; + for (const auto& lockout_iter : m_lockouts) + { + // live doesn't issue a warning for the dz's replay timer + const ExpeditionLockoutTimer& lockout = lockout_iter.second; + if (!lockout.IsInherited() && !lockout.IsReplayTimer() && + !client->HasExpeditionLockout(m_expedition_name, lockout.GetEventName())) + { + if (!warned) + { + client->Message(Chat::System, DZADD_INVITE_WARNING, m_expedition_name.c_str()); + warned = true; + } + + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + client->Message( + Chat::System, DZADD_INVITE_WARNING_TIMER, + lockout.GetEventName().c_str(), + time_remaining.days.c_str(), + time_remaining.hours.c_str(), + time_remaining.mins.c_str() + ); + } + } + + auto outapp = CreateInvitePacket(inviter_name, swap_remove_name); + client->QueuePacket(outapp.get()); +} + +void Expedition::SendLeaderMessage( + Client* leader_client, uint16_t chat_type, uint32_t string_id, const std::initializer_list& parameters) +{ + Client::SendCrossZoneMessageString(leader_client, m_leader.name, chat_type, string_id, parameters); +} + +bool Expedition::ProcessAddConflicts(Client* leader_client, Client* add_client, bool swapping) +{ + if (!add_client) // a null leader_client handled by SendLeaderMessage fallback + { + return true; + } + + bool has_conflict = false; + + auto expedition_id = add_client->GetExpeditionID(); + if (expedition_id) + { + auto string_id = (expedition_id == GetID()) ? DZADD_ALREADY_PART : DZADD_ALREADY_ASSIGNED; + SendLeaderMessage(leader_client, Chat::Red, string_id, { add_client->GetName() }); + has_conflict = true; + } + + // client with a replay lockout is allowed only if they were a previous member + auto member_iter = m_member_id_history.find(add_client->CharacterID()); + bool was_member = (member_iter != m_member_id_history.end()); + if (!was_member && m_has_replay_timer) + { + auto replay_lockout = add_client->GetExpeditionLockout(m_expedition_name, DZ_REPLAY_TIMER_NAME); + if (replay_lockout) + { + has_conflict = true; + + auto time_remaining = replay_lockout->GetDaysHoursMinutesRemaining(); + SendLeaderMessage(leader_client, Chat::Red, DZADD_REPLAY_TIMER, { + add_client->GetName(), + time_remaining.days, + time_remaining.hours, + time_remaining.mins + }); + } + } + + // check any extra event lockouts for this expedition that the client has and leader doesn't + auto client_lockouts = add_client->GetExpeditionLockouts(m_expedition_name); + for (const auto& client_lockout : client_lockouts) + { + bool is_missing_lockout = (m_lockouts.find(client_lockout.GetEventName()) == m_lockouts.end()); + if (!client_lockout.IsReplayTimer() && is_missing_lockout) + { + has_conflict = true; + + auto time_remaining = client_lockout.GetDaysHoursMinutesRemaining(); + SendLeaderMessage(leader_client, Chat::Red, DZADD_EVENT_TIMER, { + add_client->GetName(), + client_lockout.GetEventName(), + time_remaining.days, + time_remaining.hours, + time_remaining.mins, + client_lockout.GetEventName() + }); + } + } + + // swapping ignores the max player count check since it's a 1:1 change + if (!swapping && GetMemberCount() >= m_max_players) + { + SendLeaderMessage(leader_client, Chat::Red, DZADD_EXCEED_MAX, { fmt::format_int(m_max_players).str() }); + has_conflict = true; + } + + auto invite_id = add_client->GetPendingExpeditionInviteID(); + if (invite_id) + { + auto string_id = (invite_id == GetID()) ? DZADD_PENDING : DZADD_PENDING_OTHER; + SendLeaderMessage(leader_client, Chat::Red, string_id, { add_client->GetName() }); + has_conflict = true; + } + + return has_conflict; +} + +void Expedition::DzInviteResponse( + Client* add_client, bool accepted, bool has_swap_name, std::string swap_remove_name) +{ + if (!add_client) + { + return; + } + + LogExpeditionsModerate( + "Invite response by [{}] accepted [{}] swapping [{}] swap_name [{}]", + add_client->GetName(), accepted, has_swap_name, swap_remove_name + ); + + // if client accepts the invite we need to re-confirm there's no conflicts + // note current leader receives invite reply messages (if leader changed) + bool was_swap_invite = (has_swap_name && !swap_remove_name.empty()); + + // null leader_client is handled by SendLeaderMessage fallbacks + Client* leader_client = entity_list.GetClientByCharID(m_leader.char_id); + + bool has_conflicts = false; + if (accepted) + { + has_conflicts = ProcessAddConflicts(leader_client, add_client, was_swap_invite); + } + + // error if swapping and character was already removed before the accept + if (accepted && was_swap_invite && !HasMember(swap_remove_name)) + { + has_conflicts = true; + } + + if (accepted && !has_conflicts) + { + SendLeaderMessage(leader_client, Chat::Yellow, EXPEDITION_INVITE_ACCEPTED, { add_client->GetName() }); + } + else if (accepted) + { + SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_ERROR, { add_client->GetName() }); + } + else + { + SendLeaderMessage(leader_client, Chat::Red, EXPEDITION_INVITE_DECLINED, { add_client->GetName() }); + } + + if (accepted && !has_conflicts) + { + // insert pending lockouts client will receive when entering dynamic zone. + // only lockouts missing from the client when they join are added. all + // missing lockouts are not applied on entering instance because client may + // have a lockout that expires after joining and shouldn't receive it again. + ExpeditionDatabase::DeletePendingLockouts(add_client->CharacterID()); + + std::vector pending_lockouts; + for (const auto& lockout_iter : m_lockouts) + { + const ExpeditionLockoutTimer& lockout = lockout_iter.second; + if (!lockout.IsInherited() && + !add_client->HasExpeditionLockout(m_expedition_name, lockout.GetEventName())) + { + // replay timers are added to characters immediately on joining with + // a fresh expire time using the original duration + if (m_has_replay_timer && lockout.IsReplayTimer()) + { + add_client->AddNewExpeditionLockout( + lockout.GetExpeditionName(), lockout.GetEventName(), lockout.GetDuration()); + } + else + { + pending_lockouts.emplace_back(lockout); + } + } + } + + ExpeditionDatabase::InsertCharacterLockouts(add_client->CharacterID(), pending_lockouts, false, true); + + if (was_swap_invite) + { + SwapMember(add_client, swap_remove_name); + } + else + { + AddMember(add_client->GetName(), add_client->CharacterID()); + } + } +} + +bool Expedition::ConfirmLeaderCommand(Client* requester) +{ + if (!requester) + { + return false; + } + + ExpeditionMember leader; + if (RuleB(Expedition, UseDatabaseToVerifyLeaderCommands)) + { + leader = ExpeditionDatabase::GetExpeditionLeader(m_id); + } + else + { + leader = m_leader; + } + + if (leader.char_id == 0) + { + requester->MessageString(Chat::Red, UNABLE_RETRIEVE_LEADER); // unconfirmed message + return false; + } + + if (leader.char_id != requester->CharacterID()) + { + requester->MessageString(Chat::Red, EXPEDITION_NOT_LEADER, leader.name.c_str()); + return false; + } + + return true; +} + +void Expedition::TryAddClient( + Client* add_client, std::string inviter_name, std::string orig_add_name, + std::string swap_remove_name, Client* leader_client) +{ + if (!add_client) + { + return; + } + + LogExpeditionsModerate( + "Add player request for expedition [{}] by inviter [{}] add name [{}] swap name [{}]", + m_id, inviter_name, orig_add_name, swap_remove_name + ); + + // null leader client handled by ProcessAddConflicts/SendLeaderMessage fallbacks + if (!leader_client) + { + leader_client = entity_list.GetClientByName(inviter_name.c_str()); + } + + bool has_conflicts = ProcessAddConflicts(leader_client, add_client, !swap_remove_name.empty()); + if (!has_conflicts) + { + // live uses the original unsanitized input string in invite messages + uint32_t string_id = swap_remove_name.empty() ? DZADD_INVITE : DZSWAP_INVITE; + SendLeaderMessage(leader_client, Chat::Yellow, string_id, { orig_add_name.c_str() }); + SendClientExpeditionInvite(add_client, inviter_name.c_str(), swap_remove_name); + } + else if (swap_remove_name.empty()) // swap command doesn't result in this message + { + SendLeaderMessage(leader_client, Chat::Red, DZADD_INVITE_FAIL, { add_client->GetName() }); + } +} + +void Expedition::DzAddPlayer( + Client* requester, std::string add_char_name, std::string swap_remove_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + if (add_char_name.empty()) + { + requester->MessageString(Chat::Red, DZADD_NOT_ONLINE, add_char_name.c_str()); + return; + } + + // live prioritizes the "not online" message before the "already a member" + // message but we can avoid checking world if we trust member status accuracy + // live sanitizes input except for "sending invite" and "not online" msgs + auto member_data = GetMemberData(add_char_name); + if (member_data.char_id != 0 && member_data.status != ExpeditionMemberStatus::Offline) + { + requester->MessageString(Chat::Red, DZADD_ALREADY_PART, add_char_name.c_str()); + return; + } + + Client* add_client = entity_list.GetClientByName(add_char_name.c_str()); + if (add_client) + { + // client is online in this zone + TryAddClient(add_client, requester->GetName(), add_char_name, swap_remove_name, requester); + } + else + { + // forward to world to check if client is online and perform cross-zone invite + SendWorldAddPlayerInvite(requester->GetName(), swap_remove_name, add_char_name); + } +} + +void Expedition::DzAddPlayerContinue( + std::string inviter_name, std::string add_name, std::string swap_remove_name) +{ + // continuing expedition invite from leader in another zone + Client* add_client = entity_list.GetClientByName(add_name.c_str()); + if (add_client) + { + TryAddClient(add_client, inviter_name, add_name, swap_remove_name); + } +} + +void Expedition::DzMakeLeader(Client* requester, std::string new_leader_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + // live uses sanitized input name for all /dzmakeleader messages + new_leader_name = FormatName(new_leader_name); + + if (new_leader_name.empty()) + { + requester->MessageString(Chat::Red, DZMAKELEADER_NOT_ONLINE, new_leader_name.c_str()); + return; + } + + auto new_leader_data = GetMemberData(new_leader_name); + if (new_leader_data.char_id == 0) + { + requester->MessageString(Chat::Red, EXPEDITION_NOT_MEMBER, new_leader_name.c_str()); + return; + } + + // database is not updated until new leader client validated + Client* new_leader_client = entity_list.GetClientByName(new_leader_name.c_str()); + if (new_leader_client) + { + ProcessMakeLeader(requester, new_leader_client, new_leader_name, true); + } + else + { + // new leader not in this zone, let world verify and pass to new leader's zone + SendWorldMakeLeaderRequest(requester->GetName(), FormatName(new_leader_name)); + } +} + +void Expedition::DzRemovePlayer(Client* requester, std::string char_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + LogExpeditionsModerate( + "Remove player request for expedition [{}] by [{}] leader [{}] remove name [{}]", + m_id, requester->GetName(), m_leader.name, char_name + ); + + char_name = FormatName(char_name); + + // live only seems to enforce min_players for requesting expeditions, no need to check here + bool removed = RemoveMember(char_name); + if (!removed) + { + requester->MessageString(Chat::Red, EXPEDITION_NOT_MEMBER, char_name.c_str()); + } + else + { + requester->MessageString(Chat::Yellow, EXPEDITION_REMOVED, char_name.c_str(), m_expedition_name.c_str()); + } +} + +void Expedition::DzQuit(Client* requester) +{ + if (requester) + { + RemoveMember(requester->GetName()); + } +} + +void Expedition::DzSwapPlayer( + Client* requester, std::string remove_char_name, std::string add_char_name) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + if (remove_char_name.empty() || !HasMember(remove_char_name)) + { + remove_char_name = FormatName(remove_char_name); + requester->MessageString(Chat::Red, DZSWAP_CANNOT_REMOVE, remove_char_name.c_str()); + return; + } + + DzAddPlayer(requester, add_char_name, remove_char_name); +} + +void Expedition::DzPlayerList(Client* requester) +{ + if (requester) + { + requester->MessageString(Chat::Yellow, EXPEDITION_LEADER, m_leader.name.c_str()); + + std::string member_names; + for (const auto& member : m_members) + { + fmt::format_to(std::back_inserter(member_names), "{}, ", member.name); + } + + if (member_names.size() > 1) + { + member_names.erase(member_names.length() - 2); // trailing comma and space + } + + requester->MessageString(Chat::Yellow, EXPEDITION_MEMBERS, member_names.c_str()); + } +} + +void Expedition::DzKickPlayers(Client* requester) +{ + if (!requester || !ConfirmLeaderCommand(requester)) + { + return; + } + + RemoveAllMembers(); + requester->MessageString(Chat::Red, EXPEDITION_REMOVED, KICKPLAYERS_EVERYONE, m_expedition_name.c_str()); +} + +void Expedition::SetNewLeader(uint32_t new_leader_id, const std::string& new_leader_name) +{ + ExpeditionDatabase::UpdateLeaderID(m_id, new_leader_id); + ProcessLeaderChanged(new_leader_id, new_leader_name); + SendWorldLeaderChanged(); +} + +void Expedition::ProcessLeaderChanged(uint32_t new_leader_id, const std::string& new_leader_name) +{ + m_leader.char_id = new_leader_id; + m_leader.name = new_leader_name; + + // update each client's expedition window in this zone + auto outapp_leader = CreateLeaderNamePacket(); + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->QueuePacket(outapp_leader.get()); + } + } +} + +void Expedition::ProcessMakeLeader( + Client* old_leader_client, Client* new_leader_client, const std::string& new_leader_name, bool is_online) +{ + if (old_leader_client) + { + // online flag is set by world to verify new leader is online or not + if (is_online) + { + old_leader_client->MessageString(Chat::Yellow, DZMAKELEADER_NAME, new_leader_name.c_str()); + } + else + { + old_leader_client->MessageString(Chat::Red, DZMAKELEADER_NOT_ONLINE, new_leader_name.c_str()); + } + } + + if (!new_leader_client) + { + new_leader_client = entity_list.GetClientByName(new_leader_name.c_str()); + } + + if (new_leader_client) + { + new_leader_client->MessageString(Chat::Yellow, DZMAKELEADER_YOU); + SetNewLeader(new_leader_client->CharacterID(), new_leader_client->GetName()); + } +} + +void Expedition::ProcessMemberAdded(std::string char_name, uint32_t added_char_id) +{ + // adds the member to this expedition and notifies both leader and new member + Client* leader_client = entity_list.GetClientByCharID(m_leader.char_id); + if (leader_client) + { + leader_client->MessageString(Chat::Yellow, EXPEDITION_MEMBER_ADDED, char_name.c_str(), m_expedition_name.c_str()); + } + + Client* member_client = entity_list.GetClientByCharID(added_char_id); + if (member_client) + { + member_client->SetExpeditionID(GetID()); + SendClientExpeditionInfo(member_client); + member_client->MessageString(Chat::Yellow, EXPEDITION_MEMBER_ADDED, char_name.c_str(), m_expedition_name.c_str()); + } + + AddInternalMember(char_name, added_char_id); + + SendUpdatesToZoneMembers(); // live sends full update when member added +} + +void Expedition::ProcessMemberRemoved(std::string removed_char_name, uint32_t removed_char_id) +{ + if (m_members.empty()) + { + return; + } + + // cache a re-usable packet for each member + auto outapp_member_name = CreateMemberListNamePacket(removed_char_name, true); + + for (auto it = m_members.begin(); it != m_members.end();) + { + bool is_removed = (it->name == removed_char_name); + + Client* member_client = entity_list.GetClientByCharID(it->char_id); + if (member_client) + { + // all members receive the removed player name packet + member_client->QueuePacket(outapp_member_name.get()); + + if (is_removed) + { + ExpeditionDatabase::DeletePendingLockouts(member_client->CharacterID()); + member_client->SetExpeditionID(0); + member_client->QueuePacket(CreateInfoPacket(true).get()); + member_client->MessageString( + Chat::Yellow, EXPEDITION_REMOVED, it->name.c_str(), m_expedition_name.c_str() + ); + } + } + + it = is_removed ? m_members.erase(it) : it + 1; + } +} + +void Expedition::ProcessLockoutUpdate( + const std::string& event_name, uint64_t expire_time, uint32_t duration, bool remove) +{ + ExpeditionLockoutTimer lockout{ m_expedition_name, event_name, expire_time, duration }; + + if (!remove) + { + m_lockouts.emplace(event_name, lockout); + } + else + { + m_lockouts.erase(event_name); + } + + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + if (!remove) + { + member_client->AddExpeditionLockout(lockout); + } + else + { + member_client->RemoveExpeditionLockout(m_expedition_name, event_name); + } + member_client->SendExpeditionLockoutTimers(); // full client lockout list update + } + } +} + +void Expedition::SendUpdatesToZoneMembers(bool clear) +{ + if (!m_members.empty()) + { + //auto outapp_compass = CreateCompassPacket(); + auto outapp_info = CreateInfoPacket(clear); + auto outapp_members = CreateMemberListPacket(clear); + + for (const auto& member : m_members) + { + Client* member_client = entity_list.GetClientByCharID(member.char_id); + if (member_client) + { + member_client->SetExpeditionID(clear ? 0 : GetID()); + member_client->QueuePacket(outapp_info.get()); + member_client->QueuePacket(outapp_members.get()); + member_client->SendExpeditionLockoutTimers(); + if (clear) + { + member_client->MessageString( + Chat::Yellow, EXPEDITION_REMOVED, member_client->GetName(), m_expedition_name.c_str() + ); + } + } + } + } +} + +void Expedition::SendClientExpeditionInfo(Client* client) +{ + if (client) + { + client->QueuePacket(CreateInfoPacket().get()); + client->QueuePacket(CreateMemberListPacket().get()); + } +} + +std::unique_ptr Expedition::CreateInfoPacket(bool clear) +{ + uint32_t outsize = sizeof(ExpeditionInfo_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionInfo, outsize)); + auto info = reinterpret_cast(outapp->pBuffer); + if (!clear) + { + info->client_id = 0; + info->assigned = true; + strn0cpy(info->expedition_name, m_expedition_name.c_str(), sizeof(info->expedition_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 Expedition::CreateInvitePacket( + const std::string& inviter_name, const std::string& swap_remove_name) +{ + uint32_t outsize = sizeof(ExpeditionInvite_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzExpeditionInvite, outsize)); + auto outbuf = reinterpret_cast(outapp->pBuffer); + strn0cpy(outbuf->inviter_name, inviter_name.c_str(), sizeof(outbuf->inviter_name)); + strn0cpy(outbuf->expedition_name, m_expedition_name.c_str(), sizeof(outbuf->expedition_name)); + strn0cpy(outbuf->swap_name, swap_remove_name.c_str(), sizeof(outbuf->swap_name)); + outbuf->swapping = !swap_remove_name.empty(); + //outbuf->dz_zone_id = m_dynamiczone.GetZoneID(); + //outbuf->dz_instance_id = m_dynamiczone.GetInstanceID(); + return outapp; +} + +std::unique_ptr Expedition::CreateMemberListPacket(bool clear) +{ + uint32_t member_count = clear ? 0 : static_cast(m_members.size()); + uint32_t member_entries_size = sizeof(ExpeditionMemberEntry_Struct) * member_count; + uint32_t outsize = sizeof(ExpeditionMemberList_Struct) + member_entries_size; + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzMemberList, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + + buf->client_id = 0; + buf->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].status = static_cast(m_members[i].status); + } + } + + return outapp; +} + +std::unique_ptr Expedition::CreateMemberListNamePacket( + const std::string& name, bool remove_name) +{ + uint32_t outsize = sizeof(ExpeditionMemberListName_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzMemberListName, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + buf->client_id = 0; + buf->add_name = !remove_name; + strn0cpy(buf->name, name.c_str(), sizeof(buf->name)); + return outapp; +} + +std::unique_ptr Expedition::CreateMemberListStatusPacket( + const std::string& name, ExpeditionMemberStatus status) +{ + // member list status uses member list struct with a single entry + uint32_t outsize = sizeof(ExpeditionMemberList_Struct) + sizeof(ExpeditionMemberEntry_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzMemberListStatus, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + buf->client_id = 0; + buf->count = 1; + + auto entry = reinterpret_cast(buf->members); + strn0cpy(entry->name, name.c_str(), sizeof(entry->name)); + entry->status = static_cast(status); + + return outapp; +} + +std::unique_ptr Expedition::CreateLeaderNamePacket() +{ + uint32_t outsize = sizeof(ExpeditionSetLeaderName_Struct); + auto outapp = std::unique_ptr(new EQApplicationPacket(OP_DzSetLeaderName, outsize)); + auto buf = reinterpret_cast(outapp->pBuffer); + buf->client_id = 0; + strn0cpy(buf->leader_name, m_leader.name.c_str(), sizeof(buf->leader_name)); + return outapp; +} + +void Expedition::SendWorldExpeditionUpdate(bool destroyed) +{ + uint16_t opcode = destroyed ? ServerOP_ExpeditionDeleted : ServerOP_ExpeditionCreate; + uint32_t pack_size = sizeof(ServerExpeditionID_Struct); + auto pack = std::unique_ptr(new ServerPacket(opcode, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldAddPlayerInvite( + const std::string& inviter_name, const std::string& swap_remove_name, const std::string& add_name) +{ + uint32_t pack_size = sizeof(ServerDzCommand_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionDzAddPlayer, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->is_char_online = false; + strn0cpy(buf->requester_name, inviter_name.c_str(), sizeof(buf->requester_name)); + strn0cpy(buf->target_name, add_name.c_str(), sizeof(buf->target_name)); + strn0cpy(buf->remove_name, swap_remove_name.c_str(), sizeof(buf->remove_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldLeaderChanged() +{ + uint32_t pack_size = sizeof(ServerExpeditionMemberChange_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionLeaderChanged, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->char_id = m_leader.char_id; + strn0cpy(buf->char_name, m_leader.name.c_str(), sizeof(buf->char_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldLockoutUpdate( + const std::string& event_name, uint64_t expire_time, uint32_t duration, bool remove) +{ + uint32_t pack_size = sizeof(ServerExpeditionLockout_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionLockout, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->expire_time = expire_time; + buf->duration = duration; + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->remove = remove; + strn0cpy(buf->event_name, event_name.c_str(), sizeof(buf->event_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMakeLeaderRequest( + const std::string& requester_name, const std::string& new_leader_name) +{ + uint32_t pack_size = sizeof(ServerDzCommand_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionDzMakeLeader, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->is_char_online = false; + strn0cpy(buf->requester_name, requester_name.c_str(), sizeof(buf->requester_name)); + strn0cpy(buf->target_name, new_leader_name.c_str(), sizeof(buf->target_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMemberChanged(const std::string& char_name, uint32_t char_id, bool remove) +{ + // notify other zones of added or removed member + uint32_t pack_size = sizeof(ServerExpeditionMemberChange_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionMemberChange, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->removed = remove; + buf->char_id = char_id; + strn0cpy(buf->char_name, char_name.c_str(), sizeof(buf->char_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMemberStatus(uint32_t character_id, ExpeditionMemberStatus status) +{ + uint32_t pack_size = sizeof(ServerExpeditionMemberStatus_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionMemberStatus, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->status = static_cast(status); + buf->character_id = character_id; + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldMemberSwapped( + const std::string& remove_char_name, uint32_t remove_char_id, const std::string& add_char_name, uint32_t add_char_id) +{ + uint32_t pack_size = sizeof(ServerExpeditionMemberSwap_Struct); + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionMemberSwap, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->add_char_id = add_char_id; + buf->remove_char_id = remove_char_id; + strn0cpy(buf->add_char_name, add_char_name.c_str(), sizeof(buf->add_char_name)); + strn0cpy(buf->remove_char_name, remove_char_name.c_str(), sizeof(buf->remove_char_name)); + worldserver.SendPacket(pack.get()); +} + +void Expedition::SendWorldGetOnlineMembers() +{ + // request online status of all characters in our expedition tracked by world + uint32_t count = static_cast(m_members.size()); + uint32_t entries_size = sizeof(ServerExpeditionCharacterEntry_Struct) * count; + uint32_t pack_size = sizeof(ServerExpeditionCharacters_Struct) + entries_size; + auto pack = std::unique_ptr(new ServerPacket(ServerOP_ExpeditionGetOnlineMembers, pack_size)); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->sender_zone_id = zone ? zone->GetZoneID() : 0; + buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; + buf->count = count; + for (uint32_t i = 0; i < buf->count; ++i) + { + buf->entries[i].character_id = m_members[i].char_id; + buf->entries[i].character_zone_id = 0; + buf->entries[i].character_instance_id = 0; + buf->entries[i].character_online = false; + } + worldserver.SendPacket(pack.get()); +} + +void Expedition::HandleWorldMessage(ServerPacket* pack) +{ + switch (pack->opcode) + { + case ServerOP_ExpeditionCreate: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + Expedition::CacheFromDatabase(buf->expedition_id); + } + break; + } + case ServerOP_ExpeditionDeleted: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->SendUpdatesToZoneMembers(true); + } + } + break; + } + case ServerOP_ExpeditionLeaderChanged: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->ProcessLeaderChanged(buf->char_id, buf->char_name); + } + } + break; + } + case ServerOP_ExpeditionLockout: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->ProcessLockoutUpdate(buf->event_name, buf->expire_time, buf->duration, buf->remove); + } + } + break; + } + case ServerOP_ExpeditionMemberChange: + { + auto buf = reinterpret_cast(pack->pBuffer); + + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition && zone) + { + if (!zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + if (buf->removed) + { + expedition->ProcessMemberRemoved(buf->char_name, buf->char_id); + } + else + { + expedition->ProcessMemberAdded(buf->char_name, buf->char_id); + } + } + + // remove this expedition from zone cache if last member was removed + if (buf->removed && expedition->GetMemberCount() == 0) + { + zone->expedition_cache.erase(buf->expedition_id); + } + } + break; + } + case ServerOP_ExpeditionMemberSwap: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->ProcessMemberRemoved(buf->remove_char_name, buf->remove_char_id); + expedition->ProcessMemberAdded(buf->add_char_name, buf->add_char_id); + } + } + break; + } + case ServerOP_ExpeditionMemberStatus: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (zone && !zone->IsZone(buf->sender_zone_id, buf->sender_instance_id)) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->UpdateMemberStatus(buf->character_id, static_cast(buf->status)); + } + } + break; + } + case ServerOP_ExpeditionGetOnlineMembers: + { + // reply from world for online member statuses request + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + for (uint32_t i = 0; i < buf->count; ++i) + { + auto entry = reinterpret_cast(&buf->entries[i]); + auto is_online = entry->character_online; + auto status = is_online ? ExpeditionMemberStatus::Online : ExpeditionMemberStatus::Offline; + if (is_online && expedition->GetInstanceID() == entry->character_instance_id) + { + status = ExpeditionMemberStatus::InDynamicZone; + } + expedition->UpdateMemberStatus(entry->character_id, status); + } + } + break; + } + case ServerOP_ExpeditionDzAddPlayer: + { + auto buf = reinterpret_cast(pack->pBuffer); + if (buf->is_char_online) + { + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + expedition->DzAddPlayerContinue(buf->requester_name, buf->target_name, buf->remove_name); + } + } + else + { + Client* leader = entity_list.GetClientByName(buf->requester_name); + if (leader) + { + leader->MessageString(Chat::Red, DZADD_NOT_ONLINE, FormatName(buf->target_name).c_str()); + } + } + break; + } + case ServerOP_ExpeditionDzMakeLeader: + { + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) + { + auto old_leader_client = entity_list.GetClientByName(buf->requester_name); + auto new_leader_client = entity_list.GetClientByName(buf->target_name); + expedition->ProcessMakeLeader(old_leader_client, new_leader_client, buf->target_name, buf->is_char_online); + } + break; + } + } +} diff --git a/zone/expedition.h b/zone/expedition.h new file mode 100644 index 000000000..cebe46e7f --- /dev/null +++ b/zone/expedition.h @@ -0,0 +1,188 @@ +/** + * 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 + * + */ + +#ifndef EXPEDITION_H +#define EXPEDITION_H + +#include "expedition_lockout_timer.h" +#include +#include +#include +#include +#include +#include + +class Client; +class EQApplicationPacket; +class ExpeditionRequest; +class MySQLRequestResult; +class ServerPacket; + +extern const char* const DZ_YOU_NOT_ASSIGNED; +extern const char* const EXPEDITION_OTHER_BELONGS; + +enum class DynamicZoneType : uint8_t // DynamicZoneActiveType +{ + None = 0, + Expedition, + Tutorial, + Task, + Mission, + Quest +}; + +enum class ExpeditionMemberStatus : uint8_t +{ + Unknown = 0, + Online, + Offline, + InDynamicZone, + LinkDead +}; + +struct ExpeditionMember +{ + uint32_t char_id = 0; + std::string name; + ExpeditionMemberStatus status = ExpeditionMemberStatus::Online; + + ExpeditionMember() {} + ExpeditionMember(uint32_t char_id_, const std::string& name_) : char_id(char_id_), name(name_) {} + ExpeditionMember(uint32_t char_id_, const std::string& name_, ExpeditionMemberStatus status_) + : char_id(char_id_), name(name_), status(status_) {} +}; + +class Expedition +{ +public: + Expedition() = delete; + Expedition(uint32_t id, std::string expedition_name, const ExpeditionMember& leader, + uint32_t min_players, uint32_t max_players, bool replay_timer); + + static Expedition* TryCreate( + Client* requester, std::string name, uint32_t min_players, uint32_t max_players, bool replay_timer); + static void CacheFromDatabase(uint32_t expedition_id); + static bool CacheAllFromDatabase(); + static void CacheExpeditions(MySQLRequestResult& results); + static void LoadAllClientLockouts(Client* client); + static Expedition* FindCachedExpeditionByCharacterID(uint32_t character_id); + static Expedition* FindCachedExpeditionByCharacterName(const std::string& char_name); + static Expedition* FindCachedExpeditionByID(uint32_t expedition_id); + static Expedition* FindExpeditionByInstanceID(uint32_t instance_id); + static void HandleWorldMessage(ServerPacket* pack); + + uint32_t GetID() const { return m_id; } + uint32_t GetLeaderID() const { return m_leader.char_id; } + uint32_t GetMinPlayers() const { return m_min_players; } + uint32_t GetMaxPlayers() const { return m_max_players; } + uint32_t GetMemberCount() const { return static_cast(m_members.size()); } + const std::string& GetName() const { return m_expedition_name; } + const std::string& GetLeaderName() const { return m_leader.name; } + const std::unordered_map& GetLockouts() const { return m_lockouts; } + const std::vector& GetMembers() const { return m_members; } + + bool AddMember(const std::string& add_char_name, uint32_t add_char_id); + bool HasMember(const std::string& name); + bool HasMember(uint32_t character_id); + void RemoveAllMembers(); + bool RemoveMember(const std::string& remove_char_name); + void SetMemberStatus(Client* client, ExpeditionMemberStatus status); + void SetNewLeader(uint32_t new_leader_id, const std::string& new_leader_name); + void SwapMember(Client* add_client, const std::string& remove_char_name); + + void AddLockout(const std::string& event_name, uint32_t seconds); + void AddReplayLockout(uint32_t seconds); + bool HasLockout(const std::string& event_name); + bool HasReplayLockout(); + void RemoveLockout(const std::string& event_name); + + void SendClientExpeditionInfo(Client* client); + + void DzAddPlayer(Client* requester, std::string add_char_name, std::string swap_remove_name = {}); + void DzAddPlayerContinue(std::string leader_name, std::string add_char_name, std::string swap_remove_name = {}); + void DzInviteResponse(Client* add_client, bool accepted, bool has_swap_name, std::string swap_remove_name); + void DzMakeLeader(Client* requester, std::string new_leader_name); + void DzPlayerList(Client* requester); + void DzRemovePlayer(Client* requester, std::string remove_char_name); + void DzSwapPlayer(Client* requester, std::string remove_char_name, std::string add_char_name); + void DzQuit(Client* requester); + void DzKickPlayers(Client* requester); + +#if 0 + bool AssignInstance(uint32_t instance_id, bool update_db = true); + uint32_t CreateInstance(std::string zone, uint32_t version, uint32_t duration); // m_dynamiczone +#endif + uint32_t GetInstanceID() const { return 77; /*return m_instance_id;*/ } // todo: GetDynamicZoneID() + DynamicZoneType GetType() const { return DynamicZoneType::Expedition; } // m_dynamiczone + + static const uint32_t REPLAY_TIMER_ID; + static const uint32_t EVENT_TIMER_ID; + +private: + void AddInternalLockout(ExpeditionLockoutTimer&& lockout_timer); + void AddInternalMember(const std::string& char_name, uint32_t char_id, bool is_current_member = true, bool offline = false); + bool ChooseNewLeader(); + bool ConfirmLeaderCommand(Client* requester); + void LoadMembers(); + bool ProcessAddConflicts(Client* leader_client, Client* add_client, bool swapping); + void ProcessLeaderChanged(uint32_t new_leader_id, const std::string& new_leader_name); + void ProcessLockoutUpdate(const std::string& event_name, uint64_t expire_time, uint32_t duration, bool remove); + void ProcessMakeLeader(Client* old_leader, Client* new_leader, const std::string& new_leader_name, bool is_online); + void ProcessMemberAdded(std::string added_char_name, uint32_t added_char_id); + void ProcessMemberRemoved(std::string removed_char_name, uint32_t removed_char_id); + void SaveLockouts(ExpeditionRequest& request); + void SaveMembers(ExpeditionRequest& request); + void SendClientExpeditionInvite(Client* client, const std::string& inviter_name, const std::string& swap_remove_name); + void SendLeaderMessage(Client* leader_client, uint16_t chat_type, uint32_t string_id, const std::initializer_list& parameters = {}); + void SendUpdatesToZoneMembers(bool clear = false); + void SendWorldExpeditionUpdate(bool destroyed = false); + void SendWorldGetOnlineMembers(); + void SendWorldAddPlayerInvite(const std::string& inviter_name, const std::string& swap_remove_name, const std::string& add_name); + void SendWorldLeaderChanged(); + void SendWorldLockoutUpdate(const std::string& event_name, uint64_t expire_time, uint32_t duration, bool remove = false); + void SendWorldMakeLeaderRequest(const std::string& requester_name, const std::string& new_leader_name); + void SendWorldMemberChanged(const std::string& char_name, uint32_t char_id, bool remove); + void SendWorldMemberStatus(uint32_t character_id, ExpeditionMemberStatus status); + void SendWorldMemberSwapped(const std::string& remove_char_name, uint32_t remove_char_id, const std::string& add_char_name, uint32_t add_char_id); + void TryAddClient(Client* add_client, std::string inviter_name, std::string orig_add_name, std::string swap_remove_name, Client* leader_client = nullptr); + void UpdateMemberStatus(uint32_t update_character_id, ExpeditionMemberStatus status); + + ExpeditionMember GetMemberData(uint32_t character_id); + ExpeditionMember GetMemberData(const std::string& character_name); + std::unique_ptr CreateInfoPacket(bool clear = false); + std::unique_ptr CreateInvitePacket(const std::string& inviter_name, const std::string& swap_remove_name); + std::unique_ptr CreateMemberListPacket(bool clear = false); + std::unique_ptr CreateMemberListNamePacket(const std::string& name, bool remove_name); + std::unique_ptr CreateMemberListStatusPacket(const std::string& name, ExpeditionMemberStatus status); + std::unique_ptr CreateLeaderNamePacket(); + + uint32_t m_id = 0; + //uint32_t m_instance_id = 0; // todo: DynamicZone m_dynamiczone + uint32_t m_min_players = 0; + uint32_t m_max_players = 0; + bool m_has_replay_timer = false; + std::string m_expedition_name; + ExpeditionMember m_leader; + std::vector m_members; // current members + std::unordered_set m_member_id_history; // track past members to allow invites for replay timer bypass + std::unordered_map m_lockouts; +}; + +#endif diff --git a/zone/expedition_database.cpp b/zone/expedition_database.cpp new file mode 100644 index 000000000..e8ef8848e --- /dev/null +++ b/zone/expedition_database.cpp @@ -0,0 +1,608 @@ +/** + * 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 "expedition_database.h" +#include "expedition.h" +#include "expedition_lockout_timer.h" +#include "zonedb.h" +#include "../common/database.h" +#include + +uint32_t ExpeditionDatabase::InsertExpedition( + const std::string& expedition_name, uint32_t leader_id, + uint32_t min_players, uint32_t max_players, bool has_replay_lockout) +{ + LogExpeditionsDetail("Inserting new expedition [{}] leader [{}]", expedition_name, leader_id); + + std::string query = fmt::format(SQL( + INSERT INTO expedition_details + (expedition_name, leader_id, min_players, max_players, has_replay_timer) + VALUES + ('{}', {}, {}, {}, {}); + ), expedition_name, leader_id, min_players, max_players, has_replay_lockout); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to obtain an expedition id for [{}]", expedition_name); + return 0; + } + + return results.LastInsertedID(); +} + +MySQLRequestResult ExpeditionDatabase::LoadExpedition(uint32_t expedition_id) +{ + LogExpeditionsDetail("Loading expedition [{}]", expedition_id); + + // no point caching expedition if no members, inner join instead of left + std::string query = fmt::format(SQL( + SELECT + expedition_details.id, + expedition_details.instance_id, + expedition_details.expedition_name, + expedition_details.leader_id, + expedition_details.min_players, + expedition_details.max_players, + expedition_details.has_replay_timer, + character_data.name leader_name, + expedition_lockouts.event_name, + UNIX_TIMESTAMP(expedition_lockouts.expire_time), + expedition_lockouts.duration, + expedition_lockouts.is_inherited + FROM expedition_details + INNER JOIN character_data ON expedition_details.leader_id = character_data.id + LEFT JOIN expedition_lockouts + ON expedition_details.id = expedition_lockouts.expedition_id + AND expedition_lockouts.expire_time > NOW() + WHERE expedition_details.id = {}; + ), expedition_id); + + auto results = database.QueryDatabase(query); + return results; +} + +MySQLRequestResult ExpeditionDatabase::LoadAllExpeditions() +{ + LogExpeditionsDetail("Loading all expeditions from database"); + + // load all active expeditions and members to current zone cache + std::string query = SQL( + SELECT + expedition_details.id, + expedition_details.instance_id, + expedition_details.expedition_name, + expedition_details.leader_id, + expedition_details.min_players, + expedition_details.max_players, + expedition_details.has_replay_timer, + character_data.name leader_name, + expedition_lockouts.event_name, + UNIX_TIMESTAMP(expedition_lockouts.expire_time), + expedition_lockouts.duration, + expedition_lockouts.is_inherited + FROM expedition_details + INNER JOIN character_data ON expedition_details.leader_id = character_data.id + LEFT JOIN expedition_lockouts + ON expedition_details.id = expedition_lockouts.expedition_id + AND expedition_lockouts.expire_time > NOW() + ORDER BY expedition_details.id; + ); + + auto results = database.QueryDatabase(query); + return results; +} + +MySQLRequestResult ExpeditionDatabase::LoadCharacterLockouts(uint32_t character_id) +{ + LogExpeditionsDetail("Loading character [{}] lockouts", character_id); + + auto query = fmt::format(SQL( + SELECT + UNIX_TIMESTAMP(expire_time), + duration, + expedition_name, + event_name + FROM expedition_character_lockouts + WHERE character_id = {} AND is_pending = FALSE AND expire_time > NOW(); + ), character_id); + + return database.QueryDatabase(query); +} + +MySQLRequestResult ExpeditionDatabase::LoadCharacterLockouts( + uint32_t character_id, const std::string& expedition_name) +{ + LogExpeditionsDetail("Loading character [{}] lockouts for [{}]", character_id, expedition_name); + + auto query = fmt::format(SQL( + SELECT + UNIX_TIMESTAMP(expire_time), + duration, + event_name + FROM expedition_character_lockouts + WHERE + character_id = {} + AND is_pending = FALSE + AND expire_time > NOW() + AND expedition_name = '{}'; + ), character_id, expedition_name); + + return database.QueryDatabase(query); +} + +MySQLRequestResult ExpeditionDatabase::LoadExpeditionMembers(uint32_t expedition_id) +{ + LogExpeditionsDetail("Loading all members for expedition [{}]", expedition_id); + + std::string query = fmt::format(SQL( + SELECT + expedition_members.character_id, + expedition_members.is_current_member, + character_data.name + FROM expedition_members + INNER JOIN character_data ON expedition_members.character_id = character_data.id + WHERE expedition_id = {}; + ), expedition_id); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to load expedition [{}] members from db", expedition_id); + } + return results; +} + +MySQLRequestResult ExpeditionDatabase::LoadValidationData( + const std::string& character_names, const std::string& expedition_name) +{ + LogExpeditionsDetail("Loading multiple characters data for [{}] request validation", expedition_name); + + // for create validation, loads each character's lockouts and possible current expedition + auto query = fmt::format(SQL( + SELECT + character_data.id, + character_data.name, + member.expedition_id, + UNIX_TIMESTAMP(lockout.expire_time), + lockout.duration, + lockout.event_name + FROM character_data + LEFT JOIN expedition_character_lockouts lockout + ON character_data.id = lockout.character_id + AND lockout.is_pending = FALSE + AND lockout.expire_time > NOW() + AND lockout.expedition_name = '{}' + LEFT JOIN expedition_members member + ON character_data.id = member.character_id + AND member.is_current_member = TRUE + WHERE character_data.name IN ({}) + ORDER BY character_data.id; + ), expedition_name, character_names); + + auto results = database.QueryDatabase(query); + return results; +} + +void ExpeditionDatabase::DeleteCharacterLockout( + uint32_t character_id, const std::string& expedition_name, const std::string& event_name) +{ + LogExpeditionsDetail("Deleting character [{}] lockout: [{}]:[{}]", character_id, expedition_name, event_name); + + auto query = fmt::format(SQL( + DELETE FROM expedition_character_lockouts + WHERE + character_id = {} + AND is_pending = FALSE + AND expedition_name = '{}' + AND event_name = '{}'; + ), character_id, expedition_name, event_name); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions( + "Failed to delete [{}] event [{}] lockout from character [{}]", + expedition_name, event_name, character_id + ); + } +} + +void ExpeditionDatabase::DeleteMembersLockout( + const std::vector& members, + const std::string& expedition_name, const std::string& event_name) +{ + LogExpeditionsDetail("Deleting members lockout: [{}]:[{}]", expedition_name, event_name); + + std::string query_character_ids; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(query_character_ids), "{},", member.char_id); + } + + if (!query_character_ids.empty()) + { + query_character_ids.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + DELETE FROM expedition_character_lockouts + WHERE character_id + IN ({}) + AND is_pending = FALSE + AND expedition_name = '{}' + AND event_name = '{}'; + ), query_character_ids, expedition_name, event_name); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to delete [{}] event [{}] lockouts", expedition_name, event_name); + } + } +} + +void ExpeditionDatabase::AssignPendingLockouts(uint32_t character_id, const std::string& expedition_name) +{ + LogExpeditionsDetail("Assigning character [{}] pending lockouts [{}]", character_id, expedition_name); + + auto query = fmt::format(SQL( + UPDATE expedition_character_lockouts + SET is_pending = FALSE + WHERE + character_id = {} + AND is_pending = TRUE + AND expedition_name = '{}'; + ), character_id, expedition_name); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::DeletePendingLockouts(uint32_t character_id) +{ + LogExpeditionsDetail("Deleting character [{}] pending lockouts", character_id); + + auto query = fmt::format(SQL( + DELETE FROM expedition_character_lockouts + WHERE character_id = {} AND is_pending = TRUE; + ), character_id); + + database.QueryDatabase(query); +} + +void ExpeditionDatabase::DeleteExpedition(uint32_t expedition_id) +{ + LogExpeditionsDetail("Deleting expedition [{}]", expedition_id); + + auto query = fmt::format("DELETE FROM expedition_details WHERE id = {}", expedition_id); + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to delete expedition [{}]", expedition_id); + } +} + +void ExpeditionDatabase::DeleteLockout(uint32_t expedition_id, const std::string& event_name) +{ + LogExpeditionsDetail("Deleting expedition [{}] lockout event [{}]", expedition_id, event_name); + + auto query = fmt::format(SQL( + DELETE FROM expedition_lockouts + WHERE expedition_id = {} AND event_name = '{}'; + ), expedition_id, event_name); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to delete expedition [{}] lockout [{}]", expedition_id, event_name); + } +} + +void ExpeditionDatabase::DeleteAllMembers(uint32_t expedition_id) +{ + LogExpeditionsDetail("Deleting all members of expedition [{}]", expedition_id); + + auto query = fmt::format(SQL( + DELETE FROM expedition_members WHERE expedition_id = {}; + ), expedition_id); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to delete all members of expedition [{}]", expedition_id); + } +} + +uint32_t ExpeditionDatabase::GetExpeditionIDFromCharacterID(uint32_t character_id) +{ + LogExpeditionsDetail("Getting expedition id for character [{}]", character_id); + + uint32_t expedition_id = 0; + auto query = fmt::format(SQL( + SELECT expedition_id FROM expedition_members + WHERE character_id = {} AND is_current_member = TRUE; + ), character_id); + + auto results = database.QueryDatabase(query); + if (results.Success() && results.RowCount() > 0) + { + auto row = results.begin(); + expedition_id = strtoul(row[0], nullptr, 10); + } + return expedition_id; +} + +uint32_t ExpeditionDatabase::GetExpeditionIDFromInstanceID(uint32_t instance_id) +{ + LogExpeditionsDetail("Getting expedition id for instance [{}]", instance_id); + + uint32_t expedition_id = 0; + auto query = fmt::format( + "SELECT id FROM expedition_details WHERE instance_id = {};", instance_id + ); + + auto results = database.QueryDatabase(query); + if (results.Success() && results.RowCount() > 0) + { + auto row = results.begin(); + expedition_id = std::strtoul(row[0], nullptr, 10); + } + return expedition_id; +} + +ExpeditionMember ExpeditionDatabase::GetExpeditionLeader(uint32_t expedition_id) +{ + LogExpeditionsDetail("Getting expedition leader for expedition [{}]", expedition_id); + + auto query = fmt::format(SQL( + SELECT expedition_details.leader_id, character_data.name + FROM expedition_details + INNER JOIN character_data ON expedition_details.leader_id = character_data.id + WHERE expedition_id = {} + ), expedition_id); + + ExpeditionMember leader; + auto results = database.QueryDatabase(query); + if (results.Success() && results.RowCount() > 0) + { + auto row = results.begin(); + leader.char_id = strtoul(row[0], nullptr, 10); + leader.name = row[1]; + } + return leader; +} + +void ExpeditionDatabase::InsertCharacterLockouts( + uint32_t character_id, const std::vector& lockouts, + bool update_expire_times, bool is_pending) +{ + LogExpeditionsDetail("Inserting character [{}] lockouts", character_id); + + std::string insert_values; + for (const auto& lockout : lockouts) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, FROM_UNIXTIME({}), {}, '{}', '{}', {}),", + character_id, + lockout.GetExpireTime(), + lockout.GetDuration(), + lockout.GetExpeditionName(), + lockout.GetEventName(), + is_pending + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + std::string on_duplicate; + if (update_expire_times) { + on_duplicate = "expire_time = VALUES(expire_time)"; + } else { + on_duplicate = "character_id = VALUES(character_id)"; + } + + auto query = fmt::format(SQL( + INSERT INTO expedition_character_lockouts + (character_id, expire_time, duration, expedition_name, event_name, is_pending) + VALUES {} + ON DUPLICATE KEY UPDATE {}; + ), insert_values, on_duplicate); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::InsertMembersLockout( + const std::vector& members, const ExpeditionLockoutTimer& lockout) +{ + LogExpeditionsDetail( + "Inserting members lockout [{}]:[{}] with expire time [{}]", + lockout.GetExpeditionName(), lockout.GetEventName(), lockout.GetExpireTime() + ); + + std::string insert_values; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, FROM_UNIXTIME({}), {}, '{}', '{}'),", + member.char_id, + lockout.GetExpireTime(), + lockout.GetDuration(), + lockout.GetExpeditionName(), + lockout.GetEventName() + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO expedition_character_lockouts + (character_id, expire_time, duration, expedition_name, event_name) + VALUES {} + ON DUPLICATE KEY UPDATE expire_time = VALUES(expire_time); + ), insert_values); + + database.QueryDatabase(query); + } +} + +void ExpeditionDatabase::InsertLockout( + uint32_t expedition_id, const ExpeditionLockoutTimer& lockout) +{ + LogExpeditionsDetail( + "Inserting expedition [{}] lockout: [{}]:[{}] expire time: [{}]", + expedition_id, lockout.GetExpeditionName(), lockout.GetEventName(), lockout.GetExpireTime() + ); + + auto query = fmt::format(SQL( + INSERT INTO expedition_lockouts + (expedition_id, event_name, expire_time, duration, is_inherited) + VALUES + ({}, '{}', FROM_UNIXTIME({}), {}, FALSE) + ON DUPLICATE KEY UPDATE expire_time = VALUES(expire_time); + ), expedition_id, lockout.GetEventName(), lockout.GetExpireTime(), lockout.GetDuration()); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to insert expedition lockouts"); + } +} + +void ExpeditionDatabase::InsertLockouts( + uint32_t expedition_id, const std::unordered_map& lockouts) +{ + LogExpeditionsDetail("Inserting expedition [{}] lockouts", expedition_id); + + std::string insert_values; + for (const auto& lockout : lockouts) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, '{}', FROM_UNIXTIME({}), {}, {}),", + expedition_id, + lockout.second.GetEventName(), + lockout.second.GetExpireTime(), + lockout.second.GetDuration(), + lockout.second.IsInherited() + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO expedition_lockouts + (expedition_id, event_name, expire_time, duration, is_inherited) + VALUES {} + ON DUPLICATE KEY UPDATE expire_time = VALUES(expire_time); + ), insert_values); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to insert expedition lockouts"); + } + } +} + +void ExpeditionDatabase::InsertMember(uint32_t expedition_id, uint32_t character_id) +{ + LogExpeditionsDetail("Inserting character [{}] into expedition [{}]", character_id, expedition_id); + + auto query = fmt::format(SQL( + INSERT INTO expedition_members + (expedition_id, character_id, is_current_member) + VALUES + ({}, {}, TRUE) + ON DUPLICATE KEY UPDATE is_current_member = TRUE; + ), expedition_id, character_id); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to insert [{}] to expedition [{}]", character_id, expedition_id); + } +} + +void ExpeditionDatabase::InsertMembers( + uint32_t expedition_id, const std::vector& members) +{ + LogExpeditionsDetail("Inserting characters into expedition [{}]", expedition_id); + + std::string insert_values; + for (const auto& member : members) + { + fmt::format_to(std::back_inserter(insert_values), + "({}, {}, TRUE),", + expedition_id, member.char_id + ); + } + + if (!insert_values.empty()) + { + insert_values.pop_back(); // trailing comma + + auto query = fmt::format(SQL( + INSERT INTO expedition_members (expedition_id, character_id, is_current_member) + VALUES {}; + ), insert_values); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to save expedition members to database"); + } + } +} + +void ExpeditionDatabase::UpdateLeaderID(uint32_t expedition_id, uint32_t leader_id) +{ + LogExpeditionsDetail("Updating leader [{}] for expedition [{}]", leader_id, expedition_id); + + auto query = fmt::format(SQL( + UPDATE expedition_details SET leader_id = {} WHERE id = {} + ), leader_id, expedition_id); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to update expedition [{}] leader", expedition_id); + } +} + +void ExpeditionDatabase::UpdateMemberRemoved(uint32_t expedition_id, uint32_t character_id) +{ + LogExpeditionsDetail("Removing member [{}] from expedition [{}]", character_id, expedition_id); + + auto query = fmt::format(SQL( + UPDATE expedition_members SET is_current_member = FALSE + WHERE expedition_id = {} AND character_id = {}; + ), expedition_id, character_id); + + auto results = database.QueryDatabase(query); + if (!results.Success()) + { + LogExpeditions("Failed to remove [{}] from expedition [{}]", character_id, expedition_id); + } +} diff --git a/zone/expedition_database.h b/zone/expedition_database.h new file mode 100644 index 000000000..e16a94e1e --- /dev/null +++ b/zone/expedition_database.h @@ -0,0 +1,70 @@ +/** + * 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 + * + */ + +#ifndef EXPEDITION_DATABASE_H +#define EXPEDITION_DATABASE_H + +#include +#include +#include +#include +#include +#include + +class Expedition; +class ExpeditionLockoutTimer; +struct ExpeditionMember; +class MySQLRequestResult; + +namespace ExpeditionDatabase +{ + uint32_t InsertExpedition( + const std::string& expedition_name, uint32_t leader_id, + uint32_t min_players, uint32_t max_players, bool has_replay_lockout); + MySQLRequestResult LoadExpedition(uint32_t expedition_id); + MySQLRequestResult LoadAllExpeditions(); + MySQLRequestResult LoadCharacterLockouts(uint32_t character_id); + MySQLRequestResult LoadCharacterLockouts(uint32_t character_id, const std::string& expedition_name); + MySQLRequestResult LoadExpeditionMembers(uint32_t expedition_id); + MySQLRequestResult LoadValidationData(const std::string& character_names_query, const std::string& expedition_name); + void DeleteAllMembers(uint32_t expedition_id); + void DeleteCharacterLockout(uint32_t character_id, const std::string& expedition_name, const std::string& event_name); + void DeleteExpedition(uint32_t expedition_id); + void DeleteLockout(uint32_t expedition_id, const std::string& event_name); + void DeleteMembersLockout( + const std::vector& members, const std::string& expedition_name, const std::string& event_name); + void AssignPendingLockouts(uint32_t character_id, const std::string& expedition_name); + void DeletePendingLockouts(uint32_t character_id); + uint32_t GetExpeditionIDFromCharacterID(uint32_t character_id); + uint32_t GetExpeditionIDFromInstanceID(uint32_t instance_id); + ExpeditionMember GetExpeditionLeader(uint32_t expedition_id); + void InsertCharacterLockouts( + uint32_t character_id, const std::vector& lockouts, + bool update_expire_times, bool is_pending = false); + void InsertMembersLockout(const std::vector& members, const ExpeditionLockoutTimer& lockout); + void InsertLockout(uint32_t expedition_id, const ExpeditionLockoutTimer& lockout); + void InsertLockouts(uint32_t expedition_id, const std::unordered_map& lockouts); + void InsertMember(uint32_t expedition_id, uint32_t character_id); + void InsertMembers(uint32_t expedition_id, const std::vector& members); + void UpdateLeaderID(uint32_t expedition_id, uint32_t leader_id); + void UpdateMemberRemoved(uint32_t expedition_id, uint32_t character_id); +}; + +#endif diff --git a/zone/expedition_lockout_timer.cpp b/zone/expedition_lockout_timer.cpp new file mode 100644 index 000000000..e7eb8ea76 --- /dev/null +++ b/zone/expedition_lockout_timer.cpp @@ -0,0 +1,74 @@ +/** + * 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 "expedition_lockout_timer.h" +#include "../common/string_util.h" +#include +#include + +const char* const DZ_REPLAY_TIMER_NAME = "Replay Timer"; // see December 14, 2016 patch notes + +ExpeditionLockoutTimer::ExpeditionLockoutTimer( + std::string expedition_name, std::string event_name, uint64_t expire_time, uint32_t duration, bool inherited +) : + m_expedition_name(expedition_name), + m_event_name(event_name), + m_expire_time(expire_time), + m_duration(duration), + m_is_inherited(inherited) +{ + if (event_name == DZ_REPLAY_TIMER_NAME) + { + m_is_replay_timer = true; + } +} + +uint32_t ExpeditionLockoutTimer::GetSecondsRemaining() const +{ + auto now = std::chrono::system_clock::now(); + auto expire_time = std::chrono::system_clock::from_time_t(m_expire_time); + if (expire_time > now) + { + auto time_remaining = std::chrono::duration_cast(expire_time - now).count(); + return static_cast(time_remaining); + } + return 0; +} + +ExpeditionLockoutTimer::DaysHoursMinutes ExpeditionLockoutTimer::GetDaysHoursMinutesRemaining() const +{ + auto seconds = GetSecondsRemaining(); + return ExpeditionLockoutTimer::DaysHoursMinutes{ + fmt::format_int(seconds / 86400).str(), // days + fmt::format_int((seconds / 3600) % 24).str(), // hours + fmt::format_int((seconds / 60) % 60).str() // minutes + }; +} + +bool ExpeditionLockoutTimer::IsSameLockout(const ExpeditionLockoutTimer& compare_lockout) const +{ + return compare_lockout.IsSameLockout(GetExpeditionName(), GetEventName()); +} + +bool ExpeditionLockoutTimer::IsSameLockout( + const std::string& expedition_name, const std::string& event_name) const +{ + return GetExpeditionName() == expedition_name && GetEventName() == event_name; +} diff --git a/zone/expedition_lockout_timer.h b/zone/expedition_lockout_timer.h new file mode 100644 index 000000000..ff70864c4 --- /dev/null +++ b/zone/expedition_lockout_timer.h @@ -0,0 +1,64 @@ +/** + * 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 + * + */ + +#ifndef EXPEDITION_LOCKOUT_TIMER_H +#define EXPEDITION_LOCKOUT_TIMER_H + +#include + +extern const char* const DZ_REPLAY_TIMER_NAME; + +// DynamicZoneEventTimer and DynamicZoneReplayTimer in client +class ExpeditionLockoutTimer +{ +public: + ExpeditionLockoutTimer() {} + ExpeditionLockoutTimer(std::string expedition_name, std::string event_name, uint64_t expire_time, uint32_t duration, bool inherited = false); + + struct DaysHoursMinutes + { + std::string days; + std::string hours; + std::string mins; + }; + + uint32_t GetDuration() const { return m_duration; } + uint64_t GetExpireTime() const { return m_expire_time; } + uint32_t GetSecondsRemaining() const; + DaysHoursMinutes GetDaysHoursMinutesRemaining() const; + const std::string& GetExpeditionName() const { return m_expedition_name; } + const std::string& GetEventName() const { return m_event_name; } + void SetExpireTime(uint64_t expire_time) { m_expire_time = expire_time; } + void SetInherited(bool is_inherited) { m_is_inherited = is_inherited; } + bool IsInherited() const { return m_is_inherited; } + bool IsReplayTimer() const { return m_is_replay_timer; } + bool IsSameLockout(const ExpeditionLockoutTimer& compare_lockout) const; + bool IsSameLockout(const std::string& expedition_name, const std::string& event_name) const; + +private: + std::string m_expedition_name; + std::string m_event_name; + uint64_t m_expire_time = 0; + uint32_t m_duration = 0; + bool m_is_inherited = false; // inherited from expedition leader + bool m_is_replay_timer = false; +}; + +#endif diff --git a/zone/expedition_request.cpp b/zone/expedition_request.cpp new file mode 100644 index 000000000..5cf049f60 --- /dev/null +++ b/zone/expedition_request.cpp @@ -0,0 +1,370 @@ +/** + * 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 "expedition_request.h" +#include "client.h" +#include "expedition.h" +#include "expedition_database.h" +#include "expedition_lockout_timer.h" +#include "groups.h" +#include "raids.h" +#include "string_ids.h" +#include "worldserver.h" +#include + +extern WorldServer worldserver; + +struct ExpeditionRequestConflict +{ + std::string character_name; + ExpeditionLockoutTimer lockout; +}; + +ExpeditionRequest::ExpeditionRequest( + Client* requester, std::string expedition_name, uint32_t min_players, + uint32_t max_players, bool has_replay_timer +) : + m_requester(requester), + m_expedition_name(expedition_name), + m_min_players(min_players), + m_max_players(max_players), + m_has_replay_timer(has_replay_timer) +{ +} + +bool ExpeditionRequest::Validate() +{ + if (!m_requester) + { + return false; + } + + // a message is sent to leader for every member that fails a requirement + + auto start = std::chrono::steady_clock::now(); + + bool requirements_met = false; + + Raid* raid = m_requester->GetRaid(); + Group* group = m_requester->GetGroup(); + if (raid) + { + requirements_met = CanRaidRequest(raid); + } + else if (group) + { + requirements_met = CanGroupRequest(group); + } + else // solo request + { + m_leader = m_requester; + m_leader_id = m_requester->CharacterID(); + m_leader_name = m_requester->GetName(); + requirements_met = ValidateMembers(fmt::format("'{}'", m_leader_name), 1); + } + + auto end = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast>(end - start); + LogExpeditions("Create validation for [{}] members took {}s", m_members.size(), elapsed.count()); + + return requirements_met; +} + +bool ExpeditionRequest::CanRaidRequest(Raid* raid) +{ + m_leader = raid->GetLeader(); + m_leader_name = raid->leadername; + m_leader_id = m_leader ? m_leader->CharacterID() : database.GetCharacterID(raid->leadername); + + uint32_t count = 0; + std::string query_member_names; + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) + { + if (raid->members[i].membername[0]) + { + fmt::format_to(std::back_inserter(query_member_names), "'{}',", raid->members[i].membername); + ++count; + } + } + + if (!query_member_names.empty()) + { + query_member_names.pop_back(); // trailing comma + } + + return ValidateMembers(query_member_names, count); +} + +bool ExpeditionRequest::CanGroupRequest(Group* group) +{ + m_leader = nullptr; + if (group->GetLeader() && group->GetLeader()->IsClient()) + { + m_leader = group->GetLeader()->CastToClient(); + } + m_leader_name = group->GetLeaderName(); + m_leader_id = m_leader ? m_leader->CharacterID() : database.GetCharacterID(m_leader_name.c_str()); + + uint32_t count = 0; + std::string query_member_names; + for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) + { + if (group->membername[i][0]) + { + fmt::format_to(std::back_inserter(query_member_names), "'{}',", group->membername[i]); + ++count; + } + } + + if (!query_member_names.empty()) + { + query_member_names.pop_back(); // trailing comma + } + + return ValidateMembers(query_member_names, count); +} + +bool ExpeditionRequest::ValidateMembers(const std::string& query_member_names, uint32_t member_count) +{ + if (query_member_names.empty() || !LoadLeaderLockouts()) + { + return false; + } + + // get character ids for all members through database since some may be out + // of zone. also gets each member's existing expeditions and/or lockouts + auto results = ExpeditionDatabase::LoadValidationData(query_member_names, m_expedition_name); + if (!results.Success()) + { + LogExpeditions("Failed to load data to verify members for expedition request"); + return false; + } + + bool requirements_met = true; + + bool is_solo = (member_count == 1); + bool has_conflicts = CheckMembersForConflicts(results, is_solo); + if (has_conflicts) + { + requirements_met = false; + } + + // live only checks player count requirement after other expensive checks pass (?) + // maybe it's done intentionally as a way to preview lockout conflicts + if (requirements_met) + { + requirements_met = IsPlayerCountValidated(member_count); + } + + return requirements_met; +} + +bool ExpeditionRequest::LoadLeaderLockouts() +{ + // leader's lockouts are used to check member conflicts and later stored in expedition + auto results = ExpeditionDatabase::LoadCharacterLockouts(m_leader_id, m_expedition_name); + if (!results.Success()) + { + LogExpeditions("Failed to load leader id [{}] lockouts ([{}])", m_leader_id, m_leader_name); + return false; + } + + for (auto row = results.begin(); row != results.end(); ++row) + { + uint64_t expire_time = strtoull(row[0], nullptr, 10); + uint32_t duration = strtoul(row[1], nullptr, 10); + + m_lockouts.emplace(row[2], ExpeditionLockoutTimer{ + m_expedition_name, row[2], expire_time, duration, true + }); + + // on live if leader has a replay lockout it never bothers checking for event conflicts + if (m_check_event_lockouts && m_has_replay_timer && strcmp(row[2], DZ_REPLAY_TIMER_NAME) == 0) + { + m_check_event_lockouts = false; + } + } + + return true; +} + +bool ExpeditionRequest::CheckMembersForConflicts(MySQLRequestResult& results, bool is_solo) +{ + // leader lockouts were pre-loaded to compare with members below + bool has_conflicts = false; + + std::vector member_lockout_conflicts; + + bool leader_processed = false; + uint32_t last_character_id = 0; + for (auto row = results.begin(); row != results.end(); ++row) + { + auto character_id = static_cast(std::strtoul(row[0], nullptr, 10)); + std::string character_name(row[1]); + + if (character_id != last_character_id) + { + // defaults to online status, if offline group members implemented this needs to change + m_members.emplace_back(ExpeditionMember{character_id, character_name}); + + // process event lockout conflict messages from the previous character + for (const auto& member_lockout : member_lockout_conflicts) + { + SendLeaderMemberEventLockout(member_lockout.character_name, member_lockout.lockout); + } + member_lockout_conflicts.clear(); + + // current character existing expedition check + if (row[2]) + { + has_conflicts = true; + SendLeaderMemberInExpedition(character_name, is_solo); + + // solo requests break out early if requester in an expedition + if (is_solo) + { + return has_conflicts; + } + } + } + + last_character_id = character_id; + + // compare member lockouts with leader lockouts + if (row[3] && row[4] && row[5]) + { + auto expire_time = strtoull(row[3], nullptr, 10); + auto original_duration = strtoul(row[4], nullptr, 10); + std::string event_name(row[5]); + + ExpeditionLockoutTimer lockout(m_expedition_name, event_name, expire_time, original_duration); + + // replay timer conflict messages always show up before event conflicts + if (/*m_has_replay_timer && */event_name == DZ_REPLAY_TIMER_NAME) + { + has_conflicts = true; + SendLeaderMemberReplayLockout(character_name, lockout, is_solo); + // replay timers no longer also show up as event conflicts + //SendLeaderMemberEventLockout(character_name, lockout); + } + else if (m_check_event_lockouts && character_id != m_leader_id) + { + if (m_lockouts.find(event_name) == m_lockouts.end()) + { + // leader doesn't have this lockout + // queue instead of messaging now so they come after any replay lockout messages + has_conflicts = true; + member_lockout_conflicts.emplace_back(ExpeditionRequestConflict{character_name, lockout}); + } + } + } + } + + // event lockout messages for last processed character + for (const auto& member_lockout : member_lockout_conflicts) + { + SendLeaderMemberEventLockout(member_lockout.character_name, member_lockout.lockout); + } + + return has_conflicts; +} + +void ExpeditionRequest::SendLeaderMessage( + uint16_t chat_type, uint32_t string_id, const std::initializer_list& parameters) +{ + Client::SendCrossZoneMessageString(m_leader, m_leader_name, chat_type, string_id, parameters); +} + +void ExpeditionRequest::SendLeaderMemberInExpedition(const std::string& member_name, bool is_solo) +{ + if (is_solo) + { + SendLeaderMessage(Chat::Red, EXPEDITION_YOU_BELONG); + } + else if (m_requester) + { + std::string message = fmt::format(EXPEDITION_OTHER_BELONGS, m_requester->GetName(), member_name); + Client::SendCrossZoneMessage(m_leader, m_leader_name, Chat::Red, message); + } +} + +void ExpeditionRequest::SendLeaderMemberReplayLockout( + const std::string& member_name, const ExpeditionLockoutTimer& lockout, bool is_solo) +{ + if (lockout.GetSecondsRemaining() <= 0) + { + return; + } + + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + if (is_solo) + { + SendLeaderMessage(Chat::Red, EXPEDITION_YOU_PLAYED_HERE, { + time_remaining.days, time_remaining.hours, time_remaining.mins + }); + } + else + { + SendLeaderMessage(Chat::Red, EXPEDITION_REPLAY_TIMER, { + member_name, time_remaining.days, time_remaining.hours, time_remaining.mins + }); + } +} + +void ExpeditionRequest::SendLeaderMemberEventLockout( + const std::string& member_name, const ExpeditionLockoutTimer& lockout) +{ + if (lockout.GetSecondsRemaining() <= 0) + { + return; + } + + auto time_remaining = lockout.GetDaysHoursMinutesRemaining(); + SendLeaderMessage(Chat::Red, EXPEDITION_EVENT_TIMER, { + member_name, + lockout.GetEventName(), + time_remaining.days, + time_remaining.hours, + time_remaining.mins, + lockout.GetEventName() + }); +} + +bool ExpeditionRequest::IsPlayerCountValidated(uint32_t member_count) +{ + // note: offline group members count towards requirement but not added to expedition + bool requirements_met = true; + + auto bypass_status = RuleI(Expedition, MinStatusToBypassPlayerCountRequirements); + auto gm_bypass = (m_requester->GetGM() && m_requester->Admin() >= bypass_status); + + if (!gm_bypass && (member_count < m_min_players || member_count > m_max_players)) + { + requirements_met = false; + + SendLeaderMessage(Chat::Red, REQUIRED_PLAYER_COUNT, { + fmt::format_int(member_count).str(), + fmt::format_int(m_min_players).str(), + fmt::format_int(m_max_players).str() + }); + } + + return requirements_met; +} diff --git a/zone/expedition_request.h b/zone/expedition_request.h new file mode 100644 index 000000000..9c57bbb9e --- /dev/null +++ b/zone/expedition_request.h @@ -0,0 +1,76 @@ +/** + * 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 + * + */ + +#ifndef EXPEDITION_REQUEST_H +#define EXPEDITION_REQUEST_H + +#include "expedition_lockout_timer.h" +#include +#include +#include +#include + +class Client; +class Group; +class MySQLRequestResult; +class Raid; +class ServerPacket; +struct ExpeditionMember; + +class ExpeditionRequest +{ +public: + ExpeditionRequest(Client* requester, std::string expedition_name, + uint32_t min_players, uint32_t max_players, bool has_replay_timer); + + bool Validate(); + + Client* GetLeaderClient() const { return m_leader; } + uint32_t GetLeaderID() const { return m_leader_id; } + const std::string& GetLeaderName() const { return m_leader_name; } + std::vector TakeMembers() && { return std::move(m_members); } + std::unordered_map TakeLockouts() && { return std::move(m_lockouts); } + +private: + bool ValidateMembers(const std::string& query_member_names, uint32_t member_count); + bool CanRaidRequest(Raid* raid); + bool CanGroupRequest(Group* group); + bool CheckMembersForConflicts(MySQLRequestResult& results, bool is_solo); + bool IsPlayerCountValidated(uint32_t member_count); + bool LoadLeaderLockouts(); + void SendLeaderMemberInExpedition(const std::string& member_name, bool is_solo); + void SendLeaderMemberReplayLockout(const std::string& member_name, const ExpeditionLockoutTimer& lockout, bool is_solo); + void SendLeaderMemberEventLockout(const std::string& member_name, const ExpeditionLockoutTimer& lockout); + void SendLeaderMessage(uint16_t chat_type, uint32_t string_id, const std::initializer_list& parameters = {}); + + Client* m_requester = nullptr; + Client* m_leader = nullptr; + uint32_t m_leader_id = 0; + uint32_t m_min_players = 0; + uint32_t m_max_players = 0; + bool m_check_event_lockouts = true; + bool m_has_replay_timer = false; + std::string m_expedition_name; + std::string m_leader_name; + std::vector m_members; + std::unordered_map m_lockouts; +}; + +#endif diff --git a/zone/string_ids.h b/zone/string_ids.h index b7ee5ddd4..4a2263cda 100644 --- a/zone/string_ids.h +++ b/zone/string_ids.h @@ -293,8 +293,43 @@ #define TRADESKILL_MISSING_ITEM 3455 //You are missing a %1. #define TRADESKILL_MISSING_COMPONENTS 3456 //Sorry, but you don't have everything you need for this recipe in your general inventory. #define TRADESKILL_LEARN_RECIPE 3457 //You have learned the recipe %1! -#define EXPEDITION_MIN_REMAIN 3551 //You only have %1 minutes remaining before this expedition comes to an end. +#define EXPEDITION_YOU_BELONG 3500 //You cannot create this expedition since you already belong to another. +#define EXPEDITION_YOU_PLAYED_HERE 3501 //You cannot create this expedition for another %1d:%2h:%3m since you have recently played here. +#define REQUIRED_PLAYER_COUNT 3503 //You do not meet the player count requirement. You have %1 players. You must have at least %2 and no more than %3. +#define EXPEDITION_REPLAY_TIMER 3504 //%1 cannot be added to this expedition for another %2D:%3H:%4M since they have recently played in this area. +#define EXPEDITION_AVAILABLE 3507 //%1 is now available to you. +#define DZADD_INVITE 3508 //Sending an invitation to: %1. +#define DZADD_INVITE_FAIL 3511 //%1 could not be invited to join you. +#define UNABLE_RETRIEVE_LEADER 3512 //Unable to retrieve information on the leader to check permissions. +#define EXPEDITION_NOT_LEADER 3513 //You are not the expedition leader, only %1 can issue this command. +#define EXPEDITION_NOT_MEMBER 3514 //%1 is not a member of this expedition. +#define EXPEDITION_REMOVED 3516 //%1 has been removed from %2. +#define DZSWAP_INVITE 3517 //Sending an invitation to: %1. They must accept in order to swap party members. +#define DZMAKELEADER_NOT_ONLINE 3518 //%1 is not currently online. You can only transfer leadership to an online member of the expedition you are in. +#define DZLIST_REPLAY_TIMER 3519 //You have %1d:%2h:%3m remaining until you may enter %4. +#define DZMAKELEADER_NAME 3520 //%1 has been made the leader for this expedition. +#define DZMAKELEADER_YOU 3521 //You have been made the leader of this expedition. +#define EXPEDITION_INVITE_ACCEPTED 3522 //%1 has accepted your offer to join your expedition. +#define EXPEDITION_MEMBER_ADDED 3523 //%1 has been added to %2. +#define EXPEDITION_INVITE_ERROR 3524 //%1 accepted your offer to join your expedition but could not due to error(s). +#define EXPEDITION_INVITE_DECLINED 3525 //%1 has declined your offer to join your expedition. +#define EXPEDITION_ASKED_TO_JOIN 3527 //%1 has asked you to join the expedition: %2. Would you like to join? +#define EXPEDITION_NO_TIMERS 3529 //You have no outstanding timers. +#define EXPEDITION_MIN_REMAIN 3551 //You only have %1 minutes remaining before this expedition comes to an end. +#define EXPEDITION_LEADER 3552 //Expedition Leader: %1 +#define EXPEDITION_MEMBERS 3553 //Expedition Members: %1 +#define EXPEDITION_EVENT_TIMER 3561 //%1 cannot be added to this expedition since they have recently experienced %2. They must wait another %3D:%4H:%5M until they can experience it again. They may be added to the expedition later, once %2 has been completed. #define LOOT_NOT_ALLOWED 3562 //You are not allowed to loot the item: %1. +#define DZ_UNABLE_RETRIEVE_LEADER 3583 //Unable to retrieve dynamic zone leader to check permissions. +#define DZADD_NOT_ONLINE 3586 //%1 is not currently online. A player needs to be online to be added to a Dynamic Zone +#define DZADD_EXCEED_MAX 3587 //You can not add another player since you currently have the maximum number of players allowed (%1) in this zone. +#define DZADD_ALREADY_PART 3588 //You can not add %1 since they are already part of this zone. +#define DZADD_ALREADY_ASSIGNED 3590 //%1 can not be added to this dynamic zone since they are already assigned to another dynamic zone. +#define DZADD_REPLAY_TIMER 3591 //%1 can not be added to this dynamic zone for another %2D:%3H:%4M since they have recently played this zone. +#define DZADD_EVENT_TIMER 3592 //%1 can not be added to this dynamic zone since they have recently experienced %2. They must wait for another %3D:%4H:%5M, or until event %2 has occurred. +#define DZADD_PENDING 3593 //%1 currently has an outstanding invitation to join this Dynamic Zone. +#define DZADD_PENDING_OTHER 3594 //%1 currently has an outstanding invitation to join another Dynamic Zone. Players may only have one invitation outstanding. +#define DZSWAP_CANNOT_REMOVE 3595 //%1 can not be removed from this dynamic zone since they are not assigned to it. #define NOT_YOUR_TRAP 3671 //You cannot remove this, you are only allowed to remove traps you have set. #define NO_CAST_ON_PET 4045 //You cannot cast this spell on your pet. #define REWIND_WAIT 4059 //You must wait a bit longer before using the rewind command again. diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index d4fa5e572..2088cef05 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -42,6 +42,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "client.h" #include "corpse.h" #include "entity.h" +#include "expedition.h" #include "quest_parser_collection.h" #include "guild_mgr.h" #include "mob.h" @@ -2846,7 +2847,6 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) break; } - case ServerOP_ChangeSharedMem: { std::string hotfix_name = std::string((char*)pack->pBuffer); @@ -2881,6 +2881,38 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) } break; } + case ServerOP_CZClientMessage: + { + auto buf = reinterpret_cast(pack->pBuffer); + Client* client = entity_list.GetClientByName(buf->character_name); + if (client) { + client->Message(buf->chat_type, buf->message); + } + break; + } + case ServerOP_CZClientMessageString: + { + auto buf = reinterpret_cast(pack->pBuffer); + Client* client = entity_list.GetClientByName(buf->character_name); + if (client) { + client->MessageString(buf); + } + break; + } + case ServerOP_ExpeditionCreate: + case ServerOP_ExpeditionDeleted: + case ServerOP_ExpeditionLeaderChanged: + case ServerOP_ExpeditionLockout: + case ServerOP_ExpeditionMemberChange: + case ServerOP_ExpeditionMemberSwap: + case ServerOP_ExpeditionMemberStatus: + case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionDzAddPlayer: + case ServerOP_ExpeditionDzMakeLeader: + { + Expedition::HandleWorldMessage(pack); + break; + } default: { std::cout << " Unknown ZSopcode:" << (int)pack->opcode; std::cout << " size:" << pack->size << std::endl; diff --git a/zone/zone.cpp b/zone/zone.cpp index 66644edf1..a8a2e039e 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -37,6 +37,7 @@ #include "../common/string_util.h" #include "../common/eqemu_logsys.h" +#include "expedition.h" #include "guild_mgr.h" #include "map.h" #include "npc.h" @@ -1183,6 +1184,9 @@ bool Zone::Init(bool iStaticZone) { petition_list.ClearPetitions(); petition_list.ReadDatabase(); + LogInfo("Loading active Expeditions"); + Expedition::CacheAllFromDatabase(); + LogInfo("Loading timezone data"); zone->zone_time.setEQTimeZone(content_db.GetZoneTZ(zoneid, GetInstanceVersion())); @@ -2699,3 +2703,7 @@ void Zone::SetInstanceTimeRemaining(uint32 instance_time_remaining) Zone::instance_time_remaining = instance_time_remaining; } +bool Zone::IsZone(uint32 zone_id, uint16 instance_id) const +{ + return (zoneid == zone_id && instanceid == instance_id); +} diff --git a/zone/zone.h b/zone/zone.h index 824140700..f8d0fbbbb 100755 --- a/zone/zone.h +++ b/zone/zone.h @@ -81,6 +81,7 @@ struct item_tick_struct { }; class Client; +class Expedition; class Map; class Mob; class WaterMap; @@ -129,6 +130,7 @@ public: bool IsPVPZone() { return pvpzone; } bool IsSpellBlocked(uint32 spell_id, const glm::vec3 &location); bool IsUCSServerAvailable() { return m_ucss_available; } + bool IsZone(uint32 zone_id, uint16 instance_id) const; bool LoadGroundSpawns(); bool LoadZoneCFG(const char *filename, uint16 instance_id); bool LoadZoneObjects(); @@ -217,6 +219,8 @@ public: std::vector zone_grids; std::vector zone_grid_entries; + std::unordered_map> expedition_cache; + time_t weather_timer; Timer spawn2_timer; Timer hot_reload_timer;