using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using DependencyInjection.ScriptableObjects; using UnityEngine; namespace DependencyInjection { /// /// Central DI container — extended to support ScriptableObject anchors. /// [DefaultExecutionOrder(-1000)] // ensure this runs before most other scripts for timely injection public class Injector : Singleton { const BindingFlags k_bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; // Main registry: Type -> instance readonly Dictionary _registry = new(); // Wrapped Type (T) -> anchor SO instance found in project readonly Dictionary _anchorRegistry = new(); // anchor SO instance -> list of (consumer, field) bindings readonly Dictionary> _anchorWatchers = new(); private void Awake() { base.Awake(); FindAndRegisterAnchors(); var monoBehaviours = FindMonoBehaviours(); // Collect IDependencyProvider components and register what they provide var providers = monoBehaviours.OfType(); 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); } /// Register a typed instance by its explicit type. 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 }); } } /// Register a generic typed instance (infers the type from T). public void Register(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 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); } } /// /// 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. /// 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); } } /// /// Scans the project (including assets) for all instances of RuntimeAnchor and registers them. /// void FindAndRegisterAnchors() { // Ensure all ScriptableObject assets in Resources folders are loaded into memory // so that FindObjectsOfTypeAll can discover them. Resources.LoadAll(""); var allSOs = Resources.FindObjectsOfTypeAll(); 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}'."); } } } /// /// 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. /// void SetupAnchorWatchers(IEnumerable 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(); _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 — 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 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 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(); 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 GetProvidedTypes(IEnumerable providers) { var set = new HashSet(); 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(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 _lastCallback; /// /// Creates a delegate of type (e.g. Action<T,T>) /// that internally calls the provided with (prev, next) boxed. /// public static Delegate CreateDelegate(Type delegateType, Action 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).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; } } } }