From 037a9f453b7abcee1158de4d3ae34fd79dc6022b Mon Sep 17 00:00:00 2001 From: Bruno Mikoski Date: Thu, 4 Dec 2025 18:07:04 -0300 Subject: [PATCH 1/3] add: static menu for quick access --- .../Utils/SOCollectionsContextMenuItems.cs | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Scripts/Editor/Utils/SOCollectionsContextMenuItems.cs b/Scripts/Editor/Utils/SOCollectionsContextMenuItems.cs index fd347c9..5943d1a 100644 --- a/Scripts/Editor/Utils/SOCollectionsContextMenuItems.cs +++ b/Scripts/Editor/Utils/SOCollectionsContextMenuItems.cs @@ -6,6 +6,60 @@ namespace BrunoMikoski.ScriptableObjectCollections { public static class SOCollectionsProjectContextMenus { + [MenuItem("Tools/ScriptableObjectCollection/Generate All Static Access Files", priority = 2000)] + private static void GenerateAllStaticAccessFiles() + { + CollectionsRegistry.Instance.ReloadCollections(); + + int generatedCount = 0; + IReadOnlyList collections = CollectionsRegistry.Instance.Collections; + for (int i = 0; i < collections.Count; i++) + { + ScriptableObjectCollection collection = collections[i]; + if (!CodeGenerationUtility.DoesStaticFileForCollectionExist(collection)) + continue; + + CodeGenerationUtility.GenerateStaticCollectionScript(collection); + generatedCount++; + } + + AssetDatabase.Refresh(); + Debug.Log($"[SOC] Generated static access files for {generatedCount} collections."); + } + + [MenuItem("Tools/ScriptableObjectCollection/Generate All Static Access Files", validate = true)] + private static bool Validate_GenerateAllStaticAccessFiles() + { + return !EditorApplication.isPlaying; + } + + + [MenuItem("Tools/ScriptableObjectCollection/Generate Indirect Access for All Collection", priority = 2000)] + private static void GenerateIndirectAccessToAllKnowCollection() + { + CollectionsRegistry.Instance.ReloadCollections(); + + int generatedCount = 0; + IReadOnlyList collections = CollectionsRegistry.Instance.Collections; + for (int i = 0; i < collections.Count; i++) + { + ScriptableObjectCollection collection = collections[i]; + + CodeGenerationUtility.GenerateIndirectAccessForCollectionItemType(collection.GetItemType()); + generatedCount++; + } + + AssetDatabase.Refresh(); + Debug.Log($"[SOC] Generated indirect access files for {generatedCount} collections."); + } + + [MenuItem("Tools/ScriptableObjectCollection/Generate Indirect Access for All Collection", validate = true)] + private static bool Validate_GenerateIndirectAccessToAllKnowCollection() + { + return !EditorApplication.isPlaying; + } + + [MenuItem("Assets/Move to Different Collection", true, priority = 10000)] private static bool ValidateMoveToDifferentCollection() { @@ -95,4 +149,4 @@ private static void SelectCollection() Selection.activeObject = socItem.Collection; } } -} \ No newline at end of file +} From 1d7acb2ee1cd66244cd6a436226083c03135a545 Mon Sep 17 00:00:00 2001 From: Bruno Mikoski Date: Thu, 4 Dec 2025 18:07:13 -0300 Subject: [PATCH 2/3] add: valid filename check --- .../Core/ScriptableObjectCollection.cs | 4 +- .../Runtime/Extensions/StringExtensions.cs | 88 ++++++++++++++++--- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollection.cs b/Scripts/Runtime/Core/ScriptableObjectCollection.cs index cc6917f..2514aef 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollection.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollection.cs @@ -133,11 +133,11 @@ public ScriptableObject AddNew(Type itemType, string assetName = "") string parentFolderPath = Path.Combine(assetPath, "Items" ); AssetDatabaseUtils.CreatePathIfDoesntExist(parentFolderPath); - string itemName = assetName; + string itemName = assetName.ToValidFilename(); if (string.IsNullOrEmpty(itemName)) { - itemName = $"{itemType.Name}"; + itemName = $"{itemType.Name}".ToValidFilename(); } string uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(parentFolderPath, itemName + ".asset")); diff --git a/Scripts/Runtime/Extensions/StringExtensions.cs b/Scripts/Runtime/Extensions/StringExtensions.cs index 1ede1f9..e3d63ce 100644 --- a/Scripts/Runtime/Extensions/StringExtensions.cs +++ b/Scripts/Runtime/Extensions/StringExtensions.cs @@ -36,23 +36,33 @@ public static class StringExtensions "group", "into", "join", "let", "nameof", "notnull", "on", "orderby", "partial", "partial", "remove", "select", "set", "unmanaged", "value", "var", "when", "where", "where", "with", "yield","values" }; + + private static readonly char[] EXTRA_INVALID_FILE_CHARS = { '`' }; + + private static readonly string[] WINDOWS_RESERVED_NAMES = + { + "CON","PRN","AUX","NUL","COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9", + "LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9" + }; public static string Sanitize(this string input) { - input = input.StartingNumbersToWords(); - // replace white spaces with undescore, then replace all invalid chars with empty string - IEnumerable pascalCase = - INVALID_CHARS_RGX.Replace(WHITE_SPACE.Replace(input, "_"), string.Empty) - // split by underscores - .Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries) - // set first letter to uppercase - .Select(w => STARTS_WITH_LOWER_CASE_CHAR.Replace(w, m => m.Value.ToUpper())) - // replace second and all following upper case letters to lower if there is no next lower (ABC -> Abc) - .Select(w => FIRST_CHAR_FOLLOWED_BY_UPPER_CASES_ONLY.Replace(w, m => m.Value.ToLower())) - // set upper case the first lower case following a number (Ab9cd -> Ab9Cd) - .Select(w => LOWER_CASE_NEXT_TO_NUMBER.Replace(w, m => m.Value.ToUpper())) - // lower second and next upper case letters except the last if it follows by any lower (ABcDEf -> AbcDef) - .Select(w => UPPER_CASE_INSIDE.Replace(w, m => m.Value.ToLower())); + // Treat any non-alphanumeric sequence as a word separator + input = Regex.Replace(input, "[^a-zA-Z0-9]+", "_"); + + IEnumerable pascalCase = + input + .Trim('_') + // split by underscores + .Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries) + // set first letter to uppercase + .Select(w => STARTS_WITH_LOWER_CASE_CHAR.Replace(w, m => m.Value.ToUpper())) + // replace second and all following upper case letters to lower if there is no next lower (ABC -> Abc) + .Select(w => FIRST_CHAR_FOLLOWED_BY_UPPER_CASES_ONLY.Replace(w, m => m.Value.ToLower())) + // set upper case the first lower case following a number (Ab9cd -> Ab9Cd) + .Select(w => LOWER_CASE_NEXT_TO_NUMBER.Replace(w, m => m.Value.ToUpper())) + // lower second and next upper case letters except the last if it follows by any lower (ABcDEf -> AbcDef) + .Select(w => UPPER_CASE_INSIDE.Replace(w, m => m.Value.ToLower())); return string.Concat(pascalCase); } @@ -333,5 +343,55 @@ public static string GetProjectPath(this string absolutePath) string relativePath = ToPathWithConsistentSeparators(Path.GetRelativePath(projectPath, absolutePath)); return relativePath; } + + public static bool IsValidFilename(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + char[] invalid = Path.GetInvalidFileNameChars(); + if (value.IndexOfAny(invalid) >= 0) + return false; + + if (value.IndexOfAny(EXTRA_INVALID_FILE_CHARS) >= 0) + return false; + + if (value.EndsWith(" ") || value.EndsWith(".")) + return false; + + string stem = Path.GetFileNameWithoutExtension(value); + if (WINDOWS_RESERVED_NAMES.Any(r => r.Equals(stem, StringComparison.OrdinalIgnoreCase))) + return false; + + return true; + } + + public static string ToValidFilename(this string value, char replacement = '_') + { + if (string.IsNullOrEmpty(value)) + return value; + + if (value.IsValidFilename()) + return value; + + string safe = value; + + foreach (char c in Path.GetInvalidFileNameChars()) + safe = safe.Replace(c, replacement); + + foreach (char c in EXTRA_INVALID_FILE_CHARS) + safe = safe.Replace(c, replacement); + + safe = safe.TrimEnd(' ', '.'); + + string stem = Path.GetFileNameWithoutExtension(safe); + if (WINDOWS_RESERVED_NAMES.Any(r => r.Equals(stem, StringComparison.OrdinalIgnoreCase))) + safe = "_" + safe; + + if (string.IsNullOrWhiteSpace(safe)) + safe = "_"; + + return safe; + } } } From b808130d3d60217a6c09e0cb19d025756a9cbde0 Mon Sep 17 00:00:00 2001 From: Bruno Mikoski Date: Thu, 4 Dec 2025 18:07:35 -0300 Subject: [PATCH 3/3] fix: guid reserialization when not needed --- .../Core/ScriptableObjectCollectionItem.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Scripts/Runtime/Core/ScriptableObjectCollectionItem.cs b/Scripts/Runtime/Core/ScriptableObjectCollectionItem.cs index 5950dae..e63df0b 100644 --- a/Scripts/Runtime/Core/ScriptableObjectCollectionItem.cs +++ b/Scripts/Runtime/Core/ScriptableObjectCollectionItem.cs @@ -7,17 +7,7 @@ public class ScriptableObjectCollectionItem : ScriptableObject, IComparable guid; [SerializeField, CollectionReferenceLongGuid] private LongGuid collectionGUID; @@ -68,6 +58,16 @@ public int Index return cachedIndex; } } + +#if UNITY_EDITOR + private void OnValidate() + { + if (!guid.IsValid()) + { + GenerateNewGUID(); + } + } +#endif public void SetCollection(ScriptableObjectCollection collection) { @@ -108,8 +108,8 @@ public override bool Equals(object o) ScriptableObjectCollectionItem other = o as ScriptableObjectCollectionItem; if (other == null) return false; - - return ReferenceEquals(this, other); + + return guid.IsValid() && other.guid.IsValid() && guid == other.guid; } public static bool operator==(ScriptableObjectCollectionItem left, ScriptableObjectCollectionItem right) @@ -133,7 +133,7 @@ public override bool Equals(object o) public override int GetHashCode() { - return GUID.GetHashCode(); + return guid.IsValid() ? guid.GetHashCode() : 0; } } -} \ No newline at end of file +}