Essentials + extra crap

Loadings
Startups
Movement
Camera
This commit is contained in:
ThomasCharester committed 2026-01-12 18:43:51 +03:00
1 parent 662f4a1d05
commit 5f5ace1b52
734 files changed
+107323 -1713

No files matched your search

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 15c1071797bb4a818f85a6f8187f4289
timeCreated: 1678785269
@@ -0,0 +1,46 @@
using System.IO;
using System.Reflection;
using Mono.Cecil;
using UnityEditor;
using UnityEngine;
namespace FastScriptReload.Editor.AssemblyPostProcess
{
[InitializeOnLoad]
public static class AddInternalsVisibleToForAllUserAssembliesPostProcess
{
public static readonly DirectoryInfo AdjustedAssemblyRoot;
static AddInternalsVisibleToForAllUserAssembliesPostProcess()
{
AdjustedAssemblyRoot = new DirectoryInfo(Path.Combine(Application.dataPath, "..", "Temp", "Fast Script Reload", "AdjustedDlls"));
}
public static string CreateAssemblyWithInternalsContentsVisibleTo(Assembly changedAssembly, string visibleToAssemblyName)
{
if(!AdjustedAssemblyRoot.Exists)
AdjustedAssemblyRoot.Create();
using (var assembly = AssemblyDefinition.ReadAssembly(changedAssembly.Location, new ReaderParameters { ReadWrite = false }))
{
var mainModule = assembly.MainModule;
var attributeCtor = mainModule.ImportReference(
typeof(System.Runtime.CompilerServices.InternalsVisibleToAttribute).GetConstructor(new[] { typeof(string) })
);
var attribute = new CustomAttribute(attributeCtor);
attribute.ConstructorArguments.Add(
new CustomAttributeArgument(mainModule.TypeSystem.String, visibleToAssemblyName)
);
assembly.CustomAttributes.Add(attribute);
var newAssemblyPath = new FileInfo(Path.Combine(AdjustedAssemblyRoot.FullName, assembly.Name.Name) + ".dll").FullName;
assembly.Write(newAssemblyPath);
return newAssemblyPath;
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b2f64e1d1e2642f9ab712970582c0b45
timeCreated: 1678785285
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/AssemblyPostProcess/AddInternalsVisibleToForAllUserAssembliesPostProcess.cs
uploadId: 710802
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5d518c1ca9c2d974a886d5856abadb4d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bdecd2714d8a463fa3bdae705d942796
timeCreated: 1672992950
@@ -0,0 +1,36 @@
using System.Linq;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class BuilderPatternFunctionsRewriter : FastScriptReloadCodeRewriterBase
{
public BuilderPatternFunctionsRewriter(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
}
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
{
var ancestorName = node.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault()?.Identifier.ValueText;
if (string.IsNullOrEmpty(ancestorName)) //TODO: that happens too often for some methods, odd. Why? At very least don't spam user
{
LoggerScoped.LogWarning($"Unable to find ancestor for node '{node.ToFullString()}'");
}
else
{
var ancestorNameWithoutPatchedPostfix = ancestorName.Replace(FastScriptReload.Runtime.AssemblyChangesLoader.ClassnamePatchedPostfix, "");
if (node.ReturnType is IdentifierNameSyntax name && name.Identifier.ValueText == ancestorNameWithoutPatchedPostfix)
{
return node.WithReturnType(SyntaxFactory.IdentifierName(ancestorName + " "));
}
}
return base.VisitMethodDeclaration(node);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ad3d5960a86c431cbea11b8962fa361e
timeCreated: 1678960928
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/BuilderPatternFunctionsRewriter.cs
uploadId: 710802
@@ -0,0 +1,84 @@
using System.Linq;
using FastScriptReload.Runtime;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class ConstructorRewriter : FastScriptReloadCodeRewriterBase
{
private readonly bool _adjustCtorOnlyForNonNestedTypes;
public ConstructorRewriter(bool adjustCtorOnlyForNonNestedTypes, bool writeRewriteReasonAsComment)
: base(writeRewriteReasonAsComment)
{
_adjustCtorOnlyForNonNestedTypes = adjustCtorOnlyForNonNestedTypes;
}
public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
{
if (_adjustCtorOnlyForNonNestedTypes)
{
var typeNestedLevel = node.Ancestors().Count(a => a is TypeDeclarationSyntax);
if (typeNestedLevel == 1)
{
return AdjustCtorOrDestructorNameForTypeAdjustment(node, node.Identifier);
}
}
else
{
return AdjustCtorOrDestructorNameForTypeAdjustment(node, node.Identifier);
}
return base.VisitConstructorDeclaration(node);
}
public override SyntaxNode VisitDestructorDeclaration(DestructorDeclarationSyntax node)
{
if (_adjustCtorOnlyForNonNestedTypes)
{
var typeNestedLevel = node.Ancestors().Count(a => a is TypeDeclarationSyntax);
if (typeNestedLevel == 1)
{
return AdjustCtorOrDestructorNameForTypeAdjustment(node, node.Identifier);
}
}
else
{
return AdjustCtorOrDestructorNameForTypeAdjustment(node, node.Identifier);
}
return base.VisitDestructorDeclaration(node);
}
private SyntaxNode AdjustCtorOrDestructorNameForTypeAdjustment(BaseMethodDeclarationSyntax node, SyntaxToken nodeIdentifier)
{
var typeName = (node.Ancestors().First(n => n is TypeDeclarationSyntax) as TypeDeclarationSyntax).Identifier.ToString();
if (!nodeIdentifier.ToFullString().Contains(typeName))
{
//Used Roslyn version bug, some static methods are also interpreted as ctors, eg
// public static void Method()
// {
// Bar(); //treated as Ctor declaration...
// }
//
// private static void Bar()
// {
//
// }
return node;
}
if (!typeName.EndsWith(AssemblyChangesLoader.ClassnamePatchedPostfix))
{
typeName += AssemblyChangesLoader.ClassnamePatchedPostfix;
}
return AddRewriteCommentIfNeeded(
node.ReplaceToken(nodeIdentifier, SyntaxFactory.Identifier(typeName)),
$"{nameof(ConstructorRewriter)}:{nameof(AdjustCtorOrDestructorNameForTypeAdjustment)}"
);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 1d2a76467ecd40529f59bc68be8d7289
timeCreated: 1672992987
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/ConstructorRewriter.cs
uploadId: 710802
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class CreateNewFieldInitMethodRewriter: FastScriptReloadCodeRewriterBase {
private readonly Dictionary<string, List<string>> _typeToNewFieldDeclarations;
private static readonly string NewFieldsToCreateValueFnDictionaryFieldName = "__Patched_NewFieldNameToInitialValueFn";
private static readonly string NewFieldsToGetTypeFnDictionaryFieldName = "__Patched_NewFieldsToGetTypeFnDictionaryFieldName";
private static readonly string DictionaryFullNamespaceTypeName = "global::System.Collections.Generic.Dictionary";
public static Dictionary<string, Func<object>> ResolveNewFieldsToCreateValueFn(Type forType)
{
return (Dictionary<string, Func<object>>) forType.GetField(NewFieldsToCreateValueFnDictionaryFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
}
public static Dictionary<string, Func<object>> ResolveNewFieldsToTypeFn(Type forType)
{
return (Dictionary<string, Func<object>>) forType.GetField(NewFieldsToGetTypeFnDictionaryFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
}
public CreateNewFieldInitMethodRewriter(Dictionary<string, List<string>> typeToNewFieldDeclarations, bool writeRewriteReasonAsComment)
:base(writeRewriteReasonAsComment)
{
_typeToNewFieldDeclarations = typeToNewFieldDeclarations;
}
public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
{
var fullClassName = RoslynUtils.GetMemberFQDN(node, node.Identifier.ToString());
if (!_typeToNewFieldDeclarations.TryGetValue(fullClassName, out var newClassFields))
{
LoggerScoped.LogWarning($"Unable to find new-fields for type: {fullClassName}, this is not an issue if there are no new fields for that type.");
}
Func<FieldDeclarationSyntax, ExpressionSyntax> getObjectFnSyntax = fieldDeclarationNode => fieldDeclarationNode.Declaration.Variables[0].Initializer?.Value //value captured from initializer
?? SyntaxFactory.DefaultExpression(SyntaxFactory.IdentifierName(fieldDeclarationNode.Declaration.Type.ToString()));
var withDictionaryFieldNameToInitFieldValue = CreateNewFieldNameToGetObjectFnDictionary(node, newClassFields, getObjectFnSyntax, NewFieldsToCreateValueFnDictionaryFieldName);
//TODO: slightly odd scenario 'When explicit #nullable enable is used, reference types should be rewritten to typeof(type) for initialization and value types should remain typeof(type?)'
// Func<FieldDeclarationSyntax, ExpressionSyntax> getObjectTypeFnSyntax = fieldDeclarationNode => SyntaxFactory.TypeOfExpression(
// SyntaxFactory.ParseTypeName(fieldDeclarationNode.Declaration.Type.ToFullString().Replace("?", ""))
// );
Func<FieldDeclarationSyntax, ExpressionSyntax> getObjectTypeFnSyntax = fieldDeclarationNode => SyntaxFactory.TypeOfExpression(fieldDeclarationNode.Declaration.Type);
return CreateNewFieldNameToGetObjectFnDictionary(withDictionaryFieldNameToInitFieldValue, newClassFields, getObjectTypeFnSyntax, NewFieldsToGetTypeFnDictionaryFieldName);
}
private ClassDeclarationSyntax CreateNewFieldNameToGetObjectFnDictionary(ClassDeclarationSyntax node,
List<string> newClassFields, Func<FieldDeclarationSyntax, ExpressionSyntax> getObjectFnSyntax, string dictionaryFieldName)
{
var dictionaryKeyToInitValueNodes = newClassFields.SelectMany(fieldName =>
{
var fieldDeclarationNode = node.ChildNodes().OfType<FieldDeclarationSyntax>()
.Single(f => f.Declaration.Variables.First().Identifier.ToString() == fieldName);
return new[]
{
(SyntaxNodeOrToken)SyntaxFactory.AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
SyntaxFactory.ImplicitElementAccess()
.WithArgumentList(
SyntaxFactory.BracketedArgumentList(
SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
SyntaxFactory.Argument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(fieldDeclarationNode.Declaration.Variables.First()
.Identifier.ToString())))))), //variable name
SyntaxFactory.ParenthesizedLambdaExpression(getObjectFnSyntax(fieldDeclarationNode))),
SyntaxFactory.Token(SyntaxKind.CommaToken) //comma, add for all
};
}).ToArray();
var dictionaryInitializer =
SyntaxFactory.InitializerExpression(
SyntaxKind.ObjectInitializerExpression,
SyntaxFactory.SeparatedList<ExpressionSyntax>(
dictionaryKeyToInitValueNodes.ToArray()
));
var withDictionaryFieldNameToInitFieldValue = node.AddMembers(
SyntaxFactory.FieldDeclaration(
SyntaxFactory.VariableDeclaration(
SyntaxFactory.GenericName(
SyntaxFactory.Identifier(DictionaryFullNamespaceTypeName))
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SeparatedList<TypeSyntax>(
new SyntaxNodeOrToken[]
{
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.StringKeyword)),
SyntaxFactory.Token(SyntaxKind.CommaToken),
SyntaxFactory.GenericName(
SyntaxFactory.Identifier("global::System.Func"))
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList<TypeSyntax>(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.ObjectKeyword)))))
}))))
.WithVariables(
SyntaxFactory.SingletonSeparatedList<VariableDeclaratorSyntax>(
SyntaxFactory.VariableDeclarator(
SyntaxFactory.Identifier(dictionaryFieldName))
.WithInitializer(
SyntaxFactory.EqualsValueClause(
SyntaxFactory.ObjectCreationExpression(
SyntaxFactory.GenericName(
SyntaxFactory.Identifier(DictionaryFullNamespaceTypeName))
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SeparatedList<TypeSyntax>(
new SyntaxNodeOrToken[]
{
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.StringKeyword)),
SyntaxFactory.Token(SyntaxKind.CommaToken),
SyntaxFactory.QualifiedName(
SyntaxFactory.IdentifierName("global::System"),
SyntaxFactory.GenericName(
SyntaxFactory.Identifier("Func"))
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory
.SingletonSeparatedList<
TypeSyntax>(
SyntaxFactory
.PredefinedType(
SyntaxFactory.Token(
SyntaxKind
.ObjectKeyword))))))
}))))
.WithInitializer(dictionaryInitializer))))))
.WithModifiers(
SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PrivateKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
.NormalizeWhitespace()
.WithLeadingTrivia(SyntaxFactory.TriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed))
.WithTrailingTrivia(SyntaxFactory.TriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed, SyntaxFactory.ElasticCarriageReturnLineFeed))
);
return AddRewriteCommentIfNeeded(withDictionaryFieldNameToInitFieldValue, $"{nameof(CreateNewFieldInitMethodRewriter)}", true);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 6bbeaafe1bf54bda9b46e237bec3ccc0
timeCreated: 1674215064
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/CreateNewFieldInitMethodRewriter.cs
uploadId: 710802
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
// When calling other extension methods from same file compilation would fail with 'The call is ambiguous between the following methods or properties'
class ExtensionMethodsCallingOtherExtensionMethodsInSameFileRewriter : FastScriptReloadCodeRewriterBase
{
private Dictionary<string, Dictionary<string, string>> _typeNameToDeclaredMethodNameToThisArgName = new Dictionary<string, Dictionary<string, string>>();
public ExtensionMethodsCallingOtherExtensionMethodsInSameFileRewriter(bool writeRewriteReasonAsComment)
: base(writeRewriteReasonAsComment)
{
}
public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
{
var classDeclaredMethodNameToThisArgName = new Dictionary<string, string>();
_typeNameToDeclaredMethodNameToThisArgName[node.Identifier.Text] = classDeclaredMethodNameToThisArgName;
foreach (var methodDeclaration in node.Members
.Where(n => n.Kind() == SyntaxKind.MethodDeclaration)
.Select(n => (MethodDeclarationSyntax)n))
{
var parameters = methodDeclaration.ParameterList.Parameters;
if (parameters.Count > 0 && parameters[0].Modifiers.Any(SyntaxKind.ThisKeyword))
{
var thisArgumentName = parameters[0].Identifier.Text;
classDeclaredMethodNameToThisArgName.TryAdd(methodDeclaration.Identifier.Text, thisArgumentName);
}
}
return base.VisitClassDeclaration(node);
}
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
{
// Check if the invocation is an instance method with a member access (e.g., selfName.CallingOtherTest())
if (node.Expression is MemberAccessExpressionSyntax memberAccess)
{
var methodName = memberAccess.Name.Identifier.Text;
var instanceArgName = memberAccess.Expression;
var className = node.Ancestors().OfType<ClassDeclarationSyntax>().First().Identifier.Text;
if (_typeNameToDeclaredMethodNameToThisArgName.TryGetValue(className, out var declaredMethodNameToThisArgName)
&& declaredMethodNameToThisArgName.TryGetValue(methodName, out var definedInstanceArgName))
{
if (instanceArgName.ToString() == definedInstanceArgName)
{
// Rewrite the invocation to a static method call
var newInvocation = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(methodName))
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SeparatedList(
node.ArgumentList.Arguments.Insert(0, SyntaxFactory.Argument(instanceArgName)) // Add instance as first argument
)
)
).NormalizeWhitespace();
return AddRewriteCommentIfNeeded(
newInvocation,
$"{nameof(ExtensionMethodsCallingOtherExtensionMethodsInSameFileRewriter)}:Replaced extension method call with static call"
);
}
}
}
return base.VisitInvocationExpression(node);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9f739c197f9040efabf7f64461882d7b
timeCreated: 1727194643
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/ExtensionMethodsCallingOtherExtensionMethodsInSameFileRewriter.cs
uploadId: 710802
@@ -0,0 +1,64 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
public abstract class FastScriptReloadCodeRewriterBase : CSharpSyntaxRewriter
{
protected readonly bool _writeRewriteReasonAsComment;
protected FastScriptReloadCodeRewriterBase(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false) : base(visitIntoStructuredTrivia)
{
_writeRewriteReasonAsComment = writeRewriteReasonAsComment;
}
protected SyntaxToken AddRewriteCommentIfNeeded(SyntaxToken syntaxToken, string commentText, bool append = false)
{
return AddRewriteCommentIfNeeded(syntaxToken, commentText, _writeRewriteReasonAsComment, append);
}
public static SyntaxToken AddRewriteCommentIfNeeded(SyntaxToken syntaxToken, string commentText, bool writeRewriteReasonAsComment, bool append)
{
if (writeRewriteReasonAsComment)
{
if (append)
{
return syntaxToken.WithLeadingTrivia(
syntaxToken.LeadingTrivia.Add(SyntaxFactory.Comment($"/*FSR:{commentText}*/")));
}
else
{
return syntaxToken.WithTrailingTrivia(
syntaxToken.TrailingTrivia.Add(SyntaxFactory.Comment($"/*FSR:{commentText}*/")));
}
}
return syntaxToken;
}
protected T AddRewriteCommentIfNeeded<T>(T syntaxNode, string commentText, bool append = false)
where T : SyntaxNode
{
return AddRewriteCommentIfNeeded(syntaxNode, commentText, _writeRewriteReasonAsComment, append);
}
public static T AddRewriteCommentIfNeeded<T>(T syntaxNode, string commentText, bool writeRewriteReasonAsComment, bool append) where T : SyntaxNode
{
if (writeRewriteReasonAsComment)
{
if (append)
{
return syntaxNode.WithLeadingTrivia(syntaxNode.GetLeadingTrivia()
.Add(SyntaxFactory.Comment($"/*FSR:{commentText}*/")));
}
else
{
return syntaxNode.WithTrailingTrivia(syntaxNode.GetTrailingTrivia()
.Add(SyntaxFactory.Comment($"/*FSR:{commentText}*/")));
}
}
return syntaxNode;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: bcc848f0140d41cdbf1003b0f594694e
timeCreated: 1677233811
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/FastScriptReloadCodeRewriterBase.cs
uploadId: 710802
@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class FieldsWalker : CSharpSyntaxWalker {
private readonly Dictionary<string, List<NewFieldDeclaration>> _typeNameToFieldDeclarations = new Dictionary<string, List<NewFieldDeclaration>>();
public override void VisitClassDeclaration(ClassDeclarationSyntax node)
{
var className = node.Identifier;
var fullClassName = RoslynUtils.GetMemberFQDN(node, className.ToString());
if(!_typeNameToFieldDeclarations.ContainsKey(fullClassName)) {
_typeNameToFieldDeclarations[fullClassName] = new List<NewFieldDeclaration>();
}
base.VisitClassDeclaration(node);
}
public override void VisitFieldDeclaration(FieldDeclarationSyntax node)
{
var fieldName = node.Declaration.Variables.First().Identifier.ToString();
var fullClassName = RoslynUtils.GetMemberFQDNWithoutMemberName(node);
if(!_typeNameToFieldDeclarations.ContainsKey(fullClassName)) {
_typeNameToFieldDeclarations[fullClassName] = new List<NewFieldDeclaration>();
}
_typeNameToFieldDeclarations[fullClassName].Add(new NewFieldDeclaration(fieldName, node.Declaration.Type.ToString(), node));
base.VisitFieldDeclaration(node);
}
public Dictionary<string, List<NewFieldDeclaration>> GetTypeToFieldDeclarations() {
return _typeNameToFieldDeclarations;
}
public List<string> GetTypeNames() {
return _typeNameToFieldDeclarations.Select(kv => kv.Key).ToList();
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: e4a18d5133af410bab07d9c1a973fe7a
timeCreated: 1672993042
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/FieldsWalker.cs
uploadId: 710802
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using FastScriptReload.Runtime;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class HotReloadCompliantRewriter : FastScriptReloadCodeRewriterBase
{
public List<string> StrippedUsingDirectives = new List<string>();
public List<string> OriginalIdentifiersRenamedToContainPatchedPostfix = new List<string>();
public HotReloadCompliantRewriter(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
}
public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
{
return AddPatchedPostfixToTopLevelDeclarations(node, node.Identifier);
//if subclasses need to be adjusted, it's done via recursion.
// foreach (var childNode in node.ChildNodes().OfType<ClassDeclarationSyntax>())
// {
// var changed = Visit(childNode);
// node = node.ReplaceNode(childNode, changed);
// }
}
public override SyntaxNode VisitStructDeclaration(StructDeclarationSyntax node)
{
return AddPatchedPostfixToTopLevelDeclarations(node, node.Identifier);
}
public override SyntaxNode VisitEnumDeclaration(EnumDeclarationSyntax node)
{
return AddPatchedPostfixToTopLevelDeclarations(node, node.Identifier);
}
public override SyntaxNode VisitDelegateDeclaration(DelegateDeclarationSyntax node)
{
return AddPatchedPostfixToTopLevelDeclarations(node, node.Identifier);
}
public override SyntaxNode VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
{
return AddPatchedPostfixToTopLevelDeclarations(node, node.Identifier);
}
public override SyntaxNode VisitUsingDirective(UsingDirectiveSyntax node)
{
if (node.Parent is CompilationUnitSyntax)
{
StrippedUsingDirectives.Add(node.ToFullString());
return null;
}
return base.VisitUsingDirective(node);
}
private SyntaxNode AddPatchedPostfixToTopLevelDeclarations(CSharpSyntaxNode node, SyntaxToken identifier)
{
OriginalIdentifiersRenamedToContainPatchedPostfix.Add(identifier.ValueText);
var newIdentifier = SyntaxFactory.Identifier(identifier + AssemblyChangesLoader.ClassnamePatchedPostfix);
newIdentifier = AddRewriteCommentIfNeeded(newIdentifier, $"{nameof(HotReloadCompliantRewriter)}:{nameof(AddPatchedPostfixToTopLevelDeclarations)}");
node = node.ReplaceToken(identifier, newIdentifier);
return node;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f75e3e02acfa4397a558a923f7fa4f8e
timeCreated: 1672993042
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/HotReloadCompliantRewriter.cs
uploadId: 710802
@@ -0,0 +1,127 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
public class ManualUserDefinedScriptOverridesRewriter : FastScriptReloadCodeRewriterBase
{
private readonly SyntaxNode _userDefinedOverridesRoot;
public ManualUserDefinedScriptOverridesRewriter(SyntaxNode userDefinedOverridesRoot, bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
_userDefinedOverridesRoot = userDefinedOverridesRoot;
}
//TODO: refactor to use OverrideDeclarationWithMatchingUserDefinedIfExists
public override SyntaxNode VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node)
{
var methodFQDN = RoslynUtils.GetMemberFQDN(node, "operator");
var matchingInOverride = _userDefinedOverridesRoot.DescendantNodes()
//implicit conversion operators do not have name, just parameter list
.OfType<BaseMethodDeclarationSyntax>()
.FirstOrDefault(m => m.ParameterList.ToString() == node.ParameterList.ToString() //parameter lists is type / order / names, all good for targetting if there's a proper match
&& methodFQDN == RoslynUtils.GetMemberFQDN(m, "operator") //make sure same FQDN, even though there's no name there could be more implicit operators in file
);
if (matchingInOverride != null)
{
return AddRewriteCommentIfNeeded(matchingInOverride.WithTriviaFrom(node), $"User defined custom conversion override", true);
}
else {
return base.VisitConversionOperatorDeclaration(node);
}
}
//TODO: refactor to use OverrideDeclarationWithMatchingUserDefinedIfExists
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
{
var methodName = node.Identifier.ValueText;
var methodFQDN = RoslynUtils.GetMemberFQDN(node, node.Identifier.ToString());
var matchingInOverride = _userDefinedOverridesRoot.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(m => m.Identifier.ValueText == methodName
&& m.ParameterList.Parameters.Count == node.ParameterList.Parameters.Count
&& m.ParameterList.ToString() == node.ParameterList.ToString() //parameter lists is type / order / names, all good for targetting if there's a proper match
&& m.TypeParameterList?.ToString() == node.TypeParameterList?.ToString() //typed paratemets are for generics, also check
&& methodFQDN == RoslynUtils.GetMemberFQDN(m, m.Identifier.ToString()) //last check for mathod FQDN (potentially slower than others)
);
if (matchingInOverride != null)
{
return AddRewriteCommentIfNeeded(matchingInOverride.WithTriviaFrom(node), $"User defined custom method override", true);
}
else {
return base.VisitMethodDeclaration(node);
}
}
public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
{
return OverrideDeclarationWithMatchingUserDefinedIfExists(
node,
(d) => d.Identifier.ValueText,
(d) => HasSameParametersPredicate(node.ParameterList)(d.ParameterList),
(d) => base.VisitConstructorDeclaration(d)
);
}
public override SyntaxNode VisitDestructorDeclaration(DestructorDeclarationSyntax node)
{
return OverrideDeclarationWithMatchingUserDefinedIfExists(
node,
(d) => d.Identifier.ValueText,
(d) => HasSameParametersPredicate(node.ParameterList)(d.ParameterList),
(d) => base.VisitDestructorDeclaration(d)
);
}
public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
return OverrideDeclarationWithMatchingUserDefinedIfExists(
node,
(d) => d.Identifier.ValueText,
(d) => true,
(d) => base.VisitPropertyDeclaration(d)
);
}
private SyntaxNode OverrideDeclarationWithMatchingUserDefinedIfExists<T>(T node, Func<T, string> getName,
Func<T, bool> customFindMatchInOverridePredicate, Func<T, SyntaxNode> visitDefault)
where T: MemberDeclarationSyntax
{
var name = getName(node);
var fqdn = RoslynUtils.GetMemberFQDN(node, name);
var matchingInOverride = _userDefinedOverridesRoot.DescendantNodes()
.OfType<T>()
.FirstOrDefault(d =>
{
var declarationName = getName(d);
return declarationName == name
&& customFindMatchInOverridePredicate(d)
&& fqdn == RoslynUtils.GetMemberFQDN(d, declarationName); //last check for mathod FQDN (potentially slower than others)
}
);
if (matchingInOverride != null)
{
return AddRewriteCommentIfNeeded(matchingInOverride.WithTriviaFrom(node),
$"User defined custom {typeof(T)} override", true);
}
else
{
return visitDefault(node);
}
}
private Func<ParameterListSyntax, bool> HasSameParametersPredicate(ParameterListSyntax parameters)
{
return (resolvedParams) => resolvedParams.Parameters.Count == parameters.Parameters.Count
&& resolvedParams.ToString() == parameters.ToString();
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7d61f64c891c45b68a7db1ea67854127
timeCreated: 1678101034
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/ManualUserDefinedScriptOverridesRewriter.cs
uploadId: 710802
@@ -0,0 +1,18 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
public class NewFieldDeclaration
{
public string FieldName { get; }
public string TypeName { get; }
public FieldDeclarationSyntax FieldDeclarationSyntax { get; } //TODO: PERF: will that block whole tree from being garbage collected
public NewFieldDeclaration(string fieldName, string typeName, FieldDeclarationSyntax fieldDeclarationSyntax)
{
FieldName = fieldName;
TypeName = typeName;
FieldDeclarationSyntax = fieldDeclarationSyntax;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ad6e49db99ff4065b14a3fc416af560b
timeCreated: 1673019285
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/NewFieldDeclaration.cs
uploadId: 710802
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FastScriptReload.Runtime;
using FastScriptReload.Scripts.Runtime;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class NewFieldsRewriter : FastScriptReloadCodeRewriterBase
{
private readonly Dictionary<string, List<string>> _typeToNewFieldDeclarations;
public NewFieldsRewriter(Dictionary<string, List<string>> typeToNewFieldDeclarations, bool writeRewriteReasonAsComment)
:base(writeRewriteReasonAsComment)
{
_typeToNewFieldDeclarations = typeToNewFieldDeclarations;
}
public static List<MemberInfo> GetReplaceableMembers(Type type)
{ //TODO: later other might need to be included? props?
return type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Cast<MemberInfo>().ToList();
}
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
{
if (node.Expression.ToString() == "nameof")
{
var classNode = node.Ancestors().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if (classNode != null)
{
var fullClassName = RoslynUtils.GetMemberFQDN(classNode, classNode.Identifier.ToString());
if (!string.IsNullOrEmpty(fullClassName))
{
var nameofExpressionParts = node.ArgumentList.Arguments.First().ToFullString().Split('.'); //nameof could have multiple . like NewFieldCustomClass.FieldInThatClass
var fieldName = nameofExpressionParts.First(); // should take first part only to determine if new field eg. 'NewFieldCustomClass'
if (_typeToNewFieldDeclarations.TryGetValue(fullClassName, out var allNewFieldNamesForClass))
{
if (allNewFieldNamesForClass.Contains(fieldName))
{
return AddRewriteCommentIfNeeded(
SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(nameofExpressionParts.Last())), // should take last part only to for actual string eg. 'FieldInThatClass'
$"{nameof(NewFieldsRewriter)}:{nameof(VisitInvocationExpression)}");
}
}
}
}
}
return base.VisitInvocationExpression(node);
}
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
{
var classNode = node.Ancestors().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if (classNode != null)
{
var fullClassName = RoslynUtils.GetMemberFQDN(classNode, classNode.Identifier.ToString());
if (!string.IsNullOrEmpty(fullClassName))
{
var fieldName = node.Identifier.ToString();
if (_typeToNewFieldDeclarations.TryGetValue(fullClassName, out var allNewFieldNamesForClass))
{
if (allNewFieldNamesForClass.Contains(fieldName))
{
if(node.Parent is MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
//for member access expressions only rewrite if used with .this, eg this.NewField, that prevents rewriting of static class fields with same name
if (!memberAccessExpressionSyntax.ToString().StartsWith("this."))
{
return base.VisitIdentifierName(node);
}
}
var isNameOfExpression = node.Ancestors().OfType<InvocationExpressionSyntax>().Any(e => e.Expression.ToString() == "nameof");
if (!isNameOfExpression) //nameof expression will be rewritten via VisitInvocationExpression
{
return
AddRewriteCommentIfNeeded(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName(typeof(TemporaryNewFieldValues).FullName),
SyntaxFactory.GenericName(
SyntaxFactory.Identifier(nameof(TemporaryNewFieldValues.ResolvePatchedObject)))
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList<TypeSyntax>(
SyntaxFactory.IdentifierName(fullClassName + AssemblyChangesLoader.ClassnamePatchedPostfix))))))
.WithArgumentList(
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
SyntaxFactory.Argument(
SyntaxFactory.ThisExpression())))),
SyntaxFactory.IdentifierName(fieldName))
.WithTriviaFrom(node),
$"{nameof(NewFieldsRewriter)}:{nameof(VisitIdentifierName)}"
);
}
}
}
else
{
LoggerScoped.LogWarning($"Unable to find type: {fullClassName}");
}
}
}
return base.VisitIdentifierName(node);
}
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
{
var fieldName = node.Declaration.Variables.First().Identifier.ToString();
var fullClassName = RoslynUtils.GetMemberFQDNWithoutMemberName(node);
if (_typeToNewFieldDeclarations.TryGetValue(fullClassName, out var newFields))
{
if (newFields.Contains(fieldName))
{
var existingLeading = node.GetLeadingTrivia();
var existingTrailing = node.GetTrailingTrivia();
return AddRewriteCommentIfNeeded(
node
.WithLeadingTrivia(existingLeading.Add(SyntaxFactory.Comment("/* ")))
.WithTrailingTrivia(existingTrailing.Insert(0, SyntaxFactory.Comment(" */ //Auto-excluded to prevent exceptions - see docs"))),
$"{nameof(NewFieldsRewriter)}:{nameof(VisitFieldDeclaration)}"
);
}
}
return base.VisitFieldDeclaration(node);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0e9e687bdfc0444395deab1e7c68ed3f
timeCreated: 1672993042
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/NewFieldsRewriter.cs
uploadId: 710802
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 919d4ff85d4d4156bddf5c48bcc99da1
timeCreated: 1719699288
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
public static class PartialClassFinder
{
//TODO: optimize lookup, currently this loads all files in directory and checks if they have 'partial' string in them. For directories with many files this will not be sufficient
public static IEnumerable<string> FindPartialClassFilesInDirectory(string filePath, int maxDepth = int.MaxValue)
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
return Enumerable.Empty<string>();
string directory = Path.GetDirectoryName(filePath);
string fileContent = File.ReadAllText(filePath);
string partialClassName = ExtractPartialClassName(fileContent);
if (string.IsNullOrEmpty(partialClassName))
{
Debug.LogWarning($"No partial class/struct found in file: {filePath}");
return Enumerable.Empty<string>();
}
return FindPartialClassFilesRecursively(directory, partialClassName, maxDepth, currentDepth: 0);
}
private static IEnumerable<string> FindPartialClassFilesRecursively(string directory, string partialClassName, int maxDepth, int currentDepth)
{
var files = new List<string>();
try
{
// Search in current directory
files.AddRange(Directory.GetFiles(directory, "*.cs")
.Where(file => IsPartialClassFile(file, partialClassName)));
// If we haven't reached max depth, search in subdirectories
if (currentDepth < maxDepth)
{
foreach (var subDir in Directory.GetDirectories(directory))
{
files.AddRange(FindPartialClassFilesRecursively(subDir, partialClassName, maxDepth, currentDepth + 1));
}
}
}
catch (UnauthorizedAccessException ex)
{
Debug.LogWarning($"Access denied to directory: {directory}. Error: {ex.Message}");
}
catch (Exception ex)
{
Debug.LogError($"Error searching directory: {directory}. Error: {ex.Message}");
}
return files;
}
private static bool IsPartialClassFile(string filePath, string className)
{
try
{
string content = File.ReadAllText(filePath);
return content.Contains($"partial class {className}")
|| content.Contains($"partial struct {className}");
}
catch (Exception ex)
{
Debug.LogError($"Error reading file: {filePath}. Error: {ex.Message}");
return false;
}
}
private static string ExtractPartialClassName(string fileContent)
{
// This is a simple implementation and might need to be more robust
// depending on your specific needs
var partialClassDeclaration = fileContent.Split(new[] { "partial class", "partial struct" }, StringSplitOptions.RemoveEmptyEntries)
.Skip(1)
.FirstOrDefault();
if (partialClassDeclaration == null)
return string.Empty;
return partialClassDeclaration.TrimStart().Split(new[] { ' ', '{', ':' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault().Trim();
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 6f182f9db524441e80f367f8f99d8a5f
timeCreated: 1719778688
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/Partials/PartialClassFinder.cs
uploadId: 710802
@@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
internal static class PartialTreeProcessor
{
/// <summary>
/// Processes a syntax tree containing partial type definitions, merging it with other partial definitions found in the same directory.
/// </summary>
/// <param name="tree">The original syntax tree to process.</param>
/// <param name="definedPreprocessorSymbols">A collection of defined preprocessor symbols.</param>
/// <returns>A new SyntaxTree with merged partial types and added InternalsVisibleTo attribute.</returns>
/// <remarks>
/// This method performs the following steps:
/// 1. Checks if the tree contains partial types.
/// 2. If it does, processes the current tree and other partial files in the same directory.
/// 3. Combines all partial type declarations.
/// 4. Creates a new compilation unit with merged types and using directives.
/// 5. Adds the InternalsVisibleTo attribute to the resulting tree.
/// </remarks>
internal static SyntaxTree ProcessPartialTree(SyntaxTree tree, IEnumerable<string> definedPreprocessorSymbols)
{
if (!HasPartialTypes(tree))
{
return tree;
}
var combinedTypes = new Dictionary<string, List<TypeDeclarationSyntax>>();
var combinedUsingDirectives = new HashSet<UsingDirectiveSyntax>();
var combinedTypesDefined = new HashSet<string>();
ProcessTree(tree, combinedTypes, combinedUsingDirectives, combinedTypesDefined);
const int fileSearchMaxDepth = 5;
var otherPartialFiles = PartialClassFinder.FindPartialClassFilesInDirectory(tree.FilePath, fileSearchMaxDepth)
.Where(file => file != tree.FilePath);
foreach (var file in otherPartialFiles)
{
var partialTree = CSharpSyntaxTree.ParseText(File.ReadAllText(file), path: file);
ProcessTree(partialTree, combinedTypes, combinedUsingDirectives, combinedTypesDefined);
}
var combinedTypeDeclarations = combinedTypes
.Select(kvp => PartialTypeCombiner.CombinePartialType(kvp.Value))
.ToList<MemberDeclarationSyntax>();
var newRoot = SyntaxFactory.CompilationUnit()
.WithUsings(SyntaxFactory.List(combinedUsingDirectives))
.WithMembers(SyntaxFactory.List(combinedTypeDeclarations))
.WithAdditionalAnnotations(new SyntaxAnnotation("PreprocessorSymbols", string.Join(",", definedPreprocessorSymbols)));
return CSharpSyntaxTree.Create(newRoot, null, tree.FilePath)
.AddInternalsVisibleToAttribute();
}
private static bool HasPartialTypes(SyntaxTree tree)
{
var root = tree.GetCompilationUnitRoot();
return root.DescendantNodes().OfType<TypeDeclarationSyntax>().Any(type => type.Modifiers.Any(SyntaxKind.PartialKeyword));
}
private static void ProcessTree(
SyntaxTree tree,
Dictionary<string, List<TypeDeclarationSyntax>> combinedTypes,
HashSet<UsingDirectiveSyntax> combinedUsingDirectives,
HashSet<string> combinedTypesDefined)
{
var root = tree.GetCompilationUnitRoot();
combinedUsingDirectives.UnionWith(root.Usings);
foreach (var typeDecl in root.DescendantNodes().OfType<TypeDeclarationSyntax>())
{
var fullName = NamespaceHelper.GetFullyQualifiedName(typeDecl);
combinedTypesDefined.Add(fullName);
if (!combinedTypes.TryGetValue(fullName, out var declarations))
{
declarations = new List<TypeDeclarationSyntax>();
combinedTypes[fullName] = declarations;
}
declarations.Add(typeDecl);
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 4aafad9b85b44ebe902ba3cee7bda205
timeCreated: 1719954990
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/Partials/PartialTreeProcessor.cs
uploadId: 710802
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
internal static class PartialTypeCombiner
{
internal static TypeDeclarationSyntax CombinePartialType(List<TypeDeclarationSyntax> declarations)
{
if (declarations.Count == 1)
return declarations[0];
var firstDeclaration = declarations[0];
var combinedMembers = declarations.SelectMany(d => d.Members).ToList();
var combinedModifiers = declarations
.SelectMany(d => d.Modifiers)
.Where(m => !m.IsKind(SyntaxKind.PartialKeyword))
.GroupBy(m => m.ValueText)
.Select(g => g.First().WithTrailingTrivia(SyntaxFactory.Space))
.ToList();
if (!combinedModifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
combinedModifiers.Add(SyntaxFactory.Token(SyntaxKind.PartialKeyword).WithTrailingTrivia(SyntaxFactory.Space));
var combinedBaseList = declarations
.Where(d => d.BaseList != null)
.SelectMany(d => d.BaseList.Types)
.GroupBy(t => t.ToString())
.Select(g => g.First())
.ToList();
return firstDeclaration
.WithMembers(SyntaxFactory.List(combinedMembers))
.WithModifiers(SyntaxFactory.TokenList(combinedModifiers))
.WithKeyword(firstDeclaration.Keyword.WithLeadingTrivia(SyntaxFactory.Space))
.WithBaseList(combinedBaseList.Any()
? SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(combinedBaseList))
: null);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: cc8170320f3a463590d434f7e0570ea0
timeCreated: 1719955225
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/Partials/PartialTypeCombiner.cs
uploadId: 710802
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
internal static class NamespaceHelper
{
internal static string GetFullyQualifiedName(TypeDeclarationSyntax typeDeclaration)
{
var namespaces = new List<string>();
var currentNode = typeDeclaration.Parent;
while (currentNode != null)
{
if (currentNode is NamespaceDeclarationSyntax namespaceDeclaration)
{
namespaces.Add(namespaceDeclaration.Name.ToString());
}
currentNode = currentNode.Parent;
}
namespaces.Reverse();
var namespaceName = string.Join(".", namespaces);
return string.IsNullOrEmpty(namespaceName)
? typeDeclaration.Identifier.Text
: $"{namespaceName}.{typeDeclaration.Identifier.Text}";
}
}
internal static class SyntaxTreeExtensions
{
internal static SyntaxTree AddInternalsVisibleToAttribute(this SyntaxTree tree)
{
var root = tree.GetCompilationUnitRoot();
var attributeArgument = SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("Assembly-CSharp"))
);
var attribute = SyntaxFactory.Attribute(
SyntaxFactory.ParseName("System.Runtime.CompilerServices.InternalsVisibleTo"),
SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(new[] { attributeArgument }))
);
var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute))
.WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.AssemblyKeyword)));
var newRoot = root.AddAttributeLists(attributeList);
return tree.WithRootAndOptions(newRoot, tree.Options);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 1c2f0e22f1a24fcda6d0c0516b428420
timeCreated: 1719954970
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/Partials/SyntaxTreeHelpers.cs
uploadId: 710802
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
public static class SyntaxTreeMergePartialsExtensions
{
public static IEnumerable<SyntaxTree> MergePartials(
this IEnumerable<SyntaxTree> trees,
IEnumerable<string> definedPreprocessorSymbols)
{
return trees
.Select(tree => PartialTreeProcessor.ProcessPartialTree(tree, definedPreprocessorSymbols))
.ToList();
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 550c942c025f4fd5a1062891f3623c60
timeCreated: 1719174342
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/Partials/SyntaxTreeMergePartialsExtensions.cs
uploadId: 710802
@@ -0,0 +1,36 @@
using FastScriptReload.Runtime;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class RecordeRewriter : FastScriptReloadCodeRewriterBase
{
public RecordeRewriter(bool writeRewriteReasonAsComment)
: base(writeRewriteReasonAsComment)
{
}
public override SyntaxNode VisitRecordDeclaration(RecordDeclarationSyntax node)
{
return AdjustRecordName(node, node.Identifier);
}
private SyntaxNode AdjustRecordName(RecordDeclarationSyntax node, SyntaxToken nodeIdentifier)
{
var typeName = nodeIdentifier.ToString(); //Not ToFullString() as it may include spaces and break.
if (!typeName.EndsWith(AssemblyChangesLoader.ClassnamePatchedPostfix))
{
typeName += AssemblyChangesLoader.ClassnamePatchedPostfix;
}
return AddRewriteCommentIfNeeded(
node.ReplaceToken(nodeIdentifier, SyntaxFactory.Identifier(typeName)),
$"{nameof(RecordeRewriter)}:{nameof(AdjustRecordName)}"
);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: bb801c55172044ed9294ef722937f12f
timeCreated: 1705523787
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/RecordRewriter.cs
uploadId: 710802
@@ -0,0 +1,41 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
//ideally we'd use Simplifier but FSR doesn't have access to semantic model on which simplifier works
//Removes redundant type name qualifiers, eg: RootClass.NestedEnum.Value -> NestedEnum.Value, root class component is rewritten to have
//__Patched_ postfix and if original remains it'll cause type mismatch
class RedundantTypeNameQualifierRewriter : FastScriptReloadCodeRewriterBase
{
public RedundantTypeNameQualifierRewriter(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
}
public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
{
if(node.Parent is MemberAccessExpressionSyntax)
return base.VisitMemberAccessExpression(node); //only target outermost member access for full rewrite
var firstNode = node.DescendantNodes().OfType<IdentifierNameSyntax>().FirstOrDefault();
if(firstNode is IdentifierNameSyntax idNode && node.DescendantNodes().Count() > 1)
{
//enclosing type name same as first id node
if((idNode.Ancestors().First(n => n is TypeDeclarationSyntax) as TypeDeclarationSyntax)?.Identifier.ToString() == idNode.Identifier.ToString())
{
return AddRewriteCommentIfNeeded(
SyntaxFactory.ParseExpression(node.ToString().Replace($"{firstNode.Identifier.ToString()}.", string.Empty)),
$"{nameof(RedundantTypeNameQualifierRewriter)}"
);
}
}
return base.VisitMemberAccessExpression(node);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 80a540048e524157a41df1241bd5c168
timeCreated: 1679325667
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/RedundantTypeNameQualifierRewriter.cs
uploadId: 710802
@@ -0,0 +1,29 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
public class RoslynUtils
{
public static string GetMemberFQDN(MemberDeclarationSyntax memberNode, string memberName) //TODO: try get rid of member name (needs to cast to whatever member it could be to get identifier)
{
var outer = GetMemberFQDNWithoutMemberName(memberNode);
return !string.IsNullOrEmpty(outer)
? $"{outer}.{memberName}"
: memberName;
}
public static string GetMemberFQDNWithoutMemberName(MemberDeclarationSyntax memberNode) //TODO: move out to helper class
{
var fullTypeContibutingAncestorNames = memberNode.Ancestors().OfType<MemberDeclarationSyntax>().Select(da =>
{
if (da is TypeDeclarationSyntax t) return t.Identifier.ToString();
else if (da is NamespaceDeclarationSyntax n) return n.Name.ToString();
else throw new Exception("Unable to resolve full field name");
}).Reverse().ToList();
return string.Join(".", fullTypeContibutingAncestorNames);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 29bd7fad38034046a8dd4c7eea68907c
timeCreated: 1673017013
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/RoslynUtils.cs
uploadId: 710802
@@ -0,0 +1,21 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class ThisAssignmentRewriter: ThisRewriterBase {
public ThisAssignmentRewriter(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
}
public override SyntaxNode VisitThisExpression(ThisExpressionSyntax node)
{
if (node.Parent is AssignmentExpressionSyntax) {
return CreateCastedThisExpression(node);
}
return base.VisitThisExpression(node);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: c5a960528b2b44edbf644e0ac0950d35
timeCreated: 1672993042
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/ThisAssignmentRewriter.cs
uploadId: 710802
@@ -0,0 +1,22 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
class ThisCallRewriter : ThisRewriterBase
{
public ThisCallRewriter(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
}
public override SyntaxNode VisitThisExpression(ThisExpressionSyntax node)
{
if (node.Parent is ArgumentSyntax)
{
return CreateCastedThisExpression(node);
}
return base.VisitThisExpression(node);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f7e7aad39cec445db504fe5a117beae5
timeCreated: 1672993042
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/ThisCallRewriter.cs
uploadId: 710802
@@ -0,0 +1,51 @@
using System.Linq;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace FastScriptReload.Editor.Compilation.CodeRewriting
{
abstract class ThisRewriterBase : FastScriptReloadCodeRewriterBase
{
protected ThisRewriterBase(bool writeRewriteReasonAsComment, bool visitIntoStructuredTrivia = false)
: base(writeRewriteReasonAsComment, visitIntoStructuredTrivia)
{
}
protected SyntaxNode CreateCastedThisExpression(ThisExpressionSyntax node)
{
var ancestors = node.Ancestors().Where(n => n is TypeDeclarationSyntax).Cast<TypeDeclarationSyntax>().ToList();
if (ancestors.Count() > 1)
{
LoggerScoped.LogWarning($"ThisRewriter: for class: '{ancestors.First().Identifier}' - 'this' call/assignment in nested class / struct. Dynamic cast will be used but this could cause issues in some cases:" +
$"\r\n\r\n1) - If called method has multiple overrides, using dynamic will cause compiler issue as it'll no longer be able to pick correct one" +
$"\r\n\r\n If you see any issues with that message, please look at 'Limitation' section in documentation as this outlines how to deal with it.");
//TODO: casting to dynamic seems to be best option (and one that doesn't fail for nested classes), what's the performance overhead?
return SyntaxFactory.CastExpression(
SyntaxFactory.ParseTypeName("dynamic"),
node
);
}
var firstAncestor = ancestors.FirstOrDefault();
if (firstAncestor == null)
{
LoggerScoped.LogWarning($"Unable to find first ancestor for node: {node.ToFullString()}, this rewrite will not be applied");
return node;
}
var methodInType = firstAncestor.Identifier.ToString();
var resultNode = SyntaxFactory.CastExpression(
SyntaxFactory.ParseTypeName(methodInType),
SyntaxFactory.CastExpression(
SyntaxFactory.ParseTypeName(typeof(object).FullName),
node
)
);
return AddRewriteCommentIfNeeded(resultNode, $"{nameof(ThisRewriterBase)}:{nameof(CreateCastedThisExpression)}");
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0fd0b779590247168b3873c939c42d71
timeCreated: 1672993042
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/CodeRewriting/ThisRewriterBase.cs
uploadId: 710802
@@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using FastScriptReload.Editor.AssemblyPostProcess;
using HarmonyLib;
using ImmersiveVRTools.Editor.Common.Cache;
using ImmersiveVRTools.Editor.Common.Utilities;
using ImmersiveVRTools.Runtime.Common;
using ImmersiveVrToolsCommon.Runtime.Logging;
using UnityEditor;
namespace FastScriptReload.Editor.Compilation
{
[InitializeOnLoad]
public class DotnetExeDynamicCompilation: DynamicCompilationBase
{
private static string _dotnetExePath;
private static string _cscDll;
private static string _tempFolder;
private static string ApplicationContentsPath = EditorApplication.applicationContentsPath;
private static readonly List<string> _createdFilesToCleanUp = new List<string>();
private static readonly Dictionary<string, Assembly> _typeNameAssemblyCache = new Dictionary<string, Assembly>(16);
static DotnetExeDynamicCompilation()
{
#if UNITY_EDITOR_WIN
const string dotnetExecutablePath = "dotnet.exe";
#else
const string dotnetExecutablePath = "dotnet"; //mac and linux, no extension
#endif
_dotnetExePath = FindFileOrThrow(dotnetExecutablePath);
_cscDll = FindFileOrThrow("csc.dll"); //even on mac/linux need to find dll and use, not no extension one
_tempFolder = Path.GetTempPath();
EditorApplication.playModeStateChanged += obj =>
{
if (obj == PlayModeStateChange.ExitingPlayMode && _createdFilesToCleanUp.Any())
{
LoggerScoped.LogDebug($"Removing temporary files: [{string.Join(",", _createdFilesToCleanUp)}]");
foreach (var fileToCleanup in _createdFilesToCleanUp)
{
new FileInfo(fileToCleanup).IsReadOnly = false;
File.Delete(fileToCleanup);
}
_createdFilesToCleanUp.Clear();
}
};
}
private static string FindFileOrThrow(string fileName)
{
return SessionStateCache.GetOrCreateString($"FSR:FilePath_{fileName}", () =>
{
var foundFile = Directory
.GetFiles(ApplicationContentsPath, fileName, SearchOption.AllDirectories)
.FirstOrDefault();
if (foundFile == null)
{
throw new Exception($"Unable to find '{fileName}', make sure Editor version supports it. You can also add preprocessor directive 'FastScriptReload_CompileViaMCS' which will use Mono compiler instead");
}
return foundFile;
});
}
public static CompileResult Compile(List<string> filePathsWithSourceCode, UnityMainThreadDispatcher unityMainThreadDispatcher)
{
var sourceCodeCombinedFilePath = string.Empty;
try
{
var asmName = Guid.NewGuid().ToString().Replace("-", "");
var rspFile = _tempFolder + $"{asmName}.rsp";
var assemblyAttributeFilePath = _tempFolder + $"{asmName}.DynamicallyCreatedAssemblyAttribute.cs";
sourceCodeCombinedFilePath = _tempFolder + $"{asmName}.SourceCodeCombined.cs";
var outLibraryPath = $"{_tempFolder}{asmName}.dll";
var createSourceCodeCombinedResult = CreateSourceCodeCombinedContents(filePathsWithSourceCode, ActiveScriptCompilationDefines.ToList());
CreateFileAndTrackAsCleanup(sourceCodeCombinedFilePath, createSourceCodeCombinedResult.SourceCode, _createdFilesToCleanUp);
#if UNITY_EDITOR
unityMainThreadDispatcher.Enqueue(() =>
{
if ((bool)FastScriptReloadPreference.IsAutoOpenGeneratedSourceFileOnChangeEnabled.GetEditorPersistedValueOrDefault())
{
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(sourceCodeCombinedFilePath, 0);
}
});
#endif
var originalAssemblyPathToAsmWithInternalsVisibleToCompiled = PerfMeasure.Elapsed(
() => CreateAssemblyCopiesWithInternalsVisibleTo(createSourceCodeCombinedResult, asmName),
out var createInternalVisibleToAsmElapsedMilliseconds);
var shouldAddUnsafeFlag = createSourceCodeCombinedResult.SourceCode.Contains("unsafe"); //TODO: not ideal as 'unsafe' can be part of comment, not code. But compiling with that flag in more cases shouldn't cause issues
var rspFileContent = GenerateCompilerArgsRspFileContents(outLibraryPath, sourceCodeCombinedFilePath, assemblyAttributeFilePath,
originalAssemblyPathToAsmWithInternalsVisibleToCompiled, shouldAddUnsafeFlag);
CreateFileAndTrackAsCleanup(rspFile, rspFileContent, _createdFilesToCleanUp);
CreateFileAndTrackAsCleanup(assemblyAttributeFilePath, DynamicallyCreatedAssemblyAttributeSourceCode, _createdFilesToCleanUp);
var exitCode = ExecuteDotnetExeCompilation(_dotnetExePath, _cscDll, rspFile, outLibraryPath, out var outputMessages);
var compiledAssembly = Assembly.LoadFrom(outLibraryPath);
return new CompileResult(outLibraryPath, outputMessages, exitCode, compiledAssembly, createSourceCodeCombinedResult.SourceCode,
sourceCodeCombinedFilePath, createInternalVisibleToAsmElapsedMilliseconds);
}
catch (SourceCodeHasErrorsException e)
{
// FastScriptReloadManager has a special case for reporting SourceCodeHasErrorsException.
// Just pass it through.
throw e;
}
catch (Exception e)
{
LoggerScoped.LogError($"Compilation error: temporary files were not removed so they can be inspected: "
+ string.Join(", ", _createdFilesToCleanUp
.Select(f => $"<a href=\"{f}\" line=\"1\">{f}</a>")));
if (LogHowToFixMessageOnCompilationError)
{
LoggerScoped.LogWarning($@"HOW TO FIX - INSTRUCTIONS:
1) Open file that caused issue by looking at error log starting with: 'FSR: Compilation error: temporary files were not removed so they can be inspected: '. And click on file path to open.
2) Look up other error in the console, which will be like 'Error When updating files:' - this one contains exact line that failed to compile (in XXX_SourceCodeGenerated.cs file). Those are same compilation errors as you see in Unity/IDE when developing.
3) Read compiler error message as it'll help understand the issue
Error could be caused by a normal compilation issue that you created in source file (eg typo), in that case please fix and it'll recompile.
It's possible compilation fails due to existing limitation, in that case:
<b><color='orange'>You can quickly specify custom script rewrite override for part of code that's failing.</color></b>
Please use project panel to:
1) Right-click on the original file that has compilation issue
2) Click Fast Script Reload -> Add / Open User Script Rewrite Override
3) Read top comment in opened file and it'll explain how to create overrides
I'm continuously working on mitigating limitations.
If you could please get in touch with me via 'support@immersivevrtools.com' and include error you see in the console as well as created files (from paths in previous error). This way I can get it fixed for you.
You can also:
1) Look at 'limitation' section in the docs - which will explain bit more around limitations and workarounds
2) Move some of the code that you want to work on to different file - compilation happens on whole file, if you have multiple types there it could increase the chance of issues
3) Have a look at compilation error, it shows error line (in the '*.SourceCodeCombined.cs' file, it's going to be something that compiler does not accept, likely easy to spot. To workaround you can change that part of code in original file. It's specific patterns that'll break it.
*If you want to prevent that message from reappearing please go to Window -> Fast Script Reload -> Start Screen -> Logging -> tick off 'Log how to fix message on compilation error'*");
}
throw new HotReloadCompilationException(e.Message, e, sourceCodeCombinedFilePath);
}
}
private static Dictionary<string, string> CreateAssemblyCopiesWithInternalsVisibleTo(CreateSourceCodeCombinedContentsResult createSourceCodeCombinedResult, string asmName)
{
var originalAssemblyPathToAsmWithInternalsVisibleToCompiled = new Dictionary<string, string>();
try
{
var assembliesForTypesInCombinedFile = createSourceCodeCombinedResult.TypeNamesDefinitions
.Select(GetAssemblyByTypeName)
.Where(t => t != null)
.Distinct();
foreach (var assemblyForTypesInCombinedFile in assembliesForTypesInCombinedFile)
{
var createdAssemblyWithInternalsVisibleToNewlyCompiled = AddInternalsVisibleToForAllUserAssembliesPostProcess.CreateAssemblyWithInternalsContentsVisibleTo(
assemblyForTypesInCombinedFile, asmName
);
originalAssemblyPathToAsmWithInternalsVisibleToCompiled.Add(assemblyForTypesInCombinedFile.Location, createdAssemblyWithInternalsVisibleToNewlyCompiled);
}
}
catch (Exception e)
{
LoggerScoped.LogWarning($"Unable to create assembly with '{nameof(InternalsVisibleToAttribute)}' for dynamically recompiled code. {e}");
}
return originalAssemblyPathToAsmWithInternalsVisibleToCompiled;
}
private static void CreateFileAndTrackAsCleanup(string filePath, string contents, List<string> createdFilesToCleanUp)
{
File.WriteAllText(filePath, contents);
new FileInfo(filePath).IsReadOnly = true;
createdFilesToCleanUp.Add(filePath);
}
private static Assembly GetAssemblyByTypeName(string typeName)
{
// This cache is barely worth it on my machine - it's ~1ms without, ~0ms with.
// However, the number of assemblies to search is technically unbounded
// - so this might be more important for somebody else.
if (_typeNameAssemblyCache.TryGetValue(typeName, out var assembly)) return assembly;
// FSR (via Harmony) originally did this search by enumerating assembly.GetTypes().
// I can't see anything in the documentation suggesting the assembly.GetType(typeName) version misses any cases.
// It's much faster.
assembly = AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(asm => asm.GetType(typeName, false) != null);
if (assembly != null) _typeNameAssemblyCache.Add(typeName, assembly);
return assembly;
}
private static string GenerateCompilerArgsRspFileContents(string outLibraryPath, string sourceCodeCombinedFilePath, string assemblyAttributeFilePath,
Dictionary<string, string> originalAssemblyPathToAsmWithInternalsVisibleToCompiled, bool addUnsafeFlag)
{
var rspContents = new StringBuilder();
rspContents.AppendLine("-target:library");
rspContents.AppendLine($"-out:\"{outLibraryPath}\"");
// rspContents.AppendLine($"-refout:\"{tempFolder}{asmName}.ref.dll\""); //reference assembly for linking, not needed
foreach (var symbol in ActiveScriptCompilationDefines)
{
rspContents.AppendLine($"-define:{symbol}");
}
foreach (var referenceToAdd in ResolveReferencesToAdd(new List<string>()))
{
if (originalAssemblyPathToAsmWithInternalsVisibleToCompiled.TryGetValue(referenceToAdd, out var asmWithInternalsVisibleTo))
{
//Changed assembly have InternalsVisibleTo added to it to avoid any issues where types are defined internal
rspContents.AppendLine($"-r:\"{asmWithInternalsVisibleTo}\"");
}
else
{
rspContents.AppendLine($"-r:\"{referenceToAdd}\"");
}
}
rspContents.AppendLine($"\"{sourceCodeCombinedFilePath}\"");
rspContents.AppendLine($"\"{assemblyAttributeFilePath}\"");
rspContents.AppendLine($"-langversion:latest");
rspContents.AppendLine("/deterministic");
rspContents.AppendLine("/optimize-");
rspContents.AppendLine("/debug:portable");
rspContents.AppendLine("/nologo");
rspContents.AppendLine("/RuntimeMetadataVersion:v4.0.30319");
if (addUnsafeFlag)
{
rspContents.AppendLine("/unsafe");
}
rspContents.AppendLine("/nowarn:0169");
rspContents.AppendLine("/nowarn:0649");
rspContents.AppendLine("/nowarn:1701");
rspContents.AppendLine("/nowarn:1702");
rspContents.AppendLine("/utf8output");
rspContents.AppendLine("/preferreduilang:en-US");
var rspContentsString = rspContents.ToString();
return rspContentsString;
}
private static int ExecuteDotnetExeCompilation(string dotnetExePath, string cscDll, string rspFile,
string outLibraryPath, out List<string> outputMessages)
{
var process = new Process();
process.StartInfo.FileName = dotnetExePath;
process.StartInfo.Arguments = $"exec \"{cscDll}\" /nostdlib /noconfig /shared \"@{rspFile}\"";
var outMessages = new List<string>();
var stderr_completed = new ManualResetEvent(false);
var stdout_completed = new ManualResetEvent(false);
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
outMessages.Add(args.Data);
else
stderr_completed.Set();
};
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outMessages.Add(args.Data);
return;
}
stdout_completed.Set();
};
process.StartInfo.StandardOutputEncoding = process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
try
{
process.Start();
}
catch (Exception ex)
{
if (ex is Win32Exception win32Exception)
throw new SystemException(string.Format("Error running {0}: {1}", process.StartInfo.FileName,
typeof(Win32Exception)
.GetMethod("GetErrorMessage", BindingFlags.Static | BindingFlags.NonPublic)?
.Invoke(null, new object[] { win32Exception.NativeErrorCode }) ??
$"<Unable to resolve GetErrorMessage function>, NativeErrorCode: {win32Exception.NativeErrorCode}"));
throw;
}
int exitCode = -1;
try
{
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
exitCode = process.ExitCode;
}
finally
{
stderr_completed.WaitOne(TimeSpan.FromSeconds(30.0));
stdout_completed.WaitOne(TimeSpan.FromSeconds(30.0));
process.Close();
}
if (!File.Exists(outLibraryPath))
throw new Exception("Compiler failed to produce the assembly. Output: '" +
string.Join(Environment.NewLine + Environment.NewLine, outMessages) + "'");
outputMessages = new List<string>();
outputMessages.AddRange(outMessages);
return exitCode;
}
}
public class HotReloadCompilationException : Exception
{
public string SourceCodeCombinedFileCreated { get; }
public HotReloadCompilationException(string message, Exception innerException, string sourceCodeCombinedFileCreated) : base(message, innerException)
{
SourceCodeCombinedFileCreated = sourceCodeCombinedFileCreated;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 334fb2fc5cff4e98bf0f1ced9058cfc2
timeCreated: 1669637513
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/DotnetExeCompilator.cs
uploadId: 710802
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using FastScriptReload.Runtime;
using ImmersiveVRTools.Runtime.Common;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Debug = UnityEngine.Debug;
namespace FastScriptReload.Editor.Compilation
{
public class DynamicAssemblyCompiler
{
public static CompileResult Compile(List<string> filePathsWithSourceCode, UnityMainThreadDispatcher unityMainThreadDispatcher)
{
var sw = new Stopwatch();
sw.Start();
#if FastScriptReload_CompileViaMCS
var result = McsExeDynamicCompilation.Compile(filePathsWithSourceCode);
#else
var compileResult = DotnetExeDynamicCompilation.Compile(filePathsWithSourceCode, unityMainThreadDispatcher);
#endif
LoggerScoped.Log($"Files: {string.Join(",", filePathsWithSourceCode.Select(fn => new FileInfo(fn).Name))} changed " +
#if UNITY_2021_1_OR_NEWER
$"<a href=\"{compileResult.SourceCodeCombinedFileLocation}\" line=\"1\">(click here to debug [in bottom details pane])</a>" +
#else
"(to debug go to Fast Script Reload -> Start Screen -> Debugging -> Auto-open generated source file for debugging)" +
#endif
$" - compilation (took {sw.ElapsedMilliseconds}ms)"
#if ImmersiveVrTools_DebugEnabled
+ $" (of which creating internal visible to assemblies {compileResult.CreatingAssemblyWithInternalsVisibleToTook})ms"
#endif
);
return compileResult;
}
}
public class CompileResult
{
public Assembly CompiledAssembly { get; }
public string CompiledAssemblyPath { get; }
public List<string> MessagesFromCompilerProcess { get; }
public bool IsError => string.IsNullOrEmpty(CompiledAssemblyPath);
public int NativeCompilerReturnValue { get; }
public string SourceCodeCombined { get; }
public string SourceCodeCombinedFileLocation { get; }
public float CreatingAssemblyWithInternalsVisibleToTook { get; }
public CompileResult(string compiledAssemblyPath, List<string> messagesFromCompilerProcess, int nativeCompilerReturnValue, Assembly compiledAssembly, string sourceCodeCombined, string sourceCodeCombinedFileLocation, float creatingAssemblyWithInternalsVisibleToTook)
{
CompiledAssemblyPath = compiledAssemblyPath;
MessagesFromCompilerProcess = messagesFromCompilerProcess;
NativeCompilerReturnValue = nativeCompilerReturnValue;
CompiledAssembly = compiledAssembly;
SourceCodeCombined = sourceCodeCombined;
SourceCodeCombinedFileLocation = sourceCodeCombinedFileLocation;
CreatingAssemblyWithInternalsVisibleToTook = creatingAssemblyWithInternalsVisibleToTook;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 616fd2855e784e728009a00bc1f09255
timeCreated: 1669110323
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/DynamicAssemblyCompiler.cs
uploadId: 710802
@@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using FastScriptReload.Editor.Compilation.CodeRewriting;
using FastScriptReload.Editor.Compilation.ScriptGenerationOverrides;
using FastScriptReload.Runtime;
using FastScriptReload.Scripts.Runtime;
using ImmersiveVRTools.Editor.Common.Cache;
using ImmersiveVRTools.Runtime.Common.Utilities;
using ImmersiveVrToolsCommon.Runtime.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
namespace FastScriptReload.Editor.Compilation
{
[InitializeOnLoad]
public class DynamicCompilationBase
{
public static bool DebugWriteRewriteReasonAsComment;
public static bool LogHowToFixMessageOnCompilationError;
public static bool EnableExperimentalThisCallLimitationFix;
public static List<string> ReferencesExcludedFromHotReload = new List<string>();
public const string DebuggingInformationComment =
@"// DEBUGGING READ-ME: DO NOT EDIT THIS AUTO-GENERATED FILE AS IT'LL BE DELETED " +
#if !UNITY_2021_1_OR_NEWER
"WARN: on Unity versions prior to 2021, opening files in that manner can cause static values to be reinitialized"
#else
""
#endif
+
@"//
// To debug simply add a breakpoint in this file.
//
// With every code change - new file is generated, currently you'll need to re-set breakpoints after each change.
// You can also:
// - step into the function that was changed (and that will get you to correct source file)
// - add a function breakpoint in your IDE (this way you won't have to re-add it every time)
//
// Tool can automatically open dynamically-compiled code file every time to make setting breakpoints easier.
// You can adjust that behaviour via 'Window -> FastScriptReload -> Start Screen -> Debugging -> Do not auto-open generated cs file'.
//
// You can always open generated file when needed by clicking link in console, eg.
// 'FSR: Files: FunctionLibrary.cs changed (click here to debug [in bottom details pane]) - compilation (took 240ms)'
";
public static readonly string[] ActiveScriptCompilationDefines;
protected static readonly string DynamicallyCreatedAssemblyAttributeSourceCode = $"[assembly: {typeof(DynamicallyCreatedAssemblyAttribute).FullName}()]";
private static readonly string AssemblyCsharpFullPath;
static DynamicCompilationBase()
{
//needs to be set from main thread
ActiveScriptCompilationDefines = EditorUserBuildSettings.activeScriptCompilationDefines;
AssemblyCsharpFullPath = SessionStateCache.GetOrCreateString(
$"FSR:AssemblyCsharpFullPath",
() => AssetDatabase.FindAssets("Microsoft.CSharp")
.Select(g => new System.IO.FileInfo(UnityEngine.Application.dataPath + "/../" + AssetDatabase.GUIDToAssetPath(g)))
.First(fi => fi.Name.ToLower() == "Microsoft.CSharp.dll".ToLower()).FullName
);
}
protected static CreateSourceCodeCombinedContentsResult CreateSourceCodeCombinedContents(List<string> sourceCodeFiles, List<string> definedPreprocessorSymbols)
{
var combinedUsingStatements = new List<string>();
var typesDefined = new List<string>();
var trees = sourceCodeFiles
.Select(sourceCodeFile =>
{
var fileCode = File.ReadAllText(sourceCodeFile);
var tree = CSharpSyntaxTree.ParseText(fileCode, new CSharpParseOptions(preprocessorSymbols: definedPreprocessorSymbols));
return tree.WithFilePath(sourceCodeFile);
})
.ToList();
if (FastScriptReloadManager.Instance.IsPartialClassSupportEnabled)
{
trees = trees.MergePartials(definedPreprocessorSymbols).ToList();
}
// It's important to check whether the compiler was able to correctly interpret the original code.
// When the compiler encounters errors, it actually continues and still produces a tree.
// This tree even still roundtrips to the original source code.
// However, because the code didn't parse correctly, the tree's structure may be wrong.
// If we don't detect this here, FSR continues on obliviously, applying transformations to the broken tree.
// This sometimes leads to correctly generated code, because the parts of the tree that FSR cared to look at happened to be correct.
// However, this should not be relied upon.
// Transformations applied to broken trees lead to weird bugs, e.g. things at wrong nesting levels.
// The safest thing is to bail on the whole process.
//
// Note that this can happen with valid user code!!!
// This can trigger if FSR's compiler version is behind the one needed for language features in the code.
// The user may think their code is correct, but the compiler may disagree.
// In this scenario, it's particularly important to let the user know that something went wrong.
// Otherwise, they may expect valid output, and get nearly valid output from a broken tree.
// Trust me, these bugs are quite confusing when first encountered!
var errorDiagnostics = trees.SelectMany(tree => tree.GetDiagnostics()).Where(d => d.Severity == DiagnosticSeverity.Error);
if (errorDiagnostics.Any()) throw new SourceCodeHasErrorsException(errorDiagnostics);
var sourceCodeWithAdjustments = trees.Select(tree =>
{
//skip if tree.FilePath null or empty
if (string.IsNullOrEmpty(tree.FilePath))
{
LoggerScoped.LogError($"Skipping file as it has no path: {tree.FilePath}");
return tree.GetRoot().ToFullString();
}
var root = tree.GetRoot();
//WARN: needs to walk before root class name changes, otherwise it'll resolve wrong name
var fieldsWalker = new FieldsWalker();
fieldsWalker.Visit(root);
typesDefined.AddRange(fieldsWalker.GetTypeNames());
var typeToNewFieldDeclarations = new Dictionary<string, List<string>>();
if (FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)
{
var typeToFieldDeclarations = fieldsWalker.GetTypeToFieldDeclarations();
typeToNewFieldDeclarations = typeToFieldDeclarations.ToDictionary(
t => t.Key,
t =>
{
if (!ProjectTypeCache.AllTypesInNonDynamicGeneratedAssemblies.TryGetValue(t.Key, out var existingType))
{
LoggerScoped.LogDebug($"Unable to find type: {t.Key} in loaded assemblies. If that's the class you've added field to then it may not be properly working. It's possible the class was not yet loaded / used and you can ignore that warning. If it's causing any issues please contact support");
return new List<string>();
}
var existingTypeMembersToReplace = NewFieldsRewriter.GetReplaceableMembers(existingType).Select(m => m.Name).ToList();
var newFields = t.Value.Where(fD => !existingTypeMembersToReplace.Contains(fD.FieldName)).ToList();
//TODO: ideally that registration would happen outside of this class
//TODO: to work for LSR it needs to be handled in runtime
TemporaryNewFieldValues.RegisterNewFields(
existingType,
newFields.ToDictionary(
fD => fD.FieldName,
fD => new TemporaryNewFieldValues.GetNewFieldInitialValue((Type forNewlyGeneratedType) =>
{
//TODO: PERF: could cache those - they run to init every new value (for every instance when accessed)
return CreateNewFieldInitMethodRewriter.ResolveNewFieldsToCreateValueFn(forNewlyGeneratedType)[fD.FieldName]();
})
),
newFields.ToDictionary(
fD => fD.FieldName,
fD => new TemporaryNewFieldValues.GetNewFieldType((Type forNewlyGeneratedType) =>
{
//TODO: PERF: could cache those - they run to init every new value (for every instance when accessed)
return (Type)CreateNewFieldInitMethodRewriter.ResolveNewFieldsToTypeFn(forNewlyGeneratedType)[fD.FieldName]();
})
)
);
return newFields.Select(fD => fD.FieldName).ToList();
}
);
#if LiveScriptReload_Enabled
if (typeToNewFieldDeclarations.Any(kv => kv.Value.Any()))
{
LoggerScoped.LogWarning($"{nameof(FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)} is enabled. This is not supported in running build. Quite likely it'll crash remote client.");
}
#endif
}
//WARN: application order is important, eg ctors need to happen before class names as otherwise ctors will not be recognised as ctors
if (FastScriptReloadManager.Instance.EnableExperimentalThisCallLimitationFix)
{
root = new ThisCallRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
root = new ThisAssignmentRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
}
if (FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)
{
root = new NewFieldsRewriter(typeToNewFieldDeclarations, DebugWriteRewriteReasonAsComment).Visit(root);
root = new CreateNewFieldInitMethodRewriter(typeToNewFieldDeclarations, DebugWriteRewriteReasonAsComment).Visit(root);
}
root = new RedundantTypeNameQualifierRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
root = new ConstructorRewriter(adjustCtorOnlyForNonNestedTypes: true, DebugWriteRewriteReasonAsComment).Visit(root);
var hotReloadCompliantRewriter = new HotReloadCompliantRewriter(DebugWriteRewriteReasonAsComment);
root = hotReloadCompliantRewriter.Visit(root);
combinedUsingStatements.AddRange(hotReloadCompliantRewriter.StrippedUsingDirectives);
root = new BuilderPatternFunctionsRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
root = new RecordeRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
root = new ExtensionMethodsCallingOtherExtensionMethodsInSameFileRewriter(DebugWriteRewriteReasonAsComment).Visit(root);
//processed as last step to simply rewrite all changes made before
if (TryResolveUserDefinedOverridesRoot(tree.FilePath, definedPreprocessorSymbols, out var userDefinedOverridesRoot))
{
root = ProcessUserDefinedOverridesReplacements(tree.FilePath, root, userDefinedOverridesRoot);
root = AddUserDefinedOverridenTypes(userDefinedOverridesRoot, root);
}
return root.ToFullString();
}).ToList();
var sourceCodeCombinedSb = new StringBuilder();
sourceCodeCombinedSb.Append(DebuggingInformationComment);
foreach (var usingStatement in combinedUsingStatements.Distinct())
{
sourceCodeCombinedSb.Append(usingStatement);
}
foreach (var sourceCodeWithAdjustment in sourceCodeWithAdjustments)
{
sourceCodeCombinedSb.AppendLine(sourceCodeWithAdjustment);
}
LoggerScoped.LogDebug("Source Code Created:\r\n\r\n" + sourceCodeCombinedSb);
return new CreateSourceCodeCombinedContentsResult(sourceCodeCombinedSb.ToString(), typesDefined);
}
private static SyntaxNode AddUserDefinedOverridenTypes(SyntaxNode userDefinedOverridesRoot, SyntaxNode root)
{
try
{
var userDefinedOverrideTypes = userDefinedOverridesRoot.DescendantNodes().OfType<TypeDeclarationSyntax>()
.ToDictionary(n => RoslynUtils.GetMemberFQDN(n, n.Identifier.ToString()));
var allDefinedTypesInRecompiledFile = root.DescendantNodes().OfType<TypeDeclarationSyntax>()
.ToDictionary(n => RoslynUtils.GetMemberFQDN(n, n.Identifier.ToString())); //what about nested types?
var userDefinedOverrideTypesWithoutMatchnigInRecompiledFile = userDefinedOverrideTypes.Select(overridenType =>
{
if (!allDefinedTypesInRecompiledFile.ContainsKey(overridenType.Key))
{
return overridenType;
}
return default(KeyValuePair<string, TypeDeclarationSyntax>);
})
.Where(kv => kv.Key != default(string))
.ToList();
//types should be added either to root namespace or root of document
var rootNamespace = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>().FirstOrDefault();
foreach (var overridenTypeToAdd in userDefinedOverrideTypesWithoutMatchnigInRecompiledFile)
{
var newMember = FastScriptReloadCodeRewriterBase.AddRewriteCommentIfNeeded(overridenTypeToAdd.Value,
"New type defined in override file",
true, //always write reason so it's not easy to miss in generated file
true);
if (rootNamespace != null)
{
rootNamespace =
root.DescendantNodes().OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault(); //need to search again to make sure it didn't change
var newRootNamespace = rootNamespace.AddMembers(newMember);
root = root.ReplaceNode(rootNamespace, newRootNamespace);
}
else
{
root = ((CompilationUnitSyntax)root).AddMembers(newMember);
}
}
}
catch (Exception e)
{
Debug.LogError($"Unable to add user defined override types. {e}");
}
return root;
}
private static bool TryResolveUserDefinedOverridesRoot(string sourceCodeFile, List<string> definedPreprocessorSymbols, out SyntaxNode userDefinedOverridesRoot)
{
if (ScriptGenerationOverridesManager.TryGetScriptOverride(new FileInfo(sourceCodeFile), out var userDefinedOverridesFile))
{
try
{
userDefinedOverridesRoot = CSharpSyntaxTree.ParseText(File.ReadAllText(userDefinedOverridesFile.FullName), new CSharpParseOptions(preprocessorSymbols: definedPreprocessorSymbols)).GetRoot();
return true;
}
catch (Exception ex)
{
Debug.LogError($"Unable to resolve user defined overrides for file: '{userDefinedOverridesFile.FullName}' - please make sure it's compilable. Error: '{ex}'");
}
}
userDefinedOverridesRoot = null;
return false;
}
private static SyntaxNode ProcessUserDefinedOverridesReplacements(string sourceCodeFile, SyntaxNode root, SyntaxNode userDefinedOverridesRoot)
{
if (ScriptGenerationOverridesManager.TryGetScriptOverride(new FileInfo(sourceCodeFile), out var userDefinedOverridesFile))
{
try
{
var userDefinedScriptOverridesRewriter = new ManualUserDefinedScriptOverridesRewriter(userDefinedOverridesRoot,
true); //always write rewrite reason so it's not easy to miss
root = userDefinedScriptOverridesRewriter.Visit(root);
}
catch (Exception ex)
{
Debug.LogError($"Unable to resolve user defined overrides for file: '{userDefinedOverridesFile.FullName}' - please make sure it's compilable. Error: '{ex}'");
}
}
return root;
}
protected static List<string> ResolveReferencesToAdd(List<string> excludeAssyNames)
{
var referencesToAdd = new List<string>();
foreach (var assembly in AppDomain.CurrentDomain
.GetAssemblies() //TODO: PERF: just need to load once and cache? or get assembly based on changed file only?
.Where(a => excludeAssyNames.All(assyName => !a.FullName.StartsWith(assyName))
&& CustomAttributeExtensions.GetCustomAttribute<DynamicallyCreatedAssemblyAttribute>((Assembly)a) == null))
{
try
{
if (string.IsNullOrEmpty(assembly.Location))
{
LoggerScoped.LogDebug($"FastScriptReload: Assembly location is null, usually dynamic assembly, harmless.");
continue;
}
referencesToAdd.Add(assembly.Location);
}
catch (Exception)
{
LoggerScoped.LogDebug($"Unable to add a reference to assembly as unable to get location or null: {assembly.FullName} when hot-reloading, this is likely dynamic assembly and won't cause issues");
}
}
referencesToAdd = referencesToAdd.Where(r => !ReferencesExcludedFromHotReload.Any(rTe => r.EndsWith(rTe))).ToList();
if (EnableExperimentalThisCallLimitationFix || FastScriptReloadManager.Instance.AssemblyChangesLoaderEditorOptionsNeededInBuild.EnableExperimentalAddedFieldsSupport)
{
IncludeMicrosoftCsharpReferenceToSupportDynamicKeyword(referencesToAdd);
}
return referencesToAdd;
}
private static void IncludeMicrosoftCsharpReferenceToSupportDynamicKeyword(List<string> referencesToAdd)
{
//TODO: check .net4.5 backend not breaking?
//ThisRewriters will cast to dynamic - if using .NET Standard 2.1 - reference is required
referencesToAdd.Add(AssemblyCsharpFullPath);
// referencesToAdd.Add(@"C:\Program Files\Unity\Hub\Editor\2021.3.12f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Microsoft.CSharp.dll");
}
}
public class CreateSourceCodeCombinedContentsResult
{
public string SourceCode { get; }
public List<string> TypeNamesDefinitions { get; }
public CreateSourceCodeCombinedContentsResult(string sourceCode, List<string> typeNamesDefinitions)
{
SourceCode = sourceCode;
TypeNamesDefinitions = typeNamesDefinitions;
}
}
public class SourceCodeHasErrorsException : Exception
{
public SourceCodeHasErrorsException(IEnumerable<Diagnostic> errorDiagnostics) : base(MakeMessage(errorDiagnostics))
{
}
private static string MakeMessage(IEnumerable<Diagnostic> errorDiagnostics)
=> "Failed to compile the original source code. The compiler found the following errors:"
+ Environment.NewLine
+ Environment.NewLine
+ string.Join(Environment.NewLine, errorDiagnostics.Select(d => d.ToString()));
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 4301ea2066a04de58f95c515e910ae3e
timeCreated: 1669637741
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/DynamicCompilationBase.cs
uploadId: 710802
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
#if FastScriptReload_CompileViaMCS
public class McsExeDynamicCompilation : DynamicCompilationBase
{
private const int ReferenceLenghtCountWarningThreshold = 32767 - 2000; //windows can accept up to 32767 chars as args, then it starts thorowing exceptions. MCS.exe is adding references via command /r:<full path>
private static CompileResult Compile(List<string> filePathsWithSourceCode)
{
var fileSourceCode = filePathsWithSourceCode.Select(File.ReadAllText);
var providerOptions = new Dictionary<string, string>();
var provider = new Microsoft.CSharp.CSharpCodeProvider(providerOptions);
var param = new System.CodeDom.Compiler.CompilerParameters();
var excludeAssyNames = new List<string>
{
"mscorlib"
};
var referencesToAdd = ResolveReferencesToAdd(excludeAssyNames);
var referencePathCharLenght = referencesToAdd.Sum(r => r.Length);
if (referencePathCharLenght > ReferenceLenghtCountWarningThreshold)
{
LoggerScoped.LogWarning(
"Windows can accept up to 32767 chars as args, then it starts throwing exceptions. Dynamic compilation will use MCS.exe and will add references via command /r:<full path>, " +
$"currently your assembly have {referencesToAdd.Count} references which full paths amount to: {referencePathCharLenght} chars." +
$"\r\nIf you see this warning likely compilation will fail, you can:" +
$"\r\n1) Move your project to be more top-level, as references take full paths, eg 'c:\\my-source\\stuff\\unity\\my-project\\' - this then gets repeated for many references, moving it close to top level will help" +
$"\r\n2) Remove some of the assemblies if you don't need them" +
"\r\n Please let me know via support email if that's causing you issues, there may be a fix if it's affecting many users, sorry about that!");
//TODO: the process is started from Microsoft.CSharp.CSharpCodeGenerator.FromFileBatch, potentially all it'd be possible to patch that class to maybe copy all
//assemblies to some top-level location and change parameters to run from this folder, with a working directory set, this would drastically reduce char count used by full refs
//also mcs.exe allows to compile with -pkg:package1[,packageN], which somewhat bundles multiple references, maybe all unity engine refs could go in there, or all refs in general
}
param.ReferencedAssemblies.AddRange(referencesToAdd.ToArray());
param.GenerateExecutable = false;
param.GenerateInMemory = false;
providerOptions.Add(PatchMcsArgsGeneration.PreprocessorDirectivesProviderOptionsKey,
string.Join(";", ActiveScriptCompilationDefines));
var sourceCodeCombined = CreateSourceCodeCombinedContents(fileSourceCode);
var result = provider.CompileAssemblyFromSource(param, sourceCodeCombined, DynamicallyCreatedAssemblyAttributeSourceCode);
var errors = new List<string>();
foreach (var error in result.Errors)
{
errors.Add(error.ToString());
}
return new CompileResult(
result.CompiledAssembly.FullName,
errors,
result.NativeCompilerReturnValue,
result.CompiledAssembly,
sourceCodeCombined,
string.Empty
);
}
}
#endif
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 5a359917ce85466389622feaf196d024
timeCreated: 1669637866
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/McsExeDynamicCompilation.cs
uploadId: 710802
@@ -0,0 +1,110 @@
#if FastScriptReload_CompileViaMCS
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Text;
using FastScriptReload.Runtime;
using HarmonyLib;
using UnityEditor;
[InitializeOnLoad]
[PreventHotReload]
public class PatchMcsArgsGeneration
{
public const string PreprocessorDirectivesProviderOptionsKey = "PreprocessorDirectives";
static PatchMcsArgsGeneration()
{
var harmony = new Harmony(nameof(PatchMcsArgsGeneration));
var original = AccessTools.Method("Microsoft.CSharp.CSharpCodeGenerator:BuildArgs");
var postfix = AccessTools.Method(typeof(PatchMcsArgsGeneration), nameof(BuildArgsPostfix));
harmony.Patch(original, postfix: new HarmonyMethod(postfix));
}
//Copied from Microsoft.CSharp.CSharpCodeGenerator.BuildArgs
private static void BuildArgsPostfix(
CompilerParameters options,
string[] fileNames,
IDictionary<string, string> providerOptions,
ref string __result)
{
StringBuilder stringBuilder = new StringBuilder();
if (options.GenerateExecutable)
stringBuilder.Append("/target:exe ");
else
stringBuilder.Append("/target:library ");
string privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
if (privateBinPath != null && privateBinPath.Length > 0)
stringBuilder.AppendFormat("/lib:\"{0}\" ", (object) privateBinPath);
if (options.Win32Resource != null)
stringBuilder.AppendFormat("/win32res:\"{0}\" ", (object) options.Win32Resource);
if (options.IncludeDebugInformation)
stringBuilder.Append("/debug+ /optimize- ");
else
stringBuilder.Append("/debug- /optimize+ ");
if (options.TreatWarningsAsErrors)
stringBuilder.Append("/warnaserror ");
if (options.WarningLevel >= 0)
stringBuilder.AppendFormat("/warn:{0} ", (object) options.WarningLevel);
if (options.OutputAssembly == null || options.OutputAssembly.Length == 0)
{
string extension = options.GenerateExecutable ? "exe" : "dll"; //TODO:readd
// options.OutputAssembly = CSharpCodeGenerator.GetTempFileNameWithExtension(options.TempFiles, extension, !options.GenerateInMemory);
}
stringBuilder.AppendFormat("/out:\"{0}\" ", (object) options.OutputAssembly);
foreach (string referencedAssembly in options.ReferencedAssemblies)
{
if (referencedAssembly != null && referencedAssembly.Length != 0)
stringBuilder.AppendFormat("/r:\"{0}\" ", (object) referencedAssembly);
}
if (options.CompilerOptions != null)
{
stringBuilder.Append(options.CompilerOptions);
stringBuilder.Append(" ");
}
foreach (string embeddedResource in options.EmbeddedResources)
stringBuilder.AppendFormat("/resource:\"{0}\" ", (object) embeddedResource);
foreach (string linkedResource in options.LinkedResources)
stringBuilder.AppendFormat("/linkresource:\"{0}\" ", (object) linkedResource);
//WARN: that's how it's in source, quite odd, doesn't do much if compiler version specified?
// if (providerOptions != null && providerOptions.Count > 0)
// {
// string str;
// if (!providerOptions.TryGetValue("CompilerVersion", out str))
// str = "3.5";
// if (str.Length >= 1 && str[0] == 'v')
// str = str.Substring(1);
// if (str != "2.0")
// {
// }
// else
// stringBuilder.Append("/langversion:ISO-2 ");
// }
stringBuilder.Append("/langversion:experimental ");
CustomPatchAdditionAddPreprocessorDirectives(providerOptions, stringBuilder);
stringBuilder.Append("/noconfig ");
stringBuilder.Append(" -- ");
foreach (string fileName in fileNames)
stringBuilder.AppendFormat("\"{0}\" ", (object) fileName);
__result = stringBuilder.ToString();
}
private static void CustomPatchAdditionAddPreprocessorDirectives(IDictionary<string, string> providerOptions, StringBuilder stringBuilder)
{
if (providerOptions != null && providerOptions.Count > 0)
{
if (providerOptions.TryGetValue(PreprocessorDirectivesProviderOptionsKey, out var preprocessorDirectives))
{
stringBuilder.Append($"/d:\"{preprocessorDirectives}\" ");
}
}
}
}
#endif
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 26428224dc3f4d069690c15cbfb3a070
timeCreated: 1669209480
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/PatchMcsArgsGeneration.cs
uploadId: 710802
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a9e1198b8d42486bae230272101c0951
timeCreated: 1678099546
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FastScriptReload.Runtime;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace FastScriptReload.Editor.Compilation.ScriptGenerationOverrides
{
[InitializeOnLoad]
public static class ScriptGenerationOverridesManager
{
private static float LoadOverridesFolderFilesEveryNSeconds = 5;
private static readonly string TemplateInterfaceDeclaration = @"
//New interface declaration, this is very useful in cases where code depends on some internal interfaces that re-compiled code can no longer access. Simply define them here and code will compile.
//You can add any type in that manner
public interface ITestNewInterface {
bool Test { get; set; }
}";
private static readonly string TemplateTopComment = @"// You can use this file to specify custom code overrides. Those will be applied to resulting code.
// This approach is very useful if your code is failing to compile due to one of the existing limitations.
//
// While I work on reducing limitations you can simply specify override with proper code to make sure you can continue working.
//
// 1) Simply define code with same structure as your original class, make sure to include any namespace.
// 2) Rename classes and types to have '<ClassPostfix>' postfix.
//
// eg. 'MyClassName' needs to be changed to MyClassName<ClassPostfix> otherwise it won't be properly connected.
//
// 3) Add any methods that you want to override, using same method signature. Whole method body will be replaced and no code adjustments will be run on it.
// 4) You can add any additional types - this is quite useful when you hit limitation with internal interfaces - where compiler can not access them due to protection modifiers.
// You can simply redefine those here, while not ideal it'll allow you to continue using Hot-Reload without modifying your code.
//
// Tool will now attempt to create a template file for you with first found class and first method as override, please adjust as necessary.
// It'll also create an example redefined interface.
// If you can't see anything please refer to the above and create overrides file manually.
//
// You can also refer to documentation section 'User defined script rewrite overrides'
";
public static DirectoryInfo UserDefinedScriptRewriteOverridesFolder { get; }
private static double _lastTimeOverridesFolderFilesRead;
public static List<UserDefinedScriptOverride> UserDefinedScriptOverrides { get; } = new List<UserDefinedScriptOverride>();
static ScriptGenerationOverridesManager()
{
//TODO: allow to customize later from code, eg for user that'd like to include in source control
UserDefinedScriptRewriteOverridesFolder = new DirectoryInfo(Application.persistentDataPath + @"\FastScriptReload\ScriptOverrides");
UpdateUserDefinedScriptOverridesFileCache();
EditorApplication.update += Update;
}
private static void Update()
{
var timeSinceStartup = EditorApplication.timeSinceStartup;
if (_lastTimeOverridesFolderFilesRead + LoadOverridesFolderFilesEveryNSeconds < timeSinceStartup)
{
_lastTimeOverridesFolderFilesRead = timeSinceStartup;
UpdateUserDefinedScriptOverridesFileCache();
}
}
private static void UpdateUserDefinedScriptOverridesFileCache()
{
UserDefinedScriptOverrides.Clear();
if (UserDefinedScriptRewriteOverridesFolder.Exists)
{
UserDefinedScriptOverrides.AddRange(UserDefinedScriptRewriteOverridesFolder.GetFiles().Select(f => new UserDefinedScriptOverride(f)));
}
}
public static void AddScriptOverride(MonoScript script)
{
AddScriptOverride(new FileInfo(Path.Combine(Path.Combine(Application.dataPath + "//..", AssetDatabase.GetAssetPath(script)))));
}
public static void AddScriptOverride(FileInfo originalFile)
{
EnsureOverrideFolderExists();
var overridenFile = new FileInfo(Path.Combine(UserDefinedScriptRewriteOverridesFolder.FullName, originalFile.Name));
if (!overridenFile.Exists)
{
var templateString = string.Empty;
try
{
var fileCode = File.ReadAllText(originalFile.FullName);
var tree = CSharpSyntaxTree.ParseText(fileCode, new CSharpParseOptions(preprocessorSymbols: DynamicCompilationBase.ActiveScriptCompilationDefines));
var root = tree.GetRoot();
var firstType = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if (firstType != null)
{
var members = new SyntaxList<MemberDeclarationSyntax>();
var firstMethod = firstType.DescendantNodes().OfType<MethodDeclarationSyntax>().FirstOrDefault(m => m.Body != null);
if (firstMethod != null)
{
var block = SyntaxFactory.Block();
block = block.AddStatements(SyntaxFactory.EmptyStatement().WithLeadingTrivia(
SyntaxFactory.Comment(@"/* Any code will be replaced with original method of same signature in same type*/"))
);
firstMethod = firstMethod
.WithBody(block)
.WithTriviaFrom(firstMethod);
members = members.Add(firstMethod);
}
root = root.ReplaceNode(firstType, firstType
.ReplaceToken(
firstType.Identifier,
SyntaxFactory.Identifier(firstType.Identifier.ValueText + AssemblyChangesLoader.ClassnamePatchedPostfix)
)
.WithMembers(members)).NormalizeWhitespace();
var interfaceDeclaration = CSharpSyntaxTree.ParseText(TemplateInterfaceDeclaration);
root = ((CompilationUnitSyntax)root).AddMembers(
interfaceDeclaration.GetRoot().DescendantNodes().OfType<InterfaceDeclarationSyntax>().First()
);
}
templateString = root.ToFullString();
}
catch (Exception e)
{
Debug.LogError($"Unable to generate user defined script override template from your file, please refer to note at the start of the file. {e}");
}
if (!overridenFile.Exists)
{
File.WriteAllText(overridenFile.FullName,
TemplateTopComment.Replace("<ClassPostfix>", AssemblyChangesLoader.ClassnamePatchedPostfix) + templateString
);
UpdateUserDefinedScriptOverridesFileCache();
}
}
InternalEditorUtility.OpenFileAtLineExternal(overridenFile.FullName, 0);
}
public static bool TryRemoveScriptOverride(MonoScript originalScript)
{
EnsureOverrideFolderExists();
var overridenFile = new FileInfo(Path.Combine(UserDefinedScriptRewriteOverridesFolder.FullName, originalScript.name + ".cs"));
if (overridenFile.Exists)
{
return TryRemoveScriptOverride(overridenFile);
}
return false;
}
private static bool TryRemoveScriptOverride(FileInfo overridenFile)
{
try
{
overridenFile.Delete();
UpdateUserDefinedScriptOverridesFileCache();
}
catch (Exception)
{
Debug.Log($"Unable to remove: '{overridenFile.Name}' - make sure it's not locked / open in editor");
throw;
}
return true;
}
public static bool TryRemoveScriptOverride(UserDefinedScriptOverride scriptOverride)
{
return TryRemoveScriptOverride(scriptOverride.File);
}
public static bool TryGetScriptOverride(FileInfo changedFile, out FileInfo overridesFile)
{
overridesFile = UserDefinedScriptOverrides.FirstOrDefault(f => f.File.Name == changedFile.Name && f.File.Exists)?.File;
return overridesFile?.Exists ?? false;
}
private static void EnsureOverrideFolderExists()
{
if (!UserDefinedScriptRewriteOverridesFolder.Exists)
UserDefinedScriptRewriteOverridesFolder.Create();
}
}
public class UserDefinedScriptOverride
{
public FileInfo File { get; }
public UserDefinedScriptOverride(FileInfo file)
{
File = file;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f699ee76d75a40b290cad5dd79619842
timeCreated: 1678099566
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/Compilation/ScriptGenerationOverrides/ScriptGenerationOverridesManager.cs
uploadId: 710802
@@ -0,0 +1,223 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using FastScriptReload.Editor;
using System;
using System.Threading;
[InitializeOnLoad]
public class CustomFileWatcher : EditorWindow
{
public class HashEntry
{
private Dictionary<string, string> _hashes = new Dictionary<string, string>();
// Some metadata for the update function to use
// WARN: Note this data isn't exactly synced up or anything. It just reads it in when the filewatcher is initialized.
private string _searchPattern;
private bool _includeSubdirectories;
public Dictionary<string, string> Hashes => _hashes;
public string SearchPattern => _searchPattern;
public bool IncludeSubdirectories => _includeSubdirectories;
public HashEntry(Dictionary<string, string> hashes, string searchPattern, bool includeSubdirectories)
{
_hashes = hashes;
_searchPattern = searchPattern;
_includeSubdirectories = includeSubdirectories;
}
}
private static Dictionary<string, HashEntry> FileHashes;
private static object StateLock = new object();
private static object ListLock; // Shared lock object
private static Thread LivewatcherThread;
public static bool InitSignaled = false;
private static readonly int WatcherThreadRunEveryNSeconds = 500; //TODO: expose in settings
static CustomFileWatcher()
{
FileHashes = new Dictionary<string, HashEntry>();
ListLock = new object();
LivewatcherThread = null;
}
private static void UpdateFileWatcher()
{
if (FileHashes.Count > 0)
{
foreach (var kvp in FileHashes)
{
CheckForChanges(kvp.Key, kvp.Value.SearchPattern, kvp.Value.IncludeSubdirectories);
}
}
else
{
Debug.LogError("File watcher has not been initialized yet. Please initialize first.");
}
}
public static void TryEnableLivewatching()
{
if (LivewatcherThread != null)
{
Debug.LogWarning("Livewatcher is already running.");
return;
}
// Run on a separate thread every 1 second
LivewatcherThread = new Thread(() =>
{
var timer = new Timer((state) =>
{
// Go at it if we've initialized
if (FileHashes.Count > 0)
UpdateFileWatcher();
}, null, 0, WatcherThreadRunEveryNSeconds);
});
LivewatcherThread.Start();
}
public static void InitializeSingularFilewatcher(string directoryPath, string searchPattern, bool includeSubdirectories)
{
#if ImmersiveVrTools_DebugEnabled
Debug.Log("Initializing hashes for directory: " + directoryPath);
#endif
var thread = new Thread(() =>
{
lock (StateLock)
{
var hashes = new Dictionary<string, string>();
var files = Directory.GetFiles(directoryPath, searchPattern, includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
foreach (var filePath in files)
{
var hash = GetFileHash(filePath);
hashes[filePath] = hash;
}
FileHashes[directoryPath] = new HashEntry(hashes, searchPattern, includeSubdirectories);
}
});
thread.Start();
}
private static void CheckForChanges(string directoryPath, string searchPattern, bool includeSubdirectories)
{
// Not really sure if this nuclear locking is needed
lock (StateLock)
{
var hashes = FileHashes[directoryPath].Hashes;
// Time profiling: Start the stopwatch for Directory.GetFiles
#if ImmersiveVrTools_DebugEnabled
System.Diagnostics.Stopwatch getFilesStopwatch = new System.Diagnostics.Stopwatch();
getFilesStopwatch.Start();
#endif
string[] files = Directory.GetFiles(directoryPath, searchPattern, includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
#if ImmersiveVrTools_DebugEnabled
// Time profiling: Stop the stopwatch for Directory.GetFiles and log the elapsed time
getFilesStopwatch.Stop();
Debug.Log("Directory.GetFiles elapsed time: " + getFilesStopwatch.ElapsedMilliseconds + " ms");
#endif
// Check if files were created or modified
// Time profiling: Start the stopwatch for file creation/modification
var fileChangeStopwatch = new System.Diagnostics.Stopwatch();
fileChangeStopwatch.Start();
foreach (var file in files)
{
if (!hashes.ContainsKey(file))
{
// New file
#if ImmersiveVrTools_DebugEnabled
Debug.Log("New file: " + file);
#endif
continue;
}
else if (hashes[file] != GetFileHash(file))
{
// File changed
#if ImmersiveVrTools_DebugEnabled
Debug.Log("File changed: " + file);
#endif
RecordChange(file);
}
}
#if ImmersiveVrTools_DebugEnabled
// Time profiling: Stop the stopwatch for file creation/modification and log the elapsed time
fileChangeStopwatch.Stop();
Debug.Log("File creation/modification elapsed time: " + fileChangeStopwatch.ElapsedMilliseconds + " ms");
#endif
// Check if any files were deleted
// Time profiling: Start the stopwatch for file deletion
var fileDeletionStopwatch = new System.Diagnostics.Stopwatch();
fileDeletionStopwatch.Start();
foreach (var kvp in hashes)
{
if (!File.Exists(kvp.Key))
{
#if ImmersiveVrTools_DebugEnabled
Debug.Log("File deleted: " + kvp.Key);
#endif
}
}
// Time profiling: Stop the stopwatch for file deletion and log the elapsed time
#if ImmersiveVrTools_DebugEnabled
fileDeletionStopwatch.Stop();
Debug.Log("File deletion elapsed time: " + fileDeletionStopwatch.ElapsedMilliseconds + " ms");
#endif
// Update hashes
hashes.Clear();
foreach (var file in files)
{
var hash = GetFileHash(file);
hashes[file] = hash;
}
}
}
private static string GetFileHash(string filePath)
{
using (var md5 = MD5.Create())
using (var stream = File.OpenRead(filePath))
{
var hashBytes = md5.ComputeHash(stream);
var sb = new StringBuilder();
for (var i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("x2"));
}
return sb.ToString();
}
}
private static void RecordChange(string path)
{
if (FastScriptReloadManager.Instance.ShouldIgnoreFileChange()) return;
lock (ListLock)
{
FastScriptReloadManager.Instance.AddFileChangeToProcess(path);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 550c0c3f77d436e4fb133b8d029eb6a6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/CustomFileWatcher.cs
uploadId: 710802
@@ -0,0 +1,25 @@
{
"name": "FastScriptReload.Editor",
"rootNamespace": "",
"references": [
"GUID:b75b497805046c443a21f90e84a5ff4f"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"0Harmony.dll",
"Microsoft.CodeAnalysis.dll",
"Microsoft.CodeAnalysis.CSharp.dll",
"ImmersiveVRTools.Common.Editor.dll",
"ImmersiveVRTools.Common.Runtime.dll",
"System.Collections.Immutable.FSR.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 97dcf6b63da809f4aa9573d9e792e540
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/FastScriptReload.Editor.asmdef
uploadId: 710802
@@ -0,0 +1,863 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FastScriptReload.Editor.Compilation;
using FastScriptReload.Editor.Compilation.ScriptGenerationOverrides;
using FastScriptReload.Runtime;
using ImmersiveVRTools.Editor.Common.Utilities;
using ImmersiveVRTools.Runtime.Common;
using ImmersiveVrToolsCommon.Runtime.Logging;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
namespace FastScriptReload.Editor
{
[InitializeOnLoad]
[PreventHotReload]
public class FastScriptReloadManager
{
private static FastScriptReloadManager _instance;
public static FastScriptReloadManager Instance
{
get {
if (_instance == null)
{
_instance = new FastScriptReloadManager();
LoggerScoped.LogDebug("Created Manager");
}
return _instance;
}
}
private static string DataPath = Application.dataPath;
public const string FileWatcherReplacementTokenForApplicationDataPath = "<Application.dataPath>";
private const int BaseMenuItemPriority_ManualScriptOverride = 100;
private const int BaseMenuItemPriority_Exclusions = 200;
private const int BaseMenuItemPriority_FileWatcher = 300;
public Dictionary<string, Func<string>> FileWatcherTokensToResolvePathFn = new Dictionary<string, Func<string>>
{
[FileWatcherReplacementTokenForApplicationDataPath] = () => DataPath
};
private Dictionary<string, DynamicFileHotReloadState> _lastProcessedDynamicFileHotReloadStatesInSession = new Dictionary<string, DynamicFileHotReloadState>();
public IReadOnlyDictionary<string, DynamicFileHotReloadState> LastProcessedDynamicFileHotReloadStatesInSession => _lastProcessedDynamicFileHotReloadStatesInSession;
public event Action<List<DynamicFileHotReloadState>> HotReloadFailed;
public event Action<List<DynamicFileHotReloadState>> HotReloadSucceeded;
private bool _wasLockReloadAssembliesCalled;
private PlayModeStateChange _lastPlayModeStateChange;
private List<IDisposable> _fileWatchers = new List<IDisposable>();
private IEnumerable<string> _currentFileExclusions;
private int _triggerDomainReloadIfOverNDynamicallyLoadedAssembles = 100;
public bool EnableExperimentalThisCallLimitationFix { get; private set; }
public bool IsPartialClassSupportEnabled { get; private set; }
#pragma warning disable 0618
public AssemblyChangesLoaderEditorOptionsNeededInBuild AssemblyChangesLoaderEditorOptionsNeededInBuild { get; private set; } = new AssemblyChangesLoaderEditorOptionsNeededInBuild();
#pragma warning restore 0618
private List<DynamicFileHotReloadState> _dynamicFileHotReloadStateEntries = new List<DynamicFileHotReloadState>();
private DateTime _lastTimeChangeBatchRun = default(DateTime);
private bool _assemblyChangesLoaderResolverResolutionAlreadyCalled;
private bool _isEditorModeHotReloadEnabled;
private int _hotReloadPerformedCount = 0;
private bool _isOnDemandHotReloadEnabled;
private void OnWatchedFileChange(object source, FileSystemEventArgs e)
{
if (ShouldIgnoreFileChange()) return;
var filePathToUse = e.FullPath;
if (!File.Exists(filePathToUse))
{
if (!TryWorkaroundForUnityFileWatcherBug(e, ref filePathToUse))
return;
}
AddFileChangeToProcess(filePathToUse);
}
public void AddFileChangeToProcess(string filePath)
{
if (!File.Exists(filePath))
{
LoggerScoped.LogWarning($"Specified file: '{filePath}' does not exist. Hot-Reload will not be performed.");
return;
}
if (_currentFileExclusions != null && _currentFileExclusions.Any(fp => filePath.Replace("\\", "/").EndsWith(fp)))
{
LoggerScoped.LogWarning($"FastScriptReload: File: '{filePath}' changed, but marked as exclusion. Hot-Reload will not be performed. You can manage exclusions via" +
$"\r\nRight click context menu (Fast Script Reload > Add / Remove Hot-Reload exclusion)" +
$"\r\nor via Window -> Fast Script Reload -> Start Screen -> Exclusion menu");
return;
}
const int msThresholdToConsiderSameChangeFromDifferentFileWatchers = 500;
var isDuplicatedChangesComingFromDifferentFileWatcher = _dynamicFileHotReloadStateEntries
.Any(f => f.FullFileName == filePath
&& (DateTime.UtcNow - f.FileChangedOn).TotalMilliseconds < msThresholdToConsiderSameChangeFromDifferentFileWatchers);
if (isDuplicatedChangesComingFromDifferentFileWatcher)
{
LoggerScoped.LogWarning($"FastScriptReload: Looks like change to: {filePath} have already been added for processing. This can happen if you have multiple file watchers set in a way that they overlap.");
return;
}
_dynamicFileHotReloadStateEntries.Add(new DynamicFileHotReloadState(filePath, DateTime.UtcNow));
}
public bool ShouldIgnoreFileChange()
{
if (!_isEditorModeHotReloadEnabled && _lastPlayModeStateChange != PlayModeStateChange.EnteredPlayMode)
{
#if ImmersiveVrTools_DebugEnabled
LoggerScoped.Log($"Application not playing, change to: {e.Name} won't be compiled and hot reloaded");
#endif
return true;
}
return false;
}
private void StartWatchingDirectoryAndSubdirectories(string directoryPath, string filter, bool includeSubdirectories)
{
foreach (var kv in FileWatcherTokensToResolvePathFn)
{
directoryPath = directoryPath.Replace(kv.Key, kv.Value());
}
var directoryInfo = new DirectoryInfo(directoryPath);
if (!directoryInfo.Exists)
{
LoggerScoped.LogWarning($"FastScriptReload: Directory: '{directoryPath}' does not exist, make sure file-watcher setup is correct. You can access via: Window -> Fast Script Reload -> File Watcher (Advanced Setup)");
}
switch ((FileWatcherImplementation)FastScriptReloadPreference.FileWatcherImplementationInUse.GetEditorPersistedValueOrDefault())
{
case FileWatcherImplementation.UnityDefault:
var fileWatcher = new FileSystemWatcher();
fileWatcher.Path = directoryInfo.FullName;
fileWatcher.IncludeSubdirectories = includeSubdirectories;
fileWatcher.Filter = filter;
fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
fileWatcher.Changed += OnWatchedFileChange;
fileWatcher.EnableRaisingEvents = true;
_fileWatchers.Add(fileWatcher);
break;
#if UNITY_2021_1_OR_NEWER && UNITY_EDITOR_WIN
case FileWatcherImplementation.DirectWindowsApi:
// On Windows, this is a WindowsFileSystemWatcher.
// On other platforms, it's the default Mono implementation.
// The WindowsFileSystemWatcher has much lower latency on Windows.
// However, there's a small issue:
// The WindowsFileSystemWatcher can, theoretically, miss events.
// This is true in Microsoft's implementation as well as ours.
// (Actually, ours should be slightly better.)
// This can happen if a change occurs during the brief moment
// during which the previous batch of changes are being
// recorded and queued.
// It can also happen if too many changes occur at once, overwhelming
// the buffer.
// People seem to routinely use the basic MS filewatcher and ignore
// these issues, treating them as acceptably unlikely.
// Our current implementation here does that too, but we may want
// to look at eliminating this issue.
// Unfortunately, it's a limitation of the Windows API, and to
// my knowledge can't be avoided directly.
// The solution is to combine the file watcher with a polling
// mechanism which can (slowly, but reliably) catch any missed events.
var windowsFileSystemWatcher = new WindowsFileSystemWatcher()
{
Path = directoryInfo.FullName,
IncludeSubdirectories = includeSubdirectories,
Filter = filter,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName
};
windowsFileSystemWatcher.Changed += OnWatchedFileChange;
// Visual Studio is annoying.
// It doesn't actually trigger a nice Changed event.
// Instead, it goes through some elaborate procedure.
// When changing player.cs, it does:
// CREATE: Code\ua4tt1aw.4ae~
// CHANGE: Code\ua4tt1aw.4ae~
// CREATE: Code\Player.cs~RF70560f7.TMP
// REMOVE: Code\Player.cs~RF70560f7.TMP
// RENAME: Code\Player.cs -> Code\Player.cs~RF70560f7.TMP
// RENAME: Code\ua4tt1aw.4ae~ -> Code\Player.cs
// REMOVE: Code\Player.cs~RF70560f7.TMP - again, somehow?
//
// This was fine before, because the watcher implementation was polling.
// I guess this usually happens fast between polls, so it just looks like a change.
// Note that that may mean there's a potential bug if polling happens in the middle of this procedure.
//
// This fix is a temporary measure.
// We should probably think more seriously about maybe being able to catch file additions and renames.
// If we dealt with those extra events smoothly, this would probably just work.
//
// Other IDEs may do similar but different things. We want a general purpose solution, not a VS specific one.
// Perhaps the approach should be to keep track of touched files, then do some diff procedure to work out what's happened to them?
// This could, perhaps, be integrated into the file watcher robustness polling solution discussed above.
// The two systems share a need for some type of event debouncing.
windowsFileSystemWatcher.Renamed += (source, e) =>
{
if (e.Name.EndsWith(".cs"))
OnWatchedFileChange(source, e);
};
windowsFileSystemWatcher.EnableRaisingEvents = true;
_fileWatchers.Add(windowsFileSystemWatcher);
break;
#endif
case FileWatcherImplementation.CustomPolling:
CustomFileWatcher.InitializeSingularFilewatcher(directoryPath, filter, includeSubdirectories);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
static FastScriptReloadManager()
{
//do not add init code in here as with domain reload turned off it won't be properly set on play-mode enter, use Init method instead
EditorApplication.update += Instance.Update;
EditorApplication.playModeStateChanged += Instance.OnEditorApplicationOnplayModeStateChanged;
///if <see cref="FastScriptReloadPreference.WatchOnlySpecified"/> is enabled, disable auto reload automatically when launching editor. Will be enabled automatically when adding file watcher manually
if ((bool)FastScriptReloadPreference.WatchOnlySpecified.GetEditorPersistedValueOrDefault() && SessionState.GetBool("NEED_EDITOR_SESSION_INIT", true))
{
SessionState.SetBool("NEED_EDITOR_SESSION_INIT", false);
ClearFileWatchersEntries();
}
}
~FastScriptReloadManager()
{
LoggerScoped.LogDebug("Destroying FSR Manager ");
if (_instance != null)
{
if (_lastPlayModeStateChange == PlayModeStateChange.EnteredPlayMode)
{
LoggerScoped.LogError("Manager is being destroyed in play session, this indicates some sort of issue where static variables were reset, hot reload will not function properly please reset. " +
"This is usually caused by Unity triggering that reset for some reason that's outside of asset control - other static variables will also be affected and recovering just hot reload would hide wider issue.");
}
ClearFileWatchers();
}
}
private const string WatchSpecificFileOrFolderMenuItemName = "Assets/Fast Script Reload/Watch File\\Folder";
[MenuItem(WatchSpecificFileOrFolderMenuItemName, true, BaseMenuItemPriority_FileWatcher + 1)]
public static bool ToggleSelectionFileWatchersSetupValidation()
{
if (!(bool)FastScriptReloadPreference.WatchOnlySpecified.GetEditorPersistedValueOrDefault())
{
return false;
}
Menu.SetChecked(WatchSpecificFileOrFolderMenuItemName, false);
var isSelectionContaininingFolderOrScript = false;
for (var i = 0; i < Selection.objects.Length; i++)
{
if (Selection.objects[i] is MonoScript selectedMonoScript)
{
isSelectionContaininingFolderOrScript = true;
if (IsFileWatcherSetupEntryAlreadyPresent(selectedMonoScript))
{
Menu.SetChecked(WatchSpecificFileOrFolderMenuItemName, true);
break;
}
}
else if (Selection.objects[i] is DefaultAsset selectedAsset)
{
isSelectionContaininingFolderOrScript = true;
if (IsFileWatcherSetupEntryAlreadyPresent(selectedAsset))
{
Menu.SetChecked(WatchSpecificFileOrFolderMenuItemName, true);
break;
}
}
}
return isSelectionContaininingFolderOrScript;
}
/// <summary>Used to add/remove scripts/folders to the <see cref="FastScriptReloadPreference.FileWatcherSetupEntries"/></summary>
[MenuItem(WatchSpecificFileOrFolderMenuItemName, false, BaseMenuItemPriority_FileWatcher + 1)]
public static void ToggleSelectionFileWatchersSetup()
{
var isFileWatchersChange = false;
for (var i = 0; i < Selection.objects.Length; i++)
{
if (Selection.objects[i] is MonoScript selectedMonoScript)
{
if (IsFileWatcherSetupEntryAlreadyPresent(selectedMonoScript, out var foundFileWatcherSetupEntry))
{
FastScriptReloadPreference.FileWatcherSetupEntries.RemoveElement(JsonUtility.ToJson(foundFileWatcherSetupEntry));
}
else
{
FastScriptReloadPreference.FileWatcherSetupEntries.AddElement(JsonUtility.ToJson(foundFileWatcherSetupEntry));
}
isFileWatchersChange = true;
}
else if (Selection.objects[i] is DefaultAsset selectedAsset)
{
if (IsFileWatcherSetupEntryAlreadyPresent(selectedAsset, out var foundFileWatcherSetupEntry))
{
FastScriptReloadPreference.FileWatcherSetupEntries.RemoveElement(JsonUtility.ToJson(foundFileWatcherSetupEntry));
}
else
{
FastScriptReloadPreference.FileWatcherSetupEntries.AddElement(JsonUtility.ToJson(foundFileWatcherSetupEntry));
}
isFileWatchersChange = true;
}
}
if (isFileWatchersChange)
{
FastScriptReloadPreference.FileWatcherSetupEntriesChanged = true; // Ensures file watcher are updated in play mode
/// When in <see cref="FastScriptReloadPreference.WatchOnlySpecified"/> mode, <see cref="FastScriptReloadPreference.EnableAutoReloadForChangedFiles"/> state is managed automatically (disabled when no file watcher)
if ((bool)FastScriptReloadPreference.WatchOnlySpecified.GetEditorPersistedValueOrDefault())
{
var isAnyFileWatcherSet = FastScriptReloadPreference.FileWatcherSetupEntries.GetElements().Any();
FastScriptReloadPreference.EnableAutoReloadForChangedFiles.SetEditorPersistedValue(isAnyFileWatcherSet);
}
}
}
[MenuItem("Assets/Fast Script Reload/Clear Watched Files", true, BaseMenuItemPriority_FileWatcher + 2)]
public static bool ClearFastScriptReloadValidation()
{
if (!(bool)FastScriptReloadPreference.WatchOnlySpecified.GetEditorPersistedValueOrDefault())
{
return false;
}
return FastScriptReloadPreference.FileWatcherSetupEntries.GetElements().Any();
}
[MenuItem("Assets/Fast Script Reload/Clear Watched Files", false, BaseMenuItemPriority_FileWatcher + 2)]
public static void ClearFileWatchersEntries()
{
foreach (var item in FastScriptReloadPreference.FileWatcherSetupEntries.GetElements())
{
FastScriptReloadPreference.FileWatcherSetupEntries.RemoveElement(item);
}
Debug.LogWarning("File Watcher Setup has been cleared - make sure to add some.");
FastScriptReloadPreference.EnableAutoReloadForChangedFiles.SetEditorPersistedValue(false);
ClearFileWatchers();
}
[MenuItem("Assets/Fast Script Reload/Add \\ Open User Script Rewrite Override", false, BaseMenuItemPriority_ManualScriptOverride + 1)]
public static void AddHotReloadManualScriptOverride()
{
if (Selection.activeObject is MonoScript script)
{
ScriptGenerationOverridesManager.AddScriptOverride(script);
}
}
[MenuItem("Assets/Fast Script Reload/Add \\ Open User Script Rewrite Override", true)]
public static bool AddHotReloadManualScriptOverrideValidateFn()
{
return Selection.activeObject is MonoScript;
}
[MenuItem("Assets/Fast Script Reload/Remove User Script Rewrite Override", false, BaseMenuItemPriority_ManualScriptOverride + 2)]
public static void RemoveHotReloadManualScriptOverride()
{
if (Selection.activeObject is MonoScript script)
{
ScriptGenerationOverridesManager.TryRemoveScriptOverride(script);
}
}
[MenuItem("Assets/Fast Script Reload/Remove User Script Rewrite Override", true)]
public static bool RemoveHotReloadManualScriptOverrideValidateFn()
{
if (Selection.activeObject is MonoScript script)
{
return ScriptGenerationOverridesManager.TryGetScriptOverride(
new FileInfo(Path.Combine(Path.Combine(Application.dataPath + "//..", AssetDatabase.GetAssetPath(script)))),
out var _
);
}
return false;
}
[MenuItem("Assets/Fast Script Reload/Show User Script Rewrite Overrides", false, BaseMenuItemPriority_ManualScriptOverride + 3)]
public static void ShowManualScriptRewriteOverridesInUi()
{
var window = FastScriptReloadWelcomeScreen.Init();
window.OpenUserScriptRewriteOverridesSection();
}
[MenuItem("Assets/Fast Script Reload/Add Hot-Reload Exclusion", false, BaseMenuItemPriority_Exclusions + 1)]
public static void AddFileAsExcluded()
{
FastScriptReloadPreference.FilesExcludedFromHotReload.AddElement(ResolveRelativeToAssetDirectoryFilePath(Selection.activeObject));
}
[MenuItem("Assets/Fast Script Reload/Add Hot-Reload Exclusion", true)]
public static bool AddFileAsExcludedValidateFn()
{
return Selection.activeObject is MonoScript
&& !((FastScriptReloadPreference.FilesExcludedFromHotReload.GetEditorPersistedValueOrDefault() as IEnumerable<string>) ?? Array.Empty<string>())
.Contains(ResolveRelativeToAssetDirectoryFilePath(Selection.activeObject));
}
[MenuItem("Assets/Fast Script Reload/Remove Hot-Reload Exclusion", false, BaseMenuItemPriority_Exclusions + 2)]
public static void RemoveFileAsExcluded()
{
FastScriptReloadPreference.FilesExcludedFromHotReload.RemoveElement(ResolveRelativeToAssetDirectoryFilePath(Selection.activeObject));
}
[MenuItem("Assets/Fast Script Reload/Remove Hot-Reload Exclusion", true)]
public static bool RemoveFileAsExcludedValidateFn()
{
return Selection.activeObject is MonoScript
&& ((FastScriptReloadPreference.FilesExcludedFromHotReload.GetEditorPersistedValueOrDefault() as IEnumerable<string>) ?? Array.Empty<string>())
.Contains(ResolveRelativeToAssetDirectoryFilePath(Selection.activeObject));
}
[MenuItem("Assets/Fast Script Reload/Show Exclusions", false, BaseMenuItemPriority_Exclusions + 3)]
public static void ShowExcludedFilesInUi()
{
var window = FastScriptReloadWelcomeScreen.Init();
window.OpenExclusionsSection();
}
private static string ResolveRelativeToAssetDirectoryFilePath(UnityEngine.Object obj)
{
return AssetDatabase.GetAssetPath(obj.GetInstanceID());
}
public void Update()
{
_isEditorModeHotReloadEnabled = (bool)FastScriptReloadPreference.EnableExperimentalEditorHotReloadSupport.GetEditorPersistedValueOrDefault();
if (_lastPlayModeStateChange == PlayModeStateChange.ExitingPlayMode && Instance._fileWatchers.Any())
{
ClearFileWatchers();
}
if (!_isEditorModeHotReloadEnabled && !EditorApplication.isPlaying)
{
return;
}
if (_isEditorModeHotReloadEnabled)
{
EnsureInitialized();
}
else if (_lastPlayModeStateChange == PlayModeStateChange.EnteredPlayMode)
{
EnsureInitialized();
// if (_lastPlayModeStateChange != PlayModeStateChange.ExitingPlayMode && Application.isPlaying && Instance._fileWatchers.Count == 0 && FastScriptReloadPreference.FileWatcherSetupEntries.GetElementsTyped().Count > 0)
// {
// LoggerScoped.LogWarning("Reinitializing file-watchers as defined configuration does not match current instance setup. If hot reload still doesn't work you'll need to reset play session.");
// ClearFileWatchers();
// EnsureInitialized();
// }
}
AssignConfigValuesThatCanNotBeAccessedOutsideOfMainThread();
if (!_assemblyChangesLoaderResolverResolutionAlreadyCalled)
{
AssemblyChangesLoaderResolver.Instance.Resolve(); //WARN: need to resolve initially in case monobehaviour singleton is not created
_assemblyChangesLoaderResolverResolutionAlreadyCalled = true;
}
if ((bool)FastScriptReloadPreference.EnableAutoReloadForChangedFiles.GetEditorPersistedValueOrDefault() &&
(DateTime.UtcNow - _lastTimeChangeBatchRun).TotalSeconds > (int)FastScriptReloadPreference.BatchScriptChangesAndReloadEveryNSeconds.GetEditorPersistedValueOrDefault())
{
TriggerReloadForChangedFiles();
}
}
private static void ClearFileWatchers()
{
foreach (var fileWatcher in Instance._fileWatchers)
{
fileWatcher.Dispose();
}
Instance._fileWatchers.Clear();
}
private void AssignConfigValuesThatCanNotBeAccessedOutsideOfMainThread()
{
//TODO: PERF: needed in file watcher but when run on non-main thread causes exception.
_currentFileExclusions = FastScriptReloadPreference.FilesExcludedFromHotReload.GetElements();
_triggerDomainReloadIfOverNDynamicallyLoadedAssembles = (int)FastScriptReloadPreference.TriggerDomainReloadIfOverNDynamicallyLoadedAssembles.GetEditorPersistedValueOrDefault();
_isOnDemandHotReloadEnabled = (bool)FastScriptReloadPreference.EnableOnDemandReload.GetEditorPersistedValueOrDefault();
EnableExperimentalThisCallLimitationFix = (bool)FastScriptReloadPreference.EnableExperimentalThisCallLimitationFix.GetEditorPersistedValueOrDefault();
AssemblyChangesLoaderEditorOptionsNeededInBuild.UpdateValues(
(bool)FastScriptReloadPreference.IsDidFieldsOrPropertyCountChangedCheckDisabled.GetEditorPersistedValueOrDefault(),
(bool)FastScriptReloadPreference.EnableExperimentalAddedFieldsSupport.GetEditorPersistedValueOrDefault()
);
IsPartialClassSupportEnabled = (bool)FastScriptReloadPreference.IsPartialClassSupportEnabled.GetEditorPersistedValueOrDefault();
}
public void TriggerReloadForChangedFiles()
{
if (!Application.isPlaying && _hotReloadPerformedCount > _triggerDomainReloadIfOverNDynamicallyLoadedAssembles)
{
_hotReloadPerformedCount = 0;
LoggerScoped.LogWarning($"Dynamically created assembles reached over: {_triggerDomainReloadIfOverNDynamicallyLoadedAssembles} - triggering full domain reload to clean up. You can adjust that value in settings.");
#if UNITY_2019_3_OR_NEWER
CompilationPipeline.RequestScriptCompilation(); //TODO: add some timer to ensure this does not go into some kind of loop
#elif UNITY_2017_1_OR_NEWER
var editorAssembly = Assembly.GetAssembly(typeof(Editor));
var editorCompilationInterfaceType = editorAssembly.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface");
var dirtyAllScriptsMethod = editorCompilationInterfaceType.GetMethod("DirtyAllScripts", BindingFlags.Static | BindingFlags.Public);
dirtyAllScriptsMethod.Invoke(editorCompilationInterfaceType, null);
#endif
ClearLastProcessedDynamicFileHotReloadStates();
}
var assemblyChangesLoader = AssemblyChangesLoaderResolver.Instance.Resolve();
var changesAwaitingHotReload = _dynamicFileHotReloadStateEntries
.Where(e => e.IsAwaitingCompilation)
.ToList();
if (changesAwaitingHotReload.Any())
{
UpdateLastProcessedDynamicFileHotReloadStates(changesAwaitingHotReload);
foreach (var c in changesAwaitingHotReload)
{
c.IsBeingProcessed = true;
}
var unityMainThreadDispatcher = UnityMainThreadDispatcher.Instance.EnsureInitialized(); //need to pass that in, resolving on other than main thread will cause exception
Task.Run(() =>
{
List<string> sourceCodeFilesWithUniqueChangesAwaitingHotReload = null;
try
{
sourceCodeFilesWithUniqueChangesAwaitingHotReload = changesAwaitingHotReload
.GroupBy(e => e.FullFileName)
.Select(e => e.First().FullFileName).ToList();
var dynamicallyLoadedAssemblyCompilerResult = DynamicAssemblyCompiler.Compile(sourceCodeFilesWithUniqueChangesAwaitingHotReload, unityMainThreadDispatcher);
if (!dynamicallyLoadedAssemblyCompilerResult.IsError)
{
changesAwaitingHotReload.ForEach(c =>
{
c.FileCompiledOn = DateTime.UtcNow;
c.AssemblyNameCompiledIn = dynamicallyLoadedAssemblyCompilerResult.CompiledAssemblyPath;
});
//TODO: return some proper results to make sure entries are correctly updated
assemblyChangesLoader.DynamicallyUpdateMethodsForCreatedAssembly(dynamicallyLoadedAssemblyCompilerResult.CompiledAssembly, AssemblyChangesLoaderEditorOptionsNeededInBuild);
changesAwaitingHotReload.ForEach(c =>
{
c.HotSwappedOn = DateTime.UtcNow;
c.IsBeingProcessed = false;
}); //TODO: technically not all were hot swapped at same time
_hotReloadPerformedCount++;
SafeInvoke(HotReloadSucceeded, changesAwaitingHotReload);
}
else
{
if (dynamicallyLoadedAssemblyCompilerResult.MessagesFromCompilerProcess.Count > 0)
{
var msg = new StringBuilder();
foreach (string message in dynamicallyLoadedAssemblyCompilerResult.MessagesFromCompilerProcess)
{
msg.AppendLine($"Error when compiling, it's best to check code and make sure it's compilable \r\n {message}\n");
}
var errorMessage = msg.ToString();
changesAwaitingHotReload.ForEach(c =>
{
c.ErrorOn = DateTime.UtcNow;
c.ErrorText = errorMessage;
});
throw new Exception(errorMessage);
}
}
}
catch (Exception ex)
{
if (ex is SourceCodeHasErrorsException e)
LoggerScoped.LogError(e.Message + Environment.NewLine);
else
LoggerScoped.LogError($"Error when updating files: '{(sourceCodeFilesWithUniqueChangesAwaitingHotReload != null ? string.Join(",", sourceCodeFilesWithUniqueChangesAwaitingHotReload.Select(fn => new FileInfo(fn).Name)) : "unknown")}', {ex}");
changesAwaitingHotReload.ForEach(c =>
{
c.ErrorOn = DateTime.UtcNow;
c.ErrorText = ex.Message;
c.SourceCodeCombinedFilePath = (ex as HotReloadCompilationException)?.SourceCodeCombinedFileCreated;
});
SafeInvoke(HotReloadFailed, changesAwaitingHotReload);
}
});
}
_lastTimeChangeBatchRun = DateTime.UtcNow;
}
private void SafeInvoke(Action<List<DynamicFileHotReloadState>> ev, List<DynamicFileHotReloadState> changesAwaitingHotReload)
{
try
{
ev?.Invoke(changesAwaitingHotReload);
}
catch (Exception e)
{
Debug.LogError($"Error when executing event, {e}");
}
}
private void AddToLastProcessedDynamicFileHotReloadStates(DynamicFileHotReloadState c)
{
var assetGuid = AssetDatabaseHelper.AbsolutePathToGUID(c.FullFileName);
if (!string.IsNullOrEmpty(assetGuid))
{
_lastProcessedDynamicFileHotReloadStatesInSession[assetGuid] = c;
}
}
private void ClearLastProcessedDynamicFileHotReloadStates()
{
_lastProcessedDynamicFileHotReloadStatesInSession.Clear();
}
//Success entries will always be cleared - errors will remain till another change fixes them
private void UpdateLastProcessedDynamicFileHotReloadStates(List<DynamicFileHotReloadState> changesToHotReload)
{
var succeededReloads = _lastProcessedDynamicFileHotReloadStatesInSession
.Where(s => s.Value.IsChangeHotSwapped).ToList();
foreach (var kv in succeededReloads)
{
_lastProcessedDynamicFileHotReloadStatesInSession.Remove(kv.Key);
}
foreach (var changeToHotReload in changesToHotReload)
{
AddToLastProcessedDynamicFileHotReloadStates(changeToHotReload);
}
}
private void OnEditorApplicationOnplayModeStateChanged(PlayModeStateChange obj)
{
Instance._lastPlayModeStateChange = obj;
if ((bool)FastScriptReloadPreference.IsForceLockAssembliesViaCode.GetEditorPersistedValueOrDefault())
{
if (obj == PlayModeStateChange.EnteredPlayMode)
{
EditorApplication.LockReloadAssemblies();
_wasLockReloadAssembliesCalled = true;
}
}
if(obj == PlayModeStateChange.EnteredEditMode && _wasLockReloadAssembliesCalled)
{
EditorApplication.UnlockReloadAssemblies();
_wasLockReloadAssembliesCalled = false;
}
}
private static bool TryWorkaroundForUnityFileWatcherBug(FileSystemEventArgs e, ref string filePathToUse)
{
LoggerScoped.LogWarning(@"Fast Script Reload - Unity File Path Bug - Warning!
Path for changed file passed by Unity does not exist. This is a known editor bug, more info: https://issuetracker.unity3d.com/issues/filesystemwatcher-returns-bad-file-path
Best course of action is to update editor as issue is already fixed in newer (minor and major) versions.
As a workaround asset will try to resolve paths via directory search.
Workaround will search in all folders (under project root) and will use first found file. This means it's possible it'll pick up wrong file as there's no directory information available.");
var changedFileName = new FileInfo(filePathToUse).Name;
//TODO: try to look in all file watcher configured paths, some users might have code outside of assets, eg packages
// var fileFoundInAssets = FastScriptReloadPreference.FileWatcherSetupEntries.GetElementsTyped().SelectMany(setupEntries => Directory.GetFiles(DataPath, setupEntries.path, SearchOption.AllDirectories)).ToList();
var fileFoundInAssets = Directory.GetFiles(DataPath, changedFileName, SearchOption.AllDirectories);
if (fileFoundInAssets.Length == 0)
{
LoggerScoped.LogError($"FileWatcherBugWorkaround: Unable to find file '{changedFileName}', changes will not be reloaded. Please update unity editor.");
return false;
}
else if (fileFoundInAssets.Length == 1)
{
LoggerScoped.Log($"FileWatcherBugWorkaround: Original Unity passed file path: '{e.FullPath}' adjusted to found: '{fileFoundInAssets[0]}'");
filePathToUse = fileFoundInAssets[0];
return true;
}
else
{
LoggerScoped.LogWarning($"FileWatcherBugWorkaround: Multiple files found. Original Unity passed file path: '{e.FullPath}' adjusted to found: '{fileFoundInAssets[0]}'");
filePathToUse = fileFoundInAssets[0];
return true;
}
}
private static bool HotReloadDisabled_WarningMessageShownAlready;
private static void EnsureInitialized()
{
if (!(bool)FastScriptReloadPreference.EnableAutoReloadForChangedFiles.GetEditorPersistedValueOrDefault()
&& !(bool)FastScriptReloadPreference.EnableOnDemandReload.GetEditorPersistedValueOrDefault()
&& !(bool)FastScriptReloadPreference.WatchOnlySpecified.GetEditorPersistedValueOrDefault())
{
if (!HotReloadDisabled_WarningMessageShownAlready)
{
LoggerScoped.LogWarning($"Neither auto hot reload / on-demand reload / or watch specific is specified, file watchers will not be initialized. Please adjust settings and restart if you want hot reload to work.");
HotReloadDisabled_WarningMessageShownAlready = true;
}
return;
}
var isUsingCustomFileWatchers = (FileWatcherImplementation)FastScriptReloadPreference.FileWatcherImplementationInUse.GetEditorPersistedValueOrDefault()
== FileWatcherImplementation.CustomPolling;
if (!isUsingCustomFileWatchers)
{
if (Instance._fileWatchers.Count == 0 || FastScriptReloadPreference.FileWatcherSetupEntriesChanged)
{
FastScriptReloadPreference.FileWatcherSetupEntriesChanged = false;
InitializeFromFileWatcherSetupEntries();
}
}
else if(!CustomFileWatcher.InitSignaled)
{
CustomFileWatcher.TryEnableLivewatching();
InitializeFromFileWatcherSetupEntries();
CustomFileWatcher.InitSignaled = true;
}
}
private static void InitializeFromFileWatcherSetupEntries()
{
var fileWatcherSetupEntries = FastScriptReloadPreference.FileWatcherSetupEntries.GetElementsTyped();
if (fileWatcherSetupEntries.Count == 0)
{
LoggerScoped.LogWarning($"There are no file watcher setup entries. Tool will not be able to pick changes automatically");
}
foreach (var fileWatcherSetupEntry in fileWatcherSetupEntries)
{
Instance.StartWatchingDirectoryAndSubdirectories(
fileWatcherSetupEntry.path,
fileWatcherSetupEntry.filter,
fileWatcherSetupEntry.includeSubdirectories
);
}
}
private static bool IsFileWatcherSetupEntryAlreadyPresent(FileWatcherSetupEntry fileWatcherSetupEntry)
{
//TODO: could be a bit of a per hit, GetElementsTypes will parse json every time
return FastScriptReloadPreference.FileWatcherSetupEntries.GetElementsTyped()
.Any(e => e.path == fileWatcherSetupEntry.path
&& e.filter == fileWatcherSetupEntry.filter
&& e.includeSubdirectories == fileWatcherSetupEntry.includeSubdirectories);
}
private static bool IsFileWatcherSetupEntryAlreadyPresent(DefaultAsset selectedAsset)
{
FileWatcherSetupEntry fileWatcherSetupEntry;
return IsFileWatcherSetupEntryAlreadyPresent(selectedAsset, out fileWatcherSetupEntry);
}
private static bool IsFileWatcherSetupEntryAlreadyPresent(DefaultAsset selectedAsset, out FileWatcherSetupEntry fileWatcherSetupEntry)
{
var path = FileWatcherReplacementTokenForApplicationDataPath + AssetDatabase.GetAssetPath(selectedAsset).Remove(0, "Assets".Length);
fileWatcherSetupEntry = new FileWatcherSetupEntry(path, "*.cs", true);
var isFileWatcherSetupEntryAlreadyPresent = IsFileWatcherSetupEntryAlreadyPresent(fileWatcherSetupEntry);
return isFileWatcherSetupEntryAlreadyPresent;
}
private static bool IsFileWatcherSetupEntryAlreadyPresent(MonoScript selectedMonoScript)
{
FileWatcherSetupEntry fileWatcherSetupEntry;
return IsFileWatcherSetupEntryAlreadyPresent(selectedMonoScript, out fileWatcherSetupEntry);
}
private static bool IsFileWatcherSetupEntryAlreadyPresent(MonoScript selectedMonoScript, out FileWatcherSetupEntry fileWatcherSetupEntry)
{
var path = FileWatcherReplacementTokenForApplicationDataPath + AssetDatabase.GetAssetPath(selectedMonoScript).Remove(0, "Assets".Length);
var fileSeperatorIndex = path.LastIndexOf('/');
var fileName = path.Substring(fileSeperatorIndex + 1);
path = path.Substring(0, fileSeperatorIndex);
fileWatcherSetupEntry = new FileWatcherSetupEntry(path, fileName, false);
var isFileWatcherSetupEntryAlreadyPresent = IsFileWatcherSetupEntryAlreadyPresent(fileWatcherSetupEntry);
return isFileWatcherSetupEntryAlreadyPresent;
}
}
public class DynamicFileHotReloadState
{
public string FullFileName { get; set; }
public DateTime FileChangedOn { get; set; }
public bool IsAwaitingCompilation => !IsFileCompiled && !ErrorOn.HasValue && !IsBeingProcessed;
public bool IsFileCompiled => FileCompiledOn.HasValue;
public DateTime? FileCompiledOn { get; set; }
public string AssemblyNameCompiledIn { get; set; }
public bool IsAwaitingHotSwap => IsFileCompiled && !HotSwappedOn.HasValue;
public DateTime? HotSwappedOn { get; set; }
public bool IsChangeHotSwapped => HotSwappedOn.HasValue;
public string ErrorText { get; set; }
public DateTime? ErrorOn { get; set; }
public bool IsFailed => ErrorOn.HasValue;
public bool IsBeingProcessed { get; set; }
public string SourceCodeCombinedFilePath { get; set; }
public DynamicFileHotReloadState(string fullFileName, DateTime fileChangedOn)
{
FullFileName = fullFileName;
FileChangedOn = fileChangedOn;
}
}
public enum FileWatcherImplementation
{
UnityDefault = 0,
#if UNITY_EDITOR_WIN && UNITY_2021_1_OR_NEWER
DirectWindowsApi = 1,
#endif
CustomPolling = 2
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 6bcc689ed0625e04d8117933c1ccadd8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/FastScriptReloadManager.cs
uploadId: 710802
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c6a8c4c57b66cb149b99992f6f35e74e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/FastScriptReloadWelcomeScreen.cs
uploadId: 710802
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using ImmersiveVRTools.Editor.Common.WelcomeScreen.PreferenceDefinition;
namespace FastScriptReload.Editor
{
[Serializable]
public class FileWatcherSetupEntry: JsonObjectListSerializable<FileWatcherSetupEntry>
{
public string path;
public string filter;
public bool includeSubdirectories;
public FileWatcherSetupEntry(string path, string filter, bool includeSubdirectories)
{
this.path = path;
this.filter = filter;
this.includeSubdirectories = includeSubdirectories;
}
[Obsolete("Serialization required")]
public FileWatcherSetupEntry()
{
}
public override List<IJsonObjectRepresentationRenderingInfo> GenerateRenderingInfo()
{
return new List<IJsonObjectRepresentationRenderingInfo>
{
new JsonObjectRepresentationStringRenderingInfo<FileWatcherSetupEntry>("Path", (e) => e.path, (o, val) => o.path = val, 200),
new JsonObjectRepresentationStringRenderingInfo<FileWatcherSetupEntry>("Filter", (e) => e.filter, (o, val) => o.filter = val, 100),
new JsonObjectRepresentationBoolRenderingInfo<FileWatcherSetupEntry>("Include Subdirectories", (e) => e.includeSubdirectories, (o, val) => o.includeSubdirectories = val, 145),
};
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 126f0c053848416c88113f41793c7310
timeCreated: 1672215630
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/FileWatcherSetupEntry.cs
uploadId: 710802
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a4c284ed992340c389060b32cade29bc
timeCreated: 1679048638
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using ImmersiveVRTools.Editor.Common.Utilities;
using ImmersiveVRTools.Runtime.Common;
using UnityEditor;
using UnityEngine;
namespace FastScriptReload.Editor.GUI
{
[InitializeOnLoad]
public class ProjectWindowReloadStatusIndicatorDrawer
{
private static Texture HelpTexture;
private static bool IsEnabled;
static ProjectWindowReloadStatusIndicatorDrawer()
{
EditorApplication.projectWindowItemOnGUI += ProjectWindowItemOnGUI;
FastScriptReloadManager.Instance.HotReloadFailed += OnHotReloadFailed;
EditorApplication.update += () =>
{
IsEnabled = (bool)FastScriptReloadPreference.IsVisualHotReloadIndicationShownInProjectWindow.GetEditorPersistedValueOrDefault();
};
}
private static void OnHotReloadFailed(List<DynamicFileHotReloadState> failedForFileStates)
{
if (!IsEnabled)
{
return;
}
UnityMainThreadDispatcher.Instance.Enqueue(() =>
{
var lastFailed = failedForFileStates.LastOrDefault();
if (lastFailed != null)
{
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(AssetDatabaseHelper.AbsolutePathToAssetPath(lastFailed.FullFileName));
EditorGUIUtility.PingObject(script);
}
});
}
private static void ProjectWindowItemOnGUI(string guid, Rect rect)
{
if (!IsEnabled)
{
return;
}
if (HelpTexture == null)
{
HelpTexture = EditorGUIUtility.TrIconContent("_Help").image;
}
if (FastScriptReloadManager.Instance.LastProcessedDynamicFileHotReloadStatesInSession.TryGetValue(guid, out var fileHotReloadState))
{
var sideLineRect = new Rect(1, rect.y, 2, rect.height);
if (fileHotReloadState.IsFailed)
{
EditorGUI.DrawRect(sideLineRect, Color.red);
var sideInfoRect = new Rect(1, rect.y - 3, 20, 20);
if (UnityEngine.GUI.Button(sideInfoRect,
new GUIContent(new GUIContent(HelpTexture,
@"Fast Script Reload - Error
Last hot reload failed.
Please click for more details.")),
EditorStyles.linkLabel))
{
FastScriptReloadWelcomeScreen.Init().OpenInspectError(fileHotReloadState);
};
}
else if (fileHotReloadState.IsChangeHotSwapped)
{
EditorGUI.DrawRect(sideLineRect, Color.green);
}
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f4248f4ad21143d296208aa98ad3cfdf
timeCreated: 1679048660
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/GUI/ProjectWindowReloadStatusIndicatorDrawer.cs
uploadId: 710802
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 851525d631384c1f8c48bd7157252046
timeCreated: 1673020744
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using FastScriptReload.Editor.Compilation.CodeRewriting;
using FastScriptReload.Runtime;
using FastScriptReload.Scripts.Runtime;
using HarmonyLib;
using ImmersiveVRTools.Editor.Common.Utilities;
using UnityEditor;
using UnityEngine;
namespace FastScriptReload.Editor.NewFields
{
[InitializeOnLoad]
public class NewFieldsRendererDefaultEditorPatch
{
private static List<string> _cachedKeys = new List<string>();
static NewFieldsRendererDefaultEditorPatch()
{
if ((bool)FastScriptReloadPreference.EnableExperimentalAddedFieldsSupport.GetEditorPersistedValueOrDefault())
{
var harmony = new Harmony(nameof(NewFieldsRendererDefaultEditorPatch));
var renderAdditionalFieldsOnOptimizedGuiPostfix = AccessTools.Method(typeof(NewFieldsRendererDefaultEditorPatch), nameof(OnOptimizedInspectorGUI));
var noCustomEditorOriginalRenderingMethdod = AccessTools.Method("UnityEditor.GenericInspector:OnOptimizedInspectorGUI");
harmony.Patch(noCustomEditorOriginalRenderingMethdod, postfix: new HarmonyMethod(renderAdditionalFieldsOnOptimizedGuiPostfix));
var renderAdditionalFieldsDrawDefaultInspectorPostfix = AccessTools.Method(typeof(NewFieldsRendererDefaultEditorPatch), nameof(DrawDefaultInspector));
var customEditorRenderingMethod = AccessTools.Method("UnityEditor.Editor:DrawDefaultInspector");
harmony.Patch(customEditorRenderingMethod, postfix: new HarmonyMethod(renderAdditionalFieldsDrawDefaultInspectorPostfix));
#if ODIN_INSPECTOR
// Odin Inspector support
var renderAdditionalFieldsDrawOdinInspectorPostfix = AccessTools.Method(typeof(NewFieldsRendererDefaultEditorPatch), nameof(DrawOdinInspector));
var customOdinEditorRenderingMethod = AccessTools.Method("Sirenix.OdinInspector.Editor.OdinEditor:DrawOdinInspector");
harmony.Patch(customOdinEditorRenderingMethod, postfix: new HarmonyMethod(renderAdditionalFieldsDrawOdinInspectorPostfix));
#endif
}
}
#if ODIN_INSPECTOR
private static void DrawOdinInspector(UnityEditor.Editor __instance)
{
RenderNewlyAddedFields(__instance);
}
#endif
private static void OnOptimizedInspectorGUI(Rect contentRect, UnityEditor.Editor __instance)
{
RenderNewlyAddedFields(__instance);
}
private static void DrawDefaultInspector(UnityEditor.Editor __instance)
{
RenderNewlyAddedFields(__instance);
}
private static void RenderNewlyAddedFields(UnityEditor.Editor __instance)
{
//TODO: perf optimize, this will be used for many types, perhaps keep which types changed and just pass type?
if (__instance.target)
{
if (TemporaryNewFieldValues.TryGetDynamicallyAddedFieldValues(__instance.target, out var addedFieldValues))
{
EditorGUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("[FSR] Dynamically Added Fields:");
GuiTooltipHelper.AddHelperTooltip("Fields were dynamically added for hot-reload, you can adjust their values and on full reload they'll disappear from this section and move back to main one.");
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
try
{
_cachedKeys.AddRange(addedFieldValues.Keys); //otherwise collection changed exception can happen
var newFieldNameToGetTypeFn = CreateNewFieldInitMethodRewriter.ResolveNewFieldsToTypeFn(
AssemblyChangesLoader.Instance.GetRedirectedType(__instance.target.GetType())
);
if(newFieldNameToGetTypeFn.Count == 0)
return;
foreach (var addedFieldValueKey in _cachedKeys)
{
var newFieldType = (Type)newFieldNameToGetTypeFn[addedFieldValueKey]();
//rendering types come from UnityEditor.EditorGUI.DefaultPropertyField - that should handle all cases that editor can render
if (newFieldType == typeof(int)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.IntField(addedFieldValueKey, (int)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(bool)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.Toggle(addedFieldValueKey, (bool)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(float)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.FloatField(addedFieldValueKey, (float)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(string)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.TextField(addedFieldValueKey, (string)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Color)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.ColorField(addedFieldValueKey, (Color)addedFieldValues[addedFieldValueKey]);
//TODO: SerializedPropertyType.LayerMask
else if (newFieldType == typeof(Enum)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.EnumPopup(addedFieldValueKey, (Enum)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Vector2)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.Vector2Field(addedFieldValueKey, (Vector2)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Vector3)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.Vector3Field(addedFieldValueKey, (Vector3)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Vector4)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.Vector4Field(addedFieldValueKey, (Vector4)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Rect)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.RectField(addedFieldValueKey, (Rect)addedFieldValues[addedFieldValueKey]);
//TODO: SerializedPropertyType.ArraySize
//TODO: SerializedPropertyType.Character
// else if (existingValueType == typeof(char)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.TextField(addedFieldValueKey, (char)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(AnimationCurve)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.CurveField(addedFieldValueKey, (AnimationCurve)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Bounds)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.BoundsField(addedFieldValueKey, (Bounds)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Gradient)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.GradientField(addedFieldValueKey, (Gradient)addedFieldValues[addedFieldValueKey]);
//TODO: SerializedPropertyType.FixedBufferSize
else if (newFieldType == typeof(Vector2Int)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.Vector2IntField(addedFieldValueKey, (Vector2Int)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(Vector3Int)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.Vector3IntField(addedFieldValueKey, (Vector3Int)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(RectInt)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.RectIntField(addedFieldValueKey, (RectInt)addedFieldValues[addedFieldValueKey]);
else if (newFieldType == typeof(BoundsInt)) addedFieldValues[addedFieldValueKey] = EditorGUILayout.BoundsIntField(addedFieldValueKey, (BoundsInt)addedFieldValues[addedFieldValueKey]);
//TODO: SerializedPropertyType.Hash128
else if (newFieldType == typeof(Quaternion))
{
//Quaternions are rendered as euler angles in editor
addedFieldValues[addedFieldValueKey] = Quaternion.Euler(EditorGUILayout.Vector3Field(addedFieldValueKey, ((Quaternion)addedFieldValues[addedFieldValueKey]).eulerAngles));
}
else if (typeof(UnityEngine.Object).IsAssignableFrom(newFieldType))
{
addedFieldValues[addedFieldValueKey] = EditorGUILayout.ObjectField(new GUIContent(addedFieldValueKey), (UnityEngine.Object)addedFieldValues[addedFieldValueKey], newFieldType, __instance.target);
}
else
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"{newFieldType.Name} - Unable to render");
GuiTooltipHelper.AddHelperTooltip(
$"Unable to handle added-field rendering for type: {newFieldType.Name}, it won't be rendered. Best workaround is to not add this type dynamically in current version.");
EditorGUILayout.EndHorizontal();
}
}
}
finally
{
_cachedKeys.Clear();
}
}
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 4a118f6ef99b4cf5b207a27cb07d9cf2
timeCreated: 1673020744
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/NewFields/NewFieldsRendererDefaultEditorPatch.cs
uploadId: 710802
@@ -0,0 +1,411 @@
#if UNITY_2021_1_OR_NEWER
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Enumeration;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace FastScriptReload.Editor
{
/// <summary>
/// This is a Windows only file watcher, for use in Unity/Mono.
/// Mono already has the cross platform FileSystemWatcher.
/// However, it's incredibly slow on Windows.
/// This one is fast.
///
/// This doesn't include the complete API surface of FileSystemWatcher,
/// but those bits that are present should be compatible with FileSystemWatcher.
///
/// Events will be dispatched on a worker thread.
/// They may be on different threads from each other, but they won't overlap in time.
///
/// There is an issue where there's a (small) chance that events may be missed.
///
/// Firstly, this can happen if the event occurs in the brief time when previous events
/// are being recorded, before listening can start again.
/// This is not unique to this implementation - the Microsoft version has the same problem.
/// The issue should actually be a little less bad here. Microsoft fires its events on the
/// monitoring thread, and relies on the user to be smart about offloading them.
/// We fire our events on a dedicated event thread. Long running handlers shouldn't
/// pose a problem.
///
/// Secondly, this can also happen if the internal buffer used by the Windows API overflows.
/// It's set to the maximum size (which is larger than the Microsoft implementation default)
/// but it could theoretically happen.
///
/// The solution to both of these things, if we want to be completely robust, is to combine
/// the file watcher with polling to catch rare missed events.
/// </summary>
internal sealed class WindowsFileSystemWatcher : IDisposable
{
public event FileSystemEventHandler Changed;
public event FileSystemEventHandler Created;
public event FileSystemEventHandler Deleted;
public event RenamedEventHandler Renamed;
public event ErrorEventHandler Error;
public NotifyFilters NotifyFilter { get; set; } = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
public string Filter { get; set; } = "*.*";
public bool IncludeSubdirectories { get; set; } = false;
public string Path
{
get => this.path;
set
{
var changed = value != this.path;
this.path = value;
// Restart if the path changes.
if (changed && this.EnableRaisingEvents)
{
this.EnableRaisingEvents = false;
this.EnableRaisingEvents = true;
}
}
}
private string path;
private InterruptibleHandle currentHandle;
private Task monitorTask;
private Task eventsTask;
private bool disposed = false;
private readonly WeakDisposer weakDisposer;
public WindowsFileSystemWatcher()
{
this.eventsTask = Task.CompletedTask;
this.weakDisposer = new WeakDisposer(this);
AppDomain.CurrentDomain.DomainUnload += this.weakDisposer.Dispose;
#if UNITY_EDITOR
UnityEditor.EditorApplication.quitting += this.weakDisposer.Dispose;
UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += this.weakDisposer.Dispose;
#endif
}
// Note that the GC shouldn't ever destroy the FSW while it's running,
// even if no references to it are retained by the user.
// The monitoring thread holds a reference.
~WindowsFileSystemWatcher()
{
this.Dispose();
}
public void Dispose()
{
if (this.disposed) return;
this.disposed = true;
GC.SuppressFinalize(this);
this.EnableRaisingEvents = false;
this.Changed = null;
this.Created = null;
this.Deleted = null;
this.Renamed = null;
this.Error = null;
AppDomain.CurrentDomain.DomainUnload -= this.weakDisposer.Dispose;
#if UNITY_EDITOR
UnityEditor.EditorApplication.quitting -= this.weakDisposer.Dispose;
UnityEditor.AssemblyReloadEvents.beforeAssemblyReload -= this.weakDisposer.Dispose;
#endif
}
public bool EnableRaisingEvents
{
get => this.currentHandle != null;
set
{
if (value)
{
if (this.disposed) throw new ObjectDisposedException(nameof(WindowsFileSystemWatcher));
if (this.currentHandle != null) return;
this.currentHandle = CreateDirectoryHandle(this.Path);
this.monitorTask = Task.Factory.StartNew(() => this.Monitor(this.currentHandle), TaskCreationOptions.LongRunning);
}
else
{
// This cancels scheduled-but-unrun events, because they don't run if the handle is closed.
this.currentHandle?.Dispose();
this.currentHandle = null;
this.monitorTask?.Wait();
this.monitorTask = null;
// We don't wait for the events task, because we might be within the events task.
// (Ooh, the events are coming from WITHIN THE TASK. Scary.)
// This does leave a tiny chance that a single event could be triggered immediately after this,
// if we're not in the events task...
// TODO: Maybe fix this
}
}
}
private static InterruptibleHandle CreateDirectoryHandle(string directory)
{
const int FILE_LIST_DIRECTORY = 0x0001;
const int FILE_SHARE_READ = 0x00000001;
const int FILE_SHARE_WRITE = 0x00000002;
const int FILE_SHARE_DELETE = 0x00000004;
const int OPEN_EXISTING = 3;
const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
// There might be a way to do this without the OS call?
var directoryHandle = CreateFile(
lpFileName: directory,
dwDesiredAccess: FILE_LIST_DIRECTORY,
dwShareMode: FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
lpSecurityAttributes: null,
dwCreationDisposition: OPEN_EXISTING,
dwFlagsAndAttributes: FILE_FLAG_BACKUP_SEMANTICS,
hTemplateFile: new SafeFileHandle(IntPtr.Zero, false)
);
if (directoryHandle == null || directoryHandle.IsInvalid)
throw new IOException("Failed to obtain handle for directory.");
return new InterruptibleHandle(directoryHandle);
}
private unsafe void Monitor(InterruptibleHandle handle)
{
// We try to minimise the processing time taken by the monitoring thread.
// The longer it takes, the more likely a buffer overflow.
// (At our 64*1028 buffer size we're probably 99.9% fine anyway. Famous last words...)
// We do this by pushing processing immediately to another thread.
// We swap out the buffers instead of spending any time reading them.
// This is probably a silly micro optimisation, but it feels like "the right way".
const int MaxBufferPoolSize = 32; // Huge
var bufferPool = new Stack<byte[]>(MaxBufferPoolSize);
while (handle.IsOpen)
{
byte[] buffer;
lock (bufferPool) if (!bufferPool.TryPop(out buffer)) buffer = new byte[64 * 1024];
fixed (byte* bufferPointer = buffer)
{
bool ok = false;
int size = 0;
try
{
ok = ReadDirectoryChangesW(
hDirectory: handle,
lpBuffer: new HandleRef(buffer, (IntPtr)bufferPointer),
nBufferLength: buffer.Length,
bWatchSubtree: this.IncludeSubdirectories ? 1 : 0,
dwNotifyFilter: (int)this.NotifyFilter,
lpBytesReturned: out size,
overlappedPointer: null,
lpCompletionRoutine: new HandleRef(null, IntPtr.Zero)
);
}
// The directory handle could be disposed from another thread.
// That's fine, we'll just end.
catch (ObjectDisposedException) { }
catch (ArgumentNullException) { }
catch (Exception ex)
{
DispatchError(ex);
}
if (!handle.IsOpen)
break;
if (!ok)
DispatchError(new Win32Exception());
if (size == 0)
DispatchError(new InternalBufferOverflowException($"Too many changes at once in directory: {this.Path}."));
}
// Let's prevent event dispatches from overlapping or being out of order,
// because this is closer to FileSystemWatcher's behaviour.
// Overlapping/OOO would be a pretty easy way to get a nasty bug in user code.
this.eventsTask = this.eventsTask.ContinueWith(_ =>
{
this.ProcessBufferOnEventThread(handle, buffer);
// Return to pool, preventing strange leak scenarios where the pool grows unreasonably large.
// That could happen if the event threads run for a very long time.
lock (bufferPool)
{
if (bufferPool.Count < MaxBufferPoolSize) bufferPool.Push(buffer);
}
});
}
handle.Dispose();
void DispatchError(Exception ex)
{
this.eventsTask = this.eventsTask.ContinueWith(_ => this.Error?.Invoke(this, new ErrorEventArgs(ex)));
}
}
private void ProcessBufferOnEventThread(InterruptibleHandle handle, ReadOnlySpan<byte> buffer)
{
ReadOnlySpan<char> oldName = default;
bool oldMatch = false;
while (true)
{
if (handle == null || !handle.IsOpen) break;
// We're dealing with file names as Spans for two reasons:
// - FileSystemName wants them that way.
// - We can avoid the string allocation for files that don't match.
var nextEntryOffset = MemoryMarshal.Read<int>(buffer);
var action = MemoryMarshal.Read<Action>(buffer.Slice(4));
var nameLength = MemoryMarshal.Read<int>(buffer.Slice(8));
var name = MemoryMarshal.Cast<byte, char>(buffer.Slice(12, nameLength));
buffer = buffer.Slice(nextEntryOffset);
var match = string.IsNullOrEmpty(this.Filter) || FileSystemName.MatchesSimpleExpression(this.Filter, name);
try
{
switch (action)
{
case Action.RenamedOld:
oldName = name;
oldMatch = match;
break;
case Action.RenamedNew:
if (match | oldMatch)
this.Renamed?.Invoke(this, new RenamedEventArgs(WatcherChangeTypes.Renamed, this.Path, name.ToString(), oldName.ToString()));
break;
default:
if (!match) break;
var nameStr = name.ToString();
switch (action)
{
case Action.Added:
this.Created?.Invoke(this, new FileSystemEventArgs(WatcherChangeTypes.Created, this.Path, nameStr));
break;
case Action.Modified:
this.Changed?.Invoke(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, this.Path, nameStr));
break;
case Action.Removed:
this.Deleted?.Invoke(this, new FileSystemEventArgs(WatcherChangeTypes.Deleted, this.Path, nameStr));
break;
}
break;
}
}
catch (Exception ex)
{
this.Error?.Invoke(this, new ErrorEventArgs(new Exception("Exception in event handler.", ex)));
}
if (nextEntryOffset == 0) break;
}
}
private class InterruptibleHandle : IDisposable
{
public SafeFileHandle Handle { get; }
public bool IsOpen => !this.closed & !this.Handle.IsInvalid & !this.Handle.IsClosed;
private bool closed;
public InterruptibleHandle(SafeFileHandle handle)
{
this.Handle = handle;
}
~InterruptibleHandle() {
this.Dispose();
}
public unsafe void Dispose()
{
this.closed = true;
if (!(this.Handle.IsClosed)) CancelIoEx(this.Handle, null);
this.Handle.Dispose();
GC.SuppressFinalize(this);
}
public static implicit operator SafeFileHandle(InterruptibleHandle handle)
{
return handle.Handle;
}
}
/// <summary>
/// Dispose handles are setup via weak references.
/// We don't want the domain reload stuff to keep the FSW alive.
/// Note that the FSW shouldn't be collected whilst running anyway.
/// </summary>
private sealed class WeakDisposer
{
private readonly WeakReference<WindowsFileSystemWatcher> fsw;
public WeakDisposer(WindowsFileSystemWatcher fsw)
=> this.fsw = new WeakReference<WindowsFileSystemWatcher>(fsw);
public void Dispose()
{
if (this.fsw.TryGetTarget(out var fsw)) fsw.Dispose();
}
public void Dispose(object _, EventArgs __) => this.Dispose();
}
#region Windows API
private enum Action
{
Added = 1,
Removed = 2,
Modified = 3,
RenamedOld = 4,
RenamedNew = 5,
}
[DllImport("__Internal", CharSet = CharSet.Auto, BestFitMapping = false)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
int dwDesiredAccess,
int dwShareMode,
SECURITY_ATTRIBUTES lpSecurityAttributes,
int dwCreationDisposition,
int dwFlagsAndAttributes,
SafeFileHandle hTemplateFile
);
[DllImport("__Internal", EntryPoint = "ReadDirectoryChangesW", CharSet = CharSet.Auto, SetLastError = true)]
private static extern unsafe bool ReadDirectoryChangesW(
SafeFileHandle hDirectory,
HandleRef lpBuffer,
int nBufferLength,
int bWatchSubtree,
int dwNotifyFilter,
out int lpBytesReturned,
NativeOverlapped* overlappedPointer,
HandleRef lpCompletionRoutine
);
[DllImport("__Internal")]
private static extern unsafe bool CancelIoEx(SafeHandle handle, NativeOverlapped* lpOverlapped);
private class SECURITY_ATTRIBUTES { }
#endregion
}
}
#endif
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 661bb1e104ae76e48b5ab73d84174ae0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 239351
packageName: Fast Script Reload
packageVersion: 1.6.1
assetPath: Assets/FastScriptReload/Scripts/Editor/WindowsFileSystemWatcher.cs
uploadId: 710802