diff --git a/OpenRCT2.xcodeproj/project.pbxproj b/OpenRCT2.xcodeproj/project.pbxproj index edfd111be7..e87a00df28 100644 --- a/OpenRCT2.xcodeproj/project.pbxproj +++ b/OpenRCT2.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 2AA050322209A8E300D3A922 /* StaffSetCostumeAction.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 2AA050302209A8E300D3A922 /* StaffSetCostumeAction.hpp */; }; 2AA050332209A8E300D3A922 /* StaffSetOrdersAction.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 2AA050312209A8E300D3A922 /* StaffSetOrdersAction.hpp */; }; 2AF7893D220B253E0072754A /* RideSetAppearanceAction.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 2AF7893C220B253E0072754A /* RideSetAppearanceAction.hpp */; }; + 2A5354E922099C4F00A5440F /* Network.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2A5354E822099C4F00A5440F /* Network.cpp */; }; 4C29DEB3218C6AE500E8707F /* RCT12.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C29DEB2218C6AE500E8707F /* RCT12.cpp */; }; 4C358E5221C445F700ADE6BC /* ReplayManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C358E5021C445F700ADE6BC /* ReplayManager.cpp */; }; 4C3B4236205914F7000C5BB7 /* InGameConsole.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C3B4234205914F7000C5BB7 /* InGameConsole.cpp */; }; @@ -619,6 +620,8 @@ 2AA050302209A8E300D3A922 /* StaffSetCostumeAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaffSetCostumeAction.hpp; sourceTree = ""; }; 2AA050312209A8E300D3A922 /* StaffSetOrdersAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaffSetOrdersAction.hpp; sourceTree = ""; }; 2AF7893C220B253E0072754A /* RideSetAppearanceAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RideSetAppearanceAction.hpp; sourceTree = ""; }; + 2A5354E822099C4F00A5440F /* Network.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Network.cpp; sourceTree = ""; }; + 2A5354EA22099C7200A5440F /* CircularBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircularBuffer.h; sourceTree = ""; }; 4C04D69F2056AA9600F82EBA /* linenoise.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = linenoise.hpp; sourceTree = ""; }; 4C1A53EC205FD19F000F8EF5 /* SceneryObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SceneryObject.cpp; sourceTree = ""; }; 4C29DEB2218C6AE500E8707F /* RCT12.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RCT12.cpp; sourceTree = ""; }; @@ -2454,6 +2457,7 @@ F76C83781EC4E7CC00FA49E2 /* core */ = { isa = PBXGroup; children = ( + 2A5354EA22099C7200A5440F /* CircularBuffer.h */, F76C83791EC4E7CC00FA49E2 /* Collections.hpp */, F76C837A1EC4E7CC00FA49E2 /* Console.cpp */, 9344BEF720C1E6180047D165 /* Crypt.h */, @@ -3193,6 +3197,7 @@ F7CB86401EEDA0E20030C877 /* windows */ = { isa = PBXGroup; children = ( + 2A5354E822099C4F00A5440F /* Network.cpp */, C666EE551F37ACB10061AA04 /* About.cpp */, C654DF1C1F69C0430040F43D /* Banner.cpp */, C666EE561F37ACB10061AA04 /* Changelog.cpp */, @@ -3709,6 +3714,7 @@ C654DF311F69C0430040F43D /* GuestList.cpp in Sources */, 4C93F1AD1F8CD9F000A9330D /* Input.cpp in Sources */, C666EE761F37ACB10061AA04 /* Options.cpp in Sources */, + 2A5354E922099C4F00A5440F /* Network.cpp in Sources */, C666EE6E1F37ACB10061AA04 /* CustomCurrency.cpp in Sources */, C654DF2D1F69C0430040F43D /* Banner.cpp in Sources */, C666EE711F37ACB10061AA04 /* MapGen.cpp in Sources */, diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index ed52fcbaea..a5d741af45 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3731,6 +3731,21 @@ STR_6280 :{SMALLFONT}{BLACK}Chat STR_6281 :{SMALLFONT}{BLACK}Show a separate button for the Chat window in the toolbar STR_6282 :Chat STR_6283 :Chat not available at this time. Are you connected to a server? +STR_6284 :Network +STR_6285 :Network Information +STR_6286 :Receive +STR_6287 :Send +STR_6288 :Total received +STR_6289 :Total sent +STR_6290 :Base protocol +STR_6291 :Commands +STR_6292 :Map +STR_6293 :B +STR_6294 :KiB +STR_6295 :MiB +STR_6296 :GiB +STR_6297 :TiB +STR_6298 :{STRING}/sec ############# # Scenarios # diff --git a/distribution/changelog.txt b/distribution/changelog.txt index b15e476fe5..6a066ce60b 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -18,6 +18,7 @@ - Feature: [#8458] Add sprite sorting benchmark. - Feature: [#8583] Add boosters to water coaster. - Feature: [#8648] Add optional chat button to top toolbar in multiplayer games. +- Feature: [#8652] Add network window including a graph for data usage visualisation. - Change: [#7961] Add new object types: station, terrain surface, and terrain edge. - Change: [#8222] The climate setting has been moved from objective options to scenario options. - Fix: [#3832] Changing the colour scheme of track pieces does not work in multiplayer. diff --git a/src/openrct2-ui/WindowManager.cpp b/src/openrct2-ui/WindowManager.cpp index d6fc874684..9ca04f240e 100644 --- a/src/openrct2-ui/WindowManager.cpp +++ b/src/openrct2-ui/WindowManager.cpp @@ -128,6 +128,8 @@ public: return window_viewport_open(); case WC_WATER: return window_water_open(); + case WC_NETWORK: + return window_network_open(); default: Console::Error::WriteLine("Unhandled window class (%d)", wc); return nullptr; diff --git a/src/openrct2-ui/windows/Network.cpp b/src/openrct2-ui/windows/Network.cpp new file mode 100644 index 0000000000..37b689da6e --- /dev/null +++ b/src/openrct2-ui/windows/Network.cpp @@ -0,0 +1,480 @@ +/***************************************************************************** + * Copyright (c) 2014-2019 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. + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +enum { + WINDOW_NETWORK_PAGE_INFORMATION, +}; + +#define WW 450 +#define WH 210 + +enum WINDOW_NETWORK_WIDGET_IDX { + WIDX_BACKGROUND, + WIDX_TITLE, + WIDX_CLOSE, + WIDX_RESIZE, + WIDX_TAB1, +}; + +#define MAIN_NETWORK_WIDGETS \ + { WWT_FRAME, 0, 0, WW - 1, 0, WH - 1, STR_NONE, STR_NONE }, /* panel / background */ \ + { WWT_CAPTION, 0, 1, WW - 2, 1, 14, STR_NONE, STR_WINDOW_TITLE_TIP }, /* title bar */ \ + { WWT_CLOSEBOX, 0, WW - 13, WW - 3, 2, 13, STR_CLOSE_X, STR_CLOSE_WINDOW_TIP }, /* close x button */ \ + { WWT_RESIZE, 1, 0, WW - 1, 43, WH - 1, 0xFFFFFFFF, STR_NONE }, /* content panel */ \ + { WWT_TAB, 1, 3, 33, 17, 43, IMAGE_TYPE_REMAP | SPR_TAB, STR_SHOW_SERVER_INFO_TIP }, /* tab */ \ + +static rct_widget window_network_information_widgets[] = { + MAIN_NETWORK_WIDGETS + { WIDGETS_END } +}; + +static rct_widget *window_network_page_widgets[] = { + window_network_information_widgets, +}; + +static constexpr const uint64_t window_network_page_enabled_widgets[] = { + (1 << WIDX_CLOSE) | (1 << WIDX_TAB1), +}; + +static constexpr rct_string_id WindowNetworkPageTitles[] = { + STR_NETWORK_INFORMATION_TITLE, +}; + +static void window_network_information_mouseup(rct_window *w, rct_widgetindex widgetIndex); +static void window_network_information_resize(rct_window *w); +static void window_network_information_update(rct_window *w); +static void window_network_information_invalidate(rct_window *w); +static void window_network_information_paint(rct_window *w, rct_drawpixelinfo *dpi); + +struct NetworkHistory_t +{ + std::array deltaBytesReceived; + std::array deltaBytesSent; +}; + +static NetworkStats_t _networkStats; +static NetworkHistory_t _networkLastDeltaStats; +static NetworkHistory_t _networkAccumulatedStats; + +static float _graphMaxIn; +static float _graphMaxOut; + +static float _bytesInSec; +static float _bytesOutSec; +static uint32_t _bytesIn; +static uint32_t _bytesOut; + +static uint32_t _lastGraphUpdateTime; +static uint32_t _lastStatsUpdateTime; + +static CircularBuffer _networkHistory; + +static constexpr int32_t NetworkTrafficGroupColors[NETWORK_STATISTICS_GROUP_MAX] = { + PALETTE_INDEX_21, + PALETTE_INDEX_102, + PALETTE_INDEX_138, + PALETTE_INDEX_171, +}; + +static constexpr int32_t NetworkTrafficGroupNames[NETWORK_STATISTICS_GROUP_MAX] = { + STR_NETWORK, + STR_NETWORK_LEGEND_BASE, + STR_NETWORK_LEGEND_COMMANDS, + STR_NETWORK_LEGEND_MAPDATA, +}; + +static rct_window_event_list window_network_information_events = { + nullptr, + window_network_information_mouseup, + window_network_information_resize, + nullptr, + nullptr, + nullptr, + window_network_information_update, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + window_network_information_invalidate, + window_network_information_paint, + nullptr +}; + +static rct_window_event_list *window_network_page_events[] = { + &window_network_information_events, +}; +// clang-format on + +static constexpr const int32_t window_network_animation_divisor[] = { 4, 4, 2, 2 }; +static constexpr const int32_t window_network_animation_frames[] = { 8, 8, 7, 4 }; + +static void window_network_draw_tab_images(rct_window* w, rct_drawpixelinfo* dpi); +static void window_network_set_page(rct_window* w, int32_t page); + +rct_window* window_network_open() +{ + // Check if window is already open + rct_window* window = window_bring_to_front_by_class(WC_NETWORK); + if (window == nullptr) + { + window = window_create_auto_pos(320, 144, &window_network_information_events, WC_NETWORK, WF_10 | WF_RESIZABLE); + window_network_set_page(window, WINDOW_NETWORK_PAGE_INFORMATION); + + // Fill the buffer so it will start scrolling in. + _networkHistory.clear(); + for (size_t i = 0; i < _networkHistory.capacity(); i++) + { + _networkHistory.push_back(NetworkHistory_t{}); + } + } + + _networkStats = network_get_stats(); + _networkAccumulatedStats = {}; + + return window; +} + +static void window_network_set_page(rct_window* w, int32_t page) +{ + w->page = page; + w->frame_no = 0; + w->no_list_items = 0; + w->selected_list_item = -1; + + w->enabled_widgets = window_network_page_enabled_widgets[page]; + w->hold_down_widgets = 0; + w->event_handlers = window_network_page_events[page]; + w->pressed_widgets = 0; + w->widgets = window_network_page_widgets[page]; + w->widgets[WIDX_TITLE].text = WindowNetworkPageTitles[page]; + + window_event_resize_call(w); + window_event_invalidate_call(w); + window_init_scroll_widgets(w); + window_invalidate(w); +} + +static void window_network_anchor_border_widgets(rct_window* w) +{ + w->widgets[WIDX_BACKGROUND].right = w->width - 1; + w->widgets[WIDX_BACKGROUND].bottom = w->height - 1; + w->widgets[WIDX_TITLE].right = w->width - 2; + w->widgets[WIDX_RESIZE].right = w->width - 1; + w->widgets[WIDX_RESIZE].bottom = w->height - 1; + w->widgets[WIDX_CLOSE].left = w->width - 13; + w->widgets[WIDX_CLOSE].right = w->width - 3; +} + +static void window_network_set_pressed_tab(rct_window* w) +{ + for (int32_t i = 0; i < 2; i++) + { + w->pressed_widgets &= ~(1 << (WIDX_TAB1 + i)); + } + w->pressed_widgets |= 1LL << (WIDX_TAB1 + w->page); +} + +#pragma region Information page + +static void window_network_information_mouseup(rct_window* w, rct_widgetindex widgetIndex) +{ + switch (widgetIndex) + { + case WIDX_CLOSE: + window_close(w); + break; + case WIDX_TAB1: + if (w->page != widgetIndex - WIDX_TAB1) + { + window_network_set_page(w, widgetIndex - WIDX_TAB1); + } + break; + } +} + +static void window_network_information_resize(rct_window* w) +{ + window_set_resize(w, WW, WH, WW * 4, WH * 4); + window_network_anchor_border_widgets(w); +} + +static void window_network_information_update(rct_window* w) +{ + w->frame_no++; + widget_invalidate(w, WIDX_TAB1 + w->page); + window_invalidate(w); + + NetworkStats_t curStats = network_get_stats(); + + uint32_t currentTicks = platform_get_ticks(); + + float graphTimeElapsed = (currentTicks - _lastGraphUpdateTime) / 1000.0f; + _lastGraphUpdateTime = currentTicks; + + for (int i = 0; i < NETWORK_STATISTICS_GROUP_MAX; i++) + { + uint32_t deltaBytesReceived = curStats.bytesReceived[i] - _networkStats.bytesReceived[i]; + uint32_t deltaBytesSent = curStats.bytesSent[i] - _networkStats.bytesSent[i]; + + _networkLastDeltaStats.deltaBytesReceived[i] = deltaBytesReceived; + _networkLastDeltaStats.deltaBytesSent[i] = deltaBytesSent; + + _networkAccumulatedStats.deltaBytesReceived[i] += deltaBytesReceived; + _networkAccumulatedStats.deltaBytesSent[i] += deltaBytesSent; + } + + float graphMaxIn = 0.0f; + float graphMaxOut = 0.0f; + + for (size_t i = 0; i < _networkHistory.size(); i++) + { + const NetworkHistory_t& history = _networkHistory[i]; + for (int n = 1; n < NETWORK_STATISTICS_GROUP_MAX; n++) + { + graphMaxIn = (float)std::max(history.deltaBytesReceived[n], graphMaxIn); + graphMaxOut = (float)std::max(history.deltaBytesSent[n], graphMaxOut); + } + } + + _graphMaxIn = flerp(_graphMaxIn, graphMaxIn, graphTimeElapsed * 4.0f); + _graphMaxOut = flerp(_graphMaxOut, graphMaxOut, graphTimeElapsed * 4.0f); + + // Compute readable statistics. + if (currentTicks - _lastStatsUpdateTime >= 1000) + { + float statsTimeElapsed = (currentTicks - _lastStatsUpdateTime) / 1000.0f; + _lastStatsUpdateTime = currentTicks; + + _bytesIn = _networkAccumulatedStats.deltaBytesReceived[NETWORK_STATISTICS_GROUP_TOTAL]; + _bytesOut = _networkAccumulatedStats.deltaBytesSent[NETWORK_STATISTICS_GROUP_TOTAL]; + _bytesInSec = (double)_bytesIn / statsTimeElapsed; + _bytesOutSec = (double)_bytesOut / statsTimeElapsed; + + _networkAccumulatedStats = {}; + } + + _networkStats = curStats; + _networkHistory.push_back(_networkLastDeltaStats); +} + +static void window_network_information_invalidate(rct_window* w) +{ + window_network_set_pressed_tab(w); + window_network_anchor_border_widgets(w); + window_align_tabs(w, WIDX_TAB1, WIDX_TAB1); +} + +static void graph_draw_bar(rct_drawpixelinfo* dpi, int32_t x, int32_t y, int32_t height, int32_t width, int32_t colour) +{ + gfx_fill_rect(dpi, x, y, x + width, y + height, colour); +} + +static void window_network_draw_graph( + rct_window* w, rct_drawpixelinfo* dpi, int32_t x, int32_t y, int32_t height, int32_t width, int32_t barWidth, bool received) +{ + float dataMax = received ? _graphMaxIn : _graphMaxOut; + + // Draw box. + gfx_draw_line(dpi, x, y, x, y + height, COLOUR_BLACK); + gfx_draw_line(dpi, x, y + height, x + width, y + height, COLOUR_BLACK); + + gfx_draw_line(dpi, x, y, x + width, y, COLOUR_BLACK); + gfx_draw_line(dpi, x + width, y, x + width, y + height, COLOUR_BLACK); + + // Draw graph inside box + x = x + 1; + y = y + 1; + width = width - 2; + height = height - 2; + + rct_drawpixelinfo clippedDPI; + if (!clip_drawpixelinfo(&clippedDPI, dpi, x, y, width, height)) + return; + + dpi = &clippedDPI; + + for (size_t i = 0; i < _networkHistory.size(); i++) + { + NetworkHistory_t history = _networkHistory[i]; + // std::sort(history.deltaBytesReceived.begin(), history.deltaBytesReceived.end(), std::greater()); + + // NOTE: Capacity is not a mistake, we always want the full length. + uint32_t curX = std::round(((float)i / (float)_networkHistory.capacity()) * barWidth * width); + + float totalSum = 0.0f; + for (int n = 1; n < NETWORK_STATISTICS_GROUP_MAX; n++) + { + if (received) + totalSum += (float)history.deltaBytesReceived[n]; + else + totalSum += (float)history.deltaBytesSent[n]; + } + + int32_t yOffset = height; + for (int n = 1; n < NETWORK_STATISTICS_GROUP_MAX; n++) + { + float totalHeight; + float singleHeight; + + if (received) + { + totalHeight = ((float)history.deltaBytesReceived[n] / dataMax) * height; + singleHeight = ((float)history.deltaBytesReceived[n] / totalSum) * totalHeight; + } + else + { + totalHeight = ((float)history.deltaBytesSent[n] / dataMax) * height; + singleHeight = ((float)history.deltaBytesSent[n] / totalSum) * totalHeight; + } + + uint32_t lineHeight = std::ceil(singleHeight); + lineHeight = std::min(lineHeight, height); + + if (lineHeight > 0) + { + graph_draw_bar(dpi, curX, yOffset - lineHeight, lineHeight, barWidth, NetworkTrafficGroupColors[n]); + } + + yOffset -= lineHeight; + } + } +} + +static void window_network_information_paint(rct_window* w, rct_drawpixelinfo* dpi) +{ + char textBuffer[200] = {}; + + window_draw_widgets(w, dpi); + window_network_draw_tab_images(w, dpi); + + constexpr int32_t padding = 5; + constexpr int32_t heightTab = 43; + constexpr int32_t textHeight = 12; + const int32_t graphBarWidth = std::min(1, w->width / WH); + const int32_t totalHeight = w->height; + const int32_t totalHeightText = (textHeight + (padding * 2)) * 3; + const int32_t graphHeight = (totalHeight - totalHeightText - heightTab) / 2; + + rct_drawpixelinfo clippedDPI; + if (clip_drawpixelinfo(&clippedDPI, dpi, w->x, w->y, w->width, w->height)) + { + dpi = &clippedDPI; + + int32_t x = padding; + int32_t y = heightTab + padding; + + // Received stats. + { + gfx_draw_string_left(dpi, STR_NETWORK_RECEIVE, nullptr, PALETTE_INDEX_10, x, y); + + format_readable_speed(textBuffer, sizeof(textBuffer), _bytesInSec); + gfx_draw_string(dpi, textBuffer, PALETTE_INDEX_10, x + 70, y); + + gfx_draw_string_left(dpi, STR_NETWORK_TOTAL_RECEIVED, nullptr, PALETTE_INDEX_10, x + 200, y); + + format_readable_size(textBuffer, sizeof(textBuffer), _networkStats.bytesReceived[NETWORK_STATISTICS_GROUP_TOTAL]); + gfx_draw_string(dpi, textBuffer, PALETTE_INDEX_10, x + 300, y); + y += textHeight + padding; + + window_network_draw_graph(w, dpi, x, y, graphHeight, w->width - (padding * 2), graphBarWidth, true); + y += graphHeight + padding; + } + + // Sent stats. + { + gfx_draw_string_left(dpi, STR_NETWORK_SEND, nullptr, PALETTE_INDEX_10, x, y); + + format_readable_speed(textBuffer, sizeof(textBuffer), _bytesOutSec); + gfx_draw_string(dpi, textBuffer, PALETTE_INDEX_10, x + 70, y); + + gfx_draw_string_left(dpi, STR_NETWORK_TOTAL_SENT, nullptr, PALETTE_INDEX_10, x + 200, y); + + format_readable_size(textBuffer, sizeof(textBuffer), _networkStats.bytesSent[NETWORK_STATISTICS_GROUP_TOTAL]); + gfx_draw_string(dpi, textBuffer, PALETTE_INDEX_10, x + 300, y); + y += textHeight + padding; + + window_network_draw_graph(w, dpi, x, y, graphHeight, w->width - (padding * 2), graphBarWidth, false); + y += graphHeight + padding; + } + + // Draw legend + { + for (int i = 1; i < NETWORK_STATISTICS_GROUP_MAX; i++) + { + format_string(textBuffer, sizeof(textBuffer), NetworkTrafficGroupNames[i], nullptr); + + // Draw color stripe. + gfx_fill_rect(dpi, x, y + 4, x + 4, y + 6, NetworkTrafficGroupColors[i]); + + // Draw text. + gfx_draw_string(dpi, textBuffer, PALETTE_INDEX_10, x + 10, y); + + gfx_get_string_width(textBuffer); + + x += gfx_get_string_width(textBuffer) + 20; + } + } + } +} + +#pragma endregion + +static void window_network_draw_tab_image(rct_window* w, rct_drawpixelinfo* dpi, int32_t page, int32_t spriteIndex) +{ + rct_widgetindex widgetIndex = WIDX_TAB1 + page; + + if (!widget_is_disabled(w, widgetIndex)) + { + if (w->page == page) + { + int32_t numFrames = window_network_animation_frames[w->page]; + if (numFrames > 1) + { + int32_t frame = w->frame_no / window_network_animation_divisor[w->page]; + spriteIndex += (frame % numFrames); + } + } + + gfx_draw_sprite(dpi, spriteIndex, w->x + w->widgets[widgetIndex].left, w->y + w->widgets[widgetIndex].top, 0); + } +} + +static void window_network_draw_tab_images(rct_window* w, rct_drawpixelinfo* dpi) +{ + window_network_draw_tab_image(w, dpi, WINDOW_NETWORK_PAGE_INFORMATION, SPR_TAB_KIOSKS_AND_FACILITIES_0); +} diff --git a/src/openrct2-ui/windows/TopToolbar.cpp b/src/openrct2-ui/windows/TopToolbar.cpp index ee694ea80c..5e3c2d92f3 100644 --- a/src/openrct2-ui/windows/TopToolbar.cpp +++ b/src/openrct2-ui/windows/TopToolbar.cpp @@ -138,7 +138,8 @@ enum TOP_TOOLBAR_DEBUG_DDIDX { }; enum TOP_TOOLBAR_NETWORK_DDIDX { - DDIDX_MULTIPLAYER = 0 + DDIDX_MULTIPLAYER = 0, + DDIDX_NETWORK = 1, }; enum { @@ -3238,8 +3239,11 @@ static void top_toolbar_init_network_menu(rct_window* w, rct_widget* widget) { gDropdownItemsFormat[0] = STR_MULTIPLAYER; + gDropdownItemsFormat[DDIDX_MULTIPLAYER] = STR_MULTIPLAYER; + gDropdownItemsFormat[DDIDX_NETWORK] = STR_NETWORK; + window_dropdown_show_text( - w->x + widget->left, w->y + widget->top, widget->bottom - widget->top + 1, w->colours[0] | 0x80, 0, 1); + w->x + widget->left, w->y + widget->top, widget->bottom - widget->top + 1, w->colours[0] | 0x80, 0, 2); gDropdownDefaultIndex = DDIDX_MULTIPLAYER; } @@ -3294,6 +3298,9 @@ static void top_toolbar_network_menu_dropdown(int16_t dropdownIndex) case DDIDX_MULTIPLAYER: context_open_window(WC_MULTIPLAYER); break; + case DDIDX_NETWORK: + context_open_window(WC_NETWORK); + break; } } } diff --git a/src/openrct2-ui/windows/Window.h b/src/openrct2-ui/windows/Window.h index 20382b6ae8..3eefa55d37 100644 --- a/src/openrct2-ui/windows/Window.h +++ b/src/openrct2-ui/windows/Window.h @@ -37,6 +37,7 @@ rct_window* window_land_rights_open(); rct_window* window_main_open(); rct_window* window_mapgen_open(); rct_window* window_multiplayer_open(); +rct_window* window_network_open(); rct_window* window_music_credits_open(); rct_window* window_news_open(); rct_window* window_news_options_open(); diff --git a/src/openrct2/core/CircularBuffer.h b/src/openrct2/core/CircularBuffer.h new file mode 100644 index 0000000000..a46da8e787 --- /dev/null +++ b/src/openrct2/core/CircularBuffer.h @@ -0,0 +1,112 @@ +/***************************************************************************** + * Copyright (c) 2014-2019 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. + *****************************************************************************/ + +#pragma once + +#include + +template class CircularBuffer +{ +public: + typedef _TType value_type; + typedef _TType* pointer; + typedef const _TType* const_pointer; + typedef _TType& reference; + typedef const _TType& const_reference; + typedef size_t size_type; + typedef ptrdiff_t difference_type; + + reference front() + { + return _elements[_head]; + } + + const_reference front() const + { + return _elements[_head]; + } + + reference back() + { + return _elements[_tail]; + } + + const_reference back() const + { + return _elements[_tail]; + } + + reference operator[](size_type idx) + { + idx = (_head + idx) % capacity(); + return _elements[idx]; + } + + const_reference operator[](size_type idx) const + { + idx = (_head + idx) % capacity(); + return _elements[idx]; + } + + void clear() + { + _head = 0; + _tail = 0; + _size = 0; + } + + size_type size() const + { + return _size; + } + + bool empty() const + { + return _size == 0; + } + + constexpr size_type capacity() const + { + return _elements.size(); + } + + void push_back(const value_type& val) + { + if (_size == 0) + { + _elements[_head] = val; + _tail = _head; + _size++; + } + else if (_size != capacity()) + { + _tail++; + if (_tail == capacity()) + _tail = 0; + _size++; + _elements[_tail] = val; + } + else + { + _head++; + if (_head == capacity()) + _head = 0; + _tail++; + if (_tail == capacity()) + _tail = 0; + _elements[_tail] = val; + } + } + +private: + size_t _head = 0; + size_t _tail = 0; + size_t _size = 0; + std::array<_TType, _TMax> _elements; +}; diff --git a/src/openrct2/interface/Window.h b/src/openrct2/interface/Window.h index fe39cd4896..bc109a6920 100644 --- a/src/openrct2/interface/Window.h +++ b/src/openrct2/interface/Window.h @@ -420,6 +420,7 @@ enum WC_DEBUG_PAINT = 130, WC_VIEW_CLIPPING = 131, WC_OBJECT_LOAD_ERROR = 132, + WC_NETWORK = 133, // Only used for colour schemes WC_STAFF = 220, diff --git a/src/openrct2/localisation/Localisation.cpp b/src/openrct2/localisation/Localisation.cpp index d7cc8cbd2b..12b96d9579 100644 --- a/src/openrct2/localisation/Localisation.cpp +++ b/src/openrct2/localisation/Localisation.cpp @@ -1403,6 +1403,36 @@ void format_string_to_upper(utf8* dest, size_t size, rct_string_id format, const dest[upperString.size()] = '\0'; } +void format_readable_size(char* buf, size_t bufSize, uint64_t sizeBytes) +{ + constexpr uint32_t SizeTable[] = { STR_SIZE_BYTE, STR_SIZE_KILOBYTE, STR_SIZE_MEGABYTE, STR_SIZE_GIGABYTE, + STR_SIZE_TERABYTE }; + + double size = sizeBytes; + size_t idx = 0; + while (size >= 1024.0) + { + size /= 1024.0; + idx++; + } + + char sizeType[128] = {}; + format_string(sizeType, sizeof(sizeType), SizeTable[idx], nullptr); + + sprintf(buf, "%.03f %s", size, sizeType); +} + +void format_readable_speed(char* buf, size_t bufSize, uint64_t sizeBytes) +{ + char sizeText[128] = {}; + format_readable_size(sizeText, sizeof(sizeText), sizeBytes); + + const char* args[1] = { + sizeText, + }; + format_string(buf, bufSize, STR_NETWORK_SPEED_SEC, args); +} + money32 string_to_money(const char* string_to_monetise) { const char* decimal_char = language_get_string(STR_LOCALE_DECIMAL_POINT); diff --git a/src/openrct2/localisation/Localisation.h b/src/openrct2/localisation/Localisation.h index 2fdce2f3aa..1a92f4095c 100644 --- a/src/openrct2/localisation/Localisation.h +++ b/src/openrct2/localisation/Localisation.h @@ -29,6 +29,17 @@ void format_string(char* dest, size_t size, rct_string_id format, const void* ar void format_string_raw(char* dest, size_t size, const char* src, const void* args); void format_string_to_upper(char* dest, size_t size, rct_string_id format, const void* args); void generate_string_file(); + +/** + * Formats sizeBytes into buf as a human readable text, e.x.: "1024 MB" + */ +void format_readable_size(char* buf, size_t bufSize, uint64_t sizeBytes); + +/** + * Formats sizeBytesPerSec into buf as a human readable text, e.x.: "1024 MB/sec" + */ +void format_readable_speed(char* buf, size_t bufSize, uint64_t sizeBytesPerSec); + utf8* get_string_end(const utf8* text); size_t get_string_size(const utf8* text); int32_t get_string_length(const utf8* text); diff --git a/src/openrct2/localisation/StringIds.h b/src/openrct2/localisation/StringIds.h index a0b19c556a..573f5a0861 100644 --- a/src/openrct2/localisation/StringIds.h +++ b/src/openrct2/localisation/StringIds.h @@ -3906,6 +3906,27 @@ enum STR_CHAT_BUTTON_ON_TOOLBAR = 6282, STR_CHAT_UNAVAILABLE = 6283, + STR_NETWORK = 6284, + STR_NETWORK_INFORMATION_TITLE = 6285, + + STR_NETWORK_RECEIVE = 6286, + STR_NETWORK_SEND = 6287, + + STR_NETWORK_TOTAL_RECEIVED = 6288, + STR_NETWORK_TOTAL_SENT = 6289, + + STR_NETWORK_LEGEND_BASE = 6290, + STR_NETWORK_LEGEND_COMMANDS = 6291, + STR_NETWORK_LEGEND_MAPDATA = 6292, + + STR_SIZE_BYTE = 6293, + STR_SIZE_KILOBYTE = 6294, + STR_SIZE_MEGABYTE = 6295, + STR_SIZE_GIGABYTE = 6296, + STR_SIZE_TERABYTE = 6297, + + STR_NETWORK_SPEED_SEC = 6298, + // Have to include resource strings (from scenarios and objects) for the time being now that language is partially working STR_COUNT = 32768 }; diff --git a/src/openrct2/network/Network.cpp b/src/openrct2/network/Network.cpp index e450016247..d1f3da8347 100644 --- a/src/openrct2/network/Network.cpp +++ b/src/openrct2/network/Network.cpp @@ -184,6 +184,8 @@ public: void Client_Send_OBJECTS(const std::vector& objects); void Server_Send_OBJECTS(NetworkConnection& connection, const std::vector& objects) const; + NetworkStats_t GetStats() const; + std::vector> player_list; std::vector> group_list; NetworkKey _key; @@ -205,6 +207,7 @@ private: void ProcessPacket(NetworkConnection& connection, NetworkPacket& packet); void AddClient(std::unique_ptr&& socket); void RemoveClient(std::unique_ptr& connection); + NetworkPlayer* AddPlayer(const utf8* name, const std::string& keyhash); std::string MakePlayerNameUnique(const std::string& name); @@ -1396,6 +1399,27 @@ void Network::Server_Send_OBJECTS(NetworkConnection& connection, const std::vect connection.QueuePacket(std::move(packet)); } +NetworkStats_t Network::GetStats() const +{ + NetworkStats_t stats = {}; + if (mode == NETWORK_MODE_CLIENT) + { + stats = _serverConnection->Stats; + } + else + { + for (auto& connection : client_connection_list) + { + for (size_t n = 0; n < NETWORK_STATISTICS_GROUP_MAX; n++) + { + stats.bytesReceived[n] += connection->Stats.bytesReceived[n]; + stats.bytesSent[n] += connection->Stats.bytesSent[n]; + } + } + } + return stats; +} + void Network::Server_Send_AUTH(NetworkConnection& connection) { uint8_t new_playerid = 0; @@ -1412,7 +1436,6 @@ void Network::Server_Send_AUTH(NetworkConnection& connection) connection.QueuePacket(std::move(packet)); if (connection.AuthStatus != NETWORK_AUTH_OK && connection.AuthStatus != NETWORK_AUTH_REQUIREPASSWORD) { - connection.SendQueuedPackets(); connection.Socket->Disconnect(); } } @@ -1538,6 +1561,7 @@ void Network::Client_Send_GAMECMD( std::unique_ptr packet(NetworkPacket::Allocate()); *packet << (uint32_t)NETWORK_COMMAND_GAMECMD << gCurrentTicks << eax << (ebx | GAME_COMMAND_FLAG_NETWORKED) << ecx << edx << esi << edi << ebp << callback; + _serverConnection->QueuePacket(std::move(packet)); } @@ -1569,7 +1593,6 @@ void Network::Client_Send_GAME_ACTION(const GameAction* action) action->Serialise(stream); *packet << (uint32_t)NETWORK_COMMAND_GAME_ACTION << gCurrentTicks << action->GetType() << stream; - _serverConnection->QueuePacket(std::move(packet)); } @@ -1616,6 +1639,7 @@ void Network::Server_Send_TICK() rct_sprite_checksum checksum = sprite_checksum(); packet->WriteString(checksum.ToString().c_str()); } + SendPacketToClients(*packet); } @@ -1666,7 +1690,6 @@ void Network::Server_Send_SETDISCONNECTMSG(NetworkConnection& connection, const *packet << (uint32_t)NETWORK_COMMAND_SETDISCONNECTMSG; packet->WriteString(msg); connection.QueuePacket(std::move(packet)); - connection.SendQueuedPackets(); } void Network::Server_Send_GAMEINFO(NetworkConnection& connection) @@ -3853,6 +3876,11 @@ std::string network_get_version() return NETWORK_STREAM_ID; } +NetworkStats_t network_get_stats() +{ + return gNetwork.GetStats(); +} + #else int32_t network_get_mode() { @@ -4085,4 +4113,8 @@ std::string network_get_version() { return "Multiplayer disabled"; } +NetworkStats_t network_get_stats() +{ + return NetworkStats_t{}; +} #endif /* DISABLE_NETWORK */ diff --git a/src/openrct2/network/NetworkConnection.cpp b/src/openrct2/network/NetworkConnection.cpp index 2d4043a1aa..4236cd1c14 100644 --- a/src/openrct2/network/NetworkConnection.cpp +++ b/src/openrct2/network/NetworkConnection.cpp @@ -73,6 +73,9 @@ int32_t NetworkConnection::ReadPacket() if (InboundPacket.BytesTransferred == sizeof(InboundPacket.Size) + InboundPacket.Size) { _lastPacketTime = platform_get_ticks(); + + RecordPacketStats(InboundPacket, false); + return NETWORK_READPACKET_SUCCESS; } } @@ -94,7 +97,13 @@ bool NetworkConnection::SendPacket(NetworkPacket& packet) { packet.BytesTransferred += sent; } - return packet.BytesTransferred == tosend.size(); + + bool sendComplete = packet.BytesTransferred == tosend.size(); + if (sendComplete) + { + RecordPacketStats(packet, true); + } + return sendComplete; } void NetworkConnection::QueuePacket(std::unique_ptr packet, bool front) @@ -175,4 +184,32 @@ void NetworkConnection::SetLastDisconnectReason(const rct_string_id string_id, v SetLastDisconnectReason(buffer); } +void NetworkConnection::RecordPacketStats(const NetworkPacket& packet, bool sending) +{ + uint32_t packetSize = (uint32_t)packet.BytesTransferred; + uint32_t trafficGroup = NETWORK_STATISTICS_GROUP_BASE; + + switch (packet.GetCommand()) + { + case NETWORK_COMMAND_GAMECMD: + case NETWORK_COMMAND_GAME_ACTION: + trafficGroup = NETWORK_STATISTICS_GROUP_COMMANDS; + break; + case NETWORK_COMMAND_MAP: + trafficGroup = NETWORK_STATISTICS_GROUP_MAPDATA; + break; + } + + if (sending) + { + Stats.bytesSent[trafficGroup] += packetSize; + Stats.bytesSent[NETWORK_STATISTICS_GROUP_TOTAL] += packetSize; + } + else + { + Stats.bytesReceived[trafficGroup] += packetSize; + Stats.bytesReceived[NETWORK_STATISTICS_GROUP_TOTAL] += packetSize; + } +} + #endif diff --git a/src/openrct2/network/NetworkConnection.h b/src/openrct2/network/NetworkConnection.h index 1eacca3c66..473c0c3c67 100644 --- a/src/openrct2/network/NetworkConnection.h +++ b/src/openrct2/network/NetworkConnection.h @@ -29,6 +29,7 @@ public: std::unique_ptr Socket = nullptr; NetworkPacket InboundPacket; NETWORK_AUTH AuthStatus = NETWORK_AUTH_NONE; + NetworkStats_t Stats = {}; NetworkPlayer* Player = nullptr; uint32_t PingTime = 0; NetworkKey Key; @@ -53,6 +54,7 @@ private: uint32_t _lastPacketTime = 0; utf8* _lastDisconnectReason = nullptr; + void RecordPacketStats(const NetworkPacket& packet, bool sending); bool SendPacket(NetworkPacket& packet); }; diff --git a/src/openrct2/network/NetworkPacket.cpp b/src/openrct2/network/NetworkPacket.cpp index 11a53d7ab3..90a3de78a6 100644 --- a/src/openrct2/network/NetworkPacket.cpp +++ b/src/openrct2/network/NetworkPacket.cpp @@ -30,7 +30,7 @@ uint8_t* NetworkPacket::GetData() return &(*Data)[0]; } -int32_t NetworkPacket::GetCommand() +int32_t NetworkPacket::GetCommand() const { if (Data->size() >= sizeof(uint32_t)) { diff --git a/src/openrct2/network/NetworkPacket.h b/src/openrct2/network/NetworkPacket.h index 5efc0f8a26..59aacdbf1f 100644 --- a/src/openrct2/network/NetworkPacket.h +++ b/src/openrct2/network/NetworkPacket.h @@ -28,7 +28,7 @@ public: static std::unique_ptr Duplicate(NetworkPacket& packet); uint8_t* GetData(); - int32_t GetCommand(); + int32_t GetCommand() const; void Clear(); bool CommandRequiresAuth(); diff --git a/src/openrct2/network/NetworkTypes.h b/src/openrct2/network/NetworkTypes.h index 32e3489895..e48a3b6059 100644 --- a/src/openrct2/network/NetworkTypes.h +++ b/src/openrct2/network/NetworkTypes.h @@ -95,3 +95,18 @@ template struct NetworkObjectId_t // there is no way to specialize templates if they have the exact symbol. using NetworkPlayerId_t = NetworkObjectId_t; using NetworkRideId_t = NetworkObjectId_t; + +enum NetworkStatisticsGroup +{ + NETWORK_STATISTICS_GROUP_TOTAL = 0, // Entire network traffic. + NETWORK_STATISTICS_GROUP_BASE, // Messages such as Tick, Ping + NETWORK_STATISTICS_GROUP_COMMANDS, // Command / Game actions + NETWORK_STATISTICS_GROUP_MAPDATA, + NETWORK_STATISTICS_GROUP_MAX, +}; + +struct NetworkStats_t +{ + uint64_t bytesReceived[NETWORK_STATISTICS_GROUP_MAX]; + uint64_t bytesSent[NETWORK_STATISTICS_GROUP_MAX]; +}; diff --git a/src/openrct2/network/network.h b/src/openrct2/network/network.h index 70886153e8..efd94d33f5 100644 --- a/src/openrct2/network/network.h +++ b/src/openrct2/network/network.h @@ -101,3 +101,5 @@ const utf8* network_get_server_provider_email(); const utf8* network_get_server_provider_website(); std::string network_get_version(); + +NetworkStats_t network_get_stats(); diff --git a/src/openrct2/util/Util.cpp b/src/openrct2/util/Util.cpp index 500b83e0b5..027fd25bbb 100644 --- a/src/openrct2/util/Util.cpp +++ b/src/openrct2/util/Util.cpp @@ -743,16 +743,9 @@ uint8_t lerp(uint8_t a, uint8_t b, float t) return (uint8_t)(a + amount); } -float flerp(float a, float b, float t) +float flerp(float a, float b, float f) { - if (t <= 0) - return a; - if (t >= 1) - return b; - - float range = b - a; - float amount = range * t; - return a + amount; + return (a * (1.0f - f)) + (b * f); } uint8_t soft_light(uint8_t a, uint8_t b) diff --git a/test/tests/CircularBuffer.cpp b/test/tests/CircularBuffer.cpp new file mode 100644 index 0000000000..c8385bfcb2 --- /dev/null +++ b/test/tests/CircularBuffer.cpp @@ -0,0 +1,67 @@ +/***************************************************************************** + * Copyright (c) 2014-2019 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. + *****************************************************************************/ +#include +#include +#include +#include + +// CircularBuffer capacity. +constexpr size_t TEST_BUFFER_SIZE = 128; + +// Amount of elements to push into buffer, will overwrite entire buffer 8 times. +constexpr size_t TEST_PUSH_COUNT = 1024; + +TEST(CircularBufferTest, all) +{ + CircularBuffer buffer1; + ASSERT_EQ(buffer1.capacity(), TEST_BUFFER_SIZE); + + // Mirror the effects of buffer1 into buffer2 to compare. + std::vector buffer2; + + // Create + { + for (size_t i = 0; i < TEST_PUSH_COUNT; i++) + { + buffer1.push_back(i); + buffer2.push_back(i); + + // Mimic sliding window, always remove oldest element. + if (buffer2.size() > buffer1.capacity()) + { + buffer2.erase(buffer2.begin()); + } + + ASSERT_EQ(buffer1.size(), buffer2.size()); + } + } + + ASSERT_EQ(buffer1.empty(), false); + ASSERT_EQ(buffer1.size(), buffer2.size()); + + // Compare contents. + { + ASSERT_EQ(buffer1.front(), buffer1[0]); + ASSERT_EQ(buffer1.back(), buffer1[buffer1.size() - 1]); + + for (size_t i = 0; i < buffer1.size(); i++) + { + ASSERT_EQ(buffer1[i], buffer2[i]); + } + } + + // Clear + { + buffer1.clear(); + ASSERT_EQ(buffer1.empty(), true); + ASSERT_EQ(buffer1.size(), size_t(0)); + } + + SUCCEED(); +} diff --git a/test/tests/tests.vcxproj b/test/tests/tests.vcxproj index adddb17b74..d7fd9aaff5 100644 --- a/test/tests/tests.vcxproj +++ b/test/tests/tests.vcxproj @@ -56,6 +56,7 @@ +