From fd558dd3fec41a728f795fe54233ac0c142a4471 Mon Sep 17 00:00:00 2001 From: John Cheesman Date: Wed, 5 May 2021 18:32:04 +0100 Subject: [PATCH 01/43] Correct syntax in Systems example code --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2f79dcf..c63b9f7 100644 --- a/README.md +++ b/README.md @@ -266,10 +266,10 @@ 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 +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( +local mySystemClass = Concord.system({ pool = { -- This pool will be named 'pool' "position", "velocity", @@ -278,7 +278,7 @@ local mySystemClass = Concord.system( "health", "damageable", } -) +}) ``` ```lua From b53f950e3e1044805c201c9b32ab57a80818678d Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 16:30:23 +0100 Subject: [PATCH 02/43] Add documentation page building workflow --- .github/workflows/doc.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/doc.yml diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 0000000..a2215d3 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,29 @@ +name: Build Docs + +on: [push] + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + + steps: + - name: Setup Lua + uses: leafo/gh-actions-lua@v8 + with: + luaVersion: 5.4 + + - name: Setup Lua Rocks + uses: leafo/gh-actions-luarocks@v4 + + - name: Setup dependencies + run: luarocks install --only-deps https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec + + - name: Checkout & build docs + run: git clone --branch=build_docs https://github.com/lunarmodules/LDoc.git ldoc && cd ldoc && chmod +x build_versioned_docs.sh && ./build_versioned_docs.sh + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ldoc/doc_builds/ \ No newline at end of file From b701493a27b24d6c6c22a29b0aca04a19f7f6f72 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 16:32:24 +0100 Subject: [PATCH 03/43] Fix documentation page building workflow --- .github/workflows/doc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index a2215d3..3be2405 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -6,7 +6,7 @@ jobs: build: name: Build docs runs-on: ubuntu-latest - + steps: - name: Setup Lua uses: leafo/gh-actions-lua@v8 @@ -20,7 +20,7 @@ jobs: run: luarocks install --only-deps https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec - name: Checkout & build docs - run: git clone --branch=build_docs https://github.com/lunarmodules/LDoc.git ldoc && cd ldoc && chmod +x build_versioned_docs.sh && ./build_versioned_docs.sh + run: git clone https://github.com/lunarmodules/LDoc.git ldoc && cd ldoc && chmod +x build_versioned_docs.sh && ./build_versioned_docs.sh - name: Deploy uses: peaceiris/actions-gh-pages@v3 From 9aaff0fbccf94fba6ea65afb2acbfcec1e47e727 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 16:37:30 +0100 Subject: [PATCH 04/43] Fix documentation page building workflow 2 --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 3be2405..3f492b7 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -20,7 +20,7 @@ jobs: run: luarocks install --only-deps https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec - name: Checkout & build docs - run: git clone https://github.com/lunarmodules/LDoc.git ldoc && cd ldoc && chmod +x build_versioned_docs.sh && ./build_versioned_docs.sh + run: git clone https://github.com/lunarmodules/LDoc.git ldoc && lua ldoc/ldoc.lua . - name: Deploy uses: peaceiris/actions-gh-pages@v3 From 297b30aa505000b2f5d00724fc579cd3253b589a Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 16:55:04 +0100 Subject: [PATCH 05/43] Fix documentation page building workflow 3 --- .github/workflows/doc.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 3f492b7..037142f 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -19,8 +19,14 @@ jobs: - name: Setup dependencies run: luarocks install --only-deps https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec - - name: Checkout & build docs - run: git clone https://github.com/lunarmodules/LDoc.git ldoc && lua ldoc/ldoc.lua . + - name: Check out repository code + uses: actions/checkout@v2 + + - name: Checkout LDoc & build docs + run: git clone https://github.com/lunarmodules/LDoc.git ldoc + + - name: Build docs + run: lua ldoc/ldoc.lua . - name: Deploy uses: peaceiris/actions-gh-pages@v3 From 65aae3c2babbdbd224e4b6d08c5159e70762a01d Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 16:57:08 +0100 Subject: [PATCH 06/43] Fix documentation page building workflow 4 --- .github/workflows/doc.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 037142f..f0bee72 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -23,10 +23,7 @@ jobs: uses: actions/checkout@v2 - name: Checkout LDoc & build docs - run: git clone https://github.com/lunarmodules/LDoc.git ldoc - - - name: Build docs - run: lua ldoc/ldoc.lua . + run: git clone https://github.com/lunarmodules/LDoc.git ldoc && lua ldoc/ldoc.lua . - name: Deploy uses: peaceiris/actions-gh-pages@v3 From c7625ad376093a2fc6a3a58188a81ffb9caf1462 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:02:38 +0100 Subject: [PATCH 07/43] Fix documentation page building workflow 5 --- .github/workflows/doc.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index f0bee72..cfb3d8e 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -22,11 +22,11 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 - - name: Checkout LDoc & build docs + - name: Checkout & build docs run: git clone https://github.com/lunarmodules/LDoc.git ldoc && lua ldoc/ldoc.lua . - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ldoc/doc_builds/ \ No newline at end of file + # - name: Deploy + # uses: peaceiris/actions-gh-pages@v3 + # with: + # github_token: ${{ secrets.GITHUB_TOKEN }} + # publish_dir: ldoc/doc_builds/ \ No newline at end of file From cdf425d301b5dd4d2cb80cc06b985d3cf0d674d8 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:08:25 +0100 Subject: [PATCH 08/43] Fix documentation page building workflow 6 --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index cfb3d8e..93af3e4 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v2 - name: Checkout & build docs - run: git clone https://github.com/lunarmodules/LDoc.git ldoc && lua ldoc/ldoc.lua . + run: git clone https://github.com/lunarmodules/LDoc.git ldoc && .lua/bin/lua ldoc/ldoc.lua . # - name: Deploy # uses: peaceiris/actions-gh-pages@v3 From 6329e09138c6df777c46800a12a9dabcdf4ee27e Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:16:51 +0100 Subject: [PATCH 09/43] Fix documentation page building workflow 7 --- .github/workflows/doc.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 93af3e4..aebf0c5 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -8,25 +8,29 @@ jobs: 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: Setup Lua Rocks + - name: Install Luarocks uses: leafo/gh-actions-luarocks@v4 - - name: Setup dependencies - run: luarocks install --only-deps https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec + - name: Install LDoc + run: luarocks install ldoc + + - name: Show + run: luarocks show ldoc - - name: Check out repository code - uses: actions/checkout@v2 + - name: Build docs + run: lua ldoc.lua . - - name: Checkout & build docs - run: git clone https://github.com/lunarmodules/LDoc.git ldoc && .lua/bin/lua ldoc/ldoc.lua . - # - name: Deploy - # uses: peaceiris/actions-gh-pages@v3 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # publish_dir: ldoc/doc_builds/ \ No newline at end of file + # - name: Deploy + # uses: JamesIves/github-pages-deploy-action@4.1.5 + # with: + # branch: gh-pages + # folder: docs \ No newline at end of file From e141a6183bf5547bf43e842f8566425334f02e1c Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:18:57 +0100 Subject: [PATCH 10/43] Fix documentation page building workflow 8 --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index aebf0c5..ec4c9ec 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -26,7 +26,7 @@ jobs: run: luarocks show ldoc - name: Build docs - run: lua ldoc.lua . + run: ldoc . # - name: Deploy From 6ce714ce14f301ed3ba2cdf99bf14c479e49a429 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:21:22 +0100 Subject: [PATCH 11/43] Fix documentation page building workflow 9 --- .github/workflows/doc.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index ec4c9ec..4acd139 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -29,8 +29,8 @@ jobs: 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 + - name: Deploy + uses: JamesIves/github-pages-deploy-action@4.1.5 + with: + branch: gh-pages + folder: docs \ No newline at end of file From 73177f4048a6bb02a2f18e4359a03be0e5cfba5a Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:21:51 +0100 Subject: [PATCH 12/43] Fix documentation page building workflow 10 --- .github/workflows/doc.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 4acd139..ee2d94e 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -28,9 +28,8 @@ jobs: - 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 + - name: Deploy + uses: JamesIves/github-pages-deploy-action@4.1.5 + with: + branch: gh-pages + folder: docs \ No newline at end of file From 9c22986501e3777f2e7945080fa7af43c2c2dff3 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:23:53 +0100 Subject: [PATCH 13/43] Fix documentation page building workflow 11 --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index ee2d94e..8ac8c37 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -32,4 +32,4 @@ jobs: uses: JamesIves/github-pages-deploy-action@4.1.5 with: branch: gh-pages - folder: docs \ No newline at end of file + folder: concord/docs \ No newline at end of file From 4313dc78567f60394b4658cd0b7aea59a315a9b7 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 17:27:18 +0100 Subject: [PATCH 14/43] Fix documentation page building workflow 12 --- .github/workflows/doc.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 8ac8c37..5e5ae1e 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -1,6 +1,9 @@ name: Build Docs -on: [push] +on: + push: + branches: + - master jobs: build: From edc1d2fdbcb7776cc36a9d02d2a840dec548210a Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Tue, 2 Nov 2021 20:10:14 +0100 Subject: [PATCH 15/43] Fix documentation page building workflow 13 --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 5e5ae1e..ca60d46 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -35,4 +35,4 @@ jobs: uses: JamesIves/github-pages-deploy-action@4.1.5 with: branch: gh-pages - folder: concord/docs \ No newline at end of file + folder: docs \ No newline at end of file From 2acb1458f217bd828bb05246a4930bd685c6a7cc Mon Sep 17 00:00:00 2001 From: Josh Perry Date: Tue, 31 May 2022 20:20:54 +0100 Subject: [PATCH 16/43] Fixed docs link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f79dcf..aecc33c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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://tjakka5.github.io/Concord/). +Auto generated docs for Concord can be found in `docs` folder, or on the [GitHub page](https://keyslam.github.io/Concord/). --- From 940870318d1225ed3106355f48fac13f0d21d35c Mon Sep 17 00:00:00 2001 From: Tachytaenius Date: Mon, 17 Oct 2022 20:17:54 +0100 Subject: [PATCH 17/43] Fix bug where loading namespaces using folder/init.lua entries would only work if "folder" was 3 characters --- concord/utils.lua | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/concord/utils.lua b/concord/utils.lua index da83c4d..92e19aa 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -34,11 +34,18 @@ function Utils.loadNamespace(pathOrFiles, namespace) local files = love.filesystem.getDirectoryItems(pathOrFiles) for _, file in ipairs(files) do - local name = file:sub(1, #file - 4) - local path = pathOrFiles.."."..name + local isFile = love.filesystem.getInfo(pathOrFiles .. "/" .. file).type == "file" - local value = require(path) - if namespace then namespace[name] = value end + if isFile then + local name = file:sub(1, #file - 4) + local path = pathOrFiles.."."..name + + local value = require(path) + if namespace then namespace[name] = value end + else + local value = require(pathOrFiles.."."..file) + if namespace then namespace[file] = value end + end end elseif (type(pathOrFiles == "table")) then for _, path in ipairs(pathOrFiles) do @@ -61,4 +68,4 @@ function Utils.loadNamespace(pathOrFiles, namespace) return namespace end -return Utils \ No newline at end of file +return Utils From 89eab3fb72597a1bef2d63e78fa4cf34810a549c Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:08 -0300 Subject: [PATCH 18/43] List.sort Fixes #33 --- concord/list.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/concord/list.lua b/concord/list.lua index 1263a72..88dfc04 100644 --- a/concord/list.lua +++ b/concord/list.lua @@ -94,6 +94,20 @@ function List:indexOf(obj) 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 + return setmetatable(List, { __call = function() return List.new() From 695cc2dfe37853226090e101b41dedbddaf74edb Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:22 -0300 Subject: [PATCH 19/43] Add Component Negation Fixes #32 --- concord/component.lua | 4 ++++ concord/components.lua | 32 ++++++++++++++++++++++++++------ concord/pool.lua | 10 +++++++--- concord/system.lua | 25 ++++++++++++++----------- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/concord/component.lua b/concord/component.lua index d8678f0..7a9db98 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -19,6 +19,10 @@ function Component.new(name, populate) error("bad argument #1 to 'Component.new' (string expected, got "..type(name)..")", 2) end + if (string.match(name, Components.__REJECT_MATCH) ~= "") then + error("bad argument #1 to 'Component.new' (Component names can't start with '"..Components.__REJECT_PREFIX.."', got "..name..")", 2) + end + if (rawget(Components, name)) then error("bad argument #1 to 'Component.new' (ComponentClass with name '"..name.."' was already registerd)", 2) -- luacheck: ignore end diff --git a/concord/components.lua b/concord/components.lua index bf20d2d..b2743e7 100644 --- a/concord/components.lua +++ b/concord/components.lua @@ -1,12 +1,11 @@ --- Container for registered ComponentClasses -- @module Components -local PATH = (...):gsub('%.[^%.]+$', '') - -local Type = require(PATH..".type") - 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 @@ -14,22 +13,43 @@ 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 -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 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 + return true, value, rejected end --- Returns the ComponentClass with the specified name diff --git a/concord/pool.lua b/concord/pool.lua index f1c80eb..f5cd401 100644 --- a/concord/pool.lua +++ b/concord/pool.lua @@ -30,10 +30,14 @@ end -- @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 + for i=#self.__filter.require, 1, -1 do + local name = self.__filter.require[i] + if not e[name] then return false end + end - if not e[component] then return false end + for i=#self.__filter.reject, 1, -1 do + local name = self.__filter.reject[i] + if e[name] then return false end end return true diff --git a/concord/system.lua b/concord/system.lua index 756b124..fe0c9fe 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -33,7 +33,7 @@ System.mt = { Utils.shallowCopy(systemClass, system) end - for name, filter in pairs(systemClass.__filter) do + for name, filter in pairs(systemClass.__filters) do local pool = Pool(name, filter) system[name] = pool @@ -46,10 +46,10 @@ System.mt = { end, } -local validateFilters = function (baseFilters) +local validateFilters = function (definition) local filters = {} - for name, componentsList in pairs(baseFilters) do + for name, componentsList in pairs(definition) do if type(name) ~= 'string' then error("invalid name for filter (string key expected, got "..type(name)..")", 3) end @@ -58,18 +58,19 @@ local validateFilters = function (baseFilters) error("invalid component list for filter '"..name.."' (table expected, got "..type(componentsList)..")", 3) end - local filter = {} + filters[name] = { require = {}, reject = {} } + for n, component in ipairs(componentsList) do - local ok, componentClass = Components.try(component) + local ok, componentClass, rejected = Components.try(component, true) if not ok then error("invalid component for filter '"..name.."' at position #"..n.." ("..componentClass..")", 3) + elseif rejected then + table.insert(filters[name].reject, rejected) + else + table.insert(filters[name].require, component) end - - filter[#filter + 1] = componentClass end - - filters[name] = filter end return filters @@ -78,9 +79,11 @@ end --- Creates a new SystemClass. -- @param table filters A table containing filters (name = {components...}) -- @treturn System A new SystemClass -function System.new(filters) +function System.new(definition) + local filters = validateFilters(definition) + local systemClass = setmetatable({ - __filter = validateFilters(filters), + __filters = filters, __name = nil, __isSystemClass = true, From 50249d5ad334db4bc93ebe88293aabd3191a00f7 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:23 -0300 Subject: [PATCH 20/43] 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. --- concord/entity.lua | 23 ++++++++++++----------- concord/system.lua | 13 ------------- concord/world.lua | 13 ------------- 3 files changed, 12 insertions(+), 37 deletions(-) diff --git a/concord/entity.lua b/concord/entity.lua index 854fa8c..21a09cb 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -6,6 +6,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local Components = require(PATH..".components") local Type = require(PATH..".type") +local Utils = require(PATH..".utils") local Entity = {} Entity.__mt = { @@ -22,7 +23,6 @@ function Entity.new(world) local e = setmetatable({ __world = nil, - __components = {}, __isEntity = true, }, Entity.__mt) @@ -38,14 +38,12 @@ local function give(e, name, componentClass, ...) local component = componentClass:__initialize(...) e[name] = component - e.__components[name] = component e:__dirty() end -local function remove(e, name, componentClass) +local function remove(e, name) e[name] = nil - e.__components[name] = nil e:__dirty() end @@ -76,7 +74,7 @@ function Entity:ensure(name, ...) local ok, componentClass = Components.try(name) if not ok then - error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) + error("bad argument #1 to 'Entity:ensure' ("..componentClass..")", 2) end if self[name] then @@ -95,10 +93,10 @@ function Entity:remove(name) local ok, componentClass = Components.try(name) if not ok then - error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) + error("bad argument #1 to 'Entity:remove' ("..componentClass..")", 2) end - remove(self, name, componentClass) + remove(self, name) return self end @@ -169,7 +167,11 @@ end -- Use Entity:give/ensure/remove instead -- @treturn table Table of all Components the Entity has function Entity:getComponents() - return self.__components + local components = Utils.shallowCopy(self) + components.__world = nil + components.__isEntity = nil + + return components end --- Returns true if the Entity is in a World. @@ -187,8 +189,8 @@ end function Entity:serialize() local data = {} - for _, component in pairs(self.__components) do - if component.__name then + for name, component in pairs(self) do + if name ~= "__world" and name ~= "__isEntity" and component.__name == name then local componentData = component:serialize() if componentData ~= nil then @@ -215,7 +217,6 @@ function Entity:deserialize(data) component:deserialize(componentData) self[componentData.__name] = component - self.__components[componentData.__name] = component self:__dirty() end diff --git a/concord/system.lua b/concord/system.lua index fe0c9fe..3db2a44 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -85,7 +85,6 @@ function System.new(definition) local systemClass = setmetatable({ __filters = filters, - __name = nil, __isSystemClass = true, }, System.mt) systemClass.__index = systemClass @@ -161,18 +160,6 @@ function System:getWorld() return self.__world 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 -- @section Callbacks diff --git a/concord/world.lua b/concord/world.lua index e874881..b55f030 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -34,7 +34,6 @@ function World.new() __systemLookup = {}, - __name = nil, __isWorld = true, }, World.__mt) @@ -349,18 +348,6 @@ function World:deserialize(data, append) self:__flush() end ---- Returns true if the World has a name. --- @treturn boolean -function World:hasName() - return self.__name and true or false -end - ---- Returns the name of the World. --- @treturn string -function World:getName() - return self.__name -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 From c4594da19d0f154676415dbb8ad77ef53b1732b8 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:23 -0300 Subject: [PATCH 21/43] Add Component:removed() callback Fixes #37 I also added a reference to the Entity inside the Component which will help with #38 --- concord/component.lua | 20 ++++++++++++++------ concord/entity.lua | 19 +++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/concord/component.lua b/concord/component.lua index 7a9db98..5122e9a 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -47,31 +47,39 @@ function Component.new(name, populate) return componentClass end --- Internal: Populates a Component with values +-- 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.__isComponent = nil + 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. -- @return A new Component -function Component:__new() +function Component:__new(entity) local component = setmetatable({ __componentClass = self, + __entity = entity, __isComponent = true, __isComponentClass = false, }, self.__mt) @@ -82,8 +90,8 @@ end -- Internal: Creates and populates a new Component. -- @param ... Varargs passed to the populate function -- @return A new populated Component -function Component:__initialize(...) - local component = self:__new() +function Component:__initialize(entity, ...) + local component = self:__new(entity) self.__populate(component, ...) diff --git a/concord/entity.lua b/concord/entity.lua index 21a09cb..7bdbdc2 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -35,17 +35,28 @@ function Entity.new(world) end local function give(e, name, componentClass, ...) - local component = componentClass:__initialize(...) + local component = componentClass:__initialize(e, ...) + local hadComponent = not not e[name] + + if hadComponent then + e[name]:removed() + end e[name] = component - e:__dirty() + if not hadComponent then + e:__dirty() + end end local function remove(e, name) - e[name] = nil + if e[name] then + e[name]:removed() - e:__dirty() + e[name] = nil + + e:__dirty() + end end --- Gives an Entity a Component. From 07bd5d0f280b14c51ed17536e9b66b753fb17193 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:23 -0300 Subject: [PATCH 22/43] Fix utils.loadNamespace when passed a table --- concord/utils.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/concord/utils.lua b/concord/utils.lua index 92e19aa..2d3d742 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -21,13 +21,13 @@ end -- @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 + if type(pathOrFiles) ~= "string" and type(pathOrFiles) ~= "table" then error("bad argument #1 to 'loadNamespace' (string/table of strings expected, got "..type(pathOrFiles)..")", 2) end - if (type(pathOrFiles) == "string") then + if type(pathOrFiles) == "string" then 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) end @@ -47,16 +47,16 @@ function Utils.loadNamespace(pathOrFiles, namespace) if namespace then namespace[file] = value end end end - elseif (type(pathOrFiles == "table")) then + elseif type(pathOrFiles) == "table" then 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 end local name = path local dotIndex, slashIndex = path:match("^.*()%."), path:match("^.*()%/") - if (dotIndex or slashIndex) then + if dotIndex or slashIndex then name = path:sub((dotIndex or slashIndex) + 1) end From 743d662ef9089bb4884d9d83c44c6e0feaeda0cf Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:23 -0300 Subject: [PATCH 23/43] 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 --- .gitignore | 5 +- concord/filter.lua | 168 +++++++++++++++++++++++++++++++++++++++++++++ concord/list.lua | 14 +++- concord/pool.lua | 115 ------------------------------- concord/system.lua | 66 ++++++------------ concord/type.lua | 16 +++++ 6 files changed, 221 insertions(+), 163 deletions(-) create mode 100644 concord/filter.lua delete mode 100644 concord/pool.lua 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/concord/filter.lua b/concord/filter.lua new file mode 100644 index 0000000..a959609 --- /dev/null +++ b/concord/filter.lua @@ -0,0 +1,168 @@ +--- 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 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 (name, def, onComponent) + if type(def) ~= 'table' then + error("invalid component list for filter '"..name.."' (table expected, got "..type(def)..")", 3) + end + + if not onComponent and def.constructor and not Type.isCallable(def.constructor) then + error("invalid pool constructor (callable expected, got "..type(def.constructor)..")", 3) + end + + for n, component in ipairs(def) do + local ok, err, reject = Components.try(component, true) + + if not ok then + error("invalid component for filter '"..name.."' at position #"..n.." ("..err..")", 3) + 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 required, rejected = {}, {} + + Filter.validate(name, def, function (component, reject) + if reject then + table.insert(rejected, reject) + else + table.insert(required, component) + end + end) + + return required, rejected +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 constructor = def.constructor or List + local pool = constructor(def) + + local required, rejected = Filter.parse(name, def) + + local filter = setmetatable({ + pool = pool, + + __required = required, + __rejected = rejected, + + __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) + for i=#self.__required, 1, -1 do + local name = self.__required[i] + if not e[name] then return false end + end + + for i=#self.__rejected, 1, -1 do + local name = self.__rejected[i] + if e[name] then return false end + end + + return true +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/list.lua b/concord/list.lua index 88dfc04..7f10c74 100644 --- a/concord/list.lua +++ b/concord/list.lua @@ -16,7 +16,7 @@ end --- Adds an object to the List. -- 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 -- @treturn List self function List:add(obj) @@ -26,6 +26,7 @@ function List:add(obj) self[obj] = size self.size = size + if self.onAdded then self:onAdded(obj) end return self end @@ -51,6 +52,7 @@ function List:remove(obj) self[obj] = nil self.size = size - 1 + if self.onRemoved then self:onRemoved(obj) end return self end @@ -108,6 +110,16 @@ function List:sort(order) 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() diff --git a/concord/pool.lua b/concord/pool.lua deleted file mode 100644 index f5cd401..0000000 --- a/concord/pool.lua +++ /dev/null @@ -1,115 +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.require, 1, -1 do - local name = self.__filter.require[i] - if not e[name] then return false end - end - - for i=#self.__filter.reject, 1, -1 do - local name = self.__filter.reject[i] - if e[name] 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, -}) diff --git a/concord/system.lua b/concord/system.lua index 3db2a44..2d3a802 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -5,9 +5,8 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Pool = require(PATH..".pool") +local Filter = require(PATH..".filter") local Utils = require(PATH..".utils") -local Components = require(PATH..".components") local System = { ENABLE_OPTIMIZATION = true, @@ -19,7 +18,7 @@ System.mt = { local system = setmetatable({ __enabled = true, - __pools = {}, + __filters = {}, __world = world, __isSystem = true, @@ -33,11 +32,11 @@ System.mt = { Utils.shallowCopy(systemClass, system) end - for name, filter in pairs(systemClass.__filters) do - local pool = Pool(name, filter) + for name, def in pairs(systemClass.__definition) do + local filter, pool = Filter(name, Utils.shallowCopy(def, {})) system[name] = pool - system.__pools[#system.__pools + 1] = pool + table.insert(system.__filters, filter) end system:init(world) @@ -45,45 +44,20 @@ System.mt = { return system end, } - -local validateFilters = function (definition) - local filters = {} - - for name, componentsList in pairs(definition) 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 - - filters[name] = { require = {}, reject = {} } - - for n, component in ipairs(componentsList) do - local ok, componentClass, rejected = Components.try(component, true) - - if not ok then - error("invalid component for filter '"..name.."' at position #"..n.." ("..componentClass..")", 3) - elseif rejected then - table.insert(filters[name].reject, rejected) - else - table.insert(filters[name].require, component) - end - end - end - - return filters -end - --- Creates a new SystemClass. -- @param table filters A table containing filters (name = {components...}) -- @treturn System A new SystemClass function System.new(definition) - local filters = validateFilters(definition) + for name, def in pairs(definition) do + if type(name) ~= 'string' then + error("invalid name for filter (string key expected, got "..type(name)..")", 2) + end + + Filter.validate(name, def) + end local systemClass = setmetatable({ - __filters = filters, + __definition = definition, __isSystemClass = true, }, System.mt) @@ -103,8 +77,8 @@ end -- @param e The Entity to check -- @treturn System self function System:__evaluate(e) - for _, pool in ipairs(self.__pools) do - pool:evaluate(e) + for _, filter in ipairs(self.__filters) do + filter:evaluate(e) end return self @@ -114,9 +88,9 @@ end -- @param e The Entity to remove -- @treturn System self function System:__remove(e) - for _, pool in ipairs(self.__pools) do - if pool:has(e) then - pool:remove(e) + for _, filter in ipairs(self.__filters) do + if filter:has(e) then + filter:remove(e) end end @@ -126,8 +100,8 @@ end -- Internal: Clears all Entities from the System. -- @treturn System self function System:__clear() - for i = 1, #self.__pools do - self.__pools[i]:clear() + for _, filter in ipairs(self.__filters) do + filter:clear() end return self diff --git a/concord/type.lua b/concord/type.lua index 157a8d4..bf7f52a 100644 --- a/concord/type.lua +++ b/concord/type.lua @@ -3,6 +3,15 @@ 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 @@ -45,4 +54,11 @@ 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 From 892f4d47000e72678a20518607bc5a14f2ae78fc Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:24 -0300 Subject: [PATCH 24/43] Error handling overhaul --- concord/component.lua | 8 ++++---- concord/entity.lua | 16 ++++++++-------- concord/filter.lua | 35 ++++++++++++++++++++++++++++++----- concord/system.lua | 2 +- concord/utils.lua | 10 +++++++--- concord/world.lua | 14 +++++++------- 6 files changed, 57 insertions(+), 28 deletions(-) diff --git a/concord/component.lua b/concord/component.lua index 5122e9a..351d00c 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -16,19 +16,19 @@ Component.__mt = { -- @treturn Component A new ComponentClass function Component.new(name, populate) 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 - error("bad argument #1 to 'Component.new' (Component names can't start with '"..Components.__REJECT_PREFIX.."', got "..name..")", 2) + 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 - 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 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 local componentClass = setmetatable({ diff --git a/concord/entity.lua b/concord/entity.lua index 7bdbdc2..fb4617c 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -18,7 +18,7 @@ Entity.__mt = { -- @treturn Entity A new Entity function Entity.new(world) 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 local e = setmetatable({ @@ -68,7 +68,7 @@ function Entity:give(name, ...) local ok, componentClass = Components.try(name) 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 give(self, name, componentClass, ...) @@ -85,7 +85,7 @@ function Entity:ensure(name, ...) local ok, componentClass = Components.try(name) if not ok then - error("bad argument #1 to 'Entity:ensure' ("..componentClass..")", 2) + Utils.error(2, "bad argument #1 to 'Entity:ensure' (%s)", componentClass) end if self[name] then @@ -104,7 +104,7 @@ function Entity:remove(name) local ok, componentClass = Components.try(name) if not ok then - error("bad argument #1 to 'Entity:remove' ("..componentClass..")", 2) + Utils.error(2, "bad argument #1 to 'Entity:remove' (%s)", componentClass) end remove(self, name) @@ -118,7 +118,7 @@ end -- @treturn Entity self function Entity:assemble(assemblage, ...) 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 assemblage(self, ...) @@ -154,7 +154,7 @@ function Entity:has(name) local ok, componentClass = Components.try(name) 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 return self[name] and true or false @@ -167,7 +167,7 @@ function Entity:get(name) local ok, componentClass = Components.try(name) 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 return self[name] @@ -219,7 +219,7 @@ function Entity:deserialize(data) local componentData = data[i] 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 local componentClass = Components[componentData.__name] diff --git a/concord/filter.lua b/concord/filter.lua index a959609..f0f9262 100644 --- a/concord/filter.lua +++ b/concord/filter.lua @@ -6,6 +6,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') local List = require(PATH..".list") local Type = require(PATH..".type") +local Utils = require(PATH..".utils") local Components = require(PATH..".components") @@ -20,18 +21,18 @@ Filter.__mt = { -- @tparam onComponent Optional function, called when a component is valid. function Filter.validate (name, def, onComponent) if type(def) ~= 'table' then - error("invalid component list for filter '"..name.."' (table expected, got "..type(def)..")", 3) + Utils.error(3, "invalid component list for filter '%s' (table expected, got %s)", name, type(def)) end if not onComponent and def.constructor and not Type.isCallable(def.constructor) then - error("invalid pool constructor (callable expected, got "..type(def.constructor)..")", 3) + Utils.error(3, "invalid pool constructor for filter '%s' (callable expected, got %s)", name, type(def.constructor)) end for n, component in ipairs(def) do local ok, err, reject = Components.try(component, true) if not ok then - error("invalid component for filter '"..name.."' at position #"..n.." ("..err..")", 3) + Utils.error(3, "invalid component for filter '%s' at position #%d (%s)", name, n, err) end if onComponent then @@ -61,14 +62,38 @@ function Filter.parse (name, def) return required, rejected 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 constructor = def.constructor or List - local pool = constructor(def) + local pool + + if def.constructor then + pool = def.constructor(def) + Filter.isValidPool(name, pool) + else + pool = List() + end local required, rejected = Filter.parse(name, def) diff --git a/concord/system.lua b/concord/system.lua index 2d3a802..6ed6d6c 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -50,7 +50,7 @@ System.mt = { function System.new(definition) for name, def in pairs(definition) do if type(name) ~= 'string' then - error("invalid name for filter (string key expected, got "..type(name)..")", 2) + Utils.error(2, "invalid name for filter (string key expected, got %s)", type(name)) end Filter.validate(name, def) diff --git a/concord/utils.lua b/concord/utils.lua index 2d3d742..1bf8710 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -3,6 +3,10 @@ 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 @@ -22,13 +26,13 @@ end -- @treturn table The namespace table function Utils.loadNamespace(pathOrFiles, namespace) 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 if type(pathOrFiles) == "string" then local info = love.filesystem.getInfo(pathOrFiles) -- luacheck: ignore 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 local files = love.filesystem.getDirectoryItems(pathOrFiles) @@ -50,7 +54,7 @@ function Utils.loadNamespace(pathOrFiles, namespace) elseif type(pathOrFiles) == "table" then for _, path in ipairs(pathOrFiles) do 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 local name = path diff --git a/concord/world.lua b/concord/world.lua index b55f030..bdd0af5 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -52,7 +52,7 @@ end -- @treturn World self function World:addEntity(e) 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 if e.__world then @@ -70,7 +70,7 @@ end -- @treturn World self function World:removeEntity(e) 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 self.__removed:add(e) @@ -207,7 +207,7 @@ function World:addSystem(systemClass) local ok, err = tryAddSystem(self, systemClass) 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 return self @@ -225,7 +225,7 @@ function World:addSystems(...) local ok, err = tryAddSystem(self, systemClass) 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 @@ -237,7 +237,7 @@ end -- @treturn boolean function World:hasSystem(systemClass) 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 return self.__systemLookup[systemClass] and true or false @@ -248,7 +248,7 @@ end -- @treturn System System to get function World:getSystem(systemClass) 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 return self.__systemLookup[systemClass] @@ -261,7 +261,7 @@ end -- @treturn World self function World:emit(functionName, ...) 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 local shouldFlush = self.__emitSDepth == 0 From cc0fd1614ce421e6a640c71a3226446f230bea34 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:24 -0300 Subject: [PATCH 25/43] 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. --- concord/builtins/init.lua | 5 +++++ concord/builtins/serializable.lua | 12 ++++++++++++ concord/entity.lua | 12 +++++++++++- concord/world.lua | 7 ++++--- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 concord/builtins/init.lua create mode 100644 concord/builtins/serializable.lua diff --git a/concord/builtins/init.lua b/concord/builtins/init.lua new file mode 100644 index 0000000..d5b749d --- /dev/null +++ b/concord/builtins/init.lua @@ -0,0 +1,5 @@ +local PATH = (...):gsub("(%.init)$", "") + +return { + serializable = require(PATH..".serializable"), +} 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/entity.lua b/concord/entity.lua index fb4617c..ca972e9 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -8,7 +8,13 @@ local Components = require(PATH..".components") local Type = require(PATH..".type") local Utils = require(PATH..".utils") -local Entity = {} +-- Initialize built-in Components (as soon as possible) +local Builtins = require(PATH..".builtins") + +local Entity = { + SERIALIZE_BY_DEFAULT = true, +} + Entity.__mt = { __index = Entity, } @@ -31,6 +37,10 @@ function Entity.new(world) world:addEntity(e) end + if Entity.SERIALIZE_BY_DEFAULT then + e:give("serializable") + end + return e end diff --git a/concord/world.lua b/concord/world.lua index bdd0af5..71b3485 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -323,9 +323,10 @@ function World:serialize() for i = 1, self.__entities.size do local entity = self.__entities[i] - local entityData = entity:serialize() - - data[i] = entityData + if entity.serializable then + local entityData = entity:serialize() + table.insert(data, entityData) + end end return data From a4ae392341b49c32740ae5efffc5966c2ccb307c Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:24 -0300 Subject: [PATCH 26/43] World:newEntity A shortcut for Concord.entity(World) --- concord/world.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/concord/world.lua b/concord/world.lua index 71b3485..efc5a23 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -65,6 +65,12 @@ function World:addEntity(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 + --- Removes an Entity from the World. -- @tparam Entity e Entity to remove -- @treturn World self From 3d195c790f22e04f21f9f285ecf6b1567c4372c7 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:24 -0300 Subject: [PATCH 27/43] Add World:query This method allows you to query the World in order to find a set of Entities that matches a specific Filter. --- concord/filter.lua | 54 +++++++++++++++++++++++++--------------------- concord/system.lua | 2 +- concord/world.lua | 22 +++++++++++++++++++ 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/concord/filter.lua b/concord/filter.lua index f0f9262..0c4dabf 100644 --- a/concord/filter.lua +++ b/concord/filter.lua @@ -19,20 +19,25 @@ Filter.__mt = { -- @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 (name, def, onComponent) +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, "invalid component list for filter '%s' (table expected, got %s)", name, type(def)) + 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, "invalid pool constructor for filter '%s' (callable expected, got %s)", name, type(def.constructor)) + 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, "invalid component for filter '%s' at position #%d (%s)", name, n, err) + Utils.error(3 + errorLevel, "invalid component for %s at position #%d (%s)", filter, n, err) end if onComponent then @@ -49,17 +54,30 @@ end -- @treturn table required -- @treturn table rejected function Filter.parse (name, def) - local required, rejected = {}, {} + local filter = {} - Filter.validate(name, def, function (component, reject) + Filter.validate(1, name, def, function (component, reject) if reject then - table.insert(rejected, reject) + table.insert(filter, reject) + table.insert(filter, false) else - table.insert(required, component) + table.insert(filter, component) + table.insert(filter, true) end end) - return required, rejected + 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"} @@ -95,14 +113,12 @@ function Filter.new (name, def) pool = List() end - local required, rejected = Filter.parse(name, def) + local filter = Filter.parse(name, def) local filter = setmetatable({ pool = pool, - __required = required, - __rejected = rejected, - + __filter = filter, __name = name, __isFilter = true, @@ -115,17 +131,7 @@ end -- @tparam Entity e Entity to check -- @treturn boolean function Filter:eligible(e) - for i=#self.__required, 1, -1 do - local name = self.__required[i] - if not e[name] then return false end - end - - for i=#self.__rejected, 1, -1 do - local name = self.__rejected[i] - if e[name] then return false end - end - - return true + return Filter.match(e, self.__filter) end function Filter:evaluate (e) diff --git a/concord/system.lua b/concord/system.lua index 6ed6d6c..53731ff 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -53,7 +53,7 @@ function System.new(definition) Utils.error(2, "invalid name for filter (string key expected, got %s)", type(name)) end - Filter.validate(name, def) + Filter.validate(0, name, def) end local systemClass = setmetatable({ diff --git a/concord/world.lua b/concord/world.lua index efc5a23..e4aa48d 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -6,6 +6,7 @@ local PATH = (...):gsub('%.[^%.]+$', '') +local Filter = require(PATH..".filter") local Entity = require(PATH..".entity") local Type = require(PATH..".type") local List = require(PATH..".list") @@ -71,6 +72,27 @@ 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 From a55efd042a6275749ad589a78ee2383865d0d645 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:25 -0300 Subject: [PATCH 28/43] 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. --- concord/builtins/init.lua | 1 + concord/builtins/key.lua | 37 ++++++++++++++ concord/component.lua | 2 + concord/entity.lua | 18 ++++--- concord/system.lua | 2 + concord/world.lua | 103 ++++++++++++++++++++++++++++++++++---- 6 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 concord/builtins/key.lua diff --git a/concord/builtins/init.lua b/concord/builtins/init.lua index d5b749d..e3ab37b 100644 --- a/concord/builtins/init.lua +++ b/concord/builtins/init.lua @@ -2,4 +2,5 @@ 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..f083bd4 --- /dev/null +++ b/concord/builtins/key.lua @@ -0,0 +1,37 @@ +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.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 \ No newline at end of file diff --git a/concord/component.lua b/concord/component.lua index 351d00c..2b43779 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -74,6 +74,7 @@ function Component:deserialize(data) 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({ @@ -88,6 +89,7 @@ function Component:__new(entity) 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, ...) diff --git a/concord/entity.lua b/concord/entity.lua index ca972e9..120146f 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -9,7 +9,8 @@ local Type = require(PATH..".type") local Utils = require(PATH..".utils") -- Initialize built-in Components (as soon as possible) -local Builtins = require(PATH..".builtins") +local Builtins = require(PATH..".builtins.init") --luacheck: ignore +-- Builtins is unused but the require already registers the Components local Entity = { SERIALIZE_BY_DEFAULT = true, @@ -49,7 +50,7 @@ local function give(e, name, componentClass, ...) local hadComponent = not not e[name] if hadComponent then - e[name]:removed() + e[name]:removed(true) end e[name] = component @@ -61,7 +62,7 @@ end local function remove(e, name) if e[name] then - e[name]:removed() + e[name]:removed(false) e[name] = nil @@ -207,11 +208,16 @@ function Entity:getWorld() return self.__world end -function Entity:serialize() +function Entity:serialize(ignoreKey) local data = {} for name, component in pairs(self) do - if name ~= "__world" and name ~= "__isEntity" and component.__name == 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 + elseif (name ~= "__world") and (name ~= "__isEntity") and (component.__name == name) then local componentData = component:serialize() if componentData ~= nil then @@ -234,7 +240,7 @@ function Entity:deserialize(data) local componentClass = Components[componentData.__name] - local component = componentClass:__new() + local component = componentClass:__new(self) component:deserialize(componentData) self[componentData.__name] = component diff --git a/concord/system.lua b/concord/system.lua index 53731ff..e9d45d6 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -48,6 +48,8 @@ System.mt = { -- @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)) diff --git a/concord/world.lua b/concord/world.lua index e4aa48d..85a19e8 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -19,6 +19,12 @@ 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() @@ -29,6 +35,13 @@ function World.new() __events = {}, __emitSDepth = 0, + __hash = { + state = -2^53, + generator = defaultGenerator, + keys = {}, + entities = {} + }, + __added = List(), __backAdded = List(), __removed = List(), __backRemoved = List(), __dirty = List(), __backDirty = List(), @@ -101,6 +114,14 @@ function World:removeEntity(e) 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 @@ -343,16 +364,16 @@ function World:getSystems() return self.__systems end -function World:serialize() +function World:serialize(ignoreKeys) self:__flush() - local data = {} + 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() + local entityData = entity:serialize(ignoreKeys) table.insert(data, entityData) end end @@ -360,21 +381,85 @@ function World:serialize() return data end -function World:deserialize(data, append) - if (not append) then +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 entityData = data[i] + local entity = Entity(self) - local entity = Entity() - entity:deserialize(entityData) + if data[i].key then + local component = Components.key:__new() + entity.key = component:deserialize(data[i].key) - self:addEntity(entity) + entity:__dirty() + end + + entities[i] = entity + end + + for i = 1, #data do + entity[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. From 41fcfac6afa7466188b2b4a3bf982e563d178e80 Mon Sep 17 00:00:00 2001 From: flamendless Date: Tue, 14 Feb 2023 18:14:25 -0300 Subject: [PATCH 29/43] Fixed errors on World:deserialize --- concord/world.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/concord/world.lua b/concord/world.lua index 85a19e8..9113ee2 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -8,6 +8,7 @@ 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") @@ -396,7 +397,7 @@ function World:deserialize(data, startClean, ignoreGenerator) local entity = Entity(self) if data[i].key then - local component = Components.key:__new() + local component = Components.key:__new(entity) entity.key = component:deserialize(data[i].key) entity:__dirty() @@ -406,7 +407,7 @@ function World:deserialize(data, startClean, ignoreGenerator) end for i = 1, #data do - entity[i]:deserialize(data[i]) + entities[i]:deserialize(data[i]) end self:__flush() From bdfe2523b02218a28ca938f529567418053a5a3e Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:25 -0300 Subject: [PATCH 30/43] Fix require indentation --- concord/world.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/concord/world.lua b/concord/world.lua index 9113ee2..fc26e19 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -6,12 +6,12 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Filter = require(PATH..".filter") -local Entity = require(PATH..".entity") +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 Type = require(PATH..".type") +local List = require(PATH..".list") +local Utils = require(PATH..".utils") local World = { ENABLE_OPTIMIZATION = true, From 8e1b14d53b78e98b743944effcb33194bb629d65 Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:18:41 -0300 Subject: [PATCH 31/43] Ignore non-lua files in Utils.loadNamespace Fixes #48 --- concord/utils.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/concord/utils.lua b/concord/utils.lua index 1bf8710..b33b719 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -40,15 +40,12 @@ function Utils.loadNamespace(pathOrFiles, namespace) for _, file in ipairs(files) do local isFile = love.filesystem.getInfo(pathOrFiles .. "/" .. file).type == "file" - if isFile then + if isFile and string.match(file, '%.lua$') ~= nil then local name = file:sub(1, #file - 4) local path = pathOrFiles.."."..name local value = require(path) if namespace then namespace[name] = value end - else - local value = require(pathOrFiles.."."..file) - if namespace then namespace[file] = value end end end elseif type(pathOrFiles) == "table" then From 61720312cbec9e456145976e74cf2be6667374ec Mon Sep 17 00:00:00 2001 From: flamendless Date: Tue, 14 Feb 2023 18:18:42 -0300 Subject: [PATCH 32/43] Added optional table for output for entity:getComponents --- concord/entity.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/concord/entity.lua b/concord/entity.lua index 120146f..803006c 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -188,8 +188,9 @@ end -- Warning: Do not modify this table. -- Use Entity:give/ensure/remove instead -- @treturn table Table of all Components the Entity has -function Entity:getComponents() - local components = Utils.shallowCopy(self) +function Entity:getComponents(output) + output = output or {} + local components = Utils.shallowCopy(self, output) components.__world = nil components.__isEntity = nil From 16c77c6a66ee85e2244bcea3cfee3e42a532765f Mon Sep 17 00:00:00 2001 From: flamendless Date: Tue, 14 Feb 2023 18:18:42 -0300 Subject: [PATCH 33/43] Fixed bug with serialization/deserialization --- concord/builtins/key.lua | 6 +++--- concord/world.lua | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/concord/builtins/key.lua b/concord/builtins/key.lua index f083bd4..7e571ed 100644 --- a/concord/builtins/key.lua +++ b/concord/builtins/key.lua @@ -19,7 +19,7 @@ local Key = Component("key", function (self, key) end) function Key:deserialize (data) - self.value = getKey(self, data.value) + self.value = getKey(self, data) end function Key:removed (replaced) @@ -28,10 +28,10 @@ function Key:removed (replaced) if entity:inWorld() then local world = entity:getWorld() - + return world:__clearKey(entity) end end end -return Key \ No newline at end of file +return Key diff --git a/concord/world.lua b/concord/world.lua index fc26e19..9303fe0 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -398,7 +398,8 @@ function World:deserialize(data, startClean, ignoreGenerator) if data[i].key then local component = Components.key:__new(entity) - entity.key = component:deserialize(data[i].key) + component:deserialize(data[i].key) + entity.key = component entity:__dirty() end From cf05cfc97260a60d78a3db47a1283c4e30f156d2 Mon Sep 17 00:00:00 2001 From: Pablo Mayobre Date: Tue, 14 Feb 2023 18:18:42 -0300 Subject: [PATCH 34/43] Add ability to clone components Fixes #51 --- concord/entity.lua | 89 +++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/concord/entity.lua b/concord/entity.lua index 803006c..041e58e 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -45,7 +45,7 @@ function Entity.new(world) return e end -local function give(e, name, componentClass, ...) +local function createComponent(e, name, componentClass, ...) local component = componentClass:__initialize(e, ...) local hadComponent = not not e[name] @@ -60,7 +60,57 @@ local function give(e, name, componentClass, ...) end end -local function remove(e, name) +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) @@ -76,15 +126,7 @@ end -- @param ... additional arguments to pass to the Component's populate function -- @treturn Entity self function Entity:give(name, ...) - local ok, componentClass = Components.try(name) - - if not ok then - Utils.error(2, "bad argument #1 to 'Entity:get' (%s)", componentClass) - end - - give(self, name, componentClass, ...) - - return self + return giveComponent(self, false, name, ...) end --- Ensures an Entity to have a Component. @@ -93,19 +135,7 @@ end -- @param ... additional arguments to pass to the Component's populate function -- @treturn Entity self function Entity:ensure(name, ...) - local ok, componentClass = Components.try(name) - - if not ok then - Utils.error(2, "bad argument #1 to 'Entity:ensure' (%s)", componentClass) - end - - if self[name] then - return self - end - - give(self, name, componentClass, ...) - - return self + return giveComponent(self, true, name, ...) end --- Removes a Component from an Entity. @@ -118,7 +148,7 @@ function Entity:remove(name) Utils.error(2, "bad argument #1 to 'Entity:remove' (%s)", componentClass) end - remove(self, name) + removeComponent(self, name) return self end @@ -239,14 +269,7 @@ function Entity:deserialize(data) Utils.error(2, "bad argument #1 to 'Entity:deserialize' (ComponentClass '%s' wasn't yet loaded)", tostring(componentData.__name)) -- luacheck: ignore end - local componentClass = Components[componentData.__name] - - local component = componentClass:__new(self) - component:deserialize(componentData) - - self[componentData.__name] = component - - self:__dirty() + deserializeComponent(self, componentData.__name, componentData) end end From 9bccd0501965044b82c459c614c54b23dc1dfbdb Mon Sep 17 00:00:00 2001 From: Pablo Mayobre Date: Tue, 14 Feb 2023 18:18:43 -0300 Subject: [PATCH 35/43] 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 --- .luacheckrc | 1 + concord/builtins/key.lua | 4 ++++ concord/component.lua | 1 + concord/entity.lua | 3 ++- concord/world.lua | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index b557738..59fc4be 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1 +1,2 @@ +---@diagnostic disable: lowercase-global std="love+luajit" diff --git a/concord/builtins/key.lua b/concord/builtins/key.lua index 7e571ed..7b23275 100644 --- a/concord/builtins/key.lua +++ b/concord/builtins/key.lua @@ -22,6 +22,10 @@ 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 diff --git a/concord/component.lua b/concord/component.lua index 2b43779..981b999 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -95,6 +95,7 @@ end function Component:__initialize(entity, ...) local component = self:__new(entity) + ---@diagnostic disable-next-line: redundant-parameter self.__populate(component, ...) return component diff --git a/concord/entity.lua b/concord/entity.lua index 041e58e..29145fa 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -248,7 +248,8 @@ function Entity:serialize(ignoreKey) if not ignoreKey then data.key = component.value end - elseif (name ~= "__world") and (name ~= "__isEntity") and (component.__name == name) then + --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 diff --git a/concord/world.lua b/concord/world.lua index 9303fe0..1f0c714 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -476,6 +476,7 @@ end return setmetatable(World, { __call = function(_, ...) + ---@diagnostic disable-next-line: redundant-parameter return World.new(...) end, }) From 429a448ab60edea1a9aa78f9b3345f7ad329d97d Mon Sep 17 00:00:00 2001 From: Jesse Viikari Date: Tue, 14 Feb 2023 18:18:43 -0300 Subject: [PATCH 36/43] Add resources to world - setResource(name, resource) to set a resource - getResource(name) to retrieve it --- concord/world.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/concord/world.lua b/concord/world.lua index 1f0c714..7008da4 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -36,6 +36,8 @@ function World.new() __events = {}, __emitSDepth = 0, + __resources = {}, + __hash = { state = -2^53, generator = defaultGenerator, @@ -474,6 +476,22 @@ end 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 From 1e4132be21b56e47f878251bf6d02652e46222dd Mon Sep 17 00:00:00 2001 From: Brandon Blanker Lim-it Date: Tue, 14 Feb 2023 18:18:43 -0300 Subject: [PATCH 37/43] 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 --- concord/world.lua | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/concord/world.lua b/concord/world.lua index 7008da4..7b31a6e 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -52,6 +52,8 @@ function World.new() __systemLookup = {}, __isWorld = true, + + __ignoreEmits = false }, World.__mt) -- Optimization: We deep copy the World class into our instance of a world. @@ -320,7 +322,14 @@ function World:emit(functionName, ...) 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 for i = 1, #listeners do @@ -336,6 +345,12 @@ function World:emit(functionName, ...) 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 From 2386547caa1a0602394a2a2c151717ce9400a95d Mon Sep 17 00:00:00 2001 From: Pablo Mayobre Date: Tue, 14 Feb 2023 22:28:26 -0300 Subject: [PATCH 38/43] Normalize slashes to dots when calling require in Utils.loadNamespace Co-authored-by: Ulhar --- concord/utils.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/concord/utils.lua b/concord/utils.lua index b33b719..0802586 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -44,7 +44,7 @@ function Utils.loadNamespace(pathOrFiles, namespace) local name = file:sub(1, #file - 4) local path = pathOrFiles.."."..name - local value = require(path) + local value = require(path:gsub("%/", ".")) if namespace then namespace[name] = value end end end @@ -61,7 +61,7 @@ function Utils.loadNamespace(pathOrFiles, namespace) name = path:sub((dotIndex or slashIndex) + 1) end - local value = require(path) + local value = require(path:gsub("%/", ".")) if namespace then namespace[name] = value end end end From 5f4b3b97dab5fe4aa3bd387348b6bd20c5af9389 Mon Sep 17 00:00:00 2001 From: Daniel Power Date: Sat, 18 Feb 2023 16:43:56 -0330 Subject: [PATCH 39/43] Fix link to documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7872c69..f7211e4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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.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/). --- From f9da8dbe928902934a77e1b0b56f8154ddbfff9f Mon Sep 17 00:00:00 2001 From: lambtoken Date: Sat, 23 Dec 2023 09:37:03 +0100 Subject: [PATCH 40/43] Added missing do keyword in the loops --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f7211e4..8f4d7b5 100644 --- a/README.md +++ b/README.md @@ -294,14 +294,14 @@ end -- Defining a function function mySystemClass:update(dt) -- 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 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) + for _, e in ipairs(self.secondPool) do -- Do something end end From 1aaf50140135e5793b362a862cd15e8f0cfc52f0 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Mon, 27 May 2024 13:53:55 +0200 Subject: [PATCH 41/43] Update README.md to hint that components should be loaded for systems --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f7211e4..168db64 100644 --- a/README.md +++ b/README.md @@ -133,16 +133,16 @@ 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. ```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) print(Systems.systemName) - --- 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) ``` #### Method chaining From 6575686b3bb5abfb2e9c477762c2de38f22f40a5 Mon Sep 17 00:00:00 2001 From: Denis Defreyne Date: Sat, 29 Jun 2024 10:28:12 +0200 Subject: [PATCH 42/43] Fix typo in README.md A piece of example code was missing a `,`. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 168db64..987c030 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ When defining a ComponentClass you need to pass in a name and usually a `populat -- 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) +Concord.component("position", function(component, x, y) component.x = x or 0 component.y = y or 0 end) From 848652f68887db0c4261efe499facbec88959d03 Mon Sep 17 00:00:00 2001 From: Justin van der Leij Date: Sun, 30 Jun 2024 10:45:46 +0200 Subject: [PATCH 43/43] Make example for how to use Concord.utils.loadNamespace for systems more concrete --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8db02a4..6669d8e 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,12 @@ print(Concord.components.componentName) local Systems = {} Concord.utils.loadNamespace("path/to/systems", Systems) -print(Systems.systemName) +myWorld:addSystems( + Systems.healthSystem + Systems.damageSystem, + Systems.moveSystem, + -- etc +) ``` #### Method chaining