Compare commits

...

19 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
17 changed files with 1417 additions and 744 deletions

View file

@ -10,29 +10,28 @@ 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...> = typeof(setmetatable( export type Observer<T...> = {
{} :: { disconnect: (Observer<T...>) -> (),
iter: Iter<T...>, }
entities: { Entity<nil> },
disconnect: (Observer<T...>) -> () export type Monitor<T...> = {
}, disconnect: (Observer<T...>) -> (),
{} :: { added: ((jecs.Entity) -> ()) -> (),
__iter: Iter<T...>, removed: ((jecs.Entity) -> ()) -> ()
} }
))
local function observers_new<T...>( local function observers_new<T...>(
query: Query<T...>, query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())? callback: ((Entity<nil>) -> ())
): 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.ids local terms = query.filter_with :: { jecs.Id }
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]
@ -47,8 +46,10 @@ local function observers_new<T...>(
end end
local entity_index = world.entity_index :: any 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>( local function emplaced<T, a>(
entity: jecs.Entity<T>, entity: jecs.Entity<T>,
@ -60,70 +61,93 @@ local function observers_new<T...>(
local archetype = r.archetype local archetype = r.archetype
if archetypes[archetype.id] then if archetypes[archetype.id] then
i += 1 callback(entity)
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
world:added(term, emplaced) local onadded = world:added(term, emplaced)
world:changed(term, emplaced) local onchanged = world:changed(term, emplaced)
table.insert(cleanup, onadded)
table.insert(cleanup, onchanged)
end end
local function disconnect() local without = query.filter_without
table.remove(observers_on_create, table.find( if without then
observers_on_create, for _, term in without do
observer_on_create 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.remove(observers_on_delete, table.find( table.insert(cleanup, onremoved)
observers_on_delete, else
observer_on_delete 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()
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 function iter() local observer = {
local row = i disconnect = disconnect,
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...>( local function monitors_new<T...>(query: Query<T...>): Monitor<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.ids local terms = query.filter_with :: { jecs.Id<any> }
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]
@ -136,44 +160,131 @@ local function monitors_new<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 entities = {} local callback_added: ((jecs.Entity) -> ())?
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
i += 1 callback_added(entity)
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)
local EcsOnRemove = jecs.OnRemove :: jecs.Id if callback_removed == nil then
if callback ~= nil then return
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
world:added(term, emplaced) local onadded = world:added(term, emplaced)
world:removed(term, removed) local onremoved = 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,
@ -184,31 +295,29 @@ local function monitors_new<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 iter() local function monitor_added(callback)
local row = i callback_added = callback
return function() end
if row == 0 then
i = 0 local function monitor_removed(callback)
table.clear(entities) callback_removed = callback
end
local entity = entities[row]
row -= 1
return entity
end
end end
local observer = { local observer = {
disconnect = disconnect, disconnect = disconnect,
entities = entities, added = monitor_added,
__iter = iter, removed = monitor_removed
iter = iter
} }
setmetatable(observer, observer) return (observer :: any) :: Monitor<T...>
return (observer :: any) :: Observer<T...>
end end
return { return {

View file

@ -199,6 +199,7 @@ 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,15 +11,22 @@ 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() local ecs = jecs.world() :: jecs.World
do do
TITLE("one component in common") 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() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end
@ -131,11 +138,21 @@ end
do do
TITLE(testkit.color.white_underline("Mirror query")) TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new() local ecs = mirror.World.new() :: jecs.World
do do
TITLE("one component in common") 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() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end

View file

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

View file

@ -1,15 +1,19 @@
local jecs = require("@jecs") 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 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 -- 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, Vector3.new(10, 20, 30)) world:set(bob, Position, vector.create(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
@ -18,15 +22,16 @@ 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)
print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) assert(pos)
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, Vector3.new(40, 50, 60)) world:set(bob, Position, vector.create(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, Vector3.new(10, 20, 30)) world:set(alice, Position, vector.create(10, 20, 30))
world:add(alice, Walking) world:add(alice, Walking)
-- Remove tag -- Remove tag
@ -34,7 +39,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(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) print(`{name(entity)}: \{{p.x}, {p.y}, {p.z}\}`)
end end
-- Output: -- 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 jecs = require("@jecs")
local world = jecs.World.new() local world = jecs.world()
local Position = world:component() local Position = world:component()
local Velocity = world:component() local Velocity = world:component()

View file

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

View file

@ -3,32 +3,47 @@ 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.new() local world = jecs.world()
type Id<T = nil> = number & { __T: T } local Voxel = world:component() :: jecs.Id
local Voxel = world:component() :: Id local Position = world:component() :: jecs.Id<vector>
local Position = world:component() :: Id<Vector3> local Perception = world:component() :: jecs.Id<{
local Perception = world:component() :: Id<{
range: number, range: number,
fov: 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) local function distance(a: vector, b: vector)
return (b - a).Magnitude return vector.magnitude((b - a))
end 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 to_target = b - a
local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit local forward_xz = vector.normalize(vector.create(forward_dir.x, 0, forward_dir.z))
local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit 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 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 forward_angle = math.deg(math.atan2(forward_xz.z, forward_xz.z))
local angle_difference = math.abs(forward_angle - angle_to_target) local angle_difference = math.abs(forward_angle - angle_to_target)
@ -42,7 +57,7 @@ end
local map = {} local map = {}
local grid = 50 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 hash = position // grid
local voxel_id = map[hash] local voxel_id = map[hash]
if not voxel_id then if not voxel_id then
@ -79,7 +94,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) local it = world:query(Perception, Position, PrimaryPart):iter()
-- 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()
@ -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 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 = 100, range = 200,
fov = 90, fov = 90,
dir = Vector3.new(1, 0, 0), dir = vector.create(1, 0, 0),
}) })
world:set(player, Name, "LocalPlayer") 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, PrimaryPart, primary_part)
world:set(player, Position, Vector3.zero) world:set(player, Position, vector.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, Vector3.new(50, 0, 20)) world:set(enemy, Position, vector.create(50, 0, 20))
add_to_voxel(player, primary_part.Position) 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 local dt = 1 / 60
reconcile_client_owned_assembly_to_voxel(dt) reconcile_client_owned_assembly_to_voxel(dt)

20
jecs.d.ts vendored
View file

@ -184,6 +184,11 @@ 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.
@ -231,6 +236,12 @@ 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.
@ -339,12 +350,19 @@ export type ComponentRecord = {
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 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[]>( export function bulk_insert<const C extends Id[]>(
world: World, world: World,
entity: Entity, entity: Entity,
ids: C, ids: C,
values: { [K in keyof C]: TagToUndefined<InferComponent<C[K]>> }, values: TrimOptional<{ [K in keyof C]: TagToUndefined<InferComponent<C[K]>> }>,
): void; ): void;
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void; export function bulk_remove(world: World, entity: Entity, ids: Id[]): void;

628
jecs.luau

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-rc.12", "version": "0.9.0",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "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,39 +24,6 @@ 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()
@ -91,6 +58,7 @@ 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()
@ -362,6 +330,20 @@ TEST("bulk", function()
CHECK(world:get(e, c2) == 123) CHECK(world:get(e, c2) == 123)
CHECK(world:get(e, c3) == "hello") CHECK(world:get(e, c3) == "hello")
end 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()
@ -550,9 +532,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)
@ -591,100 +573,146 @@ TEST("world:children()", function()
jecs.ECS_META_RESET() jecs.ECS_META_RESET()
end) end)
TEST("world:clear()", function() -- TEST("world:purge()", function()
do CASE "should remove its components" -- 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 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:set(e1, A, true) world:clear(e)
world:set(e1, B, true)
CHECK(world:get(e, A)) CHECK(world:contains(e))
CHECK(world:get(e, B)) CHECK(not world:has(e, A))
CHECK(not world:has(e, B))
world:clear(A) print(jecs.record(world, e).archetype == nil::any)
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 "remove cleared ID from entities" do CASE "should invoke hooks"
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()
local C = world:component() world:set(B, jecs.OnRemove, function()
called += 1
end)
do local e = world:entity()
local id1 = world:entity()
local id2 = world:entity()
local id3 = world:entity()
world:set(id1, A, true) world:set(e, A, true)
world:set(e, B, true)
world:set(id2, A, true) world:clear(e)
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)
@ -756,6 +784,63 @@ 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" do CASE "Should delete children in different archetypes if they have the same parent"
local world = jecs.world() local world = jecs.world()
@ -810,7 +895,6 @@ 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)
@ -1281,7 +1365,6 @@ 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()
@ -1380,7 +1463,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 "axen" do CASE "entity ID reuse works correctly across different world ranges"
local base = jecs.world() local base = jecs.world()
base:range(1_000, 2_000) base:range(1_000, 2_000)
@ -1492,6 +1575,24 @@ 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()
@ -1607,6 +1708,36 @@ 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()
@ -2510,7 +2641,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(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, 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-rc.12" version = "0.9.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"