/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #ifndef DISABLE_NETWORK #include "ServerList.h" #include "../Context.h" #include "../Diagnostic.h" #include "../PlatformEnvironment.h" #include "../config/Config.h" #include "../core/File.h" #include "../core/FileStream.h" #include "../core/Guard.hpp" #include "../core/Http.h" #include "../core/Json.hpp" #include "../core/Memory.hpp" #include "../core/Path.hpp" #include "../core/String.hpp" #include "../localisation/Language.h" #include "../platform/Platform.h" #include "Network.h" #include "Socket.h" #include #include namespace OpenRCT2::Network { int32_t ServerListEntry::CompareTo(const ServerListEntry& other) const { const auto& a = *this; const auto& b = other; if (a.Favourite != b.Favourite) { return a.Favourite ? -1 : 1; } if (a.Local != b.Local) { return a.Local ? -1 : 1; } bool serverACompatible = a.Version == Network::GetVersion(); bool serverBCompatible = b.Version == Network::GetVersion(); if (serverACompatible != serverBCompatible) { return serverACompatible ? -1 : 1; } if (a.RequiresPassword != b.RequiresPassword) { return a.RequiresPassword ? 1 : -1; } if (a.Players != b.Players) { return a.Players > b.Players ? -1 : 1; } return String::compare(a.Name, b.Name, true); } bool ServerListEntry::IsVersionValid() const noexcept { return Version.empty() || Version == Network::GetVersion(); } std::optional ServerListEntry::FromJson(json_t& server) { Guard::Assert(server.is_object(), "ServerListEntry::FromJson expects parameter server to be object"); const auto port = Json::GetNumber(server["port"]); const auto name = Json::GetString(server["name"]); const auto description = Json::GetString(server["description"]); const auto requiresPassword = Json::GetBoolean(server["requiresPassword"]); const auto version = Json::GetString(server["version"]); const auto players = Json::GetNumber(server["players"]); const auto maxPlayers = Json::GetNumber(server["maxPlayers"]); std::string ip; // if server["ip"] or server["ip"]["v4"] are values, this will throw an exception, so check first if (server["ip"].is_object() && server["ip"]["v4"].is_array()) { ip = Json::GetString(server["ip"]["v4"][0]); } if (name.empty() || version.empty()) { LOG_VERBOSE("Cowardly refusing to add server without name or version specified."); return std::nullopt; } ServerListEntry entry; entry.Address = ip + ":" + std::to_string(port); entry.Name = name; entry.Description = description; entry.Version = version; entry.RequiresPassword = requiresPassword; entry.Players = players; entry.MaxPlayers = maxPlayers; return entry; } void ServerList::Sort() { _serverEntries.erase( std::unique( _serverEntries.begin(), _serverEntries.end(), [](const ServerListEntry& a, const ServerListEntry& b) { if (a.Favourite == b.Favourite) { return String::iequals(a.Address, b.Address); } return false; }), _serverEntries.end()); std::sort(_serverEntries.begin(), _serverEntries.end(), [](const ServerListEntry& a, const ServerListEntry& b) { return a.CompareTo(b) < 0; }); } ServerListEntry& ServerList::GetServer(size_t index) { return _serverEntries[index]; } size_t ServerList::GetCount() const { return _serverEntries.size(); } void ServerList::Add(const ServerListEntry& entry) { _serverEntries.push_back(entry); Sort(); } void ServerList::AddRange(const std::vector& entries) { _serverEntries.insert(_serverEntries.end(), entries.begin(), entries.end()); Sort(); } void ServerList::AddOrUpdateRange(const std::vector& entries) { for (auto& existsEntry : _serverEntries) { auto match = std::find_if(entries.begin(), entries.end(), [&](const ServerListEntry& entry) { return existsEntry.Address == entry.Address; }); if (match != entries.end()) { // Keep favourites auto fav = existsEntry.Favourite; existsEntry = *match; existsEntry.Favourite = fav; } } std::vector newServers; std::copy_if(entries.begin(), entries.end(), std::back_inserter(newServers), [this](const ServerListEntry& entry) { return std::find_if( _serverEntries.begin(), _serverEntries.end(), [&](const ServerListEntry& existsEntry) { return existsEntry.Address == entry.Address; }) == _serverEntries.end(); }); AddRange(newServers); } void ServerList::Clear() noexcept { _serverEntries.clear(); } std::vector ServerList::ReadFavourites() const { LOG_VERBOSE("server_list_read(...)"); std::vector entries; try { auto& env = GetContext()->GetPlatformEnvironment(); auto path = env.GetFilePath(PathId::networkServers); if (File::Exists(path)) { auto fs = FileStream(path, FileMode::open); auto numEntries = fs.ReadValue(); for (size_t i = 0; i < numEntries; i++) { ServerListEntry serverInfo; serverInfo.Address = fs.ReadString(); serverInfo.Name = fs.ReadString(); serverInfo.RequiresPassword = false; serverInfo.Description = fs.ReadString(); serverInfo.Version.clear(); serverInfo.Favourite = true; serverInfo.Players = 0; serverInfo.MaxPlayers = 0; entries.push_back(std::move(serverInfo)); } } } catch (const std::exception& e) { LOG_ERROR("Unable to read server list: %s", e.what()); entries = std::vector(); } return entries; } void ServerList::ReadAndAddFavourites() { _serverEntries.erase( std::remove_if( _serverEntries.begin(), _serverEntries.end(), [](const ServerListEntry& entry) { return entry.Favourite; }), _serverEntries.end()); auto entries = ReadFavourites(); AddRange(entries); } void ServerList::WriteFavourites() const { // Save just favourite servers std::vector favouriteServers; std::copy_if( _serverEntries.begin(), _serverEntries.end(), std::back_inserter(favouriteServers), [](const ServerListEntry& entry) { return entry.Favourite; }); WriteFavourites(favouriteServers); } bool ServerList::WriteFavourites(const std::vector& entries) const { LOG_VERBOSE("server_list_write(%d, 0x%p)", entries.size(), entries.data()); auto& env = GetContext()->GetPlatformEnvironment(); auto path = Path::Combine(env.GetDirectoryPath(DirBase::user), u8"servers.cfg"); try { auto fs = FileStream(path, FileMode::write); fs.WriteValue(static_cast(entries.size())); for (const auto& entry : entries) { fs.WriteString(entry.Address); fs.WriteString(entry.Name); fs.WriteString(entry.Description); } return true; } catch (const std::exception& e) { LOG_ERROR("Unable to write server list: %s", e.what()); return false; } } std::future> ServerList::FetchLocalServerListAsync( const INetworkEndpoint& broadcastEndpoint) const { auto broadcastAddress = broadcastEndpoint.GetHostname(); return std::async(std::launch::async, [broadcastAddress] { constexpr auto kReceiveDelayInMs = 10; constexpr auto kReceiveWaitInMs = 2000; std::string_view msg = kLanBroadcastMsg; auto udpSocket = CreateUdpSocket(); LOG_VERBOSE("Broadcasting %zu bytes to the LAN (%s)", msg.size(), broadcastAddress.c_str()); auto len = udpSocket->SendData(broadcastAddress, kLanBroadcastPort, msg.data(), msg.size()); if (len != msg.size()) { throw std::runtime_error("Unable to broadcast server query."); } std::vector entries; for (int i = 0; i < (kReceiveWaitInMs / kReceiveDelayInMs); i++) { try { // Start with initialised buffer in case we receive a non-terminated string char buffer[1024]{}; size_t recievedLen{}; std::unique_ptr endpoint; auto p = udpSocket->ReceiveData(buffer, sizeof(buffer) - 1, &recievedLen, &endpoint); if (p == ReadPacket::success) { auto sender = endpoint->GetHostname(); LOG_VERBOSE("Received %zu bytes back from %s", recievedLen, sender.c_str()); auto jinfo = Json::FromString(std::string_view(buffer)); if (jinfo.is_object()) { jinfo["ip"] = { { "v4", { sender } } }; auto entry = ServerListEntry::FromJson(jinfo); if (entry.has_value()) { (*entry).Local = true; entries.push_back(std::move(*entry)); } } } } catch (const std::exception& e) { LOG_WARNING("Error receiving data: %s", e.what()); } Platform::Sleep(kReceiveDelayInMs); } return entries; }); } std::future> ServerList::FetchLocalServerListAsync() const { return std::async(std::launch::async, [&] { // Get all possible LAN broadcast addresses auto broadcastEndpoints = GetBroadcastAddresses(); // Spin off a fetch for each broadcast address std::vector>> futures; for (const auto& broadcastEndpoint : broadcastEndpoints) { auto f = FetchLocalServerListAsync(*broadcastEndpoint); futures.push_back(std::move(f)); } // Wait and merge all results std::vector mergedEntries; for (auto& f : futures) { try { auto entries = f.get(); mergedEntries.insert(mergedEntries.begin(), entries.begin(), entries.end()); } catch (...) { // Ignore any exceptions from a particular broadcast fetch } } return mergedEntries; }); } std::future> ServerList::FetchOnlineServerListAsync() const { #ifdef DISABLE_HTTP return {}; #else auto p = std::make_shared>>(); auto f = p->get_future(); std::string masterServerUrl = kMasterServerURL; if (!Config::Get().network.MasterServerUrl.empty()) { masterServerUrl = Config::Get().network.MasterServerUrl; } Http::Request request; request.url = std::move(masterServerUrl); request.method = Http::Method::GET; request.header["Accept"] = "application/json"; Http::DoAsync(request, [p](Http::Response& response) -> void { json_t root; try { if (response.status != Http::Status::Ok) { throw MasterServerException(STR_SERVER_LIST_NO_CONNECTION); } root = Json::FromString(response.body); if (root.is_object()) { auto jsonStatus = root["status"]; if (!jsonStatus.is_number_integer()) { throw MasterServerException(STR_SERVER_LIST_INVALID_RESPONSE_JSON_NUMBER); } auto status = Json::GetNumber(jsonStatus); if (status != 200) { throw MasterServerException(STR_SERVER_LIST_MASTER_SERVER_FAILED); } auto jServers = root["servers"]; if (!jServers.is_array()) { throw MasterServerException(STR_SERVER_LIST_INVALID_RESPONSE_JSON_ARRAY); } std::vector entries; for (auto& jServer : jServers) { if (jServer.is_object()) { auto entry = ServerListEntry::FromJson(jServer); if (entry.has_value()) { entries.push_back(std::move(*entry)); } } } p->set_value(entries); } } catch (...) { p->set_exception(std::current_exception()); } }); return f; #endif } uint32_t ServerList::GetTotalPlayerCount() const { return std::accumulate(_serverEntries.begin(), _serverEntries.end(), 0, [](uint32_t acc, const ServerListEntry& entry) { return acc + entry.Players; }); } const char* MasterServerException::what() const noexcept { static std::string localisedStatusText = LanguageGetString(StatusText); return localisedStatusText.c_str(); } } // namespace OpenRCT2::Network #endif