Compare commits

...

33 commits

Author SHA1 Message Date
dai
9514fb758a
Fix line endings (#280)
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-09-22 14:08:38 +02:00
dai
e2ab3be3e5
Make optionals not required in bulk_insert (#281)
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
* Allow any amount of undefined in bulk_insert

* Also handle unknown
2025-09-21 19:11:14 +02:00
dai
35cb0bca4e
Make World.contains a number->Entity typeguard (#279)
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-09-21 14:13:59 +02:00
Ukendio
7e1f43aff5 Fix without terms for ob::observer and also add bunch of tests
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-09-20 02:35:47 +02:00
Ukendio
690e9ec4d7 Add 0 term cached queries
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-09-18 13:15:05 +02:00
Ukendio
8ed8c2a0e0 Add granular testing
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-09-16 11:14:13 +02:00
Ukendio
23bf021f01 Handle wildcard pairs in observers 2025-09-16 10:36:02 +02:00
Ukendio
4ed2fb7a40 observers should use filter_with
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-09-16 00:28:08 +02:00
Ukendio
e16e4a04e4 Enlist starting archetypes 2025-09-15 23:15:12 +02:00
Ukendio
456713c2d5 Change Observers to support cleanups and :with/without
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-09-15 16:42:48 +02:00
Ukendio
3dacb2af80 Remove focus
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-09-14 19:59:38 +02:00
PepeElToro41
1eecaac96f
fix types (#275)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-09-09 13:12:15 +02:00
Axen
d8b2d36c52
Fix bulk_insert not ensuring that archetype ids are sorted (#277)
* Fix bulk_insert not ensuring archetype ids are sorted

* Add test case
2025-09-09 13:11:55 +02:00
Ukendio
af093713b4 Add examples
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-30 16:12:14 +02:00
Ukendio
549fe97622 Inline world_remove 2025-08-30 14:43:52 +02:00
Ukendio
b0e73857b9 Cleanup tests 2025-08-30 14:43:34 +02:00
Ukendio
917c951d55 Remove eagerly 2025-08-30 13:47:08 +02:00
Ukendio
037035a9a1 Revert :clear to previous behaviour 2025-08-29 17:01:36 +02:00
Ukendio
29a66d92c2 Add component trait lazily
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-26 23:15:48 +02:00
Ukendio
5de842d144 query should use internal world type
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-23 18:33:53 +02:00
Ukendio
9d1665944e world:added should not union the parameterized type with nil 2025-08-22 18:02:58 +02:00
Ukendio
8ace046470 Optimize queries 2025-08-22 17:54:35 +02:00
dai
0874e426af
Fix bulk_insert with moving archetypes (#272)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
* Fix bulk_insert with moving archetypes

* Reword message
2025-08-21 21:32:11 +02:00
dai
51b09549db
Add record types (#271)
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
* Add record typings

* Correct
2025-08-20 22:37:32 +02:00
dai
a6c2d7152e
Allow tags in bulk_insert (#269) 2025-08-20 22:30:43 +02:00
Ukendio
2d9432ab7a Bump package versions
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-08-20 01:17:24 +02:00
Ukendio
96446f4a31 Fix 9+ term queries and cascaded deletion bug with different archetype 2025-08-20 01:15:30 +02:00
dai
bd00edc8c0
Separate Iter from IterFn (#267)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
* Separate Iter from IterFn

* Clean up
2025-08-17 21:59:06 +02:00
dai
0bc1848554
Correct Archetype field types (#268) 2025-08-17 21:58:41 +02:00
dai
65a27a798a
Separate undefined components and tags (#266)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-12 20:39:04 +02:00
Six
ac4441eb84
roblox-ts: Fix columns_map for easier lookups (#263)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
* feat: retype archetype columns_map based on query

* feat: move InferComponents into Iter
2025-08-10 18:13:13 +02:00
Laptev Stanislav
f031dcee8d
docs(api): consolidate and clarify contains method documentation (#264)
Remove duplicate contains method section and update description to be more precise about checking both entities and components. Also fix example code references to use contains instead of has for consistency.
2025-08-10 18:12:50 +02:00
Ukendio
1d650d12e9 Fix backwards edge traversal for exclusive relationships 2025-08-10 16:52:08 +02:00
19 changed files with 2627 additions and 1508 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -355,46 +355,9 @@ This operation is the same as calling:
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
Removes a component (ID) from an entity
@ -460,11 +423,11 @@ Example:
```luau [luau]
local entity = world:entity()
print(world:has(entity))
print(world:contains(entity))
world:delete(entity)
print(world:has(entity))
print(world:contains(entity))
-- Outputs:
-- true
@ -601,7 +564,7 @@ print(retrievedParent === parent) // true
## contains
Checks if an entity exists and is alive in the world.
Checks if an entity or component (id) exists and is alive in the world.
```luau
function World:contains(
@ -640,12 +603,26 @@ function World:exists(
## cleanup
Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed.
Cleans up empty archetypes.
```luau
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:
::: code-group

View file

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

View file

@ -0,0 +1,60 @@
-- 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

@ -0,0 +1,44 @@
-- 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 world = jecs.World.new()
local world = jecs.world()
local Position = world:component()
local Velocity = world:component()

View file

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

View file

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

103
jecs.d.ts vendored
View file

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

799
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",
"version": "0.9.0-rc.9",
"version": "0.9.0",
"description": "Stupidly fast Entity Component System",
"main": "jecs.luau",
"repository": {

392
test/addons/ob.luau Executable file
View file

@ -0,0 +1,392 @@
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()

View file

@ -1,114 +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/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,38 +24,6 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser")
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()
local A = jecs.component()
local B = jecs.component()
@ -90,6 +58,7 @@ TEST("Ensure archetype edges get cleaned", function()
CHECK(false)
end
end)
TEST("repeated entity cached query", function()
local pair = jecs.pair
local world = jecs.world()
@ -148,7 +117,6 @@ TEST("repeated pairs", function()
local e2 = world:entity()
print("-----")
world:set(e2, p2, true)
CHECK(world:get(e2, p2))
@ -190,7 +158,6 @@ TEST("repro", function()
end
CHECK(count == 1)
count = 0
print("----")
world:add(e2v1, jecs.pair(relation, e1v1))
CHECK(world:has(e2v1, jecs.pair(relation, e1v1)))
@ -198,10 +165,11 @@ TEST("repro", function()
count += 1
end
print(count)
CHECK(count==1)
end)
TEST("bulk", function()
do CASE "Should allow components and tags to be in disorder"
local world = jecs.world()
local A = world:component()
local B = world:component()
@ -292,6 +260,90 @@ TEST("bulk", function()
CHECK(world:has(e, E) == false)
CHECK(world:has(e, F) == 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)
TEST("repro", function()
@ -319,7 +371,25 @@ TEST("repro", function()
end)
TEST("world:add()", function()
do CASE "exclusive relations"
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()
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 A = world:component()
world:add(A, jecs.Exclusive)
@ -462,9 +532,9 @@ TEST("world:add()", function()
end)
TEST("world:children()", function()
local world = jecs.world()
local C = jecs.component()
local T = jecs.tag()
local world = jecs.world()
local e1 = world:entity()
world:set(e1, C, true)
@ -503,100 +573,146 @@ TEST("world:children()", function()
jecs.ECS_META_RESET()
end)
TEST("world:clear()", function()
do CASE "should remove its components"
-- TEST("world:purge()", function()
-- do CASE "should remove all instances of specified component"
-- 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 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)
world:clear(e)
CHECK(world:get(e, A))
CHECK(world:get(e, B))
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))
CHECK(world:contains(e))
CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
print(jecs.record(world, e).archetype == nil::any)
end
do CASE "remove cleared ID from entities"
do CASE "should invoke hooks"
local world = jecs.world()
local A = world:component()
local called = 0
world:set(A, jecs.OnRemove, function()
called += 1
end)
local B = world:component()
local C = world:component()
world:set(B, jecs.OnRemove, function()
called += 1
end)
do
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
local e = world:entity()
world:set(id1, A, true)
world:set(e, A, true)
world:set(e, B, 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: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
world:clear(e)
CHECK(world:contains(e))
CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
CHECK(called == 2)
end
end)
@ -668,6 +784,81 @@ TEST("world:contains()", function()
end)
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"
local world = jecs.world()
local A = world:component()
@ -704,7 +895,6 @@ TEST("world:delete()", function()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
world:set(A, jecs.OnRemove, function(entity, id)
world:set(entity, B, true)
end)
@ -1175,7 +1365,6 @@ TEST("world:added", function()
end
do CASE ""
local world = jecs.world()
local IsNearby = world:component()
world:set(IsNearby, jecs.Name, "IsNearby")
local person1, person2 = world:entity(), world:entity()
@ -1212,7 +1401,6 @@ TEST("world:added", function()
end)
local entity = world:entity()
print(pair(A, B))
world:set(entity, pair(A, B), 3)
CHECK(ran)
end
@ -1275,7 +1463,7 @@ TEST("world:range()", function()
CHECK(world.entity_index.alive_count == 400)
CHECK(e)
end
do CASE "axen"
do CASE "entity ID reuse works correctly across different world ranges"
local base = jecs.world()
base:range(1_000, 2_000)
@ -1387,6 +1575,24 @@ TEST("world:range()", function()
end)
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"
local world = jecs.world()
local id = world:entity()
@ -1502,6 +1708,36 @@ end)
TEST("world:query()", function()
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"
local world = jecs.world()
local Foo = world:component()
@ -1760,6 +1996,41 @@ TEST("world:query()", function()
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"
local world = jecs.world()
world:component()
@ -2044,6 +2315,140 @@ TEST("world:remove()", function()
end)
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"
local world = jecs.world()
@ -2236,7 +2641,7 @@ TEST("#repro2", function()
local entity = world:entity()
world:set(entity, pair(Lifetime, Particle), 1)
world:set(entity, pair(Lifetime, Beam), 2)
world:set(entity, pair(4 :: any, 5 :: any), 6) -- noise
world:set(entity, pair(world:component(), world:component()), 6) -- noise
CHECK(world:get(entity, pair(Lifetime, Particle)) == 1)
CHECK(world:get(entity, pair(Lifetime, Beam)) == 2)

View file

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