Skip to content

Commit 7c83163

Browse files
committed
v1.0.0
1 parent 422013d commit 7c83163

18 files changed

+578
-0
lines changed

Editor.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Attributes.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace SatorImaging.UnitySourceGenerator
4+
{
5+
///<summary>NOTE: Implement "IUnitySourceGenerator" (C# 11.0)</summary>
6+
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
7+
public sealed class UnitySourceGeneratorAttribute : Attribute
8+
{
9+
public UnitySourceGeneratorAttribute()
10+
{
11+
}
12+
13+
public bool OverwriteIfFileExists { get; set; } = false;
14+
15+
}
16+
}

Editor/Attributes.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/USGContext.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using UnityEngine;
4+
5+
6+
namespace SatorImaging.UnitySourceGenerator
7+
{
8+
public class USGContext
9+
{
10+
public string AssetPath;
11+
public string OutputPath;
12+
13+
}
14+
}

Editor/USGContext.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/USGEngine.cs

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
#if UNITY_EDITOR
2+
3+
using System;
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Text;
10+
using UnityEditor;
11+
using UnityEngine;
12+
13+
14+
namespace SatorImaging.UnitySourceGenerator
15+
{
16+
public class USGEngine : AssetPostprocessor
17+
{
18+
///<summary>This will be disabled after import event automatically.</summary>
19+
public static bool IgnoreOverwriteSettingByAttribute = false;
20+
21+
22+
const string GENERATOR_PREFIX = ".";
23+
const string GENERATOR_EXT = ".g";
24+
const string GENERATOR_DIR = @"/USG.g"; // don't append last slash. this is used by .EndsWith()
25+
const string ASSETS_DIR_NAME = "Assets";
26+
const string ASSETS_DIR_SLASH = "Assets/";
27+
const string TARGET_FILE_EXT = @".cs";
28+
const string PATH_PREFIX_TO_IGNORE = @"Packages/";
29+
readonly static char[] DIR_SEPARATORS = new char[] { '\\', '/' };
30+
31+
readonly static string s_projectDirPath;
32+
static USGEngine()
33+
{
34+
s_projectDirPath = Application.dataPath.TrimEnd(DIR_SEPARATORS);
35+
if (s_projectDirPath.EndsWith(ASSETS_DIR_NAME))
36+
s_projectDirPath = s_projectDirPath.Substring(0, s_projectDirPath.Length - ASSETS_DIR_NAME.Length);
37+
}
38+
39+
readonly static HashSet<string> s_targetFilePaths = new();
40+
static void AddAppropriateTarget(string filePath)
41+
{
42+
if (!filePath.EndsWith(TARGET_FILE_EXT) ||
43+
!filePath.StartsWith(ASSETS_DIR_SLASH))
44+
{
45+
return;
46+
}
47+
s_targetFilePaths.Add(filePath);
48+
}
49+
50+
51+
void OnPreprocessAsset()
52+
{
53+
AddAppropriateTarget(assetPath);
54+
}
55+
56+
57+
// NOTE: To avoid event invoked twice on file deletion.
58+
static bool s_processingJobQueued = false;
59+
60+
static void OnPostprocessAllAssets(
61+
string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
62+
{
63+
for (int i = 0; i < importedAssets.Length; i++)
64+
{
65+
AddAppropriateTarget(importedAssets[i]);
66+
}
67+
68+
// NOTE: Do NOT handle deleted assets because Unity tracking changes perfectly.
69+
// Even if delete file while Unity shutted down, asset deletion event happens on next Unity launch.
70+
// As a result, delete/import event loops infinitely and file cannot be deleted.
71+
if (s_processingJobQueued) return;
72+
s_processingJobQueued = true;
73+
74+
AssetDatabase.Refresh(); // load updated generator class script before event
75+
EditorApplication.delayCall += ProcessingFiles;
76+
}
77+
78+
static void ProcessingFiles()
79+
{
80+
foreach (string path in s_targetFilePaths)
81+
{
82+
ProcessFile(path);
83+
}
84+
85+
if (s_targetFilePaths.Count() > 0) AssetDatabase.Refresh();
86+
s_targetFilePaths.Clear();
87+
88+
IgnoreOverwriteSettingByAttribute = false; // always turn it off.
89+
s_processingJobQueued = false;
90+
}
91+
92+
93+
///<summary>This method respects "OverwriteIfFileExists" attribute setting.</summary>
94+
///<param name="assetsRelPath">Path need to be started with "Assets/"</param>
95+
public static void ProcessFile(string assetsRelPath)
96+
{
97+
if (!File.Exists(assetsRelPath)) throw new FileNotFoundException(assetsRelPath);
98+
99+
100+
var clsName = Path.GetFileNameWithoutExtension(assetsRelPath);
101+
102+
if (!s_typeNameToInfo.ContainsKey(clsName))
103+
{
104+
if (!clsName.EndsWith(GENERATOR_EXT))
105+
return;
106+
107+
// try find generator
108+
clsName = Path.GetFileNameWithoutExtension(clsName);
109+
clsName = Path.GetExtension(clsName);
110+
111+
if (clsName.Length == 0) return;
112+
clsName = clsName.Substring(1);
113+
114+
if (!s_typeNameToInfo.ContainsKey(clsName))
115+
return;
116+
}
117+
118+
119+
var info = s_typeNameToInfo[clsName];
120+
if (info == null) return;
121+
122+
123+
// build path
124+
string outputPath = Path.Combine(s_projectDirPath, Path.GetDirectoryName(assetsRelPath)).Replace('\\', '/');
125+
if (!outputPath.EndsWith(GENERATOR_DIR)) outputPath += GENERATOR_DIR;
126+
127+
outputPath = Path.Combine(outputPath, info.OutputFileName);
128+
129+
130+
var context = new USGContext
131+
{
132+
AssetPath = assetsRelPath.Replace('\\', '/'),
133+
OutputPath = outputPath.Replace('\\', '/'),
134+
};
135+
136+
137+
// do it.
138+
var sb = new StringBuilder();
139+
sb.AppendLine($"// <auto-generated>{info.Type.Name}</auto-generated>");
140+
141+
var isSaveFile = false;
142+
try
143+
{
144+
isSaveFile = (bool)info.EmitMethod.Invoke(null, new object[] { context, sb });
145+
}
146+
catch
147+
{
148+
Debug.LogError($"[{nameof(UnitySourceGenerator)}] Unhandled Error on Emit(): {info.Type}");
149+
throw;
150+
}
151+
152+
if (!isSaveFile || sb == null || string.IsNullOrWhiteSpace(context.OutputPath))
153+
return;
154+
155+
156+
//file check
157+
var outputDir = Path.GetDirectoryName(context.OutputPath);
158+
if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);
159+
160+
161+
if (File.Exists(context.OutputPath) &&
162+
(!info.OverwriteIfFileExists && !IgnoreOverwriteSettingByAttribute)
163+
)
164+
{
165+
return;
166+
}
167+
168+
169+
File.WriteAllText(context.OutputPath, sb.ToString());
170+
Debug.Log($"[{nameof(UnitySourceGenerator)}] Generated: {context.OutputPath}");
171+
172+
}
173+
174+
175+
176+
//internals----------------------------------------------------------------------
177+
178+
static readonly BindingFlags METHOD_FLAGS =
179+
BindingFlags.NonPublic |
180+
BindingFlags.Public |
181+
BindingFlags.Static
182+
;
183+
184+
class CachedTypeInfo
185+
{
186+
public Type Type;
187+
188+
//from attribute
189+
public bool OverwriteIfFileExists;
190+
191+
//from method
192+
public MethodInfo EmitMethod;
193+
public string OutputFileName;
194+
}
195+
196+
197+
readonly static Dictionary<string, CachedTypeInfo> s_typeNameToInfo = new();
198+
199+
[InitializeOnLoadMethod]
200+
static void CollectTargets()
201+
{
202+
/* NOTE: truncate unnecessary DLLs.
203+
// build LINQ here and copy paste resulting .Where() below
204+
var assems = AppDomain.CurrentDomain.GetAssemblies()
205+
.Where(static x =>
206+
{
207+
var n = x.GetName().Name;
208+
if ( //total 234files
209+
n.StartsWith("Unity") || // truncate to 95
210+
n.StartsWith("System.") || // truncate to 225
211+
n.StartsWith("Mono.") || // truncate to 229
212+
n == "mscorlib" || // tons of types inside
213+
n == "System" // tons of types inside
214+
)
215+
return false;
216+
return true;
217+
})
218+
.Select(static x => x.GetName().Name + ": " + x.GetTypes().Count())
219+
;
220+
Debug.Log($"#{assems.Count()} " + string.Join("\n", assems));
221+
*/
222+
223+
224+
var infos = AppDomain.CurrentDomain.GetAssemblies()
225+
.Where(static x =>
226+
{
227+
var n = x.GetName().Name;
228+
if (
229+
n.StartsWith("Unity") || // truncate 234 files to 95
230+
n.StartsWith("System.") || // these have tons of types inside
231+
n.StartsWith("Mono.") ||
232+
n == "System" ||
233+
n == "mscorlib"
234+
)
235+
return false;
236+
return true;
237+
})
238+
.SelectMany(static a => a.GetTypes())
239+
.Where(static t =>
240+
t.GetCustomAttribute<UnitySourceGeneratorAttribute>(false) != null &&
241+
// waiting for C# 11.0 //typeof(IUnitySourceGenerator).IsAssignableFrom(t) &&
242+
!s_typeNameToInfo.ContainsKey(t.Name)
243+
)
244+
.Select(static t =>
245+
{
246+
var attr = t.GetCustomAttribute<UnitySourceGeneratorAttribute>(false);
247+
return new CachedTypeInfo
248+
{
249+
Type = t,
250+
OverwriteIfFileExists = attr.OverwriteIfFileExists,
251+
};
252+
})
253+
;
254+
255+
256+
// TODO: Export constants definition
257+
foreach (var info in infos)
258+
{
259+
//Debug.Log($"[{nameof(UnitySourceGenerator)}] Processing...: {info.ClassName}");
260+
261+
var outputMethod = info.Type.GetMethod("OutputFileName", METHOD_FLAGS, null, Type.EmptyTypes, null);
262+
var emitMethod = info.Type.GetMethod("Emit", METHOD_FLAGS, null, new Type[] { typeof(USGContext), typeof(StringBuilder) }, null);
263+
264+
if (outputMethod == null || emitMethod == null)
265+
{
266+
Debug.LogError($"[{nameof(UnitySourceGenerator)}] Required static method(s) not found: {info.Type}");
267+
continue;
268+
}
269+
270+
271+
info.EmitMethod = emitMethod;
272+
273+
//filename??
274+
info.OutputFileName = (string)outputMethod.Invoke(null, null);
275+
if (string.IsNullOrWhiteSpace(info.OutputFileName))
276+
{
277+
Debug.LogError($"[{nameof(UnitySourceGenerator)}] Output file name is invalid: {info.OutputFileName}");
278+
continue;
279+
}
280+
281+
282+
//build filename
283+
string fileName = Path.GetFileNameWithoutExtension(info.OutputFileName);
284+
string fileExt = Path.GetExtension(info.OutputFileName);
285+
string outputFileName = fileName + GENERATOR_PREFIX + info.Type.Name + GENERATOR_EXT + fileExt;
286+
287+
info.OutputFileName = outputFileName;
288+
289+
//once again
290+
if (string.IsNullOrWhiteSpace(info.OutputFileName))
291+
{
292+
Debug.LogError($"[{nameof(UnitySourceGenerator)}] Output file name is invalid: {info.OutputFileName}");
293+
continue;
294+
}
295+
296+
s_typeNameToInfo.TryAdd(info.Type.Name, info);
297+
298+
299+
}//foreach
300+
}
301+
302+
303+
}
304+
}
305+
#endif

Editor/USGEngine.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)