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); } ///