MassBuilderSaveTool/src/Application/Application.cpp

456 lines
14 KiB
C++

// 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 <https://www.gnu.org/licenses/>.
#include <Corrade/Containers/ScopeGuard.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 <SDL.h>
#include <wtypesbase.h>
#include <shellapi.h>
#include <wtsapi32.h>
#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<Application*>(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::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(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;
}
}
}