Add Unity Pipeline package support

This commit is contained in:
ud18010
2026-07-31 17:34:04 +08:00
parent d1a526094e
commit 786b7ffff9
590 changed files with 51995 additions and 2 deletions
@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Unity.Pipeline.Compilation;
using UnityEngine;
namespace Unity.Pipeline.HotReload
{
/// <summary>
/// Validates that [HotReload] method bodies only access PUBLIC instance members of the
/// declaring type. In-place overrides are compiled into a separate assembly, so they can only
/// reach public members of the original type; private/internal/protected access would fail to
/// compile. This check runs up front (via a Roslyn semantic model) to produce a clear message
/// instead of a raw compiler error.
/// </summary>
public static class AccessibilityValidator
{
public static AccessibilityValidationResult ValidatePublicAccess(
string sourceCode,
Dictionary<string, string> methodBodies,
string originalTypeName)
{
var result = new AccessibilityValidationResult
{
IsValid = true,
Violations = new List<AccessibilityViolation>()
};
try
{
var tree = CSharpSyntaxTree.ParseText(sourceCode);
var root = tree.GetRoot();
var compilation = CSharpCompilation.Create(
"HotReloadInPlaceValidation",
new[] { tree },
RoslynCompilationService.GetMetadataReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var model = compilation.GetSemanticModel(tree);
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault(c => c.Identifier.ValueText == originalTypeName);
if (classDecl == null)
{
result.IsValid = false;
result.ValidationError = $"Could not find class '{originalTypeName}' in source.";
return result;
}
var classSymbol = model.GetDeclaredSymbol(classDecl);
foreach (var method in classDecl.Members.OfType<MethodDeclarationSyntax>())
{
var methodName = method.Identifier.ValueText;
if (!methodBodies.ContainsKey(methodName) || method.Body == null)
continue;
var seen = new HashSet<string>();
foreach (var name in method.Body.DescendantNodes().OfType<SimpleNameSyntax>())
{
if (IsMemberName(name))
continue;
var symbol = model.GetSymbolInfo(name).Symbol;
if (!IsInstanceMemberOf(symbol, classSymbol))
continue;
if (symbol.DeclaredAccessibility != Accessibility.Public && seen.Add(symbol.Name))
{
result.Violations.Add(new AccessibilityViolation
{
MemberName = symbol.Name,
MethodName = methodName,
AccessLevel = symbol.DeclaredAccessibility,
ViolationType = AccessibilityViolationType.PrivateAccess,
ErrorMessage = $"Cannot access non-public member '{symbol.Name}' " +
$"({symbol.DeclaredAccessibility}) in [HotReload] method '{methodName}'",
Suggestion = $"Make '{symbol.Name}' public in {originalTypeName}, or use a " +
"public property/method. In-place overrides compile in a separate assembly " +
"and can only access public members."
});
}
}
}
result.IsValid = result.Violations.Count == 0;
return result;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Accessibility validation error: {ex.Message}");
return new AccessibilityValidationResult
{
IsValid = false,
ValidationError = ex.Message,
Violations = new List<AccessibilityViolation>()
};
}
}
/// <summary>True if the name is the right-hand member name of an access (foo.Bar -> Bar).</summary>
private static bool IsMemberName(SimpleNameSyntax node)
{
if (node.Parent is MemberAccessExpressionSyntax ma && ma.Name == node)
return true;
if (node.Parent is QualifiedNameSyntax)
return true;
if (node.Parent is MemberBindingExpressionSyntax)
return true;
return false;
}
/// <summary>True if the symbol is an instance field/property/method/event of the type or a base.</summary>
private static bool IsInstanceMemberOf(ISymbol symbol, INamedTypeSymbol type)
{
if (symbol == null || symbol.IsStatic)
return false;
switch (symbol.Kind)
{
case SymbolKind.Field:
case SymbolKind.Property:
case SymbolKind.Method:
case SymbolKind.Event:
break;
default:
return false;
}
for (var t = type; t != null; t = t.BaseType)
{
if (SymbolEqualityComparer.Default.Equals(t, symbol.ContainingType))
return true;
}
return false;
}
}
/// <summary>
/// Result of accessibility validation for hot reload methods.
/// </summary>
public class AccessibilityValidationResult
{
public bool IsValid { get; set; }
public List<AccessibilityViolation> Violations { get; set; } = new List<AccessibilityViolation>();
public string ValidationError { get; set; }
public string GetFormattedErrorMessage()
{
if (!string.IsNullOrEmpty(ValidationError))
return $"HotReload Validation Error: {ValidationError}";
if (Violations.Count == 0)
return "All member access is valid for hot reload.";
var errorMessage = $"HotReload Validation Failed: {Violations.Count} accessibility violation(s) found\n\n";
for (int i = 0; i < Violations.Count; i++)
{
var violation = Violations[i];
errorMessage += $"{i + 1}. Method '{violation.MethodName}': {violation.ErrorMessage}\n";
errorMessage += $" → Suggestion: {violation.Suggestion}\n";
if (i < Violations.Count - 1)
errorMessage += "\n";
}
errorMessage += "\nFix these accessibility issues and run reload_file again.";
return errorMessage;
}
}
/// <summary>
/// Information about a specific accessibility violation in hot reload code.
/// </summary>
public class AccessibilityViolation
{
public string MemberName { get; set; }
public string MethodName { get; set; }
public Accessibility AccessLevel { get; set; }
public AccessibilityViolationType ViolationType { get; set; }
public string ErrorMessage { get; set; }
public string Suggestion { get; set; }
}
/// <summary>
/// Types of accessibility violations that can occur in hot reload methods.
/// </summary>
public enum AccessibilityViolationType
{
PrivateAccess,
InternalAccess,
ProtectedAccess,
ParseError
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1bdaafa6e8d172648a57310104db179a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,166 @@
using System;
namespace Unity.Pipeline.HotReload
{
/// <summary>
/// Marks a method as hot reloadable. The method can be overridden at runtime
/// through the hot reload system without triggering Unity domain reload.
///
/// Belongs to the helper (separate override file) workflow, together with
/// <see cref="HotReloadOverrideMethodAttribute"/>. For the in-place workflow use
/// <see cref="HotReloadAttribute"/> instead.
///
/// Pattern A: Attribute-Based Method Override
/// - Individual methods can be surgically replaced
/// - Hot reload methods must have same signature + instance parameter
/// - Access limited to public fields/properties for simplicity
/// - Best for: Quick gameplay tweaks, formula adjustments
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class HotReloadWithOverridesAttribute : Attribute
{
/// <summary>
/// Optional identifier for this hot reloadable method.
/// If not specified, uses TypeName.MethodName format.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Whether this method requires main thread execution.
/// Defaults to true for Unity API safety.
/// </summary>
public bool RequireMainThread { get; set; } = true;
}
/// <summary>
/// Marks a method for the in-place hot reload workflow: the method body is edited directly
/// in the original source file and applied via the <c>reload_file</c> command.
///
/// This attribute is exclusive to the in-place workflow and is independent of
/// <see cref="HotReloadWithOverridesAttribute"/> / <see cref="HotReloadOverrideMethodAttribute"/>, which
/// belong to the helper (separate override file) workflow.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class HotReloadAttribute : Attribute
{
/// <summary>
/// Optional identifier for this in-place hot reloadable method.
/// If not specified, uses TypeName.MethodName format.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Whether this method requires main thread execution.
/// Defaults to true for Unity API safety.
/// </summary>
public bool RequireMainThread { get; set; } = true;
}
/// <summary>
/// Marks a static method as a hot reload override for a specific method.
/// Used in hot reload files to define replacement logic for [HotReloadWithOverrides] methods.
///
/// Method signature must match original method plus instance parameter as first argument.
/// Example: void Update() becomes static void Update(TargetType instance)
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class HotReloadOverrideMethodAttribute : Attribute
{
/// <summary>
/// Target method identifier in format "TypeName.MethodName"
/// Must match a method marked with [HotReloadWithOverrides].
/// </summary>
public string TargetMethodId { get; }
/// <summary>
/// Optional description of what this hot reload does.
/// Useful for debugging and hot reload management.
/// </summary>
public string Description { get; set; }
public HotReloadOverrideMethodAttribute(string targetMethodId)
{
TargetMethodId = targetMethodId ?? throw new ArgumentNullException(nameof(targetMethodId));
}
}
/// <summary>
/// Marks a component class as hot reloadable via complete behavior replacement.
/// The component becomes a proxy that delegates to hot reload implementations.
///
/// Pattern C: Component Override
/// - Complete component behavior replacement via interfaces
/// - Maximum flexibility for major behavioral experiments
/// - Pattern C wins when multiple patterns apply to same component
/// - Best for: Major AI changes, experimental features
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class HotReloadableComponentAttribute : Attribute
{
/// <summary>
/// Optional identifier for this hot reloadable component.
/// If not specified, uses the class name.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Interface type that hot reload implementations must implement.
/// If not specified, generates interface automatically.
/// </summary>
public Type InterfaceType { get; set; }
}
/// <summary>
/// Marks a class as a hot reload implementation for a specific component.
/// Used in hot reload files to provide complete component behavior replacement.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class HotReloadComponentAttribute : Attribute
{
/// <summary>
/// Target component type to override.
/// Must be a type marked with [HotReloadableComponent].
/// </summary>
public Type TargetType { get; }
/// <summary>
/// Optional description of what this hot reload implementation does.
/// </summary>
public string Description { get; set; }
public HotReloadComponentAttribute(Type targetType)
{
TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType));
}
}
/// <summary>
/// Marks a static method as a hot reload target for Pattern B explicit wrapper system.
/// Used with HotReloadMethod&lt;T&gt; wrapper for type-safe delegate hot reload.
///
/// Pattern B: Explicit Wrapper
/// - HotReloadMethod&lt;T&gt; wrapper with type-safe delegate calls
/// - Built-in fallback logic, zero method signature changes
/// - Explicit opt-in with performance-optimized direct calls
/// - Best for: Performance-critical paths, type-safe hot reload
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class HotReloadTargetAttribute : Attribute
{
/// <summary>
/// Unique identifier that matches HotReloadMethod&lt;T&gt; wrapper registration.
/// This ID connects the wrapper to the hot reload implementation.
/// </summary>
public string TargetId { get; }
/// <summary>
/// Optional description of what this hot reload target does.
/// </summary>
public string Description { get; set; }
public HotReloadTargetAttribute(string targetId)
{
TargetId = targetId ?? throw new ArgumentNullException(nameof(targetId));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0bd43f6237214774ab4a178b811eb453
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,140 @@
using System;
using System.Reflection;
using UnityEngine;
namespace Unity.Pipeline.HotReload
{
/// <summary>
/// Helper utilities for integrating hot reload calls into methods marked with [HotReloadWithOverrides].
/// Provides convenience methods for checking and invoking hot reload overrides.
/// </summary>
public static class HotReloadHelper
{
/// <summary>
/// Execute method with hot reload override check.
/// If hot reload override exists, invokes it; otherwise invokes original method.
/// </summary>
/// <typeparam name="T">Type of the instance</typeparam>
/// <param name="instance">Instance to invoke method on</param>
/// <param name="methodName">Name of the method to invoke</param>
/// <param name="originalMethod">Original method implementation</param>
/// <param name="parameters">Method parameters</param>
/// <returns>Result of method invocation</returns>
public static object ExecuteWithHotReload<T>(T instance, string methodName, Func<object> originalMethod, params object[] parameters)
{
var methodId = GetMethodId<T>(methodName);
// Try hot reload first
if (HotReloadRegistry.TryInvokeHotReload(methodId, instance, parameters))
{
return null; // Hot reload methods currently don't return values (can be enhanced later)
}
// Fall back to original method
return originalMethod?.Invoke();
}
/// <summary>
/// Execute void method with hot reload override check.
/// Convenience overload for void methods.
/// </summary>
/// <typeparam name="T">Type of the instance</typeparam>
/// <param name="instance">Instance to invoke method on</param>
/// <param name="methodName">Name of the method to invoke</param>
/// <param name="originalMethod">Original method implementation</param>
/// <param name="parameters">Method parameters</param>
public static void ExecuteWithHotReload<T>(T instance, string methodName, Action originalMethod, params object[] parameters)
{
var methodId = GetMethodId<T>(methodName);
// Try hot reload first
if (HotReloadRegistry.TryInvokeHotReload(methodId, instance, parameters))
{
return; // Hot reload override was invoked
}
// Fall back to original method
originalMethod?.Invoke();
}
/// <summary>
/// Execute method with custom method ID and hot reload override check.
/// Use this when the method has a custom ID specified in [HotReloadWithOverrides(Id = "...")].
/// </summary>
/// <typeparam name="T">Type of the instance</typeparam>
/// <param name="instance">Instance to invoke method on</param>
/// <param name="customMethodId">Custom method ID from HotReloadWithOverrides attribute</param>
/// <param name="originalMethod">Original method implementation</param>
/// <param name="parameters">Method parameters</param>
/// <returns>Result of method invocation</returns>
public static object ExecuteWithHotReloadCustomId<T>(T instance, string customMethodId, Func<object> originalMethod, params object[] parameters)
{
// Try hot reload first
if (HotReloadRegistry.TryInvokeHotReload(customMethodId, instance, parameters))
{
return null; // Hot reload methods currently don't return values
}
// Fall back to original method
return originalMethod?.Invoke();
}
/// <summary>
/// Execute void method with custom method ID and hot reload override check.
/// Use this when the method has a custom ID specified in [HotReloadWithOverrides(Id = "...")].
/// </summary>
/// <typeparam name="T">Type of the instance</typeparam>
/// <param name="instance">Instance to invoke method on</param>
/// <param name="customMethodId">Custom method ID from HotReloadWithOverrides attribute</param>
/// <param name="originalMethod">Original method implementation</param>
/// <param name="parameters">Method parameters</param>
public static void ExecuteWithHotReloadCustomId<T>(T instance, string customMethodId, Action originalMethod, params object[] parameters)
{
// Try hot reload first
if (HotReloadRegistry.TryInvokeHotReload(customMethodId, instance, parameters))
{
return; // Hot reload override was invoked
}
// Fall back to original method
originalMethod?.Invoke();
}
/// <summary>
/// Check if a hot reload override is active for a specific method.
/// Useful for debugging or conditional logic based on hot reload state.
/// </summary>
/// <typeparam name="T">Type of the instance</typeparam>
/// <param name="methodName">Name of the method to check</param>
/// <returns>True if hot reload override is active</returns>
public static bool IsHotReloadActive<T>(string methodName)
{
var methodId = GetMethodId<T>(methodName);
var stats = HotReloadRegistry.GetStats();
return stats.ActiveOverrideIds.Contains(methodId);
}
/// <summary>
/// Check if a hot reload override is active for a specific custom method ID.
/// </summary>
/// <param name="customMethodId">Custom method ID to check</param>
/// <returns>True if hot reload override is active</returns>
public static bool IsHotReloadActive(string customMethodId)
{
var stats = HotReloadRegistry.GetStats();
return stats.ActiveOverrideIds.Contains(customMethodId);
}
/// <summary>
/// Generate standard method ID from type and method name.
/// Format: TypeName.MethodName
/// </summary>
/// <typeparam name="T">Type containing the method</typeparam>
/// <param name="methodName">Name of the method</param>
/// <returns>Standard method ID</returns>
private static string GetMethodId<T>(string methodName)
{
return $"{typeof(T).Name}.{methodName}";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2835bfdec4fc24cb67a208766efcff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,365 @@
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
{
/// <summary>
/// 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.
/// </summary>
public static class HotReloadRegistry
{
/// <summary>
/// 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).
/// </summary>
public static Dispatcher Dispatcher { get; set; }
/// <summary>
/// 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.
/// </summary>
public static IReadOnlyList<string> AllowedReloadRoots { get; set; }
// Thread-safe collections for runtime hot reload switching
private static readonly ConcurrentDictionary<string, MethodOverride> m_MethodOverrides = new();
private static readonly ConcurrentDictionary<string, List<MethodInfo>> m_ReloadableMethods = new();
private static readonly ConcurrentDictionary<string, Type> m_LoadedHotReloadTypes = new();
/// <summary>
/// Register a method as hot reloadable. Called during discovery phase.
/// </summary>
public static void RegisterReloadableMethod(MethodInfo method, HotReloadWithOverridesAttribute attribute)
{
var methodId = GetMethodId(method, attribute.Id);
if (!m_ReloadableMethods.ContainsKey(methodId))
{
m_ReloadableMethods[methodId] = new List<MethodInfo>();
}
m_ReloadableMethods[methodId].Add(method);
}
/// <summary>
/// Register a hot reload method override from compiled hot reload assembly.
/// Returns true if the override was registered, false if it was skipped.
/// </summary>
public static bool RegisterMethodOverride(MethodInfo overrideMethod, HotReloadOverrideMethodAttribute attribute, Type sourceType)
{
return RegisterMethodOverride(overrideMethod, attribute, sourceType, out _);
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
public static bool TryInvokeHotReload<T>(string methodId, T instance, object[] parameters = null)
{
return TryInvokeHotReload(methodId, (object)instance, parameters);
}
/// <summary>
/// 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.
/// </summary>
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
}
}
/// <summary>
/// Register all methods in a type that have the HotReloadWithOverrides attribute.
/// Uses reflection to discover and register methods marked with [HotReloadWithOverrides].
/// </summary>
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<HotReloadWithOverridesAttribute>();
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<HotReloadWithOverridesAttribute>();
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}");
}
}
/// <summary>
/// Register a type from loaded hot reload assembly for discovery scanning.
/// </summary>
public static void RegisterHotReloadType(Type type, string assemblyId)
{
var typeKey = $"{assemblyId}:{type.FullName}";
m_LoadedHotReloadTypes[typeKey] = type;
}
/// <summary>
/// Clear all hot reload overrides. Used by cleanup_hotreload command.
/// </summary>
public static void ClearAllOverrides()
{
var overrideCount = m_MethodOverrides.Count;
var typeCount = m_LoadedHotReloadTypes.Count;
m_MethodOverrides.Clear();
m_LoadedHotReloadTypes.Clear();
}
/// <summary>
/// Clear all registry state including reloadable methods.
/// FOR TESTING ONLY - this should not be called in production code.
/// </summary>
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();
}
/// <summary>
/// Get statistics about current hot reload state.
/// </summary>
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()
};
}
/// <summary>
/// Generate method ID from MethodInfo and optional custom ID.
/// Format: TypeName.MethodName or custom ID if provided.
/// </summary>
private static string GetMethodId(MethodInfo method, string customId = null)
{
if (!string.IsNullOrEmpty(customId))
{
return customId;
}
return $"{method.DeclaringType?.Name}.{method.Name}";
}
/// <summary>
/// Validate that hot reload method signature is compatible with original method.
/// Hot reload method must have instance parameter as first argument.
/// </summary>
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;
}
/// <summary>
/// Determine if original method requires main thread execution.
/// </summary>
private static bool GetMainThreadRequirement(MethodInfo originalMethod)
{
// Check for HotReloadWithOverridesAttribute main thread requirement
var hotReloadAttr = originalMethod.GetCustomAttribute<HotReloadWithOverridesAttribute>();
if (hotReloadAttr != null)
{
return hotReloadAttr.RequireMainThread;
}
// Default to main thread requirement for safety
return true;
}
/// <summary>
/// Information about a registered method override.
/// </summary>
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; }
}
}
/// <summary>
/// Statistics about current hot reload registry state.
/// </summary>
public class HotReloadStats
{
public int ReloadableMethodCount { get; set; }
public int ActiveOverrideCount { get; set; }
public int LoadedTypeCount { get; set; }
public List<string> ReloadableMethodIds { get; set; } = new();
public List<string> ActiveOverrideIds { get; set; } = new();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23cd8d0c782f1974e8a3b4095a178092
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,560 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Unity.Pipeline.Compilation;
using UnityEngine;
namespace Unity.Pipeline.HotReload
{
/// <summary>
/// Main orchestrator for in-place hot reload processing.
/// Handles the complete pipeline: source parsing -> validation -> transformation -> compilation.
/// </summary>
public static class InPlaceReloadProcessor
{
/// <summary>
/// Process a source file for in-place hot reload.
/// Extracts [HotReloadWithOverrides] methods, validates accessibility, transforms to static overrides, and compiles.
/// DEADLOCK FIX: Detects main thread and uses synchronous processing when needed.
/// </summary>
/// <param name="sourceFilePath">Path to source file containing [HotReloadWithOverrides] methods</param>
/// <param name="assemblyDir">Optional directory to save compiled assembly</param>
/// <param name="pdb">Emit debug symbols mapped to the original source so breakpoints bind.</param>
/// <returns>Compilation result with success/failure and diagnostic information</returns>
public static Task<InPlaceReloadResult> ProcessSourceFileAsync(string sourceFilePath, string assemblyDir = null, bool pdb = false)
{
// Runs synchronously on the calling (main) thread; returns a completed Task.
return Task.FromResult(ProcessSourceFileOnMainThread(sourceFilePath, assemblyDir, pdb));
}
/// <summary>
/// Process source file on main thread synchronously to avoid deadlocks.
/// </summary>
public static InPlaceReloadResult ProcessSourceFileOnMainThread(string sourceFilePath, string assemblyDir = null, bool pdb = false)
{
var result = new InPlaceReloadResult
{
SourceFilePath = sourceFilePath,
Success = false
};
try
{
Debug.Log($"HotReload: Processing source file synchronously on main thread: {sourceFilePath}");
// 1. Read and parse the source file (synchronous)
var sourceCode = ReadSourceFile(sourceFilePath);
if (string.IsNullOrEmpty(sourceCode))
{
result.ErrorMessage = $"Could not read source file: {sourceFilePath}";
return result;
}
// 2. Extract [HotReloadWithOverrides] methods
var extractionResult = ExtractHotReloadableMethods(sourceCode);
if (!extractionResult.HasMethods)
{
result.ErrorMessage = $"No [HotReload] methods found in {sourceFilePath}";
return result;
}
result.OriginalTypeName = extractionResult.TypeName;
result.ExtractedMethods = extractionResult.Methods.Keys.ToList();
Debug.Log($"HotReload: Extracted {extractionResult.Methods.Count} [HotReloadWithOverrides] methods from {extractionResult.TypeName}");
// 3. Validate accessibility (public members only)
var validationResult = AccessibilityValidator.ValidatePublicAccess(
sourceCode,
extractionResult.Methods,
extractionResult.TypeName);
if (!validationResult.IsValid)
{
result.ErrorMessage = validationResult.GetFormattedErrorMessage();
result.ValidationViolations = validationResult.Violations;
Debug.LogWarning($"HotReload: Accessibility validation failed: {result.ErrorMessage}");
return result;
}
Debug.Log($"HotReload: Accessibility validation passed for {extractionResult.TypeName}");
// 4. Transform method bodies to static overrides
var originalSource = File.ReadAllText(sourceFilePath);
var transformedCode = SourceCodeTransformer.TransformMethodBodies(
extractionResult.Methods,
extractionResult.TypeName,
extractionResult.MethodSignatures,
originalSource,
emitLineDirectives: pdb,
originalFilePath: sourceFilePath);
result.TransformedCode = transformedCode;
Debug.Log($"HotReload: Code transformation completed for {extractionResult.TypeName}");
// 5. Compile the transformed code (synchronous)
var compilationResult = CompileTransformedCode(
transformedCode,
extractionResult.TypeName,
assemblyDir,
pdb,
sourceFilePath);
result.Success = compilationResult.IsSuccess;
result.AssemblyName = compilationResult.AssemblyName;
result.RegisteredMethods = compilationResult.RegisteredMethods;
result.CompilationDiagnostics = compilationResult.Diagnostics;
if (result.Success)
{
Debug.Log($"HotReload: In-place reload successful for {sourceFilePath} - {result.RegisteredMethods.Count} methods registered");
}
else
{
result.ErrorMessage = compilationResult.ErrorDetails ?? "Compilation failed";
Debug.LogError($"HotReload: In-place reload compilation failed: {result.ErrorMessage}");
}
return result;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error processing source file {sourceFilePath}: {ex.Message}");
Debug.LogError($"HotReload: Stack trace: {ex.StackTrace}");
result.ErrorMessage = $"Processing error: {ex.Message}";
return result;
}
}
/// <summary>
/// Internal async processing implementation for background threads.
/// </summary>
private static async Task<InPlaceReloadResult> ProcessSourceFileInternalAsync(string sourceFilePath, string assemblyDir = null)
{
var result = new InPlaceReloadResult
{
SourceFilePath = sourceFilePath,
Success = false
};
try
{
Debug.Log($"HotReload: Processing source file asynchronously: {sourceFilePath}");
// 1. Read and parse the source file (async)
var sourceCode = await ReadSourceFileAsync(sourceFilePath);
if (string.IsNullOrEmpty(sourceCode))
{
result.ErrorMessage = $"Could not read source file: {sourceFilePath}";
return result;
}
// 2-4. Same processing as main thread version
var extractionResult = ExtractHotReloadableMethods(sourceCode);
if (!extractionResult.HasMethods)
{
result.ErrorMessage = $"No [HotReload] methods found in {sourceFilePath}";
return result;
}
result.OriginalTypeName = extractionResult.TypeName;
result.ExtractedMethods = extractionResult.Methods.Keys.ToList();
var validationResult = AccessibilityValidator.ValidatePublicAccess(
sourceCode,
extractionResult.Methods,
extractionResult.TypeName);
if (!validationResult.IsValid)
{
result.ErrorMessage = validationResult.GetFormattedErrorMessage();
result.ValidationViolations = validationResult.Violations;
return result;
}
var originalSource = File.ReadAllText(sourceFilePath);
var transformedCode = SourceCodeTransformer.TransformMethodBodies(
extractionResult.Methods,
extractionResult.TypeName,
extractionResult.MethodSignatures,
originalSource);
result.TransformedCode = transformedCode;
// 5. Compile the transformed code (async)
var compilationResult = await CompileTransformedCodeAsync(
transformedCode,
extractionResult.TypeName,
assemblyDir);
result.Success = compilationResult.IsSuccess;
result.AssemblyName = compilationResult.AssemblyName;
result.RegisteredMethods = compilationResult.RegisteredMethods;
result.CompilationDiagnostics = compilationResult.Diagnostics;
if (!result.Success)
{
result.ErrorMessage = compilationResult.ErrorDetails ?? "Compilation failed";
}
return result;
}
catch (Exception ex)
{
result.ErrorMessage = $"Processing error: {ex.Message}";
return result;
}
}
/// <summary>
/// Check if a source file contains [HotReload] methods.
/// Simple synchronous check to avoid async deadlocks in tests.
/// </summary>
/// <param name="sourceFilePath">Path to source file to check</param>
/// <returns>True if file contains [HotReload] methods</returns>
public static Task<bool> ContainsHotReloadableMethodsAsync(string sourceFilePath)
{
try
{
if (!File.Exists(sourceFilePath))
{
return Task.FromResult(false);
}
// Use synchronous read for simple attribute check to avoid deadlocks
var sourceCode = File.ReadAllText(sourceFilePath);
if (string.IsNullOrEmpty(sourceCode))
{
return Task.FromResult(false);
}
// Quick check for [HotReload] attribute
var result = sourceCode.Contains("[HotReload]");
return Task.FromResult(result);
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error checking for [HotReload] methods in {sourceFilePath}: {ex.Message}");
return Task.FromResult(false);
}
}
/// <summary>
/// Read source file content synchronously (for main thread).
/// </summary>
private static string ReadSourceFile(string filePath)
{
try
{
if (!File.Exists(filePath))
{
Debug.LogError($"HotReload: Source file not found: {filePath}");
return null;
}
return File.ReadAllText(filePath);
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error reading source file {filePath}: {ex.Message}");
return null;
}
}
/// <summary>
/// Read source file content asynchronously (for background threads).
/// </summary>
private static Task<string> ReadSourceFileAsync(string filePath)
{
try
{
if (!File.Exists(filePath))
{
Debug.LogError($"HotReload: Source file not found: {filePath}");
return Task.FromResult<string>(null);
}
// Use synchronous read wrapped in Task.FromResult to avoid deadlock issues
var content = File.ReadAllText(filePath);
return Task.FromResult(content);
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error reading source file {filePath}: {ex.Message}");
return Task.FromResult<string>(null);
}
}
/// <summary>
/// Extract [HotReloadWithOverrides] methods from source code.
/// </summary>
private static HotReloadableExtractionResult ExtractHotReloadableMethods(string sourceCode)
{
var result = new HotReloadableExtractionResult();
try
{
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var root = syntaxTree.GetRoot();
// Find the class containing [HotReload] methods
var classDeclaration = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault(c => c.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Any(m => HasHotReloadAttribute(m)));
if (classDeclaration == null)
{
return result;
}
result.TypeName = classDeclaration.Identifier.ValueText;
// Extract all [HotReload] methods
var hotReloadableMethods = classDeclaration.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => HasHotReloadAttribute(m));
foreach (var method in hotReloadableMethods)
{
var methodName = method.Identifier.ValueText;
var methodBody = ExtractMethodBody(method);
var signature = ExtractMethodSignature(method);
if (!string.IsNullOrEmpty(methodBody))
{
result.Methods[methodName] = methodBody;
result.MethodSignatures[methodName] = signature;
}
}
Debug.Log($"HotReload: Extracted {result.Methods.Count} [HotReload] methods from class {result.TypeName}");
return result;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error extracting [HotReload] methods: {ex.Message}");
return result;
}
}
/// <summary>
/// Check if method has [HotReload] attribute.
/// </summary>
private static bool HasHotReloadAttribute(MethodDeclarationSyntax method)
{
return method.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().EndsWith("HotReload") || a.Name.ToString().EndsWith("HotReloadAttribute"));
}
/// <summary>
/// Extract method body content (excluding braces).
/// </summary>
private static string ExtractMethodBody(MethodDeclarationSyntax method)
{
if (method.Body != null)
{
var bodyText = method.Body.ToString().Trim();
// Remove outer braces
if (bodyText.StartsWith("{") && bodyText.EndsWith("}"))
{
bodyText = bodyText.Substring(1, bodyText.Length - 2).Trim();
}
return bodyText;
}
return "";
}
/// <summary>
/// Extract method signature information.
/// </summary>
private static MethodSignatureInfo ExtractMethodSignature(MethodDeclarationSyntax method)
{
var signature = new MethodSignatureInfo
{
ReturnType = method.ReturnType.ToString()
};
foreach (var parameter in method.ParameterList.Parameters)
{
var paramInfo = new ParameterInfo
{
Type = parameter.Type?.ToString() ?? "object",
Name = parameter.Identifier.ValueText,
HasDefaultValue = parameter.Default != null,
DefaultValue = parameter.Default?.Value?.ToString()
};
signature.Parameters.Add(paramInfo);
}
return signature;
}
/// <summary>
/// Compile transformed code synchronously (for main thread).
/// </summary>
private static HotReloadCompilationResult CompileTransformedCode(
string transformedCode,
string originalTypeName,
string assemblyDir,
bool emitPdb = false,
string documentPath = null)
{
try
{
// Generate a temporary file name for the transformed code
var tempFileName = $"InPlace_{originalTypeName}_{DateTime.Now:yyyyMMdd_HHmmss}";
// Use HotReloadCompiler synchronous method
var compileResult = HotReloadCompiler.CompileSourceCodeOnMainThread(
transformedCode,
tempFileName,
assemblyDir,
emitPdb,
documentPath);
return compileResult;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error compiling transformed code for {originalTypeName}: {ex.Message}");
return HotReloadCompilationResult.Failure(
"Compilation Error",
ex.Message,
0,
new List<string> { ex.ToString() });
}
}
/// <summary>
/// Compile transformed code asynchronously (for background threads).
/// </summary>
private static async Task<HotReloadCompilationResult> CompileTransformedCodeAsync(
string transformedCode,
string originalTypeName,
string assemblyDir)
{
try
{
// Generate a temporary file name for the transformed code
var tempFileName = $"InPlace_{originalTypeName}_{DateTime.Now:yyyyMMdd_HHmmss}";
// Use HotReloadCompiler to compile the transformed source code
var compileResult = await HotReloadCompiler.CompileSourceCodeAsync(
transformedCode,
tempFileName,
assemblyDir);
return compileResult;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error compiling transformed code for {originalTypeName}: {ex.Message}");
return HotReloadCompilationResult.Failure(
"Compilation Error",
ex.Message,
0,
new List<string> { ex.ToString() });
}
}
/// <summary>
/// Result of extracting [HotReloadWithOverrides] methods from source code.
/// </summary>
private class HotReloadableExtractionResult
{
/// <summary>
/// Name of the class containing [HotReloadWithOverrides] methods.
/// </summary>
public string TypeName { get; set; }
/// <summary>
/// Dictionary of method names to their extracted body code.
/// </summary>
public Dictionary<string, string> Methods { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Dictionary of method names to their signature information.
/// </summary>
public Dictionary<string, MethodSignatureInfo> MethodSignatures { get; set; } = new Dictionary<string, MethodSignatureInfo>();
/// <summary>
/// Whether any [HotReloadWithOverrides] methods were found.
/// </summary>
public bool HasMethods => Methods.Count > 0;
}
}
/// <summary>
/// Result of in-place hot reload processing.
/// </summary>
public class InPlaceReloadResult
{
/// <summary>
/// Path to the source file that was processed.
/// </summary>
public string SourceFilePath { get; set; }
/// <summary>
/// Whether the processing was successful.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Name of the original type containing [HotReloadWithOverrides] methods.
/// </summary>
public string OriginalTypeName { get; set; }
/// <summary>
/// List of method names that were extracted and processed.
/// </summary>
public List<string> ExtractedMethods { get; set; } = new List<string>();
/// <summary>
/// Generated transformed code for hot reload assembly.
/// </summary>
public string TransformedCode { get; set; }
/// <summary>
/// Name of the compiled assembly (if successful).
/// </summary>
public string AssemblyName { get; set; }
/// <summary>
/// List of registered method IDs (if successful).
/// </summary>
public List<string> RegisteredMethods { get; set; } = new List<string>();
/// <summary>
/// Error message if processing failed.
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Accessibility validation violations (if any).
/// </summary>
public List<AccessibilityViolation> ValidationViolations { get; set; } = new List<AccessibilityViolation>();
/// <summary>
/// Compilation diagnostics (warnings, errors).
/// </summary>
public List<string> CompilationDiagnostics { get; set; } = new List<string>();
/// <summary>
/// Execution time in milliseconds.
/// </summary>
public long ExecutionTimeMs { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 411fb44c3329710449bcbfc2af627524
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Unity.Pipeline.HotReload
{
/// <summary>
/// Up-front, parse-only validation for the helper (separate override file) workflow used by
/// the <c>reload_file_override</c> command. Catches the common setup mistakes before compilation so
/// the user gets an actionable message instead of a confusing compiler error.
///
/// Rules:
/// 1. The file must contain at least one method annotated [HotReloadOverrideMethod(...)].
/// 2. The file must not redeclare a type that an override targets (the override's first
/// parameter type). Redeclaring the target type binds the override to a duplicate type
/// and the registration silently mismatches.
/// 3. Each [HotReloadOverrideMethod] method must be 'public static' and take the instance as its
/// first parameter.
/// </summary>
public static class OverrideFileValidator
{
public static OverrideFileValidationResult Validate(string sourceCode, string displayName)
{
var result = new OverrideFileValidationResult { DisplayName = displayName };
var root = CSharpSyntaxTree.ParseText(sourceCode ?? string.Empty).GetRoot();
// All type names declared in this file (class/struct), by simple name.
var declaredTypeNames = new HashSet<string>(root.DescendantNodes()
.OfType<TypeDeclarationSyntax>()
.Select(t => t.Identifier.ValueText));
// All methods annotated [HotReloadOverrideMethod(...)].
var overrideMethods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(HasHotReloadOverrideMethodAttribute)
.ToList();
// Rule 1: must contain at least one override method.
if (overrideMethods.Count == 0)
{
result.Errors.Add(
$"No [HotReloadOverrideMethod] overrides found in '{displayName}'. An override file must contain " +
"public static methods annotated [HotReloadOverrideMethod(\"Type.Method\")]. " +
"If you meant to edit a method body directly in its own source file, that is the in-place " +
"workflow (reload_file), not reload_file_override.");
return result;
}
foreach (var method in overrideMethods)
{
var name = method.Identifier.ValueText;
var modifiers = method.Modifiers.Select(m => m.ValueText).ToList();
// Rule 3: must be public static.
if (!modifiers.Contains("static") || !modifiers.Contains("public"))
{
result.Errors.Add(
$"Override method '{name}' must be declared 'public static'.");
}
// Rule 3: must take the instance as its first parameter.
var parameters = method.ParameterList.Parameters;
if (parameters.Count == 0)
{
result.Errors.Add(
$"Override method '{name}' must take the target instance as its first parameter, " +
$"e.g. 'public static void {name}(MyComponent instance, ...)'.");
continue;
}
// Rule 2: the file must not redeclare the target type (the first parameter type).
var targetTypeName = GetSimpleTypeName(parameters[0].Type);
if (targetTypeName != null && declaredTypeNames.Contains(targetTypeName))
{
result.Errors.Add(
$"'{displayName}' declares '{targetTypeName}', which is the type that override " +
$"'{name}' targets. Put the override in a separate file (any folder) that does not " +
$"redeclare your gameplay type '{targetTypeName}'.");
}
}
return result;
}
private static bool HasHotReloadOverrideMethodAttribute(MethodDeclarationSyntax method)
{
return method.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains("HotReloadOverrideMethod"));
}
/// <summary>
/// Extract the simple (unqualified) name of a parameter type, e.g. "BossController" from
/// "Namespace.BossController". Returns null for types we cannot reduce to a single name.
/// </summary>
private static string GetSimpleTypeName(TypeSyntax type)
{
switch (type)
{
case IdentifierNameSyntax id:
return id.Identifier.ValueText;
case QualifiedNameSyntax qualified:
return qualified.Right.Identifier.ValueText;
default:
return null;
}
}
}
/// <summary>
/// Result of validating a helper-workflow override file.
/// </summary>
public class OverrideFileValidationResult
{
public string DisplayName { get; set; }
public List<string> Errors { get; } = new List<string>();
public bool IsValid => Errors.Count == 0;
public string GetFormattedErrorMessage()
{
if (IsValid)
{
return "Override file is valid.";
}
var message = $"reload_file_override validation failed for '{DisplayName}': {Errors.Count} issue(s)\n";
for (int i = 0; i < Errors.Count; i++)
{
message += $"\n{i + 1}. {Errors[i]}";
}
return message;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f111479a805a488b81989f5451e4b203
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Unity.Pipeline.Compilation;
using UnityEngine;
namespace Unity.Pipeline.HotReload
{
/// <summary>
/// Transforms [HotReload] method bodies (edited in place on a MonoBehaviour) into static
/// hot reload override methods that the registry can dispatch to.
///
/// The transform is driven by a Roslyn <see cref="SemanticModel"/>: every identifier that binds
/// to an instance member of the declaring type (or a base, e.g. <c>transform</c> on Component) is
/// rewritten to <c>instance.&lt;member&gt;</c>; locals, parameters, static members and Unity
/// built-ins are left untouched. The output mirrors the helper workflow's override shape:
///
/// [HotReloadOverrideMethod("Type.Method")]
/// public static &lt;ret&gt; Method(Type instance, &lt;params&gt;) { ...rewritten body... }
/// </summary>
public static class SourceCodeTransformer
{
/// <summary>
/// Transform the [HotReload] methods named in <paramref name="methodBodies"/> into a
/// static override class. <paramref name="originalTypeDefinition"/> (the full source of the
/// file) is required so the semantic model can resolve member bindings in context.
/// </summary>
/// <param name="emitLineDirectives">
/// When true, each rewritten body is bracketed by <c>#line</c> directives that map it back to
/// the original source file so an attached debugger binds breakpoints in the file the user
/// edited. Requires <paramref name="originalFilePath"/>. The body is emitted with its original
/// whitespace (no <c>NormalizeWhitespace</c>) so line positions survive the transform.
/// </param>
/// <param name="originalFilePath">Absolute path of the source file, recorded in the #line directives.</param>
public static string TransformMethodBodies(
Dictionary<string, string> methodBodies,
string originalTypeName,
Dictionary<string, MethodSignatureInfo> originalMethodSignatures,
string originalTypeDefinition = null,
bool emitLineDirectives = false,
string originalFilePath = null)
{
if (string.IsNullOrEmpty(originalTypeDefinition))
{
throw new InvalidOperationException(
"In-place transformation requires the full original source to build a semantic model.");
}
try
{
var tree = CSharpSyntaxTree.ParseText(originalTypeDefinition);
var root = tree.GetRoot();
var compilation = CSharpCompilation.Create(
"HotReloadInPlaceTransform",
new[] { tree },
RoslynCompilationService.GetMetadataReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var model = compilation.GetSemanticModel(tree);
var classDecl = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault(c => c.Identifier.ValueText == originalTypeName);
if (classDecl == null)
{
throw new InvalidOperationException($"Could not find class '{originalTypeName}' in source.");
}
var classSymbol = model.GetDeclaredSymbol(classDecl);
var rewriter = new InstanceMemberQualifier(model, classSymbol);
var sb = new StringBuilder();
foreach (var u in root.DescendantNodes().OfType<UsingDirectiveSyntax>())
sb.AppendLine(u.ToString());
sb.AppendLine("using Unity.Pipeline.HotReload;");
var namespaceName = GetNamespace(classDecl);
if (!string.IsNullOrEmpty(namespaceName))
sb.AppendLine($"using {namespaceName};");
sb.AppendLine();
sb.AppendLine($"public static class {originalTypeName}HotReloadOverrides");
sb.AppendLine("{");
foreach (var method in classDecl.Members.OfType<MethodDeclarationSyntax>())
{
var methodName = method.Identifier.ValueText;
if (!methodBodies.ContainsKey(methodName) || method.Body == null)
continue;
var rewrittenBody = (BlockSyntax)rewriter.Visit(method.Body);
var returnType = method.ReturnType.ToString();
var parameters = new List<string> { $"{originalTypeName} instance" };
parameters.AddRange(method.ParameterList.Parameters.Select(
p => $"{p.Type} {p.Identifier.ValueText}"));
sb.AppendLine($" [HotReloadOverrideMethod(\"{originalTypeName}.{methodName}\")]");
sb.AppendLine($" public static {returnType} {methodName}({string.Join(", ", parameters)})");
if (emitLineDirectives && !string.IsNullOrEmpty(originalFilePath))
{
// Map the body back to the original file. The rewriter only makes intra-line
// edits, so a single #line at the body's opening brace maps every line of the
// block; emit the body with original trivia (not NormalizeWhitespace) to keep
// line counts intact. #line hidden masks the generated scaffolding that follows.
var bodyLine = method.Body.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
var escapedPath = originalFilePath.Replace("\\", "\\\\");
sb.AppendLine($"#line {bodyLine} \"{escapedPath}\"");
sb.AppendLine(rewrittenBody.ToString());
sb.AppendLine("#line hidden");
}
else
{
sb.AppendLine(" " + rewrittenBody.NormalizeWhitespace().ToString());
}
sb.AppendLine();
}
sb.AppendLine("}");
var result = sb.ToString();
Debug.Log($"HotReload: Transformed {methodBodies.Count} in-place method(s) for {originalTypeName}");
return result;
}
catch (Exception ex)
{
Debug.LogError($"HotReload: Error transforming in-place methods for {originalTypeName}: {ex.Message}");
throw new InvalidOperationException(
$"Failed to transform in-place methods for {originalTypeName}: {ex.Message}", ex);
}
}
private static string GetNamespace(SyntaxNode node)
{
for (var current = node.Parent; current != null; current = current.Parent)
{
if (current is NamespaceDeclarationSyntax ns)
return ns.Name.ToString();
}
return null;
}
/// <summary>
/// Rewrites implicit instance-member access (this.x or bare x) to instance.x using the
/// semantic model. Locals, parameters, static members and built-ins are left untouched.
/// </summary>
private class InstanceMemberQualifier : CSharpSyntaxRewriter
{
private readonly SemanticModel m_Model;
private readonly INamedTypeSymbol m_Type;
public InstanceMemberQualifier(SemanticModel model, INamedTypeSymbol type)
{
m_Model = model;
m_Type = type;
}
public override SyntaxNode VisitThisExpression(ThisExpressionSyntax node)
{
return SyntaxFactory.IdentifierName("instance").WithTriviaFrom(node);
}
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
{
if (ShouldSkip(node))
return base.VisitIdentifierName(node);
if (IsImplicitInstanceMember(node))
return Qualify(node);
return base.VisitIdentifierName(node);
}
public override SyntaxNode VisitGenericName(GenericNameSyntax node)
{
// e.g. GetComponent<Renderer>() — visit type arguments first.
var visited = (GenericNameSyntax)base.VisitGenericName(node);
if (ShouldSkip(node))
return visited;
if (IsImplicitInstanceMember(node))
return Qualify(visited);
return visited;
}
private static bool ShouldSkip(SimpleNameSyntax node)
{
// Right-hand side of a member access (foo.Bar -> Bar), or part of a qualified name.
if (node.Parent is MemberAccessExpressionSyntax ma && ma.Name == node)
return true;
if (node.Parent is QualifiedNameSyntax)
return true;
if (node.Parent is MemberBindingExpressionSyntax)
return true;
return false;
}
private bool IsImplicitInstanceMember(SimpleNameSyntax node)
{
var symbol = m_Model.GetSymbolInfo(node).Symbol;
if (symbol == null || symbol.IsStatic)
return false;
switch (symbol.Kind)
{
case SymbolKind.Field:
case SymbolKind.Property:
case SymbolKind.Method:
case SymbolKind.Event:
return InTypeHierarchy(symbol.ContainingType);
default:
return false;
}
}
private bool InTypeHierarchy(INamedTypeSymbol containingType)
{
if (containingType == null)
return false;
for (var t = m_Type; t != null; t = t.BaseType)
{
if (SymbolEqualityComparer.Default.Equals(t, containingType))
return true;
}
return false;
}
private static SyntaxNode Qualify(SimpleNameSyntax node)
{
return SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("instance"),
node.WithoutTrivia())
.WithTriviaFrom(node);
}
}
}
/// <summary>
/// Information about a method's signature for transformation purposes.
/// </summary>
public class MethodSignatureInfo
{
public string ReturnType { get; set; }
public List<ParameterInfo> Parameters { get; set; } = new List<ParameterInfo>();
public bool ReturnsValue => !string.IsNullOrEmpty(ReturnType) && ReturnType != "void";
}
/// <summary>
/// Information about a method parameter.
/// </summary>
public class ParameterInfo
{
public string Type { get; set; }
public string Name { get; set; }
public bool HasDefaultValue { get; set; }
public string DefaultValue { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 425cdc175cccce547a6a3bf6f73c1f4f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: