diff --git a/gui/GameUpdateChecker.cpp b/gui/GameUpdateChecker.cpp index cd1100c..3d06e2f 100644 --- a/gui/GameUpdateChecker.cpp +++ b/gui/GameUpdateChecker.cpp @@ -1,19 +1,19 @@ #include "GameUpdateChecker.h" #ifndef LOG_SECTION -#define LOG_SECTION "UpdateChecker" +#define LOG_SECTION "GameUpdateChecker" #endif #include "../Logger.h" -GameUpdateChecker::GameUpdateChecker(fs::path cachePath, UpdateCallback callback, std::chrono::milliseconds checkInterval) : +GameUpdateChecker::GameUpdateChecker(fs::path cachePath, GameUpdateCallback callback, std::chrono::milliseconds checkInterval) : Auth(cachePath), Callback(callback), CheckInterval(checkInterval) { LOG_DEBUG("Initializing force update"); ForceUpdate(); - LOG_DEBUG("Creating update thread"); + LOG_DEBUG("Creating game update thread"); UpdateThread = std::thread(&GameUpdateChecker::Thread, this); UpdateThread.detach(); // causes some exception when the deconstructer is trying to join it otherwise } diff --git a/gui/GameUpdateChecker.h b/gui/GameUpdateChecker.h index 6021107..7cc90f1 100644 --- a/gui/GameUpdateChecker.h +++ b/gui/GameUpdateChecker.h @@ -9,11 +9,11 @@ namespace fs = std::filesystem; -typedef std::function UpdateCallback; +typedef std::function GameUpdateCallback; class GameUpdateChecker { public: - GameUpdateChecker(fs::path cachePath, UpdateCallback callback, std::chrono::milliseconds checkInterval); + GameUpdateChecker(fs::path cachePath, GameUpdateCallback callback, std::chrono::milliseconds checkInterval); ~GameUpdateChecker(); void SetInterval(std::chrono::milliseconds newInterval); @@ -49,7 +49,7 @@ class GameUpdateChecker { std::string LatestUrl; std::string LatestVersion; - UpdateCallback Callback; + GameUpdateCallback Callback; std::thread UpdateThread; std::chrono::steady_clock::time_point UpdateWakeup; cancel_flag UpdateFlag; diff --git a/gui/UpdateChecker.cpp b/gui/UpdateChecker.cpp new file mode 100644 index 0000000..6d54fb1 --- /dev/null +++ b/gui/UpdateChecker.cpp @@ -0,0 +1,94 @@ +#include "UpdateChecker.h" + +#define GH_REPO "WorkingRobot/EGL2" +#define GH_RELEASE_URL "https://api.github.com/repos/" GH_REPO "/releases/latest" + +#ifndef LOG_SECTION +#define LOG_SECTION "UpdateChecker" +#endif + +#include "../Logger.h" +#include "../web/http.h" +#include "versioninfo.h" + +#include + +UpdateChecker::UpdateChecker(UpdateCallback callback, std::chrono::milliseconds checkInterval) : + Callback(callback), + CheckInterval(checkInterval), + LatestVersion(VERSION_STRING) +{ + LOG_DEBUG("Creating update thread"); + UpdateThread = std::thread(&UpdateChecker::Thread, this); + UpdateThread.detach(); // causes some exception when the deconstructer is trying to join it otherwise +} + +UpdateChecker::~UpdateChecker() { + UpdateFlag.cancel(); +} + +void UpdateChecker::SetInterval(std::chrono::milliseconds newInterval) +{ + CheckInterval.store(newInterval); + UpdateWakeup = std::chrono::steady_clock::time_point::min(); // force update +} + +void UpdateChecker::StopUpdateThread() +{ + UpdateFlag.cancel(); +} + +bool UpdateChecker::ForceUpdate() { + auto conn = Client::CreateConnection(); + conn->SetUrl(GH_RELEASE_URL); + conn->SetUserAgent(GH_REPO); + + if (!Client::Execute(conn, cancel_flag(), true)) { + LOG_WARN("Could not check for EGL2 update"); + return false; + } + + if (conn->GetResponseCode() != 200) { + return false; + } + + rapidjson::Document releaseInfo; + releaseInfo.Parse(conn->GetResponseBody().c_str()); + + if (releaseInfo.HasParseError()) { + LOG_ERROR("Getting release info: JSON Parse Error %d @ %zu", releaseInfo.GetParseError(), releaseInfo.GetErrorOffset()); + return false; + } + + auto version = releaseInfo["tag_name"].GetString(); + if (LatestVersion == version) { + LOG_DEBUG("NO EGL2 UPDATE"); + return false; + } + + LatestInfo.Version = version; + LatestInfo.Url = releaseInfo["html_url"].GetString(); + + auto& exeAsset = releaseInfo["assets"][0]; + LatestInfo.DownloadUrl = exeAsset["browser_download_url"].GetString(); + LatestInfo.DownloadCount = exeAsset["download_count"].GetInt(); + LatestInfo.DownloadSize = exeAsset["size"].GetInt(); + return true; +} + +UpdateInfo& UpdateChecker::GetLatestInfo() +{ + return LatestInfo; +} + +void UpdateChecker::Thread() { + while (!UpdateFlag.cancelled()) { + do { + std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + } while (std::chrono::steady_clock::now() < UpdateWakeup); + if (ForceUpdate()) { + Callback(LatestInfo); + } + UpdateWakeup = std::chrono::steady_clock::now() + CheckInterval.load(); + } +} \ No newline at end of file diff --git a/gui/UpdateChecker.h b/gui/UpdateChecker.h new file mode 100644 index 0000000..cabf861 --- /dev/null +++ b/gui/UpdateChecker.h @@ -0,0 +1,43 @@ +#pragma once +#include "../containers/cancel_flag.h" + +#include +#include + +struct UpdateInfo { + std::string Version; + std::string Url; + + std::string DownloadUrl; + int DownloadCount; + int DownloadSize; +}; + +typedef std::function UpdateCallback; + +class UpdateChecker { +public: + UpdateChecker(UpdateCallback callback, std::chrono::milliseconds checkInterval); + ~UpdateChecker(); + + void SetInterval(std::chrono::milliseconds newInterval); + + void StopUpdateThread(); + + bool ForceUpdate(); + + UpdateInfo& GetLatestInfo(); + +private: + void Thread(); + + std::atomic CheckInterval; + + std::string LatestVersion; + UpdateInfo LatestInfo; + + UpdateCallback Callback; + std::thread UpdateThread; + std::chrono::steady_clock::time_point UpdateWakeup; + cancel_flag UpdateFlag; +}; \ No newline at end of file diff --git a/gui/cMain.cpp b/gui/cMain.cpp index 9602cf4..95f8642 100644 --- a/gui/cMain.cpp +++ b/gui/cMain.cpp @@ -63,7 +63,7 @@ cMain::cMain(wxApp* app, const fs::path& settingsPath, const fs::path& manifestP SettingsPath(settingsPath), Settings(SettingsDefault()), Auth(personalAuth), - UpdateAvailable(false) { + GameUpdateAvailable(false) { LOG_DEBUG("Setting up (%s, %s)", settingsPath.string().c_str(), manifestPath.string().c_str()); this->SetIcon(wxICON(APP_ICON)); @@ -215,12 +215,12 @@ cMain::cMain(wxApp* app, const fs::path& settingsPath, const fs::path& manifestP std::thread([=]() { SetStatus(LSTR(MAIN_STATUS_STARTING)); - LOG_DEBUG("Creating update checker"); - GameChecker = std::make_unique( + LOG_DEBUG("Creating game update checker"); + GameUpdater = std::make_unique( manifestPath, - [this](const std::string& Url, const std::string& Version) { OnUpdate(Version, Url); }, + [this](const std::string& Url, const std::string& Version) { OnGameUpdate(Version, Url); }, SettingsGetUpdateInterval(&Settings)); - Mount(GameChecker->GetLatestUrl()); + Mount(GameUpdater->GetLatestUrl()); LOG_DEBUG("Enabling buttons"); SetStatus(LSTR(MAIN_STATUS_PLAYABLE)); SIDE_BUTTON_OBJ(verify)->Enable(); @@ -229,10 +229,19 @@ cMain::cMain(wxApp* app, const fs::path& settingsPath, const fs::path& manifestP auto ct = Build->GetMissingChunkCount(); LOG_DEBUG("%d missing chunks", ct); if (ct) { - OnUpdate(GameChecker->GetLatestVersion()); + OnGameUpdate(GameUpdater->GetLatestVersion()); } }).detach(); + + std::thread([=]() { + LOG_DEBUG("Creating update checker"); + Updater = std::make_unique( + [this](const UpdateInfo& Info) { OnUpdate(Info); }, + ch::milliseconds(5 * 60 * 1000)); // every 5 minutes, both for ratelimiting purposes and no real reason to be lower + LOG_DEBUG("Created update checker"); + }).detach(); + Systray = new SystrayIcon(this); Systray->SetIcon(wxICON(APP_ICON), "EGL2"); } @@ -262,8 +271,8 @@ void cMain::OnSettingsClicked(bool onStartup) { LOG_ERROR("Could not open settings file to write"); } }, &SettingsValidate, [this]() { - if (GameChecker) { - GameChecker->SetInterval(SettingsGetUpdateInterval(&Settings)); + if (GameUpdater) { + GameUpdater->SetInterval(SettingsGetUpdateInterval(&Settings)); } SetupWnd.reset(); this->Raise(); @@ -327,8 +336,8 @@ void cMain::OnVerifyClicked() { } void cMain::OnPlayClicked() { - if (UpdateAvailable) { - BeginUpdate(); + if (GameUpdateAvailable) { + BeginGameUpdate(); } else { if (FirstAuthLaunched) { @@ -389,12 +398,21 @@ void cMain::SetStatus(const wxString& string) { statusBar->SetLabel(string); } -void cMain::OnUpdate(const std::string& Version, const std::optional& Url) +void cMain::Mount(const std::string& Url) { + LOG_INFO("Setting up cache directory"); + MountedBuild::SetupCacheDirectory(Settings.CacheDir); + LOG_INFO("Mounting new url: %s", Url.c_str()); + Build.reset(new MountedBuild(GameUpdater->GetManifest(Url), fs::path(Settings.CacheDir) / MOUNT_FOLDER, Settings.CacheDir, SettingsGetStorageFlags(&Settings), Settings.BufferCount)); + LOG_INFO("Setting up game dir"); + Build->SetupGameDirectory([](unsigned int m) {}, []() {}, []() {}, cancel_flag(), Settings.ThreadCount); +} + +void cMain::OnGameUpdate(const std::string& Version, const std::optional& Url) { - UpdateAvailable = true; + GameUpdateAvailable = true; playBtn->SetLabel(LSTR(MAIN_BTN_UPDATE)); if (Url.has_value()) { - UpdateUrl = Url; + GameUpdateUrl = Url; } CallAfter([=]() { @@ -403,15 +421,15 @@ void cMain::OnUpdate(const std::string& Version, const std::optionalAddAction(42, LSTR(MAIN_NOTIF_ACTION))) { LOG_WARN("Actions aren't supported"); } - notif->Bind(wxEVT_NOTIFICATION_MESSAGE_ACTION, std::bind(&cMain::BeginUpdate, this)); - notif->Bind(wxEVT_NOTIFICATION_MESSAGE_CLICK, std::bind(&cMain::BeginUpdate, this)); + notif->Bind(wxEVT_NOTIFICATION_MESSAGE_ACTION, std::bind(&cMain::BeginGameUpdate, this)); + notif->Bind(wxEVT_NOTIFICATION_MESSAGE_CLICK, std::bind(&cMain::BeginGameUpdate, this)); if (!notif->Show(wxNotificationMessage::Timeout_Never)) { - LOG_ERROR("Couldn't launch update notification"); + LOG_ERROR("Couldn't launch game update notification"); } }); } -void cMain::BeginUpdate() +void cMain::BeginGameUpdate() { if (UpdateWnd) { UpdateWnd->Restore(); @@ -420,36 +438,54 @@ void cMain::BeginUpdate() return; } - if (!UpdateAvailable) { + if (!GameUpdateAvailable) { return; } std::thread([this]() { - if (!UpdateUrl.has_value()) { + if (!GameUpdateUrl.has_value()) { LOG_DEBUG("Downloading unavailable chunks"); } else { - LOG_DEBUG("Beginning update: %s", UpdateUrl->c_str()); - Mount(*UpdateUrl); + LOG_DEBUG("Beginning update: %s", GameUpdateUrl->c_str()); + Mount(*GameUpdateUrl); } this->CallAfter([this]() { RUN_PROGRESS(LSTR(MAIN_PROG_UPDATE), UpdateWnd, [this](bool cancelled) { LOG_DEBUG("EXIT NOTICED"); if (!cancelled) { - UpdateAvailable = false; + GameUpdateAvailable = false; playBtn->SetLabel(LSTR(MAIN_BTN_PLAY)); - UpdateUrl.reset(); + GameUpdateUrl.reset(); } }, PreloadAllChunks, Settings.ThreadCount); }); }).detach(); } -void cMain::Mount(const std::string& Url) { - LOG_INFO("Setting up cache directory"); - MountedBuild::SetupCacheDirectory(Settings.CacheDir); - LOG_INFO("Mounting new url: %s", Url.c_str()); - Build.reset(new MountedBuild(GameChecker->GetManifest(Url), fs::path(Settings.CacheDir) / MOUNT_FOLDER, Settings.CacheDir, SettingsGetStorageFlags(&Settings), Settings.BufferCount)); - LOG_INFO("Setting up game dir"); - Build->SetupGameDirectory([](unsigned int m) {}, []() {}, []() {}, cancel_flag(), Settings.ThreadCount); +void cMain::OnUpdate(const UpdateInfo& Info) +{ + UpdateUrl = Info.Url; + + CallAfter([=]() { + auto notif = new wxGenericNotificationMessage("New EGL2 Update!", wxString::Format("%s is now available!\n%s, %d downloads", Info.Version, Stats::GetReadableSize(Info.DownloadSize), Info.DownloadCount), this); + notif->SetIcon(wxICON(APP_ICON)); + if (!notif->AddAction(42, LSTR(MAIN_NOTIF_ACTION))) { + LOG_WARN("Actions aren't supported"); + } + notif->Bind(wxEVT_NOTIFICATION_MESSAGE_ACTION, std::bind(&cMain::BeginUpdate, this)); + notif->Bind(wxEVT_NOTIFICATION_MESSAGE_CLICK, std::bind(&cMain::BeginUpdate, this)); + if (!notif->Show(wxNotificationMessage::Timeout_Never)) { + LOG_ERROR("Couldn't launch update notification"); + } + }); +} + +void cMain::BeginUpdate() +{ + if (UpdateUrl.empty()) { + return; + } + wxLaunchDefaultBrowser(UpdateUrl); + UpdateUrl.clear(); } \ No newline at end of file diff --git a/gui/cMain.h b/gui/cMain.h index 6a4cf30..2d937e7 100644 --- a/gui/cMain.h +++ b/gui/cMain.h @@ -8,6 +8,7 @@ #include "cProgress.h" #include "GameUpdateChecker.h" #include "settings.h" +#include "UpdateChecker.h" #include #include @@ -62,9 +63,6 @@ class cMain : public wxFrame void SetStatus(const wxString& string); - void OnUpdate(const std::string& Version, const std::optional& Url = std::nullopt); - void BeginUpdate(); - private: void Mount(const std::string& Url); @@ -77,13 +75,22 @@ class cMain : public wxFrame bool FirstAuthLaunched = false; std::shared_ptr Auth; - bool UpdateAvailable; - std::optional UpdateUrl; + std::unique_ptr GameUpdater; + bool GameUpdateAvailable; + std::optional GameUpdateUrl; + + void OnGameUpdate(const std::string& Version, const std::optional& Url = std::nullopt); + void BeginGameUpdate(); + + std::unique_ptr Updater; + std::string UpdateUrl; + + void OnUpdate(const UpdateInfo& Info); + void BeginUpdate(); fs::path SettingsPath; SETTINGS Settings; - std::unique_ptr GameChecker; std::unique_ptr Build; friend class SystrayIcon; diff --git a/gui/taskbar.h b/gui/taskbar.h index 448c990..ed19509 100644 --- a/gui/taskbar.h +++ b/gui/taskbar.h @@ -39,7 +39,7 @@ class SystrayIcon : public wxTaskBarIcon { case EXIT_ID: if (Main->OnClose()) { Stats::StopUpdateThread(); - Main->GameChecker->StopUpdateThread(); + Main->GameUpdater->StopUpdateThread(); Main->Destroy(); Main->App->Exit(); } diff --git a/gui/versioninfo.h b/gui/versioninfo.h new file mode 100644 index 0000000..c42cf55 --- /dev/null +++ b/gui/versioninfo.h @@ -0,0 +1,9 @@ +#pragma once + +#define VERSION_MAJOR 1 +#define VERSION_MINOR 3 +#define VERSION_PATCH 2 + +#define STR_INDIR(x) #x +#define STR(x) STR_INDIR(x) +#define VERSION_STRING STR(VERSION_MAJOR) "." STR(VERSION_MINOR) "." STR(VERSION_PATCH) diff --git a/libraries/curlion/connection.cpp b/libraries/curlion/connection.cpp index 6e69ef3..be639d9 100644 --- a/libraries/curlion/connection.cpp +++ b/libraries/curlion/connection.cpp @@ -125,6 +125,11 @@ void Connection::SetUrl(const std::string& url) { curl_easy_setopt(handle_, CURLOPT_URL, url.c_str()); } +void Connection::SetUserAgent(const std::string& agent) +{ + curl_easy_setopt(handle_, CURLOPT_USERAGENT, agent.c_str()); +} + void Connection::SetProxy(const std::string& proxy) { curl_easy_setopt(handle_, CURLOPT_PROXY, proxy.c_str()); diff --git a/libraries/curlion/connection.h b/libraries/curlion/connection.h index 9b2d303..fa1e18f 100644 --- a/libraries/curlion/connection.h +++ b/libraries/curlion/connection.h @@ -281,6 +281,11 @@ class Connection : public std::enable_shared_from_this { Set the URL used in connection. */ void SetUrl(const std::string& url); + + /** + Set the user agent used in connection. + */ + void SetUserAgent(const std::string& agent); /** Set the proxy used in connection. diff --git a/resources.rc b/resources.rc index 77b8a93..493c61b 100644 --- a/resources.rc +++ b/resources.rc @@ -8,9 +8,11 @@ LOCALETYPES #include +#include "gui/versioninfo.h" + VS_VERSION_INFO VERSIONINFO FILEVERSION 1, 0, 0, 0 -PRODUCTVERSION 1, 3, 2, 0 +PRODUCTVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, 0 { BLOCK "StringFileInfo" { @@ -26,7 +28,7 @@ PRODUCTVERSION 1, 3, 2, 0 VALUE "OriginalFilename", "EGL2.exe" VALUE "ProductName", "EGL2" - VALUE "ProductVersion", "1.3.2.0" + VALUE "ProductVersion", VERSION_STRING ".0" } } BLOCK "VarFileInfo" diff --git a/web/http.h b/web/http.h index 44149fc..03506c6 100644 --- a/web/http.h +++ b/web/http.h @@ -4,6 +4,8 @@ // the only reason this is IOS and not PC is that PC can't create device auths :( #define BASIC_FN_AUTH "basic MzQ0NmNkNzI2OTRjNGE0NDg1ZDgxYjc3YWRiYjIxNDE6OTIwOWQ0YTVlMjVhNDU3ZmI5YjA3NDg5ZDMxM2I0MWE=" +#define NOMINMAX + #include "http/Client.h" #include