diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 66b60a44..c7ba186f 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/main.cpp b/UnleashedRecomp/main.cpp index a55c0181..ccf8b81c 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -212,6 +213,7 @@ int main(int argc, char *argv[]) } Config::Load(); + PersistentStorageManager::LoadBinary(); #if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12) for (auto& dll : g_D3D12RequiredModules) diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index 9f318fb4..78d5e085 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) @@ -156,3 +157,23 @@ void DisableBoostFilterMidAsmHook(PPCRegister& r11) r11.u32 = 0; } } + +// 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..c421946c 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(); + PersistentStorageManager::SaveBinary(); - m_isSavedAchievementData = true; + isSavedExtraData = true; } } else { - m_isSavedAchievementData = false; + isSavedExtraData = false; } } diff --git a/UnleashedRecomp/user/achievement_manager.h b/UnleashedRecomp/user/achievement_manager.h index eab9c829..57317fd5 100644 --- a/UnleashedRecomp/user/achievement_manager.h +++ b/UnleashedRecomp/user/achievement_manager.h @@ -4,6 +4,7 @@ enum class EAchStatus { + Unknown, Success, IOError, BadFileSize, @@ -16,7 +17,7 @@ class AchievementManager { public: static inline AchievementData Data{}; - static inline EAchStatus Status{}; + static inline EAchStatus Status{ EAchStatus::Unknown }; static std::filesystem::path GetDataPath(bool checkForMods) { diff --git a/UnleashedRecomp/user/persistent_data.cpp b/UnleashedRecomp/user/persistent_data.cpp new file mode 100644 index 00000000..1bdd75b8 --- /dev/null +++ b/UnleashedRecomp/user/persistent_data.cpp @@ -0,0 +1,18 @@ +#include "persistent_data.h" + +bool PersistentData::VerifySignature() const +{ + char sig[4] = EXT_SIGNATURE; + + return memcmp(Header.Signature, sig, sizeof(Header.Signature)) == 0; +} + +bool PersistentData::VerifyVersion() const +{ + return Header.Version == ExtVersion EXT_VERSION; +} + +bool PersistentData::VerifyHeader() const +{ + return Header.HeaderSize == sizeof(ExtHeader); +} diff --git a/UnleashedRecomp/user/persistent_data.h b/UnleashedRecomp/user/persistent_data.h new file mode 100644 index 00000000..5a8dd0ec --- /dev/null +++ b/UnleashedRecomp/user/persistent_data.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +#define EXT_FILENAME "EXT-DATA" +#define EXT_SIGNATURE { 'E', 'X', 'T', ' ' } +#define EXT_VERSION { 1, 0, 0 } + +enum class EDLCFlag +{ + ApotosAndShamar, + Spagonia, + Chunnan, + Mazuri, + Holoska, + EmpireCityAndAdabat, + Count +}; + +class PersistentData +{ +public: + struct ExtVersion + { + uint8_t Major; + uint8_t Minor; + uint8_t Revision; + uint8_t Reserved; + + bool operator==(const ExtVersion& other) const + { + return Major == other.Major && + Minor == other.Minor && + Revision == other.Revision; + } + }; + + struct ExtHeader + { + char Signature[4] EXT_SIGNATURE; + ExtVersion Version EXT_VERSION; + uint32_t HeaderSize{ sizeof(ExtHeader) }; + uint32_t Reserved; + }; + + ExtHeader Header; + bool DLCFlags[6]; + + bool VerifySignature() const; + bool VerifyVersion() const; + bool VerifyHeader() const; +}; diff --git a/UnleashedRecomp/user/persistent_storage_manager.cpp b/UnleashedRecomp/user/persistent_storage_manager.cpp new file mode 100644 index 00000000..8883ba6a --- /dev/null +++ b/UnleashedRecomp/user/persistent_storage_manager.cpp @@ -0,0 +1,123 @@ +#include "persistent_storage_manager.h" +#include +#include +#include + +bool PersistentStorageManager::ShouldDisplayDLCMessage(bool setOffendingDLCFlag) +{ + auto result = false; + + if (BinStatus != EBinStatus::Success) + return result; + + 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 } + }; + + 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; +} + +void PersistentStorageManager::LoadBinary() +{ + BinStatus = EBinStatus::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; + } + + std::error_code ec; + auto fileSize = std::filesystem::file_size(dataPath, ec); + auto dataSize = sizeof(PersistentData); + + if (fileSize != dataSize) + { + BinStatus = EBinStatus::BadFileSize; + return; + } + + std::ifstream file(dataPath, std::ios::binary); + + if (!file) + { + BinStatus = EBinStatus::IOError; + return; + } + + PersistentData data{}; + + file.read((char*)&data.Header.Signature, sizeof(data.Header.Signature)); + + if (!data.VerifySignature()) + { + BinStatus = EBinStatus::BadSignature; + file.close(); + return; + } + + file.read((char*)&data.Header.Version, sizeof(data.Header.Version)); + + // TODO: upgrade in future if the version changes. + if (!data.VerifyVersion()) + { + BinStatus = EBinStatus::BadVersion; + file.close(); + return; + } + + file.read((char*)&data.Header.HeaderSize, sizeof(data.Header.HeaderSize)); + + if (!data.VerifyHeader()) + { + BinStatus = EBinStatus::BadHeader; + file.close(); + return; + } + + file.seekg(0); + file.read((char*)&data, sizeof(data)); + file.close(); + + memcpy(&Data, &data, dataSize); +} + +void 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; + } + + file.write((const char*)&Data, sizeof(PersistentData)); + file.close(); + + BinStatus = EBinStatus::Success; +} diff --git a/UnleashedRecomp/user/persistent_storage_manager.h b/UnleashedRecomp/user/persistent_storage_manager.h new file mode 100644 index 00000000..d75d24ae --- /dev/null +++ b/UnleashedRecomp/user/persistent_storage_manager.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +enum class EBinStatus +{ + Unknown, + Success, + IOError, + BadFileSize, + BadSignature, + BadVersion, + BadHeader +}; + +class PersistentStorageManager +{ +public: + static inline PersistentData Data{}; + static inline EBinStatus BinStatus{ EBinStatus::Unknown }; + + static std::filesystem::path GetDataPath(bool checkForMods) + { + return GetSavePath(checkForMods) / EXT_FILENAME; + } + + static bool ShouldDisplayDLCMessage(bool setOffendingDLCFlag); + static void LoadBinary(); + static void SaveBinary(); +};