#if UNITY_EDITOR using System.Collections.Generic; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEngine; namespace BrewMonster { /// /// Unity Editor Window for processing assets in the current folder and making them addressable in the "gfx" group. /// public class AddressablesGfxProcessorWindow : EditorWindow { private const string GFX_GROUP_NAME = "gfx"; private const string MENU_PATH = "Tools/Addressable/GFX Adressable For Current Folder"; private Vector2 scrollPosition; private List foundAssetPaths = new List(); private List selectedFolderPaths = new List(); private string logOutput = ""; private bool hasScanned = false; [MenuItem(MENU_PATH)] public static void ShowWindow() { var window = GetWindow(false, "GFX Addressable Processor"); window.minSize = new Vector2(500, 400); window.Show(); } private void OnEnable() { UpdateCurrentFolder(); } private void OnSelectionChange() { UpdateCurrentFolder(); Repaint(); } private void UpdateCurrentFolder() { // Get all selected folders in Project window List newFolderPaths = new List(); // Check all selected objects (supports multiple selection) Object[] selectedObjects = Selection.objects; if (selectedObjects != null && selectedObjects.Length > 0) { foreach (Object obj in selectedObjects) { if (obj == null) continue; string assetPath = AssetDatabase.GetAssetPath(obj); if (!string.IsNullOrEmpty(assetPath)) { if (AssetDatabase.IsValidFolder(assetPath)) { // Add folder if not already in list if (!newFolderPaths.Contains(assetPath)) { newFolderPaths.Add(assetPath); } } else { // If a file is selected, get its directory string folderPath = System.IO.Path.GetDirectoryName(assetPath).Replace('\\', '/'); if (!string.IsNullOrEmpty(folderPath) && !newFolderPaths.Contains(folderPath)) { newFolderPaths.Add(folderPath); } } } } } // Try ProjectWindowUtil if no folders found (Unity 2019.4+) if (newFolderPaths.Count == 0) { try { var method = typeof(ProjectWindowUtil).GetMethod("GetActiveFolderPath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (method != null) { string folderPath = (string)method.Invoke(null, null); if (!string.IsNullOrEmpty(folderPath) && !newFolderPaths.Contains(folderPath)) { newFolderPaths.Add(folderPath); } } } catch { // Fallback if method doesn't exist } } // Check if folder selection has changed bool foldersChanged = selectedFolderPaths.Count != newFolderPaths.Count; if (!foldersChanged) { foreach (string folderPath in newFolderPaths) { if (!selectedFolderPaths.Contains(folderPath)) { foldersChanged = true; break; } } } if (foldersChanged) { selectedFolderPaths = newFolderPaths; hasScanned = false; foundAssetPaths.Clear(); logOutput = ""; } } private void OnGUI() { EditorGUILayout.Space(); EditorGUILayout.LabelField("GFX Addressable Processor", EditorStyles.boldLabel); EditorGUILayout.Space(); // Display selected folders EditorGUILayout.LabelField("Selected Folder(s):", EditorStyles.boldLabel); if (selectedFolderPaths.Count == 0) { EditorGUILayout.HelpBox("No folder selected. Select one or more folders in the Project window.", MessageType.Info); } else { EditorGUILayout.BeginVertical(EditorStyles.helpBox); for (int i = 0; i < selectedFolderPaths.Count; i++) { EditorGUILayout.SelectableLabel($"{i + 1}. {selectedFolderPaths[i]}", EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight)); } EditorGUILayout.EndVertical(); EditorGUILayout.HelpBox($"Scanning {selectedFolderPaths.Count} folder(s) and all subdirectories recursively.", MessageType.Info); } EditorGUILayout.Space(); // Scan button EditorGUI.BeginDisabledGroup(selectedFolderPaths.Count == 0); if (GUILayout.Button("Scan", GUILayout.Height(30))) { ScanAssets(); } EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(); // Display scan results if (hasScanned) { EditorGUILayout.LabelField($"Found {foundAssetPaths.Count} non-addressable asset(s)", EditorStyles.boldLabel); if (foundAssetPaths.Count > 0) { EditorGUILayout.Space(); // Process button if (GUILayout.Button($"Process ({foundAssetPaths.Count} assets)", GUILayout.Height(30))) { ProcessAssets(); } } else { EditorGUILayout.HelpBox("All assets in this folder are already addressable.", MessageType.Info); } } EditorGUILayout.Space(); EditorGUILayout.LabelField("Log Output:", EditorStyles.boldLabel); // Log output area scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.ExpandHeight(true)); EditorGUILayout.TextArea(logOutput, EditorStyles.textArea, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); // Clear log button if (GUILayout.Button("Clear Log")) { logOutput = ""; } } /// /// Retrieves all files (excluding directories) from a directory and all its subdirectories. /// /// The directory path to search (Unity asset path format) /// List of file paths (Unity asset paths, excluding .meta files) private List GetAllFilesRecursive(string directoryPath) { List filePaths = new List(); if (string.IsNullOrEmpty(directoryPath) || !AssetDatabase.IsValidFolder(directoryPath)) { return filePaths; } try { // Get the full system path from Unity asset path string systemPath = System.IO.Path.GetFullPath(directoryPath); if (!System.IO.Directory.Exists(systemPath)) { return filePaths; } // Get the project root path (where Assets folder is) string projectRoot = System.IO.Path.GetFullPath(Application.dataPath); projectRoot = System.IO.Path.GetDirectoryName(projectRoot); // Go up one level from Assets projectRoot = projectRoot.Replace('\\', '/'); // Get all files recursively (excluding directories) string[] allFiles = System.IO.Directory.GetFiles( systemPath, "*.*", System.IO.SearchOption.AllDirectories ); // Convert system paths back to Unity asset paths and filter foreach (string systemFile in allFiles) { // Skip .meta files if (systemFile.EndsWith(".meta", System.StringComparison.OrdinalIgnoreCase)) continue; // Convert to normalized path string normalizedSystemPath = systemFile.Replace('\\', '/'); // Convert to Unity asset path relative to project root if (normalizedSystemPath.StartsWith(projectRoot, System.StringComparison.OrdinalIgnoreCase)) { string unityPath = normalizedSystemPath.Substring(projectRoot.Length); if (unityPath.StartsWith("/")) unityPath = unityPath.Substring(1); // Ensure it's a valid asset file (not a directory) if (!string.IsNullOrEmpty(unityPath) && !AssetDatabase.IsValidFolder(unityPath)) { filePaths.Add(unityPath); } } } } catch (System.Exception e) { AddLog($"ERROR retrieving files: {e.Message}"); } return filePaths; } /// /// Scans all selected folders for assets that are not marked as addressable. /// private void ScanAssets() { foundAssetPaths.Clear(); logOutput = ""; AddLog("Starting scan..."); if (selectedFolderPaths.Count == 0) { AddLog("ERROR: No folder selected!"); hasScanned = true; return; } AddLog($"Scanning {selectedFolderPaths.Count} folder(s):"); foreach (string folderPath in selectedFolderPaths) { AddLog($" - {folderPath}"); } AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { AddLog("ERROR: Addressable settings not found. Please ensure Addressables package is installed."); hasScanned = true; return; } // Get all files recursively from all selected folders (excluding directories) List allFiles = new List(); foreach (string folderPath in selectedFolderPaths) { List folderFiles = GetAllFilesRecursive(folderPath); AddLog($"Found {folderFiles.Count} file(s) in {folderPath} and subdirectories"); allFiles.AddRange(folderFiles); } AddLog($"Found {allFiles.Count} total file(s) across all selected folders"); int nonAddressableCount = 0; foreach (string assetPath in allFiles) { if (string.IsNullOrEmpty(assetPath)) continue; // Double-check: skip meta files and folders (shouldn't happen, but safety check) if (assetPath.EndsWith(".meta") || AssetDatabase.IsValidFolder(assetPath)) continue; // Get GUID for the asset string guid = AssetDatabase.AssetPathToGUID(assetPath); if (string.IsNullOrEmpty(guid)) continue; // Check if asset is already addressable AddressableAssetEntry entry = settings.FindAssetEntry(guid); if (entry == null) { foundAssetPaths.Add(assetPath); nonAddressableCount++; } } AddLog($"Found {nonAddressableCount} non-addressable asset(s)"); hasScanned = true; Repaint(); } /// /// Processes all found assets by making them addressable and moving them to the "gfx" group. /// private void ProcessAssets() { if (foundAssetPaths.Count == 0) { AddLog("No assets to process."); return; } AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { AddLog("ERROR: Addressable settings not found."); return; } AddLog($"\n=== Processing {foundAssetPaths.Count} asset(s) ==="); // Get or create the "gfx" group AddressableAssetGroup gfxGroup = GetOrCreateGroup(settings, GFX_GROUP_NAME); if (gfxGroup == null) { AddLog("ERROR: Failed to get or create 'gfx' group."); return; } int processedCount = 0; int skippedCount = 0; int errorCount = 0; foreach (string assetPath in foundAssetPaths) { try { string guid = AssetDatabase.AssetPathToGUID(assetPath); if (string.IsNullOrEmpty(guid)) { AddLog($"ERROR: Could not get GUID for {assetPath}"); errorCount++; continue; } // Check if asset is already addressable (double-check) AddressableAssetEntry existingEntry = settings.FindAssetEntry(guid); if (existingEntry != null) { // Check if it's already in the correct group if (existingEntry.parentGroup == gfxGroup) { AddLog($"SKIP: {assetPath} is already in 'gfx' group"); skippedCount++; continue; } else { // Move to gfx group existingEntry.parentGroup = gfxGroup; AddLog($"MOVED: {assetPath} to 'gfx' group"); processedCount++; } } else { // Create new addressable entry AddressableAssetEntry newEntry = settings.CreateOrMoveEntry(guid, gfxGroup); if (newEntry != null) { // Set address to full asset path (including "Assets/") // This allows UpdateAddressablePathForGfx to properly remove the prefix newEntry.SetAddress(assetPath); AddLog($"ADDED: {assetPath} -> Address: {newEntry.address}"); processedCount++; } else { AddLog($"ERROR: Failed to create entry for {assetPath}"); errorCount++; } } } catch (System.Exception e) { AddLog($"ERROR processing {assetPath}: {e.Message}"); errorCount++; } } // Save changes if (processedCount > 0) { EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); AddLog($"\n=== Processing Complete ==="); AddLog($"Processed: {processedCount}"); AddLog($"Skipped: {skippedCount}"); AddLog($"Errors: {errorCount}"); } else { AddLog("\nNo assets were processed."); } // Trigger the next tool TriggerNextTool(); // Refresh the scan ScanAssets(); } /// /// Gets an existing AddressableAssetGroup by name, or creates it if it doesn't exist. /// private AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName) { if (settings == null || string.IsNullOrEmpty(groupName)) { return null; } // Try to find existing group AddressableAssetGroup group = settings.FindGroup(groupName); if (group != null) { AddLog($"Found existing group: {groupName}"); return group; } // Create new group try { group = settings.CreateGroup(groupName, false, false, true, null); if (group != null) { AddLog($"Created new group: {groupName}"); EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); } else { AddLog($"ERROR: Failed to create group: {groupName}"); } return group; } catch (System.Exception e) { AddLog($"ERROR creating group {groupName}: {e.Message}"); return null; } } /// /// Triggers the next tool in the workflow: UpdateAddressablePathForGfx /// private void TriggerNextTool() { AddLog("\n=== Triggering next tool ==="); AddLog("Calling UpdateAddressablePathForGfx..."); try { // Call the static method from AddressableTools AddressableTools.UpdateAddressablePathForGfx(); AddLog("Successfully called UpdateAddressablePathForGfx"); } catch (System.Exception e) { AddLog($"ERROR calling UpdateAddressablePathForGfx: {e.Message}"); } } /// /// Adds a log message to the output area. /// private void AddLog(string message) { if (string.IsNullOrEmpty(logOutput)) { logOutput = message; } else { logOutput += "\n" + message; } // Also log to Unity console Debug.Log($"[GFX Addressable Processor] {message}"); Repaint(); } } } #endif