#if UNITY_EDITOR using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEngine; namespace BrewMonster { /// /// Assign a parent folder, then register all assets under it into an Addressables group named after that folder. /// Addresses are relative to the parent folder, without the file extension (e.g. .png, .wav). Text files are skipped. /// public class FolderToAddressablesWindow : EditorWindow { private const string MenuPath = "Tools/Addressable/Folder To Addressables…"; private static readonly HashSet TextExtensions = new HashSet(System.StringComparer.OrdinalIgnoreCase) { ".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".ini", ".cfg", ".html", ".htm", }; private DefaultAsset _parentFolder; private Vector2 _scroll; private string _log = ""; [MenuItem(MenuPath)] public static void Open() { var w = GetWindow(false, "Folder To Addressables", true); w.minSize = new Vector2(420, 280); w.Show(); } private void OnGUI() { EditorGUILayout.Space(4); EditorGUILayout.LabelField("Folder To Addressables", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "Group name = last segment of the folder path. Two different paths with the same folder name share one group. " + "Address keys omit file extensions (e.g. Sub/Sprite not Sub/Sprite.png). " + "If nothing updates, ensure the target group is not Read Only in the Addressables Groups window.", MessageType.Info); EditorGUILayout.Space(4); var newFolder = (DefaultAsset)EditorGUILayout.ObjectField( "Parent folder", _parentFolder, typeof(DefaultAsset), false); if (newFolder != _parentFolder) { _parentFolder = newFolder; _log = ""; } string parentPath = _parentFolder != null ? AssetDatabase.GetAssetPath(_parentFolder) : null; bool validFolder = !string.IsNullOrEmpty(parentPath) && AssetDatabase.IsValidFolder(parentPath); if (_parentFolder != null && !validFolder) { EditorGUILayout.HelpBox("Assign a folder from the Project window (not a file).", MessageType.Warning); } string groupName = validFolder ? Path.GetFileName(parentPath.TrimEnd('/')) : ""; EditorGUILayout.LabelField("Addressables group", string.IsNullOrEmpty(groupName) ? "—" : groupName); EditorGUILayout.Space(8); EditorGUI.BeginDisabledGroup(!validFolder); if (GUILayout.Button("Start", GUILayout.Height(28))) { Run(parentPath, groupName); } EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(6); EditorGUILayout.LabelField("Log", EditorStyles.boldLabel); _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.ExpandHeight(true)); EditorGUILayout.TextArea(string.IsNullOrEmpty(_log) ? "(empty)" : _log, EditorStyles.textArea, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); } private void Run(string parentPath, string groupName) { _log = ""; var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { AppendLog("ERROR: Addressable settings not found. Install/configure the Addressables package."); EditorUtility.DisplayDialog("Folder To Addressables", "Addressable settings not found.", "OK"); return; } if (string.IsNullOrEmpty(groupName)) { AppendLog("ERROR: Could not derive group name from folder path."); return; } var group = GetOrCreateGroup(settings, groupName); if (group == null) { AppendLog($"ERROR: Could not find or create group: {groupName}"); EditorUtility.DisplayDialog("Folder To Addressables", $"Could not find or create group: {groupName}", "OK"); return; } if (group.ReadOnly) { AppendLog( "ERROR: The target Addressables group is Read Only. Unity will not apply CreateOrMoveEntry/SetAddress. " + "Open Window > Asset Management > Addressables > Groups, select the group, and disable Read Only in the Inspector."); EditorUtility.DisplayDialog( "Folder To Addressables", $"Group \"{groupName}\" is Read Only. Disable Read Only on that group, then run this tool again.", "OK"); return; } parentPath = parentPath.Replace('\\', '/').TrimEnd('/'); string parentPrefix = parentPath + "/"; // Merge GUIDs from the project tree with entries already in this group under the folder. // Ensures we refresh addresses for assets that are already addressable (not ignored when the group already existed). var guidsToProcess = new HashSet(System.StringComparer.OrdinalIgnoreCase); foreach (var g in AssetDatabase.FindAssets("", new[] { parentPath })) guidsToProcess.Add(g); if (group.entries != null) { foreach (var existing in group.entries) { if (existing == null) continue; string ap = existing.AssetPath; if (string.IsNullOrEmpty(ap)) continue; ap = ap.Replace('\\', '/'); if (AssetDatabase.IsValidFolder(ap)) continue; if (!ap.StartsWith(parentPrefix, System.StringComparison.OrdinalIgnoreCase)) continue; guidsToProcess.Add(existing.guid); } } int processed = 0; int skippedText = 0; int skippedOther = 0; int errors = 0; foreach (string guid in guidsToProcess) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(assetPath)) { skippedOther++; continue; } assetPath = assetPath.Replace('\\', '/'); if (AssetDatabase.IsValidFolder(assetPath)) { skippedOther++; continue; } // Require path under parent folder (avoid matching sibling folders e.g. .../A vs .../AB). if (!assetPath.StartsWith(parentPrefix, System.StringComparison.OrdinalIgnoreCase)) { skippedOther++; continue; } if (ShouldSkipAsText(assetPath)) { skippedText++; continue; } string relative = GetRelativeAddress(parentPath, assetPath); if (string.IsNullOrEmpty(relative)) { skippedOther++; continue; } try { var existingEntry = settings.FindAssetEntry(guid); if (existingEntry != null && existingEntry.parentGroup != null && existingEntry.parentGroup.ReadOnly) { AppendLog($"ERROR: Cannot move or edit (source group is Read Only): {assetPath}"); errors++; continue; } var entry = settings.CreateOrMoveEntry(guid, group, false, false); if (entry == null) { AppendLog($"ERROR: CreateOrMoveEntry failed: {assetPath}"); errors++; continue; } string newAddress = ToAddressWithoutExtension(relative); entry.SetAddress(newAddress, false); processed++; } catch (System.Exception ex) { AppendLog($"ERROR: {assetPath} — {ex.Message}"); errors++; } } if (processed > 0) { settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, null, true); EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); } AppendLog(""); AppendLog($"Done. Processed: {processed}, skipped (text): {skippedText}, skipped (other): {skippedOther}, errors: {errors}"); EditorUtility.DisplayDialog( "Folder To Addressables", $"Processed: {processed}\nSkipped (text): {skippedText}\nSkipped (other): {skippedOther}\nErrors: {errors}\nGroup: {groupName}", "OK"); } private static bool ShouldSkipAsText(string assetPath) { var mainType = AssetDatabase.GetMainAssetTypeAtPath(assetPath); if (mainType == typeof(TextAsset)) return true; string ext = Path.GetExtension(assetPath); return !string.IsNullOrEmpty(ext) && TextExtensions.Contains(ext); } private static string GetRelativeAddress(string parentPath, string assetPath) { assetPath = assetPath.Replace('\\', '/'); parentPath = parentPath.Replace('\\', '/').TrimEnd('/'); if (string.Equals(assetPath, parentPath, System.StringComparison.OrdinalIgnoreCase)) return null; string prefix = parentPath + "/"; if (!assetPath.StartsWith(prefix, System.StringComparison.OrdinalIgnoreCase)) return null; return assetPath.Substring(prefix.Length); } /// /// Address key: relative path with the final file extension removed (.png, .wav, .prefab, etc.). /// private static string ToAddressWithoutExtension(string relativePath) { if (string.IsNullOrEmpty(relativePath)) return relativePath; relativePath = relativePath.Replace('\\', '/'); int lastSlash = relativePath.LastIndexOf('/'); string filePart = lastSlash >= 0 ? relativePath.Substring(lastSlash + 1) : relativePath; string dirPart = lastSlash >= 0 ? relativePath.Substring(0, lastSlash) : ""; string fileNoExt = Path.GetFileNameWithoutExtension(filePart); if (string.IsNullOrEmpty(fileNoExt)) fileNoExt = filePart; if (string.IsNullOrEmpty(dirPart)) return fileNoExt; return dirPart + "/" + fileNoExt; } private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName) { if (settings == null || string.IsNullOrEmpty(groupName)) return null; AddressableAssetGroup group = settings.FindGroup(groupName); if (group != null) return group; try { group = settings.CreateGroup(groupName, false, false, true, null); if (group != null) { EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); } return group; } catch { return null; } } private void AppendLog(string line) { if (string.IsNullOrEmpty(_log)) _log = line; else _log += "\n" + line; Debug.Log($"[Folder To Addressables] {line}"); Repaint(); } } } #endif