#include "STDInclude.hpp" namespace Components { bool ServerList::SortAsc = true; int ServerList::SortKey = ServerList::Column::Ping; unsigned int ServerList::CurrentServer = 0; ServerList::Container ServerList::RefreshContainer; std::vector ServerList::OnlineList; std::vector ServerList::OfflineList; std::vector ServerList::FavouriteList; std::vector ServerList::VisibleList; std::vector* ServerList::GetList() { if (ServerList::IsOnlineList()) { return &ServerList::OnlineList; } else if (ServerList::IsOfflineList()) { return &ServerList::OfflineList; } else if (ServerList::IsFavouriteList()) { return &ServerList::FavouriteList; } return nullptr; } bool ServerList::IsFavouriteList() { return (Dvar::Var("ui_netSource").Get() == 2); } bool ServerList::IsOfflineList() { return (Dvar::Var("ui_netSource").Get() == 0); } bool ServerList::IsOnlineList() { return (Dvar::Var("ui_netSource").Get() == 1); } unsigned int ServerList::GetServerCount() { return ServerList::VisibleList.size(); } const char* ServerList::GetServerText(unsigned int index, int column) { ServerList::ServerInfo* info = ServerList::GetServer(index); if (info) { return ServerList::GetServerText(info, column); } return ""; } const char* ServerList::GetServerText(ServerList::ServerInfo* server, int column) { if (!server) return ""; switch (column) { case Column::Password: { return (server->Password ? "X" : ""); } case Column::Matchtype: { return ((server->MatchType == 1) ? "P" : "M"); } case Column::Hostname: { return server->Hostname.data(); } case Column::Mapname: { if (server->SVRunning) { return Game::UI_LocalizeMapName(server->Mapname.data()); } else { return Utils::String::VA("^3%s", Game::UI_LocalizeMapName(server->Mapname.data())); } } case Column::Players: { return Utils::String::VA("%i (%i)", server->Clients, server->MaxClients); } case Column::Gametype: { return Game::UI_LocalizeGameType(server->Gametype.data()); } case Column::Mod: { if (server->Mod != "") { return (server->Mod.data() + 5); } return ""; } case Column::Ping: { return Utils::String::VA("%i", server->Ping); } } return ""; } void ServerList::SelectServer(unsigned int index) { ServerList::CurrentServer = index; ServerList::ServerInfo* info = ServerList::GetCurrentServer(); if (info) { Dvar::Var("ui_serverSelected").Set(true); Dvar::Var("ui_serverSelectedMap").Set(info->Mapname); } else { Dvar::Var("ui_serverSelected").Set(false); } } void ServerList::UpdateVisibleList() { auto list = ServerList::GetList(); if (!list) return; std::vector tempList(*list); if (tempList.empty()) { ServerList::Refresh(); } else { list->clear(); ServerList::RefreshContainer.Mutex.lock(); ServerList::RefreshContainer.SendCount = 0; ServerList::RefreshContainer.SentCount = 0; for (auto server : tempList) { ServerList::InsertRequest(server.Addr, false); } ServerList::RefreshContainer.Mutex.unlock(); } } void ServerList::RefreshVisibleList() { Dvar::Var("ui_serverSelected").Set(false); ServerList::VisibleList.clear(); auto list = ServerList::GetList(); if (!list) return; // Refresh entirely, if there is no entry in the list if (list->empty()) { ServerList::Refresh(); return; } bool ui_browserShowFull = Dvar::Var("ui_browserShowFull").Get(); bool ui_browserShowEmpty = Dvar::Var("ui_browserShowEmpty").Get(); int ui_browserShowHardcore = Dvar::Var("ui_browserKillcam").Get(); int ui_browserShowPassword = Dvar::Var("ui_browserShowPassword").Get(); int ui_browserMod = Dvar::Var("ui_browserMod").Get(); int ui_joinGametype = Dvar::Var("ui_joinGametype").Get(); for (unsigned int i = 0; i < list->size(); ++i) { ServerList::ServerInfo* info = &(*list)[i]; // Filter full servers if (!ui_browserShowFull && info->Clients >= info->MaxClients) continue; // Filter empty servers if (!ui_browserShowEmpty && info->Clients <= 0) continue; // Filter hardcore servers if ((ui_browserShowHardcore == 0 && info->Hardcore) || (ui_browserShowHardcore == 1 && !info->Hardcore)) continue; // Filter servers with password if ((ui_browserShowPassword == 0 && info->Password) || (ui_browserShowPassword == 1 && !info->Password)) continue; // Don't show modded servers if ((ui_browserMod == 0 && info->Mod.size()) || (ui_browserMod == 1 && !info->Mod.size())) continue; // Filter by gametype if (ui_joinGametype > 0 && (ui_joinGametype -1) < *Game::gameTypeCount && Game::gameTypes[(ui_joinGametype - 1)].gameType != info->Gametype) continue; ServerList::VisibleList.push_back(i); } ServerList::SortList(); } void ServerList::Refresh() { Dvar::Var("ui_serverSelected").Set(false); Localization::Set("MPUI_SERVERQUERIED", "Sent requests: 0/0"); // ServerList::OnlineList.clear(); // ServerList::OfflineList.clear(); // ServerList::FavouriteList.clear(); auto list = ServerList::GetList(); if (list) list->clear(); ServerList::VisibleList.clear(); ServerList::RefreshContainer.Mutex.lock(); ServerList::RefreshContainer.Servers.clear(); ServerList::RefreshContainer.SendCount = 0; ServerList::RefreshContainer.SentCount = 0; ServerList::RefreshContainer.Mutex.unlock(); if (ServerList::IsOfflineList()) { Discovery::Perform(); } else if (ServerList::IsOnlineList()) { #ifdef USE_LEGACY_SERVER_LIST ServerList::RefreshContainer.AwatingList = true; ServerList::RefreshContainer.AwaitTime = Game::Com_Milliseconds(); int masterPort = Dvar::Var("masterPort").Get(); const char* masterServerName = Dvar::Var("masterServerName").Get(); ServerList::RefreshContainer.Host = Network::Address(Utils::VA("%s:%u", masterServerName, masterPort)); Logger::Print("Sending serverlist request to master: %s:%u\n", masterServerName, masterPort); Network::SendCommand(ServerList::RefreshContainer.Host, "getservers", Utils::VA("IW4 %i full empty", PROTOCOL)); //Network::SendCommand(ServerList::RefreshContainer.Host, "getservers", "0 full empty"); #else Node::SyncNodeList(); #endif } else if (ServerList::IsFavouriteList()) { ServerList::LoadFavourties(); } } void ServerList::StoreFavourite(std::string server) { //json11::Json::parse() std::vector servers; if (Utils::IO::FileExists("players/favourites.json")) { std::string data = Utils::IO::ReadFile("players/favourites.json"); json11::Json object = json11::Json::parse(data, data); if (!object.is_array()) { Logger::Print("Favourites storage file is invalid!\n"); Game::MessageBox("Favourites storage file is invalid!", "Error"); return; } auto storedServers = object.array_items(); for (unsigned int i = 0; i < storedServers.size(); ++i) { if (!storedServers[i].is_string()) continue; if (storedServers[i].string_value() == server) { Game::MessageBox("Server already marked as favourite.", "Error"); return; } servers.push_back(storedServers[i].string_value()); } } servers.push_back(server); json11::Json data = json11::Json(servers); Utils::IO::WriteFile("players/favourites.json", data.dump()); Game::MessageBox("Server added to favourites.", "Success"); } void ServerList::LoadFavourties() { if (ServerList::IsFavouriteList() && Utils::IO::FileExists("players/favourites.json")) { auto list = ServerList::GetList(); if (list) list->clear(); std::string data = Utils::IO::ReadFile("players/favourites.json"); json11::Json object = json11::Json::parse(data, data); if (!object.is_array()) { Logger::Print("Favourites storage file is invalid!\n"); Game::MessageBox("Favourites storage file is invalid!", "Error"); return; } auto servers = object.array_items(); for (unsigned int i = 0; i < servers.size(); ++i) { if(!servers[i].is_string()) continue; ServerList::InsertRequest(servers[i].string_value(), true); } } } void ServerList::InsertRequest(Network::Address address, bool acquireMutex) { if (acquireMutex) ServerList::RefreshContainer.Mutex.lock(); ServerList::Container::ServerContainer container; container.Sent = false; container.Target = address; bool alreadyInserted = false; for (auto &server : ServerList::RefreshContainer.Servers) { if (server.Target == container.Target) { alreadyInserted = true; break; } } if (!alreadyInserted) { ServerList::RefreshContainer.Servers.push_back(container); auto list = ServerList::GetList(); if (list) { for (auto server : *list) { if (server.Addr == container.Target) { --ServerList::RefreshContainer.SendCount; --ServerList::RefreshContainer.SentCount; break; } } } ++ServerList::RefreshContainer.SendCount; } if (acquireMutex) ServerList::RefreshContainer.Mutex.unlock(); } void ServerList::Insert(Network::Address address, Utils::InfoString info) { ServerList::RefreshContainer.Mutex.lock(); for (auto i = ServerList::RefreshContainer.Servers.begin(); i != ServerList::RefreshContainer.Servers.end();) { // Our desired server if (i->Target == address && i->Sent) { // Challenge did not match if (i->Challenge != info.Get("challenge")) { // Shall we remove the server from the queue? // Better not, it might send a second response with the correct challenge. // This might happen when users refresh twice (or more often) in a short period of time break; } ServerInfo server; server.Hostname = info.Get("hostname"); server.Mapname = info.Get("mapname"); server.Gametype = info.Get("gametype"); server.Shortversion = info.Get("shortversion"); server.Mod = info.Get("fs_game"); server.MatchType = atoi(info.Get("matchtype").data()); server.Clients = atoi(info.Get("clients").data()); server.SecurityLevel = atoi(info.Get("securityLevel").data()); server.MaxClients = atoi(info.Get("sv_maxclients").data()); server.Password = (atoi(info.Get("isPrivate").data()) != 0); server.Hardcore = (atoi(info.Get("hc").data()) != 0); server.SVRunning = (atoi(info.Get("sv_running").data()) != 0); server.Ping = (Game::Sys_Milliseconds() - i->SendTime); server.Addr = address; // Remove server from queue i = ServerList::RefreshContainer.Servers.erase(i); // Check if already inserted and remove auto list = ServerList::GetList(); if (!list) return; unsigned int k = 0; for (auto j = list->begin(); j != list->end(); ++k) { if (j->Addr == address) { j = list->erase(j); } else { ++j; } } // Also remove from visible list for (auto j = ServerList::VisibleList.begin(); j != ServerList::VisibleList.end();) { if (*j == k) { j = ServerList::VisibleList.erase(j); } else { ++j; } } if (info.Get("gamename") == "IW4" && server.MatchType #ifndef DEBUG && server.Shortversion == VERSION_STR #endif ) { auto lList = ServerList::GetList(); if (lList) { lList->push_back(server); ServerList::RefreshVisibleList(); } } break; } else { ++i; } } ServerList::RefreshContainer.Mutex.unlock(); } ServerList::ServerInfo* ServerList::GetCurrentServer() { return ServerList::GetServer(ServerList::CurrentServer); } void ServerList::SortList() { qsort(ServerList::VisibleList.data(), ServerList::VisibleList.size(), sizeof(int), [] (const void* first, const void* second) { const unsigned int server1 = *static_cast(first); const unsigned int server2 = *static_cast(second); ServerInfo* info1 = nullptr; ServerInfo* info2 = nullptr; auto list = ServerList::GetList(); if (!list) return 0; if (list->size() > server1) info1 = &(*list)[server1]; if (list->size() > server2) info2 = &(*list)[server2]; if (!info1) return 1; if (!info2) return -1; // Numerical comparisons if (ServerList::SortKey == ServerList::Column::Ping) { return ((info1->Ping - info2->Ping) * (ServerList::SortAsc ? 1 : -1)); } else if (ServerList::SortKey == ServerList::Column::Players) { return ((info1->Clients - info2->Clients) * (ServerList::SortAsc ? 1 : -1)); } std::string text1 = Colors::Strip(ServerList::GetServerText(info1, ServerList::SortKey)); std::string text2 = Colors::Strip(ServerList::GetServerText(info2, ServerList::SortKey)); // ASCII-based comparison return (text1.compare(text2) * (ServerList::SortAsc ? 1 : -1)); }); } ServerList::ServerInfo* ServerList::GetServer(unsigned int index) { if (ServerList::VisibleList.size() > index) { auto list = ServerList::GetList(); if (!list) return nullptr; if (list->size() > ServerList::VisibleList[index]) { return &(*list)[ServerList::VisibleList[index]]; } } return nullptr; } void ServerList::Frame() { if (!ServerList::RefreshContainer.Mutex.try_lock()) return; if (ServerList::RefreshContainer.AwatingList) { // Check if we haven't got a response within 10 seconds if (Game::Sys_Milliseconds() - ServerList::RefreshContainer.AwaitTime > 5000) { ServerList::RefreshContainer.AwatingList = false; Logger::Print("We haven't received a response from the master within %d seconds!\n", (Game::Sys_Milliseconds() - ServerList::RefreshContainer.AwaitTime) / 1000); } } // Send requests to 10 servers each frame int SendServers = 10; for (unsigned int i = 0; i < ServerList::RefreshContainer.Servers.size(); ++i) { ServerList::Container::ServerContainer* server = &ServerList::RefreshContainer.Servers[i]; if (server->Sent) continue; // Found server we can send a request to server->Sent = true; SendServers--; server->SendTime = Game::Sys_Milliseconds(); server->Challenge = fmt::sprintf("%X", Utils::Cryptography::Rand::GenerateInt()); ++ServerList::RefreshContainer.SentCount; Network::SendCommand(server->Target, "getinfo", server->Challenge); // Display in the menu, like in COD4 Localization::Set("MPUI_SERVERQUERIED", fmt::sprintf("Sent requests: %d/%d", ServerList::RefreshContainer.SentCount, ServerList::RefreshContainer.SendCount)); if (SendServers <= 0) break; } ServerList::RefreshContainer.Mutex.unlock(); } void ServerList::UpdateSource() { Dvar::Var netSource("ui_netSource"); int source = netSource.Get(); if (++source > netSource.Get()->max.i) { source = 0; } netSource.Set(source); ServerList::RefreshVisibleList(); } void ServerList::UpdateGameType() { Dvar::Var joinGametype("ui_joinGametype"); int gametype = joinGametype.Get(); if (++gametype > *Game::gameTypeCount) { gametype = 0; } joinGametype.Set(gametype); ServerList::RefreshVisibleList(); } ServerList::ServerList() { ServerList::OnlineList.clear(); ServerList::VisibleList.clear(); Dvar::OnInit([] () { Dvar::Register("ui_serverSelected", false, Game::dvar_flag::DVAR_FLAG_NONE, "Whether a server has been selected in the serverlist"); Dvar::Register("ui_serverSelectedMap", "mp_afghan", Game::dvar_flag::DVAR_FLAG_NONE, "Map of the selected server"); }); Localization::Set("MPUI_SERVERQUERIED", "Sent requests: 0/0"); Network::Handle("getServersResponse", [] (Network::Address address, std::string data) { if (ServerList::RefreshContainer.Host != address) return; // Only parse from host we sent to ServerList::RefreshContainer.AwatingList = false; ServerList::RefreshContainer.Mutex.lock(); int offset = 0; int count = ServerList::RefreshContainer.Servers.size(); ServerList::MasterEntry* entry = nullptr; // Find first entry do { entry = reinterpret_cast(const_cast(data.data()) + offset++); } while (!entry->HasSeparator() && !entry->IsEndToken()); for (int i = 0; !entry[i].IsEndToken() && entry[i].HasSeparator(); ++i) { Network::Address serverAddr = address; serverAddr.SetIP(entry[i].IP); serverAddr.SetPort(ntohs(entry[i].Port)); serverAddr.SetType(Game::NA_IP); ServerList::InsertRequest(serverAddr, false); } Logger::Print("Parsed %d servers from master\n", ServerList::RefreshContainer.Servers.size() - count); ServerList::RefreshContainer.Mutex.unlock(); }); // Set default masterServerName + port and save it #ifdef USE_LEGACY_SERVER_LIST Utils::Hook::Set(0x60AD92, "localhost"); Utils::Hook::Set(0x60AD90, Game::dvar_flag::DVAR_FLAG_SAVED); // masterServerName Utils::Hook::Set(0x60ADC6, Game::dvar_flag::DVAR_FLAG_SAVED); // masterPort #endif // Add server list feeder UIFeeder::Add(2.0f, ServerList::GetServerCount, ServerList::GetServerText, ServerList::SelectServer); // Add required UIScripts UIScript::Add("UpdateFilter", ServerList::RefreshVisibleList); UIScript::Add("RefreshFilter", ServerList::UpdateVisibleList); UIScript::Add("RefreshServers", ServerList::Refresh); UIScript::Add("JoinServer", [] () { ServerList::ServerInfo* info = ServerList::GetServer(ServerList::CurrentServer); if (info) { Party::Connect(info->Addr); } }); UIScript::Add("ServerSort", [] (UIScript::Token token) { int key = token.Get(); if (ServerList::SortKey == key) { ServerList::SortAsc = !ServerList::SortAsc; } else { ServerList::SortKey = key; ServerList::SortAsc = true; } Logger::Print("Sorting server list by token: %d\n", ServerList::SortKey); ServerList::SortList(); }); UIScript::Add("CreateListFavorite", [] () { ServerList::ServerInfo* info = ServerList::GetCurrentServer(); if (info) { ServerList::StoreFavourite(info->Addr.GetString()); } }); UIScript::Add("CreateFavorite", [] () { ServerList::StoreFavourite(Dvar::Var("ui_favoriteAddress").Get()); }); UIScript::Add("CreateCurrentServerFavorite", []() { if (Dvar::Var("cl_ingame").Get()) { std::string addressText = Network::Address(*Game::connectedHost).GetString(); if (addressText != "0.0.0.0:0" && addressText != "loopback") { ServerList::StoreFavourite(addressText); } } }); // Add required ownerDraws UIScript::AddOwnerDraw(220, ServerList::UpdateSource); UIScript::AddOwnerDraw(253, ServerList::UpdateGameType); // Add frame callback Renderer::OnFrame(ServerList::Frame); // This is placed here in case the anticheat has been disabled! #ifndef DEBUG QuickPatch::OnFrame(AntiCheat::ReadIntegrityCheck); #endif } ServerList::~ServerList() { ServerList::OnlineList.clear(); ServerList::OfflineList.clear(); ServerList::FavouriteList.clear(); ServerList::VisibleList.clear(); ServerList::RefreshContainer.Mutex.lock(); ServerList::RefreshContainer.AwatingList = false; ServerList::RefreshContainer.Servers.clear(); ServerList::RefreshContainer.Mutex.unlock(); } }