Add Ground Zero terrain pipeline and playable assets
This commit is contained in:
@@ -2,12 +2,58 @@
|
||||
|
||||
#include "AgrarianEditorAutomationLibrary.h"
|
||||
|
||||
#include "AgrarianBuildingPlacementComponent.h"
|
||||
#include "AgrarianCraftingComponent.h"
|
||||
#include "AgrarianGameCharacter.h"
|
||||
#include "AgrarianInteractable.h"
|
||||
#include "AgrarianInventoryComponent.h"
|
||||
#include "AgrarianPersistentActorComponent.h"
|
||||
#include "AgrarianPersistenceSubsystem.h"
|
||||
#include "AgrarianRecipeDataAsset.h"
|
||||
#include "AgrarianResourceNode.h"
|
||||
#include "AgrarianSaveGame.h"
|
||||
#include "AgrarianWildlifeBase.h"
|
||||
#include "Engine/World.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Editor.h"
|
||||
#include "Landscape.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
int32 GetIngredientQuantity(const UAgrarianRecipeDataAsset* RecipeAsset, const FName ItemId)
|
||||
{
|
||||
if (!RecipeAsset)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const FAgrarianItemStack& Ingredient : RecipeAsset->Recipe.Ingredients)
|
||||
{
|
||||
if (Ingredient.ItemId == ItemId)
|
||||
{
|
||||
return Ingredient.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AddNeededIngredient(TMap<FName, int32>& NeededItems, const FName ItemId, const int32 Quantity)
|
||||
{
|
||||
if (ItemId == NAME_None || Quantity <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NeededItems.FindOrAdd(ItemId) += Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf<AActor> ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
@@ -44,3 +90,745 @@ AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf<AA
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
FString UAgrarianEditorAutomationLibrary::RunPlayableLoopSmokeTest(TSubclassOf<AAgrarianGameCharacter> CharacterClass, AAgrarianResourceNode* ResourceNode, UAgrarianRecipeDataAsset* ShelterRecipe, TSubclassOf<AActor> ShelterClass)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||
if (!EditorWorld)
|
||||
{
|
||||
return TEXT("FAIL: no editor world");
|
||||
}
|
||||
|
||||
if (!CharacterClass || !ResourceNode || !ShelterRecipe || !ShelterClass)
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("FAIL: missing input CharacterClass=%s ResourceNode=%s ShelterRecipe=%s ShelterClass=%s"),
|
||||
*GetNameSafe(CharacterClass.Get()),
|
||||
*GetNameSafe(ResourceNode),
|
||||
*GetNameSafe(ShelterRecipe),
|
||||
*GetNameSafe(ShelterClass.Get()));
|
||||
}
|
||||
|
||||
FActorSpawnParameters CharacterSpawnParams;
|
||||
CharacterSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
AAgrarianGameCharacter* TestCharacter = EditorWorld->SpawnActor<AAgrarianGameCharacter>(
|
||||
CharacterClass,
|
||||
FVector(700.0f, -450.0f, 180.0f),
|
||||
FRotator::ZeroRotator,
|
||||
CharacterSpawnParams);
|
||||
|
||||
if (!TestCharacter)
|
||||
{
|
||||
return TEXT("FAIL: could not spawn test character");
|
||||
}
|
||||
|
||||
TestCharacter->SetActorLabel(TEXT("AGR_AutomationLoopCharacter"), false);
|
||||
|
||||
UAgrarianInventoryComponent* Inventory = TestCharacter->GetInventoryComponent();
|
||||
UAgrarianCraftingComponent* Crafting = TestCharacter->GetCraftingComponent();
|
||||
UAgrarianBuildingPlacementComponent* Placement = TestCharacter->GetBuildingPlacementComponent();
|
||||
|
||||
if (!Inventory || !Crafting || !Placement)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: spawned character is missing inventory, crafting, or placement component");
|
||||
}
|
||||
|
||||
const int32 StartingWood = Inventory->GetItemCount(TEXT("wood"));
|
||||
if (!ResourceNode->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: resource node does not implement AgrarianInteractable");
|
||||
}
|
||||
|
||||
if (!IAgrarianInteractable::Execute_CanInteract(ResourceNode, TestCharacter))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: resource node cannot be gathered");
|
||||
}
|
||||
|
||||
IAgrarianInteractable::Execute_Interact(ResourceNode, TestCharacter);
|
||||
|
||||
const int32 WoodAfterGather = Inventory->GetItemCount(TEXT("wood"));
|
||||
if (WoodAfterGather <= StartingWood)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: gather did not add wood, before=%d after=%d"), StartingWood, WoodAfterGather);
|
||||
}
|
||||
|
||||
// The current shelter recipe depends on prototype structure parts that do not yet have world acquisition.
|
||||
// Seed only the missing recipe inputs so this smoke test can exercise the connected craft/place/save/load path.
|
||||
for (const FAgrarianItemStack& Ingredient : ShelterRecipe->Recipe.Ingredients)
|
||||
{
|
||||
const int32 CurrentCount = Inventory->GetItemCount(Ingredient.ItemId);
|
||||
if (CurrentCount < Ingredient.Quantity)
|
||||
{
|
||||
FAgrarianItemStack MissingStack = Ingredient;
|
||||
MissingStack.Quantity = Ingredient.Quantity - CurrentCount;
|
||||
if (!Inventory->AddItem(MissingStack))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: could not seed missing ingredient %s"), *Ingredient.ItemId.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Crafting->AddKnownRecipe(ShelterRecipe->Recipe))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: could not add primitive shelter recipe to test character");
|
||||
}
|
||||
|
||||
if (!Crafting->Craft(ShelterRecipe->Recipe.RecipeId))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: primitive shelter craft failed");
|
||||
}
|
||||
|
||||
if (Inventory->GetItemCount(TEXT("primitive_shelter")) < 1)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: crafting did not add primitive_shelter to inventory");
|
||||
}
|
||||
|
||||
TArray<FAgrarianItemStack> PlacementCost;
|
||||
FAgrarianItemStack ShelterCost = ShelterRecipe->Recipe.Result;
|
||||
ShelterCost.Quantity = 1;
|
||||
PlacementCost.Add(ShelterCost);
|
||||
Placement->SetActiveBuildable(ShelterClass, PlacementCost);
|
||||
Placement->PlacementDistance = 5000.0f;
|
||||
Placement->PlacementProbeRadius = 1.0f;
|
||||
|
||||
const int32 ShelterCountBeforePlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||
int32 ShelterActorsBeforePlace = 0;
|
||||
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||
{
|
||||
ShelterActorsBeforePlace++;
|
||||
}
|
||||
|
||||
const FTransform PlacementTransform(FRotator::ZeroRotator, FVector(1500.0f, 550.0f, 300.0f));
|
||||
Placement->ServerPlaceBuildable_Implementation(ShelterClass, PlacementTransform);
|
||||
|
||||
const int32 ShelterCountAfterPlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||
int32 ShelterActorsAfterPlace = 0;
|
||||
AActor* PlacedShelter = nullptr;
|
||||
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||
{
|
||||
ShelterActorsAfterPlace++;
|
||||
PlacedShelter = *ActorIt;
|
||||
}
|
||||
|
||||
if (ShelterActorsAfterPlace <= ShelterActorsBeforePlace)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: placement did not spawn shelter, before=%d after=%d"), ShelterActorsBeforePlace, ShelterActorsAfterPlace);
|
||||
}
|
||||
|
||||
if (ShelterCountAfterPlace >= ShelterCountBeforePlace)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: placement did not consume shelter item, before=%d after=%d"), ShelterCountBeforePlace, ShelterCountAfterPlace);
|
||||
}
|
||||
|
||||
if (!PlacedShelter || !PlacedShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: placed shelter is missing persistent actor component");
|
||||
}
|
||||
|
||||
TArray<FAgrarianSavedWorldActor> SavedActors;
|
||||
for (TActorIterator<AActor> ActorIt(EditorWorld); ActorIt; ++ActorIt)
|
||||
{
|
||||
if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
if (PersistentComponent->IsSaveable())
|
||||
{
|
||||
SavedActors.Add(PersistentComponent->CaptureSaveState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SavedActors.IsEmpty())
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: save capture did not include any persistent world actors");
|
||||
}
|
||||
|
||||
int32 RestoredActorCount = 0;
|
||||
for (const FAgrarianSavedWorldActor& SavedActor : SavedActors)
|
||||
{
|
||||
if (SavedActor.ActorTypeId != TEXT("primitive_shelter"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AActor* RestoredActor = EditorWorld->SpawnActor<AActor>(ShelterClass, SavedActor.Transform);
|
||||
if (!RestoredActor)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
PersistentComponent->ApplySaveState(SavedActor);
|
||||
}
|
||||
|
||||
RestoredActorCount++;
|
||||
}
|
||||
|
||||
if (RestoredActorCount <= 0)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: restore did not spawn any saved primitive shelter actors");
|
||||
}
|
||||
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(
|
||||
TEXT("PASS: gathered wood %d->%d, crafted primitive_shelter, placed shelter, saved %d persistent actor(s), restored %d actor(s)"),
|
||||
StartingWood,
|
||||
WoodAfterGather,
|
||||
SavedActors.Num(),
|
||||
RestoredActorCount);
|
||||
#else
|
||||
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||
#endif
|
||||
}
|
||||
|
||||
FString UAgrarianEditorAutomationLibrary::RunNaturalShelterLoopSmokeTest(
|
||||
TSubclassOf<AAgrarianGameCharacter> CharacterClass,
|
||||
AAgrarianResourceNode* WoodNode,
|
||||
AAgrarianResourceNode* FiberNode,
|
||||
AAgrarianWildlifeBase* Wildlife,
|
||||
UAgrarianRecipeDataAsset* FrameRecipe,
|
||||
UAgrarianRecipeDataAsset* WallPanelRecipe,
|
||||
UAgrarianRecipeDataAsset* RoofPanelRecipe,
|
||||
UAgrarianRecipeDataAsset* ShelterRecipe,
|
||||
TSubclassOf<AActor> ShelterClass)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||
if (!EditorWorld)
|
||||
{
|
||||
return TEXT("FAIL: no editor world");
|
||||
}
|
||||
|
||||
if (!CharacterClass || !WoodNode || !FiberNode || !Wildlife || !FrameRecipe || !WallPanelRecipe || !RoofPanelRecipe || !ShelterRecipe || !ShelterClass)
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("FAIL: missing input CharacterClass=%s WoodNode=%s FiberNode=%s Wildlife=%s FrameRecipe=%s WallPanelRecipe=%s RoofPanelRecipe=%s ShelterRecipe=%s ShelterClass=%s"),
|
||||
*GetNameSafe(CharacterClass.Get()),
|
||||
*GetNameSafe(WoodNode),
|
||||
*GetNameSafe(FiberNode),
|
||||
*GetNameSafe(Wildlife),
|
||||
*GetNameSafe(FrameRecipe),
|
||||
*GetNameSafe(WallPanelRecipe),
|
||||
*GetNameSafe(RoofPanelRecipe),
|
||||
*GetNameSafe(ShelterRecipe),
|
||||
*GetNameSafe(ShelterClass.Get()));
|
||||
}
|
||||
|
||||
FActorSpawnParameters CharacterSpawnParams;
|
||||
CharacterSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
AAgrarianGameCharacter* TestCharacter = EditorWorld->SpawnActor<AAgrarianGameCharacter>(
|
||||
CharacterClass,
|
||||
FVector(720.0f, -420.0f, 180.0f),
|
||||
FRotator::ZeroRotator,
|
||||
CharacterSpawnParams);
|
||||
|
||||
if (!TestCharacter)
|
||||
{
|
||||
return TEXT("FAIL: could not spawn test character");
|
||||
}
|
||||
|
||||
TestCharacter->SetActorLabel(TEXT("AGR_AutomationNaturalShelterCharacter"), false);
|
||||
|
||||
UAgrarianInventoryComponent* Inventory = TestCharacter->GetInventoryComponent();
|
||||
UAgrarianCraftingComponent* Crafting = TestCharacter->GetCraftingComponent();
|
||||
UAgrarianBuildingPlacementComponent* Placement = TestCharacter->GetBuildingPlacementComponent();
|
||||
|
||||
if (!Inventory || !Crafting || !Placement)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: spawned character is missing inventory, crafting, or placement component");
|
||||
}
|
||||
|
||||
TMap<FName, int32> NeededItems;
|
||||
for (const FAgrarianItemStack& ShelterIngredient : ShelterRecipe->Recipe.Ingredients)
|
||||
{
|
||||
if (ShelterIngredient.ItemId == TEXT("primitive_frame"))
|
||||
{
|
||||
for (const FAgrarianItemStack& Ingredient : FrameRecipe->Recipe.Ingredients)
|
||||
{
|
||||
AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
|
||||
}
|
||||
}
|
||||
else if (ShelterIngredient.ItemId == TEXT("primitive_wall_panel"))
|
||||
{
|
||||
for (const FAgrarianItemStack& Ingredient : WallPanelRecipe->Recipe.Ingredients)
|
||||
{
|
||||
AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
|
||||
}
|
||||
}
|
||||
else if (ShelterIngredient.ItemId == TEXT("primitive_roof_panel"))
|
||||
{
|
||||
for (const FAgrarianItemStack& Ingredient : RoofPanelRecipe->Recipe.Ingredients)
|
||||
{
|
||||
AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddNeededIngredient(NeededItems, ShelterIngredient.ItemId, ShelterIngredient.Quantity);
|
||||
}
|
||||
}
|
||||
|
||||
const int32 NeededWood = NeededItems.FindRef(TEXT("wood"));
|
||||
const int32 NeededFiber = NeededItems.FindRef(TEXT("fiber"));
|
||||
const int32 NeededHide = NeededItems.FindRef(TEXT("hide"));
|
||||
|
||||
auto GatherUntil = [TestCharacter, Inventory](AAgrarianResourceNode* Node, const FName ItemId, const int32 NeededCount) -> bool
|
||||
{
|
||||
while (Inventory->GetItemCount(ItemId) < NeededCount)
|
||||
{
|
||||
if (!Node || !Node->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()) || !IAgrarianInteractable::Execute_CanInteract(Node, TestCharacter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IAgrarianInteractable::Execute_Interact(Node, TestCharacter);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!GatherUntil(WoodNode, TEXT("wood"), NeededWood))
|
||||
{
|
||||
const int32 WoodCount = Inventory->GetItemCount(TEXT("wood"));
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: could not gather enough wood, needed=%d gathered=%d"), NeededWood, WoodCount);
|
||||
}
|
||||
|
||||
if (!GatherUntil(FiberNode, TEXT("fiber"), NeededFiber))
|
||||
{
|
||||
const int32 FiberCount = Inventory->GetItemCount(TEXT("fiber"));
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: could not gather enough fiber, needed=%d gathered=%d"), NeededFiber, FiberCount);
|
||||
}
|
||||
|
||||
if (NeededHide > 0)
|
||||
{
|
||||
Wildlife->ApplyWildlifeDamage(Wildlife->MaxHealth + 10.0f, TestCharacter);
|
||||
if (!IAgrarianInteractable::Execute_CanInteract(Wildlife, TestCharacter))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: wildlife could not be harvested after lethal damage");
|
||||
}
|
||||
|
||||
IAgrarianInteractable::Execute_Interact(Wildlife, TestCharacter);
|
||||
if (Inventory->GetItemCount(TEXT("hide")) < NeededHide)
|
||||
{
|
||||
const int32 HideCount = Inventory->GetItemCount(TEXT("hide"));
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: could not harvest enough hide, needed=%d harvested=%d"), NeededHide, HideCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Crafting->AddKnownRecipe(FrameRecipe->Recipe) || !Crafting->AddKnownRecipe(WallPanelRecipe->Recipe) || !Crafting->AddKnownRecipe(RoofPanelRecipe->Recipe) || !Crafting->AddKnownRecipe(ShelterRecipe->Recipe))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: could not add shelter recipes to test character");
|
||||
}
|
||||
|
||||
auto CraftRepeated = [TestCharacter, Crafting](const UAgrarianRecipeDataAsset* RecipeAsset, const int32 Count) -> bool
|
||||
{
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
if (!RecipeAsset || !Crafting->Craft(RecipeAsset->Recipe.RecipeId))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const int32 NeededFrames = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_frame"));
|
||||
const int32 NeededWallPanels = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_wall_panel"));
|
||||
const int32 NeededRoofPanels = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_roof_panel"));
|
||||
if (!CraftRepeated(FrameRecipe, NeededFrames) || !CraftRepeated(WallPanelRecipe, NeededWallPanels) || !CraftRepeated(RoofPanelRecipe, NeededRoofPanels))
|
||||
{
|
||||
return TEXT("FAIL: could not craft primitive shelter parts from gathered materials");
|
||||
}
|
||||
|
||||
if (!Crafting->Craft(ShelterRecipe->Recipe.RecipeId))
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: primitive shelter craft failed after natural gathering");
|
||||
}
|
||||
|
||||
if (Inventory->GetItemCount(TEXT("primitive_shelter")) < 1)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: crafting did not add primitive_shelter to inventory");
|
||||
}
|
||||
|
||||
TArray<FAgrarianItemStack> PlacementCost;
|
||||
FAgrarianItemStack ShelterCost = ShelterRecipe->Recipe.Result;
|
||||
ShelterCost.Quantity = 1;
|
||||
PlacementCost.Add(ShelterCost);
|
||||
Placement->SetActiveBuildable(ShelterClass, PlacementCost);
|
||||
Placement->PlacementDistance = 5000.0f;
|
||||
Placement->PlacementProbeRadius = 1.0f;
|
||||
|
||||
const int32 ShelterCountBeforePlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||
int32 ShelterActorsBeforePlace = 0;
|
||||
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||
{
|
||||
ShelterActorsBeforePlace++;
|
||||
}
|
||||
|
||||
const FTransform PlacementTransform(FRotator::ZeroRotator, FVector(1500.0f, 550.0f, 300.0f));
|
||||
Placement->ServerPlaceBuildable_Implementation(ShelterClass, PlacementTransform);
|
||||
|
||||
const int32 ShelterCountAfterPlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||
int32 ShelterActorsAfterPlace = 0;
|
||||
AActor* PlacedShelter = nullptr;
|
||||
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||
{
|
||||
ShelterActorsAfterPlace++;
|
||||
PlacedShelter = *ActorIt;
|
||||
}
|
||||
|
||||
if (ShelterActorsAfterPlace <= ShelterActorsBeforePlace)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: placement did not spawn shelter, before=%d after=%d"), ShelterActorsBeforePlace, ShelterActorsAfterPlace);
|
||||
}
|
||||
|
||||
if (ShelterCountAfterPlace >= ShelterCountBeforePlace)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(TEXT("FAIL: placement did not consume shelter item, before=%d after=%d"), ShelterCountBeforePlace, ShelterCountAfterPlace);
|
||||
}
|
||||
|
||||
if (!PlacedShelter || !PlacedShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: placed shelter is missing persistent actor component");
|
||||
}
|
||||
|
||||
TArray<FAgrarianSavedWorldActor> SavedActors;
|
||||
for (TActorIterator<AActor> ActorIt(EditorWorld); ActorIt; ++ActorIt)
|
||||
{
|
||||
if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
if (PersistentComponent->IsSaveable())
|
||||
{
|
||||
SavedActors.Add(PersistentComponent->CaptureSaveState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SavedActors.IsEmpty())
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: save capture did not include any persistent world actors");
|
||||
}
|
||||
|
||||
int32 RestoredActorCount = 0;
|
||||
for (const FAgrarianSavedWorldActor& SavedActor : SavedActors)
|
||||
{
|
||||
if (SavedActor.ActorTypeId != TEXT("primitive_shelter"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AActor* RestoredActor = EditorWorld->SpawnActor<AActor>(ShelterClass, SavedActor.Transform);
|
||||
if (!RestoredActor)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
PersistentComponent->ApplySaveState(SavedActor);
|
||||
}
|
||||
|
||||
RestoredActorCount++;
|
||||
}
|
||||
|
||||
if (RestoredActorCount <= 0)
|
||||
{
|
||||
TestCharacter->Destroy();
|
||||
return TEXT("FAIL: restore did not spawn any saved primitive shelter actors");
|
||||
}
|
||||
|
||||
const int32 WoodGathered = Inventory->GetItemCount(TEXT("wood")) + NeededWood;
|
||||
const int32 FiberGathered = Inventory->GetItemCount(TEXT("fiber")) + NeededFiber;
|
||||
const int32 HideHarvested = Inventory->GetItemCount(TEXT("hide")) + NeededHide;
|
||||
TestCharacter->Destroy();
|
||||
return FString::Printf(
|
||||
TEXT("PASS: naturally gathered wood=%d fiber=%d hide=%d, crafted parts %d/%d/%d, crafted and placed primitive_shelter, saved %d persistent actor(s), restored %d actor(s)"),
|
||||
WoodGathered,
|
||||
FiberGathered,
|
||||
HideHarvested,
|
||||
NeededFrames,
|
||||
NeededWallPanels,
|
||||
NeededRoofPanels,
|
||||
SavedActors.Num(),
|
||||
RestoredActorCount);
|
||||
#else
|
||||
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||
#endif
|
||||
}
|
||||
|
||||
FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubclassOf<AActor> ShelterClass, const FString& SlotName)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
UWorld* TestWorld = nullptr;
|
||||
if (GEditor)
|
||||
{
|
||||
TestWorld = GEditor->PlayWorld ? GEditor->PlayWorld.Get() : GEditor->GetEditorWorldContext().World();
|
||||
}
|
||||
|
||||
if (!TestWorld)
|
||||
{
|
||||
return TEXT("FAIL: no editor or PIE world");
|
||||
}
|
||||
|
||||
UGameInstance* GameInstance = TestWorld->GetGameInstance();
|
||||
if (!GameInstance)
|
||||
{
|
||||
return TEXT("FAIL: test world has no live GameInstance");
|
||||
}
|
||||
|
||||
UAgrarianPersistenceSubsystem* Persistence = GameInstance->GetSubsystem<UAgrarianPersistenceSubsystem>();
|
||||
if (!Persistence)
|
||||
{
|
||||
return TEXT("FAIL: live GameInstance has no Agrarian persistence subsystem");
|
||||
}
|
||||
|
||||
if (!ShelterClass)
|
||||
{
|
||||
return TEXT("FAIL: missing shelter class");
|
||||
}
|
||||
|
||||
const FString PreviousSlotName = Persistence->DefaultSlotName;
|
||||
const int32 PreviousUserIndex = Persistence->UserIndex;
|
||||
const TMap<FName, TSubclassOf<AActor>> PreviousRegistry = Persistence->WorldActorClassRegistry;
|
||||
|
||||
const FString EffectiveSlotName = SlotName.IsEmpty() ? TEXT("AgrarianAutomationPersistence") : SlotName;
|
||||
Persistence->DefaultSlotName = EffectiveSlotName;
|
||||
Persistence->UserIndex = 0;
|
||||
Persistence->WorldActorClassRegistry.Reset();
|
||||
Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), ShelterClass);
|
||||
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
AActor* TestShelter = TestWorld->SpawnActor<AActor>(
|
||||
ShelterClass,
|
||||
FTransform(FRotator::ZeroRotator, FVector(1800.0f, 750.0f, 300.0f)),
|
||||
SpawnParams);
|
||||
|
||||
if (!TestShelter)
|
||||
{
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
return TEXT("FAIL: could not spawn persistence test shelter");
|
||||
}
|
||||
|
||||
TestShelter->SetActorLabel(TEXT("AGR_AutomationPersistenceShelter"), false);
|
||||
|
||||
if (!TestShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||
{
|
||||
TestShelter->Destroy();
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
return TEXT("FAIL: test shelter is missing persistent actor component");
|
||||
}
|
||||
|
||||
if (!Persistence->SaveCurrentWorld())
|
||||
{
|
||||
TestShelter->Destroy();
|
||||
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
return TEXT("FAIL: SaveCurrentWorld failed");
|
||||
}
|
||||
|
||||
if (!Persistence->DoesSaveExist())
|
||||
{
|
||||
TestShelter->Destroy();
|
||||
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
return TEXT("FAIL: save slot was not written");
|
||||
}
|
||||
|
||||
const UAgrarianSaveGame* LoadedSave = Persistence->LoadOrCreateSave();
|
||||
const int32 SavedActorCount = LoadedSave ? LoadedSave->WorldActors.Num() : 0;
|
||||
if (SavedActorCount <= 0)
|
||||
{
|
||||
TestShelter->Destroy();
|
||||
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
return TEXT("FAIL: loaded save did not include persistent world actors");
|
||||
}
|
||||
|
||||
const int32 RestoredActorCount = Persistence->RestoreWorldActors(LoadedSave, true);
|
||||
if (RestoredActorCount != SavedActorCount)
|
||||
{
|
||||
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
return FString::Printf(TEXT("FAIL: restored actor count mismatch, saved=%d restored=%d"), SavedActorCount, RestoredActorCount);
|
||||
}
|
||||
|
||||
int32 PersistentActorCountAfterRestore = 0;
|
||||
for (TActorIterator<AActor> ActorIt(TestWorld); ActorIt; ++ActorIt)
|
||||
{
|
||||
const AActor* Actor = *ActorIt;
|
||||
const UAgrarianPersistentActorComponent* PersistentComponent = Actor ? Actor->FindComponentByClass<UAgrarianPersistentActorComponent>() : nullptr;
|
||||
if (PersistentComponent && PersistentComponent->IsSaveable())
|
||||
{
|
||||
PersistentActorCountAfterRestore++;
|
||||
}
|
||||
}
|
||||
|
||||
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||
Persistence->DefaultSlotName = PreviousSlotName;
|
||||
Persistence->UserIndex = PreviousUserIndex;
|
||||
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("PASS: live persistence subsystem saved %d actor(s), restored %d actor(s), world now has %d persistent actor(s)"),
|
||||
SavedActorCount,
|
||||
RestoredActorCount,
|
||||
PersistentActorCountAfterRestore);
|
||||
#else
|
||||
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||
#endif
|
||||
}
|
||||
|
||||
FString UAgrarianEditorAutomationLibrary::ImportLandscapeHeightmapIntoEditorWorld(
|
||||
const FString& HeightmapFilename,
|
||||
const int32 Width,
|
||||
const int32 Height,
|
||||
const float XScaleCm,
|
||||
const float YScaleCm,
|
||||
const float ZScaleCm,
|
||||
const FString& ActorLabel)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
if (Width <= 1 || Height <= 1 || Width != Height)
|
||||
{
|
||||
return FString::Printf(TEXT("FAIL: invalid landscape dimensions Width=%d Height=%d"), Width, Height);
|
||||
}
|
||||
|
||||
if (FMath::IsNearlyZero(XScaleCm) || FMath::IsNearlyZero(YScaleCm) || FMath::IsNearlyZero(ZScaleCm))
|
||||
{
|
||||
return FString::Printf(TEXT("FAIL: invalid landscape scale X=%f Y=%f Z=%f"), XScaleCm, YScaleCm, ZScaleCm);
|
||||
}
|
||||
|
||||
UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||
if (!EditorWorld)
|
||||
{
|
||||
return TEXT("FAIL: no editor world");
|
||||
}
|
||||
|
||||
TArray<uint8> HeightmapBytes;
|
||||
if (!FFileHelper::LoadFileToArray(HeightmapBytes, *HeightmapFilename))
|
||||
{
|
||||
return FString::Printf(TEXT("FAIL: could not read heightmap %s"), *HeightmapFilename);
|
||||
}
|
||||
|
||||
const int64 ExpectedByteCount = static_cast<int64>(Width) * static_cast<int64>(Height) * sizeof(uint16);
|
||||
if (HeightmapBytes.Num() != ExpectedByteCount)
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("FAIL: heightmap byte count mismatch, expected=%lld actual=%d"),
|
||||
ExpectedByteCount,
|
||||
HeightmapBytes.Num());
|
||||
}
|
||||
|
||||
TArray<uint16> HeightData;
|
||||
HeightData.SetNumUninitialized(Width * Height);
|
||||
FMemory::Memcpy(HeightData.GetData(), HeightmapBytes.GetData(), HeightmapBytes.Num());
|
||||
|
||||
FActorSpawnParameters SpawnParameters;
|
||||
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
SpawnParameters.ObjectFlags = RF_Transactional;
|
||||
|
||||
const float WorldSizeX = static_cast<float>(Width - 1) * XScaleCm;
|
||||
const float WorldSizeY = static_cast<float>(Height - 1) * YScaleCm;
|
||||
const FVector LandscapeLocation(-WorldSizeX * 0.5f, -WorldSizeY * 0.5f, 0.0f);
|
||||
ALandscape* Landscape = EditorWorld->SpawnActor<ALandscape>(
|
||||
ALandscape::StaticClass(),
|
||||
LandscapeLocation,
|
||||
FRotator::ZeroRotator,
|
||||
SpawnParameters);
|
||||
|
||||
if (!Landscape)
|
||||
{
|
||||
return TEXT("FAIL: could not spawn landscape actor");
|
||||
}
|
||||
|
||||
Landscape->SetActorScale3D(FVector(XScaleCm, YScaleCm, ZScaleCm));
|
||||
if (!ActorLabel.IsEmpty())
|
||||
{
|
||||
Landscape->SetActorLabel(ActorLabel, true);
|
||||
}
|
||||
|
||||
const FGuid LandscapeGuid = FGuid::NewGuid();
|
||||
const FGuid FinalLayerGuid;
|
||||
TMap<FGuid, TArray<uint16>> ImportHeightData;
|
||||
ImportHeightData.Add(FinalLayerGuid, MoveTemp(HeightData));
|
||||
|
||||
TMap<FGuid, TArray<FLandscapeImportLayerInfo>> ImportMaterialLayerInfos;
|
||||
ImportMaterialLayerInfos.Add(FinalLayerGuid, TArray<FLandscapeImportLayerInfo>());
|
||||
TArray<FLandscapeLayer> ImportLayers;
|
||||
Landscape->Import(
|
||||
LandscapeGuid,
|
||||
0,
|
||||
0,
|
||||
Width - 1,
|
||||
Height - 1,
|
||||
1,
|
||||
63,
|
||||
ImportHeightData,
|
||||
*HeightmapFilename,
|
||||
ImportMaterialLayerInfos,
|
||||
ELandscapeImportAlphamapType::Additive,
|
||||
MakeArrayView(ImportLayers));
|
||||
|
||||
Landscape->RegisterAllComponents();
|
||||
Landscape->PostEditChange();
|
||||
Landscape->MarkPackageDirty();
|
||||
EditorWorld->MarkPackageDirty();
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("PASS: imported landscape %s size=%dx%d scale=(%.6f,%.6f,%.6f) world_size_cm=(%.2f,%.2f)"),
|
||||
*GetNameSafe(Landscape),
|
||||
Width,
|
||||
Height,
|
||||
XScaleCm,
|
||||
YScaleCm,
|
||||
ZScaleCm,
|
||||
WorldSizeX,
|
||||
WorldSizeY);
|
||||
#else
|
||||
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user