/** * 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 "groups.h" #include "raids.h" #include "string_ids.h" #include "../common/expedition_lockout_timer.h" #include "../common/repositories/character_expedition_lockouts_repository.h" #include "../common/repositories/expeditions_repository.h" constexpr char SystemName[] = "expedition"; // message string 8312 added in September 08 2020 Test patch (used by both dz and shared tasks) constexpr const char* CREATE_NOT_ALL_ADDED = "Not all players in your {} were added to the {}. The {} can take a maximum of {} players, and your {} has {}."; // message string 9265 (not in emu clients) constexpr const char* EXPEDITION_OTHER_BELONGS = "{} attempted to create an expedition but {} already belongs to one."; ExpeditionRequest::ExpeditionRequest(const DynamicZone& dz, bool disable_messages) : m_expedition_name(dz.GetName()), m_min_players(dz.GetMinPlayers()), m_max_players(dz.GetMaxPlayers()), m_disable_messages(disable_messages) { } bool ExpeditionRequest::Validate(Client* requester) { m_requester = requester; if (!m_requester) { return false; } // a message is sent to leader for every member that fails a requirement BenchTimer benchmark; 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 = CanMembersJoin({m_leader_name}); } auto elapsed = benchmark.elapsed(); LogExpeditions("Create validation for [{}] members took [{}s]", m_members.size(), elapsed); 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); // live (as of September 16, 2020) supports creation even if raid count exceeds // expedition max. members are added up to the max ordered by group number. auto raid_members = raid->GetMembers(); if (raid_members.size() > m_max_players) { // stable_sort not needed, order within a raid group may not be what is displayed std::sort(raid_members.begin(), raid_members.end(), [&](const RaidMember& lhs, const RaidMember& rhs) { if (m_leader_name == lhs.membername) { // leader always added first return true; } else if (m_leader_name == rhs.membername) { return false; } return lhs.GroupNumber < rhs.GroupNumber; }); m_not_all_added_msg = fmt::format(CREATE_NOT_ALL_ADDED, "raid", SystemName, SystemName, m_max_players, "raid", raid_members.size()); } // live still performs conflict checks for all members even those beyond max std::vector member_names; for (int i = 0; i < raid_members.size(); ++i) { member_names.emplace_back(raid_members[i].membername); } return CanMembersJoin(member_names); } bool ExpeditionRequest::CanGroupRequest(Group* group) { m_leader = nullptr; if (group->GetLeader() && group->GetLeader()->IsClient()) { m_leader = group->GetLeader()->CastToClient(); } // Group::GetLeaderName() is broken if group formed across zones, ask database instead m_leader_name = m_leader ? m_leader->GetName() : GetGroupLeaderName(group->GetID()); // group->GetLeaderName(); m_leader_id = m_leader ? m_leader->CharacterID() : database.GetCharacterID(m_leader_name.c_str()); std::vector member_names; member_names.emplace_back(m_leader_name); // leader always added first for (int i = 0; i < MAX_GROUP_MEMBERS; ++i) { if (group->membername[i][0] && m_leader_name != group->membername[i]) { member_names.emplace_back(group->membername[i]); } } if (member_names.size() > m_max_players) { m_not_all_added_msg = fmt::format(CREATE_NOT_ALL_ADDED, "group", SystemName, SystemName, m_max_players, "group", member_names.size()); } return CanMembersJoin(member_names); } std::string ExpeditionRequest::GetGroupLeaderName(uint32_t group_id) { char leader_name_buffer[64] = { 0 }; database.GetGroupLeadershipInfo(group_id, leader_name_buffer); return std::string(leader_name_buffer); } bool ExpeditionRequest::CanMembersJoin(const std::vector& member_names) { if (member_names.empty()) { return false; } bool requirements_met = true; if (CheckMembersForConflicts(member_names)) { 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(); } return requirements_met; } bool ExpeditionRequest::SaveLeaderLockouts(const std::vector& lockouts) { bool has_replay_lockout = false; for (const auto& lockout : lockouts) { if (!lockout.IsExpired()) { m_lockouts[lockout.GetEventName()] = lockout; if (lockout.IsReplayTimer()) { has_replay_lockout = true; } } } return has_replay_lockout; } bool ExpeditionRequest::CheckMembersForConflicts(const std::vector& member_names) { // order of member_names is preserved by queries for use with max member truncation auto entries = ExpeditionsRepository::GetCharactersWithExpedition(database, member_names); if (entries.empty()) { LogExpeditions("Failed to load members for expedition request"); return true; } bool is_solo = (member_names.size() == 1); bool has_conflicts = false; std::vector character_ids; for (const auto& character : entries) { if (is_solo && character.expedition_id != 0) { // live doesn't bother checking replay lockout here SendLeaderMemberInExpedition(character.name, is_solo); return true; } m_members.emplace_back(character.id, character.name, DynamicZoneMemberStatus::Online); character_ids.emplace_back(character.id); } auto member_lockouts = CharacterExpeditionLockoutsRepository::GetManyCharacterLockoutTimers( database, character_ids, m_expedition_name, DZ_REPLAY_TIMER_NAME); // on live if leader has a replay lockout it never checks for event conflicts bool leader_has_replay_lockout = false; auto lockout_iter = member_lockouts.find(m_leader_id); if (lockout_iter != member_lockouts.end()) { leader_has_replay_lockout = SaveLeaderLockouts(lockout_iter->second); } for (const auto& character : entries) { if (character.expedition_id != 0) { has_conflicts = true; SendLeaderMemberInExpedition(character.name, is_solo); } auto lockout_iter = member_lockouts.find(character.id); if (lockout_iter != member_lockouts.end()) { for (const auto& lockout : lockout_iter->second) { if (!lockout.IsExpired()) { // replay timers were sorted by query so they show up before event conflicts if (lockout.IsReplayTimer()) { has_conflicts = true; SendLeaderMemberReplayLockout(character.name, lockout, is_solo); } else if (!leader_has_replay_lockout && character.id != m_leader_id && m_lockouts.find(lockout.GetEventName()) == m_lockouts.end()) { // leader doesn't have this lockout has_conflicts = true; SendLeaderMemberEventLockout(character.name, lockout); } } } } } return has_conflicts; } void ExpeditionRequest::SendLeaderMessage( uint16_t chat_type, uint32_t string_id, const std::initializer_list& args) { if (!m_disable_messages) { Client::SendCrossZoneMessageString(m_leader, m_leader_name, chat_type, string_id, args); } } void ExpeditionRequest::SendLeaderMemberInExpedition(const std::string& member_name, bool is_solo) { if (m_disable_messages) { return; } 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 (m_disable_messages) { 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 (m_disable_messages) { 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() { // 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 && m_requester->GetGM() && m_requester->Admin() >= bypass_status); if (m_members.size() > m_max_players) { // members were sorted at start, truncate after conflict checks to act like live m_members.resize(m_max_players); } else if (!gm_bypass && m_members.size() < m_min_players) { requirements_met = false; SendLeaderMessage(Chat::System, REQUIRED_PLAYER_COUNT, { fmt::format_int(m_members.size()).str(), fmt::format_int(m_min_players).str(), fmt::format_int(m_max_players).str() }); } return requirements_met; }