diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index bc7de47..02e09a9 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -31,10 +31,39 @@ public class CollectionCustomEditor : BaseEditor private readonly Dictionary itemIndexToRect = new(); private float? reorderableListYPosition; private Dictionary collectionItemSerializedObjectCache = new(); + + [NonSerialized] private Type generatorType; + [NonSerialized] private IScriptableObjectCollectionGeneratorBase generator; + + private bool IsAutoGenerated => generatorType != null; protected virtual bool CanBeReorderable => true; - protected virtual bool DisplayAddButton => true; - protected virtual bool DisplayRemoveButton => true; + + protected virtual bool DisplayAddButton + { + get + { + // If this is a generated collection and it's set to remove items that aren't re-generated, then it + // doesn't make sense for you to add items because they will be removed next time you generate. + if (IsAutoGenerated && generator.ShouldRemoveNonGeneratedItems) + return false; + return true; + } + } + + protected virtual bool DisplayRemoveButton + { + get + { + // If this is a generated collection and it's set to remove items that aren't re-generated, then it + // doesn't make sense for you to remove items because they will be added back next time you generate. + if (IsAutoGenerated && generator.ShouldRemoveNonGeneratedItems) + return false; + + return true; + } + } + protected virtual bool AllowCustomTypeCreation => true; private static bool IsWaitingForNewTypeBeCreated @@ -51,6 +80,10 @@ protected virtual void OnEnable() CollectionsRegistry.Instance.ReloadCollections(); itemsSerializedProperty = serializedObject.FindProperty("items"); + + // Need to cache this before the reorderable list is created, because it affects how the list is displayed. + generatorType = CollectionGenerators.GetGeneratorTypeForCollection(collection.GetType()); + generator = generatorType == null ? null : CollectionGenerators.GetGenerator(generatorType); ValidateCollectionItems(); @@ -560,15 +593,23 @@ private void ValidateCollectionItems() private void DrawBottomMenu() { - using (new EditorGUILayout.HorizontalScope("Box")) + using (new EditorGUILayout.VerticalScope("Box")) { - if (GUILayout.Button($"Generate Static Access File", EditorStyles.miniButtonRight)) + if (GUILayout.Button($"Generate Static Access File", EditorStyles.miniButton)) { EditorApplication.delayCall += () => { CodeGenerationUtility.GenerateStaticCollectionScript(collection); }; } + + if (generatorType != null && GUILayout.Button($"Generate Items", GUILayout.Height(40))) + { + EditorApplication.delayCall += () => + { + CollectionGenerators.RunGenerator(generatorType); + }; + } } } diff --git a/Scripts/Editor/Extensions/SerializedPropertyExtensions.cs b/Scripts/Editor/Extensions/SerializedPropertyExtensions.cs index 572fc6f..65af165 100644 --- a/Scripts/Editor/Extensions/SerializedPropertyExtensions.cs +++ b/Scripts/Editor/Extensions/SerializedPropertyExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using UnityEditor; +using UnityEngine; namespace BrunoMikoski.ScriptableObjectCollections { @@ -23,5 +24,89 @@ public static FieldInfo GetFieldInfoFromPathByType(this SerializedProperty prope return fieldInfo; } + + public static void SetValue(this SerializedProperty serializedProperty, object value) + { + switch (serializedProperty.propertyType) + { + case SerializedPropertyType.Integer: + serializedProperty.intValue = (int)value; + break; + case SerializedPropertyType.Boolean: + serializedProperty.boolValue = (bool)value; + break; + case SerializedPropertyType.Float: + serializedProperty.floatValue = (float)value; + break; + case SerializedPropertyType.String: + serializedProperty.stringValue = (string)value; + break; + case SerializedPropertyType.Color: + serializedProperty.colorValue = (Color)value; + break; + case SerializedPropertyType.ObjectReference: + serializedProperty.objectReferenceValue = (UnityEngine.Object)value; + break; + case SerializedPropertyType.LayerMask: + serializedProperty.intValue = (LayerMask)value; + break; + case SerializedPropertyType.Enum: + serializedProperty.intValue = (int)value; + break; + case SerializedPropertyType.Vector2: + serializedProperty.vector2Value = (Vector2)value; + break; + case SerializedPropertyType.Vector3: + serializedProperty.vector3Value = (Vector3)value; + break; + case SerializedPropertyType.Vector4: + serializedProperty.vector4Value = (Vector4)value; + break; + case SerializedPropertyType.Rect: + serializedProperty.rectValue = (Rect)value; + break; + //case SerializedPropertyType.ArraySize: + //break; + case SerializedPropertyType.Character: + serializedProperty.stringValue = ((char)value).ToString(); + break; + case SerializedPropertyType.AnimationCurve: + serializedProperty.animationCurveValue = (AnimationCurve)value; + break; + case SerializedPropertyType.Bounds: + serializedProperty.boundsValue = (Bounds)value; + break; + //case SerializedPropertyType.Gradient: + //break; + case SerializedPropertyType.Quaternion: + serializedProperty.quaternionValue = (Quaternion)value; + break; + //case SerializedPropertyType.ExposedReference: + //break; + //case SerializedPropertyType.FixedBufferSize: + //break; + case SerializedPropertyType.Vector2Int: + serializedProperty.vector2IntValue = (Vector2Int)value; + break; + case SerializedPropertyType.Vector3Int: + serializedProperty.vector3IntValue = (Vector3Int)value; + break; + case SerializedPropertyType.RectInt: + serializedProperty.rectIntValue = (RectInt)value; + break; + case SerializedPropertyType.BoundsInt: + serializedProperty.boundsIntValue = (BoundsInt)value; + break; + //case SerializedPropertyType.ManagedReference: + //break; + case SerializedPropertyType.Hash128: + serializedProperty.hash128Value = (Hash128)value; + break; + default: + Debug.LogWarning( + $"Tried to copy value '{value}' from a template to an SOC item but apparently that's not supported."); + break; + } + } } } diff --git a/Scripts/Editor/Generators.meta b/Scripts/Editor/Generators.meta new file mode 100644 index 0000000..e07e8c2 --- /dev/null +++ b/Scripts/Editor/Generators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a050f0d6a4653f940a9d6fb7c1a59028 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Generators/CollectionGenerators.cs b/Scripts/Editor/Generators/CollectionGenerators.cs new file mode 100644 index 0000000..18d83bc --- /dev/null +++ b/Scripts/Editor/Generators/CollectionGenerators.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + /// + /// Responsible for managing collection generators. + /// + public static class CollectionGenerators + { + private static readonly Dictionary generatorTypeToInstance + = new Dictionary(); + + private static Type InterfaceType => typeof(IScriptableObjectCollectionGenerator<,>); + + [InitializeOnLoadMethod] + private static void Initialize() + { + // Make sure we clean this when code reloads. + generatorTypeToInstance.Clear(); + } + + public static IScriptableObjectCollectionGeneratorBase GetGenerator(Type type) + { + bool existed = generatorTypeToInstance.TryGetValue( + type, out IScriptableObjectCollectionGeneratorBase instance); + if (!existed) + { + instance = (IScriptableObjectCollectionGeneratorBase)Activator.CreateInstance(type); + generatorTypeToInstance.Add(type, instance); + } + + return instance; + } + + private static void GetGeneratorTypes(Type generatorType, out Type collectionType, out Type templateType) + { + Type interfaceType = generatorType.GetInterface(InterfaceType.Name); + Type[] genericArguments = interfaceType.GetGenericArguments(); + collectionType = genericArguments[0]; + templateType = genericArguments[1]; + } + + private static Type[] GetGeneratorTypes() + { + return InterfaceType.GetAllAssignableClasses(); + } + + public static Type GetGeneratorTypeForCollection(Type collectionType) + { + Type[] generatorTypes = GetGeneratorTypes(); + foreach (Type generatorType in generatorTypes) + { + GetGeneratorTypes(generatorType, out Type generatorCollectionType, out Type generatorTemplateType); + if (generatorCollectionType == collectionType) + return generatorType; + } + + return null; + } + + public static void RunAllGenerators() + { + Type[] generatorTypes = GetGeneratorTypes(); + foreach (Type generatorType in generatorTypes) + { + RunGeneratorInternal(generatorType, false); + } + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + public static void RunGenerator(Type generatorType, bool generateStaticAccess = false) + { + RunGeneratorInternal(generatorType, true, generateStaticAccess); + } + + public static void RunGenerator(bool generateStaticAccess = false) + where GeneratorType : IScriptableObjectCollectionGeneratorBase + { + RunGenerator(typeof(GeneratorType), generateStaticAccess); + } + + public static void RunGenerator( + IScriptableObjectCollectionGeneratorBase generator, bool generateStaticAccess = false) + { + RunGeneratorInternal(generator, true, generateStaticAccess); + } + + private static void RunGeneratorInternal(Type generatorType, bool refresh, bool generateStaticAccess = false) + { + IScriptableObjectCollectionGeneratorBase generator = GetGenerator(generatorType); + + RunGeneratorInternal(generator, refresh, generateStaticAccess); + } + + private static void RunGeneratorInternal( + IScriptableObjectCollectionGeneratorBase generator, bool refresh, bool generateStaticAccess) + { + Type generatorType = generator.GetType(); + + GetGeneratorTypes(generatorType, out Type collectionType, out Type itemTemplateType); + + // Check that the corresponding collection exists. + CollectionsRegistry.Instance.TryGetCollectionOfType( + collectionType, out ScriptableObjectCollection collection); + if (collection == null) + { + Debug.LogWarning( + $"Tried to generate items for collection '{collectionType.Name}' but no such " + + $"collection existed."); + return; + } + + // Make an empty list that will hold the generated item templates. + Type genericListType = typeof(List<>); + Type templateListType = genericListType.MakeGenericType(itemTemplateType); + IList templates = (IList)Activator.CreateInstance(templateListType); + + // Make the generator generate item templates. + MethodInfo getItemTemplatesMethod = generatorType.GetMethod( + "GetItemTemplates", BindingFlags.Public | BindingFlags.Instance); + getItemTemplatesMethod.Invoke(generator, new object[] {templates, collection}); + + // If necessary, first remove any items that weren't re-generated. + bool shouldRemoveNonGeneratedItems = (bool)generatorType + .GetProperty("ShouldRemoveNonGeneratedItems", BindingFlags.Public | BindingFlags.Instance) + .GetValue(generator); + if (shouldRemoveNonGeneratedItems) + { + for (int i = collection.Items.Count - 1; i >= 0; i--) + { + bool shouldRemoveItem = false; + + // Remove any items for which there isn't a template by the same name. + bool foundItemOfSameName = false; + for (int j = 0; j < templates.Count; j++) + { + ItemTemplate itemTemplate = (ItemTemplate)templates[j]; + if (collection.Items[i].name == itemTemplate.name) + { + foundItemOfSameName = true; + break; + } + } + if (!foundItemOfSameName) + { + // No corresponding template existed, so remove this item. + ScriptableObject itemToRemove = collection.Items[i]; + collection.RemoveAt(i); + AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(itemToRemove)); + } + } + } + + // Now try to find or create corresponding items in the collection and copy the fields over. + for (int i = 0; i < templates.Count; i++) + { + ItemTemplate itemTemplate = (ItemTemplate)templates[i]; + + if (itemTemplate == null) + continue; + + + if (!TryGetItemTemplateType(itemTemplate, out Type templateItemType)) + templateItemType = collection.GetItemType(); + + ISOCItem itemInstance = collection.GetOrAddNew(templateItemType, itemTemplate.name); + + CopyFieldsFromTemplateToItem(itemTemplate, itemInstance); + } + + if (refresh) + { + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + if (generateStaticAccess) + CodeGenerationUtility.GenerateStaticCollectionScript(collection); + } + + private static bool TryGetItemTemplateType(ItemTemplate itemTemplate, out Type resultType) + { + Type itemType = GetGenericItemType(itemTemplate); + if (itemType == null) + { + resultType = null; + return false; + } + + resultType = itemType.GetGenericArguments().First(); + return resultType != null; + } + + public static Type GetTemplateItemType(ItemTemplate itemTemplate) + { + Type itemType = GetGenericItemType(itemTemplate); + if (itemType == null) + return null; + + Type genericType = itemType.GetGenericArguments().First(); + return genericType; + } + + private static Type GetGenericItemType(ItemTemplate itemTemplate) + { + Type baseType = itemTemplate.GetType().BaseType; + + while (baseType != null) + { + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(ItemTemplate<>)) + return baseType; + baseType = baseType.BaseType; + } + return null; + } + + private static void CopyFieldsFromTemplateToItem(ItemTemplate itemTemplate, ISOCItem itemInstance) + { + SerializedObject serializedObject = new SerializedObject(itemInstance as ScriptableObject); + serializedObject.Update(); + + Type itemTemplateType = itemTemplate.GetType(); + FieldInfo[] fields = itemTemplateType.GetFields( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + foreach (FieldInfo field in fields) + { + CopyFieldToSerializedProperty(field, itemTemplate, serializedObject); + } + + serializedObject.ApplyModifiedProperties(); + } + + private static void CopyFieldToSerializedProperty( + FieldInfo field, object owner, SerializedObject serializedObject) + { + // Make sure the field is serializable. + if (field.IsPrivate && field.GetCustomAttribute() == null) + return; + + // Get the property to copy the value to. + SerializedProperty serializedProperty = serializedObject.FindProperty(field.Name); + if (serializedProperty == null) + return; + + object value = field.GetValue(owner); + + // Support arrays. + if (serializedProperty.isArray && serializedProperty.propertyType == SerializedPropertyType.Generic) + { + IEnumerable collection = (IEnumerable)value; + serializedProperty.arraySize = collection.Count(); + int index = 0; + foreach (object arrayItem in collection) + { + SerializedProperty arrayElement = serializedProperty.GetArrayElementAtIndex(index); + arrayElement.SetValue(arrayItem); + index++; + } + return; + } + + serializedProperty.SetValue(value); + } + } +} diff --git a/Scripts/Editor/Generators/CollectionGenerators.cs.meta b/Scripts/Editor/Generators/CollectionGenerators.cs.meta new file mode 100644 index 0000000..046acae --- /dev/null +++ b/Scripts/Editor/Generators/CollectionGenerators.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2729b17b3256a143aea53a857ae752d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Generators/IScriptableObjectCollectionGenerator.cs b/Scripts/Editor/Generators/IScriptableObjectCollectionGenerator.cs new file mode 100644 index 0000000..437e175 --- /dev/null +++ b/Scripts/Editor/Generators/IScriptableObjectCollectionGenerator.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + public interface IScriptableObjectCollectionGeneratorBase + { + /// + /// If specified, any items that do not match the returned item templates get removed. + /// Return false if you want to generate items but allow people to manually add items of their own. + /// + bool ShouldRemoveNonGeneratedItems { get; } + } + + /// + /// Interface for classes that generate items for a Scriptable Object Collection. + /// + /// The type of collection to generate items for. + /// The template class that represents items to add/update. + public interface IScriptableObjectCollectionGenerator + : IScriptableObjectCollectionGeneratorBase + where CollectionType : ScriptableObjectCollection + where TemplateType : ItemTemplate + { + void GetItemTemplates(List templates, CollectionType collection); + } + + /// + /// Base class for templates that represent which items there should be in a collection. + /// + public abstract class ItemTemplate + { + public string name; + } + + public class ItemTemplate : ItemTemplate where T: ISOCItem + { + } + +} diff --git a/Scripts/Editor/Generators/IScriptableObjectCollectionGenerator.cs.meta b/Scripts/Editor/Generators/IScriptableObjectCollectionGenerator.cs.meta new file mode 100644 index 0000000..c5fe2dd --- /dev/null +++ b/Scripts/Editor/Generators/IScriptableObjectCollectionGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bc2f7ec4968271b4db3df6738f0f8de4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Runtime/Core/CollectionsRegistry.cs b/Scripts/Runtime/Core/CollectionsRegistry.cs index 4a9629c..24c6983 100644 --- a/Scripts/Runtime/Core/CollectionsRegistry.cs +++ b/Scripts/Runtime/Core/CollectionsRegistry.cs @@ -223,14 +223,14 @@ public ScriptableObjectCollection GetCollectionByGUID(LongGuid guid) return null; } - public bool TryGetCollectionOfType(out T resultCollection) where T: ScriptableObjectCollection + public bool TryGetCollectionOfType(Type type, out ScriptableObjectCollection resultCollection) { for (int i = 0; i < collections.Count; i++) { ScriptableObjectCollection scriptableObjectCollection = collections[i]; - if (scriptableObjectCollection is T collectionT) + if (scriptableObjectCollection.GetType() == type) { - resultCollection = collectionT; + resultCollection = scriptableObjectCollection; return true; } } @@ -239,6 +239,13 @@ public bool TryGetCollectionOfType(out T resultCollection) where T: Scriptabl return false; } + public bool TryGetCollectionOfType(out T resultCollection) where T: ScriptableObjectCollection + { + bool didFind = TryGetCollectionOfType(typeof(T), out ScriptableObjectCollection baseCollection); + resultCollection = baseCollection as T; + return didFind; + } + public bool TryGetCollectionFromItemType(Type targetType, out ScriptableObjectCollection resultCollection) { if (TryGetCollectionsOfItemType(targetType, out List possibleCollections)) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 5f67850..25f8de0 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using UnityEngine; +using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; #endif @@ -183,6 +184,44 @@ public ScriptableObject AddNew(Type itemType, string assetName = "") return newItem; } + + public ISOCItem AddNewBaseItem(string targetName) + { + return AddNew(GetItemType(), targetName) as ISOCItem; + } + + public ISOCItem GetOrAddNewBaseItem(string targetName) + { + ISOCItem item = Items.FirstOrDefault(o => o.name.Equals(targetName, StringComparison.Ordinal)) as ISOCItem; + if (item != null) + return item; + + return AddNewBaseItem(targetName); + } + + public ISOCItem GetOrAddNew(Type collectionType, string targetName) + { + ISOCItem item = Items.FirstOrDefault(o => o.name.Equals(targetName, StringComparison.Ordinal)) as ISOCItem; + if (item != null) + return item; + + return (ISOCItem) AddNew(collectionType, targetName); + } + + public static void Rename(ISOCItem item, string newName) + { + string path = AssetDatabase.GetAssetPath(item as Object); + + // If the new name includes the full directory path or the wrong extension, get rid of that. + newName = Path.GetFileNameWithoutExtension(newName); + + // Make sure the correct extension is included. + const string extension = ".asset"; + if (!newName.EndsWith(extension)) + newName += extension; + + AssetDatabase.RenameAsset(path, newName); + } #endif public virtual Type GetItemType() @@ -464,14 +503,6 @@ public T GetOrAddNew(string targetName = null) where T : TObjectType return (T) AddNew(typeof(T), targetName); } - public TObjectType GetOrAddNew(Type collectionType, string targetName) - { - TObjectType item = Items.FirstOrDefault(o => o.name.Equals(targetName, StringComparison.Ordinal)) as TObjectType; - if (item != null) - return item; - - return (TObjectType) AddNew(collectionType, targetName); - } public TObjectType GetOrAddNew(string targetName) { diff --git a/Scripts/Runtime/Extensions/TypeExtensions.cs b/Scripts/Runtime/Extensions/TypeExtensions.cs index 4c66794..42ee1cb 100644 --- a/Scripts/Runtime/Extensions/TypeExtensions.cs +++ b/Scripts/Runtime/Extensions/TypeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace BrunoMikoski.ScriptableObjectCollections @@ -49,5 +50,26 @@ public static Type GetBaseGenericType(this Type type) return null; } + + public static Type[] GetAllAssignableClasses( + this Type type, bool includeAbstract = true, bool includeItself = false) + { + IEnumerable allTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()); + + if (type.IsGenericType) + { + if (type.IsInterface) + { + return allTypes.Where(t => t.GetInterfaces().Any( + i => i.IsGenericType && i.GetGenericTypeDefinition() == type)).ToArray(); + } + return allTypes.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == type).ToArray(); + } + + return allTypes.Where( + t => type.IsAssignableFrom(t) && (t != type || includeItself) && (includeAbstract || !t.IsAbstract)) + .ToArray(); + } } }