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

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);
}