I semi-recently started trying LogicBlocks in a project, and so I tried copying the App.cs from the GameDemo project (and its related classes/states/etc.) into my own project to learn how it works. However, I really disliked the usability/maintenance cost of having to add in & keep updated all the .Stop(), .Dispose() and event unsubscription logic in the _ExitTree() / OnExitTree() method based on whatever things were initialized in Initialize(), _Ready() / OnReady(), Setup(), and OnResolved().
To avoid having to deal with any of that, I thought to write an incremental source generator to solve the problem automatically (which I now have working). This issue is mainly to check whether there'd be any interest in me porting it into a full PR (though, I think the exact approach may have to be adjusted slightly to account for how this codebase builds):
Using a new CleanupSourceGenerator, I use RegisterPostInitializationOutput(...) to define a new IAutoCleanup mixin:
using Chickensoft.Introspection;
using Godot;
namespace Chickensoft.AutoInject;
[Mixin]
public interface IAutoCleanup : IMixin<IAutoCleanup>
{
public readonly ref struct CleanScope
{
public void Dispose()
{
}
}
CleanScope Clean() => new();
void ExitTreeCleanup() { }
void IMixin<IAutoCleanup>.Handler()
{
if (this is not Node node)
{
return;
}
node.__SetupNotificationStateIfNeeded();
var what = MixinState.Get<NotificationState>().Notification;
if (what == Node.NotificationExitTree)
{
ExitTreeCleanup();
}
}
}
public static class AutoCleanupExtensions
{
public static IAutoCleanup.CleanScope Clean<T>(this T instance) where T : IAutoCleanup
{
return ((IAutoCleanup)instance).Clean();
}
}
Then the source generator identifies any classes with [Meta] attributes containing the IAutoCleanup interface (or a derived interface of it), and scans their various Godot/AutoInject-specific initialization methods (not the constructor or _Init though). If the developer invokes the this.Clean() method to create a using block of any kind, then each statement of that block is scanned and statically evaluated to see whether it matches one of 3 criteria:
- Is it a property/field initialization from either a
new() expression or an invocation? If so, and the datatype of the property/field is disposable AND it is NOT a type extending Godot.GodotObject (or its derivatives), then it generates a .Dispose() call.
- Added an MSBuild configuration property that lets developers add names of additional disposable types to be ignored as well.
- Is it a
.Start() call on a property field? If so, generate the corresponding .Stop() call.
- I've designed these method pairs to be configurable from MSBuild options in the future.
- Is it an event subscription via
+=? If so, generate the corresponding -= unsubscribe operation.
That makes the following App logic from GameDemo (tweaked to include the using statement)...
[Meta(typeof(IAutoNode, IAutoCleanup))]
[ClassDiagram(UseVSCodePaths = true)]
public partial class App : CanvasLayer, IApp
{
public override void _Notification(int what) => this.Notify(what);
public IAppRepo AppRepo { get; set; } = default!;
public IAppLogic AppLogic { get; set; } = default!;
public LogicBlock.Binding AppBinding { get; set; } = default!;
public void Initialize()
{
using var _ = this.Clean();
Instantiator = new Instantiator(GetTree());
AppRepo = new AppRepo();
AppLogic = new AppLogic();
AppLogic.Set(AppRepo);
AppLogic.Set(new AppLogic.Data());
Menu.NewGame += OnNewGame;
Menu.LoadGame += OnLoadGame;
Menu.DeleteGame += OnDeleteGame;
AnimationPlayer.AnimationFinished += OnAnimationFinished;
this.Provide();
}
public void OnReady()
{
// Tell our type type resolver about the Godot-specific converters.
GodotSerialization.Setup();
LogicBlockSerialization.Setup();
using (this.Clean())
{
AppBinding = AppLogic.Bind()
OnOutput(blah blah); // ...omitted long chain of output handlers.
// Enter the first state to kick off the binding side effects.
AppLogic.Start<AppLogicState.SplashScreen>();
}
}
}
...generate the following partial declaration, providing an explicit implementation of the ExitTreeCleanup() method from the mixin:
// <auto-generated />
#nullable enable
namespace GameDemo;
partial class App : global::Chickensoft.AutoInject.IAutoCleanup
{
void global::Chickensoft.AutoInject.IAutoCleanup.ExitTreeCleanup()
{
// OnReady
AppLogic.Stop();
AppBinding.Dispose();
// Initialize
AppRepo.Dispose();
Menu.NewGame -= OnNewGame;
Menu.LoadGame -= OnLoadGame;
Menu.DeleteGame -= OnDeleteGame;
AnimationPlayer.AnimationFinished -= OnAnimationFinished;
}
}
It runs the operations for a single method in-order, but processes each method grouping in reverse order. And if a Dispose() would be detected, but a Start()/Stop() pair already exists for the same member property/field, then the dispose is skipped (like how AppLogic isn't disposed above, despite being a disposable type).
Sound like something you'd possibly be interested in making core?
I semi-recently started trying LogicBlocks in a project, and so I tried copying the
App.csfrom the GameDemo project (and its related classes/states/etc.) into my own project to learn how it works. However, I really disliked the usability/maintenance cost of having to add in & keep updated all the.Stop(),.Dispose()and event unsubscription logic in the_ExitTree()/OnExitTree()method based on whatever things were initialized inInitialize(),_Ready()/OnReady(),Setup(), andOnResolved().To avoid having to deal with any of that, I thought to write an incremental source generator to solve the problem automatically (which I now have working). This issue is mainly to check whether there'd be any interest in me porting it into a full PR (though, I think the exact approach may have to be adjusted slightly to account for how this codebase builds):
Using a new
CleanupSourceGenerator, I useRegisterPostInitializationOutput(...)to define a newIAutoCleanupmixin:Then the source generator identifies any classes with
[Meta]attributes containing theIAutoCleanupinterface (or a derived interface of it), and scans their various Godot/AutoInject-specific initialization methods (not the constructor or_Initthough). If the developer invokes thethis.Clean()method to create ausingblock of any kind, then each statement of that block is scanned and statically evaluated to see whether it matches one of 3 criteria:new()expression or an invocation? If so, and the datatype of the property/field is disposable AND it is NOT a type extendingGodot.GodotObject(or its derivatives), then it generates a.Dispose()call..Start()call on a property field? If so, generate the corresponding.Stop()call.+=? If so, generate the corresponding-=unsubscribe operation.That makes the following
Applogic from GameDemo (tweaked to include theusingstatement)......generate the following partial declaration, providing an explicit implementation of the
ExitTreeCleanup()method from the mixin:It runs the operations for a single method in-order, but processes each method grouping in reverse order. And if a
Dispose()would be detected, but aStart()/Stop()pair already exists for the same member property/field, then the dispose is skipped (like howAppLogicisn't disposed above, despite being a disposable type).Sound like something you'd possibly be interested in making core?