diff --git a/Bang.sln b/Bang.sln
index e202678..d7ac950 100644
--- a/Bang.sln
+++ b/Bang.sln
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bang.Analyzers", "src\Bang.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bang.Analyzers.Tests", "src\Bang.Analyzers.Tests\Bang.Analyzers.Tests.csproj", "{0EED28D5-6A33-47F9-9D9E-C587D99DFD93}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bang.Generator", "src\Bang.Generator\Bang.Generator.csproj", "{945C48DA-7A5B-4EB3-93AF-270396334BA0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,10 @@ Global
{0EED28D5-6A33-47F9-9D9E-C587D99DFD93}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EED28D5-6A33-47F9-9D9E-C587D99DFD93}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EED28D5-6A33-47F9-9D9E-C587D99DFD93}.Release|Any CPU.Build.0 = Release|Any CPU
+ {945C48DA-7A5B-4EB3-93AF-270396334BA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {945C48DA-7A5B-4EB3-93AF-270396334BA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {945C48DA-7A5B-4EB3-93AF-270396334BA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {945C48DA-7A5B-4EB3-93AF-270396334BA0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/README.md b/README.md
index 2204d75..da1f23e 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,12 @@ We are on nuget! So you can either use this repository as a submodule or simply
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
diff --git a/src/Bang.Analyzers/Bang.Analyzers.csproj b/src/Bang.Analyzers/Bang.Analyzers.csproj
index a870878..855af94 100644
--- a/src/Bang.Analyzers/Bang.Analyzers.csproj
+++ b/src/Bang.Analyzers/Bang.Analyzers.csproj
@@ -15,7 +15,7 @@
Source code analyzers for Bang
Murder.Bang.Analyzers
- 0.0.4
+ 0.0.5
Murder Authors
Murder Engine
LICENSE
diff --git a/src/Bang.Generator/Bang.Generator.csproj b/src/Bang.Generator/Bang.Generator.csproj
index 8ac2b3b..c5f5c37 100644
--- a/src/Bang.Generator/Bang.Generator.csproj
+++ b/src/Bang.Generator/Bang.Generator.csproj
@@ -1,19 +1,19 @@
- Generator
- Exe
- net7.0
+ Bang.Generator
+ netstandard2.0
+ 11
enable
enable
Murder.Bang.Generator
- 0.0.1-alpha
- Isadora
+ 0.0.5
+ Murder Authors
Murder Engine
LICENSE
- A helper generator for the Bang ECS framework.
+ Source code generator for the Bang ECS framework.
true
@@ -27,18 +27,17 @@
-
- Always
-
-
+
-
-
-
+
+
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
diff --git a/src/Bang.Generator/BangExtensionsGenerator.cs b/src/Bang.Generator/BangExtensionsGenerator.cs
new file mode 100644
index 0000000..cf20504
--- /dev/null
+++ b/src/Bang.Generator/BangExtensionsGenerator.cs
@@ -0,0 +1,104 @@
+using Bang.Generator.Extensions;
+using Bang.Generator.Metadata;
+using System.Collections.Immutable;
+using Bang.Generator.Templating;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.VisualBasic;
+
+namespace Bang.Generator;
+
+[Generator]
+public sealed class BangExtensionsGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var potentialComponents = context.PotentialComponents().Collect();
+ var stateMachines = context.PotentialStateMachines().Collect();
+ var compilation = potentialComponents
+ .Combine(stateMachines)
+ .Combine(context.CompilationProvider);
+
+ context.RegisterSourceOutput(
+ compilation,
+ (c, t) => Execute(c, t.Right, t.Left.Left, t.Left.Right)
+ );
+ }
+
+ public void Execute(
+ SourceProductionContext context,
+ Compilation compilation,
+ ImmutableArray potentialComponents,
+ ImmutableArray potentialStateMachines)
+ {
+#if DEBUG
+ // Uncomment this if you need to use a debugger.
+ // if (!System.Diagnostics.Debugger.IsAttached)
+ // {
+ // System.Diagnostics.Debugger.Launch();
+ // }
+#endif
+
+ // Bail if any important type symbol is not resolvable.
+ var bangTypeSymbols = BangTypeSymbols.FromCompilation(compilation);
+ if (bangTypeSymbols is null)
+ return;
+
+ var referencedAssemblyTypeFetcher = new ReferencedAssemblyTypeFetcher(compilation);
+
+ // Gets the best possible name for the parent lookup class assemblies.
+ var parentLookupClass = referencedAssemblyTypeFetcher
+ .GetAllCompiledClassesWithSubtypes()
+ .Where(t => t.IsSubtypeOf(bangTypeSymbols.ComponentsLookupClass))
+ .OrderBy(NumberOfParentClasses)
+ .LastOrDefault() ?? bangTypeSymbols.ComponentsLookupClass;
+
+ var projectName = compilation.AssemblyName?.Replace(".", "") ?? "My";
+
+ var metadataFetcher = new MetadataFetcher(compilation);
+
+ // All files that will be created by this generator.
+ var templates = ImmutableArray.Create(
+ Templates.ComponentTypes(projectName),
+ Templates.MessageTypes(projectName),
+ Templates.EntityExtensions(projectName),
+ Templates.LookupImplementation(projectName)
+ );
+
+ var projectMetadata = new TypeMetadata.Project(
+ projectName,
+ parentLookupClass.Name.Replace("ComponentsLookup", ""),
+ parentLookupClass.FullyQualifiedName()
+ );
+ foreach (var template in templates)
+ {
+ template.Process(projectMetadata);
+ }
+
+ // Fetch all relevant metadata.
+ var allTypeMetadata =
+ metadataFetcher.FetchMetadata(
+ bangTypeSymbols,
+ potentialComponents,
+ potentialStateMachines
+ );
+
+ // Process metadata.
+ foreach (var metadata in allTypeMetadata)
+ {
+ foreach (var template in templates)
+ {
+ template.Process(metadata);
+ }
+ }
+
+ // Generate sources.
+ foreach (var template in templates)
+ {
+ context.AddSource(template.FileName, template.GetDocumentWithReplacements());
+ }
+ }
+
+ private static int NumberOfParentClasses(INamedTypeSymbol type)
+ => type.BaseType is null ? 0 : 1 + NumberOfParentClasses(type.BaseType);
+}
diff --git a/src/Bang.Generator/Entrypoint.cs b/src/Bang.Generator/Entrypoint.cs
deleted file mode 100644
index 7e3bcce..0000000
--- a/src/Bang.Generator/Entrypoint.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-using System.Reflection;
-using System.Text;
-
-namespace Generator
-{
- ///
- /// This will generate code for fast retrieval of components within the entities.
- ///
- internal class Entrypoint
- {
- ///
- /// This expects the following arguments:
- /// `.\Entrypoint <-buildWithBinaries|-buildWithIntermediate> `
- /// is the source path to the target project, relative to the executable or absolute.
- /// is the path to the output directory, relative to the executable or absolute.
- /// is the namespace of the target.
- /// .
- /// Whenever the arguments mismatch the documentation.
- internal static async Task Main(string[] args)
- {
- List rawArguments = new();
- foreach (string arg in args)
- {
- rawArguments.AddRange(arg.Split(' '));
- }
-
- if (!TryParseArguments(rawArguments, out string[] arguments))
- {
- Warning("This expects the following arguments:\n" +
- "\t.\\Entrypoint <-buildWithBinaries|-buildWithIntermediate> \n\n" +
- "\t - is the source path to the target project, relative to the executable or absolute.\n" +
- "\t - is the path to the output directory, relative to the executable or absolute.\n" +
- "\t - is the namespace of the target.");
-
- throw new ArgumentException(nameof(args));
- }
-
- string ToRootPath(string s) =>
- Path.IsPathRooted(s) ? s : Path.GetFullPath(Path.Join(Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location), s));
-
- bool buildIntermediate = !string.Equals(arguments[0], "-buildWithIntermediate", StringComparison.InvariantCultureIgnoreCase);
- string projectPath = ToRootPath(arguments[1]);
- string outputPath = ToRootPath(arguments[2]);
- string targetNamespace = arguments[3];
-
- IEnumerable allAssemblies = GetAllLibrariesInPath(outputPath);
- if (!allAssemblies.Any())
- {
- Console.WriteLine("Unable to find the any binaries. Have you built the target project?");
- throw new InvalidOperationException();
- }
-
- List targetAssemblies = new();
- foreach (string assembly in allAssemblies)
- {
- try
- {
- targetAssemblies.Add(Assembly.LoadFrom(assembly));
- }
- catch (Exception e) when (e is FileLoadException || e is BadImageFormatException)
- {
- // Ignore invalid (or native) assemblies.
- }
- }
-
- string generatedFileDirectory = Path.Combine(projectPath, "Generated");
- CreateIfNotFound(generatedFileDirectory);
-
- Generation g = new(targetNamespace, targetAssemblies);
-
- if (buildIntermediate)
- {
- await g.GenerateIntermediate(generatedFileDirectory, outputPath);
- }
-
- await g.Generate(generatedFileDirectory);
-
- Console.WriteLine($"Finished generating components for {targetNamespace}!");
- }
-
- private static bool TryParseArguments(List arguments, out string[] result)
- {
- result = new string[4];
-
- if (arguments.Count < 4)
- {
- Warning("Expected at least 4 arguments, see documentation for Generator.");
-
- // Invalid count of arguments.
- return false;
- }
-
- int argIndex = 0;
-
- // <-buildWithBinaries|-buildWithIntermediate>
- if (arguments[argIndex][0] != '-')
- {
- Warning("Expected switch for <-buildWithBinaries|-buildWithIntermediate> argument.");
- return false;
- }
-
- result[0] = arguments[argIndex];
-
- if (GetArgumentWithMaybeSpaces(arguments, startIndex: 1, out argIndex) is not string targetSource)
- {
- return false;
- }
-
- result[1] = targetSource;
-
- if (GetArgumentWithMaybeSpaces(arguments, startIndex: argIndex, out argIndex) is not string outputPath)
- {
- return false;
- }
-
- result[2] = outputPath;
- result[3] = arguments[argIndex];
-
- return true;
- }
-
- private static string? GetArgumentWithMaybeSpaces(List arguments, int startIndex, out int lastIndex)
- {
- char firstSeparator = arguments[startIndex][0];
- if (firstSeparator != '\'' && firstSeparator != '"')
- {
- lastIndex = startIndex + 1;
- return arguments[startIndex];
- }
-
- StringBuilder result = new();
-
- int index = startIndex;
- while (index < arguments.Count)
- {
- int length = arguments[index].Length;
-
- bool isFirst = index == startIndex;
- bool isLast = arguments[index][length - 1] == firstSeparator;
-
- if (isLast) length--;
- if (isFirst) length--;
-
- result.Append(arguments[index], isFirst ? 1 : 0, length);
- if (isLast)
- {
- lastIndex = index + 1;
- return result.ToString();
- }
-
- result.Append(' ');
- index++;
- }
-
- Warning("Expected matching ' or \" char for argument.");
-
- lastIndex = -1;
- return null;
- }
-
- private static void Warning(in string warning)
- {
- Console.ForegroundColor = ConsoleColor.Yellow;
- Console.WriteLine(warning);
- Console.ResetColor();
- }
-
- ///
- /// Look recursively for all the files in .
- ///
- /// Rooted path to the binaries folder. This must be a valid directory.
- private static IEnumerable GetAllLibrariesInPath(in string path)
- {
- // TODO: Include linux.
- string[] targetExtensions = new string[] { "dll" };
-
- // 1. Filter all files that has an extension.
- // 2. Filter the target extensions.
- // 3. Distinguish the file names.
- return Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)
- .Where(s => targetExtensions.Contains(Path.GetExtension(s).TrimStart('.').ToLowerInvariant()))
- .GroupBy(s => Path.GetFileName(s))
- .Select(s => s.First());
- }
-
- ///
- /// Create a directory at if none is found.
- ///
-
- private static void CreateIfNotFound(in string path)
- {
- if (!Directory.Exists(path))
- {
- _ = Directory.CreateDirectory(path);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Bang.Generator/Extensions/HelperExtensions.cs b/src/Bang.Generator/Extensions/HelperExtensions.cs
new file mode 100644
index 0000000..a8e3bf4
--- /dev/null
+++ b/src/Bang.Generator/Extensions/HelperExtensions.cs
@@ -0,0 +1,73 @@
+using Microsoft.CodeAnalysis;
+
+namespace Bang.Generator.Extensions;
+
+public static class HelperExtensions
+{
+ private const string Message = "Message";
+ private const string Component = "Component";
+
+ public static string ToCleanMessageName(this string value)
+ => value.EndsWith(Message) ? value[..^Message.Length] : value;
+
+ public static string ToCleanComponentName(this string value)
+ => value.EndsWith(Component) ? value[..^Component.Length] : value;
+
+ public static IEnumerable Yield(this T value)
+ {
+ yield return value;
+ }
+
+ private static readonly SymbolDisplayFormat FullyQualifiedDisplayFormat =
+ new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
+
+ public static string FullyQualifiedName(this ITypeSymbol type)
+ {
+ var fullyQualifiedTypeName = type.ToDisplayString(FullyQualifiedDisplayFormat);
+ // Roslyn graces us with Nullable types as `T?` instead of `Nullable`, so we make an exception here.
+ if (fullyQualifiedTypeName.Contains("?") || type is not INamedTypeSymbol { IsGenericType: true } namedTypeSymbol)
+ {
+ return fullyQualifiedTypeName;
+ }
+
+ var genericTypes = string.Join(
+ ", ",
+ namedTypeSymbol.TypeArguments.Select(x => $"global::{x.FullyQualifiedName()}")
+ );
+
+ return $"{fullyQualifiedTypeName}<{genericTypes}>";
+
+ }
+
+ ///
+ /// Checks if the given implements the interface .
+ ///
+ /// Type declaration symbol.
+ /// Interface to be checked.
+ ///
+ public static bool ImplementsInterface(
+ this ITypeSymbol type,
+ ISymbol? interfaceToCheck
+ ) => type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, interfaceToCheck));
+
+ public static bool IsSubtypeOf(
+ this ITypeSymbol type,
+ ISymbol subtypeToCheck
+ )
+ {
+ ITypeSymbol? nextTypeToVerify = type;
+ do
+ {
+ var subtype = nextTypeToVerify?.BaseType;
+ if (subtype is not null && SymbolEqualityComparer.Default.Equals(subtype, subtypeToCheck))
+ {
+ return true;
+ }
+
+ nextTypeToVerify = subtype;
+
+ } while (nextTypeToVerify is not null);
+
+ return false;
+ }
+}
diff --git a/src/Bang.Generator/Extensions/IncrementalGeneratorExtensions.cs b/src/Bang.Generator/Extensions/IncrementalGeneratorExtensions.cs
new file mode 100644
index 0000000..90b6358
--- /dev/null
+++ b/src/Bang.Generator/Extensions/IncrementalGeneratorExtensions.cs
@@ -0,0 +1,31 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis;
+
+namespace Bang.Generator.Extensions;
+
+public static class IncrementalGeneratorExtensions
+{
+ public static IncrementalValuesProvider PotentialComponents(
+ this IncrementalGeneratorInitializationContext context
+ ) => context.SyntaxProvider.CreateSyntaxProvider(
+ (node, _) => node.IsStructOrRecordWithSubtypes(),
+ (c, _) => (TypeDeclarationSyntax)c.Node
+ );
+
+ public static IncrementalValuesProvider PotentialStateMachines(
+ this IncrementalGeneratorInitializationContext context
+ ) => context.SyntaxProvider.CreateSyntaxProvider(
+ (node, _) => node.IsClassWithSubtypes(),
+ (c, _) => (ClassDeclarationSyntax)c.Node
+ );
+
+ public static bool IsClassWithSubtypes(this SyntaxNode node)
+ => node is ClassDeclarationSyntax { BaseList.Types.Count: > 0 };
+
+ // Returns true for structs that implement an interface and records with base types.
+ // We only check if a record is a value type later on in the chain because we need a TypeSymbol.
+ public static bool IsStructOrRecordWithSubtypes(this SyntaxNode node)
+ => node is
+ RecordDeclarationSyntax { BaseList.Types.Count: > 0 } or
+ StructDeclarationSyntax { BaseList.Types.Count: > 0 };
+}
\ No newline at end of file
diff --git a/src/Bang.Generator/Generate.cs b/src/Bang.Generator/Generate.cs
deleted file mode 100644
index d953286..0000000
--- a/src/Bang.Generator/Generate.cs
+++ /dev/null
@@ -1,683 +0,0 @@
-using Bang.Components;
-using Bang.Interactions;
-using Bang.StateMachines;
-using Murder.Generator.Serialization;
-using System.Collections.Immutable;
-using System.Reflection;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace Generator
-{
- internal partial class Generation
- {
- private const string _template = "template.txt";
-
- ///
- /// This is the expected name of the file with intermediate information of already scanned data types.
- ///
- private const string _intermediateFile = ".components";
-
- private string IntermediateFile(string path) => Path.Join(path, _intermediateFile);
-
- private readonly string _targetNamespace;
- private readonly ImmutableArray _targetAssemblies;
-
- internal Generation(string targetAssembly, IEnumerable targetAssemblies)
- {
- _targetNamespace = targetAssembly;
- _targetAssemblies = targetAssemblies.ToImmutableArray();
- }
-
- internal async ValueTask GenerateIntermediate(string pathToIntermediate, string outputDirectory)
- {
- var componentsDescriptions =
- GetComponentsDescription(out var genericComponentsDescription, out int lastAvailableIndex);
-
- var messagesDescriptions =
- GetMessagesDescription(lastAvailableIndex);
-
- string path = IntermediateFile(pathToIntermediate);
- Descriptor descriptor = new(
- _targetNamespace,
- componentsDescriptions.ToDictionary(c => c.Name, c => c),
- messagesDescriptions.ToDictionary(m => m.Name, m => m),
- genericComponentsDescription.ToDictionary(m => m.GetName(), m => m));
-
- string parentPath = IntermediateFile(outputDirectory);
- if (File.Exists(parentPath))
- {
- Descriptor? parentDescriptor = await SerializationHelper.DeserializeAsDescriptor(parentPath);
- ProcessParentDescriptor(parentDescriptor, ref descriptor);
- }
-
- await SerializationHelper.Serialize(descriptor, path);
- }
-
- ///
- /// This will check for any references that were already considered in the parent path.
- ///
- private void ProcessParentDescriptor(Descriptor? parentDescriptor, ref Descriptor descriptor)
- {
- if (parentDescriptor is null || parentDescriptor.Namespace == _targetNamespace)
- {
- // It is actually the same intermediate file. Skip processing this.
- return;
- }
-
- descriptor.ParentDescriptor = parentDescriptor;
-
- ComponentDescriptor[] components = parentDescriptor.ComponentsWithParent();
- ComponentDescriptor[] messages = parentDescriptor.MessagesWithParent();
- GenericComponentDescriptor[] generics = parentDescriptor.GenericsWithParent();
-
- HashSet indices = new();
- foreach (ComponentDescriptor c in components)
- {
- indices.Add(c.Index);
-
- descriptor.ComponentsMap.Remove(c.Name);
- }
-
- foreach (ComponentDescriptor m in messages)
- {
- indices.Add(m.Index);
-
- descriptor.MessagesMap.Remove(m.Name);
- }
-
- foreach (GenericComponentDescriptor g in generics)
- {
- indices.Add(g.Index);
-
- descriptor.GenericsMap.Remove(g.GetName());
- }
-
- // Now, shift the indices of all the components we skipped.
- int shift = indices.Count;
- int recountIndex = 0;
-
- foreach (string name in descriptor.ComponentsMap.Keys)
- {
- descriptor.ComponentsMap[name].Index = recountIndex + shift;
-
- recountIndex++;
- }
-
- foreach (string name in descriptor.MessagesMap.Keys)
- {
- descriptor.MessagesMap[name].Index = recountIndex + shift;
-
- recountIndex++;
- }
-
- foreach (string name in descriptor.GenericsMap.Keys)
- {
- // For each of the generic components, we will try to map to its correspondent interface match.
- // This is done for IInteractiveComponent and IStateMachineComponent, which will point to the same index
- // of all their implementations.
- foreach (Type @interface in descriptor.GenericsMap[name].InstanceType.GetInterfaces())
- {
- if (parentDescriptor.ComponentsMap.TryGetValue(Prettify(@interface), out ComponentDescriptor? interfaceForGenericComponent))
- {
- descriptor.GenericsMap[name].Index = interfaceForGenericComponent.Index;
-
- // Found it!
- break;
- }
- }
- }
- }
-
- ///
- /// Generate the code for the Bang components.
- ///
- /// Path to the intermediate file to read from.
- /// Path to the source directory which will point to EntityExtensions.cs file.
- internal async ValueTask Generate(string generatedFileDirectory)
- {
- Descriptor descriptor =
- await SerializationHelper.DeserializeAsDescriptor(IntermediateFile(generatedFileDirectory));
-
- string outputFilePath = Path.Combine(generatedFileDirectory, "EntityExtensions.cs");
- string templatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _template);
-
- string targetAssemblyEscaped = ClassName(_targetNamespace);
-
- var (componentsDescriptions, messagesDescriptions, genericComponentsDescription) =
- (descriptor.Components, descriptor.Messages, descriptor.Generics);
-
- var (componentsDescriptionsWithParent, messagesDescriptionsWithParent, genericComponentsDescriptionWithParent) =
- (descriptor.ComponentsWithParent(), descriptor.MessagesWithParent(), descriptor.GenericsWithParent());
-
- IEnumerable targetTypes =
- componentsDescriptionsWithParent.Select(t => t.Type)
- .Concat(genericComponentsDescriptionWithParent.Select(t => t.InstanceType))
- .Concat(genericComponentsDescriptionWithParent.Select(t => t.GenericArgument))
- .Concat(messagesDescriptionsWithParent.Select(t => t.Type));
-
- Dictionary parameters = new()
- {
- { "", targetAssemblyEscaped },
- { "", GenerateNamespaces(targetTypes) },
- { "", GenerateEnums(componentsDescriptions) },
- { "", GenerateEnums(messagesDescriptions) },
- { "", GenerateComponentsGetter(componentsDescriptions) },
- { "", GenerateComponentsHas(componentsDescriptions) },
- { "", GenerateComponentsTryGet(componentsDescriptions) },
- { "", GenerateComponentsSet(componentsDescriptions) },
- { "", GenerateComponentsRemove(componentsDescriptions) },
- { "", GenerateMessagesHas(messagesDescriptions) },
- { "", GetComponentsLookupParentName(descriptor) },
- { "", GenerateRelativeSet(componentsDescriptionsWithParent) },
- { "", GenerateTypesDictionary(componentsDescriptionsWithParent, genericComponentsDescriptionWithParent) },
- { "", GenerateTypesDictionary(messagesDescriptionsWithParent) }
- };
-
- string template = await File.ReadAllTextAsync(templatePath);
- string formatted = parameters.Aggregate(template,
- (current, parameter) => current.Replace(parameter.Key, parameter.Value));
-
- await File.WriteAllTextAsync(outputFilePath, formatted);
- }
-
- private string ClassName(string name) => name.Replace('.', '_');
-
- private string GetComponentsLookupParentName(Descriptor descriptor) =>
- descriptor.ParentDescriptor is null ? "ComponentsLookup" : $"{ClassName(descriptor.ParentDescriptor.Namespace)}LookupImplementation";
-
- private List GetMessagesDescription(int startingIndex)
- {
- IEnumerable messages = ReflectionHelper.GetAllCandidateMessages(_targetAssemblies);
-
- int index = startingIndex;
- return messages.Select(m => new ComponentDescriptor(index++, Prettify(m), m)).ToList();
- }
-
- private List GetComponentsDescription(
- out List generics,
- out int lastAvailableIndex)
- {
- IEnumerable components = ReflectionHelper.GetAllCandidateComponents(_targetAssemblies);
-
- Dictionary lookup = new();
- List result = new();
-
- int index = 0;
- foreach (Type t in components)
- {
- lookup[t] = index++;
- result.Add(new(lookup[t], Prettify(t), t));
- }
-
- generics = new();
-
- Type genericStateMachineComponent = typeof(StateMachineComponent<>);
-
- Type stateMachineComponent = typeof(IStateMachineComponent);
- IEnumerable allStateMachines = ReflectionHelper.GetAllStateMachineComponents(_targetAssemblies);
-
- if (allStateMachines.Any())
- {
- if (!lookup.ContainsKey(stateMachineComponent))
- {
- // Generic has not been added yet.
- lookup[stateMachineComponent] = index++;
- result.Add(new(lookup[stateMachineComponent], Prettify(stateMachineComponent), stateMachineComponent));
- }
- }
-
- foreach (Type t in allStateMachines)
- {
- generics.Add(new(lookup[stateMachineComponent], genericStateMachineComponent, t));
- }
-
- Type interactiveComponent = typeof(IInteractiveComponent);
- Type genericInteractiveComponent = typeof(InteractiveComponent<>);
-
- foreach (Type t in ReflectionHelper.GetAllInteractionComponents(_targetAssemblies))
- {
- if (!lookup.ContainsKey(interactiveComponent))
- {
- // Generic has not been added yet.
- lookup[interactiveComponent] = index++;
- result.Add(new(lookup[interactiveComponent], Prettify(interactiveComponent), interactiveComponent));
- }
-
- generics.Add(new(lookup[interactiveComponent], genericInteractiveComponent, t));
- }
-
- Type tTransformBaseInterface = typeof(ITransformComponent);
- Type tTransformComponent = ReflectionHelper.FindTransformInterfaceComponent(_targetAssemblies);
-
- foreach (Type t in ReflectionHelper.GetAllTransformComponents(_targetAssemblies))
- {
- if (!lookup.ContainsKey(tTransformComponent))
- {
- // Interface has not been added yet.
- lookup[tTransformComponent] = index++;
-
- result.Add(new(index: lookup[tTransformComponent], name: Prettify(tTransformBaseInterface), tTransformComponent));
- result.Add(new(index: lookup[tTransformComponent], name: Prettify(tTransformBaseInterface) + "Base", tTransformBaseInterface));
- }
-
- generics.Add(new(lookup[tTransformComponent], t, genericArgument: null));
- }
-
- lastAvailableIndex = index;
- return result;
- }
-
- private string GenerateNamespaces(IEnumerable types)
- {
- StringBuilder builder = new();
-
- HashSet allNamespaces = new();
-
- foreach (Type? t in types)
- {
- if (t is not null && t.Namespace is string @namespace && !allNamespaces.Contains(@namespace))
- {
- allNamespaces.Add(@namespace);
- }
- }
-
- foreach (string @namespace in allNamespaces)
- {
- builder.Append($"using {@namespace};\n");
- }
-
- // Trim last extra enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 1, 1);
- }
-
- return builder.ToString();
- }
-
- private string GenerateEnums(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.Append($"{desc.Name} = {desc.Index},\n");
- }
-
- // Trim last extra comma and enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateComponentsGetter(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{ReflectionHelper.GetAccessModifier(desc.Type)} static {desc.Type.Name} Get{desc.Name}(this Entity e)\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" return e.GetComponent<{desc.Type.Name}>({desc.Index});\n");
- builder.AppendFormat(" }}\n\n");
- }
-
- // Trim last enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateComponentsHas(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (desc.Name is null)
- {
- // Skip syntax sugar if there is no name.
- continue;
- }
-
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{ReflectionHelper.GetAccessModifier(desc.Type)} static bool Has{desc.Name}(this Entity e)\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" return e.HasComponent({desc.Index});\n");
- builder.AppendFormat(" }}\n\n");
- }
-
- // Trim last enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateComponentsTryGet(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{ReflectionHelper.GetAccessModifier(desc.Type)} static {desc.Type.Name}? TryGet{desc.Name}(this Entity e)\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" if (!e.Has{desc.Name}())\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" return null;\n");
- builder.AppendFormat(" }}\n\n");
- builder.AppendFormat($" return e.Get{desc.Name}();\n");
- builder.AppendFormat(" }}\n\n");
- }
-
- // Trim last enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateComponentsSet(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{ReflectionHelper.GetAccessModifier(desc.Type)} static void Set{desc.Name}(this Entity e, {desc.Type.Name} component)\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" e.AddOrReplaceComponent(component, {desc.Index});\n");
- builder.AppendFormat(" }}\n\n");
-
- ConstructorInfo[] constructors = desc.Type.GetConstructors();
- IEnumerable constructorsParameters = constructors.Select(c => c.GetParameters());
-
- // Check if there are any default constructors already available.
- if (desc.Type.IsValueType && !constructorsParameters.Any(p => p.Length == 0))
- {
- // Always add a default constructor by default.
- constructorsParameters = constructorsParameters.Append(new ParameterInfo[0]);
- }
-
- // Create fancy constructors based on the component!
- foreach (ParameterInfo[] parameters in constructorsParameters)
- {
- builder.AppendFormat($" {ReflectionHelper.GetAccessModifier(desc.Type)} static void Set{desc.Name}(this Entity e");
- foreach (ParameterInfo p in parameters)
- {
- string parameterName = p.ParameterType.IsGenericType ?
- FormatGenericName(p.ParameterType, p.ParameterType.GetGenericArguments()) :
- FormatNonGenericTypeName(p.ParameterType);
-
- builder.Append($", {parameterName} {p.Name}");
- }
-
- builder.AppendFormat(")\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" e.AddOrReplaceComponent(new {desc.Type.Name}(");
- for (int c = 0; c < parameters.Length; c++)
- {
- builder.Append($"{parameters[c].Name}");
-
- if (c != parameters.Length - 1)
- {
- builder.Append($", ");
- }
- }
-
- builder.AppendFormat($"), {desc.Index});\n");
- builder.AppendFormat(" }}\n\n");
- }
- }
-
- // Trim last enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateComponentsRemove(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{ReflectionHelper.GetAccessModifier(desc.Type)} static bool Remove{desc.Name}(this Entity e)\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" return e.RemoveComponent({desc.Index});\n");
- builder.AppendFormat(" }}\n\n");
- }
-
- // Trim last enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateMessagesHas(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (desc.Name is null)
- {
- // Skip syntax sugar if there is no name.
- continue;
- }
-
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{ReflectionHelper.GetAccessModifier(desc.Type)} static bool Has{desc.Name}Message(this Entity e)\n");
- builder.AppendFormat(" {{\n");
- builder.AppendFormat($" return e.HasMessage({desc.Index});\n");
- builder.AppendFormat(" }}\n\n");
- }
-
- // Trim last enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateRelativeSet(IEnumerable descriptions)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (!typeof(IParentRelativeComponent).IsAssignableFrom(desc.Type))
- {
- // Not a relative component.
- continue;
- }
-
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.AppendFormat($"{desc.Index},\n");
- }
-
- // Trim last comma+enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string GenerateTypesDictionary(
- IEnumerable descriptions,
- IEnumerable? generics = default)
- {
- StringBuilder builder = new();
-
- foreach (ComponentDescriptor desc in descriptions)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.Append("{ ");
- builder.AppendFormat($"typeof({desc.Type.Name}), {desc.Index}");
- builder.Append(" },\n");
- }
-
- if (generics is not null)
- {
- foreach (GenericComponentDescriptor g in generics)
- {
- if (builder.Length > 0)
- {
- builder.Append(" ");
- }
-
- builder.Append("{ ");
- builder.AppendFormat("typeof({0}), {1}",
- g.GetName(),
- g.Index);
- builder.Append(" },\n");
- }
- }
-
- // Trim last comma+enter.
- if (builder.Length > 0)
- {
- builder.Remove(builder.Length - 2, 2);
- }
-
- return builder.ToString();
- }
-
- private string FormatNonGenericTypeName(Type type)
- {
- StringBuilder builder = new();
-
- string name = type.FullName!;
- if (name.Contains("&"))
- {
- builder.Append($"in {name.Substring(0, name.LastIndexOf("&", StringComparison.InvariantCulture))}");
- }
- else
- {
- builder.Append(name);
- }
-
- return builder.ToString();
- }
-
- private string FormatGenericName(Type genericType, params Type[] genericArguments)
- {
- StringBuilder builder = new();
-
- string fullname = genericType.FullName!;
-
- builder.AppendFormat("{0}<",
- fullname.Substring(0, fullname.LastIndexOf("`", StringComparison.InvariantCulture)));
- for (int a = 0; a < genericArguments.Length; a++)
- {
- builder.AppendFormat($"{genericArguments[a].FullName}");
-
- if (a != genericArguments.Length - 1)
- {
- builder.Append(", ");
- }
- }
-
- builder.Append(">");
-
- // So far, this cover cases such as:
- // System.Collections.Immutable.ImmutableDictionary`2+Builder[[...]]
- // There might be other edge cases that we might need to implement here.
- int indexAfterGeneric = fullname.IndexOf('+');
- if (indexAfterGeneric != -1)
- {
- int end = fullname.IndexOf('[');
- builder.AppendFormat(".{0}",
- fullname.Substring(indexAfterGeneric + 1, end - indexAfterGeneric - 1));
- }
-
- return builder.ToString();
- }
-
- ///
- /// Prettify the name of .
- ///
- internal static string Prettify(Type t)
- {
- StringBuilder builder = new(t.Name);
- if (t.IsInterface && builder[0] == 'I')
- {
- // If this is an interface, skip the "I" character.
- builder.Remove(0, 1);
- }
-
- string name = builder.ToString();
-
- // Remove "Component" of the name.
- Regex re = new Regex(@"(.*)(?=Component|Message)");
- Match m = re.Match(name);
- if (m.Success)
- {
- name = m.Groups[0].Value;
- }
-
- return name;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Bang.Generator/Metadata/MetadataFetcher.cs b/src/Bang.Generator/Metadata/MetadataFetcher.cs
new file mode 100644
index 0000000..99f1f92
--- /dev/null
+++ b/src/Bang.Generator/Metadata/MetadataFetcher.cs
@@ -0,0 +1,137 @@
+using Bang.Generator.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Metadata;
+
+public sealed class MetadataFetcher
+{
+ private readonly Compilation compilation;
+
+ public MetadataFetcher(Compilation compilation)
+ {
+ this.compilation = compilation;
+ }
+
+ public IEnumerable FetchMetadata(
+ BangTypeSymbols bangTypeSymbols,
+ ImmutableArray potentialComponents,
+ ImmutableArray potentialStateMachines
+ )
+ {
+ // Gets all potential components/messages from the assembly this generator is processing.
+ var allValueTypesToBeCompiled = potentialComponents
+ .SelectMany(ValueTypeFromTypeDeclarationSyntax)
+ .ToImmutableArray();
+
+ var componentIndexOffset = 0;
+ var components = FetchComponents(bangTypeSymbols, allValueTypesToBeCompiled);
+ foreach (var component in components)
+ {
+ yield return component;
+ componentIndexOffset++;
+ }
+
+ var messages = FetchMessages(bangTypeSymbols, allValueTypesToBeCompiled, componentIndexOffset);
+ foreach (var message in messages)
+ {
+ yield return message;
+ }
+
+ var stateMachines = FetchStateMachines(bangTypeSymbols, potentialStateMachines);
+ foreach (var stateMachine in stateMachines)
+ {
+ yield return stateMachine;
+ }
+
+ var interactions = FetchInteractions(bangTypeSymbols, allValueTypesToBeCompiled);
+ foreach (var interaction in interactions)
+ {
+ yield return interaction;
+ }
+ }
+
+ private IEnumerable FetchComponents(
+ BangTypeSymbols bangTypeSymbols,
+ ImmutableArray allValueTypesToBeCompiled
+ ) => allValueTypesToBeCompiled
+ .Where(t => !t.IsGenericType && t.ImplementsInterface(bangTypeSymbols.ComponentInterface))
+ .OrderBy(c => c.Name)
+ .Select((component, index) => new TypeMetadata.Component(
+ Index: index,
+ FriendlyName: component.Name.ToCleanComponentName(),
+ FullyQualifiedName: component.FullyQualifiedName(),
+ IsInternal: component.DeclaredAccessibility == Accessibility.Internal,
+ IsTransformComponent: component.ImplementsInterface(bangTypeSymbols.TransformInterface),
+ IsMurderTransformComponent: component.ImplementsInterface(bangTypeSymbols.MurderTransformInterface),
+ IsParentRelativeComponent: component.ImplementsInterface(bangTypeSymbols.ParentRelativeComponentInterface),
+ Constructors: component.Constructors
+ .Where(c => c.DeclaredAccessibility == Accessibility.Public)
+ .Select(ConstructorMetadataFromConstructor)
+ .ToImmutableArray()
+ ));
+
+ private ConstructorMetadata ConstructorMetadataFromConstructor(IMethodSymbol methodSymbol) => new(
+ methodSymbol.Parameters
+ .Select(p => new ConstructorParameter(p.Name, p.Type.FullyQualifiedName()))
+ .ToImmutableArray()
+ );
+
+ private IEnumerable FetchMessages(
+ BangTypeSymbols bangTypeSymbols,
+ ImmutableArray allValueTypesToBeCompiled,
+ int componentIndexOffset
+ ) => allValueTypesToBeCompiled
+ .Where(t => !t.IsGenericType && t.ImplementsInterface(bangTypeSymbols.MessageInterface))
+ .OrderBy(x => x.Name)
+ .Select((message, index) => new TypeMetadata.Message(
+ Index: index + componentIndexOffset,
+ TypeName: message.Name,
+ IsInternal: message.DeclaredAccessibility == Accessibility.Internal,
+ FriendlyName: message.Name.ToCleanComponentName(),
+ FullyQualifiedName: message.FullyQualifiedName()
+ ));
+
+ private IEnumerable FetchStateMachines(
+ BangTypeSymbols bangTypeSymbols,
+ ImmutableArray potentialStateMachines
+ ) => potentialStateMachines
+ .Select(GetTypeSymbol)
+ .Where(t => !t.IsAbstract && t.IsSubtypeOf(bangTypeSymbols.StateMachineClass))
+ .OrderBy(x => x.Name)
+ .Select(s => new TypeMetadata.StateMachine(
+ IsInternal: s.DeclaredAccessibility == Accessibility.Internal,
+ FullyQualifiedName: s.FullyQualifiedName())
+ ).Distinct();
+
+ private static IEnumerable FetchInteractions(
+ BangTypeSymbols bangTypeSymbols,
+ ImmutableArray allValueTypesToBeCompiled
+ ) => allValueTypesToBeCompiled
+ .Where(t => !t.IsGenericType && t.ImplementsInterface(bangTypeSymbols.InteractionInterface))
+ .OrderBy(i => i.Name)
+ .Select(i => new TypeMetadata.Interaction(
+ IsInternal: i.DeclaredAccessibility == Accessibility.Internal,
+ FullyQualifiedName: i.FullyQualifiedName())
+ );
+
+ private IEnumerable ValueTypeFromTypeDeclarationSyntax(TypeDeclarationSyntax typeDeclarationSyntax)
+ {
+ var semanticModel = compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree);
+ if (semanticModel.GetDeclaredSymbol(typeDeclarationSyntax) is not INamedTypeSymbol potentialComponentTypeSymbol)
+ return Enumerable.Empty();
+
+ // Record classes cannot be components or messages.
+ if (typeDeclarationSyntax is RecordDeclarationSyntax && !potentialComponentTypeSymbol.IsValueType)
+ return Enumerable.Empty();
+
+ return potentialComponentTypeSymbol.Yield();
+ }
+
+ private INamedTypeSymbol GetTypeSymbol(ClassDeclarationSyntax classDeclarationSyntax)
+ {
+ var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree);
+ return (INamedTypeSymbol)semanticModel.GetDeclaredSymbol(classDeclarationSyntax)!;
+ }
+}
diff --git a/src/Bang.Generator/Metadata/Models.cs b/src/Bang.Generator/Metadata/Models.cs
new file mode 100644
index 0000000..08d7ebb
--- /dev/null
+++ b/src/Bang.Generator/Metadata/Models.cs
@@ -0,0 +1,137 @@
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Metadata;
+
+public sealed class BangTypeSymbols
+{
+ public INamedTypeSymbol ComponentInterface { get; }
+ public INamedTypeSymbol MessageInterface { get; }
+ public INamedTypeSymbol ParentRelativeComponentInterface { get; }
+ public INamedTypeSymbol StateMachineClass { get; }
+ public INamedTypeSymbol InteractionInterface { get; }
+ public INamedTypeSymbol ComponentsLookupClass { get; }
+ public INamedTypeSymbol TransformInterface { get; }
+ public INamedTypeSymbol? MurderTransformInterface { get; }
+
+ private BangTypeSymbols(INamedTypeSymbol componentInterface,
+ INamedTypeSymbol messageInterface,
+ INamedTypeSymbol parentRelativeComponentInterface,
+ INamedTypeSymbol stateMachineClass,
+ INamedTypeSymbol interactionInterface,
+ INamedTypeSymbol componentsLookupClass,
+ INamedTypeSymbol transformInterface,
+ INamedTypeSymbol? murderTransformInterface)
+ {
+ MessageInterface = messageInterface;
+ StateMachineClass = stateMachineClass;
+ ComponentInterface = componentInterface;
+ TransformInterface = transformInterface;
+ InteractionInterface = interactionInterface;
+ ComponentsLookupClass = componentsLookupClass;
+ MurderTransformInterface = murderTransformInterface;
+ ParentRelativeComponentInterface = parentRelativeComponentInterface;
+ }
+
+ public static BangTypeSymbols? FromCompilation(Compilation compilation)
+ {
+ // Bail if IComponent is not resolvable.
+ var componentInterface = compilation.GetTypeByMetadataName("Bang.Components.IComponent");
+ if (componentInterface is null)
+ return null;
+
+ // Bail if IMessage is not resolvable.
+ var messageInterface = compilation.GetTypeByMetadataName("Bang.Components.IMessage");
+ if (messageInterface is null)
+ return null;
+
+ // Bail if IParentRelativeComponent is not resolvable.
+ var parentRelativeComponentInterface = compilation.GetTypeByMetadataName("Bang.Components.IParentRelativeComponent");
+ if (parentRelativeComponentInterface is null)
+ return null;
+
+ // Bail if StateMachine is not resolvable.
+ var stateMachineClass = compilation.GetTypeByMetadataName("Bang.StateMachines.StateMachine");
+ if (stateMachineClass is null)
+ return null;
+
+ // Bail if IInteraction is not resolvable.
+ var interactionInterface = compilation.GetTypeByMetadataName("Bang.Interactions.IInteraction");
+ if (interactionInterface is null)
+ return null;
+
+ // Bail if ComponentsLookup is not resolvable.
+ var componentsLookupClass = compilation.GetTypeByMetadataName("Bang.ComponentsLookup");
+ if (componentsLookupClass is null)
+ return null;
+
+ // Bail if ITransformComponent is not resolvable.
+ var transformComponentInterface = compilation.GetTypeByMetadataName("Bang.Components.ITransformComponent");
+ if (transformComponentInterface is null)
+ return null;
+
+ // This is not part of Bang, so it can be null.
+ var murderTransformComponentInterface = compilation.GetTypeByMetadataName("Murder.Components.IMurderTransformComponent");
+
+ return new BangTypeSymbols(
+ componentInterface,
+ messageInterface,
+ parentRelativeComponentInterface,
+ stateMachineClass,
+ interactionInterface,
+ componentsLookupClass,
+ transformComponentInterface,
+ murderTransformComponentInterface
+ );
+ }
+}
+
+public sealed record ConstructorParameter(
+ string Name,
+ string FullyQualifiedTypeName
+);
+
+public sealed record ConstructorMetadata(
+ ImmutableArray Parameters
+);
+
+public abstract record TypeMetadata
+{
+ public sealed record Project(
+ string ProjectName,
+ string? ParentProjectName,
+ string ParentProjectLookupClassName
+ ) : TypeMetadata;
+
+ public sealed record Component(
+ int Index,
+ bool IsInternal,
+ string FriendlyName,
+ string FullyQualifiedName,
+ bool IsTransformComponent,
+ bool IsParentRelativeComponent,
+ bool IsMurderTransformComponent,
+ ImmutableArray Constructors
+ ) : TypeMetadata;
+
+ public sealed record Message(
+ int Index,
+ bool IsInternal,
+ string TypeName,
+ string FriendlyName,
+ string FullyQualifiedName
+ ) : TypeMetadata;
+
+ // TODO: These can be turned into something like `GenericComponentConstrainedType` should we go the route
+ // to support arbitrary, user provided generic types.
+ public sealed record StateMachine(
+ bool IsInternal,
+ string FullyQualifiedName
+ ) : TypeMetadata;
+
+ public sealed record Interaction(
+ bool IsInternal,
+ string FullyQualifiedName
+ ) : TypeMetadata;
+}
+
diff --git a/src/Bang.Generator/Metadata/ReferencedAssemblyTypeFetcher.cs b/src/Bang.Generator/Metadata/ReferencedAssemblyTypeFetcher.cs
new file mode 100644
index 0000000..67d213c
--- /dev/null
+++ b/src/Bang.Generator/Metadata/ReferencedAssemblyTypeFetcher.cs
@@ -0,0 +1,56 @@
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Metadata;
+
+public sealed class ReferencedAssemblyTypeFetcher
+{
+ private readonly Compilation compilation;
+ private ImmutableArray? cacheOfAllTypesInReferencedAssemblies;
+
+ public ReferencedAssemblyTypeFetcher(Compilation compilation)
+ {
+ this.compilation = compilation;
+ }
+
+ private ImmutableArray AllTypesInReferencedAssemblies()
+ {
+ if (cacheOfAllTypesInReferencedAssemblies is not null)
+ return cacheOfAllTypesInReferencedAssemblies.Value;
+
+ var allTypesInReferencedAssemblies =
+ compilation.SourceModule.ReferencedAssemblySymbols
+ .SelectMany(assemblySymbol =>
+ assemblySymbol
+ .GlobalNamespace.GetNamespaceMembers()
+ .SelectMany(GetAllTypesInNamespace))
+ .ToImmutableArray();
+
+ cacheOfAllTypesInReferencedAssemblies = allTypesInReferencedAssemblies;
+ return allTypesInReferencedAssemblies;
+ }
+
+ public ImmutableArray GetAllCompiledClassesWithSubtypes()
+ => AllTypesInReferencedAssemblies()
+ .Where(typeSymbol => !typeSymbol.IsValueType && typeSymbol.BaseType is not null)
+ .ToImmutableArray();
+
+ // Recursive method to get all types in a namespace, including nested types.
+ private static IEnumerable GetAllTypesInNamespace(INamespaceSymbol namespaceSymbol)
+ {
+ foreach (var type in namespaceSymbol.GetTypeMembers())
+ {
+ yield return type;
+ }
+
+ var nestedTypes =
+ from nestedNamespace in namespaceSymbol.GetNamespaceMembers()
+ from nestedType in GetAllTypesInNamespace(nestedNamespace)
+ select nestedType;
+
+ foreach (var nestedType in nestedTypes)
+ {
+ yield return nestedType;
+ }
+ }
+}
diff --git a/src/Bang.Generator/ReflectionHelper.cs b/src/Bang.Generator/ReflectionHelper.cs
deleted file mode 100644
index a093e06..0000000
--- a/src/Bang.Generator/ReflectionHelper.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using Bang.Components;
-using Bang.Interactions;
-using Bang.StateMachines;
-using System.Reflection;
-
-namespace Generator
-{
- internal class ReflectionHelper
- {
- private static readonly Type _componentType = typeof(IComponent);
- private static readonly Type _messageType = typeof(IMessage);
-
- private static readonly Type[] _interfaceComponents = new Type[] { typeof(ITransformComponent) };
-
- public static IEnumerable GetAllCandidateComponents(IEnumerable targetAssemblies)
- {
- // Order by name to guarantee consistency across different runs.
- return targetAssemblies.SelectMany(s => s.GetTypes())
- .Where(t => IsCandidateComponent(t))
- .OrderBy(t => t.Name);
- }
-
- public static IEnumerable GetAllStateMachineComponents(IEnumerable targetAssemblies)
- {
- // Order by name to guarantee consistency across different runs.
- return targetAssemblies.SelectMany(s => s.GetTypes())
- .Where(t => typeof(StateMachine).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
- .OrderBy(t => t.Name);
- }
-
- public static IEnumerable GetAllInteractionComponents(IEnumerable targetAssemblies)
- {
- // Order by name to guarantee consistency across different runs.
- return targetAssemblies.SelectMany(s => s.GetTypes())
- .Where(t => typeof(IInteraction).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
- .OrderBy(t => t.Name);
- }
-
- ///
- /// This will return whatever engine specific transform interface is available within the engine.
- ///
- internal static Type FindTransformInterfaceComponent(IEnumerable targetAssemblies)
- {
- // Order by name to guarantee consistency across different runs.
- return targetAssemblies.SelectMany(s => s.GetTypes())
- .Where(t => typeof(ITransformComponent).IsAssignableFrom(t) && t.IsInterface && t != typeof(ITransformComponent))
- .OrderBy(t => t.Name)
- .First();
- }
-
- public static IEnumerable GetAllTransformComponents(IEnumerable targetAssemblies)
- {
- // Order by name to guarantee consistency across different runs.
- return targetAssemblies.SelectMany(s => s.GetTypes())
- .Where(t => typeof(ITransformComponent).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
- .OrderBy(t => t.Name);
- }
-
- ///
- /// Returns whether is a valid component for the generated code.
- ///
- private static bool IsCandidateComponent(Type t)
- {
- // Interfaces, abstract or generics are filtered out.
- // We also filter out components that inherit from a transform.
- if (t.IsInterface || t.IsAbstract || t.IsGenericType || _interfaceComponents.Any(tt => tt.IsAssignableFrom(t)))
- {
- return false;
- }
-
- return _componentType.IsAssignableFrom(t);
- }
-
- public static IEnumerable GetAllCandidateMessages(IEnumerable targetAssemblies)
- {
- IEnumerable messages = targetAssemblies.SelectMany(s => s.GetTypes())
- .Where(t => IsCandidateMessage(t));
-
- // Order by name to guarantee consistency across different runs.
- return messages.OrderBy(t => t.Name);
- }
-
-
- ///
- /// Returns whether is a valid component for the generated code.
- ///
- private static bool IsCandidateMessage(Type t)
- {
- // Interfaces, abstract or generics are filtered out.
- if (t.IsInterface || t.IsAbstract || t.IsGenericType)
- {
- return false;
- }
-
- return _messageType.IsAssignableFrom(t);
- }
-
- ///
- /// Get the modifier for when generating source code.
- ///
- public static string GetAccessModifier(Type t)
- {
- return t.IsPublic ? "public" : "internal";
- }
- }
-}
diff --git a/src/Bang.Generator/Serialization/ComponentDescriptor.cs b/src/Bang.Generator/Serialization/ComponentDescriptor.cs
deleted file mode 100644
index ae047f6..0000000
--- a/src/Bang.Generator/Serialization/ComponentDescriptor.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace Murder.Generator.Serialization
-{
- ///
- /// This is the component that describes a component which will be generated in the final code.
- ///
- internal record ComponentDescriptor
- {
- public int Index;
- public readonly string Name;
-
- public readonly Type Type;
-
- public ComponentDescriptor(int index, string name, Type type)
- => (Index, Name, Type) = (index, name, type);
- }
-}
diff --git a/src/Bang.Generator/Serialization/Descriptor.cs b/src/Bang.Generator/Serialization/Descriptor.cs
deleted file mode 100644
index 1738e19..0000000
--- a/src/Bang.Generator/Serialization/Descriptor.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System.Linq;
-
-namespace Murder.Generator.Serialization
-{
- internal record Descriptor
- {
- ///
- /// Namespace name of the target that generated this descriptor.
- ///
- public readonly string Namespace;
-
- public readonly Dictionary ComponentsMap;
- public readonly Dictionary MessagesMap;
- public readonly Dictionary GenericsMap;
-
- public Descriptor? ParentDescriptor = default;
-
- private ComponentDescriptor[]? _components;
- private ComponentDescriptor[]? _messages;
- private GenericComponentDescriptor[]? _generics;
-
- public ComponentDescriptor[] Components =>
- _components ??= ComponentsMap.Values.OrderBy(c => c.Index).ToArray();
-
- public ComponentDescriptor[] Messages =>
- _messages ??= MessagesMap.Values.OrderBy(c => c.Index).ToArray();
-
- public GenericComponentDescriptor[] Generics =>
- _generics ??= GenericsMap.Values.OrderBy(c => c.Index).ToArray();
-
- public ComponentDescriptor[] ComponentsWithParent() =>
- ParentDescriptor is null ? Components : ParentDescriptor.Components.Concat(Components).ToArray();
-
- public ComponentDescriptor[] MessagesWithParent() =>
- ParentDescriptor is null ? Messages : ParentDescriptor.Messages.Concat(Messages).ToArray();
-
- public GenericComponentDescriptor[] GenericsWithParent() =>
- ParentDescriptor is null ? Generics : ParentDescriptor.Generics.Concat(Generics).ToArray();
-
- public Descriptor(
- string @namespace,
- Dictionary componentsMap,
- Dictionary messagesMap,
- Dictionary genericsMap)
- => (Namespace, ComponentsMap, MessagesMap, GenericsMap) = (@namespace, componentsMap, messagesMap, genericsMap);
- }
-}
diff --git a/src/Bang.Generator/Serialization/GenericComponentDescriptor.cs b/src/Bang.Generator/Serialization/GenericComponentDescriptor.cs
deleted file mode 100644
index dd82546..0000000
--- a/src/Bang.Generator/Serialization/GenericComponentDescriptor.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-namespace Murder.Generator.Serialization
-{
- ///
- /// This is the component that describes a component which will be generated in the final code.
- ///
- internal record GenericComponentDescriptor
- {
- public int Index;
-
- public readonly Type InstanceType;
- public readonly Type? GenericArgument;
-
- private string? _format;
-
- public string GetName()
- {
- if (_format is null)
- {
- _format = GenericArgument is null ?
- InstanceType.Name :
- string.Format("{0}<{1}>",
- InstanceType.Name.Substring(0, InstanceType.Name.LastIndexOf("`", StringComparison.InvariantCulture)),
- GenericArgument.Name);
- }
-
- return _format;
- }
-
- public GenericComponentDescriptor(int index, Type instanceType, Type? genericArgument = null)
- => (Index, InstanceType, GenericArgument) = (index, instanceType, genericArgument);
- }
-}
diff --git a/src/Bang.Generator/Serialization/SerializationHelper.cs b/src/Bang.Generator/Serialization/SerializationHelper.cs
deleted file mode 100644
index d944a20..0000000
--- a/src/Bang.Generator/Serialization/SerializationHelper.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Newtonsoft.Json;
-
-namespace Murder.Generator.Serialization
-{
- internal class SerializationHelper
- {
- private readonly static JsonSerializerSettings _settings = new()
- {
- Formatting = Formatting.Indented,
- MissingMemberHandling = MissingMemberHandling.Ignore
- };
-
- public static async ValueTask Serialize(
- Descriptor descriptor,
- string path)
- {
- string json = JsonConvert.SerializeObject(descriptor, _settings);
- await File.WriteAllTextAsync(path, json);
- }
-
- public static async ValueTask DeserializeAsDescriptor(string path)
- {
- string json = await File.ReadAllTextAsync(path);
-
- Descriptor? descriptor = JsonConvert.DeserializeObject(json, _settings);
- if (descriptor is null)
- {
- Console.WriteLine($"Unable to deserialize descriptor at path '{path}'.");
- throw new ArgumentException(path);
- }
-
- return descriptor;
- }
- }
-}
diff --git a/src/Bang.Generator/Templating/FileTemplate.cs b/src/Bang.Generator/Templating/FileTemplate.cs
new file mode 100644
index 0000000..56b6e7f
--- /dev/null
+++ b/src/Bang.Generator/Templating/FileTemplate.cs
@@ -0,0 +1,47 @@
+using Bang.Generator.Metadata;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Templating;
+
+public sealed class FileTemplate
+{
+ private readonly string templateText;
+ private readonly ImmutableArray substitutions;
+
+ public string FileName { get; }
+
+ public FileTemplate(
+ string fileName,
+ string templateText,
+ ImmutableArray substitutions
+ )
+ {
+ FileName = fileName;
+ this.substitutions = substitutions;
+ this.templateText = templateText;
+ }
+
+ public void Process(TypeMetadata metadata)
+ {
+ foreach (var substitution in substitutions)
+ {
+ substitution.Process(metadata);
+ }
+ }
+
+ public string GetDocumentWithReplacements() => substitutions.Aggregate(
+ templateText,
+ (text, substitution) => text.Replace(
+ substitution.StringToReplaceInTemplate,
+ substitution.GetProcessedText()
+ )
+ );
+}
+
+internal sealed class ProjectPrefixSubstitution : TemplateSubstitution
+{
+ public ProjectPrefixSubstitution() : base("") { }
+
+ protected override string ProcessProject(TypeMetadata.Project project)
+ => project.ProjectName;
+}
\ No newline at end of file
diff --git a/src/Bang.Generator/Templating/TemplateSubstitution.cs b/src/Bang.Generator/Templating/TemplateSubstitution.cs
new file mode 100644
index 0000000..b26f1db
--- /dev/null
+++ b/src/Bang.Generator/Templating/TemplateSubstitution.cs
@@ -0,0 +1,67 @@
+using Bang.Generator.Metadata;
+using System.Text;
+
+namespace Bang.Generator.Templating;
+
+///
+/// Aggregates data from all components and messages and turns it into a string that gets replaced into a
+///
+public abstract class TemplateSubstitution
+{
+ private readonly StringBuilder aggregatedText = new();
+
+ protected string ProjectPrefix = "";
+ protected string ParentProjectPrefix = "";
+
+ ///
+ /// String this substitution will replace in a
+ ///
+ public string StringToReplaceInTemplate { get; }
+
+ protected TemplateSubstitution(string stringToReplaceInTemplate)
+ {
+ StringToReplaceInTemplate = stringToReplaceInTemplate;
+ }
+
+ public void Process(TypeMetadata metadata)
+ {
+ var result = metadata switch
+ {
+ TypeMetadata.Project project => SaveAndProcessProject(project),
+ TypeMetadata.Message message => ProcessMessage(message),
+ TypeMetadata.Component component => ProcessComponent(component),
+ TypeMetadata.Interaction interaction => ProcessInteraction(interaction),
+ TypeMetadata.StateMachine stateMachine => ProcessStateMachine(stateMachine),
+ _ => throw new InvalidOperationException()
+ };
+
+ if (result is not null)
+ {
+ aggregatedText.Append(result);
+ }
+ }
+
+ private string? SaveAndProcessProject(TypeMetadata.Project project)
+ {
+ ProjectPrefix = project.ProjectName;
+ ParentProjectPrefix = project.ParentProjectName ?? "";
+ return ProcessProject(project);
+ }
+
+ protected virtual string? ProcessProject(TypeMetadata.Project project) => null;
+ protected virtual string? ProcessMessage(TypeMetadata.Message metadata) => null;
+ protected virtual string? ProcessComponent(TypeMetadata.Component metadata) => null;
+ protected virtual string? ProcessInteraction(TypeMetadata.Interaction metadata) => null;
+ protected virtual string? ProcessStateMachine(TypeMetadata.StateMachine metadata) => null;
+ protected virtual string? FinalModification() => null;
+ public string GetProcessedText()
+ {
+ var finalModification = FinalModification();
+ if (finalModification is not null)
+ {
+ aggregatedText.Append(finalModification);
+ }
+
+ return aggregatedText.ToString();
+ }
+}
diff --git a/src/Bang.Generator/Templating/Templates.ComponentTypes.cs b/src/Bang.Generator/Templating/Templates.ComponentTypes.cs
new file mode 100644
index 0000000..ae57465
--- /dev/null
+++ b/src/Bang.Generator/Templating/Templates.ComponentTypes.cs
@@ -0,0 +1,31 @@
+using Bang.Generator.Metadata;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Templating;
+
+public static partial class Templates
+{
+ public static FileTemplate ComponentTypes(string projectPrefix) => new(
+ $"{projectPrefix}ComponentTypes.g.cs",
+ ComponentTypesRawTypes,
+ ImmutableArray.Create(
+ new ProjectPrefixSubstitution(),
+ new ComponentIdSubstitution()
+ )
+ );
+
+ private sealed class ComponentIdSubstitution : TemplateSubstitution
+ {
+ public ComponentIdSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata)
+ => $"""
+
+ ///
+ /// Unique Id used for the lookup of components with type .
+ ///
+ public const int {metadata.FriendlyName} = global::Bang.{ParentProjectPrefix}ComponentsLookup.{ParentProjectPrefix}NextLookupId + {metadata.Index};
+
+ """;
+ }
+}
diff --git a/src/Bang.Generator/Templating/Templates.EntityExtensions.cs b/src/Bang.Generator/Templating/Templates.EntityExtensions.cs
new file mode 100644
index 0000000..bc46b32
--- /dev/null
+++ b/src/Bang.Generator/Templating/Templates.EntityExtensions.cs
@@ -0,0 +1,173 @@
+using Bang.Generator.Metadata;
+using System.Collections.Immutable;
+using System.Text;
+
+namespace Bang.Generator.Templating;
+
+public static partial class Templates
+{
+ public static FileTemplate EntityExtensions(string projectPrefix) => new(
+ $"{projectPrefix}EntityExtensions.g.cs",
+ EntityExtensionsRawText,
+ ImmutableArray.Create(
+ new ProjectPrefixSubstitution(),
+ new ComponentGetSubstitution(),
+ new ComponentHasSubstitution(),
+ new ComponentTryGetSubstitution(),
+ new ComponentSetSubstitution(),
+ new ComponentWithSubstitution(),
+ new ComponentRemoveSubstitution(),
+ new MessageHasSubstitution()
+ )
+ );
+
+ private sealed class ComponentGetSubstitution : TemplateSubstitution
+ {
+ public ComponentGetSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata) =>
+ $"""
+ ///
+ /// Gets a component of type .
+ ///
+ {(metadata.IsInternal ? "internal" : "public")} static global::{metadata.FullyQualifiedName} Get{metadata.FriendlyName}(this global::Bang.Entities.Entity e)
+ => e.GetComponent(global::Bang.Entities.{ProjectPrefix}ComponentTypes.{metadata.FriendlyName});
+
+
+ """;
+ }
+
+ private sealed class ComponentHasSubstitution : TemplateSubstitution
+ {
+ public ComponentHasSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata) =>
+ $"""
+ ///
+ /// Checks whether this entity possesses a component of type or not.
+ ///
+ {(metadata.IsInternal ? "internal" : "public")} static bool Has{metadata.FriendlyName}(this global::Bang.Entities.Entity e)
+ => e.HasComponent(global::Bang.Entities.{ProjectPrefix}ComponentTypes.{metadata.FriendlyName});
+
+
+ """;
+ }
+
+ private sealed class ComponentTryGetSubstitution : TemplateSubstitution
+ {
+ public ComponentTryGetSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata) =>
+ $"""
+ ///
+ /// Gets a if the entity has one, otherwise returns null.
+ ///
+ {(metadata.IsInternal ? "internal" : "public")} static global::{metadata.FullyQualifiedName}? TryGet{metadata.FriendlyName}(this global::Bang.Entities.Entity e)
+ => e.Has{metadata.FriendlyName}() ? e.Get{metadata.FriendlyName}() : null;
+
+
+ """;
+ }
+
+ private sealed class ComponentSetSubstitution : TemplateSubstitution
+ {
+ public ComponentSetSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata)
+ {
+ var builder = new StringBuilder();
+
+ // Adds a special extension method for each constructor
+ foreach (var constructor in metadata.Constructors)
+ {
+ var parameterList =
+ constructor.Parameters.Any()
+ ? $", {(string.Join(", ", constructor.Parameters.Select(parameter => $"{parameter.FullyQualifiedTypeName} {parameter.Name}")))}"
+ : "";
+
+ var argumentList =
+ constructor.Parameters.Any()
+ ? $"{(string.Join(", ", constructor.Parameters.Select(x => x.Name)))}"
+ : "";
+
+ builder.Append($$"""
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ {{(metadata.IsInternal ? "internal" : "public")}} static void Set{{metadata.FriendlyName}}(this global::Bang.Entities.Entity e{{parameterList}})
+ {
+ e.AddOrReplaceComponent(new global::{{metadata.FullyQualifiedName}}({{argumentList}}), global::Bang.Entities.{{ProjectPrefix}}ComponentTypes.{{metadata.FriendlyName}});
+ }
+
+
+ """);
+ }
+
+ builder.Append($$"""
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ {{(metadata.IsInternal ? "internal" : "public")}} static void Set{{metadata.FriendlyName}}(this global::Bang.Entities.Entity e, global::{{metadata.FullyQualifiedName}} component)
+ {
+ e.AddOrReplaceComponent(component, global::Bang.Entities.{{ProjectPrefix}}ComponentTypes.{{metadata.FriendlyName}});
+ }
+
+
+ """);
+
+
+ return builder.ToString();
+ }
+ }
+
+ private sealed class ComponentWithSubstitution : TemplateSubstitution
+ {
+ public ComponentWithSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata) =>
+ $$"""
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ {{(metadata.IsInternal ? "internal" : "public")}} static global::Bang.Entities.Entity With{{metadata.FriendlyName}}(this global::Bang.Entities.Entity e, global::{{metadata.FullyQualifiedName}} component)
+ {
+ e.AddOrReplaceComponent(component, global::Bang.Entities.{{ProjectPrefix}}ComponentTypes.{{metadata.FriendlyName}});
+ return e;
+ }
+
+
+ """;
+ }
+
+ private sealed class ComponentRemoveSubstitution : TemplateSubstitution
+ {
+ public ComponentRemoveSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata) =>
+ $"""
+ ///
+ /// Removes the component of type .
+ ///
+ {(metadata.IsInternal ? "internal" : "public")} static bool Remove{metadata.FriendlyName}(this global::Bang.Entities.Entity e)
+ => e.RemoveComponent(global::Bang.Entities.{ProjectPrefix}ComponentTypes.{metadata.FriendlyName});
+
+
+ """;
+ }
+
+ private sealed class MessageHasSubstitution : TemplateSubstitution
+ {
+ public MessageHasSubstitution() : base("") { }
+
+ protected override string ProcessMessage(TypeMetadata.Message metadata) =>
+ $"""
+ ///
+ /// Checks whether the entity has a message of type or not.
+ ///
+ {(metadata.IsInternal ? "internal" : "public")} static bool Has{metadata.TypeName}(this global::Bang.Entities.Entity e)
+ => e.HasComponent(global::Bang.Entities.{ProjectPrefix}MessageTypes.{metadata.FriendlyName});
+
+
+ """;
+ }
+}
\ No newline at end of file
diff --git a/src/Bang.Generator/Templating/Templates.LookupImplementation.cs b/src/Bang.Generator/Templating/Templates.LookupImplementation.cs
new file mode 100644
index 0000000..46fe0d5
--- /dev/null
+++ b/src/Bang.Generator/Templating/Templates.LookupImplementation.cs
@@ -0,0 +1,100 @@
+using Bang.Generator.Metadata;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Templating;
+
+public static partial class Templates
+{
+ public static FileTemplate LookupImplementation(string projectPrefix) => new(
+ $"{projectPrefix}ComponentsLookup.g.cs",
+ LookupImplementationRaw,
+ ImmutableArray.Create(
+ new ParentProjectLookupClassSubstitution(),
+ new ProjectPrefixSubstitution(),
+ new RelativeComponentSetSubstitution(),
+ new ComponentTypeToIndexMapSubstitution(),
+ new MessageTypeToIndexMapSubstitution(),
+ new IdCountSubstitution()
+ )
+ );
+
+ private sealed class ParentProjectLookupClassSubstitution : TemplateSubstitution
+ {
+ public ParentProjectLookupClassSubstitution() : base("") { }
+
+ protected override string ProcessProject(TypeMetadata.Project project)
+ => $"global::{project.ParentProjectLookupClassName}";
+ }
+
+ private sealed class RelativeComponentSetSubstitution : TemplateSubstitution
+ {
+ public RelativeComponentSetSubstitution() : base("") { }
+
+ protected override string? ProcessComponent(TypeMetadata.Component metadata)
+ => metadata is { IsParentRelativeComponent: true, IsMurderTransformComponent: false }
+ ? $"""
+ global::Bang.Entities.{ProjectPrefix}ComponentTypes.{metadata.FriendlyName},
+
+ """
+ : null;
+ }
+
+ private sealed class ComponentTypeToIndexMapSubstitution : TemplateSubstitution
+ {
+ public ComponentTypeToIndexMapSubstitution() : base("") { }
+
+ protected override string ProcessComponent(TypeMetadata.Component metadata) =>
+ $$"""
+ { typeof(global::{{metadata.FullyQualifiedName}}), global::Bang.Entities.{{(metadata.IsTransformComponent ? "Bang" : ProjectPrefix)}}ComponentTypes.{{(metadata.IsTransformComponent ? "Transform" : metadata.FriendlyName)}} },
+
+ """;
+
+ protected override string ProcessStateMachine(TypeMetadata.StateMachine metadata) =>
+ $$"""
+ { typeof(global::Bang.StateMachines.StateMachineComponent), global::Bang.Entities.BangComponentTypes.StateMachine },
+
+ """;
+
+
+ protected override string ProcessInteraction(TypeMetadata.Interaction metadata) =>
+ $$"""
+ { typeof(global::Bang.Interactions.InteractiveComponent), global::Bang.Entities.BangComponentTypes.Interactive },
+
+ """;
+ }
+
+ private sealed class MessageTypeToIndexMapSubstitution : TemplateSubstitution
+ {
+ public MessageTypeToIndexMapSubstitution() : base("") { }
+
+ protected override string ProcessMessage(TypeMetadata.Message metadata) =>
+ $$"""
+ { typeof(global::{{metadata.FullyQualifiedName}}), global::Bang.Entities.{{ProjectPrefix}}MessageTypes.{{metadata.FriendlyName}} },
+
+ """;
+ }
+
+ private sealed class IdCountSubstitution : TemplateSubstitution
+ {
+ private int idCount;
+ public IdCountSubstitution() : base("") { }
+
+ protected override string? ProcessComponent(TypeMetadata.Component metadata)
+ {
+ idCount++;
+ return base.ProcessComponent(metadata);
+ }
+
+ protected override string? ProcessMessage(TypeMetadata.Message metadata)
+ {
+ idCount++;
+ return base.ProcessMessage(metadata);
+ }
+
+ protected override string FinalModification()
+ => $"""
+ public const int {ProjectPrefix}NextLookupId = {idCount} + {ParentProjectPrefix}ComponentsLookup.{ParentProjectPrefix}NextLookupId;
+
+ """;
+ }
+}
diff --git a/src/Bang.Generator/Templating/Templates.MessageTypes.cs b/src/Bang.Generator/Templating/Templates.MessageTypes.cs
new file mode 100644
index 0000000..1585feb
--- /dev/null
+++ b/src/Bang.Generator/Templating/Templates.MessageTypes.cs
@@ -0,0 +1,31 @@
+using Bang.Generator.Metadata;
+using System.Collections.Immutable;
+
+namespace Bang.Generator.Templating;
+
+public static partial class Templates
+{
+ public static FileTemplate MessageTypes(string projectPrefix) => new(
+ $"{projectPrefix}MessageTypes.g.cs",
+ MessageTypesRawText,
+ ImmutableArray.Create(
+ new ProjectPrefixSubstitution(),
+ new MessageIdSubstitution()
+ )
+ );
+
+ private sealed class MessageIdSubstitution : TemplateSubstitution
+ {
+ public MessageIdSubstitution() : base("") { }
+
+ protected override string ProcessMessage(TypeMetadata.Message metadata)
+ => $"""
+
+ ///
+ /// Unique Id used for the lookup of messages with type .
+ ///
+ public const int {metadata.FriendlyName} = global::Bang.{ParentProjectPrefix}ComponentsLookup.{ParentProjectPrefix}NextLookupId + {metadata.Index};
+
+ """;
+ }
+}
\ No newline at end of file
diff --git a/src/Bang.Generator/Templating/Templates.cs b/src/Bang.Generator/Templating/Templates.cs
new file mode 100644
index 0000000..1652bea
--- /dev/null
+++ b/src/Bang.Generator/Templating/Templates.cs
@@ -0,0 +1,109 @@
+namespace Bang.Generator.Templating;
+
+public static partial class Templates
+{
+ public const string ComponentTypesRawTypes =
+ """
+ namespace Bang.Entities
+ {
+ ///
+ /// Collection of all ids for fetching components declared in this project.
+ ///
+ public static class ComponentTypes
+ { }
+ }
+ """;
+
+ public const string MessageTypesRawText =
+ """
+ namespace Bang.Entities
+ {
+ ///
+ /// Collection of all ids for fetching components declared in this project.
+ ///
+ public static class MessageTypes
+ { }
+ }
+ """;
+
+ public const string EntityExtensionsRawText =
+ """
+ namespace Bang.Entities
+ {
+ ///
+ /// Quality of life extensions for the components declared in this project.
+ ///
+ public static class EntityExtensions
+ {
+ #region Component "Get" methods!
+
+ #endregion
+
+ #region Component "Has" checkers!
+
+ #endregion
+
+ #region Component "TryGet" methods!
+
+ #endregion
+
+ #region Component "Set" methods!
+
+ #endregion
+
+ #region Component "With" methods!
+
+ #endregion
+
+ #region Component "Remove" methods!
+
+ #endregion
+
+ #region Message "Has" checkers!
+
+ #endregion
+ }
+ }
+ """;
+
+ public const string LookupImplementationRaw =
+ """
+ using System.Collections.Immutable;
+ using System.Linq;
+
+ namespace Bang
+ {
+ ///
+ /// Auto-generated implementation of for this project.
+ ///
+ public class ComponentsLookup :
+ {
+ ///
+ /// First lookup id a implementation that inherits from this class must use.
+ ///
+
+ ///
+ /// Default constructor. This is only relevant for the internals of Bang, so you can ignore it.
+ ///
+ public ComponentsLookup()
+ {
+ MessagesIndex = base.MessagesIndex.Concat(_messagesIndex).ToImmutableDictionary();
+ ComponentsIndex = base.ComponentsIndex.Concat(_componentsIndex).ToImmutableDictionary();
+ RelativeComponents = base.RelativeComponents.Concat(_relativeComponents).ToImmutableHashSet();
+ }
+
+ private static readonly ImmutableHashSet _relativeComponents = new HashSet()
+ {
+ }.ToImmutableHashSet();
+
+ private static readonly ImmutableDictionary _componentsIndex = new Dictionary()
+ {
+ }.ToImmutableDictionary();
+
+ private static readonly ImmutableDictionary _messagesIndex = new Dictionary()
+ {
+ }.ToImmutableDictionary();
+ }
+ }
+ """;
+}
diff --git a/src/Bang.Generator/template.txt b/src/Bang.Generator/template.txt
deleted file mode 100644
index ca83202..0000000
--- a/src/Bang.Generator/template.txt
+++ /dev/null
@@ -1,80 +0,0 @@
-/* | ̄ ̄ ̄ ̄ ̄ ̄\ *
- * | STOP | *
- * | EDITING | *
- * | THIS | *
- * | FILE | *
- * | _____ / *
- * (\__/) || *
- * (•ㅅ•) || *
- * / づ *
- *  ̄ ̄ ̄ *
- * This code was generated by our own generator! In order to modify this, please run the *
- * generator with whatever settings you want. *
- * */
-
-
-using System.Collections.Immutable;
-
-namespace Bang.Entities
-{
- public enum ComponentType
- {
-
- }
-
- public enum MessageType
- {
-
- }
-
- public static class EntityExtensions
- {
- #region Component "Get" methods!
-
- #endregion
-
- #region Component "Has" checkers!
-
- #endregion
-
- #region Component "TryGet" methods!
-
- #endregion
-
- #region Component "Set" methods!
-
- #endregion
-
- #region Component "Remove" methods!
-
- #endregion
-
- #region Message "Has" checkers!
-
- #endregion
- }
-
- public class LookupImplementation :
- {
- private static readonly ImmutableHashSet _relativeComponents = new HashSet()
- {
-
- }.ToImmutableHashSet();
-
- public override ImmutableHashSet RelativeComponents => _relativeComponents;
-
- private static readonly ImmutableDictionary _componentsIndex = new Dictionary()
- {
-
- }.ToImmutableDictionary();
-
- protected override ImmutableDictionary ComponentsIndex => _componentsIndex;
-
- private static readonly ImmutableDictionary _messagesIndex = new Dictionary()
- {
-
- }.ToImmutableDictionary();
-
- protected override ImmutableDictionary MessagesIndex => _messagesIndex;
- }
-}
\ No newline at end of file
diff --git a/src/Bang/Bang.csproj b/src/Bang/Bang.csproj
index f1a019d..246afc1 100644
--- a/src/Bang/Bang.csproj
+++ b/src/Bang/Bang.csproj
@@ -14,7 +14,7 @@
true
Murder.Bang
- 0.0.4
+ 0.0.5
Murder Authors
Murder Engine
diff --git a/src/Bang/ComponentsLookup.cs b/src/Bang/ComponentsLookup.cs
index ed21b1c..8b7247a 100644
--- a/src/Bang/ComponentsLookup.cs
+++ b/src/Bang/ComponentsLookup.cs
@@ -1,4 +1,5 @@
using Bang.Components;
+using Bang.Entities;
using Bang.Interactions;
using Bang.StateMachines;
using System.Collections.Immutable;
@@ -11,20 +12,31 @@ namespace Bang
///
public abstract class ComponentsLookup
{
+ ///
+ /// Tracks the last id this particular implementation is tracking plus one.
+ ///
+ public const int NextLookupId = 3;
+
///
/// Maps all the components to their unique id.
///
- protected abstract ImmutableDictionary ComponentsIndex { get; }
+ protected ImmutableDictionary ComponentsIndex { get; init; } = new Dictionary
+ {
+ { typeof(IStateMachineComponent), BangComponentTypes.StateMachine },
+ { typeof(IInteractiveComponent), BangComponentTypes.Interactive },
+ { typeof(ITransformComponent), BangComponentTypes.Transform }
+ }.ToImmutableDictionary();
///
/// Maps all the messages to their unique id.
///
- protected abstract ImmutableDictionary MessagesIndex { get; }
+ protected ImmutableDictionary MessagesIndex { get; init; } = new Dictionary().ToImmutableDictionary();
///
/// List of all the unique id of the components that inherit from .
///
- public abstract ImmutableHashSet RelativeComponents { get; }
+ public ImmutableHashSet RelativeComponents { get; protected init; } =
+ ImmutableHashSet.Create(BangComponentTypes.Transform);
///
/// Tracks components and messages without a generator. This query will have a lower performance.
diff --git a/src/Bang/Entities/BangComponentTypes.cs b/src/Bang/Entities/BangComponentTypes.cs
new file mode 100644
index 0000000..0125f9d
--- /dev/null
+++ b/src/Bang/Entities/BangComponentTypes.cs
@@ -0,0 +1,21 @@
+namespace Bang.Entities
+{
+ ///
+ /// Ids reserved for special Bang components.
+ ///
+ public static class BangComponentTypes
+ {
+ ///
+ /// Unique Id used for the lookup of components with type .
+ ///
+ public const int StateMachine = 0;
+ ///
+ /// Unique Id used for the lookup of components with type .
+ ///
+ public const int Interactive = 1;
+ ///
+ /// Unique Id used for the lookup of components with type .
+ ///
+ public const int Transform = 2;
+ }
+}
\ No newline at end of file
diff --git a/src/Bang/Entities/Extensions.cs b/src/Bang/Entities/Extensions.cs
new file mode 100644
index 0000000..0a31263
--- /dev/null
+++ b/src/Bang/Entities/Extensions.cs
@@ -0,0 +1,135 @@
+using Bang.Components;
+using Bang.Interactions;
+using Bang.StateMachines;
+
+namespace Bang.Entities
+{
+ ///
+ /// Quality of life extensions for the default components declared in Bang.
+ ///
+ public static class Extensions
+ {
+ ///
+ /// Gets a component of type .
+ ///
+ public static ITransformComponent GetTransform(this Entity e)
+ => e.GetComponent(BangComponentTypes.Transform);
+
+ ///
+ /// Checks whether this entity possesses a component of type or not.
+ ///
+ public static bool HasTransform(this Entity e)
+ => e.HasComponent(BangComponentTypes.Transform);
+
+ ///
+ /// Gets a if the entity has one, otherwise returns null.
+ ///
+ public static ITransformComponent? TryGetTransform(this Entity e)
+ => e.HasTransform() ? e.GetTransform() : null;
+
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ public static void SetTransform(this Entity e, ITransformComponent component)
+ {
+ e.AddOrReplaceComponent(component, BangComponentTypes.Transform);
+ }
+
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ public static Entity WithTransform(this Entity e, ITransformComponent component)
+ {
+ e.AddOrReplaceComponent(component, BangComponentTypes.Transform);
+ return e;
+ }
+
+ ///
+ /// Removes the component of type .
+ ///
+ public static bool RemoveTransform(this Entity e)
+ => e.RemoveComponent(BangComponentTypes.Transform);
+
+ ///
+ /// Gets a component of type .
+ ///
+ public static IStateMachineComponent GetStateMachine(this Entity e)
+ => e.GetComponent(BangComponentTypes.StateMachine);
+
+ ///
+ /// Checks whether this entity possesses a component of type or not.
+ ///
+ public static bool HasStateMachine(this Entity e)
+ => e.HasComponent(BangComponentTypes.StateMachine);
+
+ ///
+ /// Gets a if the entity has one, otherwise returns null.
+ ///
+ public static IStateMachineComponent? TryGetStateMachine(this Entity e)
+ => e.HasStateMachine() ? e.GetStateMachine() : null;
+
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ public static void SetStateMachine(this Entity e, IStateMachineComponent component)
+ {
+ e.AddOrReplaceComponent(component, BangComponentTypes.StateMachine);
+ }
+
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ public static Entity WithStateMachine(this Entity e, IStateMachineComponent component)
+ {
+ e.AddOrReplaceComponent(component, BangComponentTypes.StateMachine);
+ return e;
+ }
+
+ ///
+ /// Removes the component of type .
+ ///
+ public static bool RemoveStateMachine(this Entity e)
+ => e.RemoveComponent(BangComponentTypes.StateMachine);
+
+ ///
+ /// Gets a component of type .
+ ///
+ public static IInteractiveComponent GetInteractive(this Entity e)
+ => e.GetComponent(BangComponentTypes.Interactive);
+
+ ///
+ /// Checks whether this entity possesses a component of type or not.
+ ///
+ public static bool HasInteractive(this Entity e)
+ => e.HasComponent(BangComponentTypes.Interactive);
+
+ ///
+ /// Gets a if the entity has one, otherwise returns null.
+ ///
+ public static IInteractiveComponent? TryGetInteractive(this Entity e)
+ => e.HasInteractive() ? e.GetInteractive() : null;
+
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ public static void SetInteractive(this Entity e, IInteractiveComponent component)
+ {
+ e.AddOrReplaceComponent(component, BangComponentTypes.Interactive);
+ }
+
+ ///
+ /// Adds or replaces the component of type .
+ ///
+ public static Entity WithInteractive(this Entity e, IInteractiveComponent component)
+ {
+ e.AddOrReplaceComponent(component, BangComponentTypes.Interactive);
+ return e;
+ }
+
+ ///
+ /// Removes the component of type .
+ ///
+ public static bool RemoveInteractive(this Entity e)
+ => e.RemoveComponent(BangComponentTypes.Interactive);
+ }
+}
\ No newline at end of file
diff --git a/src/Bang/Interactions/InteractorComponent.cs b/src/Bang/Interactions/InteractorComponent.cs
deleted file mode 100644
index 0b5ed04..0000000
--- a/src/Bang/Interactions/InteractorComponent.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using Bang.Components;
-
-namespace Bang.Interactions
-{
- ///
- /// Component used to signal that an entity is able to interact with other objects.
- ///
- public readonly struct InteractorComponent : IComponent { }
-}
\ No newline at end of file
diff --git a/src/Bang/World_Reflection.cs b/src/Bang/World_Reflection.cs
index 57ebcf5..2b200c1 100644
--- a/src/Bang/World_Reflection.cs
+++ b/src/Bang/World_Reflection.cs
@@ -32,34 +32,16 @@ public static ComponentsLookup FindLookupImplementation()
}
}
- Type? target = null;
- if (candidateLookupImplementations.Count == 1)
- {
- // Easy, just return the first implementation.
- target = candidateLookupImplementations[0];
- }
- else
- {
- // This should only be done once and we will have at *very maximum* three implementations.
- // Check whoever implements whom.
- foreach (Type t in candidateLookupImplementations)
- {
- foreach (Type tt in candidateLookupImplementations)
- {
- if (t != tt && t.IsAssignableFrom(tt))
- {
- target = tt;
- }
- }
- }
- }
-
+ var target = candidateLookupImplementations.MaxBy(NumberOfParentClasses);
if (target is not null)
{
return (ComponentsLookup)Activator.CreateInstance(target)!;
}
throw new InvalidOperationException("A generator is required to be run before running the game!");
+
+ static int NumberOfParentClasses(Type type)
+ => type.BaseType is null ? 0 : 1 + NumberOfParentClasses(type.BaseType);
}
///