diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml deleted file mode 100644 index ca60d46..0000000 --- a/.github/workflows/doc.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build Docs - -on: - push: - branches: - - master - -jobs: - build: - name: Build docs - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v2 - - - name: Setup Lua - uses: leafo/gh-actions-lua@v8 - with: - luaVersion: 5.4 - - - name: Install Luarocks - uses: leafo/gh-actions-luarocks@v4 - - - name: Install LDoc - run: luarocks install ldoc - - - name: Show - run: luarocks show ldoc - - - name: Build docs - run: ldoc . - - - name: Deploy - uses: JamesIves/github-pages-deploy-action@4.1.5 - with: - branch: gh-pages - folder: docs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 989d364..51b8439 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,4 @@ luac.out *.app *.i*86 *.x86_64 -*.hex - -# VSCode -.vscode/ \ No newline at end of file +*.hex \ No newline at end of file diff --git a/.luacheckrc b/.luacheckrc index 59fc4be..b557738 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,2 +1 @@ ----@diagnostic disable: lowercase-global std="love+luajit" diff --git a/README.md b/README.md index 6669d8e..2f79dcf 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-group.github.io/Concord/). +Auto generated docs for Concord can be found in `docs` folder, or on the [Github page](https://tjakka5.github.io/Concord/). --- @@ -133,21 +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) -myWorld:addSystems( - Systems.healthSystem - Systems.damageSystem, - Systems.moveSystem, - -- etc -) +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 @@ -175,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) @@ -271,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", @@ -283,7 +278,7 @@ local mySystemClass = Concord.system({ "health", "damageable", } -}) +) ``` ```lua @@ -299,14 +294,14 @@ end -- Defining a function function mySystemClass:update(dt) -- Iterate over all entities in the Pool - for _, e in ipairs(self.pool) do + for _, e in ipairs(self.pool) -- Do something with the Components e.position.x = e.position.x + e.velocity.x * dt e.position.y = e.position.y + e.velocity.y * dt end -- Iterate over all entities in the second Pool - for _, e in ipairs(self.secondPool) do + for _, e in ipairs(self.secondPool) -- Do something end end diff --git a/concord/builtins/init.lua b/concord/builtins/init.lua deleted file mode 100644 index e3ab37b..0000000 --- a/concord/builtins/init.lua +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 7b23275..0000000 --- a/concord/builtins/key.lua +++ /dev/null @@ -1,41 +0,0 @@ -local PATH = (...):gsub('%.builtins%.[^%.]+$', '') - -local Component = require(PATH..".component") - -local getKey = function (self, key) - local entity = self.__entity - - if not entity:inWorld() then - error("entity needs to belong to a world") - end - - local world = entity:getWorld() - - return world:__assignKey(entity, key) -end - -local Key = Component("key", function (self, key) - self.value = getKey(self, key) -end) - -function Key:deserialize (data) - self.value = getKey(self, data) -end - -function Key.__mt:__call() - return self.value -end - -function Key:removed (replaced) - if not replaced then - local entity = self.__entity - - if entity:inWorld() then - local world = entity:getWorld() - - return world:__clearKey(entity) - end - end -end - -return Key diff --git a/concord/builtins/serializable.lua b/concord/builtins/serializable.lua deleted file mode 100644 index 125f76b..0000000 --- a/concord/builtins/serializable.lua +++ /dev/null @@ -1,12 +0,0 @@ -local PATH = (...):gsub('%.builtins%.[^%.]+$', '') - -local Component = require(PATH..".component") - -local Serializable = Component("serializable") - -function Serializable:serialize () - -- Don't serialize this Component - return nil -end - -return Serializable \ No newline at end of file diff --git a/concord/component.lua b/concord/component.lua index 981b999..d8678f0 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -16,19 +16,15 @@ Component.__mt = { -- @treturn Component A new ComponentClass function Component.new(name, populate) if (type(name) ~= "string") then - Utils.error(2, "bad argument #1 to 'Component.new' (string expected, got %s)", type(name)) - end - - if (string.match(name, Components.__REJECT_MATCH) ~= "") then - Utils.error(2, "bad argument #1 to 'Component.new' (Component names can't start with '%s', got %s)", Components.__REJECT_PREFIX, name) + error("bad argument #1 to 'Component.new' (string expected, got "..type(name)..")", 2) end if (rawget(Components, name)) then - Utils.error(2, "bad argument #1 to 'Component.new' (ComponentClass with name '%s' was already registerd)", name) -- luacheck: ignore + error("bad argument #1 to 'Component.new' (ComponentClass with name '"..name.."' was already registerd)", 2) -- luacheck: ignore end if (type(populate) ~= "function" and type(populate) ~= "nil") then - Utils.error(2, "bad argument #1 to 'Component.new' (function/nil expected, got %s)", type(populate)) + error("bad argument #1 to 'Component.new' (function/nil expected, got "..type(populate)..")", 2) end local componentClass = setmetatable({ @@ -47,40 +43,31 @@ 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.__entity = nil - data.__isComponent = nil + data.__componentClass = nil + data.__isComponent = nil data.__isComponentClass = nil return data end --- Callback: When the Component gets deserialized from serialized data. function Component:deserialize(data) Utils.shallowCopy(data, self) end -- Internal: Creates a new Component. --- @param entity The Entity that will receive this Component. -- @return A new Component -function Component:__new(entity) +function Component:__new() local component = setmetatable({ __componentClass = self, - __entity = entity, __isComponent = true, __isComponentClass = false, }, self.__mt) @@ -89,13 +76,11 @@ 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, ...) - local component = self:__new(entity) +function Component:__initialize(...) + local component = self:__new() - ---@diagnostic disable-next-line: redundant-parameter self.__populate(component, ...) return component diff --git a/concord/components.lua b/concord/components.lua index b2743e7..bf20d2d 100644 --- a/concord/components.lua +++ b/concord/components.lua @@ -1,10 +1,11 @@ --- Container for registered ComponentClasses -- @module Components -local Components = {} +local PATH = (...):gsub('%.[^%.]+$', '') -Components.__REJECT_PREFIX = "!" -Components.__REJECT_MATCH = "^(%"..Components.__REJECT_PREFIX.."?)(.+)" +local Type = require(PATH..".type") + +local Components = {} --- Returns true if the containter has the ComponentClass with the specified name -- @string name Name of the ComponentClass to check @@ -13,43 +14,22 @@ function Components.has(name) return rawget(Components, name) and true or false end ---- Prefix a component's name with the currently set Reject Prefix --- @string name Name of the ComponentClass to reject --- @treturn string -function Components.reject(name) - local ok, err = Components.try(name) - - if not ok then error(err, 2) end - - return Components.__REJECT_PREFIX..name -end - --- Returns true and the ComponentClass if one was registered with the specified name -- or false and an error otherwise -- @string name Name of the ComponentClass to check --- @boolean acceptRejected Whether to accept names prefixed with the Reject Prefix. -- @treturn boolean -- @treturn Component or error string --- @treturn true if acceptRejected was true and the name had the Reject Prefix, false otherwise. -function Components.try(name, acceptRejected) +function Components.try(name) if type(name) ~= "string" then return false, "ComponentsClass name is expected to be a string, got "..type(name)..")" end - local rejected = false - if acceptRejected then - local prefix - prefix, name = string.match(name, Components.__REJECT_MATCH) - - rejected = prefix ~= "" and name - end - local value = rawget(Components, name) if not value then return false, "ComponentClass '"..name.."' does not exist / was not registered" end - return true, value, rejected + return true, value end --- Returns the ComponentClass with the specified name diff --git a/concord/entity.lua b/concord/entity.lua index 29145fa..854fa8c 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -6,16 +6,8 @@ local PATH = (...):gsub('%.[^%.]+$', '') local Components = require(PATH..".components") local Type = require(PATH..".type") -local Utils = require(PATH..".utils") - --- Initialize built-in Components (as soon as possible) -local Builtins = require(PATH..".builtins.init") --luacheck: ignore --- Builtins is unused but the require already registers the Components - -local Entity = { - SERIALIZE_BY_DEFAULT = true, -} +local Entity = {} Entity.__mt = { __index = Entity, } @@ -25,11 +17,12 @@ Entity.__mt = { -- @treturn Entity A new Entity function Entity.new(world) if (world ~= nil and not Type.isWorld(world)) then - Utils.error(2, "bad argument #1 to 'Entity.new' (world/nil expected, got %s)", type(world)) + error("bad argument #1 to 'Entity.new' (world/nil expected, got "..type(world)..")", 2) end local e = setmetatable({ __world = nil, + __components = {}, __isEntity = true, }, Entity.__mt) @@ -38,86 +31,23 @@ function Entity.new(world) world:addEntity(e) end - if Entity.SERIALIZE_BY_DEFAULT then - e:give("serializable") - end - return e end -local function createComponent(e, name, componentClass, ...) - local component = componentClass:__initialize(e, ...) - local hadComponent = not not e[name] - - if hadComponent then - e[name]:removed(true) - end +local function give(e, name, componentClass, ...) + local component = componentClass:__initialize(...) e[name] = component + e.__components[name] = component - if not hadComponent then - e:__dirty() - end + e:__dirty() end -local function deserializeComponent(e, name, componentData) - local componentClass = Components[name] - local hadComponent = not not e[name] +local function remove(e, name, componentClass) + e[name] = nil + e.__components[name] = nil - if hadComponent then - e[name]:removed(true) - end - - local component = componentClass:__new(e) - component:deserialize(componentData) - - e[name] = component - - if not hadComponent then - e:__dirty() - end -end - -local function giveComponent(e, ensure, name, ...) - local component - if Type.isComponent(name) then - component = name - name = component:getName() - end - - if ensure and e[name] then - return e - end - - local ok, componentClass = Components.try(name) - - if not ok then - Utils.error(3, "bad argument #1 to 'Entity:%s' (%s)", ensure and 'ensure' or 'give', componentClass) - end - - if component then - local data = component:deserialize() - if data == nil then - Utils.error(3, "bad argument #1 to 'Entity:$s' (Component '%s' couldn't be deserialized)", ensure and 'ensure' or 'give', name) - end - - deserializeComponent(e, name, data) - else - createComponent(e, name, componentClass, ...) - end - - return e -end - - -local function removeComponent(e, name) - if e[name] then - e[name]:removed(false) - - e[name] = nil - - e:__dirty() - end + e:__dirty() end --- Gives an Entity a Component. @@ -126,7 +56,15 @@ end -- @param ... additional arguments to pass to the Component's populate function -- @treturn Entity self function Entity:give(name, ...) - return giveComponent(self, false, name, ...) + local ok, componentClass = Components.try(name) + + if not ok then + error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) + end + + give(self, name, componentClass, ...) + + return self end --- Ensures an Entity to have a Component. @@ -135,7 +73,19 @@ end -- @param ... additional arguments to pass to the Component's populate function -- @treturn Entity self function Entity:ensure(name, ...) - return giveComponent(self, true, name, ...) + local ok, componentClass = Components.try(name) + + if not ok then + error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) + end + + if self[name] then + return self + end + + give(self, name, componentClass, ...) + + return self end --- Removes a Component from an Entity. @@ -145,10 +95,10 @@ function Entity:remove(name) local ok, componentClass = Components.try(name) if not ok then - Utils.error(2, "bad argument #1 to 'Entity:remove' (%s)", componentClass) + error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) end - removeComponent(self, name) + remove(self, name, componentClass) return self end @@ -159,7 +109,7 @@ end -- @treturn Entity self function Entity:assemble(assemblage, ...) if type(assemblage) ~= "function" then - Utils.error(2, "bad argument #1 to 'Entity:assemble' (function expected, got %s)", type(assemblage)) + error("bad argument #1 to 'Entity:assemble' (function expected, got "..type(assemblage)..")") end assemblage(self, ...) @@ -195,7 +145,7 @@ function Entity:has(name) local ok, componentClass = Components.try(name) if not ok then - Utils.error(2, "bad argument #1 to 'Entity:has' (%s)", componentClass) + error("bad argument #1 to 'Entity:has' ("..componentClass..")", 2) end return self[name] and true or false @@ -208,7 +158,7 @@ function Entity:get(name) local ok, componentClass = Components.try(name) if not ok then - Utils.error(2, "bad argument #1 to 'Entity:get' (%s)", componentClass) + error("bad argument #1 to 'Entity:get' ("..componentClass..")", 2) end return self[name] @@ -218,13 +168,8 @@ 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(output) - output = output or {} - local components = Utils.shallowCopy(self, output) - components.__world = nil - components.__isEntity = nil - - return components +function Entity:getComponents() + return self.__components end --- Returns true if the Entity is in a World. @@ -239,17 +184,11 @@ function Entity:getWorld() return self.__world end -function Entity:serialize(ignoreKey) +function Entity:serialize() local data = {} - for name, component in pairs(self) do - -- The key component needs to be treated separately. - if name == "key" and component.__name == "key" then - if not ignoreKey then - data.key = component.value - end - --We only care about components that were properly given to the entity - elseif Type.isComponent(component) and (component.__name == name) then + for _, component in pairs(self.__components) do + if component.__name then local componentData = component:serialize() if componentData ~= nil then @@ -267,10 +206,18 @@ function Entity:deserialize(data) local componentData = data[i] if (not Components.has(componentData.__name)) then - Utils.error(2, "bad argument #1 to 'Entity:deserialize' (ComponentClass '%s' wasn't yet loaded)", tostring(componentData.__name)) -- luacheck: ignore + error("bad argument #1 to 'Entity:deserialize' (ComponentClass '"..tostring(componentData.__name).."' wasn't yet loaded)") -- luacheck: ignore end - deserializeComponent(self, componentData.__name, componentData) + local componentClass = Components[componentData.__name] + + local component = componentClass:__new() + component:deserialize(componentData) + + self[componentData.__name] = component + self.__components[componentData.__name] = component + + self:__dirty() end end diff --git a/concord/filter.lua b/concord/filter.lua deleted file mode 100644 index 0c4dabf..0000000 --- a/concord/filter.lua +++ /dev/null @@ -1,199 +0,0 @@ ---- Used to filter Entities with specific Components --- A Filter has an associated Pool that can contain any amount of Entities. --- @classmod Filter - -local PATH = (...):gsub('%.[^%.]+$', '') - -local List = require(PATH..".list") -local Type = require(PATH..".type") -local Utils = require(PATH..".utils") -local Components = require(PATH..".components") - - -local Filter = {} -Filter.__mt = { - __index = Filter, -} - ---- Validates a Filter Definition to make sure every component is valid. --- @string name Name for the Filter. --- @tparam table definition Table containing the Filter Definition --- @tparam onComponent Optional function, called when a component is valid. -function Filter.validate (errorLevel, name, def, onComponent) - local filter = "World:query filter" - if name then - filter = ("filter '%s'"):format(name) - end - - if type(def) ~= 'table' then - Utils.error(3 + errorLevel, "invalid component list for %s (table expected, got %s)", filter, type(def)) - end - - if not onComponent and def.constructor and not Type.isCallable(def.constructor) then - Utils.error(3 + errorLevel, "invalid pool constructor for %s (callable expected, got %s)", filter, type(def.constructor)) - end - - for n, component in ipairs(def) do - local ok, err, reject = Components.try(component, true) - - if not ok then - Utils.error(3 + errorLevel, "invalid component for %s at position #%d (%s)", filter, n, err) - end - - if onComponent then - onComponent(component, reject) - end - end -end - ---- Parses the Filter Defintion into two tables --- required: An array of all the required component names. --- rejected: An array of all the components that will be rejected. --- @string name Name for the Filter. --- @tparam table definition Table containing the Filter Definition --- @treturn table required --- @treturn table rejected -function Filter.parse (name, def) - local filter = {} - - Filter.validate(1, name, def, function (component, reject) - if reject then - table.insert(filter, reject) - table.insert(filter, false) - else - table.insert(filter, component) - table.insert(filter, true) - end - end) - - return filter -end - -function Filter.match (e, filter) - for i=#filter, 2, -2 do - local match = filter[i - 0] - local name = filter[i - 1] - - if (not e[name]) == match then return false end - end - - return true -end - -local REQUIRED_METHODS = {"add", "remove", "has", "clear"} -local VALID_POOL_TYPES = {table=true, userdata=true, lightuserdata=true, cdata=true} - -function Filter.isValidPool (name, pool) - local poolType = type(pool) - --Check that pool is not nil - if not VALID_POOL_TYPES[poolType] then - Utils.error(3, "invalid value returned by pool '%s' constructor (table expected, got %s).", name, type(pool)) - end - - --Check if methods are callables - for _, method in ipairs(REQUIRED_METHODS) do - if not Type.isCallable(pool[method]) then - Utils.error(3, "invalid :%s method on pool '%s' (callable expected, got %s).", method, name, type(pool[method])) - end - end -end - ---- Creates a new Filter --- @string name Name for the Filter. --- @tparam table definition Table containing the Filter Definition --- @treturn Filter The new Filter --- @treturn Pool The associated Pool -function Filter.new (name, def) - local pool - - if def.constructor then - pool = def.constructor(def) - Filter.isValidPool(name, pool) - else - pool = List() - end - - local filter = Filter.parse(name, def) - - local filter = setmetatable({ - pool = pool, - - __filter = filter, - __name = name, - - __isFilter = true, - }, Filter.__mt) - - return filter, pool -end - ---- Checks if an Entity fulfills the Filter requirements. --- @tparam Entity e Entity to check --- @treturn boolean -function Filter:eligible(e) - return Filter.match(e, self.__filter) -end - -function Filter:evaluate (e) - local has = self.pool:has(e) - local eligible = self:eligible(e) - - if not has and eligible then - self.pool:add(e) - elseif has and not eligible then - self.pool:remove(e) - end - - return self -end - - --- Adds an Entity to the Pool, if it passes the Filter. --- @param e Entity to add --- @param bypass Whether to bypass the Filter or not. --- @treturn Filter self --- @treturn boolean Whether the entity was added or not. -function Filter:add (e, bypass) - if not bypass and not self:eligible(e) then - return self, false - end - - self.pool:add(e) - - return self, true -end - --- Remove an Entity from the Pool associated to this Filter. --- @param e Entity to remove --- @treturn Filter self -function Filter:remove (e) - self.pool:remove(e) - return self -end - --- Clear the Pool associated to this Filter. --- @param e Entity to remove --- @treturn Filter self -function Filter:clear (e) - self.pool:clear(e) - return self -end - --- Check if the Pool bound to this System contains the passed Entity --- @param e Entity to check --- @treturn boolean Whether the Entity exists. -function Filter:has (e) - return self.pool:has(e) -end - ---- Gets the name of the Filter --- @treturn string -function Filter:getName() - return self.__name -end - -return setmetatable(Filter, { - __call = function(_, ...) - return Filter.new(...) - end, -}) \ No newline at end of file diff --git a/concord/list.lua b/concord/list.lua index 7f10c74..1263a72 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', 'onAdded' or 'onRemoved' +-- Object may not be the string 'size' -- @param obj Object to add -- @treturn List self function List:add(obj) @@ -26,7 +26,6 @@ function List:add(obj) self[obj] = size self.size = size - if self.onAdded then self:onAdded(obj) end return self end @@ -52,7 +51,6 @@ function List:remove(obj) self[obj] = nil self.size = size - 1 - if self.onRemoved then self:onRemoved(obj) end return self end @@ -96,30 +94,6 @@ 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 - ---- 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 new file mode 100644 index 0000000..f1c80eb --- /dev/null +++ b/concord/pool.lua @@ -0,0 +1,111 @@ +--- Used to iterate over Entities with a specific Components +-- A Pool contain a any amount of Entities. +-- @classmod Pool + +local PATH = (...):gsub('%.[^%.]+$', '') + +local List = require(PATH..".list") + +local Pool = {} +Pool.__mt = { + __index = Pool, +} + +--- Creates a new Pool +-- @string name Name for the Pool. +-- @tparam table filter Table containing the required BaseComponents +-- @treturn Pool The new Pool +function Pool.new(name, filter) + local pool = setmetatable(List(), Pool.__mt) + + pool.__name = name + pool.__filter = filter + + pool.__isPool = true + + return pool +end + +--- Checks if an Entity is eligible for the Pool. +-- @tparam Entity e Entity to check +-- @treturn boolean +function Pool:eligible(e) + for i=#self.__filter, 1, -1 do + local component = self.__filter[i].__name + + if not e[component] then return false end + end + + return true +end + +-- Adds an Entity to the Pool, if it can be eligible. +-- @param e Entity to add +-- @treturn Pool self +-- @treturn boolean Whether the entity was added or not +function Pool:add(e, bypass) + if not bypass and not self:eligible(e) then + return self, false + end + + List.add(self, e) + self:onEntityAdded(e) + + return self, true +end + +-- Remove an Entity from the Pool. +-- @param e Entity to remove +-- @treturn Pool self +function Pool:remove(e) + List.remove(self, e) + self:onEntityRemoved(e) + + return self +end + +--- Evaluate whether an Entity should be added or removed from the Pool. +-- @param e Entity to add or remove +-- @treturn Pool self +function Pool:evaluate(e) + local has = self:has(e) + local eligible = self:eligible(e) + + if not has and eligible then + self:add(e, true) --Bypass the check cause we already checked + elseif has and not eligible then + self:remove(e) + end + + return self +end + +--- Gets the name of the Pool +-- @treturn string +function Pool:getName() + return self.__name +end + +--- Gets the filter of the Pool. +-- Warning: Do not modify this filter. +-- @return Filter of the Pool. +function Pool:getFilter() + return self.__filter +end + +--- Callback for when an Entity is added to the Pool. +-- @tparam Entity e Entity that was added. +function Pool:onEntityAdded(e) -- luacheck: ignore +end + +-- Callback for when an Entity is removed from the Pool. +-- @tparam Entity e Entity that was removed. +function Pool:onEntityRemoved(e) -- luacheck: ignore +end + +return setmetatable(Pool, { + __index = List, + __call = function(_, ...) + return Pool.new(...) + end, +}) diff --git a/concord/system.lua b/concord/system.lua index e9d45d6..756b124 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -5,8 +5,9 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Filter = require(PATH..".filter") +local Pool = require(PATH..".pool") local Utils = require(PATH..".utils") +local Components = require(PATH..".components") local System = { ENABLE_OPTIMIZATION = true, @@ -18,7 +19,7 @@ System.mt = { local system = setmetatable({ __enabled = true, - __filters = {}, + __pools = {}, __world = world, __isSystem = true, @@ -32,11 +33,11 @@ System.mt = { Utils.shallowCopy(systemClass, system) end - for name, def in pairs(systemClass.__definition) do - local filter, pool = Filter(name, Utils.shallowCopy(def, {})) + for name, filter in pairs(systemClass.__filter) do + local pool = Pool(name, filter) system[name] = pool - table.insert(system.__filters, filter) + system.__pools[#system.__pools + 1] = pool end system:init(world) @@ -44,23 +45,44 @@ System.mt = { return system end, } + +local validateFilters = function (baseFilters) + local filters = {} + + for name, componentsList in pairs(baseFilters) do + if type(name) ~= 'string' then + error("invalid name for filter (string key expected, got "..type(name)..")", 3) + end + + if type(componentsList) ~= 'table' then + error("invalid component list for filter '"..name.."' (table expected, got "..type(componentsList)..")", 3) + end + + local filter = {} + for n, component in ipairs(componentsList) do + local ok, componentClass = Components.try(component) + + if not ok then + error("invalid component for filter '"..name.."' at position #"..n.." ("..componentClass..")", 3) + end + + filter[#filter + 1] = componentClass + end + + filters[name] = filter + end + + return filters +end + --- Creates a new SystemClass. -- @param table filters A table containing filters (name = {components...}) -- @treturn System A new SystemClass -function System.new(definition) - definition = definition or {} - - for name, def in pairs(definition) do - if type(name) ~= 'string' then - Utils.error(2, "invalid name for filter (string key expected, got %s)", type(name)) - end - - Filter.validate(0, name, def) - end - +function System.new(filters) local systemClass = setmetatable({ - __definition = definition, + __filter = validateFilters(filters), + __name = nil, __isSystemClass = true, }, System.mt) systemClass.__index = systemClass @@ -79,8 +101,8 @@ end -- @param e The Entity to check -- @treturn System self function System:__evaluate(e) - for _, filter in ipairs(self.__filters) do - filter:evaluate(e) + for _, pool in ipairs(self.__pools) do + pool:evaluate(e) end return self @@ -90,9 +112,9 @@ end -- @param e The Entity to remove -- @treturn System self function System:__remove(e) - for _, filter in ipairs(self.__filters) do - if filter:has(e) then - filter:remove(e) + for _, pool in ipairs(self.__pools) do + if pool:has(e) then + pool:remove(e) end end @@ -102,8 +124,8 @@ end -- Internal: Clears all Entities from the System. -- @treturn System self function System:__clear() - for _, filter in ipairs(self.__filters) do - filter:clear() + for i = 1, #self.__pools do + self.__pools[i]:clear() end return self @@ -136,6 +158,18 @@ 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/type.lua b/concord/type.lua index bf7f52a..157a8d4 100644 --- a/concord/type.lua +++ b/concord/type.lua @@ -3,15 +3,6 @@ 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 @@ -54,11 +45,4 @@ function Type.isWorld(t) return type(t) == "table" and t.__isWorld or false end ---- Returns if object is a Filter. --- @param t Object to check --- @treturn boolean -function Type.isFilter(t) - return type(t) == "table" and t.__isFilter or false -end - return Type diff --git a/concord/utils.lua b/concord/utils.lua index 0802586..da83c4d 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -3,10 +3,6 @@ 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 @@ -25,43 +21,39 @@ 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 - Utils.error(2, "bad argument #1 to 'loadNamespace' (string/table of strings expected, got %s)", type(pathOrFiles)) + 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 - Utils.error(2, "bad argument #1 to 'loadNamespace' (path '%s' not found)", pathOrFiles) + if (info == nil or info.type ~= "directory") then + error("bad argument #1 to 'loadNamespace' (path '"..pathOrFiles.."' not found)", 2) end local files = love.filesystem.getDirectoryItems(pathOrFiles) for _, file in ipairs(files) do - local isFile = love.filesystem.getInfo(pathOrFiles .. "/" .. file).type == "file" + local name = file:sub(1, #file - 4) + local path = pathOrFiles.."."..name - if isFile and string.match(file, '%.lua$') ~= nil then - local name = file:sub(1, #file - 4) - local path = pathOrFiles.."."..name - - local value = require(path:gsub("%/", ".")) - if namespace then namespace[name] = value end - end + local value = require(path) + if namespace then namespace[name] = value end end - elseif type(pathOrFiles) == "table" then + elseif (type(pathOrFiles == "table")) then for _, path in ipairs(pathOrFiles) do - if type(path) ~= "string" then - Utils.error(2, "bad argument #2 to 'loadNamespace' (string/table of strings expected, got table containing %s)", type(path)) -- luacheck: ignore + 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 - local value = require(path:gsub("%/", ".")) + local value = require(path) if namespace then namespace[name] = value end end end @@ -69,4 +61,4 @@ function Utils.loadNamespace(pathOrFiles, namespace) return namespace end -return Utils +return Utils \ No newline at end of file diff --git a/concord/world.lua b/concord/world.lua index 7b31a6e..e874881 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -6,12 +6,10 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Filter = require(PATH..".filter") -local Entity = require(PATH..".entity") -local Components = require(PATH..".components") -local Type = require(PATH..".type") -local List = require(PATH..".list") -local Utils = require(PATH..".utils") +local Entity = require(PATH..".entity") +local Type = require(PATH..".type") +local List = require(PATH..".list") +local Utils = require(PATH..".utils") local World = { ENABLE_OPTIMIZATION = true, @@ -20,12 +18,6 @@ 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() @@ -36,24 +28,14 @@ function World.new() __events = {}, __emitSDepth = 0, - __resources = {}, - - __hash = { - state = -2^53, - generator = defaultGenerator, - keys = {}, - entities = {} - }, - __added = List(), __backAdded = List(), __removed = List(), __backRemoved = List(), __dirty = List(), __backDirty = List(), __systemLookup = {}, + __name = nil, __isWorld = true, - - __ignoreEmits = false }, World.__mt) -- Optimization: We deep copy the World class into our instance of a world. @@ -71,7 +53,7 @@ end -- @treturn World self function World:addEntity(e) if not Type.isEntity(e) then - Utils.error(2, "bad argument #1 to 'World:addEntity' (Entity expected, got %s)", type(e)) + error("bad argument #1 to 'World:addEntity' (Entity expected, got "..type(e)..")", 2) end if e.__world then @@ -84,47 +66,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 - -function World:query(def, onMatch) - local filter = Filter.parse(nil, def) - - local list = nil - if not Type.isCallable(onMatch) then - list = type(onMatch) == "table" and onMatch or {} - end - - for _, e in ipairs(self.__entities) do - if Filter.match(e, filter) then - if list then - table.insert(list, e) - else - onMatch(e) - end - end - end - - return list -end - --- Removes an Entity from the World. -- @tparam Entity e Entity to remove -- @treturn World self function World:removeEntity(e) if not Type.isEntity(e) then - Utils.error(2, "bad argument #1 to 'World:removeEntity' (Entity expected, got %s)", type(e)) - end - - if e.__world ~= self then - error("trying to remove an Entity from a World it doesn't belong to", 2) - end - - if e:has("key") then - e:remove("key") + error("bad argument #1 to 'World:removeEntity' (Entity expected, got "..type(e)..")", 2) end self.__removed:add(e) @@ -261,7 +208,7 @@ function World:addSystem(systemClass) local ok, err = tryAddSystem(self, systemClass) if not ok then - Utils.error(2, "bad argument #1 to 'World:addSystem' (%s)", err) + error("bad argument #1 to 'World:addSystem' ("..err..")", 2) end return self @@ -279,7 +226,7 @@ function World:addSystems(...) local ok, err = tryAddSystem(self, systemClass) if not ok then - Utils.error(2, "bad argument #%d to 'World:addSystems' (%s)", i, err) + error("bad argument #"..i.." to 'World:addSystems' ("..err..")", 2) end end @@ -291,7 +238,7 @@ end -- @treturn boolean function World:hasSystem(systemClass) if not Type.isSystemClass(systemClass) then - Utils.error(2, "bad argument #1 to 'World:hasSystem' (SystemClass expected, got %s)", type(systemClass)) + error("bad argument #1 to 'World:getSystem' (systemClass expected, got "..type(systemClass)..")", 2) end return self.__systemLookup[systemClass] and true or false @@ -302,7 +249,7 @@ end -- @treturn System System to get function World:getSystem(systemClass) if not Type.isSystemClass(systemClass) then - Utils.error(2, "bad argument #1 to 'World:getSystem' (SystemClass expected, got %s)", type(systemClass)) + error("bad argument #1 to 'World:getSystem' (systemClass expected, got "..type(systemClass)..")", 2) end return self.__systemLookup[systemClass] @@ -315,21 +262,14 @@ end -- @treturn World self function World:emit(functionName, ...) if not functionName or type(functionName) ~= "string" then - Utils.error(2, "bad argument #1 to 'World:emit' (String expected, got %s)", type(functionName)) + error("bad argument #1 to 'World:emit' (String expected, got "..type(functionName)..")") end local shouldFlush = self.__emitSDepth == 0 self.__emitSDepth = self.__emitSDepth + 1 - local listeners = self.__events[functionName] - - if not self.__ignoreEmits and Type.isCallable(self.beforeEmit) then - self.__ignoreEmits = true - local preventDefaults = self:beforeEmit(functionName, listeners, ...) - self.__ignoreEmits = false - if preventDefaults then return end - end + local listeners = self.__events[functionName] if listeners then for i = 1, #listeners do @@ -345,12 +285,6 @@ 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 @@ -382,103 +316,49 @@ function World:getSystems() return self.__systems end -function World:serialize(ignoreKeys) +function World:serialize() self:__flush() - local data = { generator = self.__hash.state } + local data = {} for i = 1, self.__entities.size do local entity = self.__entities[i] - if entity.serializable then - local entityData = entity:serialize(ignoreKeys) - table.insert(data, entityData) - end + local entityData = entity:serialize() + + data[i] = entityData end return data end -function World:deserialize(data, startClean, ignoreGenerator) - if startClean then +function World:deserialize(data, append) + if (not append) then self:clear() end - if (not ignoreGenerator) then - self.__hash.state = data.generator - end - - local entities = {} - for i = 1, #data do - local entity = Entity(self) + local entityData = data[i] - if data[i].key then - local component = Components.key:__new(entity) - component:deserialize(data[i].key) - entity.key = component + local entity = Entity() + entity:deserialize(entityData) - entity:__dirty() - end - - entities[i] = entity - end - - for i = 1, #data do - entities[i]:deserialize(data[i]) + self:addEntity(entity) 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 +--- Returns true if the World has a name. +-- @treturn boolean +function World:hasName() + return self.__name and true or false 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] +--- 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. @@ -491,25 +371,8 @@ 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 return World.new(...) end, })