// Copyright 2020-2024 CesiumGS, Inc. and Contributors #if WITH_EDITOR #include "CesiumGeoreference.h" #include "CesiumOriginShiftComponent.h" #include "CesiumSubLevelComponent.h" #include "CesiumTestHelpers.h" #include "Editor.h" #include "Engine/World.h" #include "EngineUtils.h" #include "GameFramework/PlayerController.h" #include "GlobeAwareDefaultPawn.h" #include "LevelInstance/LevelInstanceActor.h" #include "Misc/AutomationTest.h" #include "Tests/AutomationEditorCommon.h" BEGIN_DEFINE_SPEC( FSubLevelsSpec, "Cesium.Unit.SubLevels", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::ServerContext | EAutomationTestFlags::CommandletContext | EAutomationTestFlags::ProductFilter) TObjectPtr pWorld; TObjectPtr pSubLevel1; TObjectPtr pLevelComponent1; TObjectPtr pSubLevel2; TObjectPtr pLevelComponent2; TObjectPtr pGeoreference; TObjectPtr pPawn; FDelegateHandle subscriptionPostPIEStarted; END_DEFINE_SPEC(FSubLevelsSpec) using namespace CesiumTestHelpers; void FSubLevelsSpec::Define() { BeforeEach([this]() { if (IsValid(pWorld)) { // Only run the below once in order to save time loading/unloading // levels for every little test. return; } pWorld = FAutomationEditorCommonUtils::CreateNewMap(); pSubLevel1 = pWorld->SpawnActor(); trackForPlay(pSubLevel1); pSubLevel1->SetWorldAsset(TSoftObjectPtr( FSoftObjectPath("/CesiumForUnreal/Tests/Maps/SingleCube.SingleCube"))); pSubLevel1->SetIsTemporarilyHiddenInEditor(true); pLevelComponent1 = Cast(pSubLevel1->AddComponentByClass( UCesiumSubLevelComponent::StaticClass(), false, FTransform::Identity, false)); trackForPlay(pLevelComponent1); pLevelComponent1->SetOriginLongitudeLatitudeHeight( FVector(10.0, 20.0, 1000.0)); pSubLevel1->AddInstanceComponent(pLevelComponent1); pSubLevel2 = pWorld->SpawnActor(); trackForPlay(pSubLevel2); pSubLevel2->SetWorldAsset(TSoftObjectPtr(FSoftObjectPath( "/CesiumForUnreal/Tests/Maps/ConeAndCylinder.ConeAndCylinder"))); pSubLevel2->SetIsTemporarilyHiddenInEditor(true); pLevelComponent2 = Cast(pSubLevel2->AddComponentByClass( UCesiumSubLevelComponent::StaticClass(), false, FTransform::Identity, false)); trackForPlay(pLevelComponent2); pLevelComponent2->SetOriginLongitudeLatitudeHeight( FVector(-25.0, 15.0, -5000.0)); pSubLevel2->AddInstanceComponent(pLevelComponent2); pSubLevel1->LoadLevelInstance(); pSubLevel2->LoadLevelInstance(); pGeoreference = nullptr; for (TActorIterator it(pWorld); it; ++it) { pGeoreference = *it; } trackForPlay(pGeoreference); pPawn = pWorld->SpawnActor(); pPawn->AddComponentByClass( UCesiumOriginShiftComponent::StaticClass(), false, FTransform::Identity, false); trackForPlay(pPawn); pPawn->AutoPossessPlayer = EAutoReceiveInput::Player0; }); AfterEach([this]() { pSubLevel1->SetIsTemporarilyHiddenInEditor(true); pSubLevel2->SetIsTemporarilyHiddenInEditor(true); }); It("initially hides sub-levels in the Editor", [this]() { TestTrue("pGeoreference is valid", IsValid(pGeoreference)); TestTrue("pSubLevel1 is valid", IsValid(pSubLevel1)); TestTrue("pSubLevel2 is valid", IsValid(pSubLevel2)); TestTrue( "pSubLevel1 is hidden", pSubLevel1->IsTemporarilyHiddenInEditor(true)); TestTrue( "pSubLevel2 is hidden", pSubLevel1->IsTemporarilyHiddenInEditor(true)); }); Describe( "copies CesiumGeoreference origin changes to the active sub-level in the Editor", [this]() { BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { pSubLevel1->SetIsTemporarilyHiddenInEditor(false); }); It("", EAsyncExecution::TaskGraphMainThread, [this]() { TestFalse( "pSubLevel1 is hidden", pSubLevel1->IsTemporarilyHiddenInEditor(true)); pGeoreference->SetOriginLongitudeLatitudeHeight( FVector(1.0, 2.0, 3.0)); TestEqual("Longitude", pLevelComponent1->GetOriginLongitude(), 1.0); TestEqual("Latitude", pLevelComponent1->GetOriginLatitude(), 2.0); TestEqual("Height", pLevelComponent1->GetOriginHeight(), 3.0); }); }); Describe( "copies active sub-level origin changes to the CesiumGeoreference in the Editor", [this]() { BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { pSubLevel1->SetIsTemporarilyHiddenInEditor(false); }); It("", EAsyncExecution::TaskGraphMainThread, [this]() { TestFalse( "pSubLevel1 is hidden", pSubLevel1->IsTemporarilyHiddenInEditor(true)); pLevelComponent1->SetOriginLongitudeLatitudeHeight( FVector(4.0, 5.0, 6.0)); TestEqual("Longitude", pGeoreference->GetOriginLongitude(), 4.0); TestEqual("Latitude", pGeoreference->GetOriginLatitude(), 5.0); TestEqual("Height", pGeoreference->GetOriginHeight(), 6.0); }); }); Describe( "does not copy inactive sub-level origin changes to the CesiumGeoreference in the Editor", [this]() { BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { // Set the sub-level active, wait a frame so it becomes so. pSubLevel1->SetIsTemporarilyHiddenInEditor(false); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { // Set the sub-level inactive, wait a frame so it becomes so. pSubLevel1->SetIsTemporarilyHiddenInEditor(true); }); It("", EAsyncExecution::TaskGraphMainThread, [this]() { // Verify that the previously-active sub-level isn't affected by // georeference origin changes. double expectedLongitude = pGeoreference->GetOriginLongitude(); double expectedLatitude = pGeoreference->GetOriginLatitude(); double expectedHeight = pGeoreference->GetOriginHeight(); TestNotEqual("Longitude", expectedLongitude, 7.0); TestNotEqual("Latitude", expectedLatitude, 8.0); TestNotEqual("Height", expectedHeight, 9.0); pLevelComponent1->SetOriginLongitudeLatitudeHeight( FVector(7.0, 8.0, 9.0)); TestEqual( "Longitude", pGeoreference->GetOriginLongitude(), expectedLongitude); TestEqual( "Latitude", pGeoreference->GetOriginLatitude(), expectedLatitude); TestEqual("Height", pGeoreference->GetOriginHeight(), expectedHeight); }); }); Describe( "ensures only one sub-level instance is visible in the editor at a time", [this]() { BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { pSubLevel1->SetIsTemporarilyHiddenInEditor(false); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { TestFalse( "pSubLevel1 is hidden", pSubLevel1->IsTemporarilyHiddenInEditor(true)); TestTrue( "pSubLevel2 is hidden", pSubLevel2->IsTemporarilyHiddenInEditor(true)); pSubLevel2->SetIsTemporarilyHiddenInEditor(false); }); It("", EAsyncExecution::TaskGraphMainThread, [this]() { TestTrue( "pSubLevel1 is hidden", pSubLevel1->IsTemporarilyHiddenInEditor(true)); TestFalse( "pSubLevel2 is hidden", pSubLevel2->IsTemporarilyHiddenInEditor(true)); }); }); Describe("activates the closest sub-level that is in range", [this]() { LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { subscriptionPostPIEStarted = FEditorDelegates::PostPIEStarted.AddLambda( [done](bool isSimulating) { done.Execute(); }); FRequestPlaySessionParams params{}; GEditor->RequestPlaySession(params); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { FEditorDelegates::PostPIEStarted.Remove(subscriptionPostPIEStarted); }); LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { waitFor(done, GEditor->PlayWorld, 5.0f, [this]() { return !findInPlay(pSubLevel1)->IsLoaded() && !findInPlay(pSubLevel2)->IsLoaded(); }); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { TestFalse("pSubLevel1 is loaded", findInPlay(pSubLevel1)->IsLoaded()); TestFalse("pSubLevel2 is loaded", findInPlay(pSubLevel2)->IsLoaded()); // Move the player within range of the first sub-level UCesiumSubLevelComponent* pPlayLevelComponent1 = findInPlay(pLevelComponent1); FVector origin1 = findInPlay(pGeoreference) ->TransformLongitudeLatitudeHeightPositionToUnreal(FVector( pPlayLevelComponent1->GetOriginLongitude(), pPlayLevelComponent1->GetOriginLatitude(), pPlayLevelComponent1->GetOriginHeight())); findInPlay(pPawn)->SetActorLocation(origin1); }); LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { waitFor(done, GEditor->PlayWorld, 5.0f, [this]() { return findInPlay(pSubLevel1)->IsLoaded(); }); }); It("", EAsyncExecution::TaskGraphMainThread, [this]() { TestTrue("pSubLevel1 is loaded", findInPlay(pSubLevel1)->IsLoaded()); TestFalse("pSubLevel2 is loaded", findInPlay(pSubLevel2)->IsLoaded()); }); AfterEach(EAsyncExecution::TaskGraphMainThread, [this]() { GEditor->RequestEndPlayMap(); }); }); Describe("handles a rapid load / unload / reload cycle", [this]() { LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { subscriptionPostPIEStarted = FEditorDelegates::PostPIEStarted.AddLambda( [done](bool isSimulating) { done.Execute(); }); FRequestPlaySessionParams params{}; GEditor->RequestPlaySession(params); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { FEditorDelegates::PostPIEStarted.Remove(subscriptionPostPIEStarted); }); LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { waitFor(done, GEditor->PlayWorld, 5.0f, [this]() { return !findInPlay(pSubLevel1)->IsLoaded() && !findInPlay(pSubLevel2)->IsLoaded(); }); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { TestFalse("pSubLevel1 is loaded", findInPlay(pSubLevel1)->IsLoaded()); TestFalse("pSubLevel2 is loaded", findInPlay(pSubLevel2)->IsLoaded()); // Move the player within range of the first sub-level UCesiumSubLevelComponent* pPlayLevelComponent1 = findInPlay(pLevelComponent1); FVector origin1 = findInPlay(pGeoreference) ->TransformLongitudeLatitudeHeightPositionToUnreal(FVector( pPlayLevelComponent1->GetOriginLongitude(), pPlayLevelComponent1->GetOriginLatitude(), pPlayLevelComponent1->GetOriginHeight())); findInPlay(pPawn)->SetActorLocation(origin1); }); LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { // Wait for the level to load. waitFor(done, GEditor->PlayWorld, 5.0f, [this]() { return findInPlay(pSubLevel1)->IsLoaded(); }); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { // Move the player outside the sub-level, triggering an unload. UCesiumSubLevelComponent* pPlayLevelComponent1 = findInPlay(pLevelComponent1); FVector origin1 = findInPlay(pGeoreference) ->TransformLongitudeLatitudeHeightPositionToUnreal(FVector( pPlayLevelComponent1->GetOriginLongitude(), pPlayLevelComponent1->GetOriginLatitude(), pPlayLevelComponent1->GetOriginHeight() + 100000.0)); findInPlay(pPawn)->SetActorLocation(origin1); }); BeforeEach(EAsyncExecution::TaskGraphMainThread, [this]() { // Without waiting for the level to finish unloading, move the player back // inside it. UCesiumSubLevelComponent* pPlayLevelComponent1 = findInPlay(pLevelComponent1); FVector origin1 = findInPlay(pGeoreference) ->TransformLongitudeLatitudeHeightPositionToUnreal(FVector( pPlayLevelComponent1->GetOriginLongitude(), pPlayLevelComponent1->GetOriginLatitude(), pPlayLevelComponent1->GetOriginHeight())); findInPlay(pPawn)->SetActorLocation(origin1); }); LatentBeforeEach( EAsyncExecution::TaskGraphMainThread, [this](const FDoneDelegate& done) { // Wait for the level to load. waitFor(done, GEditor->PlayWorld, 5.0f, [this]() { return findInPlay(pSubLevel1)->IsLoaded(); }); }); It("", EAsyncExecution::TaskGraphMainThread, [this]() { TestTrue("pSubLevel1 is loaded", findInPlay(pSubLevel1)->IsLoaded()); TestFalse("pSubLevel2 is loaded", findInPlay(pSubLevel2)->IsLoaded()); }); AfterEach(EAsyncExecution::TaskGraphMainThread, [this]() { GEditor->RequestEndPlayMap(); }); }); } #endif // #if WITH_EDITOR