chickensoft-games / chickensoft-games/AutoInject
Feature Request: Add a mixin that automatically cleans up frequent node `_ExitTree()` logic.
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 237
- Forks
- 14
- Avg merge
- 6h 40m
- Merged PRs (30d)
- 6
Description
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 extendingGodot.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?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the GameDemo App.cs lifecycle methods and the proposed CleanupSourceGenerator, including its RegisterPostInitializationOutput entry point and IAutoCleanup mixin. Check how the repository builds its source generators before deciding how this approach fits. Done means the generator can produce ExitTreeCleanup implementations for the described cleanup patterns without requiring manual Stop, Dispose, or event-unsubscription logic.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, godot
- Domain
- game-dev, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100