Files
2026-07-21 08:56:10 +03:00

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&lt;T,T&gt;)
/// 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;
}
}
}
}