2022-05-25 12:03:26 -04:00
|
|
|
#include <STDInclude.hpp>
|
2022-12-26 07:07:24 -05:00
|
|
|
#include "MapRotation.hpp"
|
2022-05-25 12:03:26 -04:00
|
|
|
|
|
|
|
namespace Components
|
|
|
|
{
|
|
|
|
Dvar::Var MapRotation::SVRandomMapRotation;
|
|
|
|
Dvar::Var MapRotation::SVDontRotate;
|
2023-01-27 18:05:26 -05:00
|
|
|
Dvar::Var MapRotation::SVNextMap;
|
2022-05-25 12:03:26 -04:00
|
|
|
|
|
|
|
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());
|
|
|
|
|
2022-06-11 05:55:12 -04:00
|
|
|
std::ranges::shuffle(this->rotationEntries_, gen);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void MapRotation::RotationData::addEntry(const std::string& key, const std::string& value)
|
|
|
|
{
|
2022-06-11 05:55:12 -04:00
|
|
|
this->rotationEntries_.emplace_back(std::make_pair(key, value));
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2022-12-06 16:18:29 -05:00
|
|
|
std::size_t MapRotation::RotationData::getEntriesSize() const noexcept
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
2022-06-11 05:55:12 -04:00
|
|
|
return this->rotationEntries_.size();
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
MapRotation::RotationData::rotationEntry& MapRotation::RotationData::getNextEntry()
|
|
|
|
{
|
|
|
|
const auto index = this->index_;
|
2022-06-11 05:55:12 -04:00
|
|
|
++this->index_ %= this->rotationEntries_.size(); // Point index_ to the next entry
|
|
|
|
return this->rotationEntries_.at(index);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2023-01-27 14:48:51 -05:00
|
|
|
MapRotation::RotationData::rotationEntry& MapRotation::RotationData::peekNextEntry()
|
|
|
|
{
|
|
|
|
return this->rotationEntries_.at(this->index_);
|
|
|
|
}
|
|
|
|
|
2022-05-25 12:03:26 -04:00
|
|
|
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];
|
|
|
|
|
2022-12-06 16:18:29 -05:00
|
|
|
if (key == "map"s || key == "gametype"s)
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
|
|
|
this->addEntry(key, value);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-01-09 17:14:07 -05:00
|
|
|
throw MapRotationParseError();
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-06 16:18:29 -05:00
|
|
|
bool MapRotation::RotationData::empty() const noexcept
|
|
|
|
{
|
|
|
|
return this->rotationEntries_.empty();
|
|
|
|
}
|
|
|
|
|
2022-08-24 10:38:14 -04:00
|
|
|
bool MapRotation::RotationData::contains(const std::string& key, const std::string& value) const
|
|
|
|
{
|
2022-12-06 16:18:29 -05:00
|
|
|
return std::ranges::any_of(this->rotationEntries_, [&](const auto& entry)
|
|
|
|
{
|
|
|
|
return entry.first == key && entry.second == value;
|
|
|
|
});
|
2022-08-24 10:38:14 -04:00
|
|
|
}
|
|
|
|
|
2022-05-25 12:03:26 -04:00
|
|
|
void MapRotation::LoadRotation(const std::string& data)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
DedicatedRotation.parse(data);
|
|
|
|
}
|
|
|
|
catch (const std::exception& ex)
|
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::PrintError(Game::CON_CHANNEL_ERROR, "{}: {} contains invalid data!\n", ex.what(), (*Game::sv_mapRotation)->name);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2022-06-22 04:58:51 -04:00
|
|
|
Logger::Debug("DedicatedRotation size after parsing is '{}'", DedicatedRotation.getEntriesSize());
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2022-08-24 10:38:14 -04:00
|
|
|
void MapRotation::LoadMapRotation()
|
|
|
|
{
|
2023-01-27 14:48:51 -05:00
|
|
|
static auto loaded = false;
|
|
|
|
if (loaded)
|
|
|
|
{
|
|
|
|
// Load the rotation once
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
loaded = true;
|
|
|
|
|
2022-08-24 10:38:14 -04:00
|
|
|
const std::string mapRotation = (*Game::sv_mapRotation)->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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-13 11:32:45 -04:00
|
|
|
void MapRotation::AddMapRotationCommands()
|
|
|
|
{
|
2022-06-14 14:43:19 -04:00
|
|
|
Command::Add("addMap", [](Command::Params* params)
|
2022-06-13 11:32:45 -04:00
|
|
|
{
|
|
|
|
if (params->size() < 2)
|
|
|
|
{
|
|
|
|
Logger::Print("{} <map name> : add a map to the map rotation\n", params->get(0));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DedicatedRotation.addEntry("map", params->get(1));
|
|
|
|
});
|
|
|
|
|
2022-06-14 14:43:19 -04:00
|
|
|
Command::Add("addGametype", [](Command::Params* params)
|
2022-06-13 11:32:45 -04:00
|
|
|
{
|
|
|
|
if (params->size() < 2)
|
|
|
|
{
|
|
|
|
Logger::Print("{} <gametype> : add a game mode to the map rotation\n", params->get(0));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DedicatedRotation.addEntry("gametype", params->get(1));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-08-24 10:38:14 -04:00
|
|
|
bool MapRotation::Contains(const std::string& key, const std::string& value)
|
|
|
|
{
|
|
|
|
return DedicatedRotation.contains(key, value);
|
|
|
|
}
|
|
|
|
|
2022-05-25 12:03:26 -04:00
|
|
|
bool MapRotation::ShouldRotate()
|
|
|
|
{
|
|
|
|
if (!Dedicated::IsEnabled() && SVDontRotate.get<bool>())
|
|
|
|
{
|
|
|
|
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<bool>() && Dvar::Var("party_host").get<bool>())
|
|
|
|
{
|
2023-01-09 17:14:07 -05:00
|
|
|
Logger::Warning(Game::CON_CHANNEL_SERVER, "Not performing map rotation as we are hosting a party!\n");
|
2022-05-25 12:03:26 -04:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-06-24 15:22:46 -04:00
|
|
|
void MapRotation::ApplyMap(const std::string& map)
|
|
|
|
{
|
|
|
|
assert(!map.empty());
|
|
|
|
|
2022-08-26 14:12:26 -04:00
|
|
|
if ((*Game::sv_cheats)->current.enabled)
|
2022-06-24 15:22:46 -04:00
|
|
|
{
|
2022-08-26 14:12:26 -04:00
|
|
|
Command::Execute(std::format("devmap {}", map), true);
|
2022-06-24 15:22:46 -04:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-08-26 14:12:26 -04:00
|
|
|
Command::Execute(std::format("map {}", map), true);
|
2022-06-24 15:22:46 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MapRotation::ApplyGametype(const std::string& gametype)
|
|
|
|
{
|
|
|
|
assert(!gametype.empty());
|
2023-01-09 17:14:07 -05:00
|
|
|
Game::Dvar_SetStringByName("g_gametype", gametype.data());
|
2022-06-24 15:22:46 -04:00
|
|
|
}
|
|
|
|
|
2022-05-25 12:03:26 -04:00
|
|
|
void MapRotation::RestartCurrentMap()
|
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
std::string svMapname = (*Game::sv_mapname)->current.string;
|
2022-05-25 12:03:26 -04:00
|
|
|
|
|
|
|
if (svMapname.empty())
|
|
|
|
{
|
|
|
|
Logger::Print(Game::CON_CHANNEL_SERVER, "mapname dvar is empty! Defaulting to mp_afghan\n");
|
2022-12-06 16:18:29 -05:00
|
|
|
svMapname = "mp_afghan"s;
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2022-06-24 15:22:46 -04:00
|
|
|
ApplyMap(svMapname);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2022-06-24 15:22:46 -04:00
|
|
|
void MapRotation::ApplyRotation(RotationData& rotation)
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
2022-12-06 16:18:29 -05:00
|
|
|
assert(!rotation.empty());
|
2022-06-24 15:22:46 -04:00
|
|
|
|
2022-06-13 11:32:45 -04:00
|
|
|
// Continue to apply gametype until a map is found
|
2022-05-25 12:03:26 -04:00
|
|
|
std::size_t i = 0;
|
2023-01-09 17:14:07 -05:00
|
|
|
while (i < rotation.getEntriesSize())
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
2022-06-24 15:22:46 -04:00
|
|
|
const auto& entry = rotation.getNextEntry();
|
2022-12-06 16:18:29 -05:00
|
|
|
if (entry.first == "map"s)
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
2023-01-09 17:14:07 -05:00
|
|
|
Logger::Print("Loading new map: '{}'", entry.second);
|
2022-06-24 15:22:46 -04:00
|
|
|
ApplyMap(entry.second);
|
2022-05-25 12:03:26 -04:00
|
|
|
|
|
|
|
// Map was found so we exit the loop
|
2023-01-09 17:14:07 -05:00
|
|
|
break;
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
2023-01-09 17:14:07 -05:00
|
|
|
|
|
|
|
if (entry.first == "gametype"s)
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
2023-01-09 17:14:07 -05:00
|
|
|
Logger::Print("Applying new gametype: '{}'", entry.second);
|
2022-06-24 15:22:46 -04:00
|
|
|
ApplyGametype(entry.second);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
++i;
|
|
|
|
}
|
2023-01-09 17:51:28 -05:00
|
|
|
|
|
|
|
if (i == rotation.getEntriesSize())
|
|
|
|
{
|
|
|
|
Logger::PrintError(Game::CON_CHANNEL_ERROR, "Map rotation does not contain any map. Restarting\n");
|
|
|
|
RestartCurrentMap();
|
|
|
|
}
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2022-06-24 15:22:46 -04:00
|
|
|
void MapRotation::ApplyMapRotationCurrent(const std::string& data)
|
|
|
|
{
|
|
|
|
assert(!data.empty());
|
|
|
|
|
|
|
|
// Ook, ook, eek
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::Warning(Game::CON_CHANNEL_SERVER, "You are using deprecated {}", (*Game::sv_mapRotationCurrent)->name);
|
2022-06-24 15:22:46 -04:00
|
|
|
|
2022-06-25 14:22:13 -04:00
|
|
|
RotationData rotationCurrent;
|
2022-06-24 15:22:46 -04:00
|
|
|
try
|
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::Debug("Parsing {}", (*Game::sv_mapRotationCurrent)->name);
|
2022-06-25 14:22:13 -04:00
|
|
|
rotationCurrent.parse(data);
|
2022-06-24 15:22:46 -04:00
|
|
|
}
|
|
|
|
catch (const std::exception& ex)
|
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::PrintError(Game::CON_CHANNEL_ERROR, "{}: {} contains invalid data!\n", ex.what(), (*Game::sv_mapRotationCurrent)->name);
|
2022-06-24 15:22:46 -04:00
|
|
|
}
|
|
|
|
|
2022-08-10 17:03:26 -04:00
|
|
|
Game::Dvar_SetString(*Game::sv_mapRotationCurrent, "");
|
2022-06-24 15:22:46 -04:00
|
|
|
|
2022-12-06 16:18:29 -05:00
|
|
|
if (rotationCurrent.empty())
|
2022-06-24 15:22:46 -04:00
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::Print(Game::CON_CHANNEL_SERVER, "{} is empty or contains invalid data. Restarting map\n", (*Game::sv_mapRotationCurrent)->name);
|
2022-06-24 15:22:46 -04:00
|
|
|
RestartCurrentMap();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-25 14:22:13 -04:00
|
|
|
ApplyRotation(rotationCurrent);
|
2022-06-24 15:22:46 -04:00
|
|
|
}
|
|
|
|
|
2023-01-27 14:48:51 -05:00
|
|
|
void MapRotation::SetNextMap(RotationData& rotation)
|
|
|
|
{
|
|
|
|
assert(!rotation.empty());
|
|
|
|
|
|
|
|
const auto& entry = rotation.peekNextEntry();
|
|
|
|
if (entry.first == "map"s)
|
|
|
|
{
|
2023-01-27 18:05:26 -05:00
|
|
|
SVNextMap.set(entry.second);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ClearNextMap();
|
2023-01-27 14:48:51 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MapRotation::SetNextMap(const char* value)
|
|
|
|
{
|
|
|
|
assert(value);
|
2023-01-27 18:05:26 -05:00
|
|
|
SVNextMap.set(value);
|
2023-01-27 14:48:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
void MapRotation::ClearNextMap()
|
|
|
|
{
|
2023-01-27 18:05:26 -05:00
|
|
|
SVNextMap.set("");
|
2023-01-27 14:48:51 -05:00
|
|
|
}
|
|
|
|
|
2022-06-16 12:19:51 -04:00
|
|
|
void MapRotation::RandomizeMapRotation()
|
|
|
|
{
|
|
|
|
if (SVRandomMapRotation.get<bool>())
|
|
|
|
{
|
|
|
|
Logger::Print(Game::CON_CHANNEL_SERVER, "Randomizing the map rotation\n");
|
|
|
|
DedicatedRotation.randomize();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-06-22 04:58:51 -04:00
|
|
|
Logger::Debug("Map rotation was not randomized");
|
2022-06-16 12:19:51 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-25 12:03:26 -04:00
|
|
|
void MapRotation::SV_MapRotate_f()
|
|
|
|
{
|
|
|
|
if (!ShouldRotate())
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Logger::Print(Game::CON_CHANNEL_SERVER, "Rotating map...\n");
|
|
|
|
|
2022-06-24 15:22:46 -04:00
|
|
|
// This takes priority because of backwards compatibility
|
2022-08-10 17:03:26 -04:00
|
|
|
const std::string mapRotationCurrent = (*Game::sv_mapRotationCurrent)->current.string;
|
2022-06-24 15:22:46 -04:00
|
|
|
if (!mapRotationCurrent.empty())
|
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::Debug("Applying {}", (*Game::sv_mapRotationCurrent)->name);
|
2022-06-24 15:22:46 -04:00
|
|
|
ApplyMapRotationCurrent(mapRotationCurrent);
|
2023-01-27 14:48:51 -05:00
|
|
|
ClearNextMap();
|
2022-06-24 15:22:46 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-24 10:38:14 -04:00
|
|
|
LoadMapRotation();
|
2022-12-06 16:18:29 -05:00
|
|
|
if (DedicatedRotation.empty())
|
2022-05-25 12:03:26 -04:00
|
|
|
{
|
2022-08-10 17:03:26 -04:00
|
|
|
Logger::Print(Game::CON_CHANNEL_SERVER, "{} is empty or contains invalid data. Restarting map\n", (*Game::sv_mapRotation)->name);
|
2022-05-25 12:03:26 -04:00
|
|
|
RestartCurrentMap();
|
2023-01-27 14:48:51 -05:00
|
|
|
SetNextMap("map_restart");
|
2022-05-25 12:03:26 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-16 12:19:51 -04:00
|
|
|
RandomizeMapRotation();
|
|
|
|
|
2022-06-24 15:22:46 -04:00
|
|
|
ApplyRotation(DedicatedRotation);
|
2023-01-27 14:48:51 -05:00
|
|
|
SetNextMap(DedicatedRotation);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
|
|
|
|
2023-01-27 18:05:26 -05:00
|
|
|
void MapRotation::RegisterMapRotationDvars()
|
|
|
|
{
|
|
|
|
SVRandomMapRotation = Dvar::Register<bool>("sv_randomMapRotation", false, Game::DVAR_ARCHIVE, "Randomize map rotation when true");
|
|
|
|
SVDontRotate = Dvar::Register<bool>("sv_dontRotate", false, Game::DVAR_NONE, "Do not perform map rotation");
|
|
|
|
SVNextMap = Dvar::Register<const char*>("sv_nextMap", "", Game::DVAR_SERVERINFO, "");
|
|
|
|
}
|
|
|
|
|
2022-05-25 12:03:26 -04:00
|
|
|
MapRotation::MapRotation()
|
|
|
|
{
|
2022-06-13 11:32:45 -04:00
|
|
|
AddMapRotationCommands();
|
2022-05-25 12:03:26 -04:00
|
|
|
Utils::Hook::Set<void(*)()>(0x4152E8, SV_MapRotate_f);
|
|
|
|
|
2023-01-27 18:05:26 -05:00
|
|
|
Events::OnDvarInit(RegisterMapRotationDvars);
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|
2022-06-13 11:32:45 -04:00
|
|
|
|
|
|
|
bool MapRotation::unitTest()
|
|
|
|
{
|
|
|
|
RotationData rotation;
|
|
|
|
|
2022-06-22 04:58:51 -04:00
|
|
|
Logger::Debug("Testing map rotation parsing...");
|
2022-06-13 11:32:45 -04:00
|
|
|
|
|
|
|
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)
|
|
|
|
{
|
2022-11-25 13:33:53 -05:00
|
|
|
Logger::PrintError(Game::CON_CHANNEL_ERROR, "{}: parsing of 'normal' failed\n", ex.what());
|
2022-06-13 11:32:45 -04:00
|
|
|
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;
|
|
|
|
}
|
2022-05-25 12:03:26 -04:00
|
|
|
}
|