Save MVP player identity metadata

This commit is contained in:
2026-05-18 19:10:44 -07:00
parent a23f886cfa
commit 1e0d326657
5 changed files with 105 additions and 1 deletions
+4 -1
View File
@@ -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.
+8
View File
@@ -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:
@@ -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()
@@ -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())
{
+24
View File
@@ -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;