diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
new file mode 100644
index 0000000..ca60d46
--- /dev/null
+++ b/.github/workflows/doc.yml
@@ -0,0 +1,38 @@
+name: Build Docs
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build:
+ name: Build docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v2
+
+ - name: Setup Lua
+ uses: leafo/gh-actions-lua@v8
+ with:
+ luaVersion: 5.4
+
+ - name: Install Luarocks
+ uses: leafo/gh-actions-luarocks@v4
+
+ - name: Install LDoc
+ run: luarocks install ldoc
+
+ - name: Show
+ run: luarocks show ldoc
+
+ - name: Build docs
+ run: ldoc .
+
+ - name: Deploy
+ uses: JamesIves/github-pages-deploy-action@4.1.5
+ with:
+ branch: gh-pages
+ folder: docs
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 51b8439..989d364 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,7 @@ luac.out
*.app
*.i*86
*.x86_64
-*.hex
\ No newline at end of file
+*.hex
+
+# VSCode
+.vscode/
\ No newline at end of file
diff --git a/.luacheckrc b/.luacheckrc
new file mode 100644
index 0000000..59fc4be
--- /dev/null
+++ b/.luacheckrc
@@ -0,0 +1,2 @@
+---@diagnostic disable: lowercase-global
+std="love+luajit"
diff --git a/LICENSE b/LICENSE
index e3ce12d..8771a88 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,21 @@
-MIT License
-
-Copyright (c) 2018 Justin van der Leij
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+MIT License
+
+Copyright (c) 2018 Justin van der Leij
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index d2187ef..6669d8e 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,525 @@
-# Concord
-
-Concord is a feature complete ECS.
-It's main focus is on speed and usage. You should be able to quickly write code that performs well.
-
-Documentation for Concord can be found in the [Wiki tab](https://github.com/Tjakka5/Concord/wiki).
-
-Auto generated docs for Concord can be found in the [Github page](https://tjakka5.github.io/Concord/). These are still work in progress and might be incomplete though.
-
-## Installation
-Download the repository and drop it in your project, then simply require it as:
-```lua
-local Concord = require(PathToConcord).init()
-
-You will only need to call .init once when you first require it.
-```
-
-## Modules
-Below is a list of modules.
-More information about what each done can be found in the Wiki
-
-```lua
-local Concord = require("concord")
-local Entity = require("concord.entity")
-local Component = require("concord.component")
-local System = require("concord.system")
-local Instance = require("concord.instance")
-```
-
-## Contributors
-```
-Positive07: Constant support and a good rubberduck
-Brbl: Early testing and issue reporting
-Josh: Squashed a few bugs and docs
-Erasio: Took inspiration from HooECS. Also introduced me to ECS.
-```
-
-## Licence
-MIT Licensed - Copyright Justin van der Leij (Tjakka5)
+# Concord
+
+Concord is a feature complete ECS for LÖVE.
+It's main focus is performance and ease of use.
+With Concord it is possibile to easily write fast and clean code.
+
+This readme will explain how to use Concord.
+
+Additionally all of Concord is documented using the LDoc format.
+Auto generated docs for Concord can be found in `docs` folder, or on the [GitHub page](https://keyslam-group.github.io/Concord/).
+
+---
+
+## Table of Contents
+[Installation](#installation)
+[ECS](#ecs)
+[API](#api) :
+- [Components](#components)
+- [Entities](#entities)
+- [Systems](#systems)
+- [Worlds](#worlds)
+- [Assemblages](#assemblages)
+
+[Quick Example](#quick-example)
+[Contributors](#contributors)
+[License](#licence)
+
+---
+
+## Installation
+Download the repository and copy the 'concord' folder into your project. Then require it in your project like so:
+```lua
+local Concord = require("path.to.concord")
+```
+
+Concord has a bunch of modules. These can be accessed through Concord:
+
+```lua
+-- Modules
+local Entity = Concord.entity
+local Component = Concord.component
+local System = Concord.system
+local World = Concord.world
+
+-- Containers
+local Components = Concord.components
+```
+
+---
+
+## ECS
+Concord is an Entity Component System (ECS for short) library.
+This is a coding paradigm where _composition_ is used over _inheritance_.
+Because of this it is easier to write more modular code. It often allows you to combine any form of behaviour for the objects in your game (Entities).
+
+As the name might suggest, ECS consists of 3 core things: Entities, Components, and Systems. A proper understanding of these is required to use Concord effectively.
+We'll start with the simplest one.
+
+### Components
+Components are pure raw data. In Concord this is just a table with some fields.
+A position component might look like
+`{ x = 100, y = 50}`, whereas a health Component might look like `{ currentHealth = 10, maxHealth = 100 }`.
+What is most important is that Components are data and nothing more. They have 0 functionality.
+
+### Entities
+Entities are the actual objects in your game. Like a player, an enemy, a crate, or a bullet.
+Every Entity has it's own set of Components, with their own values.
+
+A crate might have the following components (Note: Not actual Concord syntax):
+```lua
+{
+ position = { x = 100, y = 200 },
+ texture = { path = "crate.png", image = Image },
+ pushable = { },
+}
+```
+
+Whereas a player might have the following components:
+```lua
+{
+ position = { x = 200, y = 300 },
+ texture = { path = "player.png", image = Image },
+ controllable = { keys = "wasd" },
+ health = { currentHealth = 10, maxHealth = 100},
+}
+```
+
+Any Component can be given to any Entity (once). Which Components an Entity has will determine how it behaves. This is done through the last thing...
+
+### Systems
+Systems are the things that actually _do_ stuff. They contain all your fancy algorithms and cool game logic.
+Each System will do one specific task like say, drawing Entities.
+For this they will only act on Entities that have the Components needed for this: `position` and `texture`. All other Components are irrelevant.
+
+In Concord this is done something alike this:
+
+```lua
+drawSystem = System({pool = {position, texture}}) -- Define a System that takes all Entities with a position and texture Component
+
+function drawSystem:draw() -- Give it a draw function
+ for _, entity in ipairs(self.pool) do -- Iterate over all Entities that this System acts on
+ local position = entity.position -- Get the position Component of this Entity
+ local texture = entity.texture -- Get the texture Component of this Entity
+
+ -- Draw the Entity
+ love.graphics.draw(texture.image, position.x, position.y)
+ end
+end
+```
+
+### To summarize...
+- Components contain only data.
+- Entities contain any set of Components.
+- Systems act on Entities that have a required set of Components.
+
+By creating Components and Systems you create modular behaviour that can apply to any Entity.
+What if we took our crate from before and gave it the `controllable` Component? It would respond to our user input of course.
+
+Or what if the enemy shot bullets with a `health` Component? It would create bullets that we'd be able to destroy by shooting them.
+
+And all that without writing a single extra line of code. Just reusing code that already existed and is guaranteed to be reuseable.
+
+---
+
+## API
+
+### General design
+
+Concord does a few things that might not be immediately clear. This segment should help understanding.
+
+#### Requiring files
+
+Since you'll have lots of Components and Systems in your game Concord makes it a bit easier to load things in.
+
+```lua
+-- Loads all files in the directory. Components automatically register into Concord.components, so loading them into a namespace isn't necessary.
+Concord.utils.loadNamespace("path/to/components")
+
+print(Concord.components.componentName)
+
+-- Loads all files in the directory, and puts the return value in the table Systems. The key is their filename without any extension
+local Systems = {}
+Concord.utils.loadNamespace("path/to/systems", Systems)
+
+myWorld:addSystems(
+ Systems.healthSystem
+ Systems.damageSystem,
+ Systems.moveSystem,
+ -- etc
+)
+```
+
+#### Method chaining
+```lua
+-- Most (if not all) methods will return self
+-- This allowes you to chain methods
+
+myEntity
+:give("position", 100, 50)
+:give("velocity", 200, 0)
+:remove("position")
+:destroy()
+
+myWorld
+:addEntity(fooEntity)
+:addEntity(barEntity)
+:clear()
+:emit("test")
+```
+
+### Components
+When defining a ComponentClass you need to pass in a name and usually a `populate` function. This will fill the Component with values.
+
+```lua
+-- Create the position class with a populate function
+-- The component variable is the actual Component given to an Entity
+-- The x and y variables are values we pass in when we create the Component
+Concord.component("position", function(component, x, y)
+ component.x = x or 0
+ component.y = y or 0
+end)
+
+-- Create a ComponentClass without a populate function
+-- Components of this type won't have any fields.
+-- This can be useful to indiciate state.
+local pushableComponentClass = Concord.component("position")
+```
+
+### Entities
+Entities can be freely made and be given Components. You pass the name of the ComponentClass and the values you want to pass. It will then create the Component for you.
+
+Entities can only have a maximum of one of each Component.
+Entities can not share Components.
+
+```lua
+-- Create a new Entity
+local myEntity = Entity()
+-- or
+local myEntity = Entity(myWorld) -- To add it to a world immediately ( See World )
+```
+
+```lua
+-- Give the entity the position Component defined above
+-- x will become 100. y will become 50
+myEntity:give("position", 100, 50)
+```
+
+```lua
+-- Retrieve a Component
+local position = myEntity.position
+
+print(position.x, position.y) -- 100, 50
+```
+
+```lua
+-- Remove a Component
+myEntity:remove("position")
+```
+
+```lua
+-- Entity:give will override a Component if the Entity already has it
+-- Entity:ensure will only put the Component if the Entity does not already have it
+
+Entity:ensure("position", 0, 0) -- Will give
+-- Position is {x = 0, y = 0}
+
+Entity:give("position", 50, 50) -- Will override
+-- Position is {x = 50, y = 50}
+
+Entity:give("position", 100, 100) -- Will override
+-- Position is {x = 100, y = 100}
+
+Entity:ensure("position", 0, 0) -- Wont do anything
+-- Position is {x = 100, y = 100}
+```
+
+```lua
+-- Retrieve all Components
+-- WARNING: Do not modify this table. It is read-only
+local allComponents = myEntity:getComponents()
+
+for ComponentClass, Component in ipairs(allComponents) do
+ -- Do stuff
+end
+```
+
+```lua
+-- Assemble the Entity ( See Assemblages )
+myEntity:assemble(assemblageFunction, 100, true, "foo")
+```
+
+```lua
+-- Check if the Entity is in a world
+local inWorld = myEntity:inWorld()
+
+-- Get the World the Entity is in
+local world = myEntity:getWorld()
+```
+
+```lua
+-- Destroy the Entity
+myEntity:destroy()
+```
+
+### Systems
+
+Systems are defined as a SystemClass. Concord will automatically create an instance of a System when it is needed.
+
+Systems get access to Entities through `pools`. They are created using a filter.
+Systems can have multiple pools.
+
+```lua
+-- Create a System
+local mySystemClass = Concord.system({pool = {"position"}}) -- Pool named 'pool' will contain all Entities with a position Component
+
+-- Create a System with multiple pools
+local mySystemClass = Concord.system({
+ pool = { -- This pool will be named 'pool'
+ "position",
+ "velocity",
+ },
+ secondPool = { -- This pool's name will be 'secondPool'
+ "health",
+ "damageable",
+ }
+})
+```
+
+```lua
+-- If a System has a :init function it will be called on creation
+
+-- world is the World the System was created for
+function mySystemClass:init(world)
+ -- Do stuff
+end
+```
+
+```lua
+-- Defining a function
+function mySystemClass:update(dt)
+ -- Iterate over all entities in the Pool
+ for _, e in ipairs(self.pool) do
+ -- Do something with the Components
+ e.position.x = e.position.x + e.velocity.x * dt
+ e.position.y = e.position.y + e.velocity.y * dt
+ end
+
+ -- Iterate over all entities in the second Pool
+ for _, e in ipairs(self.secondPool) do
+ -- Do something
+ end
+end
+```
+
+```lua
+-- Systems can be enabled and disabled
+-- When systems are disabled their callbacks won't be executed.
+-- Note that pools will still be updated
+-- This is mainly useful for systems that display debug information
+-- Systems are enabled by default
+
+-- Enable a System
+mySystem:setEnable(true)
+
+-- Disable a System
+mySystem:setEnable(false)
+
+-- Get enabled state
+local isEnabled = mySystem:isEnabled()
+print(isEnabled) -- false
+```
+
+```lua
+-- Get the World the System is in
+local world = System:getWorld()
+```
+
+### Worlds
+
+Worlds are the thing your System and Entities live in.
+With Worlds you can `:emit` a callback. All Systems with this callback will then be called.
+
+Worlds can have 1 instance of every SystemClass.
+Worlds can have any number of Entities.
+
+```lua
+-- Create World
+local myWorld = Concord.world()
+```
+
+```lua
+-- Add an Entity to the World
+myWorld:addEntity(myEntity)
+
+-- Remove an Entity from the World
+myWorld:removeEntity(myEntity)
+```
+
+```lua
+-- Add a System to the World
+myWorld:addSystem(mySystemClass)
+
+-- Add multiple Systems to the World
+myWorld:addSystems(moveSystemClass, renderSystemClass, controlSystemClass)
+```
+
+```lua
+-- Check if the World has a System
+local hasSystem = myWorld:hasSystem(mySystemClass)
+
+-- Get a System from the World
+local mySystem = myWorld:getSystem(mySystemClass)
+```
+
+```lua
+-- Emit an event
+
+-- This will call the 'update' function of all added Systems if they have one
+-- They will be called in the order they were added
+myWorld:emit("update", dt)
+
+-- You can emit any event with any parameters
+myWorld:emit("customCallback", 100, true, "Hello World")
+```
+
+```lua
+-- Remove all Entities from the World
+myWorld:clear()
+```
+
+```lua
+-- Override-able callbacks
+
+-- Called when an Entity is added to the World
+-- e is the Entity added
+function myWorld:onEntityAdded(e)
+ -- Do something
+end
+
+-- Called when an Entity is removed from the World
+-- e is the Entity removed
+function myWorld:onEntityRemoved(e)
+ -- Do something
+end
+```
+
+### Assemblages
+
+Assemblages are functions to 'make' Entities something.
+An important distinction is that they _append_ Components.
+
+```lua
+-- Make an Assemblage function
+-- e is the Entity being assembled.
+-- cuteness and legs are variables passed in
+function animal(e, cuteness, legs)
+ e
+ :give(cutenessComponentClass, cuteness)
+ :give(limbs, legs, 0) -- Variable amount of legs. 0 arm.
+end)
+
+-- Make an Assemblage that uses animal
+-- cuteness is a variables passed in
+function cat(e, cuteness)
+ e
+ :assemble(animal, cuteness * 2, 4) -- Cats are twice as cute, and have 4 legs.
+ :give(soundComponent, "meow.mp3")
+end)
+```
+
+```lua
+-- Use an Assemblage
+myEntity:assemble(cat, 100) -- 100 cuteness
+```
+
+---
+
+## Quick Example
+```lua
+local Concord = require("concord")
+
+-- Defining components
+Concord.component("position", function(c, x, y)
+ c.x = x or 0
+ c.y = y or 0
+end)
+
+Concord.component("velocity", function(c, x, y)
+ c.x = x or 0
+ c.y = y or 0
+end)
+
+local Drawable = Concord.component("drawable")
+
+
+-- Defining Systems
+local MoveSystem = Concord.system({
+ pool = {"position", "velocity"}
+})
+
+function MoveSystem:update(dt)
+ for _, e in ipairs(self.pool) do
+ e.position.x = e.position.x + e.velocity.x * dt
+ e.position.y = e.position.y + e.velocity.y * dt
+ end
+end
+
+
+local DrawSystem = Concord.system({
+ pool = {"position", "drawable"}
+})
+
+function DrawSystem:draw()
+ for _, e in ipairs(self.pool) do
+ love.graphics.circle("fill", e.position.x, e.position.y, 5)
+ end
+end
+
+
+-- Create the World
+local world = Concord.world()
+
+-- Add the Systems
+world:addSystems(MoveSystem, DrawSystem)
+
+-- This Entity will be rendered on the screen, and move to the right at 100 pixels a second
+local entity_1 = Concord.entity(world)
+:give("position", 100, 100)
+:give("velocity", 100, 0)
+:give("drawable")
+
+-- This Entity will be rendered on the screen, and stay at 50, 50
+local entity_2 = Concord.entity(world)
+:give("position", 50, 50)
+:give("drawable")
+
+-- This Entity does exist in the World, but since it doesn't match any System's filters it won't do anything
+local entity_3 = Concord.entity(world)
+:give("position", 200, 200)
+
+
+-- Emit the events
+function love.update(dt)
+ world:emit("update", dt)
+end
+
+function love.draw()
+ world:emit("draw")
+end
+```
+
+---
+
+## Contributors
+- __Positive07__: Constant support and a good rubberduck
+- __Brbl__: Early testing and issue reporting
+- __Josh__: Squashed a few bugs and generated docs
+- __Erasio__: I took inspiration from HooECS. He also introduced me to ECS
+- __Speak__: Lots of testing for new features of Concord
+- __Tesselode__: Brainstorming and helpful support
+
+---
+
+## License
+MIT Licensed - Copyright Justin van der Leij (Tjakka5)
diff --git a/concord/builtins/init.lua b/concord/builtins/init.lua
new file mode 100644
index 0000000..e3ab37b
--- /dev/null
+++ b/concord/builtins/init.lua
@@ -0,0 +1,6 @@
+local PATH = (...):gsub("(%.init)$", "")
+
+return {
+ serializable = require(PATH..".serializable"),
+ key = require(PATH..".key"),
+}
diff --git a/concord/builtins/key.lua b/concord/builtins/key.lua
new file mode 100644
index 0000000..7b23275
--- /dev/null
+++ b/concord/builtins/key.lua
@@ -0,0 +1,41 @@
+local PATH = (...):gsub('%.builtins%.[^%.]+$', '')
+
+local Component = require(PATH..".component")
+
+local getKey = function (self, key)
+ local entity = self.__entity
+
+ if not entity:inWorld() then
+ error("entity needs to belong to a world")
+ end
+
+ local world = entity:getWorld()
+
+ return world:__assignKey(entity, key)
+end
+
+local Key = Component("key", function (self, key)
+ self.value = getKey(self, key)
+end)
+
+function Key:deserialize (data)
+ self.value = getKey(self, data)
+end
+
+function Key.__mt:__call()
+ return self.value
+end
+
+function Key:removed (replaced)
+ if not replaced then
+ local entity = self.__entity
+
+ if entity:inWorld() then
+ local world = entity:getWorld()
+
+ return world:__clearKey(entity)
+ end
+ end
+end
+
+return Key
diff --git a/concord/builtins/serializable.lua b/concord/builtins/serializable.lua
new file mode 100644
index 0000000..125f76b
--- /dev/null
+++ b/concord/builtins/serializable.lua
@@ -0,0 +1,12 @@
+local PATH = (...):gsub('%.builtins%.[^%.]+$', '')
+
+local Component = require(PATH..".component")
+
+local Serializable = Component("serializable")
+
+function Serializable:serialize ()
+ -- Don't serialize this Component
+ return nil
+end
+
+return Serializable
\ No newline at end of file
diff --git a/concord/component.lua b/concord/component.lua
new file mode 100644
index 0000000..981b999
--- /dev/null
+++ b/concord/component.lua
@@ -0,0 +1,120 @@
+--- A pure data container that is contained by a single entity.
+-- @classmod Component
+
+local PATH = (...):gsub('%.[^%.]+$', '')
+
+local Components = require(PATH..".components")
+local Utils = require(PATH..".utils")
+
+local Component = {}
+Component.__mt = {
+ __index = Component,
+}
+
+--- Creates a new ComponentClass.
+-- @tparam function populate Function that populates a Component with values
+-- @treturn Component A new ComponentClass
+function Component.new(name, populate)
+ if (type(name) ~= "string") then
+ Utils.error(2, "bad argument #1 to 'Component.new' (string expected, got %s)", type(name))
+ end
+
+ if (string.match(name, Components.__REJECT_MATCH) ~= "") then
+ Utils.error(2, "bad argument #1 to 'Component.new' (Component names can't start with '%s', got %s)", Components.__REJECT_PREFIX, name)
+ end
+
+ if (rawget(Components, name)) then
+ Utils.error(2, "bad argument #1 to 'Component.new' (ComponentClass with name '%s' was already registerd)", name) -- luacheck: ignore
+ end
+
+ if (type(populate) ~= "function" and type(populate) ~= "nil") then
+ Utils.error(2, "bad argument #1 to 'Component.new' (function/nil expected, got %s)", type(populate))
+ end
+
+ local componentClass = setmetatable({
+ __populate = populate,
+
+ __name = name,
+ __isComponentClass = true,
+ }, Component.__mt)
+
+ componentClass.__mt = {
+ __index = componentClass
+ }
+
+ Components[name] = componentClass
+
+ return componentClass
+end
+
+-- Internal: Populates a Component with values.
+function Component:__populate() -- luacheck: ignore
+end
+
+-- Callback: When the Component gets removed or replaced in an Entity.
+function Component:removed() -- luacheck: ignore
+end
+
+-- Callback: When the Component gets serialized as part of an Entity.
+function Component:serialize()
+ local data = Utils.shallowCopy(self, {})
+
+ --This values shouldn't be copied over
+ data.__componentClass = nil
+ data.__entity = nil
+ data.__isComponent = nil
+ data.__isComponentClass = nil
+
+ return data
+end
+
+-- Callback: When the Component gets deserialized from serialized data.
+function Component:deserialize(data)
+ Utils.shallowCopy(data, self)
+end
+
+-- Internal: Creates a new Component.
+-- @param entity The Entity that will receive this Component.
+-- @return A new Component
+function Component:__new(entity)
+ local component = setmetatable({
+ __componentClass = self,
+
+ __entity = entity,
+ __isComponent = true,
+ __isComponentClass = false,
+ }, self.__mt)
+
+ return component
+end
+
+-- Internal: Creates and populates a new Component.
+-- @param entity The Entity that will receive this Component.
+-- @param ... Varargs passed to the populate function
+-- @return A new populated Component
+function Component:__initialize(entity, ...)
+ local component = self:__new(entity)
+
+ ---@diagnostic disable-next-line: redundant-parameter
+ self.__populate(component, ...)
+
+ return component
+end
+
+--- Returns true if the Component has a name.
+-- @treturn boolean
+function Component:hasName()
+ return self.__name and true or false
+end
+
+--- Returns the name of the Component.
+-- @treturn string
+function Component:getName()
+ return self.__name
+end
+
+return setmetatable(Component, {
+ __call = function(_, ...)
+ return Component.new(...)
+ end,
+})
diff --git a/concord/components.lua b/concord/components.lua
new file mode 100644
index 0000000..b2743e7
--- /dev/null
+++ b/concord/components.lua
@@ -0,0 +1,73 @@
+--- Container for registered ComponentClasses
+-- @module Components
+
+local Components = {}
+
+Components.__REJECT_PREFIX = "!"
+Components.__REJECT_MATCH = "^(%"..Components.__REJECT_PREFIX.."?)(.+)"
+
+--- Returns true if the containter has the ComponentClass with the specified name
+-- @string name Name of the ComponentClass to check
+-- @treturn boolean
+function Components.has(name)
+ return rawget(Components, name) and true or false
+end
+
+--- Prefix a component's name with the currently set Reject Prefix
+-- @string name Name of the ComponentClass to reject
+-- @treturn string
+function Components.reject(name)
+ local ok, err = Components.try(name)
+
+ if not ok then error(err, 2) end
+
+ return Components.__REJECT_PREFIX..name
+end
+
+--- Returns true and the ComponentClass if one was registered with the specified name
+-- or false and an error otherwise
+-- @string name Name of the ComponentClass to check
+-- @boolean acceptRejected Whether to accept names prefixed with the Reject Prefix.
+-- @treturn boolean
+-- @treturn Component or error string
+-- @treturn true if acceptRejected was true and the name had the Reject Prefix, false otherwise.
+function Components.try(name, acceptRejected)
+ if type(name) ~= "string" then
+ return false, "ComponentsClass name is expected to be a string, got "..type(name)..")"
+ end
+
+ local rejected = false
+ if acceptRejected then
+ local prefix
+ prefix, name = string.match(name, Components.__REJECT_MATCH)
+
+ rejected = prefix ~= "" and name
+ end
+
+ local value = rawget(Components, name)
+ if not value then
+ return false, "ComponentClass '"..name.."' does not exist / was not registered"
+ end
+
+ return true, value, rejected
+end
+
+--- Returns the ComponentClass with the specified name
+-- @string name Name of the ComponentClass to get
+-- @treturn Component
+function Components.get(name)
+ local ok, value = Components.try(name)
+
+ if not ok then error(value, 2) end
+
+ return value
+end
+
+return setmetatable(Components, {
+ __index = function(_, name)
+ local ok, value = Components.try(name)
+
+ if not ok then error(value, 2) end
+
+ return value end
+})
diff --git a/concord/entity.lua b/concord/entity.lua
new file mode 100644
index 0000000..29145fa
--- /dev/null
+++ b/concord/entity.lua
@@ -0,0 +1,281 @@
+--- An object that exists in a world. An entity
+-- contains components which are processed by systems.
+-- @classmod Entity
+
+local PATH = (...):gsub('%.[^%.]+$', '')
+
+local Components = require(PATH..".components")
+local Type = require(PATH..".type")
+local Utils = require(PATH..".utils")
+
+-- Initialize built-in Components (as soon as possible)
+local Builtins = require(PATH..".builtins.init") --luacheck: ignore
+-- Builtins is unused but the require already registers the Components
+
+local Entity = {
+ SERIALIZE_BY_DEFAULT = true,
+}
+
+Entity.__mt = {
+ __index = Entity,
+}
+
+--- Creates a new Entity. Optionally adds it to a World.
+-- @tparam[opt] World world World to add the entity to
+-- @treturn Entity A new Entity
+function Entity.new(world)
+ if (world ~= nil and not Type.isWorld(world)) then
+ Utils.error(2, "bad argument #1 to 'Entity.new' (world/nil expected, got %s)", type(world))
+ end
+
+ local e = setmetatable({
+ __world = nil,
+
+ __isEntity = true,
+ }, Entity.__mt)
+
+ if (world) then
+ world:addEntity(e)
+ end
+
+ if Entity.SERIALIZE_BY_DEFAULT then
+ e:give("serializable")
+ end
+
+ return e
+end
+
+local function createComponent(e, name, componentClass, ...)
+ local component = componentClass:__initialize(e, ...)
+ local hadComponent = not not e[name]
+
+ if hadComponent then
+ e[name]:removed(true)
+ end
+
+ e[name] = component
+
+ if not hadComponent then
+ e:__dirty()
+ end
+end
+
+local function deserializeComponent(e, name, componentData)
+ local componentClass = Components[name]
+ local hadComponent = not not e[name]
+
+ if hadComponent then
+ e[name]:removed(true)
+ end
+
+ local component = componentClass:__new(e)
+ component:deserialize(componentData)
+
+ e[name] = component
+
+ if not hadComponent then
+ e:__dirty()
+ end
+end
+
+local function giveComponent(e, ensure, name, ...)
+ local component
+ if Type.isComponent(name) then
+ component = name
+ name = component:getName()
+ end
+
+ if ensure and e[name] then
+ return e
+ end
+
+ local ok, componentClass = Components.try(name)
+
+ if not ok then
+ Utils.error(3, "bad argument #1 to 'Entity:%s' (%s)", ensure and 'ensure' or 'give', componentClass)
+ end
+
+ if component then
+ local data = component:deserialize()
+ if data == nil then
+ Utils.error(3, "bad argument #1 to 'Entity:$s' (Component '%s' couldn't be deserialized)", ensure and 'ensure' or 'give', name)
+ end
+
+ deserializeComponent(e, name, data)
+ else
+ createComponent(e, name, componentClass, ...)
+ end
+
+ return e
+end
+
+
+local function removeComponent(e, name)
+ if e[name] then
+ e[name]:removed(false)
+
+ e[name] = nil
+
+ e:__dirty()
+ end
+end
+
+--- Gives an Entity a Component.
+-- If the Component already exists, it's overridden by this new Component
+-- @tparam Component componentClass ComponentClass to add an instance of
+-- @param ... additional arguments to pass to the Component's populate function
+-- @treturn Entity self
+function Entity:give(name, ...)
+ return giveComponent(self, false, name, ...)
+end
+
+--- Ensures an Entity to have a Component.
+-- If the Component already exists, no action is taken
+-- @tparam Component componentClass ComponentClass to add an instance of
+-- @param ... additional arguments to pass to the Component's populate function
+-- @treturn Entity self
+function Entity:ensure(name, ...)
+ return giveComponent(self, true, name, ...)
+end
+
+--- Removes a Component from an Entity.
+-- @tparam Component componentClass ComponentClass of the Component to remove
+-- @treturn Entity self
+function Entity:remove(name)
+ local ok, componentClass = Components.try(name)
+
+ if not ok then
+ Utils.error(2, "bad argument #1 to 'Entity:remove' (%s)", componentClass)
+ end
+
+ removeComponent(self, name)
+
+ return self
+end
+
+--- Assembles an Entity.
+-- @tparam function assemblage Function that will assemble an entity
+-- @param ... additional arguments to pass to the assemblage function.
+-- @treturn Entity self
+function Entity:assemble(assemblage, ...)
+ if type(assemblage) ~= "function" then
+ Utils.error(2, "bad argument #1 to 'Entity:assemble' (function expected, got %s)", type(assemblage))
+ end
+
+ assemblage(self, ...)
+
+ return self
+end
+
+--- Destroys the Entity.
+-- Removes the Entity from its World if it's in one.
+-- @return self
+function Entity:destroy()
+ if self.__world then
+ self.__world:removeEntity(self)
+ end
+
+ return self
+end
+
+-- Internal: Tells the World it's in that this Entity is dirty.
+-- @return self
+function Entity:__dirty()
+ if self.__world then
+ self.__world:__dirtyEntity(self)
+ end
+
+ return self
+end
+
+--- Returns true if the Entity has a Component.
+-- @tparam Component componentClass ComponentClass of the Component to check
+-- @treturn boolean
+function Entity:has(name)
+ local ok, componentClass = Components.try(name)
+
+ if not ok then
+ Utils.error(2, "bad argument #1 to 'Entity:has' (%s)", componentClass)
+ end
+
+ return self[name] and true or false
+end
+
+--- Gets a Component from the Entity.
+-- @tparam Component componentClass ComponentClass of the Component to get
+-- @treturn table
+function Entity:get(name)
+ local ok, componentClass = Components.try(name)
+
+ if not ok then
+ Utils.error(2, "bad argument #1 to 'Entity:get' (%s)", componentClass)
+ end
+
+ return self[name]
+end
+
+--- Returns a table of all Components the Entity has.
+-- Warning: Do not modify this table.
+-- Use Entity:give/ensure/remove instead
+-- @treturn table Table of all Components the Entity has
+function Entity:getComponents(output)
+ output = output or {}
+ local components = Utils.shallowCopy(self, output)
+ components.__world = nil
+ components.__isEntity = nil
+
+ return components
+end
+
+--- Returns true if the Entity is in a World.
+-- @treturn boolean
+function Entity:inWorld()
+ return self.__world and true or false
+end
+
+--- Returns the World the Entity is in.
+-- @treturn World
+function Entity:getWorld()
+ return self.__world
+end
+
+function Entity:serialize(ignoreKey)
+ local data = {}
+
+ for name, component in pairs(self) do
+ -- The key component needs to be treated separately.
+ if name == "key" and component.__name == "key" then
+ if not ignoreKey then
+ data.key = component.value
+ end
+ --We only care about components that were properly given to the entity
+ elseif Type.isComponent(component) and (component.__name == name) then
+ local componentData = component:serialize()
+
+ if componentData ~= nil then
+ componentData.__name = component.__name
+ data[#data + 1] = componentData
+ end
+ end
+ end
+
+ return data
+end
+
+function Entity:deserialize(data)
+ for i = 1, #data do
+ local componentData = data[i]
+
+ if (not Components.has(componentData.__name)) then
+ Utils.error(2, "bad argument #1 to 'Entity:deserialize' (ComponentClass '%s' wasn't yet loaded)", tostring(componentData.__name)) -- luacheck: ignore
+ end
+
+ deserializeComponent(self, componentData.__name, componentData)
+ end
+end
+
+return setmetatable(Entity, {
+ __call = function(_, ...)
+ return Entity.new(...)
+ end,
+})
diff --git a/concord/filter.lua b/concord/filter.lua
new file mode 100644
index 0000000..0c4dabf
--- /dev/null
+++ b/concord/filter.lua
@@ -0,0 +1,199 @@
+--- Used to filter Entities with specific Components
+-- A Filter has an associated Pool that can contain any amount of Entities.
+-- @classmod Filter
+
+local PATH = (...):gsub('%.[^%.]+$', '')
+
+local List = require(PATH..".list")
+local Type = require(PATH..".type")
+local Utils = require(PATH..".utils")
+local Components = require(PATH..".components")
+
+
+local Filter = {}
+Filter.__mt = {
+ __index = Filter,
+}
+
+--- Validates a Filter Definition to make sure every component is valid.
+-- @string name Name for the Filter.
+-- @tparam table definition Table containing the Filter Definition
+-- @tparam onComponent Optional function, called when a component is valid.
+function Filter.validate (errorLevel, name, def, onComponent)
+ local filter = "World:query filter"
+ if name then
+ filter = ("filter '%s'"):format(name)
+ end
+
+ if type(def) ~= 'table' then
+ Utils.error(3 + errorLevel, "invalid component list for %s (table expected, got %s)", filter, type(def))
+ end
+
+ if not onComponent and def.constructor and not Type.isCallable(def.constructor) then
+ Utils.error(3 + errorLevel, "invalid pool constructor for %s (callable expected, got %s)", filter, type(def.constructor))
+ end
+
+ for n, component in ipairs(def) do
+ local ok, err, reject = Components.try(component, true)
+
+ if not ok then
+ Utils.error(3 + errorLevel, "invalid component for %s at position #%d (%s)", filter, n, err)
+ end
+
+ if onComponent then
+ onComponent(component, reject)
+ end
+ end
+end
+
+--- Parses the Filter Defintion into two tables
+-- required: An array of all the required component names.
+-- rejected: An array of all the components that will be rejected.
+-- @string name Name for the Filter.
+-- @tparam table definition Table containing the Filter Definition
+-- @treturn table required
+-- @treturn table rejected
+function Filter.parse (name, def)
+ local filter = {}
+
+ Filter.validate(1, name, def, function (component, reject)
+ if reject then
+ table.insert(filter, reject)
+ table.insert(filter, false)
+ else
+ table.insert(filter, component)
+ table.insert(filter, true)
+ end
+ end)
+
+ return filter
+end
+
+function Filter.match (e, filter)
+ for i=#filter, 2, -2 do
+ local match = filter[i - 0]
+ local name = filter[i - 1]
+
+ if (not e[name]) == match then return false end
+ end
+
+ return true
+end
+
+local REQUIRED_METHODS = {"add", "remove", "has", "clear"}
+local VALID_POOL_TYPES = {table=true, userdata=true, lightuserdata=true, cdata=true}
+
+function Filter.isValidPool (name, pool)
+ local poolType = type(pool)
+ --Check that pool is not nil
+ if not VALID_POOL_TYPES[poolType] then
+ Utils.error(3, "invalid value returned by pool '%s' constructor (table expected, got %s).", name, type(pool))
+ end
+
+ --Check if methods are callables
+ for _, method in ipairs(REQUIRED_METHODS) do
+ if not Type.isCallable(pool[method]) then
+ Utils.error(3, "invalid :%s method on pool '%s' (callable expected, got %s).", method, name, type(pool[method]))
+ end
+ end
+end
+
+--- Creates a new Filter
+-- @string name Name for the Filter.
+-- @tparam table definition Table containing the Filter Definition
+-- @treturn Filter The new Filter
+-- @treturn Pool The associated Pool
+function Filter.new (name, def)
+ local pool
+
+ if def.constructor then
+ pool = def.constructor(def)
+ Filter.isValidPool(name, pool)
+ else
+ pool = List()
+ end
+
+ local filter = Filter.parse(name, def)
+
+ local filter = setmetatable({
+ pool = pool,
+
+ __filter = filter,
+ __name = name,
+
+ __isFilter = true,
+ }, Filter.__mt)
+
+ return filter, pool
+end
+
+--- Checks if an Entity fulfills the Filter requirements.
+-- @tparam Entity e Entity to check
+-- @treturn boolean
+function Filter:eligible(e)
+ return Filter.match(e, self.__filter)
+end
+
+function Filter:evaluate (e)
+ local has = self.pool:has(e)
+ local eligible = self:eligible(e)
+
+ if not has and eligible then
+ self.pool:add(e)
+ elseif has and not eligible then
+ self.pool:remove(e)
+ end
+
+ return self
+end
+
+
+-- Adds an Entity to the Pool, if it passes the Filter.
+-- @param e Entity to add
+-- @param bypass Whether to bypass the Filter or not.
+-- @treturn Filter self
+-- @treturn boolean Whether the entity was added or not.
+function Filter:add (e, bypass)
+ if not bypass and not self:eligible(e) then
+ return self, false
+ end
+
+ self.pool:add(e)
+
+ return self, true
+end
+
+-- Remove an Entity from the Pool associated to this Filter.
+-- @param e Entity to remove
+-- @treturn Filter self
+function Filter:remove (e)
+ self.pool:remove(e)
+ return self
+end
+
+-- Clear the Pool associated to this Filter.
+-- @param e Entity to remove
+-- @treturn Filter self
+function Filter:clear (e)
+ self.pool:clear(e)
+ return self
+end
+
+-- Check if the Pool bound to this System contains the passed Entity
+-- @param e Entity to check
+-- @treturn boolean Whether the Entity exists.
+function Filter:has (e)
+ return self.pool:has(e)
+end
+
+--- Gets the name of the Filter
+-- @treturn string
+function Filter:getName()
+ return self.__name
+end
+
+return setmetatable(Filter, {
+ __call = function(_, ...)
+ return Filter.new(...)
+ end,
+})
\ No newline at end of file
diff --git a/concord/init.lua b/concord/init.lua
new file mode 100644
index 0000000..1c09669
--- /dev/null
+++ b/concord/init.lua
@@ -0,0 +1,42 @@
+---
+-- @module Concord
+
+local PATH = (...):gsub('%.init$', '')
+
+local Concord = {
+ _VERSION = "3.0",
+ _DESCRIPTION = "A feature-complete ECS library",
+ _LICENCE = [[
+ MIT LICENSE
+
+ Copyright (c) 2020 Justin van der Leij / Tjakka5
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ]]
+}
+
+Concord.entity = require(PATH..".entity")
+Concord.component = require(PATH..".component")
+Concord.components = require(PATH..".components")
+Concord.system = require(PATH..".system")
+Concord.world = require(PATH..".world")
+Concord.utils = require(PATH..".utils")
+
+return Concord
diff --git a/concord/list.lua b/concord/list.lua
new file mode 100644
index 0000000..7f10c74
--- /dev/null
+++ b/concord/list.lua
@@ -0,0 +1,127 @@
+--- Data structure that allows for fast removal at the cost of containing order.
+-- @classmod List
+
+local List = {}
+List.__mt = {
+ __index = List
+}
+
+--- Creates a new List.
+-- @treturn List A new List
+function List.new()
+ return setmetatable({
+ size = 0,
+ }, List.__mt)
+end
+
+--- Adds an object to the List.
+-- Object must be of reference type
+-- Object may not be the string 'size', 'onAdded' or 'onRemoved'
+-- @param obj Object to add
+-- @treturn List self
+function List:add(obj)
+ local size = self.size + 1
+
+ self[size] = obj
+ self[obj] = size
+ self.size = size
+
+ if self.onAdded then self:onAdded(obj) end
+ return self
+end
+
+--- Removes an object from the List.
+-- @param obj Object to remove
+-- @treturn List self
+function List:remove(obj)
+ local index = self[obj]
+ if not index then return end
+ local size = self.size
+
+ if index == size then
+ self[size] = nil
+ else
+ local other = self[size]
+
+ self[index] = other
+ self[other] = index
+
+ self[size] = nil
+ end
+
+ self[obj] = nil
+ self.size = size - 1
+
+ if self.onRemoved then self:onRemoved(obj) end
+ return self
+end
+
+--- Clears the List completely.
+-- @treturn List self
+function List:clear()
+ for i = 1, self.size do
+ local o = self[i]
+
+ self[o] = nil
+ self[i] = nil
+ end
+
+ self.size = 0
+
+ return self
+end
+
+--- Returns true if the List has the object.
+-- @param obj Object to check for
+-- @treturn boolean
+function List:has(obj)
+ return self[obj] and true or false
+end
+
+--- Returns the object at an index.
+-- @number i Index to get from
+-- @return Object at the index
+function List:get(i)
+ return self[i]
+end
+
+--- Returns the index of an object in the List.
+-- @param obj Object to get index of
+-- @treturn number index of object in the List.
+function List:indexOf(obj)
+ if (not self[obj]) then
+ error("bad argument #1 to 'List:indexOf' (Object was not in List)", 2)
+ end
+
+ return self[obj]
+end
+
+--- Sorts the List in place, using the order function.
+-- The order function is passed to table.sort internally so documentation on table.sort can be used as reference.
+-- @param order Function that takes two Entities (a and b) and returns true if a should go before than b.
+-- @treturn List self
+function List:sort(order)
+ table.sort(self, order)
+
+ for key, obj in ipairs(self) do
+ self[obj] = key
+ end
+
+ return self
+end
+
+--- Callback for when an item is added to the List.
+-- @param obj Object that was added
+function List:onAdded (obj) --luacheck: ignore
+end
+
+--- Callback for when an item is removed to the List.
+-- @param obj Object that was removed
+function List:onRemoved (obj) --luacheck: ignore
+end
+
+return setmetatable(List, {
+ __call = function()
+ return List.new()
+ end,
+})
diff --git a/concord/system.lua b/concord/system.lua
new file mode 100644
index 0000000..e9d45d6
--- /dev/null
+++ b/concord/system.lua
@@ -0,0 +1,159 @@
+--- Iterates over Entities. From these Entities its get Components and modify them.
+-- A System contains 1 or more Pools.
+-- A System is contained by 1 World.
+-- @classmod System
+
+local PATH = (...):gsub('%.[^%.]+$', '')
+
+local Filter = require(PATH..".filter")
+local Utils = require(PATH..".utils")
+
+local System = {
+ ENABLE_OPTIMIZATION = true,
+}
+
+System.mt = {
+ __index = System,
+ __call = function(systemClass, world)
+ local system = setmetatable({
+ __enabled = true,
+
+ __filters = {},
+ __world = world,
+
+ __isSystem = true,
+ __isSystemClass = false, -- Overwrite value from systemClass
+ }, systemClass)
+
+ -- Optimization: We deep copy the System class into our instance of a system.
+ -- This grants slightly faster access times at the cost of memory.
+ -- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff
+ if (System.ENABLE_OPTIMIZATION) then
+ Utils.shallowCopy(systemClass, system)
+ end
+
+ for name, def in pairs(systemClass.__definition) do
+ local filter, pool = Filter(name, Utils.shallowCopy(def, {}))
+
+ system[name] = pool
+ table.insert(system.__filters, filter)
+ end
+
+ system:init(world)
+
+ return system
+ end,
+}
+--- Creates a new SystemClass.
+-- @param table filters A table containing filters (name = {components...})
+-- @treturn System A new SystemClass
+function System.new(definition)
+ definition = definition or {}
+
+ for name, def in pairs(definition) do
+ if type(name) ~= 'string' then
+ Utils.error(2, "invalid name for filter (string key expected, got %s)", type(name))
+ end
+
+ Filter.validate(0, name, def)
+ end
+
+ local systemClass = setmetatable({
+ __definition = definition,
+
+ __isSystemClass = true,
+ }, System.mt)
+ systemClass.__index = systemClass
+
+ -- Optimization: We deep copy the World class into our instance of a world.
+ -- This grants slightly faster access times at the cost of memory.
+ -- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff
+ if (System.ENABLE_OPTIMIZATION) then
+ Utils.shallowCopy(System, systemClass)
+ end
+
+ return systemClass
+end
+
+-- Internal: Evaluates an Entity for all the System's Pools.
+-- @param e The Entity to check
+-- @treturn System self
+function System:__evaluate(e)
+ for _, filter in ipairs(self.__filters) do
+ filter:evaluate(e)
+ end
+
+ return self
+end
+
+-- Internal: Removes an Entity from the System.
+-- @param e The Entity to remove
+-- @treturn System self
+function System:__remove(e)
+ for _, filter in ipairs(self.__filters) do
+ if filter:has(e) then
+ filter:remove(e)
+ end
+ end
+
+ return self
+end
+
+-- Internal: Clears all Entities from the System.
+-- @treturn System self
+function System:__clear()
+ for _, filter in ipairs(self.__filters) do
+ filter:clear()
+ end
+
+ return self
+end
+
+--- Sets if the System is enabled
+-- @tparam boolean enable
+-- @treturn System self
+function System:setEnabled(enable)
+ if (not self.__enabled and enable) then
+ self.__enabled = true
+ self:onEnabled()
+ elseif (self.__enabled and not enable) then
+ self.__enabled = false
+ self:onDisabled()
+ end
+
+ return self
+end
+
+--- Returns is the System is enabled
+-- @treturn boolean
+function System:isEnabled()
+ return self.__enabled
+end
+
+--- Returns the World the System is in.
+-- @treturn World
+function System:getWorld()
+ return self.__world
+end
+
+--- Callbacks
+-- @section Callbacks
+
+--- Callback for system initialization.
+-- @tparam World world The World the System was added to
+function System:init(world) -- luacheck: ignore
+end
+
+--- Callback for when a System is enabled.
+function System:onEnabled() -- luacheck: ignore
+end
+
+--- Callback for when a System is disabled.
+function System:onDisabled() -- luacheck: ignore
+end
+
+return setmetatable(System, {
+ __call = function(_, ...)
+ return System.new(...)
+ end,
+})
diff --git a/concord/type.lua b/concord/type.lua
new file mode 100644
index 0000000..bf7f52a
--- /dev/null
+++ b/concord/type.lua
@@ -0,0 +1,64 @@
+--- Type
+-- Helper module to do easy type checking for Concord types
+
+local Type = {}
+
+function Type.isCallable(t)
+ if type(t) == "function" then return true end
+
+ local meta = getmetatable(t)
+ if meta and type(meta.__call) == "function" then return true end
+
+ return false
+end
+
+--- Returns if object is an Entity.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isEntity(t)
+ return type(t) == "table" and t.__isEntity or false
+end
+
+--- Returns if object is a ComponentClass.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isComponentClass(t)
+ return type(t) == "table" and t.__isComponentClass or false
+end
+
+--- Returns if object is a Component.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isComponent(t)
+ return type(t) == "table" and t.__isComponent or false
+end
+
+--- Returns if object is a SystemClass.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isSystemClass(t)
+ return type(t) == "table" and t.__isSystemClass or false
+end
+
+--- Returns if object is a System.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isSystem(t)
+ return type(t) == "table" and t.__isSystem or false
+end
+
+--- Returns if object is a World.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isWorld(t)
+ return type(t) == "table" and t.__isWorld or false
+end
+
+--- Returns if object is a Filter.
+-- @param t Object to check
+-- @treturn boolean
+function Type.isFilter(t)
+ return type(t) == "table" and t.__isFilter or false
+end
+
+return Type
diff --git a/concord/utils.lua b/concord/utils.lua
new file mode 100644
index 0000000..0802586
--- /dev/null
+++ b/concord/utils.lua
@@ -0,0 +1,72 @@
+--- Utils
+-- Helper module for misc operations
+
+local Utils = {}
+
+function Utils.error(level, str, ...)
+ error(string.format(str, ...), level + 1)
+end
+
+--- Does a shallow copy of a table and appends it to a target table.
+-- @param orig Table to copy
+-- @param target Table to append to
+function Utils.shallowCopy(orig, target)
+ for key, value in pairs(orig) do
+ target[key] = value
+ end
+
+ return target
+end
+
+--- Requires files and puts them in a table.
+-- Accepts a table of paths to Lua files: {"path/to/file_1", "path/to/another/file_2", "etc"}
+-- Accepts a path to a directory with Lua files: "my_files/here"
+-- @param pathOrFiles The table of paths or a path to a directory.
+-- @param namespace A table that will hold the required files
+-- @treturn table The namespace table
+function Utils.loadNamespace(pathOrFiles, namespace)
+ if type(pathOrFiles) ~= "string" and type(pathOrFiles) ~= "table" then
+ Utils.error(2, "bad argument #1 to 'loadNamespace' (string/table of strings expected, got %s)", type(pathOrFiles))
+ end
+
+ if type(pathOrFiles) == "string" then
+ local info = love.filesystem.getInfo(pathOrFiles) -- luacheck: ignore
+ if info == nil or info.type ~= "directory" then
+ Utils.error(2, "bad argument #1 to 'loadNamespace' (path '%s' not found)", pathOrFiles)
+ end
+
+ local files = love.filesystem.getDirectoryItems(pathOrFiles)
+
+ for _, file in ipairs(files) do
+ local isFile = love.filesystem.getInfo(pathOrFiles .. "/" .. file).type == "file"
+
+ if isFile and string.match(file, '%.lua$') ~= nil then
+ local name = file:sub(1, #file - 4)
+ local path = pathOrFiles.."."..name
+
+ local value = require(path:gsub("%/", "."))
+ if namespace then namespace[name] = value end
+ end
+ end
+ elseif type(pathOrFiles) == "table" then
+ for _, path in ipairs(pathOrFiles) do
+ if type(path) ~= "string" then
+ Utils.error(2, "bad argument #2 to 'loadNamespace' (string/table of strings expected, got table containing %s)", type(path)) -- luacheck: ignore
+ end
+
+ local name = path
+
+ local dotIndex, slashIndex = path:match("^.*()%."), path:match("^.*()%/")
+ if dotIndex or slashIndex then
+ name = path:sub((dotIndex or slashIndex) + 1)
+ end
+
+ local value = require(path:gsub("%/", "."))
+ if namespace then namespace[name] = value end
+ end
+ end
+
+ return namespace
+end
+
+return Utils
diff --git a/concord/world.lua b/concord/world.lua
new file mode 100644
index 0000000..7b31a6e
--- /dev/null
+++ b/concord/world.lua
@@ -0,0 +1,515 @@
+--- A collection of Systems and Entities.
+-- A world emits to let Systems iterate.
+-- A World contains any amount of Systems.
+-- A World contains any amount of Entities.
+-- @classmod World
+
+local PATH = (...):gsub('%.[^%.]+$', '')
+
+local Filter = require(PATH..".filter")
+local Entity = require(PATH..".entity")
+local Components = require(PATH..".components")
+local Type = require(PATH..".type")
+local List = require(PATH..".list")
+local Utils = require(PATH..".utils")
+
+local World = {
+ ENABLE_OPTIMIZATION = true,
+}
+World.__mt = {
+ __index = World,
+}
+
+local defaultGenerator = function (state)
+ local current = state
+ state = state +1
+ return string.format("%d", current), state
+end
+
+--- Creates a new World.
+-- @treturn World The new World
+function World.new()
+ local world = setmetatable({
+ __entities = List(),
+ __systems = List(),
+
+ __events = {},
+ __emitSDepth = 0,
+
+ __resources = {},
+
+ __hash = {
+ state = -2^53,
+ generator = defaultGenerator,
+ keys = {},
+ entities = {}
+ },
+
+ __added = List(), __backAdded = List(),
+ __removed = List(), __backRemoved = List(),
+ __dirty = List(), __backDirty = List(),
+
+ __systemLookup = {},
+
+ __isWorld = true,
+
+ __ignoreEmits = false
+ }, World.__mt)
+
+ -- Optimization: We deep copy the World class into our instance of a world.
+ -- This grants slightly faster access times at the cost of memory.
+ -- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff
+ if (World.ENABLE_OPTIMIZATION) then
+ Utils.shallowCopy(World, world)
+ end
+
+ return world
+end
+
+--- Adds an Entity to the World.
+-- @tparam Entity e Entity to add
+-- @treturn World self
+function World:addEntity(e)
+ if not Type.isEntity(e) then
+ Utils.error(2, "bad argument #1 to 'World:addEntity' (Entity expected, got %s)", type(e))
+ end
+
+ if e.__world then
+ error("bad argument #1 to 'World:addEntity' (Entity was already added to a world)", 2)
+ end
+
+ e.__world = self
+ self.__added:add(e)
+
+ return self
+end
+
+--- Creates a new Entity and adds it to the World.
+-- @treturn Entity e the new Entity
+function World:newEntity()
+ return Entity(self)
+end
+
+function World:query(def, onMatch)
+ local filter = Filter.parse(nil, def)
+
+ local list = nil
+ if not Type.isCallable(onMatch) then
+ list = type(onMatch) == "table" and onMatch or {}
+ end
+
+ for _, e in ipairs(self.__entities) do
+ if Filter.match(e, filter) then
+ if list then
+ table.insert(list, e)
+ else
+ onMatch(e)
+ end
+ end
+ end
+
+ return list
+end
+
+--- Removes an Entity from the World.
+-- @tparam Entity e Entity to remove
+-- @treturn World self
+function World:removeEntity(e)
+ if not Type.isEntity(e) then
+ Utils.error(2, "bad argument #1 to 'World:removeEntity' (Entity expected, got %s)", type(e))
+ end
+
+ if e.__world ~= self then
+ error("trying to remove an Entity from a World it doesn't belong to", 2)
+ end
+
+ if e:has("key") then
+ e:remove("key")
+ end
+
+ self.__removed:add(e)
+
+ return self
+end
+
+-- Internal: Marks an Entity as dirty.
+-- @param e Entity to mark as dirty
+function World:__dirtyEntity(e)
+ if not self.__dirty:has(e) then
+ self.__dirty:add(e)
+ end
+end
+
+-- Internal: Flushes all changes to Entities.
+-- This processes all entities. Adding and removing entities, as well as reevaluating dirty entities.
+-- @treturn World self
+function World:__flush()
+ -- Early out
+ if (self.__added.size == 0 and self.__removed.size == 0 and self.__dirty.size == 0) then
+ return self
+ end
+
+ -- Switch buffers
+ self.__added, self.__backAdded = self.__backAdded, self.__added
+ self.__removed, self.__backRemoved = self.__backRemoved, self.__removed
+ self.__dirty, self.__backDirty = self.__backDirty, self.__dirty
+
+ local e
+
+ -- Process added entities
+ for i = 1, self.__backAdded.size do
+ e = self.__backAdded[i]
+
+ if e.__world == self then
+ self.__entities:add(e)
+
+ for j = 1, self.__systems.size do
+ self.__systems[j]:__evaluate(e)
+ end
+
+ self:onEntityAdded(e)
+ end
+ end
+ self.__backAdded:clear()
+
+ -- Process removed entities
+ for i = 1, self.__backRemoved.size do
+ e = self.__backRemoved[i]
+
+ if e.__world == self then
+ e.__world = nil
+ self.__entities:remove(e)
+
+ for j = 1, self.__systems.size do
+ self.__systems[j]:__remove(e)
+ end
+
+ self:onEntityRemoved(e)
+ end
+ end
+ self.__backRemoved:clear()
+
+ -- Process dirty entities
+ for i = 1, self.__backDirty.size do
+ e = self.__backDirty[i]
+
+ if e.__world == self then
+ for j = 1, self.__systems.size do
+ self.__systems[j]:__evaluate(e)
+ end
+ end
+ end
+ self.__backDirty:clear()
+
+ return self
+end
+
+-- These functions won't be seen as callbacks that will be emitted to.
+local blacklistedSystemFunctions = {
+ "init",
+ "onEnabled",
+ "onDisabled",
+}
+
+local tryAddSystem = function (world, systemClass)
+ if (not Type.isSystemClass(systemClass)) then
+ return false, "SystemClass expected, got "..type(systemClass)
+ end
+
+ if (world.__systemLookup[systemClass]) then
+ return false, "SystemClass was already added to World"
+ end
+
+ -- Create instance of system
+ local system = systemClass(world)
+
+ world.__systemLookup[systemClass] = system
+ world.__systems:add(system)
+
+ for callbackName, callback in pairs(systemClass) do
+ -- Skip callback if its blacklisted
+ if (not blacklistedSystemFunctions[callbackName]) then
+ -- Make container for all listeners of the callback if it does not exist yet
+ if (not world.__events[callbackName]) then
+ world.__events[callbackName] = {}
+ end
+
+ -- Add callback to listeners
+ local listeners = world.__events[callbackName]
+ listeners[#listeners + 1] = {
+ system = system,
+ callback = callback,
+ }
+ end
+ end
+
+ -- Evaluate all existing entities
+ for j = 1, world.__entities.size do
+ system:__evaluate(world.__entities[j])
+ end
+
+ return true
+end
+
+--- Adds a System to the World.
+-- Callbacks are registered automatically
+-- Entities added before are added to the System retroactively
+-- @see World:emit
+-- @tparam System systemClass SystemClass of System to add
+-- @treturn World self
+function World:addSystem(systemClass)
+ local ok, err = tryAddSystem(self, systemClass)
+
+ if not ok then
+ Utils.error(2, "bad argument #1 to 'World:addSystem' (%s)", err)
+ end
+
+ return self
+end
+
+--- Adds multiple Systems to the World.
+-- Callbacks are registered automatically
+-- @see World:addSystem
+-- @see World:emit
+-- @param ... SystemClasses of Systems to add
+-- @treturn World self
+function World:addSystems(...)
+ for i = 1, select("#", ...) do
+ local systemClass = select(i, ...)
+
+ local ok, err = tryAddSystem(self, systemClass)
+ if not ok then
+ Utils.error(2, "bad argument #%d to 'World:addSystems' (%s)", i, err)
+ end
+ end
+
+ return self
+end
+
+--- Returns if the World has a System.
+-- @tparam System systemClass SystemClass of System to check for
+-- @treturn boolean
+function World:hasSystem(systemClass)
+ if not Type.isSystemClass(systemClass) then
+ Utils.error(2, "bad argument #1 to 'World:hasSystem' (SystemClass expected, got %s)", type(systemClass))
+ end
+
+ return self.__systemLookup[systemClass] and true or false
+end
+
+--- Gets a System from the World.
+-- @tparam System systemClass SystemClass of System to get
+-- @treturn System System to get
+function World:getSystem(systemClass)
+ if not Type.isSystemClass(systemClass) then
+ Utils.error(2, "bad argument #1 to 'World:getSystem' (SystemClass expected, got %s)", type(systemClass))
+ end
+
+ return self.__systemLookup[systemClass]
+end
+
+--- Emits a callback in the World.
+-- Calls all functions with the functionName of added Systems
+-- @string functionName Name of functions to call.
+-- @param ... Parameters passed to System's functions
+-- @treturn World self
+function World:emit(functionName, ...)
+ if not functionName or type(functionName) ~= "string" then
+ Utils.error(2, "bad argument #1 to 'World:emit' (String expected, got %s)", type(functionName))
+ end
+
+ local shouldFlush = self.__emitSDepth == 0
+
+ self.__emitSDepth = self.__emitSDepth + 1
+
+ local listeners = self.__events[functionName]
+
+ if not self.__ignoreEmits and Type.isCallable(self.beforeEmit) then
+ self.__ignoreEmits = true
+ local preventDefaults = self:beforeEmit(functionName, listeners, ...)
+ self.__ignoreEmits = false
+ if preventDefaults then return end
+ end
+
+ if listeners then
+ for i = 1, #listeners do
+ local listener = listeners[i]
+
+ if (listener.system.__enabled) then
+ if (shouldFlush) then
+ self:__flush()
+ end
+
+ listener.callback(listener.system, ...)
+ end
+ end
+ end
+
+ if not self.__ignoreEmits and Type.isCallable(self.afterEmit) then
+ self.__ignoreEmits = true
+ self:afterEmit(functionName, listeners, ...)
+ self.__ignoreEmits = false
+ end
+
+ self.__emitSDepth = self.__emitSDepth - 1
+
+ return self
+end
+
+--- Removes all entities from the World
+-- @treturn World self
+function World:clear()
+ for i = 1, self.__entities.size do
+ self:removeEntity(self.__entities[i])
+ end
+
+ for i = 1, self.__added.size do
+ local e = self.__added[i]
+ e.__world = nil
+ end
+ self.__added:clear()
+
+ self:__flush()
+
+ return self
+end
+
+function World:getEntities()
+ return self.__entities
+end
+
+function World:getSystems()
+ return self.__systems
+end
+
+function World:serialize(ignoreKeys)
+ self:__flush()
+
+ local data = { generator = self.__hash.state }
+
+ for i = 1, self.__entities.size do
+ local entity = self.__entities[i]
+
+ if entity.serializable then
+ local entityData = entity:serialize(ignoreKeys)
+ table.insert(data, entityData)
+ end
+ end
+
+ return data
+end
+
+function World:deserialize(data, startClean, ignoreGenerator)
+ if startClean then
+ self:clear()
+ end
+
+ if (not ignoreGenerator) then
+ self.__hash.state = data.generator
+ end
+
+ local entities = {}
+
+ for i = 1, #data do
+ local entity = Entity(self)
+
+ if data[i].key then
+ local component = Components.key:__new(entity)
+ component:deserialize(data[i].key)
+ entity.key = component
+
+ entity:__dirty()
+ end
+
+ entities[i] = entity
+ end
+
+ for i = 1, #data do
+ entities[i]:deserialize(data[i])
+ end
+
+ self:__flush()
+
+ return self
+end
+
+function World:setKeyGenerator(generator, initialState)
+ if not Type.isCallable(generator) then
+ Utils.error(2, "bad argument #1 to 'World:setKeyGenerator' (function expected, got %s)", type(generator))
+ end
+
+ self.__hash.generator = generator
+ self.__hash.state = initialState
+
+ return self
+end
+
+function World:__clearKey(e)
+ local key = self.__hash.keys[e]
+
+ if key then
+ self.__hash.keys[e] = nil
+ self.__hash.entities[key] = nil
+ end
+
+ return self
+end
+
+function World:__assignKey(e, key)
+ local hash = self.__hash
+
+ if not key then
+ key = hash.keys[e]
+ if key then return key end
+
+ key, hash.state = hash.generator(hash.state)
+ end
+
+ if hash.entities[key] and hash.entities[key] ~= e then
+ Utils.error(4, "Trying to assign a key that is already taken (key: '%s').", key)
+ elseif hash.keys[e] and hash.keys[e] ~= key then
+ Utils.error(4, "Trying to assign more than one key to an Entity. (old: '%s', new: '%s')", hash.keys[e], key)
+ end
+
+ hash.keys[e] = key
+ hash.entities[key] = e
+
+ return key
+end
+
+function World:getEntityByKey(key)
+ return self.__hash.entities[key]
+end
+
+--- Callback for when an Entity is added to the World.
+-- @tparam Entity e The Entity that was added
+function World:onEntityAdded(e) -- luacheck: ignore
+end
+
+--- Callback for when an Entity is removed from the World.
+-- @tparam Entity e The Entity that was removed
+function World:onEntityRemoved(e) -- luacheck: ignore
+end
+
+--- Sets a named resource in the world
+-- @string name Name of the resource
+-- @tparam Any resource Resource to set
+-- @treturn World self
+function World:setResource(name, resource)
+ self.__resources[name] = resource
+ return self
+end
+
+--- Gets a named resource from the world
+-- @string name Name of the resource
+-- @treturn Any resource
+function World:getResource(name)
+ return self.__resources[name]
+end
+
+return setmetatable(World, {
+ __call = function(_, ...)
+ ---@diagnostic disable-next-line: redundant-parameter
+ return World.new(...)
+ end,
+})
diff --git a/conf.lua b/conf.lua
deleted file mode 100644
index 4a1ab1c..0000000
--- a/conf.lua
+++ /dev/null
@@ -1,10 +0,0 @@
-function love.conf(t)
- t.identity = "Concord Example"
- t.version = "11.0"
- t.console = true
-
- t.window.vsync = false
- t.window.width = 720
- t.window.height = 720
- end
-
\ No newline at end of file
diff --git a/config.ld b/config.ld
index aada8c2..9b7ea55 100644
--- a/config.ld
+++ b/config.ld
@@ -1,3 +1,4 @@
project = 'Concord'
description = 'A feature-complete ECS library'
-file = {'lib', exclude = {'lib/type.lua', 'lib/run.lua'}}
\ No newline at end of file
+file = {'concord', exclude = {}}
+dir = 'docs'
diff --git a/docs/classes/Assemblage.html b/docs/classes/Assemblage.html
new file mode 100644
index 0000000..c61009b
--- /dev/null
+++ b/docs/classes/Assemblage.html
@@ -0,0 +1,204 @@
+
+
+
+