Open wihrl opened 2 years ago
Just out of curiosity, does anyone know if Godot 4 is supposed to work with dotnet's hot reloading capabilities out of the box? If so, is that documented anywhere I can read about it?
Probably not documented anywhere, but I'd assume that since Godot 4 is now just using the regular .NET runtime, there's nothing preventing this from working. I mean, it already kind of works in VS.
I found a workaround by using reflection to poll MethodBody.LocalSignatureMetadataToken.
/// <summary>
/// Using the standard .NET Hot Reload support does not work with Godot.
/// This class serves as a workaround to detect changes in code using reflection.
/// Replace with [assembly: System.Reflection.Metadata.MetadataUpdateHandler)] once it works.
/// </summary>
public static class HotReloadChecker
{
static readonly Dictionary<(string, string), int> _tokens = new();
public static bool Changed<T>(T instance, string methodName)
{
var type = instance.GetType();
var method = type.GetMethod(methodName)?.GetMethodBody() ??
throw new Exception($"Method {methodName} not found");
var token = method.LocalSignatureMetadataToken;
var key = (type.Name, methodName);
bool changed = false;
if (_tokens.TryGetValue(key, out var lastToken))
changed = lastToken != token;
_tokens[key] = token;
return changed;
}
}
I found a workaround by using reflection to poll MethodBody.LocalSignatureMetadataToken.
/// <summary> /// Using the standard .NET Hot Reload support does not work with Godot. /// This class serves as a workaround to detect changes in code using reflection. /// Replace with [assembly: System.Reflection.Metadata.MetadataUpdateHandler)] once it works. /// </summary> public static class HotReloadChecker { static readonly Dictionary<(string, string), int> _tokens = new(); public static bool Changed<T>(T instance, string methodName) { var type = instance.GetType(); var method = type.GetMethod(methodName)?.GetMethodBody() ?? throw new Exception($"Method {methodName} not found"); var token = method.LocalSignatureMetadataToken; var key = (type.Name, methodName); bool changed = false; if (_tokens.TryGetValue(key, out var lastToken)) changed = lastToken != token; _tokens[key] = token; return changed; } }
@wihrl how is this supposed to be used? Where should this code be placed?
I cannot find any documentation that mentions creating a class like this to aid in hot reloading. What I did find is the MetadataUpdateHandlerAttribute attribute (see info on this here, and here) but I cannot see how your code can be converted to fit this attribute class.
@wihrl how is this supposed to be used? Where should this code be placed?
I cannot find any documentation that mentions creating a class like this to aid in hot reloading. What I did find is the MetadataUpdateHandlerAttribute attribute (see info on this here, and here) but I cannot see how your code can be converted to fit this attribute class.
[MetadataUpdateHandlerAttribute]
is the official way to catch the event from hot-reloading. But it still doesn't work for Godot. (.NET hot reload itself has worked on Godot already since .NET 6.0)
I guess the @wihrl 's workaround was that you can call this method to check if the specific method has been hot-reloaded. To kinda catch the event of hot reloading, you'll need to call this method all the time.
I got a better workaround to me that using the custom attribute to register the notification and its callback.
using Godot;
using System;
using System.Collections.Generic;
using System.Reflection;
using WatchMethod = (System.Reflection.MethodInfo watched, HotReloadCallback callback);
public delegate void HotReloadCallback(string methodName);
/// <summary>
/// Using the standard .NET Hot Reload support does not work with Godot.
/// This class serves as a workaround to detect changes in code using MethodInfo.GetMethodBody().LocalSignatureMetadataToken.
/// Put [NotifyHotReload] attribute to the method you want to catch the notification when it is hot reloaded.
/// And remember to put a script to call HotReloadManager.CheckHotReloaded() to update the notification system.
///
/// Replace with [assembly: System.Reflection.Metadata.MetadataUpdateHandler)] once it works.
/// </summary>
public static class HotReloadManager {
static readonly Dictionary<MethodInfo, int> _methodTokens = new();
static List<WatchMethod> _watchMethods = new List<WatchMethod>();
static HotReloadManager() {
Initialize();
}
static void Initialize() {
GD.Print($"Initialize {nameof(HotReloadManager)}");
_watchMethods.Clear();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
foreach (var type in assembly.GetTypes()) {
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) {
var attribute = method.GetCustomAttribute<NotifyHotReloadAttribute>();
if (attribute == null) {
continue;
}
string callbackName = attribute.CallbackMethodName;
if (string.IsNullOrEmpty(callbackName)) {
continue;
}
// Callback must be static.
MethodInfo callback = type.GetMethod(
callbackName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static,
types: [typeof(string)] // The first parameter is the name of the reloaded method.
);
if (callback == null) {
continue;
}
_watchMethods.Add((method, callback.CreateDelegate<HotReloadCallback>()));
}
}
}
foreach (var method in _watchMethods) {
int token = method.watched.GetMethodBody()?.LocalSignatureMetadataToken ??
throw new Exception($"Method {method.watched}'s MethodBody not found");
_methodTokens.Add(method.watched, token);
GD.Print($"{method.watched.DeclaringType}.{method.watched.Name} registered hot-reloading.");
}
}
public static bool CheckHotReloaded() {
bool changed = false;
foreach (var method in _watchMethods) {
int token = method.watched.GetMethodBody()?.LocalSignatureMetadataToken ??
throw new Exception($"Method {method}'s MethodBody not found");
if (token != _methodTokens[method.watched]) {
GD.Print($"{method.watched.DeclaringType}.{method.watched.Name} changed!");
// Update the token.
_methodTokens[method.watched] = token;
changed = true;
// Callback.
method.callback.Invoke(method.watched.Name);
}
}
return changed;
}
}
[AttributeUsage(AttributeTargets.Method)]
public class NotifyHotReloadAttribute : Attribute {
public string CallbackMethodName;
public NotifyHotReloadAttribute(string callbackMethodName) {
CallbackMethodName = callbackMethodName;
}
}
using Godot;
public partial class HotReloadTest : Label {
private static int _hotReloadTimes = 0;
private static string _hotReloadMessage = "";
public override void _Process(double delta) {
// You should put HotReloadChecker.CheckHotReloaded() call in its own class to update it as often as you like.
HotReloadManager.CheckHotReloaded();
// Update text on the game ui.
Text = $"Hot Reload Times: {_hotReloadTimes}\n" +
$"MyMethod: {MyMethod()}\n" +
$"MyMethod2: {MyMethod2()}\n" +
_hotReloadMessage;
}
[NotifyHotReload(nameof(OnHotReloadCallback))]
public string MyMethod() {
return "Method 1";
}
[NotifyHotReload(nameof(OnHotReloadCallback))]
public string MyMethod2() {
return "Method 2";
}
// The callback method must be static and with 1 string parameter.
private static void OnHotReloadCallback(string methodName) {
_hotReloadTimes++;
_hotReloadMessage = $"{methodName} has been changed!";
}
}
https://github.com/godotengine/godot/assets/23000374/90a872af-2e75-45c5-bf96-42393dad78bf
Godot version
4.0 beta 3
System information
Win 11
Issue description
Applying method changes while debugging a C# project results in errors. The method still gets updated, but the resulting error can be seen in the hot reload output in the Output panel of Visual Studio.
[Error] Applying updates failed: Microsoft.VisualStudio.HotReload.Components.DeltaApplier.ManagedDeltaApplierFailedToConnectException: Exception of type 'Microsoft.VisualStudio.HotReload.Components.DeltaApplier.ManagedDeltaApplierFailedToConnectException' was thrown.
In Rider, this manifests itself as only being able to apply the changes once.
Steps to reproduce
Create a new C# project and setup debugging.
In the .csproj:
launchSettings.json:
Then start debugging, change a method and apply the change.
Minimal reproduction project
No response