Skip to content

Commit

Permalink
Merge pull request #21 from andreakarasho/enforce-types
Browse files Browse the repository at this point in the history
Enforce types
  • Loading branch information
andreakarasho authored May 21, 2024
2 parents 9d120b6 + 116df26 commit 689201f
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 41 deletions.
23 changes: 17 additions & 6 deletions samples/MyBattleground/Program.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,57 @@
// 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);

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();


ecs.Entity<PlayerTag>().Set<Networked>();
ecs.Entity<Likes>();
ecs.Entity<Dogs>();
ecs.Entity<Position>();
ecs.Entity<ManagedData>();
ecs.Entity<Velocity>().Set<Networked>();


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<Networked>();
ecs.Entity().Set(new Velocity());


ecs.Query<(Optional<Position>, Velocity)>();

//ecs.Entity().Set(new Position() {X = 2}).Set<Likes>();
ecs.Entity().Set(new Position() {X = 3}).Set<Networked>();

ecs.Query<(Position, Likes), Or<(Position, Networked)>>()
ecs.Query<(Position, ManagedData), Or<(Position, With<Networked>)>>()
.Each((EntityView e, ref Position maybe) => {
var isNull = Unsafe.IsNullRef(ref maybe);

Console.WriteLine("is null {0} - {1}", isNull, e.Name());
});

ecs.Query<(Optional<Position>, With<Velocity>)>()
ecs.Query<Optional<Position>, With<Velocity>>()
.Each((EntityView e, ref Position maybe) => {
var isNull = Unsafe.IsNullRef(ref maybe);

Console.WriteLine("is null {0} - {1}", isNull, e.Name());
});


ecs.Query<(With<Position>, With<Velocity>)>()
ecs.Query<Position, With<Velocity>>()
.Each((EntityView e, ref Position maybe) => {
var isNull = Unsafe.IsNullRef(ref maybe);

Expand Down Expand Up @@ -423,3 +433,4 @@ enum GameStates
}

struct Networked { }
delegate void Query2Del<T0, T1>(ref T0 t0, ref T1 t1);
3 changes: 3 additions & 0 deletions src/Match.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ static class Match
{
public static int Validate(IComparer<ulong> comparer, EcsID[] ids, ReadOnlySpan<Term> terms)
{
if (terms.IsEmpty)
return -1;

foreach (var term in terms)
{
switch (term.Op)
Expand Down
5 changes: 4 additions & 1 deletion src/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ internal Query(World world, ImmutableArray<Term> 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
Expand Down
10 changes: 5 additions & 5 deletions src/Term.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public interface IFilter { }
public readonly struct With<T> : IFilter where T : struct { }
public readonly struct Without<T> : IFilter where T : struct { }
public readonly struct Not<T> : IFilter where T : struct { }
public readonly struct Optional<T> : IFilter where T : struct { }
public readonly struct Optional<T> where T : struct { }
public readonly struct AtLeast<T> : ITuple, IAtLeast, IFilter where T : ITuple
{
static readonly ITuple _value = default(T)!;
Expand Down Expand Up @@ -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 { }
62 changes: 46 additions & 16 deletions src/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand All @@ -545,11 +552,15 @@ static Component()
Value = new ComponentInfo(HashCode, Size);
_arrayCreator.Add(Value.ID, count => Size > 0 ? new T[count] : Array.Empty<T>());

_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<T>), new (Value.ID, TermOp.Optional));
}

_typesConvertion.Add(typeof(With<T>), new (Value.ID, TermOp.With));
_typesConvertion.Add(typeof(Not<T>), new (Value.ID, TermOp.Without));
_typesConvertion.Add(typeof(Without<T>), new (Value.ID, TermOp.Without));
_typesConvertion.Add(typeof(Optional<T>), new (Value.ID, TermOp.Optional));

_componentInfosByType.Add(typeof(T), Value);

Expand Down Expand Up @@ -584,7 +595,17 @@ private static int GetSize()
}
}

static void ParseTuple(ITuple tuple, List<Term> terms)
static void Validate(Type type)
{
if (typeof(ITuple).IsAssignableFrom(type))
{

}


}

static void ParseTuple(ITuple tuple, List<Term> terms, Func<Type, (bool, string?)> validate)
{
var mainType = tuple.GetType();
TermOp? op = null;
Expand Down Expand Up @@ -617,10 +638,16 @@ static void ParseTuple(ITuple tuple, List<Term> terms)

if (typeof(ITuple).IsAssignableFrom(type))
{
ParseTuple((ITuple)tuple[i]!, terms);
ParseTuple((ITuple)tuple[i]!, terms, validate);
continue;
}

if (!op.HasValue)
{
(var isValid, var errorMsg) = validate(type);
EcsAssert.Panic(isValid, errorMsg);
}

var term = GetTerm(type);
tmpTerms.Add(term);
}
Expand All @@ -631,19 +658,22 @@ static void ParseTuple(ITuple tuple, List<Term> terms)
}
}

static void ParseType<T>(List<Term> terms) where T : struct
static void ParseType<T>(List<Term> terms, Func<Type, (bool, string?)> validate) where T : struct
{
var type = typeof(T);
if (_typesConvertion.TryGetValue(type, out var term))
if (typeof(ITuple).IsAssignableFrom(type))
{
terms.Add(term);
ParseTuple((ITuple)default(T), terms, validate);

return;
}

if (typeof(ITuple).IsAssignableFrom(type))
(var isValid, var errorMsg) = validate(type);
EcsAssert.Panic(isValid, errorMsg);

if (_typesConvertion.TryGetValue(type, out var term))
{
ParseTuple((ITuple)default(T), terms);
terms.Add(term);

return;
}
Expand All @@ -662,8 +692,8 @@ static Query()
{
var list = new List<Term>();

ParseType<TQueryData>(list);
ParseType<TQueryFilter>(list);
ParseType<TQueryData>(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), "Filters are not allowed in QueryData"));

Check warning on line 695 in src/World.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 's' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 695 in src/World.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 's' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
ParseType<TQueryFilter>(list, s => (typeof(IFilter).IsAssignableFrom(s) && s.GetInterfaces().Any(k => typeof(IFilter) == k), "You must use a IFilter type"));

Check warning on line 696 in src/World.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 's' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Terms = list.ToImmutableArray();

Expand All @@ -681,7 +711,7 @@ static Query()
{
var list = new List<Term>();

ParseType<TQueryData>(list);
ParseType<TQueryData>(list, s => (!s.GetInterfaces().Any(k => typeof(IFilter).IsAssignableFrom(k)), $"Filter '{s}' not allowed in QueryData"));

Check warning on line 714 in src/World.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 's' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Check warning on line 714 in src/World.cs

View workflow job for this annotation

GitHub Actions / build

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 's' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Terms = list.ToImmutableArray();

Expand Down
4 changes: 2 additions & 2 deletions tests/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ public void UndeletableEntity()

ent.Delete();

Assert.Throws<Exception>(() => ctx.World.Delete(ctx.World.Entity<DoNotDelete>()));
Assert.Throws<Exception>(() => ctx.World.Delete(Wildcard.ID));
Assert.ThrowsAny<Exception>(() => ctx.World.Delete(ctx.World.Entity<DoNotDelete>()));
Assert.ThrowsAny<Exception>(() => ctx.World.Delete(Wildcard.ID));
}

[Fact]
Expand Down
47 changes: 36 additions & 11 deletions tests/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public void Query_Single()
.Set(new FloatComponent())
.Set(new IntComponent());

var result = ctx.World.Query<(With<FloatComponent>, With<IntComponent>, With<NormalTag>)>()
var result = ctx.World.Query<ValueTuple, (With<FloatComponent>, With<IntComponent>, With<NormalTag>)>()
.Single();

Assert.Equal(singleton.ID, result.ID);
Expand All @@ -183,8 +183,8 @@ public void Query_Single()
.Set(new IntComponent())
.Set<NormalTag>();

Assert.Throws<Exception>(() =>
ctx.World.Query<(With<FloatComponent>, With<IntComponent>, With<NormalTag>)>().Single()
Assert.ThrowsAny<Exception>(() =>
ctx.World.Query<ValueTuple, (With<FloatComponent>, With<IntComponent>, With<NormalTag>)>().Single()
);
}

Expand All @@ -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<IntComponent>, With<FloatComponent>)>()
ctx.World.Query<Optional<IntComponent>, With<FloatComponent>>()
.Each((ref IntComponent maybeInt) => {
Assert.True(Unsafe.IsNullRef(ref maybeInt) || maybeInt.Value == 10);
count += 1;
Expand Down Expand Up @@ -227,7 +227,7 @@ public void Query_Multiple_Optional()
.Set<NormalTag>();;

var count = 0;
ctx.World.Query<(Optional<IntComponent>, Optional<FloatComponent>, With<NormalTag>)>()
ctx.World.Query<(Optional<IntComponent>, Optional<FloatComponent>), With<NormalTag>>()
.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);
Expand All @@ -253,7 +253,7 @@ public void Query_AtLeast()
.Set(new BoolComponent());

var entRes = ctx.World
.Query<AtLeast<(IntComponent, BoolComponent)>>()
.Query<ValueTuple, AtLeast<(IntComponent, BoolComponent)>>()
.Single();

Assert.Equal(ent1.ID, entRes.ID);
Expand Down Expand Up @@ -303,7 +303,7 @@ public void Query_None()
.Set(new BoolComponent());

var count = ctx.World
.Query<None<(NormalTag, NormalTag2)>>()
.Query<ValueTuple, None<(With<NormalTag>, With<NormalTag2>)>>()
.Count();

var total = ctx.World.EntityCount - 3;
Expand All @@ -329,8 +329,7 @@ public void Query_Or()
.Set(new BoolComponent());

var query = ctx.World
.Query<(NormalTag, NormalTag2),
Or<(BoolComponent, NormalTag)>>();
.Query<ValueTuple, (Or<(With<NormalTag>, With<NormalTag2>)>, Or<(BoolComponent, With<NormalTag>)>)>();

var resEnt = query.Single();
Assert.Equal(ent0.ID, resEnt.ID);
Expand Down Expand Up @@ -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<ValueTuple,
(Or<(With<NormalTag>, With<NormalTag2>)>,
Or<(BoolComponent, With<NormalTag>)>,
Or<(IntComponent, BoolComponent)>)>();

var resEnt = query.Single();
Assert.Equal(ent0.ID, resEnt.ID);
Expand All @@ -372,5 +373,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<IntComponent>();
ctx.World.Entity<FloatComponent>();

Assert.ThrowsAny<Exception>(ctx.World.Query<With<IntComponent>>);
Assert.ThrowsAny<Exception>(ctx.World.Query<(FloatComponent, With<IntComponent>)>);
}

[Fact]
public void Query_QueryData_In_FilterType()
{
using var ctx = new Context();

ctx.World.Entity<IntComponent>();
ctx.World.Entity<FloatComponent>();

Assert.ThrowsAny<Exception>(ctx.World.Query<IntComponent, FloatComponent>);
Assert.ThrowsAny<Exception>(ctx.World.Query<(FloatComponent, (IntComponent, With<IntComponent>))>);
}
}
}

0 comments on commit 689201f

Please sign in to comment.