// Copyright Epic Games, Inc. All Rights Reserved. using AutomationTool; using System; using System.Collections.Generic; using System.Linq; using System.IO; using System.Text.RegularExpressions; using EpicGames.Core; using UnrealBuildTool; using UnrealBuildBase; // Here's how we manage commandlines for builds/configs/targets. It has complexity so writing down here for reference // // Involved options: // -client // -server // These denote Client or Server builds - both for executable and cooked data // -target=Foo // Foo can be either say EngineTestClient, UnrealGame, etc. Note that UnrealGame is special for content only projects // This option has historically not been used, instead being calculated from the above options. However with // more projects having multiple game/editor targets, it is useful to explicitly choose one of those targets // -clientconfig=[Debug|Development|Test|Shipping] // Used to choose a build configuration for client _and game_ executables // -serverconfig // Similar but for server executables // // In the case of conflicts (-client and -target=UnrealGame) UAT will fail, so we try to detect that earlier and disallow it // // Additionally, for code-based projects, // However, given how builds are likely to be triggered by the editor, which can let user change the target/config/etc, when // ExecuteBuild handles the {ProjectPackagingSettings} replacement, it will first look at the existing commandline and not // spceify a conflicting mode (commandline overrides PackagingSettings). // // namespace Turnkey.Commands { internal class CreateBuild : TurnkeyCommand { protected override CommandGroup Group => CommandGroup.Builds; const string SettingsSection = "/Script/UnrealEd.ProjectPackagingSettings"; internal static string[] SplitCommandLine(string CommandLine) { // handle -foo, -foo=bar, -foo="bar", and -foo="bar baz" Regex CommandLineRegex = new Regex("(-\\w*\\s+)|(-\\w*=\\w*\\s+)|(-\\w*=\"[^\"]*\"\\s +)"); // split the args and clean up return CommandLineRegex.Split(CommandLine).Select(x => x.Trim()).Where(x => x != "").ToArray(); } string GetArchiveDirectory(string[] CommandOptions, FileReference ProjectFile) { ConfigHierarchy Config = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectFile.Directory, BuildHostPlatform.Current.Platform); string StagingDirectory = Config.GetStructEntryForSetting(SettingsSection, "StagingDirectory", "Path"); if (string.IsNullOrEmpty(StagingDirectory)) { StagingDirectory = Path.Combine(ProjectFile.Directory.FullName, "Saved", "Archives"); } string OutputDir = TurnkeyUtils.ParseParamValue("OutputDir", null, CommandOptions); if (OutputDir == null) { //if (OperatingSystem.IsWindows()) //{ // string ChosenFile = null; // System.Threading.Thread t = new System.Threading.Thread(x => // { // ChosenFile = UnrealWindowsForms.Utils.ShowOpenFileDialogAndReturnFilename("Project Files (*.uproject)|*.uproject"); // }); // t.SetApartmentState(System.Threading.ApartmentState.STA); // t.Start(); // t.Join(); // if (ChosenFile == null) // { // continue; // } //} //else //{ // while (true) { OutputDir = TurnkeyUtils.ReadInput("Enter archival directory:", StagingDirectory); } //} } return OutputDir; } private static string GetPlatformSetting(ConfigHierarchy Config, UnrealTargetPlatform Platform, string PerPlatformKey, string GenericKey, string Default) { string IniPlatformName = ConfigHierarchy.GetIniPlatformName(Platform); string Value = Config.GetMapValueForSetting(SettingsSection, PerPlatformKey, Platform.ToString()); if (Value == null && !string.IsNullOrEmpty(GenericKey)) { Config.GetString(SettingsSection, GenericKey, out Value); } if (Value == null) { Value = Default; } return Value; } // hardcoded list of content-only, non-temp-target, targets static Dictionary UETargets = new Dictionary() { { TargetType.Game, "UnrealGame" }, { TargetType.Client, "UnrealClient" }, { TargetType.Server, "UnrealServer" }, }; public static string MakeCommandLineFromPackagingSettings(UnrealTargetPlatform Platform, FileReference ProjectFile, string ExistingCommandLine) { ConfigHierarchy Config = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectFile.Directory, Platform); string[] ExistingOptions = SplitCommandLine(ExistingCommandLine); bool bBuild = CommandUtils.ParseParam(ExistingOptions, "-build"); bool bCook = CommandUtils.ParseParam(ExistingOptions, "-cook") || CommandUtils.ParseParam(ExistingOptions, "-skipcook"); bool bStage = CommandUtils.ParseParam(ExistingOptions, "-stage") || CommandUtils.ParseParam(ExistingOptions, "-skipstage"); bool bPackage = CommandUtils.ParseParam(ExistingOptions, "-package") || CommandUtils.ParseParam(ExistingOptions, "-skippackage"); bool bArchive = CommandUtils.ParseParam(ExistingOptions, "-archive"); bool bClient = CommandUtils.ParseParam(ExistingOptions, "-client"); bool bServer = CommandUtils.ParseParam(ExistingOptions, "-server"); // for targetname and configuration, the priority is -overridetarget/config (coming from editor), then look in passed in // commandline, and finally look in the ini string IniTargetName = GetPlatformSetting(Config, Platform, "PerPlatformBuildTarget", "BuildTarget", null); string TargetName = CommandUtils.ParseParamValue(ExistingOptions, "-target", IniTargetName); TargetName = TurnkeyUtils.ParseParamValue("OverrideTarget", TargetName, new string[] { }); ProjectProperties Props = ProjectUtils.GetProjectProperties(ProjectFile); List DetectedTargets = Props.Targets; //if (CommandUtils.IsNullOrEmpty(DetectedTargets)) //{ // DetectedTargets = new List // { // new SingleTargetProperties { TargetName = "UnrealGame", Rules = new (new TargetInfo("UnrealGame", Platform, UnrealTargetConfiguration.Development, null)) } // } // ProjectParams Params = new ProjectParams(Command: TurnkeyUtils.CommandUtilHelper, RawProjectPath: ProjectFile, Client: true, DedicatedServer: true); // DetectedTargets = Params.ProjectTargets; //} // figure out the target/targetype to use TargetRules ChosenTarget = null; // first, handle the special case where the editor passed down a UETarget but UBT knows it needs a generated target - swap for the generated one if (Props.bWasGenerated && !string.IsNullOrEmpty(TargetName) && UETargets.Values.Contains(TargetName, StringComparer.InvariantCultureIgnoreCase)) { // generated projects likely only have one game target, so use it if (Props.Targets.Count == 1) { ChosenTarget = Props.Targets[0].Rules; } else { // get the type of the UE target TargetType UETargetType = UETargets.FirstOrDefault(x => x.Value.Equals(TargetName, StringComparison.InvariantCultureIgnoreCase)).Key; ChosenTarget = Props.Targets.FirstOrDefault(x => x.Rules.Type == UETargetType)?.Rules; } } if (ChosenTarget == null && !CommandUtils.IsNullOrEmpty(Props.Targets)) { if (!string.IsNullOrEmpty(TargetName)) { ChosenTarget = DetectedTargets.FirstOrDefault(x => x.TargetName.Equals(TargetName, StringComparison.InvariantCultureIgnoreCase))?.Rules; if (ChosenTarget == null) { throw new AutomationException($"The target specified on the commandline, {TargetName}, could not be found in project {ProjectFile}" + (Props.bWasGenerated ? $" Note that this project requires a temporary target, so {ProjectFile.GetFileNameWithoutAnyExtensions()} is probably the target you want." : "")); } } // if we don't have one now, find one based on params if (ChosenTarget == null) { ChosenTarget = DetectedTargets.FirstOrDefault(x => (bClient && x.Rules.Type == TargetType.Client) || (bServer && x.Rules.Type == TargetType.Server) || (!bClient && !bServer && x.Rules.Type == TargetType.Game))?.Rules; } } // now figure out the final type to use, with specified target, specified flags, and detected targets TargetType FinalTargetType = TargetType.Game; string FinalTargetName = "UnrealGame"; if (ChosenTarget != null) { FinalTargetType = ChosenTarget.Type; FinalTargetName = ChosenTarget.Name; } else if (bClient || TargetName.Equals("UnrealClient", StringComparison.InvariantCultureIgnoreCase)) { FinalTargetType = TargetType.Client; FinalTargetName = "UnrealClient"; } else if (bServer || TargetName.Equals("UnrealServer", StringComparison.InvariantCultureIgnoreCase)) { FinalTargetType = TargetType.Server; FinalTargetName = "UnrealServer"; } // now that we have a target, we can use it to look on the commandline for server or clientconfig string IniBuildConfig = GetPlatformSetting(Config, Platform, "PerPlatformBuildConfig", "BuildConfiguration", "Development"); string BuildConfig = CommandUtils.ParseParamValue(ExistingOptions, FinalTargetType == TargetType.Server ? "serverconfig" : "clientconfig", IniBuildConfig); BuildConfig = TurnkeyUtils.ParseParamValue("OverrideConfiguration", BuildConfig, new string[] { }); BuildConfig = BuildConfig.Replace("PPBC_", ""); string CommandLine = ""; Func GetBool = (string Setting) => { bool bValue; if (Config.GetBool(SettingsSection, Setting, out bValue)) { return bValue; } return false; }; Action AddIfTrue = (string Setting, string ToAdd) => { if (GetBool(Setting)) { CommandLine += $" {ToAdd}"; } }; Action AddIfNotAlready = (string ToAdd) => { string[] Tokens = ToAdd.Split('='); // if we are a -option, look for mach, if we are a -foo= option, look for a -foo= option in the if ((Tokens.Count() == 1 && !ExistingOptions.Contains(Tokens[0], StringComparer.InvariantCultureIgnoreCase)) || (Tokens.Count() == 2 && !ExistingOptions.Any(x => x.StartsWith(Tokens[0] + "=", StringComparison.InvariantCultureIgnoreCase)))) { CommandLine += $" {ToAdd}"; } }; switch (FinalTargetType) { case TargetType.Client: AddIfNotAlready($"-clientconfig={BuildConfig}"); break; case TargetType.Server: AddIfNotAlready("-noclient"); AddIfNotAlready($"-serverconfig={BuildConfig}"); break; case TargetType.Game: AddIfNotAlready($"-clientconfig={BuildConfig}"); break; default: throw new AutomationException($"TypeType {FinalTargetType} is not allowed for packaging at this time"); } AddIfNotAlready($"-target={FinalTargetName}"); if (bBuild) { AddIfTrue("FullRebuild", "-clean"); } if (bCook) { if (GetBool("bUseIoStore")) { AddIfTrue("bUseZenStore", "-zenstore"); } AddIfTrue("bSkipEditorContent", "-SkipCookingEditorContent"); } if (bCook || bStage) { AddIfTrue("bGenerateChunks", "-manifests"); } if (bCook || bStage || bPackage) { string CookFlavor = GetPlatformSetting(Config, Platform, "PerPlatformTargetFlavorName", null, null); CookFlavor = TurnkeyUtils.ParseParamValue("OverrideFlavor", CookFlavor, new string[] { }); if (!string.IsNullOrEmpty(CookFlavor)) { string IniPlatformName = ConfigHierarchy.GetIniPlatformName(Platform); // remove the Platform_ from the flavor if there, so Android_ASTC would become just Android CookFlavor = CookFlavor.Replace($"{IniPlatformName}_", ""); AddIfNotAlready($"-cookflavor={CookFlavor}"); } } if (bStage) { // zenstore precludes iostore when staging (it has already done the work) if (!GetBool("bUseZenStore")) { AddIfTrue("bUseIoStore", "-iostore -pak"); } AddIfTrue("UsePakFile", "-pak"); // more complex settings below if (Platform == UnrealTargetPlatform.Win64) { AddIfTrue("IncludePrerequisites", "-prereqs"); //string AppLocalDirectory = ExecuteBuild.GetStructEntryForSetting(Config, SettingsSection, "ApplocalPrerequisitesDirectory", "Path"); //if (!string.IsNullOrEmpty(AppLocalDirectory)) //{ // CommandLine += $" -applocaldirectory=\"{AppLocalDirectory}"; //} //else //{ // AddIfTrue("IncludeAppLocalPrerequisites", $" -applocaldirectory=\"$(EngineDir)/Binaries/ThirdParty/AppLocalDependencies\""); //} } DataDrivenPlatformInfo.ConfigDataDrivenPlatformInfo DDPI = DataDrivenPlatformInfo.GetDataDrivenInfoForPlatform(Platform); if (DDPI.bCanUseCrashReporter) { AddIfTrue("IncludeCrashReporter", "-CrashReporter"); } bool bBuildHttpChunkInstallData; Config.GetBool(SettingsSection, "bBuildHttpChunkInstallData", out bBuildHttpChunkInstallData); if (bBuildHttpChunkInstallData) { string HttpChunkInstallDataDirectory = Config.GetStructEntryForSetting(SettingsSection, "HttpChunkInstallDataDirectory", "Path"); string HttpChunkInstallDataVersion; Config.GetString(SettingsSection, "HttpChunkInstallDataVersion", out HttpChunkInstallDataVersion); CommandLine += $" -manifests -createchunkinstall -chunkinstalldirectory=\"{HttpChunkInstallDataDirectory}\" -chunkinstallversion={HttpChunkInstallDataVersion}"; } } if (bStage || bPackage) { AddIfTrue("ForDistribution", "-distribution"); } return CommandLine; } static string MakeCommandLineInteractively(List Platforms, FileReference ProjectFile, string ExistingCommandLine, bool bBuild, bool bCook, bool bStage, bool bPackage, bool bArchive) { string CommandLine = ""; if (bBuild || bStage || bPackage) { List Configs = new List { "Debug", "Development", "Test", "Shipping" }; int BinaryConfigChoice = TurnkeyUtils.ReadInputInt("Select the compiled game configuration:", Configs, false, 1); string BuildConfig = Configs[BinaryConfigChoice]; //switch (TargetType) //{ // case TargetType.Client: CommandLine += $" -client -clientconfig={BuildConfig}"; break; // case TargetType.Server: CommandLine += $" -server -noclient -serverconfig={BuildConfig}"; break; // case TargetType.Game: CommandLine += $" -clientconfig={BuildConfig}"; break; // default: throw new AutomationException($"TypeType {TargetType} is not allowed for packaging at this time"); //} } if (bBuild) { if (TurnkeyUtils.GetUserConfirmation("Would you like to perform a full rebuild of all programs? [-clean]", false)) { CommandLine += " -clean"; } } if (bCook) { if (TurnkeyUtils.GetUserConfirmation("Would you like to skip cooking editor content? [-SkipCookingEditorContent]", false)) { CommandLine += " -SkipCookingEditorContent"; } } if (bStage || bCook) { if (TurnkeyUtils.GetUserConfirmation("Would you like to split your data up into chunks? This requires a UAssetManager (???) subclass to manage chunk assignments [-manifests]", false)) { CommandLine += " -manifests"; // if (TurnkeyUtils.GetUserConfirmation("Would you like to generate Http chunk install data? [-createchunkinstall]", false)) // { // ConfigHierarchy Config = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectFile.Directory, BuildHostPlatform.Current.Platform); // CommandLine += " -createchunkinstall"; // TurnkeyUtils.ReadInput("Entier the HttpChunkInstallDirectory") //// BuildCookRunParams += FString::Printf(TEXT(" -manifests -createchunkinstall -chunkinstalldirectory=\"%s\" -chunkinstallversion=%s"), *(PackagingSettings->HttpChunkInstallDataDirectory.Path), *(PackagingSettings->HttpChunkInstallDataVersion)); // } } int ContainerModeChoice = TurnkeyUtils.ReadInputInt("How would you like your files collected?", new List { "Optimized containers when staging [-iostore -pak]", "[Experimental] Optimized containers when cooking [-zenstore -pak]", "Left as fully loose files"}, false, 0); switch (ContainerModeChoice) { case 0: CommandLine += " -iostore -pak"; break; case 1: CommandLine += " -zenstore -pak"; break; case 2: CommandLine += ""; break; } } if (bStage) { bool bCanUseCrashReporter = false; foreach (UnrealTargetPlatform Platform in Platforms) { DataDrivenPlatformInfo.ConfigDataDrivenPlatformInfo DDPI = DataDrivenPlatformInfo.GetDataDrivenInfoForPlatform(Platform); bCanUseCrashReporter = bCanUseCrashReporter || DDPI.bCanUseCrashReporter; } if (bCanUseCrashReporter) { if (TurnkeyUtils.GetUserConfirmation("Would you like to include the crash reporter? [-CrashReporter]", true)) { CommandLine += " -CrashReporter"; } } if (Platforms.Contains(UnrealTargetPlatform.Win64)) { if (TurnkeyUtils.GetUserConfirmation("Would you like to include the Windows Prerequisites installer? [-prereqs]", false)) { CommandLine += " -prereqs"; } //int ContainerModeChoice = TurnkeyUtils.ReadInputInt("How would you like your files collected?", new List // { "Optimized containers when staging [-iostore -pak]", "[Experimental] Optimized containers when cooking [-zenstore -pak]", "Left as fully loose files"}, false, 0); } } if (bStage || bPackage) { if (TurnkeyUtils.GetUserConfirmation("Would you create build appropriate for distribution to end users? [-distribution]", false)) { CommandLine += " -distribution"; } } return CommandLine; } const string DividerLine = "================================================\n"; protected override void Execute(string[] CommandOptions) { int BuildChoice = TurnkeyUtils.ReadInputInt("What kind of build would you like to create?", new List { "One-off build to execute now", "Save a build to project settings to execute in Editor or Turnkey" }, true, 1); if (BuildChoice == 0) { return; } bool bMakingSavedBuild = BuildChoice == 2; int UserMode = TurnkeyUtils.ReadInputInt("Choose an interactive mode to use:", new List { "Simple Mode: Running a game (locally, remote device, etc)", "Simple Mode: Making a packaged up build (install later, share with others, etc)", "Advanced mode: Walk through all questions" }, true, 2); if (UserMode == 0) { return; } bool bAskForDevice = UserMode == 1 || UserMode == 3; bool bAskForPackage = UserMode == 2 || UserMode == 3; // get the project, always needed FileReference ProjectFile = TurnkeyUtils.GetProjectFromCommandLineOrUser(CommandOptions); // we need a platform to execute List Platforms = null; FileReference DefaultConfig = null; if (bMakingSavedBuild) { Platforms = new List { HostPlatform.Platform }; DefaultConfig = ConfigCache.GetDefaultConfigFileReference(ConfigHierarchyType.Game, ProjectFile.Directory); if (!FileReference.Exists(DefaultConfig)) { TurnkeyUtils.Log($"Unable to find DefaultGame.ini for {ProjectFile.GetFileNameWithoutExtension()}. We will continue, but just print out the info at the end."); DefaultConfig = null; } else { // make sure the file is writable while (DefaultConfig != null && File.GetAttributes(DefaultConfig.FullName).HasFlag(FileAttributes.ReadOnly)) { string ReadOnlyPrompt = "The project's DefaultGame.ini is read-only, unable to save. What would you like to do?"; List Options = new List { "Try again", "Continue anyway, printout info at the end", "Make file writeable" }; if (CommandUtils.P4Enabled) { Options.Add("Checkout with Perforce"); } else { ReadOnlyPrompt += "\n(Try re-running UAT with -p4 to enable Perforce to allow Turnkey to checkout for you)"; } int Choice = TurnkeyUtils.ReadInputInt(ReadOnlyPrompt, Options, true, 2); if (Choice == 0) { return; } else if (Choice == 2) { DefaultConfig = null; break; } else if (Choice == 3) { // clear read-only flag File.SetAttributes(DefaultConfig.FullName, File.GetAttributes(DefaultConfig.FullName) & ~FileAttributes.ReadOnly); } else if (Choice == 4) { TurnkeyUtils.Log("Attempting to create a p4 changelist and check out DefaultGame.ini..."); try { int CL = CommandUtils.P4.CreateChange(CommandUtils.P4Env.Client, $"[AUTO-GENERATED] - Adding a custom project build to DefaultGame.ini"); CommandUtils.P4.Edit(CL, CommandUtils.MakePathSafeToUseWithCommandLine(DefaultConfig.FullName)); TurnkeyUtils.Log($"Created changelist {CL}. Use P4 to check in when ready!"); } catch (Exception Ex) { TurnkeyUtils.Log($"P4 error: {Ex.Message}"); } } } } } else { Platforms = TurnkeyUtils.GetPlatformsFromCommandLineOrUser(CommandOptions, null); } if (Platforms.Count == 0 || ProjectFile == null) { return; } //if (TurnkeyUtils.ParseParam("testconfig", CommandOptions)) //{ // ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.SetValue, "SectionTestA", "InsertedValue", "True"); // ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.SetValue, "SectionTestA", "Foo", "SetValue"); // ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.AddArrayEntry, "SectionTestB", "Builds", "(Blah=123123)"); // ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.SetValue, "SectionTest2", "InsertedValue", "True"); // ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.AddArrayEntry, "SectionTest1", "Builds", "(Blah=123123)"); // ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.SetValue, "SectionTest1", "Foo2", "SetValue2"); // return; //} bool bBuild = false, bCook = false, bStage = false, bDeploy = false, bRun = false, bPackage = false, bArchive = false; string TargetType = "Game"; string TargetName = null; // string BuildConfig = "Development"; if (UserMode == 1) { bBuild = true; bCook = true; bStage = true; bDeploy = true; bRun = true; } else if (UserMode == 2) { bBuild = true; bCook = true; bStage = true; bPackage = true; bArchive = TurnkeyUtils.GetUserConfirmation("Would you like to copy the output to an archival location? [-archive]", false); } else { ProjectProperties Props = ProjectUtils.GetProjectProperties(ProjectFile); // int TargetTypeChoice = TurnkeyUtils.ReadInputInt("Select your target type:", new List { "Game (Game + Server)", "Client (No Server)", "Server (No Game)" }, false, 0); // TargetType = TargetTypeChoice == 0 ? "Game" : TargetTypeChoice == 1 ? "Client" : "Server"; int TargetTypeChoice = TurnkeyUtils.ReadInputInt("Select your target:", Props.Targets.Select(x => x.TargetName).ToList(), true, Props.Targets.IndexOf(Props.Targets.First(x => x.Rules.Type == UnrealBuildTool.TargetType.Game)) + 1); if (TargetTypeChoice == 0) { return; } TargetName = TargetType = Props.Targets[TargetTypeChoice - 1].Rules.Type.ToString(); bBuild = TurnkeyUtils.GetUserConfirmation("Would you like to compile necessary programs? [-build]", true); bCook = TurnkeyUtils.GetUserConfirmation("Would you like to cook your project's assets? [-cook]", true); bStage = TurnkeyUtils.GetUserConfirmation("Would you like gather all files into a staging directory? [-stage]", true); bPackage = TurnkeyUtils.GetUserConfirmation("Would you like to generate a final platform-specific package? [-package]", true); bArchive = TurnkeyUtils.GetUserConfirmation("Would you like to copy the output to an archival location? [-archive]", false); bDeploy = TurnkeyUtils.GetUserConfirmation("Would you like to copy the gathered files to a device? [-deploy]", !bPackage); bRun = TurnkeyUtils.GetUserConfirmation("Would you like to run the game? [-run]", bDeploy); } string BCRParams = ""; if (!bMakingSavedBuild) { BCRParams = $"-project=\"{ProjectFile}\""; } BCRParams += (bBuild ? " -build" : "") + (bCook ? " -cook" : " -skipcook") + (bStage ? " -stage" : " -skipstage") + (bPackage ? " -package" : " -skippackage") + (bArchive ? " -archive" : "") + (bDeploy ? " -deploy" : "") + (bRun ? " -run" : "") ; if (bArchive) { // when saving, use a placeholder to ask later BCRParams += " -archivedirectory=" + (bMakingSavedBuild ? "{BrowseForDir}" : GetArchiveDirectory(CommandOptions, ProjectFile)); } string PPSettings = MakeCommandLineFromPackagingSettings(HostPlatform.Platform, ProjectFile, BCRParams); string PPPrompt = DividerLine + $"The options so far are:\n {BCRParams}\nThe project's packaging settings (for {Platforms[0]}) are:\n {PPSettings}\n" + "How would you like to use the packaging settings? Not all may apply to this build and will be ignored."; if (bMakingSavedBuild) { // PPPrompt += "\nNote that different platforms " } List PPOptions = new List { "Do not add any of the packaging settings", "Interactively choose packaging settings", "Use the packaging settings shown above", }; if (bMakingSavedBuild) { PPOptions.Add("Use what the settings will be when this command is run later"); } int ExtendedParamsChoice = TurnkeyUtils.ReadInputInt(PPPrompt, PPOptions, false, PPOptions.Count - 1); //bool bUsePackagingSettings = TurnkeyUtils.GetUserConfirmation("Would you like to further control the build with your project's packaging settings? [-xyz]", true); string ExtendedParams = ""; if (ExtendedParamsChoice == 1) { ExtendedParams = MakeCommandLineInteractively(Platforms, ProjectFile, BCRParams, bBuild, bCook, bStage, bPackage, bArchive); } else if (ExtendedParamsChoice == 3) { // encode the targettype into the tag for later use ExtendedParams = " {ProjectPackagingSettings}"; } string LastPlatformParams = ""; string AddCmdLine = ""; foreach (UnrealTargetPlatform Platform in Platforms) { string PlatformParams = BCRParams; // if we are making a saved build, then leave the {Platform} tag in the string, otherwise, put in the actual platform if (bMakingSavedBuild) { PlatformParams += " -platform={Platform}"; } else { PlatformParams += " -platform=" + Platform; } if (ExtendedParamsChoice == 2) { ExtendedParams = MakeCommandLineFromPackagingSettings(Platform, ProjectFile, BCRParams + ExtendedParams + PlatformParams); } PlatformParams += ExtendedParams; if (TurnkeyUtils.GetUserConfirmation($"Would you like to add anymore options to the build commandline for {Platform}?\nCurrent commandline is:\n BuildCookRun {PlatformParams}", false)) { LastPlatformParams = TurnkeyUtils.ReadInput("Enter extra options:", LastPlatformParams); PlatformParams += " " + LastPlatformParams; } // the commandline may be saved to a commandline.txt, or we may be running with it drectly (See jira UE-99467 for upcoming work that will affect this) if (bStage || bRun) { AddCmdLine = TurnkeyUtils.ReadInput($"Enter any commandline options you want to add to the game [-addcmdline]", AddCmdLine); if (!string.IsNullOrEmpty(AddCmdLine)) { PlatformParams += $" -addcmdline=\"{AddCmdLine}\""; } } if (bDeploy || bRun) { if (bMakingSavedBuild) { PlatformParams += " -device={DeviceId}"; } else { List Devices = TurnkeyUtils.GetDevicesFromCommandLineOrUser(CommandOptions, Platform); if (Devices == null || Devices.Count == 0) { // we need a device here, if canceled, quit out return; } PlatformParams += " -device=" + string.Join("+", Devices.Select(x => x.Id)); } } TurnkeyUtils.Log($"Final Command:\nRunUAT BuildCookRun {PlatformParams}"); // if (TurnkeyUtils.GetUserConfirmation("Do you want to save this to DefaultGame.ini for use in the editor?", false)) if (bMakingSavedBuild) { TurnkeyUtils.Log(DividerLine + $"We are now ready to save this build to DefaultGame.ini!"); List ExistingBuilds; ConfigHierarchy Config = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectFile.Directory, BuildHostPlatform.Current.Platform); Config.GetArray(SettingsSection, "ProjectCustomBuilds", out ExistingBuilds); string Name; if (ExistingBuilds != null && ExistingBuilds.Count > 0) { TurnkeyUtils.Log($"Here are the existing builds your project already has (and their descrptions):"); foreach (string Build in ExistingBuilds) { string BuildName = ConfigHierarchy.GetStructEntry(Build, "Name", false); string BuildHelp = ConfigHierarchy.GetStructEntry(Build, "HelpText", false); TurnkeyUtils.Log($" {BuildName} - {BuildHelp}"); } Name = TurnkeyUtils.ReadInput("Enter unique name for this build, it must be different than the above names", ""); } else { Name = TurnkeyUtils.ReadInput("Enter a name for this build", ""); } if (!string.IsNullOrEmpty(Name)) { string HelpText = TurnkeyUtils.ReadInput("Enter the help/description for this build", ""); string RestrictedPlatforms = TurnkeyUtils.ReadInput("If you want to restrict this to certain plaforms, enter a comma separated list of platform names:", ""); if (!string.IsNullOrEmpty(RestrictedPlatforms)) { IEnumerable PlatformNames = RestrictedPlatforms.Split(", +".ToCharArray()).ToList() // fixup windows, and wrap in quotes .Select(x => (x.Equals("Win64", StringComparison.InvariantCultureIgnoreCase) ? "Windows" : x)) .Select(x => $"\"{x}\""); RestrictedPlatforms = "(" + string.Join(",", PlatformNames) + ")"; } //// fix up the commandline to be appropriate (remove certain options, like project, platform, and archive directory) //Regex CommandLineRegex = new Regex("(-\\w*\\s+)|(-\\w*=\\w*\\s+)|(-\\w*=\"[^\"]*\"\\s +)"); //// split the args and clean up //List IndividualArgs = CommandLineRegex.Split(PlatformParams).Select(x => x.Trim()).Where(x => x != "").ToList(); //// remove some known bad params //IndividualArgs = IndividualArgs.Where(x => !x.StartsWith("-project=", StringComparison.InvariantCultureIgnoreCase)).ToList(); //// update some params to have variables //for (int Index = 0; Index < IndividualArgs.Count; Index++) //{ // if (IndividualArgs[Index].StartsWith("-platform=", StringComparison.InvariantCultureIgnoreCase)) // { // IndividualArgs[Index] = "-platform={Platform}"; // } // else if (IndividualArgs[Index].StartsWith("-archivedirectory=", StringComparison.InvariantCultureIgnoreCase)) // { // IndividualArgs[Index] = "-archivedirectory={BrowseForDir}"; // } //} // generate struct string for ini file string ValueText = $"(Name=\"{Name}\",HelpText=\"{HelpText}\",SpecificPlatforms={RestrictedPlatforms},BuildCookRunParams=\"{PlatformParams}\")"; // just print out the text to put into the DefaultGame.ini later if (DefaultConfig == null) { TurnkeyUtils.Log($"Since DefaultGame.ini isn't writeable, here is the line you can manually add to DefaultGame.ini in the [{SettingsSection}] section to add the build to editor and Turnkey's ExecuteBuild command:\n {ValueText}"); } else { //string SavedCommandLine = string.Join(" ", IndividualArgs); TurnkeyUtils.Log($"Saving reusable commandline to DefaultGame.ini:\n {PlatformParams}"); TurnkeyUtils.Log($"You can now run the command via RunUAT Turnkey -command=ExecuteBuild -build=\"{Name}\" or run the editor and find it in the Platforms menu."); TurnkeyUtils.Log("Further, you can edit this in the Packaging Settings in the editor as needed"); // add this setting in to the ProjectSettings section ConfigCache.WriteSettingToDefaultConfig(ConfigHierarchyType.Game, ProjectFile.Directory, ConfigCache.ConfigDefaultUpdateType.AddArrayEntry, SettingsSection, "ProjectCustomBuilds", ValueText, Log.Logger); ConfigCache.InvalidateCaches(); } } } else { if (TurnkeyUtils.GetUserConfirmation($"Ready to execute the follwing command?\n BuildCookRun {PlatformParams}", true)) { ExecuteBuild.ExecuteBuildCookRun(PlatformParams); } } } } } }