Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce types #21

Merged
merged 2 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
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 @@
}
}

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 @@

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 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 @@
{
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 @@
{
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>))>);
}
}
}
Loading