using System.Xml; using Newtonsoft.Json; namespace UnrealSharpBuildTool.Actions; public class GenerateProject : BuildToolAction { private string _projectPath = string.Empty; private string _projectFolder = string.Empty; private string _projectRoot = string.Empty; bool ContainsUPluginOrUProjectFile(string folder) { string[] files = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories); foreach (string file in files) { if (file.EndsWith(".uplugin", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public override bool RunAction() { string folder = Program.TryGetArgument("NewProjectFolder"); _projectRoot = Program.TryGetArgument("ProjectRoot"); if (!ContainsUPluginOrUProjectFile(_projectRoot)) { throw new InvalidOperationException("Project folder must contain a .uplugin or .uproject file."); } if (folder == _projectRoot) { folder = Path.Combine(folder, "Script"); } string projectName = Program.TryGetArgument("NewProjectName"); string csProjFileName = $"{projectName}.csproj"; if (!Directory.Exists(folder)) { Directory.CreateDirectory(folder); } _projectFolder = Path.Combine(folder, projectName); _projectPath = Path.Combine(_projectFolder, csProjFileName); string version = Program.GetVersion(); using BuildToolProcess generateProjectProcess = new BuildToolProcess(); // Create a class library. generateProjectProcess.StartInfo.ArgumentList.Add("new"); generateProjectProcess.StartInfo.ArgumentList.Add("classlib"); // Assign project name to the class library. generateProjectProcess.StartInfo.ArgumentList.Add("-n"); generateProjectProcess.StartInfo.ArgumentList.Add(projectName); // Set the target framework to the current version. generateProjectProcess.StartInfo.ArgumentList.Add("-f"); generateProjectProcess.StartInfo.ArgumentList.Add(version); generateProjectProcess.StartInfo.WorkingDirectory = folder; if (!generateProjectProcess.StartBuildToolProcess()) { return false; } // dotnet new class lib generates a file named Class1, remove it. string myClassFile = Path.Combine(_projectFolder, "Class1.cs"); if (File.Exists(myClassFile)) { File.Delete(myClassFile); } string slnPath = Program.GetSolutionFile(); if (!File.Exists(slnPath)) { GenerateSolution generateSolution = new GenerateSolution(); generateSolution.RunAction(); } if (Program.HasArgument("SkipUSharpProjSetup")) { return true; } AddLaunchSettings(); ModifyCSProjFile(); string relativePath = Path.GetRelativePath(Program.GetScriptFolder(), _projectPath); AddProjectToSln(relativePath); return true; } public static void AddProjectToSln(string relativePath) { AddProjectToSln([relativePath]); } public static void AddProjectToSln(List relativePaths) { foreach (IGrouping projects in GroupPathsBySolutionFolder(relativePaths)) { using BuildToolProcess addProjectToSln = new BuildToolProcess(); addProjectToSln.StartInfo.ArgumentList.Add("sln"); addProjectToSln.StartInfo.ArgumentList.Add("add"); foreach (string relativePath in projects) { addProjectToSln.StartInfo.ArgumentList.Add(relativePath); } addProjectToSln.StartInfo.ArgumentList.Add("-s"); addProjectToSln.StartInfo.ArgumentList.Add(projects.Key); addProjectToSln.StartInfo.WorkingDirectory = Program.GetScriptFolder(); addProjectToSln.StartBuildToolProcess(); } } private static IEnumerable> GroupPathsBySolutionFolder(List relativePaths) { return relativePaths.GroupBy(GetPathRelativeToProject)!; } private static string GetPathRelativeToProject(string path) { var fullPath = Path.GetFullPath(path, Program.GetScriptFolder()); var relativePath = Path.GetRelativePath(Program.GetProjectDirectory(), fullPath); var projectDirName = Path.GetDirectoryName(relativePath)!; // If we're in the script folder we want these to be in the Script solution folder, otherwise we want these to // be in the directory for the plugin itself. var containingDirName = Path.GetDirectoryName(projectDirName)!; return containingDirName == "Script" ? containingDirName : Path.GetDirectoryName(containingDirName)!; } private void ModifyCSProjFile() { try { XmlDocument csprojDocument = new XmlDocument(); csprojDocument.Load(_projectPath); if (csprojDocument.SelectSingleNode("//ItemGroup") is not XmlElement newItemGroup) { newItemGroup = csprojDocument.CreateElement("ItemGroup"); csprojDocument.DocumentElement!.AppendChild(newItemGroup); } AppendProperties(csprojDocument); AppendConstantDefines(csprojDocument); AppendReference(csprojDocument, newItemGroup, "UnrealSharp", GetPathToBinaries()); AppendReference(csprojDocument, newItemGroup, "UnrealSharp.Core", GetPathToBinaries()); AppendSourceGeneratorReference(csprojDocument, newItemGroup); if (!Program.HasArgument("SkipIncludeProjectGlue")) { AppendGeneratedCode(csprojDocument, newItemGroup); } foreach (string dependency in Program.GetArguments("Dependency")) { AddDependency(csprojDocument, newItemGroup, dependency); } csprojDocument.Save(_projectPath); } catch (Exception ex) { throw new InvalidOperationException($"An error occurred while updating the .csproj file: {ex.Message}", ex); } } private void AddProperty(string name, string value, XmlDocument doc, XmlNode propertyGroup) { XmlNode? newProperty = propertyGroup.SelectSingleNode(name); if (newProperty == null) { newProperty = doc.CreateElement(name); propertyGroup.AppendChild(newProperty); } newProperty.InnerText = value; } private void AppendProperties(XmlDocument doc) { XmlNode? propertyGroup = doc.SelectSingleNode("//PropertyGroup"); if (propertyGroup == null) { propertyGroup = doc.CreateElement("PropertyGroup"); } AddProperty("CopyLocalLockFileAssembliesName", "true", doc, propertyGroup); AddProperty("AllowUnsafeBlocks", "true", doc, propertyGroup); AddProperty("EnableDynamicLoading", "true", doc, propertyGroup); } private void AddConstDefine(string value, XmlDocument doc, XmlNode propertyGroup, string? condition = null) { var newProperty = doc.CreateElement("DefineConstants"); propertyGroup.AppendChild(newProperty); newProperty.InnerText = value; if (condition is not null) { newProperty.SetAttribute("Condition", condition); } } private void AppendConstantDefines(XmlDocument doc) { var propertyGroup = doc.CreateElement("PropertyGroup"); AddConstDefine("WITH_EDITOR", doc, propertyGroup); AddConstDefine("$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))", doc, propertyGroup, "'$(DisableWithEditor)' == 'true'"); AddConstDefine("$(DefineConstants);$(DefineAdditionalConstants)", doc, propertyGroup, "'$(DefineAdditionalConstants)' != ''"); } private string GetPathToBinaries() { string directoryPath = Path.GetDirectoryName(_projectPath)!; string unrealSharpPath = GetRelativePathToUnrealSharp(directoryPath); return Path.Combine(unrealSharpPath, "Binaries", "Managed"); } private void AppendReference(XmlDocument doc, XmlElement itemGroup, string referenceName, string binPath) { XmlElement referenceElement = doc.CreateElement("Reference"); referenceElement.SetAttribute("Include", referenceName); XmlElement hintPath = doc.CreateElement("HintPath"); hintPath.InnerText = Path.Combine(binPath, Program.GetVersion(), referenceName + ".dll"); referenceElement.AppendChild(hintPath); itemGroup.AppendChild(referenceElement); } private void AppendSourceGeneratorReference(XmlDocument doc, XmlElement itemGroup) { string sourceGeneratorPath = Path.Combine(GetPathToBinaries(), "UnrealSharp.SourceGenerators.dll"); XmlElement sourceGeneratorReference = doc.CreateElement("Analyzer"); sourceGeneratorReference.SetAttribute("Include", sourceGeneratorPath); itemGroup.AppendChild(sourceGeneratorReference); } private void AppendPackageReference(XmlDocument doc, XmlElement itemGroup, string packageName, string packageVersion) { XmlElement packageReference = doc.CreateElement("PackageReference"); packageReference.SetAttribute("Include", packageName); packageReference.SetAttribute("Version", packageVersion); itemGroup.AppendChild(packageReference); } private void AppendGeneratedCode(XmlDocument doc, XmlElement itemGroup) { string providedGlueName = Program.TryGetArgument("GlueProjectName"); string scriptFolder = string.IsNullOrEmpty(_projectRoot) ? Program.GetScriptFolder() : Path.Combine(_projectRoot, "Script"); string generatedGluePath = Path.Combine(scriptFolder, providedGlueName, $"{providedGlueName}.csproj"); AddDependency(doc, itemGroup, generatedGluePath); } private void AddDependency(XmlDocument doc, XmlElement itemGroup, string dependency) { string relativePath = GetRelativePath(_projectFolder, dependency); XmlElement generatedCode = doc.CreateElement("ProjectReference"); generatedCode.SetAttribute("Include", relativePath); itemGroup.AppendChild(generatedCode); } private string GetRelativePathToUnrealSharp(string basePath) { string targetPath = Path.Combine(basePath, Program.BuildToolOptions.PluginDirectory); return GetRelativePath(basePath, targetPath); } public static string GetRelativePath(string basePath, string targetPath) { Uri baseUri = new Uri(basePath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? basePath : basePath + Path.DirectorySeparatorChar); Uri targetUri = new Uri(targetPath); Uri relativeUri = baseUri.MakeRelativeUri(targetUri); return OperatingSystem.IsWindows() ? Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', '\\') : Uri.UnescapeDataString(relativeUri.ToString()); } void AddLaunchSettings() { string csProjectPath = Path.Combine(Program.GetScriptFolder(), _projectFolder); string propertiesDirectoryPath = Path.Combine(csProjectPath, "Properties"); string launchSettingsPath = Path.Combine(propertiesDirectoryPath, "launchSettings.json"); if (!Directory.Exists(propertiesDirectoryPath)) { Directory.CreateDirectory(propertiesDirectoryPath); } if (File.Exists(launchSettingsPath)) { return; } Program.CreateOrUpdateLaunchSettings(launchSettingsPath); } } public class Root { [JsonProperty("profiles")] public Profiles Profiles { get; set; } = new Profiles(); } public class Profiles { [JsonProperty("UnrealSharp")] public Profile ProfileName { get; set; } = new Profile(); } public class Profile { [JsonProperty("commandName")] public string CommandName { get; set; } = string.Empty; [JsonProperty("executablePath")] public string ExecutablePath { get; set; } = string.Empty; [JsonProperty("commandLineArgs")] public string CommandLineArgs { get; set; } = string.Empty; }