diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index a4a13c5..8f7d245 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -572,7 +572,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe recipes, shows current ingredient counts, highlights craftable rows, and preloads the Agrarian player Blueprint with primitive recipe assets. - [x] Add multiplayer authority checks. -- [~] Add crafting debug tools. +- [x] Add crafting debug tools. Added `AgrarianCraftStatus` and + `AgrarianCraft ` exec commands for listing known recipes, + inspecting craftability, and triggering server-authoritative craft requests + during smoke tests and demo rehearsal. ## 0.1.H Fire System diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index eb7ca80..3f27c0f 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -153,6 +153,12 @@ reads those recipes plus the replicated inventory to show craftable status and ingredient counts. Interactive UMG recipe browsing, hotkeys, and queued crafting controls are deferred until the primitive loop settles. +Crafting debug tools live on the player controller. `AgrarianCraftStatus` +prints known recipes and current craftability, while `AgrarianCraft ` +requests a server-authoritative craft through `UAgrarianCraftingComponent`. +These commands are intended for smoke testing and investor-demo rehearsal until +proper UI input exists. + Inventory persistence saves `UAgrarianInventoryComponent::Items` into `FAgrarianSavedPlayer::Inventory` and restores through `UAgrarianInventoryComponent::RestoreSavedItems`. Restore broadcasts diff --git a/Scripts/verify_crafting_debug_tools.py b/Scripts/verify_crafting_debug_tools.py new file mode 100644 index 0000000..96e5856 --- /dev/null +++ b/Scripts/verify_crafting_debug_tools.py @@ -0,0 +1,47 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h": [ + "void AgrarianCraft(FName RecipeId);", + "void AgrarianCraftStatus();", + "void ServerAgrarianCraft(FName RecipeId);", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp": [ + "#include \"AgrarianCraftingComponent.h\"", + "void AAgrarianGamePlayerController::AgrarianCraft(FName RecipeId)", + "void AAgrarianGamePlayerController::AgrarianCraftStatus()", + "CraftingComponent->GetKnownRecipes(Recipes)", + "CraftingComponent->CanCraft(Recipe.RecipeId, FailureReason)", + "void AAgrarianGamePlayerController::ServerAgrarianCraft_Implementation(FName RecipeId)", + "CraftingComponent->Craft(RecipeId)", + ], + ROOT / "Docs" / "TechnicalDesignDocument.md": [ + "`AgrarianCraft `", + "`AgrarianCraftStatus`", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "- [x] Add crafting debug tools.", + "AgrarianCraftStatus", + ], +} + + +def main(): + 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)} missing {snippet!r}") + + if missing: + raise RuntimeError("Crafting debug tools verification failed: " + "; ".join(missing)) + + print("PASS: crafting debug exec tools are wired and documented.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index f6deda0..bc19540 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -2,6 +2,7 @@ #include "AgrarianGamePlayerController.h" +#include "AgrarianCraftingComponent.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AgrarianItemPickup.h" @@ -194,6 +195,49 @@ void AAgrarianGamePlayerController::AgrarianUseItem(FName ItemId, int32 Quantity ServerAgrarianUseItem(ItemId, Quantity); } +void AAgrarianGamePlayerController::AgrarianCraft(FName RecipeId) +{ + if (RecipeId == NAME_None) + { + ClientMessage(TEXT("Usage: AgrarianCraft ")); + return; + } + + ServerAgrarianCraft(RecipeId); +} + +void AAgrarianGamePlayerController::AgrarianCraftStatus() +{ + const AAgrarianGameCharacter* AgrarianCharacter = GetPawn(); + const UAgrarianCraftingComponent* CraftingComponent = AgrarianCharacter ? AgrarianCharacter->GetCraftingComponent() : nullptr; + if (!CraftingComponent) + { + ClientMessage(TEXT("No Agrarian crafting component found.")); + return; + } + + TArray Recipes; + CraftingComponent->GetKnownRecipes(Recipes); + if (Recipes.IsEmpty()) + { + ClientMessage(TEXT("No known Agrarian recipes.")); + return; + } + + ClientMessage(FString::Printf(TEXT("Known Agrarian recipes: %d"), Recipes.Num())); + for (const FAgrarianRecipe& Recipe : Recipes) + { + FText FailureReason; + const bool bCanCraft = CraftingComponent->CanCraft(Recipe.RecipeId, FailureReason); + const FString RecipeName = Recipe.DisplayName.IsEmpty() ? Recipe.RecipeId.ToString() : Recipe.DisplayName.ToString(); + ClientMessage(FString::Printf( + TEXT("- %s (%s): %s"), + *RecipeName, + *Recipe.RecipeId.ToString(), + bCanCraft ? TEXT("ready") : *FailureReason.ToString())); + } +} + void AAgrarianGamePlayerController::AgrarianTravel(float X, float Y, float Z) { ServerAgrarianTravel(FVector(X, Y, Z)); @@ -372,6 +416,33 @@ void AAgrarianGamePlayerController::ServerAgrarianUseItem_Implementation(FName I ClientMessage(FString::Printf(TEXT("Used %d x %s: %s."), UsedStack.Quantity, *ItemId.ToString(), *EffectSummary)); } +void AAgrarianGamePlayerController::ServerAgrarianCraft_Implementation(FName RecipeId) +{ + AAgrarianGameCharacter* AgrarianCharacter = GetPawn(); + UAgrarianCraftingComponent* CraftingComponent = AgrarianCharacter ? AgrarianCharacter->GetCraftingComponent() : nullptr; + if (!CraftingComponent) + { + ClientMessage(TEXT("No Agrarian crafting component found.")); + return; + } + + FText FailureReason; + if (!CraftingComponent->CanCraft(RecipeId, FailureReason)) + { + ClientMessage(FString::Printf(TEXT("Cannot craft %s: %s"), *RecipeId.ToString(), *FailureReason.ToString())); + return; + } + + if (CraftingComponent->Craft(RecipeId)) + { + ClientMessage(FString::Printf(TEXT("Crafted %s."), *RecipeId.ToString())); + } + else + { + ClientMessage(FString::Printf(TEXT("Failed to craft %s."), *RecipeId.ToString())); + } +} + void AAgrarianGamePlayerController::ServerAgrarianTravel_Implementation(FVector Destination) { APawn* ControlledPawn = GetPawn(); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.h b/Source/AgrarianGame/AgrarianGamePlayerController.h index 6ab3e2d..83e31f2 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.h +++ b/Source/AgrarianGame/AgrarianGamePlayerController.h @@ -75,6 +75,12 @@ public: UFUNCTION(Exec) void AgrarianUseItem(FName ItemId, int32 Quantity); + UFUNCTION(Exec) + void AgrarianCraft(FName RecipeId); + + UFUNCTION(Exec) + void AgrarianCraftStatus(); + UFUNCTION(Exec) void AgrarianTravel(float X, float Y, float Z); @@ -103,6 +109,9 @@ protected: UFUNCTION(Server, Reliable) void ServerAgrarianUseItem(FName ItemId, int32 Quantity); + UFUNCTION(Server, Reliable) + void ServerAgrarianCraft(FName RecipeId); + UFUNCTION(Server, Reliable) void ServerAgrarianTravel(FVector Destination); };