Add MassManager and move backend stuff to it.

This is a big commit, but a necessary one, as too many things were
intertwined. As a result, the code is now cleaner.
This commit is contained in:
Guillaume Jacquemin 2020-01-12 14:54:23 +01:00
parent cf3c7305c8
commit fc9d898c54
6 changed files with 620 additions and 250 deletions

View file

@ -39,6 +39,8 @@ add_executable(wxMASSManager WIN32
GUI/MainFrame.cpp GUI/MainFrame.cpp
GUI/EvtMainFrame.h GUI/EvtMainFrame.h
GUI/EvtMainFrame.cpp GUI/EvtMainFrame.cpp
MassManager/MassManager.h
MassManager/MassManager.cpp
resource.rc) resource.rc)
target_link_libraries(wxMASSManager PRIVATE target_link_libraries(wxMASSManager PRIVATE

View file

@ -14,43 +14,26 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <cstring>
#include <algorithm>
#include <vector>
#include <wx/filedlg.h> #include <wx/filedlg.h>
#include <wx/msgdlg.h> #include <wx/msgdlg.h>
#include <wx/numdlg.h> #include <wx/numdlg.h>
#include <wx/regex.h> #include <wx/regex.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#define WIN32_LEAN_AND_MEAN #include <Corrade/Containers/Optional.h>
#include <shlobj.h>
#include <wtsapi32.h>
#include <Corrade/Containers/Array.h>
#include <Corrade/Utility/Directory.h> #include <Corrade/Utility/Directory.h>
#include <Corrade/Utility/FormatStl.h>
#include <Corrade/Utility/String.h>
#include <Corrade/Utility/Unicode.h>
#include "EvtMainFrame.h" #include "EvtMainFrame.h"
using namespace Corrade; 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) { EvtMainFrame::EvtMainFrame(wxWindow* parent): MainFrame(parent) {
SetIcon(wxIcon("MAINICON")); SetIcon(wxIcon("MAINICON"));
getSaveDirectory(); if(!_manager.ready()) {
getLocalSteamId(); errorMessage("There was an issue initialising the manager:\n\n" + _manager.lastError());
return;
}
initialiseListView(); initialiseListView();
isGameRunning(); 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" 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" "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" "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.")); "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.Connect(wxEVT_FSWATCHER, wxFileSystemWatcherEventHandler(EvtMainFrame::fileUpdateEvent), nullptr, this);
_watcher.AddTree(wxFileName(Utility::Directory::toNativeSeparators(_saveDirectory), wxPATH_WIN), _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", _localSteamId)); wxFSW_EVENT_CREATE|wxFSW_EVENT_DELETE|wxFSW_EVENT_MODIFY|wxFSW_EVENT_RENAME, wxString::Format("*%s.sav", _manager.steamId()));
_gameCheckTimer.Start(3000); _gameCheckTimer.Start(3000);
} }
@ -81,11 +63,19 @@ EvtMainFrame::~EvtMainFrame() {
_watcher.Disconnect(wxEVT_FSWATCHER, wxFileSystemWatcherEventHandler(EvtMainFrame::fileUpdateEvent), nullptr, this); _watcher.Disconnect(wxEVT_FSWATCHER, wxFileSystemWatcherEventHandler(EvtMainFrame::fileUpdateEvent), nullptr, this);
} }
void EvtMainFrame::importEvent(wxCommandEvent&) { bool EvtMainFrame::ready() {
std::string slot_state = _installedListView->GetItemText(_installedListView->GetFirstSelected(), 1).ToStdString(); return _manager.ready();
}
if(slot_state != "<Empty>" && slot_state != "<Invalid data>" && void EvtMainFrame::importEvent(wxCommandEvent&) {
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), 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) { "Question", wxYES_NO|wxCENTRE|wxICON_QUESTION, this) == wxNO) {
return; return;
} }
@ -98,132 +88,107 @@ void EvtMainFrame::importEvent(wxCommandEvent&) {
const std::string source_file = dialog.GetPath().ToUTF8().data(); const std::string source_file = dialog.GetPath().ToUTF8().data();
const std::string mass_name = getMassName(source_file); Containers::Optional<std::string> mass_name = _manager.getMassName(source_file);
if(mass_name == "") { if(!mass_name) {
errorMessage(error_prefix + _manager.lastError());
return; 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) { "Question", wxYES_NO|wxCENTRE|wxICON_QUESTION, this) == wxNO) {
return; return;
} }
if(_isGameRunning) { switch(_manager.gameState()) {
errorMessage("The game is running. Aborting..."); case GameState::Unknown:
return; errorMessage(error_prefix + "For security reasons, importing is disabled if the game's status is unknown.");
} break;
case GameState::NotRunning:
const std::string dest_file = _saveDirectory + Utility::formatString("/Unit{:.2d}{}.sav", _installedListView->GetFirstSelected(), _localSteamId); if(!_manager.importMass(source_file, selected_hangar)) {
errorMessage(error_prefix + _manager.lastError());
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];
} }
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&) { void EvtMainFrame::moveEvent(wxCommandEvent&) {
const static std::string error_prefix = "Move failed:\n\n";
long source_slot = _installedListView->GetFirstSelected(); 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; "Slot", "Choose a slot", source_slot + 1, 1, 32, this) - 1;
if(choice == -1 || choice == source_slot) { if(choice == -1 || choice == source_slot) {
return; return;
} }
if(_isGameRunning) { switch(_manager.gameState()) {
errorMessage("The game is running. Aborting..."); case GameState::Unknown:
return; 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;
std::string orig_file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, source_slot, _localSteamId); case GameState::Running:
std::string dest_status = _installedListView->GetItemText(choice, 1).ToStdString(); errorMessage(error_prefix + "Moving a M.A.S.S. is disabled while the game is running.");
std::string dest_file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, choice, _localSteamId); break;
if(dest_status == "<Invalid data>") {
Utility::Directory::rm(dest_file);
}
else if(dest_status != "<Empty>") {
Utility::Directory::move(dest_file, dest_file + ".tmp");
}
Utility::Directory::move(orig_file, dest_file);
if(dest_status != "<Empty>") {
Utility::Directory::move(dest_file + ".tmp", orig_file);
} }
} }
void EvtMainFrame::deleteEvent(wxCommandEvent&) { 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), 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) { "Are you sure ?", wxYES_NO|wxCENTRE|wxICON_QUESTION, this) == wxNO) {
return; return;
} }
if(_isGameRunning) { switch(_manager.gameState()) {
errorMessage("The game is running. Aborting..."); case GameState::Unknown:
return; 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;
std::string file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, _installedListView->GetFirstSelected(), _localSteamId); case GameState::Running:
errorMessage(error_prefix + "Deleting a M.A.S.S. is disabled while the game is running.");
if(Utility::Directory::exists(file)) { break;
Utility::Directory::rm(file);
} }
} }
void EvtMainFrame::backupEvent(wxCommandEvent&) { 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"); wxString current_timestamp = wxDateTime::Now().Format("%Y-%m-%d_%H-%M-%S");
wxFileDialog save_dialog{this, "Choose output location", _saveDirectory, wxFileDialog save_dialog{this, "Choose output location", _manager.saveDirectory(),
wxString::Format("backup_%s_%s.zip", _localSteamId, current_timestamp), "Zip archive (*zip)|*zip", wxString::Format("backup_%s_%s.zip", _manager.steamId(), current_timestamp), "Zip archive (*zip)|*zip",
wxFD_SAVE|wxFD_OVERWRITE_PROMPT}; wxFD_SAVE|wxFD_OVERWRITE_PROMPT};
if(save_dialog.ShowModal() == wxID_CANCEL) { if(save_dialog.ShowModal() == wxID_CANCEL) {
return; return;
} }
wxFFileOutputStream out{save_dialog.GetPath()}; if(!_manager.backupSaves(save_dialog.GetPath().ToStdString())) {
wxZipOutputStream zip{out}; errorMessage(error_prefix + _manager.lastError());
{
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);
}
} }
} }
void EvtMainFrame::openSaveDirEvent(wxCommandEvent&) { void EvtMainFrame::openSaveDirEvent(wxCommandEvent&) {
wxExecute("explorer.exe " + Utility::Directory::toNativeSeparators(_saveDirectory)); wxExecute("explorer.exe " + Utility::Directory::toNativeSeparators(_manager.saveDirectory()));
} }
void EvtMainFrame::installedSelectionEvent(wxListEvent&) { void EvtMainFrame::installedSelectionEvent(wxListEvent&) {
@ -236,36 +201,70 @@ void EvtMainFrame::listColumnDragEvent(wxListEvent& event) {
void EvtMainFrame::fileUpdateEvent(wxFileSystemWatcherEvent& event) { void EvtMainFrame::fileUpdateEvent(wxFileSystemWatcherEvent& event) {
int event_type = event.GetChangeType(); int event_type = event.GetChangeType();
wxString event_file = event.GetPath().GetFullName();
if(event_type == wxFSW_EVENT_MODIFY && _lastWatcherEventType == wxFSW_EVENT_RENAME) { if(event_type == wxFSW_EVENT_MODIFY && _lastWatcherEventType == wxFSW_EVENT_RENAME) {
_lastWatcherEventType = event_type; _lastWatcherEventType = event_type;
return; return;
} }
_lastWatcherEventType = event_type;
wxString event_file = event.GetNewPath().GetFullName();
if(!event_file.EndsWith(".sav")) {
return;
}
wxMilliSleep(50); wxMilliSleep(50);
if(event_file == wxString::Format("Profile%s.sav", _localSteamId)) { wxRegEx regex;
getActiveSlot();
}
else {
wxRegEx regex(wxString::Format("Unit([0-3][0-9])%s.sav", _localSteamId), wxRE_ADVANCED);
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)) { if(regex.Matches(event_file)) {
long slot; long slot;
if(regex.GetMatch(event_file, 1).ToLong(&slot)) { if(regex.GetMatch(event_file, 1).ToLong(&slot) && slot >= 0 && slot < 32) {
_installedListView->SetItem(slot, 1, getSlotMassName(slot)); 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(); updateCommandsState();
} }
@ -274,43 +273,6 @@ void EvtMainFrame::gameCheckTimerEvent(wxTimerEvent&) {
isGameRunning(); 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<std::string> 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() { void EvtMainFrame::initialiseListView() {
for(long i = 0; i < 32; i++) { for(long i = 0; i < 32; i++) {
_installedListView->InsertItem(i, wxString::Format("%.2i", i + 1)); _installedListView->InsertItem(i, wxString::Format("%.2i", i + 1));
@ -325,110 +287,76 @@ void EvtMainFrame::initialiseListView() {
} }
void EvtMainFrame::isGameRunning() { void EvtMainFrame::isGameRunning() {
WTS_PROCESS_INFOW* process_infos = nullptr; GameState state = _manager.checkGameState();
unsigned long process_count = 0;
if(WTSEnumerateProcessesW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &process_infos, &process_count)) { switch(state) {
for(unsigned long i = 0; i < process_count; ++i) { case GameState::Unknown:
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 {
_gameStatus->SetLabel("not running");
_gameStatus->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_CAPTIONTEXT));
}
}
else {
_isGameRunning = false;
_gameStatus->SetLabel("unknown"); _gameStatus->SetLabel("unknown");
_gameStatus->SetForegroundColour(wxColour("orange")); _gameStatus->SetForegroundColour(wxColour("orange"));
} break;
case GameState::NotRunning:
if(process_infos != nullptr) { _gameStatus->SetLabel("not running");
WTSFreeMemory(process_infos); _gameStatus->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_CAPTIONTEXT));
process_infos = nullptr; break;
case GameState::Running:
_gameStatus->SetLabel("running");
_gameStatus->SetForegroundColour(wxColour("red"));
break;
} }
updateCommandsState(); updateCommandsState();
} }
void EvtMainFrame::refreshListView() { void EvtMainFrame::refreshListView() {
for(long i = 0; i < 32; i++) { for(int i = 0; i < 32; i++) {
_installedListView->SetItem(i, 1, getSlotMassName(i)); refreshHangar(i);
} }
updateCommandsState(); updateCommandsState();
} }
void EvtMainFrame::getActiveSlot() { 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(slot);
wxFont tmp_font = _installedListView->GetItemFont(_activeSlot);
tmp_font.SetWeight(wxFONTWEIGHT_NORMAL); tmp_font.SetWeight(wxFONTWEIGHT_NORMAL);
_installedListView->SetItemFont(_activeSlot, tmp_font); _installedListView->SetItemFont(slot, tmp_font);
if(iter == mmap.end()) { slot = _manager.getActiveSlot();
if(std::strncmp(&mmap[0x3F6], "Credit", 6) == 0) {
_activeSlot = 0;
}
else {
_activeSlot = -1;
}
}
else {
_activeSlot = *(iter + 41);
}
if(_activeSlot != -1) { if(slot != -1) {
_installedListView->SetItemFont(_activeSlot, _installedListView->GetItemFont(_activeSlot).Bold()); _installedListView->SetItemFont(slot, _installedListView->GetItemFont(slot).Bold());
} }
} }
void EvtMainFrame::updateCommandsState() { void EvtMainFrame::updateCommandsState() {
long selection = _installedListView->GetFirstSelected(); long selection = _installedListView->GetFirstSelected();
wxString state = ""; GameState game_state = _manager.gameState();
if(selection != -1) { HangarState hangar_state = _manager.hangarState(selection);
state = _installedListView->GetItemText(selection, 1);
}
_importButton->Enable(selection != -1 && !_isGameRunning); _importButton->Enable(selection != -1 && game_state != GameState::Running);
_moveButton->Enable(selection != -1 && !_isGameRunning && state != "<Empty>" && state != "<Invalid data>"); _moveButton->Enable(selection != -1 && game_state != GameState::Running && hangar_state != HangarState::Empty && hangar_state != HangarState::Invalid);
_deleteButton->Enable(selection != -1 && !_isGameRunning && state != "<Empty>"); _deleteButton->Enable(selection != -1 && game_state != GameState::Running && hangar_state != HangarState::Empty);
} }
std::string EvtMainFrame::getSlotMassName(int index) { void EvtMainFrame::refreshHangar(int slot) {
std::string unit_file = Utility::formatString("{}/Unit{:.2d}{}.sav", _saveDirectory, index, _localSteamId); if(slot < 0 && slot >= 32) {
if(Utility::Directory::exists(unit_file)) { return;
std::string mass_name = getMassName(unit_file);
return (mass_name == "" ? "<Invalid data>" : mass_name);
}
else {
return "<Empty>";
}
}
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 "";
} }
return iter + 70; _manager.refreshHangar(slot);
switch(_manager.hangarState(slot)) {
case HangarState::Empty:
_installedListView->SetItem(slot, 1, "<Empty>");
break;
case HangarState::Invalid:
_installedListView->SetItem(slot, 1, "<Invalid>");
break;
case HangarState::Filled:
_installedListView->SetItem(slot, 1, *(_manager.massName(slot)));
break;
}
} }
void EvtMainFrame::infoMessage(const wxString& message) { void EvtMainFrame::infoMessage(const wxString& message) {

View file

@ -21,6 +21,8 @@
#include <wx/fswatcher.h> #include <wx/fswatcher.h>
#include "../MassManager/MassManager.h"
#include "MainFrame.h" #include "MainFrame.h"
class EvtMainFrame: public MainFrame { class EvtMainFrame: public MainFrame {
@ -28,6 +30,8 @@ class EvtMainFrame: public MainFrame {
EvtMainFrame(wxWindow* parent); EvtMainFrame(wxWindow* parent);
~EvtMainFrame(); ~EvtMainFrame();
auto ready() -> bool;
protected: protected:
void importEvent(wxCommandEvent&); void importEvent(wxCommandEvent&);
void moveEvent(wxCommandEvent&); void moveEvent(wxCommandEvent&);
@ -40,24 +44,18 @@ class EvtMainFrame: public MainFrame {
void gameCheckTimerEvent(wxTimerEvent&); void gameCheckTimerEvent(wxTimerEvent&);
private: private:
void getSaveDirectory();
void getLocalSteamId();
void initialiseListView(); void initialiseListView();
void isGameRunning(); void isGameRunning();
void refreshListView(); void refreshListView();
void getActiveSlot(); void getActiveSlot();
void updateCommandsState(); void updateCommandsState();
std::string getSlotMassName(int index); void refreshHangar(int slot);
std::string getMassName(const std::string& filename);
void infoMessage(const wxString& message); void infoMessage(const wxString& message);
void warningMessage(const wxString& message); void warningMessage(const wxString& message);
void errorMessage(const wxString& message); void errorMessage(const wxString& message);
std::string _saveDirectory; MassManager _manager;
std::string _localSteamId;
bool _isGameRunning = false;
char _activeSlot = 0;
wxFileSystemWatcher _watcher; wxFileSystemWatcher _watcher;
int _lastWatcherEventType = 0; int _lastWatcherEventType = 0;

347
MassManager/MassManager.cpp Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
#include <cstring>
#include <algorithm>
#include <vector>
#include <wx/regex.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#define WIN32_LEAN_AND_MEAN
#include <shlobj.h>
#include <wtsapi32.h>
#include <Corrade/Containers/Array.h>
#include <Corrade/Utility/Directory.h>
#include <Corrade/Utility/FormatStl.h>
#include <Corrade/Utility/String.h>
#include <Corrade/Utility/Unicode.h>
#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<std::string> 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<std::string> {
if(hangar < 0 && hangar >= 32) {
return Containers::NullOpt;
}
return _hangars[hangar]._massName;
}
auto MassManager::getMassName(const std::string& filename) -> Containers::Optional<std::string> {
Containers::Optional<std::string> 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<std::string> 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;
}

91
MassManager/MassManager.h Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
#include <string>
#include <Corrade/Containers/StaticArray.h>
#include <Corrade/Containers/Optional.h>
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<std::string>;
auto getMassName(const std::string& filename) -> Containers::Optional<std::string>;
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<std::string> _massName = Containers::NullOpt;
std::string _filename = "";
};
Containers::StaticArray<32, Hangar> _hangars{Containers::ValueInit};
};
#endif //MASSMANAGER_H

View file

@ -25,8 +25,12 @@ class MyApp: public wxApp {
SetAppDisplayName("wxMASSManager"); SetAppDisplayName("wxMASSManager");
EvtMainFrame* main_frame = new EvtMainFrame(nullptr); EvtMainFrame* main_frame = new EvtMainFrame(nullptr);
main_frame->Show();
if(!main_frame->ready()) {
return false;
}
main_frame->Show();
return true; return true;
} }
}; };