diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index ca60d46..17994ca 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -29,7 +29,7 @@ jobs: run: luarocks show ldoc - name: Build docs - run: ldoc . + run: lua scripts/build-docs.lua - name: Deploy uses: JamesIves/github-pages-deploy-action@4.1.5 diff --git a/.gitignore b/.gitignore index 989d364..bd74f33 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ luac.out *.hex # VSCode -.vscode/ \ No newline at end of file +.vscode/ + +# LDoc preprocess output +.ldoc-guides/ \ No newline at end of file diff --git a/README.md b/README.md index c9626cc..ac2dbd4 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ local Concord = require("path.to.concord") ## Documentation -Start with **[Get Started](docs/Get%20Started.md)**. It covers Worlds, Components, Entities, Filters, Systems, and Events. +Start with **[Getting Started](docs/index.html)**. It covers Worlds, Components, Entities, Filters, Systems, and Events. ### Guides -- [Why ECS?](docs/Why%20ECS.md) — when Concord (and ECS in general) is a good fit -- [Custom Pools](docs/Custom%20Pools.md) — using a different data structure than the default Pool list -- [Pool Flushing and Timing](docs/Pool%20Flushing%20and%20Timing.md) — when Filters update, and how that interacts with Events +- [Why ECS?](docs/guides/Why-ECS.md.html) — when Concord (and ECS in general) is a good fit +- [Custom Pools](docs/guides/Custom-Pools.md.html) — using a different data structure than the default Pool list +- [Pool Flushing and Timing](docs/guides/Pool-Flushing-and-Timing.md.html) — when Filters update, and how that interacts with Events API reference is generated with LDoc and published on the [GitHub page](https://keyslam-group.github.io/Concord/). diff --git a/concord/builtins/key.lua b/concord/builtins/key.lua index 7b23275..3591178 100644 --- a/concord/builtins/key.lua +++ b/concord/builtins/key.lua @@ -1,3 +1,7 @@ +--- Built-in Component that assigns a unique key to an Entity in its World. +-- The Entity must belong to a World. Calling the Component returns the key value. +-- @builtin key + local PATH = (...):gsub('%.builtins%.[^%.]+$', '') local Component = require(PATH..".component") @@ -18,14 +22,21 @@ local Key = Component("key", function (self, key) self.value = getKey(self, key) end) +--- Assigns a key from serialized data. +-- @param data Previously serialized key value function Key:deserialize (data) self.value = getKey(self, data) end +--- Returns the key value. +-- @function Key +-- @treturn string|number The Entity's key function Key.__mt:__call() return self.value end +--- Callback: Clears the World key when the Component is removed (not replaced). +-- @tparam boolean replaced True when the Component is being overwritten function Key:removed (replaced) if not replaced then local entity = self.__entity diff --git a/concord/builtins/serializable.lua b/concord/builtins/serializable.lua index 125f76b..1107334 100644 --- a/concord/builtins/serializable.lua +++ b/concord/builtins/serializable.lua @@ -1,9 +1,16 @@ +--- Built-in marker Component. Entities with this Component are included in World:serialize. +-- The Component itself is not serialized. +-- @builtin serializable + local PATH = (...):gsub('%.builtins%.[^%.]+$', '') local Component = require(PATH..".component") local Serializable = Component("serializable") +--- Callback: When the Component gets serialized as part of an Entity. +-- Returns nil so this marker is not written to serialized data. +-- @treturn nil function Serializable:serialize () -- Don't serialize this Component return nil diff --git a/concord/component.lua b/concord/component.lua index 981b999..d04583f 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -11,8 +11,9 @@ Component.__mt = { __index = Component, } ---- Creates a new ComponentClass. --- @tparam function populate Function that populates a Component with values +--- Creates a new ComponentClass and registers it by name. +-- @string name Unique name of the ComponentClass +-- @tparam[opt] function populate Function that populates a Component with values -- @treturn Component A new ComponentClass function Component.new(name, populate) if (type(name) ~= "string") then @@ -51,11 +52,13 @@ end function Component:__populate() -- luacheck: ignore end --- Callback: When the Component gets removed or replaced in an Entity. -function Component:removed() -- luacheck: ignore +--- Callback: When the Component gets removed or replaced in an Entity. +-- @tparam boolean replaced True when the Component is being overwritten, false when it is being removed +function Component:removed(replaced) -- luacheck: ignore end --- Callback: When the Component gets serialized as part of an Entity. +--- Callback: When the Component gets serialized as part of an Entity. +-- @treturn table|nil Serialized data, or nil to omit this Component function Component:serialize() local data = Utils.shallowCopy(self, {}) @@ -68,7 +71,8 @@ function Component:serialize() return data end --- Callback: When the Component gets deserialized from serialized data. +--- Callback: When the Component gets deserialized from serialized data. +-- @tparam table data Serialized data to copy onto this Component function Component:deserialize(data) Utils.shallowCopy(data, self) end @@ -95,7 +99,7 @@ end function Component:__initialize(entity, ...) local component = self:__new(entity) - ---@diagnostic disable-next-line: redundant-parameter + -- LuaLS: redundant-parameter (varargs forwarded to populate) self.__populate(component, ...) return component diff --git a/concord/components.lua b/concord/components.lua index b2743e7..fd08fd9 100644 --- a/concord/components.lua +++ b/concord/components.lua @@ -6,7 +6,7 @@ local Components = {} Components.__REJECT_PREFIX = "!" Components.__REJECT_MATCH = "^(%"..Components.__REJECT_PREFIX.."?)(.+)" ---- Returns true if the containter has the ComponentClass with the specified name +--- Returns true if the container has the ComponentClass with the specified name -- @string name Name of the ComponentClass to check -- @treturn boolean function Components.has(name) @@ -27,10 +27,10 @@ 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. +-- @tparam[opt] boolean acceptRejected Whether to accept names prefixed with the Reject Prefix. +-- @treturn boolean ok +-- @treturn Component|string ComponentClass on success, or an error string on failure +-- @treturn string|boolean On success: the stripped Component name if the name had the Reject Prefix, otherwise false function Components.try(name, acceptRejected) if type(name) ~= "string" then return false, "ComponentsClass name is expected to be a string, got "..type(name)..")" @@ -54,7 +54,7 @@ end --- Returns the ComponentClass with the specified name -- @string name Name of the ComponentClass to get --- @treturn Component +-- @treturn Component The registered ComponentClass function Components.get(name) local ok, value = Components.try(name) diff --git a/concord/entity.lua b/concord/entity.lua index 29145fa..cf8c990 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -13,6 +13,7 @@ local Builtins = require(PATH..".builtins.init") --luacheck: ignore -- Builtins is unused but the require already registers the Components local Entity = { + --- If true, new Entities are given a "serializable" Component. SERIALIZE_BY_DEFAULT = true, } @@ -20,7 +21,8 @@ Entity.__mt = { __index = Entity, } ---- Creates a new Entity. Optionally adds it to a World. +--- Creates a new Entity. Optionally queues it to be added to a World. +-- If Entity.SERIALIZE_BY_DEFAULT is true, the Entity is given a "serializable" Component. -- @tparam[opt] World world World to add the entity to -- @treturn Entity A new Entity function Entity.new(world) @@ -121,25 +123,25 @@ local function removeComponent(e, name) end --- Gives an Entity a Component. --- If the Component already exists, it's overridden by this new Component --- @tparam Component componentClass ComponentClass to add an instance of --- @param ... additional arguments to pass to the Component's populate function +-- If the Component already exists, it's overridden by this new Component. +-- @tparam string|Component name Name of the ComponentClass, or a Component instance to copy onto the Entity +-- @param ... additional arguments to pass to the Component's populate function (ignored when copying a Component) -- @treturn Entity self function Entity:give(name, ...) return giveComponent(self, false, name, ...) end ---- Ensures an Entity to have a Component. --- If the Component already exists, no action is taken --- @tparam Component componentClass ComponentClass to add an instance of --- @param ... additional arguments to pass to the Component's populate function +--- Ensures an Entity has a Component. +-- If the Component already exists, no action is taken. +-- @tparam string|Component name Name of the ComponentClass, or a Component instance to copy onto the Entity +-- @param ... additional arguments to pass to the Component's populate function (ignored when copying a Component) -- @treturn Entity self function Entity:ensure(name, ...) return giveComponent(self, true, name, ...) end --- Removes a Component from an Entity. --- @tparam Component componentClass ComponentClass of the Component to remove +-- @string name Name of the ComponentClass to remove -- @treturn Entity self function Entity:remove(name) local ok, componentClass = Components.try(name) @@ -168,8 +170,8 @@ function Entity:assemble(assemblage, ...) end --- Destroys the Entity. --- Removes the Entity from its World if it's in one. --- @return self +-- Queues removal from its World if it's in one. The Entity stays in the World until the next flush. +-- @treturn Entity self function Entity:destroy() if self.__world then self.__world:removeEntity(self) @@ -189,7 +191,7 @@ function Entity:__dirty() end --- Returns true if the Entity has a Component. --- @tparam Component componentClass ComponentClass of the Component to check +-- @string name Name of the ComponentClass to check -- @treturn boolean function Entity:has(name) local ok, componentClass = Components.try(name) @@ -202,8 +204,8 @@ function Entity:has(name) end --- Gets a Component from the Entity. --- @tparam Component componentClass ComponentClass of the Component to get --- @treturn table +-- @string name Name of the ComponentClass to get +-- @treturn Component|nil The Component, or nil if the Entity does not have it function Entity:get(name) local ok, componentClass = Components.try(name) @@ -214,10 +216,11 @@ function Entity:get(name) return self[name] end ---- Returns a table of all Components the Entity has. --- Warning: Do not modify this table. --- Use Entity:give/ensure/remove instead --- @treturn table Table of all Components the Entity has +--- Returns a shallow copy of the Entity's fields, excluding __world and __isEntity. +-- This is primarily the Entity's Components. Extra non-component fields are also copied. +-- Mutating a copied Component still mutates the Entity's Component. +-- @tparam[opt] table output Table to copy into. A new table is created if omitted. +-- @treturn table function Entity:getComponents(output) output = output or {} local components = Utils.shallowCopy(self, output) @@ -227,7 +230,8 @@ function Entity:getComponents(output) return components end ---- Returns true if the Entity is in a World. +--- Returns true if the Entity has been assigned a World. +-- True as soon as addition is queued, and remains true until removal is flushed. -- @treturn boolean function Entity:inWorld() return self.__world and true or false @@ -239,6 +243,10 @@ function Entity:getWorld() return self.__world end +--- Serializes the Entity's Components. +-- Components that return nil from serialize are omitted. +-- @tparam[opt] boolean ignoreKey If true, the key Component is omitted +-- @treturn table Serialized Entity data function Entity:serialize(ignoreKey) local data = {} @@ -262,6 +270,8 @@ function Entity:serialize(ignoreKey) return data end +--- Deserializes Components onto the Entity. +-- @tparam table data Serialized Entity data function Entity:deserialize(data) for i = 1, #data do local componentData = data[i] diff --git a/concord/filter.lua b/concord/filter.lua index 0c4dabf..da78d12 100644 --- a/concord/filter.lua +++ b/concord/filter.lua @@ -16,9 +16,12 @@ Filter.__mt = { } --- 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. +-- Also checks that def.constructor is callable when present, unless onComponent is provided. +-- @int errorLevel Extra stack level added to error reports +-- @tparam[opt] string name Name for the Filter. If omitted, errors refer to a World:query filter. +-- @tparam table def Table containing the Filter Definition +-- @tparam[opt] function onComponent Called with (component, reject) when a component is valid. +-- reject is the stripped Component name when the entry had the Reject Prefix, otherwise false. function Filter.validate (errorLevel, name, def, onComponent) local filter = "World:query filter" if name then @@ -46,13 +49,12 @@ function Filter.validate (errorLevel, name, def, onComponent) 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 +--- Parses a Filter Definition into a single interleaved table. +-- Entries are stored as name, required pairs: {name, true, name, false, ...} +-- where true means the Component is required and false means it is rejected. +-- @tparam[opt] string name Name for the Filter. +-- @tparam table def Table containing the Filter Definition +-- @treturn table Parsed filter function Filter.parse (name, def) local filter = {} @@ -69,6 +71,10 @@ function Filter.parse (name, def) return filter end +--- Returns true if the Entity matches a parsed Filter. +-- @tparam Entity e Entity to check +-- @tparam table filter Parsed filter from Filter.parse +-- @treturn boolean function Filter.match (e, filter) for i=#filter, 2, -2 do local match = filter[i - 0] @@ -83,6 +89,10 @@ end local REQUIRED_METHODS = {"add", "remove", "has", "clear"} local VALID_POOL_TYPES = {table=true, userdata=true, lightuserdata=true, cdata=true} +--- Checks that a custom Pool implements add, remove, has, and clear as callables. +-- The Pool may be a table, userdata, lightuserdata, or cdata. +-- @string name Name of the Filter / Pool +-- @param pool Pool returned by a custom constructor function Filter.isValidPool (name, pool) local poolType = type(pool) --Check that pool is not nil @@ -99,8 +109,9 @@ function Filter.isValidPool (name, pool) end --- Creates a new Filter +-- If definition.constructor is set, it is called with the definition to create the Pool. -- @string name Name for the Filter. --- @tparam table definition Table containing the Filter Definition +-- @tparam table def Table containing the Filter Definition -- @treturn Filter The new Filter -- @treturn Pool The associated Pool function Filter.new (name, def) @@ -134,6 +145,10 @@ function Filter:eligible(e) return Filter.match(e, self.__filter) end +--- Evaluates an Entity against this Filter and updates the Pool. +-- Adds the Entity if it became eligible, removes it if it no longer is. +-- @tparam Entity e Entity to evaluate +-- @treturn Filter self function Filter:evaluate (e) local has = self.pool:has(e) local eligible = self:eligible(e) @@ -148,9 +163,9 @@ function Filter:evaluate (e) 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. +--- Adds an Entity to the Pool, if it passes the Filter. +-- @tparam Entity e Entity to add +-- @tparam[opt] boolean 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) @@ -163,24 +178,25 @@ function Filter:add (e, bypass) return self, true end --- Remove an Entity from the Pool associated to this Filter. --- @param e Entity to remove +--- Removes an Entity from the Pool associated to this Filter. +-- @tparam Entity 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 +--- Clears the Pool associated to this Filter. +-- Arguments are forwarded to the Pool's clear method. +-- @param e Forwarded to pool:clear -- @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 +--- Checks if the Pool associated to this Filter contains the passed Entity. +-- @tparam Entity e Entity to check -- @treturn boolean Whether the Entity exists. function Filter:has (e) return self.pool:has(e) diff --git a/concord/init.lua b/concord/init.lua index cf0be80..2020a25 100644 --- a/concord/init.lua +++ b/concord/init.lua @@ -1,5 +1,11 @@ ---- +--- A feature-complete ECS library. -- @module Concord +-- @field Entity Entity class +-- @field Component Component class +-- @field Components Registry of ComponentClasses +-- @field System System class +-- @field World World class +-- @field utils Utility functions local PATH = (...):gsub("%.init$", "") diff --git a/concord/list.lua b/concord/list.lua index 7f10c74..cb41f73 100644 --- a/concord/list.lua +++ b/concord/list.lua @@ -1,4 +1,4 @@ ---- Data structure that allows for fast removal at the cost of containing order. +--- Data structure that allows for fast removal at the cost of order. -- @classmod List local List = {} @@ -32,7 +32,7 @@ end --- Removes an object from the List. -- @param obj Object to remove --- @treturn List self +-- @treturn List|nil self, or nil if the object was not in the List function List:remove(obj) local index = self[obj] if not index then return end @@ -57,6 +57,7 @@ function List:remove(obj) end --- Clears the List completely. +-- Does not call onRemoved. -- @treturn List self function List:clear() for i = 1, self.size do @@ -86,6 +87,7 @@ function List:get(i) end --- Returns the index of an object in the List. +-- Errors if the object is not in the List. -- @param obj Object to get index of -- @treturn number index of object in the List. function List:indexOf(obj) @@ -98,7 +100,7 @@ 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. +-- @param order Function that takes two items (a and b) and returns true if a should go before b. -- @treturn List self function List:sort(order) table.sort(self, order) @@ -115,7 +117,7 @@ end function List:onAdded (obj) --luacheck: ignore end ---- Callback for when an item is removed to the List. +--- Callback for when an item is removed from the List. -- @param obj Object that was removed function List:onRemoved (obj) --luacheck: ignore end diff --git a/concord/system.lua b/concord/system.lua index 070c3e4..21fc2e4 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -1,5 +1,5 @@ ---- Iterates over Entities. From these Entities its get Components and modify them. --- A System contains 1 or more Pools. +--- Iterates over Entities. From these Entities it gets Components and modifies them. +-- A System contains 0 or more Pools. -- A System is contained by 1 World. -- @classmod System @@ -25,9 +25,9 @@ System.mt = { __isSystemClass = false, -- Overwrite value from systemClass }, systemClass) - -- Optimization: We deep copy the System class into our instance of a system. + -- Optimization: We shallow copy the System class into our instance of a system. -- This grants slightly faster access times at the cost of memory. - -- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff + -- Since there (generally) won't be many instances of systems this is a worthwhile tradeoff if (System.ENABLE_OPTIMIZATION) then Utils.shallowCopy(systemClass, system) end @@ -45,8 +45,8 @@ System.mt = { end, } --- Creates a new SystemClass. --- @string name Name of the System --- @tparam table definition A table containing filters (name = {components...}) +-- @tparam[opt] string name Name of the System. If omitted, or if a table is passed first, the first argument is the definition. +-- @tparam[opt] table definition A table containing filters (name = {components...}) -- @treturn System A new SystemClass function System.new(name, definition) if type(name) == "table" then @@ -74,9 +74,9 @@ function System.new(name, definition) }, System.mt) systemClass.__index = systemClass - -- Optimization: We deep copy the World class into our instance of a world. + -- Optimization: We shallow copy the System class into the SystemClass. -- This grants slightly faster access times at the cost of memory. - -- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff + -- Since there (generally) won't be many instances of systems this is a worthwhile tradeoff if (System.ENABLE_OPTIMIZATION) then Utils.shallowCopy(System, systemClass) end @@ -118,7 +118,7 @@ function System:__clear() return self end ---- Sets if the System is enabled +--- Sets whether the System is enabled. -- @tparam boolean enable -- @treturn System self function System:setEnabled(enable) @@ -133,7 +133,7 @@ function System:setEnabled(enable) return self end ---- Returns is the System is enabled +--- Returns whether the System is enabled. -- @treturn boolean function System:isEnabled() return self.__enabled diff --git a/concord/type.lua b/concord/type.lua index bf7f52a..a90787a 100644 --- a/concord/type.lua +++ b/concord/type.lua @@ -1,8 +1,11 @@ ---- Type --- Helper module to do easy type checking for Concord types +--- Helper module to do easy type checking for Concord types +-- @module Type local Type = {} +--- Returns true if the value is a function or has a __call metamethod. +-- @param t Object to check +-- @treturn boolean function Type.isCallable(t) if type(t) == "function" then return true end diff --git a/concord/utils.lua b/concord/utils.lua index 0802586..e4ddc63 100644 --- a/concord/utils.lua +++ b/concord/utils.lua @@ -1,15 +1,20 @@ ---- Utils --- Helper module for misc operations +--- Helper module for misc operations +-- @module Utils local Utils = {} +--- Raises a formatted error. +-- @int level Error level passed to error(), plus one +-- @string str Format string +-- @param ... Format arguments 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 +--- Copies keys from orig into target. Existing keys in target are overwritten. +-- @tparam table orig Table to copy from +-- @tparam table target Table to copy into +-- @treturn table target function Utils.shallowCopy(orig, target) for key, value in pairs(orig) do target[key] = value @@ -18,12 +23,14 @@ function Utils.shallowCopy(orig, target) return target end ---- Requires files and puts them in a table. --- Accepts a table of paths to Lua files: {"path/to/file_1", "path/to/another/file_2", "etc"} --- Accepts a path to a directory with Lua files: "my_files/here" --- @param pathOrFiles The table of paths or a path to a directory. --- @param namespace A table that will hold the required files --- @treturn table The namespace table +--- Requires files and stores them in a namespace table. +-- Accepts a table of require paths: {"path/to/file_1", "path/to/another/file_2", "etc"} +-- Accepts a path to a directory of Lua files: "my_files/here" +-- Directory loading uses love.filesystem and requires LÖVE. +-- If namespace is omitted, files are still required and this function returns nil. +-- @param pathOrFiles A directory path or a table of require paths +-- @tparam[opt] table namespace Table that will hold the required files +-- @treturn table|nil 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)) diff --git a/concord/world.lua b/concord/world.lua index c11c9a6..e074e48 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -1,7 +1,6 @@ --- A collection of Systems and Entities. --- A world emits to let Systems iterate. --- A World contains any amount of Systems. --- A World contains any amount of Entities. +-- A World emits events so Systems can iterate their Pools. +-- A World contains any amount of Systems and Entities, plus optional named resources. -- @classmod World local PATH = (...):gsub('%.[^%.]+$', '') @@ -57,7 +56,7 @@ function World.new() __flushing = false, }, World.__mt) - -- Optimization: We deep copy the World class into our instance of a world. + -- Optimization: We shallow copy the World class into our instance of a world. -- This grants slightly faster access times at the cost of memory. -- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff if (World.ENABLE_OPTIMIZATION) then @@ -67,7 +66,9 @@ function World.new() return world end ---- Adds an Entity to the World. +--- Queues an Entity to be added to the World. +-- The Entity's world is set immediately. The Entity is not in getEntities() +-- and is not evaluated by Systems until the next flush. -- @tparam Entity e Entity to add -- @treturn World self function World:addEntity(e) @@ -85,12 +86,20 @@ function World:addEntity(e) return self end ---- Creates a new Entity and adds it to the World. +--- Creates a new Entity and queues it to be added to the World. -- @treturn Entity e the new Entity function World:newEntity() return Entity(self) end +--- Queries flushed Entities against a Filter definition. +-- Pending (not yet flushed) additions are not included. Component changes on +-- already flushed Entities are visible immediately. +-- If onMatch is a callable it is invoked for each match and nothing is returned. +-- Otherwise matches are appended to onMatch when it is a table, or to a new table. +-- @tparam table def Filter definition (component names, with ! prefix to reject) +-- @tparam[opt] function|table onMatch Callback for each match, or a table to fill +-- @treturn table|nil The list of matches, or nil if onMatch was a callback function World:query(def, onMatch) local filter = Filter.parse(nil, def) @@ -112,7 +121,9 @@ function World:query(def, onMatch) return list end ---- Removes an Entity from the World. +--- Queues an Entity to be removed from the World. +-- If the Entity has a key Component, it is removed first. +-- The Entity remains in the World until the next flush. -- @tparam Entity e Entity to remove -- @treturn World self function World:removeEntity(e) @@ -149,8 +160,8 @@ function World:canFlush() return not self.__flushing end ---- Flushes all changes to Entities. --- This processes all entities. Adding and removing entities, as well as reevaluating dirty entities. +--- Applies queued Entity additions, removals, and dirty reevaluations. +-- Only the queued buffers are processed, not every Entity in the World. -- @treturn World self function World:flush() if not self:canFlush() then @@ -221,7 +232,8 @@ function World:flush() return self end --- These functions won't be seen as callbacks that will be emitted to. +-- Callback names that should not be registered as World events. +-- This table is an array, so lookup by callback name never matches. local blacklistedSystemFunctions = { "init", "onEnabled", @@ -269,8 +281,8 @@ local tryAddSystem = function (world, systemClass) end --- Adds a System to the World. --- Callbacks are registered automatically --- Entities added before are added to the System retroactively +-- Functions on the SystemClass are registered as event listeners. +-- Entities already in the World are evaluated against the System's Filters. -- @see World:emit -- @tparam System systemClass SystemClass of System to add -- @treturn World self @@ -285,7 +297,7 @@ function World:addSystem(systemClass) end --- Adds multiple Systems to the World. --- Callbacks are registered automatically +-- Functions on each SystemClass are registered as event listeners. -- @see World:addSystem -- @see World:emit -- @param ... SystemClasses of Systems to add @@ -303,7 +315,7 @@ function World:addSystems(...) return self end ---- Returns if the World has a System. +--- Returns whether the World has a System. -- @tparam System systemClass SystemClass of System to check for -- @treturn boolean function World:hasSystem(systemClass) @@ -326,9 +338,11 @@ function World:getSystem(systemClass) end --- Emits a callback in the World. --- Calls all functions with the functionName of added Systems +-- Calls all functions with the functionName of added Systems. -- Automatically flushes before forwarding the event, unless this emit is nested -- or a flush is already in progress. +-- If beforeEmit exists and returns a truthy value, listeners are skipped. +-- afterEmit is called after listeners when present. -- @string functionName Name of functions to call. -- @param ... Parameters passed to System's functions -- @treturn World self @@ -391,7 +405,9 @@ function World:emitNoFlush(functionName, ...) return self end ---- Removes all entities from the World +--- Removes all Entities from the World and flushes. +-- Entities that were queued to be added are detached immediately without +-- onEntityRemoved. Already flushed Entities are removed through the normal flush. -- @treturn World self function World:clear() for i = 1, self.__entities.size do @@ -409,14 +425,22 @@ function World:clear() return self end +--- Returns the List of flushed Entities in the World. +-- @treturn List function World:getEntities() return self.__entities end +--- Returns the List of Systems in the World. +-- @treturn List function World:getSystems() return self.__systems end +--- Serializes serializable Entities in the World. +-- Flushes first. Only Entities with a serializable Component are included. +-- @tparam[opt] boolean ignoreKeys If true, Entity keys are omitted +-- @treturn table Serialized world data function World:serialize(ignoreKeys) self:flush() @@ -434,6 +458,11 @@ function World:serialize(ignoreKeys) return data end +--- Deserializes Entities into the World. +-- @tparam table data Serialized world data +-- @tparam[opt] boolean startClean If true, existing Entities are cleared first +-- @tparam[opt] boolean ignoreGenerator If true, the World's key generator state is left unchanged +-- @treturn World self function World:deserialize(data, startClean, ignoreGenerator) if startClean then self:clear() @@ -468,6 +497,10 @@ function World:deserialize(data, startClean, ignoreGenerator) return self end +--- Sets the function used to generate Entity keys. +-- @tparam function generator Callable that receives the current state and returns key, newState +-- @param initialState Initial state passed to the generator +-- @treturn World self 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)) @@ -479,6 +512,7 @@ function World:setKeyGenerator(generator, initialState) return self end +-- Internal: Clears the key assigned to an Entity. function World:__clearKey(e) local key = self.__hash.keys[e] @@ -490,6 +524,10 @@ function World:__clearKey(e) return self end +-- Internal: Assigns a key to an Entity, generating one if needed. +-- @param e Entity to assign a key to +-- @param key Optional explicit key +-- @return The assigned key function World:__assignKey(e, key) local hash = self.__hash @@ -512,6 +550,9 @@ function World:__assignKey(e, key) return key end +--- Gets an Entity by its key. +-- @param key Key to look up +-- @treturn Entity|nil function World:getEntityByKey(key) return self.__hash.entities[key] end @@ -526,25 +567,25 @@ end function World:onEntityRemoved(e) -- luacheck: ignore end ---- Sets a named resource in the world +--- Sets a named resource in the World. -- @string name Name of the resource --- @tparam Any resource Resource to set +-- @param 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 +--- Gets a named resource from the World. -- @string name Name of the resource --- @treturn Any resource +-- @return The resource, or nil if none was set function World:getResource(name) return self.__resources[name] end return setmetatable(World, { __call = function(_, ...) - ---@diagnostic disable-next-line: redundant-parameter + -- LuaLS: redundant-parameter (World.new takes no arguments) return World.new(...) end, }) diff --git a/config.ld b/config.ld index 9b7ea55..afc2f6d 100644 --- a/config.ld +++ b/config.ld @@ -1,4 +1,21 @@ project = 'Concord' description = 'A feature-complete ECS library' -file = {'concord', exclude = {}} +file = {'concord', exclude = {'concord/init.lua', 'concord/builtins/init.lua'}} dir = 'docs' + +format = 'markdown' +use_markdown_titles = true + +new_type("builtin", "Builtins", true) +kind_names = {topic = 'Guides'} + +-- Preprocessed by scripts/build-docs.lua +readme = { + '.ldoc-guides/Get-Started.md', + '.ldoc-guides/Why-ECS.md', + '.ldoc-guides/Custom-Pools.md', + '.ldoc-guides/Pool-Flushing-and-Timing.md', +} + +style = '.' + diff --git a/docs/builtins/key.html b/docs/builtins/key.html new file mode 100644 index 0000000..e55ab93 --- /dev/null +++ b/docs/builtins/key.html @@ -0,0 +1,170 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Builtin key

+

Built-in Component that assigns a unique key to an Entity in its World.

+

The Entity must belong to a World. Calling the Component returns the key value.

+ + +

Functions

+ + + + + + + + + + + + + +
Key:deserialize (data)Assigns a key from serialized data.
Key ()Returns the key value.
Key:removed (replaced)Callback: Clears the World key when the Component is removed (not replaced).
+ +
+
+ + +

Functions

+ +
+
+ + Key:deserialize (data) +
+
+ Assigns a key from serialized data. + + +

Parameters:

+
    +
  • data + Previously serialized key value +
  • +
+ + + + + +
+
+ + Key () +
+
+ Returns the key value. + + + +

Returns:

+
    + + string or number + The Entity's key +
+ + + + +
+
+ + Key:removed (replaced) +
+
+ Callback: Clears the World key when the Component is removed (not replaced). + + +

Parameters:

+
    +
  • replaced + boolean + True when the Component is being overwritten +
  • +
+ + + + + +
+
+
+
+
+generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30 +
+
+ + diff --git a/docs/builtins/serializable.html b/docs/builtins/serializable.html new file mode 100644 index 0000000..a5111c5 --- /dev/null +++ b/docs/builtins/serializable.html @@ -0,0 +1,125 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Builtin serializable

+

Built-in marker Component.

+

Entities with this Component are included in World:serialize. + The Component itself is not serialized.

+ + +

Functions

+ + + + + +
Serializable:serialize ()Callback: When the Component gets serialized as part of an Entity.
+ +
+
+ + +

Functions

+ +
+
+ + Serializable:serialize () +
+
+ Callback: When the Component gets serialized as part of an Entity. + Returns nil so this marker is not written to serialized data. + + + +

Returns:

+
    + + nil + + + +
+ + + + +
+
+
+
+
+generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30 +
+
+ + diff --git a/docs/classes/Assemblage.html b/docs/classes/Assemblage.html deleted file mode 100644 index c61009b..0000000 --- a/docs/classes/Assemblage.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - Reference - - - - -
- -
- -
-
-
- - -
- - - - - - -
- -

Class Assemblage

-

Gives an entity a set of components.

-

- - -

Methods

- - - - - - - - - - - - - - - - - -
Assemblage:new (assemble)Creates a new Assemblage.
Assemblage:assemble (e, ...)Assembles an Entity.
Assemblage:hasName ()Returns true if the Assemblage has a name.
Assemblage:getName ()Returns the name of the Assemblage.
- -
-
- - -

Methods

- -
-
- - Assemblage:new (assemble) -
-
- Creates a new Assemblage. - - -

Parameters:

-
    -
  • assemble - function - Function that assembles an Entity -
  • -
- -

Returns:

-
    - - Assemblage - A new assemblage -
- - - - -
-
- - Assemblage:assemble (e, ...) -
-
- Assembles an Entity. - - -

Parameters:

-
    -
  • e - Entity - Entity to assemble -
  • -
  • ... - additional arguments to pass to the assemble function -
  • -
- -

Returns:

-
    - - Assemblage - self -
- - - - -
-
- - Assemblage:hasName () -
-
- Returns true if the Assemblage has a name. - - - -

Returns:

-
    - - boolean - -
- - - - -
-
- - Assemblage:getName () -
-
- Returns the name of the Assemblage. - - - -

Returns:

-
    - - string - -
- - - - -
-
- - -
-
-
-generated by LDoc 1.4.6 -Last updated 2020-01-04 10:27:07 -
-
- - diff --git a/docs/classes/Component.html b/docs/classes/Component.html index 7e0358c..77b2681 100644 --- a/docs/classes/Component.html +++ b/docs/classes/Component.html @@ -26,8 +26,9 @@

Concord

+

Contents

@@ -40,17 +41,28 @@ +

Builtin Components

+

Modules

+

Guides

+ @@ -59,21 +71,35 @@

Class Component

A pure data container that is contained by a single entity.

-

+

+ +

Methods

- - + + - + + + + + + + + + + + + + - +
Component:new (populate)Creates a new ComponentClass.component:new (name[, populate])Creates a new ComponentClass and registers it by name.
Component:hasName ()component:removed (replaced)Callback: When the Component gets removed or replaced in an Entity.
component:serialize ()Callback: When the Component gets serialized as part of an Entity.
component:deserialize (data)Callback: When the Component gets deserialized from serialized data.
component:hasName () Returns true if the Component has a name.
Component:getName ()component:getName () Returns the name of the Component.
@@ -86,18 +112,23 @@
- - Component:new (populate) + + component:new (name[, populate])
- Creates a new ComponentClass. + Creates a new ComponentClass and registers it by name.

Parameters:

@@ -113,8 +144,70 @@
- - Component:hasName () + + component:removed (replaced) +
+
+ Callback: When the Component gets removed or replaced in an Entity. + + +

Parameters:

+ + + + + + +
+
+ + component:serialize () +
+
+ Callback: When the Component gets serialized as part of an Entity. + + + +

Returns:

+
    + + table or nil + Serialized data, or nil to omit this Component +
+ + + + +
+
+ + component:deserialize (data) +
+
+ Callback: When the Component gets deserialized from serialized data. + + +

Parameters:

+ + + + + + +
+
+ + component:hasName ()
Returns true if the Component has a name. @@ -126,6 +219,8 @@ boolean + + @@ -133,8 +228,8 @@
- - Component:getName () + + component:getName ()
Returns the name of the Component. @@ -144,7 +239,9 @@

Returns:

    - string + string + +
@@ -153,13 +250,11 @@
- -
-generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
diff --git a/docs/classes/Entity.html b/docs/classes/Entity.html index b263a9f..8af991d 100644 --- a/docs/classes/Entity.html +++ b/docs/classes/Entity.html @@ -26,12 +26,14 @@

Concord

+

Contents

@@ -40,17 +42,28 @@ +

Builtin Components

+

Modules

+

Guides

+ @@ -63,67 +76,101 @@ contains components which are processed by systems.

+

Fields

+ + + + + +
entity.SERIALIZE_BY_DEFAULTIf true, new Entities are given a "serializable" Component.

Methods

- + - + - - + + - + - + - + - + - + - - + + - - + + - + + + + + + + + +
Entity:new ([world])entity:new ([world]) Creates a new Entity.
Entity:give (componentClass, ...)entity:give (name, ...) Gives an Entity a Component.
Entity:ensure (componentClass, ...)Ensures an Entity to have a Component.entity:ensure (name, ...)Ensures an Entity has a Component.
Entity:remove (componentClass)entity:remove (name) Removes a Component from an Entity.
Entity:assemble (assemblage, ...)entity:assemble (assemblage, ...) Assembles an Entity.
Entity:destroy ()entity:destroy () Destroys the Entity.
Entity:has (componentClass)entity:has (name) Returns true if the Entity has a Component.
Entity:get (componentClass)entity:get (name) Gets a Component from the Entity.
Entity:getComponents ()Returns a table of all Components the Entity has.entity:getComponents ([output])Returns a shallow copy of the Entity's fields, excluding __world and __isEntity.
Entity:inWorld ()Returns true if the Entity is in a World.entity:inWorld ()Returns true if the Entity has been assigned a World.
Entity:getWorld ()entity:getWorld () Returns the World the Entity is in.
entity:serialize ([ignoreKey])Serializes the Entity's Components.
entity:deserialize (data)Deserializes Components onto the Entity.


+

Fields

+ +
+
+ + entity.SERIALIZE_BY_DEFAULT +
+
+ If true, new Entities are given a "serializable" Component. + + + + + + + +
+

Methods

- - Entity:new ([world]) + + entity:new ([world])
- Creates a new Entity. Optionally adds it to a World. + Creates a new Entity. Optionally queues it to be added to a World. + If Entity.SERIALIZEBYDEFAULT is true, the Entity is given a "serializable" Component.

Parameters:

@@ -147,22 +194,22 @@
- - Entity:give (componentClass, ...) + + entity:give (name, ...)
Gives an Entity a Component. - If the Component already exists, it's overridden by this new Component + If the Component already exists, it's overridden by this new Component.

Parameters:

@@ -178,22 +225,22 @@
- - Entity:ensure (componentClass, ...) + + entity:ensure (name, ...)
- Ensures an Entity to have a Component. - If the Component already exists, no action is taken + Ensures an Entity has a Component. + If the Component already exists, no action is taken.

Parameters:

@@ -209,8 +256,8 @@
- - Entity:remove (componentClass) + + entity:remove (name)
Removes a Component from an Entity. @@ -218,9 +265,9 @@

Parameters:

@@ -236,8 +283,8 @@
- - Entity:assemble (assemblage, ...) + + entity:assemble (assemblage, ...)
Assembles an Entity. @@ -266,18 +313,19 @@
- - Entity:destroy () + + entity:destroy ()
Destroys the Entity. - Removes the Entity from its World if it's in one. + Queues removal from its World if it's in one. The Entity stays in the World until the next flush.

Returns:

    + Entity self
@@ -286,8 +334,8 @@
- - Entity:has (componentClass) + + entity:has (name)
Returns true if the Entity has a Component. @@ -295,9 +343,9 @@

Parameters:

@@ -306,6 +354,8 @@ boolean + + @@ -313,8 +363,8 @@
- - Entity:get (componentClass) + + entity:get (name)
Gets a Component from the Entity. @@ -322,17 +372,17 @@

Parameters:

Returns:

    - table - + Component or nil + The Component, or nil if the Entity does not have it
@@ -340,21 +390,31 @@
- - Entity:getComponents () + + entity:getComponents ([output])
- Returns a table of all Components the Entity has. - Warning: Do not modify this table. - Use Entity:give/ensure/remove instead + Returns a shallow copy of the Entity's fields, excluding __world and __isEntity. + This is primarily the Entity's Components. Extra non-component fields are also copied. + Mutating a copied Component still mutates the Entity's Component. +

Parameters:

+

Returns:

    - table - Table of all Components the Entity has + table + + +
@@ -362,11 +422,12 @@
- - Entity:inWorld () + + entity:inWorld ()
- Returns true if the Entity is in a World. + Returns true if the Entity has been assigned a World. + True as soon as addition is queued, and remains true until removal is flushed. @@ -375,6 +436,8 @@ boolean + + @@ -382,8 +445,8 @@
- - Entity:getWorld () + + entity:getWorld ()
Returns the World the Entity is in. @@ -395,20 +458,70 @@ World + + +
+
+ + entity:serialize ([ignoreKey]) +
+
+ Serializes the Entity's Components. + Components that return nil from serialize are omitted. + + +

Parameters:

+ + +

Returns:

+
    + + table + Serialized Entity data +
+ + + + +
+
+ + entity:deserialize (data) +
+
+ Deserializes Components onto the Entity. + + +

Parameters:

+ + + + + +
- -
-generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
diff --git a/docs/classes/Filter.html b/docs/classes/Filter.html new file mode 100644 index 0000000..316a291 --- /dev/null +++ b/docs/classes/Filter.html @@ -0,0 +1,508 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class Filter

+

Used to filter Entities with specific Components + A Filter has an associated Pool that can contain any amount of Entities.

+

+ +

+ + +

Methods

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
filter:validate (errorLevel[, name], def[, onComponent])Validates a Filter Definition to make sure every component is valid.
filter:parse ([name], def)Parses a Filter Definition into a single interleaved table.
filter:match (e, filter)Returns true if the Entity matches a parsed Filter.
filter:isValidPool (name, pool)Checks that a custom Pool implements add, remove, has, and clear as callables.
filter:new (name, def)Creates a new Filter + If definition.constructor is set, it is called with the definition to create the Pool.
filter:eligible (e)Checks if an Entity fulfills the Filter requirements.
filter:evaluate (e)Evaluates an Entity against this Filter and updates the Pool.
filter:add (e[, bypass])Adds an Entity to the Pool, if it passes the Filter.
filter:remove (e)Removes an Entity from the Pool associated to this Filter.
filter:clear (e)Clears the Pool associated to this Filter.
filter:has (e)Checks if the Pool associated to this Filter contains the passed Entity.
filter:getName ()Gets the name of the Filter
+ +
+
+ + +

Methods

+ +
+
+ + filter:validate (errorLevel[, name], def[, onComponent]) +
+
+ Validates a Filter Definition to make sure every component is valid. + Also checks that def.constructor is callable when present, unless onComponent is provided. + + +

Parameters:

+
    +
  • errorLevel + integer + Extra stack level added to error reports +
  • +
  • name + string + Name for the Filter. If omitted, errors refer to a World:query filter. + (optional) +
  • +
  • def + table + Table containing the Filter Definition +
  • +
  • onComponent + function + Called with (component, reject) when a component is valid. + reject is the stripped Component name when the entry had the Reject Prefix, otherwise false. + (optional) +
  • +
+ + + + + +
+
+ + filter:parse ([name], def) +
+
+ Parses a Filter Definition into a single interleaved table. + Entries are stored as name, required pairs: {name, true, name, false, ...} + where true means the Component is required and false means it is rejected. + + +

Parameters:

+
    +
  • name + string + Name for the Filter. + (optional) +
  • +
  • def + table + Table containing the Filter Definition +
  • +
+ +

Returns:

+
    + + table + Parsed filter +
+ + + + +
+
+ + filter:match (e, filter) +
+
+ Returns true if the Entity matches a parsed Filter. + + +

Parameters:

+
    +
  • e + Entity + Entity to check +
  • +
  • filter + table + Parsed filter from Filter.parse +
  • +
+ +

Returns:

+
    + + boolean + + + +
+ + + + +
+
+ + filter:isValidPool (name, pool) +
+
+ Checks that a custom Pool implements add, remove, has, and clear as callables. + The Pool may be a table, userdata, lightuserdata, or cdata. + + +

Parameters:

+
    +
  • name + string + Name of the Filter / Pool +
  • +
  • pool + Pool returned by a custom constructor +
  • +
+ + + + + +
+
+ + filter:new (name, def) +
+
+ Creates a new Filter + If definition.constructor is set, it is called with the definition to create the Pool. + + +

Parameters:

+
    +
  • name + string + Name for the Filter. +
  • +
  • def + table + Table containing the Filter Definition +
  • +
+ +

Returns:

+
    +
  1. + Filter + The new Filter
  2. +
  3. + Pool + The associated Pool
  4. +
+ + + + +
+
+ + filter:eligible (e) +
+
+ Checks if an Entity fulfills the Filter requirements. + + +

Parameters:

+
    +
  • e + Entity + Entity to check +
  • +
+ +

Returns:

+
    + + boolean + + + +
+ + + + +
+
+ + filter:evaluate (e) +
+
+ Evaluates an Entity against this Filter and updates the Pool. + Adds the Entity if it became eligible, removes it if it no longer is. + + +

Parameters:

+
    +
  • e + Entity + Entity to evaluate +
  • +
+ +

Returns:

+
    + + Filter + self +
+ + + + +
+
+ + filter:add (e[, bypass]) +
+
+ Adds an Entity to the Pool, if it passes the Filter. + + +

Parameters:

+
    +
  • e + Entity + Entity to add +
  • +
  • bypass + boolean + Whether to bypass the Filter or not. + (optional) +
  • +
+ +

Returns:

+
    +
  1. + Filter + self
  2. +
  3. + boolean + Whether the entity was added or not.
  4. +
+ + + + +
+
+ + filter:remove (e) +
+
+ Removes an Entity from the Pool associated to this Filter. + + +

Parameters:

+
    +
  • e + Entity + Entity to remove +
  • +
+ +

Returns:

+
    + + Filter + self +
+ + + + +
+
+ + filter:clear (e) +
+
+ Clears the Pool associated to this Filter. + Arguments are forwarded to the Pool's clear method. + + +

Parameters:

+
    +
  • e + Forwarded to pool:clear +
  • +
+ +

Returns:

+
    + + Filter + self +
+ + + + +
+
+ + filter:has (e) +
+
+ Checks if the Pool associated to this Filter contains the passed Entity. + + +

Parameters:

+
    +
  • e + Entity + Entity to check +
  • +
+ +

Returns:

+
    + + boolean + Whether the Entity exists. +
+ + + + +
+
+ + filter:getName () +
+
+ Gets the name of the Filter + + + +

Returns:

+
    + + string + + + +
+ + + + +
+
+
+
+
+generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30 +
+
+ + diff --git a/docs/classes/List.html b/docs/classes/List.html index 414888a..7821dcd 100644 --- a/docs/classes/List.html +++ b/docs/classes/List.html @@ -26,8 +26,9 @@

Concord

+

Contents

@@ -40,17 +41,28 @@ +

Builtin Components

+

Modules

+

Guides

+ @@ -58,40 +70,54 @@

Class List

-

Data structure that allows for fast removal at the cost of containing order.

-

+

Data structure that allows for fast removal at the cost of order.

+

+ +

Methods

- + - + - + - + - + - + - + + + + + + + + + + + + +
List:new ()list:new () Creates a new List.
List:add (obj)list:add (obj) Adds an object to the List.
List:remove (obj)list:remove (obj) Removes an object from the List.
List:clear ()list:clear () Clears the List completely.
List:has (obj)list:has (obj) Returns true if the List has the object.
List:get (i)list:get (i) Returns the object at an index.
List:indexOf (obj)list:indexOf (obj) Returns the index of an object in the List.
list:sort (order)Sorts the List in place, using the order function.
list:onAdded (obj)Callback for when an item is added to the List.
list:onRemoved (obj)Callback for when an item is removed from the List.

@@ -102,8 +128,8 @@
- - List:new () + + list:new ()
Creates a new List. @@ -122,13 +148,13 @@
- - List:add (obj) + + list:add (obj)
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'

Parameters:

@@ -150,8 +176,8 @@
- - List:remove (obj) + + list:remove (obj)
Removes an object from the List. @@ -167,8 +193,8 @@

Returns:

    - List - self + List or nil + self, or nil if the object was not in the List
@@ -176,11 +202,12 @@
- - List:clear () + + list:clear ()
Clears the List completely. + Does not call onRemoved. @@ -196,8 +223,8 @@
- - List:has (obj) + + list:has (obj)
Returns true if the List has the object. @@ -215,6 +242,8 @@ boolean + + @@ -222,8 +251,8 @@
- - List:get (i) + + list:get (i)
Returns the object at an index. @@ -248,11 +277,12 @@
- - List:indexOf (obj) + + list:indexOf (obj)
Returns the index of an object in the List. + Errors if the object is not in the List.

Parameters:

@@ -272,15 +302,80 @@ +
+
+ + list:sort (order) +
+
+ 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. + + +

Parameters:

+
    +
  • order + Function that takes two items (a and b) and returns true if a should go before b. +
  • +
+ +

Returns:

+
    + + List + self +
+ + + + +
+
+ + list:onAdded (obj) +
+
+ Callback for when an item is added to the List. + + +

Parameters:

+
    +
  • obj + Object that was added +
  • +
+ + + + + +
+
+ + list:onRemoved (obj) +
+
+ Callback for when an item is removed from the List. + + +

Parameters:

+
    +
  • obj + Object that was removed +
  • +
+ + + + +
- -
-generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
diff --git a/docs/classes/Pool.html b/docs/classes/Pool.html deleted file mode 100644 index 77be777..0000000 --- a/docs/classes/Pool.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - Reference - - - - -
- -
- -
-
-
- - -
- - - - - - -
- -

Class Pool

-

Used to iterate over Entities with a specific Components - A Pool contain a any amount of Entities.

-

- - -

Methods

- - - - - - - - - - - - - - - - - - - - - - - - - -
Pool:new (name, filter)Creates a new Pool
Pool:eligible (e)Checks if an Entity is eligible for the Pool.
Pool:evaluate (e)Evaluate whether an Entity should be added or removed from the Pool.
Pool:getName ()Gets the name of the Pool
Pool:getFilter ()Gets the filter of the Pool.
Pool:onEntityAdded (e)Callback for when an Entity is added to the Pool.
- -
-
- - -

Methods

- -
-
- - Pool:new (name, filter) -
-
- Creates a new Pool - - -

Parameters:

-
    -
  • name - string - Name for the Pool. -
  • -
  • filter - table - Table containing the required BaseComponents -
  • -
- -

Returns:

-
    - - Pool - The new Pool -
- - - - -
-
- - Pool:eligible (e) -
-
- Checks if an Entity is eligible for the Pool. - - -

Parameters:

-
    -
  • e - Entity - Entity to check -
  • -
- -

Returns:

-
    - - boolean - -
- - - - -
-
- - Pool:evaluate (e) -
-
- Evaluate whether an Entity should be added or removed from the Pool. - - -

Parameters:

-
    -
  • e - Entity to add or remove -
  • -
- -

Returns:

-
    - - Pool - self -
- - - - -
-
- - Pool:getName () -
-
- Gets the name of the Pool - - - -

Returns:

-
    - - string - -
- - - - -
-
- - Pool:getFilter () -
-
- Gets the filter of the Pool. - Warning: Do not modify this filter. - - - -

Returns:

-
    - - Filter of the Pool. -
- - - - -
-
- - Pool:onEntityAdded (e) -
-
- Callback for when an Entity is added to the Pool. - - -

Parameters:

-
    -
  • e - Entity - Entity that was added. -
  • -
- - - - - -
-
- - -
-
-
-generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 -
-
- - diff --git a/docs/classes/System.html b/docs/classes/System.html index a73ed8b..06fa453 100644 --- a/docs/classes/System.html +++ b/docs/classes/System.html @@ -26,8 +26,9 @@

Concord

+

Contents

@@ -41,17 +42,28 @@ +

Builtin Components

+

Modules

+

Guides

+ @@ -60,50 +72,42 @@

Class System

Iterates over Entities.

-

From these Entities its get Components and modify them. - A System contains 1 or more Pools. +

From these Entities it gets Components and modifies them. + A System contains 0 or more Pools. A System is contained by 1 World.

Methods

- + - - + + - - + + - + - - - - - - - -
System:new (table)system:new ([name[, definition]]) Creates a new SystemClass.
System:setEnabled (enable)Sets if the System is enabledsystem:setEnabled (enable)Sets whether the System is enabled.
System:isEnabled ()Returns is the System is enabledsystem:isEnabled ()Returns whether the System is enabled.
System:getWorld ()system:getWorld () Returns the World the System is in.
System:hasName ()Returns true if the System has a name.
System:getName ()Returns the name of the System.

Callbacks

- + - + - +
System:init (world)system:init (world) Callback for system initialization.
System:onEnabled ()system:onEnabled () Callback for when a System is enabled.
System:onDisabled ()system:onDisabled () Callback for when a System is disabled.
@@ -116,8 +120,8 @@
- - System:new (table) + + system:new ([name[, definition]])
Creates a new SystemClass. @@ -125,8 +129,15 @@

Parameters:

@@ -142,11 +153,11 @@
- - System:setEnabled (enable) + + system:setEnabled (enable)
- Sets if the System is enabled + Sets whether the System is enabled.

Parameters:

@@ -154,6 +165,8 @@
  • enable boolean + +
  • @@ -169,11 +182,11 @@
    - - System:isEnabled () + + system:isEnabled ()
    - Returns is the System is enabled + Returns whether the System is enabled. @@ -182,6 +195,8 @@ boolean + + @@ -189,8 +204,8 @@
    - - System:getWorld () + + system:getWorld ()
    Returns the World the System is in. @@ -202,46 +217,8 @@ World - - - -
    -
    - - System:hasName () -
    -
    - Returns true if the System has a name. - - - -

    Returns:

    -
      - - boolean - -
    - - - - -
    -
    - - System:getName () -
    -
    - Returns the name of the System. - - - -

    Returns:

    -
      - - string -
    @@ -253,8 +230,8 @@
    - - System:init (world) + + system:init (world)
    Callback for system initialization. @@ -274,8 +251,8 @@
    - - System:onEnabled () + + system:onEnabled ()
    Callback for when a System is enabled. @@ -288,8 +265,8 @@
    - - System:onDisabled () + + system:onDisabled ()
    Callback for when a System is disabled. @@ -302,13 +279,11 @@
    - -
    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
    diff --git a/docs/classes/World.html b/docs/classes/World.html index e673fa8..6930d9d 100644 --- a/docs/classes/World.html +++ b/docs/classes/World.html @@ -26,8 +26,9 @@

    Concord

    +

    Contents

    @@ -40,17 +41,28 @@ +

    Builtin Components

    +

    Modules

    +

    Guides

    + @@ -59,66 +71,108 @@

    Class World

    A collection of Systems and Entities.

    -

    - A world emits to let Systems iterate. - A World contains any amount of Systems. - A World contains any amount of Entities.

    +

    A World emits events so Systems can iterate their Pools. + A World contains any amount of Systems and Entities, plus optional named resources.

    Methods

    - + - - + + - - + + - + + + + + + + + + + + + + + + + + - + - - + + - + - + - - + + - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + +
    World:new ()world:new () Creates a new World.
    World:addEntity (e)Adds an Entity to the World.world:addEntity (e)Queues an Entity to be added to the World.
    World:removeEntity (e)Removes an Entity from the World.world:newEntity ()Creates a new Entity and queues it to be added to the World.
    World:addSystem (systemClass)world:query (def[, onMatch])Queries flushed Entities against a Filter definition.
    world:removeEntity (e)Queues an Entity to be removed from the World.
    world:canFlush ()Returns whether a Flush can currently be performed.
    world:flush ()Applies queued Entity additions, removals, and dirty reevaluations.
    world:addSystem (systemClass) Adds a System to the World.
    World:addSystems (...)world:addSystems (...) Adds multiple Systems to the World.
    World:hasSystem (systemClass)Returns if the World has a System.world:hasSystem (systemClass)Returns whether the World has a System.
    World:getSystem (systemClass)world:getSystem (systemClass) Gets a System from the World.
    World:emit (functionName, ...)world:emit (functionName, ...) Emits a callback in the World.
    World:clear ()Removes all entities from the Worldworld:emitNoFlush (functionName, ...)Emits a callback in the World without flushing.
    World:hasName ()Returns true if the World has a name.world:clear ()Removes all Entities from the World and flushes.
    World:getName ()Returns the name of the World.world:getEntities ()Returns the List of flushed Entities in the World.
    World:onEntityAdded (e)world:getSystems ()Returns the List of Systems in the World.
    world:serialize ([ignoreKeys])Serializes serializable Entities in the World.
    world:deserialize (data[, startClean[, ignoreGenerator]])Deserializes Entities into the World.
    world:setKeyGenerator (generator, initialState)Sets the function used to generate Entity keys.
    world:getEntityByKey (key)Gets an Entity by its key.
    world:onEntityAdded (e) Callback for when an Entity is added to the World.
    World:onEntityRemoved (e)world:onEntityRemoved (e) Callback for when an Entity is removed from the World.
    world:setResource (name, resource)Sets a named resource in the World.
    world:getResource (name)Gets a named resource from the World.

    @@ -129,8 +183,8 @@
    - - World:new () + + world:new ()
    Creates a new World. @@ -149,11 +203,13 @@
    - - World:addEntity (e) + + world:addEntity (e)
    - Adds an Entity to the World. + Queues an Entity to be added to the World. + The Entity's world is set immediately. The Entity is not in getEntities() + and is not evaluated by Systems until the next flush.

    Parameters:

    @@ -176,11 +232,69 @@
    - - World:removeEntity (e) + + world:newEntity ()
    - Removes an Entity from the World. + Creates a new Entity and queues it to be added to the World. + + + +

    Returns:

    +
      + + Entity + e the new Entity +
    + + + + +
    +
    + + world:query (def[, onMatch]) +
    +
    + Queries flushed Entities against a Filter definition. + Pending (not yet flushed) additions are not included. Component changes on + already flushed Entities are visible immediately. + If onMatch is a callable it is invoked for each match and nothing is returned. + Otherwise matches are appended to onMatch when it is a table, or to a new table. + + +

    Parameters:

    +
      +
    • def + table + Filter definition (component names, with ! prefix to reject) +
    • +
    • onMatch + function or table + Callback for each match, or a table to fill + (optional) +
    • +
    + +

    Returns:

    +
      + + table or nil + The list of matches, or nil if onMatch was a callback +
    + + + + +
    +
    + + world:removeEntity (e) +
    +
    + Queues an Entity to be removed from the World. + If the Entity has a key Component, it is removed first. + The Entity remains in the World until the next flush.

    Parameters:

    @@ -203,13 +317,58 @@
    - - World:addSystem (systemClass) + + world:canFlush () +
    +
    + Returns whether a Flush can currently be performed. + Flushing is forbidden while a Flush is already in progress + (for example from World:onEntityAdded / Pool:onAdded). + + + +

    Returns:

    +
      + + boolean + + + +
    + + + + +
    +
    + + world:flush () +
    +
    + Applies queued Entity additions, removals, and dirty reevaluations. + Only the queued buffers are processed, not every Entity in the World. + + + +

    Returns:

    +
      + + World + self +
    + + + + +
    +
    + + world:addSystem (systemClass)
    Adds a System to the World. - Callbacks are registered automatically - Entities added before are added to the System retroactively + Functions on the SystemClass are registered as event listeners. + Entities already in the World are evaluated against the System's Filters.

    Parameters:

    @@ -230,18 +389,18 @@

    See also:

    - - World:addSystems (...) + + world:addSystems (...)
    Adds multiple Systems to the World. - Callbacks are registered automatically + Functions on each SystemClass are registered as event listeners.

    Parameters:

    @@ -261,18 +420,18 @@

    See also:

    - - World:hasSystem (systemClass) + + world:hasSystem (systemClass)
    - Returns if the World has a System. + Returns whether the World has a System.

    Parameters:

    @@ -288,6 +447,8 @@ boolean + + @@ -295,8 +456,8 @@
    - - World:getSystem (systemClass) + + world:getSystem (systemClass)
    Gets a System from the World. @@ -322,18 +483,22 @@
    - - World:emit (functionName, ...) + + world:emit (functionName, ...)
    Emits a callback in the World. - Calls all functions with the functionName of added Systems + Calls all functions with the functionName of added Systems. + Automatically flushes before forwarding the event, unless this emit is nested + or a flush is already in progress. + If beforeEmit exists and returns a truthy value, listeners are skipped. + afterEmit is called after listeners when present.

    Parameters:

    • functionName - string + string Name of functions to call.
    • ... @@ -353,11 +518,47 @@
    - - World:clear () + + world:emitNoFlush (functionName, ...)
    - Removes all entities from the World + Emits a callback in the World without flushing. + + +

    Parameters:

    +
      +
    • functionName + string + Name of functions to call. +
    • +
    • ... + Parameters passed to System's functions +
    • +
    + +

    Returns:

    +
      + + World + self +
    + + +

    See also:

    + + + +
    +
    + + world:clear () +
    +
    + Removes all Entities from the World and flushes. + Entities that were queued to be added are detached immediately without + onEntityRemoved. Already flushed Entities are removed through the normal flush. @@ -373,18 +574,20 @@
    - - World:hasName () + + world:getEntities ()
    - Returns true if the World has a name. + Returns the List of flushed Entities in the World.

    Returns:

      - boolean + List + +
    @@ -393,18 +596,20 @@
    - - World:getName () + + world:getSystems ()
    - Returns the name of the World. + Returns the List of Systems in the World.

    Returns:

      - string + List + +
    @@ -413,8 +618,132 @@
    - - World:onEntityAdded (e) + + world:serialize ([ignoreKeys]) +
    +
    + Serializes serializable Entities in the World. + Flushes first. Only Entities with a serializable Component are included. + + +

    Parameters:

    +
      +
    • ignoreKeys + boolean + If true, Entity keys are omitted + (optional) +
    • +
    + +

    Returns:

    +
      + + table + Serialized world data +
    + + + + +
    +
    + + world:deserialize (data[, startClean[, ignoreGenerator]]) +
    +
    + Deserializes Entities into the World. + + +

    Parameters:

    +
      +
    • data + table + Serialized world data +
    • +
    • startClean + boolean + If true, existing Entities are cleared first + (optional) +
    • +
    • ignoreGenerator + boolean + If true, the World's key generator state is left unchanged + (optional) +
    • +
    + +

    Returns:

    +
      + + World + self +
    + + + + +
    +
    + + world:setKeyGenerator (generator, initialState) +
    +
    + Sets the function used to generate Entity keys. + + +

    Parameters:

    +
      +
    • generator + function + Callable that receives the current state and returns key, newState +
    • +
    • initialState + Initial state passed to the generator +
    • +
    + +

    Returns:

    +
      + + World + self +
    + + + + +
    +
    + + world:getEntityByKey (key) +
    +
    + Gets an Entity by its key. + + +

    Parameters:

    +
      +
    • key + Key to look up +
    • +
    + +

    Returns:

    +
      + + Entity or nil + + + +
    + + + + +
    +
    + + world:onEntityAdded (e)
    Callback for when an Entity is added to the World. @@ -434,8 +763,8 @@
    - - World:onEntityRemoved (e) + + world:onEntityRemoved (e)
    Callback for when an Entity is removed from the World. @@ -453,15 +782,69 @@ +
    +
    + + world:setResource (name, resource) +
    +
    + Sets a named resource in the World. + + +

    Parameters:

    +
      +
    • name + string + Name of the resource +
    • +
    • resource + Resource to set +
    • +
    + +

    Returns:

    +
      + + World + self +
    + + + + +
    +
    + + world:getResource (name) +
    +
    + Gets a named resource from the World. + + +

    Parameters:

    +
      +
    • name + string + Name of the resource +
    • +
    + +

    Returns:

    +
      + + The resource, or nil if none was set +
    + + + +
    - -
    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
    diff --git a/docs/guides/Custom-Pools.md.html b/docs/guides/Custom-Pools.md.html new file mode 100644 index 0000000..41ccd52 --- /dev/null +++ b/docs/guides/Custom-Pools.md.html @@ -0,0 +1,257 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + + +

    Custom Pools

    + +

    This article details a escape hatch when the List used by Concord Pools is not enough for your System's need.

    + +

    First we need to understand why this escape hatch is needed so we discuss some upsides and downsides of Lists, we then discuss the solution based on onAdded/onRemoved that Pools provide, and finally look at the escape hatch and its benefits.

    + +

    +

    Lists upsides and downsides

    + +

    Pools in Concord by default use a List implementation that provides fast inserts, fast removals, fast search and fast iteration.

    + +

    These are all useful for Pools:

    + +
      +
    • Pools need to check if an Entity is part of the List or not.
    • +
    • Pools need to add and remove Entities quickly
    • +
    • The user needs to be able to iterate through the Pool quickly.
    • +
    + +

    However these Lists have a few downsides:

    + +
      +
    • They consume some extra memory because they need to keep a value -> key hashmap
    • +
    • They can't guarantee the order of their members
    • +
    • Sorting is expensive, since the hashmap needs to be regenerated
    • +
    + +

    +

    List Synchronization/Duplication

    + +

    Sometimes you need to add your Entities to a different data structure like a Spatial Hash for Physics for example, when working with such a structure, you may end up with code that looks like this:

    + + +
    +local Physics = Concord.System("Physics", {
    +  boxes = {"position", "size"}
    +})
    +
    +function Physics:init ()
    +  -- Create a new spatial hash
    +  local spatialhash = spatialhash()
    +  self.spatialhash = spatialhash
    +
    +  -- Whenever there is a new box added to the boxes list
    +  -- Add the box to the spatial hash
    +  function self.boxes:onAdded (entity)
    +    spatialhash:add(entity)
    +  end
    +
    +  -- Do the same when a box is removed
    +  function self.boxes:onRemoved (entity)
    +    spatialhash:remove(entity)
    +  end
    +end
    +
    + + +

    Another fairly common example would be if you wanted a Sorted List for Rendering, and don't want the extra cost of sorting the List on each Event and instead would prefer to sort on insertion.

    + +

    As you can see we have duplication of data, our Spatial Hash has all the same Entities that our boxes List. And now the code to sync the List with the Spatial Hash lives inside of the Physics System, which makes it hard to share or reuse.

    + +

    So Concord offers an escape hatch out of this situation. If you only need the filtering part of a Pool and not the List part, you can bring your own data structure.

    + +

    +

    Bring your own data structure

    + +

    This feature is called Custom Pools

    + +

    The way to define a Custom Pool is fairly simple. You need to create a function that returns an object with 4 methods: has, add, remove and clear

    + +

    We could redo our example above by creating a Custom Pool wrapper for our Spatial Hash (this is not always needed but may be helpful if the method names or their parameters don't match)

    + + +
    +function SpatialHashPool ()
    +  local storage = {hash = spatialhash()}
    +
    +  function storage:clear()
    +    self.hash = spatialhash()
    +  end
    +
    +  function storage:add(entity)
    +    self.hash:add(entity)
    +  end
    +
    +  function storage:remove(entity)
    +    self.hash:remove(entity)
    +  end
    +
    +  function storage:has(entity)
    +    if self.hash:search(entity) > 0 then
    +      return true
    +    else
    +      return false
    +    end
    +  end
    +
    +  return storage
    +end
    +
    + + +

    Then we can use this in any System like so

    + + +
    +local Physics = Concord.System("Physics", {
    +  boxes = {"position", "size", constructor = SpatialHashPool}
    +})
    +
    + + +

    Please note that the code now lives outside of the Physics System and can now be reused by different Systems or even different projects. This abstraction helps us share and reuse code.

    + +

    Additional Methods

    + +

    The Storage we defined only exposes the methods needed by Concord which means that in order to iterate or perform other iterations you would need to use self.boxes.hash to access the Spatial Hash directly. However Concord does not enforce any limit on the methods exposed by your Pool other than those 4 required methods, so you could expose more utility functions that you need to access in your System's code.

    + +

    For example we could add

    + + +
    +function storage:query(x, y, w, h)
    +  return self.hash:query(x, y, w, h)
    +end
    +
    + + +

    And use it in our System like so:

    + + +
    +function Physics:update(dt)
    +  local visibleEntities = self.boxes:query(0, 0, 800, 600)
    +
    +  for _, entity in ipairs(visibleEntities) do
    +    -- Do something
    +  end
    +end
    +
    + + +

    Pool definition

    + +

    One advantage Custom Pools have is that when they are instantiated you get the complete Filter definition for the Pool as argument to the constructor function:

    + + +
    +function SpatialHashPool (def)
    +  print(def) -- {"position", "size"}
    +
    +  -- ...
    +end
    +
    + + +

    This could be useful if you need some additional parameters to generate your storage for example

    + + +
    +function SpatialHashPool (def)
    +  local storage = { hash = spatialhash(def.width, def.height) }
    +
    +  -- ...
    +
    +  return storage
    +end
    +
    +-------
    +
    +local Physics = Concord.System("Physics", {
    +  boxes = {"position", "size", constructor = SpatialHashPool, width = 800, height = 600}
    +})
    +
    +
    +
    +
    +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30 +
    +
    + + diff --git a/docs/guides/Get-Started.md.html b/docs/guides/Get-Started.md.html new file mode 100644 index 0000000..5f93ff6 --- /dev/null +++ b/docs/guides/Get-Started.md.html @@ -0,0 +1,12 @@ + + + + + + + Getting Started + + +

    Getting Started

    + + diff --git a/docs/guides/Pool-Flushing-and-Timing.md.html b/docs/guides/Pool-Flushing-and-Timing.md.html new file mode 100644 index 0000000..d081856 --- /dev/null +++ b/docs/guides/Pool-Flushing-and-Timing.md.html @@ -0,0 +1,225 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + + +

    Pool Flushing and Timing

    + +

    This article will help you understand when the Pools' Filters get synchronized with the latest changes to Entities in the World, the relationship with Events, how it affects your game and how you can avoid the default behaviors.

    + +

    +

    Synchronous Operations

    + +

    Concord has a synchronous part and an asynchronous part. For the most part you'll be using the synchronous part, so whenever you make a change, most of the things update immediately, for example, giving a component, removing a component, getting a reference to an entity, sorting a pool and many others are synchronous operations, they execute immediately when you perform them and you can see the effects in real time.

    + + +
    +
    +entity:give("position", 10, 20)
    +
    +entity.position -- { x = 10, y = 20 }
    +
    +entity.position.x = 30
    +
    +entity.position -- { x = 30, y = 20 }
    +
    +world:emit("update") -- All Systems with an update listener are called immediately
    +                     -- in the order you added them to the World
    +
    +
    + + +

    As you can see most operations are immediate and you can see the results right after

    + +

    +

    Asynchronous Operations

    + +

    There is one place however where we can't have immediate updates without introducing other sets of issues. That is Pools and Filters.

    + +

    As discussed in the in-depth Systems article, all Pools have an associated Filter, this Filter specified all the requirements Entities should have to be in the Pool. The one checking that Pools have all the Entities that meet their requirements is the World.

    + +

    Syncing the Pools

    + +

    In order to make sure that the Pools have the Entities they need, the World goes through all the necessary Entities and checks them against the filters of every Pool, one by one. This is a very expensive operation which we call Flush.

    + +

    You can actually trigger a Flush manually through World:flush() and you may sometimes want to do so. We will discuss the use cases later on.

    + +

    When do we sync?

    + +

    Entities change a lot, we often change multiple Components one after the other, add Entities in bulk, apply Assemblages that give multiple Components at once, delete a bunch of Entities that died simultaneously and so on.

    + +

    The World gets notified of ALL these changes, then keeps track of all the Entities that have been changed, removed, or added to the World, and waits until it's ready to perform a Flush.

    + +

    Flushes can be manually triggered as we previously discussed, but Concord also has a built-in mechanism for flushes to happen automatically.

    + +

    When can the sync cause issues?

    + +

    A sync performs additions and removals on Pools, so the biggest issue it can create is if you were iterating over one Pool, and suddenly the Entities inside were to shift around. This is not a problem with Concord, its List implementation or flushes, this also happens in regular Lua:

    + + +
    +for k, v in ipairs(t) do
    +  table.insert(t, 1, k) -- This has undefined behavior and can cause an infinite loop
    +  print(k, v)
    +end
    +
    + + +

    So it's not a good idea to add or remove inside to the current Pool we are iterating on. So we should not flush while we iterate. These iterations generally happen inside event listeners inside your Systems, since that's where Pools live, so we shouldn't flush while a System's event listener is executing.

    + +

    World:emit()

    + +

    Concord tries to keep the list of Entities consistent through an entire emit event and won't automatically flush until the Event is over. This is so that if System A and System B handle the same event their Pools are consistent with each other, this assumption may be broken if World:flush() is called manually, but that's an explicit choice by the user.

    + +

    The best time for the automatic flush then is before the Event is forwarded to the Systems, it's a spot where no updates are being made, no iteration is going on, the World has complete control of the main thread.

    + +

    However World:emit() can be called from inside World:emit() so Concord makes sure that by default a nested World:emit() doesn't cause an automatic flush.

    + +

    If you wanted to emit an Event without causing a flush at all you can use World:emitNoFlush() as a replacement to World:emit(). This can be used to gain more control on when flushes happen.

    + + +
    +love.update = function (dt)
    +  World:emit("update", dt) -- Flushes before forwarding the event to the Systems
    +end
    +
    +love.draw = function ()
    +  World:emitNoFlush("draw") -- Doesn't flush
    +end
    +
    +function RenderSystem:update ()
    +  self:getWorld():emit("prepareForRender") -- Doesn't flush
    +
    +  self:getWorld():flush() -- This is a manual flush
    +end
    +
    + + +

    World:query()

    + +

    This function is always executed against the most up to date version of the Entity list, it is not affected by flushes and it includes the latest updates at the time of executing the function.

    + + +
    +local myEntity = World:newEntity():give("position", 10, 20)
    +
    +local list = World:query({"position"}) -- list contains myEntity even though there was no flush
    +
    + + +

    If you want to perform additions/removals of Entities, keep in mind you shouldn't use the onMatch function since that happens a loop. You can still use the list alternative.

    + + +
    +-- Don't do this
    +World:query({"position"}, function (e)
    +  World:newEntity()
    +  e:destroy()
    +end)
    +
    +-- Do this instead
    +local list = World:query({"position"})
    +for _, e in ipairs(list) do
    +  -- This is safe because list is not mutated
    +  World:newEntity()
    +  e:destroy()
    +end
    +
    + + +

    Forbidden Flush

    + +

    There are a few places where flushing is forbidden the list is short though:

    + + + +

    These callbacks happen during a flush, you can't flush during a flush since the buffers are in use.

    + +

    If your code performs a flush but may be called from inside of one of these callbacks you can use World:canFlush() to check if flushing is allowed, otherwise an error will be thrown.

    + + +
    +if World:canFlush() then
    +  World:flush()
    +end
    +
    +
    +
    +
    +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30 +
    +
    + + diff --git a/docs/guides/Why-ECS.md.html b/docs/guides/Why-ECS.md.html new file mode 100644 index 0000000..0fb28c5 --- /dev/null +++ b/docs/guides/Why-ECS.md.html @@ -0,0 +1,150 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + + +

    Why ECS? Why Concord?

    + +

    There are various reasons why people choose ECS for their projects, there is a very large FAQ for ECS made by the author of FLECS, but we can go through some of the reasons here as a starting point:

    + +

    +

    It's a trend

    + +

    Yeah it's a trend, most game engines are moving towards ECS as their pattern of choice, big names in the industry are using the pattern and more people are following this trend (Unity, Unreal Engine, Overwatch (video), etc).

    + +

    This is not a good reason to use Concord but it does help that more and more documentation is being written about ECS and how to use it to organize your game code.

    + +

    But this is also a downside, ECS is NOW being used by more game engines, but OOP has been the king for way longer and is more prevalent and used in this market.

    + +

    Concord isn't here as part of the trend, and doesn't intend to follow what other ECS frameworks do. It instead presents it's own interpretation of ECS and makes a heavy focus on the ease of use and development.

    + +

    +

    It doesn't rely on inheritance

    + +

    So to follow this discussion, a reason people choose ECS is so that they can get away from the ugly parts of OOP.

    + +

    The ugly part being inheritance and all the problems it comes with. A possible solution would be mixins or decorators, but most people agree that the problems are still there.

    + +

    So that's when they start to consider composition over inheritance. This is a very common pattern in OOP where you put objects inside objects instead of trying to define their behavior in term of inheritance. And ECS is very close to this pattern, remember how we put Components inside of our Entities? That's composition.

    + +

    But in "Composition over inheritance" we would still define our behaviors in our objects, and we would still need to forward our events to each component. So that's when people start to consider ECS, behaviors existing outside of the object/entity helps organize the code more neatly.

    + +

    Concord does make use of OOP and the metaprogramming functionality in Lua, to help implements ECS, and does still allow you to work with Components and Systems as if they were objects. So if you are escaping from the "bad parts" of OOP, your OOP knowledge will not be wasted while using Concord.

    + +

    +

    It's faster

    + +

    Well this is the argument most game engines make to sell you ECS as the best architecture.

    + +

    Remember our behaviors and data are stored separately? This helps keep our data small. +Remember how we don't make distinction between entities and instead rely on filtering? This allows us to pack our entities close together in memory so that we don't have to jump around so much when looping through them in our systems.

    + +

    Unfortunately this doesn't apply to Concord because in Lua land we can't benefit from all of this, our representation of Components, Entities and Pools don't provide an advantage towards objects. Yeah Concord is fast for the majority of use cases, but this is not its selling point. You can probably write faster code without a library, and there are plenty alternatives that focus on speed.

    + +

    Lua although fast, is not used for optimization purposes, and the same applies to Concord. Concord tries to be reliable, easy to use and feature complete, it is not an optimization.

    + +

    We actually made an experiment making a super efficient ECS library, but it was awful to code with and one line of code could throw all the optimizations away. In our opinion, although it's possible to make a super-fast ECS library and speed does make a huge difference, it's not worth it when working in Lua where JIT optimization is hard

    + +

    Reminds me of Functional Programming

    + +

    If you have tried "The Elm Architecture" or "Redux" you may find the unidirectional data flow in ECS familiar.

    + +

    If we think about our Entities and Components as our Model or State, and Systems as our Update or Reducers. +The only way to mutate our Model/State is through an Event processed by our Update/Reducer

    + +

    This unidirectional data flow and the concept of using reducer functions as a way to update state is a proven concept and widely used in Functional Programming.

    + +

    This mental model has worked really well for Front-end Web developers, and is a very good way to reason about how our systems update over time while keeping our state as our only source of truth and purely as data. If you are already familiar with these architecture you'll be able to pick up Concord and ECS in general, with relative ease.

    + +

    In addition to that, if we combine ECS with immutable data structures for Components and Entities, then we wouldn't be too far away from "The Elm Architecture" and all its benefits like Time Traveling debugging.

    + +

    Unfortunately immutable data structures aren't cheap and the Lua garbage collector is not tuned for them. +Concord doesn't use immutable data structures, but if you treat your Components as just data, it does allow you to recover state through serialization and deserialization.

    + +

    +

    It's more organized

    + +

    If you find this mental model intuitive and start to reason only through this paradigm you'll find yourself writing code in a more structured way. There are very specific ways to do things in ECS and Concord will push you towards these patterns.

    + +

    This is however at the expense of a higher learning curve with a big payoff in maintainability and reusability of your code. Concord keeps this risk low by allowing you to write code outside of Concord without extra hurdles.

    + +

    In the long run we hope you'll find yourself at home using Concord, without needing these escape hatches, in order to accomplish this we try to ease this learning curve through guides, documentation and tooling that will hopefully make working with Concord a great experience.

    +
    +
    +
    +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30 +
    +
    + + diff --git a/docs/index.html b/docs/index.html index 8950dda..f16b737 100644 --- a/docs/index.html +++ b/docs/index.html @@ -27,21 +27,40 @@

    Concord

    + + +

    Contents

    + +

    Guides

    + +

    Builtin Components

    +

    Modules

    Classes

    @@ -51,63 +70,255 @@
    -

    A feature-complete ECS library

    +

    Getting Started

    -

    Modules

    - - - - - - - - - - - - - - - - - -
    ComponentsContainer for registered ComponentClasses
    Concord
    typeType - Helper module to do easy type checking for Concord types
    utilsUtils - Helper module for misc operations
    -

    Classes

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    ComponentA pure data container that is contained by a single entity.
    EntityAn object that exists in a world.
    ListData structure that allows for fast removal at the cost of containing order.
    PoolUsed to iterate over Entities with a specific Components - A Pool contain a any amount of Entities.
    SystemIterates over Entities.
    WorldA collection of Systems and Entities.
    +

    Concord is a feature complete library that allows you to use Entity - Component - System (ECS) model to write and organize your Lua code.

    +

    Concord puts a lot of effort on being easy to use and provide a great developer experience, while trying to be competitive in performance with alternative libraries.

    + +

    +

    Installation

    + +

    As a first step you will need to download this repository and add the concord folder to your project.

    + +

    In that folder you will find all the necessary files to use Concord.

    + +

    Once added you can import Concord in your code

    + + +
    +local Concord = require("path.to.concord")
    +
    + + + + +

    World

    + +

    The first thing you will define with Concord is a World.

    + +

    The World will provide a single interaction point for all our Systems, and will hold all our Entities.

    + +

    We will explain those concepts later, but first we need a World:

    + + +
    +local world = Concord.World()
    +
    + + +

    Components

    + +

    You can think of Components as the data and state that make up all of your game. +Components themselves don't exist as part of our World, they instead are the pieces that build up Entities.

    + +

    Components have a name, and some arbitrary shape that you can define. +For example if we wanted to store the Position of a given Entity, we would define a component called position with a shape that could be something like {x: number, y: number}

    + +

    In Concord this would look like this:

    + + +
    +Concord.Component("position", function (component, x, y)
    +  component.x = x
    +  component.y = y
    +end)
    +
    + + +

    The first argument is the name, and the second is what we call the populate function. +Components start as empty tables and this populate function assigns the values for each of the Component properties.

    + +

    Sometimes you don't need to store data on the Component since the Component itself will act as a flag, in those cases you don't need to provide a populate function:

    + + +
    +Concord.Component("terrestrial")
    +
    + + + + +

    Entities

    + +

    In ECS everything that exists as part of the World is an Entity. An Entity could be a Player, an Enemy, a Crate, a Bullet, etc.

    + +

    Entities by themselves are empty packages with nothing on them, and they would all behave the same if we left them like that. To tell them appart and give unique data to each of them, we need to give them some Components.

    + +

    In Concord this is very simple:

    + + +
    +local Cat = world:newEntity()
    +--local Cat = Concord.Entity(world) -- Alternative syntax
    +
    +Cat:give("position", 50, 50)
    +Cat:give("terrestrial")
    +
    + + +

    Once you give a Component to an Entity, the Component will become a part of it:

    + + +
    +print(Cat.position.x, Cat.position.y) -- Prints: 50, 50
    +
    +if Cat.terrestrial then print("Walks!") end -- Prints: Walks! 
    +
    + + + + +

    Filters

    + +

    The power of Concord and ECS in general comes from grouping a bunch of Entities based on rules. These rules acts as a Filter where we can ask the World for all Entities that have all the Components we required.

    + +

    For example we could ask the World for a list of all the Entities that are terrestrial and have a position, and that are not a machine:

    + + +
    +local animals = world:query({ "terrestrial", "position", "!machine" }) -- The ! in front of machine indicates negation
    +
    +for i, entity in ipairs(animals) do
    +  print(entity.position.x, entity.position.y)
    +end
    +
    + + +
    Side note on Filter rules + +

    If we had another Entity like this one:

    + + +
    +local Parrot = world:newEntity()
    +
    +Parrot:give("position", 100, 100)
    +
    + + +

    It would not be part of our list above, since the filter required:

    + +

    _terrestrial and position and not machine_

    + +

    And although Parrot has position and doesn't have machine, it lacks the terrestrial Component.

    + +
    + +

    This is great but the World:query operation is rather expensive, so Concord instead provides Pools. +A Pool has an associated Filter and will be updated when Entities are added or modified in the World.

    + +

    In order to use pools however, we need to speak about Systems

    + +

    Systems

    + +

    In ECS, Components and Entities generally don't have behavior themselves, and instead rely on Systems that define how a group of Entities should behave.

    + +

    For example if we want all terrestrial animals to move 5 pixels to the right every time we receive a move event we would first create a System like:

    + + +
    +local Movement = Concord.System("Movement", {animals = { "terrestrial", "position" }})
    +
    + + +

    This system is called Movement and has a Pool called animals that contains all the Entities with terrestrial and position Components.

    + +

    We now want to receive all move events. To do that we define a callback in our System like so:

    + + +
    +function Movement:move ()
    +  for i, entity in ipairs(self.animals) do
    +    entity.position.x = entity.position.x + 5
    +  end
    +end
    +
    + + +

    Whenever we receive a move event in our World, this callback will execute, and the Movement System will iterate through the Pool of animals changing their position Components to move them 5 pixels to the right.

    + +

    Events

    + +

    In the previous section we defined a move callback, but we don't yet know how to execute that callback.

    + +

    To do that first the System needs to be added to our World

    + + +
    +world:addSystem(Movement)
    +
    + + + + +

    Finally we need to fire the move event, to do that, we don't call the System directly, instead we emit the event to the World

    + + +
    +world:emit("move")
    +
    + + +

    Once emitted, all the Systems that define a move callback will be called in the order they were added to the World.

    + +

    You can also provide extra arguments to these events, for example if we had:

    + + +
    +function Movement:update(dt)
    +  -- Do something here
    +end
    +
    + + +

    You could provide the dt argument like so:

    + + +
    +world:emit("update", 5) --dt = 5
    +
    + + +

    Summary

    + +

    We defined a few big terms and how they are used in Concord, but to summarize them:

    + +
      +
    • World: This is where Entities and Systems live, it receives external and internal Events.
    • +
    • Components: Data and state, each with a name and a shape. Each Component describes a specific aspect of an Entity.
    • +
    • Entities: Everything that exists in the World. They are bundles of Components.
    • +
    • Filters: A way to group Entities based on the Components they have or don't have.
    • +
    • Systems: Behaviors that apply to groups of Entities and react to Events received by the World.
    • +
    • Events: Signals that tell the World something happened or needs to happen, the World delegates the Events to all the Systems that can respond to it.
    • +
    + +

    These are Concord definitions, and although they may apply to other ECS libraries or frameworks, some differences may exist.

    + +

    With this we have covered the basics. With the API docs you would be ready to start working with Concord, but if you want to dig into more advanced concepts read some of our Guides instead.

    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
    diff --git a/docs/ldoc.css b/docs/ldoc.css index 52c4ad2..ceab5fc 100644 --- a/docs/ldoc.css +++ b/docs/ldoc.css @@ -294,6 +294,7 @@ pre .library { color: #0e7c6b; } pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } pre .string { color: #8080ff; } pre .number { color: #f8660d; } +pre .function-name { color: #60447f; } pre .operator { color: #2239a8; font-weight: bold; } pre .preprocessor, pre .prepro { color: #a33243; } pre .global { color: #800080; } @@ -301,3 +302,56 @@ pre .user-keyword { color: #800080; } pre .prompt { color: #558817; } pre .url { color: #272fc2; text-decoration: underline; } + +/* Guide callouts and details */ + +aside.callout { + margin: 1em 0; + padding: 0.75em 1em; + border-radius: 4px; + border-left: 4px solid #0969da; + background: #e7f1fb; +} + +aside.callout-note { border-left-color: #0969da; background: #e7f1fb; } +aside.callout-tip { border-left-color: #1a7f37; background: #e6f6eb; } +aside.callout-important { border-left-color: #8250df; background: #f1e9fb; } +aside.callout-warning { border-left-color: #9a6700; background: #fff4d1; } +aside.callout-caution { border-left-color: #cf222e; background: #ffe8ea; } + +aside.callout::before { + display: block; + font-weight: bold; + margin-bottom: 0.35em; +} + +aside.callout-note::before { content: "Note"; } +aside.callout-tip::before { content: "Tip"; } +aside.callout-important::before { content: "Important"; } +aside.callout-warning::before { content: "Warning"; } +aside.callout-caution::before { content: "Caution"; } + +aside.callout p { + margin: 0.4em 0 0 0; +} + +aside.callout p:first-child { + margin-top: 0; +} + +details { + margin: 1em 0; + padding: 0.75em 1em; + background: #f7f7f7; + border: 1px solid #c0c0c0; + border-radius: 4px; +} + +details summary { + cursor: pointer; + font-weight: bold; +} + +details[open] summary { + margin-bottom: 0.6em; +} diff --git a/docs/modules/Concord.html b/docs/modules/Concord.html deleted file mode 100644 index 903787f..0000000 --- a/docs/modules/Concord.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - Reference - - - - -
    - -
    - -
    -
    -
    - - -
    - - - - - - -
    - -

    Module Concord

    -

    -

    - - - -
    -
    - - - - -
    -
    -
    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 -
    -
    - - diff --git a/docs/modules/assemblages.html b/docs/modules/assemblages.html deleted file mode 100644 index 8b4d725..0000000 --- a/docs/modules/assemblages.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - Reference - - - - -
    - -
    - -
    -
    -
    - - -
    - - - - - - -
    - -

    Module Assemblages

    -

    A container for registered Assemblages

    -

    - - -

    Functions

    - - - - - - - - - - - - - -
    register (name, assemblage)Registers an Assemblage.
    has (name)Returns true if the containter has an Assemblage with the specified name
    get (name)Returns the Assemblage with the specified name
    - -
    -
    - - -

    Functions

    - -
    -
    - - register (name, assemblage) -
    -
    - Registers an Assemblage. - - -

    Parameters:

    -
      -
    • name - string - Name to register under -
    • -
    • assemblage - Assemblage - Assemblage to register -
    • -
    - - - - - -
    -
    - - has (name) -
    -
    - Returns true if the containter has an Assemblage with the specified name - - -

    Parameters:

    -
      -
    • name - string - Name of the Assemblage to check -
    • -
    - -

    Returns:

    -
      - - boolean - -
    - - - - -
    -
    - - get (name) -
    -
    - Returns the Assemblage with the specified name - - -

    Parameters:

    -
      -
    • name - string - Name of the Assemblage to get -
    • -
    - -

    Returns:

    -
      - - Assemblage - -
    - - - - -
    -
    - - -
    -
    -
    -generated by LDoc 1.4.6 -Last updated 2020-01-04 10:27:07 -
    -
    - - diff --git a/docs/modules/components.html b/docs/modules/components.html index de521c2..d40bc69 100644 --- a/docs/modules/components.html +++ b/docs/modules/components.html @@ -26,8 +26,9 @@

    Concord

    +

    Contents

    @@ -39,19 +40,30 @@

    Modules

    +

    Builtin Components

    +

    Classes

    +

    Guides

    + @@ -59,17 +71,23 @@

    Module Components

    Container for registered ComponentClasses

    -

    +

    + +

    Functions

    - + - + + + + + @@ -91,13 +109,13 @@ has (name)
    - Returns true if the containter has the ComponentClass with the specified name + Returns true if the container has the ComponentClass with the specified name

    Parameters:

    • name - string + string Name of the ComponentClass to check
    @@ -107,6 +125,37 @@ boolean + + + + + + + +
    +
    + + reject (name) +
    +
    + Prefix a component's name with the currently set Reject Prefix + + +

    Parameters:

    +
      +
    • name + string + Name of the ComponentClass to reject +
    • +
    + +

    Returns:

    +
      + + string + + +
    @@ -115,7 +164,7 @@
    - try (name) + try (name[, acceptRejected])
    Returns true and the ComponentClass if one was registered with the specified name @@ -125,19 +174,27 @@

    Parameters:

    • name - string + string Name of the ComponentClass to check
    • +
    • acceptRejected + boolean + Whether to accept names prefixed with the Reject Prefix. + (optional) +

    Returns:

    1. boolean -
    2. + ok
    3. - Component - or error string
    4. + Component or string + ComponentClass on success, or an error string on failure +
    5. + string or boolean + On success: the stripped Component name if the name had the Reject Prefix, otherwise false
    @@ -155,7 +212,7 @@

    Parameters:

    @@ -164,7 +221,7 @@
      Component - + The registered ComponentClass
    @@ -172,13 +229,11 @@
    - -
    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
    diff --git a/docs/modules/systems.html b/docs/modules/systems.html deleted file mode 100644 index c1e22b0..0000000 --- a/docs/modules/systems.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - Reference - - - - -
    - -
    - -
    -
    -
    - - -
    - - - - - - -
    - -

    Module Systems

    -

    Container for registered SystemClasses

    -

    - - -

    Functions

    -
    has (name)Returns true if the containter has the ComponentClass with the specified nameReturns true if the container has the ComponentClass with the specified name
    try (name)reject (name)Prefix a component's name with the currently set Reject Prefix
    try (name[, acceptRejected]) Returns true and the ComponentClass if one was registered with the specified name or false and an error otherwise
    - - - - - - - - - - - - -
    register (name, systemClass)Registers a SystemClass.
    has (name)Returns true if the containter has the SystemClass with the name
    get (name)Returns the SystemClass with the name
    - -
    -
    - - -

    Functions

    - -
    -
    - - register (name, systemClass) -
    -
    - Registers a SystemClass. - - -

    Parameters:

    -
      -
    • name - string - Name to register under -
    • -
    • systemClass - System - SystemClass to register -
    • -
    - - - - - -
    -
    - - has (name) -
    -
    - Returns true if the containter has the SystemClass with the name - - -

    Parameters:

    -
      -
    • name - string - Name of the SystemClass to check -
    • -
    - -

    Returns:

    -
      - - boolean - -
    - - - - -
    -
    - - get (name) -
    -
    - Returns the SystemClass with the name - - -

    Parameters:

    -
      -
    • name - string - Name of the SystemClass to get -
    • -
    - -

    Returns:

    -
      - - SystemClass with the name -
    - - - - -
    -
    - - - - -
    -generated by LDoc 1.4.6 -Last updated 2020-01-04 10:27:07 -
    - - - diff --git a/docs/modules/type.html b/docs/modules/type.html index 2fb678f..3618a26 100644 --- a/docs/modules/type.html +++ b/docs/modules/type.html @@ -26,8 +26,9 @@

    Concord

    +

    Contents

    @@ -39,56 +40,76 @@

    Modules

    +

    Builtin Components

    +

    Classes

    +

    Guides

    +
    -

    Module type

    -

    Type - Helper module to do easy type checking for Concord types

    -

    +

    Module Type

    +

    Helper module to do easy type checking for Concord types

    +

    + +

    Functions

    - + + + + + - + - + - + - + - + + + + +
    Type.isEntity (t)isCallable (t)Returns true if the value is a function or has a __call metamethod.
    isEntity (t) Returns if object is an Entity.
    Type.isComponentClass (t)isComponentClass (t) Returns if object is a ComponentClass.
    Type.isComponent (t)isComponent (t) Returns if object is a Component.
    Type.isSystemClass (t)isSystemClass (t) Returns if object is a SystemClass.
    Type.isSystem (t)isSystem (t) Returns if object is a System.
    Type.isWorld (t)isWorld (t) Returns if object is a World.
    isFilter (t)Returns if object is a Filter.

    @@ -99,8 +120,36 @@
    - - Type.isEntity (t) + + isCallable (t) +
    +
    + Returns true if the value is a function or has a __call metamethod. + + +

    Parameters:

    +
      +
    • t + Object to check +
    • +
    + +

    Returns:

    +
      + + boolean + + + +
    + + + + +
    +
    + + isEntity (t)
    Returns if object is an Entity. @@ -118,6 +167,8 @@ boolean + + @@ -125,8 +176,8 @@
    - - Type.isComponentClass (t) + + isComponentClass (t)
    Returns if object is a ComponentClass. @@ -144,6 +195,8 @@ boolean + + @@ -151,8 +204,8 @@
    - - Type.isComponent (t) + + isComponent (t)
    Returns if object is a Component. @@ -170,6 +223,8 @@ boolean + + @@ -177,8 +232,8 @@
    - - Type.isSystemClass (t) + + isSystemClass (t)
    Returns if object is a SystemClass. @@ -196,6 +251,8 @@ boolean + + @@ -203,8 +260,8 @@
    - - Type.isSystem (t) + + isSystem (t)
    Returns if object is a System. @@ -222,6 +279,8 @@ boolean + + @@ -229,8 +288,8 @@
    - - Type.isWorld (t) + + isWorld (t)
    Returns if object is a World. @@ -248,6 +307,36 @@ boolean + + + + + + + +
    +
    + + isFilter (t) +
    +
    + Returns if object is a Filter. + + +

    Parameters:

    +
      +
    • t + Object to check +
    • +
    + +

    Returns:

    +
      + + boolean + + +
    @@ -255,13 +344,11 @@
    - -
    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
    diff --git a/docs/modules/utils.html b/docs/modules/utils.html index 35c2e4c..2912bf4 100644 --- a/docs/modules/utils.html +++ b/docs/modules/utils.html @@ -26,8 +26,9 @@

    Concord

    +

    Contents

    @@ -39,39 +40,55 @@

    Modules

    +

    Builtin Components

    +

    Classes

    +

    Guides

    +
    -

    Module utils

    -

    Utils - Helper module for misc operations

    -

    +

    Module Utils

    +

    Helper module for misc operations

    +

    + +

    Functions

    - - + + - - + + + + + +
    Utils.shallowCopy (orig, target)Does a shallow copy of a table and appends it to a target table.error (level, str, ...)Raises a formatted error.
    Utils.loadNamespace (pathOrFiles, namespace)Requires files and puts them in a table.shallowCopy (orig, target)Copies keys from orig into target.
    loadNamespace (pathOrFiles[, namespace])Requires files and stores them in a namespace table.
    @@ -83,20 +100,25 @@
    - - Utils.shallowCopy (orig, target) + + error (level, str, ...)
    - Does a shallow copy of a table and appends it to a target table. + Raises a formatted error.

    Parameters:

      -
    • orig - Table to copy +
    • level + integer + Error level passed to error(), plus one
    • -
    • target - Table to append to +
    • str + string + Format string +
    • +
    • ... + Format arguments
    @@ -106,29 +128,64 @@
    - - Utils.loadNamespace (pathOrFiles, namespace) + + shallowCopy (orig, target)
    - Requires files and puts them in a table. - Accepts a table of paths to Lua files: {"path/to/file_1", "path/to/another/file_2", "etc"} - Accepts a path to a directory with Lua files: "my_files/here" + Copies keys from orig into target. Existing keys in target are overwritten.

    Parameters:

      -
    • pathOrFiles - The table of paths or a path to a directory. +
    • orig + table + Table to copy from
    • -
    • namespace - A table that will hold the required files +
    • target + table + Table to copy into

    Returns:

      - table + table + target +
    + + + + +
    +
    + + loadNamespace (pathOrFiles[, namespace]) +
    +
    + Requires files and stores them in a namespace table. + Accepts a table of require paths: {"path/to/file1", "path/to/another/file2", "etc"} + Accepts a path to a directory of Lua files: "my_files/here" + Directory loading uses love.filesystem and requires LÖVE. + If namespace is omitted, files are still required and this function returns nil. + + +

    Parameters:

    +
      +
    • pathOrFiles + A directory path or a table of require paths +
    • +
    • namespace + table + Table that will hold the required files + (optional) +
    • +
    + +

    Returns:

    +
      + + table or nil The namespace table
    @@ -137,13 +194,11 @@
    - -
    -generated by LDoc 1.4.6 -Last updated 2020-08-18 15:20:32 +generated by LDoc 1.5.0 +Last updated 2026-09-06 21:32:30
    diff --git a/docs/modules/worlds.html b/docs/modules/worlds.html deleted file mode 100644 index 88489f7..0000000 --- a/docs/modules/worlds.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - Reference - - - - -
    - -
    - -
    -
    -
    - - -
    - - - - - - -
    - -

    Module worlds

    -

    Worlds - Container for registered Worlds

    -

    - - -

    Functions

    - - - - - - - - - - - - - -
    Worlds.register (name, world)Registers a World.
    Worlds.has (name)Returns true if the containter has the World with the name
    Worlds.get (name)Returns the World with the name
    - -
    -
    - - -

    Functions

    - -
    -
    - - Worlds.register (name, world) -
    -
    - Registers a World. - - -

    Parameters:

    -
      -
    • name - string - Name to register under -
    • -
    • world - World to register -
    • -
    - - - - - -
    -
    - - Worlds.has (name) -
    -
    - Returns true if the containter has the World with the name - - -

    Parameters:

    -
      -
    • name - string - Name of the World to check -
    • -
    - -

    Returns:

    -
      - - boolean - -
    - - - - -
    -
    - - Worlds.get (name) -
    -
    - Returns the World with the name - - -

    Parameters:

    -
      -
    • name - string - Name of the World to get -
    • -
    - -

    Returns:

    -
      - - World with the name -
    - - - - -
    -
    - - -
    -
    -
    -generated by LDoc 1.4.6 -Last updated 2020-01-04 10:27:07 -
    -
    - - diff --git a/guides/Custom Pools.md b/guides/Custom-Pools.md similarity index 100% rename from guides/Custom Pools.md rename to guides/Custom-Pools.md diff --git a/guides/Get Started.md b/guides/Get-Started.md similarity index 99% rename from guides/Get Started.md rename to guides/Get-Started.md index 6a9c4eb..c999b9c 100644 --- a/guides/Get Started.md +++ b/guides/Get-Started.md @@ -1,10 +1,10 @@ -# Concord +# Getting Started Concord is a feature complete library that allows you to use Entity - Component - System (ECS) model to write and organize your Lua code. Concord puts a lot of effort on being easy to use and provide a great developer experience, while trying to be competitive in performance with alternative libraries. -## Getting Started +## Installation As a first step you will need to download this repository and add the `concord` folder to your project. diff --git a/guides/Pool Flushing and Timing.md b/guides/Pool-Flushing-and-Timing.md similarity index 100% rename from guides/Pool Flushing and Timing.md rename to guides/Pool-Flushing-and-Timing.md diff --git a/guides/Why ECS.md b/guides/Why-ECS.md similarity index 100% rename from guides/Why ECS.md rename to guides/Why-ECS.md diff --git a/ldoc.css b/ldoc.css new file mode 100644 index 0000000..ceab5fc --- /dev/null +++ b/ldoc.css @@ -0,0 +1,357 @@ +/* BEGIN RESET + +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html { + color: #000; + background: #FFF; +} +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { + margin: 0; + padding: 0; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +fieldset,img { + border: 0; +} +address,caption,cite,code,dfn,em,strong,th,var,optgroup { + font-style: inherit; + font-weight: inherit; +} +del,ins { + text-decoration: none; +} +li { + margin-left: 20px; +} +caption,th { + text-align: left; +} +h1,h2,h3,h4,h5,h6 { + font-size: 100%; + font-weight: bold; +} +q:before,q:after { + content: ''; +} +abbr,acronym { + border: 0; + font-variant: normal; +} +sup { + vertical-align: baseline; +} +sub { + vertical-align: baseline; +} +legend { + color: #000; +} +input,button,textarea,select,optgroup,option { + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; +} +input,button,textarea,select {*font-size:100%; +} +/* END RESET */ + +body { + margin-left: 1em; + margin-right: 1em; + font-family: arial, helvetica, geneva, sans-serif; + background-color: #ffffff; margin: 0px; +} + +code, tt { font-family: monospace; font-size: 1.1em; } +span.parameter { font-family:monospace; } +span.parameter:after { content:":"; } +span.types:before { content:"("; } +span.types:after { content:")"; } +.type { font-weight: bold; font-style:italic } + +body, p, td, th { font-size: .95em; line-height: 1.2em;} + +p, ul { margin: 10px 0 0 0px;} + +strong { font-weight: bold;} + +em { font-style: italic;} + +h1 { + font-size: 1.5em; + margin: 20px 0 20px 0; +} +h2, h3, h4 { margin: 15px 0 10px 0; } +h2 { font-size: 1.25em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.06em; } + +a:link { font-weight: bold; color: #004080; text-decoration: none; } +a:visited { font-weight: bold; color: #006699; text-decoration: none; } +a:link:hover { text-decoration: underline; } + +hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +blockquote { margin-left: 3em; } + +ul { list-style-type: disc; } + +p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +pre { + background-color: rgb(245, 245, 245); + border: 1px solid #C0C0C0; /* silver */ + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; + font-family: "Andale Mono", monospace; +} + +pre.example { + font-size: .85em; +} + +table.index { border: 1px #00007f; } +table.index td { text-align: left; vertical-align: top; } + +#container { + margin-left: 1em; + margin-right: 1em; + background-color: #f0f0f0; +} + +#product { + text-align: center; + border-bottom: 1px solid #cccccc; + background-color: #ffffff; +} + +#product big { + font-size: 2em; +} + +#main { + background-color: #f0f0f0; + border-left: 2px solid #cccccc; +} + +#navigation { + float: left; + width: 14em; + vertical-align: top; + background-color: #f0f0f0; + overflow: visible; +} + +#navigation h2 { + background-color:#e7e7e7; + font-size:1.1em; + color:#000000; + text-align: left; + padding:0.2em; + border-top:1px solid #dddddd; + border-bottom:1px solid #dddddd; +} + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 14em; + padding: 1em; + width: 700px; + border-left: 2px solid #cccccc; + border-right: 2px solid #cccccc; + background-color: #ffffff; +} + +#about { + clear: both; + padding: 5px; + border-top: 2px solid #cccccc; + background-color: #ffffff; +} + +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} + +table.module_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.module_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.module_list td.summary { width: 100%; } + + +table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.function_list td.summary { width: 100%; } + +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + +dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +dl.table h3, dl.function h3 {font-size: .95em;} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #FF9; +} + + +/* styles for prettification of source */ +pre .comment { color: #558817; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #aa5050; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #8080ff; } +pre .number { color: #f8660d; } +pre .function-name { color: #60447f; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #800080; } +pre .user-keyword { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } + + +/* Guide callouts and details */ + +aside.callout { + margin: 1em 0; + padding: 0.75em 1em; + border-radius: 4px; + border-left: 4px solid #0969da; + background: #e7f1fb; +} + +aside.callout-note { border-left-color: #0969da; background: #e7f1fb; } +aside.callout-tip { border-left-color: #1a7f37; background: #e6f6eb; } +aside.callout-important { border-left-color: #8250df; background: #f1e9fb; } +aside.callout-warning { border-left-color: #9a6700; background: #fff4d1; } +aside.callout-caution { border-left-color: #cf222e; background: #ffe8ea; } + +aside.callout::before { + display: block; + font-weight: bold; + margin-bottom: 0.35em; +} + +aside.callout-note::before { content: "Note"; } +aside.callout-tip::before { content: "Tip"; } +aside.callout-important::before { content: "Important"; } +aside.callout-warning::before { content: "Warning"; } +aside.callout-caution::before { content: "Caution"; } + +aside.callout p { + margin: 0.4em 0 0 0; +} + +aside.callout p:first-child { + margin-top: 0; +} + +details { + margin: 1em 0; + padding: 0.75em 1em; + background: #f7f7f7; + border: 1px solid #c0c0c0; + border-radius: 4px; +} + +details summary { + cursor: pointer; + font-weight: bold; +} + +details[open] summary { + margin-bottom: 0.6em; +} diff --git a/scripts/build-docs.lua b/scripts/build-docs.lua new file mode 100644 index 0000000..7894c5c --- /dev/null +++ b/scripts/build-docs.lua @@ -0,0 +1,318 @@ +-- Build LDoc docs with Markdown preprocess and HTML postprocess. +-- Usage: lua scripts/build-docs.lua + +local function split_lines(text) + text = text:gsub("\r\n", "\n"):gsub("\r", "\n") + if text:sub(-1) ~= "\n" then + text = text .. "\n" + end + local lines = {} + for line in text:gmatch("(.-)\n") do + lines[#lines + 1] = line + end + return lines +end + +local function join_lines(lines) + return table.concat(lines, "\n") .. "\n" +end + +-- Strip one Markdown blockquote level inside
    , so LDoc sees +-- fenced code at column 0 and can emit highlighted
     blocks.
    +local function unquote_details(text)
    +   return text:gsub("(]*>)([%s%S]-)(
    )", function(open, body, close) + local lines = split_lines(body) + for i, line in ipairs(lines) do + lines[i] = line:match("^>%s?(.*)$") or line + end + return open .. join_lines(lines) .. close + end) +end + +-- Turn GitHub-style callouts into HTML asides LDoc will pass through. +-- > [!NOTE] +-- > body +local function convert_callouts(text) + local lines = split_lines(text) + local out = {} + local i = 1 + while i <= #lines do + local kind, rest = lines[i]:match("^>%s*%[!(%u+)%]%s*(.*)$") + if kind then + local body = {} + if rest ~= "" then + body[#body + 1] = rest + end + i = i + 1 + while i <= #lines do + local quoted = lines[i]:match("^>%s?(.*)$") + if quoted == nil then + break + end + body[#body + 1] = quoted + i = i + 1 + end + local class = kind:lower() + out[#out + 1] = ('" + else + out[#out + 1] = lines[i] + i = i + 1 + end + end + return join_lines(out) +end + +local function preprocess(text) + return convert_callouts(unquote_details(text)) +end + +-- LDoc puts the current kind first in the sidebar; keep a stable order. +local SIDEBAR_KINDS = { "Modules", "Classes", "Builtins", "Topics", "Guides" } +local SIDEBAR_LABELS = { Builtins = "Builtin Components", Topics = "Guides" } + +local function strip_getting_started(list) + list = list:gsub('
  • Getting Started
  • %s*', "") + list = list:gsub('
  • Getting Started
  • %s*', "") + return list +end + +local function reorder_sidebar(html) + return (html:gsub('', function(nav) + local blocks = {} + for _, title in ipairs(SIDEBAR_KINDS) do + nav = nav:gsub('

    ' .. title .. '

    %s*()', function(list) + if title == "Topics" or title == "Guides" then + list = strip_getting_started(list) + end + blocks[title] = '

    ' .. (SIDEBAR_LABELS[title] or title) .. '

    \n' .. list + return "" + end, 1) + end + local trimmed = nav:gsub("%s+$", "") + local parts = { trimmed } + for _, title in ipairs(SIDEBAR_KINDS) do + if blocks[title] then + parts[#parts + 1] = "" + parts[#parts + 1] = blocks[title] + end + end + parts[#parts + 1] = "" + return '' + end, 1)) +end + +local HOME = "Getting Started" +local HOME_TOPIC = "Get-Started.md.html" + +local function replace_home_link(html, path) + local item + if path:match("index%.html$") then + item = "" .. HOME .. "" + else + item = '' .. HOME .. "" + end + local home = "
      \n
    • " .. item .. "
    • \n
    " + local replaced, n = html:gsub( + '', + home, + 1 + ) + if n == 0 then + replaced = html:gsub("(

    Concord

    )", "%1\n\n" .. home, 1) + end + return replaced +end + +-- Getting Started lives at index.html; keep old topic URLs working. +local function point_getting_started_at_index(html) + html = html:gsub('href="Get%-Started%.md%.html"', 'href="../index.html"') + html = html:gsub('href="%.%./guides/Get%-Started%.md%.html"', 'href="../index.html"') + html = html:gsub('href="%.%./topics/Get%-Started%.md%.html"', 'href="../index.html"') + html = html:gsub('href="guides/Get%-Started%.md%.html"', 'href="index.html"') + html = html:gsub('href="topics/Get%-Started%.md%.html"', 'href="index.html"') + return html +end + +local function topic_home_to_index(html) + html = html:gsub('href="%.%./', 'href="') + html = html:gsub( + '", + "
      \n
    • " .. HOME .. "
    • \n
    ", + 1 + ) + return html +end + +local HOME_REDIRECT = [[ + + + + + + Getting Started + + +

    Getting Started

    + + +]] + +-- The index table uses topic filenames; the sidebar already has Markdown titles. +local function retitle_index_topics(html) + local nav = html:match('') + if not nav then + return html + end + local list = nav:match('

    Guides

    %s*]*>([%s%S]-)') + or nav:match('

    Topics

    %s*]*>([%s%S]-)') + if not list then + return html + end + local titles = {} + for href, title in list:gmatch('([^<]+)') do + titles[href] = title + end + return (html:gsub( + '([^<]+)', + function(href, name) + return ('%s'):format( + href, + titles[href] or name + ) + end + )) +end + +local INDEX_KINDS = { "Modules", "Classes", "Builtin Components", "Guides" } + +local function reorder_index_sections(html) + return (html:gsub('
    ([%s%S]-)
    ', function(content) + local blocks = {} + for _, title in ipairs(INDEX_KINDS) do + content = content:gsub( + '

    ' .. title .. '

    %s*([%s%S]-
    )', + function(table_html) + blocks[title] = '

    ' .. title .. '

    \n' .. table_html + return "" + end, + 1 + ) + end + local trimmed = content:gsub("%s+$", "") + local parts = { trimmed } + for _, title in ipairs(INDEX_KINDS) do + if blocks[title] then + parts[#parts + 1] = "" + parts[#parts + 1] = blocks[title] + end + end + parts[#parts + 1] = "" + return '
    ' .. table.concat(parts, "\n") .. '
    ' + end, 1)) +end + +-- Markdown wraps some block-level HTML in

    ; unwrap those. +local function postprocess(html, path) + html = html:gsub('

    %s*(]*>)%s*

    ', '%1') + html = html:gsub('(]*>)%s*

    ', '%1') + html = html:gsub('

    %s*()%s*

    ', '%1') + html = html:gsub('

    %s*()', '%1') + html = html:gsub('()%s*

    ', '%1') + html = html:gsub('

    %s*()%s*

    ', '%1') + html = html:gsub('

    %s*()%s*

    ', '%1') + -- Leftover callouts if a page was built without preprocess + html = html:gsub( + '
    %s*

    %[!(%u+)%]%s*([%s%S]-)

    %s*
    ', + function(kind, body) + return (''):format( + kind:lower(), + body + ) + end + ) + html = html:gsub('%s*]*>', "") + html = retitle_index_topics(html) + html = html:gsub('

    Builtins

    ', '

    Builtin Components

    ') + html = html:gsub('

    Topics

    ', '

    Guides

    ') + html = html:gsub( + '%s*Getting Started%s*[^<]*%s*%s*', + "" + ) + html = reorder_index_sections(html) + html = replace_home_link(html, path) + if path:sub(-#HOME_TOPIC) ~= HOME_TOPIC then + -- Keep the topic page's current-item markup so it can become index.html. + html = point_getting_started_at_index(html) + end + return html +end + +local function read_file(path) + local f, err = io.open(path, "r") + if not f then + error("cannot read " .. path .. ": " .. tostring(err)) + end + local text = f:read("*a") + f:close() + return text +end + +local function write_file(path, text) + local f, err = io.open(path, "w") + if not f then + error("cannot write " .. path .. ": " .. tostring(err)) + end + f:write(text) + f:close() +end + +local function run(cmd) + local ok, why, code = os.execute(cmd) + if not ok then + error(cmd .. " failed (" .. tostring(why) .. " " .. tostring(code) .. ")") + end +end + +local function list_html(dir) + local files = {} + local pipe = io.popen('find "' .. dir .. '" -name "*.html"') + for line in pipe:lines() do + files[#files + 1] = line + end + pipe:close() + return files +end + +local root = arg[0]:match("^(.*)/scripts/build%-docs%.lua$") or "." +if root == "" then + root = "." +end + +run('mkdir -p "' .. root .. '/.ldoc-guides"') + +local pipe = io.popen('ls -1 "' .. root .. '/guides/"*.md') +for path in pipe:lines() do + local name = path:match("([^/]+)$") + write_file(root .. "/.ldoc-guides/" .. name, preprocess(read_file(path))) +end +pipe:close() + +run('rm -rf "' .. root .. '/docs"') +run('mkdir "' .. root .. '/docs"') +run('cd "' .. root .. '" && ldoc --fatalwarnings .') + +for _, path in ipairs(list_html(root .. "/docs")) do + write_file(path, postprocess(read_file(path), path)) +end + +local topic_home = root .. "/docs/guides/" .. HOME_TOPIC +write_file(root .. "/docs/index.html", topic_home_to_index(read_file(topic_home))) +write_file(topic_home, HOME_REDIRECT) + +print("docs built in " .. root .. "/docs")