Nuke.Unreal
Build Unreal apps in Style.
Loading...
Searching...
No Matches
ModuleGenerator.cs
1using System.Linq;
2using System;
3using System.IO;
4using Newtonsoft.Json;
5using Newtonsoft.Json.Linq;
6using Nuke.Common.IO;
7using Nuke.Common;
8using Serilog;
9using Microsoft.CodeAnalysis;
10using Microsoft.CodeAnalysis.CSharp;
11using Microsoft.CodeAnalysis.CSharp.Syntax;
12
14{
15 public record ModuleModel(
16 string Name, string? Copyright,
17 UnrealProject Project, UnrealPlugin Plugin
18 ) : CommonModelBase(Name, Copyright);
19
21 {
22 protected ModuleModel? Model;
23 public string TemplateSubfolder => "Module";
24
25 public bool AddToTarget { get; private set; }
26
27 public ModuleGenerator SetAddToTarget(bool addToTarget)
28 {
29 AddToTarget = addToTarget;
30 return this;
31 }
32
33 public void Generate(AbsolutePath templatesPath, AbsolutePath currentFolder, string name)
34 {
35 var project = new UnrealProject(currentFolder);
36 Model = new(
37 Name: name,
38 Copyright: Unreal.ReadCopyrightFromProject(project.Folder!),
39 Project: project,
40 Plugin: new UnrealPlugin(currentFolder)
41 );
42
43 if(Directory.Exists(currentFolder / name))
44 {
45 throw new InvalidOperationException($"The module folder of {name} already exists in the current folder.");
46 }
47
48 if(!Directory.Exists(templatesPath / TemplateSubfolder))
49 templatesPath = DefaultTemplateFolder;
50
51 var templateDir = templatesPath / TemplateSubfolder;
52
53 RenderFolder(templateDir, currentFolder, Model);
54
55 if(Model.Plugin.IsValid)
56 AddModuleToPlugin();
57 else
58 AddModuleToProject();
59 }
60
61 protected void AddModuleToProjectUnit(AbsolutePath projectFile, string name)
62 {
63 var unitJson = JObject.Parse(File.ReadAllText(projectFile));
64
65 if(!unitJson.ContainsKey("Modules"))
66 {
67 unitJson.Add("Modules", new JArray());
68 }
69 if (unitJson["Modules"] is JArray modulesArray)
70 {
71 if(!modulesArray.Any(t => t["Name"]?.ToString().Equals(name, StringComparison.InvariantCultureIgnoreCase) ?? false))
72 {
73 modulesArray.Add(JObject.FromObject(new {
74 Name = name,
75 // We now just assume these here. The user can specify further if needed.
76 Type = "Runtime",
77 LoadingPhase = "Default"
78 }));
79 }
80 else throw new InvalidOperationException($"A module named {name} already exist.");
81 }
82
83 Unreal.WriteJson(unitJson, projectFile);
84 }
85
86 protected void AddModuleToPlugin()
87 {
88 var pluginFile = Model!.Plugin.Folder / $"{Model.Plugin.Name}.uplugin";
89 try
90 {
91 AddModuleToProjectUnit(pluginFile, Model.Name);
92 }
93 catch (Exception e)
94 {
95 Log.Warning($"Couldn't add module {Model.Name} to {Model.Plugin.Name}.uproject, it has to be added manually.");
96 Log.Warning(e, "Exception:");
97 }
98 }
99
100 protected void AddModuleToProject()
101 {
102 var projectFile = Model!.Project.Folder / $"{Model.Project.Name}.uproject";
103 try
104 {
105 AddModuleToProjectUnit(projectFile, Model.Name);
106 }
107 catch (Exception e)
108 {
109 Log.Warning($"Couldn't add module {Model.Name} to {Model.Project.Name}.uproject, it has to be added manually.");
110 Log.Warning(e, "Exception:");
111 }
112 if(AddToTarget)
113 {
114 var targetFile = Model.Project.Folder / "Source" / $"{Model.Project.Name}.Target.cs";
115 var editorTargetFile = Model.Project.Folder / "Source" / $"{Model.Project.Name}Editor.Target.cs";
116 try
117 {
118 AddModuleToTarget(targetFile);
119 AddModuleToTarget(editorTargetFile);
120 }
121 catch (Exception e)
122 {
123 Log.Warning($"Couldn't add module {Model.Name} to {Model.Project.Name}'s target rules, it has to be added manually.");
124 Log.Warning(e, "Exception:");
125 }
126 }
127 }
128
129 protected void AddModuleToTarget(AbsolutePath targetFile)
130 {
131 var targetText = File.ReadAllText(targetFile);
132 var targetSt = CSharpSyntaxTree.ParseText(targetText);
133
134 var targetClass = targetSt.GetCompilationUnitRoot()
135 .DescendantNodes().OfType<ClassDeclarationSyntax>()
136 .FirstOrDefault(c =>
137 c.Identifier.ValueText.EndsWith("Target", true, null)
138 && c.Identifier.ValueText.Contains(Model!.Project.Name!, StringComparison.InvariantCultureIgnoreCase)
139 );
140
141 if(targetClass == null)
142 {
143 throw new InvalidDataException($"Could not find the target rule class in {targetFile}");
144 }
145
146 var constructor = targetClass
147 .DescendantNodes().OfType<ConstructorDeclarationSyntax>()
148 .FirstOrDefault();
149
150 if(constructor == null)
151 {
152 throw new InvalidDataException($"Could not find the constructor of the target rule class in {targetFile}");
153 }
154
155 var extraModuleAdd = CSharpSyntaxTree.ParseText($"ExtraModuleNames.Add(\"{Model!.Name}\");")
156 .GetCompilationUnitRoot()
157 .DescendantNodes().OfType<ExpressionStatementSyntax>()
158 .FirstOrDefault();
159
160 if (extraModuleAdd != null)
161 {
162 var newBody = constructor.Body?.AddStatements(extraModuleAdd);
163 var newCtr = constructor.WithBody(newBody);
164
165 var root = targetSt.GetRoot().ReplaceNode(constructor, newCtr);
166
167 File.WriteAllText(targetFile, root.GetText().ToString());
168 }
169
170 }
171
172 protected void CheckInsideSourceFolder(AbsolutePath currentFolder)
173 {
174 if(!currentFolder.ToString().Contains("Source", StringComparison.InvariantCultureIgnoreCase))
175 {
176 throw new InvalidOperationException(
177 $"{currentFolder} doesn't contain a Source folder. Unreal Engine requires Module to be inside the project or plugin Source folder."
178 );
179 }
180 }
181 }
182}
static void RenderFolder(AbsolutePath templateRoot, AbsolutePath destinationFolder, object model, AbsolutePath? currentFolder=null)
Render scriban templated scaffoldings and files to destination folder.
A collection of utilities around basic functions regarding the environment of the Engine we're workin...
Definition Unreal.cs:24
static void WriteJson(object input, AbsolutePath path)
Write data in JSON with Unreal conventions of JSON format.
Definition Unreal.cs:82
static string ReadCopyrightFromProject(AbsolutePath projectFolder)
Read copyright info from the project's DefaultGame.ini
Definition Unreal.cs:321