836 lines
28 KiB
C++
836 lines
28 KiB
C++
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
#include "SaveTool.h"
|
|
|
|
#include <cstring>
|
|
|
|
#include <Corrade/Containers/Pair.h>
|
|
#include <Corrade/Containers/ScopeGuard.h>
|
|
#include <Corrade/Containers/StringStl.h>
|
|
#include <Corrade/Utility/Format.h>
|
|
#include <Corrade/Utility/Path.h>
|
|
#include <Corrade/Utility/String.h>
|
|
#include <Corrade/Utility/Unicode.h>
|
|
|
|
#include <Magnum/GL/DebugOutput.h>
|
|
#include <Magnum/GL/DefaultFramebuffer.h>
|
|
#include <Magnum/GL/Extensions.h>
|
|
#include <Magnum/GL/Renderer.h>
|
|
|
|
#include <Magnum/ImGuiIntegration/Integration.h>
|
|
#include <Magnum/ImGuiIntegration/Context.hpp>
|
|
|
|
#include <cpr/cpr.h>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <windef.h>
|
|
#include <winuser.h>
|
|
#include <processthreadsapi.h>
|
|
#include <shellapi.h>
|
|
#include <shlobj.h>
|
|
#include <wtsapi32.h>
|
|
|
|
#include "../FontAwesome/IconsFontAwesome5.h"
|
|
#include "../FontAwesome/IconsFontAwesome5Brands.h"
|
|
|
|
using namespace Containers::Literals;
|
|
|
|
extern const ImVec2 center_pivot = {0.5f, 0.5f};
|
|
|
|
#ifdef SAVETOOL_DEBUG_BUILD
|
|
Utility::Tweakable tweak;
|
|
#endif
|
|
|
|
SaveTool::SaveTool(const Arguments& arguments):
|
|
Platform::Sdl2Application{arguments,
|
|
Configuration{}.setTitle("M.A.S.S. Builder Save Tool " SAVETOOL_VERSION " (\"" SAVETOOL_CODENAME "\")")
|
|
.setSize({960, 720})}
|
|
{
|
|
#ifdef SAVETOOL_DEBUG_BUILD
|
|
tweak.enable(""_s, "../../"_s);
|
|
#endif
|
|
|
|
if(SDL_VERSION_ATLEAST(2, 0, 5)) {
|
|
if(SDL_SetHintWithPriority(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1", SDL_HINT_OVERRIDE) == SDL_TRUE) {
|
|
Utility::Debug{} << "Clickthrough is available."_s;
|
|
}
|
|
else {
|
|
Utility::Warning{} << "Clickthrough is not available (hint couldn't be set)."_s;
|
|
}
|
|
}
|
|
else {
|
|
Utility::Warning{} << "Clickthrough is not available (SDL2 is too old)."_s;
|
|
}
|
|
|
|
GL::Renderer::enable(GL::Renderer::Feature::Blending);
|
|
GL::Renderer::enable(GL::Renderer::Feature::ScissorTest);
|
|
GL::Renderer::disable(GL::Renderer::Feature::FaceCulling);
|
|
GL::Renderer::disable(GL::Renderer::Feature::DepthTest);
|
|
GL::Renderer::setBlendFunction(GL::Renderer::BlendFunction::SourceAlpha,
|
|
GL::Renderer::BlendFunction::OneMinusSourceAlpha);
|
|
GL::Renderer::setBlendEquation(GL::Renderer::BlendEquation::Add,
|
|
GL::Renderer::BlendEquation::Add);
|
|
|
|
initialiseGui();
|
|
|
|
if((_initEventId = SDL_RegisterEvents(3)) == UnsignedInt(-1)) {
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error",
|
|
"SDL_RegisterEvents() failed in SaveTool::SaveTool(). Exiting...", window());
|
|
exit(EXIT_FAILURE);
|
|
return;
|
|
}
|
|
|
|
_updateEventId = _initEventId + 1;
|
|
_fileEventId = _initEventId + 2;
|
|
|
|
initialiseToolDirectories();
|
|
|
|
if(!findGameDataDirectory()) {
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising the app", _lastError.data(), window());
|
|
exit(EXIT_FAILURE);
|
|
return;
|
|
}
|
|
|
|
_configDir = Utility::Path::join(_gameDataDir, "Saved/Config/WindowsNoEditor");
|
|
_saveDir = Utility::Path::join(_gameDataDir, "Saved/SaveGames");
|
|
_screenshotsDir = Utility::Path::join(_gameDataDir, "Saved/Screenshots/WindowsNoEditor");
|
|
|
|
if(SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising the app", SDL_GetError(), window());
|
|
exit(EXIT_FAILURE);
|
|
return;
|
|
}
|
|
|
|
checkGameState();
|
|
_gameCheckTimerId = SDL_AddTimer(2000,
|
|
[](UnsignedInt interval, void* param)->UnsignedInt{
|
|
static_cast<SaveTool*>(param)->checkGameState();
|
|
return interval;
|
|
}, this);
|
|
if(_gameCheckTimerId == 0) {
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", SDL_GetError(), window());
|
|
exit(EXIT_FAILURE);
|
|
return;
|
|
}
|
|
|
|
initialiseConfiguration();
|
|
|
|
switch(_framelimit) {
|
|
case Framelimit::Vsync:
|
|
setSwapInterval(1);
|
|
break;
|
|
case Framelimit::HalfVsync:
|
|
setSwapInterval(2);
|
|
break;
|
|
case Framelimit::FpsCap:
|
|
setSwapInterval(0);
|
|
setMinimalLoopPeriod(1000/_fpsCap);
|
|
break;
|
|
}
|
|
|
|
if(_checkUpdatesOnStartup) {
|
|
_updateThread = std::thread{[this]{ checkForUpdates(); }};
|
|
_queue.addToast(Toast::Type::Default, "Checking for updates..."_s);
|
|
}
|
|
|
|
if(GL::Context::current().isExtensionSupported<GL::Extensions::KHR::debug>() &&
|
|
GL::Context::current().detectedDriver() == GL::Context::DetectedDriver::NVidia)
|
|
{
|
|
GL::DebugOutput::setEnabled(GL::DebugOutput::Source::Api, GL::DebugOutput::Type::Other, {131185}, false);
|
|
}
|
|
|
|
if(_skipDisclaimer) {
|
|
_uiState = UiState::Initialising;
|
|
_initThread = std::thread{[this]{ initialiseManager(); }};
|
|
}
|
|
}
|
|
|
|
SaveTool::~SaveTool() {
|
|
SDL_RemoveTimer(_gameCheckTimerId);
|
|
|
|
_conf.setValue("cheat_mode"_s, _cheatMode);
|
|
_conf.setValue("unsafe_mode"_s, _unsafeMode);
|
|
_conf.setValue("startup_update_check"_s, _checkUpdatesOnStartup);
|
|
_conf.setValue("skip_disclaimer"_s, _skipDisclaimer);
|
|
|
|
switch(_framelimit) {
|
|
case Framelimit::Vsync:
|
|
_conf.setValue("frame_limit"_s, "vsync"_s);
|
|
break;
|
|
case Framelimit::HalfVsync:
|
|
_conf.setValue("frame_limit"_s, "half_vsync"_s);
|
|
break;
|
|
case Framelimit::FpsCap:
|
|
_conf.setValue<UnsignedInt>("frame_limit"_s, _fpsCap);
|
|
break;
|
|
}
|
|
|
|
_conf.save();
|
|
}
|
|
|
|
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{filename}.release();
|
|
if(action == efsw::Actions::Moved) {
|
|
event.user.data2 = Containers::String{old_filename}.release();
|
|
}
|
|
|
|
SDL_PushEvent(&event);
|
|
return;
|
|
}
|
|
|
|
void SaveTool::drawEvent() {
|
|
#ifdef SAVETOOL_DEBUG_BUILD
|
|
tweak.update();
|
|
#endif
|
|
|
|
GL::defaultFramebuffer.clear(GL::FramebufferClear::Color);
|
|
|
|
drawImGui();
|
|
|
|
swapBuffers();
|
|
redraw();
|
|
}
|
|
|
|
void SaveTool::viewportEvent(ViewportEvent& event) {
|
|
GL::defaultFramebuffer.setViewport({{}, event.framebufferSize()});
|
|
|
|
_imgui.relayout(event.windowSize());
|
|
}
|
|
|
|
void SaveTool::keyPressEvent(KeyEvent& event) {
|
|
if(_imgui.handleKeyPressEvent(event)) return;
|
|
}
|
|
|
|
void SaveTool::keyReleaseEvent(KeyEvent& event) {
|
|
if(_imgui.handleKeyReleaseEvent(event)) return;
|
|
}
|
|
|
|
void SaveTool::mousePressEvent(MouseEvent& event) {
|
|
if(_imgui.handleMousePressEvent(event)) return;
|
|
}
|
|
|
|
void SaveTool::mouseReleaseEvent(MouseEvent& event) {
|
|
if(_imgui.handleMouseReleaseEvent(event)) return;
|
|
}
|
|
|
|
void SaveTool::mouseMoveEvent(MouseMoveEvent& event) {
|
|
if(_imgui.handleMouseMoveEvent(event)) return;
|
|
}
|
|
|
|
void SaveTool::mouseScrollEvent(MouseScrollEvent& event) {
|
|
if(_imgui.handleMouseScrollEvent(event)) {
|
|
event.setAccepted();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void SaveTool::textInputEvent(TextInputEvent& event) {
|
|
if(_imgui.handleTextInputEvent(event)) return;
|
|
}
|
|
|
|
void SaveTool::anyEvent(SDL_Event& event) {
|
|
if(event.type == _initEventId) {
|
|
initEvent(event);
|
|
}
|
|
else if(event.type == _updateEventId) {
|
|
updateCheckEvent(event);
|
|
}
|
|
else if(event.type == _fileEventId) {
|
|
fileUpdateEvent(event);
|
|
}
|
|
}
|
|
|
|
void SaveTool::initEvent(SDL_Event& event) {
|
|
_initThread.join();
|
|
|
|
switch(event.user.code) {
|
|
case InitSuccess:
|
|
_uiState = UiState::ProfileManager;
|
|
ImGui::CloseCurrentPopup();
|
|
break;
|
|
case ProfileManagerFailure:
|
|
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();
|
|
|
|
cpr::Response r{std::move(*static_cast<cpr::Response*>(event.user.data1))};
|
|
delete static_cast<cpr::Response*>(event.user.data1);
|
|
|
|
if(r.elapsed > 10.0) {
|
|
_queue.addToast(Toast::Type::Error, "The request timed out."_s);
|
|
return;
|
|
}
|
|
|
|
if(r.status_code != 200) {
|
|
_queue.addToast(Toast::Type::Error, Utility::format("The request failed with error code {}: {}", r.status_code, r.reason.c_str()));
|
|
return;
|
|
}
|
|
|
|
using json = nlohmann::json;
|
|
|
|
json response = json::parse(r.text);
|
|
|
|
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, strlen(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);
|
|
}
|
|
Int major;
|
|
Int minor;
|
|
Int patch;
|
|
|
|
bool operator==(const Version& other) const {
|
|
return (major == other.major) && (minor == other.minor) && (patch == other.patch);
|
|
}
|
|
bool operator>(const Version& other) const {
|
|
return ( major * 10000 + minor * 100 + patch) >
|
|
(other.major * 10000 + other.minor * 100 + other.patch);
|
|
}
|
|
operator Containers::String() const {
|
|
return Utility::format("{}.{}.{}", major, minor, patch);
|
|
}
|
|
};
|
|
|
|
static const Version current_ver{SAVETOOL_VERSION};
|
|
|
|
for(auto& release : response) {
|
|
if(release["prerelease"] == true) {
|
|
continue;
|
|
}
|
|
|
|
Version latest_ver{to_string(release["tag_name"])};
|
|
|
|
if(latest_ver > current_ver || (latest_ver == current_ver && Utility::String::endsWith(SAVETOOL_VERSION, "-pre"))) {
|
|
_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 = to_string(release["html_url"]);
|
|
_downloadLink = to_string(release["assets"][0]["browser_download_url"]);
|
|
}
|
|
else if(latest_ver == current_ver || (current_ver > latest_ver && Utility::String::endsWith(SAVETOOL_VERSION, "-pre"))) {
|
|
_queue.addToast(Toast::Type::Success, "The application is already up to date."_s);
|
|
}
|
|
else if(current_ver > latest_ver) {
|
|
_queue.addToast(Toast::Type::Warning, "Your version is more recent than the latest one in the repo. How???"_s);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SaveTool::fileUpdateEvent(SDL_Event& event) {
|
|
if(event.user.code == StagedUpdate) {
|
|
_massManager->refreshStagedMasses();
|
|
return;
|
|
}
|
|
|
|
Containers::String filename{static_cast<char*>(event.user.data1), std::strlen(static_cast<char*>(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);
|
|
}
|
|
static bool is_moved_after_save = false;
|
|
if(event.user.code == FileMoved) {
|
|
old_filename = Containers::String{static_cast<char*>(event.user.data2), std::strlen(static_cast<char*>(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(!is_moved_after_save) {
|
|
is_moved_after_save = false;
|
|
_currentMass->setDirty();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case FileMoved:
|
|
if(is_unit) {
|
|
if(old_filename.hasSuffix(".tmp"_s)) {
|
|
is_moved_after_save = true;
|
|
return;
|
|
}
|
|
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() {
|
|
if(_conf.hasValue("cheat_mode"_s)) {
|
|
_cheatMode = _conf.value<bool>("cheat_mode"_s);
|
|
}
|
|
else {
|
|
_conf.setValue("cheat_mode"_s, _cheatMode);
|
|
}
|
|
|
|
if(_conf.hasValue("unsafe_mode"_s)) {
|
|
_unsafeMode = _conf.value<bool>("unsafe_mode"_s);
|
|
}
|
|
else {
|
|
_conf.setValue("unsafe_mode"_s, _unsafeMode);
|
|
}
|
|
|
|
if(_conf.hasValue("startup_update_check"_s)) {
|
|
_checkUpdatesOnStartup = _conf.value<bool>("startup_update_check"_s);
|
|
}
|
|
else {
|
|
_conf.setValue("startup_update_check"_s, _checkUpdatesOnStartup);
|
|
}
|
|
|
|
if(_conf.hasValue("skip_disclaimer"_s)) {
|
|
_skipDisclaimer = _conf.value<bool>("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() {
|
|
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<char*>(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<char*>(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<char*>(brand_font.data()), brand_font.size(), 16.0f, &icon_config, brand_range);
|
|
|
|
auto mono_font = _rs.getRaw("SourceCodePro-Regular.ttf"_s);
|
|
ImVector<ImWchar> 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<char*>(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);
|
|
}
|
|
|
|
void SaveTool::initialiseToolDirectories() {
|
|
_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::Path::make(_backupsDir);
|
|
}
|
|
|
|
if(!Utility::Path::exists(_stagingDir)) {
|
|
Utility::Path::make(_stagingDir);
|
|
}
|
|
|
|
//if(!Utility::Directory::exists(_armouryDir)) {
|
|
// Utility::Directory::mkpath(_armouryDir);
|
|
//}
|
|
|
|
//if(!Utility::Directory::exists(_armoursDir)) {
|
|
// Utility::Directory::mkpath(_armoursDir);
|
|
//}
|
|
|
|
//if(!Utility::Directory::exists(_weaponsDir)) {
|
|
// Utility::Directory::mkpath(_weaponsDir);
|
|
//}
|
|
|
|
//if(!Utility::Directory::exists(_stylesDir)) {
|
|
// Utility::Directory::mkpath(_stylesDir);
|
|
//}
|
|
}
|
|
|
|
auto SaveTool::findGameDataDirectory() -> bool {
|
|
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)
|
|
{
|
|
_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)) {
|
|
_lastError = _gameDataDir + " wasn't found. Make sure to play the game at least once."_s;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SaveTool::initialiseMassManager() {
|
|
_massManager.emplace(_saveDir, _currentProfile->account(), _currentProfile->isDemo(), _stagingDir);
|
|
initialiseFileWatcher();
|
|
}
|
|
|
|
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();
|
|
|
|
if(ImGui::GetIO().WantTextInput && !isTextInputActive()) {
|
|
startTextInput();
|
|
}
|
|
else if(!ImGui::GetIO().WantTextInput && isTextInputActive()) {
|
|
stopTextInput();
|
|
}
|
|
|
|
drawGui();
|
|
|
|
_imgui.updateApplicationCursor(*this);
|
|
|
|
_imgui.drawFrame();
|
|
}
|
|
|
|
void SaveTool::drawGui() {
|
|
drawMainMenu();
|
|
|
|
switch(_uiState) {
|
|
case UiState::Disclaimer:
|
|
drawDisclaimer();
|
|
break;
|
|
case UiState::Initialising:
|
|
drawInitialisation();
|
|
break;
|
|
case UiState::ProfileManager:
|
|
drawProfileManager();
|
|
break;
|
|
case UiState::MainManager:
|
|
drawManager();
|
|
break;
|
|
case UiState::MassViewer:
|
|
drawMassViewer();
|
|
break;
|
|
}
|
|
|
|
if(_aboutPopup) {
|
|
drawAbout();
|
|
}
|
|
|
|
#ifdef SAVETOOL_DEBUG_BUILD
|
|
if(_demoWindow) {
|
|
ImGui::ShowDemoWindow(&_demoWindow);
|
|
}
|
|
|
|
if(_styleEditor) {
|
|
ImGui::ShowStyleEditor(&ImGui::GetStyle());
|
|
}
|
|
|
|
if(_metricsWindow) {
|
|
ImGui::ShowMetricsWindow(&_metricsWindow);
|
|
}
|
|
#endif
|
|
|
|
_queue.draw(windowSize());
|
|
}
|
|
|
|
void SaveTool::drawDisclaimer() {
|
|
ImGui::SetNextWindowPos(ImVec2{Vector2{windowSize() / 2.0f}}, ImGuiCond_Always, center_pivot);
|
|
|
|
if(ImGui::Begin("Disclaimer##DisclaimerWindow", nullptr,
|
|
ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoBringToFrontOnFocus|
|
|
ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_MenuBar))
|
|
{
|
|
if(ImGui::BeginMenuBar()) {
|
|
ImGui::TextUnformatted("Disclaimer");
|
|
ImGui::EndMenuBar();
|
|
}
|
|
|
|
ImGui::TextUnformatted("Before you start using the app, there are a few things you should know:");
|
|
|
|
ImGui::PushTextWrapPos(windowSize().x() * 0.67f);
|
|
|
|
ImGui::Bullet();
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("For this application to work properly, it is recommended to disable Steam Cloud syncing for the game. To disable it, right-click the game in your Steam library, click \"Properties\", go to the \"General\" tab, and uncheck \"Keep game saves in the Steam Cloud for M.A.S.S. Builder\".");
|
|
|
|
ImGui::Bullet();
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("The developer of this application (Guillaume Jacquemin) isn't associated with Vermillion Digital, and both parties cannot be held responsible for data loss or corruption this app might cause. PLEASE USE AT YOUR OWN RISK!");
|
|
|
|
ImGui::Bullet();
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("This application is released under the terms of the GNU General Public Licence version 3. Please see the COPYING file for more details, or the About screen if you somehow didn't get that file with your download of the program.");
|
|
|
|
ImGui::Bullet();
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("This version of the application was tested on M.A.S.S. Builder early access version " SUPPORTED_GAME_VERSION ". It may or may not work with other versions of the game.");
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
if(ImGui::BeginTable("##DisclaimerLayoutTable", 3)) {
|
|
ImGui::TableSetupColumn("##Empty1", ImGuiTableColumnFlags_WidthStretch);
|
|
ImGui::TableSetupColumn("##Button", ImGuiTableColumnFlags_WidthFixed);
|
|
ImGui::TableSetupColumn("##Empty2", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
ImGui::TableNextRow();
|
|
ImGui::TableSetColumnIndex(0);
|
|
ImGui::Dummy({0.0f, 5.0f});
|
|
ImGui::Dummy({4.0f, 0.0f});
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Don't show next time", &_skipDisclaimer);
|
|
ImGui::TableSetColumnIndex(1);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {24.0f, 12.0f});
|
|
if(ImGui::Button("I understand the risks")) {
|
|
_uiState = UiState::Initialising;
|
|
_initThread = std::thread{[this]{ initialiseManager(); }};
|
|
}
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void SaveTool::drawInitialisation() {
|
|
ImGui::SetNextWindowPos(ImVec2{Vector2{windowSize() / 2.0f}}, ImGuiCond_Always, center_pivot);
|
|
|
|
if(ImGui::BeginPopupModal("##InitPopup", nullptr,
|
|
ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar))
|
|
{
|
|
ImGui::TextUnformatted("Initialising the manager. Please wait...");
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::OpenPopup("##InitPopup");
|
|
}
|
|
|
|
void SaveTool::drawGameState() {
|
|
ImGui::TextUnformatted("Game state:");
|
|
ImGui::SameLine();
|
|
{
|
|
switch(_gameState) {
|
|
case GameState::Unknown:
|
|
ImGui::TextColored(ImColor{0xff00a5ff}, ICON_FA_CIRCLE);
|
|
drawTooltip("unknown");
|
|
break;
|
|
case GameState::NotRunning:
|
|
ImGui::TextColored(ImColor{0xff32cd32}, ICON_FA_CIRCLE);
|
|
drawTooltip("not running");
|
|
break;
|
|
case GameState::Running:
|
|
ImGui::TextColored(ImColor{0xff0000ff}, ICON_FA_CIRCLE);
|
|
drawTooltip("running");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SaveTool::drawHelpMarker(Containers::StringView text, Float wrap_pos) {
|
|
ImGui::TextUnformatted(ICON_FA_QUESTION_CIRCLE);
|
|
drawTooltip(text, wrap_pos);
|
|
}
|
|
|
|
void SaveTool::drawTooltip(Containers::StringView text, Float wrap_pos) {
|
|
if(ImGui::IsItemHovered()){
|
|
ImGui::BeginTooltip();
|
|
if(wrap_pos > 0.0f) {
|
|
ImGui::PushTextWrapPos(wrap_pos);
|
|
}
|
|
ImGui::TextUnformatted(text.data());
|
|
if(wrap_pos > 0.0f) {
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
void SaveTool::openUri(Containers::StringView uri) {
|
|
ShellExecuteW(nullptr, nullptr, Utility::Unicode::widen(uri.data()), nullptr, nullptr, SW_SHOWDEFAULT);
|
|
}
|
|
|
|
void SaveTool::checkGameState() {
|
|
WTS_PROCESS_INFOW* process_infos = nullptr;
|
|
unsigned long process_count = 0;
|
|
|
|
if(WTSEnumerateProcessesW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &process_infos, &process_count)) {
|
|
Containers::ScopeGuard guard{process_infos, WTSFreeMemory};
|
|
|
|
for(unsigned long i = 0; i < process_count; ++i) {
|
|
if(std::wcscmp(process_infos[i].pProcessName, L"MASS_Builder-Win64-Shipping.exe") == 0) {
|
|
_gameState = GameState::Running;
|
|
break;
|
|
}
|
|
else {
|
|
_gameState = GameState::NotRunning;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
_gameState = GameState::Unknown;
|
|
}
|
|
}
|
|
|
|
void SaveTool::checkForUpdates() {
|
|
cpr::Response r = cpr::Get(cpr::Url{"https://williamjcm.ovh/git/api/v1/repos/williamjcm/MassBuilderSaveTool/releases"}, cpr::Timeout{10000});
|
|
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = _updateEventId;
|
|
event.user.code = r.status_code;
|
|
event.user.data1 = new cpr::Response{std::move(r)};
|
|
SDL_PushEvent(&event);
|
|
}
|