diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index 6fafe504f6..65b999d09f 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3635,6 +3635,10 @@ STR_6376 :{WINDOW_COLOUR_2}Ride vehicle:{NEWLINE}{BLACK}{STRINGID} for {STRIN STR_6377 :{WINDOW_COLOUR_2}Type: {BLACK}{STRINGID} for {STRINGID} STR_6378 :Receiving objects list: {INT32} / {INT32} STR_6379 :Received invalid data +STR_6380 :Update available! +STR_6381 :Join OpenRCT2 Discord! +STR_6382 :Newer release of OpenRCT2 is available: {STRING}! +STR_6383 :Open download page ############# # Scenarios # diff --git a/src/openrct2-ui/WindowManager.cpp b/src/openrct2-ui/WindowManager.cpp index 11391f891e..4b347880b1 100644 --- a/src/openrct2-ui/WindowManager.cpp +++ b/src/openrct2-ui/WindowManager.cpp @@ -135,6 +135,8 @@ public: return window_water_open(); case WC_NETWORK: return window_network_open(); + case WC_NEW_VERSION: + return window_new_version_open(); default: Console::Error::WriteLine("Unhandled window class (%d)", wc); return nullptr; diff --git a/src/openrct2-ui/windows/About.cpp b/src/openrct2-ui/windows/About.cpp index 37096c97c2..c143d2e6ce 100644 --- a/src/openrct2-ui/windows/About.cpp +++ b/src/openrct2-ui/windows/About.cpp @@ -15,9 +15,10 @@ #include #include #include +#include static constexpr const int32_t WW = 400; -static constexpr const int32_t WH = 350; +static constexpr const int32_t WH = 385; static constexpr const rct_string_id WINDOW_TITLE = STR_ABOUT; constexpr int32_t TABHEIGHT = 50; @@ -40,6 +41,8 @@ enum WINDOW_ABOUT_WIDGET_IDX { // About OpenRCT2 WIDX_CHANGELOG = WIDX_PAGE_START, + WIDX_JOIN_DISCORD, + WIDX_NEW_VERSION, // About RCT2 WIDX_MUSIC_CREDITS = WIDX_PAGE_START, @@ -53,7 +56,9 @@ enum WINDOW_ABOUT_WIDGET_IDX { static rct_widget window_about_openrct2_widgets[] = { WIDGETS_MAIN, - MakeWidget({100, WH - TABHEIGHT}, {200, 14}, WWT_BUTTON, 1, STR_CHANGELOG_ELLIPSIS), // changelog button + MakeWidget({100, WH - TABHEIGHT - (14 + 3) * 2}, {200, 14}, WWT_BUTTON, 1, STR_CHANGELOG_ELLIPSIS), // changelog button + MakeWidget({100, WH - TABHEIGHT - (14 + 3) * 1}, {200, 14}, WWT_BUTTON, 1, STR_JOIN_DISCORD), // "join discord" button + MakeWidget({100, WH - TABHEIGHT - (14 + 3) * 0}, {200, 14}, WWT_PLACEHOLDER, 1, STR_UPDATE_AVAILABLE), // "new version" button { WIDGETS_END } }; @@ -72,12 +77,13 @@ static rct_widget *window_about_page_widgets[] = { (1ULL << WIDX_CLOSE) | (1ULL << WIDX_TAB_ABOUT_OPENRCT2) | (1ULL << WIDX_TAB_ABOUT_RCT2) static uint64_t window_about_page_enabled_widgets[] = { - DEFAULT_ENABLED_WIDGETS | (1ULL << WIDX_CHANGELOG), + DEFAULT_ENABLED_WIDGETS | (1ULL << WIDX_CHANGELOG) | (1 << WIDX_JOIN_DISCORD), DEFAULT_ENABLED_WIDGETS | (1ULL << WIDX_MUSIC_CREDITS), }; static void window_about_openrct2_mouseup(rct_window *w, rct_widgetindex widgetIndex); static void window_about_openrct2_paint(rct_window *w, rct_drawpixelinfo *dpi); +static void window_about_openrct2_invalidate(rct_window *w); static void window_about_rct2_mouseup(rct_window *w, rct_widgetindex widgetIndex); static void window_about_rct2_paint(rct_window *w, rct_drawpixelinfo *dpi); @@ -109,7 +115,7 @@ static rct_window_event_list window_about_openrct2_events = { nullptr, nullptr, nullptr, - nullptr, + window_about_openrct2_invalidate, window_about_openrct2_paint, nullptr }; @@ -191,9 +197,15 @@ static void window_about_openrct2_mouseup(rct_window* w, rct_widgetindex widgetI case WIDX_TAB_ABOUT_RCT2: window_about_set_page(w, widgetIndex - WIDX_TAB_ABOUT_OPENRCT2); break; + case WIDX_JOIN_DISCORD: + OpenRCT2::GetContext()->GetUiContext()->OpenURL("https://discord.gg/ZXZd8D8"); + break; case WIDX_CHANGELOG: context_open_window(WC_CHANGELOG); break; + case WIDX_NEW_VERSION: + context_open_window(WC_NEW_VERSION); + break; } } @@ -261,6 +273,16 @@ static void window_about_openrct2_paint(rct_window* w, rct_drawpixelinfo* dpi) gfx_draw_string_centred_wrapped(dpi, &ch, aboutCoords, width, STR_STRING, w->colours[2]); } +static void window_about_openrct2_invalidate(rct_window* w) +{ + if (w->page == WINDOW_ABOUT_PAGE_OPENRCT2 && OpenRCT2::GetContext()->HasNewVersionInfo()) + { + w->enabled_widgets |= (1ULL << WIDX_NEW_VERSION); + w->widgets[WIDX_NEW_VERSION].type = WWT_BUTTON; + window_about_openrct2_widgets[WIDX_NEW_VERSION].type = WWT_BUTTON; + } +} + #pragma endregion OpenRCT2 #pragma region RCT2 diff --git a/src/openrct2-ui/windows/NewVersionInfo.cpp b/src/openrct2-ui/windows/NewVersionInfo.cpp new file mode 100644 index 0000000000..82b0f70fe0 --- /dev/null +++ b/src/openrct2-ui/windows/NewVersionInfo.cpp @@ -0,0 +1,273 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace OpenRCT2; + +// clang-format off +enum { + WIDX_BACKGROUND, + WIDX_TITLE, + WIDX_CLOSE, + WIDX_CONTENT_PANEL, + WIDX_SCROLL, + WIDX_OPEN_URL, +}; + +static constexpr const int32_t WW = 500; +static constexpr const int32_t WH = 400; +static constexpr const rct_string_id WINDOW_TITLE = STR_CHANGELOG_TITLE; +constexpr int32_t MIN_WW = 300; +constexpr int32_t MIN_WH = 250; + +static rct_widget window_new_version_widgets[] = { + WINDOW_SHIM(WINDOW_TITLE, WW, WH), + MakeWidget({0, 14}, {500, 382}, WWT_RESIZE, 1 ), // content panel + MakeWidget({3, 16}, {495, 366}, WWT_SCROLL, 1, SCROLL_BOTH), // scroll area + MakeWidget({3, 473}, {300, 14}, WWT_BUTTON, 1, STR_NEW_RELEASE_DOWNLOAD_PAGE), // changelog button + { WIDGETS_END }, +}; + +static void window_new_version_close(rct_window *w); +static void window_new_version_mouseup(rct_window *w, rct_widgetindex widgetIndex); +static void window_new_version_resize(rct_window *w); +static void window_new_version_scrollgetsize(rct_window *w, int32_t scrollIndex, int32_t *width, int32_t *height); +static void window_new_version_invalidate(rct_window *w); +static void window_new_version_paint(rct_window *w, rct_drawpixelinfo *dpi); +static void window_new_version_scrollpaint(rct_window *w, rct_drawpixelinfo *dpi, int32_t scrollIndex); + +static rct_window_event_list window_new_version_events = { + window_new_version_close, + window_new_version_mouseup, + window_new_version_resize, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + window_new_version_scrollgetsize, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + window_new_version_invalidate, + window_new_version_paint, + window_new_version_scrollpaint +}; +// clang-format on + +static void window_new_version_process_info(); +static void window_new_version_dispose_data(); + +static const NewVersionInfo* _newVersionInfo; +static std::vector _changelogLines; +static int32_t _changelogLongestLineWidth = 0; + +rct_window* window_new_version_open() +{ + rct_window* window; + + window = window_bring_to_front_by_class(WC_NEW_VERSION); + if (window != nullptr) + { + return window; + } + + if (!GetContext()->HasNewVersionInfo()) + { + return nullptr; + } + + window_new_version_process_info(); + + int32_t screenWidth = context_get_width(); + int32_t screenHeight = context_get_height(); + + window = window_create_centred( + screenWidth * 4 / 5, screenHeight * 4 / 5, &window_new_version_events, WC_NEW_VERSION, WF_RESIZABLE); + window->widgets = window_new_version_widgets; + window->enabled_widgets = (1 << WIDX_CLOSE) | (1 << WIDX_OPEN_URL); + + window_init_scroll_widgets(window); + window->min_width = MIN_WW; + window->min_height = MIN_WH; + window->max_width = MIN_WW; + window->max_height = MIN_WH; + + window->colours[0] = COLOUR_GREY; + window->colours[1] = COLOUR_LIGHT_BLUE; + window->colours[2] = COLOUR_LIGHT_BLUE; + + return window; +} + +static void window_new_version_close([[maybe_unused]] rct_window* w) +{ + window_new_version_dispose_data(); +} + +static void window_new_version_mouseup(rct_window* w, rct_widgetindex widgetIndex) +{ + switch (widgetIndex) + { + case WIDX_CLOSE: + window_close(w); + break; + case WIDX_OPEN_URL: + GetContext()->GetUiContext()->OpenURL(_newVersionInfo->url); + break; + } +} + +static void window_new_version_resize(rct_window* w) +{ + int32_t screenWidth = context_get_width(); + int32_t screenHeight = context_get_height(); + + w->max_width = (screenWidth * 4) / 5; + w->max_height = (screenHeight * 4) / 5; + + w->min_width = MIN_WW; + w->min_height = MIN_WH; + + auto download_button_width = window_new_version_widgets[WIDX_OPEN_URL].width(); + window_new_version_widgets[WIDX_OPEN_URL].left = (w->width - download_button_width) / 2; + window_new_version_widgets[WIDX_OPEN_URL].right = window_new_version_widgets[WIDX_OPEN_URL].left + download_button_width; + + if (w->width < w->min_width) + { + w->Invalidate(); + w->width = w->min_width; + } + if (w->height < w->min_height) + { + w->Invalidate(); + w->height = w->min_height; + } +} + +static void window_new_version_scrollgetsize( + [[maybe_unused]] rct_window* w, [[maybe_unused]] int32_t scrollIndex, int32_t* width, int32_t* height) +{ + *width = _changelogLongestLineWidth + 4; + + const int32_t lineHeight = font_get_line_height(gCurrentFontSpriteBase); + *height = static_cast(_changelogLines.size() * lineHeight); +} + +static void window_new_version_invalidate(rct_window* w) +{ + window_new_version_widgets[WIDX_BACKGROUND].right = w->width - 1; + window_new_version_widgets[WIDX_BACKGROUND].bottom = w->height - 1; + window_new_version_widgets[WIDX_TITLE].right = w->width - 2; + window_new_version_widgets[WIDX_CLOSE].left = w->width - 13; + window_new_version_widgets[WIDX_CLOSE].right = w->width - 3; + window_new_version_widgets[WIDX_CONTENT_PANEL].right = w->width - 1; + window_new_version_widgets[WIDX_CONTENT_PANEL].bottom = w->height - 1; + window_new_version_widgets[WIDX_SCROLL].right = w->width - 3; + window_new_version_widgets[WIDX_SCROLL].bottom = w->height - 22; + window_new_version_widgets[WIDX_OPEN_URL].bottom = w->height - 5; + window_new_version_widgets[WIDX_OPEN_URL].top = w->height - 19; +} + +static void window_new_version_paint(rct_window* w, rct_drawpixelinfo* dpi) +{ + window_draw_widgets(w, dpi); +} + +static void window_new_version_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, [[maybe_unused]] int32_t scrollIndex) +{ + gCurrentFontFlags = 0; + gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM; + + const int32_t lineHeight = font_get_line_height(gCurrentFontSpriteBase); + + ScreenCoordsXY screenCoords(3, 3 - lineHeight); + for (auto line : _changelogLines) + { + screenCoords.y += lineHeight; + if (screenCoords.y + lineHeight < dpi->y || screenCoords.y >= dpi->y + dpi->height) + continue; + + gfx_draw_string(dpi, line.c_str(), w->colours[0], screenCoords); + } +} + +static void window_new_version_process_info() +{ + _newVersionInfo = GetContext()->GetNewVersionInfo(); + + std::string::size_type pos = 0; + std::string::size_type prev = 0; + char version_info[256]; + + const char* version_info_ptr = _newVersionInfo->name.c_str(); + format_string(version_info, 256, STR_NEW_RELEASE_VERSION_INFO, &version_info_ptr); + + _changelogLines.push_back(version_info); + _changelogLines.push_back(""); + + while ((pos = _newVersionInfo->changelog.find("\n", prev)) != std::string::npos) + { + std::string line = _newVersionInfo->changelog.substr(prev, pos - prev); + for (char* ch = line.data(); *ch != '\0'; ch++) + { + if (utf8_is_format_code(*ch)) + { + *ch = FORMAT_OUTLINE_OFF; + } + } + _changelogLines.push_back(line); + prev = pos + 1; + } + + // To get the last substring (or only, if delimiter is not found) + _changelogLines.push_back(_newVersionInfo->changelog.substr(prev)); + + gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM; + _changelogLongestLineWidth = 0; + for (auto line : _changelogLines) + { + auto width = gfx_get_string_width(line.c_str()); + _changelogLongestLineWidth = std::max(width, _changelogLongestLineWidth); + } +} + +static void window_new_version_dispose_data() +{ + _changelogLines.clear(); + _changelogLines.shrink_to_fit(); +} diff --git a/src/openrct2-ui/windows/TitleMenu.cpp b/src/openrct2-ui/windows/TitleMenu.cpp index 561841220e..4702e975bf 100644 --- a/src/openrct2-ui/windows/TitleMenu.cpp +++ b/src/openrct2-ui/windows/TitleMenu.cpp @@ -209,11 +209,13 @@ static void window_title_menu_dropdown(rct_window* w, rct_widgetindex widgetInde Editor::LoadTrackManager(); break; case 4: + { auto context = OpenRCT2::GetContext(); auto env = context->GetPlatformEnvironment(); auto uiContext = context->GetUiContext(); uiContext->OpenFolder(env->GetDirectoryPath(OpenRCT2::DIRBASE::USER)); break; + } } } } diff --git a/src/openrct2-ui/windows/Window.h b/src/openrct2-ui/windows/Window.h index 66e45b12af..15055961e9 100644 --- a/src/openrct2-ui/windows/Window.h +++ b/src/openrct2-ui/windows/Window.h @@ -37,6 +37,7 @@ extern bool gWindowSceneryEyedropperEnabled; rct_window* window_about_open(); void WindowCampaignRefreshRides(); rct_window* window_changelog_open(); +rct_window* window_new_version_open(); rct_window* window_cheats_open(); rct_window* window_clear_scenery_open(); rct_window* custom_currency_window_open(); diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 03c4ed9cc4..ab6d2d7b7a 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,10 @@ namespace OpenRCT2 // false. bool _finished = false; + std::future _versionCheckFuture; + NewVersionInfo _newVersionInfo; + bool _hasNewVersionInfo = false; + public: // Singleton of Context. // Remove this when GetContext() is no longer called so that @@ -334,6 +339,17 @@ namespace OpenRCT2 crash_init(); + if (!_versionCheckFuture.valid()) + { + _versionCheckFuture = std::async(std::launch::async, [this] { + _newVersionInfo = get_latest_version(); + if (!String::StartsWith(gVersionInfoTag, _newVersionInfo.tag)) + { + _hasNewVersionInfo = true; + } + }); + } + if (gConfigGeneral.last_run_version != nullptr && String::Equals(gConfigGeneral.last_run_version, OPENRCT2_VERSION)) { gOpenRCT2ShowChangelog = false; @@ -1154,6 +1170,16 @@ namespace OpenRCT2 return parkData; } #endif + + bool HasNewVersionInfo() const override + { + return _hasNewVersionInfo; + } + + const NewVersionInfo* GetNewVersionInfo() const override + { + return &_newVersionInfo; + } }; Context* Context::Instance = nullptr; diff --git a/src/openrct2/Context.h b/src/openrct2/Context.h index 9111642757..d575ef4b12 100644 --- a/src/openrct2/Context.h +++ b/src/openrct2/Context.h @@ -28,6 +28,7 @@ struct IGameStateSnapshots; class Intent; struct rct_window; using rct_windowclass = uint8_t; +struct NewVersionInfo; struct CursorState { @@ -140,6 +141,8 @@ namespace OpenRCT2 virtual void Finish() abstract; virtual void Quit() abstract; + virtual bool HasNewVersionInfo() const abstract; + virtual const NewVersionInfo* GetNewVersionInfo() const abstract; /** * This is deprecated, use IPlatformEnvironment. */ diff --git a/src/openrct2/Version.cpp b/src/openrct2/Version.cpp index 216befbd1a..3311a1e6c1 100644 --- a/src/openrct2/Version.cpp +++ b/src/openrct2/Version.cpp @@ -9,12 +9,25 @@ #include "Version.h" -#include +#include "config/Config.h" +#include "core/Console.hpp" +#include "core/Http.h" +#include "core/Json.hpp" + +#include #ifdef OPENRCT2_BUILD_INFO_HEADER # include OPENRCT2_BUILD_INFO_HEADER #endif +const char gVersionInfoTag[] = +#ifdef OPENRCT2_VERSION_TAG + OPENRCT2_VERSION_TAG +#else + "v" OPENRCT2_VERSION +#endif + ; + const char gVersionInfoFull[] = OPENRCT2_NAME ", " #ifdef OPENRCT2_VERSION_TAG OPENRCT2_VERSION_TAG @@ -39,3 +52,57 @@ const char gVersionInfoFull[] = OPENRCT2_NAME ", " " provided by " OPENRCT2_BUILD_SERVER #endif ; + +NewVersionInfo get_latest_version() +{ + // If the check doesn't succeed, provide current version so we don't bother user + // with invalid data. + std::string tag = gVersionInfoTag; + NewVersionInfo verinfo{ tag, "", "", "" }; +#ifndef DISABLE_HTTP + auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto then = gConfigGeneral.last_version_check_time; + if (then < now - 24 * 60 * 60) + { + Http::Request request; + request.url = "https://api.github.com/repos/OpenRCT2/OpenRCT2/releases/latest"; + request.method = Http::Method::GET; + + Http::Response res; + try + { + res = Do(request); + if (res.status != Http::Status::OK) + throw std::runtime_error("bad http status"); + } + catch (std::exception& e) + { + Console::Error::WriteLine("Failed to download '%s', cause %s", request.url.c_str(), e.what()); + return {}; + } + + json_t* root = Json::FromString(res.body); + + auto get_as_string = [root](std::string name) { + std::string value; + json_t* json_value = json_object_get(root, name.c_str()); + if (json_is_string(json_value)) + { + value = (json_string_value(json_value)); + } + return value; + }; + + verinfo.tag = get_as_string("tag_name"); + verinfo.name = get_as_string("name"); + verinfo.changelog = get_as_string("body"); + verinfo.url = get_as_string("html_url"); + + json_decref(root); + + gConfigGeneral.last_version_check_time = now; + config_save_default(); + } +#endif + return verinfo; +} diff --git a/src/openrct2/Version.h b/src/openrct2/Version.h index 23b213d3ef..2a7cd559bb 100644 --- a/src/openrct2/Version.h +++ b/src/openrct2/Version.h @@ -11,6 +11,8 @@ #include "common.h" +#include + #define OPENRCT2_NAME "OpenRCT2" #define OPENRCT2_VERSION "0.2.6" @@ -77,3 +79,13 @@ #endif extern const char gVersionInfoFull[]; +extern const char gVersionInfoTag[]; +struct NewVersionInfo +{ + std::string tag; + std::string name; + std::string changelog; + std::string url; +}; + +NewVersionInfo get_latest_version(); diff --git a/src/openrct2/config/Config.cpp b/src/openrct2/config/Config.cpp index 871f215215..198611764c 100644 --- a/src/openrct2/config/Config.cpp +++ b/src/openrct2/config/Config.cpp @@ -213,6 +213,7 @@ namespace Config model->show_real_names_of_guests = reader->GetBoolean("show_real_names_of_guests", true); model->allow_early_completion = reader->GetBoolean("allow_early_completion", false); model->transparent_screenshot = reader->GetBoolean("transparent_screenshot", true); + model->last_version_check_time = reader->GetInt64("last_version_check_time", 0); } } @@ -288,6 +289,7 @@ namespace Config writer->WriteBoolean("allow_early_completion", model->allow_early_completion); writer->WriteEnum("virtual_floor_style", model->virtual_floor_style, Enum_VirtualFloorStyle); writer->WriteBoolean("transparent_screenshot", model->transparent_screenshot); + writer->WriteInt64("last_version_check_time", model->last_version_check_time); } static void ReadInterface(IIniReader* reader) diff --git a/src/openrct2/config/Config.h b/src/openrct2/config/Config.h index 0aaa994ab0..23061b144d 100644 --- a/src/openrct2/config/Config.h +++ b/src/openrct2/config/Config.h @@ -101,6 +101,7 @@ struct GeneralConfiguration utf8* last_save_track_directory; utf8* last_run_version; bool use_native_browse_dialog; + int64_t last_version_check_time; }; struct InterfaceConfiguration diff --git a/src/openrct2/interface/Window.h b/src/openrct2/interface/Window.h index 02d2a2ed80..eb0049bdac 100644 --- a/src/openrct2/interface/Window.h +++ b/src/openrct2/interface/Window.h @@ -482,6 +482,7 @@ enum WC_VIEW_CLIPPING = 131, WC_OBJECT_LOAD_ERROR = 132, WC_NETWORK = 133, + WC_NEW_VERSION = 134, // Only used for colour schemes WC_STAFF = 220, diff --git a/src/openrct2/localisation/StringIds.h b/src/openrct2/localisation/StringIds.h index 3b3411c299..abd7e8da77 100644 --- a/src/openrct2/localisation/StringIds.h +++ b/src/openrct2/localisation/StringIds.h @@ -3878,6 +3878,11 @@ enum STR_MULTIPLAYER_RECEIVED_INVALID_DATA = 6379, + STR_UPDATE_AVAILABLE = 6380, + STR_JOIN_DISCORD = 6381, + STR_NEW_RELEASE_VERSION_INFO = 6382, + STR_NEW_RELEASE_DOWNLOAD_PAGE = 6383, + // Have to include resource strings (from scenarios and objects) for the time being now that language is partially working /* MAX_STR_COUNT = 32768 */ // MAX_STR_COUNT - upper limit for number of strings, not the current count strings };