// MassBuilderSaveTool // Copyright (C) 2021-2024 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 #include #include #include #include #include #include #include "../FontAwesome/IconsFontAwesome5.h" #include "../Configuration/Configuration.h" #include "../Logger/Logger.h" #include "Application.h" using namespace Containers::Literals; extern const ImVec2 center_pivot = {0.5f, 0.5f}; #ifdef SAVETOOL_DEBUG_BUILD Utility::Tweakable tweak; #endif namespace mbst { Application::Application(const Arguments& arguments): Platform::Sdl2Application{arguments, Configuration{}.setTitle("M.A.S.S. Builder Save Tool " SAVETOOL_VERSION_STRING " (\"" SAVETOOL_CODENAME "\")") .setSize({960, 720})} { #ifdef SAVETOOL_DEBUG_BUILD tweak.enable("", "../../"); #endif LOG_INFO_FORMAT("Framebuffer size: {}x{}", framebufferSize().x(), framebufferSize().y()); LOG_INFO_FORMAT("Window size: {}x{}", windowSize().x(), windowSize().y()); LOG_INFO_FORMAT("DPI scaling: {}x{}", dpiScaling().x(), dpiScaling().y()); LOG_INFO("Configuring OpenGL renderer."); GL::Renderer::enable(GL::Renderer::Feature::Blending); GL::Renderer::enable(GL::Renderer::Feature::ScissorTest); GL::Renderer::setBlendFunction(GL::Renderer::BlendFunction::SourceAlpha, GL::Renderer::BlendFunction::OneMinusSourceAlpha); GL::Renderer::setBlendEquation(GL::Renderer::BlendEquation::Add, GL::Renderer::BlendEquation::Add); LOG_INFO("Configuring SDL2."); #if SDL_VERSION_ATLEAST(2,0,5) if(SDL_SetHintWithPriority(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1", SDL_HINT_OVERRIDE) == SDL_TRUE) { LOG_INFO("Clickthrough is enabled."); } else { LOG_WARNING("Clickthrough is disabled."); } #else LOG_WARNING_FORMAT("Clickthrough is disabled: SDL2 version is too old ({}.{}.{})", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL); #endif LOG_INFO("Registering custom events."); if((_initEventId = SDL_RegisterEvents(3)) == std::uint32_t(-1)) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "SDL_RegisterEvents() failed in Application::SaveTool(). Exiting...", window()); exit(EXIT_FAILURE); return; } _updateEventId = _initEventId + 1; _fileEventId = _initEventId + 2; LOG_INFO("Initialising the timer subsystem."); if(SDL_InitSubSystem(SDL_INIT_TIMER) != 0) { LOG_ERROR(SDL_GetError()); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising the app", SDL_GetError(), window()); exit(EXIT_FAILURE); return; } initialiseGui(); checkGameState(); _gameCheckTimerId = SDL_AddTimer(2000, [](std::uint32_t interval, void* param)->std::uint32_t{ static_cast(param)->checkGameState(); return interval; }, this); if(_gameCheckTimerId == 0) { LOG_ERROR(SDL_GetError()); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", SDL_GetError(), window()); exit(EXIT_FAILURE); return; } initialiseConfiguration(); if(conf().checkUpdatesOnStartup()) { _queue.addToast(Toast::Type::Default, "Checking for updates..."_s); _updateThread = std::thread{[this]{ checkForUpdates(); }}; } if(GL::Context::current().isExtensionSupported() && GL::Context::current().detectedDriver() == GL::Context::DetectedDriver::NVidia) { GL::DebugOutput::setEnabled(GL::DebugOutput::Source::Api, GL::DebugOutput::Type::Other, {131185}, false); } if(conf().skipDisclaimer()) { _uiState = UiState::Initialising; _initThread = std::thread{[this]{ initialiseManager(); }}; } _timeline.start(); } Application::~Application() { LOG_INFO("Cleaning up."); SDL_RemoveTimer(_gameCheckTimerId); LOG_INFO("Saving the configuration."); conf().save(); LOG_INFO("Exiting."); } void Application::drawEvent() { #ifdef SAVETOOL_DEBUG_BUILD tweak.update(); #endif GL::defaultFramebuffer.clear(GL::FramebufferClear::Color); drawImGui(); swapBuffers(); if(conf().swapInterval() == 0 && conf().fpsCap() < 301.0f) { while(_timeline.currentFrameDuration() < (1.0f / conf().fpsCap())); } redraw(); _timeline.nextFrame(); } void Application::viewportEvent(ViewportEvent& event) { GL::defaultFramebuffer.setViewport({{}, event.framebufferSize()}); const auto size = Vector2{windowSize()}/dpiScaling(); _imgui.relayout(size, windowSize(), framebufferSize()); } void Application::keyPressEvent(KeyEvent& event) { if(_imgui.handleKeyPressEvent(event)) return; } void Application::keyReleaseEvent(KeyEvent& event) { if(_imgui.handleKeyReleaseEvent(event)) return; } void Application::mousePressEvent(MouseEvent& event) { if(_imgui.handleMousePressEvent(event)) return; } void Application::mouseReleaseEvent(MouseEvent& event) { if(_imgui.handleMouseReleaseEvent(event)) return; } void Application::mouseMoveEvent(MouseMoveEvent& event) { if(_imgui.handleMouseMoveEvent(event)) return; } void Application::mouseScrollEvent(MouseScrollEvent& event) { if(_imgui.handleMouseScrollEvent(event)) { event.setAccepted(); return; } } void Application::textInputEvent(TextInputEvent& event) { if(_imgui.handleTextInputEvent(event)) return; } void Application::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 Application::drawImGui() { _imgui.newFrame(); if(ImGui::GetIO().WantTextInput && !isTextInputActive()) { startTextInput(); } else if(!ImGui::GetIO().WantTextInput && isTextInputActive()) { stopTextInput(); } drawGui(); _imgui.updateApplicationCursor(*this); _imgui.drawFrame(); } void Application::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 Application::drawDisclaimer() { ImGui::SetNextWindowPos(ImVec2{Vector2{windowSize() / 2.0f} / dpiScaling()}, 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(float(windowSize().x()) * 0.67f); ImGui::Bullet(); ImGui::SameLine(); ImGui::TextUnformatted(R"(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 " SAVETOOL_SUPPORTED_GAME_VERSION ". It may or may not work with other versions of the game."); if(conf().isRunningInWine()) { ImGui::Bullet(); ImGui::SameLine(); ImGui::TextUnformatted("You are currently running this application in Wine/Proton. It hasn't been fully tested, so some issues may arise. Furthermore, features may be unavailable."); } 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(); if(drawCheckbox("Don't show next time", conf().skipDisclaimer())) { conf().setSkipDisclaimer(!conf().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 Application::drawInitialisation() { ImGui::SetNextWindowPos(ImVec2{Vector2{windowSize() / 2.0f} / dpiScaling()}, 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 Application::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 Application::drawHelpMarker(Containers::StringView text, float wrap_pos) { ImGui::TextUnformatted(ICON_FA_QUESTION_CIRCLE); drawTooltip(text, wrap_pos); } void Application::drawTooltip(Containers::StringView text, float wrap_pos) { if(ImGui::IsItemHovered() && ImGui::BeginTooltip()) { if(wrap_pos > 0.0f) { ImGui::PushTextWrapPos(wrap_pos); } ImGui::TextUnformatted(text.cbegin(), text.cend()); if(wrap_pos > 0.0f) { ImGui::PopTextWrapPos(); } ImGui::EndTooltip(); } } bool Application::drawCheckbox(Containers::StringView label, bool value) { return ImGui::Checkbox(label.data(), &value); } void Application::openUri(Containers::StringView uri) { if(!conf().isRunningInWine()) { ShellExecuteA(nullptr, nullptr, uri.data(), nullptr, nullptr, SW_SHOWDEFAULT); } else { std::system(Utility::format("winebrowser.exe {}", uri).cbegin()); } } void Application::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; } } }