diff --git a/src/Engine.cs b/src/Engine.cs index 1d15bee..9c1de66 100644 --- a/src/Engine.cs +++ b/src/Engine.cs @@ -6,9 +6,9 @@ namespace Encompass { public abstract class Engine { - public readonly List mutateComponentTypes = new List(); - public readonly List emitMessageTypes = new List(); - public readonly List readMessageTypes = new List(); + private readonly List mutateComponentTypes = new List(); + private readonly List emitMessageTypes = new List(); + private readonly List readMessageTypes = new List(); private EntityManager entityManager; private ComponentManager componentManager; diff --git a/src/World.cs b/src/World.cs index a33e08e..b084162 100644 --- a/src/World.cs +++ b/src/World.cs @@ -4,19 +4,19 @@ namespace Encompass { public class World { - private List engines; + private List enginesInOrder; private EntityManager entityManager; private ComponentManager componentManager; private MessageManager messageManager; internal World( - List engines, + List enginesInOrder, EntityManager entityManager, ComponentManager componentManager, MessageManager messageManager ) { - this.engines = engines; + this.enginesInOrder = enginesInOrder; this.entityManager = entityManager; this.componentManager = componentManager; this.messageManager = messageManager; @@ -24,7 +24,7 @@ namespace Encompass public void Update(float dt) { - foreach (var engine in engines) + foreach (var engine in enginesInOrder) { engine.Update(dt); } diff --git a/src/WorldBuilder.cs b/src/WorldBuilder.cs index c32f8c1..9e54821 100644 --- a/src/WorldBuilder.cs +++ b/src/WorldBuilder.cs @@ -1,15 +1,22 @@ +using System; using System.Collections.Generic; +using System.Reflection; +using System.Linq; namespace Encompass { public class WorldBuilder { private List engines = new List(); + private DirectedGraph engineGraph = new DirectedGraph(); private ComponentManager componentManager; private EntityManager entityManager; private MessageManager messageManager; + private Dictionary> messageTypeToEmitters = new Dictionary>(); + private Dictionary> messageTypeToReaders = new Dictionary>(); + public WorldBuilder() { componentManager = new ComponentManager(); @@ -32,13 +39,122 @@ namespace Encompass engines.Add(engine); + engineGraph.AddVertex(engine); + + var emitMessageAttribute = engine.GetType().GetCustomAttribute(false); + if (emitMessageAttribute != null) + { + foreach (var emitMessageType in engine.GetType().GetCustomAttribute(false).emitMessageTypes) + { + if (!messageTypeToEmitters.ContainsKey(emitMessageType)) + { + messageTypeToEmitters.Add(emitMessageType, new HashSet()); + } + + messageTypeToEmitters[emitMessageType].Add(engine); + + if (messageTypeToReaders.ContainsKey(emitMessageType)) + { + foreach (var reader in messageTypeToReaders[emitMessageType]) + { + engineGraph.AddEdge(engine, reader); + } + } + } + } + + var readMessageAttribute = engine.GetType().GetCustomAttribute(false); + if (readMessageAttribute != null) + { + foreach (var readMessageType in engine.GetType().GetCustomAttribute(false).readMessageTypes) + { + if (!messageTypeToReaders.ContainsKey(readMessageType)) + { + messageTypeToReaders.Add(readMessageType, new HashSet()); + } + + messageTypeToReaders[readMessageType].Add(engine); + + if (messageTypeToEmitters.ContainsKey(readMessageType)) + { + foreach (var emitter in messageTypeToEmitters[readMessageType]) + { + engineGraph.AddEdge(emitter, engine); + } + } + } + } + return engine; } public World Build() { + if (engineGraph.Cyclic()) + { + var cycles = engineGraph.SimpleCycles(); + var errorString = "Cycle(s) found in Engines: "; + foreach (var cycle in cycles) + { + errorString += "\n" + + string.Join(" - > ", cycle.Select((engine) => engine.GetType().Name)) + + " -> " + + cycle.First().GetType().Name; + } + throw new EngineCycleException(errorString); + } + + var mutatedComponentTypes = new HashSet(); + var duplicateMutations = new List(); + var componentToEngines = new Dictionary>(); + + foreach (var engine in engines) + { + var mutateAttribute = engine.GetType().GetCustomAttribute(false); + if (mutateAttribute != null) + { + foreach (var mutateComponentType in engine.GetType().GetCustomAttribute(false).mutateComponentTypes) + { + if (mutatedComponentTypes.Contains(mutateComponentType)) + { + duplicateMutations.Add(mutateComponentType); + } + else + { + mutatedComponentTypes.Add(mutateComponentType); + } + + if (!componentToEngines.ContainsKey(mutateComponentType)) + { + componentToEngines[mutateComponentType] = new List(); + } + + componentToEngines[mutateComponentType].Add(engine); + } + } + } + + if (duplicateMutations.Count > 0) + { + var errorString = "Multiple Engines mutate the same Component: "; + foreach (var componentType in duplicateMutations) + { + errorString += "\n" + + componentType.Name + " mutated by: " + + string.Join(", ", componentToEngines[componentType].Select((engine) => engine.GetType().Name)); + } + + throw new EngineMutationConflictException(errorString); + } + + var engineOrder = new List(); + foreach (var engine in engineGraph.TopologicalSort()) + { + engineOrder.Add(engine); + } + var world = new World( - this.engines, + engineOrder, this.entityManager, this.componentManager, this.messageManager diff --git a/src/encompass-cs.csproj b/src/encompass-cs.csproj index f199fa6..d107759 100644 --- a/src/encompass-cs.csproj +++ b/src/encompass-cs.csproj @@ -21,5 +21,8 @@ + + + \ No newline at end of file diff --git a/src/exceptions/EngineCycleException.cs b/src/exceptions/EngineCycleException.cs new file mode 100644 index 0000000..2849787 --- /dev/null +++ b/src/exceptions/EngineCycleException.cs @@ -0,0 +1,13 @@ + +using System; + +namespace Encompass +{ + public class EngineCycleException : Exception + { + public EngineCycleException( + string format, + params object[] args + ) : base(string.Format(format, args)) { } + } +} diff --git a/src/exceptions/EngineMutationConflictException.cs b/src/exceptions/EngineMutationConflictException.cs new file mode 100644 index 0000000..d6cf4d7 --- /dev/null +++ b/src/exceptions/EngineMutationConflictException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Encompass +{ + public class EngineMutationConflictException : Exception + { + public EngineMutationConflictException( + string format, + params object[] args + ) : base(string.Format(format, args)) { } + } +} diff --git a/src/graph/DirectedGraph.cs b/src/graph/DirectedGraph.cs index 77d89a2..1eaaff2 100644 --- a/src/graph/DirectedGraph.cs +++ b/src/graph/DirectedGraph.cs @@ -159,6 +159,11 @@ namespace Encompass return output; } + public bool Cyclic() + { + return StronglyConnectedComponents().Any((scc) => scc.Count() > 1); + } + public IEnumerable TopologicalSort() { var dfs = NodeDFS(); diff --git a/test/DirectedGraphTest.cs b/test/DirectedGraphTest.cs index f38cb66..2a3731e 100644 --- a/test/DirectedGraphTest.cs +++ b/test/DirectedGraphTest.cs @@ -341,5 +341,34 @@ namespace Tests result.Should().ContainEquivalentOf(cycleE); result.Should().HaveCount(5); } + + [Test] + public void Cyclic() + { + var myGraph = new DirectedGraph(); + myGraph.AddVertices(1, 2, 3, 4); + myGraph.AddEdges( + Tuple.Create(1, 2), + Tuple.Create(2, 3), + Tuple.Create(3, 1), + Tuple.Create(3, 4) + ); + + Assert.That(myGraph.Cyclic(), Is.True); + } + + [Test] + public void Acyclic() + { + var myGraph = new DirectedGraph(); + myGraph.AddVertices(1, 2, 3, 4); + myGraph.AddEdges( + Tuple.Create(1, 2), + Tuple.Create(2, 3), + Tuple.Create(3, 4) + ); + + Assert.That(myGraph.Cyclic(), Is.False); + } } } diff --git a/test/WorldBuilderTest.cs b/test/WorldBuilderTest.cs new file mode 100644 index 0000000..29093eb --- /dev/null +++ b/test/WorldBuilderTest.cs @@ -0,0 +1,212 @@ +using NUnit.Framework; + +using Encompass; +using System.Collections.Generic; + +namespace Tests +{ + public class WorldBuilderTest + { + public class EngineCycleSimple + { + struct AMessage : IMessage { } + struct BMessage : IMessage { } + + [Reads(typeof(AMessage))] + [Emits(typeof(BMessage))] + class AEngine : Engine + { + public override void Update(float dt) + { + BMessage message; + this.EmitMessage(message); + } + } + + [Reads(typeof(BMessage))] + [Emits(typeof(AMessage))] + class BEngine : Engine + { + public override void Update(float dt) + { + AMessage message; + this.EmitMessage(message); + } + } + + [Test] + public void EngineCycle() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(); + worldBuilder.AddEngine(); + + Assert.Throws(() => worldBuilder.Build()); + } + } + + public class EngineCycleComplex + { + struct AMessage : IMessage { } + struct BMessage : IMessage { } + struct CMessage : IMessage { } + struct DMessage : IMessage { } + + [Reads(typeof(AMessage))] + [Emits(typeof(BMessage))] + class AEngine : Engine + { + public override void Update(float dt) + { + BMessage message; + this.EmitMessage(message); + } + } + + [Reads(typeof(BMessage))] + [Emits(typeof(CMessage))] + class BEngine : Engine + { + public override void Update(float dt) + { + CMessage message; + this.EmitMessage(message); + } + } + + [Reads(typeof(CMessage))] + [Emits(typeof(DMessage))] + class CEngine : Engine + { + public override void Update(float dt) + { + DMessage message; + this.EmitMessage(message); + } + } + + [Reads(typeof(DMessage))] + [Emits(typeof(AMessage))] + class DEngine : Engine + { + public override void Update(float dt) + { + AMessage message; + this.EmitMessage(message); + } + } + + [Test] + public void EngineCycle() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(); + worldBuilder.AddEngine(); + worldBuilder.AddEngine(); + worldBuilder.AddEngine(); + + Assert.Throws(() => worldBuilder.Build()); + } + } + + public class MutationConflict + { + struct AComponent : IComponent { } + + [Mutates(typeof(AComponent))] + class AEngine : Engine + { + public override void Update(float dt) { } + } + + [Mutates(typeof(AComponent))] + class BEngine : Engine + { + public override void Update(float dt) { } + } + + [Test] + public void MutationConflictException() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(); + worldBuilder.AddEngine(); + + Assert.Throws(() => worldBuilder.Build()); + } + } + + public class LegalEngines + { + static List order = new List(); + + struct AComponent : IComponent { } + struct BComponent : IComponent { } + + struct AMessage : IMessage { } + struct BMessage : IMessage { } + struct CMessage : IMessage { } + struct DMessage : IMessage { } + + [Mutates(typeof(AComponent))] + [Emits(typeof(AMessage))] + class AEngine : Engine + { + public override void Update(float dt) + { + order.Add(this); + } + } + + [Mutates(typeof(BComponent))] + [Emits(typeof(BMessage))] + class BEngine : Engine + { + public override void Update(float dt) + { + order.Add(this); + } + } + + [Reads(typeof(AMessage), typeof(BMessage))] + [Emits(typeof(DMessage))] + class CEngine : Engine + { + public override void Update(float dt) + { + order.Add(this); + } + } + + [Reads(typeof(DMessage))] + class DEngine : Engine + { + public override void Update(float dt) + { + order.Add(this); + } + } + + [Test] + public void EngineOrder() + { + var worldBuilder = new WorldBuilder(); + + var engineA = worldBuilder.AddEngine(); + var engineB = worldBuilder.AddEngine(); + var engineC = worldBuilder.AddEngine(); + var engineD = worldBuilder.AddEngine(); + + Assert.DoesNotThrow(() => worldBuilder.Build()); + + var world = worldBuilder.Build(); + + world.Update(0.01f); + + Assert.That(order.IndexOf(engineA), Is.LessThan(order.IndexOf(engineC))); + Assert.That(order.IndexOf(engineB), Is.LessThan(order.IndexOf(engineC))); + Assert.That(order.IndexOf(engineC), Is.LessThan(order.IndexOf(engineD))); + } + } + } +} diff --git a/test/test.csproj b/test/test.csproj index e69bcea..a294604 100644 --- a/test/test.csproj +++ b/test/test.csproj @@ -15,5 +15,6 @@ + \ No newline at end of file