// Copyright Epic Games, Inc. All Rights Reserved. #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& NeededItems, const FName ItemId, const int32 Quantity) { if (ItemId == NAME_None || Quantity <= 0) { return; } NeededItems.FindOrAdd(ItemId) += Quantity; } } AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel) { #if WITH_EDITOR if (!ActorClass) { return nullptr; } UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr; if (!EditorWorld) { return nullptr; } FActorSpawnParameters SpawnParameters; SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; SpawnParameters.ObjectFlags = RF_Transactional; AActor* SpawnedActor = EditorWorld->SpawnActor(ActorClass, Location, Rotation, SpawnParameters); if (!SpawnedActor) { return nullptr; } if (!ActorLabel.IsEmpty()) { SpawnedActor->SetActorLabel(ActorLabel, true); } SpawnedActor->MarkPackageDirty(); EditorWorld->MarkPackageDirty(); return SpawnedActor; #else return nullptr; #endif } FString UAgrarianEditorAutomationLibrary::RunPlayableLoopSmokeTest(TSubclassOf CharacterClass, AAgrarianResourceNode* ResourceNode, UAgrarianRecipeDataAsset* ShelterRecipe, TSubclassOf 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( 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 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 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 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()) { TestCharacter->Destroy(); return TEXT("FAIL: placed shelter is missing persistent actor component"); } TArray SavedActors; for (TActorIterator ActorIt(EditorWorld); ActorIt; ++ActorIt) { if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass()) { 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(ShelterClass, SavedActor.Transform); if (!RestoredActor) { continue; } if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass()) { 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 CharacterClass, AAgrarianResourceNode* WoodNode, AAgrarianResourceNode* FiberNode, AAgrarianWildlifeBase* Wildlife, UAgrarianRecipeDataAsset* FrameRecipe, UAgrarianRecipeDataAsset* WallPanelRecipe, UAgrarianRecipeDataAsset* RoofPanelRecipe, UAgrarianRecipeDataAsset* ShelterRecipe, TSubclassOf 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( 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 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 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 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 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()) { TestCharacter->Destroy(); return TEXT("FAIL: placed shelter is missing persistent actor component"); } TArray SavedActors; for (TActorIterator ActorIt(EditorWorld); ActorIt; ++ActorIt) { if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass()) { 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(ShelterClass, SavedActor.Transform); if (!RestoredActor) { continue; } if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass()) { 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 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(); 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> 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( 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()) { 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 ActorIt(TestWorld); ActorIt; ++ActorIt) { const AActor* Actor = *ActorIt; const UAgrarianPersistentActorComponent* PersistentComponent = Actor ? Actor->FindComponentByClass() : 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 HeightmapBytes; if (!FFileHelper::LoadFileToArray(HeightmapBytes, *HeightmapFilename)) { return FString::Printf(TEXT("FAIL: could not read heightmap %s"), *HeightmapFilename); } const int64 ExpectedByteCount = static_cast(Width) * static_cast(Height) * sizeof(uint16); if (HeightmapBytes.Num() != ExpectedByteCount) { return FString::Printf( TEXT("FAIL: heightmap byte count mismatch, expected=%lld actual=%d"), ExpectedByteCount, HeightmapBytes.Num()); } TArray 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(Width - 1) * XScaleCm; const float WorldSizeY = static_cast(Height - 1) * YScaleCm; const FVector LandscapeLocation(-WorldSizeX * 0.5f, -WorldSizeY * 0.5f, 0.0f); ALandscape* Landscape = EditorWorld->SpawnActor( 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> ImportHeightData; ImportHeightData.Add(FinalLayerGuid, MoveTemp(HeightData)); TMap> ImportMaterialLayerInfos; ImportMaterialLayerInfos.Add(FinalLayerGuid, TArray()); TArray 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 }