using System.Collections.Immutable; using System.Text; using Microsoft.CodeAnalysis; namespace Arch.System.SourceGenerator; public static class QueryUtils { /// /// Appends the first elements of the types specified in the from the previous specified arrays. /// /// The instance. /// The list of s which we wanna append the first elements for. /// public static StringBuilder GetFirstElements(this StringBuilder sb, IEnumerable parameterSymbols) { foreach (var symbol in parameterSymbols) if(symbol.Type.Name is not "Entity" || !symbol.GetAttributes().Any(data => data.AttributeClass.Name.Contains("Data"))) // Prevent entity being added to the type array sb.AppendLine($"ref var {symbol.Type.Name.ToLower()}FirstElement = ref chunk.GetFirst<{symbol.Type}>();"); return sb; } /// /// Appends the components of the types specified in the from the previous specified first elements. /// /// The instance. /// The list of s which we wanna append the components for. /// public static StringBuilder GetComponents(this StringBuilder sb, IEnumerable parameterSymbols) { foreach (var symbol in parameterSymbols) if(symbol.Type.Name is not "Entity") // Prevent entity being added to the type array sb.AppendLine($"ref var {symbol.Name.ToLower()} = ref Unsafe.Add(ref {symbol.Type.Name.ToLower()}FirstElement, entityIndex);"); return sb; } /// /// Inserts the types defined in the as parameters in a method. /// ref position, out velocity,... /// /// The instance. /// The of s which we wanna insert. /// public static StringBuilder InsertParams(this StringBuilder sb, IEnumerable parameterSymbols) { foreach (var symbol in parameterSymbols) sb.Append($"{CommonUtils.RefKindToString(symbol.RefKind)} {symbol.Name.ToLower()},"); if(sb.Length > 0) sb.Length--; return sb; } /// /// Creates a ComponentType array from the passed through. /// /// The . /// The with s which we wanna create a ComponentType array for. /// public static StringBuilder GetTypeArray(this StringBuilder sb, IList parameterSymbols) { if (parameterSymbols.Count == 0) { sb.Append("Array.Empty()"); return sb; } sb.Append("new ComponentType[]{"); foreach (var symbol in parameterSymbols) if(symbol.Name is not "Entity") // Prevent entity being added to the type array sb.Append($"typeof({symbol}),"); if (sb.Length > 0) sb.Length -= 1; sb.Append('}'); return sb; } /// /// Appends a set of if they are marked by the data attribute. /// ref gameTime, out somePassedList,... /// /// The instance. /// The of s which will be appended if they are marked with data. /// public static StringBuilder DataParameters(this StringBuilder sb, IEnumerable parameterSymbols) { sb.Append(','); foreach (var parameter in parameterSymbols) { if (parameter.GetAttributes().Any(attributeData => attributeData.AttributeClass.Name.Contains("Data"))) sb.Append($"{CommonUtils.RefKindToString(parameter.RefKind)} {parameter.Type} {parameter.Name.ToLower()},"); } sb.Length--; return sb; } /// /// Appends method calls made with their important data parameters. /// someQuery(World, gameTime); ... /// /// The instance. /// The of methods which we wanna call. /// public static StringBuilder CallMethods(this StringBuilder sb, IEnumerable methodNames) { foreach (var method in methodNames) { var data = new StringBuilder(); data.Append(','); foreach (var parameter in method.Parameters) { if (!parameter.GetAttributes().Any(attributeData => attributeData.AttributeClass.Name.Contains("Data"))) continue; data.Append($"{CommonUtils.RefKindToString(parameter.RefKind)} Data,"); break; } data.Length--; sb.AppendLine($"{method.Name}Query(World {data});"); } return sb; } /// /// Gets all the types of a as s and adds them to a list. /// If the attribute is generic it will add the generic parameters, if its non generic it will add the non generic types from the constructor. /// /// The . /// The where the found s are added to. public static void GetAttributeTypes(AttributeData data, List array) { if (data is not null && data.AttributeClass.IsGenericType) { array.AddRange(data.AttributeClass.TypeArguments); } else if (data is not null && !data.AttributeClass.IsGenericType) { var constructorArguments = data.ConstructorArguments[0].Values; var constructorArgumentsTypes = constructorArguments.Select(constant => constant.Value as ITypeSymbol).ToList(); array.AddRange(constructorArgumentsTypes); } } /// /// Adds a query with an entity for a given annotated method. The attributes of these methods are used to generate the query. /// /// The instance. /// The which is annotated for source generation. /// public static StringBuilder AppendQueryMethod(this StringBuilder sb, IMethodSymbol methodSymbol) { // Check for entity param var entity = methodSymbol.Parameters.Any(symbol => symbol.Type.Name.Equals("Entity")); var entityParam = entity ? methodSymbol.Parameters.First(symbol => symbol.Type.Name.Equals("Entity")) : null; // Get attributes var attributeData = methodSymbol.GetAttributeData("All"); var anyAttributeData = methodSymbol.GetAttributeData("Any"); var noneAttributeData = methodSymbol.GetAttributeData("None"); var exclusiveAttributeData = methodSymbol.GetAttributeData("Exclusive"); // Get params / components except those marked with data or entities. var components = methodSymbol.Parameters.ToList(); components.RemoveAll(symbol => symbol.Type.Name.Equals("Entity")); // Remove entitys components.RemoveAll(symbol => symbol.GetAttributes().Any(data => data.AttributeClass.Name.Contains("Data"))); // Remove data annotated params // Create all query array var allArray = components.Select(symbol => symbol.Type).ToList(); var anyArray = new List(); var noneArray = new List(); var exclusiveArray = new List(); // Get All<...> or All(...) passed types and pass them to the arrays GetAttributeTypes(attributeData, allArray); GetAttributeTypes(anyAttributeData, anyArray); GetAttributeTypes(noneAttributeData, noneArray); GetAttributeTypes(exclusiveAttributeData, exclusiveArray); // Remove doubles and entities allArray = allArray.Distinct().ToList(); anyArray = anyArray.Distinct().ToList(); noneArray = noneArray.Distinct().ToList(); exclusiveArray = exclusiveArray.Distinct().ToList(); allArray.RemoveAll(symbol => symbol.Name.Equals("Entity")); anyArray.RemoveAll(symbol => symbol.Name.Equals("Entity")); noneArray.RemoveAll(symbol => symbol.Name.Equals("Entity")); exclusiveArray.RemoveAll(symbol => symbol.Name.Equals("Entity")); // Create data modell and generate it var className = methodSymbol.ContainingSymbol.ToString(); var queryMethod = new QueryMethod { IsGlobalNamespace = methodSymbol.ContainingNamespace.IsGlobalNamespace, Namespace = methodSymbol.ContainingNamespace.ToString(), ClassName = className.Substring(className.LastIndexOf('.')+1), IsStatic = methodSymbol.IsStatic, IsEntityQuery = entity, MethodName = methodSymbol.Name, EntityParameter = entityParam, Parameters = methodSymbol.Parameters, Components = components, AllFilteredTypes = allArray, AnyFilteredTypes = anyArray, NoneFilteredTypes = noneArray, ExclusiveFilteredTypes = exclusiveArray }; return sb.AppendQueryMethod(ref queryMethod); } /// /// Adds a query with an entity for a given annotated method. The attributes of these methods are used to generate the query. /// /// The instance. /// The which is generated. /// public static StringBuilder AppendQueryMethod(this StringBuilder sb, ref QueryMethod queryMethod) { var staticModifier = queryMethod.IsStatic ? "static" : ""; // Generate code var data = new StringBuilder().DataParameters(queryMethod.Parameters); var getFirstElements = new StringBuilder().GetFirstElements(queryMethod.Components); var getComponents = new StringBuilder().GetComponents(queryMethod.Components); var insertParams = new StringBuilder().InsertParams(queryMethod.Parameters); var allTypeArray = new StringBuilder().GetTypeArray(queryMethod.AllFilteredTypes); var anyTypeArray = new StringBuilder().GetTypeArray(queryMethod.AnyFilteredTypes); var noneTypeArray = new StringBuilder().GetTypeArray(queryMethod.NoneFilteredTypes); var exclusiveTypeArray = new StringBuilder().GetTypeArray(queryMethod.ExclusiveFilteredTypes); var template = $$""" using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Arch.Core; using Arch.Core.Extensions; using Arch.Core.Utils; using ArrayExtensions = CommunityToolkit.HighPerformance.ArrayExtensions; using Component = Arch.Core.Utils.Component; {{(!queryMethod.IsGlobalNamespace ? $"namespace {queryMethod.Namespace} {{" : "")}} public {{staticModifier}} partial class {{queryMethod.ClassName}}{ private {{staticModifier}} QueryDescription {{queryMethod.MethodName}}_QueryDescription = new QueryDescription{ All = {{allTypeArray}}, Any = {{anyTypeArray}}, None = {{noneTypeArray}}, Exclusive = {{exclusiveTypeArray}} }; private {{staticModifier}} bool _{{queryMethod.MethodName}}_Initialized; private {{staticModifier}} Query _{{queryMethod.MethodName}}_Query; [MethodImpl(MethodImplOptions.AggressiveInlining)] public {{staticModifier}} void {{queryMethod.MethodName}}Query(World world {{data}}){ if(!_{{queryMethod.MethodName}}_Initialized){ _{{queryMethod.MethodName}}_Query = world.Query(in {{queryMethod.MethodName}}_QueryDescription); _{{queryMethod.MethodName}}_Initialized = true; } foreach(ref var chunk in _{{queryMethod.MethodName}}_Query.GetChunkIterator()){ var chunkSize = chunk.Size; {{(queryMethod.IsEntityQuery ? "ref var entityFirstElement = ref chunk.Entity(0);" : "")}} {{getFirstElements}} foreach(var entityIndex in chunk) { {{(queryMethod.IsEntityQuery ? $"ref readonly var {queryMethod.EntityParameter.Name.ToLower()} = ref Unsafe.Add(ref entityFirstElement, entityIndex);" : "")}} {{getComponents}} {{queryMethod.MethodName}}({{insertParams}}); } } } } {{(!queryMethod.IsGlobalNamespace ? "}" : "")}} """; sb.Append(template); return sb; } /// /// Adds a basesystem that calls a bunch of query methods. /// /// The instance. /// The which maps all query methods to a common class containing them. /// public static StringBuilder AppendBaseSystem(this StringBuilder sb, KeyValuePair> classToMethod) { // Get BaseSystem class var classSymbol = classToMethod.Key as INamedTypeSymbol; INamedTypeSymbol? parentSymbol = null; var implementsUpdate = false; var type = classSymbol; while (type != null) { // Update was implemented by user, no need to do that by source generator. if (type.MemberNames.Contains("Update")) implementsUpdate = true; type = type.BaseType; // Ignore classes which do not derive from BaseSystem if (type?.Name == "BaseSystem") { parentSymbol = type; break; } } if (parentSymbol == null || implementsUpdate) return sb; // Get generic of BaseSystem var typeSymbol = parentSymbol.TypeArguments[1]; // Generate basesystem. var baseSystem = new BaseSystem { Namespace = classSymbol.ContainingNamespace != null ? classSymbol.ContainingNamespace.ToString() : string.Empty, GenericType = typeSymbol, GenericTypeNamespace = typeSymbol.ContainingNamespace.ToString(), Name = classSymbol.Name, QueryMethods = classToMethod.Value, }; return sb.AppendBaseSystem(ref baseSystem); } /// /// Adds a basesystem that calls a bunch of query methods. /// /// The instance. /// The which is generated. /// public static StringBuilder AppendBaseSystem(this StringBuilder sb, ref BaseSystem baseSystem) { var methodCalls = new StringBuilder().CallMethods(baseSystem.QueryMethods); var template = $$""" using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using {{baseSystem.GenericTypeNamespace}}; {{(baseSystem.Namespace != string.Empty ? $"namespace {baseSystem.Namespace} {{" : "")}} public partial class {{baseSystem.Name}}{ [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Update(in {{baseSystem.GenericType.ToDisplayString()}} {{baseSystem.GenericType.Name.ToLower()}}){ {{methodCalls}} } } {{(baseSystem.Namespace != string.Empty ? "}" : "")}} """; return sb.Append(template); } }