diff --git a/Nerfed.Editor/Project/EditorAssemblyLoader.cs b/Nerfed.Editor/Project/EditorAssemblyLoader.cs index cd1b49d..65f668f 100644 --- a/Nerfed.Editor/Project/EditorAssemblyLoader.cs +++ b/Nerfed.Editor/Project/EditorAssemblyLoader.cs @@ -10,7 +10,7 @@ namespace Nerfed.Editor.Project; internal static class EditorAssemblyLoader { - private sealed class EditorAssemblyLoadContextWrapper + internal sealed class EditorAssemblyLoadContextWrapper { private EditorAssemblyLoadContext assemblyLoadContext; private readonly WeakReference weakReference; @@ -44,6 +44,16 @@ public static (Assembly, EditorAssemblyLoadContextWrapper) CreateAndLoad(Assembl return (assembly, wrapper); } + [MethodImpl(MethodImplOptions.NoInlining)] + public static (Assembly, EditorAssemblyLoadContextWrapper) CreateAndLoad(string assemblyPath) + { + EditorAssemblyLoadContext context = new EditorAssemblyLoadContext(); + WeakReference reference = new WeakReference(context, trackResurrection: true); + EditorAssemblyLoadContextWrapper wrapper = new EditorAssemblyLoadContextWrapper(context, reference); + Assembly assembly = context.LoadFromAssemblyPath(assemblyPath); + return (assembly, wrapper); + } + [MethodImpl(MethodImplOptions.NoInlining)] internal void Unload() { @@ -52,7 +62,7 @@ internal void Unload() } } - private static (Assembly, EditorAssemblyLoadContextWrapper) Load(string assemblyFilePath) + internal static (Assembly, EditorAssemblyLoadContextWrapper) Load(string assemblyFilePath) { string assemblyFileName = Path.GetFileNameWithoutExtension(assemblyFilePath); @@ -61,7 +71,12 @@ private static (Assembly, EditorAssemblyLoadContextWrapper) Load(string assembly return EditorAssemblyLoadContextWrapper.CreateAndLoad(assemblyName); } - private static bool Unload(EditorAssemblyLoadContextWrapper assemblyLoadContextWrapper) + internal static (Assembly, EditorAssemblyLoadContextWrapper) LoadFromPath(string assemblyFilePath) + { + return EditorAssemblyLoadContextWrapper.CreateAndLoad(assemblyFilePath); + } + + internal static bool Unload(EditorAssemblyLoadContextWrapper assemblyLoadContextWrapper) { if (assemblyLoadContextWrapper == null) { diff --git a/Nerfed.Editor/Project/EditorProject.cs b/Nerfed.Editor/Project/EditorProject.cs index 07ea370..15e83dd 100644 --- a/Nerfed.Editor/Project/EditorProject.cs +++ b/Nerfed.Editor/Project/EditorProject.cs @@ -11,6 +11,8 @@ internal static class EditorProject internal static string ProjectContentDirectory { get; private set; } = string.Empty; internal static string ProjectTempDirectory { get; private set; } = string.Empty; + private static readonly List<(string, EditorAssemblyLoader.EditorAssemblyLoadContextWrapper)> editorAssemblyLoadContextWrappers = []; + internal static bool Create(string projectFilePath, string projectName) { Close(); @@ -80,7 +82,11 @@ internal static void Compile() return; } + UnloadAssemblies(); + Compiler.Compiler.Compile(ProjectFilePath, "Debug"); + + LoadAssemblies(); } internal static void GenerateSolution() @@ -132,4 +138,36 @@ private static void SetupDefaultFolders() } ProjectTempDirectory = tempDirectory; } + + private static void LoadAssemblies() + { + string[] assemblies = Directory.GetFiles(Path.Combine(ProjectDirectory, "bin"), "*.dll", SearchOption.AllDirectories); + + foreach (string assembly in assemblies) + { + (System.Reflection.Assembly, EditorAssemblyLoader.EditorAssemblyLoadContextWrapper) a = EditorAssemblyLoader.LoadFromPath(assembly); + string name = a.Item1.GetName().Name; + editorAssemblyLoadContextWrappers.Add((name, a.Item2)); + Log.Info($"loaded {name}"); + } + + Nerfed.Runtime.Generator.Hook.InvokeHooks(); + } + + private static void UnloadAssemblies() + { + for (int i = editorAssemblyLoadContextWrappers.Count - 1; i >= 0; i--) + { + (string, EditorAssemblyLoader.EditorAssemblyLoadContextWrapper) a = editorAssemblyLoadContextWrappers[i]; + if (EditorAssemblyLoader.Unload(a.Item2)) + { + Log.Info($"Unloaded {a.Item1}"); + editorAssemblyLoadContextWrappers.RemoveAt(i); + } + else + { + Log.Error($"Could not unload {a.Item1}"); + } + } + } } \ No newline at end of file diff --git a/Nerfed.Runtime.Generator/HookSourceGenerator.cs b/Nerfed.Runtime.Generator/HookSourceGenerator.cs new file mode 100644 index 0000000..e1dc5bc --- /dev/null +++ b/Nerfed.Runtime.Generator/HookSourceGenerator.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Nerfed.Runtime.Generator +{ + [Generator] + public class HookSourceGenerator : ISourceGenerator + { + public void Execute(GeneratorExecutionContext context) + { + // Ensure the syntax receiver is not null and is of the expected type + if (context.SyntaxReceiver is not HookSyntaxReceiver syntaxReceiver) + return; + + // Check if we have collected any hook methods + List hookMethods = syntaxReceiver.HookMethods; + if (hookMethods == null || !hookMethods.Any()) + return; + + StringBuilder codeBuilder = new StringBuilder(); + + codeBuilder.AppendLine("using System;"); + codeBuilder.AppendLine(""); + codeBuilder.AppendLine("namespace Nerfed.Runtime.Generator;"); + codeBuilder.AppendLine(""); + codeBuilder.AppendLine($"// Generated by {typeof(HookSourceGenerator)}"); + codeBuilder.AppendLine("public static class Hook"); + codeBuilder.AppendLine("{"); + codeBuilder.AppendLine(" public static void InvokeHooks()"); + codeBuilder.AppendLine(" {"); + + foreach (MethodDeclarationSyntax method in hookMethods) + { + SemanticModel model = context.Compilation.GetSemanticModel(method.SyntaxTree); + + if (model.GetDeclaredSymbol(method) is not IMethodSymbol methodSymbol) + { + continue; + } + + if (methodSymbol.DeclaredAccessibility != Accessibility.Public || !methodSymbol.IsStatic) + { + continue; + } + + codeBuilder.AppendLine($" {methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{methodSymbol.Name}();"); + } + + codeBuilder.AppendLine(" }"); + codeBuilder.AppendLine("}"); + + // Add the generated code to the compilation + context.AddSource("Hook.g.cs", codeBuilder.ToString()); + } + + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new HookSyntaxReceiver()); + } + + public class HookSyntaxReceiver : ISyntaxReceiver + { + public List HookMethods { get; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // Check if the node is a method declaration + if (syntaxNode is MethodDeclarationSyntax methodDeclaration) + { + // Ensure the method declaration has attribute lists + if (methodDeclaration.AttributeLists.Count == 0) + return; + + // Check if the method has the Hook attribute + bool hasHookAttribute = methodDeclaration.AttributeLists + .SelectMany(attrList => attrList.Attributes) + .Any(attr => attr.Name.ToString() == "Hook"); + + if (hasHookAttribute) + { + HookMethods.Add(methodDeclaration); + } + } + } + } + } +} \ No newline at end of file diff --git a/Nerfed.Runtime.Generator/Nerfed.Runtime.Generator.csproj b/Nerfed.Runtime.Generator/Nerfed.Runtime.Generator.csproj new file mode 100644 index 0000000..b74b387 --- /dev/null +++ b/Nerfed.Runtime.Generator/Nerfed.Runtime.Generator.csproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + latest + true + Debug;Test;Release + x64 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + TRACE;LOG_INFO;PROFILING + + + + TRACE;LOG_ERROR;PROFILING + true + + + + TRACE;LOG_ERROR + true + + + diff --git a/Nerfed.Runtime/Engine.cs b/Nerfed.Runtime/Engine.cs index 80f114e..0b39be0 100644 --- a/Nerfed.Runtime/Engine.cs +++ b/Nerfed.Runtime/Engine.cs @@ -72,6 +72,7 @@ public static void Run(string[] args) AudioDevice = new AudioDevice(); OnInitialize?.Invoke(); + Nerfed.Runtime.Generator.Hook.InvokeHooks(); while (!quit) { diff --git a/Nerfed.Runtime/Hook/HookAttribute.cs b/Nerfed.Runtime/Hook/HookAttribute.cs new file mode 100644 index 0000000..7a72408 --- /dev/null +++ b/Nerfed.Runtime/Hook/HookAttribute.cs @@ -0,0 +1,7 @@ +namespace Nerfed.Runtime.Hook +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public class HookAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Nerfed.Runtime/Hook/HookTest.cs b/Nerfed.Runtime/Hook/HookTest.cs new file mode 100644 index 0000000..75508f6 --- /dev/null +++ b/Nerfed.Runtime/Hook/HookTest.cs @@ -0,0 +1,11 @@ +namespace Nerfed.Runtime.Hook +{ + public static class HookTest + { + [Hook] + public static void Test() + { + Log.Info("Hook!"); + } + } +} diff --git a/Nerfed.Runtime/Nerfed.Runtime.csproj b/Nerfed.Runtime/Nerfed.Runtime.csproj index 3c9a9ce..a1a8247 100644 --- a/Nerfed.Runtime/Nerfed.Runtime.csproj +++ b/Nerfed.Runtime/Nerfed.Runtime.csproj @@ -11,6 +11,7 @@ false Debug;Test;Release x64 + true @@ -40,4 +41,10 @@ + + + + \ No newline at end of file diff --git a/Nerfed.sln b/Nerfed.sln index a698767..01c0623 100644 --- a/Nerfed.sln +++ b/Nerfed.sln @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerfed.Editor", "Nerfed.Edi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nerfed.Compiler", "Nerfed.Compiler\Nerfed.Compiler.csproj", "{3DFEB8A4-5354-41EA-A249-27ADC7F666CF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nerfed.Runtime.Generator", "Nerfed.Runtime.Generator\Nerfed.Runtime.Generator.csproj", "{8743FDEF-4FF6-48F9-9F64-7BDEC543C105}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -45,6 +47,12 @@ Global {3DFEB8A4-5354-41EA-A249-27ADC7F666CF}.Release|x64.Build.0 = Release|x64 {3DFEB8A4-5354-41EA-A249-27ADC7F666CF}.Test|x64.ActiveCfg = Test|x64 {3DFEB8A4-5354-41EA-A249-27ADC7F666CF}.Test|x64.Build.0 = Test|x64 + {8743FDEF-4FF6-48F9-9F64-7BDEC543C105}.Debug|x64.ActiveCfg = Debug|x64 + {8743FDEF-4FF6-48F9-9F64-7BDEC543C105}.Debug|x64.Build.0 = Debug|x64 + {8743FDEF-4FF6-48F9-9F64-7BDEC543C105}.Release|x64.ActiveCfg = Release|x64 + {8743FDEF-4FF6-48F9-9F64-7BDEC543C105}.Release|x64.Build.0 = Release|x64 + {8743FDEF-4FF6-48F9-9F64-7BDEC543C105}.Test|x64.ActiveCfg = Test|x64 + {8743FDEF-4FF6-48F9-9F64-7BDEC543C105}.Test|x64.Build.0 = Test|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE