diff --git a/src/Components/Loader.cpp b/src/Components/Loader.cpp index 912f2487..b0e71229 100644 --- a/src/Components/Loader.cpp +++ b/src/Components/Loader.cpp @@ -108,6 +108,7 @@ namespace Components Loader::Register(new Branding()); Loader::Register(new RawMouse()); Loader::Register(new Bullet()); + Loader::Register(new MapRotation()); Loader::Pregame = false; } diff --git a/src/Components/Loader.hpp b/src/Components/Loader.hpp index 9851066a..77e5b0c6 100644 --- a/src/Components/Loader.hpp +++ b/src/Components/Loader.hpp @@ -139,3 +139,4 @@ namespace Components #include "Modules/Branding.hpp" #include "Modules/RawMouse.hpp" #include "Modules/Bullet.hpp" +#include "Modules/MapRotation.hpp" diff --git a/src/Components/Modules/Dedicated.cpp b/src/Components/Modules/Dedicated.cpp index d002594f..0d5e153c 100644 --- a/src/Components/Modules/Dedicated.cpp +++ b/src/Components/Modules/Dedicated.cpp @@ -4,9 +4,7 @@ namespace Components { SteamID Dedicated::PlayerGuids[18][2]; - Dvar::Var Dedicated::SVRandomMapRotation; Dvar::Var Dedicated::SVLanOnly; - Dvar::Var Dedicated::SVDontRotate; Dvar::Var Dedicated::COMLogFilter; bool Dedicated::IsEnabled() @@ -124,141 +122,6 @@ namespace Components Game::Com_Error(code, message); } - void Dedicated::RandomizeMapRotation() - { - auto rotation = Dvar::Var("sv_mapRotation").get(); - - const auto tokens = Utils::String::Split(rotation, ' '); - std::vector> mapRotationPair; - - for (std::size_t i = 0; i < (tokens.size() - 1); i += 2) - { - if (i + 1 >= tokens.size()) break; - - const auto& key = tokens[i]; - const auto& value = tokens[i + 1]; - mapRotationPair.emplace_back(std::make_pair(key, value)); - } - - const auto seed = Utils::Cryptography::Rand::GenerateInt(); - std::shuffle(mapRotationPair.begin(), mapRotationPair.end(), std::default_random_engine(seed)); - - // Rebuild map rotation using the randomized key/values - rotation.clear(); - - for (std::size_t j = 0; j < mapRotationPair.size(); j++) - { - const auto& pair = mapRotationPair[j]; - rotation.append(pair.first); - rotation.append(" "); - rotation.append(pair.second); - - if (j != mapRotationPair.size() - 1) - rotation.append(" "); // No space on last element - } - - Dvar::Var("sv_mapRotationCurrent").set(rotation); - } - - void Dedicated::ApplyMapRotation() - { - auto rotation = Dvar::Var("sv_mapRotationCurrent").get(); - const auto tokens = Utils::String::Split(rotation, ' '); - - for (std::size_t i = 0; i < (tokens.size() - 1); i += 2) - { - if (i + 1 >= tokens.size()) - { - Dvar::Var("sv_mapRotationCurrent").set(""); - Command::Execute("map_rotate", true); - return; - } - - const auto& key = tokens[i]; - const auto& value = tokens[i + 1]; - - if (key == "map") - { - // Rebuild map rotation string - rotation.clear(); - for (std::size_t j = (i + 2); j < tokens.size(); ++j) - { - if (j != (i + 2)) rotation += " "; - rotation += tokens[j]; - } - - Dvar::Var("sv_mapRotationCurrent").set(rotation); - - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER,"Loading new map: %s\n", value.data()); - Command::Execute(Utils::String::VA("map %s", value.data()), true); - break; - } - - if (key == "gametype") - { - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "Applying new gametype: %s\n", value.data()); - Dvar::Var("g_gametype").set(value); - } - else - { - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "Unsupported maprotation key '%s'!\n", key.data()); - } - } - } - - void Dedicated::MapRotate() - { - if (!Dedicated::IsEnabled() && Dedicated::SVDontRotate.get()) - { - Dedicated::SVDontRotate.set(false); - return; - } - - if (Dvar::Var("party_enable").get() && Dvar::Var("party_host").get()) - { - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "Not performing map rotation as we are hosting a party!\n"); - return; - } - - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "Rotating map...\n"); - const auto mapRotation = Dvar::Var("sv_mapRotation").get(); - - // if nothing, just restart - if (mapRotation.empty()) - { - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "No rotation defined, restarting map.\n"); - - if (!Dvar::Var("sv_cheats").get()) - { - Command::Execute(Utils::String::VA("map %s", Dvar::Var("mapname").get()), true); - } - else - { - Command::Execute(Utils::String::VA("devmap %s", Dvar::Var("mapname").get()), true); - } - - return; - } - - // First, check if the string contains nothing - if (Dvar::Var("sv_mapRotationCurrent").get().empty()) - { - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "Current map rotation has finished, reloading...\n"); - - if (Dedicated::SVRandomMapRotation.get()) - { - Logger::Print(Game::conChannel_t::CON_CHANNEL_SERVER, "Randomizing map rotation\n"); - Dedicated::RandomizeMapRotation(); - } - else - { - Dvar::Var("sv_mapRotationCurrent").set(mapRotation); - } - } - - Dedicated::ApplyMapRotation(); - } - void Dedicated::Heartbeat() { // Do not send a heartbeat if sv_lanOnly is set to true @@ -297,11 +160,8 @@ namespace Components Dedicated::Dedicated() { // Map rotation - Utils::Hook::Set(0x4152E8, Dedicated::MapRotate); Dvar::OnInit([] { - Dedicated::SVDontRotate = Dvar::Register("sv_dontRotate", false, - Game::dvar_flag::DVAR_NONE, ""); Dedicated::COMLogFilter = Dvar::Register("com_logFilter", true, Game::dvar_flag::DVAR_LATCH, "Removes ~95% of unneeded lines from the log"); }); @@ -414,7 +274,6 @@ namespace Components Dvar::OnInit([]() { - Dedicated::SVRandomMapRotation = Dvar::Register("sv_randomMapRotation", false, Game::dvar_flag::DVAR_ARCHIVE, "Randomize map rotation when true"); Dvar::Register("sv_sayName", "^7Console", Game::dvar_flag::DVAR_NONE, "The name to pose as for 'say' commands"); Dvar::Register("sv_motd", "", Game::dvar_flag::DVAR_NONE, "A custom message of the day for servers"); diff --git a/src/Components/Modules/Dedicated.hpp b/src/Components/Modules/Dedicated.hpp index a006b635..381f34e2 100644 --- a/src/Components/Modules/Dedicated.hpp +++ b/src/Components/Modules/Dedicated.hpp @@ -9,7 +9,6 @@ namespace Components static SteamID PlayerGuids[18][2]; static Dvar::Var SVLanOnly; - static Dvar::Var SVDontRotate; static Dvar::Var COMLogFilter; static bool IsEnabled(); @@ -17,11 +16,6 @@ namespace Components static void Heartbeat(); private: - static Dvar::Var SVRandomMapRotation; - - static void RandomizeMapRotation(); - static void ApplyMapRotation(); - static void MapRotate(); static void InitDedicatedServer(); static void PostInitialization(); diff --git a/src/Components/Modules/MapRotation.cpp b/src/Components/Modules/MapRotation.cpp new file mode 100644 index 00000000..f3405415 --- /dev/null +++ b/src/Components/Modules/MapRotation.cpp @@ -0,0 +1,198 @@ +#include + +namespace Components +{ + Dvar::Var MapRotation::SVRandomMapRotation; + Dvar::Var MapRotation::SVDontRotate; + + Game::dvar_t** MapRotation::SVMapRotation = reinterpret_cast(0x62C7C44); + Game::dvar_t** MapRotation::SVMapname = reinterpret_cast(0x2098DDC); + + MapRotation::RotationData MapRotation::DedicatedRotation; + + MapRotation::RotationData::RotationData() + :index_(0) + { + } + + void MapRotation::RotationData::randomize() + { + // Code from https://en.cppreference.com/w/cpp/algorithm/random_shuffle + std::random_device rd; + std::mt19937 gen(rd()); + + std::shuffle(this->rotationEntries.begin(), this->rotationEntries.end(), gen); + } + + void MapRotation::RotationData::addEntry(const std::string& key, const std::string& value) + { + this->rotationEntries.emplace_back(std::make_pair(key, value)); + } + + std::size_t MapRotation::RotationData::getEntriesSize() const + { + return this->rotationEntries.size(); + } + + MapRotation::RotationData::rotationEntry& MapRotation::RotationData::getNextEntry() + { + const auto index = this->index_; + this->index_ = (this->index_ + 1) % this->rotationEntries.size(); // Point index_ to the next entry + return this->rotationEntries.at(index); + } + + void MapRotation::RotationData::parse(const std::string& data) + { + const auto tokens = Utils::String::Split(data, ' '); + + for (std::size_t i = 0; !tokens.empty() && i < (tokens.size() - 1); i += 2) + { + const auto& key = tokens[i]; + const auto& value = tokens[i + 1]; + + if (key == "map" || key == "gametype") + { + this->addEntry(key, value); + } + else + { + throw ParseRotationError(); + } + } + } + + void MapRotation::LoadRotation(const std::string& data) + { + static auto loaded = false; + + if (loaded) + { + // Load the rotation once + return; + } + + try + { + DedicatedRotation.parse(data); + } + catch (const std::exception& ex) + { + Logger::Print(Game::CON_CHANNEL_SERVER, "%s: sv_mapRotation contains invalid data!\n", ex.what()); + } + + Logger::Print(Game::CON_CHANNEL_SERVER, "DedicatedRotation size after parsing is '%u'\n", DedicatedRotation.getEntriesSize()); + + // Shuffles values + if (SVRandomMapRotation.get()) + { + DedicatedRotation.randomize(); + } + + loaded = true; + } + + bool MapRotation::ShouldRotate() + { + if (!Dedicated::IsEnabled() && SVDontRotate.get()) + { + Logger::Print(Game::CON_CHANNEL_SERVER, "Not performing map rotation as sv_dontRotate is true\n"); + SVDontRotate.set(false); + return false; + } + + if (Dvar::Var("party_enable").get() && Dvar::Var("party_host").get()) + { + Logger::Print(Game::CON_CHANNEL_SERVER, "Not performing map rotation as we are hosting a party!\n"); + return false; + } + + return true; + } + + void MapRotation::RestartCurrentMap() + { + std::string svMapname = (*SVMapname)->current.string; + + if (svMapname.empty()) + { + Logger::Print(Game::CON_CHANNEL_SERVER, "mapname dvar is empty! Defaulting to mp_afghan\n"); + svMapname = "mp_afghan"; + } + + if (!Dvar::Var("sv_cheats").get()) + { + Command::Execute(Utils::String::VA("map %s", svMapname.data()), true); + } + else + { + Command::Execute(Utils::String::VA("devmap %s", svMapname.data()), true); + } + } + + void MapRotation::ApplyMapRotation() + { + //Continue to apply gamemode until a map is found + auto foundMap = false; + + std::size_t i = 0; + while (!foundMap && i < DedicatedRotation.getEntriesSize()) + { + const auto& entry = DedicatedRotation.getNextEntry(); + + if (entry.first == "map") + { + Logger::Print("Loading new map: '%s'\n", entry.second.data()); + Command::Execute(Utils::String::VA("map %s", entry.second.data()), true); + + // Map was found so we exit the loop + foundMap = true; + } + else if (entry.first == "gamemode") + { + Logger::Print("Applying new gametype: '%s'\n", entry.second.data()); + Dvar::Var("g_gametype").set(entry.second); + } + + ++i; + } + } + + void MapRotation::SV_MapRotate_f() + { + if (!ShouldRotate()) + { + return; + } + + Logger::Print(Game::CON_CHANNEL_SERVER, "Rotating map...\n"); + const std::string mapRotation = (*SVMapRotation)->current.string; + + if (mapRotation.empty()) + { + Logger::Print(Game::CON_CHANNEL_SERVER, "No rotation defined (sv_mapRotation is empty), restarting map.\n"); + RestartCurrentMap(); + return; + } + + LoadRotation(mapRotation); + + if (DedicatedRotation.getEntriesSize() == 0) + { + Logger::Print(Game::CON_CHANNEL_SERVER, "sv_mapRotation is empty or contains invalid data, restarting map.\n"); + RestartCurrentMap(); + return; + } + + ApplyMapRotation(); + } + + MapRotation::MapRotation() + { + Utils::Hook::Set(0x4152E8, SV_MapRotate_f); + + SVRandomMapRotation = Dvar::Register("sv_randomMapRotation", false, + Game::dvar_flag::DVAR_ARCHIVE, "Randomize map rotation when true"); + SVDontRotate = Dvar::Register("sv_dontRotate", false, + Game::dvar_flag::DVAR_NONE, ""); + } +} diff --git a/src/Components/Modules/MapRotation.hpp b/src/Components/Modules/MapRotation.hpp new file mode 100644 index 00000000..2ce7eb8c --- /dev/null +++ b/src/Components/Modules/MapRotation.hpp @@ -0,0 +1,58 @@ +#pragma once + +namespace Components +{ + class MapRotation : public Component + { + public: + MapRotation(); + + private: + struct ParseRotationError : public std::exception + { + const char* what() const noexcept override { return "Parse Rotation Error"; } + }; + + class RotationData + { + public: + using rotationEntry = std::pair; + + RotationData(); + + void randomize(); + + // In case a new way to enrich the map rotation is added (other than sv_mapRotation) + // this method should be called to add a new entry (gamemode/map & value) + void addEntry(const std::string& key, const std::string& value); + + [[nodiscard]] std::size_t getEntriesSize() const; + rotationEntry& getNextEntry(); + + void parse(const std::string& data); + + private: + std::vector rotationEntries; + + std::size_t index_; + }; + + // Rotation Dvars + static Dvar::Var SVRandomMapRotation; + static Dvar::Var SVDontRotate; + // Game Dvars + static Game::dvar_t** SVMapRotation; + static Game::dvar_t** SVMapname; + + // Holds the parsed data from sv_mapRotation + static RotationData DedicatedRotation; + + static void LoadRotation(const std::string& data); + + static bool ShouldRotate(); + static void RestartCurrentMap(); + static void ApplyMapRotation(); + + static void SV_MapRotate_f(); + }; +}