From da7789b327d7a941a5db98240fd16c8ae6435bfe Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 20:58:01 +0200 Subject: [PATCH 01/24] Use TypeCache instead of GetAllAssignableClasses --- .../Editor/Generators/CollectionGenerators.cs | 582 +++++++++--------- Scripts/Runtime/Extensions/TypeExtensions.cs | 129 ++-- 2 files changed, 345 insertions(+), 366 deletions(-) diff --git a/Scripts/Editor/Generators/CollectionGenerators.cs b/Scripts/Editor/Generators/CollectionGenerators.cs index 52172ba..76b97e8 100644 --- a/Scripts/Editor/Generators/CollectionGenerators.cs +++ b/Scripts/Editor/Generators/CollectionGenerators.cs @@ -1,291 +1,291 @@ -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[] GetAllGeneratorTypes() - { - return InterfaceType.GetAllAssignableClasses(); - } - - public static Type GetGeneratorTypeForCollection(Type collectionType, bool allowSubclasses = true) - { - Type[] generatorTypes = GetAllGeneratorTypes(); - foreach (Type generatorType in generatorTypes) - { - GetGeneratorTypes(generatorType, out Type generatorCollectionType, out Type generatorTemplateType); - if (generatorCollectionType == collectionType || collectionType.IsSubclassOf(generatorCollectionType)) - return generatorType; - } - - return null; - } - - public static void RunAllGenerators() - { - Type[] generatorTypes = GetAllGeneratorTypes(); - foreach (Type generatorType in generatorTypes) - { - RunGeneratorInternal(generatorType, false); - } - - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - - public static void RunGenerator(Type generatorType, ScriptableObjectCollection targetCollection = null, bool generateStaticAccess = false) - { - IScriptableObjectCollectionGeneratorBase generator = GetGenerator(generatorType); - - RunGeneratorInternal(generator, targetCollection, true, generateStaticAccess); - } - - 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, null, true, generateStaticAccess); - } - - private static void RunGeneratorInternal(Type generatorType, bool refresh, bool generateStaticAccess = false) - { - IScriptableObjectCollectionGeneratorBase generator = GetGenerator(generatorType); - - RunGeneratorInternal(generator, null, refresh, generateStaticAccess); - } - - private static void RunGeneratorInternal( - IScriptableObjectCollectionGeneratorBase generator, ScriptableObjectCollection collection, bool refresh, bool generateStaticAccess) - { - Type generatorType = generator.GetType(); - - GetGeneratorTypes(generatorType, out Type collectionType, out Type itemTemplateType); - - if (collection == null) - { - // Check that the corresponding collection exists. - CollectionsRegistry.Instance.TryGetCollectionOfType( - collectionType, out 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--) - { - // 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); - } - - - // Optional Callback to be called when the generation completes - MethodInfo completionCallback = generatorType.GetMethod( - "OnItemsGenerationComplete", BindingFlags.Public | BindingFlags.Instance); - if (completionCallback != null) - completionCallback!.Invoke(generator, new object[] {collection }); - - - 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); - } - } -} +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[] GetAllGeneratorTypes() + { + return TypeCache.GetTypesDerivedFrom(InterfaceType).ToArray(); + } + + public static Type GetGeneratorTypeForCollection(Type collectionType, bool allowSubclasses = true) + { + Type[] generatorTypes = GetAllGeneratorTypes(); + foreach (Type generatorType in generatorTypes) + { + GetGeneratorTypes(generatorType, out Type generatorCollectionType, out Type generatorTemplateType); + if (generatorCollectionType == collectionType || collectionType.IsSubclassOf(generatorCollectionType)) + return generatorType; + } + + return null; + } + + public static void RunAllGenerators() + { + Type[] generatorTypes = GetAllGeneratorTypes(); + foreach (Type generatorType in generatorTypes) + { + RunGeneratorInternal(generatorType, false); + } + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + public static void RunGenerator(Type generatorType, ScriptableObjectCollection targetCollection = null, bool generateStaticAccess = false) + { + IScriptableObjectCollectionGeneratorBase generator = GetGenerator(generatorType); + + RunGeneratorInternal(generator, targetCollection, true, generateStaticAccess); + } + + 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, null, true, generateStaticAccess); + } + + private static void RunGeneratorInternal(Type generatorType, bool refresh, bool generateStaticAccess = false) + { + IScriptableObjectCollectionGeneratorBase generator = GetGenerator(generatorType); + + RunGeneratorInternal(generator, null, refresh, generateStaticAccess); + } + + private static void RunGeneratorInternal( + IScriptableObjectCollectionGeneratorBase generator, ScriptableObjectCollection collection, bool refresh, bool generateStaticAccess) + { + Type generatorType = generator.GetType(); + + GetGeneratorTypes(generatorType, out Type collectionType, out Type itemTemplateType); + + if (collection == null) + { + // Check that the corresponding collection exists. + CollectionsRegistry.Instance.TryGetCollectionOfType( + collectionType, out 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--) + { + // 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); + } + + + // Optional Callback to be called when the generation completes + MethodInfo completionCallback = generatorType.GetMethod( + "OnItemsGenerationComplete", BindingFlags.Public | BindingFlags.Instance); + if (completionCallback != null) + completionCallback!.Invoke(generator, new object[] {collection }); + + + 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/Runtime/Extensions/TypeExtensions.cs b/Scripts/Runtime/Extensions/TypeExtensions.cs index 42ee1cb..6fed890 100644 --- a/Scripts/Runtime/Extensions/TypeExtensions.cs +++ b/Scripts/Runtime/Extensions/TypeExtensions.cs @@ -1,75 +1,54 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -namespace BrunoMikoski.ScriptableObjectCollections -{ - public static partial class TypeExtensions - { - private static bool IsList(this Type type) - { - if (type.IsArray) - return false; - - Type[] interfaces = type.GetInterfaces(); - for (int i = 0; i < interfaces.Length; i++) - { - Type @interface = interfaces[i]; - if (@interface.IsGenericType) - { - if (@interface.GetGenericTypeDefinition() == typeof(ICollection<>)) - return true; - } - } - return false; - } - - public static Type GetArrayOrListType(this Type type) - { - if (type.IsArray) - return type.GetElementType(); - - if (type.IsList()) - return type.GetGenericArguments()[0]; - - return null; - } - - public static Type GetBaseGenericType(this Type type) - { - Type baseType = type.BaseType; - - while (baseType != null) - { - if (baseType.IsGenericType && - baseType.GetGenericTypeDefinition() == typeof(CollectionItemIndirectReference<>)) - return baseType; - baseType = baseType.BaseType; - } - - 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(); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + public static partial class TypeExtensions + { + private static bool IsList(this Type type) + { + if (type.IsArray) + return false; + + Type[] interfaces = type.GetInterfaces(); + for (int i = 0; i < interfaces.Length; i++) + { + Type @interface = interfaces[i]; + if (@interface.IsGenericType) + { + if (@interface.GetGenericTypeDefinition() == typeof(ICollection<>)) + return true; + } + } + return false; + } + + public static Type GetArrayOrListType(this Type type) + { + if (type.IsArray) + return type.GetElementType(); + + if (type.IsList()) + return type.GetGenericArguments()[0]; + + return null; + } + + public static Type GetBaseGenericType(this Type type) + { + Type baseType = type.BaseType; + + while (baseType != null) + { + if (baseType.IsGenericType && + baseType.GetGenericTypeDefinition() == typeof(CollectionItemIndirectReference<>)) + return baseType; + baseType = baseType.BaseType; + } + + return null; + } + } +} From 1d9f0b548052fbebf2b2179585ef61bbc2716df9 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 20:59:41 +0200 Subject: [PATCH 02/24] Don't crash if collection in CollectionRegistry is null --- Scripts/Runtime/Core/CollectionsRegistry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Runtime/Core/CollectionsRegistry.cs b/Scripts/Runtime/Core/CollectionsRegistry.cs index d4492ea..4af8bdd 100644 --- a/Scripts/Runtime/Core/CollectionsRegistry.cs +++ b/Scripts/Runtime/Core/CollectionsRegistry.cs @@ -513,7 +513,7 @@ public void UpdateAutoSearchForCollections() for (int i = 0; i < Collections.Count; i++) { ScriptableObjectCollection collection = Collections[i]; - if (!collection.AutomaticallyLoaded) + if (collection != null && !collection.AutomaticallyLoaded) { SetAutoSearchForCollections(true); return; From 72089b883920ab82a98fa391b19956f00a3927f9 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 21:03:02 +0200 Subject: [PATCH 03/24] Fixed error when refreshing a graph where the last item was deleted --- Scripts/Runtime/Core/ScriptableObjectCollection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 6e0efcc..25da4b9 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -321,7 +321,7 @@ public void Swap(int targetIndex, int newIndex) (items[targetIndex], items[newIndex]) = (items[newIndex], items[targetIndex]); ObjectUtility.SetDirty(this); } - + [ContextMenu("Refresh Collection")] public void RefreshCollection() { @@ -369,6 +369,7 @@ public void RefreshCollection() RemoveAt(i); Debug.Log($"Removing item at index {i} as it is null"); changed = true; + continue; } ScriptableObject scriptableObject = items[i]; From 82b43cfabec8f10a677460c4ad5fd1fa6be0572a Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 21:27:00 +0200 Subject: [PATCH 04/24] Added sort command for collection editor --- .../CustomEditors/CollectionCustomEditor.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index 3225e4e..e6943a8 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -740,12 +740,32 @@ private void ShowOptionsForIndex(MouseUpEvent evt, int targetIndex) false, () => { CopyCollectionItemUtility.SetSource(scriptableObject); } ); + + menu.AddDisabledItem(new GUIContent("Sort Items"), false); } else { menu.AddDisabledItem( new GUIContent("Copy Values"), false); + + menu.AddItem( + new GUIContent("Sort Items"), + false, + () => + { + Undo.RecordObject(collection, "Sort Items"); + List itemsToBeSorted = new(); + foreach (int selectedIndex in collectionItemListView.selectedIndices) + { + itemsToBeSorted.Add(filteredItems[selectedIndex]); + } + + collection.Sort(new LimitedComparer(itemsToBeSorted)); + + ReloadFilteredItems(); + } + ); } if (CopyCollectionItemUtility.CanPasteToTarget(scriptableObject)) { @@ -1111,4 +1131,48 @@ private static void OnPostprocessAllAssets(string[] importedAssets, string[] del } } } + + internal struct LimitedComparer : IComparer + { + private readonly HashSet m_ItemsToBeSorted; + private readonly int m_FirstItemIndex; + public LimitedComparer(List itemsToBeSorted) + { + m_ItemsToBeSorted = itemsToBeSorted.OfType().ToHashSet(); + m_FirstItemIndex = m_ItemsToBeSorted.Select(i => i.Index).Min(); + } + + public int Compare(ScriptableObject xObject, ScriptableObject yObject) + { + if (ReferenceEquals(xObject, yObject)) return 0; + if (yObject is null) return 1; + if (xObject is null) return -1; + + if (xObject is not ScriptableObjectCollectionItem x || yObject is not ScriptableObjectCollectionItem y) + { + return 0; // or throw an exception if you want to enforce that both are of type ScriptableObjectCollectionItem + } + + bool toSortX = m_ItemsToBeSorted.Contains(x); + bool toSortY = m_ItemsToBeSorted.Contains(y); + + if (toSortX && toSortY) + { + return String.Compare(x.name, y.name, StringComparison.Ordinal); + } + + if (toSortX) + { + // compare y with the first item in the list to sort + return x.Index - m_FirstItemIndex; + } + if (toSortY) + { + // compare x with the first item in the list to sort + return m_FirstItemIndex - y.Index; + } + // neither x nor y are in the list to sort, compare by Index + return x.Index.CompareTo(y.Index); + } + } } \ No newline at end of file From 6d3d97ee10a509fd0f4ef2dac42800b8835657f0 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 21:27:11 +0200 Subject: [PATCH 05/24] fixed collection move not updating collection parent --- Scripts/Editor/CustomEditors/CollectionCustomEditor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index e6943a8..858867d 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -950,6 +950,10 @@ private void MoveItem(ScriptableObject item, ScriptableObjectCollection targetCo collection.Remove(item); targetCollection.Add(item); + if (item is ISOCItem socItem) + { + socItem.SetCollection(targetCollection); + } string itemPath = AssetDatabase.GetAssetPath(item); string targetCollectionPath = AssetDatabase.GetAssetPath(targetCollection); From c6257f2fee3ba5475182986b843ef7f0b58be98c Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 21:27:37 +0200 Subject: [PATCH 06/24] Don't move file if target folder is same --- Scripts/Editor/CustomEditors/CollectionCustomEditor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index 858867d..89fc1fb 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -968,9 +968,12 @@ private void MoveItem(ScriptableObject item, ScriptableObjectCollection targetCo string finalDirectory = hasItemsFolder ? itemsFolderPath : directory; string fileName = Path.GetFileName(itemPath); - string newPath = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(finalDirectory, fileName)); - + string newPath = Path.Combine(finalDirectory, fileName); + if(!string.Equals(Path.GetFullPath(itemPath), Path.GetFullPath(newPath), StringComparison.InvariantCultureIgnoreCase)) + { + newPath = AssetDatabase.GenerateUniqueAssetPath(newPath); AssetDatabase.MoveAsset(itemPath, newPath); + } } AssetDatabase.SaveAssets(); From 8608bf54357f8a8dcedb663e0b98437ebc559685 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 21:28:18 +0200 Subject: [PATCH 07/24] Fixed whitespace --- Scripts/Editor/CustomEditors/CollectionCustomEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index 89fc1fb..5ee7a40 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -972,7 +972,7 @@ private void MoveItem(ScriptableObject item, ScriptableObjectCollection targetCo if(!string.Equals(Path.GetFullPath(itemPath), Path.GetFullPath(newPath), StringComparison.InvariantCultureIgnoreCase)) { newPath = AssetDatabase.GenerateUniqueAssetPath(newPath); - AssetDatabase.MoveAsset(itemPath, newPath); + AssetDatabase.MoveAsset(itemPath, newPath); } } From c3727191a64b4dbc2509ae9fa3b6de076760a236 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Wed, 27 Aug 2025 21:29:19 +0200 Subject: [PATCH 08/24] Allow multiple collection namespace --- Scripts/Editor/Core/CodeGenerationUtility.cs | 39 +++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 391e329..a65dc53 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -489,8 +489,12 @@ private static string[] GetCollectionDirectives(ScriptableObjectCollection colle private static void WriteDirectAccessCollectionStatic(ScriptableObjectCollection collection, StreamWriter writer, ref int indentation, bool useBaseClass) { - AppendLine(writer, indentation, $"private static bool {HasCachedValuesName};"); - AppendLine(writer, indentation, $"private static {collection.GetType().Name} {PrivateValuesName};"); + string privateValuesName = GetCollectionSpecificVariableName(PrivateValuesName, collection.name); + string publicValuesName = GetCollectionSpecificVariableName(PublicValuesName, collection.name); + string hasCachedValuesName = GetCollectionSpecificVariableName(HasCachedValuesName, collection.name); + + AppendLine(writer, indentation, $"private static bool {hasCachedValuesName};"); + AppendLine(writer, indentation, $"private static {collection.GetType().Name} {privateValuesName};"); AppendLine(writer, indentation); @@ -508,20 +512,20 @@ private static void WriteDirectAccessCollectionStatic(ScriptableObjectCollection AppendLine(writer, indentation, - $"public static {collection.GetType().FullName} {PublicValuesName}"); + $"public static {collection.GetType().FullName} {publicValuesName}"); AppendLine(writer, indentation, "{"); indentation++; AppendLine(writer, indentation, "get"); AppendLine(writer, indentation, "{"); indentation++; - AppendLine(writer, indentation, $"if (!{HasCachedValuesName})"); + AppendLine(writer, indentation, $"if (!{hasCachedValuesName})"); indentation++; (long, long) collectionGUIDValues = collection.GUID.GetRawValues(); AppendLine(writer, indentation, - $"{HasCachedValuesName} = CollectionsRegistry.Instance.TryGetCollectionByGUID(new LongGuid({collectionGUIDValues.Item1}, {collectionGUIDValues.Item2}), out {PrivateValuesName});"); + $"{hasCachedValuesName} = CollectionsRegistry.Instance.TryGetCollectionByGUID(new LongGuid({collectionGUIDValues.Item1}, {collectionGUIDValues.Item2}), out {privateValuesName});"); indentation--; - AppendLine(writer, indentation, $"return {PrivateValuesName};"); + AppendLine(writer, indentation, $"return {privateValuesName};"); indentation--; AppendLine(writer, indentation, "}"); indentation--; @@ -553,7 +557,7 @@ private static void WriteDirectAccessCollectionStatic(ScriptableObjectCollection indentation++; (long, long) collectionItemGUIDValues = socItem.GUID.GetRawValues(); AppendLine(writer, indentation, - $"{privateHasCachedName} = {PublicValuesName}.TryGetItemByGUID(new LongGuid({collectionItemGUIDValues.Item1}, {collectionItemGUIDValues.Item2}), out {privateStaticCachedName});"); + $"{privateHasCachedName} = {publicValuesName}.TryGetItemByGUID(new LongGuid({collectionItemGUIDValues.Item1}, {collectionItemGUIDValues.Item2}), out {privateStaticCachedName});"); indentation--; AppendLine(writer, indentation, $"return {privateStaticCachedName};"); indentation--; @@ -568,12 +572,16 @@ private static void WriteDirectAccessCollectionStatic(ScriptableObjectCollection private static void WriteNonAutomaticallyLoadedCollectionItems(ScriptableObjectCollection collection, StreamWriter writer, ref int indentation, bool useBaseClass) { + string privateValuesName = GetCollectionSpecificVariableName(PrivateValuesName, collection.name); + string publicValuesName = GetCollectionSpecificVariableName(PublicValuesName, collection.name); + string hasCachedValuesName = GetCollectionSpecificVariableName(HasCachedValuesName, collection.name); + AppendLine(writer, indentation, $"public static bool IsCollectionLoaded()"); AppendLine(writer, indentation, "{"); indentation++; - AppendLine(writer, indentation, $"return {PublicValuesName} != null;"); + AppendLine(writer, indentation, $"return {publicValuesName} != null;"); indentation--; AppendLine(writer, indentation, "}"); @@ -600,8 +608,8 @@ private static void WriteNonAutomaticallyLoadedCollectionItems(ScriptableObjectC AppendLine(writer, indentation, "{"); indentation++; AppendLine(writer, indentation, "CollectionsRegistry.Instance.RegisterCollection(operation.Result);"); - AppendLine(writer, indentation, $"{HasCachedValuesName} = true;"); - AppendLine(writer, indentation, $"{PrivateValuesName} = operation.Result;"); + AppendLine(writer, indentation, $"{hasCachedValuesName} = true;"); + AppendLine(writer, indentation, $"{privateValuesName} = operation.Result;"); indentation--; AppendLine(writer, indentation, "};"); AppendLine(writer, indentation, "return collectionHandle;"); @@ -612,9 +620,9 @@ private static void WriteNonAutomaticallyLoadedCollectionItems(ScriptableObjectC AppendLine(writer, indentation, "public static void UnloadCollection()"); AppendLine(writer, indentation, "{"); indentation++; - AppendLine(writer, indentation, $"CollectionsRegistry.Instance.UnregisterCollection({PublicValuesName});"); - AppendLine(writer, indentation, $"{HasCachedValuesName} = false;"); - AppendLine(writer, indentation, $"{PrivateValuesName} = null;"); + AppendLine(writer, indentation, $"CollectionsRegistry.Instance.UnregisterCollection({publicValuesName});"); + AppendLine(writer, indentation, $"{hasCachedValuesName} = false;"); + AppendLine(writer, indentation, $"{privateValuesName} = null;"); AppendLine(writer, indentation, "Addressables.Release(collectionHandle);"); indentation--; @@ -631,5 +639,10 @@ public static bool DoesStaticFileForCollectionExist(ScriptableObjectCollection c AssetDatabase.GetAssetPath(SOCSettings.Instance.GetParentDefaultAssetScriptsFolderForCollection(collection)), $"{SOCSettings.Instance.GetStaticFilenameForCollection(collection)}.g.cs")); } + + private static string GetCollectionSpecificVariableName(string variableName, string collectionName) + { + return $"{variableName}_{collectionName}"; + } } } \ No newline at end of file From fe9c8efdc2461ba05958d230a78a98bcff048d6c Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 15:11:42 +0200 Subject: [PATCH 09/24] Fixed absurdly long error message going off screen --- Scripts/Runtime/Core/ScriptableObjectCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index d44d38b..362f94f 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -393,7 +393,7 @@ public void RefreshCollection() if (itemsFromOtherCollections.Any()) { int result = EditorUtility.DisplayDialogComplex("Items from another collections", - $"The following items {string.Join(",", itemsFromOtherCollections.Select(o => o.name).ToArray())} belong to other collection, what you want to do?", + $"The following items {string.Join(", ", itemsFromOtherCollections.Select(o => o.name).ToArray())} belong to other collection, what you want to do?", "Move to the assigned collection", $"Assign it the parent collection ", "Do nothing"); if (result == 0) From 9b94b69dd368518109bccd9c37c0461757ed46e1 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 15:27:57 +0200 Subject: [PATCH 10/24] Fixed moving asset from "Path/Asset.asset" to "Path/Asset1.asset" --- Scripts/Runtime/Utils/SOCItemUtility.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Scripts/Runtime/Utils/SOCItemUtility.cs b/Scripts/Runtime/Utils/SOCItemUtility.cs index 91bfbb0..1ea47c7 100644 --- a/Scripts/Runtime/Utils/SOCItemUtility.cs +++ b/Scripts/Runtime/Utils/SOCItemUtility.cs @@ -40,10 +40,13 @@ public static void MoveItem(ISOCItem item, ScriptableObjectCollection targetColl string finalDirectory = hasItemsFolder ? itemsFolderPath : directory; string fileName = Path.GetFileName(itemPath); + string newPathCandidate = Path.Combine(finalDirectory, fileName); - string newPath = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(finalDirectory, fileName)); - - AssetDatabase.MoveAsset(itemPath, newPath); + if (itemPath != newPathCandidate) + { + string newPath = AssetDatabase.GenerateUniqueAssetPath(newPathCandidate); + AssetDatabase.MoveAsset(itemPath, newPath); + } } AssetDatabase.SaveAssets(); From c1aac39f81880549067be096842d9b138c1425b2 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 15:41:44 +0200 Subject: [PATCH 11/24] Try to make asset moving faster by using AssetDatabase.StartAssetEditing --- .../Runtime/Core/ScriptableObjectCollection.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 362f94f..2d7ea0c 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -398,11 +398,18 @@ public void RefreshCollection() if (result == 0) { - foreach (ISOCItem itemsFromOtherCollection in itemsFromOtherCollections) + try { + AssetDatabase.StartAssetEditing(); + foreach (ISOCItem itemsFromOtherCollection in itemsFromOtherCollections) + { + SOCItemUtility.MoveItem(itemsFromOtherCollection, itemsFromOtherCollection.Collection); + changed = true; + ObjectUtility.SetDirty(itemsFromOtherCollection.Collection); + } + } + finally { - SOCItemUtility.MoveItem(itemsFromOtherCollection, itemsFromOtherCollection.Collection); - changed = true; - ObjectUtility.SetDirty(itemsFromOtherCollection.Collection); + AssetDatabase.StopAssetEditing(); } } From c5a4df5ab7578b4a14d832c376b23f77b06b969c Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 16:48:03 +0200 Subject: [PATCH 12/24] Fixed stack overflow --- Scripts/Runtime/Utils/SOCItemUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/Runtime/Utils/SOCItemUtility.cs b/Scripts/Runtime/Utils/SOCItemUtility.cs index 1ea47c7..72bbc57 100644 --- a/Scripts/Runtime/Utils/SOCItemUtility.cs +++ b/Scripts/Runtime/Utils/SOCItemUtility.cs @@ -12,9 +12,9 @@ public static class SOCItemUtility public static void MoveItem(ScriptableObject item, ScriptableObjectCollection targetCollection, Action onCompleteCallback = null) { - if (item is ISOCItem) + if (item is ISOCItem iItem) { - MoveItem(item, targetCollection, onCompleteCallback); + MoveItem(iItem, targetCollection, onCompleteCallback); } } From 8da50d70bd2109910379998f1ed6126efeef1474 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 17:12:16 +0200 Subject: [PATCH 13/24] Fixed path compare --- Scripts/Runtime/Utils/SOCItemUtility.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Scripts/Runtime/Utils/SOCItemUtility.cs b/Scripts/Runtime/Utils/SOCItemUtility.cs index 72bbc57..85957d5 100644 --- a/Scripts/Runtime/Utils/SOCItemUtility.cs +++ b/Scripts/Runtime/Utils/SOCItemUtility.cs @@ -18,6 +18,13 @@ public static void MoveItem(ScriptableObject item, ScriptableObjectCollection ta } } + public static string NormalizePath(string path) + { + return Path.GetFullPath(new Uri(path).LocalPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .ToUpperInvariant(); + } + public static void MoveItem(ISOCItem item, ScriptableObjectCollection targetCollection, Action onCompleteCallback = null) { #if UNITY_EDITOR @@ -42,7 +49,7 @@ public static void MoveItem(ISOCItem item, ScriptableObjectCollection targetColl string fileName = Path.GetFileName(itemPath); string newPathCandidate = Path.Combine(finalDirectory, fileName); - if (itemPath != newPathCandidate) + if (NormalizePath(itemPath) != NormalizePath(newPathCandidate)) { string newPath = AssetDatabase.GenerateUniqueAssetPath(newPathCandidate); AssetDatabase.MoveAsset(itemPath, newPath); From 4484f5c5272aca8183d69d7c5a206ee69882fde8 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 17:17:44 +0200 Subject: [PATCH 14/24] Don't fight with neighbor collections --- .../Core/ScriptableObjectCollection.cs | 79 +++++++++++-------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 2d7ea0c..7f1b6e0 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -21,12 +21,12 @@ public LongGuid GUID { if (guid.IsValid()) return guid; - + GenerateNewGUID(); return guid; } } - + [SerializeField, HideInInspector] protected List items = new List(); public List Items => items; @@ -39,7 +39,7 @@ public LongGuid GUID public object SyncRoot => throw new NotSupportedException(); public bool IsSynchronized => throw new NotSupportedException(); - + public bool IsFixedSize => false; public bool IsReadOnly => false; @@ -50,7 +50,7 @@ public ScriptableObject this[int index] get => items[index]; set => throw new NotSupportedException(); } - + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -78,7 +78,7 @@ public void CopyTo(Array array, int index) ++i; } } - + public void CopyTo(List list) { list.Capacity = Math.Max(list.Capacity, Count); @@ -87,7 +87,7 @@ public void CopyTo(List list) list.Add(e); } } - + public int Add(object value) { Add((ScriptableObject) value); @@ -98,10 +98,10 @@ public bool Add(ScriptableObject item) { if (item is not ISOCItem socItem) return false; - + if (items.Contains(item)) return false; - + items.Add(item); socItem.SetCollection(this); @@ -125,7 +125,7 @@ public ScriptableObject AddNew(Type itemType, string assetName = "") if (!typeof(ISOCItem).IsAssignableFrom(itemType)) throw new Exception($"{itemType} does not implement {nameof(ISOCItem)}"); - + ScriptableObject newItem = CreateInstance(itemType); string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(this)); string parentFolderPath = Path.Combine(assetPath, "Items" ); @@ -137,7 +137,7 @@ public ScriptableObject AddNew(Type itemType, string assetName = "") { itemName = $"{itemType.Name}"; } - + string uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(parentFolderPath, itemName + ".asset")); string uniqueName = Path.GetFileNameWithoutExtension(uniqueAssetPath); @@ -148,7 +148,7 @@ public ScriptableObject AddNew(Type itemType, string assetName = "") ISOCItem socItem = newItem as ISOCItem; socItem.GenerateNewGUID(); - + this.Add(newItem); AssetDatabase.CreateAsset(newItem, uniqueAssetPath); @@ -159,12 +159,12 @@ 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; @@ -173,7 +173,7 @@ public ISOCItem GetOrAddNewBaseItem(string targetName) return AddNewBaseItem(targetName); } - + public ISOCItem GetOrAddNew(Type collectionType, string targetName) { ISOCItem item = Items.FirstOrDefault(o => o.name.Equals(targetName, StringComparison.Ordinal)) as ISOCItem; @@ -194,7 +194,7 @@ public static void Rename(ISOCItem item, string newName) const string extension = ".asset"; if (!newName.EndsWith(extension)) newName += extension; - + AssetDatabase.RenameAsset(path, newName); } #endif @@ -261,7 +261,7 @@ public void Insert(int index, ScriptableObject item) items.Insert(index, item); if (item is ISOCItem socItem) socItem.SetCollection(this); - + ObjectUtility.SetDirty(this); } @@ -331,6 +331,14 @@ public void RefreshCollection() string folder = Path.GetDirectoryName(assetPath); string[] guids = AssetDatabase.FindAssets($"t:{collectionItemType.Name}", new []{folder}); + HashSet neighbors = + AssetDatabase + .FindAssets($"t:ScriptableObjectCollection", new[] { folder }) + .Select(AssetDatabase.GUIDToAssetPath) + .Select(AssetDatabase.LoadAssetAtPath) + .Where(o => o != null && o != this) + .ToHashSet(); + List itemsFromOtherCollections = new List(); for (int i = 0; i < guids.Length; i++) { @@ -347,6 +355,11 @@ public void RefreshCollection() { if (socItem.Collection != this) { + // Don't fight with neighbor collections + if (socItem.Collection.Contains(socItem) && neighbors.Contains(socItem.Collection)) + { + continue; + } itemsFromOtherCollections.Add(socItem); continue; } @@ -393,19 +406,19 @@ public void RefreshCollection() if (itemsFromOtherCollections.Any()) { int result = EditorUtility.DisplayDialogComplex("Items from another collections", - $"The following items {string.Join(", ", itemsFromOtherCollections.Select(o => o.name).ToArray())} belong to other collection, what you want to do?", + $"The following items {string.Join(",", itemsFromOtherCollections.Select(o => o.name).ToArray())} belong to other collection, what you want to do?", "Move to the assigned collection", $"Assign it the parent collection ", "Do nothing"); if (result == 0) { try { AssetDatabase.StartAssetEditing(); - foreach (ISOCItem itemsFromOtherCollection in itemsFromOtherCollections) - { - SOCItemUtility.MoveItem(itemsFromOtherCollection, itemsFromOtherCollection.Collection); - changed = true; - ObjectUtility.SetDirty(itemsFromOtherCollection.Collection); - } + foreach (ISOCItem itemsFromOtherCollection in itemsFromOtherCollections) + { + SOCItemUtility.MoveItem(itemsFromOtherCollection, itemsFromOtherCollection.Collection); + changed = true; + ObjectUtility.SetDirty(itemsFromOtherCollection.Collection); + } } finally { @@ -469,7 +482,7 @@ public bool TryGetItemByGUID(LongGuid itemGUID, out T scriptableObjectCollect ISOCItem socItem = item as ISOCItem; if (socItem == null) continue; - + if (socItem.GUID == itemGUID) { scriptableObjectCollectionItem = item as T; @@ -541,8 +554,8 @@ public T GetOrAddNew(string targetName = null) where T : TObjectType return (T) AddNew(typeof(T), targetName); } - - + + public TObjectType GetOrAddNew(string targetName) { TObjectType item = Items.FirstOrDefault(o => o.name.Equals(targetName, StringComparison.Ordinal)) as TObjectType; @@ -551,16 +564,16 @@ public TObjectType GetOrAddNew(string targetName) return AddNew(targetName); } - + public TObjectType AddNew(string targetName) { return (TObjectType) AddNew(GetItemType(), targetName); - } - - public TObjectType AddNew() + } + + public TObjectType AddNew() { return (TObjectType)AddNew(GetItemType()); - } + } #endif [Obsolete("GetItemByGUID(string targetGUID) is obsolete, please regenerate your static class")] @@ -642,8 +655,8 @@ public bool Remove(TObjectType item) ClearCachedValues(); return remove; } - - + + IEnumerator IEnumerable.GetEnumerator() { using (IEnumerator enumerator = base.GetEnumerator()) From 2869c2de8fbaa13ff008fb4869f67710924715ab Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 17:27:39 +0200 Subject: [PATCH 15/24] Fixed exception --- Scripts/Runtime/Utils/SOCItemUtility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Runtime/Utils/SOCItemUtility.cs b/Scripts/Runtime/Utils/SOCItemUtility.cs index 85957d5..ac024ba 100644 --- a/Scripts/Runtime/Utils/SOCItemUtility.cs +++ b/Scripts/Runtime/Utils/SOCItemUtility.cs @@ -20,7 +20,7 @@ public static void MoveItem(ScriptableObject item, ScriptableObjectCollection ta public static string NormalizePath(string path) { - return Path.GetFullPath(new Uri(path).LocalPath) + return Path.GetFullPath(path) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) .ToUpperInvariant(); } From e45787ffd4ba72ddb043d2e353b7ff3704d0379e Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 17:55:17 +0200 Subject: [PATCH 16/24] Improved log Fixed Add returning false when collection was indeed corrected --- .../Core/ScriptableObjectCollection.cs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 7f1b6e0..78eb8ce 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -99,12 +99,23 @@ public bool Add(ScriptableObject item) if (item is not ISOCItem socItem) return false; - if (items.Contains(item)) + bool contains = items.Contains(item); + bool valid = socItem.Collection != this; + + if (contains && valid) + { return false; + } - items.Add(item); + if (!contains) + { + items.Add(item); + } - socItem.SetCollection(this); + if (!valid) + { + socItem.SetCollection(this); + } ObjectUtility.SetDirty(this); ClearCachedValues(); @@ -316,6 +327,13 @@ public void Swap(int targetIndex, int newIndex) ObjectUtility.SetDirty(this); } + private string Desc(int index) + { + var item = items[index]; + string name = item != null ? item.name : "NULL"; + return $"{index} ({name})"; + } + public void RefreshCollection() { #if UNITY_EDITOR @@ -358,6 +376,7 @@ public void RefreshCollection() // Don't fight with neighbor collections if (socItem.Collection.Contains(socItem) && neighbors.Contains(socItem.Collection)) { + Debug.Log($"Don't fight with neighbor collections {this} {socItem.Collection}"); continue; } itemsFromOtherCollections.Add(socItem); @@ -377,8 +396,8 @@ public void RefreshCollection() { if (items[i] == null) { + Debug.Log($"Removing item at index {Desc(i)} as it is null"); RemoveAt(i); - Debug.Log($"Removing item at index {i} as it is null"); changed = true; continue; } @@ -390,8 +409,8 @@ public void RefreshCollection() { if (socItem.Collection != this) { + Debug.Log($"Removing item at index {Desc(i)} since it belongs to another collection {socItem.Collection}"); RemoveAt(i); - Debug.Log($"Removing item at index {i} since it belongs to another collection {socItem.Collection}"); changed = true; } } @@ -399,8 +418,8 @@ public void RefreshCollection() if (scriptableObject.GetType() == GetItemType() || scriptableObject.GetType().IsSubclassOf(GetItemType())) continue; + Debug.Log($"Removing item at index {Desc(i)} {scriptableObject} since it is not of type {GetItemType()}"); RemoveAt(i); - Debug.Log($"Removing item at index {i} {scriptableObject} since it is not of type {GetItemType()}"); } if (itemsFromOtherCollections.Any()) From 31f569de49f01448bd8e7fd5ebce02d03744b951 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 17:57:06 +0200 Subject: [PATCH 17/24] Fixed compare --- Scripts/Runtime/Core/ScriptableObjectCollection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 78eb8ce..27b7a08 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -100,9 +100,9 @@ public bool Add(ScriptableObject item) return false; bool contains = items.Contains(item); - bool valid = socItem.Collection != this; + bool set = socItem.Collection == this; - if (contains && valid) + if (contains && set) { return false; } @@ -112,7 +112,7 @@ public bool Add(ScriptableObject item) items.Add(item); } - if (!valid) + if (!set) { socItem.SetCollection(this); } From 003b01ece9e317dea1629d40d3127a21420b31d8 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 18:02:34 +0200 Subject: [PATCH 18/24] Removed log --- Scripts/Runtime/Core/ScriptableObjectCollection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 27b7a08..c1b543c 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -376,7 +376,6 @@ public void RefreshCollection() // Don't fight with neighbor collections if (socItem.Collection.Contains(socItem) && neighbors.Contains(socItem.Collection)) { - Debug.Log($"Don't fight with neighbor collections {this} {socItem.Collection}"); continue; } itemsFromOtherCollections.Add(socItem); From 4c83758dc3627ea9a989134a7f4f7ee80fc39d2d Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Thu, 28 Aug 2025 18:24:56 +0200 Subject: [PATCH 19/24] FUCKING FIXED LIST ORDERING WOW --- Scripts/Editor/CustomEditors/CollectionCustomEditor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs index 10304a9..03409d9 100644 --- a/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs +++ b/Scripts/Editor/CustomEditors/CollectionCustomEditor.cs @@ -347,6 +347,7 @@ private void OnKeyUpOnCollectionListView(KeyUpEvent evt) private void OnCollectionItemOrderChanged(int fromIndex, int toIndex) { + ReloadFilteredItems(); ScriptableObject sourceItem = filteredItems[fromIndex]; ScriptableObject targetItem = filteredItems[toIndex]; From b1065402a229bc16ea8351f0a0f687b3152e3c58 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Fri, 29 Aug 2025 15:53:05 +0200 Subject: [PATCH 20/24] Removed autoclean of ExtensionOld --- Scripts/Editor/Core/CodeGenerationUtility.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 53b27c9..2e99d9a 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -19,7 +19,6 @@ public static class CodeGenerationUtility private const string PrivateValuesName = "cachedValues"; private const string PublicValuesName = "Values"; private const string HasCachedValuesName = "hasCachedValues"; - private const string ExtensionOld = ".cs"; private const string ExtensionNew = ".g.cs"; @@ -343,18 +342,6 @@ public static void GenerateIndirectAccessForCollectionItemType(string collection AssetDatabaseUtils.CreatePathIfDoesntExist(targetFolder); string targetFileName = Path.Combine(targetFolder, fileName); - - // Delete any existing files that have the old deprecated extension. - string deprecatedFileName = targetFileName + ExtensionOld; -#if UNITY_2023_1_OR_NEWER - if (AssetDatabase.AssetPathExists(deprecatedFileName)) -#else - if (AssetDatabase.LoadAssetAtPath(deprecatedFileName) != null) -#endif - { - Debug.LogWarning($"Deleting deprecated Indirect Access file '{deprecatedFileName}'."); - AssetDatabase.DeleteAsset(deprecatedFileName); - } targetFileName += ExtensionNew; using (StreamWriter writer = new StreamWriter(targetFileName)) From 40df7c7db728fd59aef94f29d42bc2a95caa3a17 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Fri, 29 Aug 2025 16:00:03 +0200 Subject: [PATCH 21/24] Full removed ExtensionOld --- Scripts/Editor/Core/CodeGenerationUtility.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Scripts/Editor/Core/CodeGenerationUtility.cs b/Scripts/Editor/Core/CodeGenerationUtility.cs index 2e99d9a..7cf24cc 100644 --- a/Scripts/Editor/Core/CodeGenerationUtility.cs +++ b/Scripts/Editor/Core/CodeGenerationUtility.cs @@ -393,14 +393,6 @@ public static void GenerateStaticCollectionScript(ScriptableObjectCollection col string finalFileName = Path.Combine(finalFolder, fileName); - // Delete any existing files that have the old deprecated extension. - string deprecatedFileName = finalFileName + ExtensionOld; - if (File.Exists(deprecatedFileName)) - { - Debug.LogWarning($"Deleting deprecated Static Access file '{deprecatedFileName}'."); - AssetDatabase.DeleteAsset(deprecatedFileName); - } - finalFileName += ExtensionNew; using (StreamWriter writer = new StreamWriter(finalFileName)) { From e1138eb49703955b7b43fd1793765884d6a45cf5 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Fri, 29 Aug 2025 17:49:22 +0200 Subject: [PATCH 22/24] Added checks for accessors in CollectionsRegistry.cs --- Scripts/Runtime/Core/CollectionsRegistry.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Scripts/Runtime/Core/CollectionsRegistry.cs b/Scripts/Runtime/Core/CollectionsRegistry.cs index 5789fdd..292c1b1 100644 --- a/Scripts/Runtime/Core/CollectionsRegistry.cs +++ b/Scripts/Runtime/Core/CollectionsRegistry.cs @@ -510,9 +510,12 @@ public void SetAutoSearchForCollections(bool isOn) public void UpdateAutoSearchForCollections() { - for (int i = 0; i < Collections.Count; i++) + foreach (ScriptableObjectCollection collection in collections) { - ScriptableObjectCollection collection = Collections[i]; + if (!collection) + { + continue; + } if (collection != null && !collection.AutomaticallyLoaded) { SetAutoSearchForCollections(true); @@ -525,9 +528,12 @@ public void UpdateAutoSearchForCollections() public bool HasUniqueGUID(ISOCItem targetItem) { - for (int i = 0; i < collections.Count; i++) + foreach (ScriptableObjectCollection collection in collections) { - ScriptableObjectCollection collection = collections[i]; + if (!collection) + { + continue; + } foreach (ScriptableObject scriptableObject in collection) { if (scriptableObject is ISOCItem socItem) @@ -543,9 +549,12 @@ public bool HasUniqueGUID(ISOCItem targetItem) public bool HasUniqueGUID(ScriptableObjectCollection targetCollection) { - for (int i = 0; i < collections.Count; i++) + foreach (ScriptableObjectCollection collection in collections) { - ScriptableObjectCollection collection = collections[i]; + if (!collection) + { + continue; + } if (collection != targetCollection && collection.GUID == targetCollection.GUID) return false; } From b43af7a783aeca28a43677af6ccae40a16c66a6b Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Mon, 1 Sep 2025 14:14:19 +0200 Subject: [PATCH 23/24] Update Scripts/Runtime/Core/ScriptableObjectCollection.cs Co-authored-by: Roy Theunissen --- Scripts/Runtime/Core/ScriptableObjectCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index c1b543c..13f689e 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -351,7 +351,7 @@ public void RefreshCollection() HashSet neighbors = AssetDatabase - .FindAssets($"t:ScriptableObjectCollection", new[] { folder }) + .FindAssets($"t:{nameof(ScriptableObjectCollection)}", new[] { folder }) .Select(AssetDatabase.GUIDToAssetPath) .Select(AssetDatabase.LoadAssetAtPath) .Where(o => o != null && o != this) From 07f002cb7abe1cf2de81b0ab8399904c6c9dad68 Mon Sep 17 00:00:00 2001 From: Lou Garczynski Date: Mon, 1 Sep 2025 14:14:42 +0200 Subject: [PATCH 24/24] Update Scripts/Runtime/Core/ScriptableObjectCollection.cs Co-authored-by: Roy Theunissen --- Scripts/Runtime/Core/ScriptableObjectCollection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index 13f689e..f593b5b 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -429,7 +429,8 @@ public void RefreshCollection() if (result == 0) { - try { + try + { AssetDatabase.StartAssetEditing(); foreach (ISOCItem itemsFromOtherCollection in itemsFromOtherCollections) {