diff --git a/src/client/component/map_rotation.cpp b/src/client/component/map_rotation.cpp index 7b80bebf..1a39c0cb 100644 --- a/src/client/component/map_rotation.cpp +++ b/src/client/component/map_rotation.cpp @@ -3,6 +3,7 @@ #include "command.hpp" #include "console.hpp" +#include "map_rotation.hpp" #include "scheduler.hpp" #include "game/game.hpp" @@ -15,20 +16,23 @@ namespace map_rotation { namespace { - DWORD previous_priority{}; + rotation_data dedicated_rotation; - void set_dvar(const std::string& dvar, const std::string& value) - { - command::execute(utils::string::va("%s \"%s\"", dvar.data(), value.data()), true); - } + const game::dvar_t* sv_map_rotation; + const game::dvar_t* sv_map_rotation_current; + const game::dvar_t* sv_random_map_rotation; void set_gametype(const std::string& gametype) { - set_dvar("g_gametype", gametype); + assert(!gametype.empty()); + + game::Dvar_SetFromStringByNameFromSource("g_gametype", gametype.data(), game::DVAR_SOURCE_INTERNAL); } void launch_map(const std::string& mapname) { + assert(!mapname.empty()); + command::execute(utils::string::va("map %s", mapname.data()), false); } @@ -46,53 +50,103 @@ namespace map_rotation } } - std::string load_current_map_rotation() + void apply_rotation(rotation_data& rotation) { - auto* rotation = game::Dvar_FindVar("sv_mapRotationCurrent"); - if (!strlen(rotation->current.string)) + assert(!rotation.empty()); + + std::size_t i = 0; + while (i < rotation.get_entries_size()) { - rotation = game::Dvar_FindVar("sv_mapRotation"); - set_dvar("sv_mapRotationCurrent", rotation->current.string); - } - - return rotation->current.string; - } - - std::vector parse_current_map_rotation() - { - const auto rotation = load_current_map_rotation(); - return utils::string::split(rotation, ' '); - } - - void store_new_rotation(const std::vector& elements, const size_t index) - { - std::string value{}; - - for (auto i = index; i < elements.size(); ++i) - { - if (i != index) + const auto& entry = rotation.get_next_entry(); + if (entry.first == "map"s) { - value.push_back(' '); + console::info("Loading new map: '%s'\n", entry.second.data()); + if (!game::SV_MapExists(entry.second.data())) + { + console::info("map_rotation: '%s' map doesn't exist!\n", entry.second.data()); + launch_default_map(); + return; + } + + launch_map(entry.second); + + break; } - value.append(elements[i]); - } + if (entry.first == "gametype"s) + { + console::info("Applying new gametype: '%s'\n", entry.second.data()); + set_gametype(entry.second); + } - set_dvar("sv_mapRotationCurrent", value); + ++i; + } } - void change_process_priority() + void load_rotation(const std::string& data) { - auto* const dvar = game::Dvar_FindVar("sv_autoPriority"); - if (dvar && dvar->current.enabled) + static auto loaded = false; + if (loaded) { - scheduler::on_game_initialized([] - { - SetPriorityClass(GetCurrentProcess(), previous_priority); - }, scheduler::pipeline::main, 1s); + return; + } - previous_priority = GetPriorityClass(GetCurrentProcess()); - SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); + loaded = true; + try + { + dedicated_rotation.parse(data); + } + catch (const std::exception& ex) + { + console::error("%s: sv_map_rotation contains invalid data!\n", ex.what()); + } + + console::debug("dedicated_rotation size after parsing is '%llu'", dedicated_rotation.get_entries_size()); + } + + void load_map_rotation() + { + const std::string map_rotation = sv_map_rotation->current.string; + if (!map_rotation.empty()) + { + console::debug("sv_map_rotation is not empty. Parsing...\n"); + load_rotation(map_rotation); + } + } + + void apply_map_rotation_current(const std::string& data) + { + assert(!data.empty()); + + rotation_data rotation_current; + + try + { + rotation_current.parse(data); + } + catch (const std::exception& ex) + { + console::error("%s: sv_map_rotation_current contains invalid data!\n", ex.what()); + } + + game::Dvar_SetFromStringByNameFromSource("sv_map_rotation_current", "", game::DVAR_SOURCE_INTERNAL); + + if (rotation_current.empty()) + { + console::warn("sv_map_rotation_current is empty or contains invalid data\n"); + launch_default_map(); + return; + } + + apply_rotation(rotation_current); + } + + void randomize_map_rotation() + { + if (sv_random_map_rotation->current.enabled) + { + console::info("Randomizing map rotation\n"); + dedicated_rotation.randomize(); } } @@ -104,37 +158,28 @@ namespace map_rotation return; } - const auto rotation = parse_current_map_rotation(); + console::info("Rotating map...\n"); - for (size_t i = 0; !rotation.empty() && i < (rotation.size() - 1); i += 2) + // This takes priority because of backwards compatibility + const std::string map_rotation_current = sv_map_rotation_current->current.string; + if (!map_rotation_current.empty()) { - const auto& key = rotation[i]; - const auto& value = rotation[i + 1]; - - if (key == "gametype") - { - set_gametype(value); - } - else if (key == "map") - { - store_new_rotation(rotation, i + 2); - change_process_priority(); - if (!game::SV_MapExists(value.data())) - { - console::info("map_rotation: '%s' map doesn't exist!\n", value.data()); - launch_default_map(); - return; - } - launch_map(value); - return; - } - else - { - console::info("Invalid map rotation key: %s\n", key.data()); - } + console::debug("Applying sv_map_rotation_current\n"); + apply_map_rotation_current(map_rotation_current); + return; } - launch_default_map(); + load_map_rotation(); + if (dedicated_rotation.empty()) + { + console::warn("sv_map_rotation is empty or contains invalid data. Restarting map\n"); + launch_default_map(); + return; + } + + randomize_map_rotation(); + + apply_rotation(dedicated_rotation); } void trigger_map_rotation() @@ -152,6 +197,68 @@ namespace map_rotation } } + rotation_data::rotation_data() + : index_(0) + { + } + + void rotation_data::randomize() + { + std::random_device rd; + std::mt19937 gen(rd()); + + std::ranges::shuffle(this->rotation_entries_, gen); + } + + void rotation_data::add_entry(const std::string& key, const std::string& value) + { + this->rotation_entries_.emplace_back(std::make_pair(key, value)); + } + + bool rotation_data::contains(const std::string& key, const std::string& value) const + { + return std::ranges::any_of(this->rotation_entries_, [&](const auto& entry) + { + return entry.first == key && entry.second == value; + }); + } + + bool rotation_data::empty() const noexcept + { + return this->rotation_entries_.empty(); + } + + std::size_t rotation_data::get_entries_size() const noexcept + { + return this->rotation_entries_.size(); + } + + rotation_data::rotation_entry& rotation_data::get_next_entry() + { + const auto index = this->index_; + ++this->index_ %= this->rotation_entries_.size(); + return this->rotation_entries_.at(index); + } + + void rotation_data::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"s || key == "gametype"s) + { + this->add_entry(key, value); + } + else + { + throw parse_rotation_error(); + } + } + } + class component final : public component_interface { public: @@ -164,17 +271,16 @@ namespace map_rotation scheduler::once([] { - dvars::register_string("sv_mapRotation", "", game::DVAR_FLAG_NONE, ""); - dvars::register_string("sv_mapRotationCurrent", "", game::DVAR_FLAG_NONE, ""); - dvars::register_string("sv_autoPriority", "", game::DVAR_FLAG_NONE, "Lowers the process priority during map changes to not cause lags on other servers."); + sv_map_rotation = dvars::register_string("sv_mapRotation", "", game::DVAR_FLAG_NONE, ""); + sv_map_rotation_current = dvars::register_string("sv_mapRotationCurrent", "", game::DVAR_FLAG_NONE, ""); }, scheduler::pipeline::main); + sv_random_map_rotation = dvars::register_bool("sv_randomMapRotation", false, game::DVAR_FLAG_NONE, "Randomize map rotation"); + command::add("map_rotate", &perform_map_rotation); // Hook GScr_ExitLevel utils::hook::jump(0xE2670_b, &trigger_map_rotation, true); // not sure if working - - previous_priority = GetPriorityClass(GetCurrentProcess()); } }; } diff --git a/src/client/component/map_rotation.hpp b/src/client/component/map_rotation.hpp new file mode 100644 index 00000000..ab5d6605 --- /dev/null +++ b/src/client/component/map_rotation.hpp @@ -0,0 +1,34 @@ +#pragma once + +namespace map_rotation +{ + struct parse_rotation_error : public std::exception + { + const char* what() const noexcept override { return "Rotation parse error"; } + }; + + class rotation_data + { + public: + using rotation_entry = std::pair; + + rotation_data(); + + 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 add_entry(const std::string& key, const std::string& value); + + [[nodiscard]] bool contains(const std::string& key, const std::string& value) const; + [[nodiscard]] bool empty() const noexcept; + [[nodiscard]] std::size_t get_entries_size() const noexcept; + [[nodiscard]] rotation_entry& get_next_entry(); + + void parse(const std::string& data); + + private: + std::vector rotation_entries_; + std::size_t index_; + }; +} diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index 02d361c2..23801892 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -83,6 +83,7 @@ #include #include #include +#include #include #include