Add docs (#45)

* Initial commit

* Add section for standalone

* Fix docs

* Add pages to docs

* Remove redundant files
This commit is contained in:
Marcus 2024-05-27 03:39:20 +02:00 committed by GitHub
parent f1ba9c4a55
commit 0567856a59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 665 additions and 412 deletions

View file

@ -31,7 +31,7 @@ local Name = world:component()
local function parent(entity) local function parent(entity)
return world:target(entity, ChildOf) return world:target(entity, ChildOf)
end end
local function name(entity) local function getName(entity)
return world:get(entity, Name) return world:get(entity, Name)
end end

45
docs/api-types.md Normal file
View file

@ -0,0 +1,45 @@
# World
A World contains all ECS data
Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other, meaning they donot share the same entities nor component IDs.
---
# Entity
An unique id.
Entities consist out of a number unique to the entity in the lower 32 bits, and a counter used to track entity liveliness in the upper 32 bits. When an id is recycled, its generation count is increased. This causes recycled ids to be very large (>4 billion), which is normal.
---
# QueryIter
A result from the `World:query` function.
Queries are used to iterate over entities that match against the set collection of components.
Calling it in a loop will allow iteration over the results.
```lua
for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
-- Do something
end
```
### QueryIter.without
QueryIter.without(iter: QueryIter
...: [Entity](../api-types/Entity)): QueryIter
Create a new Query Iterator from the filter
#### Parameters
world The world.
... The collection of components to filter archetypes against.
#### Returns
The new query iterator.

View file

@ -0,0 +1,19 @@
# Getting Started
This section will provide a walk through setting up your development environment and a quick overview of the different features and concepts in Jecs with short examples.
## Installing Jecs
To use Jecs, you will need to add the library to your project's source folder.
## Installing as standalone
Head over to the [Releases](https://github.com/ukendio/jecs/releases/latest) page and install the rbxm file.
![jecs.rbxm](rbxm.png)
## Installing with Wally
Jecs is available as a package on [wally.run](https://wally.run/package/ukendio/jecs)
Add it to your project's Wally.toml like this:
```toml
[dependencies]
jecs = "0.1.0" # Make sure this is the latest version
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -6,10 +6,10 @@
type i53 = number type i53 = number
type i24 = number type i24 = number
type Ty = {i53} type Ty = { i53 }
type ArchetypeId = number type ArchetypeId = number
type Column = {any} type Column = { any }
type Archetype = { type Archetype = {
id: number, id: number,
@ -21,20 +21,19 @@ type Archetype = {
}, },
types: Ty, types: Ty,
type: string | number, type: string | number,
entities: {number}, entities: { number },
columns: {Column}, columns: { Column },
records: {}, records: {},
} }
type Record = { type Record = {
archetype: Archetype, archetype: Archetype,
row: number, row: number,
dense: i24, dense: i24,
componentRecord: ArchetypeMap componentRecord: ArchetypeMap,
} }
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } }
type ArchetypeRecord = number type ArchetypeRecord = number
--[[ --[[
@ -48,16 +47,16 @@ TODO:
]] ]]
type ArchetypeMap = { type ArchetypeMap = {
cache: {[number]: ArchetypeRecord}, cache: { [number]: ArchetypeRecord },
first: ArchetypeMap, first: ArchetypeMap,
second: ArchetypeMap, second: ArchetypeMap,
parent: ArchetypeMap, parent: ArchetypeMap,
size: number size: number,
} }
type ComponentIndex = {[i24]: ArchetypeMap} type ComponentIndex = { [i24]: ArchetypeMap }
type Archetypes = {[ArchetypeId]: Archetype} type Archetypes = { [ArchetypeId]: Archetype }
type ArchetypeDiff = { type ArchetypeDiff = {
added: Ty, added: Ty,
@ -77,65 +76,60 @@ local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local function addFlags(isPair: boolean) local function addFlags(isPair: boolean)
local typeFlags = 0x0 local typeFlags = 0x0
if isPair then if isPair then
typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID.
end end
if false then if false then
typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true
end end
if false then if false then
typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true
end end
if false then if false then
typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID.
end end
return typeFlags return typeFlags
end end
local function ECS_COMBINE(source: number, target: number): i53 local function ECS_COMBINE(source: number, target: number): i53
local e = source * 2^28 + target * ECS_ID_FLAGS_MASK local e = source * 268435456 + target * ECS_ID_FLAGS_MASK
return e return e
end end
local function ECS_IS_PAIR(e: number) local function ECS_IS_PAIR(e: number)
return (e % 2^4) // FLAGS_PAIR ~= 0 return (e % 2 ^ 4) // FLAGS_PAIR ~= 0
end
function separate(entity: number)
local _typeFlags = entity % 0x10
entity //= ECS_ID_FLAGS_MASK
return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags
end end
-- HIGH 24 bits LOW 24 bits -- HIGH 24 bits LOW 24 bits
local function ECS_GENERATION(e: i53) local function ECS_GENERATION(e: i53)
e //= 0x10 e = e // 0x10
return e % ECS_GENERATION_MASK return e % ECS_GENERATION_MASK
end
local function ECS_GENERATION_INC(e: i53)
local flags = e // 0x10
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK
return ECS_COMBINE(id, generation + 1) + flags
end
-- FIRST gets the high ID
local function ECS_ENTITY_T_HI(e: i53): i24
e = e // 0x10
return e % ECS_ENTITY_MASK
end end
-- SECOND -- SECOND
local function ECS_ENTITY_T_LO(e: i53) local function ECS_ENTITY_T_LO(e: i53)
e //= 0x10 e = e // 0x10
return e // ECS_ENTITY_MASK return e // ECS_ENTITY_MASK
end end
local function ECS_GENERATION_INC(e: i53) local function ECS_PAIR(pred: i53, obj: i53): i53
local id, generation, flags = separate(e)
return ECS_COMBINE(id, generation + 1) + flags
end
-- FIRST gets the high ID
local function ECS_ENTITY_T_HI(entity: i53): i24
entity //= 0x10
local first = entity % ECS_ENTITY_MASK
return first
end
local function ECS_PAIR(pred: number, obj: number)
local first local first
local second: number = WILDCARD local second: number = WILDCARD
@ -148,35 +142,28 @@ local function ECS_PAIR(pred: number, obj: number)
second = ECS_ENTITY_T_LO(pred) second = ECS_ENTITY_T_LO(pred)
end end
return ECS_COMBINE( return ECS_COMBINE(ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true)
ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true)
end end
local function getAlive(entityIndex: EntityIndex, id: i24) local function getAlive(entityIndex: EntityIndex, id: i24)
local entityId = entityIndex.dense[id] local entityId = entityIndex.dense[id]
local record = entityIndex.sparse[entityIndex.dense[id]] return entityId
if not record then
error(id.." is not alive")
end
return entityId
end end
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
local function ECS_PAIR_RELATION(entityIndex, e) local function ECS_PAIR_RELATION(entityIndex, e)
assert(ECS_IS_PAIR(e)) return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
end end
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
local function ECS_PAIR_OBJECT(entityIndex, e) local function ECS_PAIR_OBJECT(entityIndex, e)
assert(ECS_IS_PAIR(e)) return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
end end
local function nextEntityId(entityIndex, index: i24): i53 local function nextEntityId(entityIndex, index: i24): i53
local id = ECS_COMBINE(index, 0) local id = ECS_COMBINE(index, 0)
entityIndex.sparse[id] = { entityIndex.sparse[id] = {
dense = index dense = index,
} :: Record } :: Record
entityIndex.dense[index] = id entityIndex.dense[index] = id
@ -252,7 +239,7 @@ local function newEntity(entityId: i53, record: Record, archetype: Archetype)
return record return record
end end
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype)
local sourceRow = record.row local sourceRow = record.row
local from = record.archetype local from = record.archetype
local destinationRow = archetypeAppend(entityId, to) local destinationRow = archetypeAppend(entityId, to)
@ -265,11 +252,16 @@ local function hash(arr): string | number
return table.concat(arr, "_") return table.concat(arr, "_")
end end
local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap local function ensureComponentRecord(
componentIndex: ComponentIndex,
archetypeId: number,
componentId: number,
i: number
): ArchetypeMap
local archetypesMap = componentIndex[componentId] local archetypesMap = componentIndex[componentId]
if not archetypesMap then if not archetypesMap then
archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap archetypesMap = { size = 0, cache = {}, first = {}, second = {} } :: ArchetypeMap
componentIndex[componentId] = archetypesMap componentIndex[componentId] = archetypesMap
end end
@ -286,8 +278,7 @@ local function ECS_ID_IS_WILDCARD(e)
return first == WILDCARD or second == WILDCARD return first == WILDCARD or second == WILDCARD
end end
local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype
local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types) local ty = hash(types)
local id = world.nextArchetypeId + 1 local id = world.nextArchetypeId + 1
@ -306,26 +297,24 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp
local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) local object = ECS_PAIR_OBJECT(world.entityIndex, componentId)
local idr_r = ECS_PAIR(relation, WILDCARD) local idr_r = ECS_PAIR(relation, WILDCARD)
ensureComponentRecord( ensureComponentRecord(componentIndex, id, idr_r, i)
componentIndex, id, idr_r, i)
records[idr_r] = i records[idr_r] = i
local idr_t = ECS_PAIR(WILDCARD, object) local idr_t = ECS_PAIR(WILDCARD, object)
ensureComponentRecord( ensureComponentRecord(componentIndex, id, idr_t, i)
componentIndex, id, idr_t, i)
records[idr_t] = i records[idr_t] = i
end end
columns[i] = {} columns[i] = {}
end end
local archetype = { local archetype = {
columns = columns; columns = columns,
edges = {}; edges = {},
entities = {}; entities = {},
id = id; id = id,
records = records; records = records,
type = ty; type = ty,
types = types; types = types,
} }
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype world.archetypes[id] = archetype
@ -337,20 +326,20 @@ local World = {}
World.__index = World World.__index = World
function World.new() function World.new()
local self = setmetatable({ local self = setmetatable({
archetypeIndex = {}; archetypeIndex = {},
archetypes = {} :: Archetypes; archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex; componentIndex = {} :: ComponentIndex,
entityIndex = { entityIndex = {
dense = {}, dense = {},
sparse = {} sparse = {},
} :: EntityIndex; } :: EntityIndex,
hooks = { hooks = {
[ON_ADD] = {}; [ON_ADD] = {},
}; },
nextArchetypeId = 0; nextArchetypeId = 0,
nextComponentId = 0; nextComponentId = 0,
nextEntityId = 0; nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype; ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World) }, World)
self.ROOT_ARCHETYPE = archetypeOf(self, {}) self.ROOT_ARCHETYPE = archetypeOf(self, {})
return self return self
@ -477,7 +466,7 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev) return archetypeOf(world, types, prev)
end end
local function findInsert(types: {i53}, toAdd: i53) local function findInsert(types: { i53 }, toAdd: i53)
for i, id in types do for i, id in types do
if id == toAdd then if id == toAdd then
return -1 return -1
@ -644,20 +633,20 @@ end
-- the less creation the better -- the less creation the better
local function actualNoOperation() end local function actualNoOperation() end
local function noop(_self: Query, ...: i53): () -> (number, ...any) local function noop(_self: Query, ...): () -> ()
return actualNoOperation :: any return actualNoOperation :: any
end end
local EmptyQuery = { local EmptyQuery = {
__iter = noop; __iter = noop,
without = noop; without = noop,
} }
EmptyQuery.__index = EmptyQuery EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery) setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery) export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query function World.query(world: World, ...): Query
-- breaking? -- breaking?
if (...) == nil then if (...) == nil then
error("Missing components") error("Missing components")
@ -666,7 +655,7 @@ function World.query(world: World, ...: i53): Query
local compatibleArchetypes = {} local compatibleArchetypes = {}
local length = 0 local length = 0
local components = {...} local components = { ... }
local archetypes = world.archetypes local archetypes = world.archetypes
local queryLength = #components local queryLength = #components
@ -708,7 +697,7 @@ function World.query(world: World, ...: i53): Query
length += 1 length += 1
compatibleArchetypes[length] = { compatibleArchetypes[length] = {
archetype = archetype, archetype = archetype,
indices = indices indices = indices,
} }
end end
@ -721,7 +710,7 @@ function World.query(world: World, ...: i53): Query
preparedQuery.__index = preparedQuery preparedQuery.__index = preparedQuery
function preparedQuery:without(...) function preparedQuery:without(...)
local withoutComponents = {...} local withoutComponents = { ... }
for i = #compatibleArchetypes, 1, -1 do for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i].archetype local archetype = compatibleArchetypes[i].archetype
local records = archetype.records local records = archetype.records
@ -857,11 +846,11 @@ function World.__iter(world: World): () -> (number?, unknown?)
end end
return table.freeze({ return table.freeze({
World = World; World = World,
OnAdd = ON_ADD; OnAdd = ON_ADD,
OnRemove = ON_REMOVE; OnRemove = ON_REMOVE,
OnSet = ON_SET; OnSet = ON_SET,
Wildcard = WILDCARD, Wildcard = WILDCARD,
w = WILDCARD, w = WILDCARD,
Rest = REST, Rest = REST,

186
mkdocs.yml Normal file
View file

@ -0,0 +1,186 @@
site_name: Fusion
site_url: https://elttob.uk/Fusion/
repo_name: dphfox/Fusion
repo_url: https://github.com/dphfox/Fusion
extra:
version:
provider: mike
theme:
name: material
custom_dir: docs/assets/overrides
logo: assets/logo
favicon: assets/logo-dark.svg
palette:
- media: "(prefers-color-scheme: dark)"
scheme: fusiondoc-dark
toggle:
icon: octicons/sun-24
title: Switch to light theme
- media: "(prefers-color-scheme: light)"
scheme: fusiondoc-light
toggle:
icon: octicons/moon-24
title: Switch to dark theme
font:
text: Plus Jakarta Sans
code: JetBrains Mono
features:
- navigation.tabs
- navigation.top
- navigation.sections
- navigation.instant
- navigation.indexes
- search.suggest
- search.highlight
icon:
repo: octicons/mark-github-16
extra_css:
- assets/theme/fusiondoc.css
- assets/theme/colours.css
- assets/theme/code.css
- assets/theme/paragraph.css
- assets/theme/page.css
- assets/theme/admonition.css
- assets/theme/404.css
- assets/theme/api-reference.css
- assets/theme/dev-tools.css
extra_javascript:
- assets/scripts/smooth-scroll.js
nav:
- Home: index.md
- Tutorials:
- Get Started: tutorials/index.md
- Installing Fusion: tutorials/get-started/installing-fusion.md
- Developer Tools: tutorials/get-started/developer-tools.md
- Getting Help: tutorials/get-started/getting-help.md
- Fundamentals:
- Scopes: tutorials/fundamentals/scopes.md
- Values: tutorials/fundamentals/values.md
- Observers: tutorials/fundamentals/observers.md
- Computeds: tutorials/fundamentals/computeds.md
- Tables:
- ForValues: tutorials/tables/forvalues.md
- ForKeys: tutorials/tables/forkeys.md
- ForPairs: tutorials/tables/forpairs.md
- Animation:
- Tweens: tutorials/animation/tweens.md
- Springs: tutorials/animation/springs.md
- Roblox:
- Hydration: tutorials/roblox/hydration.md
- New Instances: tutorials/roblox/new-instances.md
- Parenting: tutorials/roblox/parenting.md
- Events: tutorials/roblox/events.md
- Change Events: tutorials/roblox/change-events.md
- Outputs: tutorials/roblox/outputs.md
- References: tutorials/roblox/references.md
- Best Practices:
- Components: tutorials/best-practices/components.md
- Instance Handling: tutorials/best-practices/instance-handling.md
- Callbacks: tutorials/best-practices/callbacks.md
- State: tutorials/best-practices/state.md
- Sharing Values: tutorials/best-practices/sharing-values.md
- Error Safety: tutorials/best-practices/error-safety.md
- Optimisation: tutorials/best-practices/optimisation.md
- Examples:
- Home: examples/index.md
- Cookbook:
- examples/cookbook/index.md
- Player List: examples/cookbook/player-list.md
- Animated Computed: examples/cookbook/animated-computed.md
- Fetch Data From Server: examples/cookbook/fetch-data-from-server.md
- Light & Dark Theme: examples/cookbook/light-and-dark-theme.md
- Button Component: examples/cookbook/button-component.md
- Loading Spinner: examples/cookbook/loading-spinner.md
- Drag & Drop: examples/cookbook/drag-and-drop.md
- API Reference:
- api-reference/index.md
- General:
- Errors: api-reference/general/errors.md
- Types:
- Contextual: api-reference/general/types/contextual.md
- Version: api-reference/general/types/version.md
- Members:
- Contextual: api-reference/general/members/contextual.md
- Safe: api-reference/general/members/safe.md
- version: api-reference/general/members/version.md
- Memory:
- Types:
- Scope: api-reference/memory/types/scope.md
- ScopedObject: api-reference/memory/types/scopedobject.md
- Task: api-reference/memory/types/task.md
- Members:
- deriveScope: api-reference/memory/members/derivescope.md
- doCleanup: api-reference/memory/members/docleanup.md
- scoped: api-reference/memory/members/scoped.md
- State:
- Types:
- UsedAs: api-reference/state/types/usedas.md
- Computed: api-reference/state/types/computed.md
- Dependency: api-reference/state/types/dependency.md
- Dependent: api-reference/state/types/dependent.md
- For: api-reference/state/types/for.md
- Observer: api-reference/state/types/observer.md
- StateObject: api-reference/state/types/stateobject.md
- Use: api-reference/state/types/use.md
- Value: api-reference/state/types/value.md
- Members:
- Computed: api-reference/state/members/computed.md
- ForKeys: api-reference/state/members/forkeys.md
- ForPairs: api-reference/state/members/forpairs.md
- ForValues: api-reference/state/members/forvalues.md
- Observer: api-reference/state/members/observer.md
- peek: api-reference/state/members/peek.md
- Value: api-reference/state/members/value.md
- Roblox:
- Types:
- Child: api-reference/roblox/types/child.md
- PropertyTable: api-reference/roblox/types/propertytable.md
- SpecialKey: api-reference/roblox/types/specialkey.md
- Members:
- Attribute: api-reference/roblox/members/attribute.md
- AttributeChange: api-reference/roblox/members/attributechange.md
- AttributeOut: api-reference/roblox/members/attributeout.md
- Children: api-reference/roblox/members/children.md
- Hydrate: api-reference/roblox/members/hydrate.md
- New: api-reference/roblox/members/new.md
- OnChange: api-reference/roblox/members/onchange.md
- OnEvent: api-reference/roblox/members/onevent.md
- Out: api-reference/roblox/members/out.md
- Ref: api-reference/roblox/members/ref.md
- Animation:
- Types:
- Animatable: api-reference/animation/types/animatable.md
- Spring: api-reference/animation/types/spring.md
- Tween: api-reference/animation/types/tween.md
- Members:
- Tween: api-reference/animation/members/tween.md
- Spring: api-reference/animation/members/spring.md
- Extras:
- Home: extras/index.md
- Backgrounds: extras/backgrounds.md
- Brand Guidelines: extras/brand-guidelines.md
markdown_extensions:
- admonition
- attr_list
- meta
- md_in_html
- pymdownx.superfences
- pymdownx.betterem
- pymdownx.details
- pymdownx.tabbed:
alternate_style: true
- pymdownx.inlinehilite
- toc:
permalink: true
- pymdownx.highlight:
guess_lang: false
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg

View file

@ -1,5 +1,5 @@
local testkit = require("../testkit")
local jecs = require("../lib/init") local jecs = require("../lib/init")
local testkit = require("../testkit")
local __ = jecs.Wildcard local __ = jecs.Wildcard
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
@ -11,329 +11,343 @@ local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...) local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
local ok, err: string? = pcall(fn, ...) local ok, err: string? = pcall(fn, ...)
if not CHECK(not ok, 2) then if not CHECK(not ok, 2) then
local i = string.find(err :: string, " ") local i = string.find(err :: string, " ")
assert(i) assert(i)
local msg = string.sub(err :: string, i+1) local msg = string.sub(err :: string, i + 1)
CHECK(msg == s, 2) CHECK(msg == s, 2)
end end
end end
local N = 10 local N = 10
TEST("world", function() TEST("world", function()
do CASE "should be iterable" do
local world = jecs.World.new() CASE("should be iterable")
local A = world:component() local world = jecs.World.new()
local B = world:component() local A = world:component()
local eA = world:entity() local B = world:component()
world:set(eA, A, true) local eA = world:entity()
local eB = world:entity() world:set(eA, A, true)
world:set(eB, B, true) local eB = world:entity()
local eAB = world:entity() world:set(eB, B, true)
world:set(eAB, A, true) local eAB = world:entity()
world:set(eAB, B, true) world:set(eAB, A, true)
world:set(eAB, B, true)
local count = 0 local count = 0
for id, data in world do for id, data in world do
count += 1 count += 1
if id == eA then if id == eA then
CHECK(data[A] == true) CHECK(data[A] == true)
CHECK(data[B] == nil) CHECK(data[B] == nil)
elseif id == eB then elseif id == eB then
CHECK(data[A] == nil) CHECK(data[A] == nil)
CHECK(data[B] == true) CHECK(data[B] == true)
elseif id == eAB then elseif id == eAB then
CHECK(data[A] == true) CHECK(data[A] == true)
CHECK(data[B] == true) CHECK(data[B] == true)
end end
end end
-- components are registered in the entity index as well -- components are registered in the entity index as well
-- so this test has to add 2 to account for them -- so this test has to add 2 to account for them
CHECK(count == 3 + 2) CHECK(count == 3 + 2)
end end
do CASE "should query all matching entities" do
local world = jecs.World.new() CASE("should query all matching entities")
local A = world:component() local world = jecs.World.new()
local B = world:component() local A = world:component()
local B = world:component()
local entities = {} local entities = {}
for i = 1, N do for i = 1, N do
local id = world:entity() local id = world:entity()
world:set(id, A, true) world:set(id, A, true)
if i > 5 then world:set(id, B, true) end if i > 5 then
entities[i] = id world:set(id, B, true)
end end
entities[i] = id
end
for id in world:query(A) do for id in world:query(A) do
table.remove(entities, CHECK(table.find(entities, id))) table.remove(entities, CHECK(table.find(entities, id)))
end end
CHECK(#entities == 0) CHECK(#entities == 0)
end
end do
CASE("should query all matching entities when irrelevant component is removed")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
do CASE "should query all matching entities when irrelevant component is removed" local entities = {}
local world = jecs.World.new() for i = 1, N do
local A = world:component() local id = world:entity()
local B = world:component()
local C = world:component()
local entities = {} -- specifically put them in disorder to track regression
for i = 1, N do -- https://github.com/Ukendio/jecs/pull/15
local id = world:entity() world:set(id, B, true)
world:set(id, A, true)
if i > 5 then
world:remove(id, B)
end
entities[i] = id
end
-- specifically put them in disorder to track regression local added = 0
-- https://github.com/Ukendio/jecs/pull/15 for id in world:query(A) do
world:set(id, B, true) added += 1
world:set(id, A, true) table.remove(entities, CHECK(table.find(entities, id)))
if i > 5 then world:remove(id, B) end end
entities[i] = id
end
local added = 0 CHECK(added == N)
for id in world:query(A) do end
added += 1
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(added == N) do
end CASE("should query all entities without B")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
do CASE "should query all entities without B" local entities = {}
local world = jecs.World.new() for i = 1, N do
local A = world:component() local id = world:entity()
local B = world:component()
local entities = {} world:set(id, A, true)
for i = 1, N do if i < 5 then
local id = world:entity() entities[i] = id
else
world:set(id, B, true)
end
end
world:set(id, A, true) for id in world:query(A):without(B) do
if i < 5 then table.remove(entities, CHECK(table.find(entities, id)))
entities[i] = id end
else
world:set(id, B, true)
end
end CHECK(#entities == 0)
end
for id in world:query(A):without(B) do do
table.remove(entities, CHECK(table.find(entities, id))) CASE("should allow setting components in arbitrary order")
end local world = jecs.World.new()
CHECK(#entities == 0) local Health = world:entity()
local Poison = world:component()
end local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
do CASE "should allow setting components in arbitrary order" CHECK(world:get(id, Poison) == 5)
local world = jecs.World.new() end
local Health = world:entity() do
local Poison = world:component() CASE("should allow deleting components")
local world = jecs.World.new()
local id = world:entity() local Health = world:entity()
world:set(id, Poison, 5) local Poison = world:component()
world:set(id, Health, 50)
CHECK(world:get(id, Poison) == 5) local id = world:entity()
end world:set(id, Poison, 5)
world:set(id, Health, 50)
local id1 = world:entity()
world:set(id1, Poison, 500)
world:set(id1, Health, 50)
do CASE "should allow deleting components" world:delete(id)
local world = jecs.World.new()
local Health = world:entity() CHECK(world:get(id, Poison) == nil)
local Poison = world:component() CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50)
end
local id = world:entity() do
world:set(id, Poison, 5) CASE("should allow remove that doesn't exist on entity")
world:set(id, Health, 50) local world = jecs.World.new()
local id1 = world:entity()
world:set(id1, Poison, 500)
world:set(id1, Health, 50)
world:delete(id) local Health = world:entity()
local Poison = world:component()
CHECK(world:get(id, Poison) == nil) local id = world:entity()
CHECK(world:get(id, Health) == nil) world:set(id, Health, 50)
CHECK(world:get(id1, Poison) == 500) world:remove(id, Poison)
CHECK(world:get(id1, Health) == 50)
end CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
do CASE "should allow remove that doesn't exist on entity" do
local world = jecs.World.new() CASE("should increment generation")
local world = jecs.World.new()
local e = world:entity()
CHECK(ECS_ID(e) == 1 + jecs.Rest)
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
local Health = world:entity() do
local Poison = world:component() CASE("should get alive from index in the dense array")
local world = jecs.World.new()
local _e = world:entity()
local e2 = world:entity()
local e3 = world:entity()
local id = world:entity() CHECK(IS_PAIR(world:entity()) == false)
world:set(id, Health, 50)
world:remove(id, Poison)
CHECK(world:get(id, Poison) == nil) local pair = ECS_PAIR(e2, e3)
CHECK(world:get(id, Health) == 50) CHECK(IS_PAIR(pair) == true)
end CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
end
do CASE "should increment generation" do
local world = jecs.World.new() CASE("should allow querying for relations")
local e = world:entity() local world = jecs.World.new()
CHECK(ECS_ID(e) == 1 + jecs.Rest) local Eats = world:entity()
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) local Apples = world:entity()
CHECK(ECS_GENERATION(e) == 0) -- 0 local bob = world:entity()
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
do CASE "should get alive from index in the dense array" world:set(bob, ECS_PAIR(Eats, Apples), true)
local world = jecs.World.new() for e, bool in world:query(ECS_PAIR(Eats, Apples)) do
local _e = world:entity() CHECK(e == bob)
local e2 = world:entity() CHECK(bool)
local e3 = world:entity() end
end
CHECK(IS_PAIR(world:entity()) == false) do
CASE("should allow wildcards in queries")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
local pair = ECS_PAIR(e2, e3) world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
CHECK(IS_PAIR(pair) == true)
CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
end
do CASE "should allow querying for relations" local w = jecs.Wildcard
local world = jecs.World.new() for e, data in world:query(ECS_PAIR(Eats, w)) do
local Eats = world:entity() CHECK(e == bob)
local Apples = world:entity() CHECK(data == "bob eats apples")
local bob = world:entity() end
for e, data in world:query(ECS_PAIR(w, Apples)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
end
world:set(bob, ECS_PAIR(Eats, Apples), true) do
for e, bool in world:query(ECS_PAIR(Eats, Apples)) do CASE("should match against multiple pairs")
CHECK(e == bob) local world = jecs.World.new()
CHECK(bool) local Eats = world:entity()
end local Apples = world:entity()
end local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
do CASE "should allow wildcards in queries" world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
local world = jecs.World.new() world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") local w = jecs.Wildcard
local count = 0
for e, data in world:query(ECS_PAIR(Eats, w)) do
count += 1
if e == bob then
CHECK(data == "bob eats apples")
else
CHECK(data == "alice eats oranges")
end
end
local w = jecs.Wildcard CHECK(count == 2)
for e, data in world:query(ECS_PAIR(Eats, w)) do count = 0
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
for e, data in world:query(ECS_PAIR(w, Apples)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
end
do CASE "should match against multiple pairs" for e, data in world:query(ECS_PAIR(w, Apples)) do
local world = jecs.World.new() count += 1
local Eats = world:entity() CHECK(data == "bob eats apples")
local Apples = world:entity() end
local Oranges =world:entity() CHECK(count == 1)
local bob = world:entity() end
local alice = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") do
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") CASE("should only relate alive entities")
local w = jecs.Wildcard local world = jecs.World.new()
local count = 0 local Eats = world:entity()
for e, data in world:query(ECS_PAIR(Eats, w)) do local Apples = world:entity()
count += 1 local Oranges = world:entity()
if e == bob then local bob = world:entity()
CHECK(data == "bob eats apples") local alice = world:entity()
else
CHECK(data == "alice eats oranges")
end
end
CHECK(count == 2) world:set(bob, Apples, "apples")
count = 0 world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
for e, data in world:query(ECS_PAIR(w, Apples)) do world:delete(Apples)
count += 1 local Wildcard = jecs.Wildcard
CHECK(data == "bob eats apples")
end
CHECK(count == 1)
end
do CASE "should only relate alive entities" local count = 0
for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do
count += 1
end
local world = jecs.World.new() world:delete(ECS_PAIR(Eats, Apples))
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
world:set(bob, Apples, "apples") CHECK(count == 0)
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil)
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") end
world:delete(Apples) do
local Wildcard = jecs.Wildcard CASE("should error when setting invalid pair")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
local count = 0 world:delete(Apples)
for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do
count += 1
end
world:delete(ECS_PAIR(Eats, Apples)) world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
end
CHECK(count == 0) do
CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) CASE("should find target for ChildOf")
end local world = jecs.World.new()
do CASE "should error when setting invalid pair" local ChildOf = world:component()
local world = jecs.World.new() local Name = world:component()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:delete(Apples) local function parent(entity)
return world:target(entity, ChildOf)
end
CHECK_NO_ERR("Apples should be dead", function() local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") local alice = world:entity()
end) local sara = world:entity()
end
do CASE "should find target for ChildOf" world:add(bob, ECS_PAIR(ChildOf, alice))
local world = jecs.World.new() world:set(bob, Name, "bob")
world:add(sara, ECS_PAIR(ChildOf, alice))
world:set(sara, Name, "sara")
CHECK(parent(bob) == alice) -- O(1)
local ChildOf = world:component() local count = 0
local Name = world:component() for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do
print(name)
local function parent(entity) count += 1
return world:target(entity, ChildOf) end
end CHECK(count == 2)
end
local bob = world:entity()
local alice = world:entity()
local sara = world:entity()
world:add(bob, ECS_PAIR(ChildOf, alice))
world:set(bob, Name, "bob")
world:add(sara, ECS_PAIR(ChildOf, alice))
world:set(sara, Name, "sara")
CHECK(parent(bob) == alice) -- O(1)
local count = 0
for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do
print(name)
count += 1
end
CHECK(count == 2)
end
end) end)
FINISH() FINISH()