// Copyright Epic Games, Inc. All Rights Reserved. // UE5 Modular Game Framework — SS_SaveManager Implementation #include "Save/SS_SaveManager.h" #include "Core/GI_GameFramework.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "HAL/PlatformFileManager.h" #include "Serialization/MemoryReader.h" #include "Serialization/MemoryWriter.h" #include "Serialization/ObjectAndNameAsStringProxyArchive.h" DEFINE_LOG_CATEGORY_STATIC(LogSave, Log, All); USS_SaveManager::USS_SaveManager() { MaxSlots = 10; SavePrefix = TEXT("FrameworkSave_"); } // ============================================================================ // Lifecycle // ============================================================================ void USS_SaveManager::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Initialize — Save directory: %s"), *GetSaveDirectory()); // Ensure save directory exists. IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (!PlatformFile.DirectoryExists(*GetSaveDirectory())) { PlatformFile.CreateDirectoryTree(*GetSaveDirectory()); } // Broadcast initial manifest. OnSaveManifestUpdated.Broadcast(GetSlotManifest()); } void USS_SaveManager::Deinitialize() { UE_LOG(LogSave, Log, TEXT("SS_SaveManager::Deinitialize")); Super::Deinitialize(); } // ============================================================================ // Slot Manifest // ============================================================================ TArray USS_SaveManager::GetSlotManifest() const { TArray Manifest; for (int32 i = 0; i < MaxSlots; ++i) { FSaveSlotInfo Info = ReadSlotHeader(i); Info.SlotIndex = i; Manifest.Add(Info); } return Manifest; } bool USS_SaveManager::DoesSlotExist(int32 SlotIndex) const { if (SlotIndex < 0 || SlotIndex >= MaxSlots) { return false; } FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); return PlatformFile.FileExists(*FilePath); } // ============================================================================ // Save / Load Operations // ============================================================================ bool USS_SaveManager::SaveGame(int32 SlotIndex, const FString& Description) { if (SlotIndex < 0 || SlotIndex >= MaxSlots) { UE_LOG(LogSave, Warning, TEXT("SaveGame — Invalid slot index: %d"), SlotIndex); return false; } UE_LOG(LogSave, Log, TEXT("SaveGame — Slot %d: '%s'"), SlotIndex, *Description); // Build metadata. FSaveSlotInfo Meta; Meta.SlotIndex = SlotIndex; Meta.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex); Meta.ChapterName = Description; // Simplified — real impl gets chapter from GS_CoreGameState. Meta.Timestamp = FDateTime::Now(); // Serialize game state to binary buffer. // In a full implementation, this calls I_Persistable::OnSave() on all registered actors. TArray SaveData; FMemoryWriter Writer(SaveData); FObjectAndNameAsStringProxyArchive Ar(Writer, true); // Ar << GameStateData... bool bSuccess = SaveToFile(SlotIndex, SaveData, Meta); OnSaveComplete.Broadcast(SlotIndex, bSuccess); OnSaveManifestUpdated.Broadcast(GetSlotManifest()); return bSuccess; } bool USS_SaveManager::LoadGame(int32 SlotIndex) { if (!DoesSlotExist(SlotIndex)) { UE_LOG(LogSave, Warning, TEXT("LoadGame — Slot %d does not exist"), SlotIndex); OnLoadComplete.Broadcast(SlotIndex, false); return false; } UE_LOG(LogSave, Log, TEXT("LoadGame — Slot %d"), SlotIndex); TArray SaveData; FSaveSlotInfo Meta; if (!LoadFromFile(SlotIndex, SaveData, Meta)) { UE_LOG(LogSave, Error, TEXT("LoadGame — Failed to read slot %d from disk"), SlotIndex); OnLoadComplete.Broadcast(SlotIndex, false); return false; } // Deserialize game state. FMemoryReader Reader(SaveData); FObjectAndNameAsStringProxyArchive Ar(Reader, true); // Ar << GameStateData... // Set active slot in GameInstance. UGI_GameFramework* GI = Cast(GetGameInstance()); if (GI) { GI->SetActiveSlot(SlotIndex); } OnLoadComplete.Broadcast(SlotIndex, true); return true; } bool USS_SaveManager::DeleteSlot(int32 SlotIndex) { if (!DoesSlotExist(SlotIndex)) { UE_LOG(LogSave, Warning, TEXT("DeleteSlot — Slot %d does not exist"), SlotIndex); return false; } FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); bool bDeleted = PlatformFile.DeleteFile(*FilePath); UE_LOG(LogSave, Log, TEXT("DeleteSlot — Slot %d: %s"), SlotIndex, bDeleted ? TEXT("Deleted") : TEXT("Failed")); OnSaveManifestUpdated.Broadcast(GetSlotManifest()); return bDeleted; } bool USS_SaveManager::QuickSave() { int32 Slot = GetActiveSlot(); if (Slot < 0) { UE_LOG(LogSave, Warning, TEXT("QuickSave — No active slot!")); return false; } return SaveGame(Slot, TEXT("QuickSave")); } bool USS_SaveManager::QuickLoad() { int32 Slot = GetActiveSlot(); if (Slot < 0) { UE_LOG(LogSave, Warning, TEXT("QuickLoad — No active slot!")); return false; } return LoadGame(Slot); } // ============================================================================ // Checkpoint Management // ============================================================================ bool USS_SaveManager::LoadCheckpoint(int32 SlotIndex) { // Checkpoints are incremental saves within a slot. // For now, loads the full slot (same as LoadGame). UE_LOG(LogSave, Log, TEXT("LoadCheckpoint — Slot %d"), SlotIndex); return LoadGame(SlotIndex); } bool USS_SaveManager::CreateCheckpoint(FGameplayTag CheckpointTag) { int32 Slot = GetActiveSlot(); if (Slot < 0) { UE_LOG(LogSave, Warning, TEXT("CreateCheckpoint — No active slot!")); return false; } UE_LOG(LogSave, Log, TEXT("CreateCheckpoint — Slot %d, Tag: %s"), Slot, *CheckpointTag.GetTagName().ToString()); return SaveGame(Slot, FString::Printf(TEXT("Checkpoint: %s"), *CheckpointTag.GetTagName().ToString())); } // ============================================================================ // Utilities // ============================================================================ int64 USS_SaveManager::GetTotalSaveSize() const { int64 TotalSize = 0; IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); for (int32 i = 0; i < MaxSlots; ++i) { FString FilePath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav"); if (PlatformFile.FileExists(*FilePath)) { TotalSize += PlatformFile.FileSize(*FilePath); } } return TotalSize; } bool USS_SaveManager::BackupAllSaves(const FString& BackupLabel) { FString BackupDir = GetSaveDirectory() / TEXT("Backups") / BackupLabel; IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (!PlatformFile.DirectoryExists(*BackupDir)) { PlatformFile.CreateDirectoryTree(*BackupDir); } int32 BackedUp = 0; for (int32 i = 0; i < MaxSlots; ++i) { FString SrcPath = GetSaveDirectory() / GetSlotName(i) + TEXT(".sav"); FString DstPath = BackupDir / GetSlotName(i) + TEXT(".sav"); if (PlatformFile.FileExists(*SrcPath)) { if (PlatformFile.CopyFile(*DstPath, *SrcPath)) { ++BackedUp; } } } UE_LOG(LogSave, Log, TEXT("BackupAllSaves — '%s': %d slots backed up"), *BackupLabel, BackedUp); return BackedUp > 0; } // ============================================================================ // Internal // ============================================================================ FString USS_SaveManager::GetSlotName(int32 SlotIndex) const { return FString::Printf(TEXT("%s%d"), *SavePrefix, SlotIndex); } FString USS_SaveManager::GetSaveDirectory() const { return FPaths::ProjectSavedDir() / TEXT("SaveGames"); } int32 USS_SaveManager::GetActiveSlot() const { UGI_GameFramework* GI = Cast(GetGameInstance()); return GI ? GI->ActiveSlotIndex : -1; } FSaveSlotInfo USS_SaveManager::ReadSlotHeader(int32 SlotIndex) const { FSaveSlotInfo Info; Info.SlotIndex = SlotIndex; FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (!PlatformFile.FileExists(*FilePath)) { Info.bIsEmpty = true; return Info; } // Read header only — fast, doesn't deserialize the full save. TArray FileData; if (FFileHelper::LoadFileToArray(FileData, *FilePath)) { // Simplified header parsing — real impl reads a FSaveSlotInfo struct prefix. Info.bIsEmpty = false; Info.SlotName = FString::Printf(TEXT("Slot %d"), SlotIndex); Info.Timestamp = PlatformFile.GetTimeStamp(*FilePath); // In full impl: deserialize header from FileData. } return Info; } bool USS_SaveManager::SaveToFile(int32 SlotIndex, const TArray& Data, const FSaveSlotInfo& Meta) { FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); // Serialize metadata header + save data. TArray FileData; FMemoryWriter Writer(FileData); // Write metadata header first. Writer << const_cast(Meta); // Then write game state data. Writer.Serialize(const_cast(Data.GetData()), Data.Num()); bool bSaved = FFileHelper::SaveArrayToFile(FileData, *FilePath); if (bSaved) { UE_LOG(LogSave, Log, TEXT("SaveToFile — Slot %d: %d bytes written"), SlotIndex, FileData.Num()); } else { UE_LOG(LogSave, Error, TEXT("SaveToFile — Slot %d: FAILED to write!"), SlotIndex); } return bSaved; } bool USS_SaveManager::LoadFromFile(int32 SlotIndex, TArray& OutData, FSaveSlotInfo& OutMeta) { FString FilePath = GetSaveDirectory() / GetSlotName(SlotIndex) + TEXT(".sav"); TArray FileData; if (!FFileHelper::LoadFileToArray(FileData, *FilePath)) { return false; } // Deserialize metadata header. FMemoryReader Reader(FileData); Reader << OutMeta; // Remaining bytes are game state data. int32 HeaderSize = Reader.Tell(); OutData.SetNum(FileData.Num() - HeaderSize); FMemory::Memcpy(OutData.GetData(), FileData.GetData() + HeaderSize, OutData.Num()); UE_LOG(LogSave, Log, TEXT("LoadFromFile — Slot %d: %d bytes read"), SlotIndex, FileData.Num()); return true; }