diff --git a/src/Components/Modules/Bots.cpp b/src/Components/Modules/Bots.cpp index 099948ab..8271928a 100644 --- a/src/Components/Modules/Bots.cpp +++ b/src/Components/Modules/Bots.cpp @@ -19,6 +19,8 @@ namespace Components std::size_t Bots::BotDataIndex; + std::vector Bots::RemoteBotNames; + struct BotMovementInfo { std::int32_t buttons; // Actions @@ -55,6 +57,17 @@ namespace Components { "activate", Game::CMD_BUTTON_ACTIVATE }, }; + void Bots::UpdateBotNames() + { + const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto* masterServerName = (*Game::com_masterServerName)->current.string; + + Network::Address master(Utils::String::VA("%s:%u", masterServerName, masterPort)); + + Logger::Print("Getting bots...\n"); + Network::Send(master, "getbots"); + } + std::vector Bots::LoadBotNames() { std::vector result; @@ -101,7 +114,7 @@ namespace Components return result; } - int Bots::BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int statStuff, int port) + int Bots::BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int stats, int port) { std::string botName; std::string clanName; @@ -109,6 +122,11 @@ namespace Components static const auto botNames = []() -> std::vector { auto names = LoadBotNames(); + if (names.empty()) + { + Logger::Print("bots.txt was empty. Using the names from the master server\n"); + names = RemoteBotNames; + } if (sv_randomBotNames->current.enabled) { @@ -133,7 +151,7 @@ namespace Components clanName = "BOT"s; } - return _snprintf_s(buffer, 0x400, _TRUNCATE, connectString, num, botName.data(), clanName.data(), protocol, checksum, statVer, statStuff, port); + return _snprintf_s(buffer, 0x400, _TRUNCATE, connectString, num, botName.data(), clanName.data(), protocol, checksum, statVer, stats, port); } void Bots::Spawn(unsigned int count) @@ -446,9 +464,9 @@ namespace Components } } - count = std::min(Game::MAX_CLIENTS, count); + count = std::clamp(count, 1, Game::MAX_CLIENTS); - Logger::Print("Spawning {} {}", count, (count == 1 ? "bot" : "bots")); + Logger::Print("Spawning {} {}\n", count, (count == 1 ? "bot" : "bots")); Spawn(count); }); @@ -476,6 +494,26 @@ namespace Components sv_randomBotNames = Game::Dvar_RegisterBool("sv_randomBotNames", false, Game::DVAR_NONE, "Randomize the bots' names"); sv_replaceBots = Game::Dvar_RegisterBool("sv_replaceBots", false, Game::DVAR_NONE, "Test clients will be replaced by connecting players when the server is full."); + Scheduler::OnGameInitialized(UpdateBotNames, Scheduler::Pipeline::MAIN); + + Network::OnClientPacket("getbotsResponse", [](const Network::Address& address, const std::string& data) + { + const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto* masterServerName = (*Game::com_masterServerName)->current.string; + + Network::Address master(Utils::String::VA("%s:%u", masterServerName, masterPort)); + if (master == address) + { + auto botNames = Utils::String::Split(data, '\n'); + Logger::Print("Got {} names from the master server\n", botNames.size()); + + for (const auto& entry : botNames) + { + RemoteBotNames.emplace_back(entry, "BOT"); + } + } + }); + // Reset BotMovementInfo.active when client is dropped Events::OnClientDisconnect([](const int clientNum) -> void { diff --git a/src/Components/Modules/Bots.hpp b/src/Components/Modules/Bots.hpp index 30925db3..d2aa35f6 100644 --- a/src/Components/Modules/Bots.hpp +++ b/src/Components/Modules/Bots.hpp @@ -10,13 +10,17 @@ namespace Components static void SV_DirectConnect_Full_Check(); private: - using botData = std::pair< std::string, std::string>; + using botData = std::pair; static const Game::dvar_t* sv_randomBotNames; static const Game::dvar_t* sv_replaceBots; static std::size_t BotDataIndex; + static std::vector RemoteBotNames; + + static void UpdateBotNames(); + static std::vector LoadBotNames(); static int BuildConnectString(char* buffer, const char* connectString, int num, int, int protocol, int checksum, int statVer, int statStuff, int port); diff --git a/src/Components/Modules/Dedicated.cpp b/src/Components/Modules/Dedicated.cpp index 30fe4245..a5748d8a 100644 --- a/src/Components/Modules/Dedicated.cpp +++ b/src/Components/Modules/Dedicated.cpp @@ -168,7 +168,7 @@ namespace Components return; } - const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto masterPort = (*Game::com_masterPort)->current.unsignedInt; const auto* masterServerName = (*Game::com_masterServerName)->current.string; Network::Address master(Utils::String::VA("%s:%u", masterServerName, masterPort)); diff --git a/src/Components/Modules/Node.cpp b/src/Components/Modules/Node.cpp index 630b389e..4f170bb4 100644 --- a/src/Components/Modules/Node.cpp +++ b/src/Components/Modules/Node.cpp @@ -184,6 +184,8 @@ namespace Components if (!Dedicated::IsEnabled()) { + if (ServerList::UseMasterServer) return; // don't run node frame if master server is active + if (Game::CL_GetLocalClientConnectionState(0) != Game::CA_DISCONNECTED) { WasIngame = true; @@ -264,7 +266,7 @@ namespace Components if (list.isnode() && (!list.port() || list.port() == address.getPort())) { - if (!Dedicated::IsEnabled() && ServerList::IsOnlineList() && list.protocol() == PROTOCOL) + if (!Dedicated::IsEnabled() && ServerList::IsOnlineList() && !ServerList::UseMasterServer && list.protocol() == PROTOCOL) { #ifdef NODE_SYSTEM_DEBUG Logger::Debug("Inserting {} into the serverlist", address.getString()); diff --git a/src/Components/Modules/ServerList.cpp b/src/Components/Modules/ServerList.cpp index 89dabe28..3a8cce16 100644 --- a/src/Components/Modules/ServerList.cpp +++ b/src/Components/Modules/ServerList.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "Discovery.hpp" #include "Events.hpp" @@ -10,6 +11,10 @@ #include "Toast.hpp" #include "UIFeeder.hpp" +#include +#include +#include + namespace Components { bool ServerList::SortAsc = true; @@ -24,6 +29,8 @@ namespace Components std::vector ServerList::VisibleList; + bool ServerList::UseMasterServer = false; + Dvar::Var ServerList::UIServerSelected; Dvar::Var ServerList::UIServerSelectedMap; Dvar::Var ServerList::NETServerQueryLimit; @@ -271,6 +278,69 @@ namespace Components SortList(); } + void ServerList::ParseNewMasterServerResponse(const std::string& servers) + { + std::lock_guard _(RefreshContainer.mutex); + + rapidjson::Document doc{}; + const rapidjson::ParseResult result = doc.Parse(servers); + if (!result || !doc.IsObject()) + { + UseMasterServer = false; + Logger::Print("Unable to parse JSON response. Using the Node System\n"); + return; + } + + if (!doc.HasMember("servers")) + { + UseMasterServer = false; + Logger::Print("Unable to parse JSON response: we were unable to find any server. Using the Node System\n"); + return; + } + + const rapidjson::Value& list = doc["servers"]; + if (!list.IsArray() || list.Empty()) + { + UseMasterServer = false; + Logger::Print("Unable to parse JSON response: we were unable to find any server. Using the Node System\n"); + return; + } + + Logger::Print("Response from the master server contains {} servers\n", list.Size()); + + std::size_t count = 0; + + for (const auto& entry : list.GetArray()) + { + if (!entry.HasMember("ip") || !entry.HasMember("port")) + { + continue; + } + + if (!entry["ip"].IsString() || !entry["port"].IsInt()) + { + continue; + } + + // Using VA because it's faster + Network::Address server(Utils::String::VA("%s:%u", entry["ip"].GetString(), entry["port"].GetInt())); + server.setType(Game::NA_IP); // Just making sure... + + InsertRequest(server); + ++count; + } + + if (!count) + { + UseMasterServer = false; + Logger::Print("Despite receiving what looked like a valid response from the master server, we got {} servers. Using the Node System\n", count); + return; + } + + UseMasterServer = true; + Logger::Print("Response from the master server was successfully parsed. We got {} servers\n", count); + } + void ServerList::Refresh([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info) { Dvar::Var("ui_serverSelected").set(false); @@ -294,29 +364,30 @@ namespace Components #ifdef IW4_USE_MASTER_SERVER else if (IsOnlineList()) { - const auto masterPort = (*Game::com_masterPort)->current.integer; + const auto masterPort = (*Game::com_masterPort)->current.unsignedInt; const auto* masterServerName = (*Game::com_masterServerName)->current.string; - // Check if our dvars can properly convert to a address - Game::netadr_t masterServerAddr; - if (!GetMasterServer(masterServerName, masterPort, masterServerAddr)) + RefreshContainer.awatingList = true; + RefreshContainer.awaitTime = Game::Sys_Milliseconds(); + + Toast::Show("cardicon_headshot", "Server Browser", "Fetching servers...", 3000); + + const auto* url = "http://iw4x.plutools.pw/v1/servers/iw4x"; + const auto reply = Utils::WebIO("IW4x", url).setTimeout(5000)->get(); + if (reply.empty()) { - Logger::Print("Could not resolve address for {}:{}", masterServerName, masterPort); - Toast::Show("cardicon_headshot", "^1Error", std::format("Could not resolve address for {}:{}", masterServerName, masterPort), 5000); + Logger::Print("Response from {} was empty or the request timed out. Using the Node System\n", url); + Toast::Show("cardicon_headshot", "^1Error", std::format("Could not get a response from {}. Using the Node System", url), 5000); UseMasterServer = false; return; } - Toast::Show("cardicon_headshot", "Server Browser", "Fetching servers...", 3000); + RefreshContainer.awatingList = false; - UseMasterServer = true; + ParseNewMasterServerResponse(reply); - RefreshContainer.awatingList = true; - RefreshContainer.awaitTime = Game::Sys_Milliseconds(); + // TODO: Figure out what to do with this. Leave it to avoid breaking other code RefreshContainer.host = Network::Address(std::format("{}:{}", masterServerName, masterPort)); - - Logger::Print("Sending server list request to master\n"); - Network::SendCommand(RefreshContainer.host, "getservers", std::format("IW4 {} full empty", PROTOCOL)); } #endif else if (IsFavouriteList()) @@ -729,7 +800,9 @@ namespace Components if (Game::Sys_Milliseconds() - RefreshContainer.awaitTime > 5000) { RefreshContainer.awatingList = false; + Logger::Print("We haven't received a response from the master within {} seconds!\n", (Game::Sys_Milliseconds() - RefreshContainer.awaitTime) / 1000); + UseMasterServer = false; Node::Synchronize(); } } diff --git a/src/Components/Modules/ServerList.hpp b/src/Components/Modules/ServerList.hpp index 6526104a..abe61896 100644 --- a/src/Components/Modules/ServerList.hpp +++ b/src/Components/Modules/ServerList.hpp @@ -51,6 +51,8 @@ namespace Components static void UpdateVisibleInfo(); + static bool UseMasterServer; + static bool GetMasterServer(const char* ip, int port, Game::netadr_t& address); static Dvar::Var UIServerSelected; @@ -123,6 +125,8 @@ namespace Components std::recursive_mutex mutex; }; + static void ParseNewMasterServerResponse(const std::string& servers); + static unsigned int GetServerCount(); static const char* GetServerText(unsigned int index, int column); static const char* GetServerInfoText(ServerInfo* server, int column, bool sorting = false);