diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 1a9c07e7..500bc0d5 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -183,6 +183,8 @@ set(UNLEASHED_RECOMP_USER_CXX_SOURCES "user/config.cpp" "user/registry.cpp" "user/paths.cpp" + "user/persistent_data.cpp" + "user/persistent_storage_manager.cpp" ) set(UNLEASHED_RECOMP_MOD_CXX_SOURCES diff --git a/UnleashedRecomp/api/SWA/System/ApplicationDocument.h b/UnleashedRecomp/api/SWA/System/ApplicationDocument.h index 21949711..1e6ab27f 100644 --- a/UnleashedRecomp/api/SWA/System/ApplicationDocument.h +++ b/UnleashedRecomp/api/SWA/System/ApplicationDocument.h @@ -80,9 +80,13 @@ namespace SWA boost::shared_ptr m_spRenderScene; SWA_INSERT_PADDING(0x04); boost::shared_ptr m_spGameParameter; - SWA_INSERT_PADDING(0x78); + SWA_INSERT_PADDING(0x0C); + boost::anonymous_shared_ptr m_spItemParamManager; + SWA_INSERT_PADDING(0x64); boost::shared_ptr m_spCriticalSection; - SWA_INSERT_PADDING(0x20); + SWA_INSERT_PADDING(0x14); + bool m_ShowDLCInfo; + SWA_INSERT_PADDING(0x08); }; // TODO: Hedgehog::Base::TSynchronizedPtr @@ -111,7 +115,9 @@ namespace SWA SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_Field10C, 0x10C); SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spRenderScene, 0x12C); SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spGameParameter, 0x138); + SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spItemParamManager, 0x14C); SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spCriticalSection, 0x1B8); + SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_ShowDLCInfo, 0x1D4); SWA_ASSERT_SIZEOF(CApplicationDocument::CMember, 0x1E0); SWA_ASSERT_OFFSETOF(CApplicationDocument, m_pMember, 0x04); diff --git a/UnleashedRecomp/api/SWA/System/GameParameter.h b/UnleashedRecomp/api/SWA/System/GameParameter.h index 95618144..da6b3735 100644 --- a/UnleashedRecomp/api/SWA/System/GameParameter.h +++ b/UnleashedRecomp/api/SWA/System/GameParameter.h @@ -7,11 +7,19 @@ namespace SWA class CGameParameter // : public Hedgehog::Universe::CMessageActor { public: - struct SSaveData; + struct SSaveData + { + SWA_INSERT_PADDING(0x8600); + be DLCFlags[8]; + SWA_INSERT_PADDING(0x15C); + }; + struct SStageParameter; SWA_INSERT_PADDING(0x94); xpointer m_pSaveData; xpointer m_pStageParameter; }; + + SWA_ASSERT_OFFSETOF(CGameParameter::SSaveData, DLCFlags, 0x8600); } diff --git a/UnleashedRecomp/hid/driver/sdl_hid.cpp b/UnleashedRecomp/hid/driver/sdl_hid.cpp index 85e6793a..7220abbe 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -310,6 +310,8 @@ void hid::Init() SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1"); SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "1"); + + SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); // Uses Button Labels. This hint is disabled for Nintendo Controllers. SDL_InitSubSystem(SDL_INIT_EVENTS); SDL_AddEventWatch(HID_OnSDLEvent, nullptr); diff --git a/UnleashedRecomp/install/installer.cpp b/UnleashedRecomp/install/installer.cpp index 9574c5d5..774a141e 100644 --- a/UnleashedRecomp/install/installer.cpp +++ b/UnleashedRecomp/install/installer.cpp @@ -67,6 +67,73 @@ static std::unique_ptr createFileSystemFromPath(const std::fi } } +static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, std::vector &fileData, Journal &journal, const std::function &progressCallback, bool checkSizeOnly) { + const std::string fileName(pair.first); + const uint32_t hashCount = pair.second; + const std::filesystem::path filePath = targetDirectory / fileName; + if (!std::filesystem::exists(filePath)) + { + journal.lastResult = Journal::Result::FileMissing; + journal.lastErrorMessage = fmt::format("File {} does not exist.", fileName); + return false; + } + + std::error_code ec; + size_t fileSize = std::filesystem::file_size(filePath, ec); + if (ec) + { + journal.lastResult = Journal::Result::FileReadFailed; + journal.lastErrorMessage = fmt::format("Failed to read file size for {}.", fileName); + return false; + } + + if (checkSizeOnly) + { + journal.progressTotal += fileSize; + } + else + { + std::ifstream fileStream(filePath, std::ios::binary); + if (fileStream.is_open()) + { + fileData.resize(fileSize); + fileStream.read((char *)(fileData.data()), fileSize); + } + + if (!fileStream.is_open() || fileStream.bad()) + { + journal.lastResult = Journal::Result::FileReadFailed; + journal.lastErrorMessage = fmt::format("Failed to read file {}.", fileName); + return false; + } + + uint64_t fileHash = XXH3_64bits(fileData.data(), fileSize); + bool fileHashFound = false; + for (uint32_t i = 0; i < hashCount && !fileHashFound; i++) + { + fileHashFound = fileHash == fileHashes[i]; + } + + if (!fileHashFound) + { + journal.lastResult = Journal::Result::FileHashFailed; + journal.lastErrorMessage = fmt::format("File {} did not match any of the known hashes.", fileName); + return false; + } + + journal.progressCounter += fileSize; + } + + if (!progressCallback()) + { + journal.lastResult = Journal::Result::Cancelled; + journal.lastErrorMessage = "Check was cancelled."; + return false; + } + + return true; +} + static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector &fileData, Journal &journal, const std::function &progressCallback) { const std::string filename(pair.first); const uint32_t hashCount = pair.second; @@ -204,6 +271,45 @@ static DLC detectDLC(const std::filesystem::path &sourcePath, VirtualFileSystem } } +static bool fillDLCSource(DLC dlc, Installer::DLCSource &dlcSource) +{ + switch (dlc) + { + case DLC::Spagonia: + dlcSource.filePairs = { SpagoniaFiles, SpagoniaFilesSize }; + dlcSource.fileHashes = SpagoniaHashes; + dlcSource.targetSubDirectory = SpagoniaDirectory; + return true; + case DLC::Chunnan: + dlcSource.filePairs = { ChunnanFiles, ChunnanFilesSize }; + dlcSource.fileHashes = ChunnanHashes; + dlcSource.targetSubDirectory = ChunnanDirectory; + return true; + case DLC::Mazuri: + dlcSource.filePairs = { MazuriFiles, MazuriFilesSize }; + dlcSource.fileHashes = MazuriHashes; + dlcSource.targetSubDirectory = MazuriDirectory; + return true; + case DLC::Holoska: + dlcSource.filePairs = { HoloskaFiles, HoloskaFilesSize }; + dlcSource.fileHashes = HoloskaHashes; + dlcSource.targetSubDirectory = HoloskaDirectory; + return true; + case DLC::ApotosShamar: + dlcSource.filePairs = { ApotosShamarFiles, ApotosShamarFilesSize }; + dlcSource.fileHashes = ApotosShamarHashes; + dlcSource.targetSubDirectory = ApotosShamarDirectory; + return true; + case DLC::EmpireCityAdabat: + dlcSource.filePairs = { EmpireCityAdabatFiles, EmpireCityAdabatFilesSize }; + dlcSource.fileHashes = EmpireCityAdabatHashes; + dlcSource.targetSubDirectory = EmpireCityAdabatDirectory; + return true; + default: + return false; + } +} + bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory, std::filesystem::path &modulePath) { modulePath = baseDirectory / PatchedDirectory / GameExecutableFile; @@ -254,6 +360,40 @@ bool Installer::checkAllDLC(const std::filesystem::path& baseDirectory) return result; } +bool Installer::checkInstallIntegrity(const std::filesystem::path &baseDirectory, Journal &journal, const std::function &progressCallback) +{ + // Run the file checks twice: once to fill out the progress counter and the file sizes, and another pass to do the hash integrity checks. + for (uint32_t checkPass = 0; checkPass < 2; checkPass++) + { + bool checkSizeOnly = (checkPass == 0); + if (!checkFiles({ GameFiles, GameFilesSize }, GameHashes, baseDirectory / GameDirectory, journal, progressCallback, checkSizeOnly)) + { + return false; + } + + if (!checkFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, baseDirectory / UpdateDirectory, journal, progressCallback, checkSizeOnly)) + { + return false; + } + + for (int i = 1; i < (int)DLC::Count; i++) + { + if (checkDLCInstall(baseDirectory, (DLC)i)) + { + Installer::DLCSource dlcSource; + fillDLCSource((DLC)i, dlcSource); + + if (!checkFiles(dlcSource.filePairs, dlcSource.fileHashes, baseDirectory / dlcSource.targetSubDirectory, journal, progressCallback, checkSizeOnly)) + { + return false; + } + } + } + } + + return true; +} + bool Installer::computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize) { for (FilePair pair : filePairs) @@ -272,6 +412,27 @@ bool Installer::computeTotalSize(std::span filePairs, const uint return true; } +bool Installer::checkFiles(std::span filePairs, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, Journal &journal, const std::function &progressCallback, bool checkSizeOnly) +{ + FilePair validationPair = {}; + uint32_t validationHashIndex = 0; + uint32_t hashIndex = 0; + uint32_t hashCount = 0; + std::vector fileData; + for (FilePair pair : filePairs) + { + hashIndex = hashCount; + hashCount += pair.second; + + if (!checkFile(pair, &fileHashes[hashIndex], targetDirectory, fileData, journal, progressCallback, checkSizeOnly)) + { + return false; + } + } + + return true; +} + bool Installer::copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback) { std::error_code ec; @@ -387,39 +548,8 @@ bool Installer::parseSources(const Input &input, Journal &journal, Sources &sour } DLC dlc = detectDLC(path, *dlcSource.sourceVfs, journal); - switch (dlc) + if (!fillDLCSource(dlc, dlcSource)) { - case DLC::Spagonia: - dlcSource.filePairs = { SpagoniaFiles, SpagoniaFilesSize }; - dlcSource.fileHashes = SpagoniaHashes; - dlcSource.targetSubDirectory = SpagoniaDirectory; - break; - case DLC::Chunnan: - dlcSource.filePairs = { ChunnanFiles, ChunnanFilesSize }; - dlcSource.fileHashes = ChunnanHashes; - dlcSource.targetSubDirectory = ChunnanDirectory; - break; - case DLC::Mazuri: - dlcSource.filePairs = { MazuriFiles, MazuriFilesSize }; - dlcSource.fileHashes = MazuriHashes; - dlcSource.targetSubDirectory = MazuriDirectory; - break; - case DLC::Holoska: - dlcSource.filePairs = { HoloskaFiles, HoloskaFilesSize }; - dlcSource.fileHashes = HoloskaHashes; - dlcSource.targetSubDirectory = HoloskaDirectory; - break; - case DLC::ApotosShamar: - dlcSource.filePairs = { ApotosShamarFiles, ApotosShamarFilesSize }; - dlcSource.fileHashes = ApotosShamarHashes; - dlcSource.targetSubDirectory = ApotosShamarDirectory; - break; - case DLC::EmpireCityAdabat: - dlcSource.filePairs = { EmpireCityAdabatFiles, EmpireCityAdabatFilesSize }; - dlcSource.fileHashes = EmpireCityAdabatHashes; - dlcSource.targetSubDirectory = EmpireCityAdabatDirectory; - break; - default: return false; } diff --git a/UnleashedRecomp/install/installer.h b/UnleashedRecomp/install/installer.h index 9f216a26..8bc04777 100644 --- a/UnleashedRecomp/install/installer.h +++ b/UnleashedRecomp/install/installer.h @@ -75,7 +75,9 @@ struct Installer static bool checkGameInstall(const std::filesystem::path &baseDirectory, std::filesystem::path &modulePath); static bool checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc); static bool checkAllDLC(const std::filesystem::path &baseDirectory); + static bool checkInstallIntegrity(const std::filesystem::path &baseDirectory, Journal &journal, const std::function &progressCallback); static bool computeTotalSize(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize); + static bool checkFiles(std::span filePairs, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, Journal &journal, const std::function &progressCallback, bool checkSizeOnly); static bool copyFiles(std::span filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function &progressCallback); static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr &targetVfs, Journal &journal); static bool parseSources(const Input &input, Journal &journal, Sources &sources); diff --git a/UnleashedRecomp/locale/locale.cpp b/UnleashedRecomp/locale/locale.cpp index 5eb3d401..8d1e74d1 100644 --- a/UnleashedRecomp/locale/locale.cpp +++ b/UnleashedRecomp/locale/locale.cpp @@ -703,6 +703,28 @@ std::unordered_map> { ELanguage::Italian, "Impossibile trovare il modulo \"%s\".\n\nAssicurati che:\n\n- Hai estratto questa copia di Unleashed Recompiled correttamente e non solo il file *.exe.\n- Non stai eseguendo Unleashed Recompiled da un file *.zip." } } }, + { + "IntegrityCheck_Success", + { + { ELanguage::English, "Installation check has finished.\n\nAll files seem to be correct.\n\nThe game will now close. Remove the launch argument to play the game." }, + { ELanguage::Japanese, "インストールチェックが終了しました\n\nすべてのファイルは正しいようです\n\nゲームは終了します、ゲームをプレイするには起動引数を削除してください" }, + { ELanguage::German, "Die Installation wurde überprüft.\n\nAlle Dateien scheinen korrekt zu sein.\n\nDas Spiel wird nun geschlossen. Entferne die Startoption, um das Spiel zu spielen." }, + { ELanguage::French, "La vérification de l'installation est terminée.\n\nTous les fichiers semblent corrects.\n\nL'application va maintenant se fermer. Retirez l'argument de lancement pour pouvoir lancer le jeu." }, + { ELanguage::Spanish, "La verificación de la instalación ha terminado.\n\nTodos los archivos parecen correctos.\n\nEl juego se cerrará ahora. Elimina el argumento de lanzamiento para jugar al juego." }, + { ELanguage::Italian, "La verifica dei file d'installazione è terminata.\n\nTutti i file sembrano corretti.\n\nIl gioco si chiuderà. Rimuovi l'argomento di avvio per poter giocare." } + } + }, + { + "IntegrityCheck_Failed", + { + { ELanguage::English, "Installation check has failed.\n\nError: %s\n\nThe game will now close. Try reinstalling the game by using the --install launch argument." }, + { ELanguage::Japanese, "インストールチェックに失敗しました\n\nエラー:%s\n\nゲームは終了します、--install 起動引数を使用してゲームを再インストールしてください" }, + { ELanguage::German, "Die Installationsprüfung ist fehlgeschlagen.\n\nFehler: %s\n\nDas Spiel wird nun geschlossen. Versuche das Spiel durch Verwendung der Startoption --install neu zu installieren." }, + { ELanguage::French, "La vérification de l'installation a échoué.\n\nErreur : %s\n\nL'application va maintenant se fermer. Essayez de réinstaller le jeu en utilisant l'argument de lancement --install." }, + { ELanguage::Spanish, "La verificación de la instalación ha fallado.\n\nError: %s\n\nEl juego se cerrará ahora. Intenta reinstalar el juego utilizando el argumento de lanzamiento --install." }, + { ELanguage::Italian, "La verifica dei file d'installazione non è andata a buon fine.\n\nErrore: %s\n\nIl gioco si chiuderà. Prova a reinstallare il gioco utilizzando l'argomento di avvio --install." } + } + }, { "Common_On", { diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index 5ef5cd46..526a3713 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -200,6 +201,7 @@ int main(int argc, char *argv[]) bool forceInstaller = false; bool forceDLCInstaller = false; bool useDefaultWorkingDirectory = false; + bool forceInstallationCheck = false; bool graphicsApiRetry = false; const char *sdlVideoDriver = nullptr; @@ -208,6 +210,7 @@ int main(int argc, char *argv[]) forceInstaller = forceInstaller || (strcmp(argv[i], "--install") == 0); forceDLCInstaller = forceDLCInstaller || (strcmp(argv[i], "--install-dlc") == 0); useDefaultWorkingDirectory = useDefaultWorkingDirectory || (strcmp(argv[i], "--use-cwd") == 0); + forceInstallationCheck = forceInstallationCheck || (strcmp(argv[i], "--install-check") == 0); graphicsApiRetry = graphicsApiRetry || (strcmp(argv[i], "--graphics-api-retry") == 0); if (strcmp(argv[i], "--sdl-video-driver") == 0) @@ -227,6 +230,62 @@ int main(int argc, char *argv[]) } Config::Load(); + + if (!PersistentStorageManager::LoadBinary()) + LOGFN_ERROR("Failed to load persistent storage binary... (status code {})", (int)PersistentStorageManager::BinStatus); + + if (forceInstallationCheck) + { + // Create the console to show progress to the user, otherwise it will seem as if the game didn't boot at all. + os::process::ShowConsole(); + + Journal journal; + double lastProgressMiB = 0.0; + double lastTotalMib = 0.0; + Installer::checkInstallIntegrity(GAME_INSTALL_DIRECTORY, journal, [&]() + { + constexpr double MiBDivisor = 1024.0 * 1024.0; + constexpr double MiBProgressThreshold = 128.0; + double progressMiB = double(journal.progressCounter) / MiBDivisor; + double totalMiB = double(journal.progressTotal) / MiBDivisor; + if (journal.progressCounter > 0) + { + if ((progressMiB - lastProgressMiB) > MiBProgressThreshold) + { + fprintf(stdout, "Checking files: %0.2f MiB / %0.2f MiB\n", progressMiB, totalMiB); + lastProgressMiB = progressMiB; + } + } + else + { + if ((totalMiB - lastTotalMib) > MiBProgressThreshold) + { + fprintf(stdout, "Scanning files: %0.2f MiB\n", totalMiB); + lastTotalMib = totalMiB; + } + } + + return true; + }); + + char resultText[512]; + uint32_t messageBoxStyle; + if (journal.lastResult == Journal::Result::Success) + { + snprintf(resultText, sizeof(resultText), "%s", Localise("IntegrityCheck_Success").c_str()); + fprintf(stdout, "%s\n", resultText); + messageBoxStyle = SDL_MESSAGEBOX_INFORMATION; + } + else + { + snprintf(resultText, sizeof(resultText), Localise("IntegrityCheck_Failed").c_str(), journal.lastErrorMessage.c_str()); + fprintf(stderr, "%s\n", resultText); + messageBoxStyle = SDL_MESSAGEBOX_ERROR; + } + + SDL_ShowSimpleMessageBox(messageBoxStyle, GameWindow::GetTitle(), resultText, GameWindow::s_pWindow); + std::_Exit(int(journal.lastResult)); + } #if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12) for (auto& dll : g_D3D12RequiredModules) diff --git a/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp b/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp index d4a05fc5..4affb80a 100644 --- a/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp +++ b/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -64,16 +65,16 @@ static bool ProcessCorruptAchievementsMessage() if (!g_corruptAchievementsMessageOpen) return false; - auto message = AchievementManager::Status == EAchStatus::IOError + auto message = AchievementManager::BinStatus == EAchBinStatus::IOError ? Localise("Title_Message_AchievementDataIOError") : Localise("Title_Message_AchievementDataCorrupt"); if (MessageWindow::Open(message, &g_corruptAchievementsMessageResult) == MSG_CLOSED) { - // Allow user to proceed if the achievement data couldn't be loaded. - // Restarting may fix this error, so it isn't worth clearing the data for. - if (AchievementManager::Status != EAchStatus::IOError) - AchievementManager::Save(true); + // Create a new save file if the file was successfully loaded and failed validation. + // If the file couldn't be opened, restarting may fix this error, so it isn't worth clearing the data for. + if (AchievementManager::BinStatus != EAchBinStatus::IOError) + AchievementManager::SaveBinary(true); g_corruptAchievementsMessageOpen = false; g_corruptAchievementsMessageOpen.notify_one(); @@ -135,9 +136,10 @@ void PressStartSaveLoadThreadMidAsmHook() g_faderBegun.wait(true); } - AchievementManager::Load(); + if (!AchievementManager::LoadBinary()) + LOGFN_ERROR("Failed to load achievement data... (status code {})", (int)AchievementManager::BinStatus); - if (AchievementManager::Status != EAchStatus::Success) + if (AchievementManager::BinStatus != EAchBinStatus::Success) { g_corruptAchievementsMessageOpen = true; g_corruptAchievementsMessageOpen.wait(true); diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index ed225827..ec4e1a8b 100644 --- a/UnleashedRecomp/patches/misc_patches.cpp +++ b/UnleashedRecomp/patches/misc_patches.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include void AchievementManagerUnlockMidAsmHook(PPCRegister& id) @@ -172,3 +173,23 @@ PPC_FUNC(sub_82B4DB48) __imp__sub_82B4DB48(ctx, base); } + +// DLC save data flag check. +// +// The DLC checks are fundamentally broken in this game, resulting in this method always +// returning true and displaying the DLC info message when it shouldn't be. +// +// The original intent here seems to have been to display the message every time new DLC +// content is installed, but the flags in the save data never get written to properly, +// causing this function to always pass in some way. +// +// We bypass the save data completely and write to external persistent storage to store +// whether we've seen the DLC info message instead. This way we can retain the original +// broken game behaviour, whilst also providing a fix for this issue that is safe. +PPC_FUNC_IMPL(__imp__sub_824EE620); +PPC_FUNC(sub_824EE620) +{ + __imp__sub_824EE620(ctx, base); + + ctx.r3.u32 = PersistentStorageManager::ShouldDisplayDLCMessage(true); +} diff --git a/UnleashedRecomp/patches/resident_patches.cpp b/UnleashedRecomp/patches/resident_patches.cpp index 64a220d7..2741d0e9 100644 --- a/UnleashedRecomp/patches/resident_patches.cpp +++ b/UnleashedRecomp/patches/resident_patches.cpp @@ -2,11 +2,10 @@ #include #include #include +#include #include #include -bool m_isSavedAchievementData = false; - // SWA::Message::MsgRequestStartLoading::Impl PPC_FUNC_IMPL(__imp__sub_824DCF38); PPC_FUNC(sub_824DCF38) @@ -99,20 +98,23 @@ PPC_FUNC(sub_824E5170) App::s_isSaving = pSaveIcon->m_IsVisible; + static bool isSavedExtraData = false; + if (pSaveIcon->m_IsVisible) { App::s_isSaveDataCorrupt = false; - if (!m_isSavedAchievementData) + if (!isSavedExtraData) { - AchievementManager::Save(); + AchievementManager::SaveBinary(); + PersistentStorageManager::SaveBinary(); - m_isSavedAchievementData = true; + isSavedExtraData = true; } } else { - m_isSavedAchievementData = false; + isSavedExtraData = false; } } diff --git a/UnleashedRecomp/user/achievement_data.cpp b/UnleashedRecomp/user/achievement_data.cpp index 2369b0df..dd734659 100644 --- a/UnleashedRecomp/user/achievement_data.cpp +++ b/UnleashedRecomp/user/achievement_data.cpp @@ -11,7 +11,7 @@ bool AchievementData::VerifySignature() const bool AchievementData::VerifyVersion() const { - return Version == AchVersion ACH_VERSION; + return Version <= ACH_VERSION; } bool AchievementData::VerifyChecksum() diff --git a/UnleashedRecomp/user/achievement_data.h b/UnleashedRecomp/user/achievement_data.h index bd38d219..3c8c1a5a 100644 --- a/UnleashedRecomp/user/achievement_data.h +++ b/UnleashedRecomp/user/achievement_data.h @@ -4,27 +4,12 @@ #define ACH_FILENAME "ACH-DATA" #define ACH_SIGNATURE { 'A', 'C', 'H', ' ' } -#define ACH_VERSION { 1, 0, 0 } +#define ACH_VERSION 1 #define ACH_RECORDS 50 class AchievementData { public: - struct AchVersion - { - uint8_t Major; - uint8_t Minor; - uint8_t Revision; - uint8_t Reserved; - - bool operator==(const AchVersion& other) const - { - return Major == other.Major && - Minor == other.Minor && - Revision == other.Revision; - } - }; - #pragma pack(push, 1) struct AchRecord { @@ -35,10 +20,10 @@ public: #pragma pack(pop) char Signature[4] ACH_SIGNATURE; - AchVersion Version ACH_VERSION; - uint32_t Checksum; - uint32_t Reserved; - AchRecord Records[ACH_RECORDS]; + uint32_t Version{ ACH_VERSION }; + uint32_t Checksum{}; + uint32_t Reserved{}; + AchRecord Records[ACH_RECORDS]{}; bool VerifySignature() const; bool VerifyVersion() const; diff --git a/UnleashedRecomp/user/achievement_manager.cpp b/UnleashedRecomp/user/achievement_manager.cpp index dbc6c242..6d2a5772 100644 --- a/UnleashedRecomp/user/achievement_manager.cpp +++ b/UnleashedRecomp/user/achievement_manager.cpp @@ -105,11 +105,11 @@ void AchievementManager::Reset() *(bool*)g_memory.Translate(0x833647C4) = false; } -void AchievementManager::Load() +bool AchievementManager::LoadBinary() { AchievementManager::Reset(); - Status = EAchStatus::Success; + BinStatus = EAchBinStatus::Success; auto dataPath = GetDataPath(true); @@ -119,7 +119,7 @@ void AchievementManager::Load() dataPath = GetDataPath(false); if (!std::filesystem::exists(dataPath)) - return; + return true; } std::error_code ec; @@ -128,16 +128,16 @@ void AchievementManager::Load() if (fileSize != dataSize) { - Status = EAchStatus::BadFileSize; - return; + BinStatus = EAchBinStatus::BadFileSize; + return false; } std::ifstream file(dataPath, std::ios::binary); if (!file) { - Status = EAchStatus::IOError; - return; + BinStatus = EAchBinStatus::IOError; + return false; } AchievementData data{}; @@ -146,19 +146,18 @@ void AchievementManager::Load() if (!data.VerifySignature()) { - Status = EAchStatus::BadSignature; + BinStatus = EAchBinStatus::BadSignature; file.close(); - return; + return false; } file.read((char*)&data.Version, sizeof(data.Version)); - // TODO: upgrade in future if the version changes. if (!data.VerifyVersion()) { - Status = EAchStatus::BadVersion; + BinStatus = EAchBinStatus::BadVersion; file.close(); - return; + return false; } file.seekg(0); @@ -166,22 +165,24 @@ void AchievementManager::Load() if (!data.VerifyChecksum()) { - Status = EAchStatus::BadChecksum; + BinStatus = EAchBinStatus::BadChecksum; file.close(); - return; + return false; } file.close(); memcpy(&Data, &data, dataSize); + + return true; } -void AchievementManager::Save(bool ignoreStatus) +bool AchievementManager::SaveBinary(bool ignoreStatus) { - if (!ignoreStatus && Status != EAchStatus::Success) + if (!ignoreStatus && BinStatus != EAchBinStatus::Success) { LOGN_WARNING("Achievement data will not be saved in this session!"); - return; + return false; } LOGN("Saving achievements..."); @@ -191,7 +192,7 @@ void AchievementManager::Save(bool ignoreStatus) if (!file) { LOGN_ERROR("Failed to write achievement data."); - return; + return false; } Data.Checksum = Data.CalculateChecksum(); @@ -199,5 +200,7 @@ void AchievementManager::Save(bool ignoreStatus) file.write((const char*)&Data, sizeof(AchievementData)); file.close(); - Status = EAchStatus::Success; + BinStatus = EAchBinStatus::Success; + + return true; } diff --git a/UnleashedRecomp/user/achievement_manager.h b/UnleashedRecomp/user/achievement_manager.h index eab9c829..4bff7577 100644 --- a/UnleashedRecomp/user/achievement_manager.h +++ b/UnleashedRecomp/user/achievement_manager.h @@ -2,7 +2,7 @@ #include -enum class EAchStatus +enum class EAchBinStatus { Success, IOError, @@ -16,7 +16,7 @@ class AchievementManager { public: static inline AchievementData Data{}; - static inline EAchStatus Status{}; + static inline EAchBinStatus BinStatus{ EAchBinStatus::Success }; static std::filesystem::path GetDataPath(bool checkForMods) { @@ -29,6 +29,6 @@ public: static void Unlock(uint16_t id); static void UnlockAll(); static void Reset(); - static void Load(); - static void Save(bool ignoreStatus = false); + static bool LoadBinary(); + static bool SaveBinary(bool ignoreStatus = false); }; diff --git a/UnleashedRecomp/user/persistent_data.cpp b/UnleashedRecomp/user/persistent_data.cpp new file mode 100644 index 00000000..f087f39e --- /dev/null +++ b/UnleashedRecomp/user/persistent_data.cpp @@ -0,0 +1,13 @@ +#include "persistent_data.h" + +bool PersistentData::VerifySignature() const +{ + char sig[4] = EXT_SIGNATURE; + + return memcmp(Signature, sig, sizeof(Signature)) == 0; +} + +bool PersistentData::VerifyVersion() const +{ + return Version <= EXT_VERSION; +} diff --git a/UnleashedRecomp/user/persistent_data.h b/UnleashedRecomp/user/persistent_data.h new file mode 100644 index 00000000..fec827fa --- /dev/null +++ b/UnleashedRecomp/user/persistent_data.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#define EXT_FILENAME "EXT-DATA" +#define EXT_SIGNATURE { 'E', 'X', 'T', ' ' } +#define EXT_VERSION 1 + +enum class EDLCFlag +{ + ApotosAndShamar, + Spagonia, + Chunnan, + Mazuri, + Holoska, + EmpireCityAndAdabat, + Count +}; + +class PersistentData +{ +public: + char Signature[4] EXT_SIGNATURE; + uint32_t Version{ EXT_VERSION }; + uint64_t Reserved{}; + bool DLCFlags[6]{}; + + bool VerifySignature() const; + bool VerifyVersion() const; +}; diff --git a/UnleashedRecomp/user/persistent_storage_manager.cpp b/UnleashedRecomp/user/persistent_storage_manager.cpp new file mode 100644 index 00000000..6b92d239 --- /dev/null +++ b/UnleashedRecomp/user/persistent_storage_manager.cpp @@ -0,0 +1,117 @@ +#include "persistent_storage_manager.h" +#include +#include +#include + +bool PersistentStorageManager::ShouldDisplayDLCMessage(bool setOffendingDLCFlag) +{ + if (BinStatus != EExtBinStatus::Success) + return true; + + static std::unordered_map flags = + { + { EDLCFlag::ApotosAndShamar, DLC::ApotosShamar }, + { EDLCFlag::Spagonia, DLC::Spagonia }, + { EDLCFlag::Chunnan, DLC::Chunnan }, + { EDLCFlag::Mazuri, DLC::Mazuri }, + { EDLCFlag::Holoska, DLC::Holoska }, + { EDLCFlag::EmpireCityAndAdabat, DLC::EmpireCityAdabat } + }; + + auto result = false; + + for (auto& pair : flags) + { + if (!Data.DLCFlags[(int)pair.first] && Installer::checkDLCInstall(GetGamePath(), pair.second)) + { + if (setOffendingDLCFlag) + Data.DLCFlags[(int)pair.first] = true; + + result = true; + } + } + + return result; +} + +bool PersistentStorageManager::LoadBinary() +{ + BinStatus = EExtBinStatus::Success; + + auto dataPath = GetDataPath(true); + + if (!std::filesystem::exists(dataPath)) + { + // Try loading base persistent data as fallback. + dataPath = GetDataPath(false); + + if (!std::filesystem::exists(dataPath)) + return true; + } + + std::error_code ec; + auto fileSize = std::filesystem::file_size(dataPath, ec); + auto dataSize = sizeof(PersistentData); + + if (fileSize != dataSize) + { + BinStatus = EExtBinStatus::BadFileSize; + return false; + } + + std::ifstream file(dataPath, std::ios::binary); + + if (!file) + { + BinStatus = EExtBinStatus::IOError; + return false; + } + + PersistentData data{}; + + file.read((char*)&data.Signature, sizeof(data.Signature)); + + if (!data.VerifySignature()) + { + BinStatus = EExtBinStatus::BadSignature; + file.close(); + return false; + } + + file.read((char*)&data.Version, sizeof(data.Version)); + + if (!data.VerifyVersion()) + { + BinStatus = EExtBinStatus::BadVersion; + file.close(); + return false; + } + + file.seekg(0); + file.read((char*)&data, sizeof(data)); + file.close(); + + memcpy(&Data, &data, dataSize); + + return true; +} + +bool PersistentStorageManager::SaveBinary() +{ + LOGN("Saving persistent storage binary..."); + + std::ofstream file(GetDataPath(true), std::ios::binary); + + if (!file) + { + LOGN_ERROR("Failed to write persistent storage binary."); + return false; + } + + file.write((const char*)&Data, sizeof(PersistentData)); + file.close(); + + BinStatus = EExtBinStatus::Success; + + return true; +} diff --git a/UnleashedRecomp/user/persistent_storage_manager.h b/UnleashedRecomp/user/persistent_storage_manager.h new file mode 100644 index 00000000..4cad7b88 --- /dev/null +++ b/UnleashedRecomp/user/persistent_storage_manager.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +enum class EExtBinStatus +{ + Success, + IOError, + BadFileSize, + BadSignature, + BadVersion +}; + +class PersistentStorageManager +{ +public: + static inline PersistentData Data{}; + static inline EExtBinStatus BinStatus{ EExtBinStatus::Success }; + + static std::filesystem::path GetDataPath(bool checkForMods) + { + return GetSavePath(checkForMods) / EXT_FILENAME; + } + + static bool ShouldDisplayDLCMessage(bool setOffendingDLCFlag); + static bool LoadBinary(); + static bool SaveBinary(); +};