From b6ad795383f3e338929b9605d165ffbcdb133f9b Mon Sep 17 00:00:00 2001 From: William JCM Date: Fri, 1 Apr 2022 09:36:33 +0200 Subject: [PATCH] SaveTool: fragment files more. SaveTool.cpp was getting on the unmanageable side. --- src/CMakeLists.txt | 3 + src/SaveTool/SaveTool.cpp | 496 ----------------------- src/SaveTool/SaveTool_FileWatcher.cpp | 139 +++++++ src/SaveTool/SaveTool_Initialisation.cpp | 255 ++++++++++++ src/SaveTool/SaveTool_UpdateChecker.cpp | 162 ++++++++ 5 files changed, 559 insertions(+), 496 deletions(-) create mode 100644 src/SaveTool/SaveTool_FileWatcher.cpp create mode 100644 src/SaveTool/SaveTool_Initialisation.cpp create mode 100644 src/SaveTool/SaveTool_UpdateChecker.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 61fd23d..bf67d34 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -114,12 +114,15 @@ add_executable(MassBuilderSaveTool WIN32 SaveTool/SaveTool.cpp SaveTool/SaveTool_drawAbout.cpp SaveTool/SaveTool_drawMainMenu.cpp + SaveTool/SaveTool_FileWatcher.cpp + SaveTool/SaveTool_Initialisation.cpp SaveTool/SaveTool_MainManager.cpp SaveTool/SaveTool_MassViewer.cpp SaveTool/SaveTool_MassViewer_Frame.cpp SaveTool/SaveTool_MassViewer_Armour.cpp SaveTool/SaveTool_MassViewer_Weapons.cpp SaveTool/SaveTool_ProfileManager.cpp + SaveTool/SaveTool_UpdateChecker.cpp ProfileManager/ProfileManager.h ProfileManager/ProfileManager.cpp Profile/Profile.h diff --git a/src/SaveTool/SaveTool.cpp b/src/SaveTool/SaveTool.cpp index 4532f20..1978a38 100644 --- a/src/SaveTool/SaveTool.cpp +++ b/src/SaveTool/SaveTool.cpp @@ -16,13 +16,7 @@ #include "SaveTool.h" -#include - -#include #include -#include -#include -#include #include #include @@ -35,15 +29,10 @@ #include -#include -#include -#include #include -#include #include #include "../FontAwesome/IconsFontAwesome5.h" -#include "../FontAwesome/IconsFontAwesome5Brands.h" using namespace Containers::Literals; @@ -204,39 +193,6 @@ SaveTool::~SaveTool() { Utility::Debug{} << "Exiting..."; } -void SaveTool::handleFileAction(efsw::WatchID watch_id, - const std::string&, - const std::string& filename, - efsw::Action action, - std::string old_filename) -{ - SDL_Event event; - SDL_zero(event); - event.type = _fileEventId; - - if(watch_id == _watchIDs[StagingDir] && Utility::String::endsWith(filename, ".sav")) { - event.user.code = StagedUpdate; - SDL_PushEvent(&event); - return; - } - - if(Utility::String::endsWith(filename, "Config.sav")) { - return; - } // TODO: actually do something when config files will finally be handled - - if(!Utility::String::endsWith(filename, _currentProfile->account() + ".sav")) { - return; - } - - event.user.code = action; - event.user.data1 = Containers::String{Containers::AllocatedInit, filename.c_str()}.release(); - if(action == efsw::Actions::Moved) { - event.user.data2 = Containers::String{Containers::AllocatedInit, old_filename.c_str()}.release(); - } - - SDL_PushEvent(&event); -} - void SaveTool::drawEvent() { #ifdef SAVETOOL_DEBUG_BUILD tweak.update(); @@ -299,406 +255,6 @@ void SaveTool::anyEvent(SDL_Event& event) { } } -void SaveTool::initEvent(SDL_Event& event) { - _initThread.join(); - - switch(event.user.code) { - case InitSuccess: - _uiState = UiState::ProfileManager; - ImGui::CloseCurrentPopup(); - break; - case ProfileManagerFailure: - Utility::Error{} << "Error initialising ProfileManager:" << _profileManager->lastError(); - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising ProfileManager", _profileManager->lastError().data(), window()); - exit(EXIT_FAILURE); - break; - default: - break; - } -} - -void SaveTool::updateCheckEvent(SDL_Event& event) { - _updateThread.join(); - - if(event.user.code == CurlInitFailed) { - _queue.addToast(Toast::Type::Error, "Couldn't initialise libcurl. Update check aborted."_s); - return; - } - else if(event.user.code == CurlError) { - Containers::String error{static_cast(event.user.data2), CURL_ERROR_SIZE, nullptr}; - _queue.addToast(Toast::Type::Error, error, std::chrono::milliseconds{5000}); - _queue.addToast(Toast::Type::Error, static_cast(event.user.data1), std::chrono::milliseconds{5000}); - return; - } - else if(event.user.code == CurlTimeout) { - _queue.addToast(Toast::Type::Error, "The request timed out."_s); - return; - } - else if(event.user.code != 200) { - _queue.addToast(Toast::Type::Error, Utility::format("The request failed with error code {}", event.user.code)); - return; - } - - struct Version { - explicit Version(Containers::StringView str) { - std::size_t start_point = 0; - - if(str[0] == 'v') { - start_point++; - } - - auto components = Containers::StringView{str.data() + start_point}.split('.'); - - major = std::strtol(components[0].data(), nullptr, 10); - minor = std::strtol(components[1].data(), nullptr, 10); - patch = std::strtol(components[2].data(), nullptr, 10); - - fullVersion = major * 10000 + minor * 100 + patch; - - if(str.hasSuffix("-pre")) { - prerelease = true; - } - } - Int fullVersion; - Int major = 0; - Int minor = 0; - Int patch = 0; - bool prerelease = false; - - bool operator==(const Version& other) const { - return fullVersion == other.fullVersion; - } - bool operator>(const Version& other) const { - if((fullVersion > other.fullVersion) || - (fullVersion == other.fullVersion && prerelease == false && other.prerelease == true)) - { - return true; - } - else { - return false; - } - } - operator Containers::String() const { - return Utility::format("{}.{}.{}{}", major, minor, patch, prerelease ? "-pre" : ""); - } - }; - - static const Version current_ver{SAVETOOL_VERSION}; - - Containers::String response{static_cast(event.user.data1), strlen(static_cast(event.user.data1)), nullptr}; - auto components = response.split('\n'); - - Version latest_ver{components.front()}; - - if(latest_ver > current_ver) { - _queue.addToast(Toast::Type::Warning, "Your version is out of date.\nCheck the settings for more information."_s, - std::chrono::milliseconds{5000}); - _updateAvailable = true; - _latestVersion = latest_ver; - _releaseLink = Utility::format("https://williamjcm.ovh/git/williamjcm/MassBuilderSaveTool/releases/tag/v{}", components.front()); - _downloadLink = components.back(); - } - else if(latest_ver == current_ver || (current_ver > latest_ver && current_ver.prerelease == true)) { - _queue.addToast(Toast::Type::Success, "The application is already up to date."_s); - } - else if(current_ver > latest_ver && current_ver.prerelease == false) { - _queue.addToast(Toast::Type::Warning, "Your version is more recent than the latest one in the repo. How???"_s); - } -} - -void SaveTool::fileUpdateEvent(SDL_Event& event) { - if(event.user.code == StagedUpdate) { - _massManager->refreshStagedMasses(); - return; - } - - Containers::String filename{static_cast(event.user.data1), std::strlen(static_cast(event.user.data1)), nullptr}; - Containers::String old_filename; - - Int index = 0; - Int old_index = 0; - bool is_current_profile = filename == _currentProfile->filename(); - bool is_unit = filename.hasPrefix(_currentProfile->isDemo() ? "DemoUnit"_s : "Unit"_s); - if(is_unit) { - index = ((filename[_currentProfile->isDemo() ? 8 : 4] - 0x30) * 10) + - (filename[_currentProfile->isDemo() ? 9 : 5] - 0x30); - } - - if(event.user.code == FileMoved) { - old_filename = Containers::String{static_cast(event.user.data2), std::strlen(static_cast(event.user.data2)), nullptr}; - old_index = ((old_filename[_currentProfile->isDemo() ? 8 : 4] - 0x30) * 10) + - (old_filename[_currentProfile->isDemo() ? 9 : 5] - 0x30); - } - - switch(event.user.code) { - case FileAdded: - if(is_unit) { - if(!_currentMass || _currentMass != &(_massManager->hangar(index))) { - _massManager->refreshHangar(index); - } - else { - _currentMass->setDirty(); - } - } - break; - case FileDeleted: - if(is_current_profile) { - _currentProfile = nullptr; - _uiState = UiState::ProfileManager; - _profileManager->refreshProfiles(); - } - else if(is_unit) { - if(!_currentMass || _currentMass != &(_massManager->hangar(index))) { - _massManager->refreshHangar(index); - } - } - break; - case FileModified: - if(is_current_profile) { - _currentProfile->refreshValues(); - } - else if(is_unit) { - if(!_currentMass || _currentMass != &(_massManager->hangar(index))) { - _massManager->refreshHangar(index); - } - else { - if(_modifiedBySaveTool && _currentMass->filename() == filename) { - auto handle = CreateFileW(Utility::Unicode::widen(Containers::StringView{filename}).data(), GENERIC_READ, 0, - nullptr, OPEN_EXISTING, 0, nullptr); - if(handle && handle != INVALID_HANDLE_VALUE) { - CloseHandle(handle); - _modifiedBySaveTool = false; - } - } - else { - _currentMass->setDirty(); - } - } - } - break; - case FileMoved: - if(is_unit) { - if(old_filename.hasSuffix(".sav"_s)) { - _massManager->refreshHangar(index); - _massManager->refreshHangar(old_index); - } - } - break; - default: - _queue.addToast(Toast::Type::Warning, "Unknown file action type"_s); - } -} - -void SaveTool::initialiseConfiguration() { - Utility::Debug{} << "Reading configuration file..."; - - if(_conf.hasValue("cheat_mode"_s)) { - _cheatMode = _conf.value("cheat_mode"_s); - } - else { - _conf.setValue("cheat_mode"_s, _cheatMode); - } - - if(_conf.hasValue("unsafe_mode"_s)) { - _unsafeMode = _conf.value("unsafe_mode"_s); - } - else { - _conf.setValue("unsafe_mode"_s, _unsafeMode); - } - - if(_conf.hasValue("startup_update_check"_s)) { - _checkUpdatesOnStartup = _conf.value("startup_update_check"_s); - } - else { - _conf.setValue("startup_update_check"_s, _checkUpdatesOnStartup); - } - - if(_conf.hasValue("skip_disclaimer"_s)) { - _skipDisclaimer = _conf.value("skip_disclaimer"_s); - } - else { - _conf.setValue("skip_disclaimer"_s, _skipDisclaimer); - } - - if(_conf.hasValue("frame_limit"_s)) { - std::string frame_limit = _conf.value("frame_limit"_s); - if(frame_limit == "vsync"_s) { - _framelimit = Framelimit::Vsync; - } - else if(frame_limit == "half_vsync"_s) { - _framelimit = Framelimit::HalfVsync; - } - else { - _framelimit = Framelimit::FpsCap; - _fpsCap = std::stoul(frame_limit); - } - } - else { - _conf.setValue("frame_limit"_s, "vsync"_s); - } - - _conf.save(); -} - -void SaveTool::initialiseGui() { - Utility::Debug{} << "Initialising ImGui..."; - - ImGui::CreateContext(); - - ImGuiIO& io = ImGui::GetIO(); - - auto reg_font = _rs.getRaw("SourceSansPro-Regular.ttf"_s); - ImFontConfig font_config; - font_config.FontDataOwnedByAtlas = false; - std::strcpy(font_config.Name, "Source Sans Pro"); - io.Fonts->AddFontFromMemoryTTF(const_cast(reg_font.data()), reg_font.size(), 20.0f, &font_config); - - auto icon_font = _rs.getRaw(FONT_ICON_FILE_NAME_FAS); - static const ImWchar icon_range[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; - ImFontConfig icon_config; - icon_config.FontDataOwnedByAtlas = false; - icon_config.MergeMode = true; - icon_config.PixelSnapH = true; - icon_config.OversampleH = icon_config.OversampleV = 1; - icon_config.GlyphMinAdvanceX = 18.0f; - io.Fonts->AddFontFromMemoryTTF(const_cast(icon_font.data()), icon_font.size(), 16.0f, &icon_config, icon_range); - - auto brand_font = _rs.getRaw(FONT_ICON_FILE_NAME_FAB); - static const ImWchar brand_range[] = { ICON_MIN_FAB, ICON_MAX_FAB, 0 }; - io.Fonts->AddFontFromMemoryTTF(const_cast(brand_font.data()), brand_font.size(), 16.0f, &icon_config, brand_range); - - auto mono_font = _rs.getRaw("SourceCodePro-Regular.ttf"_s); - ImVector range; - ImFontGlyphRangesBuilder builder; - builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); - builder.AddChar(u'š'); // This allows displaying Vladimír Vondruš' name in Corrade's and Magnum's licences. - builder.BuildRanges(&range); - io.Fonts->AddFontFromMemoryTTF(const_cast(mono_font.data()), mono_font.size(), 18.0f, &font_config, range.Data); - - _imgui = ImGuiIntegration::Context(*ImGui::GetCurrentContext(), windowSize()); - - io.IniFilename = nullptr; - - ImGuiStyle& style = ImGui::GetStyle(); - - style.WindowTitleAlign = {0.5f, 0.5f}; - style.FrameRounding = 3.2f; - style.Colors[ImGuiCol_WindowBg] = ImColor(0xff1f1f1f); -} - -void SaveTool::initialiseManager() { - SDL_Event event; - SDL_zero(event); - event.type = _initEventId; - - _profileManager.emplace(_saveDir, _backupsDir); - if(!_profileManager->ready()) { - event.user.code = ProfileManagerFailure; - SDL_PushEvent(&event); - return; - } - - event.user.code = InitSuccess; - SDL_PushEvent(&event); -} - -auto SaveTool::initialiseToolDirectories() -> bool { - Utility::Debug{} << "Initialising Save Tool directories..."; - - _backupsDir = Utility::Path::join(Utility::Path::split(*Utility::Path::executableLocation()).first(), "backups"); - _stagingDir = Utility::Path::join(Utility::Path::split(*Utility::Path::executableLocation()).first(), "staging"); - //_armouryDir = Utility::Directory::join(Utility::Directory::path(Utility::Directory::executableLocation()), "armoury"); - //_armoursDir = Utility::Directory::join(_armouryDir, "armours"); - //_weaponsDir = Utility::Directory::join(_armouryDir, "weapons"); - //_stylesDir = Utility::Directory::join(_armouryDir, "styles"); - - if(!Utility::Path::exists(_backupsDir)) { - Utility::Debug{} << "Backups directory not found, creating..."; - if(!Utility::Path::make(_backupsDir)) { - Utility::Error{} << (_lastError = "Couldn't create the backups directory."); - return false; - } - } - - if(!Utility::Path::exists(_stagingDir)) { - Utility::Debug{} << "Staging directory not found, creating..."; - if(!Utility::Path::make(_stagingDir)) { - Utility::Error{} << (_lastError = "Couldn't create the backups directory."); - return false; - } - } - - //if(!Utility::Directory::exists(_armouryDir)) { - // Utility::Debug{} << "Armoury directory not found, creating..."; - // if(!Utility::Path::make(_armouryDir)) { - // Utility::Error{} << (_lastError = "Couldn't create the armoury directory."); - // return false; - // } - //} - - //if(!Utility::Directory::exists(_armoursDir)) { - // Utility::Debug{} << "Armours directory not found, creating..."; - // if(!Utility::Path::make(_armoursDir)) { - // Utility::Error{} << (_lastError = "Couldn't create the armours directory."); - // return false; - // } - //} - - //if(!Utility::Directory::exists(_weaponsDir)) { - // Utility::Debug{} << "Weapons directory not found, creating..."; - // if(!Utility::Path::make(_weaponsDir)) { - // Utility::Error{} << (_lastError = "Couldn't create the weapons directory."); - // return false; - // } - //} - - //if(!Utility::Directory::exists(_stylesDir)) { - // Utility::Debug{} << "Styles directory not found, creating..."; - // if(!Utility::Path::make(_stylesDir)) { - // Utility::Error{} << (_lastError = "Couldn't create the styles directory."); - // return false; - // } - //} - - return true; -} - -auto SaveTool::findGameDataDirectory() -> bool { - Utility::Debug{} << "Searching for the game's save directory..."; - - wchar_t* localappdata_path = nullptr; - Containers::ScopeGuard guard{localappdata_path, CoTaskMemFree}; - if(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_NO_APPCONTAINER_REDIRECTION, nullptr, &localappdata_path) != S_OK) - { - Utility::Error{} << (_lastError = "SHGetKnownFolderPath() failed in SaveTool::findGameDataDirectory()"_s); - return false; - } - - _gameDataDir = Utility::Path::join(Utility::Path::fromNativeSeparators(Utility::Unicode::narrow(localappdata_path)), "MASS_Builder"_s); - - if(!Utility::Path::exists(_gameDataDir)) { - Utility::Error{} << (_lastError = _gameDataDir + " wasn't found. Make sure to play the game at least once."_s); - return false; - } - - _configDir = Utility::Path::join(_gameDataDir, "Saved/Config/WindowsNoEditor"_s); - _saveDir = Utility::Path::join(_gameDataDir, "Saved/SaveGames"_s); - _screenshotsDir = Utility::Path::join(_gameDataDir, "Saved/Screenshots/WindowsNoEditor"_s); - - return true; -} - -void SaveTool::initialiseMassManager() { - _massManager.emplace(_saveDir, _currentProfile->account(), _currentProfile->isDemo(), _stagingDir); -} - -void SaveTool::initialiseFileWatcher() { - _fileWatcher.emplace(); - _watchIDs[SaveDir] = _fileWatcher->addWatch(_saveDir, this, false); - _watchIDs[StagingDir] = _fileWatcher->addWatch(_stagingDir, this, false); - _fileWatcher->watch(); -} - void SaveTool::drawImGui() { _imgui.newFrame(); @@ -895,55 +451,3 @@ void SaveTool::checkGameState() { _gameState = GameState::Unknown; } } - -inline auto writeData(char* ptr, std::size_t size, std::size_t nmemb, Containers::String* buf)-> std::size_t { - if(!ptr || !buf) return 0; - (*buf) = Utility::format("{}{}", *buf, Containers::StringView{ptr, size * nmemb}); - return size * nmemb; -} - -void SaveTool::checkForUpdates() { - SDL_Event event; - SDL_zero(event); - event.type = _updateEventId; - - auto curl = curl_easy_init(); - if(!curl) { - event.user.code = CurlInitFailed; - } - - if(curl) { - Containers::String response_body{Containers::AllocatedInit, ""}; - Containers::String error_buffer{ValueInit, CURL_ERROR_SIZE * 2}; - - curl_easy_setopt(curl, CURLOPT_URL, "https://williamjcm.ovh/mbst/version"); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); - curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer.data()); - curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 10000L); - - auto code = curl_easy_perform(curl); - - if(code == CURLE_OK) { - long status = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); - event.user.code = Int(status); - event.user.data1 = response_body.release(); - } - else if(code == CURLE_OPERATION_TIMEDOUT) { - event.user.code = CurlTimeout; - } - else { - event.user.code = CurlError; - event.user.data1 = const_cast(curl_easy_strerror(code)); - event.user.data2 = Containers::String{error_buffer}.release(); - } - - curl_easy_cleanup(curl); - } - - SDL_PushEvent(&event); -} diff --git a/src/SaveTool/SaveTool_FileWatcher.cpp b/src/SaveTool/SaveTool_FileWatcher.cpp new file mode 100644 index 0000000..65a7550 --- /dev/null +++ b/src/SaveTool/SaveTool_FileWatcher.cpp @@ -0,0 +1,139 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2022 Guillaume Jacquemin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include + +#include +#include + +#include "SaveTool.h" + +void SaveTool::handleFileAction(efsw::WatchID watch_id, + const std::string&, + const std::string& filename, + efsw::Action action, + std::string old_filename) +{ + SDL_Event event; + SDL_zero(event); + event.type = _fileEventId; + + if(watch_id == _watchIDs[StagingDir] && Utility::String::endsWith(filename, ".sav")) { + event.user.code = StagedUpdate; + SDL_PushEvent(&event); + return; + } + + if(Utility::String::endsWith(filename, "Config.sav")) { + return; + } // TODO: actually do something when config files will finally be handled + + if(!Utility::String::endsWith(filename, _currentProfile->account() + ".sav")) { + return; + } + + event.user.code = action; + event.user.data1 = Containers::String{Containers::AllocatedInit, filename.c_str()}.release(); + if(action == efsw::Actions::Moved) { + event.user.data2 = Containers::String{Containers::AllocatedInit, old_filename.c_str()}.release(); + } + + SDL_PushEvent(&event); +} + +void SaveTool::fileUpdateEvent(SDL_Event& event) { + if(event.user.code == StagedUpdate) { + _massManager->refreshStagedMasses(); + return; + } + + Containers::String filename{static_cast(event.user.data1), std::strlen(static_cast(event.user.data1)), nullptr}; + Containers::String old_filename; + + Int index = 0; + Int old_index = 0; + bool is_current_profile = filename == _currentProfile->filename(); + bool is_unit = filename.hasPrefix(_currentProfile->isDemo() ? "DemoUnit"_s : "Unit"_s); + if(is_unit) { + index = ((filename[_currentProfile->isDemo() ? 8 : 4] - 0x30) * 10) + + (filename[_currentProfile->isDemo() ? 9 : 5] - 0x30); + } + + if(event.user.code == FileMoved) { + old_filename = Containers::String{static_cast(event.user.data2), std::strlen(static_cast(event.user.data2)), nullptr}; + old_index = ((old_filename[_currentProfile->isDemo() ? 8 : 4] - 0x30) * 10) + + (old_filename[_currentProfile->isDemo() ? 9 : 5] - 0x30); + } + + switch(event.user.code) { + case FileAdded: + if(is_unit) { + if(!_currentMass || _currentMass != &(_massManager->hangar(index))) { + _massManager->refreshHangar(index); + } + else { + _currentMass->setDirty(); + } + } + break; + case FileDeleted: + if(is_current_profile) { + _currentProfile = nullptr; + _uiState = UiState::ProfileManager; + _profileManager->refreshProfiles(); + } + else if(is_unit) { + if(!_currentMass || _currentMass != &(_massManager->hangar(index))) { + _massManager->refreshHangar(index); + } + } + break; + case FileModified: + if(is_current_profile) { + _currentProfile->refreshValues(); + } + else if(is_unit) { + if(!_currentMass || _currentMass != &(_massManager->hangar(index))) { + _massManager->refreshHangar(index); + } + else { + if(_modifiedBySaveTool && _currentMass->filename() == filename) { + auto handle = CreateFileW(Utility::Unicode::widen(Containers::StringView{filename}).data(), GENERIC_READ, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + if(handle && handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + _modifiedBySaveTool = false; + } + } + else { + _currentMass->setDirty(); + } + } + } + break; + case FileMoved: + if(is_unit) { + if(old_filename.hasSuffix(".sav"_s)) { + _massManager->refreshHangar(index); + _massManager->refreshHangar(old_index); + } + } + break; + default: + _queue.addToast(Toast::Type::Warning, "Unknown file action type"_s); + } +} diff --git a/src/SaveTool/SaveTool_Initialisation.cpp b/src/SaveTool/SaveTool_Initialisation.cpp new file mode 100644 index 0000000..2323b58 --- /dev/null +++ b/src/SaveTool/SaveTool_Initialisation.cpp @@ -0,0 +1,255 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2022 Guillaume Jacquemin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include +#include + +#include + +#include "../FontAwesome/IconsFontAwesome5.h" +#include "../FontAwesome/IconsFontAwesome5Brands.h" + +#include "SaveTool.h" + +void SaveTool::initEvent(SDL_Event& event) { + _initThread.join(); + + switch(event.user.code) { + case InitSuccess: + _uiState = UiState::ProfileManager; + ImGui::CloseCurrentPopup(); + break; + case ProfileManagerFailure: + Utility::Error{} << "Error initialising ProfileManager:" << _profileManager->lastError(); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising ProfileManager", _profileManager->lastError().data(), window()); + exit(EXIT_FAILURE); + break; + default: + break; + } +} + +void SaveTool::initialiseConfiguration() { + Utility::Debug{} << "Reading configuration file..."; + + if(_conf.hasValue("cheat_mode"_s)) { + _cheatMode = _conf.value("cheat_mode"_s); + } + else { + _conf.setValue("cheat_mode"_s, _cheatMode); + } + + if(_conf.hasValue("unsafe_mode"_s)) { + _unsafeMode = _conf.value("unsafe_mode"_s); + } + else { + _conf.setValue("unsafe_mode"_s, _unsafeMode); + } + + if(_conf.hasValue("startup_update_check"_s)) { + _checkUpdatesOnStartup = _conf.value("startup_update_check"_s); + } + else { + _conf.setValue("startup_update_check"_s, _checkUpdatesOnStartup); + } + + if(_conf.hasValue("skip_disclaimer"_s)) { + _skipDisclaimer = _conf.value("skip_disclaimer"_s); + } + else { + _conf.setValue("skip_disclaimer"_s, _skipDisclaimer); + } + + if(_conf.hasValue("frame_limit"_s)) { + std::string frame_limit = _conf.value("frame_limit"_s); + if(frame_limit == "vsync"_s) { + _framelimit = Framelimit::Vsync; + } + else if(frame_limit == "half_vsync"_s) { + _framelimit = Framelimit::HalfVsync; + } + else { + _framelimit = Framelimit::FpsCap; + _fpsCap = std::stoul(frame_limit); + } + } + else { + _conf.setValue("frame_limit"_s, "vsync"_s); + } + + _conf.save(); +} + +void SaveTool::initialiseGui() { + Utility::Debug{} << "Initialising ImGui..."; + + ImGui::CreateContext(); + + ImGuiIO& io = ImGui::GetIO(); + + auto reg_font = _rs.getRaw("SourceSansPro-Regular.ttf"_s); + ImFontConfig font_config; + font_config.FontDataOwnedByAtlas = false; + std::strcpy(font_config.Name, "Source Sans Pro"); + io.Fonts->AddFontFromMemoryTTF(const_cast(reg_font.data()), reg_font.size(), 20.0f, &font_config); + + auto icon_font = _rs.getRaw(FONT_ICON_FILE_NAME_FAS); + static const ImWchar icon_range[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + ImFontConfig icon_config; + icon_config.FontDataOwnedByAtlas = false; + icon_config.MergeMode = true; + icon_config.PixelSnapH = true; + icon_config.OversampleH = icon_config.OversampleV = 1; + icon_config.GlyphMinAdvanceX = 18.0f; + io.Fonts->AddFontFromMemoryTTF(const_cast(icon_font.data()), icon_font.size(), 16.0f, &icon_config, icon_range); + + auto brand_font = _rs.getRaw(FONT_ICON_FILE_NAME_FAB); + static const ImWchar brand_range[] = { ICON_MIN_FAB, ICON_MAX_FAB, 0 }; + io.Fonts->AddFontFromMemoryTTF(const_cast(brand_font.data()), brand_font.size(), 16.0f, &icon_config, brand_range); + + auto mono_font = _rs.getRaw("SourceCodePro-Regular.ttf"_s); + ImVector range; + ImFontGlyphRangesBuilder builder; + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); + builder.AddChar(u'š'); // This allows displaying Vladimír Vondruš' name in Corrade's and Magnum's licences. + builder.BuildRanges(&range); + io.Fonts->AddFontFromMemoryTTF(const_cast(mono_font.data()), mono_font.size(), 18.0f, &font_config, range.Data); + + _imgui = ImGuiIntegration::Context(*ImGui::GetCurrentContext(), windowSize()); + + io.IniFilename = nullptr; + + ImGuiStyle& style = ImGui::GetStyle(); + + style.WindowTitleAlign = {0.5f, 0.5f}; + style.FrameRounding = 3.2f; + style.Colors[ImGuiCol_WindowBg] = ImColor(0xff1f1f1f); +} + +void SaveTool::initialiseManager() { + SDL_Event event; + SDL_zero(event); + event.type = _initEventId; + + _profileManager.emplace(_saveDir, _backupsDir); + if(!_profileManager->ready()) { + event.user.code = ProfileManagerFailure; + SDL_PushEvent(&event); + return; + } + + event.user.code = InitSuccess; + SDL_PushEvent(&event); +} + +auto SaveTool::initialiseToolDirectories() -> bool { + Utility::Debug{} << "Initialising Save Tool directories..."; + + _backupsDir = Utility::Path::join(Utility::Path::split(*Utility::Path::executableLocation()).first(), "backups"); + _stagingDir = Utility::Path::join(Utility::Path::split(*Utility::Path::executableLocation()).first(), "staging"); + //_armouryDir = Utility::Directory::join(Utility::Directory::path(Utility::Directory::executableLocation()), "armoury"); + //_armoursDir = Utility::Directory::join(_armouryDir, "armours"); + //_weaponsDir = Utility::Directory::join(_armouryDir, "weapons"); + //_stylesDir = Utility::Directory::join(_armouryDir, "styles"); + + if(!Utility::Path::exists(_backupsDir)) { + Utility::Debug{} << "Backups directory not found, creating..."; + if(!Utility::Path::make(_backupsDir)) { + Utility::Error{} << (_lastError = "Couldn't create the backups directory."); + return false; + } + } + + if(!Utility::Path::exists(_stagingDir)) { + Utility::Debug{} << "Staging directory not found, creating..."; + if(!Utility::Path::make(_stagingDir)) { + Utility::Error{} << (_lastError = "Couldn't create the backups directory."); + return false; + } + } + + //if(!Utility::Directory::exists(_armouryDir)) { + // Utility::Debug{} << "Armoury directory not found, creating..."; + // if(!Utility::Path::make(_armouryDir)) { + // Utility::Error{} << (_lastError = "Couldn't create the armoury directory."); + // return false; + // } + //} + + //if(!Utility::Directory::exists(_armoursDir)) { + // Utility::Debug{} << "Armours directory not found, creating..."; + // if(!Utility::Path::make(_armoursDir)) { + // Utility::Error{} << (_lastError = "Couldn't create the armours directory."); + // return false; + // } + //} + + //if(!Utility::Directory::exists(_weaponsDir)) { + // Utility::Debug{} << "Weapons directory not found, creating..."; + // if(!Utility::Path::make(_weaponsDir)) { + // Utility::Error{} << (_lastError = "Couldn't create the weapons directory."); + // return false; + // } + //} + + //if(!Utility::Directory::exists(_stylesDir)) { + // Utility::Debug{} << "Styles directory not found, creating..."; + // if(!Utility::Path::make(_stylesDir)) { + // Utility::Error{} << (_lastError = "Couldn't create the styles directory."); + // return false; + // } + //} + + return true; +} + +auto SaveTool::findGameDataDirectory() -> bool { + Utility::Debug{} << "Searching for the game's save directory..."; + + wchar_t* localappdata_path = nullptr; + Containers::ScopeGuard guard{localappdata_path, CoTaskMemFree}; + if(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_NO_APPCONTAINER_REDIRECTION, nullptr, &localappdata_path) != S_OK) + { + Utility::Error{} << (_lastError = "SHGetKnownFolderPath() failed in SaveTool::findGameDataDirectory()"_s); + return false; + } + + _gameDataDir = Utility::Path::join(Utility::Path::fromNativeSeparators(Utility::Unicode::narrow(localappdata_path)), "MASS_Builder"_s); + + if(!Utility::Path::exists(_gameDataDir)) { + Utility::Error{} << (_lastError = _gameDataDir + " wasn't found. Make sure to play the game at least once."_s); + return false; + } + + _configDir = Utility::Path::join(_gameDataDir, "Saved/Config/WindowsNoEditor"_s); + _saveDir = Utility::Path::join(_gameDataDir, "Saved/SaveGames"_s); + _screenshotsDir = Utility::Path::join(_gameDataDir, "Saved/Screenshots/WindowsNoEditor"_s); + + return true; +} + +void SaveTool::initialiseMassManager() { + _massManager.emplace(_saveDir, _currentProfile->account(), _currentProfile->isDemo(), _stagingDir); +} + +void SaveTool::initialiseFileWatcher() { + _fileWatcher.emplace(); + _watchIDs[SaveDir] = _fileWatcher->addWatch(_saveDir, this, false); + _watchIDs[StagingDir] = _fileWatcher->addWatch(_stagingDir, this, false); + _fileWatcher->watch(); +} diff --git a/src/SaveTool/SaveTool_UpdateChecker.cpp b/src/SaveTool/SaveTool_UpdateChecker.cpp new file mode 100644 index 0000000..aaf83df --- /dev/null +++ b/src/SaveTool/SaveTool_UpdateChecker.cpp @@ -0,0 +1,162 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2022 Guillaume Jacquemin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#include + +#include "SaveTool.h" + +void SaveTool::updateCheckEvent(SDL_Event& event) { + _updateThread.join(); + + if(event.user.code == CurlInitFailed) { + _queue.addToast(Toast::Type::Error, "Couldn't initialise libcurl. Update check aborted."_s); + return; + } + else if(event.user.code == CurlError) { + Containers::String error{static_cast(event.user.data2), CURL_ERROR_SIZE, nullptr}; + _queue.addToast(Toast::Type::Error, error, std::chrono::milliseconds{5000}); + _queue.addToast(Toast::Type::Error, static_cast(event.user.data1), std::chrono::milliseconds{5000}); + return; + } + else if(event.user.code == CurlTimeout) { + _queue.addToast(Toast::Type::Error, "The request timed out."_s); + return; + } + else if(event.user.code != 200) { + _queue.addToast(Toast::Type::Error, Utility::format("The request failed with error code {}", event.user.code)); + return; + } + + struct Version { + explicit Version(Containers::StringView str) { + std::size_t start_point = 0; + + if(str[0] == 'v') { + start_point++; + } + + auto components = Containers::StringView{str.data() + start_point}.split('.'); + + major = std::strtol(components[0].data(), nullptr, 10); + minor = std::strtol(components[1].data(), nullptr, 10); + patch = std::strtol(components[2].data(), nullptr, 10); + + fullVersion = major * 10000 + minor * 100 + patch; + + if(str.hasSuffix("-pre")) { + prerelease = true; + } + } + Int fullVersion; + Int major = 0; + Int minor = 0; + Int patch = 0; + bool prerelease = false; + + bool operator==(const Version& other) const { + return fullVersion == other.fullVersion; + } + bool operator>(const Version& other) const { + if((fullVersion > other.fullVersion) || + (fullVersion == other.fullVersion && prerelease == false && other.prerelease == true)) + { + return true; + } + else { + return false; + } + } + operator Containers::String() const { + return Utility::format("{}.{}.{}{}", major, minor, patch, prerelease ? "-pre" : ""); + } + }; + + static const Version current_ver{SAVETOOL_VERSION}; + + Containers::String response{static_cast(event.user.data1), strlen(static_cast(event.user.data1)), nullptr}; + auto components = response.split('\n'); + + Version latest_ver{components.front()}; + + if(latest_ver > current_ver) { + _queue.addToast(Toast::Type::Warning, "Your version is out of date.\nCheck the settings for more information."_s, + std::chrono::milliseconds{5000}); + _updateAvailable = true; + _latestVersion = latest_ver; + _releaseLink = Utility::format("https://williamjcm.ovh/git/williamjcm/MassBuilderSaveTool/releases/tag/v{}", components.front()); + _downloadLink = components.back(); + } + else if(latest_ver == current_ver || (current_ver > latest_ver && current_ver.prerelease == true)) { + _queue.addToast(Toast::Type::Success, "The application is already up to date."_s); + } + else if(current_ver > latest_ver && current_ver.prerelease == false) { + _queue.addToast(Toast::Type::Warning, "Your version is more recent than the latest one in the repo. How???"_s); + } +} + +inline auto writeData(char* ptr, std::size_t size, std::size_t nmemb, Containers::String* buf)-> std::size_t { + if(!ptr || !buf) return 0; + (*buf) = Utility::format("{}{}", *buf, Containers::StringView{ptr, size * nmemb}); + return size * nmemb; +} + +void SaveTool::checkForUpdates() { + SDL_Event event; + SDL_zero(event); + event.type = _updateEventId; + + auto curl = curl_easy_init(); + if(!curl) { + event.user.code = CurlInitFailed; + } + + if(curl) { + Containers::String response_body{Containers::AllocatedInit, ""}; + Containers::String error_buffer{ValueInit, CURL_ERROR_SIZE * 2}; + + curl_easy_setopt(curl, CURLOPT_URL, "https://williamjcm.ovh/mbst/version"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer.data()); + curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 10000L); + + auto code = curl_easy_perform(curl); + + if(code == CURLE_OK) { + long status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + event.user.code = Int(status); + event.user.data1 = response_body.release(); + } + else if(code == CURLE_OPERATION_TIMEDOUT) { + event.user.code = CurlTimeout; + } + else { + event.user.code = CurlError; + event.user.data1 = const_cast(curl_easy_strerror(code)); + event.user.data2 = Containers::String{error_buffer}.release(); + } + + curl_easy_cleanup(curl); + } + + SDL_PushEvent(&event); +}