384 lines
16 KiB
C++
384 lines
16 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Editor/EditorEngine.h"
|
|
#include "PlayLevel.h"
|
|
#include "HeadMountedDisplayTypes.h"
|
|
#include "Editor.h"
|
|
#include "GameFramework/GameModeBase.h"
|
|
#include "HAL/PlatformApplicationMisc.h"
|
|
#include "Settings/LevelEditorPlaySettings.h"
|
|
#include "DataDrivenShaderPlatformInfo.h"
|
|
#include "Net/OnlineEngineInterface.h"
|
|
#include "Misc/StringBuilder.h"
|
|
|
|
void UEditorEngine::StartPlayInNewProcessSession(FRequestPlaySessionParams& InRequestParams)
|
|
{
|
|
check(InRequestParams.SessionDestination == EPlaySessionDestinationType::NewProcess);
|
|
|
|
EPlayNetMode NetMode;
|
|
InRequestParams.EditorPlaySettings->GetPlayNetMode(NetMode);
|
|
|
|
// Standalone requires no server, and ListenServer doesn't require a separate server.
|
|
const bool bNetModeRequiresSeparateServer = NetMode == EPlayNetMode::PIE_Client;
|
|
const bool bLaunchExtraServerAnyways = InRequestParams.EditorPlaySettings->bLaunchSeparateServer;
|
|
const bool bNeedsServer = bNetModeRequiresSeparateServer || bLaunchExtraServerAnyways;
|
|
|
|
bool bServerWasLaunched = false;
|
|
|
|
if (bNeedsServer)
|
|
{
|
|
const bool bIsDedicatedServer = true;
|
|
LaunchNewProcess(InRequestParams, 0, EPlayNetMode::PIE_ListenServer, bIsDedicatedServer);
|
|
|
|
bServerWasLaunched = true;
|
|
}
|
|
|
|
int32 NumClients;
|
|
InRequestParams.EditorPlaySettings->GetPlayNumberOfClients(NumClients);
|
|
|
|
// If the have a net mode that requires a server but they didn't create (or couldn't create due to single-process
|
|
// limitations) a dedicated one, then we launch an extra world context acting as a server in-process.
|
|
int32 NumRequestedInstances = FMath::Max(NumClients, 1);
|
|
for (int32 InstanceIndex = 0; InstanceIndex < NumRequestedInstances; InstanceIndex++)
|
|
{
|
|
EPlayNetMode LocalNetMode = NetMode;
|
|
|
|
// If they want to launch a Listen Server and have multiple clients, the subsequent clients need to be
|
|
// treated as Clients so they connect to the listen server instead of launching multiple Listen Servers.
|
|
if (NetMode == EPlayNetMode::PIE_ListenServer && InstanceIndex > 0)
|
|
{
|
|
LocalNetMode = EPlayNetMode::PIE_Client;
|
|
}
|
|
|
|
// Dedicated servers should have been launched above, so this is only clients + listen servers.
|
|
const bool bIsDedicatedServer = false;
|
|
|
|
LaunchNewProcess(InRequestParams, InstanceIndex, LocalNetMode, bIsDedicatedServer);
|
|
}
|
|
|
|
// Now that we've launched the new process, we'll cancel the request so that the UI lets us go into PIE.
|
|
// This doesn't clear our tracked sessions, so next time PIE is started it will close any standalone instances.
|
|
CancelRequestPlaySession();
|
|
}
|
|
|
|
void UEditorEngine::LaunchNewProcess(const FRequestPlaySessionParams& InParams, const int32 InInstanceNum, EPlayNetMode NetMode, bool bIsDedicatedServer)
|
|
{
|
|
// All dedicated servers should be considered hosts as well.
|
|
if (bIsDedicatedServer)
|
|
{
|
|
NetMode = EPlayNetMode::PIE_ListenServer;
|
|
}
|
|
|
|
// Apply various launch arguments based on their settings.
|
|
FString CommandLine;
|
|
FString UnrealURLParams;
|
|
|
|
if (InParams.GameModeOverride)
|
|
{
|
|
UnrealURLParams += FString::Printf(TEXT("?game=%s"), *InParams.GameModeOverride->GetPathName());
|
|
}
|
|
|
|
if (bIsDedicatedServer)
|
|
{
|
|
CommandLine += TEXT("-server -log");
|
|
}
|
|
else if (NetMode == EPlayNetMode::PIE_ListenServer)
|
|
{
|
|
UnrealURLParams += TEXT("?Listen");
|
|
}
|
|
|
|
if (NetMode == EPlayNetMode::PIE_ListenServer)
|
|
{
|
|
// Add any additional url parameters the user might have specified, for both listen and dedicated servers
|
|
FString AdditionalServerGameOptions;
|
|
InParams.EditorPlaySettings->GetAdditionalServerGameOptions(AdditionalServerGameOptions);
|
|
|
|
if (AdditionalServerGameOptions.Len() > 0)
|
|
{
|
|
UnrealURLParams += AdditionalServerGameOptions;
|
|
}
|
|
}
|
|
|
|
// Allow loading specific GameUserSettings from the ini which differ per-process.
|
|
FString GameUserSettingsOverride = GGameUserSettingsIni.Replace(TEXT("GameUserSettings"), *FString::Printf(TEXT("PIEGameUserSettings%d"), InInstanceNum));
|
|
|
|
// Construct parms:
|
|
// -Override GameUserSettings.ini
|
|
// -Allow saving of config files (since we are giving them an override INI)
|
|
// -Force the OSS (Steam is the only thing that implements this right now) to use passthrough sockets instead of connecting to the platform session int.
|
|
CommandLine += FString::Printf(TEXT(" GameUserSettingsINI=\"%s\" -MultiprocessSaveConfig -forcepassthrough"), *GameUserSettingsOverride);
|
|
|
|
if (bIsDedicatedServer)
|
|
{
|
|
// Allow server specific launch parameters. Only works with separate process standalone servers.
|
|
CommandLine += FString::Printf(TEXT(" %s"), *InParams.EditorPlaySettings->AdditionalServerLaunchParameters);
|
|
}
|
|
|
|
// If they're not a host, configure the URL Params to connect to the server (instead of a specifying a map later)
|
|
if (NetMode == EPlayNetMode::PIE_Client)
|
|
{
|
|
FString ServerIP = TEXT("127.0.0.1");
|
|
uint16 ServerPort;
|
|
InParams.EditorPlaySettings->GetServerPort(ServerPort);
|
|
UnrealURLParams += FString::Printf(TEXT(" %s:%hu"), *ServerIP, ServerPort);
|
|
}
|
|
|
|
// Add Messaging and a SessionName for the Unreal Front End
|
|
CommandLine += TEXT(" -messaging -SessionName=\"Play in Standalone Game\"");
|
|
|
|
// Allow overriding the localization for testing other languages.
|
|
const FString& PreviewGameLanguage = FTextLocalizationManager::Get().GetConfiguredGameLocalizationPreviewLanguage();
|
|
if (!PreviewGameLanguage.IsEmpty())
|
|
{
|
|
CommandLine += TEXT(" -culture=");
|
|
CommandLine += PreviewGameLanguage;
|
|
}
|
|
|
|
if (InParams.SessionPreviewTypeOverride.Get(EPlaySessionPreviewType::NoPreview) == EPlaySessionPreviewType::MobilePreview)
|
|
{
|
|
// Allow targeting a specific Mobile Device.
|
|
if (InParams.MobilePreviewTargetDevice.IsSet())
|
|
{
|
|
CommandLine += FString::Printf(TEXT(" -MobileTargetDevice=\"%s\""), *InParams.MobilePreviewTargetDevice.GetValue());
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, we'll fall back to ES31 emulation.
|
|
CommandLine += TEXT(" -featureleveles31");
|
|
}
|
|
|
|
// If we're currently running in OpenGL mode, pass that onto our newly spawned processes.
|
|
if (IsOpenGLPlatform(GShaderPlatformForFeatureLevel[GMaxRHIFeatureLevel]))
|
|
{
|
|
CommandLine += TEXT(" -opengl");
|
|
}
|
|
|
|
// Fake touch events since we're on a desktop and not a mobile device.
|
|
CommandLine += TEXT(" -faketouches");
|
|
|
|
// Ensure the executable writes out a differently named config file to avoid multiple instances overwriting each other.
|
|
// ToDo: Should this be on all multi-client launches?
|
|
CommandLine += TEXT(" -MultiprocessSaveConfig");
|
|
}
|
|
|
|
// In order for the previewer to adjust its safe zone according to the device profile specified in the editor play settings,
|
|
// we need to pass the PIESafeZoneOverride's values as command line variables to the new process that we are about to launch.
|
|
FMargin PIESafeZoneOverride = InParams.EditorPlaySettings->PIESafeZoneOverride;
|
|
if (!PIESafeZoneOverride.GetDesiredSize().IsZero())
|
|
{
|
|
CommandLine += FString::Printf(TEXT(" -SafeZonePaddingLeft=%f -SafeZonePaddingRight=%f -SafeZonePaddingTop=%f -SafeZonePaddingBottom=%f"),
|
|
PIESafeZoneOverride.Left,
|
|
PIESafeZoneOverride.Right,
|
|
PIESafeZoneOverride.Top,
|
|
PIESafeZoneOverride.Bottom
|
|
);
|
|
}
|
|
|
|
if (InParams.SessionPreviewTypeOverride.Get(EPlaySessionPreviewType::NoPreview) == EPlaySessionPreviewType::VulkanPreview)
|
|
{
|
|
// Vulkan only supports a sub-set
|
|
CommandLine += TEXT(" -vulkan -faketouches -featureleveles31");
|
|
}
|
|
|
|
// VRPreview handling
|
|
if (InParams.SessionPreviewTypeOverride.Get(EPlaySessionPreviewType::NoPreview) == EPlaySessionPreviewType::VRPreview)
|
|
{
|
|
if (InParams.EditorPlaySettings->IsHMDForPrimaryProcessOnly())
|
|
{
|
|
// If they're trying to launch a new process (from the editor) in VR, this will fail because the editor
|
|
// owns the HMD resource, so we warn, and then fall back. They will need to use single-process for VR preview.
|
|
CommandLine += TEXT(" -nohmd");
|
|
UE_LOG(LogPlayLevel, Warning, TEXT("Standalone Game VR not supported, please use VR Preview. Launching separate process PIE with -nohmd."));
|
|
}
|
|
else if (InInstanceNum != 0) // PIE instance 0 is normally run in the editor process, so we may not see it here. That instance get the real HMD, so no simulator argument is passed.
|
|
{
|
|
CommandLine += TEXT(" -HMDSimulator");
|
|
UE_LOG(LogPlayLevel, Log, TEXT("Launching separate process PIE with -HMDSimulator. See bOneHeadsetEachProcess editor preference tooltip for more information about this."));
|
|
}
|
|
}
|
|
|
|
// if we had -emulatestereo on the commandline, also pass it to the new process
|
|
if (InParams.EditorPlaySettings->bEmulateStereo || FParse::Param(FCommandLine::Get(), TEXT("emulatestereo")))
|
|
{
|
|
CommandLine += TEXT(" -emulatestereo");
|
|
}
|
|
|
|
// Allow disabling the sound in the new clients.
|
|
if (InParams.EditorPlaySettings->DisableStandaloneSound)
|
|
{
|
|
CommandLine += TEXT(" -nosound");
|
|
}
|
|
|
|
// Allow the user to specify their own additional launch parameters to be set.
|
|
if (InParams.EditorPlaySettings->AdditionalLaunchParameters.Len() > 0)
|
|
{
|
|
CommandLine += FString::Printf(TEXT(" %s"), *InParams.EditorPlaySettings->AdditionalLaunchParameters);
|
|
}
|
|
|
|
// The Play in Editor request may have had its own parameters as well.
|
|
if (InParams.AdditionalStandaloneCommandLineParameters.IsSet())
|
|
{
|
|
CommandLine += FString::Printf(TEXT(" %s"), *InParams.AdditionalStandaloneCommandLineParameters.GetValue());
|
|
}
|
|
|
|
// Allow servers to override which port they are launched on.
|
|
if (NetMode == EPlayNetMode::PIE_ListenServer)
|
|
{
|
|
uint16 ServerPort;
|
|
InParams.EditorPlaySettings->GetServerPort(ServerPort);
|
|
|
|
CommandLine += FString::Printf(TEXT(" -port=%hu"), ServerPort);
|
|
}
|
|
|
|
// Allow emulating adverse network conditions.
|
|
if (InParams.EditorPlaySettings->IsNetworkEmulationEnabled())
|
|
{
|
|
NetworkEmulationTarget CurrentTarget = (NetMode == EPlayNetMode::PIE_ListenServer) ? NetworkEmulationTarget::Server : NetworkEmulationTarget::Client;
|
|
if (InParams.EditorPlaySettings->NetworkEmulationSettings.IsEmulationEnabledForTarget(CurrentTarget))
|
|
{
|
|
CommandLine += InParams.EditorPlaySettings->NetworkEmulationSettings.BuildPacketSettingsForCmdLine();
|
|
}
|
|
}
|
|
|
|
// Decide if fullscreen or windowed based on what is specified in the params
|
|
if (!CommandLine.Contains(TEXT("-fullscreen")) && !CommandLine.Contains(TEXT("-windowed")))
|
|
{
|
|
// Nothing specified fallback to window otherwise keep what is specified
|
|
CommandLine += TEXT(" -windowed");
|
|
}
|
|
|
|
if (!bIsDedicatedServer)
|
|
{
|
|
// Get desktop metrics
|
|
FDisplayMetrics DisplayMetrics;
|
|
FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics);
|
|
|
|
// We don't use GetWindowSizeAndPositionForInstanceIndex here because that is for PIE windows and uses a separate system for saving window positions,
|
|
// so we'll just respect the settings object for viewport size. If you're in standlone (non multiplayer) we respect viewport resolution, while
|
|
// networked modes respect the multiplayer version.
|
|
FIntPoint WindowSize;
|
|
if (NetMode == EPlayNetMode::PIE_Standalone)
|
|
{
|
|
WindowSize.X = InParams.EditorPlaySettings->NewWindowWidth;
|
|
WindowSize.Y = InParams.EditorPlaySettings->NewWindowHeight;
|
|
}
|
|
else
|
|
{
|
|
InParams.EditorPlaySettings->GetClientWindowSize(WindowSize);
|
|
}
|
|
|
|
// If not center window nor NewWindowPosition is FIntPoint::NoneValue (-1,-1)
|
|
if (!InParams.EditorPlaySettings->CenterNewWindow && InParams.EditorPlaySettings->NewWindowPosition != FIntPoint::NoneValue)
|
|
{
|
|
FIntPoint WindowPosition = InParams.EditorPlaySettings->NewWindowPosition;
|
|
|
|
WindowPosition.X += FMath::Max(InInstanceNum - 1, 0) * WindowSize.X;
|
|
WindowPosition.Y += static_cast<int32>(SWindowDefs::DefaultTitleBarSize * FPlatformApplicationMisc::GetDPIScaleFactorAtPoint(0, 0));
|
|
|
|
// If they don't want to center the new window, we add a specific location. This will get saved to user settings
|
|
// via SAVEWINPOS and not end up reflected in our PlayInEditor settings.
|
|
CommandLine += FString::Printf(TEXT(" -WinX=%d -WinY=%d SAVEWINPOS=1"), WindowPosition.X, WindowPosition.Y);
|
|
}
|
|
|
|
// If the user didn't specify a resolution in the settings, default to full resolution.
|
|
if (WindowSize.X <= 0)
|
|
{
|
|
WindowSize.X = DisplayMetrics.PrimaryDisplayWidth;
|
|
}
|
|
|
|
if (WindowSize.Y <= 0)
|
|
{
|
|
WindowSize.Y = DisplayMetrics.PrimaryDisplayHeight;
|
|
}
|
|
|
|
CommandLine += FString::Printf(TEXT(" -ResX=%d -ResY=%d"), WindowSize.X, WindowSize.Y);
|
|
|
|
// If they request a size larger than their display, add -ForceRes to prevent the engine
|
|
// from automatically resizing the new instance to fit within the bounds of the screen.
|
|
if ((WindowSize.X <= 0 || WindowSize.X > DisplayMetrics.PrimaryDisplayWidth) || (WindowSize.Y <= 0 || WindowSize.Y > DisplayMetrics.PrimaryDisplayHeight))
|
|
{
|
|
CommandLine += TEXT(" -ForceRes");
|
|
}
|
|
|
|
// Pass through `-AUTH_LOGIN` etc arguments for external processes using the PIE credentials, if applicable.
|
|
const int32 NumLogins = UOnlineEngineInterface::Get()->GetNumPIELogins();
|
|
if (SupportsOnlinePIE() && InInstanceNum >= 0 && InInstanceNum < NumLogins)
|
|
{
|
|
CommandLine += UOnlineEngineInterface::Get()->GetPIELoginCommandLineArgs(InInstanceNum);
|
|
}
|
|
}
|
|
|
|
FString GameNameOrProjectFile;
|
|
if (FPaths::IsProjectFilePathSet())
|
|
{
|
|
GameNameOrProjectFile = FString::Printf(TEXT("\"%s\""), *FPaths::GetProjectFilePath());
|
|
}
|
|
else
|
|
{
|
|
GameNameOrProjectFile = FApp::GetProjectName();
|
|
}
|
|
|
|
// Build the final command line
|
|
FWorldContext & EditorContext = GetEditorWorldContext();
|
|
FString MapName = EditorContext.World()->GetOutermost()->GetName();
|
|
|
|
// Launch a new process.
|
|
TMap<FString, FStringFormatArg> NamedArguments;
|
|
NamedArguments.Add(TEXT("GameNameOrProjectFile"), GameNameOrProjectFile);
|
|
if (NetMode != EPlayNetMode::PIE_Client)
|
|
{
|
|
// If we're not a client, build a PlayWorld URL to load to.
|
|
if (NetMode != EPlayNetMode::PIE_Standalone)
|
|
{
|
|
FString ServerMapNameOverride;
|
|
InParams.EditorPlaySettings->GetServerMapNameOverride(ServerMapNameOverride);
|
|
|
|
// Allow the user to override which map the server should load.
|
|
if (ServerMapNameOverride.Len() > 0)
|
|
{
|
|
UE_LOG(LogPlayLevel, Log, TEXT("Map Override specified in configuration, using %s instead of current map (%s)"), *ServerMapNameOverride, *MapName);
|
|
MapName = ServerMapNameOverride;
|
|
}
|
|
}
|
|
|
|
NamedArguments.Add(TEXT("PlayWorldURL"), BuildPlayWorldURL(*MapName, false, UnrealURLParams));
|
|
}
|
|
else
|
|
{
|
|
// Otherwise hosts just connect and accept whatever the server's settings are.
|
|
NamedArguments.Add(TEXT("PlayWorldURL"), UnrealURLParams);
|
|
}
|
|
TStringBuilder<64> SubprocessCommandLine;
|
|
ECommandLineArgumentFlags CommandLineArgFlags = ECommandLineArgumentFlags::ServerContext;
|
|
if (!bIsDedicatedServer)
|
|
{
|
|
CommandLineArgFlags = ECommandLineArgumentFlags::ClientContext;
|
|
}
|
|
FCommandLine::BuildSubprocessCommandLine(CommandLineArgFlags, false /*bOnlyInherited*/, SubprocessCommandLine);
|
|
NamedArguments.Add(TEXT("SubprocessCommandLine"), *SubprocessCommandLine);
|
|
NamedArguments.Add(TEXT("CommandLineParams"), CommandLine);
|
|
|
|
FString FinalCommandLine = FString::Format(
|
|
TEXT("{GameNameOrProjectFile} {PlayWorldURL} -game -PIEVIACONSOLE {SubprocessCommandLine} {CommandLineParams}"), NamedArguments);
|
|
|
|
// Create a handle that we can keep track of for later killing.
|
|
FPlayOnPCInfo& NewSession = PlayOnLocalPCSessions.Add_GetRef(FPlayOnPCInfo());
|
|
const TCHAR* ExecutablePath = FPlatformProcess::ExecutablePath();
|
|
uint32 ProcessID = 0;
|
|
const bool bLaunchDetatched = true;
|
|
const bool bLaunchMinimized = false;
|
|
const bool bLaunchWindowHidden = false;
|
|
const uint32 PriorityModifier = 0;
|
|
NewSession.ProcessHandle = FPlatformProcess::CreateProc(
|
|
FPlatformProcess::ExecutablePath(), *FinalCommandLine, bLaunchDetatched,
|
|
bLaunchMinimized, bLaunchWindowHidden, &ProcessID,
|
|
PriorityModifier, nullptr, nullptr, nullptr);
|
|
|
|
if (!NewSession.ProcessHandle.IsValid())
|
|
{
|
|
UE_LOG(LogPlayLevel, Error, TEXT("Failed to run a copy of the game on this PC."));
|
|
}
|
|
|
|
// Notify anyone listening that we started a new Standalone Process.
|
|
FEditorDelegates::BeginStandaloneLocalPlay.Broadcast(ProcessID);
|
|
} |