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");
}
}
}