Skip to content

Instantly share code, notes, and snippets.

@MattRix
Last active March 7, 2025 12:35
Show Gist options
  • Select an option

  • Save MattRix/be5613e48ce701608b235f594244408c to your computer and use it in GitHub Desktop.

Select an option

Save MattRix/be5613e48ce701608b235f594244408c to your computer and use it in GitHub Desktop.

Revisions

  1. MattRix revised this gist Mar 7, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion AssetLibrary.cs
    Original file line number Diff line number Diff line change
    @@ -59,7 +59,7 @@ private static string GetProperTypeName(System.Type type)
    {
    if (type == null)
    return "UnityEngine.Object";
    if (!string.IsNullOrEmpty(type.Namespace) && type.Namespace.StartsWith("UnityEngine") && type.Namespace != "UnityEngine")
    if (!string.IsNullOrEmpty(type.Namespace) && type.Namespace != "UnityEngine")
    return type.FullName;
    return type.Name;
    }
  2. MattRix revised this gist Feb 23, 2025. 1 changed file with 17 additions and 11 deletions.
    28 changes: 17 additions & 11 deletions AssetLibrary.cs
    Original file line number Diff line number Diff line change
    @@ -24,7 +24,7 @@ private static void GenerateAssetLibrary()
    }

    [ContextMenu("Regenerate")]
    public void Regenerate()
    protected void Regenerate()
    {
    GenerateClass(this);
    }
    @@ -382,23 +382,29 @@ private static string SanitizeFieldName(string name)
    return sb.ToString();
    }
    #endif
    }

    #if UNITY_EDITOR
    [CustomEditor(typeof(AssetLibrary), true)]
    [CanEditMultipleObjects]
    public class AssetLibraryEditor : Editor
    {
    public override void OnInspectorGUI()
    #if UNITY_EDITOR
    //note: this is a subclass of AssetLibrary so we can mark the Regenerate() method as protected
    [CustomEditor(typeof(AssetLibrary), true)]
    [CanEditMultipleObjects]
    public class AssetLibraryEditor : Editor
    {
    DrawDefaultInspector();
    if (GUILayout.Button("Regenerate"))
    public override void OnInspectorGUI()
    {
    (target as AssetLibrary).Regenerate();
    DrawDefaultInspector();
    if (GUILayout.Button("Regenerate"))
    {
    (target as AssetLibrary).Regenerate();
    }
    }
    }
    #endif
    }



    #if UNITY_EDITOR

    // This static class ensures that after a domain reload the pending hookup gets processed.
    [InitializeOnLoad]
    public static class AssetLibraryDelayedHookup
  3. MattRix revised this gist Feb 18, 2025. 1 changed file with 18 additions and 2 deletions.
    20 changes: 18 additions & 2 deletions AssetLibrary.cs
    Original file line number Diff line number Diff line change
    @@ -250,6 +250,7 @@ private static void GenerateClass(AssetLibrary assetLib)
    SerializedProperty scriptProp = so.FindProperty("m_Script");
    scriptProp.objectReferenceValue = newScript;
    so.ApplyModifiedProperties();
    // Mark the asset dirty so Unity reimports it.
    if(assetLib != null) EditorUtility.SetDirty(assetLib);
    AssetDatabase.SaveAssets();
    }
    @@ -259,12 +260,15 @@ private static void GenerateClass(AssetLibrary assetLib)
    return;
    }

    // Instead of scheduling a delayCall that might get lost on reload,
    // store the asset path persistently so we can process it on load.
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetPath);
    }

    // Made internal so our post-reload processor can call it.
    internal static void HookupAssetReferences(string assetLibAssetPath)
    {
    // If still compiling, re-save the pending key and exit.
    if (EditorApplication.isCompiling)
    {
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetLibAssetPath);
    @@ -279,6 +283,7 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    return;
    }

    // Re-scan the asset folder.
    ScanAssetLibraryFolder(assetLib, out List<AssetEntry> directEntries, out List<SubfolderEntry> subfolderEntries);

    SerializedObject so = new SerializedObject(assetLib);
    @@ -290,6 +295,7 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    if (prop != null)
    {
    Object assetObj = AssetDatabase.LoadAssetAtPath<Object>(entry.assetPath);
    // For GameObjects, if the field expects a component type.
    if (assetObj is GameObject go && entry.typeName != "GameObject")
    {
    System.Type compType = GetTypeByName(entry.typeName);
    @@ -332,6 +338,7 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    so.ApplyModifiedProperties();
    AssetDatabase.SaveAssets();

    // Re-select the asset to force an inspector update.
    Selection.activeObject = assetLib;
    EditorGUIUtility.PingObject(assetLib);
    Debug.Log("Asset Library references hooked up for: " + assetLibAssetPath);
    @@ -340,15 +347,22 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    // Helper to find a type by its simple name.
    private static System.Type GetTypeByName(string typeName)
    {
    List<System.Type> candidates = new List<System.Type>();
    foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
    {
    foreach (var type in assembly.GetTypes())
    {
    if (type.Name == typeName)
    return type;
    candidates.Add(type);
    }
    }
    return null;
    // Prefer a candidate with no namespace (top-level type).
    foreach (var candidate in candidates)
    {
    if (string.IsNullOrEmpty(candidate.Namespace))
    return candidate;
    }
    return candidates.Count > 0 ? candidates[0] : null;
    }

    private static string SanitizeFieldName(string name)
    @@ -385,6 +399,7 @@ public override void OnInspectorGUI()
    }
    }

    // This static class ensures that after a domain reload the pending hookup gets processed.
    [InitializeOnLoad]
    public static class AssetLibraryDelayedHookup
    {
    @@ -398,6 +413,7 @@ private static void ProcessPendingHookup()
    string pendingPath = EditorPrefs.GetString("PendingAssetLibraryHookup", "");
    if (!string.IsNullOrEmpty(pendingPath))
    {
    // Clear the pending key before processing.
    EditorPrefs.DeleteKey("PendingAssetLibraryHookup");
    AssetLibrary.HookupAssetReferences(pendingPath);
    }
  4. MattRix revised this gist Feb 18, 2025. 1 changed file with 17 additions and 13 deletions.
    30 changes: 17 additions & 13 deletions AssetLibrary.cs
    Original file line number Diff line number Diff line change
    @@ -52,6 +52,18 @@ private class SubfolderEntry
    public string arrayElementType;
    }

    // Returns a proper type name:
    // If the type is in UnityEngine (i.e. "UnityEngine.Button"), then use the fully qualified name.
    // Otherwise, keep the short name since "using UnityEngine;" is present.
    private static string GetProperTypeName(System.Type type)
    {
    if (type == null)
    return "UnityEngine.Object";
    if (!string.IsNullOrEmpty(type.Namespace) && type.Namespace.StartsWith("UnityEngine") && type.Namespace != "UnityEngine")
    return type.FullName;
    return type.Name;
    }

    // Scans the asset library folder to build our entries.
    private static void ScanAssetLibraryFolder(AssetLibrary assetLib, out List<AssetEntry> directEntries, out List<SubfolderEntry> subfolderEntries)
    {
    @@ -82,7 +94,8 @@ private static void ScanAssetLibraryFolder(AssetLibrary assetLib, out List<Asset
    {
    assetPath = relativePath,
    assetName = Path.GetFileNameWithoutExtension(relativePath),
    typeName = obj.GetType().Name,
    // Use GetProperTypeName to ensure proper type name (e.g., UnityEngine.UI.Button)
    typeName = GetProperTypeName(obj.GetType()),
    fieldName = SanitizeFieldName(Path.GetFileNameWithoutExtension(relativePath))
    };

    @@ -95,7 +108,7 @@ private static void ScanAssetLibraryFolder(AssetLibrary assetLib, out List<Asset
    continue;
    if (comp is Transform)
    continue;
    entry.typeName = comp.GetType().Name;
    entry.typeName = GetProperTypeName(comp.GetType());
    break;
    }
    }
    @@ -145,7 +158,7 @@ private static void ScanAssetLibraryFolder(AssetLibrary assetLib, out List<Asset
    {
    assetPath = relativePath,
    assetName = Path.GetFileNameWithoutExtension(relativePath),
    typeName = obj.GetType().Name,
    typeName = GetProperTypeName(obj.GetType()),
    fieldName = SanitizeFieldName(Path.GetFileNameWithoutExtension(relativePath))
    };
    break; // Use the first found library.
    @@ -156,7 +169,7 @@ private static void ScanAssetLibraryFolder(AssetLibrary assetLib, out List<Asset
    {
    assetPath = relativePath,
    assetName = Path.GetFileNameWithoutExtension(relativePath),
    typeName = obj.GetType().Name,
    typeName = GetProperTypeName(obj.GetType()),
    fieldName = SanitizeFieldName(Path.GetFileNameWithoutExtension(relativePath))
    };
    childAssets.Add(childEntry);
    @@ -237,7 +250,6 @@ private static void GenerateClass(AssetLibrary assetLib)
    SerializedProperty scriptProp = so.FindProperty("m_Script");
    scriptProp.objectReferenceValue = newScript;
    so.ApplyModifiedProperties();
    // Mark the asset dirty so Unity reimports it.
    if(assetLib != null) EditorUtility.SetDirty(assetLib);
    AssetDatabase.SaveAssets();
    }
    @@ -247,15 +259,12 @@ private static void GenerateClass(AssetLibrary assetLib)
    return;
    }

    // Instead of scheduling a delayCall that might get lost on reload,
    // store the asset path persistently so we can process it on load.
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetPath);
    }

    // Made internal so our post-reload processor can call it.
    internal static void HookupAssetReferences(string assetLibAssetPath)
    {
    // If still compiling, re-save the pending key and exit.
    if (EditorApplication.isCompiling)
    {
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetLibAssetPath);
    @@ -270,7 +279,6 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    return;
    }

    // Re-scan the asset folder.
    ScanAssetLibraryFolder(assetLib, out List<AssetEntry> directEntries, out List<SubfolderEntry> subfolderEntries);

    SerializedObject so = new SerializedObject(assetLib);
    @@ -282,7 +290,6 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    if (prop != null)
    {
    Object assetObj = AssetDatabase.LoadAssetAtPath<Object>(entry.assetPath);
    // For GameObjects, if the field expects a component type.
    if (assetObj is GameObject go && entry.typeName != "GameObject")
    {
    System.Type compType = GetTypeByName(entry.typeName);
    @@ -325,7 +332,6 @@ internal static void HookupAssetReferences(string assetLibAssetPath)
    so.ApplyModifiedProperties();
    AssetDatabase.SaveAssets();

    // Re-select the asset to force an inspector update.
    Selection.activeObject = assetLib;
    EditorGUIUtility.PingObject(assetLib);
    Debug.Log("Asset Library references hooked up for: " + assetLibAssetPath);
    @@ -379,7 +385,6 @@ public override void OnInspectorGUI()
    }
    }

    // This static class ensures that after a domain reload the pending hookup gets processed.
    [InitializeOnLoad]
    public static class AssetLibraryDelayedHookup
    {
    @@ -393,7 +398,6 @@ private static void ProcessPendingHookup()
    string pendingPath = EditorPrefs.GetString("PendingAssetLibraryHookup", "");
    if (!string.IsNullOrEmpty(pendingPath))
    {
    // Clear the pending key before processing.
    EditorPrefs.DeleteKey("PendingAssetLibraryHookup");
    AssetLibrary.HookupAssetReferences(pendingPath);
    }
  5. MattRix revised this gist Feb 14, 2025. No changes.
  6. MattRix revised this gist Feb 14, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    Place an AssetLibrary scriptableobject in a folder, and press the "Regenerate" button. It'll populate itself with named and typed references to assets in the same folder. Assets in subfolders will be brought in as arrays, unless you place another AssetLibrary in a subfolder, then you'll get a sort of nested AssetLibrary structure.
    Place an AssetLibrary scriptableobject in a folder, and press the "Regenerate" button. It'll populate itself with named and typed references to assets in the same folder. Assets in subfolders will be brought in as arrays, unless you place another AssetLibrary in a subfolder, in which case you'll get a sort of nested AssetLibrary structure.
  7. MattRix revised this gist Feb 14, 2025. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    Place an AssetLibrary scriptableobject in a folder, and press the "Regenerate" button. It'll populate itself with named and typed references to assets in the same folder. Assets in subfolders will be brought in as arrays, unless you place another AssetLibrary in a subfolder, then you'll get a sort of nested AssetLibrary structure.
  8. MattRix created this gist Feb 14, 2025.
    402 changes: 402 additions & 0 deletions AssetLibrary.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,402 @@
    using System.IO;
    using System.Text;
    using System.Collections.Generic;
    using UnityEngine;
    #if UNITY_EDITOR
    using UnityEditor;
    #endif

    [CreateAssetMenu(menuName = "AssetLibrary")]
    public class AssetLibrary : ScriptableObject
    {
    #if UNITY_EDITOR

    [MenuItem("Assets/Generate Asset Library", true)]
    private static bool ValidateGenerateAssetLibrary() => Selection.activeObject is AssetLibrary;

    [MenuItem("Assets/Generate Asset Library", false, 1)]
    private static void GenerateAssetLibrary()
    {
    var assetLib = Selection.activeObject as AssetLibrary;
    if (assetLib == null)
    return;
    GenerateClass(assetLib);
    }

    [ContextMenu("Regenerate")]
    public void Regenerate()
    {
    GenerateClass(this);
    }

    // Helper classes.
    private class AssetEntry
    {
    public string assetPath;
    public string assetName;
    public string fieldName;
    public string typeName;
    }

    private class SubfolderEntry
    {
    public string folderPath; // e.g., "Assets/MyFolder/Subfolder"
    public string folderName; // e.g., "Subfolder"
    public string fieldName; // sanitized field name
    public bool isLibraryReference; // true if subfolder contains an AssetLibrary subclass
    // For library reference:
    public string libraryAssetPath;
    public string libraryTypeName;
    // For non-library array:
    public List<AssetEntry> childAssets;
    public string arrayElementType;
    }

    // Scans the asset library folder to build our entries.
    private static void ScanAssetLibraryFolder(AssetLibrary assetLib, out List<AssetEntry> directEntries, out List<SubfolderEntry> subfolderEntries)
    {
    directEntries = new List<AssetEntry>();
    subfolderEntries = new List<SubfolderEntry>();

    string assetPath = AssetDatabase.GetAssetPath(assetLib);
    string folderPath = Path.GetDirectoryName(assetPath);
    string projectFolder = Directory.GetParent(Application.dataPath).FullName;
    string folderSystemPath = Path.Combine(projectFolder, folderPath);

    // Process direct assets (non-recursive)
    string[] files = Directory.GetFiles(folderSystemPath);
    foreach (var file in files)
    {
    if (file.EndsWith(".meta"))
    continue;
    string relativePath = file.Replace(projectFolder + Path.DirectorySeparatorChar, "").Replace("\\", "/");
    if (relativePath.EndsWith(".cs") || relativePath.EndsWith(".js"))
    continue;
    if (relativePath == assetPath)
    continue;
    Object obj = AssetDatabase.LoadMainAssetAtPath(relativePath);
    if (obj == null)
    continue;

    var entry = new AssetEntry
    {
    assetPath = relativePath,
    assetName = Path.GetFileNameWithoutExtension(relativePath),
    typeName = obj.GetType().Name,
    fieldName = SanitizeFieldName(Path.GetFileNameWithoutExtension(relativePath))
    };

    // For GameObjects, try to use its first non-Transform component.
    if (obj is GameObject go)
    {
    foreach (Component comp in go.GetComponents<Component>())
    {
    if (comp == null)
    continue;
    if (comp is Transform)
    continue;
    entry.typeName = comp.GetType().Name;
    break;
    }
    }

    // Ensure field name uniqueness.
    int duplicateCount = 1;
    string originalFieldName = entry.fieldName;
    while (directEntries.Exists(e => e.fieldName == entry.fieldName))
    {
    entry.fieldName = originalFieldName + "_" + duplicateCount;
    duplicateCount++;
    }
    directEntries.Add(entry);
    }

    // Process immediate subfolders.
    string[] subfolderPaths = AssetDatabase.GetSubFolders(folderPath);
    foreach (var subfolder in subfolderPaths)
    {
    var subEntry = new SubfolderEntry();
    subEntry.folderPath = subfolder;
    subEntry.folderName = Path.GetFileName(subfolder);
    subEntry.fieldName = SanitizeFieldName(subEntry.folderName);

    string subfolderSystemPath = Path.Combine(projectFolder, subfolder);
    string[] subFiles = Directory.GetFiles(subfolderSystemPath);
    List<AssetEntry> childAssets = new List<AssetEntry>();

    bool foundLibrary = false;
    AssetEntry libraryEntry = null;
    foreach (var file in subFiles)
    {
    if (file.EndsWith(".meta"))
    continue;
    string relativePath = file.Replace(projectFolder + Path.DirectorySeparatorChar, "").Replace("\\", "/");
    if (relativePath.EndsWith(".cs") || relativePath.EndsWith(".js"))
    continue;
    Object obj = AssetDatabase.LoadMainAssetAtPath(relativePath);
    if (obj == null)
    continue;

    // Check for an AssetLibrary subclass in this folder.
    if (obj is AssetLibrary && obj.GetType() != typeof(AssetLibrary))
    {
    foundLibrary = true;
    libraryEntry = new AssetEntry
    {
    assetPath = relativePath,
    assetName = Path.GetFileNameWithoutExtension(relativePath),
    typeName = obj.GetType().Name,
    fieldName = SanitizeFieldName(Path.GetFileNameWithoutExtension(relativePath))
    };
    break; // Use the first found library.
    }
    else
    {
    var childEntry = new AssetEntry
    {
    assetPath = relativePath,
    assetName = Path.GetFileNameWithoutExtension(relativePath),
    typeName = obj.GetType().Name,
    fieldName = SanitizeFieldName(Path.GetFileNameWithoutExtension(relativePath))
    };
    childAssets.Add(childEntry);
    }
    }
    if (foundLibrary && libraryEntry != null)
    {
    subEntry.isLibraryReference = true;
    subEntry.libraryAssetPath = libraryEntry.assetPath;
    subEntry.libraryTypeName = libraryEntry.typeName;
    }
    else
    {
    subEntry.isLibraryReference = false;
    subEntry.childAssets = childAssets;
    string commonType = null;
    foreach (var child in childAssets)
    {
    if (commonType == null)
    commonType = child.typeName;
    else if (commonType != child.typeName)
    {
    commonType = "UnityEngine.Object";
    break;
    }
    }
    subEntry.arrayElementType = commonType ?? "UnityEngine.Object";
    }
    subfolderEntries.Add(subEntry);
    }
    }

    private static void GenerateClass(AssetLibrary assetLib)
    {
    // Determine the folder containing the assetLib.
    string assetPath = AssetDatabase.GetAssetPath(assetLib);
    string folderPath = Path.GetDirectoryName(assetPath);
    string assetLibName = assetLib.name;
    string classFileName = assetLibName + ".cs";
    string classFilePath = Path.Combine(folderPath, classFileName);

    // Scan the asset folder.
    ScanAssetLibraryFolder(assetLib, out List<AssetEntry> directEntries, out List<SubfolderEntry> subfolderEntries);

    // Generate the new subclass code.
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("using UnityEngine;");
    sb.AppendLine();
    sb.AppendLine("public class " + assetLibName + " : AssetLibrary");
    sb.AppendLine("{");

    // Direct asset fields.
    foreach (var entry in directEntries)
    {
    sb.AppendLine(" public " + entry.typeName + " " + entry.fieldName + ";");
    }

    // Subfolder fields.
    foreach (var subEntry in subfolderEntries)
    {
    if (subEntry.isLibraryReference)
    sb.AppendLine(" public " + subEntry.libraryTypeName + " " + subEntry.fieldName + ";");
    else
    sb.AppendLine(" public " + subEntry.arrayElementType + "[] " + subEntry.fieldName + ";");
    }
    sb.AppendLine("}");

    string classCode = sb.ToString();
    File.WriteAllText(classFilePath, classCode);
    AssetDatabase.ImportAsset(classFilePath);
    AssetDatabase.Refresh();

    // Update the ScriptableObject to use the newly generated script.
    MonoScript newScript = AssetDatabase.LoadAssetAtPath<MonoScript>(classFilePath);
    if (newScript != null)
    {
    SerializedObject so = new SerializedObject(assetLib);
    SerializedProperty scriptProp = so.FindProperty("m_Script");
    scriptProp.objectReferenceValue = newScript;
    so.ApplyModifiedProperties();
    // Mark the asset dirty so Unity reimports it.
    if(assetLib != null) EditorUtility.SetDirty(assetLib);
    AssetDatabase.SaveAssets();
    }
    else
    {
    Debug.LogError("Failed to load generated script: " + classFilePath);
    return;
    }

    // Instead of scheduling a delayCall that might get lost on reload,
    // store the asset path persistently so we can process it on load.
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetPath);
    }

    // Made internal so our post-reload processor can call it.
    internal static void HookupAssetReferences(string assetLibAssetPath)
    {
    // If still compiling, re-save the pending key and exit.
    if (EditorApplication.isCompiling)
    {
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetLibAssetPath);
    return;
    }

    AssetLibrary assetLib = AssetDatabase.LoadAssetAtPath<AssetLibrary>(assetLibAssetPath);
    if (assetLib == null)
    {
    Debug.Log("AssetLibrary not loaded yet; retrying hookup...");
    EditorPrefs.SetString("PendingAssetLibraryHookup", assetLibAssetPath);
    return;
    }

    // Re-scan the asset folder.
    ScanAssetLibraryFolder(assetLib, out List<AssetEntry> directEntries, out List<SubfolderEntry> subfolderEntries);

    SerializedObject so = new SerializedObject(assetLib);

    // Hook up direct asset fields.
    foreach (var entry in directEntries)
    {
    SerializedProperty prop = so.FindProperty(entry.fieldName);
    if (prop != null)
    {
    Object assetObj = AssetDatabase.LoadAssetAtPath<Object>(entry.assetPath);
    // For GameObjects, if the field expects a component type.
    if (assetObj is GameObject go && entry.typeName != "GameObject")
    {
    System.Type compType = GetTypeByName(entry.typeName);
    if (compType != null)
    {
    Component comp = go.GetComponent(compType);
    if (comp != null)
    assetObj = comp;
    else
    Debug.LogWarning($"GameObject '{go.name}' does not have a component of type '{entry.typeName}'");
    }
    }
    prop.objectReferenceValue = assetObj;
    }
    }

    // Hook up subfolder fields.
    foreach (var subEntry in subfolderEntries)
    {
    SerializedProperty prop = so.FindProperty(subEntry.fieldName);
    if (prop != null)
    {
    if (subEntry.isLibraryReference)
    {
    Object assetObj = AssetDatabase.LoadAssetAtPath<Object>(subEntry.libraryAssetPath);
    prop.objectReferenceValue = assetObj;
    }
    else if (subEntry.childAssets != null)
    {
    prop.arraySize = subEntry.childAssets.Count;
    for (int i = 0; i < subEntry.childAssets.Count; i++)
    {
    SerializedProperty elementProp = prop.GetArrayElementAtIndex(i);
    Object assetObj = AssetDatabase.LoadAssetAtPath<Object>(subEntry.childAssets[i].assetPath);
    elementProp.objectReferenceValue = assetObj;
    }
    }
    }
    }
    so.ApplyModifiedProperties();
    AssetDatabase.SaveAssets();

    // Re-select the asset to force an inspector update.
    Selection.activeObject = assetLib;
    EditorGUIUtility.PingObject(assetLib);
    Debug.Log("Asset Library references hooked up for: " + assetLibAssetPath);
    }

    // Helper to find a type by its simple name.
    private static System.Type GetTypeByName(string typeName)
    {
    foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
    {
    foreach (var type in assembly.GetTypes())
    {
    if (type.Name == typeName)
    return type;
    }
    }
    return null;
    }

    private static string SanitizeFieldName(string name)
    {
    if (string.IsNullOrEmpty(name))
    return "field";
    StringBuilder sb = new StringBuilder();
    if (!char.IsLetter(name[0]) && name[0] != '_')
    sb.Append('_');
    foreach (char c in name)
    {
    if (char.IsLetterOrDigit(c) || c == '_')
    sb.Append(c);
    else
    sb.Append('_');
    }
    return sb.ToString();
    }
    #endif
    }

    #if UNITY_EDITOR
    [CustomEditor(typeof(AssetLibrary), true)]
    [CanEditMultipleObjects]
    public class AssetLibraryEditor : Editor
    {
    public override void OnInspectorGUI()
    {
    DrawDefaultInspector();
    if (GUILayout.Button("Regenerate"))
    {
    (target as AssetLibrary).Regenerate();
    }
    }
    }

    // This static class ensures that after a domain reload the pending hookup gets processed.
    [InitializeOnLoad]
    public static class AssetLibraryDelayedHookup
    {
    static AssetLibraryDelayedHookup()
    {
    EditorApplication.delayCall += ProcessPendingHookup;
    }

    private static void ProcessPendingHookup()
    {
    string pendingPath = EditorPrefs.GetString("PendingAssetLibraryHookup", "");
    if (!string.IsNullOrEmpty(pendingPath))
    {
    // Clear the pending key before processing.
    EditorPrefs.DeleteKey("PendingAssetLibraryHookup");
    AssetLibrary.HookupAssetReferences(pendingPath);
    }
    }
    }
    #endif