Compare commits

..

49 commits
v3.0 ... main

Author SHA1 Message Date
Justin van der Leij
848652f688
Make example for how to use Concord.utils.loadNamespace for systems more concrete 2024-06-30 10:45:46 +02:00
Justin van der Leij
30b21c4c25
Merge pull request #78 from denisdefreyne/patch-1
Fix typo in README.md
2024-06-30 10:38:53 +02:00
Justin van der Leij
9f187f12e5
Merge pull request #76 from lambtoken/typo-fix
Added missing do keyword in the loops
2024-06-30 10:38:15 +02:00
Denis Defreyne
6575686b3b
Fix typo in README.md
A piece of example code was missing a `,`.
2024-06-29 10:28:12 +02:00
Justin van der Leij
1aaf501401
Update README.md to hint that components should be loaded for systems 2024-05-27 13:53:55 +02:00
lambtoken
f9da8dbe92 Added missing do keyword in the loops 2023-12-23 09:37:03 +01:00
Justin van der Leij
7b8f7b2f0a
Merge pull request #70 from DanielPower/patch-1
Fix link to documentation
2023-02-26 16:28:39 +01:00
Daniel Power
5f4b3b97da
Fix link to documentation 2023-02-18 16:43:56 -03:30
Pablo Mayobre
2386547caa
Normalize slashes to dots when calling require in Utils.loadNamespace
Co-authored-by: Ulhar <ulhar@protonmail.ch>
2023-02-14 22:28:26 -03:00
Brandon Blanker Lim-it
1e4132be21 Added beforeEmit and afterEmit World callbacks (#54)
* Added beforeEmit and afterEmit World callbacks

* Fixed beforeEmit/afterEmit to handle recursive/nested emits;

* Added preventDefaults in beforeEmit
2023-02-14 22:20:34 -03:00
Jesse Viikari
429a448ab6 Add resources to world
- setResource(name, resource) to set a resource
- getResource(name) to retrieve it
2023-02-14 22:20:34 -03:00
Pablo Mayobre
9bccd05019 Usability improvements
- Now entity.key() is the same as entity.key.value
- Entity:serialize only serializes component given correctly
- Any other value inside the Entity is ignored
- Disable some diagnostics used in Lua language server by sumneko
2023-02-14 22:20:34 -03:00
Pablo Mayobre
cf05cfc972 Add ability to clone components
Fixes #51
2023-02-14 22:20:34 -03:00
flamendless
16c77c6a66 Fixed bug with serialization/deserialization 2023-02-14 22:20:34 -03:00
flamendless
61720312cb Added optional table for output for entity:getComponents 2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
8e1b14d53b Ignore non-lua files in Utils.loadNamespace
Fixes #48
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
bdfe2523b0 Fix require indentation 2023-02-14 22:20:34 -03:00
flamendless
41fcfac6af Fixed errors on World:deserialize 2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
a55efd042a Entity's Keys
You can now give the 'key' component to Entities.

A key will be generated automatically and stored in Entity.key.value.

You can then use this key to fetch the Entity from the World with World:getEntityByKey(key)

The keys are generated with a generator function that can be overriden.
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
3d195c790f Add World:query
This method allows you to query the World in order to find a set of Entities that matches a specific Filter.
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
a4ae392341 World:newEntity
A shortcut for Concord.entity(World)
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
cc0fd1614c Serializable component
You can remove the component to tell Concord an Entity shouldn't be serialized.

It's given automatically on Entity creation, but this can be disabled by changing Entity.SERIALIZE_BY_DEFAULT to false.
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
892f4d4700 Error handling overhaul 2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
743d662ef9 Replaced Pools with Filters
Filters allow for a Pool constructor (defaults to Lists) that can be used to define Custom Pools.

The constructor is a function that takes the Filter Definition and returns a Custom Pool with these functions:

:add(e) - Add the Entity to the pool
:remove(e) - Remove the Entity from the pool
:has(e) boolean - Checks if the Entity exists in the pool
:clear() - Clears the Pool from Entities

Fixes #40
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
07bd5d0f28 Fix utils.loadNamespace when passed a table 2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
c4594da19d Add Component:removed() callback
Fixes #37

I also added a reference to the Entity inside the Component which will help with #38
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
50249d5ad3 Removed deprecated functionality
Removed hasName/getName on Systems and Worlds.

Removed Entity.__components since it had a duplicate version of the components stored in the Entity itself.
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
695cc2dfe3 Add Component Negation
Fixes #32
2023-02-14 22:20:34 -03:00
Pablo Ariel Mayobre
89eab3fb72 List.sort
Fixes #33
2023-02-14 22:20:34 -03:00
Justin van der Leij
a45d89457b
Merge pull request #66 from wolfboyft/master
Fix bug where loading namespaces using folder/init.lua entries would only work if "folder" was 3 characters
2022-10-17 21:53:30 +02:00
Tachytaenius
940870318d
Fix bug where loading namespaces using folder/init.lua entries would only work if "folder" was 3 characters 2022-10-17 20:17:54 +01:00
Justin van der Leij
821b36c903
Merge pull request #46 from john-cheesman/correct-readme-code
Correct syntax in Systems example code
2022-10-17 15:12:18 +02:00
Justin van der Leij
7a4bfaf33c
Merge pull request #59 from josh-perry/fix-readme-docs-link
Fixed docs link in README
2022-06-01 17:51:12 +02:00
Josh Perry
2acb1458f2 Fixed docs link in README 2022-05-31 20:20:54 +01:00
Justin van der Leij
edc1d2fdbc Fix documentation page building workflow 13 2021-11-02 20:10:14 +01:00
Justin van der Leij
4313dc7856 Fix documentation page building workflow 12 2021-11-02 17:27:18 +01:00
Justin van der Leij
9c22986501 Fix documentation page building workflow 11 2021-11-02 17:23:53 +01:00
Justin van der Leij
73177f4048 Fix documentation page building workflow 10 2021-11-02 17:21:51 +01:00
Justin van der Leij
6ce714ce14 Fix documentation page building workflow 9 2021-11-02 17:21:22 +01:00
Justin van der Leij
e141a6183b Fix documentation page building workflow 8 2021-11-02 17:18:57 +01:00
Justin van der Leij
6329e09138 Fix documentation page building workflow 7 2021-11-02 17:16:51 +01:00
Justin van der Leij
cdf425d301 Fix documentation page building workflow 6 2021-11-02 17:08:25 +01:00
Justin van der Leij
c7625ad376 Fix documentation page building workflow 5 2021-11-02 17:02:38 +01:00
Justin van der Leij
65aae3c2ba Fix documentation page building workflow 4 2021-11-02 16:57:08 +01:00
Justin van der Leij
297b30aa50 Fix documentation page building workflow 3 2021-11-02 16:55:04 +01:00
Justin van der Leij
9aaff0fbcc Fix documentation page building workflow 2 2021-11-02 16:37:30 +01:00
Justin van der Leij
b701493a27 Fix documentation page building workflow 2021-11-02 16:32:24 +01:00
Justin van der Leij
b53f950e3e Add documentation page building workflow 2021-11-02 16:30:23 +01:00
John Cheesman
fd558dd3fe
Correct syntax in Systems example code 2021-05-05 18:32:04 +01:00
17 changed files with 735 additions and 300 deletions

38
.github/workflows/doc.yml vendored Normal file
View file

@ -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

3
.gitignore vendored
View file

@ -38,3 +38,6 @@ luac.out
*.i*86 *.i*86
*.x86_64 *.x86_64
*.hex *.hex
# VSCode
.vscode/

View file

@ -1 +1,2 @@
---@diagnostic disable: lowercase-global
std="love+luajit" std="love+luajit"

View file

@ -7,7 +7,7 @@ With Concord it is possibile to easily write fast and clean code.
This readme will explain how to use Concord. This readme will explain how to use Concord.
Additionally all of Concord is documented using the LDoc format. 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://tjakka5.github.io/Concord/). Auto generated docs for Concord can be found in `docs` folder, or on the [GitHub page](https://keyslam-group.github.io/Concord/).
--- ---
@ -133,16 +133,21 @@ Concord does a few things that might not be immediately clear. This segment shou
Since you'll have lots of Components and Systems in your game Concord makes it a bit easier to load things in. Since you'll have lots of Components and Systems in your game Concord makes it a bit easier to load things in.
```lua ```lua
-- 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)
print(Systems.systemName)
-- Loads all files in the directory. Components automatically register into Concord.components, so loading them into a namespace isn't necessary. -- 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") Concord.utils.loadNamespace("path/to/components")
print(Concord.components.componentName) 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 #### Method chaining
@ -170,7 +175,7 @@ When defining a ComponentClass you need to pass in a name and usually a `populat
-- Create the position class with a populate function -- Create the position class with a populate function
-- The component variable is the actual Component given to an Entity -- 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 -- The x and y variables are values we pass in when we create the Component
Concord.component("position" function(component, x, y) Concord.component("position", function(component, x, y)
component.x = x or 0 component.x = x or 0
component.y = y or 0 component.y = y or 0
end) end)
@ -266,10 +271,10 @@ Systems can have multiple pools.
```lua ```lua
-- Create a System -- Create a System
local mySystemClass = Concord.system(pool = {"position"}) -- Pool named 'pool' will contain all Entities with a position Component local mySystemClass = Concord.system({pool = {"position"}}) -- Pool named 'pool' will contain all Entities with a position Component
-- Create a System with multiple pools -- Create a System with multiple pools
local mySystemClass = Concord.system( local mySystemClass = Concord.system({
pool = { -- This pool will be named 'pool' pool = { -- This pool will be named 'pool'
"position", "position",
"velocity", "velocity",
@ -278,7 +283,7 @@ local mySystemClass = Concord.system(
"health", "health",
"damageable", "damageable",
} }
) })
``` ```
```lua ```lua
@ -294,14 +299,14 @@ end
-- Defining a function -- Defining a function
function mySystemClass:update(dt) function mySystemClass:update(dt)
-- Iterate over all entities in the Pool -- Iterate over all entities in the Pool
for _, e in ipairs(self.pool) for _, e in ipairs(self.pool) do
-- Do something with the Components -- Do something with the Components
e.position.x = e.position.x + e.velocity.x * dt e.position.x = e.position.x + e.velocity.x * dt
e.position.y = e.position.y + e.velocity.y * dt e.position.y = e.position.y + e.velocity.y * dt
end end
-- Iterate over all entities in the second Pool -- Iterate over all entities in the second Pool
for _, e in ipairs(self.secondPool) for _, e in ipairs(self.secondPool) do
-- Do something -- Do something
end end
end end

View file

@ -0,0 +1,6 @@
local PATH = (...):gsub("(%.init)$", "")
return {
serializable = require(PATH..".serializable"),
key = require(PATH..".key"),
}

41
concord/builtins/key.lua Normal file
View file

@ -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

View file

@ -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

View file

@ -16,15 +16,19 @@ Component.__mt = {
-- @treturn Component A new ComponentClass -- @treturn Component A new ComponentClass
function Component.new(name, populate) function Component.new(name, populate)
if (type(name) ~= "string") then if (type(name) ~= "string") then
error("bad argument #1 to 'Component.new' (string expected, got "..type(name)..")", 2) 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 end
if (rawget(Components, name)) then if (rawget(Components, name)) then
error("bad argument #1 to 'Component.new' (ComponentClass with name '"..name.."' was already registerd)", 2) -- luacheck: ignore Utils.error(2, "bad argument #1 to 'Component.new' (ComponentClass with name '%s' was already registerd)", name) -- luacheck: ignore
end end
if (type(populate) ~= "function" and type(populate) ~= "nil") then if (type(populate) ~= "function" and type(populate) ~= "nil") then
error("bad argument #1 to 'Component.new' (function/nil expected, got "..type(populate)..")", 2) Utils.error(2, "bad argument #1 to 'Component.new' (function/nil expected, got %s)", type(populate))
end end
local componentClass = setmetatable({ local componentClass = setmetatable({
@ -43,31 +47,40 @@ function Component.new(name, populate)
return componentClass return componentClass
end end
-- Internal: Populates a Component with values -- Internal: Populates a Component with values.
function Component:__populate() -- luacheck: ignore function Component:__populate() -- luacheck: ignore
end 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() function Component:serialize()
local data = Utils.shallowCopy(self, {}) local data = Utils.shallowCopy(self, {})
--This values shouldn't be copied over --This values shouldn't be copied over
data.__componentClass = nil data.__componentClass = nil
data.__isComponent = nil data.__entity = nil
data.__isComponent = nil
data.__isComponentClass = nil data.__isComponentClass = nil
return data return data
end end
-- Callback: When the Component gets deserialized from serialized data.
function Component:deserialize(data) function Component:deserialize(data)
Utils.shallowCopy(data, self) Utils.shallowCopy(data, self)
end end
-- Internal: Creates a new Component. -- Internal: Creates a new Component.
-- @param entity The Entity that will receive this Component.
-- @return A new Component -- @return A new Component
function Component:__new() function Component:__new(entity)
local component = setmetatable({ local component = setmetatable({
__componentClass = self, __componentClass = self,
__entity = entity,
__isComponent = true, __isComponent = true,
__isComponentClass = false, __isComponentClass = false,
}, self.__mt) }, self.__mt)
@ -76,11 +89,13 @@ function Component:__new()
end end
-- Internal: Creates and populates a new Component. -- Internal: Creates and populates a new Component.
-- @param entity The Entity that will receive this Component.
-- @param ... Varargs passed to the populate function -- @param ... Varargs passed to the populate function
-- @return A new populated Component -- @return A new populated Component
function Component:__initialize(...) function Component:__initialize(entity, ...)
local component = self:__new() local component = self:__new(entity)
---@diagnostic disable-next-line: redundant-parameter
self.__populate(component, ...) self.__populate(component, ...)
return component return component

View file

@ -1,12 +1,11 @@
--- Container for registered ComponentClasses --- Container for registered ComponentClasses
-- @module Components -- @module Components
local PATH = (...):gsub('%.[^%.]+$', '')
local Type = require(PATH..".type")
local Components = {} local Components = {}
Components.__REJECT_PREFIX = "!"
Components.__REJECT_MATCH = "^(%"..Components.__REJECT_PREFIX.."?)(.+)"
--- Returns true if the containter has the ComponentClass with the specified name --- Returns true if the containter has the ComponentClass with the specified name
-- @string name Name of the ComponentClass to check -- @string name Name of the ComponentClass to check
-- @treturn boolean -- @treturn boolean
@ -14,22 +13,43 @@ function Components.has(name)
return rawget(Components, name) and true or false return rawget(Components, name) and true or false
end 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 --- Returns true and the ComponentClass if one was registered with the specified name
-- or false and an error otherwise -- or false and an error otherwise
-- @string name Name of the ComponentClass to check -- @string name Name of the ComponentClass to check
-- @boolean acceptRejected Whether to accept names prefixed with the Reject Prefix.
-- @treturn boolean -- @treturn boolean
-- @treturn Component or error string -- @treturn Component or error string
function Components.try(name) -- @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 if type(name) ~= "string" then
return false, "ComponentsClass name is expected to be a string, got "..type(name)..")" return false, "ComponentsClass name is expected to be a string, got "..type(name)..")"
end 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) local value = rawget(Components, name)
if not value then if not value then
return false, "ComponentClass '"..name.."' does not exist / was not registered" return false, "ComponentClass '"..name.."' does not exist / was not registered"
end end
return true, value return true, value, rejected
end end
--- Returns the ComponentClass with the specified name --- Returns the ComponentClass with the specified name

View file

@ -6,8 +6,16 @@ local PATH = (...):gsub('%.[^%.]+$', '')
local Components = require(PATH..".components") local Components = require(PATH..".components")
local Type = require(PATH..".type") 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,
}
local Entity = {}
Entity.__mt = { Entity.__mt = {
__index = Entity, __index = Entity,
} }
@ -17,12 +25,11 @@ Entity.__mt = {
-- @treturn Entity A new Entity -- @treturn Entity A new Entity
function Entity.new(world) function Entity.new(world)
if (world ~= nil and not Type.isWorld(world)) then if (world ~= nil and not Type.isWorld(world)) then
error("bad argument #1 to 'Entity.new' (world/nil expected, got "..type(world)..")", 2) Utils.error(2, "bad argument #1 to 'Entity.new' (world/nil expected, got %s)", type(world))
end end
local e = setmetatable({ local e = setmetatable({
__world = nil, __world = nil,
__components = {},
__isEntity = true, __isEntity = true,
}, Entity.__mt) }, Entity.__mt)
@ -31,23 +38,86 @@ function Entity.new(world)
world:addEntity(e) world:addEntity(e)
end end
if Entity.SERIALIZE_BY_DEFAULT then
e:give("serializable")
end
return e return e
end end
local function give(e, name, componentClass, ...) local function createComponent(e, name, componentClass, ...)
local component = componentClass:__initialize(...) local component = componentClass:__initialize(e, ...)
local hadComponent = not not e[name]
if hadComponent then
e[name]:removed(true)
end
e[name] = component e[name] = component
e.__components[name] = component
e:__dirty() if not hadComponent then
e:__dirty()
end
end end
local function remove(e, name, componentClass) local function deserializeComponent(e, name, componentData)
e[name] = nil local componentClass = Components[name]
e.__components[name] = nil local hadComponent = not not e[name]
e:__dirty() 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 end
--- Gives an Entity a Component. --- Gives an Entity a Component.
@ -56,15 +126,7 @@ end
-- @param ... additional arguments to pass to the Component's populate function -- @param ... additional arguments to pass to the Component's populate function
-- @treturn Entity self -- @treturn Entity self
function Entity:give(name, ...) function Entity:give(name, ...)
local ok, componentClass = Components.try(name) return giveComponent(self, false, name, ...)
if not ok then
error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2)
end
give(self, name, componentClass, ...)
return self
end end
--- Ensures an Entity to have a Component. --- Ensures an Entity to have a Component.
@ -73,19 +135,7 @@ end
-- @param ... additional arguments to pass to the Component's populate function -- @param ... additional arguments to pass to the Component's populate function
-- @treturn Entity self -- @treturn Entity self
function Entity:ensure(name, ...) function Entity:ensure(name, ...)
local ok, componentClass = Components.try(name) return giveComponent(self, true, name, ...)
if not ok then
error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2)
end
if self[name] then
return self
end
give(self, name, componentClass, ...)
return self
end end
--- Removes a Component from an Entity. --- Removes a Component from an Entity.
@ -95,10 +145,10 @@ function Entity:remove(name)
local ok, componentClass = Components.try(name) local ok, componentClass = Components.try(name)
if not ok then if not ok then
error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) Utils.error(2, "bad argument #1 to 'Entity:remove' (%s)", componentClass)
end end
remove(self, name, componentClass) removeComponent(self, name)
return self return self
end end
@ -109,7 +159,7 @@ end
-- @treturn Entity self -- @treturn Entity self
function Entity:assemble(assemblage, ...) function Entity:assemble(assemblage, ...)
if type(assemblage) ~= "function" then if type(assemblage) ~= "function" then
error("bad argument #1 to 'Entity:assemble' (function expected, got "..type(assemblage)..")") Utils.error(2, "bad argument #1 to 'Entity:assemble' (function expected, got %s)", type(assemblage))
end end
assemblage(self, ...) assemblage(self, ...)
@ -145,7 +195,7 @@ function Entity:has(name)
local ok, componentClass = Components.try(name) local ok, componentClass = Components.try(name)
if not ok then if not ok then
error("bad argument #1 to 'Entity:has' ("..componentClass..")", 2) Utils.error(2, "bad argument #1 to 'Entity:has' (%s)", componentClass)
end end
return self[name] and true or false return self[name] and true or false
@ -158,7 +208,7 @@ function Entity:get(name)
local ok, componentClass = Components.try(name) local ok, componentClass = Components.try(name)
if not ok then if not ok then
error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) Utils.error(2, "bad argument #1 to 'Entity:get' (%s)", componentClass)
end end
return self[name] return self[name]
@ -168,8 +218,13 @@ end
-- Warning: Do not modify this table. -- Warning: Do not modify this table.
-- Use Entity:give/ensure/remove instead -- Use Entity:give/ensure/remove instead
-- @treturn table Table of all Components the Entity has -- @treturn table Table of all Components the Entity has
function Entity:getComponents() function Entity:getComponents(output)
return self.__components output = output or {}
local components = Utils.shallowCopy(self, output)
components.__world = nil
components.__isEntity = nil
return components
end end
--- Returns true if the Entity is in a World. --- Returns true if the Entity is in a World.
@ -184,11 +239,17 @@ function Entity:getWorld()
return self.__world return self.__world
end end
function Entity:serialize() function Entity:serialize(ignoreKey)
local data = {} local data = {}
for _, component in pairs(self.__components) do for name, component in pairs(self) do
if component.__name then -- 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() local componentData = component:serialize()
if componentData ~= nil then if componentData ~= nil then
@ -206,18 +267,10 @@ function Entity:deserialize(data)
local componentData = data[i] local componentData = data[i]
if (not Components.has(componentData.__name)) then if (not Components.has(componentData.__name)) then
error("bad argument #1 to 'Entity:deserialize' (ComponentClass '"..tostring(componentData.__name).."' wasn't yet loaded)") -- luacheck: ignore Utils.error(2, "bad argument #1 to 'Entity:deserialize' (ComponentClass '%s' wasn't yet loaded)", tostring(componentData.__name)) -- luacheck: ignore
end end
local componentClass = Components[componentData.__name] deserializeComponent(self, componentData.__name, componentData)
local component = componentClass:__new()
component:deserialize(componentData)
self[componentData.__name] = component
self.__components[componentData.__name] = component
self:__dirty()
end end
end end

199
concord/filter.lua Normal file
View file

@ -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,
})

View file

@ -16,7 +16,7 @@ end
--- Adds an object to the List. --- Adds an object to the List.
-- Object must be of reference type -- Object must be of reference type
-- Object may not be the string 'size' -- Object may not be the string 'size', 'onAdded' or 'onRemoved'
-- @param obj Object to add -- @param obj Object to add
-- @treturn List self -- @treturn List self
function List:add(obj) function List:add(obj)
@ -26,6 +26,7 @@ function List:add(obj)
self[obj] = size self[obj] = size
self.size = size self.size = size
if self.onAdded then self:onAdded(obj) end
return self return self
end end
@ -51,6 +52,7 @@ function List:remove(obj)
self[obj] = nil self[obj] = nil
self.size = size - 1 self.size = size - 1
if self.onRemoved then self:onRemoved(obj) end
return self return self
end end
@ -94,6 +96,30 @@ function List:indexOf(obj)
return self[obj] return self[obj]
end 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, { return setmetatable(List, {
__call = function() __call = function()
return List.new() return List.new()

View file

@ -1,111 +0,0 @@
--- Used to iterate over Entities with a specific Components
-- A Pool contain a any amount of Entities.
-- @classmod Pool
local PATH = (...):gsub('%.[^%.]+$', '')
local List = require(PATH..".list")
local Pool = {}
Pool.__mt = {
__index = Pool,
}
--- Creates a new Pool
-- @string name Name for the Pool.
-- @tparam table filter Table containing the required BaseComponents
-- @treturn Pool The new Pool
function Pool.new(name, filter)
local pool = setmetatable(List(), Pool.__mt)
pool.__name = name
pool.__filter = filter
pool.__isPool = true
return pool
end
--- Checks if an Entity is eligible for the Pool.
-- @tparam Entity e Entity to check
-- @treturn boolean
function Pool:eligible(e)
for i=#self.__filter, 1, -1 do
local component = self.__filter[i].__name
if not e[component] then return false end
end
return true
end
-- Adds an Entity to the Pool, if it can be eligible.
-- @param e Entity to add
-- @treturn Pool self
-- @treturn boolean Whether the entity was added or not
function Pool:add(e, bypass)
if not bypass and not self:eligible(e) then
return self, false
end
List.add(self, e)
self:onEntityAdded(e)
return self, true
end
-- Remove an Entity from the Pool.
-- @param e Entity to remove
-- @treturn Pool self
function Pool:remove(e)
List.remove(self, e)
self:onEntityRemoved(e)
return self
end
--- Evaluate whether an Entity should be added or removed from the Pool.
-- @param e Entity to add or remove
-- @treturn Pool self
function Pool:evaluate(e)
local has = self:has(e)
local eligible = self:eligible(e)
if not has and eligible then
self:add(e, true) --Bypass the check cause we already checked
elseif has and not eligible then
self:remove(e)
end
return self
end
--- Gets the name of the Pool
-- @treturn string
function Pool:getName()
return self.__name
end
--- Gets the filter of the Pool.
-- Warning: Do not modify this filter.
-- @return Filter of the Pool.
function Pool:getFilter()
return self.__filter
end
--- Callback for when an Entity is added to the Pool.
-- @tparam Entity e Entity that was added.
function Pool:onEntityAdded(e) -- luacheck: ignore
end
-- Callback for when an Entity is removed from the Pool.
-- @tparam Entity e Entity that was removed.
function Pool:onEntityRemoved(e) -- luacheck: ignore
end
return setmetatable(Pool, {
__index = List,
__call = function(_, ...)
return Pool.new(...)
end,
})

View file

@ -5,9 +5,8 @@
local PATH = (...):gsub('%.[^%.]+$', '') local PATH = (...):gsub('%.[^%.]+$', '')
local Pool = require(PATH..".pool") local Filter = require(PATH..".filter")
local Utils = require(PATH..".utils") local Utils = require(PATH..".utils")
local Components = require(PATH..".components")
local System = { local System = {
ENABLE_OPTIMIZATION = true, ENABLE_OPTIMIZATION = true,
@ -19,7 +18,7 @@ System.mt = {
local system = setmetatable({ local system = setmetatable({
__enabled = true, __enabled = true,
__pools = {}, __filters = {},
__world = world, __world = world,
__isSystem = true, __isSystem = true,
@ -33,11 +32,11 @@ System.mt = {
Utils.shallowCopy(systemClass, system) Utils.shallowCopy(systemClass, system)
end end
for name, filter in pairs(systemClass.__filter) do for name, def in pairs(systemClass.__definition) do
local pool = Pool(name, filter) local filter, pool = Filter(name, Utils.shallowCopy(def, {}))
system[name] = pool system[name] = pool
system.__pools[#system.__pools + 1] = pool table.insert(system.__filters, filter)
end end
system:init(world) system:init(world)
@ -45,44 +44,23 @@ System.mt = {
return system return system
end, end,
} }
local validateFilters = function (baseFilters)
local filters = {}
for name, componentsList in pairs(baseFilters) do
if type(name) ~= 'string' then
error("invalid name for filter (string key expected, got "..type(name)..")", 3)
end
if type(componentsList) ~= 'table' then
error("invalid component list for filter '"..name.."' (table expected, got "..type(componentsList)..")", 3)
end
local filter = {}
for n, component in ipairs(componentsList) do
local ok, componentClass = Components.try(component)
if not ok then
error("invalid component for filter '"..name.."' at position #"..n.." ("..componentClass..")", 3)
end
filter[#filter + 1] = componentClass
end
filters[name] = filter
end
return filters
end
--- Creates a new SystemClass. --- Creates a new SystemClass.
-- @param table filters A table containing filters (name = {components...}) -- @param table filters A table containing filters (name = {components...})
-- @treturn System A new SystemClass -- @treturn System A new SystemClass
function System.new(filters) function System.new(definition)
local systemClass = setmetatable({ definition = definition or {}
__filter = validateFilters(filters),
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,
__name = nil,
__isSystemClass = true, __isSystemClass = true,
}, System.mt) }, System.mt)
systemClass.__index = systemClass systemClass.__index = systemClass
@ -101,8 +79,8 @@ end
-- @param e The Entity to check -- @param e The Entity to check
-- @treturn System self -- @treturn System self
function System:__evaluate(e) function System:__evaluate(e)
for _, pool in ipairs(self.__pools) do for _, filter in ipairs(self.__filters) do
pool:evaluate(e) filter:evaluate(e)
end end
return self return self
@ -112,9 +90,9 @@ end
-- @param e The Entity to remove -- @param e The Entity to remove
-- @treturn System self -- @treturn System self
function System:__remove(e) function System:__remove(e)
for _, pool in ipairs(self.__pools) do for _, filter in ipairs(self.__filters) do
if pool:has(e) then if filter:has(e) then
pool:remove(e) filter:remove(e)
end end
end end
@ -124,8 +102,8 @@ end
-- Internal: Clears all Entities from the System. -- Internal: Clears all Entities from the System.
-- @treturn System self -- @treturn System self
function System:__clear() function System:__clear()
for i = 1, #self.__pools do for _, filter in ipairs(self.__filters) do
self.__pools[i]:clear() filter:clear()
end end
return self return self
@ -158,18 +136,6 @@ function System:getWorld()
return self.__world return self.__world
end end
--- Returns true if the System has a name.
-- @treturn boolean
function System:hasName()
return self.__name and true or false
end
--- Returns the name of the System.
-- @treturn string
function System:getName()
return self.__name
end
--- Callbacks --- Callbacks
-- @section Callbacks -- @section Callbacks

View file

@ -3,6 +3,15 @@
local Type = {} 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. --- Returns if object is an Entity.
-- @param t Object to check -- @param t Object to check
-- @treturn boolean -- @treturn boolean
@ -45,4 +54,11 @@ function Type.isWorld(t)
return type(t) == "table" and t.__isWorld or false return type(t) == "table" and t.__isWorld or false
end 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 return Type

View file

@ -3,6 +3,10 @@
local Utils = {} 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. --- Does a shallow copy of a table and appends it to a target table.
-- @param orig Table to copy -- @param orig Table to copy
-- @param target Table to append to -- @param target Table to append to
@ -21,39 +25,43 @@ end
-- @param namespace A table that will hold the required files -- @param namespace A table that will hold the required files
-- @treturn table The namespace table -- @treturn table The namespace table
function Utils.loadNamespace(pathOrFiles, namespace) function Utils.loadNamespace(pathOrFiles, namespace)
if (type(pathOrFiles) ~= "string" and type(pathOrFiles) ~= "table") then if type(pathOrFiles) ~= "string" and type(pathOrFiles) ~= "table" then
error("bad argument #1 to 'loadNamespace' (string/table of strings expected, got "..type(pathOrFiles)..")", 2) Utils.error(2, "bad argument #1 to 'loadNamespace' (string/table of strings expected, got %s)", type(pathOrFiles))
end end
if (type(pathOrFiles) == "string") then if type(pathOrFiles) == "string" then
local info = love.filesystem.getInfo(pathOrFiles) -- luacheck: ignore local info = love.filesystem.getInfo(pathOrFiles) -- luacheck: ignore
if (info == nil or info.type ~= "directory") then if info == nil or info.type ~= "directory" then
error("bad argument #1 to 'loadNamespace' (path '"..pathOrFiles.."' not found)", 2) Utils.error(2, "bad argument #1 to 'loadNamespace' (path '%s' not found)", pathOrFiles)
end end
local files = love.filesystem.getDirectoryItems(pathOrFiles) local files = love.filesystem.getDirectoryItems(pathOrFiles)
for _, file in ipairs(files) do for _, file in ipairs(files) do
local name = file:sub(1, #file - 4) local isFile = love.filesystem.getInfo(pathOrFiles .. "/" .. file).type == "file"
local path = pathOrFiles.."."..name
local value = require(path) if isFile and string.match(file, '%.lua$') ~= nil then
if namespace then namespace[name] = value end 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 end
elseif (type(pathOrFiles == "table")) then elseif type(pathOrFiles) == "table" then
for _, path in ipairs(pathOrFiles) do for _, path in ipairs(pathOrFiles) do
if (type(path) ~= "string") then if type(path) ~= "string" then
error("bad argument #2 to 'loadNamespace' (string/table of strings expected, got table containing "..type(path)..")", 2) -- luacheck: ignore Utils.error(2, "bad argument #2 to 'loadNamespace' (string/table of strings expected, got table containing %s)", type(path)) -- luacheck: ignore
end end
local name = path local name = path
local dotIndex, slashIndex = path:match("^.*()%."), path:match("^.*()%/") local dotIndex, slashIndex = path:match("^.*()%."), path:match("^.*()%/")
if (dotIndex or slashIndex) then if dotIndex or slashIndex then
name = path:sub((dotIndex or slashIndex) + 1) name = path:sub((dotIndex or slashIndex) + 1)
end end
local value = require(path) local value = require(path:gsub("%/", "."))
if namespace then namespace[name] = value end if namespace then namespace[name] = value end
end end
end end

View file

@ -6,10 +6,12 @@
local PATH = (...):gsub('%.[^%.]+$', '') local PATH = (...):gsub('%.[^%.]+$', '')
local Entity = require(PATH..".entity") local Filter = require(PATH..".filter")
local Type = require(PATH..".type") local Entity = require(PATH..".entity")
local List = require(PATH..".list") local Components = require(PATH..".components")
local Utils = require(PATH..".utils") local Type = require(PATH..".type")
local List = require(PATH..".list")
local Utils = require(PATH..".utils")
local World = { local World = {
ENABLE_OPTIMIZATION = true, ENABLE_OPTIMIZATION = true,
@ -18,6 +20,12 @@ World.__mt = {
__index = World, __index = World,
} }
local defaultGenerator = function (state)
local current = state
state = state +1
return string.format("%d", current), state
end
--- Creates a new World. --- Creates a new World.
-- @treturn World The new World -- @treturn World The new World
function World.new() function World.new()
@ -28,14 +36,24 @@ function World.new()
__events = {}, __events = {},
__emitSDepth = 0, __emitSDepth = 0,
__resources = {},
__hash = {
state = -2^53,
generator = defaultGenerator,
keys = {},
entities = {}
},
__added = List(), __backAdded = List(), __added = List(), __backAdded = List(),
__removed = List(), __backRemoved = List(), __removed = List(), __backRemoved = List(),
__dirty = List(), __backDirty = List(), __dirty = List(), __backDirty = List(),
__systemLookup = {}, __systemLookup = {},
__name = nil,
__isWorld = true, __isWorld = true,
__ignoreEmits = false
}, World.__mt) }, World.__mt)
-- Optimization: We deep copy the World class into our instance of a world. -- Optimization: We deep copy the World class into our instance of a world.
@ -53,7 +71,7 @@ end
-- @treturn World self -- @treturn World self
function World:addEntity(e) function World:addEntity(e)
if not Type.isEntity(e) then if not Type.isEntity(e) then
error("bad argument #1 to 'World:addEntity' (Entity expected, got "..type(e)..")", 2) Utils.error(2, "bad argument #1 to 'World:addEntity' (Entity expected, got %s)", type(e))
end end
if e.__world then if e.__world then
@ -66,12 +84,47 @@ function World:addEntity(e)
return self return self
end 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. --- Removes an Entity from the World.
-- @tparam Entity e Entity to remove -- @tparam Entity e Entity to remove
-- @treturn World self -- @treturn World self
function World:removeEntity(e) function World:removeEntity(e)
if not Type.isEntity(e) then if not Type.isEntity(e) then
error("bad argument #1 to 'World:removeEntity' (Entity expected, got "..type(e)..")", 2) 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 end
self.__removed:add(e) self.__removed:add(e)
@ -208,7 +261,7 @@ function World:addSystem(systemClass)
local ok, err = tryAddSystem(self, systemClass) local ok, err = tryAddSystem(self, systemClass)
if not ok then if not ok then
error("bad argument #1 to 'World:addSystem' ("..err..")", 2) Utils.error(2, "bad argument #1 to 'World:addSystem' (%s)", err)
end end
return self return self
@ -226,7 +279,7 @@ function World:addSystems(...)
local ok, err = tryAddSystem(self, systemClass) local ok, err = tryAddSystem(self, systemClass)
if not ok then if not ok then
error("bad argument #"..i.." to 'World:addSystems' ("..err..")", 2) Utils.error(2, "bad argument #%d to 'World:addSystems' (%s)", i, err)
end end
end end
@ -238,7 +291,7 @@ end
-- @treturn boolean -- @treturn boolean
function World:hasSystem(systemClass) function World:hasSystem(systemClass)
if not Type.isSystemClass(systemClass) then if not Type.isSystemClass(systemClass) then
error("bad argument #1 to 'World:getSystem' (systemClass expected, got "..type(systemClass)..")", 2) Utils.error(2, "bad argument #1 to 'World:hasSystem' (SystemClass expected, got %s)", type(systemClass))
end end
return self.__systemLookup[systemClass] and true or false return self.__systemLookup[systemClass] and true or false
@ -249,7 +302,7 @@ end
-- @treturn System System to get -- @treturn System System to get
function World:getSystem(systemClass) function World:getSystem(systemClass)
if not Type.isSystemClass(systemClass) then if not Type.isSystemClass(systemClass) then
error("bad argument #1 to 'World:getSystem' (systemClass expected, got "..type(systemClass)..")", 2) Utils.error(2, "bad argument #1 to 'World:getSystem' (SystemClass expected, got %s)", type(systemClass))
end end
return self.__systemLookup[systemClass] return self.__systemLookup[systemClass]
@ -262,14 +315,21 @@ end
-- @treturn World self -- @treturn World self
function World:emit(functionName, ...) function World:emit(functionName, ...)
if not functionName or type(functionName) ~= "string" then if not functionName or type(functionName) ~= "string" then
error("bad argument #1 to 'World:emit' (String expected, got "..type(functionName)..")") Utils.error(2, "bad argument #1 to 'World:emit' (String expected, got %s)", type(functionName))
end end
local shouldFlush = self.__emitSDepth == 0 local shouldFlush = self.__emitSDepth == 0
self.__emitSDepth = self.__emitSDepth + 1 self.__emitSDepth = self.__emitSDepth + 1
local listeners = self.__events[functionName] 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 if listeners then
for i = 1, #listeners do for i = 1, #listeners do
@ -285,6 +345,12 @@ function World:emit(functionName, ...)
end 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 self.__emitSDepth = self.__emitSDepth - 1
return self return self
@ -316,49 +382,103 @@ function World:getSystems()
return self.__systems return self.__systems
end end
function World:serialize() function World:serialize(ignoreKeys)
self:__flush() self:__flush()
local data = {} local data = { generator = self.__hash.state }
for i = 1, self.__entities.size do for i = 1, self.__entities.size do
local entity = self.__entities[i] local entity = self.__entities[i]
local entityData = entity:serialize() if entity.serializable then
local entityData = entity:serialize(ignoreKeys)
data[i] = entityData table.insert(data, entityData)
end
end end
return data return data
end end
function World:deserialize(data, append) function World:deserialize(data, startClean, ignoreGenerator)
if (not append) then if startClean then
self:clear() self:clear()
end end
if (not ignoreGenerator) then
self.__hash.state = data.generator
end
local entities = {}
for i = 1, #data do for i = 1, #data do
local entityData = data[i] local entity = Entity(self)
local entity = Entity() if data[i].key then
entity:deserialize(entityData) local component = Components.key:__new(entity)
component:deserialize(data[i].key)
entity.key = component
self:addEntity(entity) entity:__dirty()
end
entities[i] = entity
end
for i = 1, #data do
entities[i]:deserialize(data[i])
end end
self:__flush() self:__flush()
return self
end end
--- Returns true if the World has a name. function World:setKeyGenerator(generator, initialState)
-- @treturn boolean if not Type.isCallable(generator) then
function World:hasName() Utils.error(2, "bad argument #1 to 'World:setKeyGenerator' (function expected, got %s)", type(generator))
return self.__name and true or false end
self.__hash.generator = generator
self.__hash.state = initialState
return self
end end
--- Returns the name of the World. function World:__clearKey(e)
-- @treturn string local key = self.__hash.keys[e]
function World:getName()
return self.__name 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 end
--- Callback for when an Entity is added to the World. --- Callback for when an Entity is added to the World.
@ -371,8 +491,25 @@ end
function World:onEntityRemoved(e) -- luacheck: ignore function World:onEntityRemoved(e) -- luacheck: ignore
end 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, { return setmetatable(World, {
__call = function(_, ...) __call = function(_, ...)
---@diagnostic disable-next-line: redundant-parameter
return World.new(...) return World.new(...)
end, end,
}) })