diff --git a/VisualPinball.Unity/Documentation~/creators-guide/setup/running-vpe.md b/VisualPinball.Unity/Documentation~/creators-guide/setup/running-vpe.md index ce29a5268..a7995d89f 100644 --- a/VisualPinball.Unity/Documentation~/creators-guide/setup/running-vpe.md +++ b/VisualPinball.Unity/Documentation~/creators-guide/setup/running-vpe.md @@ -4,7 +4,7 @@ description: How to run VPE --- # Running VPE -Now we can begin with some simple game play. Open [Visual Pinball](https://github.com/vpinball/vpinball), create a new "blank" table, and save it somewhere. In Unity, go to *Visual Pinball -> Import VPX* and choose the `.vpx` file you've just created. +Now we can begin with some simple game play. Open [Visual Pinball](https://github.com/vpinball/vpinball), create a new "Full Example Table", and save it somewhere. In Unity, go to *Pinball -> Import VPX* and choose the `.vpx` file you've just created. You should now see Visual Pinball's blank table in the Editor's scene view: diff --git a/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table-ugly-gizmos.png b/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table-ugly-gizmos.png index dcc302bec..9d2107d69 100644 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table-ugly-gizmos.png and b/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table-ugly-gizmos.png differ diff --git a/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table.png b/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table.png index 673b7f57a..d8b6696d7 100644 Binary files a/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table.png and b/VisualPinball.Unity/Documentation~/creators-guide/setup/unity-imported-table.png differ diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/AssetReferenceLocator.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/AssetReferenceLocator.cs new file mode 100644 index 000000000..49a22d018 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/AssetReferenceLocator.cs @@ -0,0 +1,503 @@ +// Visual Pinball Engine +// Copyright (C) 2025 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace VisualPinball.Unity.Editor +{ + public class AssetReferenceLocator : EditorWindow + { + [Serializable] + private class RefResult + { + public string assetPath; // Referencer (could be in Assets/ or Packages/org.visualpinball.*) + public List contexts = new List(); // Optional details (where inside) + } + + private UnityEngine.Object _target; // Object-based search (takes precedence if both set to avoid ambiguity) + private string _guidInput = ""; // GUID-based search + + private bool _deepInspect = true; + private bool _includeScenes = true; + private bool _includePrefabs = true; + private bool _includeMaterials = true; + private bool _includeScriptableObjects = true; + private bool _includePackages = true; // Scan Packages/org.visualpinball.* + + private static readonly string PackagePrefix = "Packages/org.visualpinball."; + + private Vector2 _scroll; + private List _results = new List(); + private string _status = ""; + + // ---------- Compatibility helpers to avoid generic type inference issues ---------- + private static T LoadAtPath(string path) where T : UnityEngine.Object + { +#if UNITY_2019_1_OR_NEWER + return AssetDatabase.LoadAssetAtPath(path); +#else + return (T)AssetDatabase.LoadAssetAtPath(path, typeof(T)); +#endif + } + + private static UnityEngine.Object LoadAny(string path) + { +#if UNITY_2019_1_OR_NEWER + return AssetDatabase.LoadAssetAtPath(path); +#else + return AssetDatabase.LoadAssetAtPath(path, typeof(UnityEngine.Object)); +#endif + } + + [MenuItem("Pinball/Tools/Asset Reference Locator", false, 413)] + public static void Open() + { + GetWindow(true, "Asset Reference Locator").Show(); + } + + private void OnGUI() + { + GUILayout.Label("Find where an asset is directly referenced (Assets/ + Packages/org.visualpinball.*)", + EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "Search by Asset (object field) OR by GUID. If both are set, the asset search is used. Results list only DIRECT references.", + MessageType.Info); + + _target = EditorGUILayout.ObjectField("Target Asset (optional)", _target, typeof(UnityEngine.Object), + false); + _guidInput = EditorGUILayout.TextField( + new GUIContent("Target GUID (optional)", + "32 hex characters; resolves to an asset if present, otherwise falls back to YAML text scan."), + _guidInput); + + _deepInspect = + EditorGUILayout.ToggleLeft("Deep Inspect (show component/property where referenced)", _deepInspect); + + EditorGUILayout.BeginHorizontal(); + _includeScenes = EditorGUILayout.ToggleLeft("Scenes", _includeScenes); + _includePrefabs = EditorGUILayout.ToggleLeft("Prefabs", _includePrefabs); + _includeMaterials = EditorGUILayout.ToggleLeft("Materials", _includeMaterials); + _includeScriptableObjects = EditorGUILayout.ToggleLeft("ScriptableObjects", _includeScriptableObjects); + EditorGUILayout.EndHorizontal(); + + _includePackages = EditorGUILayout.ToggleLeft("Include Packages/org.visualpinball.*", _includePackages); + + EditorGUILayout.Space(); + using (new EditorGUI.DisabledScope(_target == null && string.IsNullOrWhiteSpace(_guidInput))) + { + if (GUILayout.Button("Find References", GUILayout.Height(28))) + FindReferences(); + } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Status:", _status); + EditorGUILayout.Space(); + + EditorGUILayout.LabelField($"Results: {_results.Count}"); + _scroll = EditorGUILayout.BeginScrollView(_scroll); + foreach (var r in _results) + { + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Asset:", r.assetPath); + if (GUILayout.Button("Select", GUILayout.Width(80))) + { + var obj = LoadAny(r.assetPath); + Selection.activeObject = obj; + EditorGUIUtility.PingObject(obj); + } + + EditorGUILayout.EndHorizontal(); + + if (_deepInspect && r.contexts.Count > 0) + { + EditorGUILayout.LabelField("Where:"); + foreach (var c in r.contexts.Take(10)) + EditorGUILayout.LabelField(" • " + c); + if (r.contexts.Count > 10) + EditorGUILayout.LabelField($" (+{r.contexts.Count - 10} more)"); + } + + EditorGUILayout.EndVertical(); + } + + EditorGUILayout.EndScrollView(); + } + + private void FindReferences() + { + _results.Clear(); + _status = ""; + + string guid = SanitizeGuid(_guidInput); + bool guidMode = !string.IsNullOrEmpty(guid); + + if (_target == null && !guidMode) + { + _status = "Pick a target asset or enter a GUID."; + Repaint(); + return; + } + + // Try to resolve GUID to an asset path if provided + string targetPath = null; + UnityEngine.Object searchTarget = _target; + if (guidMode) + { + targetPath = AssetDatabase.GUIDToAssetPath(guid); + if (!string.IsNullOrEmpty(targetPath)) + { + // Populate the object field with the resolved asset + searchTarget = LoadAny(targetPath); + _target = searchTarget; // reflect in the UI as requested + } + } + else + { + targetPath = AssetDatabase.GetAssetPath(_target); + } + + var addedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + + try + { + // If GUID resolved, include the asset itself in the results first + if (!string.IsNullOrEmpty(targetPath)) + { + var rr = new RefResult { assetPath = targetPath }; + rr.contexts.Add("[Target] Resolved from input" + (guidMode ? " GUID" : " asset")); + _results.Add(rr); + addedPaths.Add(targetPath); + } + + // Gather candidates from Assets/ and optionally Packages/org.visualpinball.* + var allPaths = AssetDatabase.GetAllAssetPaths(); + IEnumerable candidates = + allPaths.Where(p => p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)); + if (_includePackages) + candidates = candidates.Concat(allPaths.Where(p => + p.StartsWith(PackagePrefix, StringComparison.OrdinalIgnoreCase))); + + var candidateArray = candidates.Distinct().ToArray(); + + int total = candidateArray.Length; + int hits = 0; + for (int i = 0; i < total; i++) + { + string path = candidateArray[i]; + if (EditorUtility.DisplayCancelableProgressBar("Scanning Assets + org.visualpinball packages", path, + (float)i / total)) + break; // user cancelled + + bool isHit = false; + + if (!string.IsNullOrEmpty(targetPath)) + { + // Only list DIRECT references (non-recursive dependency lookup) + string[] deps = AssetDatabase.GetDependencies(path, false); // direct only + if (deps != null && deps.Length > 0 && deps.Contains(targetPath)) + isHit = true; + } + else if (guidMode) + { + // Fallback: GUID text search for direct mentions in YAML-like files + if (IsYamlLike(path)) + { + if (FileContainsGuid(path, guid)) + isHit = true; + } + } + + if (!isHit) continue; + if (addedPaths.Contains(path)) continue; // avoid duplicates (e.g., target itself) + + var result = new RefResult { assetPath = path }; + + if (_deepInspect && searchTarget != null) + { + // Narrow deep inspection to relevant types to avoid heavy work + string ext = Path.GetExtension(path).ToLowerInvariant(); + try + { + if (_includeMaterials && ext == ".mat") + { + var mat = LoadAtPath(path); + FindContextsInObject(mat, searchTarget, result); + } + else if (_includePrefabs && ext == ".prefab") + { + FindContextsInPrefab(path, searchTarget, result); + } + else if (_includeScenes && ext == ".unity") + { + FindContextsInScene(path, searchTarget, result); + } + else if (_includeScriptableObjects) + { + FindContextsInGenericAsset(path, searchTarget, result); + } + } + catch (Exception ex) + { + // Don't fail the whole scan if one asset fails deep inspect + result.contexts.Add("[Deep Inspect Error] " + ex.Message); + } + } + else if (_deepInspect && guidMode && string.IsNullOrEmpty(targetPath)) + { + // For GUID-only text search, provide line number hints + var lines = FindGuidLines(path, guid, maxLines: 5); + foreach (var ln in lines) + result.contexts.Add("[YAML] " + ln); + } + + _results.Add(result); + addedPaths.Add(path); + hits++; + } + + if (guidMode && string.IsNullOrEmpty(targetPath) && + EditorSettings.serializationMode != SerializationMode.ForceText) + { + _status = + $"Found {hits} direct references via GUID text search. Note: project serialization is {EditorSettings.serializationMode}; ForceText is recommended for reliable GUID scanning."; + } + else + { + _status = $"Found {_results.Count} item(s). Direct references listed."; + } + } + catch (Exception ex) + { + Debug.LogException(ex); + _status = ex.Message; + } + finally + { + EditorUtility.ClearProgressBar(); + Repaint(); + } + } + + // ---------------- Deep Inspect Helpers ---------------- + + private void FindContextsInObject(UnityEngine.Object obj, UnityEngine.Object searchTarget, RefResult result) + { + if (obj == null || searchTarget == null) return; + var so = new SerializedObject(obj); + FindObjectReferenceProperties(so, searchTarget, + (propPath) => { result.contexts.Add($"{obj.GetType().Name}.{propPath}"); }); + } + + private void FindContextsInGenericAsset(string path, UnityEngine.Object searchTarget, RefResult result) + { + // Try main asset first (type-agnostic) + var main = LoadAny(path); + if (main != null) + FindContextsInObject(main, searchTarget, result); + + // Also scan sub-assets (e.g., Materials inside FBX, nested ScriptableObjects) + foreach (var sub in AssetDatabase.LoadAllAssetsAtPath(path)) + { + if (sub == null || ReferenceEquals(sub, main)) continue; + FindContextsInObject(sub, searchTarget, result); + } + } + + private void FindContextsInPrefab(string prefabPath, UnityEngine.Object searchTarget, RefResult result) + { + var root = PrefabUtility.LoadPrefabContents(prefabPath); + try + { + foreach (var t in root.GetComponentsInChildren(true)) + { + var go = t.gameObject; + var comps = go.GetComponents(); + foreach (var c in comps) + { + if (c == null) continue; // missing script + var so = new SerializedObject(c); + FindObjectReferenceProperties(so, searchTarget, + (propPath) => + { + result.contexts.Add($"{GetHierarchyPath(go)} → {c.GetType().Name}.{propPath}"); + }); + } + } + } + finally + { + PrefabUtility.UnloadPrefabContents(root); + } + } + + private void FindContextsInScene(string scenePath, UnityEngine.Object searchTarget, RefResult result) + { + var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); + try + { + foreach (var root in scene.GetRootGameObjects()) + { + foreach (var t in root.GetComponentsInChildren(true)) + { + var go = t.gameObject; + var comps = go.GetComponents(); + foreach (var c in comps) + { + if (c == null) continue; // missing script + var so = new SerializedObject(c); + FindObjectReferenceProperties(so, searchTarget, + (propPath) => + { + result.contexts.Add( + $"[Scene {Path.GetFileName(scenePath)}] {GetHierarchyPath(go)} → {c.GetType().Name}.{propPath}"); + }); + } + } + } + } + finally + { + EditorSceneManager.CloseScene(scene, true); + } + } + + private static void FindObjectReferenceProperties(SerializedObject so, UnityEngine.Object target, + Action onHit) + { + if (so == null || target == null) return; + var it = so.GetIterator(); + bool enterChildren = true; + while (it.NextVisible(enterChildren)) + { + enterChildren = false; + if (it.propertyType == SerializedPropertyType.ObjectReference) + { + if (it.objectReferenceValue == target) + { + onHit?.Invoke(it.propertyPath); + } + } + } + } + + private static string GetHierarchyPath(GameObject go) + { + if (go == null) return ""; + var stack = new List(); + var t = go.transform; + while (t != null) + { + stack.Add(t.name); + t = t.parent; + } + + stack.Reverse(); + return string.Join("/", stack); + } + + // ---------------- GUID Search Helpers ---------------- + + private static string SanitizeGuid(string input) + { + if (string.IsNullOrWhiteSpace(input)) return null; + var hex = new System.Text.StringBuilder(32); + foreach (char c in input) + { + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + hex.Append(char.ToLowerInvariant(c)); + } + + if (hex.Length != 32) return null; // Unity GUIDs are 32 hex chars + return hex.ToString(); + } + + private static bool IsYamlLike(string assetPath) + { + string ext = Path.GetExtension(assetPath).ToLowerInvariant(); + return ext == ".prefab" || ext == ".unity" || ext == ".asset" || ext == ".mat" || + ext == ".controller" || ext == ".overridecontroller" || ext == ".anim" || + ext == ".shadergraph" || ext == ".vfx" || ext == ".uss" || ext == ".uxml" || + ext == ".shader"; + } + + private static string ToAbsolutePath(string assetDbPath) + { + // assetDbPath is like "Assets/..." or "Packages/..." + var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + return Path.GetFullPath(Path.Combine(projectRoot, assetDbPath)); + } + + private static bool FileContainsGuid(string assetDbPath, string guid) + { + try + { + string p = ToAbsolutePath(assetDbPath); + if (!File.Exists(p)) return false; + // Lightweight scan first + string text = File.ReadAllText(p); + if (text.IndexOf("guid:" + guid, StringComparison.OrdinalIgnoreCase) >= 0) return true; + if (text.IndexOf("guid: " + guid, StringComparison.OrdinalIgnoreCase) >= 0) return true; + return false; + } + catch + { + return false; + } + } + + private static IEnumerable FindGuidLines(string assetDbPath, string guid, int maxLines = 5) + { + var list = new List(); + try + { + string p = ToAbsolutePath(assetDbPath); + if (!File.Exists(p)) return list; + int n = 0; + int lineNo = 0; + foreach (var line in File.ReadLines(p)) + { + lineNo++; + if (line.IndexOf(guid, StringComparison.OrdinalIgnoreCase) >= 0) + { + list.Add($"line {lineNo}: {TrimForContext(line)}"); + n++; + if (n >= maxLines) break; + } + } + } + catch + { + } + + return list; + } + + private static string TrimForContext(string s) + { + if (string.IsNullOrEmpty(s)) return s; + s = s.Trim(); + if (s.Length > 120) s = s.Substring(0, 117) + "..."; + return s; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/AssetReferenceLocator.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/AssetReferenceLocator.cs.meta new file mode 100644 index 000000000..feb88ccff --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/AssetReferenceLocator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 469cfb2f89004cf3a63f1240954d49a8 +timeCreated: 1755773988 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/PackageReferenceFinder.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/PackageReferenceFinder.cs new file mode 100644 index 000000000..935896afa --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/PackageReferenceFinder.cs @@ -0,0 +1,372 @@ +// Visual Pinball Engine +// Copyright (C) 2025 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; // for Scene + GetRootGameObjects +using Object = UnityEngine.Object; + +namespace VisualPinball.Unity.Editor +{ + public class PackageReferenceLocator : EditorWindow + { + [Serializable] + private class DepUsage + { + public string depPath; // Package dependency path (e.g., Packages/org.visualpinball.*) + public Object depObject; // Cached object for UI + public List referencerObjects = new List(); // Project assets or scene GameObjects that reference depPath + } + + // --- Options --- + [SerializeField] private string _packagePrefix = "Packages/org.visualpinball."; // “indicated package” + [SerializeField] private bool _includeScenes = true; + [SerializeField] private bool _includePrefabs = true; + [SerializeField] private bool _includeMaterials = true; + [SerializeField] private bool _includeScriptableObjects = true; + [SerializeField] private bool _includeEverythingElse = true; // FBX, textures, animations, etc. + [SerializeField] private bool _onlyOpenScenes = false; // When true, closed scenes are skipped; open scenes yield GameObjects instead of the .unity asset + + private Vector2 _scroll; + private readonly List _results = new List(); + private string _status = ""; + + [MenuItem("Pinball/Tools/Package Reference Locator", false, 411)] + public static void Open() + { + GetWindow(true, "Package Reference Locator").Show(); + } + + private void OnGUI() + { + GUILayout.Label("Package dependencies used by the Project (grouped by dependency)", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "When 'Only search open scenes' is enabled, scene references are shown as the specific GameObjects in the open scenes that reference the dependency (directly or via assets like Materials).", + MessageType.Info); + + _packagePrefix = EditorGUILayout.TextField( + new GUIContent("Package Prefix", "Only dependencies under this path are considered."), + _packagePrefix); + + _onlyOpenScenes = EditorGUILayout.ToggleLeft("Only search open scenes (skip closed scenes; list GameObjects for open scenes)", _onlyOpenScenes); + + EditorGUILayout.LabelField("Project Asset Type Filters (referencers):", EditorStyles.boldLabel); + EditorGUILayout.BeginHorizontal(); + _includeScenes = EditorGUILayout.ToggleLeft("Scenes", _includeScenes); + _includePrefabs = EditorGUILayout.ToggleLeft("Prefabs", _includePrefabs); + _includeMaterials = EditorGUILayout.ToggleLeft("Materials", _includeMaterials); + _includeScriptableObjects = EditorGUILayout.ToggleLeft("ScriptableObjects", _includeScriptableObjects); + _includeEverythingElse = EditorGUILayout.ToggleLeft("Everything else", _includeEverythingElse); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + if (GUILayout.Button("Scan Project → Package Dependencies", GUILayout.Height(28))) + ScanProjectToPackageDependencies(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Status:", _status); + EditorGUILayout.Space(); + + EditorGUILayout.LabelField($"Dependencies found: {_results.Count}"); + _scroll = EditorGUILayout.BeginScrollView(_scroll); + + foreach (var dep in _results) + { + EditorGUILayout.BeginVertical("box"); + + // Dependency header row: ObjectField only + using (new EditorGUI.DisabledScope(true)) + EditorGUILayout.ObjectField("Dependency", dep.depObject, typeof(Object), false); + + // Referencers list (object fields only; no dots, no buttons) + EditorGUILayout.LabelField($"Referenced by ({dep.referencerObjects.Count}):"); + foreach (var obj in dep.referencerObjects) + { + using (new EditorGUI.DisabledScope(true)) + EditorGUILayout.ObjectField(obj, obj != null ? obj.GetType() : typeof(Object), false); + } + + EditorGUILayout.EndVertical(); + } + + EditorGUILayout.EndScrollView(); + } + + private void ScanProjectToPackageDependencies() + { + _results.Clear(); + _status = ""; + + if (string.IsNullOrWhiteSpace(_packagePrefix)) + { + _status = "Please enter a package prefix (e.g., Packages/org.visualpinball.)."; + Repaint(); + return; + } + + try + { + // Build a set of currently open scene paths (for fast membership checks) + var openScenePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < EditorSceneManager.sceneCount; i++) + { + var scn = EditorSceneManager.GetSceneAt(i); + if (scn.IsValid() && scn.isLoaded && !string.IsNullOrEmpty(scn.path)) + openScenePaths.Add(scn.path); + } + + var allPaths = AssetDatabase.GetAllAssetPaths(); + + // Project assets that act as referencers (apply type filters; for scenes, skip closed ones if toggle is on) + var projectAssets = allPaths + .Where(p => p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + .Where(p => PassesTypeFilter(p) && PassesSceneOpenFilter(p, openScenePaths)) + .Distinct() + .ToArray(); + + // Map: dependency (package) path -> set of project referencers (Object) + var map = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + int total = projectAssets.Length; + + // Caches to avoid repeated AssetDatabase work in scene deep scans + var depObjCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + var depsSetCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); // assetPath -> deps(set) + + for (int i = 0; i < total; i++) + { + string referencerPath = projectAssets[i]; + + if (EditorUtility.DisplayCancelableProgressBar("Scanning project assets (dependencies)", + referencerPath, total == 0 ? 0 : (float)i / total)) + { + _status = "Scan cancelled."; + break; + } + + bool isScene = string.Equals(Path.GetExtension(referencerPath), ".unity", StringComparison.OrdinalIgnoreCase); + bool isOpenScene = isScene && openScenePaths.Contains(referencerPath); + + // For open scenes we want to include *transitive* deps (textures via materials, etc.) + bool recursive = isScene && _onlyOpenScenes; + + // Get dependencies of this referencer + string[] deps = AssetDatabase.GetDependencies(referencerPath, recursive); + if (deps == null || deps.Length == 0) continue; + + foreach (var dep in deps) + { + // Only collect dependencies inside the indicated package + if (!dep.StartsWith(_packagePrefix, StringComparison.OrdinalIgnoreCase)) continue; + + if (!map.TryGetValue(dep, out var set)) + { + set = new HashSet(); + map[dep] = set; + } + + // Open-scene mode: list *GameObjects* that cause/hold the reference + if (_onlyOpenScenes && isOpenScene) + { + // Get/load the dependency object + if (!depObjCache.TryGetValue(dep, out var depObj) || depObj == null) + { + depObj = LoadAny(dep); + depObjCache[dep] = depObj; + } + + var scene = EditorSceneManager.GetSceneByPath(referencerPath); + foreach (var go in FindSceneGameObjectsReferencing(scene, dep, depObj, depsSetCache)) + set.Add(go); + } + else + { + // Normal case: store the referencer asset object (prefab, material, SO, or closed scene asset) + var obj = LoadAny(referencerPath); + if (obj != null) set.Add(obj); + } + } + } + + EditorUtility.ClearProgressBar(); + + // Build result list (no limits) + foreach (var kv in map.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)) + { + var depPath = kv.Key; + var usage = new DepUsage + { + depPath = depPath, + depObject = LoadAny(depPath), + referencerObjects = kv.Value + .OrderBy(o => GetObjectSortKey(o), StringComparer.OrdinalIgnoreCase) + .ToList() + }; + _results.Add(usage); + } + + if (_results.Count == 0) + _status = "No project assets reference dependencies in the indicated package (with current filters)."; + else + _status = $"Found {_results.Count} package dependencies referenced by project assets."; + } + catch (Exception ex) + { + Debug.LogException(ex); + _status = ex.Message; + } + finally + { + EditorUtility.ClearProgressBar(); + Repaint(); + } + } + + // ----- Scene deep-scan to find GameObjects that (directly or indirectly) reference a given asset ----- + + private static IEnumerable FindSceneGameObjectsReferencing(Scene scene, string targetDepPath, Object targetDepObj, + Dictionary> depsSetCache) + { + var results = new HashSet(); + if (!scene.IsValid() || !scene.isLoaded) return results; + + foreach (var root in scene.GetRootGameObjects()) + { + foreach (var t in root.GetComponentsInChildren(true)) + { + var go = t.gameObject; + var comps = go.GetComponents(); + foreach (var c in comps) + { + if (c == null) continue; // missing script + var so = new SerializedObject(c); + var it = so.GetIterator(); + bool enterChildren = true; + while (it.NextVisible(enterChildren)) + { + enterChildren = false; + if (it.propertyType != SerializedPropertyType.ObjectReference) continue; + + var refObj = it.objectReferenceValue; + if (refObj == null) continue; + + // 1) Direct reference to the dependency object? + if (targetDepObj != null && refObj == targetDepObj) + { + results.Add(go); + break; + } + + // 2) Indirect: does this referenced *asset* depend on the dependency path? + var refPath = AssetDatabase.GetAssetPath(refObj); + if (string.IsNullOrEmpty(refPath)) continue; // scene object or non-asset + if (AssetDependsOn(refPath, targetDepPath, depsSetCache)) + { + results.Add(go); + break; + } + } + } + } + } + + return results; + } + + private static bool AssetDependsOn(string assetPath, string targetDepPath, + Dictionary> depsSetCache) + { + if (!depsSetCache.TryGetValue(assetPath, out var set)) + { + // Recursive to include textures/shaders/etc. pulled by materials/controllers/etc. + var deps = AssetDatabase.GetDependencies(assetPath, true) ?? Array.Empty(); + set = new HashSet(deps, StringComparer.OrdinalIgnoreCase); + depsSetCache[assetPath] = set; + } + return set.Contains(targetDepPath); + } + + // ---------------- Helpers ---------------- + + private static string GetObjectSortKey(Object obj) + { + if (obj == null) return "~"; + // For scene objects, include scene and hierarchy path; for assets, use asset path + if (obj is GameObject go) + { + var sceneName = go.scene.IsValid() ? go.scene.path : ""; + return sceneName + "|" + GetHierarchyPath(go); + } + return AssetDatabase.GetAssetPath(obj) ?? obj.name; + } + + private static string GetHierarchyPath(GameObject go) + { + if (go == null) return ""; + var stack = new List(); + var t = go.transform; + while (t != null) + { + stack.Add(t.name); + t = t.parent; + } + stack.Reverse(); + return string.Join("/", stack); + } + + private static Object LoadAny(string path) + { +#if UNITY_2019_1_OR_NEWER + return AssetDatabase.LoadAssetAtPath(path); +#else + return AssetDatabase.LoadAssetAtPath(path, typeof(UnityEngine.Object)); +#endif + } + + private bool PassesTypeFilter(string assetPath) + { + string ext = Path.GetExtension(assetPath).ToLowerInvariant(); + + // Scenes + if (ext == ".unity") return _includeScenes; + + // Prefabs + if (ext == ".prefab") return _includePrefabs; + + // Materials + if (ext == ".mat") return _includeMaterials; + + // ScriptableObjects (most are .asset) + if (ext == ".asset") return _includeScriptableObjects; + + // Everything else (textures, meshes, FBX, audio, anims, shaders, etc.) + return _includeEverythingElse; + } + + private bool PassesSceneOpenFilter(string assetPath, HashSet openScenePaths) + { + if (!_onlyOpenScenes) return true; // not filtering + if (Path.GetExtension(assetPath).ToLowerInvariant() != ".unity") return true; // only affects scenes + // when filtering, allow only open scenes + return openScenePaths.Contains(assetPath); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/PackageReferenceFinder.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/PackageReferenceFinder.cs.meta new file mode 100644 index 000000000..2f83a676c --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/PackageReferenceFinder.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 057ae830b310a3142b4d4b74f9d5886f +timeCreated: 1755769334 \ No newline at end of file