engines must declare component reads + validation for Reads and Writes arguments

pull/5/head
Evan Hemsley 2019-07-16 11:17:07 -07:00
parent 7ab512e522
commit 9b58473ae8
14 changed files with 230 additions and 77 deletions

View File

@ -74,37 +74,47 @@ namespace Encompass
protected TComponent GetComponentByID<TComponent>(Guid componentID) where TComponent : struct, IComponent protected TComponent GetComponentByID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{ {
if (!readTypes.Contains(typeof(TComponent)))
if (componentManager.GetComponentTypeByID(componentID) == typeof(TComponent))
{ {
return (TComponent)componentManager.GetComponentByID(componentID); throw new IllegalReadException("Engine {0} tried to read undeclared Component {1}", this.GetType().Name, typeof(TComponent).Name);
} }
else
if (componentManager.GetComponentTypeByID(componentID) != typeof(TComponent))
{ {
throw new ComponentTypeMismatchException("Expected Component to be of type {0} but was actually of type {1}", typeof(TComponent).Name, componentManager.GetComponentTypeByID(componentID).Name); throw new ComponentTypeMismatchException("Expected Component to be of type {0} but was actually of type {1}", typeof(TComponent).Name, componentManager.GetComponentTypeByID(componentID).Name);
} }
return (TComponent)componentManager.GetComponentByID(componentID);
} }
protected IEnumerable<ValueTuple<Guid, TComponent>> ReadComponents<TComponent>() where TComponent : struct, IComponent protected IEnumerable<ValueTuple<Guid, TComponent>> ReadComponents<TComponent>() where TComponent : struct, IComponent
{ {
if (!readTypes.Contains(typeof(TComponent)))
{
throw new IllegalReadException("Engine {0} tried to read undeclared Component {1}", this.GetType().Name, typeof(TComponent).Name);
}
return componentManager.GetActiveComponentsByType<TComponent>(); return componentManager.GetActiveComponentsByType<TComponent>();
} }
protected ValueTuple<Guid, TComponent> ReadComponent<TComponent>() where TComponent : struct, IComponent protected ValueTuple<Guid, TComponent> ReadComponent<TComponent>() where TComponent : struct, IComponent
{ {
if (!readTypes.Contains(typeof(TComponent)))
{
throw new IllegalReadException("Engine {0} tried to read undeclared Component {1}", this.GetType().Name, typeof(TComponent).Name);
}
return componentManager.GetActiveComponentByType<TComponent>(); return componentManager.GetActiveComponentByType<TComponent>();
} }
internal void UpdateComponentInWorld<TComponent>(Guid componentID, TComponent newComponent) where TComponent : struct, IComponent internal void UpdateComponentInWorld<TComponent>(Guid componentID, TComponent newComponent) where TComponent : struct, IComponent
{ {
if (writeTypes.Contains(typeof(TComponent))) if (!writeTypes.Contains(typeof(TComponent)))
{ {
componentManager.UpdateComponent(componentID, newComponent); throw new IllegalWriteException("Engine {0} tried to write undeclared Component {1}", this.GetType().Name, typeof(TComponent).Name);
}
else
{
throw new IllegalComponentMutationException("Engine {0} tried to write undeclared Component {1}", this.GetType().Name, typeof(TComponent).Name);
} }
componentManager.UpdateComponent(componentID, newComponent);
} }
protected void UpdateComponent<TComponent>(Guid componentID, TComponent newComponentValue) where TComponent : struct, IComponent protected void UpdateComponent<TComponent>(Guid componentID, TComponent newComponentValue) where TComponent : struct, IComponent
@ -114,38 +124,33 @@ namespace Encompass
protected void EmitMessage<TMessage>(TMessage message) where TMessage : struct, IMessage protected void EmitMessage<TMessage>(TMessage message) where TMessage : struct, IMessage
{ {
if (writeTypes.Contains(typeof(TMessage))) if (!writeTypes.Contains(typeof(TMessage)))
{ {
messageManager.AddMessage(message); throw new IllegalWriteException("Engine {0} tried to emit undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
}
else
{
throw new IllegalMessageEmitException("Engine {0} tried to emit undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
} }
messageManager.AddMessage(message);
} }
protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage
{ {
if (readTypes.Contains(typeof(TMessage))) if (!readTypes.Contains(typeof(TMessage)))
{ {
return messageManager.GetMessagesByType<TMessage>(); throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
}
else
{
throw new IllegalMessageReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
} }
return messageManager.GetMessagesByType<TMessage>();
} }
protected bool Some<TMessage>() where TMessage : struct, IMessage protected bool Some<TMessage>() where TMessage : struct, IMessage
{ {
if (readTypes.Contains(typeof(TMessage))) if (!readTypes.Contains(typeof(TMessage)))
{ {
return messageManager.GetMessagesByType<TMessage>().Any(); throw new IllegalReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
}
else
{
throw new IllegalMessageReadException("Engine {0} tried to read undeclared Message {1}", this.GetType().Name, typeof(TMessage).Name);
} }
return messageManager.GetMessagesByType<TMessage>().Any();
} }
protected void Destroy(Guid entityID) protected void Destroy(Guid entityID)

View File

@ -17,8 +17,8 @@ namespace Encompass
private readonly DrawLayerManager drawLayerManager; private readonly DrawLayerManager drawLayerManager;
private readonly RenderManager renderManager; private readonly RenderManager renderManager;
private readonly Dictionary<Type, HashSet<Engine>> messageTypeToEmitters = new Dictionary<Type, HashSet<Engine>>(); private readonly Dictionary<Type, HashSet<Engine>> typeToEmitters = new Dictionary<Type, HashSet<Engine>>();
private readonly Dictionary<Type, HashSet<Engine>> messageTypeToReaders = new Dictionary<Type, HashSet<Engine>>(); private readonly Dictionary<Type, HashSet<Engine>> typeToReaders = new Dictionary<Type, HashSet<Engine>>();
public WorldBuilder() public WorldBuilder()
{ {
@ -55,38 +55,58 @@ namespace Encompass
entityManager.RegisterEntityTracker(engine as IEntityTracker); entityManager.RegisterEntityTracker(engine as IEntityTracker);
} }
foreach (var emitMessageType in engine.writeTypes) foreach (var writeType in engine.writeTypes)
{ {
if (!messageTypeToEmitters.ContainsKey(emitMessageType)) if (!typeToEmitters.ContainsKey(writeType))
{ {
messageTypeToEmitters.Add(emitMessageType, new HashSet<Engine>()); typeToEmitters.Add(writeType, new HashSet<Engine>());
} }
messageTypeToEmitters[emitMessageType].Add(engine); typeToEmitters[writeType].Add(engine);
if (messageTypeToReaders.ContainsKey(emitMessageType)) if (typeToReaders.ContainsKey(writeType))
{ {
foreach (var reader in messageTypeToReaders[emitMessageType]) foreach (var reader in typeToReaders[writeType])
{ {
engineGraph.AddEdge(engine, reader); if (engine == reader)
{
if (writeType.GetInterfaces().Contains(typeof(IMessage)))
{
throw new EngineMessageSelfCycleException("Engine both reads and writes Message {0}", writeType.Name);
}
}
else
{
engineGraph.AddEdge(engine, reader);
}
} }
} }
} }
foreach (var readMessageType in engine.readTypes) foreach (var readType in engine.readTypes)
{ {
if (!messageTypeToReaders.ContainsKey(readMessageType)) if (!typeToReaders.ContainsKey(readType))
{ {
messageTypeToReaders.Add(readMessageType, new HashSet<Engine>()); typeToReaders.Add(readType, new HashSet<Engine>());
} }
messageTypeToReaders[readMessageType].Add(engine); typeToReaders[readType].Add(engine);
if (messageTypeToEmitters.ContainsKey(readMessageType)) if (typeToEmitters.ContainsKey(readType))
{ {
foreach (var emitter in messageTypeToEmitters[readMessageType]) foreach (var emitter in typeToEmitters[readType])
{ {
engineGraph.AddEdge(emitter, engine); if (emitter == engine)
{
if (readType.GetInterfaces().Contains(typeof(IMessage)))
{
throw new EngineMessageSelfCycleException("Engine both reads and writes Message {0}", readType.Name);
}
}
else
{
engineGraph.AddEdge(emitter, engine);
}
} }
} }
} }
@ -177,7 +197,7 @@ namespace Encompass
string.Join(", ", componentToEngines[componentType].Select((engine) => engine.GetType().Name)); string.Join(", ", componentToEngines[componentType].Select((engine) => engine.GetType().Name));
} }
throw new EngineMutationConflictException(errorString); throw new EngineWriteConflictException(errorString);
} }
var engineOrder = new List<Engine>(); var engineOrder = new List<Engine>();

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
namespace Encompass
{
[AttributeUsage(AttributeTargets.Class)]
public class Writes : Attribute
{
public readonly List<Type> writeTypes;
public Writes(params Type[] writeTypes)
{
this.writeTypes = new List<Type>(writeTypes);
}
}
}

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Encompass.Exceptions;
namespace Encompass namespace Encompass
{ {
@ -8,9 +10,17 @@ namespace Encompass
{ {
public readonly List<Type> readTypes; public readonly List<Type> readTypes;
public Reads(params Type[] readMessageTypes) public Reads(params Type[] readTypes)
{ {
this.readTypes = new List<Type>(readMessageTypes); foreach (var readType in readTypes)
{
if (!readType.GetInterfaces().Contains(typeof(IMessage)) && !readType.GetInterfaces().Contains(typeof(IComponent)))
{
throw new IllegalReadTypeException("{0} must be a Message or Component", readType.Name);
}
}
this.readTypes = new List<Type>(readTypes);
} }
} }
} }

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Encompass.Exceptions;
namespace Encompass
{
[AttributeUsage(AttributeTargets.Class)]
public class Writes : Attribute
{
public readonly List<Type> writeTypes;
public Writes(params Type[] writeTypes)
{
foreach (var writeType in writeTypes)
{
if (!writeType.GetInterfaces().Contains(typeof(IMessage)) && !writeType.GetInterfaces().Contains(typeof(IComponent)))
{
throw new IllegalWriteTypeException("{0} must be a Message or Component", writeType.Name);
}
}
this.writeTypes = new List<Type>(writeTypes);
}
}
}

View File

@ -2,9 +2,9 @@ using System;
namespace Encompass.Exceptions namespace Encompass.Exceptions
{ {
public class EngineMutationConflictException : Exception public class EngineMessageSelfCycleException : Exception
{ {
public EngineMutationConflictException( public EngineMessageSelfCycleException(
string format, string format,
params object[] args params object[] args
) : base(string.Format(format, args)) { } ) : base(string.Format(format, args)) { }

View File

@ -2,9 +2,9 @@ using System;
namespace Encompass.Exceptions namespace Encompass.Exceptions
{ {
public class IllegalComponentMutationException : Exception public class EngineWriteConflictException : Exception
{ {
public IllegalComponentMutationException( public EngineWriteConflictException(
string format, string format,
params object[] args params object[] args
) : base(string.Format(format, args)) { } ) : base(string.Format(format, args)) { }

View File

@ -2,9 +2,9 @@ using System;
namespace Encompass.Exceptions namespace Encompass.Exceptions
{ {
public class IllegalMessageReadException : Exception public class IllegalReadException : Exception
{ {
public IllegalMessageReadException( public IllegalReadException(
string format, string format,
params object[] args params object[] args
) : base(string.Format(format, args)) { } ) : base(string.Format(format, args)) { }

View File

@ -2,9 +2,9 @@ using System;
namespace Encompass.Exceptions namespace Encompass.Exceptions
{ {
public class IllegalMessageEmitException : Exception public class IllegalReadTypeException : Exception
{ {
public IllegalMessageEmitException( public IllegalReadTypeException(
string format, string format,
params object[] args params object[] args
) : base(string.Format(format, args)) { } ) : base(string.Format(format, args)) { }

View File

@ -0,0 +1,12 @@
using System;
namespace Encompass.Exceptions
{
public class IllegalWriteException : Exception
{
public IllegalWriteException(
string format,
params object[] args
) : base(string.Format(format, args)) { }
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace Encompass.Exceptions
{
public class IllegalWriteTypeException : Exception
{
public IllegalWriteTypeException(
string format,
params object[] args
) : base(string.Format(format, args)) { }
}
}

View File

@ -27,7 +27,7 @@ namespace Encompass
} }
protected List<T> _vertices = new List<T>(); protected List<T> _vertices = new List<T>();
protected Dictionary<T, List<T>> _neighbors = new Dictionary<T, List<T>>(); protected Dictionary<T, HashSet<T>> _neighbors = new Dictionary<T, HashSet<T>>();
public IEnumerable<T> Vertices { get { return _vertices; } } public IEnumerable<T> Vertices { get { return _vertices; } }
@ -40,7 +40,7 @@ namespace Encompass
if (!VertexExists(vertex)) if (!VertexExists(vertex))
{ {
_vertices.Add(vertex); _vertices.Add(vertex);
_neighbors.Add(vertex, new List<T>()); _neighbors.Add(vertex, new HashSet<T>());
} }
} }

View File

@ -17,6 +17,7 @@ namespace Tests
static List<MockMessage> resultMessages; static List<MockMessage> resultMessages;
[Reads(typeof(MockComponent))]
public class ReadComponentsTestEngine : Engine public class ReadComponentsTestEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -25,6 +26,7 @@ namespace Tests
} }
} }
[Reads(typeof(MockComponent))]
public class ReadComponentTestEngine : Engine public class ReadComponentTestEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -110,6 +112,7 @@ namespace Tests
Assert.Throws<InvalidOperationException>(() => world.Update(0.01f)); Assert.Throws<InvalidOperationException>(() => world.Update(0.01f));
} }
[Reads(typeof(MockComponent))]
[Writes(typeof(MockComponent))] [Writes(typeof(MockComponent))]
public class UpdateComponentTestEngine : Engine public class UpdateComponentTestEngine : Engine
{ {
@ -147,6 +150,7 @@ namespace Tests
Assert.AreEqual("blaze it", resultComponent.myString); Assert.AreEqual("blaze it", resultComponent.myString);
} }
[Reads(typeof(MockComponent))]
public class UndeclaredUpdateComponentTestEngine : Engine public class UndeclaredUpdateComponentTestEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -177,7 +181,7 @@ namespace Tests
var world = worldBuilder.Build(); var world = worldBuilder.Build();
var ex = Assert.Throws<IllegalComponentMutationException>(() => world.Update(0.01f)); var ex = Assert.Throws<IllegalWriteException>(() => world.Update(0.01f));
Assert.That(ex.Message, Is.EqualTo("Engine UndeclaredUpdateComponentTestEngine tried to write undeclared Component MockComponent")); Assert.That(ex.Message, Is.EqualTo("Engine UndeclaredUpdateComponentTestEngine tried to write undeclared Component MockComponent"));
} }
@ -263,7 +267,7 @@ namespace Tests
var world = worldBuilder.Build(); var world = worldBuilder.Build();
var ex = Assert.Throws<IllegalMessageEmitException>(() => world.Update(0.01f)); var ex = Assert.Throws<IllegalWriteException>(() => world.Update(0.01f));
Assert.That(ex.Message, Is.EqualTo("Engine UndeclaredMessageEmitEngine tried to emit undeclared Message MockMessage")); Assert.That(ex.Message, Is.EqualTo("Engine UndeclaredMessageEmitEngine tried to emit undeclared Message MockMessage"));
} }
@ -321,12 +325,13 @@ namespace Tests
var world = worldBuilder.Build(); var world = worldBuilder.Build();
Assert.Throws<IllegalMessageReadException>(() => world.Update(0.01f)); Assert.Throws<IllegalReadException>(() => world.Update(0.01f));
} }
static ValueTuple<Guid, MockComponent> pairA; static ValueTuple<Guid, MockComponent> pairA;
static ValueTuple<Guid, MockComponent> pairB; static ValueTuple<Guid, MockComponent> pairB;
[Reads(typeof(MockComponent))]
class SameValueComponentReadEngine : Engine class SameValueComponentReadEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -366,6 +371,7 @@ namespace Tests
static IEnumerable<ValueTuple<Guid, MockComponent>> emptyComponentReadResult; static IEnumerable<ValueTuple<Guid, MockComponent>> emptyComponentReadResult;
[Reads(typeof(MockComponent))]
class ReadEmptyMockComponentsEngine : Engine class ReadEmptyMockComponentsEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -388,6 +394,7 @@ namespace Tests
struct DestroyerComponent : IComponent { } struct DestroyerComponent : IComponent { }
[Reads(typeof(DestroyerComponent))]
class DestroyerEngine : Engine class DestroyerEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -402,6 +409,8 @@ namespace Tests
} }
static IEnumerable<ValueTuple<Guid, MockComponent>> results; static IEnumerable<ValueTuple<Guid, MockComponent>> results;
[Reads(typeof(MockComponent))]
class ReaderEngine : Engine class ReaderEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -439,6 +448,7 @@ namespace Tests
Assert.That(results, Does.Not.Contain((componentBID, mockComponent))); Assert.That(results, Does.Not.Contain((componentBID, mockComponent)));
} }
[Reads(typeof(DestroyerComponent))]
class DestroyAndAddComponentEngine : Engine class DestroyAndAddComponentEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -472,6 +482,8 @@ namespace Tests
} }
static Entity entityFromComponentIDResult; static Entity entityFromComponentIDResult;
[Reads(typeof(MockComponent))]
class GetEntityFromComponentIDEngine : Engine class GetEntityFromComponentIDEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -501,6 +513,8 @@ namespace Tests
} }
static MockComponent mockComponentByIDResult; static MockComponent mockComponentByIDResult;
[Reads(typeof(MockComponent))]
class GetComponentByIDEngine : Engine class GetComponentByIDEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -530,6 +544,7 @@ namespace Tests
struct OtherComponent : IComponent { } struct OtherComponent : IComponent { }
[Reads(typeof(MockComponent), typeof(OtherComponent))]
class GetComponentByIDWithTypeMismatchEngine : Engine class GetComponentByIDWithTypeMismatchEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)
@ -559,6 +574,8 @@ namespace Tests
struct EntityIDComponent : IComponent { public Guid entityID; } struct EntityIDComponent : IComponent { public Guid entityID; }
static bool hasEntity; static bool hasEntity;
[Reads(typeof(EntityIDComponent))]
class HasEntityTestEngine : Engine class HasEntityTestEngine : Engine
{ {
public override void Update(double dt) public override void Update(double dt)

View File

@ -133,7 +133,74 @@ namespace Tests
worldBuilder.AddEngine(new AEngine()); worldBuilder.AddEngine(new AEngine());
worldBuilder.AddEngine(new BEngine()); worldBuilder.AddEngine(new BEngine());
Assert.Throws<EngineMutationConflictException>(() => worldBuilder.Build()); Assert.Throws<EngineWriteConflictException>(() => worldBuilder.Build());
}
}
public class EngineMessageSelfCycle
{
struct AMessage : IMessage { }
[Reads(typeof(AMessage))]
[Writes(typeof(AMessage))]
class AEngine : Engine
{
public override void Update(double dt)
{
}
}
[Test]
public void ThrowsError()
{
var worldBuilder = new WorldBuilder();
Assert.Throws<EngineMessageSelfCycleException>(() => worldBuilder.AddEngine(new AEngine()), "Engine both reads and writes Message AMessage");
}
}
public class IllegalReadType
{
struct ANonMessage { }
[Reads(typeof(ANonMessage))]
class MyEngine : Engine
{
public override void Update(double dt)
{
}
}
[Test]
public void ThrowsError()
{
var worldBuilder = new WorldBuilder();
Assert.Throws<IllegalReadTypeException>(() => worldBuilder.AddEngine(new MyEngine()), "ANonMessage must be a Message or Component");
}
}
public class IllegalWriteType
{
struct ANonMessage { }
[Writes(typeof(ANonMessage))]
class MyEngine : Engine
{
public override void Update(double dt)
{
}
}
[Test]
public void ThrowsError()
{
var worldBuilder = new WorldBuilder();
Assert.Throws<IllegalWriteTypeException>(() => worldBuilder.AddEngine(new MyEngine()), "ANonMessage must be a Message or Component");
} }
} }