diff --git a/OpenRCT2.xcodeproj/project.pbxproj b/OpenRCT2.xcodeproj/project.pbxproj index a7b3e30d16..19504974b2 100644 --- a/OpenRCT2.xcodeproj/project.pbxproj +++ b/OpenRCT2.xcodeproj/project.pbxproj @@ -512,7 +512,7 @@ F76C86551EC4E88300FA49E2 /* NetworkServerAdvertiser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C84061EC4E7CC00FA49E2 /* NetworkServerAdvertiser.cpp */; }; F76C86581EC4E88300FA49E2 /* NetworkUser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C84091EC4E7CC00FA49E2 /* NetworkUser.cpp */; }; F76C865A1EC4E88300FA49E2 /* ServerList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C840B1EC4E7CC00FA49E2 /* ServerList.cpp */; }; - F76C865C1EC4E88300FA49E2 /* TcpSocket.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C840D1EC4E7CC00FA49E2 /* TcpSocket.cpp */; }; + F76C865C1EC4E88300FA49E2 /* Socket.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C840D1EC4E7CC00FA49E2 /* Socket.cpp */; }; F76C86601EC4E88300FA49E2 /* BannerObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C84121EC4E7CC00FA49E2 /* BannerObject.cpp */; }; F76C86621EC4E88300FA49E2 /* EntranceObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C84141EC4E7CC00FA49E2 /* EntranceObject.cpp */; }; F76C86641EC4E88300FA49E2 /* FootpathItemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F76C84161EC4E7CC00FA49E2 /* FootpathItemObject.cpp */; }; @@ -1660,8 +1660,8 @@ F76C840A1EC4E7CC00FA49E2 /* NetworkUser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkUser.h; sourceTree = ""; }; F76C840B1EC4E7CC00FA49E2 /* ServerList.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ServerList.cpp; sourceTree = ""; }; F76C840C1EC4E7CC00FA49E2 /* ServerList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ServerList.h; sourceTree = ""; }; - F76C840D1EC4E7CC00FA49E2 /* TcpSocket.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TcpSocket.cpp; sourceTree = ""; }; - F76C840E1EC4E7CC00FA49E2 /* TcpSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TcpSocket.h; sourceTree = ""; }; + F76C840D1EC4E7CC00FA49E2 /* Socket.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Socket.cpp; sourceTree = ""; }; + F76C840E1EC4E7CC00FA49E2 /* Socket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Socket.h; sourceTree = ""; }; F76C840F1EC4E7CC00FA49E2 /* Twitch.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Twitch.cpp; sourceTree = ""; }; F76C84101EC4E7CC00FA49E2 /* twitch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = twitch.h; sourceTree = ""; }; F76C84121EC4E7CC00FA49E2 /* BannerObject.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = BannerObject.cpp; sourceTree = ""; }; @@ -2820,8 +2820,8 @@ F76C840A1EC4E7CC00FA49E2 /* NetworkUser.h */, F76C840B1EC4E7CC00FA49E2 /* ServerList.cpp */, F76C840C1EC4E7CC00FA49E2 /* ServerList.h */, - F76C840D1EC4E7CC00FA49E2 /* TcpSocket.cpp */, - F76C840E1EC4E7CC00FA49E2 /* TcpSocket.h */, + F76C840D1EC4E7CC00FA49E2 /* Socket.cpp */, + F76C840E1EC4E7CC00FA49E2 /* Socket.h */, F76C840F1EC4E7CC00FA49E2 /* Twitch.cpp */, F76C84101EC4E7CC00FA49E2 /* twitch.h */, ); @@ -4123,7 +4123,7 @@ 93F76EFF20BFF77B00D4512C /* Paint.Wall.cpp in Sources */, F76C86581EC4E88300FA49E2 /* NetworkUser.cpp in Sources */, F76C865A1EC4E88300FA49E2 /* ServerList.cpp in Sources */, - F76C865C1EC4E88300FA49E2 /* TcpSocket.cpp in Sources */, + F76C865C1EC4E88300FA49E2 /* Socket.cpp in Sources */, C688784B202899B90084B384 /* Intro.cpp in Sources */, C68878FD20289B9B0084B384 /* MiniRollerCoaster.cpp in Sources */, 2A1F4FE0221FF4B0003CA045 /* Twitch.cpp in Sources */, diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 73674304cb..69033cec49 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -1,5 +1,6 @@ 0.2.2+ (in development) ------------------------------------------------------------------------ +- Feature: [#2339] Find local servers automatically when fetching servers. - Feature: [#7296] Allow assigning a keyboard shortcut for the scenery picker. - Feature: [#8029] Add the Hungarian Forint (HUF) to the list of available currencies. - Feature: [#8481] Multi-threaded rendering. diff --git a/src/openrct2-ui/WindowManager.cpp b/src/openrct2-ui/WindowManager.cpp index 8c8e497466..d897f0f641 100644 --- a/src/openrct2-ui/WindowManager.cpp +++ b/src/openrct2-ui/WindowManager.cpp @@ -100,10 +100,12 @@ public: return window_save_prompt_open(); case WC_SCENERY: return window_scenery_open(); +#ifndef DISABLE_NETWORK case WC_SERVER_LIST: return window_server_list_open(); case WC_SERVER_START: return window_server_start_open(); +#endif case WC_KEYBOARD_SHORTCUT_LIST: return window_shortcut_keys_open(); case WC_STAFF_LIST: diff --git a/src/openrct2-ui/windows/ServerList.cpp b/src/openrct2-ui/windows/ServerList.cpp index 16f665d7e8..7bc4c7a175 100644 --- a/src/openrct2-ui/windows/ServerList.cpp +++ b/src/openrct2-ui/windows/ServerList.cpp @@ -7,52 +7,39 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#ifndef DISABLE_NETWORK -#ifndef DISABLE_HTTP -using namespace OpenRCT2::Network; -#endif +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include -#define WWIDTH_MIN 500 -#define WHEIGHT_MIN 300 -#define WWIDTH_MAX 1200 -#define WHEIGHT_MAX 800 -#define ITEM_HEIGHT (3 + 9 + 3) - -class MasterServerException : public std::exception -{ -public: - rct_string_id StatusText; - - MasterServerException(rct_string_id statusText) - : StatusText(statusText) - { - } -}; +# define WWIDTH_MIN 500 +# define WHEIGHT_MIN 300 +# define WWIDTH_MAX 1200 +# define WHEIGHT_MAX 800 +# define ITEM_HEIGHT (3 + 9 + 3) static char _playerName[32 + 1]; -static std::vector _serverEntries; -static std::mutex _mutex; +static ServerList _serverList; +static std::future, rct_string_id>> _fetchFuture; static uint32_t _numPlayersOnline = 0; -static rct_string_id status_text = STR_SERVER_LIST_CONNECTING; +static rct_string_id _statusText = STR_SERVER_LIST_CONNECTING; // clang-format off enum { @@ -138,18 +125,9 @@ static int32_t _hoverButtonIndex = -1; static std::string _version; static void server_list_get_item_button(int32_t buttonIndex, int32_t x, int32_t y, int32_t width, int32_t* outX, int32_t* outY); -static void server_list_load_server_entries(); -static void server_list_save_server_entries(); -static void dispose_server_entry_list(); -static server_entry& add_server_entry(const std::string& address); -static void sort_servers(); static void join_server(std::string address); -#ifndef DISABLE_HTTP -static void fetch_servers(); -static void fetch_servers_callback(Http::Response& response); -static void RefreshServersFromJson(const json_t* jsonServers); -#endif -static bool is_version_valid(const std::string& version); +static void server_list_fetch_servers_begin(); +static void server_list_fetch_servers_check(rct_window* w); rct_window* window_server_list_open() { @@ -183,20 +161,18 @@ rct_window* window_server_list_open() safe_strcpy(_playerName, gConfigNetwork.player_name.c_str(), sizeof(_playerName)); - server_list_load_server_entries(); - window->no_list_items = (uint16_t)_serverEntries.size(); + _serverList.ReadAndAddFavourites(); + window->no_list_items = (uint16_t)_serverList.GetCount(); -#ifndef DISABLE_HTTP - fetch_servers(); -#endif + server_list_fetch_servers_begin(); return window; } static void window_server_list_close(rct_window* w) { - std::lock_guard guard(_mutex); - dispose_server_entry_list(); + _serverList = {}; + _fetchFuture = {}; } static void window_server_list_mouseup(rct_window* w, rct_widgetindex widgetIndex) @@ -212,10 +188,10 @@ static void window_server_list_mouseup(rct_window* w, rct_widgetindex widgetInde case WIDX_LIST: { int32_t serverIndex = w->selected_list_item; - if (serverIndex >= 0 && serverIndex < (int32_t)_serverEntries.size()) + if (serverIndex >= 0 && serverIndex < (int32_t)_serverList.GetCount()) { - const auto& server = _serverEntries[serverIndex]; - if (is_version_valid(server.version)) + const auto& server = _serverList.GetServer(serverIndex); + if (server.IsVersionValid()) { join_server(server.address); } @@ -228,9 +204,7 @@ static void window_server_list_mouseup(rct_window* w, rct_widgetindex widgetInde break; } case WIDX_FETCH_SERVERS: -#ifndef DISABLE_HTTP - fetch_servers(); -#endif + server_list_fetch_servers_begin(); break; case WIDX_ADD_SERVER: window_text_input_open(w, widgetIndex, STR_ADD_SERVER, STR_ENTER_HOSTNAME_OR_IP_ADDRESS, STR_NONE, 0, 128); @@ -249,27 +223,26 @@ static void window_server_list_resize(rct_window* w) static void window_server_list_dropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex) { auto serverIndex = w->selected_list_item; - if (serverIndex >= 0 && serverIndex < (int32_t)_serverEntries.size()) + if (serverIndex >= 0 && serverIndex < (int32_t)_serverList.GetCount()) { - auto& server = _serverEntries[serverIndex]; + auto& server = _serverList.GetServer(serverIndex); switch (dropdownIndex) { case DDIDX_JOIN: - if (is_version_valid(server.version)) + if (server.IsVersionValid()) { join_server(server.address); } else { - set_format_arg(0, void*, _serverEntries[serverIndex].version.c_str()); + set_format_arg(0, void*, server.version.c_str()); context_show_error(STR_UNABLE_TO_CONNECT_TO_SERVER, STR_MULTIPLAYER_INCORRECT_SOFTWARE_VERSION); } break; case DDIDX_FAVOURITE: { - std::lock_guard guard(_mutex); server.favourite = !server.favourite; - server_list_save_server_entries(); + _serverList.WriteFavourites(); } break; } @@ -283,6 +256,7 @@ static void window_server_list_update(rct_window* w) window_update_textbox_caret(); widget_invalidate(w, WIDX_PLAYER_NAME_INPUT); } + server_list_fetch_servers_check(w); } static void window_server_list_scroll_getsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height) @@ -294,25 +268,25 @@ static void window_server_list_scroll_getsize(rct_window* w, int32_t scrollIndex static void window_server_list_scroll_mousedown(rct_window* w, int32_t scrollIndex, int32_t x, int32_t y) { int32_t serverIndex = w->selected_list_item; - if (serverIndex < 0) - return; - if (serverIndex >= (int32_t)_serverEntries.size()) - return; - - rct_widget* listWidget = &w->widgets[WIDX_LIST]; - int32_t ddx = w->x + listWidget->left + x + 2 - w->scrolls[0].h_left; - int32_t ddy = w->y + listWidget->top + y + 2 - w->scrolls[0].v_top; - - gDropdownItemsFormat[0] = STR_JOIN_GAME; - if (_serverEntries[serverIndex].favourite) + if (serverIndex >= 0 && serverIndex < (int32_t)_serverList.GetCount()) { - gDropdownItemsFormat[1] = STR_REMOVE_FROM_FAVOURITES; + const auto& server = _serverList.GetServer(serverIndex); + + auto listWidget = &w->widgets[WIDX_LIST]; + int32_t ddx = w->x + listWidget->left + x + 2 - w->scrolls[0].h_left; + int32_t ddy = w->y + listWidget->top + y + 2 - w->scrolls[0].v_top; + + gDropdownItemsFormat[0] = STR_JOIN_GAME; + if (server.favourite) + { + gDropdownItemsFormat[1] = STR_REMOVE_FROM_FAVOURITES; + } + else + { + gDropdownItemsFormat[1] = STR_ADD_TO_FAVOURITES; + } + window_dropdown_show_text(ddx, ddy, 0, COLOUR_GREY, 0, 2); } - else - { - gDropdownItemsFormat[1] = STR_ADD_TO_FAVOURITES; - } - window_dropdown_show_text(ddx, ddy, 0, COLOUR_GREY, 0, 2); } static void window_server_list_scroll_mouseover(rct_window* w, int32_t scrollIndex, int32_t x, int32_t y) @@ -387,14 +361,15 @@ static void window_server_list_textinput(rct_window* w, rct_widgetindex widgetIn case WIDX_ADD_SERVER: { - std::lock_guard guard(_mutex); - auto& entry = add_server_entry(text); + ServerListEntry entry; + entry.address = text; + entry.name = text; entry.favourite = true; - sort_servers(); - server_list_save_server_entries(); - } + _serverList.Add(entry); + _serverList.WriteFavourites(); window_invalidate(w); break; + } } } @@ -424,7 +399,7 @@ static void window_server_list_invalidate(rct_window* w) window_server_list_widgets[WIDX_START_SERVER].top = buttonTop; window_server_list_widgets[WIDX_START_SERVER].bottom = buttonBottom; - w->no_list_items = (uint16_t)_serverEntries.size(); + w->no_list_items = (uint16_t)_serverList.GetCount(); } static void window_server_list_paint(rct_window* w, rct_drawpixelinfo* dpi) @@ -439,13 +414,11 @@ static void window_server_list_paint(rct_window* w, rct_drawpixelinfo* dpi) gfx_draw_string_left( dpi, STR_NETWORK_VERSION, (void*)&versionCStr, COLOUR_WHITE, w->x + 324, w->y + w->widgets[WIDX_START_SERVER].top + 1); - gfx_draw_string_left(dpi, status_text, (void*)&_numPlayersOnline, COLOUR_WHITE, w->x + 8, w->y + w->height - 15); + gfx_draw_string_left(dpi, _statusText, (void*)&_numPlayersOnline, COLOUR_WHITE, w->x + 8, w->y + w->height - 15); } static void window_server_list_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32_t scrollIndex) { - std::lock_guard guard(_mutex); - uint8_t paletteIndex = ColourMapA[w->colours[1]].mid_light; gfx_clear(dpi, paletteIndex); @@ -459,31 +432,35 @@ static void window_server_list_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi continue; // if (y + ITEM_HEIGHT < dpi->y) continue; - server_entry* serverDetails = &_serverEntries[i]; + const auto& serverDetails = _serverList.GetServer(i); bool highlighted = i == w->selected_list_item; // Draw hover highlight if (highlighted) { gfx_filter_rect(dpi, 0, y, width, y + ITEM_HEIGHT, PALETTE_DARKEN_1); - _version = serverDetails->version; + _version = serverDetails.version; w->widgets[WIDX_LIST].tooltip = STR_NETWORK_VERSION_TIP; } int32_t colour = w->colours[1]; - if (serverDetails->favourite) + if (serverDetails.favourite) { colour = COLOUR_YELLOW; } + else if (serverDetails.local) + { + colour = COLOUR_MOSS_GREEN; + } // Draw server information - if (highlighted && !serverDetails->description.empty()) + if (highlighted && !serverDetails.description.empty()) { - gfx_draw_string(dpi, serverDetails->description.c_str(), colour, 3, y + 3); + gfx_draw_string(dpi, serverDetails.description.c_str(), colour, 3, y + 3); } else { - gfx_draw_string(dpi, serverDetails->name.c_str(), colour, 3, y + 3); + gfx_draw_string(dpi, serverDetails.name.c_str(), colour, 3, y + 3); } int32_t right = width - 3 - 14; @@ -491,7 +468,7 @@ static void window_server_list_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi // Draw compatibility icon right -= 10; int32_t compatibilitySpriteId; - if (serverDetails->version.empty()) + if (serverDetails.version.empty()) { // Server not online... compatibilitySpriteId = SPR_G2_RCT1_CLOSE_BUTTON_0; @@ -499,7 +476,7 @@ static void window_server_list_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi else { // Server online... check version - bool correctVersion = serverDetails->version == network_get_version(); + bool correctVersion = serverDetails.version == network_get_version(); compatibilitySpriteId = correctVersion ? SPR_G2_RCT1_OPEN_BUTTON_2 : SPR_G2_RCT1_CLOSE_BUTTON_2; } gfx_draw_sprite(dpi, compatibilitySpriteId, right, y + 1, 0); @@ -507,7 +484,7 @@ static void window_server_list_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi // Draw lock icon right -= 8; - if (serverDetails->requiresPassword) + if (serverDetails.requiresPassword) { gfx_draw_sprite(dpi, SPR_G2_LOCKED, right, y + 4, 0); } @@ -516,9 +493,9 @@ static void window_server_list_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi // Draw number of players char players[32]; players[0] = 0; - if (serverDetails->maxplayers > 0) + if (serverDetails.maxplayers > 0) { - snprintf(players, 32, "%d/%d", serverDetails->players, serverDetails->maxplayers); + snprintf(players, 32, "%d/%d", serverDetails.players, serverDetails.maxplayers); } int32_t numPlayersStringWidth = gfx_get_string_width(players); gfx_draw_string(dpi, players, w->colours[1], right - numPlayersStringWidth, y + 3); @@ -533,81 +510,6 @@ static void server_list_get_item_button(int32_t buttonIndex, int32_t x, int32_t *outY = y + 2; } -static void server_list_load_server_entries() -{ - auto entries = server_list_read(); - { - std::lock_guard guard(_mutex); - dispose_server_entry_list(); - _serverEntries = entries; - sort_servers(); - } -} - -static void server_list_save_server_entries() -{ - // Save just favourite servers - std::vector favouriteServers; - std::copy_if( - _serverEntries.begin(), _serverEntries.end(), std::back_inserter(favouriteServers), - [](const server_entry& entry) { return entry.favourite; }); - server_list_write(favouriteServers); -} - -static void dispose_server_entry_list() -{ - _serverEntries.clear(); - _serverEntries.shrink_to_fit(); -} - -static server_entry& add_server_entry(const std::string& address) -{ - auto entry = std::find_if(std::begin(_serverEntries), std::end(_serverEntries), [address](const server_entry& e) { - return e.address == address; - }); - if (entry != _serverEntries.end()) - { - return *entry; - } - - server_entry newserver; - newserver.address = address; - newserver.name = address; - _serverEntries.push_back(newserver); - return _serverEntries.back(); -} - -static bool server_compare(const server_entry& a, const server_entry& b) -{ - // Order by favourite - if (a.favourite != b.favourite) - { - return a.favourite; - } - - // Then by version - bool serverACompatible = a.version == network_get_version(); - bool serverBCompatible = b.version == network_get_version(); - if (serverACompatible != serverBCompatible) - { - return serverACompatible; - } - - // Then by password protection - if (a.requiresPassword != b.requiresPassword) - { - return !a.requiresPassword; - } - - // Then by name - return String::Compare(a.name, b.name, true) < 0; -} - -static void sort_servers() -{ - std::sort(_serverEntries.begin(), _serverEntries.end(), server_compare); -} - static void join_server(std::string address) { int32_t port = gConfigNetwork.default_port; @@ -635,140 +537,83 @@ static void join_server(std::string address) } } -#ifndef DISABLE_HTTP -static void fetch_servers() +static void server_list_fetch_servers_begin() { - std::string masterServerUrl = OPENRCT2_MASTER_SERVER_URL; - if (!gConfigNetwork.master_server_url.empty()) + if (_fetchFuture.valid()) { - masterServerUrl = gConfigNetwork.master_server_url; + // A fetch is already in progress + return; } - { - std::lock_guard guard(_mutex); - _serverEntries.erase( - std::remove_if( - _serverEntries.begin(), _serverEntries.end(), [](const server_entry& server) { return !server.favourite; }), - _serverEntries.end()); - sort_servers(); - } + _serverList.Clear(); + _serverList.ReadAndAddFavourites(); + _statusText = STR_SERVER_LIST_CONNECTING; - Http::Request request; - request.url = masterServerUrl; - request.method = Http::Method::GET; - request.header["Accept"] = "application/json"; - status_text = STR_SERVER_LIST_CONNECTING; - Http::DoAsync(request, fetch_servers_callback); -} + _fetchFuture = std::async(std::launch::async, [] { + // Spin off background fetches + auto lanF = _serverList.FetchLocalServerListAsync(); + auto wanF = _serverList.FetchOnlineServerListAsync(); -static uint32_t get_total_player_count() -{ - return std::accumulate(_serverEntries.begin(), _serverEntries.end(), 0, [](uint32_t acc, const server_entry& entry) { - return acc + entry.players; + // Merge or deal with errors + std::vector allEntries; + try + { + auto entries = lanF.get(); + allEntries.insert(allEntries.end(), entries.begin(), entries.end()); + } + catch (...) + { + } + + auto status = STR_NONE; + try + { + auto entries = wanF.get(); + allEntries.insert(allEntries.end(), entries.begin(), entries.end()); + } + catch (const MasterServerException& e) + { + status = e.StatusText; + } + catch (...) + { + status = STR_SERVER_LIST_NO_CONNECTION; + } + return std::make_tuple(allEntries, status); }); } -static void fetch_servers_callback(Http::Response& response) +static void server_list_fetch_servers_check(rct_window* w) { - json_t* root = nullptr; - try + if (_fetchFuture.valid()) { - if (response.status != Http::Status::OK) + auto status = _fetchFuture.wait_for(std::chrono::seconds::zero()); + if (status == std::future_status::ready) { - throw MasterServerException(STR_SERVER_LIST_NO_CONNECTION); - } - - root = Json::FromString(response.body); - auto jsonStatus = json_object_get(root, "status"); - if (!json_is_number(jsonStatus)) - { - throw MasterServerException(STR_SERVER_LIST_INVALID_RESPONSE_JSON_NUMBER); - } - - auto status = (int32_t)json_integer_value(jsonStatus); - if (status != 200) - { - throw MasterServerException(STR_SERVER_LIST_MASTER_SERVER_FAILED); - } - - auto jsonServers = json_object_get(root, "servers"); - if (!json_is_array(jsonServers)) - { - throw MasterServerException(STR_SERVER_LIST_INVALID_RESPONSE_JSON_ARRAY); - } - - RefreshServersFromJson(jsonServers); - } - catch (const MasterServerException& e) - { - status_text = e.StatusText; - window_invalidate_by_class(WC_SERVER_LIST); - } - catch (const std::exception& e) - { - status_text = STR_SERVER_LIST_NO_CONNECTION; - window_invalidate_by_class(WC_SERVER_LIST); - log_warning("Unable to connect to master server: %s", e.what()); - } - - if (root != nullptr) - { - json_decref(root); - root = nullptr; - } -} - -static void RefreshServersFromJson(const json_t* jsonServers) -{ - auto count = (int32_t)json_array_size(jsonServers); - for (int32_t i = 0; i < count; i++) - { - auto server = json_array_get(jsonServers, i); - if (!json_is_object(server)) - { - continue; - } - - auto port = json_object_get(server, "port"); - auto name = json_object_get(server, "name"); - auto description = json_object_get(server, "description"); - auto requiresPassword = json_object_get(server, "requiresPassword"); - auto version = json_object_get(server, "version"); - auto players = json_object_get(server, "players"); - auto maxPlayers = json_object_get(server, "maxPlayers"); - auto ip = json_object_get(server, "ip"); - auto ip4 = json_object_get(ip, "v4"); - auto addressIp = json_array_get(ip4, 0); - - if (name == nullptr || version == nullptr) - { - log_verbose("Cowardly refusing to add server without name or version specified."); - continue; - } - - auto address = String::StdFormat("%s:%d", json_string_value(addressIp), (int32_t)json_integer_value(port)); - { - std::lock_guard guard(_mutex); - auto& newserver = add_server_entry(address); - newserver.name = json_string_value(name); - newserver.requiresPassword = json_is_true(requiresPassword); - newserver.description = (description == nullptr ? "" : json_string_value(description)); - newserver.version = json_string_value(version); - newserver.players = (uint8_t)json_integer_value(players); - newserver.maxplayers = (uint8_t)json_integer_value(maxPlayers); + try + { + auto [entries, statusText] = _fetchFuture.get(); + _serverList.AddRange(entries); + _numPlayersOnline = _serverList.GetTotalPlayerCount(); + _statusText = STR_X_PLAYERS_ONLINE; + if (statusText != STR_NONE) + { + _statusText = statusText; + } + } + catch (const MasterServerException& e) + { + _statusText = e.StatusText; + } + catch (const std::exception& e) + { + _statusText = STR_SERVER_LIST_NO_CONNECTION; + log_warning("Unable to connect to master server: %s", e.what()); + } + _fetchFuture = {}; + window_invalidate(w); } } - - sort_servers(); - _numPlayersOnline = get_total_player_count(); - - status_text = STR_X_PLAYERS_ONLINE; - window_invalidate_by_class(WC_SERVER_LIST); } #endif - -static bool is_version_valid(const std::string& version) -{ - return version.empty() || version == network_get_version(); -} diff --git a/src/openrct2-ui/windows/ServerStart.cpp b/src/openrct2-ui/windows/ServerStart.cpp index 5fc1d49cd0..631ea62f60 100644 --- a/src/openrct2-ui/windows/ServerStart.cpp +++ b/src/openrct2-ui/windows/ServerStart.cpp @@ -7,18 +7,20 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#include "../interface/Theme.h" +#ifndef DISABLE_NETWORK -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +# include "../interface/Theme.h" + +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include static char _port[7]; static char _name[65]; @@ -345,3 +347,5 @@ static void window_server_start_paint(rct_window* w, rct_drawpixelinfo* dpi) gfx_draw_string_left(dpi, STR_PASSWORD, nullptr, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_PASSWORD_INPUT].top); gfx_draw_string_left(dpi, STR_MAX_PLAYERS, nullptr, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_MAXPLAYERS].top); } + +#endif diff --git a/src/openrct2-ui/windows/Window.h b/src/openrct2-ui/windows/Window.h index 8c8d770e81..76c45cb3f4 100644 --- a/src/openrct2-ui/windows/Window.h +++ b/src/openrct2-ui/windows/Window.h @@ -43,8 +43,10 @@ rct_window* window_news_open(); rct_window* window_news_options_open(); rct_window* window_options_open(); rct_window* window_save_prompt_open(); +#ifndef DISABLE_NETWORK rct_window* window_server_list_open(); rct_window* window_server_start_open(); +#endif rct_window* window_shortcut_change_open(int32_t selected_key); rct_window* window_shortcut_keys_open(); rct_window* window_staff_list_open(); diff --git a/src/openrct2/core/Json.cpp b/src/openrct2/core/Json.cpp index 94b129133a..07c868630b 100644 --- a/src/openrct2/core/Json.cpp +++ b/src/openrct2/core/Json.cpp @@ -53,11 +53,11 @@ namespace Json fs.Write(jsonOutput, jsonOutputSize); } - json_t* FromString(const std::string& raw) + json_t* FromString(std::string_view raw) { json_t* root; json_error_t error; - root = json_loads(raw.c_str(), 0, &error); + root = json_loadb(raw.data(), raw.size(), 0, &error); if (root == nullptr) { throw JsonException(&error); diff --git a/src/openrct2/core/Json.hpp b/src/openrct2/core/Json.hpp index c3d97fc408..53687b9890 100644 --- a/src/openrct2/core/Json.hpp +++ b/src/openrct2/core/Json.hpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace Json { @@ -23,7 +24,7 @@ namespace Json json_t* ReadFromFile(const utf8* path, size_t maxSize = MAX_JSON_SIZE); void WriteToFile(const utf8* path, const json_t* json, size_t flags = 0); - json_t* FromString(const std::string& raw); + json_t* FromString(std::string_view raw); } // namespace Json class JsonException final : public std::runtime_error diff --git a/src/openrct2/network/Network.cpp b/src/openrct2/network/Network.cpp index 4ca115b4f7..f95d040ff4 100644 --- a/src/openrct2/network/Network.cpp +++ b/src/openrct2/network/Network.cpp @@ -75,7 +75,7 @@ static constexpr uint32_t CHUNK_SIZE = 1024 * 63; # include "NetworkPlayer.h" # include "NetworkServerAdvertiser.h" # include "NetworkUser.h" -# include "TcpSocket.h" +# include "Socket.h" # include # include @@ -201,6 +201,7 @@ public: void Server_Send_OBJECTS(NetworkConnection& connection, const std::vector& objects) const; NetworkStats_t GetStats() const; + json_t* GetServerInfoAsJson() const; std::vector> player_list; std::vector> group_list; @@ -680,11 +681,7 @@ bool Network::BeginServer(uint16_t port, const std::string& address) status = NETWORK_STATUS_CONNECTED; listening_port = port; _serverState.gamestateSnapshotsEnabled = gConfigNetwork.desync_debugging; - - if (gConfigNetwork.advertise) - { - _advertiser = CreateServerAdvertiser(listening_port); - } + _advertiser = CreateServerAdvertiser(listening_port); if (gConfigNetwork.pause_server_if_no_clients) { @@ -1814,11 +1811,8 @@ void Network::Server_Send_SETDISCONNECTMSG(NetworkConnection& connection, const connection.QueuePacket(std::move(packet)); } -void Network::Server_Send_GAMEINFO(NetworkConnection& connection) +json_t* Network::GetServerInfoAsJson() const { - std::unique_ptr packet(NetworkPacket::Allocate()); - *packet << (uint32_t)NETWORK_COMMAND_GAMEINFO; -# ifndef DISABLE_HTTP json_t* obj = json_object(); json_object_set_new(obj, "name", json_string(gConfigNetwork.server_name.c_str())); json_object_set_new(obj, "requiresPassword", json_boolean(_password.size() > 0)); @@ -1828,6 +1822,15 @@ void Network::Server_Send_GAMEINFO(NetworkConnection& connection) json_object_set_new(obj, "description", json_string(gConfigNetwork.server_description.c_str())); json_object_set_new(obj, "greeting", json_string(gConfigNetwork.server_greeting.c_str())); json_object_set_new(obj, "dedicated", json_boolean(gOpenRCT2Headless)); + return obj; +} + +void Network::Server_Send_GAMEINFO(NetworkConnection& connection) +{ + std::unique_ptr packet(NetworkPacket::Allocate()); + *packet << (uint32_t)NETWORK_COMMAND_GAMEINFO; +# ifndef DISABLE_HTTP + json_t* obj = GetServerInfoAsJson(); // Provider details json_t* jsonProvider = json_object(); @@ -4179,6 +4182,10 @@ bool network_gamestate_snapshots_enabled() return network_get_server_state().gamestateSnapshotsEnabled; } +json_t* network_get_server_info_as_json() +{ + return gNetwork.GetServerInfoAsJson(); +} #else int32_t network_get_mode() { @@ -4436,4 +4443,8 @@ NetworkServerState_t network_get_server_state() { return NetworkServerState_t{}; } +json_t* network_get_server_info_as_json() +{ + return nullptr; +} #endif /* DISABLE_NETWORK */ diff --git a/src/openrct2/network/NetworkConnection.cpp b/src/openrct2/network/NetworkConnection.cpp index ff090665f9..2f8c14725d 100644 --- a/src/openrct2/network/NetworkConnection.cpp +++ b/src/openrct2/network/NetworkConnection.cpp @@ -14,7 +14,7 @@ # include "../core/String.hpp" # include "../localisation/Localisation.h" # include "../platform/platform.h" -# include "TcpSocket.h" +# include "Socket.h" # include "network.h" constexpr size_t NETWORK_DISCONNECT_REASON_BUFFER_SIZE = 256; diff --git a/src/openrct2/network/NetworkConnection.h b/src/openrct2/network/NetworkConnection.h index def4a85304..2872ac6b6f 100644 --- a/src/openrct2/network/NetworkConnection.h +++ b/src/openrct2/network/NetworkConnection.h @@ -14,7 +14,7 @@ # include "NetworkKey.h" # include "NetworkPacket.h" # include "NetworkTypes.h" -# include "TcpSocket.h" +# include "Socket.h" # include # include diff --git a/src/openrct2/network/NetworkServerAdvertiser.cpp b/src/openrct2/network/NetworkServerAdvertiser.cpp index 1b8623bfb4..bf9c921c51 100644 --- a/src/openrct2/network/NetworkServerAdvertiser.cpp +++ b/src/openrct2/network/NetworkServerAdvertiser.cpp @@ -18,21 +18,20 @@ # include "../localisation/Date.h" # include "../management/Finance.h" # include "../peep/Peep.h" +# include "../platform/Platform2.h" # include "../platform/platform.h" # include "../util/Util.h" # include "../world/Map.h" # include "../world/Park.h" # include "Http.h" +# include "Socket.h" # include "network.h" +# include # include # include # include -# ifndef DISABLE_HTTP - -using namespace OpenRCT2::Network; - enum MASTER_SERVER_STATUS { MASTER_SERVER_STATUS_OK = 200, @@ -49,7 +48,12 @@ class NetworkServerAdvertiser final : public INetworkServerAdvertiser private: uint16_t _port; + std::unique_ptr _lanListener; + uint32_t _lastListenTime{}; + ADVERTISE_STATUS _status = ADVERTISE_STATUS::UNREGISTERED; + +# ifndef DISABLE_HTTP uint32_t _lastAdvertiseTime = 0; uint32_t _lastHeartbeatTime = 0; @@ -61,12 +65,16 @@ private: // See https://github.com/OpenRCT2/OpenRCT2/issues/6277 and 4953 bool _forceIPv4 = false; +# endif public: explicit NetworkServerAdvertiser(uint16_t port) { _port = port; + _lanListener = CreateUdpSocket(); +# ifndef DISABLE_HTTP _key = GenerateAdvertiseKey(); +# endif } ADVERTISE_STATUS GetStatus() const override @@ -75,6 +83,61 @@ public: } void Update() override + { + UpdateLAN(); +# ifndef DISABLE_HTTP + if (gConfigNetwork.advertise) + { + UpdateWAN(); + } +# endif + } + +private: + void UpdateLAN() + { + auto ticks = Platform::GetTicks(); + if (ticks > _lastListenTime + 500) + { + if (_lanListener->GetStatus() != SOCKET_STATUS_LISTENING) + { + _lanListener->Listen(NETWORK_LAN_BROADCAST_PORT); + } + else + { + char buffer[256]{}; + size_t recievedBytes{}; + std::unique_ptr endpoint; + auto p = _lanListener->ReceiveData(buffer, sizeof(buffer) - 1, &recievedBytes, &endpoint); + if (p == NETWORK_READPACKET_SUCCESS) + { + std::string sender = endpoint->GetHostname(); + log_verbose("Received %zu bytes from %s on LAN broadcast port", recievedBytes, sender.c_str()); + if (String::Equals(buffer, NETWORK_LAN_BROADCAST_MSG)) + { + auto body = GetBroadcastJson(); + auto bodyDump = json_dumps(body, JSON_COMPACT); + size_t sendLen = strlen(bodyDump) + 1; + log_verbose("Sending %zu bytes back to %s", sendLen, sender.c_str()); + _lanListener->SendData(*endpoint, bodyDump, sendLen); + free(bodyDump); + json_decref(body); + } + } + } + _lastListenTime = ticks; + } + } + + json_t* GetBroadcastJson() + { + auto root = network_get_server_info_as_json(); + json_object_set(root, "port", json_integer(_port)); + return root; + } + +# ifndef DISABLE_HTTP + void UpdateWAN() { switch (_status) { @@ -96,9 +159,10 @@ public: } } -private: void SendRegistration(bool forceIPv4) { + using namespace OpenRCT2::Network; + _lastAdvertiseTime = platform_get_ticks(); // Send the registration request @@ -132,6 +196,8 @@ private: void SendHeartbeat() { + using namespace OpenRCT2::Network; + Http::Request request; request.url = GetMasterServerUrl(); request.method = Http::Method::PUT; @@ -260,6 +326,7 @@ private: } return result; } +# endif }; std::unique_ptr CreateServerAdvertiser(uint16_t port) @@ -267,23 +334,4 @@ std::unique_ptr CreateServerAdvertiser(uint16_t port) return std::make_unique(port); } -# else // DISABLE_HTTP - -class DummyNetworkServerAdvertiser final : public INetworkServerAdvertiser -{ -public: - virtual ADVERTISE_STATUS GetStatus() const override - { - return ADVERTISE_STATUS::DISABLED; - }; - virtual void Update() override{}; -}; - -std::unique_ptr CreateServerAdvertiser(uint16_t port) -{ - return std::make_unique(); -} - -# endif // DISABLE_HTTP - #endif // DISABLE_NETWORK diff --git a/src/openrct2/network/ServerList.cpp b/src/openrct2/network/ServerList.cpp index d565ab0d68..d2efbb0fac 100644 --- a/src/openrct2/network/ServerList.cpp +++ b/src/openrct2/network/ServerList.cpp @@ -7,22 +7,155 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -#include "ServerList.h" +#ifndef DISABLE_NETWORK -#include "../Context.h" -#include "../PlatformEnvironment.h" -#include "../core/FileStream.hpp" -#include "../core/Memory.hpp" -#include "../core/Path.hpp" -#include "../core/String.hpp" -#include "../platform/platform.h" +# include "ServerList.h" + +# include "../Context.h" +# include "../PlatformEnvironment.h" +# include "../config/Config.h" +# include "../core/FileStream.hpp" +# include "../core/Json.hpp" +# include "../core/Memory.hpp" +# include "../core/Path.hpp" +# include "../core/String.hpp" +# include "../network/Http.h" +# include "../platform/platform.h" +# include "Socket.h" +# include "network.h" + +# include +# include using namespace OpenRCT2; -std::vector server_list_read() +int32_t ServerListEntry::CompareTo(const ServerListEntry& other) const +{ + const auto& a = *this; + const auto& b = other; + + // Order by favourite + if (a.favourite != b.favourite) + { + return a.favourite ? -1 : 1; + } + + // Order by local + if (a.local != b.local) + { + return a.local ? -1 : 1; + } + + // Then by version + bool serverACompatible = a.version == network_get_version(); + bool serverBCompatible = b.version == network_get_version(); + if (serverACompatible != serverBCompatible) + { + return serverACompatible ? -1 : 1; + } + + // Then by password protection + if (a.requiresPassword != b.requiresPassword) + { + return a.requiresPassword ? 1 : -1; + } + + // Then by number of players + if (a.players != b.players) + { + return a.players > b.players ? -1 : 1; + } + + // Then by name + return String::Compare(a.name, b.name, true); +} + +bool ServerListEntry::IsVersionValid() const +{ + return version.empty() || version == network_get_version(); +} + +opt::optional ServerListEntry::FromJson(const json_t* server) +{ + auto port = json_object_get(server, "port"); + auto name = json_object_get(server, "name"); + auto description = json_object_get(server, "description"); + auto requiresPassword = json_object_get(server, "requiresPassword"); + auto version = json_object_get(server, "version"); + auto players = json_object_get(server, "players"); + auto maxPlayers = json_object_get(server, "maxPlayers"); + auto ip = json_object_get(server, "ip"); + auto ip4 = json_object_get(ip, "v4"); + auto addressIp = json_array_get(ip4, 0); + + if (name == nullptr || version == nullptr) + { + log_verbose("Cowardly refusing to add server without name or version specified."); + return {}; + } + else + { + ServerListEntry entry; + entry.address = String::StdFormat("%s:%d", json_string_value(addressIp), (int32_t)json_integer_value(port)); + entry.name = (name == nullptr ? "" : json_string_value(name)); + entry.description = (description == nullptr ? "" : json_string_value(description)); + entry.version = json_string_value(version); + entry.requiresPassword = json_is_true(requiresPassword); + entry.players = (uint8_t)json_integer_value(players); + entry.maxplayers = (uint8_t)json_integer_value(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() +{ + _serverEntries.clear(); +} + +std::vector ServerList::ReadFavourites() const { log_verbose("server_list_read(...)"); - std::vector entries; + std::vector entries; try { auto env = GetContext()->GetPlatformEnvironment(); @@ -33,7 +166,7 @@ std::vector server_list_read() auto numEntries = fs.ReadValue(); for (size_t i = 0; i < numEntries; i++) { - server_entry serverInfo; + ServerListEntry serverInfo; serverInfo.address = fs.ReadStdString(); serverInfo.name = fs.ReadStdString(); serverInfo.requiresPassword = false; @@ -49,12 +182,32 @@ std::vector server_list_read() catch (const std::exception& e) { log_error("Unable to read server list: %s", e.what()); - entries = std::vector(); + entries = std::vector(); } return entries; } -bool server_list_write(const std::vector& 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()); @@ -80,3 +233,178 @@ bool server_list_write(const std::vector& entries) 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 == NETWORK_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)); + + auto ip4 = json_array(); + json_array_append_new(ip4, json_string(sender.c_str())); + auto ip = json_object(); + json_object_set_new(ip, "v4", ip4); + json_object_set_new(jinfo, "ip", ip); + + auto entry = ServerListEntry::FromJson(jinfo); + if (entry.has_value()) + { + (*entry).local = true; + entries.push_back(*entry); + } + + json_decref(jinfo); + } + } + 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 + using namespace OpenRCT2::Network; + + 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); + auto jsonStatus = json_object_get(root, "status"); + if (!json_is_number(jsonStatus)) + { + throw MasterServerException(STR_SERVER_LIST_INVALID_RESPONSE_JSON_NUMBER); + } + + auto status = (int32_t)json_integer_value(jsonStatus); + if (status != 200) + { + throw MasterServerException(STR_SERVER_LIST_MASTER_SERVER_FAILED); + } + + auto jServers = json_object_get(root, "servers"); + if (!json_is_array(jServers)) + { + throw MasterServerException(STR_SERVER_LIST_INVALID_RESPONSE_JSON_ARRAY); + } + + std::vector entries; + auto count = json_array_size(jServers); + for (size_t i = 0; i < count; i++) + { + auto jServer = json_array_get(jServers, i); + if (json_is_object(jServer)) + { + auto entry = ServerListEntry::FromJson(jServer); + if (entry.has_value()) + { + entries.push_back(*entry); + } + } + } + + p->set_value(entries); + } + catch (...) + { + p->set_exception(std::current_exception()); + } + json_decref(root); + }); + 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 diff --git a/src/openrct2/network/ServerList.h b/src/openrct2/network/ServerList.h index 22afcfbe69..b0cb5e1bc2 100644 --- a/src/openrct2/network/ServerList.h +++ b/src/openrct2/network/ServerList.h @@ -10,21 +10,66 @@ #pragma once #include "../common.h" +#include "../core/Optional.hpp" +#include +#include #include #include -struct server_entry +struct json_t; +struct INetworkEndpoint; + +struct ServerListEntry { std::string address; std::string name; std::string description; std::string version; - bool requiresPassword = false; - bool favourite = false; - uint8_t players = 0; - uint8_t maxplayers = 0; + bool requiresPassword{}; + bool favourite{}; + uint8_t players{}; + uint8_t maxplayers{}; + bool local{}; + + int32_t CompareTo(const ServerListEntry& other) const; + bool IsVersionValid() const; + + static opt::optional FromJson(const json_t* root); }; -std::vector server_list_read(); -bool server_list_write(const std::vector& entries); +class ServerList +{ +private: + std::vector _serverEntries; + + void Sort(); + std::vector ReadFavourites() const; + bool WriteFavourites(const std::vector& entries) const; + std::future> FetchLocalServerListAsync(const INetworkEndpoint& broadcastEndpoint) const; + +public: + ServerListEntry& GetServer(size_t index); + size_t GetCount() const; + void Add(const ServerListEntry& entry); + void AddRange(const std::vector& entries); + void Clear(); + + void ReadAndAddFavourites(); + void WriteFavourites() const; + + std::future> FetchLocalServerListAsync() const; + std::future> FetchOnlineServerListAsync() const; + uint32_t GetTotalPlayerCount() const; +}; + +class MasterServerException : public std::exception +{ +public: + rct_string_id StatusText; + + MasterServerException(rct_string_id statusText) + : StatusText(statusText) + { + } +}; diff --git a/src/openrct2/network/TcpSocket.cpp b/src/openrct2/network/Socket.cpp similarity index 58% rename from src/openrct2/network/TcpSocket.cpp rename to src/openrct2/network/Socket.cpp index 51bdc33ff8..12fed91b78 100644 --- a/src/openrct2/network/TcpSocket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -36,13 +36,15 @@ #endif #define FLAG_NO_PIPE 0 #else - #include #include - #include - #include - #include - #include + #include #include + #include + #include + #include + #include + #include + #include #include "../common.h" using SOCKET = int32_t; #define SOCKET_ERROR -1 @@ -58,7 +60,7 @@ #endif // _WIN32 // clang-format on -# include "TcpSocket.h" +# include "Socket.h" constexpr auto CONNECT_TIMEOUT = std::chrono::milliseconds(3000); @@ -66,8 +68,6 @@ constexpr auto CONNECT_TIMEOUT = std::chrono::milliseconds(3000); static bool _wsaInitialised = false; # endif -class TcpSocket; - class SocketException : public std::runtime_error { public: @@ -77,7 +77,123 @@ public: } }; -class TcpSocket final : public ITcpSocket +class NetworkEndpoint final : public INetworkEndpoint +{ +private: + sockaddr _address{}; + socklen_t _addressLen{}; + +public: + NetworkEndpoint() + { + } + + NetworkEndpoint(const sockaddr* address, socklen_t addressLen) + { + std::memcpy(&_address, address, addressLen); + _addressLen = addressLen; + } + + const sockaddr& GetAddress() const + { + return _address; + } + + socklen_t GetAddressLen() const + { + return _addressLen; + } + + int32_t GetPort() const + { + if (_address.sa_family == AF_INET) + { + return ((sockaddr_in*)&_address)->sin_port; + } + else + { + return ((sockaddr_in6*)&_address)->sin6_port; + } + } + + std::string GetHostname() const override + { + char hostname[256]{}; + int res = getnameinfo(&_address, _addressLen, hostname, sizeof(hostname), nullptr, 0, NI_NUMERICHOST); + if (res == 0) + { + return hostname; + } + return {}; + } +}; + +class Socket +{ +protected: + static bool ResolveAddress(const std::string& address, uint16_t port, sockaddr_storage* ss, socklen_t* ss_len) + { + return ResolveAddress(AF_UNSPEC, address, port, ss, ss_len); + } + + static bool ResolveAddressIPv4(const std::string& address, uint16_t port, sockaddr_storage* ss, socklen_t* ss_len) + { + return ResolveAddress(AF_INET, address, port, ss, ss_len); + } + + static bool SetNonBlocking(SOCKET socket, bool on) + { +# ifdef _WIN32 + u_long nonBlocking = on; + return ioctlsocket(socket, FIONBIO, &nonBlocking) == 0; +# else + int32_t flags = fcntl(socket, F_GETFL, 0); + return fcntl(socket, F_SETFL, on ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK)) == 0; +# endif + } + + static bool SetOption(SOCKET socket, int32_t a, int32_t b, bool value) + { + int32_t ivalue = value ? 1 : 0; + return setsockopt(socket, a, b, (const char*)&ivalue, sizeof(ivalue)) == 0; + } + +private: + static bool ResolveAddress( + int32_t family, const std::string& address, uint16_t port, sockaddr_storage* ss, socklen_t* ss_len) + { + std::string serviceName = std::to_string(port); + + addrinfo hints = {}; + hints.ai_family = family; + if (address.empty()) + { + hints.ai_flags = AI_PASSIVE; + } + + addrinfo* result = nullptr; + int errorcode = getaddrinfo(address.empty() ? nullptr : address.c_str(), serviceName.c_str(), &hints, &result); + if (errorcode != 0) + { + log_error("Resolving address failed: Code %d.", errorcode); + log_error("Resolution error message: %s.", gai_strerror(errorcode)); + return false; + } + if (result == nullptr) + { + return false; + } + else + { + std::memcpy(ss, result->ai_addr, result->ai_addrlen); + *ss_len = (socklen_t)result->ai_addrlen; + freeaddrinfo(result); + return true; + } + } +}; + +class TcpSocket final : public ITcpSocket, protected Socket { private: SOCKET_STATUS _status = SOCKET_STATUS_CLOSED; @@ -100,12 +216,12 @@ public: CloseSocket(); } - SOCKET_STATUS GetStatus() override + SOCKET_STATUS GetStatus() const override { return _status; } - const char* GetError() override + const char* GetError() const override { return _error.empty() ? nullptr : _error.c_str(); } @@ -123,7 +239,7 @@ public: } sockaddr_storage ss{}; - int32_t ss_len; + socklen_t ss_len; if (!ResolveAddress(address, port, &ss, &ss_len)) { throw SocketException("Unable to resolve address."); @@ -209,7 +325,7 @@ public: int32_t rc = getnameinfo( (struct sockaddr*)&client_addr, client_len, hostName, sizeof(hostName), nullptr, 0, NI_NUMERICHOST | NI_NUMERICSERV); - SetTCPNoDelay(socket, true); + SetOption(socket, IPPROTO_TCP, TCP_NODELAY, true); if (rc == 0) { tcpSocket = std::unique_ptr(new TcpSocket(socket, hostName)); @@ -236,7 +352,7 @@ public: _status = SOCKET_STATUS_RESOLVING; sockaddr_storage ss{}; - int32_t ss_len; + socklen_t ss_len; if (!ResolveAddress(address, port, &ss, &ss_len)) { throw SocketException("Unable to resolve address."); @@ -249,7 +365,7 @@ public: throw SocketException("Unable to create socket."); } - SetTCPNoDelay(_socket, true); + SetOption(_socket, IPPROTO_TCP, TCP_NODELAY, true); if (!SetNonBlocking(_socket, true)) { throw SocketException("Failed to set non-blocking mode."); @@ -446,61 +562,212 @@ private: } _status = SOCKET_STATUS_CLOSED; } +}; - bool ResolveAddress(const std::string& address, uint16_t port, sockaddr_storage* ss, int32_t* ss_len) +class UdpSocket final : public IUdpSocket, protected Socket +{ +private: + SOCKET_STATUS _status = SOCKET_STATUS_CLOSED; + uint16_t _listeningPort = 0; + SOCKET _socket = INVALID_SOCKET; + NetworkEndpoint _endpoint; + + std::string _hostName; + std::string _error; + +public: + UdpSocket() = default; + + ~UdpSocket() override { - std::string serviceName = std::to_string(port); + CloseSocket(); + } - addrinfo hints = {}; - hints.ai_family = AF_UNSPEC; - if (address.empty()) + SOCKET_STATUS GetStatus() const override + { + return _status; + } + + const char* GetError() const override + { + return _error.empty() ? nullptr : _error.c_str(); + } + + void Listen(uint16_t port) override + { + Listen("", port); + } + + void Listen(const std::string& address, uint16_t port) override + { + if (_status != SOCKET_STATUS_CLOSED) { - hints.ai_flags = AI_PASSIVE; + throw std::runtime_error("Socket not closed."); } - addrinfo* result = nullptr; - int errorcode = getaddrinfo(address.empty() ? nullptr : address.c_str(), serviceName.c_str(), &hints, &result); - if (errorcode != 0) + sockaddr_storage ss{}; + socklen_t ss_len; + if (!ResolveAddressIPv4(address, port, &ss, &ss_len)) { - log_error("Resolving address failed: Code %d.", errorcode); - log_error("Resolution error message: %s.", gai_strerror(errorcode)); - return false; + throw SocketException("Unable to resolve address."); } - if (result == nullptr) + + // Create the listening socket + _socket = CreateSocket(); + try { - return false; + // Bind to address:port and listen + if (bind(_socket, (sockaddr*)&ss, ss_len) != 0) + { + throw SocketException("Unable to bind to socket."); + } + } + catch (const std::exception&) + { + CloseSocket(); + throw; + } + + _listeningPort = port; + _status = SOCKET_STATUS_LISTENING; + } + + size_t SendData(const std::string& address, uint16_t port, const void* buffer, size_t size) override + { + sockaddr_storage ss{}; + socklen_t ss_len; + if (!ResolveAddressIPv4(address, port, &ss, &ss_len)) + { + throw SocketException("Unable to resolve address."); + } + NetworkEndpoint endpoint((const sockaddr*)&ss, ss_len); + return SendData(endpoint, buffer, size); + } + + size_t SendData(const INetworkEndpoint& destination, const void* buffer, size_t size) override + { + if (_socket == INVALID_SOCKET) + { + _socket = CreateSocket(); + } + + const auto& dest = dynamic_cast(&destination); + if (dest == nullptr) + { + throw std::invalid_argument("destination is not compatible."); + } + auto ss = &dest->GetAddress(); + auto ss_len = dest->GetAddressLen(); + + if (_status != SOCKET_STATUS_LISTENING) + { + _endpoint = *dest; + } + + size_t totalSent = 0; + do + { + const char* bufferStart = (const char*)buffer + totalSent; + size_t remainingSize = size - totalSent; + int32_t sentBytes = sendto(_socket, bufferStart, (int32_t)remainingSize, FLAG_NO_PIPE, (const sockaddr*)ss, ss_len); + if (sentBytes == SOCKET_ERROR) + { + return totalSent; + } + totalSent += sentBytes; + } while (totalSent < size); + return totalSent; + } + + NETWORK_READPACKET ReceiveData( + void* buffer, size_t size, size_t* sizeReceived, std::unique_ptr* sender) override + { + sockaddr_in senderAddr{}; + socklen_t senderAddrLen = sizeof(sockaddr_in); + if (_status != SOCKET_STATUS_LISTENING) + { + senderAddrLen = _endpoint.GetAddressLen(); + std::memcpy(&senderAddr, &_endpoint.GetAddress(), senderAddrLen); + } + auto readBytes = recvfrom(_socket, (char*)buffer, (int32_t)size, 0, (sockaddr*)&senderAddr, &senderAddrLen); + if (readBytes <= 0) + { + *sizeReceived = 0; + return NETWORK_READPACKET_NO_DATA; } else { - std::memcpy(ss, result->ai_addr, result->ai_addrlen); - *ss_len = (int32_t)result->ai_addrlen; - freeaddrinfo(result); - return true; + *sizeReceived = readBytes; + if (sender != nullptr) + { + *sender = std::make_unique((sockaddr*)&senderAddr, senderAddrLen); + } + return NETWORK_READPACKET_SUCCESS; } } - static bool SetNonBlocking(SOCKET socket, bool on) + void Close() override { -# ifdef _WIN32 - u_long nonBlocking = on; - return ioctlsocket(socket, FIONBIO, &nonBlocking) == 0; -# else - int32_t flags = fcntl(socket, F_GETFL, 0); - return fcntl(socket, F_SETFL, on ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK)) == 0; -# endif + CloseSocket(); } - static bool SetTCPNoDelay(SOCKET socket, bool enabled) + const char* GetHostName() const override { - return setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, (const char*)&enabled, sizeof(enabled)) == 0; + return _hostName.empty() ? nullptr : _hostName.c_str(); + } + +private: + explicit UdpSocket(SOCKET socket, const std::string& hostName) + { + _socket = socket; + _hostName = hostName; + _status = SOCKET_STATUS_CONNECTED; + } + + SOCKET CreateSocket() + { + auto sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock == INVALID_SOCKET) + { + throw SocketException("Unable to create socket."); + } + + // Enable send and receiving of broadcast messages + if (!SetOption(sock, SOL_SOCKET, SO_BROADCAST, true)) + { + log_warning("SO_BROADCAST failed. %d", LAST_SOCKET_ERROR()); + } + + // Turn off IPV6_V6ONLY so we can accept both v4 and v6 connections + if (!SetOption(sock, IPPROTO_IPV6, IPV6_V6ONLY, false)) + { + log_warning("IPV6_V6ONLY failed. %d", LAST_SOCKET_ERROR()); + } + + if (!SetOption(sock, SOL_SOCKET, SO_REUSEADDR, true)) + { + log_warning("SO_REUSEADDR failed. %d", LAST_SOCKET_ERROR()); + } + + if (!SetNonBlocking(sock, true)) + { + throw SocketException("Failed to set non-blocking mode."); + } + + return sock; + } + + void CloseSocket() + { + if (_socket != INVALID_SOCKET) + { + closesocket(_socket); + _socket = INVALID_SOCKET; + } + _status = SOCKET_STATUS_CLOSED; } }; -std::unique_ptr CreateTcpSocket() -{ - return std::make_unique(); -} - bool InitialiseWSA() { # ifdef _WIN32 @@ -532,6 +799,112 @@ void DisposeWSA() # endif } +std::unique_ptr CreateTcpSocket() +{ + return std::make_unique(); +} + +std::unique_ptr CreateUdpSocket() +{ + return std::make_unique(); +} + +# ifdef _WIN32 +static std::vector GetNetworkInterfaces() +{ + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + { + return {}; + } + + // Get all the network interfaces, requires a trial and error approch + // until we find the capacity required to store all of them. + DWORD len = 0; + size_t capacity = 16; + std::vector interfaces; + for (;;) + { + interfaces.resize(capacity); + if (WSAIoctl( + sock, SIO_GET_INTERFACE_LIST, nullptr, 0, interfaces.data(), (DWORD)(capacity * sizeof(INTERFACE_INFO)), &len, + nullptr, nullptr) + == 0) + { + break; + } + if (WSAGetLastError() != WSAEFAULT) + { + closesocket(sock); + return {}; + } + capacity *= 2; + } + interfaces.resize(len / sizeof(INTERFACE_INFO)); + interfaces.shrink_to_fit(); + return interfaces; +} +# endif + +std::vector> GetBroadcastAddresses() +{ + std::vector> baddresses; +# ifdef _WIN32 + auto interfaces = GetNetworkInterfaces(); + for (const auto& ifo : interfaces) + { + if (ifo.iiFlags & IFF_LOOPBACK) + continue; + if (!(ifo.iiFlags & IFF_BROADCAST)) + continue; + + // iiBroadcast is unusable, because it always seems to be set to 255.255.255.255. + sockaddr_storage address{}; + memcpy(&address, &ifo.iiAddress.Address, sizeof(sockaddr)); + ((sockaddr_in*)&address)->sin_addr.s_addr = ifo.iiAddress.AddressIn.sin_addr.s_addr + | ~ifo.iiNetmask.AddressIn.sin_addr.s_addr; + baddresses.push_back(std::make_unique((const sockaddr*)&address, (socklen_t)sizeof(sockaddr))); + } +# else + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock == -1) + { + return baddresses; + } + + char buf[4 * 1024]{}; + ifconf ifconfx{}; + ifconfx.ifc_len = sizeof(buf); + ifconfx.ifc_buf = buf; + if (ioctl(sock, SIOCGIFCONF, &ifconfx) == -1) + { + close(sock); + return baddresses; + } + + const char* buf_end = buf + ifconfx.ifc_len; + for (const char* p = buf; p < buf_end;) + { + auto req = (const ifreq*)p; + if (req->ifr_addr.sa_family == AF_INET) + { + ifreq r; + strcpy(r.ifr_name, req->ifr_name); + if (ioctl(sock, SIOCGIFFLAGS, &r) != -1 && (r.ifr_flags & IFF_BROADCAST) && ioctl(sock, SIOCGIFBRDADDR, &r) != -1) + { + baddresses.push_back(std::make_unique(&r.ifr_broadaddr, sizeof(sockaddr))); + } + } + p += sizeof(ifreq); +# if defined(AF_LINK) && !defined(SUNOS) + p += req->ifr_addr.sa_len - sizeof(struct sockaddr); +# endif + } + close(sock); +# endif + return baddresses; +} + namespace Convert { uint16_t HostToNetwork(uint16_t value) diff --git a/src/openrct2/network/TcpSocket.h b/src/openrct2/network/Socket.h similarity index 59% rename from src/openrct2/network/TcpSocket.h rename to src/openrct2/network/Socket.h index 1e6186f20a..601168b31b 100644 --- a/src/openrct2/network/TcpSocket.h +++ b/src/openrct2/network/Socket.h @@ -13,6 +13,7 @@ #include #include +#include enum SOCKET_STATUS { @@ -31,18 +32,28 @@ enum NETWORK_READPACKET NETWORK_READPACKET_DISCONNECTED }; +/** + * Represents an address and port. + */ +interface INetworkEndpoint +{ + virtual ~INetworkEndpoint() + { + } + + virtual std::string GetHostname() const abstract; +}; + /** * Represents a TCP socket / connection or listener. */ interface ITcpSocket { public: - virtual ~ITcpSocket() - { - } + virtual ~ITcpSocket() = default; - virtual SOCKET_STATUS GetStatus() abstract; - virtual const char* GetError() abstract; + virtual SOCKET_STATUS GetStatus() const abstract; + virtual const char* GetError() const abstract; virtual const char* GetHostName() const abstract; virtual void Listen(uint16_t port) abstract; @@ -59,10 +70,34 @@ public: virtual void Close() abstract; }; -std::unique_ptr CreateTcpSocket(); +/** + * Represents a UDP socket / listener. + */ +interface IUdpSocket +{ +public: + virtual ~IUdpSocket() = default; + + virtual SOCKET_STATUS GetStatus() const abstract; + virtual const char* GetError() const abstract; + virtual const char* GetHostName() const abstract; + + virtual void Listen(uint16_t port) abstract; + virtual void Listen(const std::string& address, uint16_t port) abstract; + + virtual size_t SendData(const std::string& address, uint16_t port, const void* buffer, size_t size) abstract; + virtual size_t SendData(const INetworkEndpoint& destination, const void* buffer, size_t size) abstract; + virtual NETWORK_READPACKET ReceiveData( + void* buffer, size_t size, size_t* sizeReceived, std::unique_ptr* sender) abstract; + + virtual void Close() abstract; +}; bool InitialiseWSA(); void DisposeWSA(); +std::unique_ptr CreateTcpSocket(); +std::unique_ptr CreateUdpSocket(); +std::vector> GetBroadcastAddresses(); namespace Convert { diff --git a/src/openrct2/network/network.h b/src/openrct2/network/network.h index f19a83f8b3..b57dd058cc 100644 --- a/src/openrct2/network/network.h +++ b/src/openrct2/network/network.h @@ -10,6 +10,8 @@ #pragma once #define NETWORK_DEFAULT_PORT 11753 +#define NETWORK_LAN_BROADCAST_PORT 11754 +#define NETWORK_LAN_BROADCAST_MSG "openrct2.server.query" #define MAX_SERVER_DESCRIPTION_LENGTH 256 #include "../common.h" @@ -19,6 +21,7 @@ #include #include +struct json_t; struct GameAction; struct Peep; struct LocationXYZ16; @@ -109,3 +112,4 @@ std::string network_get_version(); NetworkStats_t network_get_stats(); NetworkServerState_t network_get_server_state(); +json_t* network_get_server_info_as_json();