diff --git a/CMakeLists.txt b/CMakeLists.txt index d638b2e..627076e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,8 @@ add_executable(wxMASSManager WIN32 GUI/MainFrame.cpp GUI/EvtMainFrame.h GUI/EvtMainFrame.cpp + MassManager/MassManager.h + MassManager/MassManager.cpp resource.rc) target_link_libraries(wxMASSManager PRIVATE diff --git a/GUI/EvtMainFrame.cpp b/GUI/EvtMainFrame.cpp index 58c96cc..28c15b8 100644 --- a/GUI/EvtMainFrame.cpp +++ b/GUI/EvtMainFrame.cpp @@ -14,43 +14,26 @@ // 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 -#define WIN32_LEAN_AND_MEAN -#include -#include - -#include +#include #include -#include -#include -#include #include "EvtMainFrame.h" using namespace Corrade; -constexpr unsigned char mass_name_locator[] = { 'N', 'a', 'm', 'e', '_', '4', '5', '_', 'A', '0', '3', '7', 'C', '5', 'D', '5', '4', 'E', '5', '3', '4', '5', '6', '4', '0', '7', 'B', 'D', 'F', '0', '9', '1', '3', '4', '4', '5', '2', '9', 'B', 'B', '\0', 0x0C, '\0', '\0', '\0', 'S', 't', 'r', 'P', 'r', 'o', 'p', 'e', 'r', 't', 'y', '\0' }; - -constexpr unsigned char steamid_locator[] = { 'A', 'c', 'c', 'o', 'u', 'n', 't', '\0', 0x0C, '\0', '\0', '\0', 'S', 't', 'r', 'P', 'r', 'o', 'p', 'e', 'r', 't', 'y', '\0' }; - -constexpr unsigned char active_slot_locator[] = { 'A', 'c', 't', 'i', 'v', 'e', 'F', 'r', 'a', 'm', 'e', 'S', 'l', 'o', 't', '\0', 0x0C, '\0', '\0', '\0', 'I', 'n', 't', 'P', 'r', 'o', 'p', 'e', 'r', 't', 'y', '\0' }; - EvtMainFrame::EvtMainFrame(wxWindow* parent): MainFrame(parent) { SetIcon(wxIcon("MAINICON")); - getSaveDirectory(); - getLocalSteamId(); + if(!_manager.ready()) { + errorMessage("There was an issue initialising the manager:\n\n" + _manager.lastError()); + return; + } + initialiseListView(); isGameRunning(); @@ -62,13 +45,12 @@ EvtMainFrame::EvtMainFrame(wxWindow* parent): MainFrame(parent) { warningMessage(wxString::FromUTF8("Before you start using this app, a few things you should know:\n\n" "For this application to work properly, Steam Cloud syncing needs to be disabled for the game.\nTo disable it, right-click the game in your Steam library, click \"Properties\", go to the \"Updates\" tab, and uncheck \"Enable Steam Cloud synchronization for M.A.S.S. Builder\".\n\n" - "Please avoid using this application while the game is running. Bad Thingsā„¢ could happen to your data.\n\n" "DISCLAIMER: The developer of this application cannot be held responsible for data loss or corruption. PLEASE USE AT YOUR OWN RISK!\n\n" "Last but not least, this application is released under the terms of the GNU General Public Licence version 3. Please see the COPYING file for more details.")); _watcher.Connect(wxEVT_FSWATCHER, wxFileSystemWatcherEventHandler(EvtMainFrame::fileUpdateEvent), nullptr, this); - _watcher.AddTree(wxFileName(Utility::Directory::toNativeSeparators(_saveDirectory), wxPATH_WIN), - wxFSW_EVENT_CREATE|wxFSW_EVENT_DELETE|wxFSW_EVENT_MODIFY|wxFSW_EVENT_RENAME, wxString::Format("*%s.sav", _localSteamId)); + _watcher.AddTree(wxFileName(Utility::Directory::toNativeSeparators(_manager.saveDirectory()), wxPATH_WIN), + wxFSW_EVENT_CREATE|wxFSW_EVENT_DELETE|wxFSW_EVENT_MODIFY|wxFSW_EVENT_RENAME, wxString::Format("*%s.sav", _manager.steamId())); _gameCheckTimer.Start(3000); } @@ -81,11 +63,19 @@ EvtMainFrame::~EvtMainFrame() { _watcher.Disconnect(wxEVT_FSWATCHER, wxFileSystemWatcherEventHandler(EvtMainFrame::fileUpdateEvent), nullptr, this); } -void EvtMainFrame::importEvent(wxCommandEvent&) { - std::string slot_state = _installedListView->GetItemText(_installedListView->GetFirstSelected(), 1).ToStdString(); +bool EvtMainFrame::ready() { + return _manager.ready(); +} - if(slot_state != "" && slot_state != "" && - wxMessageBox(wxString::Format("Hangar %.2d is already occupied by the M.A.S.S. named \"%s\". Are you sure you want to import a M.A.S.S. to this hangar ?", _installedListView->GetFirstSelected() + 1, slot_state), +void EvtMainFrame::importEvent(wxCommandEvent&) { + const static std::string error_prefix = "Importing failed:\n\n"; + + long selected_hangar = _installedListView->GetFirstSelected(); + HangarState hangar_state = _manager.hangarState(selected_hangar); + + if(hangar_state == HangarState::Filled && + wxMessageBox(wxString::Format("Hangar %.2d is already occupied by the M.A.S.S. named \"%s\". Are you sure you want to import a M.A.S.S. to this hangar ?", + selected_hangar + 1, *(_manager.massName(selected_hangar))), "Question", wxYES_NO|wxCENTRE|wxICON_QUESTION, this) == wxNO) { return; } @@ -98,132 +88,107 @@ void EvtMainFrame::importEvent(wxCommandEvent&) { const std::string source_file = dialog.GetPath().ToUTF8().data(); - const std::string mass_name = getMassName(source_file); + Containers::Optional mass_name = _manager.getMassName(source_file); - if(mass_name == "") { + if(!mass_name) { + errorMessage(error_prefix + _manager.lastError()); return; } - if(wxMessageBox(wxString::Format("Are you sure you want to import the M.A.S.S. named \"%s\" to hangar %.2d ?", mass_name, _installedListView->GetFirstSelected() + 1), + if(wxMessageBox(wxString::Format("Are you sure you want to import the M.A.S.S. named \"%s\" to hangar %.2d ?", *mass_name, selected_hangar + 1), "Question", wxYES_NO|wxCENTRE|wxICON_QUESTION, this) == wxNO) { return; } - if(_isGameRunning) { - errorMessage("The game is running. Aborting..."); - return; - } - - const std::string dest_file = _saveDirectory + Utility::formatString("/Unit{:.2d}{}.sav", _installedListView->GetFirstSelected(), _localSteamId); - - if(Utility::Directory::exists(dest_file)) { - Utility::Directory::rm(dest_file); - } - - Utility::Directory::copy(source_file, dest_file); - - { - auto mmap = Utility::Directory::map(dest_file); - - auto iter = std::search(mmap.begin(), mmap.end(), &steamid_locator[0], &steamid_locator[23]); - - if(iter == mmap.end()) { - errorMessage("Couldn't find the SteamID in the unit file at " + source_file + ". Aborting..."); - Utility::Directory::rm(dest_file); - return; - } - - iter += 37; - - for(int i = 0; i < 17; ++i) { - *(iter + i) = _localSteamId[i]; - } + switch(_manager.gameState()) { + case GameState::Unknown: + errorMessage(error_prefix + "For security reasons, importing is disabled if the game's status is unknown."); + break; + case GameState::NotRunning: + if(!_manager.importMass(source_file, selected_hangar)) { + errorMessage(error_prefix + _manager.lastError()); + } + break; + case GameState::Running: + errorMessage(error_prefix + "Importing a M.A.S.S. is disabled while the game is running."); + break; } } void EvtMainFrame::moveEvent(wxCommandEvent&) { + const static std::string error_prefix = "Move failed:\n\n"; + long source_slot = _installedListView->GetFirstSelected(); - long choice = wxGetNumberFromUser(wxString::Format("Which hangar do you want to move the M.A.S.S. named \"%s\" to ?\nNotes:\n- If the destination hangar is the same as the source, nothing will happen.\n- If the destination already contains a M.A.S.S., the two will be swapped.\n- If the destination contains invalid data, it will be cleared first.", _installedListView->GetItemText(source_slot, 1)), + long choice = wxGetNumberFromUser(wxString::Format("Which hangar do you want to move the M.A.S.S. named \"%s\" to ?\nNotes:\n" + "- If the destination hangar is the same as the source, nothing will happen.\n" + "- If the destination already contains a M.A.S.S., the two will be swapped.\n" + "- If the destination contains invalid data, it will be cleared first.", + *(_manager.massName(source_slot))), "Slot", "Choose a slot", source_slot + 1, 1, 32, this) - 1; if(choice == -1 || choice == source_slot) { return; } - if(_isGameRunning) { - errorMessage("The game is running. Aborting..."); - return; - } - - std::string orig_file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, source_slot, _localSteamId); - std::string dest_status = _installedListView->GetItemText(choice, 1).ToStdString(); - std::string dest_file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, choice, _localSteamId); - - if(dest_status == "") { - Utility::Directory::rm(dest_file); - } - else if(dest_status != "") { - Utility::Directory::move(dest_file, dest_file + ".tmp"); - } - - Utility::Directory::move(orig_file, dest_file); - - if(dest_status != "") { - Utility::Directory::move(dest_file + ".tmp", orig_file); + switch(_manager.gameState()) { + case GameState::Unknown: + errorMessage(error_prefix + "For security reasons, moving a M.A.S.S. is disabled if the game's status is unknown."); + break; + case GameState::NotRunning: + if(!_manager.moveMass(source_slot, choice)) { + errorMessage(error_prefix + _manager.lastError()); + } + break; + case GameState::Running: + errorMessage(error_prefix + "Moving a M.A.S.S. is disabled while the game is running."); + break; } } void EvtMainFrame::deleteEvent(wxCommandEvent&) { + const static std::string error_prefix = "Deletion failed:\n\n"; + if(wxMessageBox(wxString::Format("Are you sure you want to delete the data in hangar %.2d ? This operation cannot be undone.", _installedListView->GetFirstSelected() + 1), "Are you sure ?", wxYES_NO|wxCENTRE|wxICON_QUESTION, this) == wxNO) { return; } - if(_isGameRunning) { - errorMessage("The game is running. Aborting..."); - return; - } - - std::string file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, _installedListView->GetFirstSelected(), _localSteamId); - - if(Utility::Directory::exists(file)) { - Utility::Directory::rm(file); + switch(_manager.gameState()) { + case GameState::Unknown: + errorMessage(error_prefix + "For security reasons, deleting a M.A.S.S. is disabled if the game's status is unknown."); + break; + case GameState::NotRunning: + if(!_manager.deleteMass(_installedListView->GetFirstSelected())) { + errorMessage(error_prefix + _manager.lastError()); + } + break; + case GameState::Running: + errorMessage(error_prefix + "Deleting a M.A.S.S. is disabled while the game is running."); + break; } } void EvtMainFrame::backupEvent(wxCommandEvent&) { + const static std::string error_prefix = "Backup failed:\n\n"; + wxString current_timestamp = wxDateTime::Now().Format("%Y-%m-%d_%H-%M-%S"); - wxFileDialog save_dialog{this, "Choose output location", _saveDirectory, - wxString::Format("backup_%s_%s.zip", _localSteamId, current_timestamp), "Zip archive (*zip)|*zip", + wxFileDialog save_dialog{this, "Choose output location", _manager.saveDirectory(), + wxString::Format("backup_%s_%s.zip", _manager.steamId(), current_timestamp), "Zip archive (*zip)|*zip", wxFD_SAVE|wxFD_OVERWRITE_PROMPT}; if(save_dialog.ShowModal() == wxID_CANCEL) { return; } - wxFFileOutputStream out{save_dialog.GetPath()}; - wxZipOutputStream zip{out}; - - { - zip.PutNextEntry(wxString::Format("Profile%s.sav", _localSteamId)); - wxFFileInputStream profile_stream{wxString::Format("%s\\Profile%s.sav", Utility::Directory::toNativeSeparators(_saveDirectory), _localSteamId), "rb"}; - zip.Write(profile_stream); - } - - for(int i = 0; i < 32; ++i) { - std::string unit_file = Utility::formatString("Unit{:.2d}{}.sav", i, _localSteamId); - if(Utility::Directory::exists(Utility::Directory::join(_saveDirectory, unit_file))) { - zip.PutNextEntry(unit_file); - wxFFileInputStream unit_stream{Utility::Directory::toNativeSeparators(Utility::Directory::join(_saveDirectory, unit_file))}; - zip.Write(unit_stream); - } + if(!_manager.backupSaves(save_dialog.GetPath().ToStdString())) { + errorMessage(error_prefix + _manager.lastError()); } } void EvtMainFrame::openSaveDirEvent(wxCommandEvent&) { - wxExecute("explorer.exe " + Utility::Directory::toNativeSeparators(_saveDirectory)); + wxExecute("explorer.exe " + Utility::Directory::toNativeSeparators(_manager.saveDirectory())); } void EvtMainFrame::installedSelectionEvent(wxListEvent&) { @@ -236,37 +201,71 @@ void EvtMainFrame::listColumnDragEvent(wxListEvent& event) { void EvtMainFrame::fileUpdateEvent(wxFileSystemWatcherEvent& event) { int event_type = event.GetChangeType(); + wxString event_file = event.GetPath().GetFullName(); if(event_type == wxFSW_EVENT_MODIFY && _lastWatcherEventType == wxFSW_EVENT_RENAME) { _lastWatcherEventType = event_type; return; } - _lastWatcherEventType = event_type; - - wxString event_file = event.GetNewPath().GetFullName(); - - if(!event_file.EndsWith(".sav")) { - return; - } - wxMilliSleep(50); - if(event_file == wxString::Format("Profile%s.sav", _localSteamId)) { - getActiveSlot(); - } - else { - wxRegEx regex(wxString::Format("Unit([0-3][0-9])%s.sav", _localSteamId), wxRE_ADVANCED); + wxRegEx regex; - if(regex.Matches(event_file)) { - long slot; + switch (event_type) { + case wxFSW_EVENT_CREATE: + case wxFSW_EVENT_DELETE: + regex.Compile(wxString::Format("Unit([0-3][0-9])%s\\.sav", _manager.steamId()), wxRE_ADVANCED); + if(regex.Matches(event_file)) { + long slot; - if(regex.GetMatch(event_file, 1).ToLong(&slot)) { - _installedListView->SetItem(slot, 1, getSlotMassName(slot)); + if(regex.GetMatch(event_file, 1).ToLong(&slot) && slot >= 0 && slot < 32) { + refreshHangar(slot); + } } - } + break; + case wxFSW_EVENT_MODIFY: + if(_lastWatcherEventType == wxFSW_EVENT_RENAME) { + break; + } + if(event_file == _manager.profileSaveName()) { + getActiveSlot(); + } + else { + regex.Compile(wxString::Format("Unit([0-3][0-9])%s\\.sav", _manager.steamId()), wxRE_ADVANCED); + if(regex.Matches(event_file)) { + long slot; + + if(regex.GetMatch(event_file, 1).ToLong(&slot) && slot >= 0 && slot < 32) { + refreshHangar(slot); + } + } + } + break; + case wxFSW_EVENT_RENAME: + wxString new_name = event.GetNewPath().GetFullName(); + + long slot; + if(regex.Compile(wxString::Format("Unit([0-3][0-9])%s\\.sav\\.tmp", _manager.steamId()), wxRE_ADVANCED), regex.Matches(new_name)) { + if(regex.GetMatch(new_name, 1).ToLong(&slot) && slot >= 0 && slot < 32) { + refreshHangar(slot); + } + } + else if(regex.Compile(wxString::Format("Unit([0-3][0-9])%s\\.sav", _manager.steamId()), wxRE_ADVANCED), regex.Matches(new_name)) { + if(regex.GetMatch(new_name, 1).ToLong(&slot) && slot >= 0 && slot < 32) { + refreshHangar(slot); + if(regex.Matches(event_file)) { + if(regex.GetMatch(event_file, 1).ToLong(&slot) && slot >= 0 && slot < 32) { + refreshHangar(slot); + } + } + } + } + break; } + _lastWatcherEventType = event_type; + updateCommandsState(); } @@ -274,43 +273,6 @@ void EvtMainFrame::gameCheckTimerEvent(wxTimerEvent&) { isGameRunning(); } -void EvtMainFrame::getSaveDirectory() { - wchar_t h[MAX_PATH]; - if(!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, h))) { - errorMessage("Couldn't get the path for %LOCALAPPDATA%. :/"); - Close(); - return; - } - - _saveDirectory = Utility::Directory::join(Utility::Directory::fromNativeSeparators(Utility::Unicode::narrow(h)), "MASS_Builder/Saved/SaveGames"); - - if(!Utility::Directory::exists(_saveDirectory)) { - errorMessage("Couldn't find the M.A.S.S. Builder save directory at " + _saveDirectory + ". Please run the game at least once to create it."); - Close(); - } -} - -void EvtMainFrame::getLocalSteamId() { - std::vector listing = Utility::Directory::list(_saveDirectory); - - wxRegEx regex; - if(!regex.Compile("Profile([0-9]{17}).sav", wxRE_ADVANCED)) { - errorMessage("Couldn't compile the regex."); - Close(); - return; - } - - for(const std::string& s : listing) { - if(regex.Matches(s)) { - _localSteamId = regex.GetMatch(s, 1); - return; - } - } - - errorMessage("Couldn't find your save files. Please play at least once."); - Close(); -} - void EvtMainFrame::initialiseListView() { for(long i = 0; i < 32; i++) { _installedListView->InsertItem(i, wxString::Format("%.2i", i + 1)); @@ -325,110 +287,76 @@ void EvtMainFrame::initialiseListView() { } void EvtMainFrame::isGameRunning() { - WTS_PROCESS_INFOW* process_infos = nullptr; - unsigned long process_count = 0; + GameState state = _manager.checkGameState(); - if(WTSEnumerateProcessesW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &process_infos, &process_count)) { - for(unsigned long i = 0; i < process_count; ++i) { - if(std::wcscmp(process_infos[i].pProcessName, L"MASS_Builder-Win64-Shipping.exe") == 0) { - _isGameRunning = true; - break; - } - else { - _isGameRunning = false; - } - } - - if(_isGameRunning) { - _gameStatus->SetLabel("running"); - _gameStatus->SetForegroundColour(wxColour("red")); - } - else { + switch(state) { + case GameState::Unknown: + _gameStatus->SetLabel("unknown"); + _gameStatus->SetForegroundColour(wxColour("orange")); + break; + case GameState::NotRunning: _gameStatus->SetLabel("not running"); _gameStatus->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_CAPTIONTEXT)); - } - } - else { - _isGameRunning = false; - _gameStatus->SetLabel("unknown"); - _gameStatus->SetForegroundColour(wxColour("orange")); - } - - if(process_infos != nullptr) { - WTSFreeMemory(process_infos); - process_infos = nullptr; + break; + case GameState::Running: + _gameStatus->SetLabel("running"); + _gameStatus->SetForegroundColour(wxColour("red")); + break; } updateCommandsState(); } void EvtMainFrame::refreshListView() { - for(long i = 0; i < 32; i++) { - _installedListView->SetItem(i, 1, getSlotMassName(i)); + for(int i = 0; i < 32; i++) { + refreshHangar(i); } updateCommandsState(); } void EvtMainFrame::getActiveSlot() { - auto mmap = Utility::Directory::mapRead(Utility::formatString("{}/Profile{}.sav", _saveDirectory, _localSteamId)); + char slot = _manager.activeSlot(); - auto iter = std::search(mmap.begin(), mmap.end(), &active_slot_locator[0], &active_slot_locator[31]); - - wxFont tmp_font = _installedListView->GetItemFont(_activeSlot); + wxFont tmp_font = _installedListView->GetItemFont(slot); tmp_font.SetWeight(wxFONTWEIGHT_NORMAL); - _installedListView->SetItemFont(_activeSlot, tmp_font); + _installedListView->SetItemFont(slot, tmp_font); - if(iter == mmap.end()) { - if(std::strncmp(&mmap[0x3F6], "Credit", 6) == 0) { - _activeSlot = 0; - } - else { - _activeSlot = -1; - } - } - else { - _activeSlot = *(iter + 41); - } + slot = _manager.getActiveSlot(); - if(_activeSlot != -1) { - _installedListView->SetItemFont(_activeSlot, _installedListView->GetItemFont(_activeSlot).Bold()); + if(slot != -1) { + _installedListView->SetItemFont(slot, _installedListView->GetItemFont(slot).Bold()); } } void EvtMainFrame::updateCommandsState() { long selection = _installedListView->GetFirstSelected(); - wxString state = ""; - if(selection != -1) { - state = _installedListView->GetItemText(selection, 1); - } + GameState game_state = _manager.gameState(); + HangarState hangar_state = _manager.hangarState(selection); - _importButton->Enable(selection != -1 && !_isGameRunning); - _moveButton->Enable(selection != -1 && !_isGameRunning && state != "" && state != ""); - _deleteButton->Enable(selection != -1 && !_isGameRunning && state != ""); + _importButton->Enable(selection != -1 && game_state != GameState::Running); + _moveButton->Enable(selection != -1 && game_state != GameState::Running && hangar_state != HangarState::Empty && hangar_state != HangarState::Invalid); + _deleteButton->Enable(selection != -1 && game_state != GameState::Running && hangar_state != HangarState::Empty); } -std::string EvtMainFrame::getSlotMassName(int index) { - std::string unit_file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, index, _localSteamId); - if(Utility::Directory::exists(unit_file)) { - std::string mass_name = getMassName(unit_file); - return (mass_name == "" ? "" : mass_name); - } - else { - return ""; - } -} - -std::string EvtMainFrame::getMassName(const std::string& filename) { - auto mmap = Utility::Directory::mapRead(filename); - - auto iter = std::search(mmap.begin(), mmap.end(), &mass_name_locator[0], &mass_name_locator[56]); - - if(iter == mmap.end()) { - return ""; +void EvtMainFrame::refreshHangar(int slot) { + if(slot < 0 && slot >= 32) { + return; } - return iter + 70; + _manager.refreshHangar(slot); + + switch(_manager.hangarState(slot)) { + case HangarState::Empty: + _installedListView->SetItem(slot, 1, ""); + break; + case HangarState::Invalid: + _installedListView->SetItem(slot, 1, ""); + break; + case HangarState::Filled: + _installedListView->SetItem(slot, 1, *(_manager.massName(slot))); + break; + } } void EvtMainFrame::infoMessage(const wxString& message) { diff --git a/GUI/EvtMainFrame.h b/GUI/EvtMainFrame.h index d6d3c0a..6d3d707 100644 --- a/GUI/EvtMainFrame.h +++ b/GUI/EvtMainFrame.h @@ -21,6 +21,8 @@ #include +#include "../MassManager/MassManager.h" + #include "MainFrame.h" class EvtMainFrame: public MainFrame { @@ -28,6 +30,8 @@ class EvtMainFrame: public MainFrame { EvtMainFrame(wxWindow* parent); ~EvtMainFrame(); + auto ready() -> bool; + protected: void importEvent(wxCommandEvent&); void moveEvent(wxCommandEvent&); @@ -40,24 +44,18 @@ class EvtMainFrame: public MainFrame { void gameCheckTimerEvent(wxTimerEvent&); private: - void getSaveDirectory(); - void getLocalSteamId(); void initialiseListView(); void isGameRunning(); void refreshListView(); void getActiveSlot(); void updateCommandsState(); - std::string getSlotMassName(int index); - std::string getMassName(const std::string& filename); + void refreshHangar(int slot); void infoMessage(const wxString& message); void warningMessage(const wxString& message); void errorMessage(const wxString& message); - std::string _saveDirectory; - std::string _localSteamId; - bool _isGameRunning = false; - char _activeSlot = 0; + MassManager _manager; wxFileSystemWatcher _watcher; int _lastWatcherEventType = 0; diff --git a/MassManager/MassManager.cpp b/MassManager/MassManager.cpp new file mode 100644 index 0000000..fbc7565 --- /dev/null +++ b/MassManager/MassManager.cpp @@ -0,0 +1,347 @@ +// wxMASSManager +// Copyright (C) 2020 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 + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include +#include +#include + +#include "MassManager.h" + +constexpr unsigned char mass_name_locator[] = { 'N', 'a', 'm', 'e', '_', '4', '5', '_', 'A', '0', '3', '7', 'C', '5', 'D', '5', '4', 'E', '5', '3', '4', '5', '6', '4', '0', '7', 'B', 'D', 'F', '0', '9', '1', '3', '4', '4', '5', '2', '9', 'B', 'B', '\0', 0x0C, '\0', '\0', '\0', 'S', 't', 'r', 'P', 'r', 'o', 'p', 'e', 'r', 't', 'y', '\0' }; + +constexpr unsigned char steamid_locator[] = { 'A', 'c', 'c', 'o', 'u', 'n', 't', '\0', 0x0C, '\0', '\0', '\0', 'S', 't', 'r', 'P', 'r', 'o', 'p', 'e', 'r', 't', 'y', '\0' }; + +constexpr unsigned char active_slot_locator[] = { 'A', 'c', 't', 'i', 'v', 'e', 'F', 'r', 'a', 'm', 'e', 'S', 'l', 'o', 't', '\0', 0x0C, '\0', '\0', '\0', 'I', 'n', 't', 'P', 'r', 'o', 'p', 'e', 'r', 't', 'y', '\0' }; + +MassManager::MassManager() { + _ready = findSaveDirectory() && findSteamId(); + + if(!_ready) { + return; + } + + _profileSaveName = Utility::formatString("Profile{}.sav", _steamId); + + for(int i = 0; i < 32; ++i) { + _hangars[i]._filename = Utility::formatString("Unit{:.2d}{}.sav", i, _steamId); + refreshHangar(i); + } +} + +auto MassManager::ready() -> bool { + return _ready; +} + +auto MassManager::lastError() -> std::string const& { + return _lastError; +} + +auto MassManager::saveDirectory() -> std::string const& { + return _saveDirectory; +} + +auto MassManager::steamId() -> std::string const& { + return _steamId; +} + +auto MassManager::profileSaveName() -> std::string const& { + return _profileSaveName; +} + +auto MassManager::checkGameState() -> GameState { + WTS_PROCESS_INFOW* process_infos = nullptr; + unsigned long process_count = 0; + + if(WTSEnumerateProcessesW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &process_infos, &process_count)) { + 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; + } + + if(process_infos != nullptr) { + WTSFreeMemory(process_infos); + process_infos = nullptr; + } + + return _gameState; +} + +auto MassManager::gameState() -> GameState { + return _gameState; +} + +auto MassManager::getActiveSlot() -> char{ + auto mmap = Utility::Directory::mapRead(Utility::Directory::join(_saveDirectory, _profileSaveName)); + + auto iter = std::search(mmap.begin(), mmap.end(), &active_slot_locator[0], &active_slot_locator[31]); + + if(iter == mmap.end()) { + if(std::strncmp(&mmap[0x3F6], "Credit", 6) == 0) { + _activeSlot = 0; + } + else { + _lastError = "The profile save seems to be corrupted or the game didn't release the handle on the file."; + _activeSlot = -1; + } + } + else { + _activeSlot = *(iter + 41); + } + + return _activeSlot; +} + +auto MassManager::activeSlot() -> char { + return _activeSlot; +} + +auto MassManager::importMass(const std::string& source, int hangar) -> bool { + if(hangar < 0 && hangar >= 32) { + _lastError = "Hangar out of range in MassManager::importMass()"; + return false; + } + + Utility::Directory::copy(source, source + ".tmp"); + + { + auto mmap = Utility::Directory::map(source + ".tmp"); + + auto iter = std::search(mmap.begin(), mmap.end(), &steamid_locator[0], &steamid_locator[23]); + + if(iter == mmap.end()) { + _lastError = "The M.A.S.S. file at " + source + " seems to be corrupt."; + Utility::Directory::rm(source + ".tmp"); + return false; + } + + iter += 37; + + if(std::strncmp(iter, _steamId.c_str(), _steamId.length()) != 0) { + for(int i = 0; i < 17; ++i) { + *(iter + i) = _steamId[i]; + } + } + } + + const std::string dest = Utility::Directory::join(_saveDirectory, _hangars[hangar]._filename); + + if(Utility::Directory::exists(dest)) { + Utility::Directory::rm(dest); + } + + Utility::Directory::move(source + ".tmp", dest); + + return true; +} + +auto MassManager::moveMass(int source, int destination) -> bool { + if(source < 0 && source >= 32) { + _lastError = "Source hangar out of range in MassManager::moveMass()"; + return false; + } + + if(destination < 0 && destination >= 32) { + _lastError = "Destination hangar out of range in MassManager::moveMass()"; + return false; + } + + std::string source_file = Utility::Directory::join(_saveDirectory, _hangars[source]._filename); + std::string dest_file = Utility::Directory::join(_saveDirectory, _hangars[destination]._filename); + HangarState dest_state = _hangars[destination]._state; + + switch(dest_state) { + case HangarState::Empty: + break; + case HangarState::Invalid: + Utility::Directory::rm(dest_file); + break; + case HangarState::Filled: + Utility::Directory::move(dest_file, dest_file + ".tmp"); + break; + } + + Utility::Directory::move(source_file, dest_file); + + if(dest_state == HangarState::Filled) { + Utility::Directory::move(dest_file + ".tmp", source_file); + } + + return true; +} + +bool MassManager::deleteMass(int hangar) { + if(hangar < 0 && hangar >= 32) { + _lastError = "Hangar number out of range in MassManager::deleteMass()"; + return false; + } + + std::string file = Utility::Directory::join(_saveDirectory, _hangars[hangar]._filename); + + if(Utility::Directory::exists(file)) { + if(!Utility::Directory::rm(file)) { + _lastError = "The M.A.S.S. file couldn't be deleted."; + return false; + } + } + + return true; +} + +auto MassManager::backupSaves(const std::string& filename) -> bool { + if(filename.empty() || (filename.length() < 5 && !Utility::String::endsWith(filename, ".zip"))) { + _lastError = "Invalid filename " + filename + " in MassManager::backupSaves()"; + return false; + } + + if(Utility::Directory::exists(filename)) { + if(!Utility::Directory::rm(filename)) { + _lastError = "Couldn't overwrite " + filename + " in MassManager::backupSaves()"; + } + } + + wxFFileOutputStream out{filename}; + wxZipOutputStream zip{out}; + + { + zip.PutNextEntry(_profileSaveName); + wxFFileInputStream profile_stream{Utility::Directory::toNativeSeparators(Utility::Directory::join(_saveDirectory, _profileSaveName)), "rb"}; + zip.Write(profile_stream); + } + + for(int i = 0; i < 32; ++i) { + std::string unit_file = Utility::Directory::join(_saveDirectory, _hangars[i]._filename); + if(Utility::Directory::exists(unit_file)) { + zip.PutNextEntry(_hangars[i]._filename); + wxFFileInputStream unit_stream{Utility::Directory::toNativeSeparators(unit_file)}; + zip.Write(unit_stream); + } + } + + return true; +} + +void MassManager::refreshHangar(int hangar) { + if(hangar < 0 && hangar >= 32) { + return; + } + + std::string unit_file = Utility::Directory::join(_saveDirectory, _hangars[hangar]._filename); + + if(!Utility::Directory::exists(unit_file)) { + _hangars[hangar]._state = HangarState::Empty; + _hangars[hangar]._massName = Containers::NullOpt; + } + else { + Containers::Optional name = getMassName(unit_file); + + _hangars[hangar]._state = name ? HangarState::Filled : HangarState::Invalid; + _hangars[hangar]._massName = name; + } +} + +auto MassManager::hangarState(int hangar) -> HangarState { + if(hangar < 0 && hangar >= 32) { + return HangarState::Empty; + } + + return _hangars[hangar]._state; +} + +auto MassManager::massName(int hangar) -> Containers::Optional { + if(hangar < 0 && hangar >= 32) { + return Containers::NullOpt; + } + + return _hangars[hangar]._massName; +} + +auto MassManager::getMassName(const std::string& filename) -> Containers::Optional { + Containers::Optional name = Containers::NullOpt; + + auto mmap = Utility::Directory::mapRead(filename); + + auto iter = std::search(mmap.begin(), mmap.end(), &mass_name_locator[0], &mass_name_locator[56]); + + if(iter != mmap.end()) { + name = std::string{iter + 70}; + } + else { + _lastError = "Couldn't find the M.A.S.S. name in " + filename; + } + + return name; +} + +auto MassManager::findSaveDirectory() -> bool { + wchar_t h[MAX_PATH]; + if(!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, h))) { + _lastError = "SHGetFolderPathW() failed in MassManager::findSaveDirectory()"; + return false; + } + + _saveDirectory = Utility::Directory::join(Utility::Directory::fromNativeSeparators(Utility::Unicode::narrow(h)), "MASS_Builder/Saved/SaveGames"); + + if(!Utility::Directory::exists(_saveDirectory)) { + _lastError = _saveDirectory + " wasn't found."; + return false; + } + + return true; +} + +auto MassManager::findSteamId() -> bool { + std::vector listing = Utility::Directory::list(_saveDirectory); + + wxRegEx regex; + if(!regex.Compile("Profile([0-9]{17}).sav", wxRE_ADVANCED)) { + _lastError = "Couldn't compile the regex in MassManager::findSteamId()"; + return false; + } + + for(const std::string& s : listing) { + if(regex.Matches(s)) { + _steamId = regex.GetMatch(s, 1); + return true; + } + } + + _lastError = "Couldn't find the profile save."; + return false; +} diff --git a/MassManager/MassManager.h b/MassManager/MassManager.h new file mode 100644 index 0000000..f3ab238 --- /dev/null +++ b/MassManager/MassManager.h @@ -0,0 +1,91 @@ +#ifndef MASSMANAGER_H +#define MASSMANAGER_H + +// wxMASSManager +// Copyright (C) 2020 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 + +using namespace Corrade; + +enum class GameState : uint8_t { + Unknown, NotRunning, Running +}; + +enum class HangarState : uint8_t { + Empty, Invalid, Filled +}; + +class MassManager { + public: + MassManager(); + + auto ready() -> bool; + auto lastError() -> std::string const&; + + auto saveDirectory() -> std::string const&; + auto steamId() -> std::string const&; + auto profileSaveName() -> std::string const&; + + auto checkGameState() -> GameState; + auto gameState() -> GameState; + + auto getActiveSlot() -> char; + auto activeSlot() -> char; + + auto importMass(const std::string& source, int hangar) -> bool; + + auto moveMass(int source, int destination) -> bool; + + auto deleteMass(int hangar) -> bool; + + auto backupSaves(const std::string& filename) -> bool; + + void refreshHangar(int hangar); + auto hangarState(int hangar) -> HangarState; + auto massName(int hangar) -> Containers::Optional; + + auto getMassName(const std::string& filename) -> Containers::Optional; + + private: + auto findSaveDirectory() -> bool; + auto findSteamId() -> bool; + + bool _ready; + + std::string _lastError = ""; + + std::string _saveDirectory = ""; + std::string _steamId = ""; + std::string _profileSaveName = ""; + + GameState _gameState = GameState::Unknown; + + char _activeSlot = -1; + + struct Hangar { + HangarState _state = HangarState::Empty; + Containers::Optional _massName = Containers::NullOpt; + std::string _filename = ""; + }; + + Containers::StaticArray<32, Hangar> _hangars{Containers::ValueInit}; +}; + +#endif //MASSMANAGER_H diff --git a/main.cpp b/main.cpp index f85324c..6cf85fb 100644 --- a/main.cpp +++ b/main.cpp @@ -25,8 +25,12 @@ class MyApp: public wxApp { SetAppDisplayName("wxMASSManager"); EvtMainFrame* main_frame = new EvtMainFrame(nullptr); - main_frame->Show(); + if(!main_frame->ready()) { + return false; + } + + main_frame->Show(); return true; } };