using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Unity.Pipeline.Threading;
using UnityEngine;
namespace Unity.Pipeline.HotReload
{
///
/// Central registry for managing hot reload method and component overrides.
/// Handles runtime method resolution, override registration, and fallback logic.
/// Thread-safe for runtime hot reload operations.
///
public static class HotReloadRegistry
{
///
/// Dispatcher used to marshal main-thread-required hot-reload overrides to the main thread.
/// Injected by RuntimePipelineManager (= its server's dispatcher). Null = run on the
/// current thread (no marshaling).
///
public static Dispatcher Dispatcher { get; set; }
///
/// Absolute roots a running Player is allowed to hot reload source files from (Assets folder
/// + loaded package locations). Baked into RuntimePipelineManager at build time (a running
/// Player cannot resolve the project layout) and published here when the runtime server
/// starts, so reload commands can validate incoming file paths. Null until published.
///
public static IReadOnlyList AllowedReloadRoots { get; set; }
// Thread-safe collections for runtime hot reload switching
private static readonly ConcurrentDictionary m_MethodOverrides = new();
private static readonly ConcurrentDictionary> m_ReloadableMethods = new();
private static readonly ConcurrentDictionary m_LoadedHotReloadTypes = new();
///
/// Register a method as hot reloadable. Called during discovery phase.
///
public static void RegisterReloadableMethod(MethodInfo method, HotReloadWithOverridesAttribute attribute)
{
var methodId = GetMethodId(method, attribute.Id);
if (!m_ReloadableMethods.ContainsKey(methodId))
{
m_ReloadableMethods[methodId] = new List();
}
m_ReloadableMethods[methodId].Add(method);
}
///
/// Register a hot reload method override from compiled hot reload assembly.
/// Returns true if the override was registered, false if it was skipped.
///
public static bool RegisterMethodOverride(MethodInfo overrideMethod, HotReloadOverrideMethodAttribute attribute, Type sourceType)
{
return RegisterMethodOverride(overrideMethod, attribute, sourceType, out _);
}
///
/// Register a hot reload method override from compiled hot reload assembly.
/// Returns true if the override was registered, false if it was skipped (target not
/// reloadable or signature mismatch). The out parameter carries a user-facing reason
/// when registration is skipped.
///
public static bool RegisterMethodOverride(MethodInfo overrideMethod, HotReloadOverrideMethodAttribute attribute, Type sourceType, out string skipReason)
{
skipReason = null;
var targetMethodId = attribute.TargetMethodId;
// Validate that target method exists and is reloadable
if (!m_ReloadableMethods.ContainsKey(targetMethodId))
{
Debug.LogWarning($"HotReload: Target method '{targetMethodId}' not found or not marked [HotReloadWithOverrides]");
skipReason = $"target '{targetMethodId}' is not registered as [HotReloadWithOverrides]. " +
"Ensure the component is in the scene and in play mode, and that it calls " +
"HotReloadRegistry.RegisterReloadableType(...) in Awake.";
return false;
}
var originalMethods = m_ReloadableMethods[targetMethodId];
if (!originalMethods.Any())
{
Debug.LogWarning($"HotReload: No original methods registered for '{targetMethodId}'");
skipReason = $"no original methods registered for '{targetMethodId}'.";
return false;
}
// Validate signature compatibility (basic check - instance parameter + matching return type)
var originalMethod = originalMethods.First();
if (!ValidateSignatureCompatibility(originalMethod, overrideMethod))
{
Debug.LogError($"HotReload: Signature mismatch for '{targetMethodId}'. Override method must have instance parameter as first argument.");
skipReason = $"signature mismatch for '{targetMethodId}'. The override must be " +
$"'public static {originalMethod.ReturnType.Name} {overrideMethod.Name}" +
$"({originalMethod.DeclaringType?.Name} instance, ...)'. " +
"A common cause is the override file redeclaring the target type.";
return false;
}
var methodOverride = new MethodOverride
{
TargetMethodId = targetMethodId,
OverrideMethod = overrideMethod,
SourceType = sourceType,
RequireMainThread = GetMainThreadRequirement(originalMethod),
Description = attribute.Description
};
m_MethodOverrides[targetMethodId] = methodOverride;
return true;
}
///
/// Attempt to invoke hot reload override if available, otherwise invoke original method.
/// Returns true if hot reload override was invoked, false if original method should be called.
///
public static bool TryInvokeHotReload(string methodId, T instance, object[] parameters = null)
{
return TryInvokeHotReload(methodId, (object)instance, parameters);
}
///
/// Non-generic dispatch entry point. This is the method woven into [HotReload]
/// methods at compile time (a non-generic signature keeps the injected IL simple).
/// Returns true if a hot reload override was invoked, false if the original body should run.
///
public static bool TryInvokeHotReload(string methodId, object instance, object[] parameters = null)
{
if (!m_MethodOverrides.TryGetValue(methodId, out var methodOverride))
{
return false; // No hot reload override available
}
try
{
// Prepare parameters with instance as first parameter
var invokeParams = new object[parameters?.Length + 1 ?? 1];
invokeParams[0] = instance;
if (parameters != null && parameters.Length > 0)
{
Array.Copy(parameters, 0, invokeParams, 1, parameters.Length);
}
// Invoke on appropriate thread. The dispatcher is injected by the runtime manager
// (HotReloadRegistry is reached from hot-reloaded runtime code, not a command).
// If no dispatcher was injected, run on the current thread.
if (methodOverride.RequireMainThread && Dispatcher != null && !Dispatcher.IsMainThread())
{
Dispatcher.Invoke(() =>
{
methodOverride.OverrideMethod.Invoke(null, invokeParams);
});
}
else
{
methodOverride.OverrideMethod.Invoke(null, invokeParams);
}
return true;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error invoking override for '{methodId}': {ex.Message}");
Debug.LogError($"HotReload: Stack trace: {ex.StackTrace}");
return false; // Fall back to original method
}
}
///
/// Register all methods in a type that have the HotReloadWithOverrides attribute.
/// Uses reflection to discover and register methods marked with [HotReloadWithOverrides].
///
public static void RegisterReloadableType(System.Type type)
{
if (type == null)
{
Debug.LogWarning("HotReload: Cannot register null type");
return;
}
int registeredCount = 0;
// Get all instance methods (public and non-public)
var instanceMethods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var method in instanceMethods)
{
var hotReloadAttr = method.GetCustomAttribute();
if (hotReloadAttr != null)
{
RegisterReloadableMethod(method, hotReloadAttr);
registeredCount++;
}
}
// Get all static methods (public and non-public)
var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
foreach (var method in staticMethods)
{
var hotReloadAttr = method.GetCustomAttribute();
if (hotReloadAttr != null)
{
RegisterReloadableMethod(method, hotReloadAttr);
registeredCount++;
}
}
if (registeredCount > 0)
{
Debug.Log($"HotReload: Registered {registeredCount} reloadable methods from type {type.Name}");
}
else
{
Debug.Log($"HotReload: No methods with [HotReloadWithOverrides] attribute found in type {type.Name}");
}
}
///
/// Register a type from loaded hot reload assembly for discovery scanning.
///
public static void RegisterHotReloadType(Type type, string assemblyId)
{
var typeKey = $"{assemblyId}:{type.FullName}";
m_LoadedHotReloadTypes[typeKey] = type;
}
///
/// Clear all hot reload overrides. Used by cleanup_hotreload command.
///
public static void ClearAllOverrides()
{
var overrideCount = m_MethodOverrides.Count;
var typeCount = m_LoadedHotReloadTypes.Count;
m_MethodOverrides.Clear();
m_LoadedHotReloadTypes.Clear();
}
///
/// Clear all registry state including reloadable methods.
/// FOR TESTING ONLY - this should not be called in production code.
///
public static void ClearAllForTesting()
{
var overrideCount = m_MethodOverrides.Count;
var typeCount = m_LoadedHotReloadTypes.Count;
var reloadableCount = m_ReloadableMethods.Sum(kvp => kvp.Value.Count);
m_MethodOverrides.Clear();
m_LoadedHotReloadTypes.Clear();
m_ReloadableMethods.Clear();
}
///
/// Get statistics about current hot reload state.
///
public static HotReloadStats GetStats()
{
return new HotReloadStats
{
ReloadableMethodCount = m_ReloadableMethods.Sum(kvp => kvp.Value.Count),
ActiveOverrideCount = m_MethodOverrides.Count,
LoadedTypeCount = m_LoadedHotReloadTypes.Count,
ReloadableMethodIds = m_ReloadableMethods.Keys.ToList(),
ActiveOverrideIds = m_MethodOverrides.Keys.ToList()
};
}
///
/// Generate method ID from MethodInfo and optional custom ID.
/// Format: TypeName.MethodName or custom ID if provided.
///
private static string GetMethodId(MethodInfo method, string customId = null)
{
if (!string.IsNullOrEmpty(customId))
{
return customId;
}
return $"{method.DeclaringType?.Name}.{method.Name}";
}
///
/// Validate that hot reload method signature is compatible with original method.
/// Hot reload method must have instance parameter as first argument.
///
private static bool ValidateSignatureCompatibility(MethodInfo originalMethod, MethodInfo overrideMethod)
{
var originalParams = originalMethod.GetParameters();
var overrideParams = overrideMethod.GetParameters();
// Hot reload method must have at least one parameter (the instance)
if (overrideParams.Length == 0)
{
return false;
}
// First parameter must be compatible with declaring type of original method
var firstParam = overrideParams[0];
if (!originalMethod.DeclaringType.IsAssignableFrom(firstParam.ParameterType))
{
return false;
}
// Remaining parameters must match original method parameters
if (overrideParams.Length - 1 != originalParams.Length)
{
return false;
}
for (int i = 0; i < originalParams.Length; i++)
{
if (overrideParams[i + 1].ParameterType != originalParams[i].ParameterType)
{
return false;
}
}
// Return types must match
return originalMethod.ReturnType == overrideMethod.ReturnType;
}
///
/// Determine if original method requires main thread execution.
///
private static bool GetMainThreadRequirement(MethodInfo originalMethod)
{
// Check for HotReloadWithOverridesAttribute main thread requirement
var hotReloadAttr = originalMethod.GetCustomAttribute();
if (hotReloadAttr != null)
{
return hotReloadAttr.RequireMainThread;
}
// Default to main thread requirement for safety
return true;
}
///
/// Information about a registered method override.
///
private class MethodOverride
{
public string TargetMethodId { get; set; }
public MethodInfo OverrideMethod { get; set; }
public Type SourceType { get; set; }
public bool RequireMainThread { get; set; }
public string Description { get; set; }
}
}
///
/// Statistics about current hot reload registry state.
///
public class HotReloadStats
{
public int ReloadableMethodCount { get; set; }
public int ActiveOverrideCount { get; set; }
public int LoadedTypeCount { get; set; }
public List ReloadableMethodIds { get; set; } = new();
public List ActiveOverrideIds { get; set; } = new();
}
}