Skip to content
This repository has been archived by the owner on Jun 10, 2024. It is now read-only.

feat: add static reflection #2

Merged
merged 5 commits into from
Feb 4, 2023
Merged
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
9 changes: 6 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ dotnet_diagnostic.IDE0059.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA1305.severity = none
dotnet_diagnostic.CA1051.severity = none
dotnet_diagnostic.RCS1070.severity = none
dotnet_diagnostic.RCS1163.severity = none
dotnet_diagnostic.IDE0060.severity = none

# C# Style Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
Expand All @@ -163,7 +166,7 @@ csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = true:warning
# Expression-bodied members
csharp_style_expression_bodied_methods = true:warning
csharp_style_expression_bodied_constructors = true:warning
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_operators = true:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
Expand Down Expand Up @@ -363,12 +366,12 @@ dotnet_naming_rule.private_fields_are_camel_case.style = private_field_style

# Constant fields (not private) (private)
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_symbols.constant_fields.required_modifiers = const, static
dotnet_naming_rule.constants_should_be_upper_case.severity = warning
dotnet_naming_rule.constants_should_be_upper_case.symbols = constants
dotnet_naming_rule.constants_should_be_upper_case.style = constant_style
dotnet_naming_symbols.constants.applicable_kinds = field, local
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_symbols.constants.required_modifiers = const, static
dotnet_naming_style.constant_style.capitalization = all_upper

# Static readonly fields (not private) (private)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
SuperNodes.Tests/coverage
SuperNodes.Tests/badges

LocalPackages/
.godot/
bin/
obj/
Expand Down
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"markdownlint.config": {
"MD033": false
}
}
254 changes: 236 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord](https://img.shields.io/badge/Chickensoft%20Discord-%237289DA.svg?style=flat&logo=discord&logoColor=white)][discord] ![line coverage][line-coverage]

Supercharge your Godot nodes with power ups and third party source generators.
**Supercharge your Godot nodes with lifecycle-aware power-ups and third party source generators.**

---

<p align="center">
<img alt="Chicken CLI Logo" src="doc_assets/super_nodes.svg" width="200">
</p>

SuperNodes is a source generator for Godot 4 projects written in C#. By adding just two lines of boilerplate code to each of your node scripts, you can use multiple lifecycle-aware third-party source generators harmoniously and add additional state to multiple types of nodes by injecting methods and properties, something that isn't possible with [default interface implementations][default-interfaces] alone.

Expand All @@ -12,7 +18,7 @@ Naturally, there are a few caveats, and you should only use PowerUps to create w

> Need help with source generators, SuperNodes, and PowerUps? Join the [Chickensoft Discord][Discord] and we'll be happy to help you out!

## Installation
## 📦 Installation

Simply add SuperNodes as an analyzer dependency to your C# project.

Expand All @@ -23,7 +29,7 @@ Simply add SuperNodes as an analyzer dependency to your C# project.
</ItemGroup>
```

## Enhanced Nodes
## 🔮 Enhanced Nodes

To turn your ordinary Godot script class into a SuperNode, add the `[SuperNode]` attribute and a partial method signature for the `_Notification` method.

Expand Down Expand Up @@ -61,7 +67,7 @@ Alternatively, SuperNodes will call any method you've defined that matches a God

Just defining a SuperNode doesn't do much. Let's make it useful!

## Using Compatible Source Generators
## 🎰 Using Compatible Source Generators

As mentioned previously, SuperNodes can help you use multiple third-party source generators which want to observe a node's lifecycle events in harmony.

Expand Down Expand Up @@ -145,13 +151,13 @@ SuperNodes can invoke generated implementations for multiple source generators.
public partial class MySuperNode : Node { /* ... */ }
```

## PowerUps
## 🔋 PowerUps

If you can't find a source generator that meets your needs (and you can't be bothered to make your own), you can define "PowerUps" that can be applied to your SuperNodes.

To make a PowerUp, make a Godot node subclass, mark it with the `[PowerUp]` attribute, and create a method with the signature `public void On{NameOfYourPowerUp}(long what)`. The `OnMyPowerUp` method will be called from any generated SuperNodes implementations for SuperNodes that use this PowerUp, ensuring your PowerUp can respond to the node's lifecycle changes.

> Like the `[SuperNode]` attribute, the `[PowerUp]` attribute is generated by `SuperNodes` and may not exist until your IDE runs the source generators next. If you want to force the source generators to execute, simply run `dotnet build` in your project.
> Like the `[SuperNode]` attribute, the `[PowerUp]` attribute is generated by `SuperNodes` and may not exist until your IDE runs the source generators next. If you want to force the source generators to execute, simply run `dotnet build` in your project.

For example: here's a custom PowerUp which prints a message whenever it enters or exits the scene tree.

Expand Down Expand Up @@ -243,7 +249,7 @@ The code from the PowerUp is essentially duplicated exactly, but the class is ch

Any namespaces your PowerUp defines in its file will also get copied over.

## PowerUp Constraints
## 🛑 PowerUp Constraints

PowerUps can only be applied to nodes that are descendants (or distant descendants) of a particular Godot node class.

Expand All @@ -253,14 +259,14 @@ For example, if you tried to apply a PowerUp which extended `Node3D` to a `Node2
[PowerUp]
public class MyPowerUp : Node3D { /* ... */ }

[SuperNode]
[SuperNode(nameof(MyPowerUp))]
public partial class MySuperNode : Node2D { /* .. */ }

// This won't work: SuperNodes will report a problem because MySuperNode
// doesn't have Node3D anywhere in its base class hierarchy!
```

## PowerUps and Source Generators
## 🔋 + 🎰 PowerUps and Source Generators

SuperNodes can apply both PowerUps and Invoke generated methods from other source generators, as long as none of the PowerUps have the same name as the generated source methods.

Expand All @@ -269,14 +275,14 @@ SuperNodes calls generated methods and applied power-ups in the order they are s
For example:

```csharp
[SuperNode("Gen1", nameof(MyPowerUp), "Gen2", nameof("OtherPowerUp"))]
[SuperNode("Gen1", nameof(MyPowerUp), "Gen2", nameof(OtherPowerUp))]
public partial class MySuperNode : Node { /* ... */ }
```

SuperNodes will perform invocations in the following order:

- `Gen1` generated method implementation from another generator
- `OnMyPowerUp` from the mixed-in `MyPowerUp`
- `OnMyPowerUp` from the mixed-in `MyPowerUp`
- `Gen2` generated method implementation from another generator
- `OnOtherPowerUp` from the mixed-in `OtherPowerUp`
- Any defined script handlers, such as `OnReady`, `OnProcess`, etc.
Expand All @@ -285,7 +291,7 @@ SuperNodes will perform invocations in the following order:

The following list contains every possible lifecycle handlers you can implement in your SuperNode. Each one corresponds to a `Notification` type found in `Godot.Node` or `Godot.Object`.

If Godot's notifications are updated or renamed, new versions of SuperNodes can be released that adapt accordingly.
If Godot's notifications are updated or renamed, new versions of SuperNodes can be released that adapt accordingly.

Note that `OnProcess` and `OnPhysicsProcess` are special cases that each have a single `double delta` parameter that is supplied by `GetProcessDeltaTime()` and `GetPhysicsProcessDeltaTime()`, respectively.

Expand Down Expand Up @@ -336,11 +342,226 @@ Note that `OnProcess` and `OnPhysicsProcess` are special cases that each have a
- `OnEnabled` = `NotificationEnabled`
- `OnSceneInstantiated` = `NotificationSceneInstantiated`

## Credits
## ⚡️ Tips and Tricks

This project would not have been possible without all the amazing resources at [csharp-generator-resources][generators].
### 🔏 Interface Implementations

PowerUps can be used to implement an interface on any SuperNode that applies them.

```csharp
public interface IMyInterface { /* ... */ }

[PowerUp]
public class MyPowerUp : Node, IMyInterface { /* ... */ }

[SuperNode(nameof(MyPowerUp))]
public partial class MySuperNode : Node2D { /* .. */ }

/// SuperNodes will generate a partial implementation of MySuperNode in
/// MySuperNode.MyPowerUp.g.cs that makes MySuperNode implement IMyInterface.
```

### 🪞 Static Reflection Lookups

SuperNodes generates static reflection tables for fields and properties in node scripts (and applied PowerUps). PowerUps can reference these tables in their lifecycle handler to enumerate all fields and properties on a SuperNode and automatically perform initialization or inspection, depending on what is needed.

For example, the following code includes a Node script named `MyNode` that has various properties and fields with attributes applied to them.

```csharp
namespace MyNamespace;

using System;
using Godot;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ExampleAttribute : Attribute {
public string A { get; }
public int B { get; }
public bool C { get; }
public ExampleAttribute(string a = "", int b = 0, bool c = false) {
A = a;
B = b;
C = c;
}
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class PlainExampleAttribute : Attribute { }

[SuperNode]
public partial class MyNode : Node {
public override partial void _Notification(long what);

[Example(c: true)]
public string PropertyA { get; set; } = "hello, world!";

[Example("hello", 1, true)]
public int PropertyB { get; set; } = 1;

[PlainExample]
public int PropertyC { get; set; } = 1;

SuperNodes was heavily inspired by [PartialMixins] and [GodotSharp.SourceGenerators], along with many other projects and tutorials.
private float _fieldA = 1.0f;

[Example(b: 1, c: true)]
private float _fieldB = 1.0f;

[PlainExample]
private float _fieldC = 1.0f;

public void OnReady() { }

public void OnProcess(double delta) { }
}
```

SuperNodes will generate a static reflection table implementation named something like `MyNamespace.MyNode_Static.g.cs` with the following static properties:

```csharp
#nullable enable
using Godot;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace MyNamespace {
partial class MyNode {
/// <summary>
/// A list of all properties and fields on this node script, along with
/// basic information about the member and its attributes.
/// This is provided to allow PowerUps to access script member data
/// without having to resort to reflection.
/// </summary>
internal static ScriptPropertyOrField[] PropertiesAndFields
= new ScriptPropertyOrField[] {
new ScriptPropertyOrField(
"_fieldA",
typeof(float),
new Dictionary<string, ScriptAttributeDescription>()
),
new ScriptPropertyOrField(
"_fieldB",
typeof(float),
new Dictionary<string, ScriptAttributeDescription>() {
["global::MyNamespace.ExampleAttribute"] =
new ScriptAttributeDescription(
"ExampleAttribute",
typeof(global::MyNamespace.ExampleAttribute),
ImmutableArray.Create<dynamic>(
"",
1,
true
)
)
}.ToImmutableDictionary()
),
new ScriptPropertyOrField(
"_fieldC",
typeof(float),
new Dictionary<string, ScriptAttributeDescription>() {
["global::MyNamespace.PlainExampleAttribute"] =
new ScriptAttributeDescription(
"PlainExampleAttribute",
typeof(global::MyNamespace.PlainExampleAttribute),
Array.Empty<dynamic>().ToImmutableArray()
)
}.ToImmutableDictionary()
),
new ScriptPropertyOrField(
"PropertyA",
typeof(string),
new Dictionary<string, ScriptAttributeDescription>() {
["global::MyNamespace.ExampleAttribute"] =
new ScriptAttributeDescription(
"ExampleAttribute",
typeof(global::MyNamespace.ExampleAttribute),
ImmutableArray.Create<dynamic>(
"",
0,
true
)
)
}.ToImmutableDictionary()
),
new ScriptPropertyOrField(
"PropertyB",
typeof(int),
new Dictionary<string, ScriptAttributeDescription>() {
["global::MyNamespace.ExampleAttribute"] =
new ScriptAttributeDescription(
"ExampleAttribute",
typeof(global::MyNamespace.ExampleAttribute),
ImmutableArray.Create<dynamic>(
"hello",
1,
true
)
)
}.ToImmutableDictionary()
),
new ScriptPropertyOrField(
"PropertyC",
typeof(int),
new Dictionary<string, ScriptAttributeDescription>() {
["global::MyNamespace.PlainExampleAttribute"] =
new ScriptAttributeDescription(
"PlainExampleAttribute",
typeof(global::MyNamespace.PlainExampleAttribute),
Array.Empty<dynamic>().ToImmutableArray()
)
}.ToImmutableDictionary()
)
};

/// <summary>
/// Calls the given type receiver with the generic type of the given
/// script property or field. Generated by SuperNodes.
/// </summary>
/// <typeparam name="TResult">The return type of the type receiver's
/// receive method.</typeparam>
/// <param name="scriptProperty">The name of the script property or field
/// to get the type of.</param>
/// <param name="receiver">The type receiver to call with the type
/// of the script property or field.</param>
/// <returns>The result of the type receiver's receive method.</returns>
/// <exception cref="System.ArgumentException">Thrown if the given script
/// property or field does not exist.</exception>
internal static TResult GetScriptPropertyOrFieldType<TResult>(
string scriptProperty, ITypeReceiver<TResult> receiver
) {
switch (scriptProperty) {
case "_fieldA":
return receiver.Receive<float>();
case "_fieldB":
return receiver.Receive<float>();
case "_fieldC":
return receiver.Receive<float>();
case "PropertyA":
return receiver.Receive<string>();
case "PropertyB":
return receiver.Receive<int>();
case "PropertyC":
return receiver.Receive<int>();
default:
throw new System.ArgumentException(
$"No field or property named '{scriptProperty}' was found on MyNode."
);
}
}
}
}
#nullable disable
```

### 🔌 Sharing PowerUps in Separate Packages

PowerUps can be distributed as source-only nuget packages! An example repository, `SharedPowerUps` is included to illustrate how to create a source-only nuget package.

The included example project, `SuperNodes.Example`, shows how to reference a source-only nuget package. Source-only packages have to be carefully designed so that they are fed into the consuming package's source generators. You can [read all about it here](SharedPowerUps/README.md).

## 🙏 Credits

This project would not have been possible without all the amazing resources at [csharp-generator-resources][generators].

Special thanks to those in the Godot and Chickensoft Discord Servers for supplying tips, information, and help along the way!

Expand All @@ -351,11 +572,8 @@ Special thanks to those in the Godot and Chickensoft Discord Servers for supplyi
[chickensoft-website]: https://chickensoft.games
[discord]: https://discord.gg/gSjaPgMmYW
[line-coverage]: https://raw.githubusercontent.com/chickensoft-games/SuperNodes/main/SuperNodes.Tests/reports/line_coverage.svg
[branch-coverage]: https://raw.githubusercontent.com/chickensoft-games/SuperNodes/main/SuperNodes.Tests/reports/branch_coverage.svg

<!-- Content -->
[godot-generator-problems]: https://github.com/godotengine/godot/issues/66597
[default-interfaces]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods
[generators]: https://github.com/amis92/csharp-source-generators
[PartialMixins]: https://github.com/LokiMidgard/PartialMixins
[GodotSharp.SourceGenerators]: https://github.com/Cat-Lips/GodotSharp.SourceGenerators
Loading