Back up save before overwrite
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user