Skip to content

Commit 37d9e94

Browse files
Add prefab / scene enum source gen
1 parent 10e3619 commit 37d9e94

File tree

6 files changed

+310
-0
lines changed

6 files changed

+310
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[*.cs]
2+
3+
# IDE0058: Expression value is never used
4+
dotnet_diagnostic.IDE0058.severity = silent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Visual Studio ignores
2+
.vs/*
3+
Properties/*
4+
bin/
5+
obj/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Diagnostics;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
/*
11+
Source Generators are separate libraries from the main Godot project.
12+
They target the .netstandard2.0 framework and are Class Libraries,
13+
not Console Applications. They should be in their own separate projects.
14+
15+
To loop through files from the main project, add the following to the
16+
main project's .csproj file:
17+
18+
<ItemGroup>
19+
<AdditionalFiles Include="Scenes\Prefabs\**\*.tscn" />
20+
</ItemGroup>
21+
22+
Additionally, include the following to link the source generator:
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\SourceGenerators\MySourceGenerator\MySourceGenerator.csproj"
26+
OutputItemType="Analyzer"
27+
ReferenceOutputAssembly="false" />
28+
</ItemGroup>
29+
30+
For debugging the output of the source generator, in the main project in
31+
VS2022 Solution Explorer, navigate to Dependencies > Analyzers >
32+
MySourceGenerator > ... > Prefabs.g.cs.
33+
34+
Note: The entire VS2022 IDE must be closed and re-opened if the source
35+
generator is rebuilt.
36+
37+
To avoid this inconvenience, add the following to the main .csproj file:
38+
39+
<PropertyGroup>
40+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
41+
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
42+
</PropertyGroup>
43+
44+
This will place the Prefabs.g.cs file in the Generated folder in the root
45+
folder of the main Godot project. You will not need to close and re-open
46+
VS2022 whenever the source generator and main Godot projects are rebuilt.
47+
48+
Remember to delete the Generated/ folder and set <EmitCompilerGeneratedFiles>
49+
to false when you are done debugging the source generator output or you will
50+
run into a duplicate scripts error in the assembly later on.
51+
52+
Always build the source generator project first, followed by the main
53+
Godot project.
54+
*/
55+
56+
namespace MySourceGenerator
57+
{
58+
[Generator]
59+
public class GameAssetsSrcGen : ISourceGenerator
60+
{
61+
public void Initialize(GeneratorInitializationContext context)
62+
{
63+
// No initialization required for this generator
64+
}
65+
66+
public void Execute(GeneratorExecutionContext context)
67+
{
68+
// Get all additional text files
69+
IEnumerable<AdditionalText> tscnFiles = context.AdditionalFiles
70+
.Where(file => Path.GetExtension(file.Path)
71+
.Equals(".tscn", StringComparison.OrdinalIgnoreCase));
72+
73+
char sep = Path.DirectorySeparatorChar;
74+
75+
// Separate tscn files into Prefabs and Scenes
76+
IEnumerable<AdditionalText> prefabFiles = tscnFiles.Where(file => file.Path.Contains($"{sep}Prefabs{sep}"));
77+
IEnumerable<AdditionalText> sceneFiles = tscnFiles.Where(file => file.Path.Contains($"{sep}Scenes{sep}") && !file.Path.Contains($"{sep}Prefabs{sep}"));
78+
79+
// Generate the Prefabs class
80+
string prefabSourceCode = GeneratePrefabsClass(context, prefabFiles);
81+
82+
// Generate the Scenes class
83+
string sceneSourceCode = GenerateScenesClass(context, sceneFiles);
84+
85+
// Add the generated source code to the compilation
86+
context.AddSource("Prefabs.g.cs", SourceText.From(prefabSourceCode, Encoding.UTF8));
87+
context.AddSource("Scenes.g.cs", SourceText.From(sceneSourceCode, Encoding.UTF8));
88+
}
89+
90+
private static string GeneratePrefabsClass(GeneratorExecutionContext context, IEnumerable<AdditionalText> tscnFiles)
91+
{
92+
StringBuilder sb = new StringBuilder();
93+
string rootFolderName = GetRootFolderName(context);
94+
List<string> relativePaths = new List<string>();
95+
List<string> enumNames = new List<string>();
96+
97+
foreach (AdditionalText file in tscnFiles)
98+
{
99+
string relativePath = GetRelativePath(file.Path, rootFolderName);
100+
string enumName = GetEnumName(relativePath, "Prefabs");
101+
102+
relativePaths.Add(relativePath);
103+
enumNames.Add(enumName);
104+
}
105+
106+
sb.AppendLine($"namespace {context.Compilation.AssemblyName};");
107+
sb.AppendLine();
108+
sb.AppendLine("using System.Collections.Generic;");
109+
sb.AppendLine();
110+
sb.AppendLine("public enum Prefab");
111+
sb.AppendLine("{");
112+
113+
for (int i = 0; i < enumNames.Count; i++)
114+
{
115+
sb.AppendLine($" {enumNames[i]},");
116+
}
117+
118+
sb.AppendLine("}");
119+
sb.AppendLine();
120+
121+
sb.AppendLine("public static class MapPrefabsToPaths");
122+
sb.AppendLine("{");
123+
sb.AppendLine(" private static readonly Dictionary<Prefab, string> prefabPaths = new Dictionary<Prefab, string>");
124+
sb.AppendLine(" {");
125+
126+
for (int i = 0; i < enumNames.Count; i++)
127+
{
128+
string resourcePath = $"res://{relativePaths[i]}";
129+
sb.AppendLine($" {{ Prefab.{enumNames[i]}, \"{resourcePath}\" }},");
130+
}
131+
132+
sb.AppendLine(" };");
133+
sb.AppendLine();
134+
sb.AppendLine(" public static string GetPath(Prefab prefab)");
135+
sb.AppendLine(" {");
136+
sb.AppendLine(" if (prefabPaths.TryGetValue(prefab, out string path))");
137+
sb.AppendLine(" {");
138+
sb.AppendLine(" return path;");
139+
sb.AppendLine(" }");
140+
sb.AppendLine();
141+
sb.AppendLine(" return null;");
142+
sb.AppendLine(" }");
143+
144+
sb.AppendLine("}");
145+
146+
return sb.ToString();
147+
}
148+
149+
private static string GenerateScenesClass(GeneratorExecutionContext context, IEnumerable<AdditionalText> tscnFiles)
150+
{
151+
StringBuilder sb = new StringBuilder();
152+
string rootFolderName = GetRootFolderName(context);
153+
List<string> relativePaths = new List<string>();
154+
List<string> enumNames = new List<string>();
155+
156+
foreach (AdditionalText file in tscnFiles)
157+
{
158+
string relativePath = GetRelativePath(file.Path, rootFolderName);
159+
string enumName = GetEnumName(relativePath, "Scenes");
160+
161+
relativePaths.Add(relativePath);
162+
enumNames.Add(enumName);
163+
}
164+
165+
sb.AppendLine($"namespace {context.Compilation.AssemblyName};");
166+
sb.AppendLine();
167+
sb.AppendLine("using System.Collections.Generic;");
168+
sb.AppendLine();
169+
sb.AppendLine("public enum Scene");
170+
sb.AppendLine("{");
171+
172+
for (int i = 0; i < enumNames.Count; i++)
173+
{
174+
sb.AppendLine($" {enumNames[i]},");
175+
}
176+
177+
sb.AppendLine("}");
178+
sb.AppendLine();
179+
180+
sb.AppendLine("public static class MapScenesToPaths");
181+
sb.AppendLine("{");
182+
sb.AppendLine(" private static readonly Dictionary<Scene, string> scenePaths = new Dictionary<Scene, string>");
183+
sb.AppendLine(" {");
184+
185+
for (int i = 0; i < enumNames.Count; i++)
186+
{
187+
string resourcePath = $"res://{relativePaths[i]}";
188+
sb.AppendLine($" {{ Scene.{enumNames[i]}, \"{resourcePath}\" }},");
189+
}
190+
191+
sb.AppendLine(" };");
192+
sb.AppendLine();
193+
sb.AppendLine(" public static string GetPath(Scene scene)");
194+
sb.AppendLine(" {");
195+
sb.AppendLine(" if (scenePaths.TryGetValue(scene, out string path))");
196+
sb.AppendLine(" {");
197+
sb.AppendLine(" return path;");
198+
sb.AppendLine(" }");
199+
sb.AppendLine();
200+
sb.AppendLine(" return null;");
201+
sb.AppendLine(" }");
202+
203+
sb.AppendLine("}");
204+
205+
return sb.ToString();
206+
}
207+
208+
private static string GetRootFolderName(GeneratorExecutionContext context)
209+
{
210+
if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.projectdir", out string projectDir))
211+
{
212+
return Path.GetFileName(projectDir.TrimEnd('\\', '/'));
213+
}
214+
215+
Debug.Print("Could not find goodot root folder! Defaulting to current directory.");
216+
return "";
217+
}
218+
219+
private static string GetRelativePath(string filePath, string rootFolderName)
220+
{
221+
string normalizedFilePath = filePath.Replace("\\", "/");
222+
string rootFolderIdentifier = $"{rootFolderName.ToLower()}/";
223+
int rootFolderIndex = normalizedFilePath.ToLower().IndexOf(rootFolderIdentifier);
224+
225+
Debug.Assert(rootFolderIndex != -1, $"Root folder identifier not found in file path: {normalizedFilePath}");
226+
227+
return normalizedFilePath.Substring(rootFolderIndex + rootFolderIdentifier.Length);
228+
}
229+
230+
private static string GetEnumName(string relativePath, string folderName)
231+
{
232+
string folderIdentifier = folderName + "/";
233+
string enumName = relativePath
234+
.Substring(relativePath.IndexOf(folderIdentifier) + folderIdentifier.Length)
235+
.Replace("/", "_")
236+
.Replace(".tscn", "")
237+
.SnakeCaseToPascalCase();
238+
239+
return enumName;
240+
}
241+
}
242+
}
243+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
10+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
11+
</ItemGroup>
12+
13+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.11.35219.272
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySourceGenerator", "MySourceGenerator.csproj", "{8DC7B967-1D70-4859-A250-5B7112A2F51B}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{8DC7B967-1D70-4859-A250-5B7112A2F51B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{8DC7B967-1D70-4859-A250-5B7112A2F51B}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{8DC7B967-1D70-4859-A250-5B7112A2F51B}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{8DC7B967-1D70-4859-A250-5B7112A2F51B}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {499373B5-196F-46BA-A3BF-A958282AED3E}
24+
EndGlobalSection
25+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading;
6+
7+
namespace MySourceGenerator
8+
{
9+
public static class StringExtensions
10+
{
11+
public static string SnakeCaseToPascalCase(this string str)
12+
{
13+
return string.Concat(
14+
str.Split('_')
15+
.Select(Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase)
16+
);
17+
}
18+
}
19+
}
20+

0 commit comments

Comments
 (0)