using AssetBank.Settings; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; using Newtonsoft.Json.Linq; namespace AssetBank.Editor.Tools { /// /// Handles the core logic for scanning the project and managing the component stats. /// public class ComponentStatsController { private static readonly string ProjectRoot = Path.GetDirectoryName(Application.dataPath); public enum ScanSource { Scenes, Prefabs, Both } /// /// Scans the project for components and gathers statistics about them. /// /// The type of assets to scan. /// A dictionary mapping component names to their statistics. public Dictionary ScanProject(ScanSource scanSource) { var settings = ProjectExporterSettings.GetOrCreateSettings(); var ignoredFolders = settings.FoldersToIgnore; var assetsToScan = new List(); if (scanSource == ScanSource.Scenes || scanSource == ScanSource.Both) { assetsToScan.AddRange(AssetDatabase.FindAssets("t:Scene", new[] { "Assets" })); } if (scanSource == ScanSource.Prefabs || scanSource == ScanSource.Both) { assetsToScan.AddRange(AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" })); } var assetPaths = assetsToScan.Distinct().Select(AssetDatabase.GUIDToAssetPath).ToList(); var filteredPaths = assetPaths.Where(path => !ignoredFolders.Any(folder => path.StartsWith(folder, StringComparison.OrdinalIgnoreCase))).ToList(); var tempDir = Path.Combine("Library", "ComponentScanTemp"); if (Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); } Directory.CreateDirectory(tempDir); var componentStats = new Dictionary(); try { EditorUtility.DisplayProgressBar("Scanning Components", "Preparing to scan...", 0f); for (int i = 0; i < filteredPaths.Count; i++) { var assetPath = filteredPaths[i]; var progress = (float)i / filteredPaths.Count; EditorUtility.DisplayProgressBar("Scanning Components", $"Processing: {Path.GetFileName(assetPath)}", progress); var jsonPath = Path.Combine(tempDir, $"{Path.GetFileNameWithoutExtension(assetPath)}_{i}.json"); if (ExecutePythonConversion(assetPath, jsonPath)) { ParseComponentsFromJson(jsonPath, componentStats); } } } finally { if (Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); } EditorUtility.ClearProgressBar(); } return componentStats; } private string FindPythonExecutable() { var venvPath = Path.Combine(ProjectRoot, "venv", "bin", "python3"); if (File.Exists(venvPath)) { return venvPath; } venvPath = Path.Combine(ProjectRoot, "venv", "bin", "python"); if (File.Exists(venvPath)) { return venvPath; } return "python3"; } private bool ExecutePythonConversion(string assetPath, string jsonOutputPath) { var pythonExecutable = FindPythonExecutable(); var scriptGuid = AssetDatabase.FindAssets("convert_scene").FirstOrDefault(); if (string.IsNullOrEmpty(scriptGuid)) { UnityEngine.Debug.LogError("Conversion script 'convert_scene.py' not found."); return false; } var scriptPath = Path.Combine(ProjectRoot, AssetDatabase.GUIDToAssetPath(scriptGuid)); var absoluteAssetPath = Path.Combine(ProjectRoot, assetPath); var process = new Process { StartInfo = new ProcessStartInfo { FileName = pythonExecutable, Arguments = $"\"{scriptPath}\" \"{absoluteAssetPath}\" \"{jsonOutputPath}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode == 0) return true; UnityEngine.Debug.LogError($"Failed to convert {assetPath}. Error: {error}"); return false; } /// /// Parses a JSON file and updates the statistics for each component found. /// private void ParseComponentsFromJson(string jsonPath, Dictionary stats) { try { var jsonContent = File.ReadAllText(jsonPath); var jsonArray = JArray.Parse(jsonContent); foreach (var item in jsonArray) { if (item["data"] is not JObject dataObject) continue; var componentProperty = dataObject.Properties().FirstOrDefault(); if (componentProperty == null) continue; var componentName = componentProperty.Name; if (!stats.ContainsKey(componentName)) { stats[componentName] = new ComponentStats { Name = componentName }; } var currentStats = stats[componentName]; currentStats.TotalOccurrences++; // Pass the *value* of the component property to the counter var propertyCount = CountProperties(componentProperty.Value); currentStats.TotalProperties += propertyCount; if (propertyCount > currentStats.MaxProperties) { currentStats.MaxProperties = propertyCount; } } } catch (Exception e) { UnityEngine.Debug.LogError($"Failed to parse JSON file {jsonPath}. Error: {e.Message}"); } } /// /// Recursively counts the number of properties in a JToken. /// private int CountProperties(JToken token) { int count = 0; if (token is JObject obj) { foreach (var prop in obj.Properties()) { count++; // Count the key itself count += CountProperties(prop.Value); // Recursively count properties in the value } } else if (token is JArray arr) { foreach (var item in arr) { count += CountProperties(item); } } return count; } public void UpdateAndSaveScanResults(Dictionary stats) { var settings = ComponentStatsSettings.GetOrCreateSettings(); settings.ComponentStats.Clear(); settings.ComponentStats.AddRange(stats.Values.OrderBy(s => s.Name)); settings.LastScanTime = DateTime.Now.ToString("g"); // "g" for general short date/time EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); } public void SaveStats(Dictionary stats) { var settings = ComponentStatsSettings.GetOrCreateSettings(); settings.ComponentStats.Clear(); settings.ComponentStats.AddRange(stats.Values.OrderBy(s => s.Name)); EditorUtility.SetDirty(settings); AssetDatabase.SaveAssets(); EditorUtility.DisplayDialog("Success", $"Saved {settings.ComponentStats.Count} component stats.", "OK"); } } }