#include namespace Components { Dvar::Var MapRotation::SVRandomMapRotation; Dvar::Var MapRotation::SVDontRotate; Game::dvar_t** MapRotation::SVMapRotation = reinterpret_cast(0x62C7C44); Game::dvar_t** MapRotation::SVMapRotationCurrent = reinterpret_cast(0x2098DF0); 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::ranges::shuffle(this->rotationEntries_, 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->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(); } } } json11::Json MapRotation::RotationData::to_json() const { std::vector mapVector; std::vector gametypeVector; for (const auto& [key, val] : this->rotationEntries_) { if (key == "map") { mapVector.emplace_back(val); } else if (key == "gametype") { gametypeVector.emplace_back(val); } } json11::Json mapRotationJson = json11::Json::object { {"maps", mapVector}, {"gametypes", gametypeVector}, }; return mapRotationJson; } void MapRotation::LoadRotation(const std::string& data) { static auto loaded = false; if (loaded) { // Load the rotation once return; } loaded = true; try { DedicatedRotation.parse(data); } catch (const std::exception& ex) { Logger::PrintError(Game::CON_CHANNEL_ERROR, "{}: {} contains invalid data!\n", ex.what(), (*SVMapRotation)->name); } Logger::Debug("DedicatedRotation size after parsing is '{}'", DedicatedRotation.getEntriesSize()); } void MapRotation::AddMapRotationCommands() { Command::Add("addMap", [](Command::Params* params) { if (params->size() < 2) { Logger::Print("{} : add a map to the map rotation\n", params->get(0)); return; } DedicatedRotation.addEntry("map", params->get(1)); }); Command::Add("addGametype", [](Command::Params* params) { if (params->size() < 2) { Logger::Print("{} : add a game mode to the map rotation\n", params->get(0)); return; } DedicatedRotation.addEntry("gametype", params->get(1)); }); } 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::ApplyMap(const std::string& map) { assert(!map.empty()); if (Dvar::Var("sv_cheats").get()) { Command::Execute(Utils::String::VA("devmap %s", map.data()), true); } else { Command::Execute(Utils::String::VA("map %s", map.data()), true); } } void MapRotation::ApplyGametype(const std::string& gametype) { assert(!gametype.empty()); Dvar::Var("g_gametype").set(gametype.data()); } 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"; } ApplyMap(svMapname); } void MapRotation::ApplyRotation(RotationData& rotation) { assert(rotation.getEntriesSize() != 0); // Continue to apply gametype until a map is found auto foundMap = false; std::size_t i = 0; while (!foundMap && i < rotation.getEntriesSize()) { const auto& entry = rotation.getNextEntry(); if (entry.first == "map") { Logger::Debug("Loading new map: '{}'", entry.second); ApplyMap(entry.second); // Map was found so we exit the loop foundMap = true; } else if (entry.first == "gametype") { Logger::Debug("Applying new gametype: '{}'", entry.second); ApplyGametype(entry.second); } ++i; } } void MapRotation::ApplyMapRotationCurrent(const std::string& data) { assert(!data.empty()); // Ook, ook, eek Logger::Warning(Game::CON_CHANNEL_SERVER, "You are using deprecated {}", (*SVMapRotationCurrent)->name); RotationData currentRotation; try { Logger::Debug("Parsing {}", (*SVMapRotationCurrent)->name); currentRotation.parse(data); } catch (const std::exception& ex) { Logger::PrintError(Game::CON_CHANNEL_ERROR, "{}: {} contains invalid data!\n", ex.what(), (*SVMapRotationCurrent)->name); } Game::Dvar_SetString(*SVMapRotationCurrent, ""); if (currentRotation.getEntriesSize() == 0) { Logger::Print(Game::CON_CHANNEL_SERVER, "{} is empty or contains invalid data. Restarting map\n", (*SVMapRotationCurrent)->name); RestartCurrentMap(); return; } ApplyRotation(currentRotation); } void MapRotation::RandomizeMapRotation() { if (SVRandomMapRotation.get()) { Logger::Print(Game::CON_CHANNEL_SERVER, "Randomizing the map rotation\n"); DedicatedRotation.randomize(); } else { Logger::Debug("Map rotation was not randomized"); } } void MapRotation::SV_MapRotate_f() { if (!ShouldRotate()) { return; } Logger::Print(Game::CON_CHANNEL_SERVER, "Rotating map...\n"); // This takes priority because of backwards compatibility const std::string mapRotationCurrent = (*SVMapRotationCurrent)->current.string; if (!mapRotationCurrent.empty()) { Logger::Debug("Applying {}", (*SVMapRotationCurrent)->name); ApplyMapRotationCurrent(mapRotationCurrent); return; } const std::string mapRotation = (*SVMapRotation)->current.string; // People may have sv_mapRotation empty because they only use 'addMap' or 'addGametype' if (!mapRotation.empty()) { Logger::Debug("sv_mapRotation is not empty. Parsing..."); LoadRotation(mapRotation); } if (DedicatedRotation.getEntriesSize() == 0) { Logger::Print(Game::CON_CHANNEL_SERVER, "{} is empty or contains invalid data. Restarting map\n", (*SVMapRotation)->name); RestartCurrentMap(); return; } RandomizeMapRotation(); ApplyRotation(DedicatedRotation); } MapRotation::MapRotation() { AddMapRotationCommands(); 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, "Do not perform map rotation"); } bool MapRotation::unitTest() { RotationData rotation; Logger::Debug("Testing map rotation parsing..."); const auto* normal = "map mp_highrise map mp_terminal map mp_firingrange map mp_trailerpark gametype dm map mp_shipment_long"; try { DedicatedRotation.parse(normal); } catch (const std::exception& ex) { Logger::PrintError(Game::CON_CHANNEL_ERROR, "{}: parsing of 'normal' failed", ex.what()); return false; } const auto* mistake = "spdevmap mp_dome"; auto success = false; try { DedicatedRotation.parse(mistake); } catch (const std::exception& ex) { Logger::Debug("{}: parsing of 'normal' failed as expected", ex.what()); success = true; } return success; } }