diff --git a/Godot 4 Tests/Run.cs b/Godot 4 Tests/Run.cs index f428958..3fc2d2d 100644 --- a/Godot 4 Tests/Run.cs +++ b/Godot 4 Tests/Run.cs @@ -21,6 +21,7 @@ private static IEnumerable> Tests yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; + yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; diff --git a/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.cs b/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.cs new file mode 100644 index 0000000..1a1569e --- /dev/null +++ b/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.cs @@ -0,0 +1,38 @@ +using System.Threading; +using FluentAssertions; +using Godot; +using GodotSharp.BuildingBlocks.TestRunner; + +namespace GodotTests.TestScenes; + +[SceneTree] +[CancellationSource] +public partial class CancellationSourceTests : Node, ITest +{ + private CancellationToken capturedToken; + + void ITest.InitTests() + { + Cts.Should().BeNull(); + } + + void ITest.EnterTests() + { + // After entering tree, CTS should be created + Cts.Should().NotBeNull(); + Cts.Token.IsCancellationRequested.Should().BeFalse(); + Cts.Token.CanBeCanceled.Should().BeTrue(); + + // Capture token to verify it gets cancelled on exit + capturedToken = Cts.Token; + } + + void ITest.ExitTests() + { + // After exiting tree, the captured token should be cancelled + capturedToken.IsCancellationRequested.Should().BeTrue(); + + // The property should now return null + Cts.Should().BeNull(); + } +} diff --git a/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.cs.uid b/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.cs.uid new file mode 100644 index 0000000..ec40cfa --- /dev/null +++ b/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.cs.uid @@ -0,0 +1 @@ +uid://docd60525k7es diff --git a/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.tscn b/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.tscn new file mode 100644 index 0000000..5f75e3a --- /dev/null +++ b/Godot 4 Tests/TestScenes/CancellationSource/CancellationSourceTests.tscn @@ -0,0 +1,6 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://TestScenes/Feature.CancellationSource/CancellationSourceTests.cs" id="1"] + +[node name="Root" type="Node"] +script = ExtResource("1") diff --git a/NRT.Tests/GD4.NRT/TestCancellationSource.cs b/NRT.Tests/GD4.NRT/TestCancellationSource.cs new file mode 100644 index 0000000..2825592 --- /dev/null +++ b/NRT.Tests/GD4.NRT/TestCancellationSource.cs @@ -0,0 +1,6 @@ +using Godot; + +namespace NRT.Tests; + +[CancellationSource] +public partial class TestCancellationSource : Node; diff --git a/README.md b/README.md index 0adc1be..5d94a2c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ C# Source Generators for use with the Godot Game Engine * [NEW] `AutoEnum` enum/class attribute (GD4 only): * (enum) Generates efficient Str/Parse for enums * (class) Generates enum for static data classes (for editor/network use) +* [NEW] `CancellationSource` class attribute (GD4 only): + * Generates a `CancellationTokenSource` property (`Cts`) on a Node + * Automatically creates a new source on tree enter, cancels and disposes on tree exit + * Handles re-entry (fresh source each time the node enters the tree) * `Autoload` class attribute: * Provide strongly typed access to autoload nodes defined in godot.project * `CodeComments` class attribute: @@ -80,6 +84,7 @@ C# Source Generators for use with the Godot Game Engine - [`AnimNames`](#animnames) - [`AudioBus`](#audiobus) - [`AutoEnum`](#autoenum) + - [`CancellationSource`](#cancellationsource) - [`Autoload`](#autoload) - [`CodeComments`](#codecomments) - [`GlobalGroups`](#globalgroups) @@ -405,6 +410,74 @@ var s = MapData.Outside.ToStr(); // s = "Outside" var d = MapData.FromStr(s); // d = MapData.Outside ``` +### `CancellationSource` + * Class attribute (GD4 only) + * Generates a `CancellationTokenSource` property (`Cts`) on a Node + * Automatically creates a new source when the node enters the tree + * Cancels and disposes the source when the node exits the tree + * Handles re-entry (fresh source each time) + * Uses lazy initialization to avoid constructor conflicts +#### Examples: +```cs +[CancellationSource] +public partial class MyNode : Node +{ + public override void _Ready() + { + DoWork(Cts.Token); + } + + private async void DoWork(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + await DoSomethingAsync(token); + } + } +} +``` +Generates: +```cs +partial class MyNode +{ + private CancellationTokenSource __cancellationTokenSource; + private bool __cancellationSourceInitialized; + + public CancellationTokenSource Cts + { + get + { + __InitCancellationSource(); + return __cancellationTokenSource; + } + } + + private void __InitCancellationSource() + { + if (__cancellationSourceInitialized) return; + __cancellationSourceInitialized = true; + + if (IsInsideTree()) + __cancellationTokenSource = new CancellationTokenSource(); + + TreeEntered += __CancellationSource_OnTreeEntered; + TreeExiting += __CancellationSource_OnTreeExiting; + } + + private void __CancellationSource_OnTreeEntered() + { + __cancellationTokenSource = new CancellationTokenSource(); + } + + private void __CancellationSource_OnTreeExiting() + { + __cancellationTokenSource?.Cancel(); + __cancellationTokenSource?.Dispose(); + __cancellationTokenSource = null; + } +} +``` + ### `Autoload` * `Autoload` class attribute * Provides strongly typed access to autoload nodes defined in editor project settings diff --git a/SourceGenerators/CancellationSourceExtensions/CancellationSourceAttribute.cs b/SourceGenerators/CancellationSourceExtensions/CancellationSourceAttribute.cs new file mode 100644 index 0000000..dddc868 --- /dev/null +++ b/SourceGenerators/CancellationSourceExtensions/CancellationSourceAttribute.cs @@ -0,0 +1,4 @@ +namespace Godot; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class CancellationSourceAttribute : Attribute; diff --git a/SourceGenerators/CancellationSourceExtensions/CancellationSourceDataModel.cs b/SourceGenerators/CancellationSourceExtensions/CancellationSourceDataModel.cs new file mode 100644 index 0000000..ad6c628 --- /dev/null +++ b/SourceGenerators/CancellationSourceExtensions/CancellationSourceDataModel.cs @@ -0,0 +1,11 @@ +using Microsoft.CodeAnalysis; + +namespace GodotSharp.SourceGenerators.CancellationSourceExtensions; + +internal class CancellationSourceDataModel(INamedTypeSymbol symbol) : ClassDataModel(symbol) +{ + protected override string Str() + { + return $"ClassName: {ClassName}"; + } +} diff --git a/SourceGenerators/CancellationSourceExtensions/CancellationSourceSourceGenerator.cs b/SourceGenerators/CancellationSourceExtensions/CancellationSourceSourceGenerator.cs new file mode 100644 index 0000000..9cd39da --- /dev/null +++ b/SourceGenerators/CancellationSourceExtensions/CancellationSourceSourceGenerator.cs @@ -0,0 +1,22 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Scriban; + +namespace GodotSharp.SourceGenerators.CancellationSourceExtensions; + +[Generator] +internal class CancellationSourceSourceGenerator : SourceGeneratorForDeclaredTypeWithAttribute +{ + private static Template CancellationSourceTemplate => field ??= Template.Parse(Resources.CancellationSourceTemplate); + + protected override (string GeneratedCode, DiagnosticDetail Error) GenerateCode(Compilation compilation, SyntaxNode node, INamedTypeSymbol symbol, AttributeData attribute, AnalyzerConfigOptions options) + { + var model = new CancellationSourceDataModel(symbol); + Log.Debug($"--- MODEL ---\n{model}\n"); + + var output = CancellationSourceTemplate.Render(model, Shared.Utils); + Log.Debug($"--- OUTPUT ---\n{output}\n"); + + return (output, null); + } +} diff --git a/SourceGenerators/CancellationSourceExtensions/CancellationSourceTemplate.scriban b/SourceGenerators/CancellationSourceExtensions/CancellationSourceTemplate.scriban new file mode 100644 index 0000000..79fa457 --- /dev/null +++ b/SourceGenerators/CancellationSourceExtensions/CancellationSourceTemplate.scriban @@ -0,0 +1,49 @@ +{{-#####-CONTENT-#####-}} + +{{~ capture body ~}} + [{{EditorBrowsableNever}}] + private System.Threading.CancellationTokenSource __cancellationTokenSource; + + [{{EditorBrowsableNever}}] + private bool __cancellationSourceInitialized; + + public System.Threading.CancellationTokenSource Cts + { + get + { + __InitCancellationSource(); + return __cancellationTokenSource; + } + } + + [{{EditorBrowsableNever}}] + private void __InitCancellationSource() + { + if (__cancellationSourceInitialized) return; + __cancellationSourceInitialized = true; + + if (IsInsideTree()) + __cancellationTokenSource = new System.Threading.CancellationTokenSource(); + + TreeEntered += __CancellationSource_OnTreeEntered; + TreeExiting += __CancellationSource_OnTreeExiting; + } + + [{{EditorBrowsableNever}}] + private void __CancellationSource_OnTreeEntered() + { + __cancellationTokenSource = new System.Threading.CancellationTokenSource(); + } + + [{{EditorBrowsableNever}}] + private void __CancellationSource_OnTreeExiting() + { + __cancellationTokenSource?.Cancel(); + __cancellationTokenSource?.Dispose(); + __cancellationTokenSource = null; + } +{{~ end ~}} + +{{-#####-MAIN-#####-}} + +{{~ RenderClass body ~}} diff --git a/SourceGenerators/CancellationSourceExtensions/Resources.cs b/SourceGenerators/CancellationSourceExtensions/Resources.cs new file mode 100644 index 0000000..b624c87 --- /dev/null +++ b/SourceGenerators/CancellationSourceExtensions/Resources.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace GodotSharp.SourceGenerators.CancellationSourceExtensions; + +internal static class Resources +{ + private const string cancellationSourceTemplate = "GodotSharp.SourceGenerators.CancellationSourceExtensions.CancellationSourceTemplate.scriban"; + public static readonly string CancellationSourceTemplate = Assembly.GetExecutingAssembly().GetEmbeddedResource(cancellationSourceTemplate); +}