adds some convenience methods and doc comments

pull/5/head
Evan Hemsley 2019-10-24 12:48:36 -07:00
parent 27c3fa1058
commit 85f99a565c
14 changed files with 429 additions and 20 deletions

View File

@ -187,16 +187,19 @@ namespace Encompass
internal (Guid, TComponent) ReadFirstExistingOrPendingComponentByType<TComponent>() where TComponent : struct, IComponent
{
if (!SomeExistingOrPendingComponent<TComponent>()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); }
return ReadExistingAndPendingComponentsByType<TComponent>().First();
}
internal (Guid, TComponent) ReadFirstExistingComponentByType<TComponent>() where TComponent : struct, IComponent
{
if (!SomeExistingComponent<TComponent>()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); }
return ReadExistingComponentsByType<TComponent>().First();
}
internal (Guid, TComponent) ReadFirstPendingComponentByType<TComponent>() where TComponent : struct, IComponent
{
if (!SomeExistingComponent<TComponent>()) { throw new Exceptions.NoComponentOfTypeException($"No Component with type {typeof(TComponent)} exists"); }
return ReadPendingComponentsByType<TComponent>().First();
}

View File

@ -6,6 +6,11 @@ using Encompass.Exceptions;
namespace Encompass
{
/// <summary>
/// Engines are the Encompass notion of an ECS System.
/// They are responsible for reading the World state, reading messages, emitting messages, and creating or mutating Entities and Components.
/// Engines run once per World Update.
/// </summary>
public abstract class Engine
{
internal readonly HashSet<Type> sendTypes = new HashSet<Type>();
@ -77,23 +82,42 @@ namespace Encompass
this.componentMessageManager = componentMessageManager;
}
/// <summary>
/// Runs once per World update with the calculated delta-time.
/// </summary>
/// <param name="dt">The time in seconds that has elapsed since the previous frame.</param>
public abstract void Update(double dt);
/// <summary>
/// Creates and returns a new empty Entity.
/// </summary>
protected Entity CreateEntity()
{
return entityManager.CreateEntity();
}
/// <summary>
/// Returns true if an Entity with the specified ID exists.
/// </summary>
protected bool EntityExists(Guid entityID)
{
return entityManager.EntityExists(entityID);
}
/// <summary>
/// Returns the Entity with the specified ID.
/// </summary>
/// <exception cref="Encompass.Exceptions.EntityNotFoundException">
/// Thrown when an Entity with the given ID does not exist.
/// </exception>
protected Entity GetEntity(Guid entityID)
{
return entityManager.GetEntity(entityID);
}
/// <summary>
/// Returns the Entity ID associated with the specified Component Type and ID.
/// </summary>
protected Guid GetEntityIDByComponentID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -107,11 +131,37 @@ namespace Encompass
return componentMessageManager.GetEntityIDByComponentID(componentID);
}
/// <summary>
/// Returns the Entity associated with the specified Component Type and ID.
/// </summary>
protected Entity GetEntityByComponentID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{
return GetEntity(GetEntityIDByComponentID<TComponent>(componentID));
}
/// <summary>
/// Returns an Entity containing the specified Component type.
/// </summary>
protected Entity ReadEntity<TComponent>() where TComponent : struct, IComponent
{
var (id, component) = ReadComponent<TComponent>();
return GetEntityByComponentID<TComponent>(id);
}
/// <summary>
/// Returns all Entities containing the specified Component type.
/// </summary>
protected IEnumerable<Entity> ReadEntities<TComponent>() where TComponent : struct, IComponent
{
foreach (var (id, component) in ReadComponents<TComponent>())
{
yield return GetEntityByComponentID<TComponent>(id);
}
}
/// <summary>
/// Returns the Component struct with the specified Component Type and ID.
/// </summary>
protected TComponent GetComponentByID<TComponent>(Guid componentID) where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -142,7 +192,10 @@ namespace Encompass
return GetEntity(componentManager.GetEntityIDByComponentID(componentID));
}
protected IEnumerable<ValueTuple<Guid, TComponent>> ReadComponents<TComponent>() where TComponent : struct, IComponent
/// <summary>
/// Returns all of the Components with the specified Component Type.
/// </summary>
protected IEnumerable<(Guid, TComponent)> ReadComponents<TComponent>() where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>));
@ -164,7 +217,10 @@ namespace Encompass
}
}
protected ValueTuple<Guid, TComponent> ReadComponent<TComponent>() where TComponent : struct, IComponent
/// <summary>
/// Returns a Component with the specified Component Type and ID. If multiples exist, an arbitrary Component is returned.
/// </summary>
protected (Guid, TComponent) ReadComponent<TComponent>() where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>));
@ -186,6 +242,9 @@ namespace Encompass
}
}
/// <summary>
/// Returns true if any Component with the specified Component Type exists.
/// </summary>
protected bool SomeComponent<TComponent>() where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -208,7 +267,16 @@ namespace Encompass
}
}
protected ValueTuple<Guid, TComponent> GetComponent<TComponent>(Entity entity) where TComponent : struct, IComponent
/// <summary>
/// Returns a Component with the specified Type that exists on the Entity.
/// </summary>
/// <exception cref="Encompass.Exceptions.NoComponentOfTypeOnEntityException">
/// Thrown when the Entity does not have a Component of the specified Type
/// </exception>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it reads the given Component Type.
/// </exception>
protected (Guid, TComponent) GetComponent<TComponent>(Entity entity) where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
var existingRead = receiveTypes.Contains(typeof(ComponentMessage<TComponent>));
@ -241,6 +309,12 @@ namespace Encompass
}
}
/// <summary>
/// Returns true if the Entity has a Component of the given Type.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that is Reads the given Component Type.
/// </exception>
protected bool HasComponent<TComponent>(Entity entity) where TComponent : struct, IComponent
{
var pendingRead = receiveTypes.Contains(typeof(PendingComponentMessage<TComponent>));
@ -264,6 +338,12 @@ namespace Encompass
}
}
/// <summary>
/// Sets Component data for the specified Component Type on the specified Entity. If Component data for this Type already existed on the Entity, the component data is overwritten.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalWriteException">
/// Thrown when the Engine does not declare that it Writes the given Component Type.
/// </exception>
protected Guid SetComponent<TComponent>(Entity entity, TComponent component) where TComponent : struct, IComponent
{
var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0;
@ -288,11 +368,22 @@ namespace Encompass
return componentID;
}
/// <summary>
/// Overwrites Component struct data associated with the specified Component ID.
/// </summary>
protected Guid SetComponent<TComponent>(Guid componentID, TComponent component) where TComponent : struct, IComponent
{
return SetComponent(GetEntityByComponentID<TComponent>(componentID), component);
}
/// <summary>
/// Sets Draw Component data for the specified Component Type on the specified Entity.
/// This method must be used for the Draw Component to be readable by an OrderedRenderer.
/// If Component data for this Type already existed on the Entity, the component data is overwritten.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalWriteException">
/// Thrown when the Engine does not declare that it Writes the given Component Type.
/// </exception>
protected Guid SetDrawComponent<TComponent>(Entity entity, TComponent component, int layer = 0) where TComponent : struct, IComponent, IDrawComponent
{
var priority = writePriorities.ContainsKey(typeof(TComponent)) ? writePriorities[typeof(TComponent)] : 0;
@ -317,6 +408,12 @@ namespace Encompass
return componentID;
}
/// <summary>
/// Sends a Message.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalSendException">
/// Thrown when the Engine does not declare that it Sends the Message Type.
/// </exception>
protected void SendMessage<TMessage>(TMessage message) where TMessage : struct, IMessage
{
if (!sendTypes.Contains(typeof(TMessage)))
@ -327,6 +424,10 @@ namespace Encompass
messageManager.AddMessage(message);
}
/// <summary>
/// Sends a message after the specified number of seconds.
/// </summary>
/// <param name="time">The time in seconds that will elapse before the message is sent.</param>
protected void SendMessageDelayed<TMessage>(TMessage message, double time) where TMessage : struct, IMessage
{
messageManager.AddMessageDelayed(message, time);
@ -355,6 +456,12 @@ namespace Encompass
componentMessageManager.AddPendingComponentMessage(message);
}
/// <summary>
/// Reads all messages of the specified Type.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it Receives the specified Message Type.
/// </exception>
protected IEnumerable<TMessage> ReadMessages<TMessage>() where TMessage : struct, IMessage
{
if (!receiveTypes.Contains(typeof(TMessage)))
@ -365,11 +472,23 @@ namespace Encompass
return messageManager.GetMessagesByType<TMessage>();
}
/// <summary>
/// Reads an arbitrary message of the specified Type.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it Receives the specified Message Type.
/// </exception>
protected TMessage ReadMessage<TMessage>() where TMessage : struct, IMessage
{
return ReadMessages<TMessage>().First();
}
/// <summary>
/// Returns true if a Message of the specified Type has been sent this frame.
/// </summary>
/// <exception cref="Encompass.Exceptions.IllegalReadException">
/// Thrown when the Engine does not declare that it Receives the specified Message Type.
/// </exception>
protected bool SomeMessage<TMessage>() where TMessage : struct, IMessage
{
if (!receiveTypes.Contains(typeof(TMessage)))
@ -380,11 +499,48 @@ namespace Encompass
return ReadMessages<TMessage>().Any();
}
/// <summary>
/// Destroys the Entity with the specified ID. This also removes all of the Components associated with the Entity.
/// Entity destruction takes place after all the Engines have been processed by World Update.
/// </summary>
protected void Destroy(Guid entityID)
{
entityManager.MarkForDestroy(entityID);
}
/// <summary>
/// Destroys the specified Entity. This also removes all of the Components associated with the Entity.
/// Entity destruction takes place after all the Engines have been processed by World Update.
/// </summary>
protected void Destroy(Entity entity)
{
entityManager.MarkForDestroy(entity.ID);
}
/// <summary>
/// Destroys an arbitrary Entity containing a Component of the specified Type.
/// Entity destruction takes place after all the Engines have been processed by World Update.
/// </summary>
protected void DestroyWith<TComponent>() where TComponent : struct, IComponent
{
Destroy(ReadEntity<TComponent>());
}
/// <summary>
/// Destroys all Entities containing a Component of the specified Type.
/// Entity destruction takes place after all the Engines have been processed by World Update.
/// </summary>
protected void DestroyAllWith<TComponent>() where TComponent : struct, IComponent
{
foreach (var entity in ReadEntities<TComponent>())
{
Destroy(entity);
}
}
/// <summary>
/// Removes the Component with the specified ID from its Entity.
/// </summary>
protected void RemoveComponent(Guid componentID)
{
componentManager.MarkForRemoval(componentID);

View File

@ -2,6 +2,10 @@
namespace Encompass.Engines
{
/// <summary>
/// A Spawner is a special type of Engine that runs a Spawn method in response to each Message it receives.
/// Spawners are useful for organizing the building of new Entities in your game.
/// </summary>
public abstract class Spawner<TMessage> : Engine where TMessage : struct, IMessage
{
protected Spawner() : base()

View File

@ -2,6 +2,10 @@
namespace Encompass
{
/// <summary>
/// An Entity is a structure composed of a unique ID and a collection of Components.
/// An Entity may only have a single Component of any particular Type.
/// </summary>
public struct Entity : IEquatable<Entity>
{
public readonly Guid ID;

View File

@ -0,0 +1,25 @@
using System;
using System.Runtime.Serialization;
namespace Encompass.Exceptions
{
[Serializable]
internal class NoComponentOfTypeException : Exception
{
public NoComponentOfTypeException()
{
}
public NoComponentOfTypeException(string message) : base(message)
{
}
public NoComponentOfTypeException(string message, Exception innerException) : base(message, innerException)
{
}
protected NoComponentOfTypeException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@ -1,4 +1,7 @@
namespace Encompass
{
/// <summary>
/// Structs that implement IComponent are considered to be Components.
/// </summary>
public interface IComponent { }
}

View File

@ -1,4 +1,7 @@
namespace Encompass
{
/// <summary>
/// Structs that implement IDrawComponent are considered to be DrawComponents.
/// </summary>
public interface IDrawComponent { }
}

View File

@ -1,4 +1,7 @@
namespace Encompass
{
/// <summary>
/// Structs that implement IMessage are considered to be Messages.
/// </summary>
public interface IMessage { }
}

View File

@ -1,5 +1,9 @@
namespace Encompass
{
/// <summary>
/// GeneralRenderer is a Renderer which generically reads the game state in order to draw elements to the screen.
/// GeneralRenderers have a layer specified when they are added to the World.
/// </summary>
public abstract class GeneralRenderer : Renderer
{
public abstract void Render();

View File

@ -2,6 +2,9 @@
namespace Encompass
{
/// <summary>
/// OrdereredRenderer provides a structure for the common pattern of wishing to draw a specific DrawComponent at a specific layer.
/// </summary>
public abstract class OrderedRenderer<TComponent> : Renderer where TComponent : struct, IComponent, IDrawComponent
{
public abstract void Render(Guid drawComponentID, TComponent drawComponent);

View File

@ -2,6 +2,9 @@ using System.Collections.Generic;
namespace Encompass
{
/// <summary>
/// The World is a collection of Engines, Renderers, Entities, Components, and Messages that compose the simulation.
/// </summary>
public class World
{
private readonly List<Engine> enginesInOrder;
@ -28,6 +31,10 @@ namespace Encompass
this.renderManager = renderManager;
}
/// <summary>
/// Drives the simulation. Should be called from your game engine's update loop.
/// </summary>
/// <param name="dt">The time in seconds that has passed since the previous frame.</param>
public void Update(double dt)
{
messageManager.ProcessDelayedMessages(dt);
@ -45,6 +52,9 @@ namespace Encompass
componentManager.WriteComponents();
}
/// <summary>
/// Causes the Renderers to draw.
/// </summary>
public void Draw()
{
renderManager.Draw();

View File

@ -7,6 +7,14 @@ using Encompass.Engines;
namespace Encompass
{
/// <summary>
/// WorldBuilder is used to construct a World from Engines, Renderers, and an initial state of Entities, Components, and Messages.
/// </summary>
/// <remarks>
/// WorldBuilder enforces certain rules about Engine structure. It is forbidden to have messages create cycles between Engines,
/// and no Component may be written by more than one Engine.
/// The WorldBuilder uses Engines and their Message read/emit information to determine a valid ordering of the Engines, which is given to the World.
/// </remarks>
public class WorldBuilder
{
private readonly List<Engine> engines = new List<Engine>();
@ -35,26 +43,42 @@ namespace Encompass
renderManager = new RenderManager(componentManager, drawLayerManager);
}
/// <summary>
/// Creates and returns a new empty Entity.
/// </summary>
public Entity CreateEntity()
{
return entityManager.CreateEntity();
}
/// <summary>
/// Specifies that the given Message should be sent immediately on the first World Update.
/// </summary>
public void SendMessage<TMessage>(TMessage message) where TMessage : struct, IMessage
{
messageManager.AddMessage(message);
}
/// <summary>
/// Specifies that the given Message should be sent after the specified number of seconds after the first World Update.
/// </summary>
public void SendMessageDelayed<TMessage>(TMessage message, double time) where TMessage : struct, IMessage
{
messageManager.AddMessageDelayed(message, time);
}
/// <summary>
/// Sets Component data for the specified Component Type on the specified Entity.
/// </summary>
public Guid SetComponent<TComponent>(Entity entity, TComponent component, int priority = 0) where TComponent : struct, IComponent
{
return componentManager.MarkComponentForWrite(entity, component, priority);
}
/// <summary>
/// Sets Draw Component data for the specified Component Type on the specified Entity.
/// This method must be used for the Draw Component to be readable by an OrderedRenderer.
/// </summary>
public Guid SetDrawComponent<TComponent>(Entity entity, TComponent component, int priority = 0, int layer = 0) where TComponent : struct, IComponent, IDrawComponent
{
return componentManager.MarkDrawComponentForWrite(entity, component, priority, layer);
@ -66,6 +90,10 @@ namespace Encompass
AddEngine((Engine)Activator.CreateInstance(typeof(ComponentMessageEmitter<>).MakeGenericType(componentType)));
}
/// <summary>
/// Adds the specified Engine to the World.
/// </summary>
/// <param name="engine">An instance of an Engine.</param>
public Engine AddEngine<TEngine>(TEngine engine) where TEngine : Engine
{
engine.AssignEntityManager(entityManager);
@ -126,6 +154,9 @@ namespace Encompass
return engine;
}
/// <summary>
/// Adds the specified OrderedRenderer to the World.
/// </summary>
public OrderedRenderer<TComponent> AddOrderedRenderer<TComponent>(OrderedRenderer<TComponent> renderer) where TComponent : struct, IComponent, IDrawComponent
{
renderer.AssignEntityManager(entityManager);
@ -134,6 +165,12 @@ namespace Encompass
return renderer;
}
/// <summary>
/// Adds the specified GeneralRenderer to the World at the specified layer.
/// Higher layer numbers draw on top of lower layer numbers.
/// </summary>
/// <param name="renderer">An instance of a GeneralRenderer.</param>
/// <param name="layer">The layer at which the GeneralRenderer should render. Higher numbers draw over lower numbers.</param>
public TRenderer AddGeneralRenderer<TRenderer>(TRenderer renderer, int layer) where TRenderer : GeneralRenderer
{
renderer.AssignEntityManager(entityManager);
@ -164,6 +201,11 @@ namespace Encompass
}
}
/// <summary>
/// Builds the World out of the state specified on the WorldBuilder.
/// Validates and constructs an ordering of the given Engines.
/// </summary>
/// <returns>An instance of World.</returns>
public World Build()
{
BuildEngineGraph();

View File

@ -487,6 +487,38 @@ namespace Tests
Assert.That(results, Does.Contain((componentCID, mockComponent)));
}
[Receives(typeof(DestroyComponentMessage))]
class DestroyEntityEngine : Engine
{
public override void Update(double dt)
{
foreach (var message in ReadMessages<DestroyComponentMessage>())
{
Destroy(message.entity);
}
}
}
[Test]
public void DestroyEntityWithoutID()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new AddComponentEngine());
worldBuilder.AddEngine(new DestroyEntityEngine());
worldBuilder.AddEngine(new ReaderEngine());
var mockComponent = new MockComponent { };
var entity = worldBuilder.CreateEntity();
var componentID = worldBuilder.SetComponent(entity, mockComponent);
worldBuilder.SendMessage(new DestroyComponentMessage { entity = entity });
var world = worldBuilder.Build();
world.Update(0.01);
Assert.DoesNotThrow(() => world.Update(0.01));
Assert.That(results, Does.Not.Contain((componentID, mockComponent)));
}
[Reads(typeof(DestroyerComponent), typeof(MockComponent))]
class DestroyAndAddComponentEngine : Engine
{
@ -864,7 +896,7 @@ namespace Tests
}
[Receives(typeof(DestroyComponentMessage))]
class DestroyEntityEngine : Engine
class DestroyEntityByIDEngine : Engine
{
public override void Update(double dt)
{
@ -880,7 +912,7 @@ namespace Tests
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new AddComponentEngine());
worldBuilder.AddEngine(new DestroyEntityEngine());
worldBuilder.AddEngine(new DestroyEntityByIDEngine());
var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent { });
@ -891,5 +923,122 @@ namespace Tests
Assert.DoesNotThrow(() => world.Update(0.01));
}
static Entity readEntity;
[Reads(typeof(MockComponent))]
class ReadEntityByComponentTypeEngine : Engine
{
public override void Update(double dt)
{
readEntity = ReadEntity<MockComponent>();
}
}
[Test]
public void GetEntityByComponentType()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new ReadEntityByComponentTypeEngine());
var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent { });
var world = worldBuilder.Build();
world.Update(0.01);
entity.Should().BeEquivalentTo(readEntity);
}
static Entity[] readEntities;
[Reads(typeof(MockComponent))]
class ReadEntitiesWithComponentTypeEngine : Engine
{
public override void Update(double dt)
{
readEntities = ReadEntities<MockComponent>().ToArray();
}
}
[Test]
public void ReadEntities()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new ReadEntitiesWithComponentTypeEngine());
worldBuilder.AddEngine(new DestroyAllWithEngine());
var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent { });
var entityB = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entityB, new MockComponent { });
var world = worldBuilder.Build();
world.Update(0.01);
readEntities.Should().Contain(entity);
readEntities.Should().Contain(entityB);
}
[Reads(typeof(MockComponent))]
class DestroyWithEngine : Engine
{
public override void Update(double dt)
{
if (SomeComponent<MockComponent>())
{
DestroyWith<MockComponent>();
}
}
}
[Test]
public void DestroyWith()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new ReadEntitiesWithComponentTypeEngine());
worldBuilder.AddEngine(new DestroyWithEngine());
var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent { });
var world = worldBuilder.Build();
world.Update(0.01);
world.Update(0.01); // update twice so the read happens after destroy
readEntities.Should().BeEmpty();
}
[Reads(typeof(MockComponent))]
class DestroyAllWithEngine : Engine
{
public override void Update(double dt)
{
DestroyAllWith<MockComponent>();
}
}
[Test]
public void DestroyAllWith()
{
var worldBuilder = new WorldBuilder();
worldBuilder.AddEngine(new ReadEntitiesWithComponentTypeEngine());
worldBuilder.AddEngine(new DestroyAllWithEngine());
var entity = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entity, new MockComponent { });
var entityB = worldBuilder.CreateEntity();
worldBuilder.SetComponent(entityB, new MockComponent { });
var world = worldBuilder.Build();
world.Update(0.01);
world.Update(0.01); // update twice so the read happens after destroy
readEntities.Should().BeEmpty();
}
}
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>Tests</RootNamespace>
<AssemblyName>EncompassECS.Framework.Tests</AssemblyName>