411 lines
17 KiB
C#
411 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
using DependencyInjection.ScriptableObjects;
|
|
using UnityEngine;
|
|
|
|
namespace DependencyInjection
|
|
{
|
|
/// <summary>
|
|
/// Central DI container — extended to support ScriptableObject anchors.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(-1000)] // ensure this runs before most other scripts for timely injection
|
|
public class Injector : Singleton<Injector>
|
|
{
|
|
const BindingFlags k_bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
|
|
// Main registry: Type -> instance
|
|
readonly Dictionary<Type, object> _registry = new();
|
|
|
|
// Wrapped Type (T) -> anchor SO instance found in project
|
|
readonly Dictionary<Type, object> _anchorRegistry = new();
|
|
|
|
// anchor SO instance -> list of (consumer, field) bindings
|
|
readonly Dictionary<object, List<AnchorBinding>> _anchorWatchers = new();
|
|
|
|
private void Awake()
|
|
{
|
|
base.Awake();
|
|
|
|
FindAndRegisterAnchors();
|
|
var monoBehaviours = FindMonoBehaviours();
|
|
|
|
// Collect IDependencyProvider components and register what they provide
|
|
var providers = monoBehaviours.OfType<IDependencyProvider>();
|
|
foreach (var provider in providers)
|
|
{
|
|
RegisterProvider(provider);
|
|
}
|
|
|
|
// Inject all injectable MonoBehaviours
|
|
var injectables = monoBehaviours.Where(IsInjectable);
|
|
foreach (var injectable in injectables)
|
|
{
|
|
Inject(injectable);
|
|
}
|
|
|
|
// Set up reactive re-injection for any injected anchor fields
|
|
SetupAnchorWatchers(injectables);
|
|
}
|
|
|
|
/// <summary>Register a typed instance by its explicit type.</summary>
|
|
public void RegisterInstance(Type type, object instance)
|
|
{
|
|
if (_registry.ContainsKey(type))
|
|
{
|
|
Debug.LogWarning($"[Injector] Overwriting existing registration for '{type.Name}'.");
|
|
}
|
|
_registry[type] = instance;
|
|
|
|
// Automatically assign the instance to its project anchor if one exists
|
|
if (_anchorRegistry.TryGetValue(type, out var anchorInstance))
|
|
{
|
|
var anchorInterface = GetRuntimeAnchorInterface(anchorInstance.GetType());
|
|
var registerMethod = anchorInterface.GetMethod("Register");
|
|
registerMethod.Invoke(anchorInstance, new[] { instance });
|
|
}
|
|
}
|
|
|
|
/// <summary>Register a generic typed instance (infers the type from T).</summary>
|
|
public void Register<T>(T instance)
|
|
{
|
|
RegisterInstance(typeof(T), instance);
|
|
}
|
|
|
|
void Inject(object instance)
|
|
{
|
|
var type = instance.GetType();
|
|
|
|
// Fields
|
|
foreach (var field in type.GetFields(k_bindingFlags)
|
|
.Where(f => Attribute.IsDefined(f, typeof(InjectAttribute))))
|
|
{
|
|
|
|
if (field.GetValue(instance) != null)
|
|
{
|
|
Debug.LogWarning($"[Injector] Field '{field.Name}' of '{type.Name}' is already set.");
|
|
continue;
|
|
}
|
|
|
|
var resolved = Resolve(field.FieldType);
|
|
if (resolved == null)
|
|
{
|
|
throw new Exception($"[Injector] Cannot resolve field '{field.Name}' ({field.FieldType.Name}) in '{type.Name}'.");
|
|
}
|
|
field.SetValue(instance, resolved);
|
|
}
|
|
|
|
// Reactive Fields [RuntimeInject] - injects the Value of a RuntimeAnchor<T>
|
|
foreach (var field in type.GetFields(k_bindingFlags)
|
|
.Where(f => Attribute.IsDefined(f, typeof(RuntimeInjectAttribute))))
|
|
{
|
|
if (_anchorRegistry.TryGetValue(field.FieldType, out var anchorInstance))
|
|
{
|
|
var anchorInterface = GetRuntimeAnchorInterface(anchorInstance.GetType());
|
|
var valueProp = anchorInterface.GetProperty("Value");
|
|
var val = valueProp.GetValue(anchorInstance);
|
|
field.SetValue(instance, val);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"[Injector] No RuntimeAnchor found in project for type '{field.FieldType.Name}' requested by '{type.Name}'.");
|
|
}
|
|
}
|
|
|
|
// Methods
|
|
foreach (var method in type.GetMethods(k_bindingFlags)
|
|
.Where(m => Attribute.IsDefined(m, typeof(InjectAttribute))))
|
|
{
|
|
|
|
var paramTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
|
|
var resolved = paramTypes.Select(Resolve).ToArray();
|
|
|
|
if (resolved.Any(r => r == null))
|
|
{
|
|
throw new Exception($"[Injector] Cannot resolve all parameters for method '{method.Name}' in '{type.Name}'.");
|
|
}
|
|
method.Invoke(instance, resolved);
|
|
}
|
|
|
|
// Properties
|
|
foreach (var prop in type.GetProperties(k_bindingFlags)
|
|
.Where(p => Attribute.IsDefined(p, typeof(InjectAttribute))))
|
|
{
|
|
|
|
var resolved = Resolve(prop.PropertyType);
|
|
if (resolved == null)
|
|
{
|
|
throw new Exception($"[Injector] Cannot resolve property '{prop.Name}' ({prop.PropertyType.Name}) in '{type.Name}'.");
|
|
}
|
|
prop.SetValue(instance, resolved);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scan the provider for any methods marked with [Provide] and register their return values by type.
|
|
/// This allows providers to offer multiple dependencies without needing a separate provider class for each.
|
|
/// Note: provider methods must have no parameters and return a non-null value.
|
|
/// </summary>
|
|
void RegisterProvider(IDependencyProvider provider)
|
|
{
|
|
foreach (var method in provider.GetType().GetMethods(k_bindingFlags))
|
|
{
|
|
if (!Attribute.IsDefined(method, typeof(ProvideAttribute))) continue;
|
|
|
|
var returnType = method.ReturnType;
|
|
var provided = method.Invoke(provider, null);
|
|
|
|
if (provided == null)
|
|
{
|
|
throw new Exception($"[Injector] Provider method '{method.Name}' in '{provider.GetType().Name}' returned null.");
|
|
}
|
|
RegisterInstance(returnType, provided);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans the project (including assets) for all instances of RuntimeAnchor<T> and registers them.
|
|
/// </summary>
|
|
void FindAndRegisterAnchors()
|
|
{
|
|
// Ensure all ScriptableObject assets in Resources folders are loaded into memory
|
|
// so that FindObjectsOfTypeAll can discover them.
|
|
Resources.LoadAll<ScriptableObject>("");
|
|
|
|
var allSOs = Resources.FindObjectsOfTypeAll<ScriptableObject>();
|
|
foreach (var so in allSOs)
|
|
{
|
|
var anchorInterface = GetRuntimeAnchorInterface(so.GetType());
|
|
if (anchorInterface == null) continue;
|
|
|
|
var wrappedType = anchorInterface.GetGenericArguments()[0];
|
|
if (!_anchorRegistry.ContainsKey(wrappedType))
|
|
{
|
|
_anchorRegistry[wrappedType] = so;
|
|
Debug.Log($"[Injector] Registered project anchor '{so.name}' for type '{wrappedType.Name}'.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// After initial injection, scan all injected anchor fields.
|
|
/// Whenever an anchor's value changes, re-inject the new value into every consumer
|
|
/// that subscribed through their [Inject] field.
|
|
/// </summary>
|
|
void SetupAnchorWatchers(IEnumerable<MonoBehaviour> injectables)
|
|
{
|
|
foreach (var consumer in injectables)
|
|
{
|
|
foreach (var field in consumer.GetType().GetFields(k_bindingFlags))
|
|
{
|
|
if (Attribute.IsDefined(field, typeof(RuntimeInjectAttribute)))
|
|
{
|
|
if (_anchorRegistry.TryGetValue(field.FieldType, out var anchorInstance))
|
|
{
|
|
var anchorInterface = GetRuntimeAnchorInterface(anchorInstance.GetType());
|
|
RegisterAnchorBinding(anchorInstance, consumer, field, anchorInterface);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RegisterAnchorBinding(object anchorInstance, MonoBehaviour consumer, FieldInfo field, Type anchorInterface)
|
|
{
|
|
if (!_anchorWatchers.TryGetValue(anchorInstance, out var bindings))
|
|
{
|
|
bindings = new List<AnchorBinding>();
|
|
_anchorWatchers[anchorInstance] = bindings;
|
|
|
|
// Subscribe to the anchor's OnValueChanged event via reflection
|
|
var eventInfo = anchorInterface.GetEvent("OnValueChanged");
|
|
if (eventInfo != null)
|
|
{
|
|
// Build a handler that re-dispatches to all bindings for this anchor
|
|
var handler = BuildAnchorChangedDelegate(anchorInstance, eventInfo);
|
|
if (handler != null)
|
|
{
|
|
eventInfo.AddEventHandler(anchorInstance, handler);
|
|
}
|
|
}
|
|
}
|
|
bindings.Add(new AnchorBinding(consumer, field));
|
|
}
|
|
|
|
Delegate BuildAnchorChangedDelegate(object anchorInstance, EventInfo eventInfo)
|
|
{
|
|
// The event is Action<T,T> — we need to create a matching delegate that
|
|
// calls OnAnchorValueChanged(anchorInstance, prev, next) for all bindings.
|
|
var handlerType = eventInfo.EventHandlerType;
|
|
var invokeMethod = handlerType.GetMethod("Invoke");
|
|
if (invokeMethod == null) return null;
|
|
|
|
// Use a closure-capturing lambda via dynamic delegate creation
|
|
Action<object, object> callback = (prev, next) => OnAnchorValueChanged(anchorInstance, next);
|
|
|
|
// Create a wrapper delegate of the right type via DynamicMethod or Delegate.CreateDelegate
|
|
// For simplicity we use a MethodInfo approach with a helper
|
|
return AnchorDelegateHelper.CreateDelegate(handlerType, callback);
|
|
}
|
|
|
|
void OnAnchorValueChanged(object anchorInstance, object newValue)
|
|
{
|
|
if (!_anchorWatchers.TryGetValue(anchorInstance, out var bindings)) return;
|
|
|
|
Debug.Log($"[Injector] Anchor '{anchorInstance.GetType().Name}' changed — refreshing {bindings.Count} binding(s).");
|
|
|
|
foreach (var binding in bindings)
|
|
{
|
|
if (binding.Consumer == null) continue; // Destroyed MonoBehaviour
|
|
|
|
binding.Field.SetValue(binding.Consumer, newValue);
|
|
}
|
|
}
|
|
|
|
static Type GetRuntimeAnchorInterface(Type fieldType)
|
|
{
|
|
// Walk the type hierarchy to find RuntimeAnchor<T>
|
|
var t = fieldType;
|
|
while (t != null && t != typeof(object))
|
|
{
|
|
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(RuntimeAnchor<>))
|
|
{
|
|
return t;
|
|
}
|
|
t = t.BaseType;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
object Resolve(Type type)
|
|
{
|
|
_registry.TryGetValue(type, out var instance);
|
|
return instance;
|
|
}
|
|
|
|
public void ValidateDependencies()
|
|
{
|
|
var monoBehaviours = FindMonoBehaviours();
|
|
var providers = monoBehaviours.OfType<IDependencyProvider>();
|
|
var provided = GetProvidedTypes(providers);
|
|
|
|
var missing = monoBehaviours
|
|
.SelectMany(mb => mb.GetType().GetFields(k_bindingFlags), (mb, f) => (mb, f))
|
|
.Where(t => Attribute.IsDefined(t.f, typeof(InjectAttribute)))
|
|
.Where(t => !provided.Contains(t.f.FieldType) && t.f.GetValue(t.mb) == null)
|
|
.Select(t => $" • {t.mb.GetType().Name} missing {t.f.FieldType.Name} on '{t.mb.gameObject.name}'")
|
|
.ToList();
|
|
|
|
if (missing.Count == 0)
|
|
{
|
|
Debug.Log("[Injector] ✓ All dependencies valid.");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"[Injector] {missing.Count} unresolved dependencies:\n{string.Join("\n", missing)}");
|
|
}
|
|
}
|
|
|
|
public void ClearDependencies()
|
|
{
|
|
foreach (var mb in FindMonoBehaviours())
|
|
{
|
|
foreach (var field in mb.GetType().GetFields(k_bindingFlags)
|
|
.Where(f => Attribute.IsDefined(f, typeof(InjectAttribute))))
|
|
{
|
|
field.SetValue(mb, null);
|
|
}
|
|
}
|
|
_anchorWatchers.Clear();
|
|
Debug.Log("[Injector] All injectable fields cleared.");
|
|
}
|
|
|
|
HashSet<Type> GetProvidedTypes(IEnumerable<IDependencyProvider> providers)
|
|
{
|
|
var set = new HashSet<Type>();
|
|
foreach (var p in providers)
|
|
{
|
|
foreach (var m in p.GetType().GetMethods(k_bindingFlags))
|
|
{
|
|
if (Attribute.IsDefined(m, typeof(ProvideAttribute))) set.Add(m.ReturnType);
|
|
}
|
|
}
|
|
return set;
|
|
}
|
|
|
|
static MonoBehaviour[] FindMonoBehaviours() =>
|
|
FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.InstanceID);
|
|
|
|
static bool IsInjectable(MonoBehaviour obj) =>
|
|
obj.GetType().GetMembers(k_bindingFlags)
|
|
.Any(m => Attribute.IsDefined(m, typeof(InjectAttribute)) ||
|
|
Attribute.IsDefined(m, typeof(RuntimeInjectAttribute)));
|
|
|
|
readonly struct AnchorBinding
|
|
{
|
|
public readonly MonoBehaviour Consumer;
|
|
public readonly FieldInfo Field;
|
|
public AnchorBinding(MonoBehaviour consumer, FieldInfo field)
|
|
{
|
|
Consumer = consumer;
|
|
Field = field;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class AnchorDelegateHelper
|
|
{
|
|
|
|
// Temporary storage for the callback during delegate creation (single-threaded Unity)
|
|
static Action<object, object> _lastCallback;
|
|
|
|
/// <summary>
|
|
/// Creates a delegate of type <paramref name="delegateType"/> (e.g. Action<T,T>)
|
|
/// that internally calls the provided <paramref name="callback"/> with (prev, next) boxed.
|
|
/// </summary>
|
|
public static Delegate CreateDelegate(Type delegateType, Action<object, object> callback)
|
|
{
|
|
try
|
|
{
|
|
// Retrieve Invoke signature to know parameter types
|
|
var invokeMethod = delegateType.GetMethod("Invoke");
|
|
if (invokeMethod == null) return null;
|
|
|
|
var paramTypes = invokeMethod.GetParameters().Select(p => p.ParameterType).ToArray();
|
|
if (paramTypes.Length != 2) return null;
|
|
|
|
// Build a DynamicMethod that boxes the args and calls the callback
|
|
var dm = new DynamicMethod(
|
|
"AnchorHandler",
|
|
typeof(void),
|
|
paramTypes,
|
|
typeof(AnchorDelegateHelper).Module,
|
|
skipVisibility: true
|
|
);
|
|
|
|
var il = dm.GetILGenerator();
|
|
// Load callback onto the stack, then two args (boxed), then call Invoke
|
|
il.Emit(OpCodes.Ldsfld,
|
|
typeof(AnchorDelegateHelper).GetField(nameof(_lastCallback),
|
|
BindingFlags.Static | BindingFlags.NonPublic)!);
|
|
il.Emit(OpCodes.Ldarg_0);
|
|
if (paramTypes[0].IsValueType) il.Emit(OpCodes.Box, paramTypes[0]);
|
|
il.Emit(OpCodes.Ldarg_1);
|
|
if (paramTypes[1].IsValueType) il.Emit(OpCodes.Box, paramTypes[1]);
|
|
il.Emit(OpCodes.Callvirt,
|
|
typeof(Action<object, object>).GetMethod("Invoke")!);
|
|
il.Emit(OpCodes.Ret);
|
|
|
|
_lastCallback = callback; // store for the closure
|
|
return dm.CreateDelegate(delegateType);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning($"[AnchorDelegateHelper] Could not create delegate: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
} |