Skip to content

Commit

Permalink
Add scoping/array support to NewbieInject
Browse files Browse the repository at this point in the history
  • Loading branch information
DerpyNewbie committed Nov 3, 2024
1 parent 07047ee commit 9e6101b
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 26 deletions.
18 changes: 17 additions & 1 deletion Packages/dev.derpynewbie.common/Editor/NewbieInjectMenuItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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!"
);
}
Expand All @@ -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;
}
}
}
}
205 changes: 180 additions & 25 deletions Packages/dev.derpynewbie.common/Editor/NewbieInjectProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type, Component>();
var foundComponentsDict = new Dictionary<FieldInfo, List<Component>>();
var perObjectComponentsDict = new Dictionary<FieldInfo, Dictionary<GameObject, List<Component>>>();

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<NewbieInject>();

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<Component> injectingComponents;
switch (injectOption.Scope)
{
default:
{
injectingComponents = new List<Component>();
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<GameObject, List<Component>>();
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<GameObject, List<Component>>();
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<GameObject, List<Component>>();
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();
Expand All @@ -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<NewbieInject>().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<FieldInfo> GetInjectFields()
{
var result = new List<FieldInfo>();
Expand All @@ -93,16 +236,28 @@ public static List<Component> GetComponentsInScene(Scene scene, Type type)
{
var components = new List<Component>();
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)
{
Expand Down
31 changes: 31 additions & 0 deletions Packages/dev.derpynewbie.common/Runtime/NewbieInject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,36 @@ namespace DerpyNewbie.Common
/// </remarks>
public sealed class NewbieInject : Attribute
{
public readonly SearchScope Scope;

public NewbieInject()
{
Scope = SearchScope.Scene;
}

public NewbieInject(SearchScope scope)
{
Scope = scope;
}
}

public enum SearchScope
{
/// <summary>
/// Searches from whole Scene
/// </summary>
Scene,
/// <summary>
/// Searches from attached GameObject
/// </summary>
Self,
/// <summary>
/// Searches from children of attached GameObject
/// </summary>
Children,
/// <summary>
/// Searches from parents of attached GameObject
/// </summary>
Parents,
}
}

0 comments on commit 9e6101b

Please sign in to comment.