Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/PlayLevelViaLauncher.cpp
2025-05-18 13:04:45 +08:00

740 lines
30 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Algo/AllOf.h"
#include "AnalyticsEventAttribute.h"
#include "Async/Async.h"
#include "CookerSettings.h"
#include "DesktopPlatformModule.h"
#include "Editor/EditorEngine.h"
#include "Editor/EditorPerProjectUserSettings.h"
#include "EditorAnalytics.h"
#include "Experimental/ZenServerInterface.h"
#include "Framework/Docking/TabManager.h"
#include "Framework/Notifications/NotificationManager.h"
#include "GameProjectGenerationModule.h"
#include "ILauncherServicesModule.h"
#include "Interfaces/ITargetPlatform.h"
#include "Interfaces/ITargetPlatformManagerModule.h"
#include "ITargetDeviceServicesModule.h"
#include "Logging/MessageLog.h"
#include "Misc/CoreMisc.h"
#include "PlatformInfo.h"
#include "PlayLevel.h"
#include "Settings/EditorExperimentalSettings.h"
#include "Settings/LevelEditorPlaySettings.h"
#include "Settings/ProjectPackagingSettings.h"
#include "Settings/PlatformsMenuSettings.h"
#include "TargetReceipt.h"
#include "UnrealEdMisc.h"
#include "Widgets/Notifications/SNotificationList.h"
#define LOCTEXT_NAMESPACE "PlayLevel"
static void HandleHyperlinkNavigate()
{
FGlobalTabmanager::Get()->TryInvokeTab(FName("OutputLog"));
}
static void HandleCancelButtonClicked(ILauncherWorkerPtr LauncherWorker)
{
if (LauncherWorker.IsValid())
{
LauncherWorker->Cancel();
}
}
static void HandleOutputReceived(const FString& InMessage)
{
if (InMessage.Contains(TEXT("Error:")))
{
UE_LOG(LogPlayLevel, Error, TEXT("UAT: %s"), *InMessage);
}
else if (InMessage.Contains(TEXT("Warning:")))
{
UE_LOG(LogPlayLevel, Warning, TEXT("UAT: %s"), *InMessage);
}
else
{
UE_LOG(LogPlayLevel, Log, TEXT("UAT: %s"), *InMessage);
}
}
void UEditorEngine::StartPlayUsingLauncherSession(FRequestPlaySessionParams& InRequestParams)
{
check(InRequestParams.SessionDestination == EPlaySessionDestinationType::Launcher);
// Cache the DeviceId we've been asked to run on. This is used by the UI to know which device
// clicking the button (without choosing from the dropdown) should use.
LastPlayUsingLauncherDeviceId = InRequestParams.LauncherTargetDevice->DeviceId;
LauncherSessionInfo = FLauncherCachedInfo();
LauncherSessionInfo->PlayUsingLauncherDeviceName = PlaySessionRequest->LauncherTargetDevice->DeviceName;
if (!ensureAlwaysMsgf(PlaySessionRequest->LauncherTargetDevice.IsSet(), TEXT("PlayUsingLauncher should not be called without a target device set!")))
{
CancelRequestPlaySession();
return;
}
if (!ensureAlwaysMsgf(LastPlayUsingLauncherDeviceId.Len() > 0, TEXT("PlayUsingLauncher should not be called without a target device id set!")))
{
CancelRequestPlaySession();
return;
}
ILauncherServicesModule& LauncherServicesModule = FModuleManager::LoadModuleChecked<ILauncherServicesModule>(TEXT("LauncherServices"));
ITargetDeviceServicesModule& TargetDeviceServicesModule = FModuleManager::LoadModuleChecked<ITargetDeviceServicesModule>("TargetDeviceServices");
//if the device is not authorized to be launched to, we need to pop an error instead of trying to launch
FString LaunchPlatformName = LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@")));
FString LaunchPlatformNameFromID = LastPlayUsingLauncherDeviceId.Right(LastPlayUsingLauncherDeviceId.Find(TEXT("@")));
ITargetPlatform* LaunchPlatform = GetTargetPlatformManagerRef().FindTargetPlatform(LaunchPlatformName);
FString IniPlatformName = LaunchPlatformName;
// create a temporary device group and launcher profile
ILauncherDeviceGroupRef DeviceGroup = LauncherServicesModule.CreateDeviceGroup(FGuid::NewGuid(), TEXT("PlayOnDevices"));
if (LaunchPlatform != nullptr)
{
IniPlatformName = LaunchPlatform->IniPlatformName();
if (LaunchPlatformNameFromID.Equals(LaunchPlatformName))
{
// create a temporary list of devices for the target platform
TArray<ITargetDevicePtr> TargetDevices;
LaunchPlatform->GetAllDevices(TargetDevices);
for (const ITargetDevicePtr& PlayDevice : TargetDevices)
{
// compose the device id
FString PlayDeviceId = LaunchPlatformName + TEXT("@") + PlayDevice.Get()->GetId().GetDeviceName();
if (PlayDevice.IsValid() && !PlayDevice->IsAuthorized())
{
CancelPlayUsingLauncher();
}
else
{
DeviceGroup->AddDevice(PlayDeviceId);
UE_LOG(LogPlayLevel, Log, TEXT("Launcher Device ID: %s"), *PlayDeviceId);
}
}
}
else
{
ITargetDevicePtr PlayDevice = LaunchPlatform->GetDefaultDevice();
if (PlayDevice.IsValid() && !PlayDevice->IsAuthorized())
{
CancelPlayUsingLauncher();
}
else
{
DeviceGroup->AddDevice(LastPlayUsingLauncherDeviceId);
UE_LOG(LogPlayLevel, Log, TEXT("Launcher Device ID: %s"), *LastPlayUsingLauncherDeviceId);
}
}
if (DeviceGroup.Get().GetNumDevices() == 0)
{
return;
}
}
// set the build/launch configuration
EBuildConfiguration BuildConfiguration = EBuildConfiguration::Development;
const ULevelEditorPlaySettings* EditorPlaySettings = PlaySessionRequest->EditorPlaySettings;
switch (EditorPlaySettings->LaunchConfiguration)
{
case LaunchConfig_Debug:
BuildConfiguration = EBuildConfiguration::Debug;
break;
case LaunchConfig_Development:
BuildConfiguration = EBuildConfiguration::Development;
break;
case LaunchConfig_Test:
BuildConfiguration = EBuildConfiguration::Test;
break;
case LaunchConfig_Shipping:
BuildConfiguration = EBuildConfiguration::Shipping;
break;
default:
{
const UProjectPackagingSettings* AllPlatformPackagingSettings = GetDefault<UProjectPackagingSettings>();
const UPlatformsMenuSettings* PlatformsSettings = GetDefault<UPlatformsMenuSettings>();
EProjectPackagingBuildConfigurations BuildConfig = PlatformsSettings->GetBuildConfigurationForPlatform(*IniPlatformName);
// if PPBC_MAX is set, then the project default should be used instead of the per platform build config
if (BuildConfig == EProjectPackagingBuildConfigurations::PPBC_MAX)
{
BuildConfig = AllPlatformPackagingSettings->BuildConfiguration;
}
switch (BuildConfig)
{
case EProjectPackagingBuildConfigurations::PPBC_Debug:
case EProjectPackagingBuildConfigurations::PPBC_DebugGame:
BuildConfiguration = EBuildConfiguration::Debug;
break;
case EProjectPackagingBuildConfigurations::PPBC_Development:
BuildConfiguration = EBuildConfiguration::Development;
break;
case EProjectPackagingBuildConfigurations::PPBC_Test:
BuildConfiguration = EBuildConfiguration::Test;
break;
case EProjectPackagingBuildConfigurations::PPBC_Shipping:
BuildConfiguration = EBuildConfiguration::Shipping;
break;
}
break;
}
}
// does the project have any code?
FGameProjectGenerationModule& GameProjectModule = FModuleManager::LoadModuleChecked<FGameProjectGenerationModule>(TEXT("GameProjectGeneration"));
LauncherSessionInfo->bPlayUsingLauncherHasCode = GameProjectModule.Get().ProjectHasCodeFiles();
// Figure out if we need to build anything
ELauncherProfileBuildModes::Type BuildMode;
if (EditorPlaySettings->BuildGameBeforeLaunch == EPlayOnBuildMode::PlayOnBuild_Always)
{
BuildMode = ELauncherProfileBuildModes::Build;
}
else if (EditorPlaySettings->BuildGameBeforeLaunch == EPlayOnBuildMode::PlayOnBuild_Never)
{
BuildMode = ELauncherProfileBuildModes::DoNotBuild;
}
else
{
BuildMode = ELauncherProfileBuildModes::Auto;
}
// Assume it's building unless disabled
LauncherSessionInfo->bPlayUsingLauncherBuild = (BuildMode != ELauncherProfileBuildModes::DoNotBuild);
// Setup launch profile, keep the setting here to a minimum.
ILauncherProfileRef LauncherProfile = LauncherServicesModule.CreateProfile(TEXT("Launch On Device"));
LauncherProfile->SetBuildMode(BuildMode);
LauncherProfile->SetBuildConfiguration(BuildConfiguration);
if (InRequestParams.EditorPlaySettings && !InRequestParams.EditorPlaySettings->AdditionalLaunchParameters.IsEmpty())
{
LauncherProfile->SetAdditionalCommandLineParameters(InRequestParams.EditorPlaySettings->AdditionalLaunchParameters);
}
LauncherProfile->AddCookedPlatform(LaunchPlatformName);
LauncherProfile->SetDeviceIsASimulator(InRequestParams.LauncherTargetDevice->bIsSimulator);
// select the quickest cook mode based on which in editor cook mode is enabled
const UCookerSettings& CookerSettings = *GetDefault<UCookerSettings>();
const UEditorExperimentalSettings& ExperimentalSettings = *GetDefault<UEditorExperimentalSettings>();
bool bInEditorCooking = false;
bool bCookOnTheFly = false;
ELauncherProfileCookModes::Type CurrentLauncherCookMode = ELauncherProfileCookModes::ByTheBook;
if (!CookerSettings.bCookOnTheFlyForLaunchOn)
{
bInEditorCooking = Algo::AllOf(LauncherProfile->GetCookedPlatforms(),
[this](const FString& PlatformName) { return CanCookByTheBookInEditor(PlatformName); });
CurrentLauncherCookMode = bInEditorCooking ? ELauncherProfileCookModes::ByTheBookInEditor: ELauncherProfileCookModes::ByTheBook;
}
else
{
bCookOnTheFly = true;
bInEditorCooking = Algo::AllOf(LauncherProfile->GetCookedPlatforms(),
[this](const FString& PlatformName) { return CanCookOnTheFlyInEditor(PlatformName); });
CurrentLauncherCookMode = bInEditorCooking ? ELauncherProfileCookModes::OnTheFlyInEditor : ELauncherProfileCookModes::OnTheFly;
}
bool bIncrementalCooking = (CookerSettings.bIterativeCookingForLaunchOn || ExperimentalSettings.bSharedCookedBuilds) && !bCookOnTheFly;
if (CurrentLauncherCookMode == ELauncherProfileCookModes::OnTheFlyInEditor ||
CurrentLauncherCookMode == ELauncherProfileCookModes::ByTheBookInEditor)
{
// For now World Partition doesn't support InEditor cooking because its cooking is destructive -
// it moves UObjects out of the generator package into the streaming packages. To allow cooking it in
// the editor process, we will need to make it non-destructive or restore the package afterwards.
FWorldContext& EditorContext = GetEditorWorldContext();
if (EditorContext.World()->IsPartitionedWorld())
{
FString ErrorMsg = FString::Printf(TEXT("Error launching map %s : Quick launch with WorldPartition doesn't yet support cooking in the editor process.\n")
TEXT("To launch this map using Quick launch, set EditorPerProjectUserSettings.ini:[/Script/UnrealEd.EditorExperimentalSettings]:bDisableCookInEditor=true and relaunch the editor."),
*EditorContext.World()->GetOutermost()->GetName());
UE_LOG(LogPlayLevel, Error, TEXT("%s"), *ErrorMsg);
FMessageLog("EditorErrors").Error(FText::FromString(ErrorMsg));
FMessageLog("EditorErrors").Open();
CancelRequestPlaySession();
return;
}
}
TStringBuilder<256> CookOptions;
CookOptions << LauncherProfile->GetCookOptions();
ensure(CookOptions.Len() == 0);
auto SetCookOption = [&CookOptions](FStringView Option, bool bOptionOn)
{
ensure(CookOptions.ToView().Find(Option) == INDEX_NONE);
if (bOptionOn)
{
CookOptions << (CookOptions.Len() > 0 ? TEXTVIEW(" ") : TEXTVIEW(""));
CookOptions << Option;
}
};
// content only projects won't have multiple targets to pick from, and pasing -target=UnrealGame will fail if what C++ thinks
// is a content only project needs a temporary target.cs file in UBT,
// only set the BuildTarget in code-based projects
if (LauncherSessionInfo->bPlayUsingLauncherHasCode)
{
const FTargetInfo* TargetInfo = GetDefault<UPlatformsMenuSettings>()->GetLaunchOnTargetInfo();
if (TargetInfo != nullptr)
{
LauncherProfile->SetBuildTarget(TargetInfo->Name);
LauncherProfile->SetBuildTargetSpecified(true);
}
}
LauncherProfile->SetCookMode(CurrentLauncherCookMode);
LauncherProfile->SetUnversionedCooking(!bIncrementalCooking); // Unversioned cooking is not allowed with incremental cooking
LauncherProfile->SetIncrementalCookMode(bIncrementalCooking ? ELauncherProfileIncrementalCookMode::ModifiedOnly : ELauncherProfileIncrementalCookMode::None);
SetCookOption(TEXTVIEW("-IgnoreIniSettingsOutOfDate"), bIncrementalCooking && CookerSettings.bIgnoreIniSettingsOutOfDateForIteration);
SetCookOption(TEXTVIEW("-IgnoreScriptPackagesOutOfDate"), bIncrementalCooking && CookerSettings.bIgnoreScriptPackagesOutOfDateForIteration);
SetCookOption(TEXTVIEW("-IterateSharedCookedbuild"), bIncrementalCooking && ExperimentalSettings.bSharedCookedBuilds);
LauncherProfile->SetDeployedDeviceGroup(DeviceGroup);
LauncherProfile->SetIncrementalDeploying(bIncrementalCooking);
LauncherProfile->SetEditorExe(FUnrealEdMisc::Get().GetExecutableForCommandlets());
LauncherProfile->SetShouldUpdateDeviceFlash(InRequestParams.LauncherTargetDevice->bUpdateDeviceFlash);
LauncherProfile->SetCookOptions(*CookOptions);
if (!InRequestParams.LauncherTargetDevice->Architecture.IsEmpty())
{
LauncherProfile->SetClientArchitectures({&InRequestParams.LauncherTargetDevice->Architecture, 1});
}
if (LauncherProfile->IsBuildingUAT() && !GetDefault<UEditorPerProjectUserSettings>()->bAlwaysBuildUAT && bUATSuccessfullyCompiledOnce)
{
// UAT was built on a first launch and there's no need to rebuild it any more
LauncherProfile->SetBuildUAT(false);
}
const FString DummyIOSDeviceName(FString::Printf(TEXT("All_iOS_On_%s"), FPlatformProcess::ComputerName()));
const FString DummyTVOSDeviceName(FString::Printf(TEXT("All_tvOS_On_%s"), FPlatformProcess::ComputerName()));
if ((LaunchPlatformName != TEXT("IOS") && LaunchPlatformName != TEXT("TVOS")) ||
(!LauncherSessionInfo->PlayUsingLauncherDeviceName.Contains(DummyIOSDeviceName) && !LauncherSessionInfo->PlayUsingLauncherDeviceName.Contains(DummyTVOSDeviceName)))
{
LauncherProfile->SetLaunchMode(ELauncherProfileLaunchModes::DefaultRole);
}
const bool bUseZenStore = GetDefault<UProjectPackagingSettings>()->GetUseZenStoreEffective();
LauncherProfile->SetUseZenStore(bUseZenStore);
#if UE_WITH_ZEN
if (bUseZenStore)
{
static UE::Zen::FScopeZenService EditorStaticZenService;
}
#endif
if (bUseZenStore || LauncherProfile->GetCookMode() == ELauncherProfileCookModes::OnTheFlyInEditor || LauncherProfile->GetCookMode() == ELauncherProfileCookModes::OnTheFly)
{
LauncherProfile->SetDeploymentMode(ELauncherProfileDeploymentModes::FileServer);
}
switch(EditorPlaySettings->PackFilesForLaunch)
{
default:
case EPlayOnPakFileMode::NoPak:
break;
case EPlayOnPakFileMode::PakNoCompress:
LauncherProfile->SetCompressed( false );
LauncherProfile->SetDeployWithUnrealPak( true );
break;
case EPlayOnPakFileMode::PakCompress:
LauncherProfile->SetCompressed( true );
LauncherProfile->SetDeployWithUnrealPak( true );
break;
}
TArray<UBlueprint*> ErroredBlueprints;
FInternalPlayLevelUtils::ResolveDirtyBlueprints(!EditorPlaySettings->bAutoCompileBlueprintsOnLaunch, ErroredBlueprints, false);
TArray<FString> MapNames;
FWorldContext & EditorContext = GetEditorWorldContext();
// Load maps in place as we saved them above
FString EditorMapName = EditorContext.World()->GetOutermost()->GetName();
MapNames.Add(EditorMapName);
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 FString& 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> IniMapSections;
StartCookByTheBookInEditor(TargetPlatforms, CookedMaps, CookDirectories, GetDefault<UProjectPackagingSettings>()->CulturesToStage, 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 = FAppStyle::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)
)
);
// Launch doesn't block PIE/Compile requests as it's an async background process, so we just
// cancel the request to denote it as having been handled. This has to come after we've used
// anything we might need from the original request.
CancelRequestPlaySession();
TSharedPtr<SNotificationItem> NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
if (!NotificationItem.IsValid())
{
return;
}
// analytics for launch on
TArray<FAnalyticsEventAttribute> AnalyticsParamArray;
if (LaunchPlatform != nullptr)
{
LaunchPlatform->GetPlatformSpecificProjectAnalytics(AnalyticsParamArray);
}
FEditorAnalytics::ReportEvent(TEXT("Editor.LaunchOn.Started"), LaunchPlatformName, LauncherSessionInfo->bPlayUsingLauncherHasCode, AnalyticsParamArray);
NotificationItem->SetCompletionState(SNotificationItem::CS_Pending);
TWeakPtr<SNotificationItem> NotificationItemPtr(NotificationItem);
if (GEditor->LauncherWorker.IsValid() && GEditor->LauncherWorker->GetStatus() != ELauncherWorkerStatus::Completed)
{
if (EditorPlaySettings->EnablePIEEnterAndExitSounds)
{
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, LauncherSessionInfo->bPlayUsingLauncherHasCode, NotificationItemPtr);
GEditor->LauncherWorker->OnCompleted().AddUObject(this, &UEditorEngine::HandleLaunchCompleted, LauncherSessionInfo->bPlayUsingLauncherHasCode, NotificationItemPtr);
GEditor->LauncherWorker->OnCanceled().AddUObject(this, &UEditorEngine::HandleLaunchCanceled, LauncherSessionInfo->bPlayUsingLauncherHasCode, NotificationItemPtr);
}
else
{
GEditor->LauncherWorker.Reset();
if (EditorPlaySettings->EnablePIEEnterAndExitSounds)
{
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();
// analytics for launch on
TArray<FAnalyticsEventAttribute> ParamArray;
ParamArray.Add(FAnalyticsEventAttribute(TEXT("Time"), 0.0));
FEditorAnalytics::ReportEvent(TEXT("Editor.LaunchOn.Failed"), LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))), LauncherSessionInfo->bPlayUsingLauncherHasCode, EAnalyticsErrorCodes::LauncherFailed, ParamArray);
LauncherSessionInfo.Reset();
}
}
void UEditorEngine::CancelPlayingViaLauncher()
{
if (LauncherWorker.IsValid())
{
LauncherWorker->CancelAndWait();
}
}
/**
* Cancel Play using Launcher on error
*
* if the physical device is not authorized to be launched to, we need to pop an error instead of trying to launch
*/
void UEditorEngine::CancelPlayUsingLauncher()
{
FText LaunchingText = LOCTEXT("LauncherTaskInProgressNotificationNotAuthorized", "Cannot launch to this device until this computer is authorized from the device");
FNotificationInfo Info(LaunchingText);
Info.ExpireDuration = 5.0f;
TSharedPtr<SNotificationItem> Notification = FSlateNotificationManager::Get().AddNotification(Info);
if (Notification.IsValid())
{
Notification->SetCompletionState(SNotificationItem::CS_Fail);
Notification->ExpireAndFadeout();
}
}
/* 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())
{
const ULevelEditorPlaySettings* EditorPlaySettings = GetDefault<ULevelEditorPlaySettings>();
if (EditorPlaySettings->EnablePIEEnterAndExitSounds)
{
if (CompletionState == SNotificationItem::CS_Fail)
{
GEditor->PlayEditorSound(TEXT("/Engine/EditorSounds/Notifications/CompileFailed_Cue.CompileFailed_Cue"));
}
else if (CompletionState == SNotificationItem::CS_Success)
{
GEditor->PlayEditorSound(TEXT("/Engine/EditorSounds/Notifications/CompileSuccess_Cue.CompileSuccess_Cue"));
}
}
TSharedPtr<SNotificationItem> NotificationItem = NotificationItemPtr.Pin();
NotificationItem->SetText(Text);
NotificationItem->SetCompletionState(CompletionState);
if (CompletionState == SNotificationItem::CS_Success || CompletionState == SNotificationItem::CS_Fail)
{
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)
{
if (!LauncherSessionInfo.IsSet())
{
UE_LOG(LogPlayLevel, Warning, TEXT("HandleStageStarted called for Stage: %s but the session was canceled, ignoring."), *InStage);
return;
}
bool bSetNotification = true;
FFormatNamedArguments Arguments;
FText NotificationText;
if (InStage.Contains(TEXT("Cooking")) || InStage.Contains(TEXT("Cook Task")))
{
FString PlatformName = LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@")));
PlatformName = PlatformInfo::FindPlatformInfo(*PlatformName)->VanillaInfo->Name.ToString();
Arguments.Add(TEXT("PlatformName"), FText::FromString(PlatformName));
NotificationText = FText::Format(LOCTEXT("LauncherTaskProcessingNotification", "Processing Assets for {PlatformName}..."), Arguments);
}
else if (InStage.Contains(TEXT("Build Task")))
{
FString PlatformName = LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@")));
PlatformName = PlatformInfo::FindPlatformInfo(*PlatformName)->VanillaInfo->Name.ToString();
Arguments.Add(TEXT("PlatformName"), FText::FromString(PlatformName));
if (!LauncherSessionInfo->bPlayUsingLauncherBuild)
{
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(LauncherSessionInfo->PlayUsingLauncherDeviceName));
if (LauncherSessionInfo->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::GetProjectName()));
Arguments.Add(TEXT("DeviceName"), FText::FromString(LauncherSessionInfo->PlayUsingLauncherDeviceName));
if (LauncherSessionInfo->PlayUsingLauncherDeviceName.Len() == 0)
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskRunNotificationNoDevice", "Running {GameName}..."), Arguments);
}
else
{
NotificationText = FText::Format(LOCTEXT("LauncherTaskRunNotification", "Running {GameName} on {DeviceName}..."), Arguments);
}
}
else
{
bSetNotification = false;
}
if (bSetNotification)
{
TGraphTask<FLauncherNotificationTask>::CreateTask().ConstructAndDispatchWhenReady(
NotificationItemPtr,
SNotificationItem::CS_Pending,
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" ), LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.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" ), LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ParamArray);
LauncherSessionInfo.Reset();
}
void UEditorEngine::HandleLaunchCompleted(bool Succeeded, double TotalTime, int32 ErrorCode, bool bHasCode, TWeakPtr<SNotificationItem> NotificationItemPtr)
{
const FString DummyIOSDeviceName(FString::Printf(TEXT("All_iOS_On_%s"), FPlatformProcess::ComputerName()));
const FString DummyTVOSDeviceName(FString::Printf(TEXT("All_tvOS_On_%s"), FPlatformProcess::ComputerName()));
if (Succeeded)
{
FText CompletionMsg;
if ((LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))) == TEXT("IOS") && LauncherSessionInfo->PlayUsingLauncherDeviceName.Contains(DummyIOSDeviceName)) ||
(LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))) == TEXT("TVOS") && LauncherSessionInfo->PlayUsingLauncherDeviceName.Contains(DummyTVOSDeviceName)))
{
CompletionMsg = LOCTEXT("DeploymentTaskCompleted", "Deployment complete! Open the app on your device to launch.");
}
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" ), LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ParamArray);
UE_LOG(LogPlayLevel, Log, TEXT("Launch On Completed. Time: %f"), TotalTime);
bUATSuccessfullyCompiledOnce = true;
}
else
{
FText CompletionMsg;
if ((LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))) == TEXT("IOS") && LauncherSessionInfo->PlayUsingLauncherDeviceName.Contains(DummyIOSDeviceName)) ||
(LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))) == TEXT("TVOS") && LauncherSessionInfo->PlayUsingLauncherDeviceName.Contains(DummyTVOSDeviceName)))
{
CompletionMsg = LOCTEXT("DeploymentTaskFailed", "Deployment failed!");
}
else
{
CompletionMsg = LOCTEXT("LauncherTaskFailed", "Launch failed!");
}
AsyncTask(ENamedThreads::GameThread, [=]
{
FMessageLog MessageLog("PackagingResults");
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" ), LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@"))), bHasCode, ErrorCode, ParamArray);
}
LauncherSessionInfo.Reset();
}
FString UEditorEngine::GetPlayOnTargetPlatformName() const
{
return LastPlayUsingLauncherDeviceId.Left(LastPlayUsingLauncherDeviceId.Find(TEXT("@")));
}
#undef LOCTEXT_NAMESPACE // "PlayLevel"