From 74c670a0e10732abdc064a1eb5f5a4cb8d0f4295 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 19 May 2026 14:05:17 -0700 Subject: [PATCH] Add reconnect state retention QA gate --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- Docs/QA/MvpQaGates.md | 25 +++++ ...erify_reconnect_state_retention_qa_gate.py | 96 +++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_reconnect_state_retention_qa_gate.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index b2f55e9..0ab5d02 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -862,7 +862,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Can craft a shelter. Added a craft-shelter QA gate tied to primitive frame/wall/roof/shelter recipes, native build placement, `BP_PrimitiveShelter`, shelter persistence/protection hooks, and the natural shelter playable-loop smoke test. - [x] Can survive one full day/night cycle. Added a full day/night survival QA gate tied to the `4 real hours = 1 in-game day` calendar, replicated world time and solar phase, authoritative hunger/thirst/stamina/body-temperature/health pressure, fire and shelter mitigation, critical survival HUD visibility, and save/load persistence coverage before investor demos treat the gate as play-proven. - [x] Can die from survival pressure. Added a survival-pressure death QA gate requiring starvation, dehydration, cold exposure, sickness, and bleeding to reduce health on server authority, trigger `UpdateDeathState`, replicate `bIsDead` and `LastDeathReason`, show death/respawn UI feedback, support server respawn, and remain covered by player stat persistence. -- [ ] Can reconnect and retain state. +- [x] Can reconnect and retain state. Added a reconnect state-retention QA gate tied to logout/restart player snapshots, safe player identity, transform, survival, care history, inventory restore, normal-spawn fallback behavior, and the two-client manual reconnect evidence path. - [ ] Can restart server and retain placed shelter. - [ ] No critical log spam during 30-minute test. - [ ] Clean up Unreal API deprecation warnings from packaged builds, starting diff --git a/Docs/QA/MvpQaGates.md b/Docs/QA/MvpQaGates.md index 93c0f29..378728f 100644 --- a/Docs/QA/MvpQaGates.md +++ b/Docs/QA/MvpQaGates.md @@ -179,3 +179,28 @@ Required evidence: This gate is gameplay/server-relevant and should be covered by the next server package deployment. + +## Reconnect State Retention + +The reconnect state retention gate proves a player can disconnect, reconnect, +and recover the MVP state that matters for a continuing survival session instead +of starting over from a blank pawn. + +Required evidence: + +- `AAgrarianGameGameMode::Logout` captures a player snapshot before the pawn is + released. +- `AAgrarianGameGameMode::RestartPlayer` restores a matching snapshot after the + normal MVP spawn path. +- `UAgrarianPersistenceSubsystem::SavePlayerSnapshot` and + `RestorePlayerSnapshot` preserve transform, safe player identity, survival, + care history, and inventory. +- Inventory restore uses `RestoreSavedItems` so derived carry weight and + inventory listeners refresh after reconnect. +- If no matching snapshot exists, the player falls back to the normal MVP spawn + point instead of failing the join. +- The manual two-client test records reconnect evidence alongside world + time/weather consistency. + +This gate is server-relevant and must be rechecked after multiplayer package +deployment. diff --git a/Scripts/verify_reconnect_state_retention_qa_gate.py b/Scripts/verify_reconnect_state_retention_qa_gate.py new file mode 100644 index 0000000..a6a90d1 --- /dev/null +++ b/Scripts/verify_reconnect_state_retention_qa_gate.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Verify the MVP reconnect state-retention QA gate is covered.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" +QA_DOC = ROOT / "Docs" / "QA" / "MvpQaGates.md" +NETWORKING = ROOT / "Docs" / "MultiplayerNetworkingDesign.md" +GAME_MODE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianGameGameMode.h" +GAME_MODE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGameGameMode.cpp" +PERSISTENCE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.h" +PERSISTENCE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianPersistenceSubsystem.cpp" +SAVE_GAME_H = ROOT / "Source" / "AgrarianGame" / "AgrarianSaveGame.h" +INVENTORY_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianInventoryComponent.cpp" +DISCONNECT_VERIFY = ROOT / "Scripts" / "verify_disconnect_reconnect_handling.py" +IDENTITY_VERIFY = ROOT / "Scripts" / "verify_player_identity_persistence.py" +STATS_VERIFY = ROOT / "Scripts" / "verify_player_stats_persistence.py" +INVENTORY_VERIFY = ROOT / "Scripts" / "verify_inventory_persistence.py" +TWO_CLIENT_VERIFY = ROOT / "Scripts" / "verify_two_client_connection_gate.py" + +REQUIRED = { + QA_DOC: [ + "## Reconnect State Retention", + "Logout", + "RestartPlayer", + "transform, safe player identity, survival", + "RestoreSavedItems", + ], + NETWORKING: [ + "## Disconnect And Reconnect", + "player reconnect snapshot", + "preserves transform, survival, care history, and", + "inventory through `UAgrarianPersistenceSubsystem::SavePlayerSnapshot`", + "normal MVP spawn point", + ], + GAME_MODE_H: [ + "virtual void RestartPlayer(AController* NewPlayer) override;", + "virtual void Logout(AController* Exiting) override;", + ], + GAME_MODE_CPP: [ + "Super::RestartPlayer(NewPlayer)", + "Persistence->RestorePlayerSnapshot(AgrarianCharacter)", + "Persistence->SavePlayerSnapshot(AgrarianCharacter)", + "Super::Logout(Exiting)", + ], + PERSISTENCE_H: [ + "bool SavePlayerSnapshot(const AAgrarianGameCharacter* Character) const;", + "bool RestorePlayerSnapshot(AAgrarianGameCharacter* Character) const;", + ], + PERSISTENCE_CPP: [ + "SavedPlayer.Transform = Character->GetActorTransform();", + "SavedPlayer.Survival = SurvivalComponent->Survival;", + "SavedPlayer.CareHistory = SurvivalComponent->CareHistory;", + "SavedPlayer.Inventory = InventoryComponent->Items;", + "Character->SetActorTransform(SavedPlayer->Transform", + "InventoryComponent->RestoreSavedItems(SavedPlayer->Inventory);", + "return UniqueId->ToString();", + ], + SAVE_GAME_H: [ + "struct FAgrarianSavedPlayer", + "FAgrarianSavedPlayerIdentity Identity;", + "FTransform Transform;", + "FAgrarianSurvivalSnapshot Survival;", + "TArray Inventory;", + ], + INVENTORY_CPP: [ + "UAgrarianInventoryComponent::RestoreSavedItems", + "BroadcastInventoryChanged()", + ], + DISCONNECT_VERIFY: ["PASS: MVP disconnect/reconnect snapshot handling"], + IDENTITY_VERIFY: ["player identity persistence"], + STATS_VERIFY: ["player stats persist"], + INVENTORY_VERIFY: ["inventory persistence"], + TWO_CLIENT_VERIFY: ["Two-Client Connection"], + ROADMAP: [ + "[x] Can reconnect and retain state.", + ], +} + + +def main() -> None: + missing: list[str] = [] + for path, snippets in REQUIRED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}") + if missing: + raise SystemExit("FAILED: " + "; ".join(missing)) + print("OK: reconnect state-retention QA gate is tied to logout/restart snapshots and persistence coverage.") + + +if __name__ == "__main__": + main()