MassBuilderSaveTool/src/SaveTool/SaveTool.cpp

507 lines
18 KiB
C++

// MassBuilderSaveTool
// Copyright (C) 2021 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 <cstdarg>
#include <Corrade/Utility/FormatStl.h>
#include <Corrade/Utility/String.h>
#include <Corrade/Utility/Unicode.h>
#include <Magnum/GL/DefaultFramebuffer.h>
#include <Magnum/GL/Renderer.h>
#include <Magnum/ImGuiIntegration/Integration.h>
#include <Magnum/ImGuiIntegration/Context.hpp>
#include <windef.h>
#include <winuser.h>
#include <processthreadsapi.h>
#include <shellapi.h>
#include "../FontAwesome/IconsFontAwesome5.h"
#include "../FontAwesome/IconsFontAwesome5Brands.h"
extern const ImVec2 center_pivot = {0.5f, 0.5f};
#ifdef MANAGER_DEBUG_BUILD
#include <Corrade/Utility/Tweakable.h>
#define _tw CORRADE_TWEAKABLE
Utility::Tweakable tweak;
#endif
SaveTool::SaveTool(const Arguments& arguments):
Platform::Sdl2Application{arguments,
Configuration{}.setTitle("M.A.S.S. Builder Save Tool version rewrite-0.0.1")
.setSize({960, 720})}
{
#ifdef MANAGER_DEBUG_BUILD
tweak.enable("", "../../");
#endif
if(SDL_VERSION_ATLEAST(2, 0, 5)) {
if(SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1") == SDL_TRUE) {
Utility::Debug{} << "Clickthrough is available.";
}
else {
Utility::Debug{} << "Clickthrough is not available (hint couldn't be set).";
}
}
else {
Utility::Debug{} << "Clickthrough is not available (SDL2 is too old).";
}
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(1)) == std::uint32_t(-1)) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error",
"SDL_RegisterEvents failed in SaveTool::SaveTool(). Exiting...", nullptr);
exit(EXIT_FAILURE);
}
}
SaveTool::~SaveTool() {
SDL_RemoveTimer(_gameCheckTimerId);
}
void SaveTool::handleFileAction(efsw::WatchID watch_id,
const std::string& dir,
const std::string& filename,
efsw::Action action,
std::string old_filename)
{
if(watch_id == _watchIDs[StagingDir] && Utility::String::endsWith(filename, ".sav")) {
_massManager->refreshStagedMasses();
return;
}
if(Utility::String::endsWith(filename, "Config.sav")) {
return;
}
switch(action) {
case efsw::Actions::Add:
if(Utility::String::endsWith(filename, _currentProfile->steamId() + ".sav")) {
if(Utility::String::beginsWith(filename, Utility::formatString("{}Unit", _currentProfile->type() == ProfileType::Demo ? "Demo" : ""))) {
int index = ((filename[_currentProfile->type() == ProfileType::Demo ? 8 : 4] - 0x30) * 10) +
(filename[_currentProfile->type() == ProfileType::Demo ? 9 : 5] - 0x30);
_massManager->refreshHangar(index);
}
}
break;
case efsw::Actions::Delete:
if(Utility::String::endsWith(filename, _currentProfile->steamId() + ".sav")) {
if(Utility::String::beginsWith(filename, Utility::formatString("{}Unit", _currentProfile->type() == ProfileType::Demo ? "Demo" : ""))) {
int index = ((filename[_currentProfile->type() == ProfileType::Demo ? 8 : 4] - 0x30) * 10) +
(filename[_currentProfile->type() == ProfileType::Demo ? 9 : 5] - 0x30);
_massManager->refreshHangar(index);
}
}
break;
case efsw::Actions::Modified:
if(filename == _currentProfile->filename()) {
_currentProfile->refreshValues();
}
else if(Utility::String::endsWith(filename, _currentProfile->steamId() + ".sav")) {
if(Utility::String::beginsWith(filename, Utility::formatString("{}Unit", _currentProfile->type() == ProfileType::Demo ? "Demo" : ""))) {
int index = ((filename[_currentProfile->type() == ProfileType::Demo ? 8 : 4] - 0x30) * 10) +
(filename[_currentProfile->type() == ProfileType::Demo ? 9 : 5] - 0x30);
_massManager->refreshHangar(index);
}
}
break;
case efsw::Actions::Moved:
if(Utility::String::endsWith(filename, _currentProfile->steamId() + ".sav")) {
if(Utility::String::beginsWith(filename, Utility::formatString("{}Unit", _currentProfile->type() == ProfileType::Demo ? "Demo" : ""))) {
int index = ((filename[_currentProfile->type() == ProfileType::Demo ? 8 : 4] - 0x30) * 10) +
(filename[_currentProfile->type() == ProfileType::Demo ? 9 : 5] - 0x30);
_massManager->refreshHangar(index);
int old_index = ((old_filename[_currentProfile->type() == ProfileType::Demo ? 8 : 4] - 0x30) * 10) +
(old_filename[_currentProfile->type() == ProfileType::Demo ? 9 : 5] - 0x30);
_massManager->refreshHangar(old_index);
}
}
break;
default:
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "Unknown file watcher action type.", window());
break;
}
}
void SaveTool::drawEvent() {
#ifdef MANAGER_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);
}
}
void SaveTool::initEvent(SDL_Event& event) {
_thread.join();
switch(event.user.code) {
case InitSuccess:
_uiState = UiState::ProfileManager;
ImGui::CloseCurrentPopup();
SDL_InitSubSystem(SDL_INIT_TIMER);
_mbManager->checkGameState();
_gameCheckTimerId = SDL_AddTimer(2000,
[](std::uint32_t interval, void* param)->std::uint32_t{
static_cast<MassBuilderManager*>(param)->checkGameState();
return interval;
},
_mbManager.get());
if(_gameCheckTimerId == 0) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", SDL_GetError(), window());
}
break;
case MbManagerFailure:
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising MassBuilderManager", _mbManager->lastError().c_str(), window());
exit(EXIT_FAILURE);
break;
case ProfileManagerFailure:
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error initialising ProfileManager", _profileManager->lastError().c_str(), window());
exit(EXIT_FAILURE);
break;
default:
break;
}
}
void SaveTool::initialiseGui() {
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
auto reg_font = _rs.getRaw("SourceSansPro-Regular.ttf");
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");
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;
_mbManager.emplace();
if(!_mbManager->ready()) {
event.user.code = MbManagerFailure;
SDL_PushEvent(&event);
return;
}
_profileManager.emplace(_mbManager->saveDirectory());
if(!_profileManager->ready()) {
event.user.code = ProfileManagerFailure;
SDL_PushEvent(&event);
return;
}
event.user.code = InitSuccess;
SDL_PushEvent(&event);
}
void SaveTool::initialiseMassManager() {
_currentProfile->refreshValues();
_massManager.emplace(_profileManager->saveDirectory(),
_currentProfile->steamId(),
_currentProfile->type() == ProfileType::Demo);
initialiseFileWatcher();
}
void SaveTool::initialiseFileWatcher() {
_fileWatcher.emplace();
_watchIDs[SaveDir] = _fileWatcher->addWatch(_profileManager->saveDirectory(), this, false);
_watchIDs[StagingDir] = _fileWatcher->addWatch(_massManager->stagingAreaDirectory(), 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);
//GL::Renderer::enable(GL::Renderer::Feature::ScissorTest);
//GL::Renderer::disable(GL::Renderer::Feature::FaceCulling);
//GL::Renderer::disable(GL::Renderer::Feature::DepthTest);
_imgui.drawFrame();
//GL::Renderer::enable(GL::Renderer::Feature::DepthTest);
//GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);
//GL::Renderer::disable(GL::Renderer::Feature::ScissorTest);
}
void SaveTool::drawGui() {
drawMainMenu();
if(_uiState == UiState::Disclaimer) {
drawDisclaimer();
}
else if(_uiState == UiState::Initialising) {
drawInitialisation();
}
else if(_uiState == UiState::ProfileManager) {
drawProfileManager();
}
else if(_uiState == UiState::MainManager) {
drawManager();
}
if(_aboutPopup) {
drawAbout();
}
#ifdef MANAGER_DEBUG_BUILD
if(_demoWindow) {
ImGui::ShowDemoWindow(&_demoWindow);
}
if(_styleEditor) {
ImGui::ShowStyleEditor(&ImGui::GetStyle());
}
if(_metricsWindow) {
ImGui::ShowMetricsWindow(&_metricsWindow);
}
#endif
}
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_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 0.6.5. 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(1);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {24.0f, 12.0f});
if(ImGui::Button("I understand the risks")) {
_uiState = UiState::Initialising;
_thread = 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(_mbManager->gameState()) {
case GameState::Unknown:
ImGui::TextColored(ImColor{0xff00a5ff}, "unknown");
break;
case GameState::NotRunning:
ImGui::TextColored(ImColor{0xff32cd32}, "not running");
break;
case GameState::Running:
ImGui::TextColored(ImColor{0xff0000ff}, "running");
break;
}
}
}
void SaveTool::drawHelpMarker(const char* text, float wrap_pos) {
ImGui::TextUnformatted(ICON_FA_QUESTION_CIRCLE);
drawTooltip(text, wrap_pos);
}
void SaveTool::drawTooltip(const char* text, float wrap_pos) {
if(ImGui::IsItemHovered()){
ImGui::BeginTooltip();
if(wrap_pos > 0.0f) {
ImGui::PushTextWrapPos(wrap_pos);
}
ImGui::TextUnformatted(text);
if(wrap_pos > 0.0f) {
ImGui::PopTextWrapPos();
}
ImGui::EndTooltip();
}
}
void SaveTool::drawUnsafeText(const char* text, ...) {
va_list args;
va_start(args, text);
if(!_unsafeMode && _mbManager->gameState() != GameState::NotRunning) {
ImGui::TextDisabledV(text, args);
}
else {
ImGui::TextV(text, args);
}
va_end(args);
}
void SaveTool::openUri(const std::string& uri) {
ShellExecuteW(nullptr, nullptr, Utility::Unicode::widen(uri).c_str(), nullptr, nullptr, SW_SHOW);
}