using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using Unity.Pipeline.Threading;
using UnityEngine;
namespace Unity.Pipeline.CodeReload
{
///
/// Central registry for managing code reload method and component overrides.
/// Handles runtime method resolution, override registration, and fallback logic.
/// Thread-safe for runtime code reload operations.
///
///
/// Deliberately NOT [NoAutoStaticsCleanup]: RegisterReloadableMethod appends without deduplicating,
/// and RuntimePipelineDriver.Awake re-registers on every Play Mode entry, so opting out would
/// accumulate duplicates across FEPM sessions. Let AutoStaticsCleanup reset these statics instead.
///
public static class CodeReloadRegistry
{
///
/// Dispatcher used to marshal main-thread-required code-reload overrides to the main thread.
/// Injected by RuntimePipelineDriver (= 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 code reload source files from (Assets folder
/// + loaded package locations). Baked into RuntimePipelineBuildInfo 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; }
// Facts established at domain-reload / Play Mode entry that only matter once the user
// actually reloads (target count, receiver state, connected players). Collected here and
// printed as ONE console line on the first override registration ATTEMPT of the session —
// attempt, not success, so a reload against an empty registry still gets the summary next
// to its skip reasons. Statics reset with the domain, so each session collects afresh.
private static readonly List s_FirstReloadNotes = new();
private static bool s_FirstReloadNotesFlushed;
///
/// Record a startup fact for the "first reload this session" summary line. Once that line
/// has been printed, a late note (e.g. a player connecting mid-session) is logged directly.
///
/// Short clause, no trailing period, e.g. "3 reloadable method(s) registered".
public static void NoteForFirstReload(string note)
{
if (string.IsNullOrEmpty(note)) return;
lock (s_FirstReloadNotes)
{
if (s_FirstReloadNotesFlushed)
{
Debug.Log($"CodeReload: {note}.");
return;
}
s_FirstReloadNotes.Add(note);
}
}
private static void FlushFirstReloadNotes()
{
string line = null;
lock (s_FirstReloadNotes)
{
if (s_FirstReloadNotesFlushed) return;
s_FirstReloadNotesFlushed = true;
if (s_FirstReloadNotes.Count > 0)
line = "CodeReload: first reload this session — " + string.Join("; ", s_FirstReloadNotes) + ".";
s_FirstReloadNotes.Clear();
}
if (line != null)
Debug.Log(line);
}
// Thread-safe collections for runtime code reload switching
private static readonly ConcurrentDictionary m_MethodOverrides = new();
private static readonly ConcurrentDictionary> m_ReloadableMethods = new();
private static readonly ConcurrentDictionary m_LoadedCodeReloadTypes = new();
///
/// Raised after a method override is registered or removed (once per method — a
/// multi-method reload fires it several times back to back, so debounce reactions).
/// Lets UI that reflects reloadable code (e.g. a debug panel overlay) rebuild when a
/// compiled method's body is swapped.
///
/// INTERNAL for now — deliberately held back from the first public API surface; make it
/// public when the introspection surface ships (external consumers poll
/// GetStats().ActiveOverrideCount meanwhile). The raise sites stay: register, unregister
/// (revert), and ClearAllOverrides — the transitions a subscriber needs.
///
internal static event Action OverridesChanged;
private static void RaiseOverridesChanged()
{
try
{
OverridesChanged?.Invoke();
}
catch (Exception ex)
{
Debug.LogError($"CodeReload: OverridesChanged handler threw: {ex}");
}
}
///
/// Register a method as reloadable. Called during discovery phase.
///
/// The method marked with .
/// The attribute instance (supplies the override id and main-thread requirement).
public static void RegisterReloadableMethod(MethodInfo method, CodeReloadAttribute attribute)
{
var methodId = GetMethodId(method, attribute.Id);
if (!m_ReloadableMethods.ContainsKey(methodId))
{
m_ReloadableMethods[methodId] = new List();
}
m_ReloadableMethods[methodId].Add(method);
}
///
/// Register a code reload method override from compiled code reload assembly.
/// Returns true if the override was registered, false if it was skipped.
///
/// The override method marked with .
/// The attribute instance (identifies the target method).
/// The type the override method was compiled into.
/// True if the override was registered.
public static bool RegisterMethodOverride(MethodInfo overrideMethod, CodeReloadOverrideMethodAttribute attribute, Type sourceType)
{
return RegisterMethodOverride(overrideMethod, attribute, sourceType, out _);
}
///
/// Register a code reload method override from compiled code 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.
///
/// The override method marked with .
/// The attribute instance (identifies the target method).
/// The type the override method was compiled into.
/// A user-facing reason when registration is skipped, or null on success.
/// True if the override was registered.
public static bool RegisterMethodOverride(MethodInfo overrideMethod, CodeReloadOverrideMethodAttribute attribute, Type sourceType, out string skipReason)
{
FlushFirstReloadNotes();
skipReason = null;
var targetMethodId = attribute.TargetMethodId;
// Validate that target method exists and is reloadable
if (!m_ReloadableMethods.ContainsKey(targetMethodId))
{
Debug.LogWarning($"CodeReload: Target method '{targetMethodId}' not found or not marked [CodeReload]");
skipReason = $"target '{targetMethodId}' is not registered as a reloadable [CodeReload] method. " +
"Ensure the component is in the scene and in play mode (RuntimePipelineDriver auto-discovers it).";
return false;
}
var originalMethods = m_ReloadableMethods[targetMethodId];
if (!originalMethods.Any())
{
Debug.LogWarning($"CodeReload: 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($"CodeReload: 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, ...)'.";
return false;
}
var methodOverride = new MethodOverride
{
TargetMethodId = targetMethodId,
OverrideMethod = overrideMethod,
SourceType = sourceType,
RequireMainThread = GetMainThreadRequirement(originalMethod),
Description = attribute.Description
};
m_MethodOverrides[targetMethodId] = methodOverride;
RaiseOverridesChanged();
return true;
}
///
/// Register an IL2CPP-safe override that dispatches through
/// (a IlInterpreter interpreter invoke) instead of a loaded . The target
/// must already be registered reloadable (same precondition as ).
///
internal static bool RegisterInterpreterMethodOverride(
string targetMethodId, Func