Files
ProjectEleri/Plugins/DirectiveUtilities/Source/DirectiveUtilitiesRuntime/Private/Libraries/DirectiveUtilSaveGameFunctionLibrary.cpp

194 lines
4.9 KiB
C++

// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/SaveGame.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "PlatformFeatures.h"
#include "SaveGameSystem.h"
#include <ctime>
namespace
{
FString GetSaveGamesDirectory()
{
return FPaths::ProjectSavedDir() / TEXT("SaveGames");
}
FString GetSaveSlotFilePath(const FString& SlotName)
{
return GetSaveGamesDirectory() / (SlotName + TEXT(".sav"));
}
bool IsValidSaveSlotName(const FString& SlotName)
{
return UDirectiveUtilStringFunctionLibrary::IsValidFileName(SlotName);
}
ISaveGameSystem* GetSaveGameSystem()
{
return IPlatformFeaturesModule::Get().GetSaveGameSystem();
}
FDateTime ConvertUtcFileTimeToLocal(const FDateTime& UtcTimestamp)
{
const int64 UnixSeconds = UtcTimestamp.ToUnixTimestamp();
const int32 Milliseconds = UtcTimestamp.GetMillisecond();
const time_t Time = static_cast<time_t>(UnixSeconds);
tm LocalTm;
#if PLATFORM_WINDOWS
if (localtime_s(&LocalTm, &Time) != 0)
{
return UtcTimestamp;
}
#else
if (localtime_r(&Time, &LocalTm) == nullptr)
{
return UtcTimestamp;
}
#endif
return FDateTime(
LocalTm.tm_year + 1900,
LocalTm.tm_mon + 1,
LocalTm.tm_mday,
LocalTm.tm_hour,
LocalTm.tm_min,
LocalTm.tm_sec,
Milliseconds);
}
}
TArray<FString> UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames()
{
TArray<FString> SlotNames;
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
{
if (SaveSystem->GetSaveGameNames(SlotNames, 0))
{
return SlotNames;
}
}
TArray<FString> Files;
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
SlotNames.Reserve(Files.Num());
for (const FString& File : Files)
{
SlotNames.Add(FPaths::GetBaseFilename(File));
}
return SlotNames;
}
bool UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(const FString& SlotName, FDateTime& OutTimestamp)
{
OutTimestamp = FDateTime();
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
{
if (!SaveSystem->DoesSaveGameExist(*SlotName, 0))
{
return false;
}
}
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
if (Timestamp == FDateTime::MinValue())
{
return false;
}
OutTimestamp = ConvertUtcFileTimeToLocal(Timestamp);
return true;
}
bool UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(USaveGame* SaveGameObject, TArray<uint8>& OutBytes)
{
OutBytes.Reset();
if (!SaveGameObject)
{
return false;
}
return UGameplayStatics::SaveGameToMemory(SaveGameObject, OutBytes);
}
USaveGame* UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(const TArray<uint8>& SaveData)
{
if (SaveData.Num() == 0)
{
return nullptr;
}
return UGameplayStatics::LoadGameFromMemory(SaveData);
}
bool UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(const FString& SlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
return UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex);
}
bool UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(const FString& SlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
return UGameplayStatics::DeleteGameInSlot(SlotName, UserIndex);
}
bool UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName)
|| OldSlotName.Equals(NewSlotName, ESearchCase::CaseSensitive))
{
return false;
}
const bool bCaseOnlyRename = OldSlotName.Equals(NewSlotName, ESearchCase::IgnoreCase);
if (bCaseOnlyRename)
{
TArray<uint8> SaveData;
if (!UGameplayStatics::LoadDataFromSlot(SaveData, OldSlotName, UserIndex) || SaveData.Num() == 0)
{
return false;
}
if (!UGameplayStatics::SaveDataToSlot(SaveData, NewSlotName, UserIndex))
{
return false;
}
// Slot-name case sensitivity belongs to the active platform backend. Deleting
// the old spelling here can delete the newly written slot on a case-insensitive
// filesystem, so treat both spellings as the same logical slot and rewrite it.
return true;
}
if (!UGameplayStatics::DoesSaveGameExist(OldSlotName, UserIndex) || UGameplayStatics::DoesSaveGameExist(NewSlotName, UserIndex))
{
return false;
}
TArray<uint8> SaveData;
if (!UGameplayStatics::LoadDataFromSlot(SaveData, OldSlotName, UserIndex))
{
return false;
}
if (!UGameplayStatics::SaveDataToSlot(SaveData, NewSlotName, UserIndex))
{
return false;
}
// If this delete fails the new copy is kept alongside the original, so the save is never lost.
return UGameplayStatics::DeleteGameInSlot(OldSlotName, UserIndex);
}