using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; namespace PuzzleS.Editor { /// /// Handles asset post-processing for puzzle step data. /// Automatically builds level data from folder structure. /// public class PuzzleAssetProcessor : AssetPostprocessor { // Base path for puzzle data private const string PuzzleDataBasePath = "Assets/Data/Puzzles"; /// /// Called after assets have been imported, deleted, or moved. /// private static void OnPostprocessAllAssets( string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { bool puzzleAssetsChanged = false; // Check for changes to puzzle step assets foreach (string assetPath in importedAssets.Concat(movedAssets)) { if (IsPuzzleAssetPath(assetPath) && Path.GetExtension(assetPath) == ".asset") { var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset is PuzzleStepSO) { // Find which level this step belongs to string assetDir = Path.GetDirectoryName(assetPath); string levelFolderPath = FindLevelFolder(assetDir); if (!string.IsNullOrEmpty(levelFolderPath)) { ProcessPuzzleLevelFolder(levelFolderPath); puzzleAssetsChanged = true; } } } } // Check for changes to puzzle folder structure foreach (string assetPath in deletedAssets.Concat(movedFromAssetPaths)) { if (IsPuzzleAssetPath(assetPath)) { // Extract parent folders for processing string assetDir = Path.GetDirectoryName(assetPath); string levelFolderPath = FindLevelFolder(assetDir); if (!string.IsNullOrEmpty(levelFolderPath) && Directory.Exists(levelFolderPath)) { ProcessPuzzleLevelFolder(levelFolderPath); puzzleAssetsChanged = true; } } } if (puzzleAssetsChanged) { // Make sure changes are saved AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[PuzzleProcessor] Puzzle data processing complete"); } } /// /// Checks if an asset path is within the puzzle data folder structure /// private static bool IsPuzzleAssetPath(string assetPath) { // Unity's AssetDatabase always uses forward slashes, so we need to normalize for comparison string normalizedPath = assetPath.Replace('\\', '/'); string normalizedBasePath = PuzzleDataBasePath; return normalizedPath.StartsWith(normalizedBasePath) && !normalizedPath.Contains("/.") && // Skip hidden folders !Path.GetExtension(normalizedPath).Equals(".meta"); } /// /// Find the level folder that contains this asset path /// Assumes level folders are direct children of PuzzleDataBasePath /// private static string FindLevelFolder(string assetPath) { if (string.IsNullOrEmpty(assetPath)) return null; // Unity's AssetDatabase always uses forward slashes string normalizedPath = assetPath.Replace('\\', '/'); if (!normalizedPath.StartsWith(PuzzleDataBasePath)) return null; // Get the relative path from the base path string relativePath = normalizedPath.Substring(PuzzleDataBasePath.Length); if (relativePath.StartsWith("/")) relativePath = relativePath.Substring(1); // Split into path components and get the first folder (level name) string[] pathComponents = relativePath.Split('/'); // First component after PuzzleDataBasePath should be the level name if (pathComponents.Length > 0) { // Use proper path joining with Unity's forward slash convention return PuzzleDataBasePath + "/" + pathComponents[0]; } return null; } /// /// Process a level folder to build/update a PuzzleLevelDataSO with all puzzle steps /// private static void ProcessPuzzleLevelFolder(string folderPath) { if (!Directory.Exists(folderPath)) return; // Get level name from folder name string levelName = Path.GetFileName(folderPath); if (string.IsNullOrEmpty(levelName)) return; // Find all puzzle step assets in this level (including subfolders) var stepAssets = FindAssetsOfTypeRecursive(folderPath); if (stepAssets.Count == 0) return; // Get or create level data asset var levelDataAsset = GetOrCreateLevelDataAsset(folderPath, levelName); if (levelDataAsset == null) return; // Update level data levelDataAsset.levelId = levelName; levelDataAsset.displayName = levelName; // Default display name matches folder name levelDataAsset.allSteps = stepAssets; // Pre-process initial steps (those with no dependencies) levelDataAsset.initialSteps = FindInitialSteps(stepAssets); // Pre-process dependencies (which steps are required by each step) PrecomputeDependencies(levelDataAsset); // Mark as dirty and save EditorUtility.SetDirty(levelDataAsset); // Setup addressables SetupAddressableAsset(AssetDatabase.GetAssetPath(levelDataAsset), $"Puzzles/{levelName}"); Debug.Log($"[PuzzleProcessor] Processed level: {levelName} with {stepAssets.Count} steps"); } /// /// Find all assets of a specific type in a folder and its subfolders /// private static List FindAssetsOfTypeRecursive(string folderPath) where T : ScriptableObject { var result = new List(); if (!Directory.Exists(folderPath)) return result; var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name, new[] { folderPath }); foreach (var guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset != null) { result.Add(asset); } } return result; } /// /// Get existing level data asset or create a new one /// private static PuzzleLevelDataSO GetOrCreateLevelDataAsset(string folderPath, string levelName) { // Check for existing level data asset string levelDataAssetPath = $"{folderPath}/{levelName}.asset"; var levelDataAsset = AssetDatabase.LoadAssetAtPath(levelDataAssetPath); if (levelDataAsset == null) { // Create new level data asset levelDataAsset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(levelDataAsset, levelDataAssetPath); Debug.Log($"[PuzzleProcessor] Created new level data asset: {levelDataAssetPath}"); } return levelDataAsset; } /// /// Find steps that have no dependencies (initial steps) /// private static List FindInitialSteps(List steps) { var initialSteps = new List(); var dependentSteps = new HashSet(); // Find all steps that are dependencies of other steps foreach (var step in steps) { if (step == null) continue; foreach (var unlockStep in step.unlocks) { if (unlockStep != null && steps.Contains(unlockStep)) { dependentSteps.Add(unlockStep); } } } // Initial steps are those not depended on by any other step foreach (var step in steps) { if (step != null && !dependentSteps.Contains(step)) { initialSteps.Add(step); } } return initialSteps; } /// /// Pre-compute dependency information for a puzzle level /// private static void PrecomputeDependencies(PuzzleLevelDataSO levelDataAsset) { levelDataAsset.stepDependencies.Clear(); // Build reverse dependency map (which steps are required by each step) var reverseDependencies = new Dictionary>(); foreach (var step in levelDataAsset.allSteps) { if (step == null) continue; foreach (var unlockStep in step.unlocks) { if (unlockStep == null) continue; if (!reverseDependencies.ContainsKey(unlockStep.stepId)) { reverseDependencies[unlockStep.stepId] = new List(); } reverseDependencies[unlockStep.stepId].Add(step.stepId); } } // Convert to serialized form foreach (var entry in reverseDependencies) { levelDataAsset.stepDependencies[entry.Key] = entry.Value.ToArray(); } } /// /// Set up an asset as an Addressable with the specified address /// private static void SetupAddressableAsset(string assetPath, string address) { if (!File.Exists(assetPath)) return; // Get Addressable settings var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null) { Debug.LogWarning("[PuzzleProcessor] Addressable Asset Settings not found. Make sure Addressables is set up in your project."); return; } // Get default group var defaultGroup = settings.DefaultGroup; if (defaultGroup == null) { Debug.LogWarning("[PuzzleProcessor] Default Addressable Asset Group not found."); return; } // Get asset GUID var guid = AssetDatabase.AssetPathToGUID(assetPath); // Check if entry exists var existingEntry = settings.FindAssetEntry(guid); if (existingEntry != null) { // Update existing entry existingEntry.SetAddress(address); } else { // Create new entry settings.CreateOrMoveEntry(guid, defaultGroup); var newEntry = settings.FindAssetEntry(guid); if (newEntry != null) { newEntry.SetAddress(address); } } } } }