// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Xml; using System.Xml.Linq; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /* UnrealPluginLanguage (UPL) is a simple XML-based language for manipulating XML and returning * strings. It contains an section which is evaluated once per architecture before any * other sections. The state is maintained and carried forward to the next section evaluated * so the order the sections are executed matters. * * While UPL is a general system for modifying and quering XML it is specifically used to allow * plug-ins to affect the global configuration of the package that they are a part of. For * example, this allows a plug-in to modify an Android's APK AndroidManfiest.xml file or an * IOS IPA's plist file. UBT will also query a plug-in's UPL xml file for strings to be included * in files that must be common to the package such as some .java files on Android. * * If you need to see the instructions executed in your plugin context add the following to * enable tracing: * * * * After this instuction all the nodes actually executed in your context will be written to the * log until you do a . You can also get a dump of all the variables in * your context with this command: * * * * Bool, Int, and String variable types are supported. Any attribute may reference a variable * and will be replaced with the string equivalent before evaluation using this syntax: * * $B(name) = boolean variable "name"'s value * $I(name) = integer variable "name"'s value * $S(name) = string variable "name"'s value * $E(name) = element variable "name"'s value * * The following variables are initialized automatically: * * $S(Output) = the output returned for evaluating the section (initialized to Input) * $S(UnrealArch) = target Unreal arch (arm32, arm64, x86, x64) * $S(Architecture) = target architecture (armeabi-armv7a, arm64-v8a, x86, x86_64) * $S(PluginDir) = directory the XML file was loaded from * $S(EngineDir) = engine directory * $S(BuildDir) = project's platform appropriate build directory (within the Intermediate folder) * $S(Configuration) = configuration type (Debug, DebugGame, Development, Test, Shipping) * $B(Distribution) = true if distribution build * $B(IsEmbedded) = true if project compiled for embedded use [Android-only] * $I(EngineMajorVersion) = major version of engine (ex. 4) * $I(EngineMinorVersion) = minor version of engine (ex. 21) * $I(EnginePatchVersion) = patch version of engine (ex. 0) * $S(EngineVersion) = engine version string (ex. "4.21.0") * * Note: with the exception of the above variables, all are in the context of the plugin to * prevent namespace collision; trying to set a new value to any of the above, with the * exception of Output, will only affect the current context. * * The following nodes allow manipulation of variables: * * * * * * * * * with value creates an empty XML element with the tag set to value. * with value and text creates an XML element with tag set to value of unparsed text. * with xml will parse the XML provided. Remember to escape any special characters! * * Variables may also be set from a property in an ini file: * * * * * * * * * Strings may also be set from an environment variable using the node. * The environment variable must be specified as the 'value' attribute and wrapped in a pair * of '%' characters. * * * * You can also check if a specific environment variable is defined (again, wrapped in '%' characters): * * * * A general example for using environment variable nodes: * * * * * Boolean variables may also be set to the result of applying operators: * * * * * * * * * * * * Integer variables may use these arithmetic operations: * * * * * * * Strings are manipulated with the following: * * * * * * * * * is equivalent to calling setStringSubstring with "start" set to * the index of the character immediately following the find string. Passing -1 for length (or * omitting it) will return the entire string to the right of the find string. * * String length may be retrieved with: * * * * The index of a search string may be found in source with: * * * * The following shortcut string comparisons may also be used instead of using * and checking the result: * * * * * * Messages are written to the log with this node: * * * * Conditional execution uses the following form: * * * * * * * * * * * The and blocks are optional. The condition must be in a boolean variable. * The boolean operator nodes may be combined to create a final state for more complex * conditions: * * * * * * * * * * * * Note the "isIntel" could also be done like this: * * * * * Two shortcut nodes are available for conditional execution: * * * * * * is the equivalent of: * * * * * * * * * and * * * * * * is the equivalent of: * * * * * * Execution may be stopped with: * * * * Loops may be created using these nodes: * * * * * * * * * The body will execute until the condition is false or a is hit. The * will restart execution of the loop if the condition is still true or exit. * * Note: outside a body will act the same as * * Here is an example loop which writes 1 to 5 to the log, skipping 3. Note the update of the * while condition should be done before the continue otherwise it may not exit. * * * * * * * * * * * * * * * * It is possible to use variable replacement in generating the result variable * name as well. This makes the creation of arrays in loops possible: * * * * This may be retrieved using the following (value is treated as the variable * name): * * * * For boolean and integer types, you may use and . * * Nodes for inserting text into the section are as follows: * * body * * * * * The first one will insert either text or nodes into the returned section * string. Please note you must use escaped characters for: * * < = < * > = > * & = & * * evaluates variables in value before insertion. If value contains * double quote ("), you must escape it with ". * * is a shortcut to insert a system.LoadLibrary try/catch * block with an optional logged message for failure to load case. * * You can do a search and replace in the Output with: * * * * Note you can also manipulate the actual $S(Output) directly, the above are more efficient: * * * * * XML manipulation uses the following nodes: * * * body * * * * * * * instructions * * The current element is referenced with tag="$". Element variables are referenced with $varname * since using $E(varname) will be expanded to the string equivalent of the XML. * * addElement, addElements, and removeElement by default are applied to all matching tags. An * optional once="true" attribute may be added to only apply to first matching tag. * * , , and are updated with: * * * * * * Any attributes in the above commands are copied to the element added to the manifest so you * can do the following, for example: * * * * Finally, these nodes allow copying of files useful for staging jar and so files: * * * * * If force is false the file(s) are replaced only if length or timestamp don't match. Default is true. * * The following should be used as the base for the src and dst paths: * * $S(PluginDir) = directory the XML file was loaded from * $S(EngineDir) = engine directory * $S(BuildDir) = project's platform appropriate build directory * * While it is possible to write outside the APK directory, it is not recommended. * * If you must remove files (like development-only files from distribution builds) you can * use this node: * * * * It is restricted to only removing files from the BuildDir. If recursive is true it will search all subfolders. * Here is example usage to remove the Oculus Signature Files (osig) from the assets directory: * * * * The following sections are evaluated during the packaging or deploy stages: * * ** For all platforms ** * * * * ** Android Specific sections ** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Here is the complete list of supported nodes: * * * * => / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // Helper struct to initialize aliased namespaces struct AliasedXMLNamespace { public string Alias { get; set; } public string Url { get; set; } public override string ToString() { return String.Format("xmlns:{0}=\"{1}\"", Alias, Url); } } class UnrealPluginLanguage { /** The merged XML program to run */ private XDocument XDoc; /** XML namespace */ private XNamespace XMLDefaultNameSpace; private Dictionary XMLNameSpaceAliases = new Dictionary(); private string XMLRootDefinition; private UnrealTargetPlatform TargetPlatform; /** Trace flag to enable debugging */ private static bool bGlobalTrace = false; private static bool bGlobalTraceFilename = false; /** Project file reference */ private FileReference? ProjectFile; private static XDocument XMLDummy = XDocument.Parse(""); private class UPLContext { public string Filename; /** Variable state */ public Dictionary BoolVariables; public Dictionary IntVariables; public Dictionary StringVariables; public Dictionary ElementVariables; /** Local context trace */ public bool bTrace; public bool bTraceFilename; public UPLContext(string inFilename, (string UEArch, string NativeArch) Arch, string PluginDir) { Filename = inFilename; BoolVariables = new Dictionary(); IntVariables = new Dictionary(); StringVariables = new Dictionary(); ElementVariables = new Dictionary(); if (PluginDir == null || String.IsNullOrEmpty(PluginDir)) { PluginDir = "."; } StringVariables["PluginDir"] = PluginDir.Replace("\\", "/"); StringVariables["AbsPluginDir"] = Path.GetFullPath(PluginDir).Replace("\\", "/"); StringVariables["UnrealArch"] = Arch.UEArch; StringVariables["Architecture"] = Arch.NativeArch; bTrace = false; bTraceFilename = false; } } private UPLContext GlobalContext; private Dictionary Contexts; private int ContextIndex; private string? LastError; private ILogger Logger; // First entry in InXMLNameSpaceAliases will be used as default namespace public UnrealPluginLanguage(FileReference? InProjectFile, List InXMLFiles, List<(string UEArch, string NativeArch)> InArchs, List? InXMLNameSpaceAliases, UnrealTargetPlatform InTargetPlatform, ILogger InLogger) { ProjectFile = InProjectFile; Logger = InLogger; LastError = null; Contexts = new Dictionary(); GlobalContext = new UPLContext("", ("", ""), ""); ContextIndex = 0; if (InXMLNameSpaceAliases != null && InXMLNameSpaceAliases.Count > 0) { XMLDefaultNameSpace = InXMLNameSpaceAliases[0].Url; StringBuilder RootNamespaces = new StringBuilder(); foreach (AliasedXMLNamespace AliasToNamespace in InXMLNameSpaceAliases) { XMLNameSpaceAliases[AliasToNamespace.Alias] = AliasToNamespace.Url; RootNamespaces.AppendFormat("xmlns:{0}=\"{1}\" ", AliasToNamespace.Alias, AliasToNamespace.Url); } XMLRootDefinition = String.Join(" ", RootNamespaces); } else { XMLDefaultNameSpace = ""; XMLRootDefinition = ""; } TargetPlatform = InTargetPlatform; string PathPrefix = Path.GetFileName(Directory.GetCurrentDirectory()).Equals("Source") ? ".." : "Engine"; XDoc = XDocument.Parse(""); foreach (string Basename in InXMLFiles) { string Filename = Path.Combine(PathPrefix, Basename.Replace("\\", "/")); Logger.LogInformation("UPL: {FileName}", Filename); if (File.Exists(Filename)) { string PluginDir = Path.GetDirectoryName(Filename)!; try { XDocument MergeDoc = XDocument.Load(Filename, LoadOptions.SetLineInfo); MergeXML(MergeDoc, Filename, PluginDir, InArchs); } catch (Exception e) { LastError = String.Format("Unreal Plugin file {0} parsing failed! {1}", Filename, e); Logger.LogError("\n{LastError}", LastError); } } else { LastError = String.Format("Unreal Plugin file {0} missing!", Filename); Logger.LogError("\n{LastError}", LastError); Logger.LogInformation("\nCWD: {Cwd}", Directory.GetCurrentDirectory()); } } } public string? GetLastError() { return LastError; } public bool GetTrace() { return bGlobalTrace; } public void SetTrace() { bGlobalTrace = true; } public void ClearTrace() { bGlobalTrace = false; } public bool GetTraceFilename() { return bGlobalTraceFilename; } public void SetTraceFilename() { bGlobalTraceFilename = true; } public void ClearTraceFilename() { bGlobalTraceFilename = false; } public string GetUPLHash() { return ContentHash.MD5(XDoc.ToString()).ToString(); } public bool MergeXML(XDocument MergeDoc, string Filename, string PluginDir, List<(string UEArch, string NativeArch)> Archs) { if (MergeDoc == null) { return false; } // create a context for each architecture ContextIndex++; foreach ((string UEArch, string NativeArch) Arch in Archs) { UPLContext Context = new UPLContext(Filename, Arch, PluginDir); Contexts[Arch.NativeArch + "_" + ContextIndex] = Context; } // line numbers are lost in the merge so add them as an attribute for later tracing of warnings/errors foreach (XElement Element in MergeDoc.Root!.Descendants()) { if (((IXmlLineInfo)Element).HasLineInfo()) { Element.Add(new XAttribute("__line", ((IXmlLineInfo)Element).LineNumber)); } } // merge in the nodes foreach (XElement Element in MergeDoc.Root!.Elements()) { XElement? Parent = XDoc.Root!.Element(Element.Name); if (Parent != null) { XElement Entry = new XElement("Context", new XAttribute("index", ContextIndex.ToString())); Entry.Add(Element.Elements()); Parent.Add(Entry); } else { XElement Entry = new XElement("Context", new XAttribute("index", ContextIndex.ToString())); Entry.Add(Element.Elements()); XElement Base = new XElement(Element.Name); Base.Add(Entry); XDoc.Root.Add(Base); } } return true; } public void SaveXML(string Filename) { XDoc?.Save(Filename); } private string DumpContext(UPLContext Context) { StringBuilder Text = new StringBuilder(); foreach (KeyValuePair Variable in Context.BoolVariables) { Text.AppendLine(String.Format("\tbool {0} = {1}", Variable.Key, Variable.Value.ToString().ToLower())); } foreach (KeyValuePair Variable in Context.IntVariables) { Text.AppendLine(String.Format("\tint {0} = {1}", Variable.Key, Variable.Value)); } foreach (KeyValuePair Variable in Context.StringVariables) { Text.AppendLine(String.Format("\tstring {0} = {1}", Variable.Key, Variable.Value)); } foreach (KeyValuePair Variable in Context.ElementVariables) { Text.AppendLine(String.Format("\telement {0} = {1}", Variable.Key, Variable.Value)); } return Text.ToString(); } public string DumpVariables() { string Result = "Global Context:\n" + DumpContext(GlobalContext); foreach (KeyValuePair Context in Contexts) { Result += "Context " + Context.Key + ": " + Context.Value.StringVariables["PluginDir"] + "\n" + DumpContext(Context.Value); } return Result; } private bool GetCondition(UPLContext Context, XElement Node, string? Condition, out bool Result) { Result = false; if (Condition == null) { return false; } if (!Context.BoolVariables.TryGetValue(Condition, out Result)) { if (!GlobalContext.BoolVariables.TryGetValue(Condition, out Result)) { Logger.LogWarning("\nMissing condition '{Condition}' in '{Node}' (skipping instruction)", Condition, TraceNodeString(Context, Node)); return false; } } return true; } private string ExpandVariables(UPLContext Context, string InputString) { string Result = InputString; for (int Idx = Result.IndexOf("$B("); Idx != -1; Idx = Result.IndexOf("$B(", Idx)) { // Find the end of the variable name int EndIdx = Result.IndexOf(')', Idx + 3); if (EndIdx == -1) { break; } // Extract the variable name from the string string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3)); // Find the value for it, either from the dictionary or the environment block bool Value; if (!Context.BoolVariables.TryGetValue(Name, out Value)) { if (!GlobalContext.BoolVariables.TryGetValue(Name, out Value)) { Idx = EndIdx + 1; continue; } } // Replace the variable, or skip past it Result = Result.Substring(0, Idx) + Value.ToString().ToLower() + Result.Substring(EndIdx + 1); } for (int Idx = Result.IndexOf("$I("); Idx != -1; Idx = Result.IndexOf("$I(", Idx)) { // Find the end of the variable name int EndIdx = Result.IndexOf(')', Idx + 3); if (EndIdx == -1) { break; } // Extract the variable name from the string string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3)); // Find the value for it, either from the dictionary or the environment block int Value; if (!Context.IntVariables.TryGetValue(Name, out Value)) { if (!GlobalContext.IntVariables.TryGetValue(Name, out Value)) { Idx = EndIdx + 1; continue; } } // Replace the variable, or skip past it Result = Result.Substring(0, Idx) + Value.ToString() + Result.Substring(EndIdx + 1); } for (int Idx = Result.IndexOf("$S("); Idx != -1; Idx = Result.IndexOf("$S(", Idx)) { // Find the end of the variable name int EndIdx = Result.IndexOf(')', Idx + 3); if (EndIdx == -1) { break; } // Extract the variable name from the string string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3)); // Find the value for it, either from the dictionary or the environment block string? Value; if (!Context.StringVariables.TryGetValue(Name, out Value)) { if (!GlobalContext.StringVariables.TryGetValue(Name, out Value)) { Idx = EndIdx + 1; continue; } } // Replace the variable, or skip past it Result = Result.Substring(0, Idx) + Value + Result.Substring(EndIdx + 1); } for (int Idx = Result.IndexOf("$E("); Idx != -1; Idx = Result.IndexOf("$E(", Idx)) { // Find the end of the variable name int EndIdx = Result.IndexOf(')', Idx + 3); if (EndIdx == -1) { break; } // Extract the variable name from the string string Name = Result.Substring(Idx + 3, EndIdx - (Idx + 3)); // Find the value for it, either from the dictionary or the environment block XElement? Value; if (!Context.ElementVariables.TryGetValue(Name, out Value)) { if (!GlobalContext.ElementVariables.TryGetValue(Name, out Value)) { Idx = EndIdx + 1; continue; } } // Replace the variable, or skip past it Result = Result.Substring(0, Idx) + Value + Result.Substring(EndIdx + 1); } return Result; } private string TraceNodeString(UPLContext Context, XElement Node, bool ShowFilename = true) { XAttribute? LineAttrib = null; string Result = Node.Name.ToString(); foreach (XAttribute Attrib in Node.Attributes()) { string AttribStr = Attrib.ToString(); if (AttribStr.StartsWith("__line=")) { LineAttrib = Attrib; } else { Result += " " + AttribStr; } } if (ShowFilename) { Result += ", File: " + Context.Filename; } if (LineAttrib != null) { Result += ", Line: " + LineAttrib.Value; } return Result; } private bool StringToBool(string? Input) { if (Input == null) { return false; } Input = Input.ToLower(); return !(Input.Equals("0") || Input.Equals("false") || Input.Equals("off") || Input.Equals("no")); } private int StringToInt(UPLContext Context, XElement Node, string? Input) { int Result = 0; if (!Int32.TryParse(Input, out Result)) { Logger.LogWarning("\nInvalid integer '{Input}' in '{Node}' (defaulting to 0)", Input, TraceNodeString(Context, Node)); } return Result; } [return: NotNullIfNotNull("Fallback")] private string? GetAttribute(UPLContext Context, XElement Node, string AttributeName, bool bExpand = true, bool bRequired = true, string? Fallback = null) { XAttribute? Attribute = Node.Attribute(AttributeName); if (Attribute == null) { if (bRequired) { Logger.LogWarning("\nMissing attribute '{Attribute}' in '{Node}' (skipping instruction)", AttributeName, TraceNodeString(Context, Node)); } return Fallback; } string Result = Attribute.Value; return bExpand ? ExpandVariables(Context, Result) : Result; } [return: NotNullIfNotNull("Fallback")] private string? GetAttributeWithNamespace(UPLContext Context, XElement Node, XNamespace Namespace, string AttributeName, bool bExpand = true, bool bRequired = true, string? Fallback = null) { XAttribute? Attribute = Node.Attribute(Namespace + AttributeName); if (Attribute == null) { if (bRequired) { Logger.LogWarning("\nMissing attribute '{Attribute}' in '{Node}' (skipping instruction)", AttributeName, TraceNodeString(Context, Node)); } return Fallback; } string Result = Attribute.Value; return bExpand ? ExpandVariables(Context, Result) : Result; } private static Dictionary? ConfigCache = null; private ConfigCacheIni_UPL GetConfigCacheIni_UPL(string baseIniName) { ConfigCache ??= new Dictionary(); ConfigCacheIni_UPL? config = null; if (!ConfigCache.TryGetValue(baseIniName, out config)) { // note: use our own ConfigCacheIni since EngineConfiguration.cs only parses RequiredSections! config = ConfigCacheIni_UPL.CreateConfigCacheIni_UPL(TargetPlatform, baseIniName, DirectoryReference.FromFile(ProjectFile), Logger); ConfigCache.Add(baseIniName, config); } return config; } private static bool FilesAreDifferent(string SourceFilename, string DestFilename, ILogger Logger) { // source must exist FileInfo SourceInfo = new FileInfo(SourceFilename); if (!SourceInfo.Exists) { Logger.LogInformation("File {SourceFilename} does not exist", SourceFilename); return false; } // different if destination doesn't exist FileInfo DestInfo = new FileInfo(DestFilename); if (!DestInfo.Exists) { return true; } // file lengths differ? if (SourceInfo.Length != DestInfo.Length) { return true; } // validate timestamps TimeSpan Diff = DestInfo.LastWriteTimeUtc - SourceInfo.LastWriteTimeUtc; if (Diff.TotalSeconds < -1 || Diff.TotalSeconds > 1) { return true; } // could check actual bytes just to be sure, but good enough return false; } private static void CopyFileDirectory(string SourceDir, string DestDir, ILogger Logger, bool bForce = false) { if (!Directory.Exists(SourceDir)) { return; } string[] Files = Directory.GetFiles(SourceDir, "*.*", SearchOption.AllDirectories); foreach (string Filename in Files) { // make the dst filename with the same structure as it was in SourceDir string DestFilename = Path.Combine(DestDir, Utils.MakePathRelativeTo(Filename, SourceDir)); if (bForce || FilesAreDifferent(Filename, DestFilename, Logger)) { if (File.Exists(DestFilename)) { File.SetAttributes(DestFilename, FileAttributes.Normal); File.Delete(DestFilename); } // make the subdirectory if needed string DestSubdir = Path.GetDirectoryName(DestFilename)!; if (!Directory.Exists(DestSubdir)) { Directory.CreateDirectory(DestSubdir); } File.Copy(Filename, DestFilename); // remove any read only flags and keep timestamp FileInfo DestFileInfo = new FileInfo(DestFilename); DestFileInfo.Attributes &= ~FileAttributes.ReadOnly; File.SetLastWriteTimeUtc(DestFilename, File.GetLastWriteTimeUtc(Filename)); } } } private static void DeleteFiles(string Filespec, bool Recursive, ILogger Logger) { string BaseDir = Path.GetDirectoryName(Filespec)!; string Mask = Path.GetFileName(Filespec); if (!Directory.Exists(BaseDir)) { return; } string[] Files = Directory.GetFiles(BaseDir, Mask, Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); foreach (string Filename in Files) { File.SetAttributes(Filename, FileAttributes.Normal); File.Delete(Filename); Logger.LogInformation("\nDeleted file {Filename}", Filename); } } XNamespace? TrimNamespaceAliasFromName(ref string Name) { int Index = Name.IndexOf(":"); if (Index >= 0) { XNamespace? Namespace; string NamespaceAlias = Name.Substring(0, Index); if (!XMLNameSpaceAliases.TryGetValue(NamespaceAlias, out Namespace)) { Namespace = XMLDefaultNameSpace; } Name = Name.Substring(Index + 1); return Namespace; } else { return null; } } private void AddAttribute(XElement Element, string Name, string Value) { XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Name); XAttribute? Attribute = Element.Attribute(XMLNameSpace != null? XMLNameSpace + Name : Name); if (Attribute != null) { Attribute.SetValue(Value); } else { Element.Add(new XAttribute(XMLNameSpace != null ? XMLNameSpace + Name : Name, Value)); } } private void RemoveAttribute(XElement Element, string Name) { XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Name); XAttribute? Attribute = Element.Attribute(XMLNameSpace != null ? XMLNameSpace + Name : Name); Attribute?.Remove(); } private void AddElements(XElement Target, XElement Source) { if (Source.HasElements) { foreach (XElement Index in Source.Elements()) { // if (Target.Element(Index.Name) == null) { Target.Add(Index); } } } } public string ProcessPluginNode(string Architecture, string NodeName, string Input) { return ProcessPluginNode(Architecture, NodeName, Input, ref XMLDummy); } public string ProcessPluginNode(string Architecture, string NodeName, string Input, ref XDocument XMLWork) { // add all instructions to execution list Stack ExecutionStack = new Stack(); XElement? StartNode = XDoc.Root!.Element(NodeName); if (StartNode != null) { foreach (XElement Instruction in StartNode.Elements().Reverse()) { ExecutionStack.Push(Instruction); } } if (ExecutionStack.Count == 0) { return Input; } Stack ContextStack = new Stack(); UPLContext CurrentContext = GlobalContext; // update Output in global context GlobalContext.StringVariables["Output"] = Input; Stack ElementStack = new Stack(); XElement CurrentElement = XMLWork.Elements().First(); // run the instructions while (ExecutionStack.Count > 0) { XElement Node = ExecutionStack.Pop(); if (bGlobalTrace || CurrentContext.bTrace) { Logger.LogInformation("Execute: '{Node}'", TraceNodeString(CurrentContext, Node, bGlobalTraceFilename || CurrentContext.bTraceFilename)); } switch (Node.Name.ToString()) { case "trace": { string? TraceFilename = GetAttribute(CurrentContext, Node, "filename"); if (TraceFilename != null) { CurrentContext.bTraceFilename = StringToBool(TraceFilename); } string? Enable = GetAttribute(CurrentContext, Node, "enable"); if (Enable != null) { CurrentContext.bTrace = StringToBool(Enable); if (!bGlobalTrace && CurrentContext.bTrace) { Logger.LogInformation("Context: '{Context}' using Architecture='{Arch}', NodeName='{NodeName}', Input='{Input}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input); } } } break; case "dumpvars": { if (!bGlobalTrace && !CurrentContext.bTrace) { Logger.LogInformation("Context: '{Context}' using Architecture='{Arch}', NodeName='{NodeName}', Input='{Input}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input); } Logger.LogInformation("Variables:\n{Variables}", DumpContext(CurrentContext)); } break; case "Context": { ContextStack.Push(CurrentContext); string? index = GetAttribute(CurrentContext, Node, "index"); CurrentContext = Contexts[Architecture + "_" + index]; ExecutionStack.Push(new XElement("PopContext")); foreach (XElement instruction in Node.Elements().Reverse()) { ExecutionStack.Push(instruction); } if (bGlobalTrace || CurrentContext.bTrace) { Logger.LogInformation("Context: '{Context}' using Architecture='{Arch}', NodeName='{NodeName}', Input='{Input}'", CurrentContext.StringVariables["PluginDir"], Architecture, NodeName, Input); } } break; case "PopContext": { CurrentContext = ContextStack.Pop(); } break; case "PopElement": { CurrentElement = ElementStack.Pop(); } break; case "isArch": { string? arch = GetAttribute(CurrentContext, Node, "arch"); if (arch != null && arch.Equals(Architecture)) { foreach (XElement instruction in Node.Elements().Reverse()) { ExecutionStack.Push(instruction); } } } break; case "isDistribution": { bool Result = false; if (GetCondition(CurrentContext, Node, "Distribution", out Result)) { if (Result) { foreach (XElement Instruction in Node.Elements().Reverse()) { ExecutionStack.Push(Instruction); } } } } break; case "if": { bool Result; if (GetCondition(CurrentContext, Node, GetAttribute(CurrentContext, Node, "condition"), out Result)) { XElement? ResultNode = Node.Element(Result ? "true" : "false"); if (ResultNode != null) { foreach (XElement Instruction in ResultNode.Elements().Reverse()) { ExecutionStack.Push(Instruction); } } } } break; case "while": { bool Result; if (GetCondition(CurrentContext, Node, GetAttribute(CurrentContext, Node, "condition"), out Result)) { if (Result) { IEnumerable ResultNode = Node.Elements(); if (ResultNode != null) { ExecutionStack.Push(Node); foreach (XElement Instruction in ResultNode.Reverse()) { ExecutionStack.Push(Instruction); } } } } } break; case "return": { while (ExecutionStack.Count > 0) { ExecutionStack.Pop(); } } break; case "break": { // remove up to while (or acts like a return if outside by removing everything) while (ExecutionStack.Count > 0) { Node = ExecutionStack.Pop(); if (Node.Name.ToString().Equals("while")) { break; } } } break; case "continue": { // remove up to while (or acts like a return if outside by removing everything) while (ExecutionStack.Count > 0) { Node = ExecutionStack.Pop(); if (Node.Name.ToString().Equals("while")) { ExecutionStack.Push(Node); break; } } } break; case "log": { string? Text = GetAttribute(CurrentContext, Node, "text"); if (Text != null) { Logger.LogInformation("{Message}", Text); } } break; case "loopElements": { string? Tag = GetAttribute(CurrentContext, Node, "tag"); ElementStack.Push(CurrentElement); ExecutionStack.Push(new XElement("PopElement")); IEnumerable WorkList = (Tag == "$") ? CurrentElement.Elements().Reverse() : CurrentElement.Descendants(Tag).Reverse(); foreach (XElement WorkNode in WorkList) { foreach (XElement Instruction in Node.Elements().Reverse()) { ExecutionStack.Push(Instruction); } ElementStack.Push(WorkNode); ExecutionStack.Push(new XElement("PopElement")); } } break; case "addAttribute": { string? Tag = GetAttribute(CurrentContext, Node, "tag"); string? Name = GetAttribute(CurrentContext, Node, "name"); string? Value = GetAttribute(CurrentContext, Node, "value"); if (Tag != null && Name != null && Value != null) { if (Tag.StartsWith("$")) { XElement? Target = CurrentElement; if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } AddAttribute(Target!, Name, Value); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { AddAttribute(CurrentElement, Name, Value); } foreach (XElement WorkNode in CurrentElement.Descendants(Tag)) { AddAttribute(WorkNode, Name, Value); } } } } break; case "removeAttribute": { string? Tag = GetAttribute(CurrentContext, Node, "tag"); string? Name = GetAttribute(CurrentContext, Node, "name"); if (Tag != null && Name != null) { if (Tag.StartsWith("$")) { XElement? Target = CurrentElement; if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { Logger.LogInformation("\nMissing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } RemoveAttribute(Target, Name); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { RemoveAttribute(CurrentElement, Name); } foreach (XElement WorkNode in CurrentElement.Descendants(Tag)) { RemoveAttribute(WorkNode, Name); } } } } break; case "addPermission": { string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name"); if (Name != null) { // make sure it isn't already added bool bFound = false; foreach (XElement Element in XMLWork.Descendants("uses-permission")) { XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { bFound = true; break; } } } // add it if not found if (!bFound) { // Get the attributes and apply any variable expansion needed List AttributeList = Node.Attributes().ToList(); foreach (XAttribute Attribute in AttributeList) { string NewValue = ExpandVariables(CurrentContext, Attribute.Value); Attribute.SetValue(NewValue); } XMLWork.Element("manifest")?.Add(new XElement("uses-permission", AttributeList)); } } } break; case "removePermission": { string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name"); if (Name != null) { foreach (XElement Element in XMLWork.Descendants("uses-permission")) { XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { Element.Remove(); break; } } } } } break; case "addFeature": { string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name"); if (Name != null) { // make sure it isn't already added bool bFound = false; foreach (XElement Element in XMLWork.Descendants("uses-feature")) { XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { bFound = true; break; } } } // add it if not found if (!bFound) { // Get the attributes and apply any variable expansion needed List AttributeList = Node.Attributes().ToList(); foreach (XAttribute Attribute in AttributeList) { string NewValue = ExpandVariables(CurrentContext, Attribute.Value); Attribute.SetValue(NewValue); } XMLWork.Element("manifest")?.Add(new XElement("uses-feature", AttributeList)); } } } break; case "removeFeature": { string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name"); if (Name != null) { foreach (XElement Element in XMLWork.Descendants("uses-feature")) { XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { Element.Remove(); break; } } } } } break; case "addLibrary": { string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name"); if (Name != null) { // make sure it isn't already added bool bFound = false; foreach (XElement Element in XMLWork.Descendants("uses-library")) { XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { bFound = true; break; } } } // add it if not found if (!bFound) { // Get the attributes and apply any variable expansion needed List AttributeList = Node.Attributes().ToList(); foreach (XAttribute Attribute in AttributeList) { string NewValue = ExpandVariables(CurrentContext, Attribute.Value); Attribute.SetValue(NewValue); } XMLWork.Element("manifest")?.Element("application")?.Add(new XElement("uses-library", AttributeList)); } } } break; case "removeLibrary": { string? Name = GetAttributeWithNamespace(CurrentContext, Node, XMLDefaultNameSpace, "name"); if (Name != null) { foreach (XElement Element in XMLWork.Descendants("uses-library")) { XAttribute? Attribute = Element.Attribute(XMLDefaultNameSpace + "name"); if (Attribute != null) { if (Attribute.Value == Name) { Element.Remove(); break; } } } } } break; case "removeElement": { string? Tag = GetAttribute(CurrentContext, Node, "tag"); bool bOnce = StringToBool(GetAttribute(CurrentContext, Node, "once", true, false)); if (Tag != null) { if (Tag == "$") { XElement Parent = CurrentElement.Parent!; CurrentElement.Remove(); CurrentElement = Parent; } else { // use a list since Remove() may modify it foreach (XElement Element in XMLWork.Descendants(Tag).ToList()) { Element.Remove(); if (bOnce) { break; } } } } } break; case "addElement": { string? Tag = GetAttribute(CurrentContext, Node, "tag"); string? Name = GetAttribute(CurrentContext, Node, "name"); bool bOnce = StringToBool(GetAttribute(CurrentContext, Node, "once", true, false)); if (Tag != null && Name != null) { XElement? Element; if (!CurrentContext.ElementVariables.TryGetValue(Name, out Element)) { if (!GlobalContext.ElementVariables.TryGetValue(Name, out Element)) { Logger.LogWarning("Missing element variable '{Name}' in '{Node}' (skipping instruction)", Name, TraceNodeString(CurrentContext, Node)); continue; } } if (Tag.StartsWith("$")) { XElement? Target = CurrentElement; if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { Logger.LogWarning("Missing element variable '{Name}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } Target.Add(new XElement(Element)); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { CurrentElement.Add(new XElement(Element)); } // make sure we don't recurse forever if Tag is in Element List AddSet = new List(); foreach (XElement WorkNode in CurrentElement.Descendants(Tag)) { AddSet.Add(WorkNode); if (bOnce) { break; } } foreach (XElement WorkNode in AddSet) { WorkNode.Add(new XElement(Element)); } } } } break; case "addElements": { string? Tag = GetAttribute(CurrentContext, Node, "tag"); bool bOnce = StringToBool(GetAttribute(CurrentContext, Node, "once", true, false)); if (Tag != null) { if (Tag.StartsWith("$")) { XElement? Target = CurrentElement; if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Target)) { Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } AddElements(Target, Node); } else { if (CurrentElement.Name.ToString().Equals(Tag)) { AddElements(CurrentElement, Node); } // make sure we don't recurse forever if Tag is in Node List AddSet = new List(); foreach (XElement WorkNode in CurrentElement.Descendants(Tag)) { AddSet.Add(WorkNode); if (bOnce) { break; } } foreach (XElement WorkNode in AddSet) { AddElements(WorkNode, Node); } } } } break; case "insert": { if (Node.HasElements) { foreach (XElement Element in Node.Elements()) { string Value = Element.ToString().Replace(" " + XMLRootDefinition + " ", ""); GlobalContext.StringVariables["Output"] += Value + "\n"; } } else { string Value = Node.Value.ToString(); // trim trailing tabs int Index = Value.Length; while (Index > 0 && Value[Index - 1] == '\t') { Index--; } if (Index < Value.Length) { Value = Value.Substring(0, Index); } // trim leading newlines Index = 0; while (Index < Value.Length && Value[Index] == '\n') { Index++; } if (Index < Value.Length) { GlobalContext.StringVariables["Output"] += Value.Substring(Index); } } } break; case "insertNewline": GlobalContext.StringVariables["Output"] += "\n"; break; case "insertValue": { string? Value = GetAttribute(CurrentContext, Node, "value"); if (Value != null) { GlobalContext.StringVariables["Output"] += Value; } } break; case "replace": { string? Find = GetAttribute(CurrentContext, Node, "find"); string? With = GetAttribute(CurrentContext, Node, "with"); if (Find != null && With != null) { GlobalContext.StringVariables["Output"] = GlobalContext.StringVariables["Output"].Replace(Find, With); } } break; case "copyFile": { string? Src = GetAttribute(CurrentContext, Node, "src"); string? Dst = GetAttribute(CurrentContext, Node, "dst"); bool bForce = StringToBool(GetAttribute(CurrentContext, Node, "force", true, false, "true")); if (Src != null && Dst != null) { if (File.Exists(Src)) { // check to see if newer than last time we copied if (bForce || FilesAreDifferent(Src, Dst, Logger)) { if (File.Exists(Dst)) { File.SetAttributes(Dst, FileAttributes.Normal); File.Delete(Dst); } Directory.CreateDirectory(Path.GetDirectoryName(Dst)!); File.Copy(Src, Dst, true); Logger.LogInformation("File {Src} copied to {Dst}", Src, Dst); // remove any read only flags and keep timestamp FileInfo DestFileInfo = new FileInfo(Dst); DestFileInfo.Attributes &= ~FileAttributes.ReadOnly; File.SetLastWriteTimeUtc(Dst, File.GetLastWriteTimeUtc(Src)); } } else { Logger.LogInformation("File {Src} does not exist, not copied!", Src); } } } break; case "copyDir": { string? Src = GetAttribute(CurrentContext, Node, "src"); string? Dst = GetAttribute(CurrentContext, Node, "dst"); bool bForce = StringToBool(GetAttribute(CurrentContext, Node, "force", true, false, "true")); if (Src != null && Dst != null) { CopyFileDirectory(Src, Dst, Logger, bForce); Logger.LogInformation("\nDirectory {Src} copied to {Dst} ({Force})", Src, Dst, bForce); } } break; case "deleteFiles": { string? Filespec = GetAttribute(CurrentContext, Node, "filespec"); bool bRecursive = StringToBool(GetAttribute(CurrentContext, Node, "recursive", true, false, "false")); if (Filespec != null) { if (Filespec.Contains(':') || Filespec.Contains("..")) { Logger.LogInformation("\nFilespec {FileSpec} not allowed; ignored.", Filespec); } else { // force relative to BuildDir (and only from global context so someone doesn't try to be clever) DeleteFiles(Path.Combine(GlobalContext.StringVariables["BuildDir"], Filespec), bRecursive, Logger); } } } break; case "loadLibrary": { string? Name = GetAttribute(CurrentContext, Node, "name"); string? FailMsg = GetAttribute(CurrentContext, Node, "failmsg", true, false); if (Name != null) { string Work = "\t\ttry\n" + "\t\t{\n" + "\t\t\tSystem.loadLibrary(\"" + Name + "\");\n" + "\t\t}\n" + "\t\tcatch (java.lang.UnsatisfiedLinkError e)\n" + "\t\t{\n"; if (FailMsg != null) { Work += "\t\t\tLog.debug(e.toString());\n"; Work += "\t\t\tLog.debug(\"" + FailMsg + "\");\n"; } GlobalContext.StringVariables["Output"] += Work + "\t\t}\n"; } } break; case "setBool": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false, "false"); if (Result != null) { CurrentContext.BoolVariables[Result] = StringToBool(Value); } } break; case "setBoolEnvVarDefined": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Value = GetAttribute(CurrentContext, Node, "value"); if (Result != null) { CurrentContext.BoolVariables[Result] = (Value != null && Environment.ExpandEnvironmentVariables(Value).Length > 0); } } break; case "setBoolFrom": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false, "false"); if (Result != null) { Value = ExpandVariables(CurrentContext, "$B(" + Value + ")"); CurrentContext.BoolVariables[Result] = StringToBool(Value); } } break; case "setBoolFromProperty": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Ini = GetAttribute(CurrentContext, Node, "ini"); string? Section = GetAttribute(CurrentContext, Node, "section"); string? Property = GetAttribute(CurrentContext, Node, "property"); string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "false"); if (Result != null && Ini != null && Section != null && Property != null) { bool Value = StringToBool(DefaultVal); ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini); if (ConfigIni != null) { if (!ConfigIni.GetBool(Section, Property, out Value)) { Value = StringToBool(DefaultVal); } } CurrentContext.BoolVariables[Result] = Value; } } break; case "setBoolFromPropertyContains": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Ini = GetAttribute(CurrentContext, Node, "ini"); string? Section = GetAttribute(CurrentContext, Node, "section"); string? Property = GetAttribute(CurrentContext, Node, "property"); string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "false"); string Contains = GetAttribute(CurrentContext, Node, "contains", true, true, ""); if (Result != null && Ini != null && Section != null && Property != null) { bool Value = StringToBool(DefaultVal); ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini); if (ConfigIni != null) { List? StringList; if (ConfigIni.GetArray(Section, Property, out StringList)) { Value = false; foreach (string Entry in StringList) { if (Entry.Equals(Contains)) { Value = true; break; } } } } CurrentContext.BoolVariables[Result] = Value; } } break; case "setBoolContains": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string Find = GetAttribute(CurrentContext, Node, "find", true, false, ""); if (Result != null) { CurrentContext.BoolVariables[Result] = Source.Contains(Find); } } break; case "setBoolStartsWith": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string Find = GetAttribute(CurrentContext, Node, "find", true, false, ""); if (Result != null) { CurrentContext.BoolVariables[Result] = Source.StartsWith(Find); } } break; case "setBoolEndsWith": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string Find = GetAttribute(CurrentContext, Node, "find", true, false, ""); if (Result != null) { CurrentContext.BoolVariables[Result] = Source.EndsWith(Find); } } break; case "setBoolNot": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, "false"); if (Result != null) { CurrentContext.BoolVariables[Result] = !StringToBool(Source); } } break; case "setBoolAnd": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "false"); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "false"); if (Result != null) { CurrentContext.BoolVariables[Result] = StringToBool(Arg1) && StringToBool(Arg2); } } break; case "setBoolOr": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "false"); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "false"); if (Result != null) { CurrentContext.BoolVariables[Result] = StringToBool(Arg1) || StringToBool(Arg2); } } break; case "setBoolIsEqual": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, ""); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, ""); if (Result != null) { CurrentContext.BoolVariables[Result] = Arg1.Equals(Arg2); } } break; case "setBoolIsLess": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false); string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false); if (Result != null) { CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) < StringToInt(CurrentContext, Node, Arg2)); } } break; case "setBoolIsLessEqual": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false); string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false); if (Result != null) { CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) <= StringToInt(CurrentContext, Node, Arg2)); } } break; case "setBoolIsGreater": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false); string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false); if (Result != null) { CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) > StringToInt(CurrentContext, Node, Arg2)); } } break; case "setBoolIsGreaterEqual": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false); string? Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false); if (Result != null) { CurrentContext.BoolVariables[Result] = (StringToInt(CurrentContext, Node, Arg1) >= StringToInt(CurrentContext, Node, Arg2)); } } break; case "setBoolFileExists": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? FilePath = GetAttribute(CurrentContext, Node, "file"); if (Result != null && FilePath != null) { CurrentContext.BoolVariables[Result] = File.Exists(FilePath); } } break; case "setInt": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false, "0"); if (Result != null) { CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Value); } } break; case "setIntFrom": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false, "0"); if (Result != null) { Value = ExpandVariables(CurrentContext, "$I(" + Value + ")"); CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Value); } } break; case "setIntFromProperty": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Ini = GetAttribute(CurrentContext, Node, "ini"); string? Section = GetAttribute(CurrentContext, Node, "section"); string? Property = GetAttribute(CurrentContext, Node, "property"); string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, "0"); if (Result != null && Ini != null && Section != null && Property != null) { int DefaultInt = StringToInt(CurrentContext, Node, DefaultVal); int Value = DefaultInt; ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini); if (ConfigIni != null) { if (!ConfigIni.GetInt32(Section, Property, out Value)) { Value = DefaultInt; } } CurrentContext.IntVariables[Result] = Value; } } break; case "setIntFromPropertyArrayNum": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Ini = GetAttribute(CurrentContext, Node, "ini"); string? Section = GetAttribute(CurrentContext, Node, "section"); string? Property = GetAttribute(CurrentContext, Node, "property"); if (Result != null && Ini != null && Section != null && Property != null) { int Value = 0; ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini); if (ConfigIni != null) { List? StringList; if (ConfigIni.GetArray(Section, Property, out StringList)) { Value = StringList.Count; } } CurrentContext.IntVariables[Result] = Value; } } break; case "setIntAdd": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "0"); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "0"); if (Result != null) { CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) + StringToInt(CurrentContext, Node, Arg2); } } break; case "setIntSubtract": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "0"); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "0"); if (Result != null) { CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) - StringToInt(CurrentContext, Node, Arg2); } } break; case "setIntMultiply": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "1"); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "1"); if (Result != null) { CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) * StringToInt(CurrentContext, Node, Arg2); } } break; case "setIntDivide": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, "1"); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, "1"); if (Result != null) { int Denominator = StringToInt(CurrentContext, Node, Arg2); if (Denominator == 0) { CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1); } else { CurrentContext.IntVariables[Result] = StringToInt(CurrentContext, Node, Arg1) / Denominator; } } } break; case "setIntLength": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); if (Result != null) { CurrentContext.IntVariables[Result] = Source.Length; } } break; case "setIntFindString": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string Find = GetAttribute(CurrentContext, Node, "find", true, false, ""); if (Result != null) { CurrentContext.IntVariables[Result] = Source.IndexOf(Find); } } break; case "setString": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false, ""); if (Result != null) { if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringFrom": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Value = GetAttribute(CurrentContext, Node, "value", true, false, "0"); if (Result != null) { Value = ExpandVariables(CurrentContext, "$S(" + Value + ")"); CurrentContext.StringVariables[Result] = Value; } } break; case "setStringFromEnvVar": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Value = GetAttribute(CurrentContext, Node, "value"); if (Result != null && Value != null) { CurrentContext.StringVariables[Result] = Environment.ExpandEnvironmentVariables(Value); } } break; case "setStringFromTag": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Tag = GetAttribute(CurrentContext, Node, "tag", true, false, "$"); if (Result != null) { XElement? Element = CurrentElement; if (Tag.StartsWith("$")) { if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element)) { Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } } CurrentContext.StringVariables[Result] = Element.Name.ToString(); } } break; case "setStringFromAttribute": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Tag = GetAttribute(CurrentContext, Node, "tag"); string? Name = GetAttribute(CurrentContext, Node, "name"); if (Result != null && Tag != null && Name != null) { XElement? Element = CurrentElement; if (Tag.StartsWith("$")) { if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element)) { Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } } XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Name); XAttribute? Attribute = Element.Attribute(XMLNameSpace != null ? XMLNameSpace + Name : Name); CurrentContext.StringVariables[Result] = (Attribute != null) ? Attribute.Value : ""; } } break; case "setStringFromTagText": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Tag = GetAttribute(CurrentContext, Node, "tag", true, false, "$"); if (Result != null) { XElement? Element = CurrentElement; if (Tag.StartsWith("$")) { if (Tag.Length > 1) { if (!CurrentContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element)) { if (!GlobalContext.ElementVariables.TryGetValue(Tag.Substring(1), out Element)) { Logger.LogWarning("Missing element variable '{Tag}' in '{Node}' (skipping instruction)", Tag, TraceNodeString(CurrentContext, Node)); continue; } } } } if (Element.Value == null) { Logger.LogWarning("Expected text in element '{Element}' in '{Node}' but found none (skipping instruction)", Element.Name.ToString(), TraceNodeString(CurrentContext, Node)); continue; } CurrentContext.StringVariables[Result] = Element.Value; } } break; case "setStringFromProperty": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Ini = GetAttribute(CurrentContext, Node, "ini"); string? Section = GetAttribute(CurrentContext, Node, "section"); string? Property = GetAttribute(CurrentContext, Node, "property"); string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, ""); if (Result != null && Ini != null && Section != null && Property != null) { string Value = DefaultVal; ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini); if (ConfigIni != null) { if (!ConfigIni.GetString(Section, Property, out Value)) { // If the string was not found in the config, Value will have been set to an empty string // Set it back to the DefaultVal Value = DefaultVal; } } if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringFromPropertyArray": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Ini = GetAttribute(CurrentContext, Node, "ini"); string? Section = GetAttribute(CurrentContext, Node, "section"); string? Property = GetAttribute(CurrentContext, Node, "property"); string? IndexStr = GetAttribute(CurrentContext, Node, "index", true); string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, ""); if (Result != null && Ini != null && Section != null && Property != null && IndexStr != null) { string Value = DefaultVal; ConfigCacheIni_UPL ConfigIni = GetConfigCacheIni_UPL(Ini); if (ConfigIni != null) { List? StringList; if (ConfigIni.GetArray(Section, Property, out StringList)) { int Index = StringToInt(CurrentContext, Node, IndexStr); if (Index >= 0 && Index < StringList.Count) { Value = StringList.ElementAt(Index); } } } if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringAdd": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Arg1 = GetAttribute(CurrentContext, Node, "arg1", true, false, ""); string Arg2 = GetAttribute(CurrentContext, Node, "arg2", true, false, ""); if (Result != null) { string Value = Arg1 + Arg2; if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringSubstring": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string Start = GetAttribute(CurrentContext, Node, "start", true, false, "0"); string Length = GetAttribute(CurrentContext, Node, "length", true, false, "0"); if (Result != null && Source != null) { int Index = StringToInt(CurrentContext, Node, Start); int Count = StringToInt(CurrentContext, Node, Length); Index = (Index < 0) ? 0 : (Index > Source.Length) ? Source.Length : Index; Count = (Index + Count > Source.Length) ? Source.Length - Index : Count; string Value = Source.Substring(Index, Count); if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringSubstringAfterFind": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string Find = GetAttribute(CurrentContext, Node, "find", true, false, ""); string Length = GetAttribute(CurrentContext, Node, "length", true, false, "-1"); string DefaultVal = GetAttribute(CurrentContext, Node, "default", true, false, ""); if (Result != null) { string Value = DefaultVal; int Index = Source.IndexOf(Find); if (Index != -1) { Index += Find.Length; int Count = StringToInt(CurrentContext, Node, Length); Count = Count == -1 || (Index + Count > Source.Length) ? Source.Length - Index : Count; Value = Source.Substring(Index, Count); } if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringReplace": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); string? Find = GetAttribute(CurrentContext, Node, "find"); string With = GetAttribute(CurrentContext, Node, "with", true, false, ""); if (Result != null && Find != null) { string Value = Source.Replace(Find, With); if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringToLower": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); if (Result != null) { string Value = Source.ToLower(); if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setStringToUpper": { string? Result = GetAttribute(CurrentContext, Node, "result"); string Source = GetAttribute(CurrentContext, Node, "source", true, false, ""); if (Result != null) { string Value = Source.ToUpper(); if (Result == "Output") { GlobalContext.StringVariables["Output"] = Value; } else { CurrentContext.StringVariables[Result] = Value; } } } break; case "setElement": { string? Result = GetAttribute(CurrentContext, Node, "result"); string? Value = GetAttribute(CurrentContext, Node, "value", true, false); string? Text = GetAttribute(CurrentContext, Node, "text", true, false); string? Parse = GetAttribute(CurrentContext, Node, "xml", true, false); if (Result != null) { if (Value != null) { XNamespace? XMLNameSpace = TrimNamespaceAliasFromName(ref Value); XElement NewElement = new XElement(XMLNameSpace != null ? XMLNameSpace + Value : Value); if (Text != null) { NewElement.Value = Text; } CurrentContext.ElementVariables[Result] = NewElement; } else if (Parse != null) { try { CurrentContext.ElementVariables[Result] = XElement.Parse(Parse); } catch (Exception e) { Logger.LogError(e, "XML parsing {Parse} failed! {Ex} (skipping instruction)", Parse, e); } } } } break; default: Logger.LogWarning("Unknown command: {Name}", Node.Name); break; } } return GlobalContext.StringVariables["Output"]; } public void Init(IEnumerable Architectures, bool bDistribution, string EngineDirectory, string BuildDirectory, string ProjectDirectory, string Configuration, bool bIsEmbedded, bool bPerArchBuildDir = false, Dictionary? ArchRemapping = null) { GlobalContext.BoolVariables["Distribution"] = bDistribution; GlobalContext.BoolVariables["IsEmbedded"] = bIsEmbedded; GlobalContext.StringVariables["Configuration"] = Configuration; GlobalContext.StringVariables["EngineDir"] = EngineDirectory.Replace("\\", "/"); GlobalContext.StringVariables["BuildDir"] = BuildDirectory.Replace("\\", "/"); GlobalContext.StringVariables["ProjectDir"] = ProjectDirectory.Replace("\\", "/"); ReadOnlyBuildVersion Version = ReadOnlyBuildVersion.Current; GlobalContext.IntVariables["EngineMajorVersion"] = Version.MajorVersion; GlobalContext.IntVariables["EngineMinorVersion"] = Version.MinorVersion; GlobalContext.IntVariables["EnginePatchVersion"] = Version.PatchVersion; GlobalContext.IntVariables["EngineChangelist"] = Version.Changelist; GlobalContext.StringVariables["EngineVersion"] = Version.MajorVersion.ToString() + "." + Version.MinorVersion.ToString() + "." + Version.PatchVersion.ToString(); GlobalContext.StringVariables["EngineBranchName"] = Version.BranchName ?? ""; if (GlobalContext.StringVariables["EngineDir"].Length < 1) { GlobalContext.StringVariables["EngineDir"] = "./"; } if (GlobalContext.StringVariables["BuildDir"].Length < 1) { GlobalContext.StringVariables["BuildDir"] = "./"; } if (GlobalContext.StringVariables["ProjectDir"].Length < 1) { GlobalContext.StringVariables["ProjectDir"] = "./"; } GlobalContext.StringVariables["AbsEngineDir"] = Path.GetFullPath(GlobalContext.StringVariables["EngineDir"]).Replace("\\", "/"); GlobalContext.StringVariables["AbsBuildDir"] = Path.GetFullPath(GlobalContext.StringVariables["BuildDir"]).Replace("\\", "/"); GlobalContext.StringVariables["AbsProjectDir"] = Path.GetFullPath(GlobalContext.StringVariables["ProjectDir"]).Replace("\\", "/"); foreach (string Arch in Architectures) { if (bPerArchBuildDir) { string ActiveArch = Arch; if (ArchRemapping != null && ArchRemapping.ContainsKey(ActiveArch)) { ActiveArch = ArchRemapping[ActiveArch]; } string ArchBuildDirectory = Path.Combine(GlobalContext.StringVariables["BuildDir"], ActiveArch.Replace("-", "_")); string ArchBuildDir = ArchBuildDirectory.Replace("\\", "/"); string ArchAbsBuildDir = Path.GetFullPath(ArchBuildDir).Replace("\\", "/"); // add it to all the architecture contexts (overrides global context) for (int Index = 1; Index <= ContextIndex; Index++) { UPLContext ArchContext = Contexts[Arch + "_" + Index]; ArchContext.StringVariables["BuildDir"] = ArchBuildDir; ArchContext.StringVariables["AbsBuildDir"] = ArchAbsBuildDir; } } Logger.LogInformation("UPL Init: {Arch}", Arch); ProcessPluginNode(Arch, "init", ""); } if (bGlobalTrace) { Logger.LogInformation("\nVariables:\n{Variables}", DumpVariables()); } } public void SetGlobalContextVariable(string VariableName, bool Value) { if (VariableName != null) { GlobalContext.BoolVariables[VariableName] = Value; } } public void SetGlobalContextVariable(string VariableName, int Value) { if (VariableName != null) { GlobalContext.IntVariables[VariableName] = Value; } } public void SetGlobalContextVariable(string VariableName, string Value) { if (VariableName != null && Value != null) { GlobalContext.StringVariables[VariableName] = Value; } } } /// /// Equivalent of FConfigCacheIni_UPL. Parses ini files. This version reads ALL sections since ConfigCacheIni_UPL does NOT /// class ConfigCacheIni_UPL { /// /// Exception when parsing ini files /// public class IniParsingException : Exception { public IniParsingException(string Message) : base(Message) { } public IniParsingException(string Format, params object[] Args) : base(String.Format(Format, Args)) { } } /// /// command class for being able to create config caches over and over without needing to read the ini files /// public class Command { public string? TrimmedLine; } class SectionCommand : Command { public FileReference? Filename; public int LineIndex; } class KeyValueCommand : Command { public string? Key; public string? Value; public ParseAction LastAction; } // cached ini files static Dictionary> FileCache = new Dictionary>(); static Dictionary IniCache = new Dictionary(); static Dictionary BaseIniCache = new Dictionary(); // static creation functions for ini files public static ConfigCacheIni_UPL CreateConfigCacheIni_UPL(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference? ProjectDirectory, ILogger Logger, DirectoryReference? EngineDirectory = null) { if (EngineDirectory == null) { EngineDirectory = Unreal.EngineDirectory; } // cache base ini for use as the seed for the rest if (!BaseIniCache.ContainsKey(BaseIniName)) { BaseIniCache.Add(BaseIniName, new ConfigCacheIni_UPL(BuildHostPlatform.Current.Platform, BaseIniName, null, Logger, EngineDirectory, EngineOnly: true)); } // build the new ini and cache it for later re-use ConfigCacheIni_UPL BaseCache = BaseIniCache[BaseIniName]; string Key = GetIniPlatformName(Platform) + BaseIniName + EngineDirectory.FullName + (ProjectDirectory != null ? ProjectDirectory.FullName : ""); if (!IniCache.ContainsKey(Key)) { IniCache.Add(Key, new ConfigCacheIni_UPL(Platform, BaseIniName, ProjectDirectory, Logger, EngineDirectory, BaseCache: BaseCache)); } return IniCache[Key]; } /// /// List of values (or a single value) /// public class IniValues : List { public IniValues() { } public IniValues(IniValues Other) : base(Other) { } public override string ToString() { return String.Join(",", ToArray()); } } /// /// Ini section (map of keys and values) /// public class IniSection : Dictionary { public IniSection() : base(StringComparer.InvariantCultureIgnoreCase) { } public IniSection(IniSection Other) : this() { foreach (KeyValuePair Pair in Other) { Add(Pair.Key, new IniValues(Pair.Value)); } } public override string ToString() { return "IniSection"; } } /// /// True if we are loading a hierarchy of config files that should be merged together /// bool bIsMergingConfigs; /// /// All sections parsed from ini file /// Dictionary Sections; private ConfigCacheIni_UPL() { Sections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); } /// /// Constructor. Parses a single ini file. No Platform settings, no engine hierarchy. Do not use this with ini files that have hierarchy! /// /// The ini file to load /// public ConfigCacheIni_UPL(FileReference Filename, ILogger Logger) : this() { bIsMergingConfigs = false; ParseIniFile(Filename, Logger); } /// /// Constructor. Parses ini hierarchy for the specified project. No Platform settings. /// /// Ini name (Engine, Editor, etc) /// Project path /// Logger for /// public ConfigCacheIni_UPL(string BaseIniName, string? ProjectDirectory, ILogger Logger, string? EngineDirectory = null) : this(BuildHostPlatform.Current.Platform, BaseIniName, ProjectDirectory, Logger, EngineDirectory) { } /// /// Constructor. Parses ini hierarchy for the specified project. No Platform settings. /// /// Ini name (Engine, Editor, etc) /// Project path /// Logger for output /// public ConfigCacheIni_UPL(string BaseIniName, DirectoryReference ProjectDirectory, ILogger Logger, DirectoryReference? EngineDirectory = null) : this(BuildHostPlatform.Current.Platform, BaseIniName, ProjectDirectory, Logger, EngineDirectory) { } /// /// Constructor. Parses ini hierarchy for the specified platform and project. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) /// /// public ConfigCacheIni_UPL(UnrealTargetPlatform Platform, string BaseIniName, string? ProjectDirectory, ILogger Logger, string? EngineDirectory = null) : this(Platform, BaseIniName, (ProjectDirectory == null) ? null : new DirectoryReference(ProjectDirectory), Logger, (EngineDirectory == null) ? null : new DirectoryReference(EngineDirectory)) { } /// /// Constructor. Parses ini hierarchy for the specified platform and project. /// /// Project path /// Target platform /// Ini name (Engine, Editor, etc) /// /// /// /// public ConfigCacheIni_UPL(UnrealTargetPlatform Platform, string BaseIniName, DirectoryReference? ProjectDirectory, ILogger Logger, DirectoryReference? EngineDirectory = null, bool EngineOnly = false, ConfigCacheIni_UPL? BaseCache = null) : this() { bIsMergingConfigs = true; if (EngineDirectory == null) { EngineDirectory = Unreal.EngineDirectory; } if (BaseCache != null) { foreach (KeyValuePair Pair in BaseCache.Sections) { Sections.Add(Pair.Key, new IniSection(Pair.Value)); } } if (EngineOnly) { foreach (FileReference IniFileName in EnumerateEngineIniFileNames(EngineDirectory, BaseIniName)) { if (FileReference.Exists(IniFileName)) { ParseIniFile(IniFileName, Logger); } } } else { foreach (FileReference IniFileName in EnumerateCrossPlatformIniFileNames(ProjectDirectory, EngineDirectory, Platform, BaseIniName, BaseCache != null)) { if (FileReference.Exists(IniFileName)) { ParseIniFile(IniFileName, Logger); } } } } /// /// Finds a section in INI /// /// /// Found section or null public IniSection? FindSection(string SectionName) { IniSection? Section; Sections.TryGetValue(SectionName, out Section); return Section; } /// /// Finds values associated with the specified key (does not copy the list) /// private bool GetList(string SectionName, string Key, [NotNullWhen(true)] out IniValues? Value) { Value = null; IniSection? Section = FindSection(SectionName); if (Section == null) { return false; } if (Section.TryGetValue(Key, out Value) && Value != null) { return true; } return false; } /// /// Gets all values associated with the specified key /// /// Section where the key is located /// Key name /// Copy of the list containing all values associated with the specified key /// True if the key exists public bool GetArray(string SectionName, string Key, [NotNullWhen(true)] out List? Value) { Value = null; if (GetList(SectionName, Key, out IniValues? ValueList) && ValueList != null) { Value = new List(ValueList); return true; } return false; } /// /// Gets a single string value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetString(string SectionName, string Key, out string Value) { Value = String.Empty; IniValues? ValueList; bool Result = GetList(SectionName, Key, out ValueList); if (Result && ValueList != null && ValueList.Count > 0) { Value = ValueList[0]; Result = true; } else { Result = false; } return Result; } /// /// Gets a single bool value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetBool(string SectionName, string Key, out bool Value) { Value = false; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { // C# Boolean type expects "False" or "True" but since we're not case sensitive, we need to suppor that manually if (String.Equals(TextValue, "true", StringComparison.CurrentCultureIgnoreCase) || String.Equals(TextValue, "1")) { Value = true; } else if (String.Equals(TextValue, "false", StringComparison.CurrentCultureIgnoreCase) || String.Equals(TextValue, "0")) { Value = false; } else { // Failed to parse Result = false; } } return Result; } /// /// Gets a single Int32 value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetInt32(string SectionName, string Key, out int Value) { Value = 0; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Int32.TryParse(TextValue, out Value); } return Result; } /// /// Gets a single GUID value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetGUID(string SectionName, string Key, out Guid Value) { Value = Guid.Empty; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { string HexString = ""; if (TextValue.Contains("A=") && TextValue.Contains("B=") && TextValue.Contains("C=") && TextValue.Contains("D=")) { char[] Separators = new char[] { '(', ')', '=', ',', ' ', 'A', 'B', 'C', 'D' }; string[] ComponentValues = TextValue.Split(Separators, StringSplitOptions.RemoveEmptyEntries); if (ComponentValues.Length == 4) { for (int ComponentIndex = 0; ComponentIndex < 4; ComponentIndex++) { int IntegerValue; Result &= Int32.TryParse(ComponentValues[ComponentIndex], out IntegerValue); HexString += IntegerValue.ToString("X8"); } } } else { HexString = TextValue; } try { Value = Guid.ParseExact(HexString, "N"); Result = true; } catch (Exception) { Result = false; } } return Result; } /// /// Gets a single float value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetSingle(string SectionName, string Key, out float Value) { Value = 0.0f; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Single.TryParse(TextValue, out Value); } return Result; } /// /// Gets a single double value associated with the specified key. /// /// Section name /// Key name /// Value associated with the specified key. If the key has more than one value, only the first one is returned /// True if the key exists public bool GetDouble(string SectionName, string Key, out double Value) { Value = 0.0; string TextValue; bool Result = GetString(SectionName, Key, out TextValue); if (Result) { Result = Double.TryParse(TextValue, out Value); } return Result; } private static bool ExtractPath(string Source, out string Path) { int start = Source.IndexOf('"'); int end = Source.LastIndexOf('"'); if (start != 1 && end != -1 && start < end) { ++start; Path = Source.Substring(start, end - start); return true; } else { Path = ""; } return false; } public bool GetPath(string SectionName, string Key, out string Value) { string temp; if (GetString(SectionName, Key, out temp)) { return ExtractPath(temp, out Value); } else { Value = ""; } return false; } /// /// List of actions that can be performed on a single line from ini file /// enum ParseAction { None, New, Add, Remove } /// /// Checks what action should be performed on a single line from ini file /// private ParseAction GetActionForLine(ref string Line) { if (String.IsNullOrEmpty(Line) || Line.StartsWith(";") || Line.StartsWith("//")) { return ParseAction.None; } else if (Line.StartsWith("-")) { Line = Line.Substring(1).TrimStart(); return ParseAction.Remove; } else if (Line.StartsWith("+")) { Line = Line.Substring(1).TrimStart(); return ParseAction.Add; } else { // We use Add rather than New when we're not merging config files together in order // to mimic the behavior of the C++ config cache when loading a single file return (bIsMergingConfigs) ? ParseAction.New : ParseAction.Add; } } /// /// Loads and parses ini file. /// public void ParseIniFile(FileReference Filename, ILogger Logger) { string[]? IniLines = null; List? Commands = null; if (!FileCache.ContainsKey(Filename.FullName)) { try { IniLines = File.ReadAllLines(Filename.FullName); Commands = new List(); FileCache.Add(Filename.FullName, Commands); } catch (Exception ex) { Logger.LogInformation("Error reading ini file: {Filename} Exception: {Ex}", Filename, ex.Message); } } else { Commands = FileCache[Filename.FullName]; } if (IniLines != null && Commands != null) { IniSection? CurrentSection = null; // Line Index for exceptions int LineIndex = 1; bool bMultiLine = false; string SingleValue = ""; string Key = ""; ParseAction LastAction = ParseAction.None; // Parse each line foreach (string Line in IniLines) { string TrimmedLine = Line.Trim(); // Multiline value support bool bWasMultiLine = bMultiLine; bMultiLine = TrimmedLine.EndsWith("\\"); if (bMultiLine) { TrimmedLine = TrimmedLine.Substring(0, TrimmedLine.Length - 1).TrimEnd(); } if (!bWasMultiLine) { if (TrimmedLine.StartsWith("[")) { CurrentSection = FindOrAddSection(TrimmedLine, Filename, LineIndex); LastAction = ParseAction.None; if (CurrentSection != null) { SectionCommand Command = new SectionCommand(); Command.Filename = Filename; Command.LineIndex = LineIndex; Command.TrimmedLine = TrimmedLine; Commands.Add(Command); } } else { if (LastAction != ParseAction.None) { throw new IniParsingException("Parsing new key/value pair when the previous one has not yet been processed ({0}, {1}) in {2}, line {3}: {4}", Key, SingleValue, Filename, LineIndex, TrimmedLine); } // Check if the line is empty or a comment, also remove any +/- markers LastAction = GetActionForLine(ref TrimmedLine); if (LastAction != ParseAction.None) { /* if (CurrentSection == null) { throw new IniParsingException("Trying to parse key/value pair that doesn't belong to any section in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); }*/ ParseKeyValuePair(TrimmedLine, Filename, LineIndex, out Key, out SingleValue); } } } if (bWasMultiLine) { SingleValue += TrimmedLine; } if (!bMultiLine && LastAction != ParseAction.None && CurrentSection != null) { ProcessKeyValuePair(CurrentSection, Key, SingleValue, LastAction); KeyValueCommand Command = new KeyValueCommand(); Command.Key = Key; Command.Value = SingleValue; Command.LastAction = LastAction; Commands.Add(Command); LastAction = ParseAction.None; SingleValue = ""; Key = ""; } else if (CurrentSection == null) { LastAction = ParseAction.None; } LineIndex++; } } else if (Commands != null) { IniSection? CurrentSection = null; // run each command for (int Idx = 0; Idx < Commands.Count; ++Idx) { Command Command = Commands[Idx]; if (Command is SectionCommand SectionCommand) { CurrentSection = FindOrAddSection(SectionCommand.TrimmedLine!, SectionCommand.Filename!, SectionCommand.LineIndex); } else if (Command is KeyValueCommand KeyValueCommand) { ProcessKeyValuePair(CurrentSection!, KeyValueCommand.Key!, KeyValueCommand.Value!, KeyValueCommand.LastAction); } } } } /// /// Splits a line into key and value /// private static void ParseKeyValuePair(string TrimmedLine, FileReference Filename, int LineIndex, out string Key, out string Value) { int AssignIndex = TrimmedLine.IndexOf('='); if (AssignIndex < 0) { throw new IniParsingException("Failed to find value when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Key = TrimmedLine.Substring(0, AssignIndex).Trim(); if (String.IsNullOrEmpty(Key)) { throw new IniParsingException("Empty key when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Value = TrimmedLine.Substring(AssignIndex + 1).Trim(); if (Value.StartsWith("\"")) { // Remove quotes int QuoteEnd = Value.LastIndexOf('\"'); if (QuoteEnd == 0) { throw new IniParsingException("Mismatched quotes when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } Value = Value.Substring(1, Value.Length - 2); } } /// /// Processes parsed key/value pair /// private static void ProcessKeyValuePair(IniSection CurrentSection, string Key, string SingleValue, ParseAction Action) { switch (Action) { case ParseAction.New: { // New/replace IniValues? Value; if (CurrentSection.TryGetValue(Key, out Value) == false) { Value = new IniValues(); CurrentSection.Add(Key, Value); } Value.Clear(); Value.Add(SingleValue); } break; case ParseAction.Add: { IniValues? Value; if (CurrentSection.TryGetValue(Key, out Value) == false) { Value = new IniValues(); CurrentSection.Add(Key, Value); } Value.Add(SingleValue); } break; case ParseAction.Remove: { IniValues? Value; if (CurrentSection.TryGetValue(Key, out Value)) { int ExistingIndex = Value.FindIndex(X => (String.Equals(SingleValue, X, StringComparison.CurrentCultureIgnoreCase))); if (ExistingIndex >= 0) { Value.RemoveAt(ExistingIndex); } } } break; } } /// /// Finds an existing section or adds a new one /// private IniSection FindOrAddSection(string TrimmedLine, FileReference Filename, int LineIndex) { int SectionEndIndex = TrimmedLine.IndexOf(']'); if (SectionEndIndex < 1) { throw new IniParsingException("Mismatched brackets when parsing section name in {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } // comment could follow the ] but will just be trimmed out string SectionName = TrimmedLine.Substring(1, SectionEndIndex - 1); if (String.IsNullOrEmpty(SectionName)) { throw new IniParsingException("Empty section name when parsing {0}, line {1}: {2}", Filename, LineIndex, TrimmedLine); } { IniSection? CurrentSection; if (Sections.TryGetValue(SectionName, out CurrentSection) == false) { CurrentSection = new IniSection(); Sections.Add(SectionName, CurrentSection); } return CurrentSection; } } /// /// Returns a list of INI filenames for the engine /// private static IEnumerable EnumerateEngineIniFileNames(DirectoryReference EngineDirectory, string BaseIniName) { // Engine/Config/Base.ini (included in every ini type, required) yield return FileReference.Combine(EngineDirectory, "Config", "Base.ini"); // Engine/Config/Base* ini yield return FileReference.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Base* ini yield return FileReference.Combine(EngineDirectory, "Restricted", "NotForLicensees", "Config", "Base" + BaseIniName + ".ini"); } /// /// Returns a list of INI filenames for the given project /// private static IEnumerable EnumerateCrossPlatformIniFileNames(DirectoryReference? ProjectDirectory, DirectoryReference EngineDirectory, UnrealTargetPlatform Platform, string BaseIniName, bool SkipEngine) { if (!SkipEngine) { // Engine/Config/Base.ini (included in every ini type, required) yield return FileReference.Combine(EngineDirectory, "Config", "Base.ini"); // Engine/Config/Base* ini yield return FileReference.Combine(EngineDirectory, "Config", "Base" + BaseIniName + ".ini"); // Engine/Config/NotForLicensees/Base* ini yield return FileReference.Combine(EngineDirectory, "Restricted", "NotForLicensees", "Config", "Base" + BaseIniName + ".ini"); // Engine/Config/NoRedist/Base* ini yield return FileReference.Combine(EngineDirectory, "Restricted", "NoRedist", "Config", "Base" + BaseIniName + ".ini"); } if (ProjectDirectory != null) { // Game/Config/Default* ini yield return FileReference.Combine(ProjectDirectory, "Config", "Default" + BaseIniName + ".ini"); // Game/Config/NotForLicensees/Default* ini yield return FileReference.Combine(ProjectDirectory, "Restricted", "NotForLicensees", "Config", "Default" + BaseIniName + ".ini"); // Game/Config/NoRedist/Default* ini yield return FileReference.Combine(ProjectDirectory, "Restricted", "NoRedist", "Config", "Default" + BaseIniName + ".ini"); } string PlatformName = GetIniPlatformName(Platform); // Engine/Config/Platform/Platform* ini yield return FileReference.Combine(EngineDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); if (ProjectDirectory != null) { // Game/Config/Platform/Platform* ini yield return FileReference.Combine(ProjectDirectory, "Config", PlatformName, PlatformName + BaseIniName + ".ini"); } DirectoryReference? UserSettingsFolder = Unreal.UserSettingDirectory; // Match FPlatformProcess::UserSettingsDir() DirectoryReference? PersonalFolder = Unreal.UserDirectory; // Match FPlatformProcess::UserDir() if (UserSettingsFolder != null) { // /Unreal/EngineConfig/User* ini yield return FileReference.Combine(UserSettingsFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); } if (PersonalFolder != null) { // /Unreal/EngineConfig/User* ini yield return FileReference.Combine(PersonalFolder, "Unreal Engine", "Engine", "Config", "User" + BaseIniName + ".ini"); } // Game/Config/User* ini if (ProjectDirectory != null) { yield return FileReference.Combine(ProjectDirectory, "Config", "User" + BaseIniName + ".ini"); } } /// /// Returns the platform name to use as part of platform-specific config files /// private static string GetIniPlatformName(UnrealTargetPlatform TargetPlatform) { if (TargetPlatform == UnrealTargetPlatform.Win64) { return "Windows"; } else { return TargetPlatform.ToString(); } } } }