322 lines
12 KiB
C#
322 lines
12 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|