From 1e0d326657576750f08fdd4a5030576fb23f687f Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 19:10:44 -0700 Subject: [PATCH] Save MVP player identity metadata --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 5 +- Docs/PersistenceDesignDocument.md | 8 +++ Scripts/verify_player_identity_persistence.py | 51 +++++++++++++++++++ .../AgrarianPersistenceSubsystem.cpp | 18 +++++++ Source/AgrarianGame/AgrarianSaveGame.h | 24 +++++++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_player_identity_persistence.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 3c31048..6c98ddf 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -733,7 +733,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Decide MVP persistence scope. - [x] Decide what tile metadata is stored in save data vs external tile registry. -- [ ] Save player identity. +- [x] Save player identity. Player save records now keep a + backwards-compatible `PlayerId` plus `FAgrarianSavedPlayerIdentity`, prefer a + valid `APlayerState` network ID when available, and retain safe display/pawn + metadata without storing credentials. - [ ] Save player stats. - [ ] Save long-term character care history placeholders without applying aging gameplay yet. - [ ] Save player inventory. diff --git a/Docs/PersistenceDesignDocument.md b/Docs/PersistenceDesignDocument.md index 0c4dcfb..a5b31aa 100644 --- a/Docs/PersistenceDesignDocument.md +++ b/Docs/PersistenceDesignDocument.md @@ -401,6 +401,14 @@ Do not store: Player records should store stable IDs and gameplay state, not sensitive account credentials. +For the MVP `USaveGame` path, each saved player keeps both a backwards-compatible +`PlayerId` string and an `FAgrarianSavedPlayerIdentity` block. The stable ID +prefers the replicated `APlayerState` unique network ID when one exists, then +falls back to player name, then pawn name for local prototype sessions. The +identity block also records the display player name, raw network ID, whether +the network ID was used, and the last known pawn name. It deliberately does not +store passwords, tokens, emails, or platform credentials. + ## Testing Gates Minimum persistence smoke test: diff --git a/Scripts/verify_player_identity_persistence.py b/Scripts/verify_player_identity_persistence.py new file mode 100644 index 0000000..17ff306 --- /dev/null +++ b/Scripts/verify_player_identity_persistence.py @@ -0,0 +1,51 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianSaveGame.h": [ + "struct FAgrarianSavedPlayerIdentity", + "FString StablePlayerId;", + "FString PlayerName;", + "FString NetworkId;", + "bool bUsedNetworkId = false;", + "FAgrarianSavedPlayerIdentity Identity;", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.cpp": [ + "SavedPlayer.Identity.StablePlayerId = SavedPlayer.PlayerId;", + "SavedPlayer.Identity.LastKnownPawnName = Character->GetName();", + "SavedPlayer.Identity.PlayerName = PlayerState->GetPlayerName();", + "SavedPlayer.Identity.NetworkId = UniqueId->ToString();", + "SavedPlayer.Identity.bUsedNetworkId = true;", + "return UniqueId->ToString();", + ], + ROOT / "Docs" / "PersistenceDesignDocument.md": [ + "`FAgrarianSavedPlayerIdentity`", + "prefers the replicated `APlayerState` unique network ID", + "store passwords, tokens, emails, or platform credentials", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Save player identity.", + "`FAgrarianSavedPlayerIdentity`", + "without storing credentials", + ], +} + + +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("Player identity persistence verification failed: " + "; ".join(missing)) + + print("PASS: player identity persistence captures safe stable identity metadata.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp index 66ebe8b..7b2ca25 100644 --- a/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp +++ b/Source/AgrarianGame/AgrarianPersistenceSubsystem.cpp @@ -326,6 +326,18 @@ bool UAgrarianPersistenceSubsystem::CapturePlayerIntoSave(const AAgrarianGameCha FAgrarianSavedPlayer SavedPlayer; SavedPlayer.PlayerId = GetPlayerPersistenceId(Character); + SavedPlayer.Identity.StablePlayerId = SavedPlayer.PlayerId; + SavedPlayer.Identity.LastKnownPawnName = Character->GetName(); + if (const APlayerState* PlayerState = Character->GetPlayerState()) + { + SavedPlayer.Identity.PlayerName = PlayerState->GetPlayerName(); + const FUniqueNetIdRepl& UniqueId = PlayerState->GetUniqueId(); + if (UniqueId.IsValid()) + { + SavedPlayer.Identity.NetworkId = UniqueId->ToString(); + SavedPlayer.Identity.bUsedNetworkId = true; + } + } SavedPlayer.Transform = Character->GetActorTransform(); SavedPlayer.Survival = SurvivalComponent->Survival; SavedPlayer.CareHistory = SurvivalComponent->CareHistory; @@ -448,6 +460,12 @@ FString UAgrarianPersistenceSubsystem::GetPlayerPersistenceId(const AAgrarianGam if (const APlayerState* PlayerState = Character->GetPlayerState()) { + const FUniqueNetIdRepl& UniqueId = PlayerState->GetUniqueId(); + if (UniqueId.IsValid()) + { + return UniqueId->ToString(); + } + const FString PlayerName = PlayerState->GetPlayerName(); if (!PlayerName.IsEmpty()) { diff --git a/Source/AgrarianGame/AgrarianSaveGame.h b/Source/AgrarianGame/AgrarianSaveGame.h index a5b786c..1ccc5d6 100644 --- a/Source/AgrarianGame/AgrarianSaveGame.h +++ b/Source/AgrarianGame/AgrarianSaveGame.h @@ -7,6 +7,27 @@ #include "AgrarianTypes.h" #include "AgrarianSaveGame.generated.h" +USTRUCT(BlueprintType) +struct FAgrarianSavedPlayerIdentity +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + FString StablePlayerId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + FString PlayerName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + FString NetworkId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + bool bUsedNetworkId = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + FString LastKnownPawnName; +}; + USTRUCT(BlueprintType) struct FAgrarianSavedPlayer { @@ -15,6 +36,9 @@ struct FAgrarianSavedPlayer UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") FString PlayerId; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") + FAgrarianSavedPlayerIdentity Identity; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save") FTransform Transform;