From c6a25f21c21c101816523e863c9233b2ccc2610f Mon Sep 17 00:00:00 2001 From: Hyper <34012267+hyperbx@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:59:48 +0000 Subject: [PATCH] Implemented achievement data verification (#161) --- UnleashedRecomp/CMakeLists.txt | 1 + UnleashedRecomp/api/SWA.h | 1 + .../api/SWA/Achievement/AchievementID.h | 55 ++++++ .../api/SWA/Achievement/AchievementManager.h | 3 +- .../api/SWA/Achievement/AchievementTest.h | 3 +- UnleashedRecomp/locale/locale.cpp | 18 +- UnleashedRecomp/main.cpp | 3 - UnleashedRecomp/patches/misc_patches.cpp | 4 +- UnleashedRecomp/patches/resident_patches.cpp | 10 +- .../patches/ui/CTitleStateIntro_patches.cpp | 37 +++- UnleashedRecomp/ui/achievement_menu.cpp | 12 +- UnleashedRecomp/ui/message_window.cpp | 2 +- UnleashedRecomp/user/achievement_data.cpp | 166 ++---------------- UnleashedRecomp/user/achievement_data.h | 52 ++---- UnleashedRecomp/user/achievement_manager.cpp | 164 +++++++++++++++++ UnleashedRecomp/user/achievement_manager.h | 32 ++++ 16 files changed, 350 insertions(+), 213 deletions(-) create mode 100644 UnleashedRecomp/api/SWA/Achievement/AchievementID.h create mode 100644 UnleashedRecomp/user/achievement_manager.cpp create mode 100644 UnleashedRecomp/user/achievement_manager.h diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 3bbcb45..9235f9f 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -178,6 +178,7 @@ set(UNLEASHED_RECOMP_INSTALL_CXX_SOURCES set(UNLEASHED_RECOMP_USER_CXX_SOURCES "user/achievement_data.cpp" + "user/achievement_manager.cpp" "user/config.cpp" ) diff --git a/UnleashedRecomp/api/SWA.h b/UnleashedRecomp/api/SWA.h index 01e0c61..8882888 100644 --- a/UnleashedRecomp/api/SWA.h +++ b/UnleashedRecomp/api/SWA.h @@ -52,6 +52,7 @@ #include "Hedgehog/Universe/Engine/hhUpdateInfo.h" #include "Hedgehog/Universe/Engine/hhUpdateUnit.h" #include "Hedgehog/Universe/Thread/hhParallelJob.h" +#include "SWA/Achievement/AchievementID.h" #include "SWA/Achievement/AchievementManager.h" #include "SWA/Achievement/AchievementTest.h" #include "SWA/CSD/CsdDatabaseWrapper.h" diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementID.h b/UnleashedRecomp/api/SWA/Achievement/AchievementID.h new file mode 100644 index 0000000..971894f --- /dev/null +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementID.h @@ -0,0 +1,55 @@ +#pragma once + +enum EAchievementID : uint32_t +{ + eAchievementID_StillBroken = 24, + eAchievementID_LookingBetter, + eAchievementID_StillAJigsawPuzzle, + eAchievementID_PickingUpThePieces, + eAchievementID_AlmostThere, + eAchievementID_OneMoreToGo, + eAchievementID_WorldSavior = 31, + eAchievementID_PartlyCloudy, + eAchievementID_Sunny, + eAchievementID_HalfMoon, + eAchievementID_FullMoon, + eAchievementID_BlueStreak, + eAchievementID_PowerOverwhelming, + eAchievementID_GettingTheHangOfThings, + eAchievementID_CreatureOfTheNight, + eAchievementID_HelpingHand, + eAchievementID_LayTheSmackdown, + eAchievementID_WallCrawler, + eAchievementID_Airdevil, + eAchievementID_Hyperdrive, + eAchievementID_Basher, + eAchievementID_Smasher, + eAchievementID_Crasher, + eAchievementID_Thrasher, + eAchievementID_SocialButterfly, + eAchievementID_HungryHungryHedgehog, + eAchievementID_AcePilot, + eAchievementID_DayTripper, + eAchievementID_HardDaysNight, + eAchievementID_GetOnTheExorciseBandwagon, + eAchievementID_GyroWithRelish = 64, + eAchievementID_PigInABlanket, + eAchievementID_ExoticToppings, + eAchievementID_SausageFriedRice, + eAchievementID_IcedHotdog, + eAchievementID_KebabOnABun, + eAchievementID_KetchupAndMustard, + eAchievementID_HardBoiled, + eAchievementID_FriedClamRoll, + eAchievementID_FirstTimeCustomer, + eAchievementID_OhYouShouldntHave, + eAchievementID_ThatsEnoughSeriously, + eAchievementID_Hedgehunk, + eAchievementID_IAintAfraidOfNoGhost, + eAchievementID_BFFs, + eAchievementID_SpeedingTicket, + eAchievementID_ComboKing, + eAchievementID_RingLeader, + eAchievementID_KnockoutBrawler, + eAchievementID_BlueMeteor +}; diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h b/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h index 60903bc..00c78fe 100644 --- a/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementManager.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace SWA::Achievement { @@ -11,7 +12,7 @@ namespace SWA::Achievement { public: SWA_INSERT_PADDING(0x08); - be m_AchievementID; + be m_AchievementID; }; SWA_INSERT_PADDING(0x98); diff --git a/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h b/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h index a21cbd9..02b4d8d 100644 --- a/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h +++ b/UnleashedRecomp/api/SWA/Achievement/AchievementTest.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace SWA { @@ -9,7 +10,7 @@ namespace SWA public: SWA_INSERT_PADDING(0x38); be m_Unk1; - be m_AchievementID; + be m_AchievementID; uint8_t m_Unk2; }; } diff --git a/UnleashedRecomp/locale/locale.cpp b/UnleashedRecomp/locale/locale.cpp index b8ff79b..a5229f9 100644 --- a/UnleashedRecomp/locale/locale.cpp +++ b/UnleashedRecomp/locale/locale.cpp @@ -343,7 +343,7 @@ std::unordered_map> g_lo } }, { - // Notes: message appears when the SYS-DATA is corrupted (mismatching file size). + // Notes: message appears when SYS-DATA is corrupted (mismatching file size) upon pressing start at the title screen. // To make this occur, open the file in any editor and just remove a large chunk of data. // Do not localise this unless absolutely necessary, these strings are from the XEX. "Title_Message_SaveDataCorrupt", @@ -356,6 +356,22 @@ std::unordered_map> g_lo { ELanguage::Italian, "I file di salvataggio sembrano danneggiati\ne non possono essere caricati." } } }, + { + // Notes: message appears when ACH-DATA is corrupted (mismatching file size, bad signature, incorrect version or invalid checksum) upon pressing start at the title screen. + // To make this occur, open the file in any editor and just remove a large chunk of data. + "Title_Message_AchievementDataCorrupt", + { + { ELanguage::English, "The achievement data appears to be\ncorrupted and cannot be loaded.\n\nProceeding from this point will\nclear your achievement data." } + } + }, + { + // Notes: message appears when ACH-DATA cannot be loaded upon pressing start at the title screen. + // To make this occur, lock the ACH-DATA file using an external program so that it cannot be accessed by the game. + "Title_Message_AchievementDataIOError", + { + { ELanguage::English, "The achievement data could not be loaded.\nYour achievements will not be saved." } + } + }, { "Common_On", { diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index 262a4b8..0ff2803 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -184,8 +183,6 @@ int main(int argc, char *argv[]) ModLoader::Init(); - AchievementData::Load(); - KiSystemStartup(); uint32_t entry = LdrLoadModule(modulePath); diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index a18ad48..57940a4 100644 --- a/UnleashedRecomp/patches/misc_patches.cpp +++ b/UnleashedRecomp/patches/misc_patches.cpp @@ -1,11 +1,11 @@ #include #include -#include +#include #include void AchievementManagerUnlockMidAsmHook(PPCRegister& id) { - AchievementData::Unlock(id.u32); + AchievementManager::Unlock(id.u32); } bool DisableHintsMidAsmHook() diff --git a/UnleashedRecomp/patches/resident_patches.cpp b/UnleashedRecomp/patches/resident_patches.cpp index ac0b397..06d4b1a 100644 --- a/UnleashedRecomp/patches/resident_patches.cpp +++ b/UnleashedRecomp/patches/resident_patches.cpp @@ -1,8 +1,8 @@ -#include -#include #include -#include #include +#include +#include +#include #include bool m_isSavedAchievementData = false; @@ -93,9 +93,7 @@ PPC_FUNC(sub_824E5170) if (!m_isSavedAchievementData) { - LOGN("Saving achievements..."); - - AchievementData::Save(); + AchievementManager::Save(); m_isSavedAchievementData = true; } diff --git a/UnleashedRecomp/patches/ui/CTitleStateIntro_patches.cpp b/UnleashedRecomp/patches/ui/CTitleStateIntro_patches.cpp index a6e53ba..e8dcad9 100644 --- a/UnleashedRecomp/patches/ui/CTitleStateIntro_patches.cpp +++ b/UnleashedRecomp/patches/ui/CTitleStateIntro_patches.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -13,6 +14,9 @@ static int g_quitMessageResult = -1; static std::atomic g_corruptSaveMessageOpen = false; static int g_corruptSaveMessageResult = -1; +static bool g_corruptAchievementsMessageOpen = false; +static int g_corruptAchievementsMessageResult = -1; + static bool ProcessQuitMessage() { if (g_corruptSaveMessageOpen) @@ -60,7 +64,36 @@ static bool ProcessCorruptSaveMessage() return true; } -void StorageDevicePromptMidAsmHook() {} +static bool ProcessCorruptAchievementsMessage() +{ + if (!g_corruptAchievementsMessageOpen) + return false; + + auto message = AchievementManager::Status == EAchStatus::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); + + g_corruptAchievementsMessageOpen = false; + g_corruptAchievementsMessageResult = -1; + } + + return true; +} + +void StorageDevicePromptMidAsmHook() +{ + AchievementManager::Load(); + + if (AchievementManager::Status != EAchStatus::Success) + g_corruptAchievementsMessageOpen = true; +} // Save data validation hook. PPC_FUNC_IMPL(__imp__sub_822C55B0); @@ -82,7 +115,7 @@ PPC_FUNC(sub_82587E50) { __imp__sub_82587E50(ctx, base); } - else if (!ProcessCorruptSaveMessage()) + else if (!ProcessCorruptSaveMessage() && !ProcessCorruptAchievementsMessage()) { auto pInputState = SWA::CInputState::GetInstance(); diff --git a/UnleashedRecomp/ui/achievement_menu.cpp b/UnleashedRecomp/ui/achievement_menu.cpp index e8fef4f..3ebd147 100644 --- a/UnleashedRecomp/ui/achievement_menu.cpp +++ b/UnleashedRecomp/ui/achievement_menu.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -276,7 +276,7 @@ static void DrawAchievement(int rowIndex, float yOffset, Achievement& achievemen if (!isUnlocked) return; - auto timestamp = AchievementData::GetTimestamp(achievement.ID); + auto timestamp = AchievementManager::GetTimestamp(achievement.ID); if (!timestamp) return; @@ -485,7 +485,7 @@ static void DrawAchievementTotal(ImVec2 min, ImVec2 max) auto uv1 = ImVec2((columnIndex + 1) * spriteSize / textureWidth, (rowIndex + 1) * spriteSize / textureHeight); constexpr auto recordsHalfTotal = ACH_RECORDS / 2; - auto records = AchievementData::GetTotalRecords(); + auto records = AchievementManager::GetTotalRecords(); ImVec4 colBronze = ImGui::ColorConvertU32ToFloat4(IM_COL32(198, 105, 15, 255 * alpha)); ImVec4 colSilver = ImGui::ColorConvertU32ToFloat4(IM_COL32(220, 220, 220, 255 * alpha)); @@ -613,7 +613,7 @@ static void DrawContentContainer() { auto achievement = std::get<0>(tpl); - if (AchievementData::IsUnlocked(achievement.ID)) + if (AchievementManager::IsUnlocked(achievement.ID)) DrawAchievement(rowCount++, yOffset, achievement, true); } @@ -621,7 +621,7 @@ static void DrawContentContainer() { auto achievement = std::get<0>(tpl); - if (!AchievementData::IsUnlocked(achievement.ID)) + if (!AchievementManager::IsUnlocked(achievement.ID)) DrawAchievement(rowCount++, yOffset, achievement, false); } @@ -804,7 +804,7 @@ void AchievementMenu::Open() if (Config::Language == ELanguage::English) achievement.Name = xdbf::FixInvalidSequences(achievement.Name); - g_achievements.push_back(std::make_tuple(achievement, AchievementData::GetTimestamp(achievement.ID))); + g_achievements.push_back(std::make_tuple(achievement, AchievementManager::GetTimestamp(achievement.ID))); } std::sort(g_achievements.begin(), g_achievements.end(), [](const auto& a, const auto& b) diff --git a/UnleashedRecomp/ui/message_window.cpp b/UnleashedRecomp/ui/message_window.cpp index 3c3f7f0..8968de7 100644 --- a/UnleashedRecomp/ui/message_window.cpp +++ b/UnleashedRecomp/ui/message_window.cpp @@ -288,7 +288,7 @@ void MessageWindow::Draw() ImVec2 centre = { res.x / 2, res.y / 2 }; - float maxWidth = Scale(640.0f); + auto maxWidth = Scale(820); auto fontSize = Scale(28); auto textSize = MeasureCentredParagraph(g_fntSeurat, fontSize, maxWidth, 5, g_text.c_str()); auto textMarginX = Scale(37); diff --git a/UnleashedRecomp/user/achievement_data.cpp b/UnleashedRecomp/user/achievement_data.cpp index 650e830..2369b0d 100644 --- a/UnleashedRecomp/user/achievement_data.cpp +++ b/UnleashedRecomp/user/achievement_data.cpp @@ -1,70 +1,22 @@ #include "achievement_data.h" -#include -#include -#include -#define NUM_RECORDS sizeof(Data.Records) / sizeof(Record) +#define NUM_RECORDS sizeof(Records) / sizeof(AchRecord) -time_t AchievementData::GetTimestamp(uint16_t id) +bool AchievementData::VerifySignature() const { - for (int i = 0; i < NUM_RECORDS; i++) - { - if (!Data.Records[i].ID) - break; + char sig[4] = ACH_SIGNATURE; - if (Data.Records[i].ID == id) - return Data.Records[i].Timestamp; - } - - return 0; + return memcmp(Signature, sig, sizeof(Signature)) == 0; } -int AchievementData::GetTotalRecords() +bool AchievementData::VerifyVersion() const { - auto result = 0; - - for (int i = 0; i < NUM_RECORDS; i++) - { - if (!Data.Records[i].ID) - break; - - result++; - } - - return result; + return Version == AchVersion ACH_VERSION; } -bool AchievementData::IsUnlocked(uint16_t id) +bool AchievementData::VerifyChecksum() { - for (int i = 0; i < NUM_RECORDS; i++) - { - if (!Data.Records[i].ID) - break; - - if (Data.Records[i].ID == id) - return true; - } - - return false; -} - -void AchievementData::Unlock(uint16_t id) -{ - if (IsUnlocked(id)) - return; - - for (int i = 0; i < NUM_RECORDS; i++) - { - if (Data.Records[i].ID == 0) - { - Data.Records[i].ID = id; - Data.Records[i].Timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - break; - } - } - - if (Config::AchievementNotifications) - AchievementOverlay::Open(id); + return Checksum == CalculateChecksum(); } uint32_t AchievementData::CalculateChecksum() @@ -73,109 +25,11 @@ uint32_t AchievementData::CalculateChecksum() for (int i = 0; i < NUM_RECORDS; i++) { - auto& record = Data.Records[i]; + auto& record = Records[i]; - for (size_t j = 0; j < sizeof(Record); j++) + for (size_t j = 0; j < sizeof(AchRecord); j++) result ^= ((uint8_t*)(&record))[j]; } return result; } - -bool AchievementData::VerifySignature() -{ - char sig[4] = ACH_SIGNATURE; - - return Data.Signature[0] == sig[0] && - Data.Signature[1] == sig[1] && - Data.Signature[2] == sig[2] && - Data.Signature[3] == sig[3]; -} - -bool AchievementData::VerifyVersion() -{ - return Data.Version == Version ACH_VERSION; -} - -bool AchievementData::VerifyChecksum() -{ - return Data.Checksum == CalculateChecksum(); -} - -void AchievementData::Load() -{ - auto dataPath = GetDataPath(true); - - if (!std::filesystem::exists(dataPath)) - { - // Try loading base achievement data as fallback. - dataPath = GetDataPath(false); - - if (!std::filesystem::exists(dataPath)) - return; - } - - std::ifstream file(dataPath, std::ios::binary); - - if (!file) - { - LOGN_ERROR("Failed to read achievement data."); - return; - } - - file.read((char*)&Data.Signature, sizeof(Data.Signature)); - - if (!VerifySignature()) - { - LOGN_ERROR("Invalid achievement data signature."); - - char sig[4] = ACH_SIGNATURE; - - Data.Signature[0] = sig[0]; - Data.Signature[1] = sig[1]; - Data.Signature[2] = sig[2]; - Data.Signature[3] = sig[3]; - - file.close(); - - return; - } - - file.read((char*)&Data.Version, sizeof(Data.Version)); - - if (!VerifyVersion()) - { - LOGN_ERROR("Unsupported achievement data version."); - Data.Version = ACH_VERSION; - file.close(); - return; - } - - file.seekg(0); - file.read((char*)&Data, sizeof(Data)); - - // TODO: display error message to user before wiping data? - if (!VerifyChecksum()) - { - LOGN_WARNING("Achievement data checksum mismatch."); - memset(&Data.Records, 0, sizeof(Data.Records)); - } - - file.close(); -} - -void AchievementData::Save() -{ - std::ofstream file(GetDataPath(true), std::ios::binary); - - if (!file) - { - LOGN_ERROR("Failed to write achievement data."); - return; - } - - Data.Checksum = CalculateChecksum(); - - file.write((const char*)&Data, sizeof(Data)); - file.close(); -} diff --git a/UnleashedRecomp/user/achievement_data.h b/UnleashedRecomp/user/achievement_data.h index ec99519..bd38d21 100644 --- a/UnleashedRecomp/user/achievement_data.h +++ b/UnleashedRecomp/user/achievement_data.h @@ -2,6 +2,7 @@ #include +#define ACH_FILENAME "ACH-DATA" #define ACH_SIGNATURE { 'A', 'C', 'H', ' ' } #define ACH_VERSION { 1, 0, 0 } #define ACH_RECORDS 50 @@ -9,23 +10,14 @@ class AchievementData { public: -#pragma pack(push, 1) - struct Record - { - uint16_t ID; - time_t Timestamp; - uint16_t Reserved[3]; - }; -#pragma pack(pop) - - struct Version + struct AchVersion { uint8_t Major; uint8_t Minor; uint8_t Revision; uint8_t Reserved; - bool operator==(const Version& other) const + bool operator==(const AchVersion& other) const { return Major == other.Major && Minor == other.Minor && @@ -33,31 +25,23 @@ public: } }; - class Data +#pragma pack(push, 1) + struct AchRecord { - public: - char Signature[4]; - Version Version{}; - uint32_t Checksum; - uint32_t Reserved; - Record Records[ACH_RECORDS]; + uint16_t ID; + time_t Timestamp; + uint16_t Reserved[3]; }; +#pragma pack(pop) - static inline Data Data{ ACH_SIGNATURE, ACH_VERSION }; + char Signature[4] ACH_SIGNATURE; + AchVersion Version ACH_VERSION; + uint32_t Checksum; + uint32_t Reserved; + AchRecord Records[ACH_RECORDS]; - static std::filesystem::path GetDataPath(bool checkForMods) - { - return GetSavePath(checkForMods) / "ACH-DATA"; - } - - static time_t GetTimestamp(uint16_t id); - static int GetTotalRecords(); - static bool IsUnlocked(uint16_t id); - static void Unlock(uint16_t id); - static uint32_t CalculateChecksum(); - static bool VerifySignature(); - static bool VerifyVersion(); - static bool VerifyChecksum(); - static void Load(); - static void Save(); + bool VerifySignature() const; + bool VerifyVersion() const; + bool VerifyChecksum(); + uint32_t CalculateChecksum(); }; diff --git a/UnleashedRecomp/user/achievement_manager.cpp b/UnleashedRecomp/user/achievement_manager.cpp new file mode 100644 index 0000000..fc449db --- /dev/null +++ b/UnleashedRecomp/user/achievement_manager.cpp @@ -0,0 +1,164 @@ +#include "achievement_manager.h" +#include +#include +#include + +#define NUM_RECORDS sizeof(AchievementManager::Data.Records) / sizeof(AchievementData::AchRecord) + +time_t AchievementManager::GetTimestamp(uint16_t id) +{ + for (int i = 0; i < NUM_RECORDS; i++) + { + if (!Data.Records[i].ID) + break; + + if (Data.Records[i].ID == id) + return Data.Records[i].Timestamp; + } + + return 0; +} + +size_t AchievementManager::GetTotalRecords() +{ + auto result = 0; + + for (int i = 0; i < NUM_RECORDS; i++) + { + if (!Data.Records[i].ID) + break; + + result++; + } + + return result; +} + +bool AchievementManager::IsUnlocked(uint16_t id) +{ + for (int i = 0; i < NUM_RECORDS; i++) + { + if (!Data.Records[i].ID) + break; + + if (Data.Records[i].ID == id) + return true; + } + + return false; +} + +void AchievementManager::Unlock(uint16_t id) +{ + if (IsUnlocked(id)) + return; + + for (int i = 0; i < NUM_RECORDS; i++) + { + if (Data.Records[i].ID == 0) + { + Data.Records[i].ID = id; + Data.Records[i].Timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + break; + } + } + + if (Config::AchievementNotifications) + AchievementOverlay::Open(id); +} + +void AchievementManager::Load() +{ + Data = {}; + Status = EAchStatus::Success; + + auto dataPath = GetDataPath(true); + + if (!std::filesystem::exists(dataPath)) + { + // Try loading base achievement data as fallback. + dataPath = GetDataPath(false); + + if (!std::filesystem::exists(dataPath)) + return; + } + + std::error_code ec; + auto fileSize = std::filesystem::file_size(dataPath, ec); + auto dataSize = sizeof(AchievementData); + + if (fileSize != dataSize) + { + Status = EAchStatus::BadFileSize; + return; + } + + std::ifstream file(dataPath, std::ios::binary); + + if (!file) + { + Status = EAchStatus::IOError; + return; + } + + AchievementData data{}; + + file.read((char*)&data.Signature, sizeof(data.Signature)); + + if (!data.VerifySignature()) + { + Status = EAchStatus::BadSignature; + file.close(); + return; + } + + file.read((char*)&data.Version, sizeof(data.Version)); + + // TODO: upgrade in future if the version changes. + if (!data.VerifyVersion()) + { + Status = EAchStatus::BadVersion; + file.close(); + return; + } + + file.seekg(0); + file.read((char*)&data, sizeof(data)); + + if (!data.VerifyChecksum()) + { + Status = EAchStatus::BadChecksum; + file.close(); + return; + } + + file.close(); + + memcpy(&Data, &data, dataSize); +} + +void AchievementManager::Save(bool ignoreStatus) +{ + if (!ignoreStatus && Status != EAchStatus::Success) + { + LOGN_WARNING("Achievement data will not be saved in this session!"); + return; + } + + LOGN("Saving achievements..."); + + std::ofstream file(GetDataPath(true), std::ios::binary); + + if (!file) + { + LOGN_ERROR("Failed to write achievement data."); + return; + } + + Data.Checksum = Data.CalculateChecksum(); + + file.write((const char*)&Data, sizeof(AchievementData)); + file.close(); + + Status = EAchStatus::Success; +} diff --git a/UnleashedRecomp/user/achievement_manager.h b/UnleashedRecomp/user/achievement_manager.h new file mode 100644 index 0000000..443f176 --- /dev/null +++ b/UnleashedRecomp/user/achievement_manager.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +enum class EAchStatus +{ + Success, + IOError, + BadFileSize, + BadSignature, + BadVersion, + BadChecksum +}; + +class AchievementManager +{ +public: + static inline AchievementData Data{}; + static inline EAchStatus Status{}; + + static std::filesystem::path GetDataPath(bool checkForMods) + { + return GetSavePath(checkForMods) / ACH_FILENAME; + } + + static time_t GetTimestamp(uint16_t id); + static size_t GetTotalRecords(); + static bool IsUnlocked(uint16_t id); + static void Unlock(uint16_t id); + static void Load(); + static void Save(bool ignoreStatus = false); +};