Compare commits

..

No commits in common. "main" and "v0.9.0-rc.9" have entirely different histories.

19 changed files with 1503 additions and 2622 deletions

View file

@ -10,28 +10,29 @@ type Entity<T> = jecs.Entity<T>
export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...) export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
export type Observer<T...> = { export type Observer<T...> = typeof(setmetatable(
disconnect: (Observer<T...>) -> (), {} :: {
} iter: Iter<T...>,
entities: { Entity<nil> },
export type Monitor<T...> = { disconnect: (Observer<T...>) -> ()
disconnect: (Observer<T...>) -> (), },
added: ((jecs.Entity) -> ()) -> (), {} :: {
removed: ((jecs.Entity) -> ()) -> () __iter: Iter<T...>,
} }
))
local function observers_new<T...>( local function observers_new<T...>(
query: Query<T...>, query: Query<T...>,
callback: ((Entity<nil>) -> ()) callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...> ): Observer<T...>
query:cached()
query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: Query<T...> & { world: World }).world
callback = callback callback = callback
local archetypes = {} local archetypes = {}
local terms = query.filter_with :: { jecs.Id } local terms = query.ids
local first = terms[1] local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first] local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
@ -46,10 +47,8 @@ local function observers_new<T...>(
end end
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local i = 0
for _, archetype in query:archetypes() do local entities = {}
archetypes[archetype.id] = true
end
local function emplaced<T, a>( local function emplaced<T, a>(
entity: jecs.Entity<T>, entity: jecs.Entity<T>,
@ -61,93 +60,70 @@ local function observers_new<T...>(
local archetype = r.archetype local archetype = r.archetype
if archetypes[archetype.id] then if archetypes[archetype.id] then
callback(entity) i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, id, value)
end
end end
end end
local cleanup = {}
for _, term in terms do for _, term in terms do
if jecs.IS_PAIR(term) then if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term) term = jecs.ECS_PAIR_FIRST(term)
end end
local onadded = world:added(term, emplaced) world:added(term, emplaced)
local onchanged = world:changed(term, emplaced) world:changed(term, emplaced)
table.insert(cleanup, onadded)
table.insert(cleanup, onchanged)
end end
local without = query.filter_without local function disconnect()
if without then table.remove(observers_on_create, table.find(
for _, term in without do observers_on_create,
if jecs.IS_PAIR(term) then observer_on_create
local rel = jecs.ECS_PAIR_FIRST(term) ))
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onremoved = world:removed(rel, function(entity, id)
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback(entity)
end
end
end)
table.insert(cleanup, onremoved) table.remove(observers_on_delete, table.find(
else observers_on_delete,
local onremoved = world:removed(term, function(entity, id) observer_on_delete
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback(entity)
end
end
end)
table.insert(cleanup, onremoved)
end
end
end
local function disconnect()
table.remove(observers_on_create, table.find(
observers_on_create,
observer_on_create
))
table.remove(observers_on_delete, table.find(
observers_on_delete,
observer_on_delete
)) ))
table.clear(archetypes)
for _, disconnect in cleanup do
disconnect()
end
end end
local observer = { local function iter()
disconnect = disconnect, local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end
end
local observer = {
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
} }
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...> return (observer :: any) :: Observer<T...>
end end
local function monitors_new<T...>(query: Query<T...>): Monitor<T...> local function monitors_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached() query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: Query<T...> & { world: World }).world
local archetypes = {} local archetypes = {}
local terms = query.filter_with :: { jecs.Id<any> } local terms = query.ids
local first = terms[1] local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first] local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
@ -160,131 +136,44 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
observer_on_delete.callback = function(archetype) observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil archetypes[archetype.id] = nil
end end
for _, archetype in query:archetypes() do
archetypes[archetype.id] = true
end
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local i = 0
local callback_added: ((jecs.Entity) -> ())? local entities = {}
local callback_removed: ((jecs.Entity) -> ())?
local function emplaced<T, a>( local function emplaced<T, a>(
entity: jecs.Entity<T>, entity: jecs.Entity<T>,
id: jecs.Id<a>, id: jecs.Id<a>,
value: a? value: a?
) )
if callback_added == nil then
return
end
local r = jecs.entity_index_try_get_fast( local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any) :: jecs.Record entity_index, entity :: any) :: jecs.Record
local archetype = r.archetype local archetype = r.archetype
if archetypes[archetype.id] then if archetypes[archetype.id] then
callback_added(entity) i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, jecs.OnAdd)
end
end end
end end
local function removed(entity: jecs.Entity, component: jecs.Id) local function removed(entity: jecs.Entity, component: jecs.Id)
if callback_removed == nil then local EcsOnRemove = jecs.OnRemove :: jecs.Id
return if callback ~= nil then
callback(entity, EcsOnRemove)
end end
local r = jecs.record(world, entity)
if not archetypes[r.archetype.id] then
return
end
callback_removed(entity)
end end
local cleanup = {}
for _, term in terms do for _, term in terms do
if jecs.IS_PAIR(term) then if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term) term = jecs.ECS_PAIR_FIRST(term)
end end
local onadded = world:added(term, emplaced) world:added(term, emplaced)
local onremoved = world:removed(term, removed) world:removed(term, removed)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end end
local without = query.filter_without
if without then
for _, term in without do
if jecs.IS_PAIR(term) then
local rel = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onadded = world:added(rel, function(entity, id)
if callback_removed == nil then
return
end
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_removed(entity)
end
end
end)
local onremoved = world:removed(rel, function(entity, id)
if callback_added == nil then
return
end
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_added(entity)
end
end
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
else
local onadded = world:added(term, function(entity, id)
if callback_removed == nil then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_removed(entity)
end
end
end)
local onremoved = world:removed(term, function(entity, id)
if callback_added == nil then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_added(entity)
end
end
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end
end
end
local function disconnect() local function disconnect()
table.remove(observers_on_create, table.find( table.remove(observers_on_create, table.find(
observers_on_create, observers_on_create,
@ -295,29 +184,31 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
observers_on_delete, observers_on_delete,
observer_on_delete observer_on_delete
)) ))
table.clear(archetypes)
for _, disconnect in cleanup do
disconnect()
end
end end
local function monitor_added(callback) local function iter()
callback_added = callback local row = i
end return function()
if row == 0 then
local function monitor_removed(callback) i = 0
callback_removed = callback table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end
end end
local observer = { local observer = {
disconnect = disconnect, disconnect = disconnect,
added = monitor_added, entities = entities,
removed = monitor_removed __iter = iter,
iter = iter
} }
return (observer :: any) :: Monitor<T...> setmetatable(observer, observer)
return (observer :: any) :: Observer<T...>
end end
return { return {

View file

@ -199,7 +199,6 @@ do
end end
local q = world:query(A, B, C, D) local q = world:query(A, B, C, D)
q:archetypes()
START() START()
for id in q do for id in q do
end end

View file

@ -11,22 +11,15 @@ end
local jecs = require("@jecs") local jecs = require("@jecs")
local mirror = require("@mirror") local mirror = require("@mirror")
type i53 = number
do do
TITLE(testkit.color.white_underline("Jecs query")) TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.world() :: jecs.World local ecs = jecs.world()
do do
TITLE("one component in common") TITLE("one component in common")
local function view_bench(world: jecs.World, local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
A: jecs.Id,
B: jecs.Id,
C: jecs.Id,
D: jecs.Id,
E: jecs.Id,
F: jecs.Id,
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end
@ -138,21 +131,11 @@ end
do do
TITLE(testkit.color.white_underline("Mirror query")) TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new() :: jecs.World local ecs = mirror.World.new()
do do
TITLE("one component in common") TITLE("one component in common")
local function view_bench( local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
world: mirror.World,
A: jecs.Id,
B: jecs.Id,
C: jecs.Id,
D: jecs.Id,
E: jecs.Id,
F: jecs.Id,
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end

View file

@ -2,12 +2,33 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new()
local jecs = require(ReplicatedStorage.Lib:Clone()) local jecs = require(ReplicatedStorage.Lib:Clone())
local mirror = require(ReplicatedStorage.mirror:Clone()) local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.world() local mcs = mirror.world()
local ecs = jecs.world() local ecs = jecs.world()
local A1 = Matter.component()
local A2 = Matter.component()
local A3 = Matter.component()
local A4 = Matter.component()
local A5 = Matter.component()
local A6 = Matter.component()
local A7 = Matter.component()
local A8 = Matter.component()
local B1 = ecr.component()
local B2 = ecr.component()
local B3 = ecr.component()
local B4 = ecr.component()
local B5 = ecr.component()
local B6 = ecr.component()
local B7 = ecr.component()
local B8 = ecr.component()
local D1 = ecs:component() local D1 = ecs:component()
local D2 = ecs:component() local D2 = ecs:component()
local D3 = ecs:component() local D3 = ecs:component()
@ -26,53 +47,90 @@ local E6 = mcs:component()
local E7 = mcs:component() local E7 = mcs:component()
local E8 = mcs:component() local E8 = mcs:component()
local registry2 = ecr.registry()
local function flip() local function flip()
return math.random() >= 0.3 return math.random() >= 0.25
end end
local N = 2 ^ 16 - 2 local N = 2 ^ 16 - 2
local archetypes = {}
local hm = 0
for i = 1, N do for i = 1, N do
local id = registry2.create()
local combination = ""
local n = newWorld:spawn()
local entity = ecs:entity() local entity = ecs:entity()
local m = mcs:entity() local m = mcs:entity()
if flip() then if flip() then
ecs:add(entity, entity) registry2:set(id, B1, { value = true })
mcs:add(m, m) ecs:set(entity, D1, { value = true })
newWorld:insert(n, A1({ value = true }))
mcs:set(m, E1, { value = 2 })
end end
if flip() then if flip() then
ecs:set(entity, D1, true) combination ..= "B"
mcs:set(m, E1, true) registry2:set(id, B2, { value = true })
ecs:set(entity, D2, { value = true })
mcs:set(m, E2, { value = 2 })
newWorld:insert(n, A2({ value = true }))
end end
if flip() then if flip() then
ecs:set(entity, D2, true) combination ..= "C"
mcs:set(m, E2, true) registry2:set(id, B3, { value = true })
ecs:set(entity, D3, { value = true })
mcs:set(m, E3, { value = 2 })
newWorld:insert(n, A3({ value = true }))
end end
if flip() then if flip() then
ecs:set(entity, D3, true) combination ..= "D"
mcs:set(m, E3, true) registry2:set(id, B4, { value = true })
ecs:set(entity, D4, { value = true })
mcs:set(m, E4, { value = 2 })
newWorld:insert(n, A4({ value = true }))
end end
if flip() then if flip() then
ecs:set(entity, D4, true) combination ..= "E"
mcs:set(m, E4, true) registry2:set(id, B5, { value = true })
ecs:set(entity, D5, { value = true })
mcs:set(m, E5, { value = 2 })
newWorld:insert(n, A5({ value = true }))
end end
if flip() then if flip() then
ecs:set(entity, D5, true) combination ..= "F"
mcs:set(m, E5, true) registry2:set(id, B6, { value = true })
ecs:set(entity, D6, { value = true })
mcs:set(m, E6, { value = 2 })
newWorld:insert(n, A6({ value = true }))
end end
if flip() then if flip() then
ecs:set(entity, D6, true) combination ..= "G"
mcs:set(m, E6, true) registry2:set(id, B7, { value = true })
ecs:set(entity, D7, { value = true })
mcs:set(m, E7, { value = 2 })
newWorld:insert(n, A7({ value = true }))
end end
if flip() then if flip() then
ecs:set(entity, D7, true) combination ..= "H"
mcs:set(m, E7, true) registry2:set(id, B8, { value = true })
newWorld:insert(n, A8({ value = true }))
ecs:set(entity, D8, { value = true })
mcs:set(m, E8, { value = 2 })
end end
if flip() then
ecs:set(entity, D8, true) if combination:find("BCDF") then
mcs:set(m, E8, true) if not archetypes[combination] then
print(combination)
end
hm += 1
end end
archetypes[combination] = true
end end
print("TEST", hm)
local count = 0 local count = 0
@ -80,11 +138,7 @@ for _, archetype in ecs:query(D2, D4, D6, D8):archetypes() do
count += #archetype.entities count += #archetype.entities
end end
print(count)
local mq = mcs:query(E2, E4, E6, E8):cached()
local jq = ecs:query(D2, D4, D6, D8):cached()
print(count, #jq:archetypes())
return { return {
ParameterGenerator = function() ParameterGenerator = function()
@ -103,12 +157,12 @@ return {
-- end, -- end,
-- --
Mirror = function() Mirror = function()
for entityId, firstComponent in mq do for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do
end end
end, end,
Jecs = function() Jecs = function()
for entityId, firstComponent in jq do for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do
end end
end, end,
}, },

View file

@ -355,8 +355,45 @@ This operation is the same as calling:
world:target(entity, jecs.ChildOf, 0) world:target(entity, jecs.ChildOf, 0)
``` ```
::: ## contains
Checks if an entity or component (id) exists in the world.
```luau
function World:contains(
entity: Entity,
): boolean
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
print(world:contains(entity))
print(world:contains(1))
print(world:contains(2))
-- Outputs:
-- true
-- true
-- false
```
```ts [typescript]
const entity = world.entity();
print(world.contains(entity));
print(world.contains(1));
print(world.contains(2));
// Outputs:
// true
// true
// false
```
:::
## remove ## remove
@ -423,11 +460,11 @@ Example:
```luau [luau] ```luau [luau]
local entity = world:entity() local entity = world:entity()
print(world:contains(entity)) print(world:has(entity))
world:delete(entity) world:delete(entity)
print(world:contains(entity)) print(world:has(entity))
-- Outputs: -- Outputs:
-- true -- true
@ -564,7 +601,7 @@ print(retrievedParent === parent) // true
## contains ## contains
Checks if an entity or component (id) exists and is alive in the world. Checks if an entity exists and is alive in the world.
```luau ```luau
function World:contains( function World:contains(
@ -603,26 +640,12 @@ function World:exists(
## cleanup ## cleanup
Cleans up empty archetypes. Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed.
```luau ```luau
function World:cleanup(): void function World:cleanup(): void
``` ```
:::info
It is recommended to profile the optimal interval you should cleanup because it varies completely from game to game.
Here are a couple of reasons from Sander Mertens:
- some applications are memory constrained, so any wasted memory on empty
archetypes has to get cleaned up
- many archetypes can get created during game startup but aren't used later
on, so it would be wasteful to keep them around
- empty archetypes can slow queries down, especially if there are many more
empty ones than non-empty ones
- if the total number of component permutations (/relationships) is too
high, you have no choice but to periodically cleanup empty archetypes
:::
Example: Example:
::: code-group ::: code-group

View file

@ -1,19 +1,15 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local world = jecs.world() local world = jecs.World.new()
local Position = world:component() :: jecs.Id<vector> local Position = world:component()
local Walking = world:component() local Walking = world:component()
local Name = world:component() :: jecs.Id<string> local Name = world:component()
local function name(e: jecs.Entity): string
return assert(world:get(e, Name))
end
-- Create an entity with name Bob -- Create an entity with name Bob
local bob = world:entity() local bob = world:entity()
-- The set operation finds or creates a component, and sets it. -- The set operation finds or creates a component, and sets it.
world:set(bob, Position, vector.create(10, 20, 30)) world:set(bob, Position, Vector3.new(10, 20, 30))
-- Name the entity Bob -- Name the entity Bob
world:set(bob, Name, "Bob") world:set(bob, Name, "Bob")
-- The add operation adds a component without setting a value. This is -- The add operation adds a component without setting a value. This is
@ -22,16 +18,15 @@ world:add(bob, Walking)
-- Get the value for the Position component -- Get the value for the Position component
local pos = world:get(bob, Position) local pos = world:get(bob, Position)
assert(pos) print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`)
print(`\{{pos.x}, {pos.y}, {pos.z}\}`)
-- Overwrite the value of the Position component -- Overwrite the value of the Position component
world:set(bob, Position, vector.create(40, 50, 60)) world:set(bob, Position, Vector3.new(40, 50, 60))
local alice = world:entity() local alice = world:entity()
-- Create another named entity -- Create another named entity
world:set(alice, Name, "Alice") world:set(alice, Name, "Alice")
world:set(alice, Position, vector.create(10, 20, 30)) world:set(alice, Position, Vector3.new(10, 20, 30))
world:add(alice, Walking) world:add(alice, Walking)
-- Remove tag -- Remove tag
@ -39,7 +34,7 @@ world:remove(alice, Walking)
-- Iterate all entities with Position -- Iterate all entities with Position
for entity, p in world:query(Position) do for entity, p in world:query(Position) do
print(`{name(entity)}: \{{p.x}, {p.y}, {p.z}\}`) print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`)
end end
-- Output: -- Output:

View file

@ -1,60 +0,0 @@
-- Using world:target is the recommended way to grab the target in a wildcard
-- query. However the random access can add up in very hot paths. Accessing its
-- column can drastically improve performance, especially when there are
-- multiple adjacent pairs.
local jecs = require("@jecs")
local pair = jecs.pair
local __ = jecs.Wildcard
local world = jecs.world()
local Likes = world:entity()
local function name(e, name: string): string
if name then
world:set(e, jecs.Name, name)
return name
end
return assert(world:get(e, jecs.Name))
end
local e1 = world:entity()
name(e1, "e1")
local e2 = world:entity()
name(e2, "e2")
local e3 = world:entity()
name(e3, "e3")
world:add(e1, pair(Likes, e2))
world:add(e1, pair(Likes, e3))
local likes = jecs.component_record(world, pair(Likes, __))
assert(likes)
local likes_cr = likes.records
local likes_counts = likes.counts
local archetypes = world:query(pair(Likes, __)):archetypes()
for _, archetype in archetypes do
local types = archetype.types
-- Get the starting index which is what the (R, *) alias is at
local wc = likes_cr[archetype.id]
local count = likes_counts[archetype.id]
local entities = archetype.entities
for i = #entities, 1, -1 do
-- It is generally a good idea to iterate backwards on arrays if you
-- need to delete the iterated entity to prevent iterator invalidation
local entity = entities[i]
for cr = wc, wc + count - 1 do
local person = jecs.pair_second(world, types[cr])
print(`entity ${entity} likes ${person}`)
end
end
end
-- Output:
-- entity $273 likes $275
-- entity $273 likes $274

View file

@ -1,44 +0,0 @@
-- To get the most out of performance, you can lift the inner loop of queries to
-- the system in which you can do archetype-specific optimizations like finding
-- the parent once per archetype rather than per entity.
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard
local world = jecs.world()
local Position = world:component() :: jecs.Id<vector>
local Visible = world:entity()
local parent = world:entity()
world:set(parent, Position, vector.zero)
world:add(parent, Visible)
local child = world:entity()
world:set(child, Position, vector.one)
world:add(child, pair(ChildOf, parent))
local parents = jecs.component_record(world, pair(ChildOf, __))
assert(parents)
local parent_cr = parents.records
local archetypes = world:query(Position, pair(ChildOf, __)):archetypes()
for _, archetype in archetypes do
local types = archetype.types
local p = jecs.pair_second(world, types[parent_cr[archetype.id]])
if world:has(p, Visible) then
local columns = archetype.columns_map
local positions = columns[Position]
for row, entity in archetype.entities do
local pos = positions[row]
print(`Child ${entity} of ${p} is visible at {pos}`)
end
end
end
-- Output:
-- Child $274 of $273 is visibile at 1,1,1

View file

@ -1,5 +1,5 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local world = jecs.world() local world = jecs.World.new()
local Position = world:component() local Position = world:component()
local Velocity = world:component() local Velocity = world:component()

View file

@ -1,16 +1,19 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local pair = jecs.pair local pair = jecs.pair
local world = jecs.world() local world = jecs.World.new()
local Name = world:component() :: jecs.Id<string> local Name = world:component()
local function named(ctr, name)
local function name(e: jecs.Entity): string local e = ctr(world)
return assert(world:get(e, Name)) world:set(e, Name, name)
return e
end
local function name(e)
return world:get(e, Name)
end end
local Position = world:component() :: jecs.Id<vector> local Position = named(world.component, "Position") :: jecs.Entity<vector>
world:set(Position, Name, "Position")
local Previous = jecs.Rest local Previous = jecs.Rest
local added = world local added = world
@ -26,14 +29,10 @@ local removed = world
:cached() :cached()
local e1 = world:entity() local e1 = named(world.entity, "e1")
world:set(e1, Name, "e1")
world:set(e1, Position, vector.create(10, 20, 30)) world:set(e1, Position, vector.create(10, 20, 30))
local e2 = named(world.entity, "e2")
local e2 = world:entity()
world:set(e2, Name, "e2")
world:set(e2, Position, vector.create(10, 20, 30)) world:set(e2, Position, vector.create(10, 20, 30))
for entity, p in added do for entity, p in added do
print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`) print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`)
world:set(entity, pair(Previous, Position), p) world:set(entity, pair(Previous, Position), p)
@ -41,10 +40,10 @@ end
world:set(e1, Position, vector.create(999, 999, 1998)) world:set(e1, Position, vector.create(999, 999, 1998))
for entity, new, old in changed do for _, archetype in changed:archetypes() do
if new ~= old then if new ~= old then
print(`{name(entity)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`) print(`{name(e)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`)
world:set(entity, pair(Previous, Position), new) world:set(e, pair(Previous, Position), new)
end end
end end

View file

@ -3,47 +3,32 @@ local pair = jecs.pair
local ChildOf = jecs.ChildOf local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard local __ = jecs.Wildcard
local Name = jecs.Name local Name = jecs.Name
local world = jecs.world() local world = jecs.World.new()
local Voxel = world:component() :: jecs.Id type Id<T = nil> = number & { __T: T }
local Position = world:component() :: jecs.Id<vector> local Voxel = world:component() :: Id
local Perception = world:component() :: jecs.Id<{ local Position = world:component() :: Id<Vector3>
local Perception = world:component() :: Id<{
range: number, range: number,
fov: number, fov: number,
dir: vector, dir: Vector3,
}> }>
type part = { local PrimaryPart = world:component() :: Id<Part>
Position: vector
}
local PrimaryPart = world:component() :: jecs.Id<part>
local local_player = { local local_player = game:GetService("Players").LocalPlayer
Character = {
PrimaryPart = {
Position = vector.create(50, 0, 30)
}
}
}
local workspace = {
CurrentCamera = {
CFrame = {
LookVector = vector.create(0, 0, -1)
}
}
}
local function distance(a: vector, b: vector) local function distance(a: Vector3, b: Vector3)
return vector.magnitude((b - a)) return (b - a).Magnitude
end end
local function is_in_fov(a: vector, b: vector, forward_dir: vector, fov_angle: number) local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number)
local to_target = b - a local to_target = b - a
local forward_xz = vector.normalize(vector.create(forward_dir.x, 0, forward_dir.z)) local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit
local to_target_xz = vector.normalize(vector.create(to_target.x, 0, to_target.z)) local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit
local angle_to_target = math.deg(math.atan2(to_target_xz.z, to_target_xz.x)) local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X))
local forward_angle = math.deg(math.atan2(forward_xz.z, forward_xz.z)) local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X))
local angle_difference = math.abs(forward_angle - angle_to_target) local angle_difference = math.abs(forward_angle - angle_to_target)
@ -57,7 +42,7 @@ end
local map = {} local map = {}
local grid = 50 local grid = 50
local function add_to_voxel(source: jecs.Entity, position: vector, prev_voxel_id: jecs.Entity?) local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?)
local hash = position // grid local hash = position // grid
local voxel_id = map[hash] local voxel_id = map[hash]
if not voxel_id then if not voxel_id then
@ -94,7 +79,7 @@ local function update_camera_direction(dt: number)
end end
local function perceive_enemies(dt: number) local function perceive_enemies(dt: number)
local it = world:query(Perception, Position, PrimaryPart):iter() local it = world:query(Perception, Position, PrimaryPart)
-- There is only going to be one entity matching the query -- There is only going to be one entity matching the query
local e, self_perception, self_position, self_primary_part = it() local e, self_perception, self_position, self_primary_part = it()
@ -108,28 +93,28 @@ local function perceive_enemies(dt: number)
if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then
local p = target_position local p = target_position
print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.x}, {p.y}, {p.z})`) print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`)
end end
end end
end end
local player = world:entity() local player = world:entity()
world:set(player, Perception, { world:set(player, Perception, {
range = 200, range = 100,
fov = 90, fov = 90,
dir = vector.create(1, 0, 0), dir = Vector3.new(1, 0, 0),
}) })
world:set(player, Name, "LocalPlayer") world:set(player, Name, "LocalPlayer")
local primary_part = local_player.Character.PrimaryPart local primary_part = (local_player.Character :: Model).PrimaryPart :: Part
world:set(player, PrimaryPart, primary_part) world:set(player, PrimaryPart, primary_part)
world:set(player, Position, vector.zero) world:set(player, Position, Vector3.zero)
local enemy = world:entity() local enemy = world:entity()
world:set(enemy, Name, "Enemy $1") world:set(enemy, Name, "Enemy $1")
world:set(enemy, Position, vector.create(50, 0, 20)) world:set(enemy, Position, Vector3.new(50, 0, 20))
add_to_voxel(player, primary_part.Position) add_to_voxel(player, primary_part.Position)
add_to_voxel(enemy, assert(world:get(enemy, Position))) add_to_voxel(enemy, world)
local dt = 1 / 60 local dt = 1 / 60
reconcile_client_owned_assembly_to_voxel(dt) reconcile_client_owned_assembly_to_voxel(dt)

103
jecs.d.ts vendored
View file

@ -7,14 +7,10 @@ export type Entity<TData = unknown> = number & {
readonly __type_TData: TData; readonly __type_TData: TData;
}; };
type TagDiscriminator = {
readonly __nominal_Tag: unique symbol;
};
/** /**
* An entity with no associated data when used as a component * An entity with no associated data when used as a component
*/ */
export type Tag = Entity<TagDiscriminator>; export type Tag = Entity<undefined>;
/** /**
* A pair of entities: * A pair of entities:
@ -30,12 +26,12 @@ export type Pair<P = unknown, O = unknown> = number & {
* An `Id` can be either a single Entity or a Pair of Entities. * An `Id` can be either a single Entity or a Pair of Entities.
* By providing `TData`, you can specifically require an Id that yields that type. * By providing `TData`, you can specifically require an Id that yields that type.
*/ */
export type Id<TData = unknown> = Entity<TData> | Pair<TData, unknown> | Pair<TagDiscriminator, TData>; export type Id<TData = unknown> = Entity<TData> | Pair<TData, unknown> | Pair<undefined, TData>;
export type InferComponent<E> = E extends Entity<infer D> export type InferComponent<E> = E extends Entity<infer D>
? D ? D
: E extends Pair<infer P, infer O> : E extends Pair<infer P, infer O>
? P extends TagDiscriminator ? P extends undefined
? O ? O
: P : P
: never; : never;
@ -47,30 +43,22 @@ type InferComponents<A extends Id[]> = { [K in keyof A]: InferComponent<A[K]> };
type ArchetypeId = number; type ArchetypeId = number;
export type Column<T> = T[]; export type Column<T> = T[];
export type Archetype<T extends Id[]> = { export type Archetype<T extends unknown[]> = {
id: number; id: number;
types: Entity[]; types: number[];
type: string; type: string;
entities: Entity[]; entities: number[];
columns: Column<unknown>[]; columns: Column<unknown>[];
columns_map: { [K in T[number]]: Column<InferComponent<K>> }; columns_map: Record<Id, Column<T[number]>>
}; };
type IterFn<T extends Id[]> = IterableFunction<LuaTuple<[Entity, ...InferComponents<T>]>>; type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
type Iter<T extends Id[]> = IterFn<T> & {
/**
* This isn't callable
* @hidden
* @deprecated
*/
(): never
};
export type CachedQuery<T extends Id[]> = { export type CachedQuery<T extends unknown[]> = {
/** /**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents]. * Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/ */
iter(): IterFn<T>; iter(): Iter<T>;
/** /**
* Returns the matched archetypes of the query * Returns the matched archetypes of the query
@ -79,11 +67,11 @@ export type CachedQuery<T extends Id[]> = {
archetypes(): Archetype<T>[]; archetypes(): Archetype<T>[];
} & Iter<T>; } & Iter<T>;
export type Query<T extends Id[]> = { export type Query<T extends unknown[]> = {
/** /**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents]. * Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/ */
iter(): IterFn<T>; iter(): Iter<T>;
/** /**
* Creates and returns a cached version of this query for efficient reuse. * Creates and returns a cached version of this query for efficient reuse.
@ -131,7 +119,7 @@ export class World {
* @returns An entity (Tag) with no data. * @returns An entity (Tag) with no data.
*/ */
entity(): Tag; entity(): Tag;
entity<T extends Entity>(id: T): T; entity<T extends Entity>(id: T): InferComponent<T> extends undefined ? Tag : T;
/** /**
* Creates a new entity in the first 256 IDs, typically used for static * Creates a new entity in the first 256 IDs, typically used for static
@ -160,7 +148,7 @@ export class World {
* @param entity The target entity. * @param entity The target entity.
* @param component The component (or tag) to add. * @param component The component (or tag) to add.
*/ */
add<C>(entity: Entity, component: TagDiscriminator extends InferComponent<C> ? C : Id<TagDiscriminator>): void; add<C>(entity: Entity, component: undefined extends InferComponent<C> ? C : Id<undefined>): void;
/** /**
* Installs a hook on the given component. * Installs a hook on the given component.
@ -184,11 +172,6 @@ export class World {
*/ */
cleanup(): void; cleanup(): void;
/**
* Removes all instances of specified component
*/
// purge<T>(component: Id<T>): void
/** /**
* Clears all components and relationships from the given entity, but * Clears all components and relationships from the given entity, but
* does not delete the entity from the world. * does not delete the entity from the world.
@ -236,12 +219,6 @@ export class World {
*/ */
contains(entity: Entity): boolean; contains(entity: Entity): boolean;
/**
* Checks if an entity exists in the world.
* @param entity The entity to verify.
*/
contains(entity: number): entity is Entity;
/** /**
* Checks if an entity with the given ID is currently alive, ignoring its generation. * Checks if an entity with the given ID is currently alive, ignoring its generation.
* @param entity The entity to verify. * @param entity The entity to verify.
@ -269,11 +246,11 @@ export class World {
* @param components The list of components to query. * @param components The list of components to query.
* @returns A Query object to iterate over results. * @returns A Query object to iterate over results.
*/ */
query<T extends Id[]>(...components: T): Query<T>; query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
added<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void; added<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void
changed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void; changed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void
removed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>) => void): () => void; removed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>) => void): () => void
} }
export function world(): World; export function world(): World;
@ -320,11 +297,11 @@ export function ECS_PAIR_FIRST(pair: Pair): number;
export function ECS_PAIR_SECOND(pair: Pair): number; export function ECS_PAIR_SECOND(pair: Pair): number;
type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & { type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & {
readonly __nominal_StatefulHook: unique symbol; readonly __nominal_StatefulHook: unique symbol,
}; }
type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & { type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & {
readonly __nominal_StatelessHook: unique symbol; readonly __nominal_StatelessHook: unique symbol,
}; }
export declare const OnAdd: StatefulHook; export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook; export declare const OnRemove: StatelessHook;
@ -342,34 +319,12 @@ export declare const Exclusive: Tag;
export declare const Rest: Entity; export declare const Rest: Entity;
export type ComponentRecord = { export type ComponentRecord = {
records: Map<Id, number>; records: Map<Id, number>,
counts: Map<Id, number>; counts: Map<Id, number>,
size: number; size: number,
}; }
export function component_record(world: World, id: Id): ComponentRecord; export function component_record(world: World, id: Id): ComponentRecord
type TagToUndefined<T> = T extends TagDiscriminator ? undefined : T export function bulk_insert<const C extends Id[]>(world: World, entity: Entity, ids: C, values: InferComponents<C>): void
type TrimOptional<T extends unknown[]> = T extends [...infer L, infer R] export function bulk_remove(world: World, entity: Entity, ids: Id[]): void
? unknown extends R
? L | T | TrimOptional<L>
: R extends undefined
? L | T | TrimOptional<L>
: T
: T
export function bulk_insert<const C extends Id[]>(
world: World,
entity: Entity,
ids: C,
values: TrimOptional<{ [K in keyof C]: TagToUndefined<InferComponent<C[K]>> }>,
): void;
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void;
export type EntityRecord<T extends Id[]> = {
archetype: Archetype<T>,
row: number,
dense: number,
};
export function record<T extends Id[] = []>(world: World, entity: Entity): EntityRecord<T>;

811
jecs.luau

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "@rbxts/jecs", "name": "@rbxts/jecs",
"version": "0.9.0", "version": "0.9.0-rc.9",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

View file

@ -1,392 +0,0 @@
local jecs = require("@jecs")
local testkit = require("@testkit")
local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local FOCUS = test.FOCUS
local ob = require("@addons/ob")
TEST("addons/ob::observer", function()
local world = jecs.world()
do CASE "should match against archetypes that were already created"
local A = world:component()
local e1 = world:entity()
world:add(e1, A)
local c = 1
ob.observer(world:query(A), function()
c+=1
end)
world:remove(e1, A)
world:add(e1, A)
CHECK(c==2)
end
do CASE "Should enter observer at query:without(pair(R, t1)) when adding pair(R, t2)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
ob.observer(world:query(A):without(jecs.pair(B, C)), function()
c += 1
end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
end
do CASE "Should enter observer at query:without(pair(R, t1)) when removing pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
ob.observer(world:query(A):without(jecs.pair(B, D)), function()
c += 1
end)
local child = world:entity()
world:add(child, jecs.pair(B, C))
world:add(child, jecs.pair(B, D))
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, jecs.pair(B, C))
CHECK(c==1)
world:add(child, jecs.pair(B, C))
CHECK(c==1)
world:remove(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
world:remove(child, jecs.pair(B, C))
CHECK(c==2)
world:remove(child, jecs.pair(B, D))
CHECK(c==3)
end
do CASE "Should enter observer at query:without(pair(R, *)) when adding pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local c = 1
ob.observer(world:query(A):without(jecs.pair(B, jecs.w)), function() c+= 1 end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
end
do CASE "Should enter observer at query:without(t1) when removing t1"
local A = world:component()
local B = world:component()
local c = 1
ob.observer(world:query(A):without(B), function() c+= 1 end)
local child = world:entity()
world:add(child, B)
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, B)
CHECK(c==2)
world:remove(child, A)
CHECK(c==2)
end
do CASE "observers should accept pairs"
local A = world:component()
local B = world:component()
local c = 1
ob.observer(world:query(jecs.pair(A, B)), function() c+= 1 end)
local child = world:entity()
world:add(child, jecs.pair(A, B))
CHECK(c == 2)
world:remove(child, jecs.pair(A, B))
CHECK(c == 2)
end
do CASE "Ensure ordering between signals and observers"
local A = world:component()
local B = world:component()
local count = 1
local function counter()
count += 1
end
ob.observer(world:query(A, B), counter)
world:added(A, counter)
world:added(A, counter)
for _ in world:query(A) do
end
local e = world:entity()
world:add(e, A)
CHECK(count == 3)
world:add(e, B)
CHECK(count == 4)
end
do CASE "Rematch entities in observers"
local A = world:component()
local count = 1
local function counter()
count += 1
end
ob.observer(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)
CHECK(count == 2)
world:remove(e, A)
CHECK(count == 2)
world:set(e, A, false)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Call off pairs"
local A = world:component()
local callcount = 1
world:added(A, function(entity)
callcount += 1
end)
world:added(A, function(entity)
callcount += 1
end)
local e = world:entity()
local e2 = world:entity()
world:add(e2, jecs.pair(A, e))
world:add(e, jecs.pair(A, e2))
CHECK(callcount == 1 + 2 * 2)
end
end)
TEST("addons/ob::monitor", function()
local world = jecs.world()
do CASE "should match against archetypes that were already created"
local A = world:component()
local e1 = world:entity()
world:add(e1, A)
local monitor = ob.monitor(world:query(A))
local c = 1
monitor.added(function()
c += 1
end)
world:remove(e1, A)
world:add(e1, A)
CHECK(c==2)
end
do CASE "Should enter monitor at query:without(pair(R, t1)) when adding pair(R, t2)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
local r = 1
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, C)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
r += 1
end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
CHECK(r==1)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
CHECK(r==2)
end
do CASE "Should enter monitor at query:without(pair(R, t1)) when removing pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
local r = 1
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, D)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
r += 1
end)
local child = world:entity()
world:add(child, jecs.pair(B, C))
world:add(child, jecs.pair(B, D))
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, jecs.pair(B, C))
CHECK(c==1)
world:add(child, jecs.pair(B, C))
CHECK(c==1)
world:remove(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
CHECK(r==2)
world:remove(child, jecs.pair(B, C))
CHECK(c==2)
CHECK(r==2)
world:remove(child, jecs.pair(B, D))
CHECK(c==3)
CHECK(r==2)
end
do CASE "Should enter monitor at query:without(pair(R, *)) when adding pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local c = 1
local r = 1
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, jecs.w)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
r += 1
end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
CHECK(r==2)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
CHECK(r==2)
end
do CASE "Should enter monitor at query:without(t1) when removing t1"
local A = world:component()
local B = world:component()
local c = 1
local monitor = ob.monitor(world:query(A):without(B))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1
end)
local child = world:entity()
world:add(child, B)
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, B)
CHECK(c==2)
world:remove(child, A)
CHECK(c==3)
end
do CASE "monitors should accept pairs"
local A = world:component()
local B = world:component()
local c = 1
local monitor = ob.monitor(world:query(jecs.pair(A, B)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1
end)
local child = world:entity()
world:add(child, jecs.pair(A, B))
CHECK(c == 2)
world:remove(child, jecs.pair(A, B))
CHECK(c == 3)
end
do CASE "Don't report changed components in monitor"
local A = world:component()
local count = 1
local function counter()
count += 1
end
local monitor = ob.monitor(world:query(A))
monitor.added(counter)
monitor.removed(counter)
local e = world:entity()
world:set(e, A, false)
CHECK(count == 2)
world:remove(e, A)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
world:set(e, A, false)
CHECK(count == 4)
end
end)
return FINISH()

114
test/addons/observers.luau Executable file
View file

@ -0,0 +1,114 @@
local jecs = require("@jecs")
local testkit = require("@testkit")
local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local FOCUS = test.FOCUS
local ob = require("@addons/ob")
TEST("addons/observers", function()
local world = jecs.world()
do CASE "monitors should accept pairs"
local A = world:component()
local B = world:component()
local c = 1
ob.monitor(world:query(jecs.pair(A, B)), function (_, event)
c += 1
end)
local child = world:entity()
world:add(child, jecs.pair(A, B))
CHECK(c == 2)
world:remove(child, jecs.pair(A, B))
CHECK(c == 3)
end
do CASE "Ensure ordering between signals and observers"
local A = world:component()
local B = world:component()
local count = 1
local function counter()
count += 1
end
ob.observer(world:query(A, B), counter)
world:added(A, counter)
world:added(A, counter)
for _ in world:query(A) do
end
local e = world:entity()
world:add(e, A)
CHECK(count == 3)
world:add(e, B)
CHECK(count == 4)
end
do CASE "Rematch entities in observers"
local A = world:component()
local count = 1
local function counter()
count += 1
end
ob.observer(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)
CHECK(count == 2)
world:remove(e, A)
CHECK(count == 2)
world:set(e, A, false)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Don't report changed components in monitor"
local A = world:component()
local count = 1
local function counter()
count += 1
end
ob.monitor(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)
CHECK(count == 2)
world:remove(e, A)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Call off pairs"
local A = world:component()
local callcount = 1
world:added(A, function(entity)
callcount += 1
end)
world:added(A, function(entity)
callcount += 1
end)
local e = world:entity()
local e2 = world:entity()
world:add(e2, jecs.pair(A, e))
world:add(e, jecs.pair(A, e2))
CHECK(callcount == 1 + 2 * 2)
end
end)
return FINISH()

View file

@ -24,6 +24,38 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser") local entity_visualiser = require("@tools/entity_visualiser")
local dwi = entity_visualiser.stringify local dwi = entity_visualiser.stringify
TEST("ardi", function()
local world = jecs.world()
local r = world:entity()
world:add(r, jecs.pair(jecs.OnDelete, jecs.Delete))
local e = world:entity()
local e1 = world:entity()
world:add(e, jecs.pair(r, e1))
world:delete(r)
CHECK(not world:contains(e))
end)
TEST("dai", function()
local world = jecs.world()
local C = world:component()
world:set(C, jecs.Name, "C")
CHECK(world:get(C, jecs.Name) == "C")
world:entity(2000)
CHECK(world:get(C, jecs.Name) == "C")
end)
TEST("another axen banger", function()
-- taken from jecs.luau
local world = jecs.world()
world:range(2000, 3000)
local e0v1_id = jecs.ECS_COMBINE(1000, 1) -- id can be both within or outside the world's range
local e0v1 = world:entity(e0v1_id)
assert(world:contains(e0v1)) -- fails
end)
TEST("Ensure archetype edges get cleaned", function() TEST("Ensure archetype edges get cleaned", function()
local A = jecs.component() local A = jecs.component()
local B = jecs.component() local B = jecs.component()
@ -58,7 +90,6 @@ TEST("Ensure archetype edges get cleaned", function()
CHECK(false) CHECK(false)
end end
end) end)
TEST("repeated entity cached query", function() TEST("repeated entity cached query", function()
local pair = jecs.pair local pair = jecs.pair
local world = jecs.world() local world = jecs.world()
@ -117,6 +148,7 @@ TEST("repeated pairs", function()
local e2 = world:entity() local e2 = world:entity()
print("-----")
world:set(e2, p2, true) world:set(e2, p2, true)
CHECK(world:get(e2, p2)) CHECK(world:get(e2, p2))
@ -158,6 +190,7 @@ TEST("repro", function()
end end
CHECK(count == 1) CHECK(count == 1)
count = 0 count = 0
print("----")
world:add(e2v1, jecs.pair(relation, e1v1)) world:add(e2v1, jecs.pair(relation, e1v1))
CHECK(world:has(e2v1, jecs.pair(relation, e1v1))) CHECK(world:has(e2v1, jecs.pair(relation, e1v1)))
@ -165,185 +198,100 @@ TEST("repro", function()
count += 1 count += 1
end end
print(count)
CHECK(count==1) CHECK(count==1)
end) end)
TEST("bulk", function() TEST("bulk", function()
do CASE "Should allow components and tags to be in disorder" local world = jecs.world()
local world = jecs.world() local A = world:component()
local A = world:component() local B = world:component()
local B = world:component() local C = world:component()
local C = world:component()
local D = world:component() local D = world:component()
local E = world:entity() local E = world:entity()
local F = world:component() local F = world:component()
local e = world:entity() local e = world:entity()
local r = jecs.entity_index_try_get(world.entity_index, e) local r = jecs.entity_index_try_get(world.entity_index, e)
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 }) jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
CHECK(world:get(e, A) == 1) CHECK(world:get(e, A) == 1)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3) CHECK(world:get(e, C) == 3)
jecs.bulk_insert(world, e, jecs.bulk_insert(world, e,
{ D, E, F }, { D, E, F },
{ 4, nil, 5 } { 4, nil, 5 }
) )
CHECK(world:get(e, A) == 1) CHECK(world:get(e, A) == 1)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3) CHECK(world:get(e, C) == 3)
CHECK(world:get(e, D) == 4) CHECK(world:get(e, D) == 4)
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 5) CHECK(world:get(e, F) == 5)
jecs.bulk_insert(world, e, jecs.bulk_insert(world, e,
{ A, D, E, F, C }, { A, D, E, F, C },
{ 10, 40, nil, 50, 30 } { 10, 40, nil, 50, 30 }
) )
CHECK(world:get(e, A) == 10) CHECK(world:get(e, A) == 10)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 30) CHECK(world:get(e, C) == 30)
CHECK(world:get(e, D) == 40) CHECK(world:get(e, D) == 40)
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50) CHECK(world:get(e, F) == 50)
local G = world:component() local G = world:component()
world:set(e, G, 100) world:set(e, G, 100)
CHECK(world:get(e, A) == 10) CHECK(world:get(e, A) == 10)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 30) CHECK(world:get(e, C) == 30)
CHECK(world:get(e, D) == 40) CHECK(world:get(e, D) == 40)
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50) CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 100) CHECK(world:get(e, G) == 100)
world:remove(e, B) world:remove(e, B)
CHECK(world:get(e, A) == 10) CHECK(world:get(e, A) == 10)
CHECK(world:has(e, B) == false) CHECK(world:has(e, B) == false)
CHECK(world:get(e, C) == 30) CHECK(world:get(e, C) == 30)
CHECK(world:get(e, D) == 40) CHECK(world:get(e, D) == 40)
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50) CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 100) CHECK(world:get(e, G) == 100)
jecs.bulk_remove(world, e, { A, B, C, D }) jecs.bulk_remove(world, e, { A, B, C, D })
CHECK(world:has(e, A) == false) CHECK(world:has(e, A) == false)
CHECK(world:has(e, B) == false) CHECK(world:has(e, B) == false)
CHECK(world:has(e, C) == false) CHECK(world:has(e, C) == false)
CHECK(world:has(e, D) == false) CHECK(world:has(e, D) == false)
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50) CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 100) CHECK(world:get(e, G) == 100)
jecs.bulk_insert(world, e, { D, G }, { 999, 1 }) jecs.bulk_insert(world, e, { D, G }, { 999, 1 })
CHECK(world:has(e, A) == false) CHECK(world:has(e, A) == false)
CHECK(world:has(e, B) == false) CHECK(world:has(e, B) == false)
CHECK(world:has(e, C) == false) CHECK(world:has(e, C) == false)
CHECK(world:get(e, D) == 999) CHECK(world:get(e, D) == 999)
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50) CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 1) CHECK(world:get(e, G) == 1)
jecs.bulk_remove(world, e, { A, B, C, D, E, F, G }) jecs.bulk_remove(world, e, { A, B, C, D, E, F, G })
CHECK(world:has(e, A) == false) CHECK(world:has(e, A) == false)
CHECK(world:has(e, B) == false) CHECK(world:has(e, B) == false)
CHECK(world:has(e, C) == false) CHECK(world:has(e, C) == false)
CHECK(world:has(e, D) == false) CHECK(world:has(e, D) == false)
CHECK(world:has(e, E) == false) CHECK(world:has(e, E) == false)
CHECK(world:has(e, F) == false) CHECK(world:has(e, F) == false)
CHECK(world:has(e, G) == false) CHECK(world:has(e, G) == false)
end
do CASE "Should bulk add by default when there is no values"
local world = jecs.world()
local t1, t2, t3 = world:entity(), world:entity(), world:entity()
local count = 0
local function counter()
count += 1
end
world:added(t1, counter)
world:added(t2, counter)
world:added(t3, counter)
local e = world:entity()
jecs.bulk_insert(world, e, {t1,t2,t3}, {})
CHECK(world:has(e, t1, t2, t3))
CHECK(count == 3)
end
do CASE "Should bulk add by default when there is no values"
local world = jecs.world()
local c1, c2, c3 = world:component(), world:component(), world:component()
local count = 0
local function counter()
count += 1
end
world:changed(c1, counter)
world:changed(c2, counter)
world:changed(c3, counter)
local e = world:entity()
jecs.bulk_insert(world, e, {c1,c2,c3}, {1,2,3})
jecs.bulk_insert(world, e, {c1,c2,c3}, {4,5,6})
CHECK(world:has(e, c1, c2, c3))
CHECK(count == 3)
end
do CASE "Should bulk add with hooks moving archetypes without previous"
local world = jecs.world()
local c1, c2, c3 = world:component(), world:component(), world:component()
world:added(c1, function(e)
world:set(e, c3, "hello")
end)
local e = world:entity()
jecs.bulk_insert(world, e, {c1,c2}, {true, 123})
CHECK(world:get(e, c1) == true)
CHECK(world:get(e, c2) == 123)
CHECK(world:get(e, c3) == "hello")
end
do CASE "Should bulk add with hooks moving archetypes with previous"
local world = jecs.world()
local c1, c2, c3 = world:component(), world:component(), world:component()
world:added(c1, function(e)
world:set(e, c3, "hello")
end)
local e = world:entity()
world:add(e, world:entity())
jecs.bulk_insert(world, e, {c1,c2}, {true, 123})
CHECK(world:get(e, c1) == true)
CHECK(world:get(e, c2) == 123)
CHECK(world:get(e, c3) == "hello")
end
do CASE "Should ensure archetype ids are sorted"
local world = jecs.world()
local c1, c2, c3 = world:component(), world:component(), world:component()
local e = world:entity()
jecs.bulk_insert(world, e, { c2, c1 }, { 2, 1 })
jecs.bulk_insert(world, e, { c1 }, { 1 })
world:set(e, c3, 3)
CHECK(world:get(e, c1) == 1)
CHECK(world:get(e, c2) == 2)
CHECK(world:get(e, c3) == 3)
end
end) end)
TEST("repro", function() TEST("repro", function()
@ -371,25 +319,7 @@ TEST("repro", function()
end) end)
TEST("world:add()", function() TEST("world:add()", function()
do CASE "Removing exclusive pair should traverse backwards on edge" do CASE "exclusive relations"
local world = jecs.world()
local a = world:entity()
local b = world:entity()
local c = world:entity()
world:add(a, pair(ChildOf, b))
world:add(a, pair(ChildOf, c))
CHECK(not world:has(a, pair(ChildOf, b)))
CHECK(world:has(a, pair(ChildOf, c)))
world:remove(a, pair(ChildOf, c))
CHECK(not world:has(a, pair(ChildOf, b)))
CHECK(not world:has(a, pair(ChildOf, c)))
CHECK(not world:target(a, ChildOf))
end
do CASE "Exclusive relations"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
world:add(A, jecs.Exclusive) world:add(A, jecs.Exclusive)
@ -532,9 +462,9 @@ TEST("world:add()", function()
end) end)
TEST("world:children()", function() TEST("world:children()", function()
local world = jecs.world()
local C = jecs.component() local C = jecs.component()
local T = jecs.tag() local T = jecs.tag()
local world = jecs.world()
local e1 = world:entity() local e1 = world:entity()
world:set(e1, C, true) world:set(e1, C, true)
@ -573,146 +503,100 @@ TEST("world:children()", function()
jecs.ECS_META_RESET() jecs.ECS_META_RESET()
end) end)
-- TEST("world:purge()", function() TEST("world:clear()", function()
-- do CASE "should remove all instances of specified component" do CASE "should remove its components"
-- local world = jecs.world()
-- local A = world:component()
-- local B = world:component()
-- local e = world:entity()
-- local e1 = world:entity()
-- local _e2 = world:entity()
-- world:set(e, A, true)
-- world:set(e, B, true)
-- world:set(e1, A, true)
-- world:set(e1, B, true)
-- CHECK(world:get(e, A))
-- CHECK(world:get(e, B))
-- world:purge(A)
-- CHECK(world:get(e, A) == nil)
-- CHECK(world:get(e, B))
-- CHECK(world:get(e1, A) == nil)
-- CHECK(world:get(e1, B))
-- end
-- do CASE "remove purged component from entities"
-- local world = jecs.world()
-- local A = world:component()
-- local B = world:component()
-- local C = world:component()
-- do
-- local id1 = world:entity()
-- local id2 = world:entity()
-- local id3 = world:entity()
-- world:set(id1, A, true)
-- world:set(id2, A, true)
-- world:set(id2, B, true)
-- world:set(id3, A, true)
-- world:set(id3, B, true)
-- world:set(id3, C, true)
-- world:purge(A)
-- CHECK(not world:has(id1, A))
-- CHECK(not world:has(id2, A))
-- CHECK(not world:has(id3, A))
-- CHECK(world:has(id2, B))
-- CHECK(world:has(id3, B, C))
-- world:purge(C)
-- CHECK(world:has(id2, B))
-- CHECK(world:has(id3, B))
-- CHECK(world:contains(A))
-- CHECK(world:contains(C))
-- CHECK(world:has(A, jecs.Component))
-- CHECK(world:has(B, jecs.Component))
-- end
-- do
-- local id1 = world:entity()
-- local id2 = world:entity()
-- local id3 = world:entity()
-- local tgt = world:entity()
-- world:add(id1, pair(A, tgt))
-- world:add(id1, pair(B, tgt))
-- world:add(id1, pair(C, tgt))
-- world:add(id2, pair(A, tgt))
-- world:add(id2, pair(B, tgt))
-- world:add(id2, pair(C, tgt))
-- world:add(id3, pair(A, tgt))
-- world:add(id3, pair(B, tgt))
-- world:add(id3, pair(C, tgt))
-- world:purge(B)
-- CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
-- CHECK(not world:has(id1, pair(B, tgt)))
-- CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
-- CHECK(not world:has(id1, pair(B, tgt)))
-- CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
-- end
-- end
-- end)
TEST("world:clear", function()
do CASE "remove all components on entity"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local e = world:entity() local e = world:entity()
local e1 = world:entity()
local _e2 = world:entity()
world:set(e, A, true) world:set(e, A, true)
world:set(e, B, true) world:set(e, B, true)
world:clear(e) world:set(e1, A, true)
world:set(e1, B, true)
CHECK(world:contains(e)) CHECK(world:get(e, A))
CHECK(not world:has(e, A)) CHECK(world:get(e, B))
CHECK(not world:has(e, B))
print(jecs.record(world, e).archetype == nil::any) world:clear(A)
CHECK(world:get(e, A) == nil)
CHECK(world:get(e, B))
CHECK(world:get(e1, A) == nil)
CHECK(world:get(e1, B))
end end
do CASE "should invoke hooks" do CASE "remove cleared ID from entities"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
local called = 0
world:set(A, jecs.OnRemove, function()
called += 1
end)
local B = world:component() local B = world:component()
world:set(B, jecs.OnRemove, function() local C = world:component()
called += 1
end)
local e = world:entity() do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(e, A, true) world:set(id1, A, true)
world:set(e, B, true)
world:clear(e) world:set(id2, A, true)
world:set(id2, B, true)
world:set(id3, A, true)
world:set(id3, B, true)
world:set(id3, C, true)
world:clear(A)
CHECK(not world:has(id1, A))
CHECK(not world:has(id2, A))
CHECK(not world:has(id3, A))
CHECK(world:has(id2, B))
CHECK(world:has(id3, B, C))
world:clear(C)
CHECK(world:has(id2, B))
CHECK(world:has(id3, B))
CHECK(world:contains(A))
CHECK(world:contains(C))
CHECK(world:has(A, jecs.Component))
CHECK(world:has(B, jecs.Component))
end
do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
local tgt = world:entity()
world:add(id1, pair(A, tgt))
world:add(id1, pair(B, tgt))
world:add(id1, pair(C, tgt))
world:add(id2, pair(A, tgt))
world:add(id2, pair(B, tgt))
world:add(id2, pair(C, tgt))
world:add(id3, pair(A, tgt))
world:add(id3, pair(B, tgt))
world:add(id3, pair(C, tgt))
world:clear(B)
CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
CHECK(not world:has(id1, pair(B, tgt)))
CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
end
CHECK(world:contains(e))
CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
CHECK(called == 2)
end end
end) end)
@ -784,81 +668,6 @@ TEST("world:contains()", function()
end) end)
TEST("world:delete()", function() TEST("world:delete()", function()
do CASE "OnDelete cleanup policy cascades deletion to entites with idr_r pairs"
local world = jecs.world()
local r = world:entity()
world:add(r, jecs.pair(jecs.OnDelete, jecs.Delete))
local e = world:entity()
local e1 = world:entity()
world:add(e, jecs.pair(r, e1))
world:delete(r)
CHECK(not world:contains(e))
end
do CASE "OnDeleteTarget works correctly regardless of adjacent archetype iteration order"
local world = jecs.world()
local t = world:entity()
local c = world:component()
world:add(c, t)
local component = world:component()
local lifetime = world:component()
local tag = world:entity()
local rel1 = world:entity()
local rel2 = world:entity()
local rel3 = world:entity()
local destroyed = false
world:removed(lifetime, function(e)
destroyed = true
end)
local parent = world:entity()
world:set(parent, component, "foo")
world:add(parent, jecs.pair(rel1, component))
local other1 = world:entity()
world:add(other1, tag)
world:add(other1, jecs.pair(jecs.ChildOf, parent))
world:add(other1, jecs.pair(rel1, component))
local child = world:entity()
world:set(child, lifetime, "")
world:add(child, jecs.pair(jecs.ChildOf, parent))
world:add(child, jecs.pair(rel3, parent))
world:add(child, jecs.pair(rel2, other1))
world:delete(parent)
CHECK(destroyed)
CHECK(not world:contains(child))
end
if true then
return
end
do CASE "Should delete children in different archetypes if they have the same parent"
local world = jecs.world()
local component = world:entity()
local parent = world:entity()
local child = world:entity()
world:add(child, jecs.pair(jecs.ChildOf, parent))
local child2 = world:entity()
world:add(child2, component) -- important, they need to be in different archetypes
world:add(child2, jecs.pair(jecs.ChildOf, parent))
world:delete(parent)
CHECK(not world:contains(child))
CHECK(not world:contains(child2)) -- fails
end
do CASE "idr_t//delete_mask@3102..3108" do CASE "idr_t//delete_mask@3102..3108"
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
@ -895,6 +704,7 @@ TEST("world:delete()", function()
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local C = world:component()
world:set(A, jecs.OnRemove, function(entity, id) world:set(A, jecs.OnRemove, function(entity, id)
world:set(entity, B, true) world:set(entity, B, true)
end) end)
@ -1365,6 +1175,7 @@ TEST("world:added", function()
end end
do CASE "" do CASE ""
local world = jecs.world()
local IsNearby = world:component() local IsNearby = world:component()
world:set(IsNearby, jecs.Name, "IsNearby") world:set(IsNearby, jecs.Name, "IsNearby")
local person1, person2 = world:entity(), world:entity() local person1, person2 = world:entity(), world:entity()
@ -1401,6 +1212,7 @@ TEST("world:added", function()
end) end)
local entity = world:entity() local entity = world:entity()
print(pair(A, B))
world:set(entity, pair(A, B), 3) world:set(entity, pair(A, B), 3)
CHECK(ran) CHECK(ran)
end end
@ -1463,7 +1275,7 @@ TEST("world:range()", function()
CHECK(world.entity_index.alive_count == 400) CHECK(world.entity_index.alive_count == 400)
CHECK(e) CHECK(e)
end end
do CASE "entity ID reuse works correctly across different world ranges" do CASE "axen"
local base = jecs.world() local base = jecs.world()
base:range(1_000, 2_000) base:range(1_000, 2_000)
@ -1575,24 +1387,6 @@ TEST("world:range()", function()
end) end)
TEST("world:entity()", function() TEST("world:entity()", function()
do CASE "entity mirroring preserves IDs across world ranges"
local world = jecs.world()
world:range(2000, 3000)
local e0v1_id = jecs.ECS_COMBINE(1000, 1) -- id can be both within or outside the world's range
local e0v1 = world:entity(e0v1_id)
CHECK(world:contains(e0v1)) -- fails
end
do CASE "component names persist after entity creation"
local world = jecs.world()
local C = world:component()
world:set(C, jecs.Name, "C")
CHECK(world:get(C, jecs.Name) == "C")
world:entity(2000)
CHECK(world:get(C, jecs.Name) == "C")
end
do CASE "desired id" do CASE "desired id"
local world = jecs.world() local world = jecs.world()
local id = world:entity() local id = world:entity()
@ -1708,36 +1502,6 @@ end)
TEST("world:query()", function() TEST("world:query()", function()
local N = 2^8 local N = 2^8
do CASE "queries should accept zero-ids provided they use :with for the leading component"
local world = jecs.world()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:set(e1, A, "A")
local e2 = world:entity()
world:set(e2, A, "A")
world:set(e2, B, "B")
for e, a in world:query():with(A) do
CHECK(e == e1 or e == e2)
CHECK(a == nil)
if e == e1 then
CHECK(world:has(e1, A))
CHECK(not world:has(e1, B))
elseif e == e2 then
CHECK(world:has(e2, A, B))
end
end
for e, a in world:query():with(A):without(B) do
CHECK(e == e1)
CHECK(a == nil)
CHECK(world:has(e1, A))
CHECK(not world:has(e1, B))
end
end
do CASE "cached" do CASE "cached"
local world = jecs.world() local world = jecs.world()
local Foo = world:component() local Foo = world:component()
@ -1996,41 +1760,6 @@ TEST("world:query()", function()
end end
end end
do CASE "query more than 8 components"
local world = jecs.world()
local components = {}
for i = 1, 15 do
local id = world:component()
world:component() -- make the components sparsely interleaved
components[i] = id
end
local e1 = world:entity()
for i, id in components do
world:set(e1, id, 13 ^ i)
end
local q = world:query(unpack(components))
for entity, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o in q do
CHECK(a == 13 ^ 1)
CHECK(b == 13 ^ 2)
CHECK(c == 13 ^ 3)
CHECK(d == 13 ^ 4)
CHECK(e == 13 ^ 5)
CHECK(f == 13 ^ 6)
CHECK(g == 13 ^ 7)
CHECK(h == 13 ^ 8)
CHECK(i == 13 ^ 9)
CHECK(j == 13 ^ 10)
CHECK(k == 13 ^ 11)
CHECK(l == 13 ^ 12)
CHECK(m == 13 ^ 13)
CHECK(n == 13 ^ 14)
CHECK(o == 13 ^ 15)
end
end
do CASE "should be able to get next results" do CASE "should be able to get next results"
local world = jecs.world() local world = jecs.world()
world:component() world:component()
@ -2315,140 +2044,6 @@ TEST("world:remove()", function()
end) end)
TEST("world:set()", function() TEST("world:set()", function()
do CASE "Removing exclusive pair should traverse backwards on edge"
local world = jecs.world()
local a = world:entity()
local b = world:entity()
local c = world:entity()
local BattleLink = world:component()
world:add(BattleLink, jecs.Exclusive)
world:set(a, pair(BattleLink, b), {
timestamp = 1,
transform = vector.create(1, 2, 3)
})
world:set(a, pair(BattleLink, c), {
timestamp = 2,
transform = vector.create(1, 2, 3)
})
CHECK(not world:has(a, pair(BattleLink, b)))
CHECK(world:has(a, pair(BattleLink, c)))
world:remove(a, pair(BattleLink, c))
CHECK(not world:has(a, pair(BattleLink, b)))
CHECK(not world:has(a, pair(BattleLink, c)))
CHECK(not world:target(a, BattleLink))
end
do CASE "Exclusive relations"
local world = jecs.world()
local A = world:component()
world:add(A, jecs.Exclusive)
local B = world:component()
local C = world:component()
local e = world:entity()
world:set(e, pair(A, B), true)
world:set(e, pair(A, C), true)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
-- We have to test the path that checks the uncached method
local e1 = world:entity()
world:set(e1, pair(A, B), true)
world:set(e1, pair(A, C), true)
CHECK(world:has(e1, pair(A, B)) == false)
CHECK(world:has(e1, pair(A, C)) == true)
end
do CASE "exclusive relations invoke hooks"
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
local e_ptr: jecs.Entity = (jecs.Rest :: any) + 1
world:add(A, jecs.Exclusive)
local on_remove_call = false
world:set(A, jecs.OnRemove, function(e, id)
on_remove_call = true
end)
local on_add_call_count = 0
world:set(A, jecs.OnAdd, function(e, id)
on_add_call_count += 1
end)
local e = world:entity()
CHECK(e == e_ptr)
world:set(e, pair(A, B))
CHECK(on_add_call_count == 1)
world:set(e, pair(A, C))
CHECK(on_add_call_count == 2)
CHECK(on_remove_call)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
-- We have to ensure that it actually invokes hooks everytime it
-- traverses the archetype
e = world:entity()
world:add(e, pair(A, B))
CHECK(on_add_call_count == 3)
world:add(e, pair(A, C))
CHECK(on_add_call_count == 4)
CHECK(on_remove_call)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
end
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
world:add(A, jecs.Exclusive)
local call_count = 0
world:set(A, jecs.OnRemove, function(e, id)
call_count += 1
if call_count == 1 then
world:set(e, C, true)
else
world:set(e, D, true)
end
end)
local e = world:entity()
world:set(e, pair(A, B), true)
world:set(e, pair(A, C), true)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
CHECK(world:has(e, C))
-- We have to ensure that it actually invokes hooks everytime it
-- traverses the archetype
e = world:entity()
world:set(e, pair(A, B), true)
world:set(e, pair(A, C), true)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
CHECK(world:has(e, D))
end
do CASE "archetype move" do CASE "archetype move"
local world = jecs.world() local world = jecs.world()
@ -2641,7 +2236,7 @@ TEST("#repro2", function()
local entity = world:entity() local entity = world:entity()
world:set(entity, pair(Lifetime, Particle), 1) world:set(entity, pair(Lifetime, Particle), 1)
world:set(entity, pair(Lifetime, Beam), 2) world:set(entity, pair(Lifetime, Beam), 2)
world:set(entity, pair(world:component(), world:component()), 6) -- noise world:set(entity, pair(4 :: any, 5 :: any), 6) -- noise
CHECK(world:get(entity, pair(Lifetime, Particle)) == 1) CHECK(world:get(entity, pair(Lifetime, Particle)) == 1)
CHECK(world:get(entity, pair(Lifetime, Beam)) == 2) CHECK(world:get(entity, pair(Lifetime, Beam)) == 2)

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.9.0" version = "0.9.0-rc.9"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"