diff --git a/fennecs.tests/Conceptual/EventConceptTests.cs b/fennecs.tests/Conceptual/EventConceptTests.cs index 484e708b..44032d52 100644 --- a/fennecs.tests/Conceptual/EventConceptTests.cs +++ b/fennecs.tests/Conceptual/EventConceptTests.cs @@ -17,12 +17,12 @@ static void Added(Entity entity, T value) } } - public interface IValue + private interface IValue { T Value { get; set; } } - public interface IModified + private interface IModified { static void Modified(C value) { @@ -38,7 +38,7 @@ static void Removed(Entity entity, T value) } } - public record struct TestComponent(int Value) : + private record struct TestComponent(int Value) : IValue, IAdded, IModified, diff --git a/fennecs.tests/Storage/R.Tests.cs b/fennecs.tests/Storage/R.Tests.cs index e2d99f7d..f8b08a35 100644 --- a/fennecs.tests/Storage/R.Tests.cs +++ b/fennecs.tests/Storage/R.Tests.cs @@ -13,4 +13,13 @@ public void Can_Read() Assert.Equal(1, rw.read); } + + [Fact] + public void Implicitly_Casts_to_Value() + { + var x = 1; + var r = new R(ref x); + + Assert.Equal(1, r); + } } diff --git a/fennecs.tests/Storage/RW.Tests.cs b/fennecs.tests/Storage/RW.Tests.cs index 5e102d09..cf21a795 100644 --- a/fennecs.tests/Storage/RW.Tests.cs +++ b/fennecs.tests/Storage/RW.Tests.cs @@ -1,4 +1,5 @@ -using fennecs.storage; +using fennecs.events; +using fennecs.storage; namespace fennecs.tests.Storage; @@ -17,6 +18,19 @@ public void Can_Read() Assert.Equal(1, rw.read); } + [Fact] + public void Implicitly_Casts_to_Value() + { + using var world = new World(); + var entity = world.Spawn(); + + var x = 1; + var match = default(Match); + var rw = new RW(ref x, ref entity, ref match); + + Assert.Equal(1, rw); + } + [Fact] public void Can_Write() { @@ -101,4 +115,88 @@ public void Can_Remove_Relation() entity.RW(match).Remove(); Assert.False(entity.Has(match)); } + + + private struct Type69 : Modified; + + [Fact] + public void Triggers_Entities_on_Modified_Value() + { + using var world = new World(); + var entity = world.Spawn(); + entity.Add(new Type69()); + + var match = default(Match); + var rw = entity.RW(match); + + var modified = false; + + Modified.Entities += entities => + { + Assert.Equal(1, entities.Length); + Assert.Equal(entity, entities[0]); + modified = true; + }; + + rw.write = new(); + Assert.True(modified); + + Modified.Clear(); + } + + private class Type42 : Modified; + + [Fact] + public void Triggers_Entities_on_Modified_Reference() + { + using var world = new World(); + var entity = world.Spawn(); + entity.Add(new Type42()); + + var match = default(Match); + var rw = entity.RW(match); + + var modified = false; + + Modified.Entities += entities => + { + Assert.Equal(1, entities.Length); + Assert.Equal(entity, entities[0]); + modified = true; + }; + + rw.write = new(); + Assert.True(modified); + + Modified.Clear(); + } + + [Fact] + public void Triggers_Values_on_Modified_Reference() + { + using var world = new World(); + var entity = world.Spawn(); + var original = new Type42(); + entity.Add(original); + + var match = default(Match); + var rw = entity.RW(match); + + var modified = false; + var updated = new Type42(); + + Modified.Values += (entities, originals, updateds) => + { + Assert.Equal(1, entities.Length); + Assert.Equal(entity, entities[0]); + Assert.Equal(original, originals[0]); + Assert.Equal(updated, updateds[0]); + modified = true; + }; + + rw.write = updated; + Assert.True(modified); + + Modified.Clear(); + } } diff --git a/fennecs.tests/Stream/Stream.2.Tests.cs b/fennecs.tests/Stream/Stream.2.Tests.cs index 6904e485..ddb2ac98 100644 --- a/fennecs.tests/Stream/Stream.2.Tests.cs +++ b/fennecs.tests/Stream/Stream.2.Tests.cs @@ -6,6 +6,67 @@ namespace fennecs.tests.Stream; // ReSharper disable once ClassNeverInstantiated.Global public class Stream2Tests(ITestOutputHelper output) { + [Fact] public void Can_Use_RW_Inferred() + { + using var world = new World(); + var entity = world.Spawn(); + entity.Add(123).Add(890f); + + var stream = world.Stream(); + + stream.For(static (a, b) => { + Assert.Equal(123, a.read); + Assert.Equal(890f, b.read); + b.write = 456f; + }); + } + + [Fact] public void Can_Use_WR_Inferred() + { + using var world = new World(); + var entity = world.Spawn(); + entity.Add(123).Add(890f); + + var stream = world.Stream(); + + stream.For(static (a, b) => { + Assert.Equal(123, a.read); + Assert.Equal(890f, b.read); + }); + } + + [Fact] public void Can_Use_WW_Inferred() + { + using var world = new World(); + var entity = world.Spawn(); + entity.Add(123).Add(890f); + + var stream = world.Stream(); + + stream.For(static (a, b) => + { + Assert.Equal(123, a.read); + Assert.Equal(890f, b.read); + a.write = 456; + b.write = 789f; + }); + } + + [Fact] public void Can_Use_RR_Inferred() + { + using var world = new World(); + var entity = world.Spawn(); + entity.Add(123).Add(890f); + + var stream = world.Stream(); + + stream.For(static (a, b) => + { + Assert.Equal(123, a.read); + Assert.Equal(890f, b.read); + }); + } + [Fact] public void Can_Enumerate_Stream() { diff --git a/fennecs/Archetype.cs b/fennecs/Archetype.cs index 0d3f41d2..d1822a38 100644 --- a/fennecs/Archetype.cs +++ b/fennecs/Archetype.cs @@ -38,8 +38,10 @@ public sealed class Archetype : IEnumerable, IComparable /// Does this Archetype currently contain no Entities? /// public bool IsEmpty => Count == 0; - - + + public ReadOnlySpan Span => IdentityStorage.Span; + + /// /// The World this Archetype is a part of. /// diff --git a/fennecs/Delegates.2.cs b/fennecs/Delegates.2.cs index be9698b6..39df8092 100644 --- a/fennecs/Delegates.2.cs +++ b/fennecs/Delegates.2.cs @@ -1,12 +1,16 @@ using fennecs.storage; + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace fennecs; +public delegate void ComponentActionWW(RW comp0, RW comp1) where C0 : notnull where C1 : notnull; +public delegate void ComponentActionWR(RW comp0, R comp1) where C0 : notnull where C1 : notnull; +public delegate void ComponentActionRW(R comp0, RW comp1) where C0 : notnull where C1 : notnull; +public delegate void ComponentActionRR(R comp0, R comp1) where C0 : notnull where C1 : notnull; -public delegate void ComponentActionR(R comp0) where C0 : notnull; -public delegate void ComponentActionW(RW comp0) where C0 : notnull; - -public delegate void ComponentActionER(EntityRef entity, R comp0) where C0 : notnull; -public delegate void ComponentActionEW(EntityRef entity, RW comp0) where C0 : notnull; +public delegate void EntityComponentActionWW(EntityRef entity, R comp0, R comp1) where C0 : notnull where C1 : notnull; +public delegate void EntityComponentActionRR(EntityRef entity, R comp0, R comp1) where C0 : notnull where C1 : notnull; +public delegate void EntityComponentActionWR(EntityRef entity, RW comp0, R comp1) where C0 : notnull where C1 : notnull; +public delegate void EntityComponentActionRW(EntityRef entity, R comp0, RW comp1) where C0 : notnull where C1 : notnull; diff --git a/fennecs/Stream.2.cs b/fennecs/Stream.2.cs index cd838913..ef18e455 100644 --- a/fennecs/Stream.2.cs +++ b/fennecs/Stream.2.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Immutable; +using System.Runtime.CompilerServices; using fennecs.pools; namespace fennecs; @@ -15,11 +16,41 @@ public record Stream(Query Query, Match Match0, Match Match1) { private readonly ImmutableArray _streamTypes = [TypeExpression.Of(Match0), TypeExpression.Of(Match1)]; + #region New Stream.For - #region Stream.For + /// + [OverloadResolutionPriority(0b00)] + public void For(ComponentActionWW action) + { + using var worldLock = World.Lock(); + + foreach (var table in Filtered) + { + using var join = table.CrossJoin(_streamTypes.AsSpan()); + var count = table.Count; + if (join.Empty) continue; + do + { + var (s0, s1) = join.Select; + var span0 = s0.Span; + var match0 = default(Match);//s0.Match; + var span1 = s1.Span; + var match1 = default(Match);//s1.Match; + for (var i = 0; i < count; i++) + { + var entity = table[i]; + action( + new(ref span0[i], in entity, in match0), + new(ref span1[i], in entity, in match1) + ); + } + } while (join.Iterate()); + } + } /// - public void For(ComponentAction action) + [OverloadResolutionPriority(0b01)] + public void For(ComponentActionWR action) { using var worldLock = World.Lock(); @@ -27,15 +58,108 @@ public void For(ComponentAction action) { using var join = table.CrossJoin(_streamTypes.AsSpan()); if (join.Empty) continue; + var count = table.Count; do { var (s0, s1) = join.Select; - Unroll8(s0, s1, action); + var span0 = s0.Span; + var match0 = default(Match);//s0.Match; + var span1 = s1.Span; + //var match1 = default(Match);//s1.Match; + for (var i = 0; i < count; i++) + { + var entity = table[i]; + action( + new(ref span0[i], in entity, in match0), + new(in span1[i]) + ); + } + } while (join.Iterate()); + } + } + + /// + [OverloadResolutionPriority(0b10)] + public void For(ComponentActionRW action) + { + using var worldLock = World.Lock(); + + foreach (var table in Filtered) + { + using var join = table.CrossJoin(_streamTypes.AsSpan()); + if (join.Empty) continue; + var count = table.Count; + do + { + var (s0, s1) = join.Select; + //var entities = table.Span; + var span0 = s0.Span; + //var match0 = default(Match);//s0.Match; + var span1 = s1.Span; + var match1 = default(Match);//s1.Match; + for (var i = 0; i < count; i++) + { + var entity = table[i]; + action( + new(in span0[i]), + new(ref span1[i], in entity, in match1) + ); + } } while (join.Iterate()); } } + /// + [OverloadResolutionPriority(0b11)] + public void For(ComponentActionRR action) + { + using var worldLock = World.Lock(); + + foreach (var table in Filtered) + { + using var join = table.CrossJoin(_streamTypes.AsSpan()); + if (join.Empty) continue; + var count = table.Count; + do + { + var (s0, s1) = join.Select; + var span0 = s0.Span; + //var match0 = default(Match);//s0.Match; + var span1 = s1.Span; + //var match1 = default(Match);//s1.Match; + for (var i = 0; i < count; i++) + { + action( + new(in span0[i]), + new(in span1[i]) + ); + } + } while (join.Iterate()); + } + } + + #endregion + + #region Stream.For + + /// + public void For(ComponentAction action) + { + using var worldLock = World.Lock(); + + foreach (var table in Filtered) + { + using var join = table.CrossJoin(_streamTypes.AsSpan()); + if (join.Empty) continue; + do + { + var (s0, s1) = join.Select; + Unroll8(s0, s1, action); + } while (join.Iterate()); + } + } + /// public void For(U uniform, UniformComponentAction action) { @@ -301,7 +425,7 @@ public void Blit(C1 value, Match match = default) #endregion #region Unroll - + private static void Unroll8(Span span0, Span span1, ComponentAction action) { var c = span0.Length / 8 * 8; diff --git a/fennecs/events/Modified.cs b/fennecs/events/Modified.cs index 09deca1e..4d30d1b5 100644 --- a/fennecs/events/Modified.cs +++ b/fennecs/events/Modified.cs @@ -45,4 +45,13 @@ internal static void Invoke(ReadOnlySpan entities, ReadOnlySpan origi Entities?.Invoke(entities); Values?.Invoke(entities, original, updated); } + + /// + /// For testing purposes, clears the event handlers. + /// + internal static void Clear() + { + Entities = null; + Values = null; + } } diff --git a/fennecs/expressions/Identity.cs b/fennecs/expressions/Identity.cs index ef0ea323..d174ca3c 100644 --- a/fennecs/expressions/Identity.cs +++ b/fennecs/expressions/Identity.cs @@ -9,7 +9,7 @@ namespace fennecs; /// real Entity, tracked object, or virtual concept (e.g. any/none Match Expression). /// [StructLayout(LayoutKind.Explicit)] -internal readonly record struct Identity : IComparable +public readonly record struct Identity : IComparable { [FieldOffset(0)] internal readonly ulong Value; diff --git a/fennecs/storage/R.cs b/fennecs/storage/R.cs index acbc0b9f..fb1ae017 100644 --- a/fennecs/storage/R.cs +++ b/fennecs/storage/R.cs @@ -15,5 +15,5 @@ public readonly ref struct R(ref readonly T val) where T : notnull /// /// Implicitly casts a to its underlying value. /// - public static implicit operator T(R self) => self._value; + public static implicit operator T(R self) => self._value; } \ No newline at end of file diff --git a/fennecs/storage/RW.cs b/fennecs/storage/RW.cs index 5fce5340..50f51388 100644 --- a/fennecs/storage/RW.cs +++ b/fennecs/storage/RW.cs @@ -26,7 +26,7 @@ public T write set { // Optimizes away the write and null checks if it's not modifiable. - if (typeof(T).IsAssignableFrom(typeof(Modified))) + if (typeof(Modified).IsAssignableFrom(typeof(T))) { var original = _value; _value = value; @@ -35,7 +35,8 @@ public T write //_writtenEntities?.Add(_entity); //_writtenOriginals?.Add(original); //_writtenUpdates?.Add(value); - + + Modified.Clear(); // TODO: Handle this in the outer scope, where the lists come from. Modified.Invoke([_entity], [original], [value]); } @@ -75,4 +76,9 @@ public void Remove() { _entity.Remove(_match); } + + /// + /// Implicitly casts a to its underlying value. + /// + public static implicit operator T(RW self) => self._value; } \ No newline at end of file diff --git a/fennecs/storage/RWImmediate.cs b/fennecs/storage/RWImmediate.cs index 8c0ab4d9..28b66711 100644 --- a/fennecs/storage/RWImmediate.cs +++ b/fennecs/storage/RWImmediate.cs @@ -27,7 +27,7 @@ public T write set { // Optimizes away the write and null checks if it's not modifiable. - if (typeof(T).IsAssignableFrom(typeof(Modified))) + if (typeof(Modified).IsAssignableFrom(typeof(T))) { var original = _value; _value = value;