diff --git a/Packages/dev.derpynewbie.common/Editor/NewbieInjectMenuItem.cs b/Packages/dev.derpynewbie.common/Editor/NewbieInjectMenuItem.cs index 99af67a..cb710e5 100644 --- a/Packages/dev.derpynewbie.common/Editor/NewbieInjectMenuItem.cs +++ b/Packages/dev.derpynewbie.common/Editor/NewbieInjectMenuItem.cs @@ -11,6 +11,7 @@ public static class NewbieInjectMenuItem public const string DoInject = MenuNamespace + "Inject NewbieInject fields"; public const string DoInjectOnBuild = MenuNamespace + "Inject On Build"; public const string DoInjectOnPlay = MenuNamespace + "Inject On Play"; + public const string DoClear = MenuNamespace + "Clear NewbieInject fields"; static NewbieInjectMenuItem() { @@ -45,7 +46,7 @@ private static void InjectMenu() EditorUtility.DisplayDialog( "Inject", - $"Injected reference to {NewbieInjectProcessor.UpdatedComponentCount} objects ({NewbieInjectProcessor.UpdatedFieldCount} fields).", + $"Injected reference to {NewbieInjectProcessor.ComponentUpdateCount} objects ({NewbieInjectProcessor.ProcessedFieldCount} fields).", "OK!" ); } @@ -63,5 +64,20 @@ private static void DoInjectOnPlayMenu() NewbieInjectConfig.InjectOnPlay = !NewbieInjectConfig.InjectOnPlay; UpdateMenuCheck(); } + + [MenuItem(DoClear, priority = 3)] + private static void ClearMenu() + { + try + { + NewbieInjectProcessor.Clear(SceneManager.GetActiveScene()); + } + catch (InvalidOperationException ex) + { + UnityEngine.Debug.LogException(ex); + EditorUtility.DisplayDialog("Clear", "Clearing aborted", "OK!"); + return; + } + } } } \ No newline at end of file diff --git a/Packages/dev.derpynewbie.common/Editor/NewbieInjectProcessor.cs b/Packages/dev.derpynewbie.common/Editor/NewbieInjectProcessor.cs index 7b937e4..f9877a9 100644 --- a/Packages/dev.derpynewbie.common/Editor/NewbieInjectProcessor.cs +++ b/Packages/dev.derpynewbie.common/Editor/NewbieInjectProcessor.cs @@ -4,58 +4,147 @@ using System.Linq; using System.Reflection; using System.Text; +using HarmonyLib; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using VRC.SDKBase.Editor.BuildPipeline; using static DerpyNewbie.Common.Editor.NewbieCommonsEditorUtil; +using Debug = UnityEngine.Debug; namespace DerpyNewbie.Common.Editor { public static class NewbieInjectProcessor { - public static int UpdatedFieldCount { get; private set; } = 0; - public static int UpdatedComponentCount { get; private set; } = 0; + public static int ProcessedFieldCount { get; private set; } = 0; + public static int ComponentUpdateCount { get; private set; } = 0; public static void Inject(Scene scene, bool showProgress = true, bool printResult = true) { var stopwatch = Stopwatch.StartNew(); - UpdatedFieldCount = 0; - UpdatedComponentCount = 0; + ProcessedFieldCount = 0; + ComponentUpdateCount = 0; var injectFields = GetInjectFields(); - var foundComponentsDict = new Dictionary(); + var foundComponentsDict = new Dictionary>(); + var perObjectComponentsDict = new Dictionary>>(); foreach (var field in injectFields) { - if (!foundComponentsDict.ContainsKey(field.FieldType)) - { - var foundComponent = GetComponentsInScene(scene, field.FieldType).FirstOrDefault(); - foundComponentsDict.Add(field.FieldType, foundComponent); - Log( - $"Found requested component `{field.FieldType.FullName}` at `{GetHierarchyName(foundComponent)}`"); - } - foreach (var component in GetComponentsInScene(scene, field.DeclaringType)) { + var injectOption = field.GetCustomAttribute(); + if (showProgress && EditorUtility.DisplayCancelableProgressBar( "Injecting Reference", - $"{injectFields.Count}/{UpdatedFieldCount} Injecting field `{field.Module.Name}:{field.Name}:{field.FieldType.Name}` for `{GetHierarchyName(component)}`", - injectFields.Count / (float)UpdatedFieldCount)) + $"{injectFields.Count}/{ProcessedFieldCount} Injecting field `{field.Module.Name}:{field.Name}:{field.FieldType.Name}({injectOption.Scope.ToString()})` for `{GetHierarchyName(component)}`", + injectFields.Count / (float)ProcessedFieldCount)) { EditorUtility.ClearProgressBar(); throw new InvalidOperationException("Operation cancelled by user interruption"); } + var go = component.gameObject; + List injectingComponents; + switch (injectOption.Scope) + { + default: + { + injectingComponents = new List(); + break; + } + case SearchScope.Scene: + { + if (!foundComponentsDict.TryGetValue(field, out injectingComponents)) + { + injectingComponents = GetComponentsInScene(scene, field.FieldType); + foundComponentsDict.Add(field, injectingComponents); + + Log( + $"Found Scene scoped component `{field.FieldType.FullName}` at `{injectingComponents.Select(GetHierarchyName).Join()}`"); + } + + break; + } + case SearchScope.Self: + { + if (!perObjectComponentsDict.TryGetValue(field, out var goSearchCache)) + { + goSearchCache = new Dictionary>(); + perObjectComponentsDict.Add(field, goSearchCache); + } + + if (!goSearchCache.TryGetValue(go, out injectingComponents)) + { + injectingComponents = go.GetComponents(GetComponentType(field.FieldType)).ToList(); + goSearchCache.Add(go, injectingComponents); + Log( + $"Found Self scoped component `{GetHierarchyName(go)}:{field.FieldType.FullName}` at `{injectingComponents.Select(GetHierarchyName).Join()}`"); + } + + break; + } + case SearchScope.Children: + { + if (!perObjectComponentsDict.TryGetValue(field, out var goSearchCache)) + { + goSearchCache = new Dictionary>(); + perObjectComponentsDict.Add(field, goSearchCache); + } + + if (!goSearchCache.TryGetValue(go, out injectingComponents)) + { + injectingComponents = component + .GetComponentsInChildren(GetComponentType(field.FieldType)).ToList(); + goSearchCache.Add(go, injectingComponents); + + Log( + $"Found Children scoped component `{GetHierarchyName(go)}:{field.FieldType.FullName}` at `{injectingComponents.Select(GetHierarchyName).Join()}`"); + } + + break; + } + case SearchScope.Parents: + { + if (!perObjectComponentsDict.TryGetValue(field, out var goSearchCache)) + { + goSearchCache = new Dictionary>(); + perObjectComponentsDict.Add(field, goSearchCache); + } + + if (!goSearchCache.TryGetValue(go, out injectingComponents)) + { + injectingComponents = component + .GetComponentsInParent(GetComponentType(field.FieldType)).ToList(); + goSearchCache.Add(go, injectingComponents); + Log( + $"Found Parents scoped component `{GetHierarchyName(go)}:{field.FieldType.FullName}` at `{injectingComponents.Select(GetHierarchyName).Join()}`"); + } + + break; + } + } + var serializedObject = new SerializedObject(component); var serializedProperty = serializedObject.FindProperty(field.Name); - serializedProperty.objectReferenceValue = foundComponentsDict[field.FieldType]; + if (field.FieldType.IsArray) + { + serializedProperty.ClearArray(); + serializedProperty.arraySize = injectingComponents.Count; + for (var i = 0; i < injectingComponents.Count; i++) + serializedProperty.GetArrayElementAtIndex(i).objectReferenceValue = injectingComponents[i]; + } + else + { + serializedProperty.objectReferenceValue = injectingComponents.FirstOrDefault(); + } + serializedObject.ApplyModifiedProperties(); - ++UpdatedComponentCount; + ++ComponentUpdateCount; } - ++UpdatedFieldCount; + ++ProcessedFieldCount; } stopwatch.Stop(); @@ -65,18 +154,72 @@ public static void Inject(Scene scene, bool showProgress = true, bool printResul if (printResult) { - var sb = new StringBuilder("Injection Result:"); + var sb = new StringBuilder("Scene Injection Result:" + + "\n====== Scene Search ======"); foreach (var pair in foundComponentsDict) - sb.Append("\n").Append(pair.Key.FullName).Append(", ").Append(GetHierarchyName(pair.Value)); + sb.Append("\n") + .Append(pair.Key.FieldType.FullName).Append(", ") + .Append(pair.Value.Select(GetHierarchyName).Join()); + + sb.Append("\n\n====== GameObject Search ======"); + + foreach (var pair in perObjectComponentsDict) + { + foreach (var goSearch in pair.Value) + { + sb.Append("\n") + .Append(pair.Key.FieldType.FullName) + .Append(":") + .Append(pair.Key.GetCustomAttribute().Scope.ToString()) + .Append(", ") + .Append("(").Append(GetHierarchyName(goSearch.Key)).Append("), ") + .Append(goSearch.Value.Select(GetHierarchyName).Join()); + } + } sb.Append( - $"\n\n{UpdatedFieldCount} fields affected, {UpdatedComponentCount} components updated in {stopwatch.ElapsedMilliseconds} ms."); + $"\n\n{ProcessedFieldCount} fields affected, {ComponentUpdateCount} component updates in {stopwatch.ElapsedMilliseconds} ms."); Log(sb.ToString()); } } + public static void Clear(Scene scene) + { + var stopwatch = Stopwatch.StartNew(); + + ProcessedFieldCount = 0; + ComponentUpdateCount = 0; + + var injectFields = GetInjectFields(); + foreach (var field in injectFields) + { + foreach (var component in GetComponentsInScene(scene, field.DeclaringType)) + { + var serializedObject = new SerializedObject(component); + var serializedProperty = serializedObject.FindProperty(field.Name); + if (field.FieldType.IsArray) + { + serializedProperty.ClearArray(); + serializedProperty.arraySize = 0; + } + else + { + serializedProperty.objectReferenceValue = null; + } + + serializedObject.ApplyModifiedProperties(); + ++ComponentUpdateCount; + } + + ++ProcessedFieldCount; + } + + Debug.Log( + $"{ProcessedFieldCount} fields affected, {ComponentUpdateCount} component updates in {stopwatch.ElapsedMilliseconds} ms."); + } + public static List GetInjectFields() { var result = new List(); @@ -93,16 +236,28 @@ public static List GetComponentsInScene(Scene scene, Type type) { var components = new List(); foreach (var o in scene.GetRootGameObjects()) - components.AddRange(o.GetComponentsInChildren(type)); + components.AddRange(o.GetComponentsInChildren(GetComponentType(type))); return components; } + public static Type GetComponentType(Type type) + { + if (type.IsArray || type.IsPointer || type.IsByRef || type.IsMarshalByRef) return type.GetElementType(); + return type; + } + public static string GetHierarchyName(Component component) { - if (component == null) - return "null"; + return component == null ? "null" : GetHierarchyName(component.transform); + } - var t = component.transform; + public static string GetHierarchyName(GameObject go) + { + return go == null ? "null" : GetHierarchyName(go.transform); + } + + public static string GetHierarchyName(Transform t) + { StringBuilder sb = new StringBuilder(t.name); while (t.parent != null) { diff --git a/Packages/dev.derpynewbie.common/Runtime/NewbieInject.cs b/Packages/dev.derpynewbie.common/Runtime/NewbieInject.cs index 884a754..fdd6712 100644 --- a/Packages/dev.derpynewbie.common/Runtime/NewbieInject.cs +++ b/Packages/dev.derpynewbie.common/Runtime/NewbieInject.cs @@ -11,5 +11,36 @@ namespace DerpyNewbie.Common /// public sealed class NewbieInject : Attribute { + public readonly SearchScope Scope; + + public NewbieInject() + { + Scope = SearchScope.Scene; + } + + public NewbieInject(SearchScope scope) + { + Scope = scope; + } + } + + public enum SearchScope + { + /// + /// Searches from whole Scene + /// + Scene, + /// + /// Searches from attached GameObject + /// + Self, + /// + /// Searches from children of attached GameObject + /// + Children, + /// + /// Searches from parents of attached GameObject + /// + Parents, } } \ No newline at end of file