From ebc7aa1ec6f62323e5ca37488e7192076cd462f8 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 15:12:20 -0700 Subject: [PATCH] Add MVP server travel flow --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 5 +- Docs/MultiplayerNetworkingDesign.md | 13 ++++ Docs/Ops/DedicatedServerBuildRunbook.md | 9 +++ Scripts/verify_server_travel_flow.py | 75 +++++++++++++++++++ .../AgrarianGamePlayerController.cpp | 40 ++++++++++ .../AgrarianGamePlayerController.h | 6 ++ 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_server_travel_flow.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index e363662..c2f869f 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -704,7 +704,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe script already exist, then added a reusable Ubuntu gameplay-server bootstrap script and standardized the MVP endpoint as `play.agrariangame.com` on `7777/udp`. -- [ ] Add server travel flow. +- [x] Add server travel flow. Added an allowlisted `AgrarianServerTravel + GroundZero` admin/dev command that routes through server authority and travels + to `/Game/Agrarian/Maps/L_GroundZeroTerrain_Test?listen` for repeatable MVP + listen-server and dedicated-server tests. - [x] Define server authority over streamed terrain tiles. - [x] Define server response when a client requests a missing tile. - [x] Add player join flow. diff --git a/Docs/MultiplayerNetworkingDesign.md b/Docs/MultiplayerNetworkingDesign.md index 9d6e245..01badf8 100644 --- a/Docs/MultiplayerNetworkingDesign.md +++ b/Docs/MultiplayerNetworkingDesign.md @@ -138,6 +138,19 @@ MVP join flow: The first MVP does not need account services, matchmaking, or full character persistence. It does need a deterministic flow that can be tested repeatedly. +## Server Travel Flow + +MVP server travel is intentionally allowlisted. The admin/dev command +`AgrarianServerTravel GroundZero` routes through the server and resolves to: + +```text +/Game/Agrarian/Maps/L_GroundZeroTerrain_Test?listen +``` + +This gives listen-server and dedicated-server tests one repeatable travel path +without exposing arbitrary map URLs to clients. Future travel expansion should +add explicit tile/session validation before allowing any new map or region. + ## Spawn And Respawn Initial MVP spawn: diff --git a/Docs/Ops/DedicatedServerBuildRunbook.md b/Docs/Ops/DedicatedServerBuildRunbook.md index 57c9114..31b6fc4 100644 --- a/Docs/Ops/DedicatedServerBuildRunbook.md +++ b/Docs/Ops/DedicatedServerBuildRunbook.md @@ -78,6 +78,15 @@ First cloud/server launch target: ./AgrarianGameServer L_GroundZeroTerrain_Test?listen -log -port=7777 ``` +In-game/admin MVP travel command: + +```text +AgrarianServerTravel GroundZero +``` + +This command is allowlisted to the Ground Zero MVP map and should remain the +only server travel path until tile/session validation is stronger. + Systemd launch target created by the bootstrap script: ```bash diff --git a/Scripts/verify_server_travel_flow.py b/Scripts/verify_server_travel_flow.py new file mode 100644 index 0000000..05257fc --- /dev/null +++ b/Scripts/verify_server_travel_flow.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Validate MVP server travel flow wiring.""" + +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] + + +def read(relative_path: str) -> str: + path = ROOT / relative_path + if not path.exists(): + raise AssertionError(f"Missing required file: {relative_path}") + return path.read_text(encoding="utf-8") + + +def require(content: str, needle: str, context: str) -> None: + if needle not in content: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def main() -> int: + errors: list[str] = [] + checks = { + "Source/AgrarianGame/AgrarianGamePlayerController.h": [ + "void AgrarianServerTravel(FName MapName);", + "void ServerAgrarianServerTravel(FName MapName);", + ], + "Source/AgrarianGame/AgrarianGamePlayerController.cpp": [ + "GroundZeroServerTravelMapPath", + "ResolveAgrarianServerTravelMap", + "AgrarianServerTravel(FName MapName)", + "ServerAgrarianServerTravel_Implementation", + "HasAuthority()", + "AgrarianServerTravel GroundZero", + "World->ServerTravel(TravelURL, false, false)", + "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test", + "?listen", + ], + "Docs/MultiplayerNetworkingDesign.md": [ + "Server Travel Flow", + "AgrarianServerTravel GroundZero", + "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test?listen", + "without exposing arbitrary map URLs", + ], + "Docs/Ops/DedicatedServerBuildRunbook.md": [ + "AgrarianServerTravel GroundZero", + "allowlisted to the Ground Zero MVP map", + ], + "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "[x] Add server travel flow.", + "AgrarianServerTravel", + "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test?listen", + ], + } + + for relative_path, needles in checks.items(): + try: + content = read(relative_path) + for needle in needles: + require(content, needle, relative_path) + except AssertionError as exc: + errors.append(str(exc)) + + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + print("PASS: MVP server travel flow is wired and documented.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 7d70b96..e59fc14 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -22,6 +22,20 @@ namespace { const FVector GroundZeroDeveloperTravelHomeLocation(-22000.0f, -3500.0f, 1148.0f); + const TCHAR* GroundZeroServerTravelMapPath = TEXT("/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"); + + bool ResolveAgrarianServerTravelMap(const FName MapName, FString& OutTravelMap) + { + if (MapName == NAME_None + || MapName == TEXT("GroundZero") + || MapName == TEXT("L_GroundZeroTerrain_Test")) + { + OutTravelMap = GroundZeroServerTravelMapPath; + return true; + } + + return false; + } bool ApplyAgrarianItemUseEffect(const FName ItemId, const int32 Quantity, UAgrarianSurvivalComponent* SurvivalComponent, FString& OutEffectSummary) { @@ -260,6 +274,11 @@ void AAgrarianGamePlayerController::AgrarianTravelHome() ServerAgrarianTravel(GroundZeroDeveloperTravelHomeLocation); } +void AAgrarianGamePlayerController::AgrarianServerTravel(FName MapName) +{ + ServerAgrarianServerTravel(MapName); +} + void AAgrarianGamePlayerController::ServerAgrarianGrantItem_Implementation(FName ItemId, int32 Quantity) { AAgrarianGameCharacter* AgrarianCharacter = GetPawn(); @@ -521,3 +540,24 @@ void AAgrarianGamePlayerController::ServerAgrarianTravel_Implementation(FVector ClientMessage(TEXT("Developer travel failed: invalid destination.")); } } + +void AAgrarianGamePlayerController::ServerAgrarianServerTravel_Implementation(FName MapName) +{ + UWorld* World = GetWorld(); + if (!World || !HasAuthority()) + { + ClientMessage(TEXT("Agrarian server travel failed: no authoritative world.")); + return; + } + + FString TravelMap; + if (!ResolveAgrarianServerTravelMap(MapName, TravelMap)) + { + ClientMessage(TEXT("Usage: AgrarianServerTravel GroundZero")); + return; + } + + const FString TravelURL = FString::Printf(TEXT("%s?listen"), *TravelMap); + ClientMessage(FString::Printf(TEXT("Agrarian server travel requested: %s"), *TravelURL)); + World->ServerTravel(TravelURL, false, false); +} diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index fbce13d..27e8b4f 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -90,6 +90,9 @@ public: UFUNCTION(Exec) void AgrarianTravelHome(); + UFUNCTION(Exec) + void AgrarianServerTravel(FName MapName); + protected: UFUNCTION(Server, Reliable) void ServerAgrarianGrantItem(FName ItemId, int32 Quantity); @@ -120,4 +123,7 @@ protected: UFUNCTION(Server, Reliable) void ServerAgrarianTravel(FVector Destination); + + UFUNCTION(Server, Reliable) + void ServerAgrarianServerTravel(FName MapName); };