Add Unity Pipeline package support
This commit is contained in:
+198
@@ -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
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bdaafa6e8d172648a57310104db179a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+166
@@ -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<T> wrapper for type-safe delegate hot reload.
|
||||
///
|
||||
/// Pattern B: Explicit Wrapper
|
||||
/// - HotReloadMethod<T> 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<T> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bd43f6237214774ab4a178b811eb453
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+140
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a2835bfdec4fc24cb67a208766efcff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+365
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23cd8d0c782f1974e8a3b4095a178092
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+560
@@ -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; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 411fb44c3329710449bcbfc2af627524
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+136
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f111479a805a488b81989f5451e4b203
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+266
@@ -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.<member></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 <ret> Method(Type instance, <params>) { ...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; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 425cdc175cccce547a6a3bf6f73c1f4f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user