Files
UnrealEngineUWP/Engine/Source/Editor/UnrealEd/Private/PlayLevel.cpp
Marc Audy d46179b265 Don't rename selection set in to the PIE world as that has a number of knock on problems
Selection set will not be added to the transaction buffer if it contains any PIE objects
Transaction will not actually be created when clicking on a PIE object in the scene outliner
#codereview Matt.Kuhlenschmidt, Richard.TalbotWatkin

[CL 2606122 by Marc Audy in Main branch]
2015-06-30 14:05:38 -04:00

3622 lines
132 KiB
C++

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#include "UnrealEd.h"
#include "SoundDefinitions.h"
#include "LevelUtils.h"
#include "BusyCursor.h"
#include "ScopedTransaction.h"
#include "Database.h"
#include "PackageTools.h"
#include "Runtime/Engine/Public/Slate/SceneViewport.h"
#include "BlueprintUtilities.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Editor/LevelEditor/Public/LevelEditor.h"
#include "Editor/LevelEditor/Public/SLevelViewport.h"
#include "Toolkits/AssetEditorManager.h"
#include "Toolkits/ToolkitManager.h"
#include "BlueprintEditorModule.h"
#include "TargetPlatform.h"
#include "MainFrame.h"
#include "MessageLog.h"
#include "UObjectToken.h"
#include "MapErrors.h"
#include "LauncherServices.h"
#include "ISettingsModule.h"
#include "TargetDeviceServices.h"
#include "GameProjectGenerationModule.h"
#include "SourceCodeNavigation.h"
#include "PhysicsPublic.h"
#include "Runtime/Analytics/Analytics/Public/Interfaces/IAnalyticsProvider.h"
#include "EngineAnalytics.h"
#include "Engine/GameInstance.h"
#include "EditorAnalytics.h"
#include "Runtime/Engine/Classes/Engine/UserInterfaceSettings.h"
#include "Runtime/Engine/Classes/Engine/RendererSettings.h"
#include "SScissorRectBox.h"
#include "Online.h"
#include "SNotificationList.h"
#include "SGameLayerManager.h"
#include "NotificationManager.h"
#include "Engine/Selection.h"
#include "TimerManager.h"
#include "AI/Navigation/NavigationSystem.h"
#include "Runtime/HeadMountedDisplay/Public/HeadMountedDisplay.h"
#include "Components/AudioComponent.h"
#include "Engine/Note.h"
#include "UnrealEngine.h"
#include "GameFramework/GameMode.h"
#include "Engine/NavigationObjectBase.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerStart.h"
#include "GameFramework/PlayerState.h"
#include "GameFramework/WorldSettings.h"
#include "Engine/LevelStreaming.h"
#include "Engine/LocalPlayer.h"
#include "Components/ModelComponent.h"
#include "EngineUtils.h"
#include "GameMapsSettings.h"
#include "GameFramework/Pawn.h"
#include "GameDelegates.h"
#include "GeneralProjectSettings.h"
DEFINE_LOG_CATEGORY_STATIC(LogPlayLevel, Log, All);
#define LOCTEXT_NAMESPACE "PlayLevel"
inline FName GetOnlineIdentifier(const FWorldContext& WorldContext)
{
return FName(*FString::Printf(TEXT(":%s"), *WorldContext.ContextHandle.ToString()));
}
void UEditorEngine::EndPlayMap()
{
if (GEngine->HMDDevice.IsValid())
{
GEngine->HMDDevice->OnEndPlay();
}
// Matinee must be closed before PIE can stop - matinee during PIE will be editing a PIE-world actor
if( GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_InterpEdit) )
{
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "PIENeedsToCloseMatineeMessage", "Closing 'Play in Editor' must close UnrealMatinee.") );
GLevelEditorModeTools().DeactivateMode( FBuiltinEditorModes::EM_InterpEdit );
}
EndPlayOnLocalPc();
const FScopedBusyCursor BusyCursor;
check(PlayWorld);
// Enable screensavers when ending PIE.
EnableScreenSaver( true );
// Make a list of all the actors that should be selected
TArray<UObject *> SelectedActors;
if ( ActorsThatWereSelected.Num() > 0 )
{
for ( int32 ActorIndex = 0; ActorIndex < ActorsThatWereSelected.Num(); ++ActorIndex )
{
TWeakObjectPtr<AActor> Actor = ActorsThatWereSelected[ ActorIndex ].Get();
if (Actor.IsValid())
{
SelectedActors.Add( Actor.Get() );
}
}
ActorsThatWereSelected.Empty();
}
else
{
for ( FSelectionIterator It( GetSelectedActorIterator() ); It; ++It )
{
AActor* Actor = static_cast<AActor*>( *It );
if (Actor)
{
checkSlow( Actor->IsA(AActor::StaticClass()) );
AActor* EditorActor = EditorUtilities::GetEditorWorldCounterpartActor(Actor);
if (EditorActor)
{
SelectedActors.Add( EditorActor );
}
}
}
}
// Deselect all objects, to avoid problems caused by property windows still displaying
// properties for an object that gets garbage collected during the PIE clean-up phase.
GEditor->SelectNone( true, true, false );
GetSelectedActors()->DeselectAll();
GetSelectedObjects()->DeselectAll();
GetSelectedComponents()->DeselectAll();
// For every actor that was selected previously, make sure it's editor equivalent is selected
for ( int32 ActorIndex = 0; ActorIndex < SelectedActors.Num(); ++ActorIndex )
{
AActor* Actor = Cast<AActor>( SelectedActors[ ActorIndex ] );
if (Actor)
{
SelectActor( Actor, true, false );
}
}
// let the editor know
FEditorDelegates::EndPIE.Broadcast(bIsSimulatingInEditor);
// clean up any previous Play From Here sessions
if ( GameViewport != NULL && GameViewport->Viewport != NULL )
{
// Remove close handler binding
GameViewport->OnCloseRequested().Unbind();
GameViewport->CloseRequested(GameViewport->Viewport);
}
CleanupGameViewport();
// find objects like Textures in the playworld levels that won't get garbage collected as they are marked RF_Standalone
for( FObjectIterator It; It; ++It )
{
UObject* Object = *It;
if ((Object->GetOutermost()->PackageFlags & PKG_PlayInEditor) != 0)
{
if (Object->HasAnyFlags(RF_Standalone))
{
// Clear RF_Standalone flag from objects in the levels used for PIE so they get cleaned up.
Object->ClearFlags(RF_Standalone);
}
// Close any asset editors that are currently editing this object
FAssetEditorManager::Get().CloseAllEditorsForAsset(Object);
}
}
// Clean up each world individually
TArray<FName> OnlineIdentifiers;
TArray<UWorld*> WorldsBeingCleanedUp;
for (int32 WorldIdx = WorldList.Num()-1; WorldIdx >= 0; --WorldIdx)
{
FWorldContext &ThisContext = WorldList[WorldIdx];
if (ThisContext.WorldType == EWorldType::PIE)
{
if (ThisContext.World())
{
WorldsBeingCleanedUp.Add(ThisContext.World());
}
TeardownPlaySession(ThisContext);
// Cleanup online subsystems instantiated during PIE
FName OnlineIdentifier = GetOnlineIdentifier(ThisContext);
if (IOnlineSubsystem::DoesInstanceExist(OnlineIdentifier))
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get(OnlineIdentifier);
if (OnlineSub)
{
// Stop ticking and clean up, but do not destroy as we may be in a failed online delegate
OnlineSub->Shutdown();
}
OnlineIdentifiers.Add(OnlineIdentifier);
}
// Remove world list after online has shutdown in case any async actions require the world context
WorldList.RemoveAt(WorldIdx);
}
}
if (OnlineIdentifiers.Num())
{
UE_LOG(LogPlayLevel, Display, TEXT("Shutting down PIE online subsystems"));
// Cleanup online subsystem shortly as we might be in a failed delegate
// have to do this in batch because timer delegate doesn't recognize bound data
// as a different delegate
FTimerDelegate DestroyTimer;
DestroyTimer.BindUObject(this, &UEditorEngine::CleanupPIEOnlineSessions, OnlineIdentifiers);
GetTimerManager()->SetTimer(CleanupPIEOnlineSessionsTimerHandle, DestroyTimer, 0.1f, false);
}
{
// Clear out viewport index
PlayInEditorViewportIndex = -1;
// We could have been toggling back and forth between simulate and pie before ending the play map
// Make sure the property windows are cleared of any pie actors
GUnrealEd->UpdateFloatingPropertyWindows();
// Clean up any pie actors being referenced
GEngine->BroadcastLevelActorListChanged();
}
// Lose the EditorWorld pointer (this is only maintained while PIEing)
if (EditorWorld->GetNavigationSystem())
{
EditorWorld->GetNavigationSystem()->OnPIEEnd();
}
FGameDelegates::Get().GetEndPlayMapDelegate().Broadcast();
EditorWorld->bAllowAudioPlayback = true;
EditorWorld = NULL;
// mark everything contained in the PIE worlds to be deleted
for (UWorld* World : WorldsBeingCleanedUp)
{
for (auto LevelIt(World->GetLevelIterator()); LevelIt; ++LevelIt)
{
if (const ULevel* Level = *LevelIt)
{
CastChecked<UWorld>(Level->GetOuter())->MarkObjectsPendingKill();
}
}
}
// Garbage Collect
CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS );
// Make sure that all objects in the temp levels were entirely garbage collected.
for( FObjectIterator ObjectIt; ObjectIt; ++ObjectIt )
{
UObject* Object = *ObjectIt;
if( Object->GetOutermost()->PackageFlags & PKG_PlayInEditor )
{
UWorld* TheWorld = UWorld::FindWorldInPackage(Object->GetOutermost());
if ( TheWorld )
{
StaticExec(GWorld, *FString::Printf(TEXT("OBJ REFS CLASS=WORLD NAME=%s"), *TheWorld->GetPathName()));
}
else
{
UE_LOG(LogPlayLevel, Error, TEXT("No PIE world was found when attempting to gather references after GC."));
}
TMap<UObject*,UProperty*> Route = FArchiveTraceRoute::FindShortestRootPath( Object, true, GARBAGE_COLLECTION_KEEPFLAGS );
FString ErrorString = FArchiveTraceRoute::PrintRootPath( Route, Object );
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Path"), FText::FromString(ErrorString));
// We cannot safely recover from this.
FMessageLog("PIE").CriticalError()
->AddToken(FUObjectToken::Create(Object, FText::FromString(Object->GetFullName())))
->AddToken(FTextToken::Create(FText::Format(LOCTEXT("PIEObjectStillReferenced", "Object from PIE level still referenced. Shortest path from root: {Path}"), Arguments)));
}
}
// Final cleanup/reseting
FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext();
UPackage* Package = EditorWorldContext.World()->GetOutermost();
// Spawn note actors dropped in PIE.
if(GEngine->PendingDroppedNotes.Num() > 0)
{
const FScopedTransaction Transaction( NSLOCTEXT("UnrealEd", "CreatePIENoteActors", "Create PIE Notes") );
for(int32 i=0; i<GEngine->PendingDroppedNotes.Num(); i++)
{
FDropNoteInfo& NoteInfo = GEngine->PendingDroppedNotes[i];
ANote* NewNote = EditorWorldContext.World()->SpawnActor<ANote>(NoteInfo.Location, NoteInfo.Rotation);
if(NewNote)
{
NewNote->Text = NoteInfo.Comment;
if( NewNote->GetRootComponent() != NULL )
{
NewNote->GetRootComponent()->SetRelativeScale3D( FVector(2.f) );
}
}
}
Package->MarkPackageDirty();
GEngine->PendingDroppedNotes.Empty();
}
// Restores realtime viewports that have been disabled for PIE.
RestoreRealtimeViewports();
// Don't actually need to reset this delegate but doing so allows is to check invalid attempts to execute the delegate
FScopedConditionalWorldSwitcher::SwitchWorldForPIEDelegate = FOnSwitchWorldForPIE();
// Set the autosave timer to have at least 10 seconds remaining before autosave
const static float SecondsWarningTillAutosave = 10.0f;
GUnrealEd->GetPackageAutoSaver().ForceMinimumTimeTillAutoSave(SecondsWarningTillAutosave);
for(TObjectIterator<UAudioComponent> It; It; ++It)
{
UAudioComponent* AudioComp = *It;
if (AudioComp->GetWorld() == EditorWorldContext.World())
{
AudioComp->ReregisterComponent();
}
}
// no longer queued
bIsPlayWorldQueued = false;
bIsSimulateInEditorQueued = false;
bRequestEndPlayMapQueued = false;
bUseVRPreviewForPlayWorld = false;
// display any info if required.
FMessageLog("PIE").Notify(LOCTEXT("PIEErrorsPresent", "Errors/warnings reported while playing in editor."));
}
void UEditorEngine::CleanupPIEOnlineSessions(TArray<FName> OnlineIdentifiers)
{
for (FName& OnlineIdentifier : OnlineIdentifiers)
{
UE_LOG(LogPlayLevel, Display, TEXT("Destroying online subsystem %s"), *OnlineIdentifier.ToString());
IOnlineSubsystem::Destroy(OnlineIdentifier);
NumOnlinePIEInstances--;
}
NumOnlinePIEInstances = 0;
}
void UEditorEngine::TeardownPlaySession(FWorldContext &PieWorldContext)
{
check(PieWorldContext.WorldType == EWorldType::PIE);
PlayWorld = PieWorldContext.World();
PlayWorld->bIsTearingDown = true;
if (!PieWorldContext.RunAsDedicated)
{
// Slate data for this pie world
FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle);
// Destroy Viewport
if ( PieWorldContext.GameViewport != NULL && PieWorldContext.GameViewport->Viewport != NULL )
{
PieWorldContext.GameViewport->CloseRequested(PieWorldContext.GameViewport->Viewport);
}
CleanupGameViewport();
// Clean up the slate PIE viewport if we have one
if (SlatePlayInEditorSession)
{
if (SlatePlayInEditorSession->DestinationSlateViewport.IsValid())
{
TSharedPtr<ILevelViewport> Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
if( !bIsSimulatingInEditor)
{
// Set the editor viewport location to match that of Play in Viewport if we aren't simulating in the editor, we have a valid player to get the location from
if (bLastViewAndLocationValid == true)
{
bLastViewAndLocationValid = false;
Viewport->GetLevelViewportClient().SetViewLocation( LastViewLocation );
if( Viewport->GetLevelViewportClient().IsPerspective() )
{
// Rotation only matters for perspective viewports not orthographic
Viewport->GetLevelViewportClient().SetViewRotation( LastViewRotation );
}
}
}
// No longer simulating in the viewport
Viewport->GetLevelViewportClient().SetIsSimulateInEditorViewport( false );
// Clear out the hit proxies before GC'ing
Viewport->GetLevelViewportClient().Viewport->InvalidateHitProxy();
}
else if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid())
{
// Unregister the game viewport from slate. This sends a final message to the viewport
// so it can have a chance to release mouse capture, mouse lock, etc.
FSlateApplication::Get().UnregisterGameViewport();
// Viewport client is cleaned up. Make sure its not being accessed
SlatePlayInEditorSession->SlatePlayInEditorWindowViewport->SetViewportClient(NULL);
// The window may have already been destroyed in the case that the PIE window close box was pressed
if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid())
{
// Destroy the SWindow
FSlateApplication::Get().DestroyWindowImmediately(SlatePlayInEditorSession->SlatePlayInEditorWindow.Pin().ToSharedRef());
}
}
}
// Disassociate the players from their PlayerControllers.
// This is done in the GameEngine path in UEngine::LoadMap.
// But since PIE is just shutting down, and not loading a
// new map, we need to do it manually here for now.
//for (auto It = GEngine->GetLocalPlayerIterator(PlayWorld); It; ++It)
for (FLocalPlayerIterator It(GEngine, PlayWorld); It; ++It)
{
if(It->PlayerController)
{
if(It->PlayerController->GetPawn())
{
PlayWorld->DestroyActor(It->PlayerController->GetPawn(), true);
}
PlayWorld->DestroyActor(It->PlayerController, true);
It->PlayerController = NULL;
}
}
}
// Change GWorld to be the play in editor world during cleanup.
check( EditorWorld == GWorld );
GWorld = PlayWorld;
GIsPlayInEditorWorld = true;
// Remember Simulating flag so that we know if OnSimulateSessionFinished is required after everything has been cleaned up.
bool bWasSimulatingInEditor = bIsSimulatingInEditor;
// Clear Simulating In Editor bit
bIsSimulatingInEditor = false;
// Stop all audio and remove references to temp level.
if (FAudioDevice* AudioDevice = PlayWorld->GetAudioDevice())
{
AudioDevice->Flush( PlayWorld );
AudioDevice->ResetInterpolation();
AudioDevice->OnEndPIE(bIsSimulatingInEditor);
AudioDevice->TransientMasterVolume = 1.0f;
}
// Clean up all streaming levels
PlayWorld->bIsLevelStreamingFrozen = false;
PlayWorld->bShouldForceUnloadStreamingLevels = true;
PlayWorld->FlushLevelStreaming();
// cleanup refs to any duplicated streaming levels
for ( int32 LevelIndex=0; LevelIndex<PlayWorld->StreamingLevels.Num(); LevelIndex++ )
{
ULevelStreaming* StreamingLevel = PlayWorld->StreamingLevels[LevelIndex];
if( StreamingLevel != NULL )
{
const ULevel* PlayWorldLevel = StreamingLevel->GetLoadedLevel();
if ( PlayWorldLevel != NULL )
{
UWorld* World = Cast<UWorld>( PlayWorldLevel->GetOuter() );
if( World != NULL )
{
// Attempt to move blueprint debugging references back to the editor world
if( EditorWorld != NULL && EditorWorld->StreamingLevels.IsValidIndex(LevelIndex) )
{
const ULevel* EditorWorldLevel = EditorWorld->StreamingLevels[LevelIndex]->GetLoadedLevel();
if ( EditorWorldLevel != NULL )
{
UWorld* SublevelEditorWorld = Cast<UWorld>(EditorWorldLevel->GetOuter());
if( SublevelEditorWorld != NULL )
{
World->TransferBlueprintDebugReferences(SublevelEditorWorld);
}
}
}
}
}
}
}
// Construct a list of editors that are active for objects being debugged. We will refresh these when we have cleaned up to ensure no invalid objects exist in them
TArray< IBlueprintEditor* > Editors;
FAssetEditorManager& AssetEditorManager = FAssetEditorManager::Get();
const UWorld::FBlueprintToDebuggedObjectMap& EditDebugObjectsPre = PlayWorld->GetBlueprintObjectsBeingDebugged();
for (UWorld::FBlueprintToDebuggedObjectMap::TConstIterator EditIt(EditDebugObjectsPre); EditIt; ++EditIt)
{
if (UBlueprint* TargetBP = EditIt.Key().Get())
{
if(IBlueprintEditor* EachEditor = static_cast<IBlueprintEditor*>(AssetEditorManager.FindEditorForAsset(TargetBP, false)))
{
Editors.AddUnique( EachEditor );
}
}
}
// Go through and let all the PlayWorld Actor's know they are being destroyed
for (FActorIterator ActorIt(PlayWorld); ActorIt; ++ActorIt)
{
ActorIt->RouteEndPlay(EEndPlayReason::EndPlayInEditor);
}
PieWorldContext.OwningGameInstance->Shutdown();
// Move blueprint debugging pointers back to the objects in the editor world
PlayWorld->TransferBlueprintDebugReferences(EditorWorld);
FPhysScene* PhysScene = PlayWorld->GetPhysicsScene();
if (PhysScene)
{
PhysScene->WaitPhysScenes();
PhysScene->KillVisualDebugger();
}
// Clean up the temporary play level.
PlayWorld->CleanupWorld();
// Remove from root (Seamless travel may have done this)
PlayWorld->RemoveFromRoot();
PlayWorld = NULL;
// Refresh any editors we had open in case they referenced objects that no longer exist.
for (int32 iEditors = 0; iEditors < Editors.Num(); iEditors++)
{
Editors[ iEditors ]->RefreshEditors();
}
// Restore GWorld.
GWorld = EditorWorld;
GIsPlayInEditorWorld = false;
FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext();
// Let the viewport know about leaving PIE/Simulate session. Do it after everything's been cleaned up
// as the viewport will play exit sound here and this has to be done after GetAudioDevice()->Flush
// otherwise all sounds will be immediately stopped.
if (!PieWorldContext.RunAsDedicated)
{
// Slate data for this pie world
FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle);
if (SlatePlayInEditorSession && SlatePlayInEditorSession->DestinationSlateViewport.IsValid())
{
TSharedPtr<ILevelViewport> Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
if( Viewport->HasPlayInEditorViewport() )
{
Viewport->EndPlayInEditorSession();
}
// Let the Slate viewport know that we're leaving Simulate mode
if( bWasSimulatingInEditor )
{
Viewport->OnSimulateSessionFinished();
}
Viewport->GetLevelViewportClient().SetReferenceToWorldContext(EditorWorldContext);
}
// Remove the slate info from the map (note that the UWorld* is long gone at this point, but the WorldContext still exists. It will be removed outside of this function)
SlatePlayInEditorMap.Remove(PieWorldContext.ContextHandle);
}
}
void UEditorEngine::PlayMap( const FVector* StartLocation, const FRotator* StartRotation, int32 Destination, int32 InPlayInViewportIndex, bool bUseMobilePreview, bool bMovieCapture )
{
// queue up a Play From Here request, this way the load/save won't conflict with the TransBuffer, which doesn't like
// loading and saving to happen during a transaction
// save the StartLocation if we have one
if (StartLocation)
{
PlayWorldLocation = *StartLocation;
PlayWorldRotation = StartRotation ? *StartRotation : FRotator::ZeroRotator;
bHasPlayWorldPlacement = true;
}
else
{
bHasPlayWorldPlacement = false;
}
// remember where to send the play map request
PlayWorldDestination = Destination;
// Set whether or not we want to use mobile preview mode (PC platform only)
bUseMobilePreviewForPlayWorld = bUseMobilePreview;
bUseVRPreviewForPlayWorld = false;
// Set whether or not we want to start movie capturing immediately (PC platform only)
bStartMovieCapture = bMovieCapture;
// tell the editor to kick it off next Tick()
bIsPlayWorldQueued = true;
// Not wanting to simulate
bIsSimulateInEditorQueued = false;
// Unless we've been asked to play in a specific viewport window, this index will be -1
PlayInEditorViewportIndex = InPlayInViewportIndex;
}
void UEditorEngine::RequestPlaySession( bool bAtPlayerStart, TSharedPtr<class ILevelViewport> DestinationViewport, bool bInSimulateInEditor, const FVector* StartLocation, const FRotator* StartRotation, int32 DestinationConsole, bool bUseMobilePreview, bool bUseVRPreview )
{
// Remember whether or not we were attempting to play from playerstart or from viewport
GIsPIEUsingPlayerStart = bAtPlayerStart;
// queue up a Play From Here request, this way the load/save won't conflict with the TransBuffer, which doesn't like
// loading and saving to happen during a transaction
// save the StartLocation if we have one
if (!bInSimulateInEditor && StartLocation != NULL)
{
PlayWorldLocation = *StartLocation;
PlayWorldRotation = StartRotation ? *StartRotation : FRotator::ZeroRotator;
bHasPlayWorldPlacement = true;
}
else
{
bHasPlayWorldPlacement = false;
}
// remember where to send the play map request
PlayWorldDestination = DestinationConsole;
RequestedDestinationSlateViewport = DestinationViewport;
// Set whether or not we want to use mobile preview mode (PC platform only)
bUseMobilePreviewForPlayWorld = bUseMobilePreview;
bUseVRPreviewForPlayWorld = bUseVRPreview;
// Not capturing a movie
bStartMovieCapture = false;
// tell the editor to kick it off next Tick()
bIsPlayWorldQueued = true;
// Store whether we want to play in editor, or only simulate in editor
bIsSimulateInEditorQueued = bInSimulateInEditor;
// Unless we have been asked to play in a specific viewport window, this index will be -1
PlayInEditorViewportIndex = -1;
// @todo gmp: temp hack for Rocket demo
bPlayOnLocalPcSession = false;
bPlayUsingLauncher = false;
}
// @todo gmp: temp hack for Rocket demo
void UEditorEngine::RequestPlaySession( const FVector* StartLocation, const FRotator* StartRotation, bool MobilePreview )
{
bPlayOnLocalPcSession = true;
bPlayUsingLauncher = false;
bPlayUsingMobilePreview = MobilePreview;
if (StartLocation != NULL)
{
PlayWorldLocation = *StartLocation;
PlayWorldRotation = StartRotation ? *StartRotation : FRotator::ZeroRotator;
bHasPlayWorldPlacement = true;
}
else
{
bHasPlayWorldPlacement = false;
}
bIsPlayWorldQueued = true;
}
void UEditorEngine::RequestPlaySession(const FString& DeviceId, const FString& DeviceName)
{
bPlayOnLocalPcSession = false;
bPlayUsingLauncher = true;
// always use playerstart on remote devices (for now?)
bHasPlayWorldPlacement = false;
// remember the platform name to run on
PlayUsingLauncherDeviceId = DeviceId;
PlayUsingLauncherDeviceName = DeviceName;
bIsPlayWorldQueued = true;
}
void UEditorEngine::CancelRequestPlaySession()
{
bIsPlayWorldQueued = false;
bPlayOnLocalPcSession = false;
bPlayUsingLauncher = false;
bPlayUsingMobilePreview = false;
}
void UEditorEngine::PlaySessionPaused()
{
FEditorDelegates::PausePIE.Broadcast(bIsSimulatingInEditor);
}
void UEditorEngine::PlaySessionResumed()
{
FEditorDelegates::ResumePIE.Broadcast(bIsSimulatingInEditor);
}
void UEditorEngine::PlaySessionSingleStepped()
{
FEditorDelegates::SingleStepPIE.Broadcast(bIsSimulatingInEditor);
}
/* fits the window position to make sure it falls within the confines of the desktop */
void FitWindowPositionToWorkArea(FIntPoint &WinPos, FIntPoint &WinSize, const FMargin &WinPadding)
{
const int32 HorzPad = WinPadding.GetTotalSpaceAlong<Orient_Horizontal>();
const int32 VertPad = WinPadding.GetTotalSpaceAlong<Orient_Vertical>();
FIntPoint TotalSize( WinSize.X + HorzPad, WinSize.Y + VertPad );
FDisplayMetrics DisplayMetrics;
FSlateApplication::Get().GetDisplayMetrics(DisplayMetrics);
// Limit the size, to make sure it fits within the desktop area
{
FIntPoint NewWinSize;
NewWinSize.X = FMath::Min(TotalSize.X, DisplayMetrics.VirtualDisplayRect.Right - DisplayMetrics.VirtualDisplayRect.Left );
NewWinSize.Y = FMath::Min(TotalSize.Y, DisplayMetrics.VirtualDisplayRect.Bottom - DisplayMetrics.VirtualDisplayRect.Top );
if( NewWinSize != TotalSize )
{
TotalSize = NewWinSize;
WinSize.X = NewWinSize.X - HorzPad;
WinSize.Y = NewWinSize.Y - VertPad;
}
}
const FSlateRect PreferredWorkArea( DisplayMetrics.VirtualDisplayRect.Left,
DisplayMetrics.VirtualDisplayRect.Top,
DisplayMetrics.VirtualDisplayRect.Right - TotalSize.X,
DisplayMetrics.VirtualDisplayRect.Bottom - TotalSize.Y );
// if no more windows fit horizontally, place them in a new row
if (WinPos.X > PreferredWorkArea.Right)
{
WinPos.X = PreferredWorkArea.Left;
WinPos.Y += TotalSize.Y;
if (WinPos.Y > PreferredWorkArea.Bottom)
{
WinPos.Y = PreferredWorkArea.Top;
}
}
// if no more rows fit vertically, stack windows on top of each other
else if (WinPos.Y > PreferredWorkArea.Bottom)
{
WinPos.Y = PreferredWorkArea.Top;
WinPos.X += TotalSize.X;
if (WinPos.X > PreferredWorkArea.Right)
{
WinPos.X = PreferredWorkArea.Left;
}
}
// Clamp values to make sure they fall within the desktop area
WinPos.X = FMath::Clamp(WinPos.X, (int32)PreferredWorkArea.Left, (int32)PreferredWorkArea.Right);
WinPos.Y = FMath::Clamp(WinPos.Y, (int32)PreferredWorkArea.Top, (int32)PreferredWorkArea.Bottom);
}
/* advances the windows position to the next location and fits */
void AdvanceWindowPositionForNextPIEWindow(FIntPoint &WinPos, const FIntPoint &WinSize, const FMargin &WinPadding, bool bVertical)
{
const int32 HorzPad = WinPadding.GetTotalSpaceAlong<Orient_Horizontal>();
const int32 VertPad = WinPadding.GetTotalSpaceAlong<Orient_Vertical>();
const FIntPoint TotalSize( WinSize.X + HorzPad, WinSize.Y + VertPad );
if(bVertical)
{
WinPos.Y += TotalSize.Y;
}
else
{
WinPos.X += TotalSize.X;
}
}
/* returns the size of the window depending on the net mode. */
void GetWindowSizeForInstanceType(FIntPoint &WindowSize, const ULevelEditorPlaySettings* PlayInSettings)
{
const EPlayNetMode PlayNetMode = [&PlayInSettings]{ EPlayNetMode NetMode(PIE_Standalone); return (PlayInSettings->GetPlayNetMode(NetMode) ? NetMode : PIE_Standalone); }();
if (PlayNetMode == PIE_Standalone)
{
WindowSize.X = PlayInSettings->StandaloneWindowWidth;
WindowSize.Y = PlayInSettings->StandaloneWindowHeight;
}
else
{
PlayInSettings->GetClientWindowSize(WindowSize);
}
}
/* sets the size of the window depending on the net mode */
void SetWindowSizeForInstanceType(const FIntPoint &WindowSize, ULevelEditorPlaySettings* PlayInSettings)
{
const EPlayNetMode PlayNetMode = [&PlayInSettings]{ EPlayNetMode NetMode(PIE_Standalone); return (PlayInSettings->GetPlayNetMode(NetMode) ? NetMode : PIE_Standalone); }();
if (PlayNetMode == PIE_Standalone)
{
PlayInSettings->StandaloneWindowWidth = WindowSize.X;
PlayInSettings->StandaloneWindowHeight = WindowSize.Y;
}
else
{
PlayInSettings->SetClientWindowSize(WindowSize);
}
}
/**
* Generate the command line for pie instance. Window position, size etc.
*
* @param WinPos Window position. This will contain the X & Y position to use for the next window. (Not changed for dedicated server window).
* @param InstanceNum PIE instance index.
* @param bIsDedicatedServer Is this instance a dedicate server. true if so else false.
*/
FString GenerateCmdLineForNextPieInstance(FIntPoint &WinPos, int32 &InstanceNum, bool bIsDedicatedServer)
{
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
// Get GameSettings INI override
FString GameUserSettingsOverride = GGameUserSettingsIni.Replace(TEXT("GameUserSettings"), *FString::Printf(TEXT("PIEGameUserSettings%d"), InstanceNum++));
// Construct parms:
// -Override GameUserSettings.ini
// -Force no steam
// -Allow saving of config files (since we are giving them an override INI)
const FString AdditionalLaunchOptions = [&PlayInSettings]{ FString LaunchOptions; return (PlayInSettings->GetAdditionalLaunchOptions(LaunchOptions) ? LaunchOptions : FString()); }();
FString CmdLine = FString::Printf(TEXT("GameUserSettingsINI=\"%s\" -MultiprocessSaveConfig %s -MultiprocessOSS "), *GameUserSettingsOverride, *AdditionalLaunchOptions);
if (bIsDedicatedServer)
{
// Append dedicated server options
CmdLine += TEXT("-server -log ");
}
else
{
// Default to what we expect the border to be (on windows at least) to prevent it occurring offscreen if TLW call fails
FMargin WindowBorderSize(8.0f, 30.0f, 8.0f, 8.0f);
TSharedPtr<SWindow> TopLevelWindow = FSlateApplication::Get().GetActiveTopLevelWindow();
if (TopLevelWindow.IsValid())
{
WindowBorderSize = TopLevelWindow->GetWindowBorderSize(true);
}
// Get the size of the window based on the type
FIntPoint WinSize(0,0);
GetWindowSizeForInstanceType(WinSize, PlayInSettings);
// Make sure the window is going to fit where we want it
FitWindowPositionToWorkArea(WinPos, WinSize, WindowBorderSize);
// Set the size, incase it was modified
SetWindowSizeForInstanceType(WinSize, GetMutableDefault<ULevelEditorPlaySettings>());
// Listen server or clients: specify default win position and SAVEWINPOS so the final positions are saved
// in order to preserve PIE networking window setup
CmdLine += FString::Printf(TEXT("WinX=%d WinY=%d SAVEWINPOS=1"), WinPos.X + (int32)WindowBorderSize.Left, WinPos.Y + (int32)WindowBorderSize.Top);
// Advance window for next PIE instance...
AdvanceWindowPositionForNextPIEWindow(WinPos, WinSize, WindowBorderSize, false);
}
return CmdLine;
}
void GetMultipleInstancePositions(int32 index, int32 &LastX, int32 &LastY)
{
ULevelEditorPlaySettings* PlayInSettings = Cast<ULevelEditorPlaySettings>(ULevelEditorPlaySettings::StaticClass()->GetDefaultObject());
if (PlayInSettings->MultipleInstancePositions.IsValidIndex(index) &&
(PlayInSettings->MultipleInstanceLastHeight == PlayInSettings->NewWindowHeight) &&
(PlayInSettings->MultipleInstanceLastWidth == PlayInSettings->NewWindowWidth))
{
PlayInSettings->NewWindowPosition = PlayInSettings->MultipleInstancePositions[index];
LastX = PlayInSettings->NewWindowPosition.X;
LastY = PlayInSettings->NewWindowPosition.Y;
}
else
{
PlayInSettings->NewWindowPosition = FIntPoint(LastX, LastY);
}
FIntPoint WinPos(LastX, LastY);
// Get the size of the window based on the type
FIntPoint WinSize(0, 0);
GetWindowSizeForInstanceType(WinSize, PlayInSettings);
// Advance window and make sure the window is going to fit where we want it
const FMargin WinPadding(16, 16);
AdvanceWindowPositionForNextPIEWindow(WinPos, WinSize, WinPadding, false);
FitWindowPositionToWorkArea(WinPos, WinSize, WinPadding);
// Set the size, incase it was modified
SetWindowSizeForInstanceType(WinSize, PlayInSettings);
LastX = WinPos.X;
LastY = WinPos.Y;
}
void UEditorEngine::StartQueuedPlayMapRequest()
{
const bool bWantSimulateInEditor = bIsSimulateInEditorQueued;
EndPlayOnLocalPc();
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
// Launch multi-player instances if necessary
// (note that if you have 'RunUnderOneProcess' checked and do a bPlayOnLocalPcSession (standalone) - play standalone 'wins' - multiple instances will be launched for multiplayer)
const EPlayNetMode PlayNetMode = [&PlayInSettings]{ EPlayNetMode NetMode(PIE_Standalone); return (PlayInSettings->GetPlayNetMode(NetMode) ? NetMode : PIE_Standalone); }();
const bool CanRunUnderOneProcess = [&PlayInSettings]{ bool RunUnderOneProcess(false); return (PlayInSettings->GetRunUnderOneProcess(RunUnderOneProcess) && RunUnderOneProcess); }();
if (PlayNetMode != PIE_Standalone && (!CanRunUnderOneProcess || bPlayOnLocalPcSession) && !bPlayUsingLauncher)
{
int32 NumClients = 0;
// If we start to the right of the editor work area, call FitToWorkArea and it will find the next place we can place a new instance window if that's not preferable.
const FSlateRect PreferredWorkArea = FSlateApplication::Get().GetPreferredWorkArea();
FIntPoint WinPosition((int32)PreferredWorkArea.Right, (int32)PreferredWorkArea.Top);
// We'll need to spawn a server if we're playing outside the editor or the editor wants to run as a client
if (bPlayOnLocalPcSession || PlayNetMode == PIE_Client)
{
PlayStandaloneLocalPc(TEXT(""), &WinPosition, NumClients, true);
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
if (!CanPlayNetDedicated)
{
// Listen server counts as a client
NumClients++;
}
}
// If we're playing in the editor
if (!bPlayOnLocalPcSession)
{
if (bStartMovieCapture)
{
// @todo Fix for UE4. This is a temp workaround.
PlayForMovieCapture();
}
else
{
PlayInEditor(GetEditorWorldContext().World(), bWantSimulateInEditor);
// Editor counts as a client
NumClients++;
}
}
// Spawn number of clients
const int32 PlayNumberOfClients = [&PlayInSettings]{ int32 NumberOfClients(0); return (PlayInSettings->GetPlayNumberOfClients(NumberOfClients) ? NumberOfClients : 0); }();
for (int32 i = NumClients; i < PlayNumberOfClients; ++i)
{
PlayStandaloneLocalPc(TEXT("127.0.0.1"), &WinPosition, i, false);
}
}
else
{
// Launch standalone PIE session
if (bPlayOnLocalPcSession)
{
PlayStandaloneLocalPc();
}
else if (bPlayUsingLauncher)
{
PlayUsingLauncher();
}
else if (bStartMovieCapture)
{
// @todo Fix for UE4. This is a temp workaround.
PlayForMovieCapture();
}
else
{
PlayInEditor( GetEditorWorldContext().World(), bWantSimulateInEditor );
}
}
// note that we no longer have a queued request
bIsPlayWorldQueued = false;
bIsSimulateInEditorQueued = false;
}
/* Temporarily renames streaming levels for pie saving */
class FScopedRenameStreamingLevels
{
public:
FScopedRenameStreamingLevels( UWorld* InWorld, const FString& AutosavePackagePrefix, const FString& MapnamePrefix )
: World(InWorld)
{
if(InWorld->StreamingLevels.Num() > 0)
{
for(int32 LevelIndex=0; LevelIndex < InWorld->StreamingLevels.Num(); ++LevelIndex)
{
ULevelStreaming* StreamingLevel = InWorld->StreamingLevels[LevelIndex];
if ( StreamingLevel )
{
const FString WorldAssetPackageName = StreamingLevel->GetWorldAssetPackageName();
const FName WorldAssetPackageFName = StreamingLevel->GetWorldAssetPackageFName();
PreviousStreamingPackageNames.Add( WorldAssetPackageFName );
FString StreamingLevelPackageName = FString::Printf(TEXT("%s%s/%s%s"), *AutosavePackagePrefix, *FPackageName::GetLongPackagePath( WorldAssetPackageName ), *MapnamePrefix, *FPackageName::GetLongPackageAssetName( WorldAssetPackageName ));
StreamingLevelPackageName.ReplaceInline(TEXT("//"), TEXT("/"));
StreamingLevel->SetWorldAssetByPackageName(FName(*StreamingLevelPackageName));
}
}
}
World->StreamingLevelsPrefix = MapnamePrefix;
}
~FScopedRenameStreamingLevels()
{
check(World.IsValid());
check( PreviousStreamingPackageNames.Num() == World->StreamingLevels.Num() );
if(World->StreamingLevels.Num() > 0)
{
for(int32 LevelIndex=0; LevelIndex < World->StreamingLevels.Num(); ++LevelIndex)
{
ULevelStreaming* StreamingLevel = World->StreamingLevels[LevelIndex];
if ( StreamingLevel )
{
StreamingLevel->SetWorldAssetByPackageName(PreviousStreamingPackageNames[LevelIndex]);
}
}
}
World->StreamingLevelsPrefix.Empty();
}
private:
TWeakObjectPtr<UWorld> World;
TArray<FName> PreviousStreamingPackageNames;
};
void UEditorEngine::SaveWorldForPlay(TArray<FString>& SavedMapNames)
{
UWorld* World = GWorld;
// check if PersistentLevel has any external references
if( PackageUsingExternalObjects(World->PersistentLevel) && EAppReturnType::Yes != FMessageDialog::Open( EAppMsgType::YesNo, NSLOCTEXT("UnrealEd", "Warning_UsingExternalPackage", "This map is using externally referenced packages which won't be found when in a game and all references will be broken. Perform a map check for more details.\n\nWould you like to continue?")) )
{
return;
}
const FString PlayOnConsolePackageName = FPackageName::FilenameToLongPackageName(FPaths::Combine(*FPaths::GameSavedDir(), *PlayOnConsoleSaveDir)) + TEXT("/");
// make a per-platform name for the map
const FString ConsoleName = FString(TEXT("PC"));
const FString Prefix = FString(PLAYWORLD_CONSOLE_BASE_PACKAGE_PREFIX) + ConsoleName;
// Temporarily rename streaming levels for pie saving
FScopedRenameStreamingLevels ScopedRenameStreamingLevels( World, PlayOnConsolePackageName, Prefix );
const FString WorldPackageName = World->GetOutermost()->GetName();
// spawn a play-from-here player start or a temporary player start
AActor* PlayerStart = NULL;
bool bCreatedPlayerStart = false;
SpawnPlayFromHereStart( World, PlayerStart, PlayWorldLocation, PlayWorldRotation );
if (PlayerStart != NULL)
{
bCreatedPlayerStart = true;
}
else
{
PlayerStart = CheckForPlayerStart();
if( PlayerStart == NULL )
{
FActorSpawnParameters SpawnInfo;
SpawnInfo.bNoCollisionFail = true;
PlayerStart = World->SpawnActor<AActor>( APlayerStart::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo );
bCreatedPlayerStart = true;
}
}
// save out all open map packages
TArray<FString> SavedWorldFileNames;
bool bSavedWorld = SavePlayWorldPackages(World, *Prefix, /*out*/ SavedWorldFileNames);
// Remove the player start we added if we made one
if (bCreatedPlayerStart)
{
World->DestroyActor(PlayerStart);
}
if (bSavedWorld)
{
// Convert the filenames into map names
SavedMapNames.Reserve(SavedMapNames.Num() + SavedWorldFileNames.Num());
for (int32 Index = 0; Index < SavedWorldFileNames.Num(); ++Index)
{
const FString MapName = FPackageName::FilenameToLongPackageName(SavedWorldFileNames[Index]);
SavedMapNames.Add(MapName);
}
}
}
// @todo gmp: temp hack for Rocket demo
void UEditorEngine::EndPlayOnLocalPc( )
{
for (int32 i=0; i < PlayOnLocalPCSessions.Num(); ++i)
{
if (PlayOnLocalPCSessions[i].ProcessHandle.IsValid())
{
if ( FPlatformProcess::IsProcRunning(PlayOnLocalPCSessions[i].ProcessHandle) )
{
FPlatformProcess::TerminateProc(PlayOnLocalPCSessions[i].ProcessHandle);
}
PlayOnLocalPCSessions[i].ProcessHandle.Reset();
}
}
PlayOnLocalPCSessions.Empty();
}
// @todo gmp: temp hack for Rocket demo
void UEditorEngine::PlayStandaloneLocalPc(FString MapNameOverride, FIntPoint* WindowPos, int32 PIENum, bool bIsServer)
{
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
//const ULevelEditorPlaySettings* PlayInSettings = InPlaySettings != NULL ? InPlaySettings : GetDefault<ULevelEditorPlaySettings>();
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
FString CmdLine;
if (WindowPos != NULL) // If WindowPos == NULL, we're just launching one instance
{
CmdLine = GenerateCmdLineForNextPieInstance(*WindowPos, PIENum, bIsServer && CanPlayNetDedicated);
}
const FString URLParms = bIsServer && !CanPlayNetDedicated ? TEXT("?Listen") : FString();
// select map to play
TArray<FString> SavedMapNames;
if (MapNameOverride.IsEmpty())
{
FWorldContext & EditorContext = GetEditorWorldContext();
if (EditorContext.World()->WorldComposition)
{
// Open world composition from original folder
FString MapName = EditorContext.World()->GetOutermost()->GetName();
SavedMapNames.Add(MapName);
}
else
{
SaveWorldForPlay(SavedMapNames);
}
}
else
{
SavedMapNames.Add(MapNameOverride);
}
if (SavedMapNames.Num() == 0)
{
return;
}
FString GameNameOrProjectFile;
if (FPaths::IsProjectFilePathSet())
{
GameNameOrProjectFile = FString::Printf(TEXT("\"%s\""), *FPaths::GetProjectFilePath());
}
else
{
GameNameOrProjectFile = FApp::GetGameName();
}
FString AdditionalParameters(TEXT(""));
bool bRunningDebug = FParse::Param(FCommandLine::Get(), TEXT("debug"));
if (bRunningDebug)
{
AdditionalParameters += TEXT(" -debug");
}
// apply additional settings
if (bPlayUsingMobilePreview)
{
if (IsOpenGLPlatform(GShaderPlatformForFeatureLevel[GMaxRHIFeatureLevel]))
{
AdditionalParameters += TEXT(" -opengl");
}
AdditionalParameters += TEXT(" -featureleveles2 -faketouches");
}
if (PlayInSettings->DisableStandaloneSound)
{
AdditionalParameters += TEXT(" -nosound");
}
if (PlayInSettings->AdditionalLaunchParameters.Len() > 0)
{
AdditionalParameters += TEXT(" ");
AdditionalParameters += PlayInSettings->AdditionalLaunchParameters;
}
FIntPoint WinSize(0, 0);
GetWindowSizeForInstanceType(WinSize, PlayInSettings);
// Check if centered
FString Params;
if (PlayInSettings->CenterStandaloneWindow)
{
Params = FString::Printf(TEXT("%s %s -game -PIEVIACONSOLE -ResX=%d -ResY=%d %s%s %s"),
*GameNameOrProjectFile,
*BuildPlayWorldURL(*SavedMapNames[0], false, URLParms),
WinSize.X,
WinSize.Y,
*FCommandLine::GetSubprocessCommandline(),
*AdditionalParameters,
*CmdLine
);
}
else
{
FIntPoint WinPos(0, 0);
Params = FString::Printf(TEXT("%s %s -game -PIEVIACONSOLE -WinX=%d -WinY=%d -ResX=%d -ResY=%d %s%s %s"),
*GameNameOrProjectFile,
*BuildPlayWorldURL(*SavedMapNames[0], false, URLParms),
PlayInSettings->StandaloneWindowPosition.X,
PlayInSettings->StandaloneWindowPosition.Y,
WinSize.X,
WinSize.Y,
*FCommandLine::GetSubprocessCommandline(),
*AdditionalParameters,
*CmdLine
);
}
// launch the game process
FString GamePath = FPlatformProcess::GenerateApplicationPath(FApp::GetName(), FApp::GetBuildConfiguration());
FPlayOnPCInfo *NewSession = new (PlayOnLocalPCSessions) FPlayOnPCInfo();
NewSession->ProcessHandle = FPlatformProcess::CreateProc(*GamePath, *Params, true, false, false, NULL, 0, NULL, NULL);
if (!NewSession->ProcessHandle.IsValid())
{
UE_LOG(LogPlayLevel, Error, TEXT("Failed to run a copy of the game on this PC."));
}
}
static void HandleOutputReceived(const FString& InMessage)
{
UE_LOG(LogPlayLevel, Log, TEXT("%s"), *InMessage);
}
static void HandleCancelButtonClicked(ILauncherWorkerPtr LauncherWorker)
{
if (LauncherWorker.IsValid())
{
LauncherWorker->Cancel();
}
}
/* FMainFrameActionCallbacks callbacks
*****************************************************************************/
class FLauncherNotificationTask
{
public:
FLauncherNotificationTask( TWeakPtr<SNotificationItem> InNotificationItemPtr, SNotificationItem::ECompletionState InCompletionState, const FText& InText )
: CompletionState(InCompletionState)
, NotificationItemPtr(InNotificationItemPtr)
, Text(InText)
{ }
void DoTask( ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent )
{
if (NotificationItemPtr.IsValid())
{
if (CompletionState == SNotificationItem::CS_Fail)
{
GEditor->PlayEditorSound(TEXT("/Engine/EditorSounds/Notifications/CompileFailed_Cue.CompileFailed_Cue"));
}
else
{
GEditor->PlayEditorSound(TEXT("/Engine/EditorSounds/Notifications/CompileSuccess_Cue.CompileSuccess_Cue"));
}
TSharedPtr<SNotificationItem> NotificationItem = NotificationItemPtr.Pin();
NotificationItem->SetText(Text);
NotificationItem->SetCompletionState(CompletionState);
NotificationItem->ExpireAndFadeout();
}
}
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
ENamedThreads::Type GetDesiredThread( ) { return ENamedThreads::GameThread; }
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FLauncherNotificationTask, STATGROUP_TaskGraphTasks);
}
private:
SNotificationItem::ECompletionState CompletionState;
TWeakPtr<SNotificationItem> NotificationItemPtr;
FText Text;
};
void UEditorEngine::HandleStageStarted(const FString& InStage, TWeakPtr<SNotificationItem> NotificationItemPtr)
{
FFormatNamedArguments Arguments;
FText NotificationText;
if (InStage.Contains(TEXT("Cooking")) || InStage.Contains(TEXT("Cook Task")))
{
FString PlatformName = PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@")));
if (PlatformName.Contains(TEXT("NoEditor")))
{
PlatformName = PlatformName.Left(PlatformName.Find(TEXT("NoEditor")));
}
Arguments.Add(TEXT("PlatformName"), FText::FromString(PlatformName));
NotificationText = FText::Format(LOCTEXT("LauncherTaskProcessingNotification", "Processing Assets for {PlatformName}..."), Arguments);
}
else if (InStage.Contains(TEXT("Build Task")))
{
EPlayOnBuildMode bBuildType = GetDefault<ULevelEditorPlaySettings>()->BuildGameBeforeLaunch;
FString PlatformName = PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@")));
if (PlatformName.Contains(TEXT("NoEditor")))
{
PlatformName = PlatformName.Left(PlatformName.Find(TEXT("NoEditor")));
}
Arguments.Add(TEXT("PlatformName"), FText::FromString(PlatformName));
if (FRocketSupport::IsRocket() || !bPlayUsingLauncherHasCode || !bPlayUsingLauncherHasCompiler || bBuildType == EPlayOnBuildMode::PlayOnBuild_Never)
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskValidateNotification", "Validating Executable for {PlatformName}..."), Arguments);
}
else
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskBuildNotification", "Building Executable for {PlatformName}..."), Arguments);
}
}
else if (InStage.Contains(TEXT("Deploy Task")))
{
Arguments.Add(TEXT("DeviceName"), FText::FromString(PlayUsingLauncherDeviceName));
if(PlayUsingLauncherDeviceName.Len() == 0)
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskStageNotificationNoDevice", "Deploying Executable and Assets..."), Arguments);
}
else
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskStageNotification", "Deploying Executable and Assets to {DeviceName}..."), Arguments);
}
}
else if (InStage.Contains(TEXT("Run Task")))
{
Arguments.Add(TEXT("GameName"), FText::FromString(FApp::GetGameName()));
Arguments.Add(TEXT("DeviceName"), FText::FromString(PlayUsingLauncherDeviceName));
if(PlayUsingLauncherDeviceName.Len() == 0)
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskStageNotificationNoDevice", "Running {GameName}..."), Arguments);
}
else
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskStageNotification", "Running {GameName} on {DeviceName}..."), Arguments);
}
}
NotificationItemPtr.Pin()->SetText(NotificationText);
}
void UEditorEngine::HandleStageCompleted(const FString& InStage, double StageTime, bool bHasCode, TWeakPtr<SNotificationItem> NotificationItemPtr)
{
UE_LOG(LogPlayLevel, Log, TEXT("Completed Launch On Stage: %s, Time: %f"), *InStage, StageTime);
// analytics for launch on
TArray<FAnalyticsEventAttribute> ParamArray;
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Time"), StageTime));
ParamArray.Add(FAnalyticsEventAttribute(TEXT("StageName"), InStage));
FEditorAnalytics::ReportEvent(TEXT( "Editor.LaunchOn.StageComplete" ), PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ParamArray);
}
void UEditorEngine::HandleLaunchCanceled(double TotalTime, bool bHasCode, TWeakPtr<SNotificationItem> NotificationItemPtr)
{
TGraphTask<FLauncherNotificationTask>::CreateTask().ConstructAndDispatchWhenReady(
NotificationItemPtr,
SNotificationItem::CS_Fail,
LOCTEXT("LaunchtaskFailedNotification", "Launch canceled!")
);
// analytics for launch on
TArray<FAnalyticsEventAttribute> ParamArray;
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Time"), TotalTime));
FEditorAnalytics::ReportEvent(TEXT( "Editor.LaunchOn.Canceled" ), PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ParamArray);
bPlayUsingLauncher = false;
}
void UEditorEngine::HandleLaunchCompleted(bool Succeeded, double TotalTime, int32 ErrorCode, bool bHasCode, TWeakPtr<SNotificationItem> NotificationItemPtr, TSharedPtr<class FMessageLog> MessageLog)
{
if (Succeeded)
{
FText CompletionMsg;
const FString DummyDeviceName(FString::Printf(TEXT("All_iOS_On_%s"), FPlatformProcess::ComputerName()));
if (PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))) == TEXT("IOS") && PlayUsingLauncherDeviceName.Contains(DummyDeviceName))
{
CompletionMsg = LOCTEXT("LauncherTaskCompleted", "Deployment complete! Open the app on your device to launch.");
TSharedPtr<SNotificationItem> NotificationItem = NotificationItemPtr.Pin();
// NotificationItem->SetExpireDuration(30.0f);
}
else
{
CompletionMsg = LOCTEXT("LauncherTaskCompleted", "Launch complete!!");
}
TGraphTask<FLauncherNotificationTask>::CreateTask().ConstructAndDispatchWhenReady(
NotificationItemPtr,
SNotificationItem::CS_Success,
CompletionMsg
);
// analytics for launch on
TArray<FAnalyticsEventAttribute> ParamArray;
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Time"), TotalTime));
FEditorAnalytics::ReportEvent(TEXT( "Editor.LaunchOn.Completed" ), PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ParamArray);
UE_LOG(LogPlayLevel, Log, TEXT("Launch On Completed. Time: %f"), TotalTime);
}
else
{
FText CompletionMsg;
const FString DummyDeviceName(FString::Printf(TEXT("All_iOS_On_%s"), FPlatformProcess::ComputerName()));
if (PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))) == TEXT("IOS") && PlayUsingLauncherDeviceName.Contains(DummyDeviceName))
{
CompletionMsg = LOCTEXT("LauncherTaskFailed", "Deployment failed!");
}
else
{
CompletionMsg = LOCTEXT("LauncherTaskFailed", "Launch failed!");
}
MessageLog->Error()
->AddToken(FTextToken::Create(CompletionMsg))
->AddToken(FTextToken::Create(FText::FromString(FEditorAnalytics::TranslateErrorCode(ErrorCode))));
// flush log, because it won't be destroyed until the notification popup closes
MessageLog->NumMessages(EMessageSeverity::Info);
TGraphTask<FLauncherNotificationTask>::CreateTask().ConstructAndDispatchWhenReady(
NotificationItemPtr,
SNotificationItem::CS_Fail,
CompletionMsg
);
TArray<FAnalyticsEventAttribute> ParamArray;
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Time"), TotalTime));
FEditorAnalytics::ReportEvent(TEXT( "Editor.LaunchOn.Failed" ), PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ErrorCode, ParamArray);
}
bPlayUsingLauncher = false;
}
static void HandleHyperlinkNavigate()
{
FGlobalTabmanager::Get()->InvokeTab(FName("OutputLog"));
}
struct FInternalPlayLevelUtils
{
static int32 ResolveDirtyBlueprints(const bool bPromptForCompile, TArray<UBlueprint*>& ErroredBlueprints, const bool bForceLevelScriptRecompile = true)
{
const bool bAutoCompile = !bPromptForCompile;
FString PromtDirtyList;
TArray<UBlueprint*> InNeedOfRecompile;
ErroredBlueprints.Empty();
double BPRegenStartTime = FPlatformTime::Seconds();
for (TObjectIterator<UBlueprint> BlueprintIt; BlueprintIt; ++BlueprintIt)
{
UBlueprint* Blueprint = *BlueprintIt;
// do not try to recompile BPs that have not changed since they last failed to compile, so don't check Blueprint->IsUpToDate()
const bool bIsDirtyAndShouldBeRecompiled = Blueprint->IsPossiblyDirty();
if (!FBlueprintEditorUtils::IsDataOnlyBlueprint(Blueprint)
&& (bIsDirtyAndShouldBeRecompiled || (FBlueprintEditorUtils::IsLevelScriptBlueprint(Blueprint) && bForceLevelScriptRecompile))
&& (Blueprint->Status != BS_Unknown)
&& !Blueprint->IsPendingKill())
{
InNeedOfRecompile.Add(Blueprint);
if (bPromptForCompile)
{
PromtDirtyList += FString::Printf(TEXT("\n %s"), *Blueprint->GetName());
}
}
else if (BS_Error == Blueprint->Status && Blueprint->bDisplayCompilePIEWarning)
{
ErroredBlueprints.Add(Blueprint);
}
}
bool bRunCompilation = bAutoCompile;
if (bPromptForCompile)
{
FFormatNamedArguments Args;
Args.Add(TEXT("DirtyBlueprints"), FText::FromString(PromtDirtyList));
const FText PromptMsg = FText::Format(NSLOCTEXT("PlayInEditor", "PrePIE_BlueprintsDirty", "One or more blueprints have been modified without being recompiled. Do you want to compile them now? \n{DirtyBlueprints}"), Args);
EAppReturnType::Type PropmtResponse = FMessageDialog::Open(EAppMsgType::YesNo, PromptMsg);
bRunCompilation = (PropmtResponse == EAppReturnType::Yes);
}
int32 RecompiledCount = 0;
FMessageLog BlueprintLog("BlueprintLog");
if (bRunCompilation && (InNeedOfRecompile.Num() > 0))
{
const FText LogPageLabel = (bAutoCompile) ? LOCTEXT("BlueprintAutoCompilationPageLabel", "Pre-Play auto-recompile") :
LOCTEXT("BlueprintCompilationPageLabel", "Pre-Play recompile");
BlueprintLog.NewPage(LogPageLabel);
// Recompile all necessary blueprints in a single loop, saving GC until the end
for (auto BlueprintIt = InNeedOfRecompile.CreateIterator(); BlueprintIt; ++BlueprintIt)
{
UBlueprint* Blueprint = *BlueprintIt;
int32 CurrItIndex = BlueprintIt.GetIndex();
// gather dependencies so we can ensure that they're getting recompiled as well
TArray<UBlueprint*> Dependencies;
FBlueprintEditorUtils::GetDependentBlueprints(Blueprint, Dependencies);
// if the user made a change, but didn't hit "compile", then dependent blueprints
// wouldn't have been marked dirty, so here we make sure to add those dependencies
// to the end of the InNeedOfRecompile array (so we hit them too in this loop)
for (auto DependencyIt = Dependencies.CreateIterator(); DependencyIt; ++DependencyIt)
{
UBlueprint* DependentBp = *DependencyIt;
int32 ExistingIndex = InNeedOfRecompile.Find(DependentBp);
// if this dependent blueprint is already set up to compile
// later in this loop, then there is no need to add it to be recompiled again
if (ExistingIndex >= CurrItIndex)
{
continue;
}
// if this blueprint wasn't slated to be recompiled
if (ExistingIndex == INDEX_NONE)
{
// we need to make sure this gets recompiled as well
// (since it depends on this other one that is dirty)
InNeedOfRecompile.Add(DependentBp);
}
// else this is a circular dependency... it has previously been compiled
// ... is there a case where we'd want to recompile this again?
}
Blueprint->BroadcastChanged();
UE_LOG(LogPlayLevel, Log, TEXT("[PlayLevel] Compiling %s before play..."), *Blueprint->GetName());
FKismetEditorUtilities::CompileBlueprint(Blueprint, /*bIsRegeneratingOnLoad =*/false, /*bSkipGarbageCollection =*/true);
const bool bHadError = (!Blueprint->IsUpToDate() && Blueprint->Status != BS_Unknown);
// Check if the Blueprint has already been added to the error list to prevent it from being added again
if (bHadError && ErroredBlueprints.Find(Blueprint) == INDEX_NONE)
{
ErroredBlueprints.Add(Blueprint);
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Name"), FText::FromString(Blueprint->GetName()));
BlueprintLog.Info(FText::Format(LOCTEXT("BlueprintCompileFailed", "Blueprint {Name} failed to compile"), Arguments));
}
else
{
++RecompiledCount;
}
UE_LOG(LogPlayLevel, Log, TEXT("PlayLevel: Blueprint regeneration took %d ms (%i blueprints)"), (int32)((FPlatformTime::Seconds() - BPRegenStartTime) * 1000), InNeedOfRecompile.Num());
}
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}
else if (bAutoCompile)
{
UE_LOG(LogPlayLevel, Log, TEXT("PlayLevel: No blueprints needed recompiling"));
}
return RecompiledCount;
}
};
void UEditorEngine::PlayUsingLauncher()
{
if (!PlayUsingLauncherDeviceId.IsEmpty())
{
ILauncherServicesModule& LauncherServicesModule = FModuleManager::LoadModuleChecked<ILauncherServicesModule>(TEXT("LauncherServices"));
ITargetDeviceServicesModule& TargetDeviceServicesModule = FModuleManager::LoadModuleChecked<ITargetDeviceServicesModule>("TargetDeviceServices");
// create a temporary device group and launcher profile
ILauncherDeviceGroupRef DeviceGroup = LauncherServicesModule.CreateDeviceGroup(FGuid::NewGuid(), TEXT("PlayOnDevices"));
DeviceGroup->AddDevice(PlayUsingLauncherDeviceId);
UE_LOG(LogPlayLevel, Log, TEXT("Launcher Device ID: %s"), *PlayUsingLauncherDeviceId);
// does the project have any code?
FGameProjectGenerationModule& GameProjectModule = FModuleManager::LoadModuleChecked<FGameProjectGenerationModule>(TEXT("GameProjectGeneration"));
bPlayUsingLauncherHasCode = GameProjectModule.Get().ProjectRequiresBuild(FName(*PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@")))));
bPlayUsingLauncherHasCompiler = FSourceCodeNavigation::IsCompilerAvailable();
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
// Setup launch profile, keep the setting here to a minimum.
ILauncherProfileRef LauncherProfile = LauncherServicesModule.CreateProfile(TEXT("Play On Device"));
EPlayOnBuildMode bBuildType = PlayInSettings->BuildGameBeforeLaunch;
if ((bBuildType == EPlayOnBuildMode::PlayOnBuild_Always) || (bBuildType == PlayOnBuild_Default && (bPlayUsingLauncherHasCode) && bPlayUsingLauncherHasCompiler))
{
LauncherProfile->SetBuildGame(true);
// set the build configuration to be the same as the running editor
FString ExeName = FUnrealEdMisc::Get().GetExecutableForCommandlets();
if (ExeName.Contains(TEXT("Debug")))
{
LauncherProfile->SetBuildConfiguration(EBuildConfigurations::Debug);
}
else
{
LauncherProfile->SetBuildConfiguration(EBuildConfigurations::Development);
}
}
// select the quickest cook mode based on which in editor cook mode is enabled
bool bIncrimentalCooking = true;
LauncherProfile->AddCookedPlatform(PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))));
ELauncherProfileCookModes::Type CurrentLauncherCookMode = ELauncherProfileCookModes::ByTheBook;
bool bCanCookByTheBookInEditor = true;
bool bCanCookOnTheFlyInEditor = true;
for ( const auto &PlatformName : LauncherProfile->GetCookedPlatforms() )
{
if ( CanCookByTheBookInEditor(PlatformName) == false )
{
bCanCookByTheBookInEditor = false;
}
if ( CanCookOnTheFlyInEditor(PlatformName)== false )
{
bCanCookOnTheFlyInEditor = false;
}
}
if ( bCanCookByTheBookInEditor )
{
CurrentLauncherCookMode = ELauncherProfileCookModes::ByTheBookInEditor;
}
if ( bCanCookOnTheFlyInEditor )
{
CurrentLauncherCookMode = ELauncherProfileCookModes::OnTheFlyInEditor;
bIncrimentalCooking = false;
}
LauncherProfile->SetCookMode( CurrentLauncherCookMode );
LauncherProfile->SetUnversionedCooking(!bIncrimentalCooking);
LauncherProfile->SetIncrementalCooking(bIncrimentalCooking);
LauncherProfile->SetDeployedDeviceGroup(DeviceGroup);
LauncherProfile->SetIncrementalDeploying(bIncrimentalCooking);
LauncherProfile->SetEditorExe(FUnrealEdMisc::Get().GetExecutableForCommandlets());
const FString DummyDeviceName(FString::Printf(TEXT("All_iOS_On_%s"), FPlatformProcess::ComputerName()));
if (PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))) != TEXT("IOS") || !PlayUsingLauncherDeviceName.Contains(DummyDeviceName))
{
LauncherProfile->SetLaunchMode(ELauncherProfileLaunchModes::DefaultRole);
}
if ( LauncherProfile->GetCookMode() == ELauncherProfileCookModes::OnTheFlyInEditor || LauncherProfile->GetCookMode() == ELauncherProfileCookModes::OnTheFly )
{
LauncherProfile->SetDeploymentMode(ELauncherProfileDeploymentModes::FileServer);
}
TArray<UBlueprint*> ErroredBlueprints;
FInternalPlayLevelUtils::ResolveDirtyBlueprints(!PlayInSettings->bAutoCompileBlueprintsOnLaunch, ErroredBlueprints, false);
TArray<FString> MapNames;
FWorldContext & EditorContext = GetEditorWorldContext();
if (EditorContext.World()->WorldComposition || (LauncherProfile->GetCookMode() == ELauncherProfileCookModes::ByTheBookInEditor) || (LauncherProfile->GetCookMode() == ELauncherProfileCookModes::OnTheFlyInEditor) )
{
// Daniel: Only reason we actually need to save any packages is because if a new package is created it won't be on disk yet and CookOnTheFly will early out if the package doesn't exist (even though it could be in memory and not require loading at all)
// future me can optimize this by either adding extra allowances to CookOnTheFlyServer code or only saving packages which doesn't exist if it becomes a problem
// if this returns false, it means we should stop what we're doing and return to the editor
bool bPromptUserToSave = true;
bool bSaveMapPackages = true;
bool bSaveContentPackages = true;
if (!FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages))
{
CancelRequestPlaySession();
return;
}
// Open world composition from original folder
// Or if using by book in editor don't need to resave the package just cook it by the book
FString MapName = EditorContext.World()->GetOutermost()->GetName();
MapNames.Add(MapName);
}
else
{
SaveWorldForPlay(MapNames);
if (MapNames.Num() == 0)
{
CancelRequestPlaySession();
return;
}
}
FString InitialMapName;
if (MapNames.Num() > 0)
{
InitialMapName = MapNames[0];
}
LauncherProfile->GetDefaultLaunchRole()->SetInitialMap(InitialMapName);
for (const FString& MapName : MapNames)
{
LauncherProfile->AddCookedMap(MapName);
}
if ( LauncherProfile->GetCookMode() == ELauncherProfileCookModes::ByTheBookInEditor )
{
TArray<ITargetPlatform*> TargetPlatforms;
for ( const auto &PlatformName : LauncherProfile->GetCookedPlatforms() )
{
ITargetPlatform* TargetPlatform = GetTargetPlatformManager()->FindTargetPlatform(PlatformName);
// todo pass in all the target platforms instead of just the single platform
// crashes if two requests are inflight but we can support having multiple platforms cooking at once
TargetPlatforms.Add( TargetPlatform );
}
const TArray<FString> &CookedMaps = LauncherProfile->GetCookedMaps();
// const TArray<FString>& CookedMaps = ChainState.Profile->GetCookedMaps();
TArray<FString> CookDirectories;
TArray<FString> CookCultures;
TArray<FString> IniMapSections;
StartCookByTheBookInEditor(TargetPlatforms, CookedMaps, CookDirectories, CookCultures, IniMapSections );
FIsCookFinishedDelegate &CookerFinishedDelegate = LauncherProfile->OnIsCookFinished();
CookerFinishedDelegate.BindUObject(this, &UEditorEngine::IsCookByTheBookInEditorFinished);
FCookCanceledDelegate &CookCancelledDelegate = LauncherProfile->OnCookCanceled();
CookCancelledDelegate.BindUObject(this, &UEditorEngine::CancelCookByTheBookInEditor);
}
ILauncherPtr Launcher = LauncherServicesModule.CreateLauncher();
GEditor->LauncherWorker = Launcher->Launch(TargetDeviceServicesModule.GetDeviceProxyManager(), LauncherProfile);
// create notification item
FText LaunchingText = LOCTEXT("LauncherTaskInProgressNotificationNoDevice", "Launching...");
FNotificationInfo Info(LaunchingText);
Info.Image = FEditorStyle::GetBrush(TEXT("MainFrame.CookContent"));
Info.bFireAndForget = false;
Info.ExpireDuration = 10.0f;
Info.Hyperlink = FSimpleDelegate::CreateStatic(HandleHyperlinkNavigate);
Info.HyperlinkText = LOCTEXT("ShowOutputLogHyperlink", "Show Output Log");
Info.ButtonDetails.Add(
FNotificationButtonInfo(
LOCTEXT("LauncherTaskCancel", "Cancel"),
LOCTEXT("LauncherTaskCancelToolTip", "Cancels execution of this task."),
FSimpleDelegate::CreateStatic(HandleCancelButtonClicked, GEditor->LauncherWorker)
)
);
TSharedPtr<SNotificationItem> NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
if (!NotificationItem.IsValid())
{
return;
}
// analytics for launch on
int32 ErrorCode = 0;
FEditorAnalytics::ReportEvent(TEXT( "Editor.LaunchOn.Started" ), PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))), bPlayUsingLauncherHasCode);
NotificationItem->SetCompletionState(SNotificationItem::CS_Pending);
TWeakPtr<SNotificationItem> NotificationItemPtr(NotificationItem);
if (GEditor->LauncherWorker.IsValid() && GEditor->LauncherWorker->GetStatus() != ELauncherWorkerStatus::Completed)
{
TSharedPtr<FMessageLog> MessageLog = MakeShareable(new FMessageLog("PackagingResults"));
GEditor->PlayEditorSound(TEXT("/Engine/EditorSounds/Notifications/CompileStart_Cue.CompileStart_Cue"));
GEditor->LauncherWorker->OnOutputReceived().AddStatic(HandleOutputReceived);
GEditor->LauncherWorker->OnStageStarted().AddUObject(this, &UEditorEngine::HandleStageStarted, NotificationItemPtr);
GEditor->LauncherWorker->OnStageCompleted().AddUObject(this, &UEditorEngine::HandleStageCompleted, bPlayUsingLauncherHasCode, NotificationItemPtr);
GEditor->LauncherWorker->OnCompleted().AddUObject(this, &UEditorEngine::HandleLaunchCompleted, bPlayUsingLauncherHasCode, NotificationItemPtr, MessageLog);
GEditor->LauncherWorker->OnCanceled().AddUObject(this, &UEditorEngine::HandleLaunchCanceled, bPlayUsingLauncherHasCode, NotificationItemPtr);
}
else
{
GEditor->LauncherWorker.Reset();
GEditor->PlayEditorSound(TEXT("/Engine/EditorSounds/Notifications/CompileFailed_Cue.CompileFailed_Cue"));
NotificationItem->SetText(LOCTEXT("LauncherTaskFailedNotification", "Failed to launch task!"));
NotificationItem->SetCompletionState(SNotificationItem::CS_Fail);
NotificationItem->ExpireAndFadeout();
bPlayUsingLauncher = false;
// analytics for launch on
TArray<FAnalyticsEventAttribute> ParamArray;
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Time"), 0.0));
FEditorAnalytics::ReportEvent(TEXT( "Editor.LaunchOn.Failed" ), PlayUsingLauncherDeviceId.Left(PlayUsingLauncherDeviceId.Find(TEXT("@"))), bPlayUsingLauncherHasCode, EAnalyticsErrorCodes::LauncherFailed, ParamArray );
}
}
}
void UEditorEngine::PlayForMovieCapture()
{
TArray<FString> SavedMapNames;
SaveWorldForPlay(SavedMapNames);
if (SavedMapNames.Num() == 0)
{
return;
}
// this parameter tells UE4Editor to run in game mode
FString EditorCommandLine = SavedMapNames[0];
EditorCommandLine += TEXT(" -game");
// renderer overrides - hack
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("d3d11")) ? TEXT(" -d3d11") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("sm5")) ? TEXT(" -sm5") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("dx11")) ? TEXT(" -dx11") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("d3d10")) ? TEXT(" -d3d10") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("sm4")) ? TEXT(" -sm4") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("dx10")) ? TEXT(" -dx10") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("opengl")) ? TEXT(" -opengl") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("opengl3")) ? TEXT(" -opengl3") : TEXT("");
EditorCommandLine += FParse::Param(FCommandLine::Get(), TEXT("opengl4")) ? TEXT(" -opengl4") : TEXT("");
// this parameter tells UGameEngine to add the auto-save dir to the paths array and repopulate the package file cache
// this is needed in order to support streaming levels as the streaming level packages will be loaded only when needed (thus
// their package names need to be findable by the package file caching system)
// (we add to EditorCommandLine because the URL is ignored by WindowsTools)
EditorCommandLine += TEXT(" -PIEVIACONSOLE");
// if we want to start movie capturing right away, then append the argument for that
if (bStartMovieCapture)
{
//disable movies
EditorCommandLine += FString::Printf(TEXT(" -nomovie"));
//set res options
EditorCommandLine += FString::Printf(TEXT(" -ResX=%d"), GEditor->MatineeCaptureResolutionX);
EditorCommandLine += FString::Printf(TEXT(" -ResY=%d"), GEditor->MatineeCaptureResolutionY);
if( GUnrealEd->MatineeScreenshotOptions.bNoTextureStreaming )
{
EditorCommandLine += TEXT(" -NoTextureStreaming");
}
//set fps
EditorCommandLine += FString::Printf(TEXT(" -BENCHMARK -FPS=%d"), GEditor->MatineeScreenshotOptions.MatineeCaptureFPS);
if (GEditor->MatineeScreenshotOptions.MatineeCaptureType.GetValue() != EMatineeCaptureType::AVI)
{
EditorCommandLine += FString::Printf(TEXT(" -MATINEESSCAPTURE=%s"), *GEngine->MatineeScreenshotOptions.MatineeCaptureName);//*GEditor->MatineeNameForRecording);
switch(GEditor->MatineeScreenshotOptions.MatineeCaptureType.GetValue())
{
case EMatineeCaptureType::BMP:
EditorCommandLine += TEXT(" -MATINEESSFORMAT=BMP");
break;
case EMatineeCaptureType::PNG:
EditorCommandLine += TEXT(" -MATINEESSFORMAT=PNG");
break;
case EMatineeCaptureType::JPEG:
EditorCommandLine += TEXT(" -MATINEESSFORMAT=JPEG");
break;
default: break;
}
// If buffer visualization dumping is enabled, we need to tell capture process to enable it too
static const auto CVarDumpFrames = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.BufferVisualizationDumpFrames"));
if (CVarDumpFrames && CVarDumpFrames->GetValueOnGameThread())
{
EditorCommandLine += TEXT(" -MATINEEBUFFERVISUALIZATIONDUMP");
}
}
else
{
EditorCommandLine += FString::Printf(TEXT(" -MATINEEAVICAPTURE=%s"), *GEngine->MatineeScreenshotOptions.MatineeCaptureName);//*GEditor->MatineeNameForRecording);
}
EditorCommandLine += FString::Printf(TEXT(" -MATINEEPACKAGE=%s"), *GEngine->MatineeScreenshotOptions.MatineePackageCaptureName);//*GEditor->MatineePackageNameForRecording);
if (GEditor->MatineeScreenshotOptions.bCompressMatineeCapture == 1)
{
EditorCommandLine += TEXT(" -CompressCapture");
}
}
FString GamePath = FPlatformProcess::GenerateApplicationPath(FApp::GetName(), FApp::GetBuildConfiguration());
FString Params;
if (FPaths::IsProjectFilePathSet())
{
Params = FString::Printf(TEXT("\"%s\" %s %s"), *FPaths::GetProjectFilePath(), *EditorCommandLine, *FCommandLine::GetSubprocessCommandline());
}
else
{
Params = FString::Printf(TEXT("%s %s %s"), FApp::GetGameName(), *EditorCommandLine, *FCommandLine::GetSubprocessCommandline());
}
if ( FRocketSupport::IsRocket() )
{
Params += TEXT(" -rocket");
}
bool bRunningDebug = FParse::Param(FCommandLine::Get(), TEXT("debug"));
if (bRunningDebug)
{
Params += TEXT(" -debug");
}
FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*GamePath, *Params, true, false, false, NULL, 0, NULL, NULL);
if (ProcessHandle.IsValid())
{
bool bCloseEditor = false;
GConfig->GetBool(TEXT("MatineeCreateMovieOptions"), TEXT("CloseEditor"), bCloseEditor, GEditorPerProjectIni);
if (bCloseEditor)
{
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.RequestCloseEditor();
}
}
else
{
UE_LOG(LogPlayLevel, Error, TEXT("Failed to run a copy of the game for matinee capture."));
}
FPlatformProcess::CloseProc(ProcessHandle);
}
void UEditorEngine::RequestEndPlayMap()
{
if( PlayWorld )
{
bRequestEndPlayMapQueued = true;
// Cache the postion and rotation of the camera (the controller may be destroyed before we end the pie session and we need them to preserve the camera position)
if (bLastViewAndLocationValid == false)
{
for (int32 WorldIdx = WorldList.Num() - 1; WorldIdx >= 0; --WorldIdx)
{
FWorldContext &ThisContext = WorldList[WorldIdx];
if (ThisContext.WorldType == EWorldType::PIE)
{
FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(ThisContext.ContextHandle);
if ((SlatePlayInEditorSession != nullptr) && (SlatePlayInEditorSession->EditorPlayer.IsValid() == true) )
{
if( SlatePlayInEditorSession->EditorPlayer.Get()->PlayerController != nullptr )
{
SlatePlayInEditorSession->EditorPlayer.Get()->PlayerController->GetPlayerViewPoint( LastViewLocation, LastViewRotation );
bLastViewAndLocationValid = true;
break;
}
}
}
}
}
}
}
bool UEditorEngine::SavePlayWorldPackages(UWorld* InWorld, const TCHAR* Prefix, TArray<FString>& OutSavedFilenames)
{
{
// if this returns false, it means we should stop what we're doing and return to the editor
bool bPromptUserToSave = true;
bool bSaveMapPackages = false;
bool bSaveContentPackages = true;
if (!FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages))
{
return false;
}
}
// Update cull distance volumes before saving.
InWorld->UpdateCullDistanceVolumes();
// Clean up any old worlds.
CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS );
// Save temporary copies of all levels to be used for playing in editor or using standalone PC/console
return FEditorFileUtils::SaveWorlds(InWorld, FPaths::Combine(*FPaths::GameSavedDir(), *PlayOnConsoleSaveDir), Prefix, OutSavedFilenames);
}
FString UEditorEngine::BuildPlayWorldURL(const TCHAR* MapName, bool bSpectatorMode, FString AdditionalURLOptions)
{
// the URL we are building up
FString URL(MapName);
// If we hold down control, start in spectating mode
if (bSpectatorMode)
{
// Start in spectator mode
URL += TEXT("?SpectatorOnly=1");
}
// Add any game-specific options set in the INI file
URL += InEditorGameURLOptions;
// Add any additional options that were specified for this call
URL += AdditionalURLOptions;
// Add any additional options that are set in the Play In Settings menu
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
FString AdditionalServerGameOptions;
if(PlayInSettings->GetAdditionalServerGameOptions(AdditionalServerGameOptions))
{
URL += *AdditionalServerGameOptions;
}
return URL;
}
bool UEditorEngine::SpawnPlayFromHereStart( UWorld* World, AActor*& PlayerStart, const FVector& StartLocation, const FRotator& StartRotation )
{
// null it out in case we don't need to spawn one, and the caller relies on us setting it
PlayerStart = NULL;
if( bHasPlayWorldPlacement )
{
// spawn the PlayerStartPIE in the given world
FActorSpawnParameters SpawnParameters;
SpawnParameters.OverrideLevel = World->PersistentLevel;
PlayerStart = World->SpawnActor<AActor>(PlayFromHerePlayerStartClass, StartLocation, StartRotation, SpawnParameters);
// make sure we were able to spawn the PlayerStartPIE there
if(!PlayerStart)
{
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Prompt_22", "Failed to create entry point. Try another location, or you may have to rebuild your level."));
return false;
}
// tag the start
ANavigationObjectBase* NavPlayerStart = Cast<ANavigationObjectBase>(PlayerStart);
if (NavPlayerStart)
{
NavPlayerStart->bIsPIEPlayerStart = true;
}
}
// true means we didn't need to spawn, or we succeeded
return true;
}
void UEditorEngine::PlayInEditor( UWorld* InWorld, bool bInSimulateInEditor )
{
// Broadcast PreBeginPIE before checks that might block PIE below (BeginPIE is broadcast below after the checks)
FEditorDelegates::PreBeginPIE.Broadcast(bInSimulateInEditor);
double PIEStartTime = FPlatformTime::Seconds();
// Block PIE when there is a transaction recording into the undo buffer
if (GEditor->IsTransactionActive())
{
FFormatNamedArguments Args;
Args.Add(TEXT("TransactionName"), GEditor->GetTransactionName());
FText NotificationText;
if (bInSimulateInEditor)
{
NotificationText = FText::Format(NSLOCTEXT("UnrealEd", "SIECantStartDuringTransaction", "Can't Simulate when performing {TransactionName} operation"), Args);
}
else
{
NotificationText = FText::Format(NSLOCTEXT("UnrealEd", "PIECantStartDuringTransaction", "Can't Play In Editor when performing {TransactionName} operation"), Args);
}
FNotificationInfo Info(NotificationText);
Info.ExpireDuration = 5.0f;
Info.bUseLargeFont = true;
FSlateNotificationManager::Get().AddNotification(Info);
return;
}
// Prompt the user that Matinee must be closed before PIE can occur.
if( GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_InterpEdit) )
{
const bool bContinuePIE = EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo, NSLOCTEXT("UnrealEd", "PIENeedsToCloseMatineeQ", "'Play in Editor' must close UnrealMatinee. Continue?") );
if ( !bContinuePIE )
{
return;
}
GLevelEditorModeTools().DeactivateMode( FBuiltinEditorModes::EM_InterpEdit );
}
// Make sure there's no outstanding load requests
FlushAsyncLoading();
FBlueprintEditorUtils::FindAndSetDebuggableBlueprintInstances();
// Broadcast BeginPIE after checks that might block PIE above (PreBeginPIE is broadcast above before the checks)
FEditorDelegates::BeginPIE.Broadcast(bInSimulateInEditor);
// let navigation know PIE starts so it can avoid any blueprint creation/deletion/instantiation affect editor map's navmesh changes
if (InWorld->GetNavigationSystem())
{
InWorld->GetNavigationSystem()->OnPIEStart();
}
ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault<ULevelEditorPlaySettings>();
check(PlayInSettings);
TArray<UBlueprint*> ErroredBlueprints;
FInternalPlayLevelUtils::ResolveDirtyBlueprints(!PlayInSettings->AutoRecompileBlueprints, ErroredBlueprints);
if (ErroredBlueprints.Num() && !GIsDemoMode)
{
FString ErroredBlueprintList;
for (auto Blueprint : ErroredBlueprints)
{
ErroredBlueprintList += FString::Printf(TEXT("\n %s"), *Blueprint->GetName());
}
FFormatNamedArguments Args;
Args.Add(TEXT("ErrorBlueprints"), FText::FromString(ErroredBlueprintList));
// There was at least one blueprint with an error, make sure the user is OK with that.
const bool bContinuePIE = EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo, FText::Format( NSLOCTEXT("PlayInEditor", "PrePIE_BlueprintErrors", "One or more blueprints has an unresolved compiler error, are you sure you want to Play in Editor?{ErrorBlueprints}"), Args ) );
if ( !bContinuePIE )
{
FEditorDelegates::EndPIE.Broadcast(bInSimulateInEditor);
if (InWorld->GetNavigationSystem())
{
InWorld->GetNavigationSystem()->OnPIEEnd();
}
return;
}
else
{
// The user wants to ignore the compiler errors, mark the Blueprints and do not warn them again unless the Blueprint attempts to compile
for (auto Blueprint : ErroredBlueprints)
{
Blueprint->bDisplayCompilePIEWarning = false;
}
}
}
const FScopedBusyCursor BusyCursor;
// If there's level already being played, close it. (This may change GWorld)
if(PlayWorld)
{
// immediately end the playworld
EndPlayMap();
}
if (GEngine->HMDDevice.IsValid())
{
GEngine->HMDDevice->OnBeginPlay();
}
// remember old GWorld
EditorWorld = InWorld;
// Clear any messages from last time
GEngine->ClearOnScreenDebugMessages();
// Flush all audio sources from the editor world
FAudioDevice* AudioDevice = EditorWorld->GetAudioDevice();
if (AudioDevice)
{
AudioDevice->Flush( EditorWorld );
AudioDevice->ResetInterpolation();
AudioDevice->OnBeginPIE(bInSimulateInEditor);
}
EditorWorld->bAllowAudioPlayback = false;
if (!PlayInSettings->EnableSound && AudioDevice)
{
AudioDevice->TransientMasterVolume = 0.0f;
}
if (!GEditor->bAllowMultiplePIEWorlds)
{
PlayInSettings->SetRunUnderOneProcess( false );
}
EPlayNetMode PlayNetMode( PIE_Standalone );
PlayInSettings->GetPlayNetMode(PlayNetMode); // Ignore disabled state here
const EPlayNetMode OrigPlayNetMode( PlayNetMode );
bool CanRunUnderOneProcess = [&PlayInSettings]{ bool RunUnderOneProcess(false); return (PlayInSettings->GetRunUnderOneProcess(RunUnderOneProcess) && RunUnderOneProcess); }();
if (CanRunUnderOneProcess)
{
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
const int32 PlayNumberOfClients = [&PlayInSettings]{ int32 NumberOfClients(0); return (PlayInSettings->GetPlayNumberOfClients(NumberOfClients) ? NumberOfClients : 0); }();
const bool WillAutoConnectToServer = [&PlayInSettings]{ bool AutoConnectToServer(false); return (PlayInSettings->GetAutoConnectToServer(AutoConnectToServer) && AutoConnectToServer); }();
if (!CanPlayNetDedicated && (PlayNumberOfClients == 1 || !WillAutoConnectToServer))
{
// Since we don't expose PlayNetMode as an option when doing RunUnderOnProcess,
// we take 1 player and !PlayNetdedicated and being standalone.
// If auto connect is off, launch as standalone unless there is a dedicated server
PlayNetMode = EPlayNetMode::PIE_Standalone;
}
else
{
// We are doing multi-player under one process so make sure the NetMode is ListenServer
PlayNetMode = EPlayNetMode::PIE_ListenServer;
}
PlayInSettings->SetPlayNetMode(PlayNetMode);
}
// Can't allow realtime viewports whilst in PIE so disable it for ALL viewports here.
DisableRealtimeViewports();
bool bAnyBlueprintErrors = ErroredBlueprints.Num() ? true : false;
bool bStartInSpectatorMode = false;
bool bSupportsOnlinePIE = false;
const int32 PlayNumberOfClients = [&PlayInSettings]{ int32 NumberOfClients(0); return (PlayInSettings->GetPlayNumberOfClients(NumberOfClients) ? NumberOfClients : 0); }();
if (SupportsOnlinePIE())
{
bool bHasRequiredLogins = PlayNumberOfClients <= PIELogins.Num();
if (bHasRequiredLogins)
{
// If we support online PIE use it even if we're standalone
bSupportsOnlinePIE = true;
}
else
{
FText ErrorMsg = LOCTEXT("PIELoginFailure", "Not enough login credentials to launch all PIE instances, modify [/Script/UnrealEd.UnrealEdEngine].PIELogins");
UE_LOG(LogOnline, Verbose, TEXT("%s"), *ErrorMsg.ToString());
FMessageLog("PIE").Warning(ErrorMsg);
}
}
FModifierKeysState KeysState = FSlateApplication::Get().GetModifierKeys();
if (bInSimulateInEditor || KeysState.IsControlDown())
{
// if control is pressed, start in spectator mode
bStartInSpectatorMode = true;
}
CanRunUnderOneProcess = [&PlayInSettings]{ bool RunUnderOneProcess(false); return (PlayInSettings->GetRunUnderOneProcess(RunUnderOneProcess) && RunUnderOneProcess); }();
if (bInSimulateInEditor || (PlayNetMode == EPlayNetMode::PIE_Standalone && PlayNumberOfClients <= 1 && !bSupportsOnlinePIE) || !CanRunUnderOneProcess)
{
// Only spawning 1 PIE instance under this process, only set the PIEInstance value if we're not connecting to another local instance of the game, otherwise it will run the wrong streaming levels
const int32 PIEInstance = ( !CanRunUnderOneProcess && PlayNetMode == EPlayNetMode::PIE_Client ) ? INDEX_NONE : 0;
UGameInstance* const GameInstance = CreatePIEGameInstance(PIEInstance, bInSimulateInEditor, bAnyBlueprintErrors, bStartInSpectatorMode, false, PIEStartTime);
if (bInSimulateInEditor)
{
ToggleBetweenPIEandSIE( true );
}
}
else
{
if (bSupportsOnlinePIE)
{
// Make sure all instances of PIE are logged in before creating/launching worlds
LoginPIEInstances(bAnyBlueprintErrors, bStartInSpectatorMode, PIEStartTime);
}
else
{
// Normal, non-online creation/launching of worlds
SpawnIntraProcessPIEWorlds(bAnyBlueprintErrors, bStartInSpectatorMode);
}
}
PlayInSettings->MultipleInstanceLastHeight = PlayInSettings->NewWindowHeight;
PlayInSettings->MultipleInstanceLastWidth = PlayInSettings->NewWindowWidth;
PlayInSettings->SetPlayNetMode(OrigPlayNetMode);
}
void UEditorEngine::SpawnIntraProcessPIEWorlds(bool bAnyBlueprintErrors, bool bStartInSpectatorMode)
{
double PIEStartTime = FPlatformTime::Seconds();
// Has to be false or this function wouldn't be called
bool bInSimulateInEditor = false;
ULevelEditorPlaySettings* PlayInSettings = Cast<ULevelEditorPlaySettings>(ULevelEditorPlaySettings::StaticClass()->GetDefaultObject());
int32 PIEInstance = 0;
// Spawning multiple PIE instances
if (PlayInSettings->MultipleInstancePositions.Num() == 0)
{
PlayInSettings->MultipleInstancePositions.SetNum(1);
}
PlayInSettings->MultipleInstancePositions[0] = PlayInSettings->NewWindowPosition;
int32 NextX = 0;
int32 NextY = 0;
int32 SettingsIndex = 1;
int32 ClientNum = 0;
PIEInstance = 1;
const bool WillAutoConnectToServer = [&PlayInSettings]{ bool AutoConnectToServer(false); return (PlayInSettings->GetAutoConnectToServer(AutoConnectToServer) && AutoConnectToServer); }();
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
// Server
FString ServerPrefix;
if (CanPlayNetDedicated || WillAutoConnectToServer)
{
PlayInSettings->SetPlayNetMode(EPlayNetMode::PIE_ListenServer);
if (!CanPlayNetDedicated)
{
ClientNum++;
GetMultipleInstancePositions(SettingsIndex++, NextX, NextY);
}
UGameInstance* const ServerGameInstance = CreatePIEGameInstance(PIEInstance, bInSimulateInEditor, bAnyBlueprintErrors, bStartInSpectatorMode, CanPlayNetDedicated, PIEStartTime);
if (ServerGameInstance)
{
ServerPrefix = ServerGameInstance->GetWorldContext()->PIEPrefix;
}
else
{
// Failed, abort
return;
}
PIEInstance++;
}
// Clients
const int32 PlayNumberOfClients = [&PlayInSettings]{ int32 NumberOfClients(0); return (PlayInSettings->GetPlayNumberOfClients(NumberOfClients) ? NumberOfClients : 0); }();
for (; ClientNum < PlayNumberOfClients; ++ClientNum)
{
// Only launch as clients if they should connect
if (WillAutoConnectToServer)
{
PlayInSettings->SetPlayNetMode(EPlayNetMode::PIE_Client);
}
else
{
PlayInSettings->SetPlayNetMode(EPlayNetMode::PIE_Standalone);
}
GetMultipleInstancePositions(SettingsIndex++, NextX, NextY);
UGameInstance* const ClientGameInstance = CreatePIEGameInstance(PIEInstance, bInSimulateInEditor, bAnyBlueprintErrors, bStartInSpectatorMode, false, PIEStartTime);
if (ClientGameInstance)
{
ClientGameInstance->GetWorldContext()->PIERemapPrefix = ServerPrefix;
}
else
{
// Failed, abort
return;
}
PIEInstance++;
}
// Restore window settings
GetMultipleInstancePositions(0, NextX, NextY); // restore cached settings
}
bool UEditorEngine::CreatePIEWorldFromLogin(FWorldContext& PieWorldContext, EPlayNetMode PlayNetMode, FPieLoginStruct& DataStruct)
{
ULevelEditorPlaySettings* PlayInSettings = Cast<ULevelEditorPlaySettings>(ULevelEditorPlaySettings::StaticClass()->GetDefaultObject());
PlayInSettings->SetPlayNetMode(PlayNetMode);
// Set window position
GetMultipleInstancePositions(DataStruct.SettingsIndex, DataStruct.NextX, DataStruct.NextY);
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
const bool ActAsClient = PlayNetMode == EPlayNetMode::PIE_Client || PlayNetMode == EPlayNetMode::PIE_Standalone;
UGameInstance* const GameInstance = CreatePIEGameInstance(PieWorldContext.PIEInstance, false, DataStruct.bAnyBlueprintErrors, DataStruct.bStartInSpectatorMode, ActAsClient ? false : CanPlayNetDedicated, DataStruct.PIEStartTime);
// Restore window settings
GetMultipleInstancePositions(0, DataStruct.NextX, DataStruct.NextY); // restore cached settings
if (GameInstance)
{
GameInstance->GetWorldContext()->bWaitingOnOnlineSubsystem = false;
if (PlayNetMode == EPlayNetMode::PIE_ListenServer)
{
// If any clients finished before us, update their PIERemapPrefix
for (FWorldContext &WorldContext : WorldList)
{
if (WorldContext.WorldType == EWorldType::PIE && WorldContext.World() != NULL && WorldContext.ContextHandle != PieWorldContext.ContextHandle)
{
WorldContext.PIERemapPrefix = PieWorldContext.PIEPrefix;
}
}
}
else
{
// Grab a valid PIERemapPrefix
for (FWorldContext &WorldContext : WorldList)
{
// This relies on the server being the first in the WorldList. Might be risky.
if (WorldContext.WorldType == EWorldType::PIE && WorldContext.World() != NULL && WorldContext.ContextHandle != PieWorldContext.ContextHandle)
{
PieWorldContext.PIERemapPrefix = WorldContext.PIEPrefix;
break;
}
}
}
return true;
}
else
{
return false;
}
}
bool UEditorEngine::SupportsOnlinePIE() const
{
if (bOnlinePIEEnabled && PIELogins.Num() > 0)
{
// If we can't get the identity interface then things are either not configured right or disabled
IOnlineIdentityPtr IdentityInt = Online::GetIdentityInterface();
return IdentityInt.IsValid();
}
return false;
}
void UEditorEngine::LoginPIEInstances(bool bAnyBlueprintErrors, bool bStartInSpectatorMode, double PIEStartTime)
{
ULevelEditorPlaySettings* PlayInSettings = Cast<ULevelEditorPlaySettings>(ULevelEditorPlaySettings::StaticClass()->GetDefaultObject());
/** Setup the common data values for each login instance */
FPieLoginStruct DataStruct;
DataStruct.SettingsIndex = 1;
DataStruct.bAnyBlueprintErrors = bAnyBlueprintErrors;
DataStruct.bStartInSpectatorMode = bStartInSpectatorMode;
DataStruct.PIEStartTime = PIEStartTime;
int32 ClientNum = 0;
int32 PIEInstance = 1;
int32 NextX = 0;
int32 NextY = 0;
const EPlayNetMode PlayNetMode = [&PlayInSettings]{ EPlayNetMode NetMode(PIE_Standalone); return (PlayInSettings->GetPlayNetMode(NetMode) ? NetMode : PIE_Standalone); }();
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
const bool WillAutoConnectToServer = [&PlayInSettings]{ bool AutoConnectToServer(false); return (PlayInSettings->GetAutoConnectToServer(AutoConnectToServer) && AutoConnectToServer); }();
// Server
if (WillAutoConnectToServer || CanPlayNetDedicated)
{
FWorldContext &PieWorldContext = CreateNewWorldContext(EWorldType::PIE);
PieWorldContext.PIEInstance = PIEInstance++;
PieWorldContext.RunAsDedicated = CanPlayNetDedicated;
PieWorldContext.bWaitingOnOnlineSubsystem = true;
// Update login struct parameters
DataStruct.WorldContextHandle = PieWorldContext.ContextHandle;
DataStruct.NetMode = PlayNetMode;
// Always get the interface (it will create the subsystem regardless)
FName OnlineIdentifier = GetOnlineIdentifier(PieWorldContext);
UE_LOG(LogPlayLevel, Display, TEXT("Creating online subsystem for server %s"), *OnlineIdentifier.ToString());
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get(OnlineIdentifier);
IOnlineIdentityPtr IdentityInt = OnlineSub->GetIdentityInterface();
check(IdentityInt.IsValid());
NumOnlinePIEInstances++;
if (!CanPlayNetDedicated)
{
DataStruct.NextX = NextX;
DataStruct.NextY = NextY;
GetMultipleInstancePositions(DataStruct.SettingsIndex, NextX, NextY);
// Login to online platform before creating world
FOnlineAccountCredentials AccountCreds;
AccountCreds.Id = PIELogins[ClientNum].Id;
AccountCreds.Token = PIELogins[ClientNum].Token;
AccountCreds.Type = PIELogins[ClientNum].Type;
FOnLoginCompleteDelegate Delegate;
Delegate.BindUObject(this, &UEditorEngine::OnLoginPIEComplete, DataStruct);
// Login first and continue the flow later
FDelegateHandle DelegateHandle = IdentityInt->AddOnLoginCompleteDelegate_Handle(0, Delegate);
OnLoginPIECompleteDelegateHandlesForPIEInstances.Add(OnlineIdentifier, DelegateHandle);
IdentityInt->Login(0, AccountCreds);
ClientNum++;
}
else
{
// Dedicated servers don't use a login
OnlineSub->SetForceDedicated(true);
if (CreatePIEWorldFromLogin(PieWorldContext, EPlayNetMode::PIE_ListenServer, DataStruct))
{
FMessageLog("PIE").Info(LOCTEXT("LoggingInDedicated", "Dedicated Server logged in"));
}
else
{
// Failed to create world, this creates a dialog elsewhere
return;
}
}
}
// Clients
const int32 PlayNumberOfClients = [&PlayInSettings]{ int32 NumberOfClients(0); return (PlayInSettings->GetPlayNumberOfClients(NumberOfClients) ? NumberOfClients : 0); }();
for (; ClientNum < PlayNumberOfClients; ++ClientNum)
{
PlayInSettings->SetPlayNetMode(PlayNetMode);
FWorldContext &PieWorldContext = CreateNewWorldContext(EWorldType::PIE);
PieWorldContext.PIEInstance = PIEInstance++;
PieWorldContext.bWaitingOnOnlineSubsystem = true;
// Update login struct parameters
DataStruct.WorldContextHandle = PieWorldContext.ContextHandle;
DataStruct.SettingsIndex++;
DataStruct.NextX = NextX;
DataStruct.NextY = NextY;
GetMultipleInstancePositions(DataStruct.SettingsIndex, NextX, NextY);
DataStruct.NetMode = WillAutoConnectToServer ? EPlayNetMode::PIE_Client : EPlayNetMode::PIE_Standalone;
FName OnlineIdentifier = GetOnlineIdentifier(PieWorldContext);
UE_LOG(LogPlayLevel, Display, TEXT("Creating online subsystem for client %s"), *OnlineIdentifier.ToString());
IOnlineIdentityPtr IdentityInt = Online::GetIdentityInterface(OnlineIdentifier);
check(IdentityInt.IsValid());
NumOnlinePIEInstances++;
FOnlineAccountCredentials AccountCreds;
AccountCreds.Id = PIELogins[ClientNum].Id;
AccountCreds.Token = PIELogins[ClientNum].Token;
AccountCreds.Type = PIELogins[ClientNum].Type;
FOnLoginCompleteDelegate Delegate;
Delegate.BindUObject(this, &UEditorEngine::OnLoginPIEComplete, DataStruct);
IdentityInt->ClearOnLoginCompleteDelegate_Handle(0, OnLoginPIECompleteDelegateHandlesForPIEInstances.FindRef(OnlineIdentifier));
OnLoginPIECompleteDelegateHandlesForPIEInstances.Add(OnlineIdentifier, IdentityInt->AddOnLoginCompleteDelegate_Handle(0, Delegate));
IdentityInt->Login(0, AccountCreds);
}
// Restore window settings
GetMultipleInstancePositions(0, NextX, NextY); // restore cached settings
}
void UEditorEngine::OnLoginPIEComplete(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& ErrorString, FPieLoginStruct DataStruct)
{
// This is needed because pie login may change the state of the online objects that called this function
GetTimerManager()->SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UEditorEngine::OnLoginPIEComplete_Deferred, LocalUserNum, bWasSuccessful, ErrorString, DataStruct));
}
void UEditorEngine::OnLoginPIEComplete_Deferred(int32 LocalUserNum, bool bWasSuccessful, FString ErrorString, FPieLoginStruct DataStruct)
{
UE_LOG(LogOnline, Verbose, TEXT("OnLoginPIEComplete LocalUserNum: %d bSuccess: %d %s"), LocalUserNum, bWasSuccessful, *ErrorString);
FWorldContext* PieWorldContext = GetWorldContextFromHandle(DataStruct.WorldContextHandle);
if (!PieWorldContext)
{
// This will fail if PIE was ended before this callback happened, silently return
return;
}
FName OnlineIdentifier = GetOnlineIdentifier(*PieWorldContext);
IOnlineIdentityPtr IdentityInt = Online::GetIdentityInterface(OnlineIdentifier);
// Cleanup the login delegate before calling create below
FDelegateHandle* DelegateHandle = OnLoginPIECompleteDelegateHandlesForPIEInstances.Find(OnlineIdentifier);
if (DelegateHandle)
{
IdentityInt->ClearOnLoginCompleteDelegate_Handle(0, *DelegateHandle);
OnLoginPIECompleteDelegateHandlesForPIEInstances.Remove(OnlineIdentifier);
}
// Create the new world
if (CreatePIEWorldFromLogin(*PieWorldContext, DataStruct.NetMode, DataStruct))
{
// Logging after the create so a new MessageLog Page is created
if (bWasSuccessful)
{
if (DataStruct.NetMode != EPlayNetMode::PIE_Client)
{
FMessageLog("PIE").Info(LOCTEXT("LoggedInClient", "Server logged in"));
}
else
{
FMessageLog("PIE").Info(LOCTEXT("LoggedInClient", "Client logged in"));
}
}
else
{
if (DataStruct.NetMode != EPlayNetMode::PIE_Client)
{
FMessageLog("PIE").Warning(LOCTEXT("LoggedInClientFailure", "Server failed to login"));
}
else
{
FMessageLog("PIE").Warning(LOCTEXT("LoggedInClientFailure", "Client failed to login"));
}
}
}
}
UGameInstance* UEditorEngine::CreatePIEGameInstance(int32 PIEInstance, bool bInSimulateInEditor, bool bAnyBlueprintErrors, bool bStartInSpectatorMode, bool bRunAsDedicated, float PIEStartTime)
{
const FString WorldPackageName = EditorWorld->GetOutermost()->GetName();
// Start a new PIE log page
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Package"), FText::FromString(FPackageName::GetLongPackageAssetName(WorldPackageName)));
Arguments.Add(TEXT("TimeStamp"), FText::AsDateTime(FDateTime::Now()));
FText PIESessionLabel = bInSimulateInEditor ?
FText::Format(LOCTEXT("SIESessionLabel", "SIE session: {Package} ({TimeStamp})"), Arguments) :
FText::Format(LOCTEXT("PIESessionLabel", "PIE session: {Package} ({TimeStamp})"), Arguments);
FMessageLog("PIE").NewPage(PIESessionLabel);
}
// create a new GameInstance
FStringClassReference GameInstanceClassName = GetDefault<UGameMapsSettings>()->GameInstanceClass;
UClass* GameInstanceClass = (GameInstanceClassName.IsValid() ? LoadObject<UClass>(NULL, *GameInstanceClassName.ToString()) : UGameInstance::StaticClass());
// If the GameInstance class from the settings cannot be found, fall back to the base class
if(GameInstanceClass == nullptr)
{
GameInstanceClass = UGameInstance::StaticClass();
}
UGameInstance* GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);
// We need to temporarily add the GameInstance to the root because the InitPIE call can do garbage collection wiping out the GameInstance
GameInstance->AddToRoot();
bool bSuccess = GameInstance->InitializePIE(bAnyBlueprintErrors, PIEInstance);
if (!bSuccess)
{
FEditorDelegates::EndPIE.Broadcast(bInSimulateInEditor);
if (EditorWorld->GetNavigationSystem())
{
EditorWorld->GetNavigationSystem()->OnPIEEnd();
}
return nullptr;
}
FWorldContext* const PieWorldContext = GameInstance->GetWorldContext();
check(PieWorldContext);
PlayWorld = PieWorldContext->World();
PieWorldContext->RunAsDedicated = bRunAsDedicated;
GWorld = PlayWorld;
SetPlayInEditorWorld( PlayWorld );
#if PLATFORM_64BITS
const FString PlatformBitsString( TEXT( "64" ) );
#else
const FString PlatformBitsString( TEXT( "32" ) );
#endif
const FText WindowTitleOverride = GetDefault<UGeneralProjectSettings>()->ProjectDisplayedTitle;
FFormatNamedArguments Args;
Args.Add( TEXT("GameName"), FText::FromString( FString( WindowTitleOverride.IsEmpty() ? FApp::GetGameName() : WindowTitleOverride.ToString() ) ) );
Args.Add( TEXT("PlatformBits"), FText::FromString( PlatformBitsString ) );
Args.Add( TEXT("RHIName"), FText::FromName( LegacyShaderPlatformToShaderFormat( GShaderPlatformForFeatureLevel[GMaxRHIFeatureLevel] ) ) );
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
const EPlayNetMode PlayNetMode = [&PlayInSettings]{ EPlayNetMode NetMode(PIE_Standalone); return (PlayInSettings->GetPlayNetMode(NetMode) ? NetMode : PIE_Standalone); }();
if (PlayNetMode == PIE_Client)
{
Args.Add(TEXT("NetMode"), FText::FromString(FString::Printf(TEXT("Client %d"), PieWorldContext->PIEInstance - 1)));
}
else if (PlayNetMode == PIE_ListenServer)
{
Args.Add( TEXT("NetMode"), FText::FromString( TEXT("Server")));
}
else
{
Args.Add( TEXT("NetMode"), FText::FromString( TEXT("Standalone")));
}
const FText ViewportName = FText::Format( NSLOCTEXT("UnrealEd", "PlayInEditor_RHI_F", "{GameName} Game Preview {NetMode} ({PlatformBits}-bit/{RHIName})" ), Args );
// Make a list of all the selected actors
TArray<UObject *> SelectedActors;
TArray<UObject*> SelectedComponents;
for ( FSelectionIterator It( GetSelectedActorIterator() ); It; ++It )
{
AActor* Actor = static_cast<AActor*>( *It );
if (Actor)
{
checkSlow( Actor->IsA(AActor::StaticClass()) );
SelectedActors.Add( Actor );
}
}
// Unselect everything
GEditor->SelectNone( true, true, false );
GetSelectedActors()->DeselectAll();
GetSelectedObjects()->DeselectAll();
GetSelectedComponents()->DeselectAll();
// For every actor that was selected previously, make sure it's sim equivalent is selected
for ( int32 ActorIndex = 0; ActorIndex < SelectedActors.Num(); ++ActorIndex )
{
AActor* Actor = Cast<AActor>( SelectedActors[ ActorIndex ] );
if (Actor)
{
ActorsThatWereSelected.Add( Actor );
AActor* SimActor = EditorUtilities::GetSimWorldCounterpartActor(Actor);
if (SimActor && !SimActor->bHidden && bInSimulateInEditor)
{
SelectActor( SimActor, true, false );
}
}
}
// For play in editor, this is the viewport widget where the game is being displayed
TSharedPtr<SViewport> PieViewportWidget;
// Initialize the viewport client.
UGameViewportClient* ViewportClient = NULL;
ULocalPlayer *NewLocalPlayer = NULL;
if (!PieWorldContext->RunAsDedicated)
{
bool bCreateNewAudioDevice = PlayInSettings->IsCreateAudioDeviceForEveryPlayer();
ViewportClient = NewObject<UGameViewportClient>(this, GameViewportClientClass);
ViewportClient->Init(*PieWorldContext, GameInstance, bCreateNewAudioDevice);
GameViewport = ViewportClient;
GameViewport->bIsPlayInEditorViewport = true;
PieWorldContext->GameViewport = ViewportClient;
// Add a handler for viewport close requests
ViewportClient->OnCloseRequested().BindUObject(this, &UEditorEngine::OnViewportCloseRequested);
FSlatePlayInEditorInfo& SlatePlayInEditorSession = SlatePlayInEditorMap.Add(PieWorldContext->ContextHandle, FSlatePlayInEditorInfo());
SlatePlayInEditorSession.DestinationSlateViewport = RequestedDestinationSlateViewport; // Might be invalid depending how pie was launched. Code below handles this.
RequestedDestinationSlateViewport = NULL;
FString Error;
NewLocalPlayer = ViewportClient->SetupInitialLocalPlayer(Error);
if(!NewLocalPlayer)
{
FMessageDialog::Open( EAppMsgType::Ok, FText::Format(NSLOCTEXT("UnrealEd", "Error_CouldntSpawnPlayer", "Couldn't spawn player: {0}"), FText::FromString(Error)) );
// go back to using the real world as GWorld
RestoreEditorWorld( EditorWorld );
EndPlayMap();
return nullptr;
}
if (!bInSimulateInEditor)
{
SlatePlayInEditorSession.EditorPlayer = NewLocalPlayer;
}
// Note: For K2 debugging purposes this MUST be created before beginplay is called because beginplay can trigger breakpoints
// and we need to be able to refocus the pie viewport afterwards so it must be created first in order for us to find it
{
// Only create a separate viewport and window if we aren't playing in a current viewport
if( SlatePlayInEditorSession.DestinationSlateViewport.IsValid() )
{
TSharedPtr<ILevelViewport> LevelViewportRef = SlatePlayInEditorSession.DestinationSlateViewport.Pin();
LevelViewportRef->StartPlayInEditorSession( ViewportClient, bInSimulateInEditor );
}
else
{
// Create the top level pie window and add it to Slate
uint32 NewWindowHeight = PlayInSettings->NewWindowHeight;
uint32 NewWindowWidth = PlayInSettings->NewWindowWidth;
FIntPoint NewWindowPosition = PlayInSettings->NewWindowPosition;
bool CenterNewWindow = PlayInSettings->CenterNewWindow;
// Setup size for PIE window
if ((NewWindowWidth <= 0) || (NewWindowHeight <= 0))
{
// Get desktop metrics
FDisplayMetrics DisplayMetrics;
FSlateApplication::Get().GetDisplayMetrics( DisplayMetrics );
const FVector2D DisplaySize(
DisplayMetrics.PrimaryDisplayWorkAreaRect.Right - DisplayMetrics.PrimaryDisplayWorkAreaRect.Left,
DisplayMetrics.PrimaryDisplayWorkAreaRect.Bottom - DisplayMetrics.PrimaryDisplayWorkAreaRect.Top
);
// Use a centered window at the default window size
NewWindowPosition.X = 0;
NewWindowPosition.Y = 0;
NewWindowWidth = 0.75 * DisplaySize.X;
NewWindowHeight = 0.75 * DisplaySize.Y;
CenterNewWindow = true;
}
bool bUseOSWndBorder = false;
bool bRenderDirectlyToWindow = false;
bool bEnableStereoRendering = false;
if (bUseVRPreviewForPlayWorld)
{
// modify window and viewport properties for VR.
bUseOSWndBorder = true;
bRenderDirectlyToWindow = true;
bEnableStereoRendering = true;
CenterNewWindow = true;
}
TSharedRef<SWindow> PieWindow = SNew(SWindow)
.Title(ViewportName)
.ScreenPosition(FVector2D( NewWindowPosition.X, NewWindowPosition.Y ))
.ClientSize(FVector2D( NewWindowWidth, NewWindowHeight ))
.AutoCenter(CenterNewWindow ? EAutoCenter::PreferredWorkArea : EAutoCenter::None)
.UseOSWindowBorder(bUseOSWndBorder)
.SizingRule(ESizingRule::UserSized);
// Setup a delegate for switching to the play world on slate input events, drawing and ticking
FOnSwitchWorldHack OnWorldSwitch = FOnSwitchWorldHack::CreateUObject( this, &UEditorEngine::OnSwitchWorldForSlatePieWindow );
PieWindow->SetOnWorldSwitchHack( OnWorldSwitch );
if (PlayInSettings->PIEAlwaysOnTop && FSlateApplication::Get().GetActiveTopLevelWindow().IsValid())
FSlateApplication::Get().AddWindowAsNativeChild(PieWindow, FSlateApplication::Get().GetActiveTopLevelWindow().ToSharedRef(), true);
else
FSlateApplication::Get().AddWindow(PieWindow);
TSharedRef<SOverlay> ViewportOverlayWidgetRef = SNew(SOverlay);
TSharedRef<SGameLayerManager> GameLayerManagerRef = SNew(SGameLayerManager)
.SceneViewport_UObject(this, &UEditorEngine::GetGameSceneViewport, ViewportClient)
[
ViewportOverlayWidgetRef
];
PieViewportWidget =
SNew( SViewport )
.IsEnabled(FSlateApplication::Get().GetNormalExecutionAttribute())
.EnableGammaCorrection( false )// Gamma correction in the game is handled in post processing in the scene renderer
.RenderDirectlyToWindow( bRenderDirectlyToWindow )
.EnableStereoRendering( bEnableStereoRendering )
[
GameLayerManagerRef
];
// Create a viewport widget for the game to render in.
PieWindow->SetContent( PieViewportWidget.ToSharedRef() );
// Ensure the PIE window appears does not appear behind other windows.
PieWindow->BringToFront();
ViewportClient->SetViewportOverlayWidget( PieWindow, ViewportOverlayWidgetRef );
ViewportClient->SetGameLayerManager(GameLayerManagerRef);
// Set up a notification when the window is closed so we can clean up PIE
{
struct FLocal
{
static void OnPIEWindowClosed( const TSharedRef< SWindow >& WindowBeingClosed, TWeakPtr< SViewport > PIEViewportWidget, int32 index )
{
// Save off the window position
const FVector2D PIEWindowPos = WindowBeingClosed->GetPositionInScreen();
ULevelEditorPlaySettings* LevelEditorPlaySettings = ULevelEditorPlaySettings::StaticClass()->GetDefaultObject<ULevelEditorPlaySettings>();
if (index <= 0)
{
LevelEditorPlaySettings->NewWindowPosition.X = FPlatformMath::RoundToInt(PIEWindowPos.X);
LevelEditorPlaySettings->NewWindowPosition.Y = FPlatformMath::RoundToInt(PIEWindowPos.Y);
}
else
{
if (index >= LevelEditorPlaySettings->MultipleInstancePositions.Num())
{
LevelEditorPlaySettings->MultipleInstancePositions.SetNum(index + 1);
}
LevelEditorPlaySettings->MultipleInstancePositions[index] = FIntPoint(PIEWindowPos.X, PIEWindowPos.Y);
}
LevelEditorPlaySettings->PostEditChange();
LevelEditorPlaySettings->SaveConfig();
// Route the callback
PIEViewportWidget.Pin()->OnWindowClosed( WindowBeingClosed );
if (PIEViewportWidget.Pin()->IsStereoRenderingAllowed() && GEngine->HMDDevice.IsValid())
{
// restore previously minimized root window.
TSharedPtr<SWindow> RootWindow = FGlobalTabmanager::Get()->GetRootWindow();
if (RootWindow.IsValid())
{
RootWindow->Restore();
}
}
}
};
const bool CanPlayNetDedicated = [&PlayInSettings]{ bool PlayNetDedicated(false); return (PlayInSettings->GetPlayNetDedicated(PlayNetDedicated) && PlayNetDedicated); }();
PieWindow->SetOnWindowClosed(FOnWindowClosed::CreateStatic(&FLocal::OnPIEWindowClosed, TWeakPtr<SViewport>(PieViewportWidget),
PieWorldContext->PIEInstance - (CanPlayNetDedicated ? 1 : 0)));
}
// Create a new viewport that the viewport widget will use to render the game
SlatePlayInEditorSession.SlatePlayInEditorWindowViewport = MakeShareable( new FSceneViewport( ViewportClient, PieViewportWidget ) );
PieViewportWidget->SetViewportInterface( SlatePlayInEditorSession.SlatePlayInEditorWindowViewport.ToSharedRef() );
SlatePlayInEditorSession.SlatePlayInEditorWindow = PieWindow;
// Let the viewport client know what viewport is using it. We need to set the Viewport Frame as
// well (which in turn sets the viewport) so that SetRes command will work.
ViewportClient->SetViewportFrame(SlatePlayInEditorSession.SlatePlayInEditorWindowViewport.Get());
// Mark the viewport as PIE viewport
ViewportClient->Viewport->SetPlayInEditorViewport( ViewportClient->bIsPlayInEditorViewport );
// Ensure the window has a valid size before calling BeginPlay
SlatePlayInEditorSession.SlatePlayInEditorWindowViewport->ResizeFrame( NewWindowWidth, NewWindowHeight, EWindowMode::Windowed, PieWindow->GetPositionInScreen().X, PieWindow->GetPositionInScreen().Y );
// Change the system resolution to match our window, to make sure game and slate window are kept syncronised
FSystemResolution::RequestResolutionChange(NewWindowWidth, NewWindowHeight, EWindowMode::Windowed);
if (bUseVRPreviewForPlayWorld && GEngine->HMDDevice.IsValid())
{
GEngine->HMDDevice->EnableStereo(true);
// minimize the root window to provide max performance for the preview.
TSharedPtr<SWindow> RootWindow = FGlobalTabmanager::Get()->GetRootWindow();
if (RootWindow.IsValid())
{
RootWindow->Minimize();
}
}
}
UGameViewportClient::OnViewportCreated().Broadcast();
}
}
if ( GameViewport != NULL && GameViewport->Viewport != NULL )
{
// Set the game viewport that was just created as a pie viewport.
GameViewport->Viewport->SetPlayInEditorViewport( true );
}
// Disable the screensaver when PIE is running.
EnableScreenSaver( false );
EditorWorld->TransferBlueprintDebugReferences(PlayWorld);
// This must have already been set with a call to DisableRealtimeViewports() outside of this method.
check(!IsAnyViewportRealtime());
// By this point it is safe to remove the GameInstance from the root and allow it to garbage collected as per usual
GameInstance->RemoveFromRoot();
bSuccess = GameInstance->StartPIEGameInstance(NewLocalPlayer, bInSimulateInEditor, bAnyBlueprintErrors, bStartInSpectatorMode);
if (!bSuccess)
{
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_CouldntStartInstance", "Failed to start PIE game instance"));
RestoreEditorWorld( EditorWorld );
EndPlayMap();
return nullptr;
}
// Set up a delegate to be called in Slate when GWorld needs to change. Slate does not have direct access to the playworld to switch itself
FScopedConditionalWorldSwitcher::SwitchWorldForPIEDelegate = FOnSwitchWorldForPIE::CreateUObject( this, &UEditorEngine::OnSwitchWorldsForPIE );
if( PieViewportWidget.IsValid() )
{
// Register the new viewport widget with Slate for viewport specific message routing.
FSlateApplication::Get().RegisterGameViewport( PieViewportWidget.ToSharedRef() );
}
// go back to using the real world as GWorld
RestoreEditorWorld( EditorWorld );
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("MapName"), FText::FromString(GameInstance->PIEMapName));
Arguments.Add(TEXT("StartTime"), FPlatformTime::Seconds() - PIEStartTime);
FMessageLog("PIE").Info( FText::Format(LOCTEXT("PIEStartTime", "Play in editor start time for {MapName} {StartTime}"), Arguments) );
}
// Update the details window with the actors we have just selected
GUnrealEd->UpdateFloatingPropertyWindows();
// Clean up any editor actors being referenced
GEngine->BroadcastLevelActorListChanged();
return GameInstance;
}
void UEditorEngine::OnViewportCloseRequested(FViewport* InViewport)
{
RequestEndPlayMap();
}
const FSceneViewport* UEditorEngine::GetGameSceneViewport(UGameViewportClient* ViewportClient) const
{
return ViewportClient->GetGameViewport();
}
FViewport* UEditorEngine::GetActiveViewport()
{
// Get the Level editor module and request the Active Viewport.
FLevelEditorModule& LevelEditorModule = FModuleManager::Get().GetModuleChecked<FLevelEditorModule>( TEXT("LevelEditor") );
TSharedPtr<ILevelViewport> ActiveLevelViewport = LevelEditorModule.GetFirstActiveViewport();
if ( ActiveLevelViewport.IsValid() )
{
return ActiveLevelViewport->GetActiveViewport();
}
return NULL;
}
FViewport* UEditorEngine::GetPIEViewport()
{
// Check both cases where the PIE viewport may be, otherwise return NULL if none are found.
if( GameViewport )
{
return GameViewport->Viewport;
}
else
{
for (auto It = WorldList.CreateIterator(); It; ++It)
{
FWorldContext &WorldContext = *It;
if (WorldContext.WorldType == EWorldType::PIE)
{
// We can't use FindChecked here because when using the dedicated server option we don't initialize this map
// (we don't use a viewport for the PIE context in this case)
FSlatePlayInEditorInfo * SlatePlayInEditorSessionPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle);
if (SlatePlayInEditorSessionPtr != NULL && SlatePlayInEditorSessionPtr->SlatePlayInEditorWindowViewport.IsValid() )
{
return SlatePlayInEditorSessionPtr->SlatePlayInEditorWindowViewport.Get();
}
}
}
}
return NULL;
}
void UEditorEngine::ToggleBetweenPIEandSIE( bool bNewSession )
{
bIsToggleBetweenPIEandSIEQueued = false;
// The first PIE world context is the one that can toggle between PIE and SIE
// Network PIE/SIE toggling is not really meant to be supported.
FSlatePlayInEditorInfo * SlateInfoPtr = NULL;
for (auto It = WorldList.CreateIterator(); It && !SlateInfoPtr; ++It)
{
FWorldContext &WorldContext = *It;
if (WorldContext.WorldType == EWorldType::PIE && !WorldContext.RunAsDedicated)
{
SlateInfoPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle);
break;
}
}
if (!SlateInfoPtr)
{
return;
}
if( FEngineAnalytics::IsAvailable() && !bNewSession )
{
FString ToggleType = bIsSimulatingInEditor ? TEXT("SIEtoPIE") : TEXT("PIEtoSIE");
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.PIE"), TEXT("ToggleBetweenPIEandSIE"), ToggleType );
}
FSlatePlayInEditorInfo & SlatePlayInEditorSession = *SlateInfoPtr;
// This is only supported inside SLevelEditor viewports currently
TSharedPtr<ILevelViewport> LevelViewport = SlatePlayInEditorSession.DestinationSlateViewport.Pin();
if( ensure(LevelViewport.IsValid()) )
{
FLevelEditorViewportClient& EditorViewportClient = LevelViewport->GetLevelViewportClient();
// Toggle to pie if currently simulating
if( bIsSimulatingInEditor )
{
// The undo system may have a reference to a SIE object that is about to be destroyed, so clear the transactions
ResetTransaction( NSLOCTEXT("UnrealEd", "ToggleBetweenPIEandSIE", "Toggle Between PIE and SIE") );
// The Game's viewport needs to know about the change away from simluate before the PC is (potentially) created
GameViewport->GetGameViewport()->SetPlayInEditorIsSimulate(false);
// The editor viewport client wont be visible so temporarily disable it being realtime
EditorViewportClient.SetRealtime( false, true );
if (!SlatePlayInEditorSession.EditorPlayer.IsValid())
{
OnSwitchWorldsForPIE(true);
UWorld* World = GameViewport->GetWorld();
AGameMode* AuthGameMode = World->GetAuthGameMode();
if (AuthGameMode) // If there is no GameMode, we are probably the client and cannot RestartPlayer.
{
APlayerController* PC = World->GetFirstPlayerController();
AuthGameMode->RemovePlayerControllerFromPlayerCount(PC);
PC->PlayerState->bOnlySpectator = false;
AuthGameMode->NumPlayers++;
bool bNeedsRestart = true;
if (PC->GetPawn() == NULL)
{
// Use the "auto-possess" pawn in the world, if there is one.
for (FConstPawnIterator Iterator = World->GetPawnIterator(); Iterator; ++Iterator)
{
APawn* Pawn = *Iterator;
if (Pawn && Pawn->AutoPossessPlayer == EAutoReceiveInput::Player0)
{
if (Pawn->Controller == nullptr)
{
PC->Possess(Pawn);
bNeedsRestart = false;
}
break;
}
}
}
if (bNeedsRestart)
{
AuthGameMode->RestartPlayer(PC);
if (PC->GetPawn())
{
// If there was no player start, then try to place the pawn where the camera was.
if (PC->StartSpot == nullptr || Cast<AWorldSettings>(PC->StartSpot.Get()))
{
const FVector Location = EditorViewportClient.GetViewLocation();
const FRotator Rotation = EditorViewportClient.GetViewRotation();
PC->SetControlRotation(Rotation);
PC->GetPawn()->TeleportTo(Location, Rotation);
}
}
}
}
OnSwitchWorldsForPIE(false);
}
// A game viewport already exists, tell the level viewport its in to swap to it
LevelViewport->SwapViewportsForPlayInEditor();
// No longer simulating
GameViewport->SetIsSimulateInEditorViewport(false);
EditorViewportClient.SetIsSimulateInEditorViewport(false);
bIsSimulatingInEditor = false;
}
else
{
// Swap to simulate from PIE
LevelViewport->SwapViewportsForSimulateInEditor();
GameViewport->SetIsSimulateInEditorViewport(true);
GameViewport->GetGameViewport()->SetPlayInEditorIsSimulate(true);
EditorViewportClient.SetIsSimulateInEditorViewport(true);
bIsSimulatingInEditor = true;
// Make sure the viewport is in real-time mode
EditorViewportClient.SetRealtime( true );
// The Simulate window should show stats
EditorViewportClient.SetShowStats( true );
if( SlatePlayInEditorSession.EditorPlayer.IsValid() )
{
// Move the editor camera to where the player was.
FVector ViewLocation;
FRotator ViewRotation;
SlatePlayInEditorSession.EditorPlayer.Get()->PlayerController->GetPlayerViewPoint( ViewLocation, ViewRotation );
EditorViewportClient.SetViewLocation( ViewLocation );
if( EditorViewportClient.IsPerspective() )
{
// Rotation only matters for perspective viewports not orthographic
EditorViewportClient.SetViewRotation( ViewRotation );
}
}
}
}
// Backup ActorsThatWereSelected as this will be cleared whilst deselecting
TArray<TWeakObjectPtr<class AActor> > BackupOfActorsThatWereSelected(ActorsThatWereSelected);
// Unselect everything
GEditor->SelectNone( true, true, false );
GetSelectedActors()->DeselectAll();
GetSelectedObjects()->DeselectAll();
// restore the backup
ActorsThatWereSelected = BackupOfActorsThatWereSelected;
// make sure each selected actors sim equivalent is selected if we're Simulating but not if we're Playing
for ( int32 ActorIndex = 0; ActorIndex < ActorsThatWereSelected.Num(); ++ActorIndex )
{
TWeakObjectPtr<AActor> Actor = ActorsThatWereSelected[ ActorIndex ].Get();
if (Actor.IsValid())
{
AActor* SimActor = EditorUtilities::GetSimWorldCounterpartActor(Actor.Get());
if (SimActor && !SimActor->bHidden)
{
SelectActor( SimActor, bIsSimulatingInEditor, false );
}
}
}
}
int32 UEditorEngine::OnSwitchWorldForSlatePieWindow( int32 WorldID )
{
static const int32 EditorWorldID = 0;
static const int32 PieWorldID = 1;
int32 RestoreID = -1;
if( WorldID == -1 && GWorld != PlayWorld && PlayWorld != NULL)
{
// When we have an invalid world id we always switch to the pie world in the PIE window
const bool bSwitchToPIE = true;
OnSwitchWorldsForPIE( bSwitchToPIE );
// The editor world was active restore it later
RestoreID = EditorWorldID;
}
else if( WorldID == PieWorldID && GWorld != PlayWorld)
{
const bool bSwitchToPIE = true;
// Want to restore the PIE world and the current world is not already the pie world
OnSwitchWorldsForPIE( bSwitchToPIE );
}
else if( WorldID == EditorWorldID && GWorld != EditorWorld)
{
const bool bSwitchToPIE = false;
// Want to restore the editor world and the current world is not already the editor world
OnSwitchWorldsForPIE( bSwitchToPIE );
}
else
{
// Current world is already the same as the world being switched to (nested calls to this for example)
}
return RestoreID;
}
void UEditorEngine::OnSwitchWorldsForPIE( bool bSwitchToPieWorld )
{
if( bSwitchToPieWorld )
{
SetPlayInEditorWorld( PlayWorld );
}
else
{
RestoreEditorWorld( EditorWorld );
}
}
bool UEditorEngine::PackageUsingExternalObjects( ULevel* LevelToCheck, bool bAddForMapCheck )
{
check(LevelToCheck);
bool bFoundExternal = false;
TArray<UObject*> ExternalObjects;
if(PackageTools::CheckForReferencesToExternalPackages(NULL, NULL, LevelToCheck, &ExternalObjects ))
{
for(int32 ObjectIndex = 0; ObjectIndex < ExternalObjects.Num(); ++ObjectIndex)
{
// If the object in question has external references and is not pending deletion, add it to the log and tell the user about it below
UObject* ExternalObject = ExternalObjects[ObjectIndex];
if(!ExternalObject->IsPendingKill())
{
bFoundExternal = true;
if( bAddForMapCheck )
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("ObjectName"), FText::FromString(ExternalObject->GetFullName()));
FMessageLog("MapCheck").Warning()
->AddToken(FUObjectToken::Create(ExternalObject))
->AddToken(FTextToken::Create(FText::Format(LOCTEXT( "MapCheck_Message_UsingExternalObject", "{ObjectName} : Externally referenced"), Arguments ) ))
->AddToken(FMapErrorToken::Create(FMapErrors::UsingExternalObject));
}
}
}
}
return bFoundExternal;
}
UWorld* UEditorEngine::CreatePIEWorldBySavingToTemp(FWorldContext &WorldContext, UWorld* InWorld, FString &PlayWorldMapName)
{
double StartTime = FPlatformTime::Seconds();
UWorld * LoadedWorld = NULL;
// We haven't saved it off yet
TArray<FString> SavedMapNames;
SaveWorldForPlay(SavedMapNames);
if (SavedMapNames.Num() == 0)
{
UE_LOG(LogPlayLevel, Warning, TEXT("PIE: Unable to save editor world to temp file"));
return LoadedWorld;
}
// Before loading the map, we need to set these flags to true so that postload will work properly
GIsPlayInEditorWorld = true;
const FName SavedMapFName = FName(*SavedMapNames[0]);
UWorld::WorldTypePreLoadMap.FindOrAdd(SavedMapFName) = EWorldType::PIE;
// Load the package we saved
UPackage* EditorLevelPackage = LoadPackage(NULL, *SavedMapNames[0], LOAD_PackageForPIE);
// Clean up the world type list now that PostLoad has occurred
UWorld::WorldTypePreLoadMap.Remove(SavedMapFName);
if( EditorLevelPackage )
{
// Find world object and use its PersistentLevel pointer.
LoadedWorld = UWorld::FindWorldInPackage(EditorLevelPackage);
if (LoadedWorld)
{
PostCreatePIEWorld(LoadedWorld);
UE_LOG(LogPlayLevel, Log, TEXT("PIE: Created PIE world by saving and reloading to %s (%fs)"), *LoadedWorld->GetPathName(), float(FPlatformTime::Seconds() - StartTime));
}
else
{
UE_LOG(LogPlayLevel, Warning, TEXT("PIE: Unable to find World in loaded package: %s"), *EditorLevelPackage->GetPathName());
}
}
// After loading the map, reset these so that things continue as normal
GIsPlayInEditorWorld = false;
PlayWorldMapName = SavedMapNames[0];
return LoadedWorld;
}
UWorld* UEditorEngine::CreatePIEWorldByDuplication(FWorldContext &WorldContext, UWorld* InWorld, FString &PlayWorldMapName)
{
double StartTime = FPlatformTime::Seconds();
UPackage* InPackage = Cast<UPackage>(InWorld->GetOutermost());
UWorld* CurrentWorld = InWorld;
UWorld* NewPIEWorld = NULL;
const FString WorldPackageName = InPackage->GetName();
// Preserve the old path keeping EditorWorld name the same
PlayWorldMapName = UWorld::ConvertToPIEPackageName(WorldPackageName, WorldContext.PIEInstance);
// Display a busy cursor while we prepare the PIE world
const FScopedBusyCursor BusyCursor;
// Before loading the map, we need to set these flags to true so that postload will work properly
GIsPlayInEditorWorld = true;
const FName PlayWorldMapFName = FName(*PlayWorldMapName);
UWorld::WorldTypePreLoadMap.FindOrAdd(PlayWorldMapFName) = EWorldType::PIE;
// Create a package for the PIE world
UE_LOG( LogPlayLevel, Log, TEXT("Creating play world package: %s"), *PlayWorldMapName );
UPackage* PlayWorldPackage = CastChecked<UPackage>(CreatePackage(NULL,*PlayWorldMapName));
PlayWorldPackage->PackageFlags |= PKG_PlayInEditor;
PlayWorldPackage->PIEInstanceID = WorldContext.PIEInstance;
PlayWorldPackage->FileName = InPackage->FileName;
PlayWorldPackage->SetGuid( InPackage->GetGuid() );
// check(GPlayInEditorID == -1 || GPlayInEditorID == WorldContext.PIEInstance);
// Currently GPlayInEditorID is not correctly reset after map loading, so it's not safe to assert here
GPlayInEditorID = WorldContext.PIEInstance;
{
double SDOStart = FPlatformTime::Seconds();
// Reset any GUID fixups with lazy pointers
FLazyObjectPtr::ResetPIEFixups();
// Prepare string asset references for fixup
TArray<FString> PackageNamesBeingDuplicatedForPIE;
PackageNamesBeingDuplicatedForPIE.Add(PlayWorldMapName);
for ( auto LevelIt = EditorWorld->StreamingLevels.CreateConstIterator(); LevelIt; ++LevelIt )
{
ULevelStreaming* StreamingLevel = *LevelIt;
if ( StreamingLevel )
{
const FString StreamingLevelPIEName = UWorld::ConvertToPIEPackageName(StreamingLevel->GetWorldAssetPackageName(), WorldContext.PIEInstance);
PackageNamesBeingDuplicatedForPIE.Add(StreamingLevelPIEName);
}
}
FStringAssetReference::SetPackageNamesBeingDuplicatedForPIE(PackageNamesBeingDuplicatedForPIE);
// NULL GWorld before various PostLoad functions are called, this makes it easier to debug invalid GWorld accesses
GWorld = NULL;
// Duplicate the editor world to create the PIE world
NewPIEWorld = CastChecked<UWorld>( StaticDuplicateObject(
EditorWorld, // Source root
PlayWorldPackage, // Destination root
*EditorWorld->GetName(),// Name for new object
RF_AllFlags, // FlagMask
NULL, // DestClass
SDO_DuplicateForPie // bDuplicateForPIE
) );
FStringAssetReference::ClearPackageNamesBeingDuplicatedForPIE();
// Store prefix we used to rename this world and streaming levels package names
NewPIEWorld->StreamingLevelsPrefix = UWorld::BuildPIEPackagePrefix(WorldContext.PIEInstance);
// Fixup model components. The index buffers have been created for the components in the EditorWorld and the order
// in which components were post-loaded matters. So don't try to guarantee a particular order here, just copy the
// elements over.
if ( NewPIEWorld->PersistentLevel->Model != NULL
&& NewPIEWorld->PersistentLevel->Model == EditorWorld->PersistentLevel->Model
&& NewPIEWorld->PersistentLevel->ModelComponents.Num() == EditorWorld->PersistentLevel->ModelComponents.Num() )
{
NewPIEWorld->PersistentLevel->Model->ClearLocalMaterialIndexBuffersData();
for (int32 ComponentIndex = 0; ComponentIndex < NewPIEWorld->PersistentLevel->ModelComponents.Num(); ++ComponentIndex)
{
UModelComponent* SrcComponent = EditorWorld->PersistentLevel->ModelComponents[ComponentIndex];
UModelComponent* DestComponent = NewPIEWorld->PersistentLevel->ModelComponents[ComponentIndex];
DestComponent->CopyElementsFrom(SrcComponent);
}
}
UE_LOG(LogPlayLevel, Log, TEXT("PIE: StaticDuplicateObject took: (%fs)"), float(FPlatformTime::Seconds() - SDOStart));
}
// Clean up the world type list now that PostLoad has occurred
UWorld::WorldTypePreLoadMap.Remove(PlayWorldMapFName);
GPlayInEditorID = -1;
check( NewPIEWorld );
NewPIEWorld->FeatureLevel = EditorWorld->FeatureLevel;
PostCreatePIEWorld(NewPIEWorld);
// After loading the map, reset these so that things continue as normal
GIsPlayInEditorWorld = false;
UE_LOG(LogPlayLevel, Log, TEXT("PIE: Created PIE world by copying editor world from %s to %s (%fs)"), *EditorWorld->GetPathName(), *NewPIEWorld->GetPathName(), float(FPlatformTime::Seconds() - StartTime));
return NewPIEWorld;
}
void UEditorEngine::PostCreatePIEWorld(UWorld *NewPIEWorld)
{
double WorldInitStart = FPlatformTime::Seconds();
// Init the PIE world
NewPIEWorld->WorldType = EWorldType::PIE;
NewPIEWorld->InitWorld();
UE_LOG(LogPlayLevel, Log, TEXT("PIE: World Init took: (%fs)"), float(FPlatformTime::Seconds() - WorldInitStart));
// Tag PlayWorld Actors that also exist in EditorWorld. At this point, no temporary/run-time actors exist in PlayWorld
for( FActorIterator PlayActorIt(NewPIEWorld); PlayActorIt; ++PlayActorIt )
{
GEditor->ObjectsThatExistInEditorWorld.Set(*PlayActorIt);
}
}
UWorld* UEditorEngine::CreatePIEWorldFromEntry(FWorldContext &WorldContext, UWorld* InWorld, FString &PlayWorldMapName)
{
double StartTime = FPlatformTime::Seconds();
// Create the world
UWorld *LoadedWorld = UWorld::CreateWorld( EWorldType::PIE, false );
check(LoadedWorld);
if (LoadedWorld->GetOutermost() != GetTransientPackage())
{
LoadedWorld->GetOutermost()->PIEInstanceID = WorldContext.PIEInstance;
}
// Force default GameMode class so project specific code doesn't fire off.
// We want this world to truly remain empty while we wait for connect!
check(LoadedWorld->GetWorldSettings());
LoadedWorld->GetWorldSettings()->DefaultGameMode = AGameMode::StaticClass();
PlayWorldMapName = UGameMapsSettings::GetGameDefaultMap();
return LoadedWorld;
}
bool UEditorEngine::WorldIsPIEInNewViewport(UWorld *InWorld)
{
FWorldContext &WorldContext = GetWorldContextFromWorldChecked(InWorld);
if (WorldContext.WorldType == EWorldType::PIE)
{
FSlatePlayInEditorInfo * SlateInfoPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle);
if (SlateInfoPtr)
{
return SlateInfoPtr->SlatePlayInEditorWindow.IsValid();
}
}
return false;
}
void UEditorEngine::SetPIEInstanceWindowSwitchDelegate(FPIEInstanceWindowSwitch InSwitchDelegate)
{
PIEInstanceWindowSwitchDelegate = InSwitchDelegate;
}
void UEditorEngine::FocusNextPIEWorld(UWorld *CurrentPieWorld, bool previous)
{
// Get the current world's idx
int32 CurrentIdx = 0;
for (CurrentIdx = 0; CurrentPieWorld && CurrentIdx < WorldList.Num(); ++CurrentIdx)
{
if (WorldList[CurrentIdx].World() == CurrentPieWorld)
{
break;
}
}
// Step through the list to find the next or previous
int32 step = previous? -1 : 1;
CurrentIdx += (WorldList.Num() + step);
while ( CurrentPieWorld && WorldList[ CurrentIdx % WorldList.Num() ].World() != CurrentPieWorld )
{
FWorldContext &Context = WorldList[CurrentIdx % WorldList.Num()];
if (Context.World() && Context.WorldType == EWorldType::PIE && Context.GameViewport != NULL)
{
break;
}
CurrentIdx += step;
}
if (WorldList[CurrentIdx % WorldList.Num()].World())
{
FSlatePlayInEditorInfo * SlateInfoPtr = SlatePlayInEditorMap.Find(WorldList[CurrentIdx % WorldList.Num()].ContextHandle);
if (SlateInfoPtr && SlateInfoPtr->SlatePlayInEditorWindow.IsValid())
{
// Force window to front
SlateInfoPtr->SlatePlayInEditorWindow.Pin()->BringToFront();
// Set viewport widget to have keyboard focus
FSlateApplication::Get().SetKeyboardFocus(SlateInfoPtr->SlatePlayInEditorWindowViewport->GetViewportWidget().Pin(), EFocusCause::Navigation);
// Execute notifcation delegate incase game code has to do anything else
PIEInstanceWindowSwitchDelegate.ExecuteIfBound();
}
}
}
UGameViewportClient * UEditorEngine::GetNextPIEViewport(UGameViewportClient * CurrentViewport)
{
// Get the current world's idx
int32 CurrentIdx = 0;
for (CurrentIdx = 0; CurrentViewport && CurrentIdx < WorldList.Num(); ++CurrentIdx)
{
if (WorldList[CurrentIdx].GameViewport == CurrentViewport)
{
break;
}
}
// Step through the list to find the next or previous
int32 step = 1;
CurrentIdx += (WorldList.Num() + step);
while ( CurrentViewport && WorldList[ CurrentIdx % WorldList.Num() ].GameViewport != CurrentViewport )
{
FWorldContext &Context = WorldList[CurrentIdx % WorldList.Num()];
if (Context.GameViewport && Context.WorldType == EWorldType::PIE)
{
return Context.GameViewport;
}
CurrentIdx += step;
}
return NULL;
}
void UEditorEngine::RemapGamepadControllerIdForPIE(class UGameViewportClient* GameViewport, int32 &ControllerId)
{
// Increment the controller id if we are the focused window, and RouteGamepadToSecondWindow is true (and we are running multiple clients).
// This cause the focused window to NOT handle the input, decrement controllerID, and pass it to the next window.
const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
const bool CanRouteGamepadToSecondWindow = [&PlayInSettings]{ bool RouteGamepadToSecondWindow(false); return (PlayInSettings->GetRouteGamepadToSecondWindow(RouteGamepadToSecondWindow) && RouteGamepadToSecondWindow); }();
const bool CanRunUnderOneProcess = [&PlayInSettings]{ bool RunUnderOneProcess(false); return (PlayInSettings->GetRunUnderOneProcess(RunUnderOneProcess) && RunUnderOneProcess); }();
if ( CanRouteGamepadToSecondWindow && CanRunUnderOneProcess && GameViewport->GetWindow().IsValid() && GameViewport->GetWindow()->HasFocusedDescendants())
{
ControllerId++;
}
}
void UEditorEngine::AutomationPlayUsingLauncher(const FString& InLauncherDeviceId)
{
PlayUsingLauncherDeviceId = InLauncherDeviceId;
PlayUsingLauncherDeviceName = PlayUsingLauncherDeviceId.Right(PlayUsingLauncherDeviceId.Find(TEXT("@")));
PlayUsingLauncher();
}
#undef LOCTEXT_NAMESPACE