Cleanup the editor assembly and provide a tool overview doc

This commit is contained in:
Michal Adam Pikulski
2025-10-21 14:54:58 +02:00
parent 0a240da9a7
commit 2abcf5c76a
38 changed files with 169 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
namespace PuzzleS.Editor
{
/// <summary>
/// Handles asset post-processing for puzzle step data.
/// Automatically builds level data from folder structure.
/// </summary>
public class PuzzleAssetProcessor : AssetPostprocessor
{
// Base path for puzzle data
private const string PuzzleDataBasePath = "Assets/Data/Puzzles";
/// <summary>
/// Called after assets have been imported, deleted, or moved.
/// </summary>
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<ScriptableObject>(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");
}
}
/// <summary>
/// Checks if an asset path is within the puzzle data folder structure
/// </summary>
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");
}
/// <summary>
/// Find the level folder that contains this asset path
/// Assumes level folders are direct children of PuzzleDataBasePath
/// </summary>
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;
}
/// <summary>
/// Process a level folder to build/update a PuzzleLevelDataSO with all puzzle steps
/// </summary>
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<PuzzleStepSO>(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");
}
/// <summary>
/// Find all assets of a specific type in a folder and its subfolders
/// </summary>
private static List<T> FindAssetsOfTypeRecursive<T>(string folderPath) where T : ScriptableObject
{
var result = new List<T>();
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<T>(assetPath);
if (asset != null)
{
result.Add(asset);
}
}
return result;
}
/// <summary>
/// Get existing level data asset or create a new one
/// </summary>
private static PuzzleLevelDataSO GetOrCreateLevelDataAsset(string folderPath, string levelName)
{
// Check for existing level data asset
string levelDataAssetPath = $"{folderPath}/{levelName}.asset";
var levelDataAsset = AssetDatabase.LoadAssetAtPath<PuzzleLevelDataSO>(levelDataAssetPath);
if (levelDataAsset == null)
{
// Create new level data asset
levelDataAsset = ScriptableObject.CreateInstance<PuzzleLevelDataSO>();
AssetDatabase.CreateAsset(levelDataAsset, levelDataAssetPath);
Debug.Log($"[PuzzleProcessor] Created new level data asset: {levelDataAssetPath}");
}
return levelDataAsset;
}
/// <summary>
/// Find steps that have no dependencies (initial steps)
/// </summary>
private static List<PuzzleStepSO> FindInitialSteps(List<PuzzleStepSO> steps)
{
var initialSteps = new List<PuzzleStepSO>();
var dependentSteps = new HashSet<PuzzleStepSO>();
// 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;
}
/// <summary>
/// Pre-compute dependency information for a puzzle level
/// </summary>
private static void PrecomputeDependencies(PuzzleLevelDataSO levelDataAsset)
{
levelDataAsset.stepDependencies.Clear();
// Build reverse dependency map (which steps are required by each step)
var reverseDependencies = new Dictionary<string, List<string>>();
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<string>();
}
reverseDependencies[unlockStep.stepId].Add(step.stepId);
}
}
// Convert to serialized form
foreach (var entry in reverseDependencies)
{
levelDataAsset.stepDependencies[entry.Key] = entry.Value.ToArray();
}
}
/// <summary>
/// Set up an asset as an Addressable with the specified address
/// </summary>
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);
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5097566c60d341dbb6f2bf5175b048cb
timeCreated: 1760532147