Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Godot 4 Tests/Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private static IEnumerable<Func<ITest>> Tests
yield return ITest.GetTest<AutoloadExtensionTests>;
yield return ITest.GetTest<BuiltInScriptTest>;
yield return ITest.GetTest<CachedNodes>;
yield return ITest.GetTest<CancellationSourceTests>;
yield return ITest.GetTest<DiscardWorkaroundTest>;
yield return ITest.GetTest<EditableChildrenTest>;
yield return ITest.GetTest<EditableChildrenWithTraversalTest>;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://docd60525k7es
Original file line number Diff line number Diff line change
@@ -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")
6 changes: 6 additions & 0 deletions NRT.Tests/GD4.NRT/TestCancellationSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Godot;

namespace NRT.Tests;

[CancellationSource]
public partial class TestCancellationSource : Node;
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Godot;

[AttributeUsage(AttributeTargets.Class)]
public sealed class CancellationSourceAttribute : Attribute;
Original file line number Diff line number Diff line change
@@ -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}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Scriban;

namespace GodotSharp.SourceGenerators.CancellationSourceExtensions;

[Generator]
internal class CancellationSourceSourceGenerator : SourceGeneratorForDeclaredTypeWithAttribute<Godot.CancellationSourceAttribute>
{
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}<END>\n");

return (output, null);
}
}
Original file line number Diff line number Diff line change
@@ -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 ~}}
9 changes: 9 additions & 0 deletions SourceGenerators/CancellationSourceExtensions/Resources.cs
Original file line number Diff line number Diff line change
@@ -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);
}