From a5bb50fa606e3940ef4aff7bfc0e612f0578a1b6 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 14:23:20 +0200 Subject: [PATCH 01/12] Small rename for clarity. --- Scripts/Editor/Generators/CollectionGenerators.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Scripts/Editor/Generators/CollectionGenerators.cs b/Scripts/Editor/Generators/CollectionGenerators.cs index aab620b..52172ba 100644 --- a/Scripts/Editor/Generators/CollectionGenerators.cs +++ b/Scripts/Editor/Generators/CollectionGenerators.cs @@ -47,14 +47,14 @@ private static void GetGeneratorTypes(Type generatorType, out Type collectionTyp templateType = genericArguments[1]; } - private static Type[] GetGeneratorTypes() + private static Type[] GetAllGeneratorTypes() { return InterfaceType.GetAllAssignableClasses(); } public static Type GetGeneratorTypeForCollection(Type collectionType, bool allowSubclasses = true) { - Type[] generatorTypes = GetGeneratorTypes(); + Type[] generatorTypes = GetAllGeneratorTypes(); foreach (Type generatorType in generatorTypes) { GetGeneratorTypes(generatorType, out Type generatorCollectionType, out Type generatorTemplateType); @@ -67,7 +67,7 @@ public static Type GetGeneratorTypeForCollection(Type collectionType, bool allow public static void RunAllGenerators() { - Type[] generatorTypes = GetGeneratorTypes(); + Type[] generatorTypes = GetAllGeneratorTypes(); foreach (Type generatorType in generatorTypes) { RunGeneratorInternal(generatorType, false); From 068f72ad751e298b9c6565b6714c78f85e968391 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 15:03:57 +0200 Subject: [PATCH 02/12] Added context menu item to edit a collection's generator. --- .../CustomEditors/CollectionCustomEditor.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index 354ccd2..f3e29d4 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -962,5 +962,31 @@ public static ScriptableObject AddNewItem(ScriptableObjectCollection collection, LAST_ADDED_COLLECTION_ITEM = collectionItem; return collectionItem; } + + [MenuItem("CONTEXT/ScriptableObjectCollection/Edit Generator", false, int.MaxValue)] + private static void EditGenerator(MenuCommand command) + { + Type collectionType = command.context.GetType(); + Type generatorType = CollectionGenerators.GetGeneratorTypeForCollection(collectionType); + string[] scriptGuids = AssetDatabase.FindAssets($"t:script {generatorType.Name}"); + if (scriptGuids.Length == 0) + { + Debug.LogWarning($"Could not find corresponding script for generator '{generatorType}'. " + + $"Check that the generator's script is called '{generatorType.Name}'."); + return; + } + + string scriptPath = AssetDatabase.GUIDToAssetPath(scriptGuids[0]); + MonoScript script = AssetDatabase.LoadAssetAtPath(scriptPath); + AssetDatabase.OpenAsset(script); + } + + [MenuItem("CONTEXT/ScriptableObjectCollection/Edit Generator", true)] + private static bool EditGeneratorValidator(MenuCommand command) + { + Type collectionType = command.context.GetType(); + Type generatorType = CollectionGenerators.GetGeneratorTypeForCollection(collectionType); + return generatorType != null; + } } } From 0472a4a8315b6515a2ce0e4d34028a211f0ab5b4 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 16:20:33 +0200 Subject: [PATCH 03/12] Cleanup of code generation. --- Scripts/Editor/Core/CodeGenerationUtility.cs | 90 ++++++++++++++----- .../EditorWindows/CreateCollectionWizard.cs | 4 +- .../CreateNewCollectionItemFromBaseWizard.cs | 5 +- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index f366f2f..5fdc971 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -11,28 +11,35 @@ namespace BrunoMikoski.ScriptableObjectCollections { public static class CodeGenerationUtility { - public static bool CreateNewEmptyScript(string fileName, string parentFolder, string nameSpace, - string classAttributes, string classDeclarationString, string[] innerContent, params string[] directives) + public static bool CreateNewScript( + string fileName, string parentFolder, string nameSpace, string[] directives, params string[] lines) { + // Make sure the folder exists. AssetDatabaseUtils.CreatePathIfDoesntExist(parentFolder); + + // Check that the file doesn't exist yet. string finalFilePath = Path.Combine(parentFolder, $"{fileName}.cs"); - if (File.Exists(Path.GetFullPath(finalFilePath))) return false; using StreamWriter writer = new StreamWriter(finalFilePath); - bool hasNameSpace = !string.IsNullOrEmpty(nameSpace); int indentation = 0; - foreach (string directive in directives) + // First write the directives. + if (directives != null && directives.Length > 0) { - if (string.IsNullOrWhiteSpace(directive)) - continue; - - writer.WriteLine($"using {directive};"); + foreach (string directive in directives) + { + if (string.IsNullOrWhiteSpace(directive)) + continue; + + writer.WriteLine($"using {directive};"); + } + writer.WriteLine(); } - - writer.WriteLine(); + + // Then write the namespace. + bool hasNameSpace = !string.IsNullOrEmpty(nameSpace); if (hasNameSpace) { writer.WriteLine($"namespace {nameSpace}"); @@ -40,28 +47,63 @@ public static bool CreateNewEmptyScript(string fileName, string parentFolder, st indentation++; } + // Add the contents of the file. + if (lines != null) + { + foreach (string line in lines) + { + if (line == "}") + indentation--; + + writer.WriteLine(GetIndentation(indentation) + line); + + if (line == "{") + indentation++; + } + } + + // If necessary, end the namespace. + if (hasNameSpace) + writer.WriteLine("}"); + + return true; + } + + public static bool CreateNewScript(string fileName, string parentFolder, string nameSpace, + string classAttributes, string classDeclarationString, string[] innerContent, params string[] directives) + { + List lines = new List(); + int indentation = 0; + + // Add class definition if (!string.IsNullOrEmpty(classAttributes)) - writer.WriteLine($"{GetIndentation(indentation)}{classAttributes}"); - - writer.WriteLine($"{GetIndentation(indentation)}{classDeclarationString}"); - writer.WriteLine(GetIndentation(indentation)+"{"); + lines.Add($"{GetIndentation(indentation)}{classAttributes}"); + lines.Add($"{GetIndentation(indentation)}{classDeclarationString}"); + + // Start class braces + lines.Add(GetIndentation(indentation)+"{"); indentation++; - if(innerContent != null) + + // Add class inner content + if (innerContent != null) { foreach (string content in innerContent) { - if (content == "}") indentation--; - writer.WriteLine(GetIndentation(indentation)+content); - if (content == "{") indentation++; + if (content == "}") + indentation--; + + lines.Add(GetIndentation(indentation)+content); + + if (content == "{") + indentation++; } } + + // End class braces indentation--; - writer.WriteLine(GetIndentation(indentation)+"}"); - - if (hasNameSpace) - writer.WriteLine("}"); + lines.Add(GetIndentation(indentation)+"}"); - return true; + return CreateNewScript(fileName, parentFolder, nameSpace, directives, lines.ToArray()); } private static string GetIndentation(int indentation) diff --git a/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs b/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs index 1d79308..dae541e 100644 --- a/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs +++ b/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs @@ -640,7 +640,7 @@ private bool CreateCollectionItemScript() string folder = ScriptsFolderPath; LastScriptsTargetFolder.Value = ScriptsFolderPathWithoutParentFolder; - return CodeGenerationUtility.CreateNewEmptyScript(collectionItemName, + return CodeGenerationUtility.CreateNewScript(collectionItemName, folder, Namespace, string.Empty, @@ -652,7 +652,7 @@ private bool CreateCollectionScript() { string folder = ScriptsFolderPath; - bool result = CodeGenerationUtility.CreateNewEmptyScript(CollectionName, + bool result = CodeGenerationUtility.CreateNewScript(CollectionName, folder, Namespace, $"[CreateAssetMenu(menuName = \"ScriptableObject Collection/Collections/Create {CollectionName}\", fileName = \"{CollectionName}\", order = 0)]", diff --git a/Scripts/Editor/EditorWindows/CreateNewCollectionItemFromBaseWizard.cs b/Scripts/Editor/EditorWindows/CreateNewCollectionItemFromBaseWizard.cs index fc62999..8dc5512 100644 --- a/Scripts/Editor/EditorWindows/CreateNewCollectionItemFromBaseWizard.cs +++ b/Scripts/Editor/EditorWindows/CreateNewCollectionItemFromBaseWizard.cs @@ -108,9 +108,8 @@ private void OnGUI() targetNamespace = targetType.Namespace; string parentFolder = AssetDatabase.GetAssetPath(targetFolder); - CodeGenerationUtility.CreateNewEmptyScript(newClassName, - parentFolder, targetNamespace, string.Empty,$"public class {newClassName} : {targetType}", null, - targetNamespace); + CodeGenerationUtility.CreateNewScript(newClassName, + parentFolder, targetNamespace, string.Empty,$"public class {newClassName} : {targetType}", null); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); From 31583fc11973f901504cbdd5f59906600c685e9c Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 16:24:21 +0200 Subject: [PATCH 04/12] Created a reusable utility for getting the script for a class. --- .../CustomEditors/CollectionCustomEditor.cs | 17 +++-------- Scripts/Editor/Utils/ScriptUtility.cs | 28 +++++++++++++++++++ Scripts/Editor/Utils/ScriptUtility.cs.meta | 11 ++++++++ 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 Scripts/Editor/Utils/ScriptUtility.cs create mode 100644 Scripts/Editor/Utils/ScriptUtility.cs.meta diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index f3e29d4..57c8f0d 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -968,25 +968,16 @@ private static void EditGenerator(MenuCommand command) { Type collectionType = command.context.GetType(); Type generatorType = CollectionGenerators.GetGeneratorTypeForCollection(collectionType); - string[] scriptGuids = AssetDatabase.FindAssets($"t:script {generatorType.Name}"); - if (scriptGuids.Length == 0) - { - Debug.LogWarning($"Could not find corresponding script for generator '{generatorType}'. " + - $"Check that the generator's script is called '{generatorType.Name}'."); - return; - } - - string scriptPath = AssetDatabase.GUIDToAssetPath(scriptGuids[0]); - MonoScript script = AssetDatabase.LoadAssetAtPath(scriptPath); - AssetDatabase.OpenAsset(script); + + if (ScriptUtility.TryGetScriptOfClass(generatorType, out MonoScript script)) + AssetDatabase.OpenAsset(script); } [MenuItem("CONTEXT/ScriptableObjectCollection/Edit Generator", true)] private static bool EditGeneratorValidator(MenuCommand command) { Type collectionType = command.context.GetType(); - Type generatorType = CollectionGenerators.GetGeneratorTypeForCollection(collectionType); - return generatorType != null; + return CollectionGenerators.GetGeneratorTypeForCollection(collectionType) != null; } } } diff --git a/Scripts/Editor/Utils/ScriptUtility.cs b/Scripts/Editor/Utils/ScriptUtility.cs new file mode 100644 index 0000000..1e842d4 --- /dev/null +++ b/Scripts/Editor/Utils/ScriptUtility.cs @@ -0,0 +1,28 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + /// + /// Utilities for dealing with scripts. + /// + public static class ScriptUtility + { + public static bool TryGetScriptOfClass(Type classType, out MonoScript script) + { + string[] scriptGuids = AssetDatabase.FindAssets($"t:script {classType.Name}"); + if (scriptGuids.Length == 0) + { + Debug.LogWarning($"Could not find corresponding script for class '{classType.Name}'. " + + $"Check that the script is called '{classType.Name}'."); + script = null; + return false; + } + + string scriptPath = AssetDatabase.GUIDToAssetPath(scriptGuids[0]); + script = AssetDatabase.LoadAssetAtPath(scriptPath); + return true; + } + } +} diff --git a/Scripts/Editor/Utils/ScriptUtility.cs.meta b/Scripts/Editor/Utils/ScriptUtility.cs.meta new file mode 100644 index 0000000..2a1e527 --- /dev/null +++ b/Scripts/Editor/Utils/ScriptUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3fcd80c1e2d16a4ea0949be85e1881e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From cdb0ea1d7ea65d55cd2f63e89cc8fd7425d792ec Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 16:52:12 +0200 Subject: [PATCH 05/12] The code generation utility now optionally can use a code template. --- Scripts/Editor/Core/CodeGenerationUtility.cs | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 5fdc971..366940e 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -68,6 +68,40 @@ public static bool CreateNewScript( return true; } + + public static bool CreateNewScript( + string fileName, string parentFolder, string nameSpace, string[] directives, + string codeTemplateFileName, Dictionary replacements) + { + // Try to find the specified code template. + string[] codeTemplateCandidates = AssetDatabase.FindAssets($"t:TextAsset {codeTemplateFileName}.cs"); + TextAsset codeTemplate = null; + if (codeTemplateCandidates.Length > 0) + { + string codeTemplatePath = AssetDatabase.GUIDToAssetPath(codeTemplateCandidates[0]); + codeTemplate = AssetDatabase.LoadAssetAtPath(codeTemplatePath); + } + + // Make sure the template exists. + if (codeTemplateCandidates.Length == 0 || codeTemplate == null) + { + Debug.LogError($"Tried to create new script '{parentFolder}/{fileName}' but code template " + + $"'{codeTemplateFileName}.cs.txt' could not be found. Make sure this template exists."); + return false; + } + + string codeTemplateText = codeTemplate.text; + + // Apply any specified replacements. + foreach (KeyValuePair tagToReplacement in replacements) + { + codeTemplateText = codeTemplateText.Replace($"##{tagToReplacement.Key}##", tagToReplacement.Value); + } + + // Now create the script. + string[] lines = codeTemplateText.Split("\r\n"); + return CreateNewScript(fileName, parentFolder, nameSpace, directives, lines); + } public static bool CreateNewScript(string fileName, string parentFolder, string nameSpace, string classAttributes, string classDeclarationString, string[] innerContent, params string[] directives) From 7ae1375c423047b19bf0664aeb3ba5e1999e7fdc Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 17:00:27 +0200 Subject: [PATCH 06/12] Added string extensions. --- .../Runtime/Extensions/StringExtensions.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/Scripts/Runtime/Extensions/StringExtensions.cs b/Scripts/Runtime/Extensions/StringExtensions.cs index 5b704c7..1ede1f9 100644 --- a/Scripts/Runtime/Extensions/StringExtensions.cs +++ b/Scripts/Runtime/Extensions/StringExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using UnityEngine; namespace BrunoMikoski.ScriptableObjectCollections { @@ -20,6 +21,9 @@ public static class StringExtensions private const char UNDERSCORE = '_'; private const char DEFAULT_SEPARATOR = ' '; + private static readonly char[] DIRECTORY_SEPARATORS = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + private const string ASSETS_FOLDER = "Assets"; + private static readonly string[] RESERVED_KEYWORDS = { "abstract", "as", "base", " bool", " break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", @@ -251,5 +255,83 @@ public static string ToPathWithConsistentSeparators(this string path) { return path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } + + public static bool HasParentDirectory(this string path) + { + return path.LastIndexOfAny(DIRECTORY_SEPARATORS) != -1; + } + + public static string GetParentDirectory(this string path) + { + int lastDirectorySeparator = path.LastIndexOfAny(DIRECTORY_SEPARATORS); + if (lastDirectorySeparator == -1) + return path; + + return path.Substring(0, lastDirectorySeparator); + } + + public static bool StartsWithAny(this string path, params string[] prefixes) + { + foreach (string prefix in prefixes) + { + if (path.StartsWith(prefix)) + return true; + } + + return false; + } + + public static string RemovePrefix(this string name, string prefix) + { + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(prefix)) + return name; + + if (!name.StartsWith(prefix)) + return name; + + return name.Substring(prefix.Length); + } + + public static string RemovePrefix(this string name, char prefix) + { + return RemovePrefix(name, prefix.ToString()); + } + + public static string RemoveSuffix(this string name, string suffix) + { + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(suffix)) + return name; + + if (!name.EndsWith(suffix)) + return name; + + return name.Substring(0, name.Length - suffix.Length); + } + + public static string RemoveSuffix(this string name, char suffix) + { + return RemoveSuffix(name, suffix.ToString()); + } + + private static string RemoveAssetsPrefix(this string path) + { + return RemovePrefix(path.ToPathWithConsistentSeparators(), ASSETS_FOLDER + Path.AltDirectorySeparatorChar); + } + + public static string GetAbsolutePath(this string projectPath) + { + string absolutePath = RemoveAssetsPrefix(projectPath); + return Application.dataPath + Path.AltDirectorySeparatorChar + absolutePath; + } + + public static string GetProjectPath(this string absolutePath) + { + absolutePath = absolutePath.ToPathWithConsistentSeparators(); + string projectPath = RemoveSuffix(Application.dataPath, ASSETS_FOLDER); + projectPath = RemoveSuffix(projectPath, Path.AltDirectorySeparatorChar); + + string relativePath = ToPathWithConsistentSeparators(Path.GetRelativePath(projectPath, absolutePath)); + return relativePath; + } } } From db600cf4ad761e391d6272877a24ed2a13f085b1 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 17:01:09 +0200 Subject: [PATCH 07/12] Added a utility for creating asmdefs/asmrefs for editor folders. --- Scripts/Editor/Utils/AsmDefUtility.cs | 286 +++++++++++++++++++++ Scripts/Editor/Utils/AsmDefUtility.cs.meta | 11 + 2 files changed, 297 insertions(+) create mode 100644 Scripts/Editor/Utils/AsmDefUtility.cs create mode 100644 Scripts/Editor/Utils/AsmDefUtility.cs.meta diff --git a/Scripts/Editor/Utils/AsmDefUtility.cs b/Scripts/Editor/Utils/AsmDefUtility.cs new file mode 100644 index 0000000..cd086f8 --- /dev/null +++ b/Scripts/Editor/Utils/AsmDefUtility.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + /// + /// Contains useful utilities for organizing asmdefs. + /// + public static class AsmDefUtility + { + private const char Separator = '.'; + + [Serializable] + private struct AsmRef + { + public string reference; + + public AsmRef(AssemblyDefinitionAsset reference) + { + string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(reference)); + this.reference = $"GUID:{guid}"; + } + + public override string ToString() + { + return JsonUtility.ToJson(this, true); + } + } + + [Serializable] + private struct AsmDef + { + public string name; + public string rootNamespace; + public string[] references; + public string[] includePlatforms; + public string[] excludePlatforms; + public bool allowUnsafeCode; + public bool overrideReferences; + public string[] precompiledReferences; + public bool autoReferenced; + public string[] defineConstraints; + public string[] versionDefines; + public bool noEngineReferences; + + public AsmDef(string name, params AssemblyDefinitionAsset[] references) + { + this.name = name; + rootNamespace = ""; + this.references = new string[references.Length]; + for (int i = 0; i < references.Length; i++) + { + string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(references[i])); + this.references[i] = $"GUID:{guid}"; + } + includePlatforms = new string[0]; + excludePlatforms = new string[0]; + allowUnsafeCode = false; + overrideReferences = false; + precompiledReferences = new string[0]; + autoReferenced = true; + defineConstraints = new string[0]; + versionDefines = new string[0]; + noEngineReferences = false; + } + + public override string ToString() + { + return JsonUtility.ToJson(this, true); + } + } + + private static List GetSelectedEditorFolders() + { + List results = new List(); + for (int i = 0; i < Selection.objects.Length; i++) + { + if (Selection.objects[i].name == "Editor") + results.Add(Selection.objects[i]); + } + + return results; + } + + public static AssemblyDefinitionAsset GetParentEditorAsmDef(string path) + { + string currentFolder = path.GetParentDirectory(); + + while (currentFolder.HasParentDirectory()) + { + string parent = currentFolder.GetParentDirectory(); + + if (currentFolder == parent) + { + // This directory is like Harry Potter because it has no more parents left! + break; + } + + string editorFolderNextToParent = parent + Path.AltDirectorySeparatorChar + "Editor"; + + // It existed! Let's check that it has an asmdef. + if (AssetDatabase.IsValidFolder(editorFolderNextToParent)) + { + // Try to find asmdef files in this folder. Note that there can only be one or zero. + string[] asmdefFileResults = AssetDatabase.FindAssets("t:asmdef", new[] {editorFolderNextToParent}); + + // Check if one existed. + if (asmdefFileResults.Length == 1) + { + string asmdefFilePath = AssetDatabase.GUIDToAssetPath(asmdefFileResults[0]); + + // It existed! This folder is a valid asm def to reference! + return AssetDatabase.LoadAssetAtPath(asmdefFilePath); + } + } + + currentFolder = parent; + } + + return null; + } + + private static string GetAsmRefFileName(string path, string asmDefPath) + { + // If the asmdef is at "Assets/ProjectName/Scripts/Editor" then we'd like to get the filename relative to + // "Assets/ProjectName/Scripts". Then we can filter out the "Scripts" folder, add it to the name of the asmDef + // we reference, and then we get a file in the same naming convention as the asmdef we reference. + string asmDefDirectory = Path.GetDirectoryName(asmDefPath).ToPathWithConsistentSeparators(); + string asmDefParentDirectory = asmDefDirectory.GetParentDirectory(); + + path = Path.GetRelativePath(asmDefParentDirectory, path); + + // The name should basically just be the folder relative to the asmdef that's referenced. + string asmFileName = path.Replace(Path.DirectorySeparatorChar, Separator) + .Replace(Path.AltDirectorySeparatorChar, Separator); + + // Sometimes people add special characters so it shows up at the top. We don't want that for our filename, + // so strip those out. Hyphens and spaces don't look nice either. + string[] specialChars = { "_", "[", "]", "-", " " }; + foreach (string specialChar in specialChars) + { + asmFileName = asmFileName.Replace(specialChar, ""); + } + + // Remove any script folders from the name. + string[] scriptFolderNames = {"Scripts", "Runtime"}; + List segments = new List(asmFileName.Split(Separator)); + while (segments.Count > 0 && segments[0].StartsWithAny(scriptFolderNames)) + { + segments.RemoveAt(0); + } + asmFileName = string.Join(Separator, segments); + + string fileNameBase = Path.GetFileNameWithoutExtension(asmDefPath).RemoveSuffix(Separator + "Editor"); + string fileNameFinal = fileNameBase + Separator + asmFileName; + return fileNameFinal; + } + + public static void CreateAsmRef(string folderPath, AssemblyDefinitionAsset asmDef) + { + string asmDefPath = AssetDatabase.GetAssetPath(asmDef); + string fileName = GetAsmRefFileName(folderPath, asmDefPath); + string filePath = folderPath.GetAbsolutePath() + Path.AltDirectorySeparatorChar + fileName + ".asmref"; + + AsmRef asmRef = new AsmRef(asmDef); + + File.WriteAllText(filePath, asmRef.ToString()); + AssetDatabase.ImportAsset(filePath.GetProjectPath()); + } + + public static AssemblyDefinitionAsset CreateEditorFolderAsmDef( + string folderName, AssemblyDefinitionAsset runtimeAsmDef) + { + string asmDefPath = AssetDatabase.GetAssetPath(runtimeAsmDef); + string fileName = Path.GetFileNameWithoutExtension(asmDefPath) + Separator + "Editor"; + string filePath = folderName.GetAbsolutePath() + Path.AltDirectorySeparatorChar + fileName + ".asmdef"; + + AsmDef asmDef = new AsmDef(fileName, runtimeAsmDef); + + File.WriteAllText(filePath, asmDef.ToString()); + + filePath = filePath.GetProjectPath(); + AssetDatabase.ImportAsset(filePath, ImportAssetOptions.ForceSynchronousImport); + return AssetDatabase.LoadAssetAtPath(filePath); + } + + private static void AddAsmRefToTopLevelEditorFolder(Object selectedEditorFolder) + { + string path = AssetDatabase.GetAssetPath(selectedEditorFolder); + AddAsmRefToTopLevelEditorFolder(path); + } + + public static void AddAsmRefToTopLevelEditorFolder(string folderPath) + { + AssemblyDefinitionAsset editorAsmDefToReference = GetParentEditorAsmDef(folderPath); + + if (editorAsmDefToReference == null) + { + Debug.LogWarning($"Can't create asmref for folder {folderPath} because we can't find an editor folder asmdef."); + return; + } + + CreateAsmRef(folderPath, editorAsmDefToReference); + } + + public static AssemblyDefinitionAsset GetAsmDefInFolder(string path) + { + string[] asmDefsGuids = AssetDatabase.FindAssets("t:asmdef", new[] { path }); + for (int i = 0; i < asmDefsGuids.Length; i++) + { + string asmDefPath = AssetDatabase.GUIDToAssetPath(asmDefsGuids[i]); + string asmDefDirectory = Path.GetDirectoryName(asmDefPath); + + if (asmDefDirectory == path) + return AssetDatabase.LoadAssetAtPath(asmDefPath); + } + + return null; + } + + public static AssemblyDefinitionAsset GetAsmDefInFolderOrParent(string path) + { + string currentPath = path; + + // First check if there's an asmdef in the start folder. + AssemblyDefinitionAsset asmDefInStartFolder = GetAsmDefInFolder(path); + if (asmDefInStartFolder != null) + return asmDefInStartFolder; + + // Now keep checking every parent folder if there's an asmdef there. + while (currentPath.HasParentDirectory()) + { + // Go to the parent folder. + currentPath = currentPath.GetParentDirectory(); + + // See if there's an asmdef in this parent folder. + AssemblyDefinitionAsset asmDefInFolder = GetAsmDefInFolder(currentPath); + if (asmDefInFolder != null) + return asmDefInFolder; + } + + return null; + } + + public static AssemblyDefinitionAsset CreateEmptyEditorFolderForRuntimeAsmDef( + AssemblyDefinitionAsset runtimeAsmDef, string dummyNamespace = null) + { + string runtimeAsmDefFolder = Path.GetDirectoryName(AssetDatabase.GetAssetPath(runtimeAsmDef)); + + AssetDatabase.CreateFolder(runtimeAsmDefFolder, "Editor"); + string editorFolder = runtimeAsmDefFolder + Path.AltDirectorySeparatorChar + "Editor"; + + // Create a dummy file because you can't have asmdefs in empty folders. + CreateDummyScript(dummyNamespace, editorFolder); + + return CreateEditorFolderAsmDef(editorFolder, runtimeAsmDef); + } + + private static void CreateDummyScript(string dummyNamespace, string editorFolder) + { + bool hasNamespace = !string.IsNullOrEmpty(dummyNamespace); + string dummyFilePath = editorFolder + Path.AltDirectorySeparatorChar + "Dummy.cs"; + + StringBuilder sb = new StringBuilder(); + if (hasNamespace) + { + sb.AppendLine(dummyNamespace); + sb.AppendLine("{"); + sb.Append(" "); + } + + sb.AppendLine("public class Dummy {}"); + if (hasNamespace) + sb.AppendLine("}\r\n"); + + File.WriteAllText(dummyFilePath, sb.ToString()); + AssetDatabase.ImportAsset(dummyFilePath, ImportAssetOptions.ForceSynchronousImport); + } + } +} diff --git a/Scripts/Editor/Utils/AsmDefUtility.cs.meta b/Scripts/Editor/Utils/AsmDefUtility.cs.meta new file mode 100644 index 0000000..4fb4803 --- /dev/null +++ b/Scripts/Editor/Utils/AsmDefUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93b84e258c4d3d74aafdc9ba9331625e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 982b7b50fc696d70baf7b511f1b04c1db0df4cc5 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 17:24:08 +0200 Subject: [PATCH 08/12] Code generation now tries to create asmrefs for folders. --- Scripts/Editor/Core/CodeGenerationUtility.cs | 27 ++++++++++++++++++++ Scripts/Editor/Utils/AsmDefUtility.cs | 4 +++ 2 files changed, 31 insertions(+) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 366940e..5aa4588 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -5,6 +5,7 @@ using System.Text; using UnityEditor; using UnityEditor.Compilation; +using UnityEditorInternal; using UnityEngine; namespace BrunoMikoski.ScriptableObjectCollections @@ -14,9 +15,35 @@ public static class CodeGenerationUtility public static bool CreateNewScript( string fileName, string parentFolder, string nameSpace, string[] directives, params string[] lines) { + parentFolder = parentFolder.ToPathWithConsistentSeparators(); + // Make sure the folder exists. AssetDatabaseUtils.CreatePathIfDoesntExist(parentFolder); + // Check if the created folder is an editor folder. + const string editorFolderName = "Editor"; + bool isEditorFolder = parentFolder.Contains($"/{editorFolderName}/") || + parentFolder.EndsWith($"/{editorFolderName}"); + if (isEditorFolder) + { + // Figure out what the last editor folder is. This is because you can create an item with path + // 'Assets/ProjectName/Scripts/Editor/SomeSubFolder', in which case it should be making an asmref + // for 'Assets/ProjectName/Scripts/Editor' and not for the subfolder. + int lastOccurrenceOfEditorName = parentFolder.LastIndexOf($"/{editorFolderName}", + StringComparison.OrdinalIgnoreCase); + string editorFolderPath = parentFolder.Substring( + 0, lastOccurrenceOfEditorName + editorFolderName.Length + 1); + + // Find out if there is an editor asmdef that we should be referencing. + AssemblyDefinitionAsset editorAsmDefToReference = AsmDefUtility + .GetParentEditorAsmDef(editorFolderPath); + if (editorAsmDefToReference != null) + { + // If so, add an asmref for it, otherwise it might not be able to reference editor code correctly. + AsmDefUtility.AddAsmRefToTopLevelEditorFolder(editorFolderPath); + } + } + // Check that the file doesn't exist yet. string finalFilePath = Path.Combine(parentFolder, $"{fileName}.cs"); if (File.Exists(Path.GetFullPath(finalFilePath))) diff --git a/Scripts/Editor/Utils/AsmDefUtility.cs b/Scripts/Editor/Utils/AsmDefUtility.cs index cd086f8..2b97722 100644 --- a/Scripts/Editor/Utils/AsmDefUtility.cs +++ b/Scripts/Editor/Utils/AsmDefUtility.cs @@ -167,6 +167,10 @@ public static void CreateAsmRef(string folderPath, AssemblyDefinitionAsset asmDe string asmDefPath = AssetDatabase.GetAssetPath(asmDef); string fileName = GetAsmRefFileName(folderPath, asmDefPath); string filePath = folderPath.GetAbsolutePath() + Path.AltDirectorySeparatorChar + fileName + ".asmref"; + + // If the file already exists, skip it. + if (File.Exists(filePath)) + return; AsmRef asmRef = new AsmRef(asmDef); From 551b1aac13ae204b1ad712fed1982b072511dc36 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 17:32:03 +0200 Subject: [PATCH 09/12] Added a wizard for creating a generator script. --- .../CustomEditors/CollectionCustomEditor.cs | 17 ++- .../EditorWindows/CreateCollectionWizard.cs | 2 +- .../EditorWindows/GeneratorCreationWizard.cs | 133 ++++++++++++++++++ .../GeneratorCreationWizard.cs.meta | 11 ++ .../Generators/GeneratorTemplate.cs.txt | 25 ++++ .../Generators/GeneratorTemplate.cs.txt.meta | 7 + 6 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs create mode 100644 Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs.meta create mode 100644 Scripts/Editor/Generators/GeneratorTemplate.cs.txt create mode 100644 Scripts/Editor/Generators/GeneratorTemplate.cs.txt.meta diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index 57c8f0d..5029644 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -963,7 +963,22 @@ public static ScriptableObject AddNewItem(ScriptableObjectCollection collection, return collectionItem; } - [MenuItem("CONTEXT/ScriptableObjectCollection/Edit Generator", false, int.MaxValue)] + [MenuItem("CONTEXT/ScriptableObjectCollection/Create Generator", false, 99999)] + private static void CreateGenerator(MenuCommand command) + { + Type collectionType = command.context.GetType(); + + GeneratorCreationWizard.Show(collectionType); + } + + [MenuItem("CONTEXT/ScriptableObjectCollection/Create Generator", true)] + private static bool CreateGeneratorValidator(MenuCommand command) + { + Type collectionType = command.context.GetType(); + return CollectionGenerators.GetGeneratorTypeForCollection(collectionType) == null; + } + + [MenuItem("CONTEXT/ScriptableObjectCollection/Edit Generator", false, 99999)] private static void EditGenerator(MenuCommand command) { Type collectionType = command.context.GetType(); diff --git a/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs b/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs index dae541e..c5ebe16 100644 --- a/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs +++ b/Scripts/Editor/EditorWindows/CreateCollectionWizard.cs @@ -265,7 +265,7 @@ private int MaximumNamespaceDepth private static readonly EditorPreferenceString LastScriptsTargetFolder = new EditorPreferenceString(LAST_TARGET_SCRIPTS_FOLDER_KEY, null, true); - private static readonly EditorPreferenceString LastGeneratedCollectionScriptPath = + public static readonly EditorPreferenceString LastGeneratedCollectionScriptPath = new EditorPreferenceString(LAST_GENERATED_COLLECTION_SCRIPT_PATH_KEY, null, true); private static readonly EditorPreferenceBool CreateFolderForThisCollection = diff --git a/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs b/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs new file mode 100644 index 0000000..bd03db8 --- /dev/null +++ b/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + /// + /// Wizard for creating generator scripts. + /// + public sealed class GeneratorCreationWizard : EditorWindow + { + private const string GeneratorNameField = "NameInputField"; + private const string CollectionSuffix = "Collection"; + + private string generatorName; + private string templateName; + + private string targetFolderPath; + + private static Type collectionType; + + public static void Show(Type collectionType) + { + GeneratorCreationWizard.collectionType = collectionType; + + GeneratorCreationWizard generatorCreationWizard = GetWindow("Create new generator"); + generatorCreationWizard.minSize = new Vector2(550, 95); + generatorCreationWizard.maxSize = new Vector2(550, 95); + + generatorCreationWizard.InferGeneratorNames(); + generatorCreationWizard.InferFolder(); + + generatorCreationWizard.ShowPopup(); + } + + private void InferGeneratorNames() + { + string baseName = collectionType.Name.RemoveSuffix(CollectionSuffix); + + // Add the 'generator' suffix. + generatorName = baseName + "Generator"; + + templateName = baseName + "Template"; + } + + private void InferFolder() + { + targetFolderPath = Path.GetDirectoryName(CreateCollectionWizard.LastGeneratedCollectionScriptPath.Value); + + // If we can find the script that the collection belongs to, default to that. + if (ScriptUtility.TryGetScriptOfClass(collectionType, out MonoScript collectionScript)) + { + targetFolderPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(collectionScript)); + targetFolderPath += "/Editor"; + } + + targetFolderPath = targetFolderPath.ToPathWithConsistentSeparators(); + } + + private void OnGUI() + { + if (collectionType == null) + { + Close(); + return; + } + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Collection Type", GUILayout.Width(EditorGUIUtility.labelWidth)); + EditorGUILayout.LabelField(collectionType.Name, EditorStyles.boldLabel); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + + GUI.SetNextControlName(GeneratorNameField); + generatorName = EditorGUILayout.TextField("Generator Name", generatorName); + + targetFolderPath = EditorGUILayout.TextField("Script Folder", targetFolderPath); + + if (GUI.GetNameOfFocusedControl() != GeneratorNameField) + { + if (!string.IsNullOrEmpty(generatorName)) + generatorName = generatorName.Sanitize(); + } + + EditorGUILayout.Space(); + + using (new EditorGUI.DisabledScope(!AreSettingsValid())) + { + if (GUILayout.Button("Create")) + CreateGeneratorCodeFile(); + } + } + + private void CreateGeneratorCodeFile() + { + string targetNamespace = String.Empty; + if (!string.IsNullOrEmpty(collectionType.Namespace)) + targetNamespace = collectionType.Namespace; + + Dictionary replacements = new Dictionary + { + {"TemplateType", templateName}, + {"GeneratorType", generatorName}, + {"CollectionType", collectionType.Name}, + }; + string[] directives = {"System.Collections.Generic", "BrunoMikoski.ScriptableObjectCollections"}; + CodeGenerationUtility.CreateNewScript( + generatorName, + targetFolderPath, targetNamespace, directives, "GeneratorTemplate", replacements); + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + Close(); + } + + private bool AreSettingsValid() + { + if (string.IsNullOrEmpty(generatorName)) + return false; + + if (targetFolderPath == null) + return false; + + if (AssetDatabase.LoadAssetAtPath(Path.Combine(targetFolderPath, $"{generatorName}.cs")) != null) + return false; + + return true; + } + } +} diff --git a/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs.meta b/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs.meta new file mode 100644 index 0000000..5941f13 --- /dev/null +++ b/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa8deeefe16c2e647a2c382ade7d242b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Generators/GeneratorTemplate.cs.txt b/Scripts/Editor/Generators/GeneratorTemplate.cs.txt new file mode 100644 index 0000000..6206f56 --- /dev/null +++ b/Scripts/Editor/Generators/GeneratorTemplate.cs.txt @@ -0,0 +1,25 @@ +/// +/// Template for SOC items to generate. Any values assigned here will be copied over to the corresponding SOC item. +/// This syntax lets you specify which items should exist and with which fields, without having to create instances +/// of your SOC items, which would have a lot of overhead and would force you to have public setters for everything. +/// +public sealed class ##TemplateType## : ItemTemplate +{ + // TODO: Define any fields here for the generated items. The fields of the SOC items will be updated accordingly. + // public string path; +} + +/// +/// Automatically generates SOC items. +/// +public sealed class ##GeneratorType## + : IScriptableObjectCollectionGenerator<##CollectionType##, ##TemplateType##> +{ + public bool ShouldRemoveNonGeneratedItems => true; + + public void GetItemTemplates(List<##TemplateType##> templates, ##CollectionType## collection) + { + // TODO: Create instances of the template for every item that should exist in the collection + templates.Add(new ##TemplateType## { name = "Hello world" }); + } +} diff --git a/Scripts/Editor/Generators/GeneratorTemplate.cs.txt.meta b/Scripts/Editor/Generators/GeneratorTemplate.cs.txt.meta new file mode 100644 index 0000000..f508a0f --- /dev/null +++ b/Scripts/Editor/Generators/GeneratorTemplate.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: aa64559d5ccea1c4a9f82817b6bb2d55 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 6e27d00b5325aec7f8f5a26a81704423ebe11acb Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 17:52:40 +0200 Subject: [PATCH 10/12] Fixed indentation being incorrect in generated code. --- Scripts/Editor/Core/CodeGenerationUtility.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 5aa4588..2dc3e41 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -77,13 +77,17 @@ public static bool CreateNewScript( // Add the contents of the file. if (lines != null) { - foreach (string line in lines) + for (int i = 0; i < lines.Length; i++) { + string line = lines[i]; + + line = line.TrimStart(); + if (line == "}") indentation--; - + writer.WriteLine(GetIndentation(indentation) + line); - + if (line == "{") indentation++; } From 4c34aa93b01287b3672296b4632cc154b129a7d2 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 18:00:44 +0200 Subject: [PATCH 11/12] Made sure the code generation writer is closed correctly. --- Scripts/Editor/Core/CodeGenerationUtility.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 2dc3e41..2d493ea 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -96,6 +96,8 @@ public static bool CreateNewScript( // If necessary, end the namespace. if (hasNameSpace) writer.WriteLine("}"); + + writer.Close(); return true; } From ca8fc24588c70c9abdc247aac7cc4473fbc45042 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Fri, 8 Sep 2023 18:08:08 +0200 Subject: [PATCH 12/12] The newly created generator script is now automatically opened. --- Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs b/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs index bd03db8..ac5e52d 100644 --- a/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs +++ b/Scripts/Editor/EditorWindows/GeneratorCreationWizard.cs @@ -113,6 +113,11 @@ private void CreateGeneratorCodeFile() AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); + // Open the new generator script. + string generatorScriptPath = targetFolderPath + "/" + generatorName + ".cs"; + MonoScript generatorScript = AssetDatabase.LoadAssetAtPath(generatorScriptPath); + AssetDatabase.OpenAsset(generatorScript); + Close(); }