From a7277b47fcc187626be016466307548788467929 Mon Sep 17 00:00:00 2001 From: Kawe Mazidjatari <48657826+Mauler125@users.noreply.github.com> Date: Sun, 29 Oct 2023 20:08:10 +0100 Subject: [PATCH] Launcher UX Improvements * Fix bug regarding searching for prerelease builds, prerelease was assumed to be always at the top but this isn't true. * Make the prerelease toggle button check for updates instantly. * Automatically launch the game after the update has finished. --- r5dev/sdklauncher/base_surface.cpp | 161 ++++++++++++++++++++++-- r5dev/sdklauncher/base_surface.h | 9 +- r5dev/sdklauncher/sdklauncher.cpp | 60 +++++++-- r5dev/sdklauncher/sdklauncher.h | 1 + r5dev/sdklauncher/sdklauncher_utils.cpp | 23 +++- r5dev/sdklauncher/sdkupdater.cpp | 17 ++- r5dev/sdklauncher/sdkupdater.h | 2 +- 7 files changed, 244 insertions(+), 29 deletions(-) diff --git a/r5dev/sdklauncher/base_surface.cpp b/r5dev/sdklauncher/base_surface.cpp index 2076c19a..7dd4e39c 100644 --- a/r5dev/sdklauncher/base_surface.cpp +++ b/r5dev/sdklauncher/base_surface.cpp @@ -12,6 +12,7 @@ #include "tier1/utlmap.h" #include "tier2/curlutils.h" #include "zip/src/ZipFile.h" +#include "utility/vdf_parser.h" #define WINDOW_SIZE_X 400 #define WINDOW_SIZE_Y 224 @@ -29,6 +30,9 @@ CBaseSurface::CBaseSurface() this->SetMaximizeBox(false); this->SetBackColor(Drawing::Color(47, 54, 61)); + this->Load += &OnLoad; + this->FormClosing += &OnClose; + this->m_BaseGroup = new UIX::UIXGroupBox(); this->m_ManageButton = new UIX::UIXButton(); this->m_RepairButton = new UIX::UIXButton(); @@ -102,16 +106,13 @@ CBaseSurface::CBaseSurface() this->m_ExperimentalBuildsCheckbox->SetTabIndex(0); this->m_ExperimentalBuildsCheckbox->SetText(XorStr("Check for playtest/event updates")); this->m_ExperimentalBuildsCheckbox->SetAnchor(Forms::AnchorStyles::Top | Forms::AnchorStyles::Left); + this->m_ExperimentalBuildsCheckbox->Click += OnExperimentalBuildsClick; m_BaseGroup->AddControl(this->m_ExperimentalBuildsCheckbox); // TODO: Use a toggle item instead; remove this field. m_bPartialInstall = true; m_bUpdateViewToggled = false; - - Threading::Thread([this] { - this->Frame(); - }).Start(); } void CBaseSurface::ToggleUpdateView(bool bValue) @@ -119,7 +120,13 @@ void CBaseSurface::ToggleUpdateView(bool bValue) // Game must be installed before this can be called!! Assert(m_bIsInstalled); - this->m_ManageButton->SetText(bValue ? XorStr("Update Apex") : XorStr("Install Apex")); + // Don't switch it to the same view, else we install the callbacks + // twice and thus call them twice. + if (bValue == m_bUpdateViewToggled) + return; + + m_bUpdateViewToggled = bValue; + this->m_ManageButton->SetText(bValue ? XorStr("Update && Launch Apex") : XorStr("Launch Apex")); if (bValue) { @@ -131,8 +138,23 @@ void CBaseSurface::ToggleUpdateView(bool bValue) this->m_ManageButton->Click -= &OnUpdateClick; this->m_ManageButton->Click += &OnLaunchClick; } +} - m_bUpdateViewToggled = bValue; +bool CBaseSurface::CheckForUpdate() +{ + printf("%s: runframe; interval=%f\n", __FUNCTION__, g_flUpdateCheckRate); + const bool updateAvailable = SDKLauncher_CheckForUpdate(m_ExperimentalBuildsCheckbox->Checked()); + + if (m_bIsInstalled && updateAvailable) + { + printf("%s: found update; interval=%f\n", __FUNCTION__, g_flUpdateCheckRate); + ToggleUpdateView(true); + + return true; + } + + ToggleUpdateView(false); + return false; } @@ -142,9 +164,8 @@ void CBaseSurface::Frame() { printf("%s: runframe; interval=%f\n", __FUNCTION__, g_flUpdateCheckRate); - if (!m_bUpdateViewToggled && m_bIsInstalled && SDKLauncher_CheckForUpdate(m_ExperimentalBuildsCheckbox->Checked())) + if (CheckForUpdate()) { - ToggleUpdateView(true); printf("%s: found update; interval=%f\n", __FUNCTION__, g_flUpdateCheckRate); } @@ -152,6 +173,124 @@ void CBaseSurface::Frame() } } +//----------------------------------------------------------------------------- +// Purpose: load callback +// Input : *pSender - +//----------------------------------------------------------------------------- +void CBaseSurface::OnLoad(Forms::Control* pSender) +{ + CBaseSurface* pBaseSurface = (CBaseSurface*)pSender->FindForm(); + pBaseSurface->LoadSettings(); + + Threading::Thread([pBaseSurface] { + pBaseSurface->Frame(); + }).Start(); +} + +//----------------------------------------------------------------------------- +// Purpose: close callback +// Input : *pSender - +//----------------------------------------------------------------------------- +void CBaseSurface::OnClose(const std::unique_ptr& /*pEventArgs*/, Forms::Control* pSender) +{ + ((CBaseSurface*)pSender->FindForm())->SaveSettings(); +} + +void CBaseSurface::LoadSettings() +{ + const fs::path settingsPath(Format("platform/%s/%s", SDK_SYSTEM_CFG_PATH, LAUNCHER_SETTING_FILE)); + if (!fs::exists(settingsPath)) + { + return; + } + + bool success; + std::ifstream fileStream(settingsPath, fstream::in); + vdf::object vRoot = vdf::read(fileStream, &success); + + if (!success) + { + printf("%s: Failed to parse VDF file: '%s'\n", __FUNCTION__, + settingsPath.u8string().c_str()); + return; + } + + try + { + string& attributeView = vRoot.attribs["version"]; + + int settingsVersion = atoi(attributeView.c_str()); + if (settingsVersion != SDK_LAUNCHER_VERSION) + return; + + vdf::object* pSubKey = vRoot.childs["vars"].get(); + if (!pSubKey) + return; + + attributeView = pSubKey->attribs["experimentalBuilds"]; + this->m_ExperimentalBuildsCheckbox->SetChecked(attributeView != "0"); + } + catch (const std::exception& e) + { + printf("%s: Exception while parsing VDF file: %s\n", __FUNCTION__, e.what()); + } +} + +//----------------------------------------------------------------------------- +// Purpose: gets the control item value +// Input : *pControl - +//----------------------------------------------------------------------------- +const char* GetControlValue(Forms::Control* pControl) +{ + switch (pControl->GetType()) + { + case Forms::ControlTypes::CheckBox: + case Forms::ControlTypes::RadioButton: + { + return reinterpret_cast(pControl)->Checked() ? "1" : "0"; + } + default: + { + return pControl->Text().ToCString(); + } + } +} + + +void CBaseSurface::SaveSettings() +{ + const fs::path settingsPath(Format("platform/%s/%s", SDK_SYSTEM_CFG_PATH, LAUNCHER_SETTING_FILE)); + const fs::path parentPath = settingsPath.parent_path(); + + if (!fs::exists(parentPath) && !fs::create_directories(parentPath)) + { + printf("%s: Failed to create directory: '%s'\n", __FUNCTION__, + parentPath.relative_path().u8string().c_str()); + return; + } + + std::ofstream fileStream(settingsPath, fstream::out); + if (!fileStream) + { + printf("%s: Failed to create VDF file: '%s'\n", __FUNCTION__, + settingsPath.u8string().c_str()); + return; + } + + vdf::object vRoot; + vRoot.set_name("LauncherSettings"); + vRoot.add_attribute("version", std::to_string(SDK_LAUNCHER_VERSION)); + + vdf::object* vVars = new vdf::object(); + vVars->set_name("vars"); + + // Game. + vVars->add_attribute("experimentalBuilds", GetControlValue(this->m_ExperimentalBuildsCheckbox)); + vRoot.add_child(std::unique_ptr(vVars)); + + vdf::write(fileStream, vRoot); +} + void CBaseSurface::OnUpdateClick(Forms::Control* Sender) { //CBaseSurface* pSurf = (CBaseSurface*)Sender; @@ -283,6 +422,12 @@ void CBaseSurface::OnAdvancedClick(Forms::Control* Sender) pAdvancedSurface->ShowDialog((Forms::Form*)Sender->FindForm()); } +void CBaseSurface::OnExperimentalBuildsClick(Forms::Control* Sender) +{ + CBaseSurface* pBaseSurface = reinterpret_cast(Sender->FindForm()); + pBaseSurface->CheckForUpdate(); +} + void CBaseSurface::OnSupportClick(Forms::Control* /*Sender*/) { ShellExecute(0, 0, XorStr("https://ko-fi.com/amos0"), 0, 0, SW_SHOW); diff --git a/r5dev/sdklauncher/base_surface.h b/r5dev/sdklauncher/base_surface.h index 504c9bcc..f394c0c5 100644 --- a/r5dev/sdklauncher/base_surface.h +++ b/r5dev/sdklauncher/base_surface.h @@ -9,14 +9,18 @@ public: {}; void ToggleUpdateView(bool bValue); + bool CheckForUpdate(); protected: + static void OnLoad(Forms::Control* pSender); + static void OnClose(const std::unique_ptr& pEventArgs, Forms::Control* pSender); + static void OnInstallClick(Forms::Control* Sender); static void OnUpdateClick(Forms::Control* Sender); static void OnLaunchClick(Forms::Control* Sender); static void OnAdvancedClick(Forms::Control* Sender); - + static void OnExperimentalBuildsClick(Forms::Control* Sender); static void OnSupportClick(Forms::Control* Sender); static void OnJoinClick(Forms::Control* Sender); @@ -25,6 +29,9 @@ protected: private: void Frame(); + void LoadSettings(); + void SaveSettings(); + enum class eMode { NONE = -1, diff --git a/r5dev/sdklauncher/sdklauncher.cpp b/r5dev/sdklauncher/sdklauncher.cpp index 2776a39b..90c28b35 100644 --- a/r5dev/sdklauncher/sdklauncher.cpp +++ b/r5dev/sdklauncher/sdklauncher.cpp @@ -56,7 +56,12 @@ int CLauncher::HandleCommandLine(int argc, char* argv[]) string arg = argv[i]; eLaunchMode mode = eLaunchMode::LM_NONE; - if ((arg == "-developer") || (arg == "-dev")) + if ((arg == "-launch")) + { + LaunchGameDefault(); + return -2; + } + else if ((arg == "-developer") || (arg == "-dev")) { mode = eLaunchMode::LM_GAME_DEV; } @@ -326,6 +331,26 @@ bool CLauncher::LaunchProcess() const return true; } +/////////////////////////////////////////////////////////////////////////////// +// Purpose: launches the game with default parameters +// Output : true on success, false otherwise +/////////////////////////////////////////////////////////////////////////////// +bool CLauncher::LaunchGameDefault() const +{ + // Hack: free the console before launching since we only want it to be shown + // for the command line interface. + FreeConsole(); + + // !TODO: parameter building and settings loading should be its own class!! + if (g_pLauncher->CreateLaunchContext(eLaunchMode::LM_CLIENT, 0, "", "startup_launcher.cfg")) + { + g_pLauncher->LaunchProcess(); + return true; + } + + return false; +} + /////////////////////////////////////////////////////////////////////////////// // Purpose: Window enumerator callback. // Input : hwnd - @@ -386,6 +411,17 @@ void LauncherLoggerSink(LogType_t logType, LogLevel_t logLevel, eDLL_T context, } } +void RunGUI() +{ +#ifdef NDEBUG + FreeConsole(); +#endif // NDEBUG + curl_global_init(CURL_GLOBAL_ALL); + g_CoreMsgVCallback = LauncherLoggerSink; + g_pLauncher->RunSurface(); + curl_global_cleanup(); +} + /////////////////////////////////////////////////////////////////////////////// // EntryPoint. /////////////////////////////////////////////////////////////////////////////// @@ -400,21 +436,23 @@ int main(int argc, char* argv[]/*, char* envp[]*/) g_pLauncher->InitLogger(); if (argc < 2) { -#ifdef NDEBUG - FreeConsole(); -#endif // NDEBUG - curl_global_init(CURL_GLOBAL_ALL); - g_CoreMsgVCallback = LauncherLoggerSink; - g_pLauncher->RunSurface(); - curl_global_cleanup(); + RunGUI(); } else { int results = g_pLauncher->HandleCommandLine(argc, argv); - if (results != -1) + if (results == -1) + { + return g_pLauncher->HandleInput(); + } + else if (results == -2) + { + RunGUI(); + } + else + { return results; - - return g_pLauncher->HandleInput(); + } } return EXIT_SUCCESS; diff --git a/r5dev/sdklauncher/sdklauncher.h b/r5dev/sdklauncher/sdklauncher.h index 83155b82..d96c59c7 100644 --- a/r5dev/sdklauncher/sdklauncher.h +++ b/r5dev/sdklauncher/sdklauncher.h @@ -50,6 +50,7 @@ public: bool CreateLaunchContext(eLaunchMode lMode, uint64_t nProcessorAffinity = NULL, const char* szCommandLine = nullptr, const char* szConfig = nullptr); void SetupLaunchContext(const char* szConfig, const char* szGameDll, const char* szCommandLine); bool LaunchProcess() const; + bool LaunchGameDefault() const; CBaseSurface* GetMainSurface() const { return m_pSurface; } diff --git a/r5dev/sdklauncher/sdklauncher_utils.cpp b/r5dev/sdklauncher/sdklauncher_utils.cpp index ff74f43a..91b94fe0 100644 --- a/r5dev/sdklauncher/sdklauncher_utils.cpp +++ b/r5dev/sdklauncher/sdklauncher_utils.cpp @@ -1,10 +1,10 @@ #include "sdklauncher_utils.h" #include "windows/window.h" +#include "tier0/binstream.h" #include "tier1/xorstr.h" #include "tier1/utlmap.h" #include "tier2/curlutils.h" #include "zip/src/ZipFile.h" -#include bool g_bPartialInstall = false; //bool g_bExperimentalBuilds = false; @@ -331,12 +331,25 @@ bool SDKLauncher_AcquireReleaseManifest(const char* url, string& responseMessage { auto& release = responseJson[i]; - if (preRelease && release["prerelease"]) + if (preRelease) { - outManifest = release; - break; + if (i == responseJson.size() - 1 && outManifest.empty()) + { + // TODO: we probably do not want to take the first one, as it might not be ordered + // needs to be confirmed! + outManifest = responseJson[0]; // Just take the first one then. + break; + } + + const bool isPreRelease = release["prerelease"].get(); + + if (isPreRelease) + { + outManifest = release; + break; + } } - else if (!release["prerelease"]) + else { outManifest = release; break; diff --git a/r5dev/sdklauncher/sdkupdater.cpp b/r5dev/sdklauncher/sdkupdater.cpp index 673d0518..5f8424b7 100644 --- a/r5dev/sdklauncher/sdkupdater.cpp +++ b/r5dev/sdklauncher/sdkupdater.cpp @@ -122,17 +122,28 @@ void MoveFiles(const char* pSourceDir, const char* pDestDir) FindClose(hFind); } +//----------------------------------------------------------------------------- +// Purpose: print version numbers, warnings, etc +//----------------------------------------------------------------------------- +void PrintHeader() +{ + printf("********************************************************************************\n"); + printf("R5 updater [Version %s]\n", UPDATER_VERSION); + printf("!!! DO NOT INTERRUPT THE APPLICATION WHILE THE UPDATE IS IN PROGRESS !!!\n"); + printf("********************************************************************************\n"); +} + //----------------------------------------------------------------------------- // Purpose: entry point //----------------------------------------------------------------------------- int main(int argc, char** argv) { - printf("R5 updater [Version %s]\n", UPDATER_VERSION); + PrintHeader(); // Make sure the caller passed in a process id. if (argc < 1) { - printf("%s: Updater must be invoked with a ProcessID of the parent process!\n", "Error"); + printf("%s: Updater must be invoked with a Process ID of the parent process!\n", "Error"); Sleep(UPDATER_SLEEP_TIME_BEFORE_EXIT); return EXIT_FAILURE; @@ -199,7 +210,7 @@ int main(int argc, char** argv) BOOL createResult = CreateProcessA( "launcher.exe", // lpApplicationName - NULL, // lpCommandLine + (LPSTR)"-launch", // lpCommandLine NULL, // lpProcessAttributes NULL, // lpThreadAttributes FALSE, // bInheritHandles diff --git a/r5dev/sdklauncher/sdkupdater.h b/r5dev/sdklauncher/sdkupdater.h index 73580ca9..81851a0e 100644 --- a/r5dev/sdklauncher/sdkupdater.h +++ b/r5dev/sdklauncher/sdkupdater.h @@ -1,7 +1,7 @@ #ifndef SDKUPDATER_H #define SDKUPDATER_H -#define UPDATER_VERSION "v0.2a" +#define UPDATER_VERSION "v0.3a" #define UPDATER_SLEEP_TIME_BEFORE_EXIT 15000 #endif // SDKUPDATER_H