diff --git a/src/gaemstone/ECS/EntityBuilder.cs b/src/gaemstone/ECS/EntityBuilder.cs index 13efe7b..2dc3846 100644 --- a/src/gaemstone/ECS/EntityBuilder.cs +++ b/src/gaemstone/ECS/EntityBuilder.cs @@ -56,7 +56,7 @@ public class EntityBuilder relation == Universe.ChildOf) { _parent = target; return this; } if (_toAdd.Count == 31) throw new NotSupportedException( - $"Must not add more than 31 IDs at once with EntityBuilder"); + "Must not add more than 31 IDs at once with EntityBuilder"); _toAdd.Add(id); return this; @@ -88,7 +88,7 @@ public class EntityBuilder if (Path != null) { if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException( - $"Entity already has parent set (via ChildOf), so path must not be absolute"); + "Entity already has parent set (via ChildOf), so path must not be absolute"); // If path specifies more than just a name, ensure the parent entity exists. if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(Universe, parent, Path.Parent!); } diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index 4c0bc4c..44e0833 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -74,7 +74,7 @@ public static class SystemExtensions var param = method.GetParameters(); if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { query = new(expr ?? throw new ArgumentException( - "System must specify SystemAttribute.Expression", nameof(method))); + "System must specify ExpressionAttribute", nameof(method))); callback = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); } else { var gen = IterActionGenerator.GetOrBuild(universe, method); @@ -116,8 +116,7 @@ public static class SystemExtensions callback.Prepare(iter); - var query = flecsIter->priv.iter.query.query; - if (query != null && ecs_query_get_filter(query)->term_count == 0) + if (flecsIter->field_count == 0) callback.Callback(iter); else while (iter.Next()) callback.Callback(iter); diff --git a/src/gaemstone/ECS/TermAttributes.cs b/src/gaemstone/ECS/TermAttributes.cs index dc361a2..6a88d68 100644 --- a/src/gaemstone/ECS/TermAttributes.cs +++ b/src/gaemstone/ECS/TermAttributes.cs @@ -2,12 +2,12 @@ using System; namespace gaemstone.ECS; -// TODO: Make it possible to use [Source] on systems. -[AttributeUsage(AttributeTargets.Parameter)] +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] public class SourceAttribute : Attribute { public Type Type { get; } - public SourceAttribute(Type type) => Type = type; + // TODO: Support path as source too. + internal SourceAttribute(Type type) => Type = type; } public class SourceAttribute : SourceAttribute { public SourceAttribute() : base(typeof(TEntity)) { } } @@ -25,12 +25,8 @@ public class InAttribute : Attribute { } [AttributeUsage(AttributeTargets.Parameter)] public class OutAttribute : Attribute { } -// [AttributeUsage(AttributeTargets.Parameter)] -// public class OrAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Parameter)] -public class NotAttribute : Attribute { } +public class OrAttribute : Attribute { } -// Parameters with nullable syntax are equivalent to [Optional]. [AttributeUsage(AttributeTargets.Parameter)] -public class OptionalAttribute : Attribute { } +public class NotAttribute : Attribute { } diff --git a/src/gaemstone/Utility/CollectionExtensions.cs b/src/gaemstone/Utility/CollectionExtensions.cs index ac6a901..f40bc7e 100644 --- a/src/gaemstone/Utility/CollectionExtensions.cs +++ b/src/gaemstone/Utility/CollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace gaemstone.Utility; @@ -11,4 +12,11 @@ public static class CollectionExtensions where T : struct => MaybeGet((ReadOnlySpan)span, index); public static T? MaybeGet(this ReadOnlySpan span, int index) where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; + + public static T? FirstOrNull(this IEnumerable enumerable) + where T : struct + { + using var enumerator = enumerable.GetEnumerator(); + return enumerator.MoveNext() ? enumerator.Current : null; + } } diff --git a/src/gaemstone/Utility/IL/IterActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs index 2aa8faf..599a758 100644 --- a/src/gaemstone/Utility/IL/IterActionGenerator.cs +++ b/src/gaemstone/Utility/IL/IterActionGenerator.cs @@ -5,12 +5,11 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using gaemstone.ECS; +using GCHandle = System.Runtime.InteropServices.GCHandle; namespace gaemstone.Utility.IL; -// TODO: Implement "or" operator. // TODO: Support tuple syntax to match relationship pairs. public unsafe class IterActionGenerator { @@ -63,7 +62,7 @@ public unsafe class IterActionGenerator var instanceArg = IL.Argument(0); var iteratorArg = IL.Argument(1); - var fieldIndex = 1; + var fieldIndex = 0; var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>(); foreach (var p in Parameters) { // If the parameter is unique, we don't create a term for it. @@ -87,23 +86,29 @@ public unsafe class IterActionGenerator }, }; + // If this and the previous parameter are marked with [Or], do not advance the field index. + if ((fieldIndex == 0) || (p.Kind != ParamKind.Or) || (paramData[^1].Info.Kind != ParamKind.Or)) + fieldIndex++; + var spanType = typeof(Span<>).MakeGenericType(p.FieldType); - var fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); + var fieldLocal = (ILocal?)null; var tempLocal = (ILocal?)null; switch (p.Kind) { - case ParamKind.Has or ParamKind.Not: + // FIXME: Currently would not work with [Or]'d components. + case ParamKind.Has or ParamKind.Not or ParamKind.Or: if (!p.ParameterType.IsValueType) break; - // If a "has" or "not" parameter is a struct, we require a temporary local that - // we can later load onto the stack when loading the arguments for the action. + // If parameter is a struct, we require a temporary local that we can + // later load onto the stack when loading the arguments for the action. IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); tempLocal = IL.Local(p.ParameterType); IL.LoadAddr(tempLocal); IL.Init(tempLocal.LocalType); break; - case ParamKind.Nullable: + case ParamKind.Nullable or ParamKind.Or: IL.Comment($"{p.Info.Name}Field = iterator.MaybeField<{p.FieldType.Name}>({fieldIndex})"); + fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); IL.Load(iteratorArg); IL.LoadConst(fieldIndex); IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType)); @@ -117,6 +122,7 @@ public unsafe class IterActionGenerator default: IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})"); + fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); IL.Load(iteratorArg); IL.LoadConst(fieldIndex); IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); @@ -125,7 +131,6 @@ public unsafe class IterActionGenerator } paramData.Add((p, term, fieldLocal, tempLocal)); - fieldIndex++; } // If there's any reference type parameters, we need to define a GCHandle local. @@ -134,12 +139,12 @@ public unsafe class IterActionGenerator .Any(p => !p.Info.UnderlyingType.IsValueType); var handleLocal = hasReferenceType ? IL.Local() : null; - var countLocal = IL.Local("iter_count"); var indexLocal = IL.Local("iter_index"); + var countLocal = IL.Local("iter_count"); + IL.Set(indexLocal, 0); IL.Load(iteratorArg, _iteratorCountProp); IL.Store(countLocal); - IL.Set(indexLocal, 0); // If all parameters are fixed, iterator count will be 0, but since // the query matched, we want to run the callback at least once. @@ -158,6 +163,7 @@ public unsafe class IterActionGenerator IL.Load(instanceArg); foreach (var (info, term, fieldLocal, tempLocal) in paramData) { + var isValueType = info.UnderlyingType.IsValueType; switch (info.Kind) { case ParamKind.GlobalUnique: @@ -170,10 +176,10 @@ public unsafe class IterActionGenerator _uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!); break; - case ParamKind.Has or ParamKind.Not: - if (info.ParameterType.IsValueType) - IL.LoadObj(tempLocal!); - else IL.LoadNull(); + // FIXME: Currently would not work with [Or]'d components. + case ParamKind.Has or ParamKind.Not or ParamKind.Or: + if (isValueType) IL.LoadObj(tempLocal!); + else IL.LoadNull(); break; default: @@ -214,7 +220,7 @@ public unsafe class IterActionGenerator IL.MarkLabel(doneLabel); } - if (!info.UnderlyingType.IsValueType) { + if (!isValueType) { IL.Comment($"Convert nint to {info.UnderlyingType.GetFriendlyName()}"); IL.Call(_handleFromIntPtrMethod); IL.Store(handleLocal!); @@ -252,7 +258,7 @@ public unsafe class IterActionGenerator public Type? Source { get; } public bool IsRequired => (Kind < ParamKind.Nullable); - public bool IsByRef => (Kind >= ParamKind.In) && (Kind <= ParamKind.Ref); + public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref); public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null); private ParamInfo(ParameterInfo info, ParamKind kind, @@ -265,59 +271,77 @@ public unsafe class IterActionGenerator // Reference types have a backing type of nint - they're pointers. FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint); - if (UnderlyingType.Has()) Source = underlyingType; - if (Info.Get() is SourceAttribute attr) Source = attr.Type; - // TODO: Needs support for the new attributes. + Source = Info.Get()?.Type + ?? (UnderlyingType.Has() ? UnderlyingType : null); } public static ParamInfo Build(ParameterInfo info) { - if (info.IsOptional) throw new ArgumentException("Optional parameters are not supported\nParameter: " + info); - if (info.ParameterType.IsArray) throw new ArgumentException("Arrays are not supported\nParameter: " + info); - if (info.ParameterType.IsPointer) throw new ArgumentException("Pointers are not supported\nParameter: " + info); - - if (_globalUniqueParameters.ContainsKey(info.ParameterType)) - return new(info, ParamKind.GlobalUnique, info.ParameterType, info.ParameterType); - if (_uniqueParameters.ContainsKey(info.ParameterType)) - return new(info, ParamKind.Unique, info.ParameterType, info.ParameterType); + if (info.IsOptional) throw new ArgumentException($"Optional parameters are not supported\nParameter: {info}"); + if (info.ParameterType.IsArray) throw new ArgumentException($"Arrays are not supported\nParameter: {info}"); + if (info.ParameterType.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}"); + if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}"); + + // Find out initial parameter kind from provided attribute. + var fromAttributes = new List(); + if (info.Has< InAttribute>()) fromAttributes.Add(ParamKind.In); + if (info.Has()) fromAttributes.Add(ParamKind.Out); + if (info.Has()) fromAttributes.Add(ParamKind.Has); + if (info.Has< OrAttribute>()) fromAttributes.Add(ParamKind.Or); + if (info.Has()) fromAttributes.Add(ParamKind.Not); + // Throw an error if multiple incompatible attributes were found. + if (fromAttributes.Count > 1) throw new ArgumentException( + "Parameter must not be marked with multiple attributes: " + + string.Join(", ", fromAttributes.Select(a => $"[{a}]")) + + $"\nParameter: {info}"); + var kind = fromAttributes.FirstOrNull() ?? ParamKind.Normal; + + // Handle unique parameters such as Universe, EntityRef, ... + var isGlobalUnique = _globalUniqueParameters.ContainsKey(info.ParameterType); + var isUnique = _uniqueParameters.ContainsKey(info.ParameterType); + if (isGlobalUnique || isUnique) { + if (kind != ParamKind.Normal) throw new ArgumentException( + $"Unique parameter {info.ParameterType.Name} does not support [{kind}]\nParameter: {info}"); + kind = isGlobalUnique ? ParamKind.GlobalUnique : ParamKind.Unique; + return new(info, kind, info.ParameterType, info.ParameterType); + } - var isByRef = info.ParameterType.IsByRef; var isNullable = info.IsNullable(); + var isByRef = info.ParameterType.IsByRef; - if (info.Has()) { - if (isByRef || isNullable) throw new ArgumentException( - "Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info); - return new(info, ParamKind.Not, info.ParameterType, info.ParameterType); + if (info.ParameterType.Has() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) { + if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}"); + kind = ParamKind.Has; } - if (info.Has() || info.ParameterType.Has()) { - if (isByRef || isNullable) throw new ArgumentException( - "Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info); - return new(info, ParamKind.Has, info.ParameterType, info.ParameterType); + if (kind is ParamKind.Not or ParamKind.Has) { + if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}"); + if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); + return new(info, kind, info.ParameterType, info.ParameterType); } - var kind = ParamKind.Normal; var underlyingType = info.ParameterType; - if (info.IsNullable()) { + if (isNullable) { + if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); if (info.ParameterType.IsValueType) underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!; kind = ParamKind.Nullable; } if (info.ParameterType.IsByRef) { - if (kind == ParamKind.Nullable) throw new ArgumentException( - "ByRef and Nullable are not supported together\nParameter: " + info); + if (kind != ParamKind.Normal) throw new ArgumentException( + $"Parameter does not support [{kind}]\nParameter: {info}"); underlyingType = info.ParameterType.GetElementType()!; if (!underlyingType.IsValueType) throw new ArgumentException( - "Reference types can't also be ByRef\nParameter: " + info); + $"Reference types can't also be ByRef\nParameter: {info}"); kind = info.IsIn ? ParamKind.In : info.IsOut ? ParamKind.Out : ParamKind.Ref; } if (underlyingType.IsPrimitive) throw new ArgumentException( - "Primitives are not supported\nParameter: " + info); + $"Primitives are not supported\nParameter: {info}"); return new(info, kind, info.ParameterType, underlyingType); } @@ -331,38 +355,61 @@ public unsafe class IterActionGenerator /// For example or . /// GlobalUnique, + /// /// Not part of the resulting query's terms. /// Unique value for each iterated entity. /// For example . /// Unique, + /// Passed by value. Normal, - /// Struct passed with the "in" modifier. + + /// + /// Struct passed with the "in" modifier, allowing direct pointer access. + /// Manually applied with . + /// Marks a component as being read from. + /// In, - /// Struct passed with the "out" modifier. + + /// + /// Struct passed with the "out" modifier, allowing direct pointer access. + /// Manually applied with . + /// Marks a component as being written to. + /// Out, - /// Struct passed with the "ref" modifier. + + /// + /// Struct passed with the "ref" modifier, allowing direct pointer access. + /// Marks a component as being read from and written to. + /// Ref, + /// /// Only checks for presence. /// Manually applied with . /// Automatically applied for types with . + /// Marks a component as not being accessed. /// Has, - /// Struct or class passed as . - Nullable, + /// - /// Matches any terms in a chain of "or" terms. - /// Applied with . - /// Implies . + /// Struct or class passed as . /// - Or, + Nullable, + /// /// Only checks for absence. /// Applied with . /// Not, + + /// + /// Matches any terms in a chain of "or" terms. + /// Applied with . + /// Implies . + /// + Or, } }