Back up save before overwrite

This commit is contained in:
2026-05-18 19:37:35 -07:00
parent 65599b9d2f
commit dfad8809c7
5 changed files with 89 additions and 1 deletions
+4 -1
View File
@@ -778,7 +778,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
`FAgrarianSavedTileRegistryState` with the active Ground Zero tile ID,
registry path, schema version, generation version, and package version while
keeping the authoritative registry in `Data/Tiles/ground_zero_tiles.json`.
- [ ] Add backup-before-save option.
- [x] Add backup-before-save option. `UAgrarianPersistenceSubsystem` now has
`bBackupBeforeSave` enabled by default; before overwriting an existing slot,
`WriteSave` copies the current `.sav` into `Saved/SaveGames/Backups` with a
UTC timestamp.
- [ ] Add recovery plan for corrupted save.
- [ ] Document persistence limitations.
+6
View File
@@ -467,6 +467,12 @@ GameMode registers the current MVP persistent actor classes, checks
`DoesSaveExist`, and calls `LoadCurrentWorld` without clearing existing map
actors so map-authored resources and startup content remain owned by the map.
Backup-before-save is enabled by default through
`UAgrarianPersistenceSubsystem::bBackupBeforeSave`. Before overwriting an
existing save, `WriteSave` copies the current slot file from `Saved/SaveGames`
into `Saved/SaveGames/Backups` with a UTC timestamp. Missing save files simply
skip backup creation, which keeps the first save path simple.
## Testing Gates
Minimum persistence smoke test:
+48
View File
@@ -0,0 +1,48 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EXPECTED = {
ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.h": [
"bool bBackupBeforeSave = true;",
"bool BackupExistingSave() const;",
],
ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.cpp": [
"if (SaveGame && bBackupBeforeSave)",
"BackupExistingSave();",
"FPaths::ProjectSavedDir()",
"TEXT(\"SaveGames\")",
"TEXT(\"Backups\")",
"FDateTime::UtcNow().ToString",
"IFileManager::Get().Copy",
],
ROOT / "Docs" / "PersistenceDesignDocument.md": [
"`UAgrarianPersistenceSubsystem::bBackupBeforeSave`",
"`Saved/SaveGames/Backups`",
"UTC timestamp",
],
ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [
"[x] Add backup-before-save option.",
"`bBackupBeforeSave`",
"`Saved/SaveGames/Backups`",
],
}
def main() -> None:
missing = []
for path, snippets in EXPECTED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)}: {snippet}")
if missing:
raise RuntimeError("Backup-before-save verification failed: " + "; ".join(missing))
print("PASS: backup-before-save is wired before slot overwrite.")
if __name__ == "__main__":
main()
@@ -10,6 +10,9 @@
#include "AgrarianSurvivalComponent.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "HAL/FileManager.h"
#include "Misc/DateTime.h"
#include "Misc/Paths.h"
#include "GameFramework/PlayerState.h"
#include "Kismet/GameplayStatics.h"
@@ -33,6 +36,11 @@ UAgrarianSaveGame* UAgrarianPersistenceSubsystem::LoadOrCreateSave() const
bool UAgrarianPersistenceSubsystem::WriteSave(UAgrarianSaveGame* SaveGame) const
{
if (SaveGame && bBackupBeforeSave)
{
BackupExistingSave();
}
return SaveGame ? UGameplayStatics::SaveGameToSlot(SaveGame, DefaultSlotName, UserIndex) : false;
}
@@ -316,6 +324,25 @@ bool UAgrarianPersistenceSubsystem::LoadCurrentWorld(int32& RestoredPlayerCount,
return bRestoredWorldState;
}
bool UAgrarianPersistenceSubsystem::BackupExistingSave() const
{
const FString SaveFilePath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SaveGames"), DefaultSlotName + TEXT(".sav"));
if (!FPaths::FileExists(SaveFilePath))
{
return false;
}
const FString BackupDirectory = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SaveGames"), TEXT("Backups"));
IFileManager::Get().MakeDirectory(*BackupDirectory, true);
const FString Timestamp = FDateTime::UtcNow().ToString(TEXT("%Y%m%dT%H%M%SZ"));
const FString BackupFileName = FString::Printf(TEXT("%s-%s.sav"), *DefaultSlotName, *Timestamp);
const FString BackupFilePath = FPaths::Combine(BackupDirectory, BackupFileName);
const bool bCopied = IFileManager::Get().Copy(*BackupFilePath, *SaveFilePath, true, true) == COPY_OK;
UE_LOG(LogTemp, Log, TEXT("Agrarian save backup %s: %s"), bCopied ? TEXT("created") : TEXT("failed"), *BackupFilePath);
return bCopied;
}
bool UAgrarianPersistenceSubsystem::CapturePlayerIntoSave(const AAgrarianGameCharacter* Character, UAgrarianSaveGame* SaveGame) const
{
const UAgrarianSurvivalComponent* SurvivalComponent = Character ? Character->GetSurvivalComponent() : nullptr;
@@ -23,6 +23,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
int32 UserIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
bool bBackupBeforeSave = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
TMap<FName, TSubclassOf<AActor>> WorldActorClassRegistry;
@@ -78,6 +81,7 @@ public:
bool LoadCurrentWorld(int32& RestoredPlayerCount, int32& RestoredWorldActorCount, bool bClearExistingActors = true) const;
protected:
bool BackupExistingSave() const;
bool CapturePlayerIntoSave(const AAgrarianGameCharacter* Character, UAgrarianSaveGame* SaveGame) const;
bool RestorePlayerFromSave(AAgrarianGameCharacter* Character, const UAgrarianSaveGame* SaveGame) const;
void FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const;