using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditorInternal; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; namespace Asset_Cleaner { class BacklinkStore { public bool Initialized { get; private set; } public Dictionary UnusedFiles { get; private set; } public Dictionary UnusedScenes { get; private set; } public Dictionary Backward { get; private set; } public Dictionary FoldersWithQty { get; private set; } Dictionary _forward; List Folders { get; set; } public void Init() { FoldersWithQty = new Dictionary(); _forward = new Dictionary(); Backward = new Dictionary(); var defaultAss = typeof(DefaultAsset); var asmdefAss = typeof(AssemblyDefinitionAsset); var paths = AssetDatabase.GetAllAssetPaths() .Distinct() .Where(s => s.StartsWith("Assets") || s.StartsWith("ProjectSettings")) .Where(p => { var t = AssetDatabase.GetMainAssetTypeAtPath(p); return !t.IsAssignableFromInverse(defaultAss) && !t.IsAssignableFromInverse(asmdefAss); }) .ToArray(); var i = 0f; var total = (float) paths.Length; foreach (var path in paths) { _FillFwAndBacklinks(path); var percent = i * 100f / total; if (Math.Abs(percent % 5f) < 0.01f) { if (EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Building the cache...", percent)) Debug.LogError("Cache build aborted"); } i++; } EditorUtility.ClearProgressBar(); // FillFoldersWithQtyByPaths List foldersAll = new List(); foreach (var path in paths) { var folders = GetAllFoldersFromPath(path); foldersAll.AddRange(folders); } Folders = foldersAll.Distinct().OrderBy(p => p).ToList(); UpdateUnusedAssets(); Initialized = true; } void _FillFwAndBacklinks(string path) { var dependencies = _Dependencies(path); var hs = new FwMeta {Dependencies = new HashSet(dependencies)}; _forward.Add(path, hs); foreach (var backPath in dependencies) { if (!Backward.TryGetValue(backPath, out var val)) { val = new BwMeta(); val.Lookup = new HashSet(); Backward.Add(backPath, val); } val.Lookup.Add(path); } } void UpdateFoldersWithQtyByPath(string path) { var folders = GetAllFoldersFromPath(path); foreach (var folder in folders) { if (!Folders.Exists(p => p == folder)) Folders.Add(folder); } } static List GetAllFoldersFromPath(string p) { var result = new List(); var i = p.IndexOf('/', 0); while (i > 0) { var item = p.Substring(0, i); result.Add(item); i = p.IndexOf('/', i + 1); } return result.Distinct().ToList(); } public void UpdateUnusedAssets() { var all = new HashSet(_forward.Keys); var withBacklinks = new HashSet(Backward.Where(kv => kv.Value.Lookup.Count > 0).Select(kv => kv.Key)); all.ExceptWith(withBacklinks); all.RemoveWhere(SearchUtils.IsFileIgrnoredBySettings); var unusedAssets = all; var scenes = unusedAssets.Where(s => AssetDatabase.GetMainAssetTypeAtPath(s).IsAssignableFromInverse(typeof(SceneAsset))).ToArray(); unusedAssets.ExceptWith(scenes); var files = unusedAssets; UnusedFiles = new Dictionary(); foreach (var file in files) UnusedFiles[file] = CommonUtils.Size(file); UnusedScenes = new Dictionary(); foreach (var scene in scenes) UnusedScenes[scene] = CommonUtils.Size(scene); // UpdateFoldersWithQty(); foreach (var folder in Folders) { var unusedFilesQty = UnusedFiles.Count(p => p.Key.StartsWith(folder)); var unusedScenesQty = UnusedScenes.Count(p => p.Key.StartsWith(folder)); long size = 0; size = UnusedFiles.Where((p => p.Key.StartsWith(folder))).Sum(p => p.Value); size += UnusedScenes.Where(p => p.Key.StartsWith(folder)).Sum(p => p.Value); FoldersWithQty.TryGetValue(folder, out var folderWithQty); if (folderWithQty == null) { FoldersWithQty.Add(folder, new UnusedQty(unusedFilesQty, unusedScenesQty, size)); } else { folderWithQty.UnusedFilesQty = unusedFilesQty; folderWithQty.UnusedScenesQty = unusedScenesQty; folderWithQty.UnusedSize = size; } } } public void Remove(string path) { if (!_forward.TryGetValue(path, out var fwMeta)) return; foreach (var dependency in fwMeta.Dependencies) { if (!Backward.TryGetValue(dependency, out var dep)) continue; dep.Lookup.Remove(path); } _forward.Remove(path); UpdateFoldersWithQtyByPath(path); } public void Replace(string src, string dest) { _Upd(_forward); _Upd(Backward); UpdateFoldersWithQtyByPath(dest); void _Upd(Dictionary dic) { if (!dic.TryGetValue(src, out var refs)) return; dic.Remove(src); dic.Add(dest, refs); } } public void RebuildFor(string path, bool remove) { if (!_forward.TryGetValue(path, out var fwMeta)) { fwMeta = new FwMeta(); _forward.Add(path, fwMeta); } else if (remove) { foreach (var dependency in fwMeta.Dependencies) { if (!Backward.TryGetValue(dependency, out var backDep)) continue; backDep.Lookup.Remove(path); } fwMeta.Dependencies = null; } var dependencies = _Dependencies(path); fwMeta.Dependencies = new HashSet(dependencies); foreach (var backPath in dependencies) { if (!Backward.TryGetValue(backPath, out var bwMeta)) { bwMeta = new BwMeta {Lookup = new HashSet()}; Backward.Add(backPath, bwMeta); } else if (remove) bwMeta.Lookup.Remove(path); bwMeta.Lookup.Add(path); } if (!remove) { UpdateFoldersWithQtyByPath(path); } } static string[] _Dependencies(string s) { if (s[0] == 'A') return AssetDatabase.GetDependencies(s, false); var obj = LoadAllOrMain(s)[0]; return GetDependenciesManualPaths().ToArray(); Object[] LoadAllOrMain(string assetPath) { // prevents error "Do not use readobjectthreaded on scene objects!" return typeof(SceneAsset) == AssetDatabase.GetMainAssetTypeAtPath(assetPath) ? new[] {AssetDatabase.LoadMainAssetAtPath(assetPath)} : AssetDatabase.LoadAllAssetsAtPath(assetPath); } IEnumerable GetDependenciesManualPaths() { if (obj is EditorBuildSettings) { foreach (var scene in EditorBuildSettings.scenes) yield return scene.path; } using (var so = new SerializedObject(obj)) { var props = so.GetIterator(); while (props.Next(true)) { switch (props.propertyType) { case SerializedPropertyType.ObjectReference: var propsObjectReferenceValue = props.objectReferenceValue; if (!propsObjectReferenceValue) continue; var assetPath = AssetDatabase.GetAssetPath(propsObjectReferenceValue); yield return assetPath; break; #if later case SerializedPropertyType.Generic: case SerializedPropertyType.ExposedReference: case SerializedPropertyType.ManagedReference: break; #endif default: continue; } } } } } class FwMeta { public HashSet Dependencies; } public class BwMeta { public HashSet Lookup; } public class UnusedQty { public int UnusedFilesQty; public int UnusedScenesQty; public long UnusedSize; public UnusedQty() { Init(0, 0, 0); } public UnusedQty(int unusedFilesQty, int unusedScenesQty, long unusedSize) { Init(unusedFilesQty, unusedScenesQty, unusedSize); } private void Init(int unusedFilesQty, int unusedScenesQty, long unusedSize) { UnusedFilesQty = unusedFilesQty; UnusedScenesQty = unusedScenesQty; UnusedSize = unusedSize; } } } }