// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.IO; using AutomationTool; using UnrealBuildTool; using AutomationScripts; using EpicGames.Core; using UnrealBuildBase; public class ModifyStageContext { // any assets that end up in this list that are already in the DeploymentContext will be removed during Apply public List UFSFilesToStage = new List(); // files in this list will remove the matching cooked package from the DeploymentContext and these uncooked assets will replace them public List FilesToUncook = new List(); // these files will just be staged public List NonUFSFilesToStage = new List(); [ConfigFile(ConfigHierarchyType.Game, "CookedEditorSettings")] public bool bStageShaderDirs = true; [ConfigFile(ConfigHierarchyType.Game, "CookedEditorSettings")] public bool bStageBuildDirs = true; [ConfigFile(ConfigHierarchyType.Game, "CookedEditorSettings")] public bool bStageExtrasDirs = false; [ConfigFile(ConfigHierarchyType.Game, "CookedEditorSettings")] public bool bStagePlatformDirs = true; [ConfigFile(ConfigHierarchyType.Game, "CookedEditorSettings")] public bool bStageRestrictedDirs = false; public DirectoryReference EngineDirectory; public DirectoryReference ProjectDirectory; public string ProjectName; public string IniPlatformName; // when creating a cooked editor against a premade client, this is the sub-directory in the Releases directory to compare against public DirectoryReference ReleaseMetadataLocation = null; // commandline etc helper private BuildCommand Command; public ModifyStageContext(DirectoryReference EngineDirectory, ProjectParams Params, BuildCommand Command) { this.EngineDirectory = EngineDirectory; this.Command = Command; // cache some useful properties ProjectDirectory = Params.RawProjectPath.Directory; ProjectName = Params.RawProjectPath.GetFileNameWithoutAnyExtensions(); IniPlatformName = ConfigHierarchy.GetIniPlatformName(Params.ClientTargetPlatforms[0].Type); ConfigCache.ReadSettings(ProjectDirectory, BuildHostPlatform.Current.Platform, this); // cache info for DLC against a release if (Params.BasedOnReleaseVersionPathOverride != null) { ReleaseMetadataLocation = DirectoryReference.Combine(new DirectoryReference(Params.BasedOnReleaseVersionPathOverride), "Metadata"); } } public void Apply(DeploymentContext SC) { if (ReleaseMetadataLocation != null) { // remove files that we are about to stage that were already in the shipped client RemoveReleasedFiles(SC); } // maps can't be cooked and loaded by the editor, so make sure no cooked ones exist UncookMaps(SC); Dictionary StagedUFSFiles = MimicStageFiles(SC, UFSFilesToStage); Dictionary StagedNonUFSFiles = MimicStageFiles(SC, NonUFSFilesToStage); Dictionary StagedUncookFiles = MimicStageFiles(SC, FilesToUncook); // filter out already-cooked assets foreach (var CookedFile in SC.FilesToStage.UFSFiles) { // remove any of the entries in the "staged" UFSFilesToStage that match already staged files // we don't check extension here because the UFSFilesToStage should only contain .uasset/.umap files, and not .uexp, etc, // and .uasset/.umap files are going to be in SC.FilesToStage StagedUFSFiles.Remove(CookedFile.Key); } // remove already-cooked assets to be replaced with string[] CookedExtensions = { ".uasset", ".umap", ".ubulk", ".uexp" }; foreach (var UncookedFile in StagedUncookFiles) { string PathWithNoExtension = Path.ChangeExtension(UncookedFile.Key.Name, null); // we need to remove cooked files that match the files to Uncook, and there can be several extensions // for each source asset, so remove them all foreach (string CookedExtension in CookedExtensions) { StagedFileReference PathWithExtension = new StagedFileReference(PathWithNoExtension + CookedExtension); SC.FilesToStage.UFSFiles.Remove(PathWithExtension); StagedUFSFiles.Remove(PathWithExtension); } } // stage the filtered UFSFiles SC.StageFiles(StagedFileType.UFS, StagedUFSFiles.Values); // stage the Uncooked files now that any cooked ones are removed from SC SC.StageFiles(StagedFileType.UFS, StagedUncookFiles.Values); // stage the processed NonUFSFiles SC.StageFiles(StagedFileType.NonUFS, StagedNonUFSFiles.Values); // now remove or whitelist restricted files HandleRestrictedFiles(SC, ref SC.FilesToStage.UFSFiles); HandleRestrictedFiles(SC, ref SC.FilesToStage.NonUFSFiles); } #region Private implementation private StagedFileReference MakeRelativeStagedReference(DeploymentContext SC, FileSystemReference Ref) { return MakeRelativeStagedReference(SC, Ref, out _); } private StagedFileReference MakeRelativeStagedReference(DeploymentContext SC, FileSystemReference Ref, out DirectoryReference RootDir) { if (Ref.IsUnderDirectory(ProjectDirectory)) { RootDir = ProjectDirectory; return Project.ApplyDirectoryRemap(SC, new StagedFileReference(ProjectName + "/" + Ref.MakeRelativeTo(ProjectDirectory).Replace('\\', '/'))); } else if (Ref.IsUnderDirectory(EngineDirectory)) { RootDir = EngineDirectory; return Project.ApplyDirectoryRemap(SC, new StagedFileReference( "Engine/" + Ref.MakeRelativeTo(EngineDirectory).Replace('\\', '/'))); } throw new Exception(); } private void RemoveReleasedFiles(DeploymentContext SC) { HashSet ShippedFiles = new HashSet(); Action FindShippedFiles = (string ParamName, string FileNamePortion) => { FileReference UFSManifestFile = Command.ParseOptionalFileReferenceParam(ParamName); if (UFSManifestFile == null) { UFSManifestFile = FileReference.Combine(ReleaseMetadataLocation, $"Manifest_{FileNamePortion}_{SC.StageTargetPlatform.PlatformType}.txt"); } if (FileReference.Exists(UFSManifestFile)) { foreach (string Line in File.ReadAllLines(UFSManifestFile.FullName)) { string[] Tokens = Line.Split("\t".ToCharArray()); if (Tokens?.Length > 1) { ShippedFiles.Add(new StagedFileReference(Tokens[0])); } } } }; FindShippedFiles("ClientUFSManifest", "UFSFiles"); FindShippedFiles("ClientNonUFSManifest", "NonUFSFiles"); FindShippedFiles("ClientDebugManifest", "DebugFiles"); ShippedFiles.RemoveWhere(x => x.HasExtension(".ttf") && !x.Name.Contains("LastResort")); var RemappedNonUFS = NonUFSFilesToStage.Select(x => MakeRelativeStagedReference(SC, x)); UFSFilesToStage.RemoveAll(x => ShippedFiles.Contains(MakeRelativeStagedReference(SC, x))); NonUFSFilesToStage.RemoveAll(x => ShippedFiles.Contains(MakeRelativeStagedReference(SC, x))); } private Dictionary MimicStageFiles(DeploymentContext SC, List SourceFiles) { Dictionary Mapping = new Dictionary(); foreach (FileReference FileRef in new HashSet(SourceFiles)) { DirectoryReference RootDir; StagedFileReference StagedFile = MakeRelativeStagedReference(SC, FileRef, out RootDir); // check if the remapped file is restricted FileReference StagedFileRef = FileReference.Combine(RootDir, StagedFile.Name); if (StagedFileRef.ContainsAnyNames(SC.RestrictedFolderNames, RootDir)) { // Console.WriteLine("{0} is restricted", FileRef.FullName); if (bStageRestrictedDirs) { // if we want to stage restricted files, then we need to whitelist the folder if (!SC.WhitelistDirectories.Contains(StagedFile.Directory)) { Console.WriteLine("Whitelisting dir {0}", StagedFile.Directory.Name); SC.WhitelistDirectories.Add(StagedFile.Directory); } } else { // Console.WriteLine(" .. skipping"); // otherwise, don't return this file in the output continue; } } // add the mapping Mapping.Add(StagedFile, FileRef); } return Mapping; } private void HandleRestrictedFiles(DeploymentContext SC, ref Dictionary Files) { if (bStageRestrictedDirs) { foreach (var Pair in Files) { if (SC.RestrictedFolderNames.Any(x => Pair.Key.ContainsName(x))) { Console.WriteLine("Whitelisting dir {0}", Pair.Value.Directory.FullName); SC.WhitelistDirectories.Add(Pair.Key.Directory); } } } else { // remove entries where any restricted folder names are in the name Files = Files.Where(x => !SC.RestrictedFolderNames.Any(y => x.Key.ContainsName(y))).ToDictionary(x => x.Key, x => x.Value); } //foreach (var Pair in Files) //{ // if (SC.RestrictedFolderNames.Any(x => Pair.Key.ContainsName(x)) // { // // Console.WriteLine("{0} is restricted", FileRef.FullName); // if (bStageRestrictedDirs) // { // // if we want to stage restricted files, then we need to whitelist the folder // if (!SC.WhitelistDirectories.Contains(StagedFile.Directory)) // { // Console.WriteLine("Whitelisting dir {0}", StagedFile.Directory.Name); // SC.WhitelistDirectories.Add(StagedFile.Directory); // } // } // else // { // // Console.WriteLine(" .. skipping"); // // otherwise, don't return this file in the output // continue; // } // } //} } private void UncookMaps(DeploymentContext SC) { FilesToUncook.AddRange(SC.FilesToStage.UFSFiles.Values.Where(x => x.GetExtension() == ".umap")); FilesToUncook.AddRange(UFSFilesToStage.Where(x => x.GetExtension() == ".umap")); } #endregion } public class MakeCookedEditor : BuildCommand { public override void ExecuteBuild() { LogInformation("************************* MakeCookedEditor"); ProjectParams BuildParams = GetParams(); LogInformation("Build? {0}", BuildParams.Build); Project.Build(this, BuildParams); Project.Cook(BuildParams); Project.CopyBuildToStagingDirectory(BuildParams); //this will do packaging if requested, and also symbol upload if requested. Project.Package(BuildParams); Project.Archive(BuildParams); PrintRunTime(); Project.Deploy(BuildParams); } protected virtual void StageEngineEditorFiles(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context) { StagePlatformExtensionFiles(Params, SC, Context, Unreal.EngineDirectory); StagePluginFiles(Params, SC, Context, true); // engine shaders if (Context.bStageShaderDirs) { Context.NonUFSFilesToStage.AddRange(DirectoryReference.EnumerateFiles(DirectoryReference.Combine(Unreal.EngineDirectory, "Shaders"), "*", SearchOption.AllDirectories)); GatherTargetDependencies(Params, SC, Context, "ShaderCompileWorker"); } StageIniPathArray(Params, SC, "EngineExtraStageFiles", Unreal.EngineDirectory, Context); Context.FilesToUncook.Add(FileReference.Combine(Context.EngineDirectory, "Content", "EngineMaterials", "DefaultMaterial.uasset")); } protected virtual void StageProjectEditorFiles(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context) { // always stage the main exe, in case DLC mode is on, then it won't by default GatherTargetDependencies(Params, SC, Context, SC.StageExecutables[0]); StagePlatformExtensionFiles(Params, SC, Context, Context.ProjectDirectory); StagePluginFiles(Params, SC, Context, false); // add stripped out editor .ini files back in Context.UFSFilesToStage.AddRange(DirectoryReference.EnumerateFiles(DirectoryReference.Combine(Context.ProjectDirectory, "Config"), "*Editor*", SearchOption.AllDirectories)); StageIniPathArray(Params, SC, "ProjectExtraStageFiles", Context.ProjectDirectory, Context); if (Context.ReleaseMetadataLocation != null) { // we need to remap this file, so stage it directly SC.StageFile(StagedFileType.UFS, FileReference.Combine(Context.ReleaseMetadataLocation, "DevelopmentAssetRegistry.bin"), new StagedFileReference($"{Context.ProjectName}/EditorClientAssetRegistry.bin")); } } protected virtual void StagePluginDirectory(DirectoryReference PluginDir, ModifyStageContext Context) { foreach (DirectoryReference Subdir in DirectoryReference.EnumerateDirectories(PluginDir)) { StagePluginSubdirectory(Subdir, Context); } } protected virtual void StagePluginSubdirectory(DirectoryReference PluginSubdir, ModifyStageContext Context) { string DirNameLower = PluginSubdir.GetDirectoryName().ToLower(); if (DirNameLower == "content" || DirNameLower == "resources" || DirNameLower == "config" || DirNameLower == "scripttemplates") { Context.UFSFilesToStage.AddRange(DirectoryReference.EnumerateFiles(PluginSubdir, "*", SearchOption.AllDirectories)); } if (DirNameLower == "shaders" && Context.bStageShaderDirs) { Context.NonUFSFilesToStage.AddRange(DirectoryReference.EnumerateFiles(PluginSubdir, "*", SearchOption.AllDirectories)); } } protected virtual ModifyStageContext CreateContext(ProjectParams Params) { return new ModifyStageContext(Unreal.EngineDirectory, Params, this); } protected virtual void ModifyParams(ProjectParams BuildParams) { } protected virtual void PreModifyDeploymentContext(ProjectParams Params, DeploymentContext SC) { ModifyStageContext Context = CreateContext(Params); DefaultPreModifyDeploymentContext(Params, SC, Context); Context.Apply(SC); } protected virtual void ModifyDeploymentContext(ProjectParams Params, DeploymentContext SC) { ModifyStageContext Context = CreateContext(Params); DefaultModifyDeploymentContext(Params, SC, Context); Context.Apply(SC); } protected virtual void SetupDLCMode(FileReference ProjectFile, out string DLCName, out string ReleaseVersion, out TargetType Type) { bool bBuildAgainstRelease; ConfigHierarchy GameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectFile.Directory, BuildHostPlatform.Current.Platform); if (GameConfig.GetBool("CookedEditorSettings", "bBuildAgainstRelease", out bBuildAgainstRelease) && bBuildAgainstRelease) { GameConfig.GetString("CookedEditorSettings", "DLCPluginName", out DLCName); GameConfig.GetString("CookedEditorSettings", "ReleaseName", out ReleaseVersion); // if not set, default to gamename if (string.IsNullOrEmpty(ReleaseVersion)) { ReleaseVersion = ProjectFile.GetFileNameWithoutAnyExtensions(); } string TargetTypeString; GameConfig.GetString("CookedEditorSettings", "ReleaseTargetType", out TargetTypeString); Type = (TargetType)Enum.Parse(typeof(TargetType), TargetTypeString); } else { DLCName = null; ReleaseVersion = null; Type = TargetType.Game; } } protected void StagePlatformExtensionFiles(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context, DirectoryReference RootDir) { if (!Context.bStagePlatformDirs) { return; } DirectoryReference[] RootPlatformsFolders = { DirectoryReference.Combine(RootDir, "Platforms"), DirectoryReference.Combine(RootDir, "Restricted", "NotForLicensees", "Platforms"), }; List RootFoldersToStrip = new List { "Source", "Binaries" }; List SubFoldersToStrip = new List { "Source", "Intermediate", "Tests", "Binaries" + Path.DirectorySeparatorChar + HostPlatform.Current.HostEditorPlatform.ToString() }; if (!Context.bStageShaderDirs) { RootFoldersToStrip.Add("Shaders"); } if (!Context.bStageBuildDirs) { RootFoldersToStrip.Add("Build"); } if (!Context.bStageExtrasDirs) { RootFoldersToStrip.Add("Extras"); } foreach (DirectoryReference PlatformsDir in RootPlatformsFolders) { if (!DirectoryReference.Exists(PlatformsDir)) { continue; } foreach (DirectoryReference PlatformDir in DirectoryReference.EnumerateDirectories(PlatformsDir, "*", SearchOption.TopDirectoryOnly)) { foreach (DirectoryReference Subdir in DirectoryReference.EnumerateDirectories(PlatformDir, "*", SearchOption.TopDirectoryOnly)) { // Remvoe some unnecessary folders that can be large List ContextFileList = Context.UFSFilesToStage; if (Subdir.GetDirectoryName() == "Shaders") { ContextFileList = Context.NonUFSFilesToStage; } List FilesToStage = new List(); // if we aren't in a bad subdir, add files if (!RootFoldersToStrip.Contains(Subdir.GetDirectoryName(), StringComparer.InvariantCultureIgnoreCase)) { FilesToStage.AddRange(DirectoryReference.EnumerateFiles(Subdir, "*", SearchOption.AllDirectories)); // now remove files in subdirs we want to skip FilesToStage.RemoveAll(x => x.ContainsAnyNames(SubFoldersToStrip, Subdir)); ContextFileList.AddRange(FilesToStage); } } } } } protected void StagePluginFiles(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context, bool bEnginePlugins) { List ActivePlugins = new List(); foreach (StageTarget Target in SC.StageTargets) { if (Target.Receipt.TargetType == TargetType.Editor) { IEnumerable TargetPlugins = Target.Receipt.RuntimeDependencies.Where(x => x.Path.GetExtension().ToLower() == ".uplugin"); // grab just engine plugins, or non-engine plugins depending TargetPlugins = TargetPlugins.Where(x => (bEnginePlugins ? x.Path.IsUnderDirectory(Unreal.EngineDirectory) : !x.Path.IsUnderDirectory(Unreal.EngineDirectory))); // convert to paths ActivePlugins.AddRange(TargetPlugins.Select(x => x.Path)); } } foreach (FileReference ActivePlugin in ActivePlugins) { StagePluginDirectory(ActivePlugin.Directory, Context); } } protected void StageIniPathArray(ProjectParams Params, DeploymentContext SC, string IniKey, DirectoryReference BaseDirectory, ModifyStageContext Context) { List Entries; ConfigHierarchy GameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, Context.ProjectDirectory, BuildHostPlatform.Current.Platform); if (GameConfig.GetArray("CookedEditorSettings", IniKey, out Entries)) { foreach (string Entry in Entries) { Dictionary Props = ParseStructProperties(Entry); string SubPath = Props["Path"]; string FileWildcard = "*"; List FileList = Context.UFSFilesToStage; SearchOption SearchMode = SearchOption.AllDirectories; if (Props.ContainsKey("Files")) { FileWildcard = Props["Files"]; } if (Props.ContainsKey("NonUFS") && bool.Parse(Props["NonUFS"]) == true) { FileList = Context.NonUFSFilesToStage; } if (Props.ContainsKey("Recursive") && bool.Parse(Props["Recursive"]) == false) { SearchMode = SearchOption.TopDirectoryOnly; } // now enumerate files based on the settings DirectoryReference Dir = DirectoryReference.Combine(BaseDirectory, SubPath); if (DirectoryReference.Exists(Dir)) { FileList.AddRange(DirectoryReference.EnumerateFiles(Dir, FileWildcard, SearchMode)); } } } } protected void DefaultPreModifyDeploymentContext(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context) { } protected void DefaultModifyDeploymentContext(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context) { StageEngineEditorFiles(Params, SC, Context); StageProjectEditorFiles(Params, SC, Context); // final filtering // we already cooked assets, so remove assets we may have found, except for the Uncook ones Context.UFSFilesToStage.RemoveAll(x => x.GetExtension() == ".uasset"); // don't need the .target files Context.NonUFSFilesToStage.RemoveAll(x => x.GetExtension() == ".target"); if (!Context.bStageShaderDirs) { // don't need standalone shaders Context.UFSFilesToStage.RemoveAll(x => x.GetExtension() == ".glsl"); Context.UFSFilesToStage.RemoveAll(x => x.GetExtension() == ".hlsl"); } // move some files from UFS to NonUFS if they ended up there List UFSIncompatibleExtensions = new List { ".py", ".pyc" }; Context.NonUFSFilesToStage.AddRange(Context.UFSFilesToStage.Where(x => UFSIncompatibleExtensions.Contains(x.GetExtension()))); Context.UFSFilesToStage.RemoveAll(x => UFSIncompatibleExtensions.Contains(x.GetExtension())); } private ProjectParams GetParams() { FileReference ProjectPath = ParseProjectParam(); // setup DLC defaults, then ask project if it should string DLCName; string BasedOnReleaseVersion; TargetType ReleaseType; SetupDLCMode(ProjectPath, out DLCName, out BasedOnReleaseVersion, out ReleaseType); var Params = new ProjectParams ( Command: this, RawProjectPath: ProjectPath // standard cookededitor settings // , Client:false // , EditorTargets: new ParamList() // , SkipBuildClient: true , NoBootstrapExe: true // , Client: true , DLCName: DLCName , BasedOnReleaseVersion: BasedOnReleaseVersion ); string TargetPlatformType = "CookedEditor"; string TargetPlatformName = ProjectPath.GetFileNameWithoutAnyExtensions() + TargetPlatformType; // cook the cooked editor targetplatorm as the "client" Params.ClientCookedTargets.Clear(); Params.ClientCookedTargets.Add(TargetPlatformName); //Params.ClientCookedTargets.Add("CrashReportClientEditor"); Params.ClientTargetPlatforms = new List() { new TargetPlatformDescriptor(Params.ClientTargetPlatforms[0].Type, TargetPlatformType) }; // Params.EditorTargets.Clear(); // Params.EditorTargets.Add(TargetPlatformName); Params.ServerCookedTargets.Clear(); // when making cooked editors, we some special commandline options to override some assumptions about editor data Params.AdditionalCookerOptions += " -ini:Engine:[Core.System]:CanStripEditorOnlyExportsAndImports=False"; // We tend to "over-cook" packages to get everything we might need, so some non-editor BPs that are referencing editor BPs may // get cooked. This is okay, because the editor stuff should exist. We may want to revist this, and not cook anything that would // cause the issues Params.AdditionalCookerOptions += " -AllowUnsafeBlueprintCalls"; Params.AdditionalCookerOptions += " -dpcvars=cook.displaymode=2,r.ForceDebugViewModes=1"; // set up cooking against a client, as DLC if (BasedOnReleaseVersion != null) { // make the platform name, like "WindowsClient", or "LinuxGame", of the premade build we are cooking/staging against string IniPlatformName = ConfigHierarchy.GetIniPlatformName(Params.ClientTargetPlatforms[0].Type); string ReleaseTargetName = IniPlatformName + (ReleaseType == TargetType.Game ? "NoEditor" : ReleaseType.ToString()); Params.AdditionalCookerOptions += " -CookAgainstFixedBase"; Params.AdditionalCookerOptions += $" -DevelopmentAssetRegistryPlatformOverride={ReleaseTargetName}"; Params.AdditionalIoStoreOptions += $" -DevelopmentAssetRegistryPlatformOverride={ReleaseTargetName}"; // point to where the premade asset registry can be found Params.BasedOnReleaseVersionPathOverride = CommandUtils.CombinePaths(ProjectPath.Directory.FullName, "Releases", BasedOnReleaseVersion, ReleaseTargetName); Params.DLCOverrideStagedSubDir = ""; Params.DLCIncludeEngineContent = true; } // set up override functions Params.PreModifyDeploymentContextCallback = new Action((ProjectParams P, DeploymentContext SC) => { PreModifyDeploymentContext(P, SC); }); Params.ModifyDeploymentContextCallback = new Action((ProjectParams P, DeploymentContext SC) => { ModifyDeploymentContext(P, SC); }); ModifyParams(Params); return Params; } protected static void GatherTargetDependencies(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context, string ReceiptName) { GatherTargetDependencies(Params, SC, Context, ReceiptName, UnrealTargetConfiguration.Development); } protected static void GatherTargetDependencies(ProjectParams Params, DeploymentContext SC, ModifyStageContext Context, string ReceiptName, UnrealTargetConfiguration Configuration) { string Architecture = Params.SpecifiedArchitecture; if (string.IsNullOrEmpty(Architecture)) { Architecture = ""; if (PlatformExports.IsPlatformAvailable(SC.StageTargetPlatform.IniPlatformType)) { Architecture = PlatformExports.GetDefaultArchitecture(SC.StageTargetPlatform.IniPlatformType, Params.RawProjectPath); } } FileReference ReceiptFilename = TargetReceipt.GetDefaultPath(Params.RawProjectPath.Directory, ReceiptName, SC.StageTargetPlatform.IniPlatformType, Configuration, Architecture); if (!FileReference.Exists(ReceiptFilename)) { ReceiptFilename = TargetReceipt.GetDefaultPath(Unreal.EngineDirectory, ReceiptName, SC.StageTargetPlatform.IniPlatformType, Configuration, Architecture); } TargetReceipt Receipt; if (!TargetReceipt.TryRead(ReceiptFilename, out Receipt)) { throw new AutomationException("Missing or invalid target receipt ({0})", ReceiptFilename); } foreach (BuildProduct BuildProduct in Receipt.BuildProducts) { Context.NonUFSFilesToStage.Add(BuildProduct.Path); } foreach (RuntimeDependency RuntimeDependency in Receipt.RuntimeDependencies) { if (RuntimeDependency.Type == StagedFileType.UFS) { Context.UFSFilesToStage.Add(RuntimeDependency.Path); } else// if (RuntimeDependency.Type == StagedFileType.NonUFS) { Context.NonUFSFilesToStage.Add(RuntimeDependency.Path); } //else //{ // // otherwise, just stage it directly // // @todo: add a FilesToStage type to context like SC has? // SC.StageFile(RuntimeDependency.Type, RuntimeDependency.Path); //} } Context.NonUFSFilesToStage.Add(ReceiptFilename); } // @todo: Move this into UBT or something private static Dictionary ParseStructProperties(string PropsString) { // we expect parens around a properly encoded struct if (!PropsString.StartsWith("(") || !PropsString.EndsWith(")")) { return null; } // strip () PropsString = PropsString.Substring(1, PropsString.Length - 2); List Props = new List(); int TokenStart = 0; int StrLen = PropsString.Length; while (TokenStart < StrLen) { // get the next location of each special character int NextComma = PropsString.IndexOf(',', TokenStart); int NextQuote = PropsString.IndexOf('\"', TokenStart); // comma first? easy if (NextComma != -1 && NextComma < NextQuote) { Props.Add(PropsString.Substring(TokenStart, NextComma - TokenStart)); TokenStart = NextComma + 1; } // comma but no quotes else if (NextComma != -1 && NextQuote == -1) { Props.Add(PropsString.Substring(TokenStart, NextComma - TokenStart)); TokenStart = NextComma + 1; } // neither found, use the rest else if (NextComma == -1 && NextQuote == -1) { Props.Add(PropsString.Substring(TokenStart)); break; } // quote first? look for quote after else { NextQuote = PropsString.IndexOf('\"', NextQuote + 1); // are we at the end? if (NextQuote + 1 == StrLen) { // use the rest of the string Props.Add(PropsString.Substring(TokenStart)); break; } // it's expected that the following character is a comma, if not, give up if (PropsString[NextQuote + 1] != ',') { break; } // if next is comma, we are done this token Props.Add(PropsString.Substring(TokenStart, (NextQuote - TokenStart) + 1)); // skip over the quote and following commma TokenStart = NextQuote + 2; } } // now make a dictionary from the properties Dictionary KeyValues = new Dictionary(); foreach (string AProp in Props) { string Prop = AProp.Trim(" \t".ToCharArray()); // find the first = (UE4 properties can't have an equal sign, so it's valid to do) int Equals = Prop.IndexOf('='); // we must have one if (Equals == -1) { continue; } string Key = Prop.Substring(0, Equals); string Value = Prop.Substring(Equals + 1); // trim off any quotes around the entire value Value = Value.Trim(" \"".ToCharArray()); Key = Key.Trim(" ".ToCharArray()); KeyValues.Add(Key, Value); } // convert to array type return KeyValues; } }