diff --git a/sfse/GameEvents.h b/sfse/GameEvents.h index 0f45da8..8eb8339 100644 --- a/sfse/GameEvents.h +++ b/sfse/GameEvents.h @@ -685,8 +685,15 @@ struct TESEscortWaitStartEvent {}; struct TESEscortWaitStopEvent {}; struct TESExitBleedoutEvent {}; struct TESExitFurnitureEvent {}; -struct TESFormDeleteEvent {}; -struct TESFormIDRemapEvent {}; +struct TESFormDeleteEvent +{ + u32 formId; //00 +}; +struct TESFormIDRemapEvent +{ + u32 oldID; //00 + u32 newID; //04 +}; struct TESFurnitureEvent {}; struct TESGrabReleaseEvent {}; struct TESInitScriptEvent {}; diff --git a/sfse/Hooks_Serialization.cpp b/sfse/Hooks_Serialization.cpp new file mode 100644 index 0000000..301c9f2 --- /dev/null +++ b/sfse/Hooks_Serialization.cpp @@ -0,0 +1,105 @@ +#include "Hooks_Serialization.h" + +#include "sfse_common/BranchTrampoline.h" +#include "sfse_common/Relocation.h" +#include "sfse_common/SafeWrite.h" + +#include "sfse/PluginManager.h" +#include "sfse/Serialization.h" + +#include "xbyak/xbyak.h" + +class BGSSaveLoadGame; +class BGSSaveLoadManager; + +typedef void (*_SaveGame)(BGSSaveLoadGame* a_this, void* a_unk1, void* a_unk2, const char* a_name); +RelocAddr <_SaveGame> SaveGame_Call(0x024ACCB0 + 0x12B); +RelocAddr <_SaveGame> SaveGame_Original(0x024AFCC0); + +typedef bool (*_LoadGame)(BGSSaveLoadGame* a_this, const char* a_name, void* a_unk1, void* a_unk2); +RelocAddr <_LoadGame> LoadGame_Call(0x024DFF80 + 0x572); +RelocAddr <_LoadGame> LoadGame_Original(0x024B55B0); + +typedef bool (*_DeleteSaveFile)(const char* a_filePath); +RelocAddr <_DeleteSaveFile> DeleteSaveFile_Call(0x024DF75C + 0x65); +RelocAddr <_DeleteSaveFile> DeleteSaveFile_Original(0x024DE118); + +typedef bool (*_VM_SaveGame)(void* a_this, void* a_storage, void* a_handleReaderWriter, bool a_flag); +typedef bool (*_VM_LoadGame)(void* a_this, void* a_storage, void* a_handleReaderWriter, bool* a_flag, bool* b_flag); +typedef void* (*_VM_DropAllRunningData)(void* a_this); +_VM_SaveGame VM_SaveGame_Original = nullptr; +_VM_LoadGame VM_LoadGame_Original = nullptr; +_VM_DropAllRunningData VM_DropAllRunningData_Original = nullptr; +RelocAddr VirtualMachine_IVMSaveLoadInterface_VTable(0x0545A240); + +void SaveGame_Hook(BGSSaveLoadGame* a_this, void* a_unk1, void* a_unk2, const char* a_name) +{ + Serialization::SetSaveName(a_name, true); + PluginManager::dispatchMessage(0, SFSEMessagingInterface::kMessage_PreSaveGame, (void*)a_name, (u32)strlen(a_name), NULL); + SaveGame_Original(a_this, a_unk1, a_unk2, a_name); + PluginManager::dispatchMessage(0, SFSEMessagingInterface::kMessage_PostSaveGame, (void*)a_name, (u32)strlen(a_name), NULL); + Serialization::SetSaveName(NULL); +} + +bool LoadGame_Hook(BGSSaveLoadGame* a_this, const char* a_name, void* a_unk1, void* a_unk2) +{ + Serialization::SetSaveName(a_name, false); + Serialization::HandleBeginLoad(); + PluginManager::dispatchMessage(0, SFSEMessagingInterface::kMessage_PreLoadGame, (void*)a_name, (u32)strlen(a_name), NULL); + bool result = LoadGame_Original(a_this, a_name, a_unk1, a_unk2); + PluginManager::dispatchMessage(0, SFSEMessagingInterface::kMessage_PostLoadGame, (void*)a_name, (u32)strlen(a_name), NULL); + Serialization::HandleEndLoad(); + Serialization::SetSaveName(NULL); + return result; +} + +bool DeleteSaveFile_Hook(const char* a_filePath) +{ + bool result = DeleteSaveFile_Original(a_filePath); + Serialization::HandleDeleteSave(a_filePath); + return result; +} + +void* VM_DropAllRunningData_Hook(void* a_this) +{ + void* result = VM_DropAllRunningData_Original(a_this); + Serialization::HandleRevertGlobalData(); + return result; +} + +bool VM_SaveGame_Hook(void* a_this, void* a_storage, void* a_handleReaderWriter, bool a_flag) +{ + bool result = VM_SaveGame_Original(a_this, a_storage, a_handleReaderWriter, a_flag); + Serialization::HandleSaveGlobalData(); + return result; +} + +bool VM_LoadGame_Hook(void* a_this, void* a_storage, void* a_handleReaderWriter, bool* a_flag, bool* b_flag) +{ + bool result = VM_LoadGame_Original(a_this, a_storage, a_handleReaderWriter, a_flag, b_flag); + Serialization::HandleLoadGlobalData(); + return result; +} + +void Hooks_Serialization_Apply() +{ + //write call hooks for SaveGame, LoadGame & DeleteSaveFile + g_branchTrampoline.write5Call(SaveGame_Call.getUIntPtr(), (uintptr_t)SaveGame_Hook); + g_branchTrampoline.write5Call(LoadGame_Call.getUIntPtr(), (uintptr_t)LoadGame_Hook); + g_branchTrampoline.write5Call(DeleteSaveFile_Call.getUIntPtr(), (uintptr_t)DeleteSaveFile_Hook); + + //get pointers to IVMSaveLoadInterface vfunc entries + uintptr_t VM_SaveGame_VFunc = VirtualMachine_IVMSaveLoadInterface_VTable.getUIntPtr() + (0x1 * 0x8); + uintptr_t VM_LoadGame_VFunc = VirtualMachine_IVMSaveLoadInterface_VTable.getUIntPtr() + (0x2 * 0x8); + uintptr_t VM_DropAllRunningData_VFunc = VirtualMachine_IVMSaveLoadInterface_VTable.getUIntPtr() + (0x7 * 0x8); + + //save original vfuncs + VM_SaveGame_Original = *reinterpret_cast<_VM_SaveGame*>(VM_SaveGame_VFunc); + VM_LoadGame_Original = *reinterpret_cast<_VM_LoadGame*>(VM_LoadGame_VFunc); + VM_DropAllRunningData_Original = *reinterpret_cast<_VM_DropAllRunningData*>(VM_DropAllRunningData_VFunc); + + //overwrite vfuncs + safeWrite64(VM_SaveGame_VFunc, (uintptr_t)VM_SaveGame_Hook); + safeWrite64(VM_LoadGame_VFunc, (uintptr_t)VM_LoadGame_Hook); + safeWrite64(VM_DropAllRunningData_VFunc, (uintptr_t)VM_DropAllRunningData_Hook); +} \ No newline at end of file diff --git a/sfse/Hooks_Serialization.h b/sfse/Hooks_Serialization.h new file mode 100644 index 0000000..77c6057 --- /dev/null +++ b/sfse/Hooks_Serialization.h @@ -0,0 +1,3 @@ +#pragma once + +void Hooks_Serialization_Apply(); \ No newline at end of file diff --git a/sfse/PluginAPI.h b/sfse/PluginAPI.h index c9d0324..d8bfc4e 100644 --- a/sfse/PluginAPI.h +++ b/sfse/PluginAPI.h @@ -104,6 +104,10 @@ struct SFSEMessagingInterface kMessage_PostPostLoad, // sent right after kMessage_PostPostLoad to facilitate the correct dispatching/registering of messages/listeners kMessage_PostDataLoad, // sent right after all game data has loaded (Passes TESDataHandler as pointer) kMessage_PostPostDataLoad, // sent after all game data has loaded, and all PostDataLoad events have handled (Passes TESDataHandler as pointer) + kMessage_PreSaveGame, + kMessage_PostSaveGame, + kMessage_PreLoadGame, + kMessage_PostLoadGame, }; std::uint32_t interfaceVersion; diff --git a/sfse/Serialization.cpp b/sfse/Serialization.cpp new file mode 100644 index 0000000..40f7b10 --- /dev/null +++ b/sfse/Serialization.cpp @@ -0,0 +1,182 @@ +#include "Serialization.h" +#include "GameEvents.h" + +#include "sfse_common/Log.h" +#include "sfse_common/Errors.h" +#include "sfse_common/sfse_version.h" +#include "sfse_common/FileStream.h" +#include "sfse/GameSettings.h" + +#include +#include +#include +#include + +namespace Serialization +{ + const char* kSavegamePath = "\\My Games\\" SAVE_FOLDER_NAME "\\"; + + std::unordered_map changedIDs; + std::unordered_set deletedIDs; + std::string s_savePath; + + struct IDRemapDeleteListener : + public BSTEventSink, + public BSTEventSink + { + IDRemapDeleteListener() + { + GetEventSource()->RegisterSink(static_cast*>(this)); + GetEventSource()->RegisterSink(static_cast*>(this)); + } + + virtual EventResult ProcessEvent(const TESFormIDRemapEvent& arEvent, BSTEventSource* eventSource) + { + changedIDs[arEvent.oldID] = arEvent.newID; + return EventResult::kContinue; + }; + + virtual EventResult ProcessEvent(const TESFormDeleteEvent& arEvent, BSTEventSource* eventSource) + { + deletedIDs.insert(arEvent.formId); + return EventResult::kContinue; + }; + }; + + void RemoveFileExtension(std::string& path) + { + size_t lastDot = path.find_last_of('.'); + if (lastDot != std::string::npos) { + path.erase(lastDot); + } + } + + std::string MakeSavePath(std::string name, const char* extension, bool hasExtension) + { + if (hasExtension) + { + RemoveFileExtension(name); + } + + char path[MAX_PATH]; + ASSERT(SUCCEEDED(SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, path))); + + std::string result = path; + result += kSavegamePath; + Setting* localSavePath = (*SettingT::pCollection)->GetSetting("sLocalSavePath:General"); + if (localSavePath && (localSavePath->GetType() == Setting::kType_String)) + result += localSavePath->data.s; + else + result += "Saves\\"; + + result += "\\"; + result += name; + if (extension) + result += extension; + return result; + } + + void SetSaveName(const char* name, bool hasExtension) + { + if (name) + { + _MESSAGE("save name is %s", name); + s_savePath = MakeSavePath(name, ".sfse", hasExtension); + _MESSAGE("full save path: %s", s_savePath.c_str()); + } + else + { + _MESSAGE("cleared save path"); + s_savePath.clear(); + } + } + + void HandleBeginLoad() + { + //if the remap listener isn't already registered, register it now. + static IDRemapDeleteListener listener{}; + changedIDs.clear(); + deletedIDs.clear(); + } + + void HandleEndLoad() + { + changedIDs.clear(); + deletedIDs.clear(); + } + + bool ResolveFormId(u32 formId, u32* formIdOut) + { + if (auto iter = changedIDs.find(formId); iter != changedIDs.end()) { + (*formIdOut) = iter->second; + return true; + } + + if (deletedIDs.find(formId) == deletedIDs.end()) + { + (*formIdOut) = formId; + return true; + } + else + { + return false; + } + } + + bool ResolveHandle(u64 handle, u64* handleOut) + { + u32 formId = static_cast(handle & 0x00000000FFFFFFFF); + if (auto iter = changedIDs.find(formId); iter != changedIDs.end()) { + (*handleOut) = (handle & 0xFFFFFFFF00000000) | static_cast(iter->second); + return true; + } + + if (deletedIDs.find(formId) == deletedIDs.end()) + { + (*handleOut) = handle; + return true; + } + else + { + return false; + } + } + + void HandleRevertGlobalData() + { + _MESSAGE("RevertGlobalData"); + + //TODO: add implementation for revert callbacks. + } + + void HandleSaveGlobalData() + { + _MESSAGE("SaveGlobalData"); + + //TODO: add implementation for serialization & save callbacks. + } + + void HandleLoadGlobalData() + { + _MESSAGE("LoadGlobalData"); + + //TODO: add implementation for deserialization & load callbacks. + } + + void HandleDeleteSave(std::string filePath) + { + //check if old file is gone + FileStream saveFile; + if (!saveFile.open(filePath.c_str())) + { + RemoveFileExtension(filePath); + filePath += ".sfse"; + _MESSAGE("deleting co-save %s", filePath.c_str()); + DeleteFile(filePath.c_str()); + } + else + { + _MESSAGE("skipped delete of co-save for file %s", filePath.c_str()); + } + } +} \ No newline at end of file diff --git a/sfse/Serialization.h b/sfse/Serialization.h new file mode 100644 index 0000000..c56e574 --- /dev/null +++ b/sfse/Serialization.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include "sfse_common/Types.h" + +namespace Serialization +{ + void SetSaveName(const char* name, bool hasExtension = false); + void HandleBeginLoad(); + void HandleEndLoad(); + bool ResolveFormId(u32 formId, u32* formIdOut); + bool ResolveHandle(u64 handle, u64* handleOut); + void HandleRevertGlobalData(); + void HandleSaveGlobalData(); + void HandleLoadGlobalData(); + + void HandleDeleteSave(std::string filePath); +} \ No newline at end of file diff --git a/sfse/sfse.cpp b/sfse/sfse.cpp index 4d90a77..3b9951d 100644 --- a/sfse/sfse.cpp +++ b/sfse/sfse.cpp @@ -12,6 +12,7 @@ #include "Hooks_Version.h" #include "Hooks_Script.h" #include "Hooks_Scaleform.h" +#include "Hooks_Serialization.h" #include "Hooks_Data.h" #include "Hooks_Command.h" @@ -146,6 +147,7 @@ void SFSE_Initialize() Hooks_Version_Apply(); Hooks_Script_Apply(); Hooks_Scaleform_Apply(); + Hooks_Serialization_Apply(); Hooks_Data_Apply(); Hooks_Command_Apply();