Skip to content

Commit

Permalink
Some fixes and read me
Browse files Browse the repository at this point in the history
  • Loading branch information
genaray committed Jan 8, 2023
1 parent c8be492 commit afb33b8
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 54 deletions.
1 change: 0 additions & 1 deletion Arch.System.SourceGenerator/IMethodSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace Arch.System.SourceGenerator;

public static class IMethodSymbolExtensions
{

public static INamedTypeSymbol GetAttribute(this IMethodSymbol ms, string name)
{
foreach (var attribute in ms.GetAttributes())
Expand Down
106 changes: 55 additions & 51 deletions Arch.System.SourceGenerator/SourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ public class QueryGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
_classToMethods = new(512);
if (!Debugger.IsAttached)
{
//Debugger.Launch();
}
if (!Debugger.IsAttached) Debugger.Launch();

// Register the generic attributes
var attributes = $$"""
namespace Arch.System.SourceGenerator
{
Expand All @@ -30,29 +28,25 @@ namespace Arch.System.SourceGenerator
{{new StringBuilder().AppendGenericAttributes("Exclusive", "Exclusive", 25)}}
}
""";
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("Attributes.g.cs", SourceText.From(attributes, Encoding.UTF8)));

// Add the marker attribute to the compilation
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"Attributes.g.cs",
SourceText.From(attributes, Encoding.UTF8)
)
);

// Do a simple filter for enums
IncrementalValuesProvider<MethodDeclarationSyntax> methodDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is MethodDeclarationSyntax { AttributeLists.Count: > 0 }, // select methods with attributes
transform: static (ctx, _) => GetIfMethodHasAttributeOf(ctx, "Arch.System.SourceGenerator.UpdateAttribute")) // sect the enum with the [EnumExtensions] attribute
.Where(static m => m is not null)!; // filter out attributed enums that we don't care about
// Do a simple filter for methods marked with update
IncrementalValuesProvider<MethodDeclarationSyntax> methodDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (s, _) => s is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
transform: static (ctx, _) => GetIfMethodHasAttributeOf(ctx, "Arch.System.SourceGenerator.UpdateAttribute"))
.Where(static m => m is not null)!; // filter out attributed methods that we don't care about

// Combine the selected enums with the `Compilation`
IncrementalValueProvider<(Compilation, ImmutableArray<MethodDeclarationSyntax>)> compilationAndMethods = context.CompilationProvider.Combine(methodDeclarations.Collect());

// Generate the source using the compilation and enums
context.RegisterSourceOutput(compilationAndMethods, static (spc, source) => Execute(source.Item1, source.Item2, spc));
context.RegisterSourceOutput(compilationAndMethods, static (spc, source) => Generate(source.Item1, source.Item2, spc));
}

private static void Add(IMethodSymbol methodSymbol)

/// <summary>
/// Adds a <see cref="IMethodSymbol"/> to its class.
/// Stores them in <see cref="_classToMethods"/>.
/// </summary>
/// <param name="methodSymbol">The <see cref="IMethodSymbol"/> which will be added/mapped to its class.</param>
private static void AddMethodToClass(IMethodSymbol methodSymbol)
{
if (!_classToMethods.TryGetValue(methodSymbol.ContainingSymbol, out var list))
{
Expand All @@ -62,7 +56,44 @@ private static void Add(IMethodSymbol methodSymbol)
list.Add(methodSymbol.Name+"Query");
}

static void Execute(Compilation compilation, ImmutableArray<MethodDeclarationSyntax> methods, SourceProductionContext context)
/// <summary>
/// Returns a <see cref="MethodDeclarationSyntax"/> if its annocated with a attribute of <see cref="name"/>.
/// </summary>
/// <param name="context">Its <see cref="GeneratorSyntaxContext"/>.</param>
/// <param name="name">The attributes name.</param>
/// <returns></returns>
private static MethodDeclarationSyntax? GetIfMethodHasAttributeOf(GeneratorSyntaxContext context, string name)
{
// we know the node is a EnumDeclarationSyntax thanks to IsSyntaxTargetForGeneration
var enumDeclarationSyntax = (MethodDeclarationSyntax)context.Node;

// loop through all the attributes on the method
foreach (var attributeListSyntax in enumDeclarationSyntax.AttributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
if (ModelExtensions.GetSymbolInfo(context.SemanticModel, attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) continue;

var attributeContainingTypeSymbol = attributeSymbol.ContainingType;
var fullName = attributeContainingTypeSymbol.ToDisplayString();

// Is the attribute the [EnumExtensions] attribute?
if (fullName != name) continue;
return enumDeclarationSyntax;
}
}

// we didn't find the attribute we were looking for
return null;
}

/// <summary>
/// Generates queries and partial classes for the found marked methods.
/// </summary>
/// <param name="compilation">The <see cref="Compilation"/>.</param>
/// <param name="methods">The <see cref="ImmutableArray{MethodDeclarationSyntax}"/> array, the methods which we will generate queries and classes for.</param>
/// <param name="context">The <see cref="SourceProductionContext"/>.</param>
private static void Generate(Compilation compilation, ImmutableArray<MethodDeclarationSyntax> methods, SourceProductionContext context)
{
if (methods.IsDefaultOrEmpty) return;

Expand All @@ -73,7 +104,7 @@ static void Execute(Compilation compilation, ImmutableArray<MethodDeclarationSyn
var semanticModel = compilation.GetSemanticModel(methodSyntax.SyntaxTree);
var methodSymbol = ModelExtensions.GetDeclaredSymbol(semanticModel, methodSyntax) as IMethodSymbol;

Add(methodSymbol);
AddMethodToClass(methodSymbol);

var entity = methodSymbol.Parameters.Any(symbol => symbol.Type.Name.Equals("Entity"));
var sb = new StringBuilder();
Expand Down Expand Up @@ -109,31 +140,4 @@ public partial class {{classSymbol.Name}}{
context.AddSource($"{classSymbol.Name}.g.cs", CSharpSyntaxTree.ParseText(template).GetRoot().NormalizeWhitespace().ToFullString());
}
}

static MethodDeclarationSyntax? GetIfMethodHasAttributeOf(GeneratorSyntaxContext context, string name)
{
// we know the node is a EnumDeclarationSyntax thanks to IsSyntaxTargetForGeneration
var enumDeclarationSyntax = (MethodDeclarationSyntax)context.Node;

// loop through all the attributes on the method
foreach (var attributeListSyntax in enumDeclarationSyntax.AttributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
if (ModelExtensions.GetSymbolInfo(context.SemanticModel, attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) continue;

var attributeContainingTypeSymbol = attributeSymbol.ContainingType;
var fullName = attributeContainingTypeSymbol.ToDisplayString();

// Is the attribute the [EnumExtensions] attribute?
if (fullName != name) continue;
return enumDeclarationSyntax;
}
}

// we didn't find the attribute we were looking for
return null;
}


}
134 changes: 132 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,133 @@
# Arch.Extended
An extension package for Arch providing several utilities and new features.
> :warning: Still WIP.
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=for-the-badge)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
[![Nuget](https://img.shields.io/nuget/v/Arch?style=for-the-badge)](https://www.nuget.org/packages/Arch/)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg?style=for-the-badge)](https://opensource.org/licenses/Apache-2.0)
![C#](https://img.shields.io/badge/c%23-%23239120.svg?style=for-the-badge&logo=c-sharp&logoColor=white)

Extensions for [Arch](https://github.com/genaray/Arch) with some useful features like Systems, Source Generator and Utils.
-
- 🛠️ **_Productive_** > Adds some useful tools and features to the main repository!
- ☕️ **_SIMPLE_** > Works easily, reliably and understandably!
- 💪 _**MAINTAINED**_ > It's actively being worked on, maintained, and supported!
- 🚢 _**SUPPORT**_ > Supports .NetStandard 2.1, .Net Core 6 and 7 and therefore you may use it with Unity or Godot!

# Features & Tools
- ⚙️ **_Systems_** > By means of systems, it is now easy to organize, reuse and arrange queries.
- ✍️ **_Source Generator_** > Declarative syntax using attributes and source generator, let your queries write themselves!

# Systems Code sample

The Arch.System package provides a number of useful classes to organize and structure queries.
These are organized into "systems" and can also be grouped.

The example below demonstrates a slightly larger code sample

```cs
// Components ( ignore the formatting, this saves space )
public struct Position{ float X, Y };
public struct Velocity{ float Dx, Dy };

// BaseSystem provides several usefull methods for interacting and structuring systems
public class MovementSystem : BaseSystem<World, float>{

private QueryDescription _desc = new QueryDescription().WithAll<Position, Velocity>();
public MovementSystem(World world) : base(world) {}

// Can be called once per frame
public override void Update(in float deltaTime)
{
// Run query, can also run multiple queries inside the update
World.Query(in _desc, (ref Position pos, ref Velocity vel) => {
pos.X += vel.X;
pos.Y += vel.Y;
});
}
}

public class Game
{
public static void Main(string[] args)
{
var deltaTime = 0.05f; // This is mostly given by engines, frameworks
// Create a world and a group of systems which will be controlled
var world = World.Create();
var _systems = new Group<float>(
new MovementSystem(world), // Run in order
new MyOtherSystem(...),
...
);

_systems.Initialize(); // Inits all registered systems
_systems.BeforeUpdate(in deltaTime); // Calls .BeforeUpdate on all systems ( can be overriden )
_systems.Update(in deltaTime); // Calls .Update on all systems ( can be overriden )
_systems.AfterUpdate(in deltaTime); // Calls .AfterUpdate on all System ( can be overriden )
_systems.Dispose(); // Calls .Dispose on all systems ( can be overriden )
}
}
```

# Systems source generator

The Arch.System.SourceGenerator provides some code generation utils.
With them, queries within systems can be written virtually by themselves and it saves some boilerplate code.

The only thing you have to pay attention to is that the system class is partial and inherits from BaseSystem.
The attributes can be used to meaningfully describe what query to generate, and the query will always call the annotated method.

```cs
// Components ( ignore the formatting, this saves space )
public struct Position{ float X, Y };
public struct Velocity{ float Dx, Dy };

// BaseSystem provides several usefull methods for interacting and structuring systems
public partial class MovementSystem : BaseSystem<World, float>{

public MovementSystem(World world) : base(world) {}

// Generates a query and calls this annotated method for all entities with position and velocity components.
[Update]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MoveEntity(ref Position pos, ref Velocity vel)
{
pos.X += vel.X;
pos.Y += vel.Y;
}

/// Generates a query and calls this method for all entities with position, velocity, player, mob, particle, either moving or idle and no dead component.
/// All, Any, None are seperate attributes and do not require each other.
[Update]
[All<Player, Mob, Particle>, Any<Moving, Idle>, None<Dead>]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MoveEntityWithConstraints(ref Position pos, ref Velocity vel)
{
pos.X += vel.X;
pos.Y += vel.Y;
}
}
```

If you use the source generator and its attributes in a class that does not override `BaseSystem.Update`, this method is implemented by the source generator itself.
If the method is already implemented by the user, only "query" methods are generated, which you can call yourself.

```csharp
public partial class MovementSystem : BaseSystem<World, GameTime>
{
private readonly QueryDescription _customQuery = new QueryDescription().WithAll<Position, Velocity>();
public DebugSystem(World world) : base(world) { }

public override void Update(in GameTime t)
{
World.Query(in _customQuery, (in Entity entity) => Console.WriteLine($"Custom : {entity}")); // Manual query
MoveEntityQuery(); // Call source generated query, which calls the MoveEntity method
}

[Update]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MoveEntity(ref Position pos, ref Velocity vel)
{
pos.X += vel.X;
pos.Y += vel.Y;
}
}
```

0 comments on commit afb33b8

Please sign in to comment.