From e74377d8b4096f725669eb0e20cf5d87cd4ce51c Mon Sep 17 00:00:00 2001 From: andreakarasho Date: Tue, 21 May 2024 22:51:39 +0200 Subject: [PATCH 1/2] strict types --- samples/MyBattleground/Program.cs | 18 +++++++---- src/Term.cs | 8 ++--- src/World.cs | 54 +++++++++++++++++++++++-------- tests/Entity.cs | 4 +-- tests/Query.cs | 26 ++++++++++++++- 5 files changed, 84 insertions(+), 26 deletions(-) diff --git a/samples/MyBattleground/Program.cs b/samples/MyBattleground/Program.cs index 80dbf94..e6cf9b6 100644 --- a/samples/MyBattleground/Program.cs +++ b/samples/MyBattleground/Program.cs @@ -1,39 +1,44 @@ // https://github.com/jasonliang-dev/entity-component-system using System.Diagnostics; -using System; using TinyEcs; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Diagnostics.CodeAnalysis; + const int ENTITIES_COUNT = (524_288 * 2 * 1); using var ecs = new World(); + ecs.Entity().Set(); ecs.Entity(); ecs.Entity(); ecs.Entity(); +ecs.Entity(); ecs.Entity().Set(); ecs.Entity().Set(new Velocity()); ecs.Entity().Set(new Position() { X = 9 }).Set(new Velocity()); +ecs.Entity().Set(new Position() { X = 99 }).Set(new Velocity()); +ecs.Entity().Set(new Position() { X = 999 }).Set(new Velocity()); +ecs.Entity().Set(new Position() { X = 9999 }).Set(new Velocity()).Set(); ecs.Entity().Set(new Velocity()); +ecs.Query(); + //ecs.Entity().Set(new Position() {X = 2}).Set(); ecs.Entity().Set(new Position() {X = 3}).Set(); -ecs.Query<(Position, Likes), Or<(Position, Networked)>>() +ecs.Query<(Position, ManagedData), Or<(Position, With)>>() .Each((EntityView e, ref Position maybe) => { var isNull = Unsafe.IsNullRef(ref maybe); Console.WriteLine("is null {0} - {1}", isNull, e.Name()); }); -ecs.Query<(Optional, With)>() +ecs.Query, With>() .Each((EntityView e, ref Position maybe) => { var isNull = Unsafe.IsNullRef(ref maybe); @@ -41,7 +46,7 @@ }); -ecs.Query<(With, With)>() +ecs.Query>() .Each((EntityView e, ref Position maybe) => { var isNull = Unsafe.IsNullRef(ref maybe); @@ -423,3 +428,4 @@ enum GameStates } struct Networked { } +delegate void Query2Del(ref T0 t0, ref T1 t1); diff --git a/src/Term.cs b/src/Term.cs index 6ae92bc..aa4e20d 100644 --- a/src/Term.cs +++ b/src/Term.cs @@ -82,7 +82,7 @@ public interface IFilter { } } -public interface IAtLeast { } -public interface IExactly { } -public interface INone { } -public interface IOr { } +public interface IAtLeast : IFilter { } +public interface IExactly : IFilter { } +public interface INone : IFilter { } +public interface IOr : IFilter { } diff --git a/src/World.cs b/src/World.cs index 27b9824..1e58871 100644 --- a/src/World.cs +++ b/src/World.cs @@ -506,7 +506,14 @@ public static ComponentInfo GetComponent(EcsID id, int size) private static Term GetTerm(Type type) { var ok = _typesConvertion.TryGetValue(type, out var term); - EcsAssert.Assert(ok, $"component not found with type {type}"); + if (!ok) + { + var exists = _componentInfosByType.ContainsKey(type); + if (exists) + EcsAssert.Assert(ok, $"The tag '{type}' cannot be used as query data"); + else + EcsAssert.Assert(ok, $"Component '{type}' not found! Try to register it using 'world.Entity<{type}>()'"); + } return term; } @@ -525,9 +532,9 @@ static Component() var tuple = (ITuple)default(T); EcsAssert.Panic(tuple.Length == 2, "Relations must be composed by 2 arguments only."); - var firstId = GetTerm(tuple[0]!.GetType()); - var secondId = GetTerm(tuple[1]!.GetType()); - var pairId = IDOp.Pair(firstId.IDs[0], secondId.IDs[0]); + var firstId = _componentInfosByType[tuple[0]!.GetType()].ID; + var secondId = _componentInfosByType[tuple[1]!.GetType()].ID; + var pairId = IDOp.Pair(firstId, secondId); HashCode = pairId; Size = 0; @@ -545,11 +552,15 @@ static Component() Value = new ComponentInfo(HashCode, Size); _arrayCreator.Add(Value.ID, count => Size > 0 ? new T[count] : Array.Empty()); - _typesConvertion.Add(typeof(T), new (Value.ID, TermOp.With)); + if (Size > 0) + { + _typesConvertion.Add(typeof(T), new (Value.ID, TermOp.With)); + _typesConvertion.Add(typeof(Optional), new (Value.ID, TermOp.Optional)); + } + _typesConvertion.Add(typeof(With), new (Value.ID, TermOp.With)); _typesConvertion.Add(typeof(Not), new (Value.ID, TermOp.Without)); _typesConvertion.Add(typeof(Without), new (Value.ID, TermOp.Without)); - _typesConvertion.Add(typeof(Optional), new (Value.ID, TermOp.Optional)); _componentInfosByType.Add(typeof(T), Value); @@ -584,7 +595,17 @@ private static int GetSize() } } - static void ParseTuple(ITuple tuple, List terms) + static void Validate(Type type) + { + if (typeof(ITuple).IsAssignableFrom(type)) + { + + } + + + } + + static void ParseTuple(ITuple tuple, List terms, Func validate) { var mainType = tuple.GetType(); TermOp? op = null; @@ -617,10 +638,13 @@ static void ParseTuple(ITuple tuple, List terms) if (typeof(ITuple).IsAssignableFrom(type)) { - ParseTuple((ITuple)tuple[i]!, terms); + ParseTuple((ITuple)tuple[i]!, terms, validate); continue; } + (var isValid, var errorMsg) = validate(type); + EcsAssert.Panic(isValid, errorMsg); + var term = GetTerm(type); tmpTerms.Add(term); } @@ -631,9 +655,13 @@ static void ParseTuple(ITuple tuple, List terms) } } - static void ParseType(List terms) where T : struct + static void ParseType(List terms, Func validate) where T : struct { var type = typeof(T); + + (var isValid, var errorMsg) = validate(type); + EcsAssert.Panic(isValid, errorMsg); + if (_typesConvertion.TryGetValue(type, out var term)) { terms.Add(term); @@ -643,7 +671,7 @@ static void ParseType(List terms) where T : struct if (typeof(ITuple).IsAssignableFrom(type)) { - ParseTuple((ITuple)default(T), terms); + ParseTuple((ITuple)default(T), terms, validate); return; } @@ -662,8 +690,8 @@ static Query() { var list = new List(); - ParseType(list); - ParseType(list); + ParseType(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), "Filters are not allowed in QueryData")); + ParseType(list, s => (typeof(IFilter).IsAssignableFrom(s) && s.GetInterfaces().All(k => typeof(IFilter).IsAssignableFrom(k)), "You must use a IFilter type")); Terms = list.ToImmutableArray(); @@ -681,7 +709,7 @@ static Query() { var list = new List(); - ParseType(list); + ParseType(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), "Filters are not allowed in QueryData")); Terms = list.ToImmutableArray(); diff --git a/tests/Entity.cs b/tests/Entity.cs index e7b89b6..4ad27bb 100644 --- a/tests/Entity.cs +++ b/tests/Entity.cs @@ -296,8 +296,8 @@ public void UndeletableEntity() ent.Delete(); - Assert.Throws(() => ctx.World.Delete(ctx.World.Entity())); - Assert.Throws(() => ctx.World.Delete(Wildcard.ID)); + Assert.ThrowsAny(() => ctx.World.Delete(ctx.World.Entity())); + Assert.ThrowsAny(() => ctx.World.Delete(Wildcard.ID)); } [Fact] diff --git a/tests/Query.cs b/tests/Query.cs index 6340aa8..441c237 100644 --- a/tests/Query.cs +++ b/tests/Query.cs @@ -183,7 +183,7 @@ public void Query_Single() .Set(new IntComponent()) .Set(); - Assert.Throws(() => + Assert.ThrowsAny(() => ctx.World.Query<(With, With, With)>().Single() ); } @@ -372,5 +372,29 @@ public void Query_Nested_Or() resEnt = query.Single(); Assert.Equal(ent2.ID, resEnt.ID); } + + [Fact] + public void Query_FilterTypes_In_QueryData() + { + using var ctx = new Context(); + + ctx.World.Entity(); + ctx.World.Entity(); + + Assert.ThrowsAny(ctx.World.Query>); + Assert.ThrowsAny(ctx.World.Query<(FloatComponent, With)>); + } + + [Fact] + public void Query_QueryData_In_FilterType() + { + using var ctx = new Context(); + + ctx.World.Entity(); + ctx.World.Entity(); + + Assert.ThrowsAny(ctx.World.Query); + Assert.ThrowsAny(ctx.World.Query<(FloatComponent, (IntComponent, With))>); + } } } From 116df260291672c976317a3ca5a143cc2c977318 Mon Sep 17 00:00:00 2001 From: andreakarasho Date: Tue, 21 May 2024 23:35:41 +0200 Subject: [PATCH 2/2] fix tests --- samples/MyBattleground/Program.cs | 7 ++++++- src/Match.cs | 3 +++ src/Query.cs | 5 ++++- src/Term.cs | 2 +- src/World.cs | 24 +++++++++++++----------- tests/Query.cs | 21 +++++++++++---------- 6 files changed, 38 insertions(+), 24 deletions(-) diff --git a/samples/MyBattleground/Program.cs b/samples/MyBattleground/Program.cs index e6cf9b6..d58eab3 100644 --- a/samples/MyBattleground/Program.cs +++ b/samples/MyBattleground/Program.cs @@ -6,6 +6,11 @@ const int ENTITIES_COUNT = (524_288 * 2 * 1); +var tt = typeof(AtLeast<(PlayerTag, Networked)>); +var inter = tt.GetInterfaces(); + +var f0 = typeof(IFilter).IsAssignableFrom(tt); +var f1 = tt.GetInterfaces().Any(k => typeof(IFilter) == k); using var ecs = new World(); @@ -26,7 +31,7 @@ ecs.Entity().Set(new Velocity()); -ecs.Query(); +ecs.Query<(Optional, Velocity)>(); //ecs.Entity().Set(new Position() {X = 2}).Set(); ecs.Entity().Set(new Position() {X = 3}).Set(); diff --git a/src/Match.cs b/src/Match.cs index 19a74c8..f1674a4 100644 --- a/src/Match.cs +++ b/src/Match.cs @@ -4,6 +4,9 @@ static class Match { public static int Validate(IComparer comparer, EcsID[] ids, ReadOnlySpan terms) { + if (terms.IsEmpty) + return -1; + foreach (var term in terms) { switch (term.Op) diff --git a/src/Query.cs b/src/Query.cs index 1f397f3..23c8911 100644 --- a/src/Query.cs +++ b/src/Query.cs @@ -140,7 +140,10 @@ internal Query(World world, ImmutableArray terms) ref var subQuery = ref _subQuery; - foreach (var or in terms.Where(s => s.Op == TermOp.Or).Reverse()) + // NOTE: current Or rule: (Or<..>, Or<..>). + // Unsupported nested Or<> (Or<.., Or<...>>) + // which needs a terms.Where(..).Reverse() + foreach (var or in terms.Where(s => s.Op == TermOp.Or)) { var orIds = or.IDs.Select(s => new Term(s, TermOp.With)); subQuery = World.GetQuery diff --git a/src/Term.cs b/src/Term.cs index aa4e20d..331ddfd 100644 --- a/src/Term.cs +++ b/src/Term.cs @@ -47,7 +47,7 @@ public interface IFilter { } public readonly struct With : IFilter where T : struct { } public readonly struct Without : IFilter where T : struct { } public readonly struct Not : IFilter where T : struct { } -public readonly struct Optional : IFilter where T : struct { } +public readonly struct Optional where T : struct { } public readonly struct AtLeast : ITuple, IAtLeast, IFilter where T : ITuple { static readonly ITuple _value = default(T)!; diff --git a/src/World.cs b/src/World.cs index 1e58871..453a407 100644 --- a/src/World.cs +++ b/src/World.cs @@ -642,8 +642,11 @@ static void ParseTuple(ITuple tuple, List terms, Func terms, Func(List terms, Func validate) where T : struct { var type = typeof(T); + if (typeof(ITuple).IsAssignableFrom(type)) + { + ParseTuple((ITuple)default(T), terms, validate); + + return; + } (var isValid, var errorMsg) = validate(type); EcsAssert.Panic(isValid, errorMsg); @@ -669,13 +678,6 @@ static void ParseType(List terms, Func validate) return; } - if (typeof(ITuple).IsAssignableFrom(type)) - { - ParseTuple((ITuple)default(T), terms, validate); - - return; - } - EcsAssert.Panic(false, $"Type {type} is not registered. Register {type} using world.Entity() or assign it to an entity."); } @@ -691,7 +693,7 @@ static Query() var list = new List(); ParseType(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), "Filters are not allowed in QueryData")); - ParseType(list, s => (typeof(IFilter).IsAssignableFrom(s) && s.GetInterfaces().All(k => typeof(IFilter).IsAssignableFrom(k)), "You must use a IFilter type")); + ParseType(list, s => (typeof(IFilter).IsAssignableFrom(s) && s.GetInterfaces().Any(k => typeof(IFilter) == k), "You must use a IFilter type")); Terms = list.ToImmutableArray(); @@ -709,7 +711,7 @@ static Query() { var list = new List(); - ParseType(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), "Filters are not allowed in QueryData")); + ParseType(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), $"Filter '{s}' not allowed in QueryData")); Terms = list.ToImmutableArray(); diff --git a/tests/Query.cs b/tests/Query.cs index 441c237..1e13ad8 100644 --- a/tests/Query.cs +++ b/tests/Query.cs @@ -173,7 +173,7 @@ public void Query_Single() .Set(new FloatComponent()) .Set(new IntComponent()); - var result = ctx.World.Query<(With, With, With)>() + var result = ctx.World.Query, With, With)>() .Single(); Assert.Equal(singleton.ID, result.ID); @@ -184,7 +184,7 @@ public void Query_Single() .Set(); Assert.ThrowsAny(() => - ctx.World.Query<(With, With, With)>().Single() + ctx.World.Query, With, With)>().Single() ); } @@ -199,7 +199,7 @@ public void Query_Optional() ctx.World.Entity().Set(new FloatComponent()).Set(new IntComponent() { Value = 10 }); var count = 0; - ctx.World.Query<(Optional, With)>() + ctx.World.Query, With>() .Each((ref IntComponent maybeInt) => { Assert.True(Unsafe.IsNullRef(ref maybeInt) || maybeInt.Value == 10); count += 1; @@ -227,7 +227,7 @@ public void Query_Multiple_Optional() .Set();; var count = 0; - ctx.World.Query<(Optional, Optional, With)>() + ctx.World.Query<(Optional, Optional), With>() .Each((ref IntComponent maybeInt, ref FloatComponent maybeFloat) => { Assert.True(Unsafe.IsNullRef(ref maybeInt) || maybeInt.Value == 10); Assert.True(Unsafe.IsNullRef(ref maybeFloat) || maybeFloat.Value == 0.5f); @@ -253,7 +253,7 @@ public void Query_AtLeast() .Set(new BoolComponent()); var entRes = ctx.World - .Query>() + .Query>() .Single(); Assert.Equal(ent1.ID, entRes.ID); @@ -303,7 +303,7 @@ public void Query_None() .Set(new BoolComponent()); var count = ctx.World - .Query>() + .Query, With)>>() .Count(); var total = ctx.World.EntityCount - 3; @@ -329,8 +329,7 @@ public void Query_Or() .Set(new BoolComponent()); var query = ctx.World - .Query<(NormalTag, NormalTag2), - Or<(BoolComponent, NormalTag)>>(); + .Query, With)>, Or<(BoolComponent, With)>)>(); var resEnt = query.Single(); Assert.Equal(ent0.ID, resEnt.ID); @@ -358,8 +357,10 @@ public void Query_Nested_Or() .Set(new BoolComponent()); var query = ctx.World - .Query<(NormalTag, NormalTag2), - Or<(BoolComponent, NormalTag, Or<(IntComponent, BoolComponent)>)>>(); + .Query, With)>, + Or<(BoolComponent, With)>, + Or<(IntComponent, BoolComponent)>)>(); var resEnt = query.Single(); Assert.Equal(ent0.ID, resEnt.ID);