/***************************************************************************** * Copyright (c) 2014-2020 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 "../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 "../platform/Platform.h" # include "Socket.h" # include "network.h" # include # include # include using namespace OpenRCT2; 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_get_version(); bool serverBCompatible = b.Version == network_get_version(); 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_get_version(); } 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::Equals(a.Address, b.Address, true); } 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::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::NETWORK_SERVERS); if (File::Exists(path)) { auto fs = FileStream(path, FILE_MODE_OPEN); auto numEntries = fs.ReadValue(); for (size_t i = 0; i < numEntries; i++) { ServerListEntry serverInfo; serverInfo.Address = fs.ReadStdString(); serverInfo.Name = fs.ReadStdString(); serverInfo.RequiresPassword = false; serverInfo.Description = fs.ReadStdString(); serverInfo.Version = ""; 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, FILE_MODE_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 RECV_DELAY_MS = 10; constexpr auto RECV_WAIT_MS = 2000; std::string_view msg = NETWORK_LAN_BROADCAST_MSG; auto udpSocket = CreateUdpSocket(); log_verbose("Broadcasting %zu bytes to the LAN (%s)", msg.size(), broadcastAddress.c_str()); auto len = udpSocket->SendData(broadcastAddress, NETWORK_LAN_BROADCAST_PORT, 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 < (RECV_WAIT_MS / RECV_DELAY_MS); 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 == NetworkReadPacket::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(RECV_DELAY_MS); } 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 = OPENRCT2_MASTER_SERVER_URL; if (!gConfigNetwork.master_server_url.empty()) { masterServerUrl = gConfigNetwork.master_server_url; } Http::Request request; request.url = 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; }); } #endif