Skip to content

Commit

Permalink
Port iter::targets() C++ api
Browse files Browse the repository at this point in the history
  • Loading branch information
BeanCheeseBurrito committed Nov 13, 2024
1 parent 58464a2 commit dbac90e
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 7 deletions.
51 changes: 51 additions & 0 deletions src/Flecs.NET.Examples/Queries/IterTargets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// This example shows how to iterate matching targets of a relationship without
// iterating the same entity multiple times.
//
// When creating a (Relationship, *) query, the query will return one result per
// matching relationship pair, which returns the same entity multiple times.
// This example uses a (Relationship, _) query which at most returns a single
// result per matching pair, and a targets() function that iterates the targets
// for the current entity.

using Flecs.NET.Core;

// Tags
file struct Eats;
file struct Pizza;
file struct Salad;

public static unsafe class Queries_IterTargets
{
public static void Main()
{
using World world = World.Create();

world.Entity("Bob")
.Add<Eats, Pizza>()
.Add<Eats, Salad>();

// Ecs.Any ensures that only a single result
// is returned per entity, as opposed to
// Ecs.Wildcard which returns a result per
// matched pair.
using Query query = world.QueryBuilder()
.With<Eats>(Ecs.Any)
.Build();

query.Each((Iter it, int row) =>
{
Entity e = it.Entity(row);
Console.WriteLine($"{e} eats:");

it.Targets(0, (Entity tgt) =>
{
Console.WriteLine($" - {tgt}");
});
});
}
}

// Output:
// Bob eats:
// - Pizza
// - Salad
182 changes: 175 additions & 7 deletions src/Flecs.NET.Tests/Cpp/QueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Flecs.NET.Tests.Cpp;
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
[SuppressMessage("ReSharper", "AccessToModifiedClosure")]
[SuppressMessage("ReSharper", "VariableHidesOuterVariable")]
public unsafe class QueryTests
{
public static int InvokedCount;
Expand Down Expand Up @@ -2844,10 +2845,10 @@ private void RunWithIterFiniInterrupt()
Entity e1 = world.Entity()
.Set(new Position(10, 20))
.Add<Foo>();
Entity e2 = world.Entity()
world.Entity()
.Set(new Position(10, 20))
.Add<Bar>();
Entity e3 = world.Entity()
world.Entity()
.Set(new Position(10, 20))
.Add<Hello>();

Expand Down Expand Up @@ -3065,7 +3066,7 @@ private void EmptyTablesEachWithIter()
.QueryFlags(EcsQueryMatchEmptyTables)
.Build();

q.Each((Iter it, int i, ref Position p, ref Velocity v) =>
q.Each((Iter _, int _, ref Position p, ref Velocity v) =>
{
p.X += v.X;
p.Y += v.Y;
Expand Down Expand Up @@ -3095,12 +3096,12 @@ private void PairWithVariableSrc()
Entity other = world.Entity()
.Set(new OtherComp(10));

for (int i = 0; i < 3; ++i)
for (int i = 0; i < 3; i++)
world.Entity()
.Set(new ThisComp(i))
.Add<RelData>(other);

Query<RelData, ThisComp, OtherComp> q = world.QueryBuilder<RelData, ThisComp, OtherComp>()
using Query<RelData, ThisComp, OtherComp> q = world.QueryBuilder<RelData, ThisComp, OtherComp>()
.TermAt(0).Second("$other")
.TermAt(2).Src("$other")
.Build();
Expand Down Expand Up @@ -3130,12 +3131,12 @@ private void PairWithVariableSrcNoRowFields()
world.Entity()
.Set(new OtherComp(1));

for (int i = 0; i < 3; ++i)
for (int i = 0; i < 3; i++)
world.Entity()
.Set(new ThisComp(i))
.Add<RelData>(other);

Query<RelData, ThisComp, OtherComp> q = world.QueryBuilder<RelData, ThisComp, OtherComp>()
using Query<RelData, ThisComp, OtherComp> q = world.QueryBuilder<RelData, ThisComp, OtherComp>()
.TermAt(0).Second("$other")
.TermAt(2).Src("$other")
.Build();
Expand All @@ -3149,4 +3150,171 @@ private void PairWithVariableSrcNoRowFields()

Assert.Equal(7, isPresent);
}

[Fact]
private void IterTargets()
{
using World world = World.Create();

Entity likes = world.Entity();
Entity pizza = world.Entity();
Entity salad = world.Entity();
Entity alice = world.Entity()
.Add(likes, pizza)
.Add(likes, salad);

using Query q = world.QueryBuilder()
.With(likes, Ecs.Any)
.Build();

int count = 0;
int tgtCount = 0;

q.Each((Iter it, int row) =>
{
Entity e = it.Entity(row);
Assert.True(e == alice);

it.Targets(0, (Entity tgt) =>
{
if (tgtCount == 0)
Assert.True(tgt == pizza);
if (tgtCount == 1)
Assert.True(tgt == salad);
tgtCount++;
});

count++;
});

Assert.Equal(1, count);
Assert.Equal(2, tgtCount);
}

[Fact]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private void IterTargets2ndField()
{
using World world = World.Create();

Entity likes = world.Entity();
Entity pizza = world.Entity();
Entity salad = world.Entity();
Entity alice = world.Entity()
.Add<Position>()
.Add(likes, pizza)
.Add(likes, salad);

using Query q = world.QueryBuilder()
.With<Position>()
.With(likes, Ecs.Any)
.Build();

int count = 0;
int tgtCount = 0;

q.Each((Iter it, int row) =>
{
Entity e = it.Entity(row);
Assert.True(e == alice);

it.Targets(1, (Entity tgt) =>
{
if (tgtCount == 0)
Assert.True(tgt == pizza);
if (tgtCount == 1)
Assert.True(tgt == salad);
tgtCount++;
});

count++;
});

Assert.Equal(1, count);
Assert.Equal(2, tgtCount);
}

[Fact]
private void IterTargetsFieldOutOfRange()
{
using World world = World.Create();

Entity likes = world.Entity();
Entity pizza = world.Entity();
Entity salad = world.Entity();
Entity alice = world.Entity()
.Add(likes, pizza)
.Add(likes, salad);

using Query q = world.QueryBuilder()
.With(likes, Ecs.Any)
.Build();

q.Each((Iter it, int row) =>
{
Entity e = it.Entity(row);
Assert.True(e == alice);

Assert.Throws<Ecs.AssertionException>(() =>
{
it.Targets(1, (Entity _) => { });
});
});
}

[Fact]
private void IterTargetsFieldNotAPair()
{
using World world = World.Create();

Entity likes = world.Entity();
Entity pizza = world.Entity();
Entity salad = world.Entity();
Entity alice = world.Entity()
.Add<Position>()
.Add(likes, pizza)
.Add(likes, salad);

using Query q = world.QueryBuilder()
.With<Position>()
.Build();

q.Each((Iter it, int row) =>
{
Entity e = it.Entity(row);
Assert.True(e == alice);

Assert.Throws<Ecs.AssertionException>(() =>
{
it.Targets(1, (Entity _) => { });
});
});
}

[Fact]
private void IterTargetsFieldNotSet()
{
using World world = World.Create();

Entity likes = world.Entity();
Entity alice = world.Entity()
.Add<Position>();

using Query q = world.QueryBuilder()
.With<Position>()
.With(likes, Ecs.Any).Optional()
.Build();

q.Each((Iter it, int row) =>
{
Entity e = it.Entity(row);
Assert.True(e == alice);
Assert.True(!it.IsSet(1));

Assert.Throws<Ecs.AssertionException>(() =>
{
it.Targets(1, (Entity _) => { });
});
});
}
}
46 changes: 46 additions & 0 deletions src/Flecs.NET/Core/Iter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,52 @@ public Entity GetVar(string name)
return new Entity(Handle->world, ecs_iter_get_var(Handle, varId));
}

/// <summary>
/// Iterate targets for pair field.
/// </summary>
/// <param name="index">The field index.</param>
/// <param name="callback">The callback.</param>
public void Targets(int index, Ecs.EachEntityCallback callback)
{
Ecs.Assert(Handle->table != null, nameof(ECS_INVALID_OPERATION));
Ecs.Assert(index < Handle->field_count, nameof(ECS_INVALID_PARAMETER));
Ecs.Assert(Utils.Bool(ecs_field_is_set(Handle, (byte)index)), nameof(ECS_INVALID_PARAMETER));

ecs_type_t* tableType = ecs_table_get_type(Handle->table);
ecs_table_record_t *tr = Handle->trs[index];

int end = tr->index + tr->count;
for (int i = tr->index; i < end; i++)
{
ulong id = tableType->array[i];
Ecs.Assert(Ecs.IsPair(id), "Field must be a pair.");
callback(new Entity(Handle->world, Ecs.PairSecond(Handle->real_world, id)));
}
}

/// <summary>
/// Iterate targets for pair field.
/// </summary>
/// <param name="index">The field index.</param>
/// <param name="callback">The callback.</param>
public void Targets(int index, delegate*<Entity, void> callback)
{
Ecs.Assert(Handle->table != null, nameof(ECS_INVALID_OPERATION));
Ecs.Assert(index < Handle->field_count, nameof(ECS_INVALID_PARAMETER));
Ecs.Assert(Utils.Bool(ecs_field_is_set(Handle, (byte)index)), nameof(ECS_INVALID_PARAMETER));

ecs_type_t* tableType = ecs_table_get_type(Handle->table);
ecs_table_record_t *tr = Handle->trs[index];

int end = tr->index + tr->count;
for (int i = tr->index; i < end; i++)
{
ulong id = tableType->array[i];
Ecs.Assert(Ecs.IsPair(id), "Field must be a pair.");
callback(new Entity(Handle->world, Ecs.PairSecond(Handle->real_world, id)));
}
}

/// <summary>
/// Progress iterator.
/// </summary>
Expand Down

0 comments on commit dbac90e

Please sign in to comment.