Compare commits

..

No commits in common. "main" and "v0.8.3" have entirely different histories.
main ... v0.8.3

24 changed files with 1299 additions and 2130 deletions

View file

@ -1,217 +0,0 @@
--!strict
local jecs = require("@jecs")
type World = jecs.World
type Query<T...> = jecs.Query<T...>
type Id<T=any> = jecs.Id<T>
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...>,
}
))
local function observers_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
callback = callback
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
local observer_on_create = observers_on_create[#observers_on_create]
observer_on_create.callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
local observer_on_delete = observers_on_delete[#observers_on_delete]
observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
local r = entity_index.sparse_array[jecs.ECS_ID(entity)]
local archetype = r.archetype
if archetypes[archetype.id] then
i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, id, value)
end
end
end
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)
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
))
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
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...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
local observer_on_create = observers_on_create[#observers_on_create]
observer_on_create.callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
local observer_on_delete = observers_on_delete[#observers_on_delete]
observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
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
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local EcsOnRemove = jecs.OnRemove :: jecs.Id
if callback ~= nil then
callback(entity, EcsOnRemove)
end
end
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)
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
))
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
end
end
local observer = {
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
}
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...>
end
return {
monitor = monitors_new,
observer = observers_new
}

320
addons/observers.luau Executable file
View file

@ -0,0 +1,320 @@
--!strict
local jecs = require("@jecs")
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, <e>(e: jecs.Entity<e>, id: jecs.Id<T>, value: T?) -> ()) -> () -> (),
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, <e>(e: jecs.Entity<e>, id: jecs.Id<T>, value: T) -> ()) -> () -> (),
observer: <T...>(
PatchedWorld,
jecs.Query<T...>,
(<a>(jecs.Entity, jecs.Id<a>, a) -> ())?
) -> () -> (jecs.Entity),
monitor: (
PatchedWorld,
any,
(<a>(jecs.Entity, jecs.Id<a>) -> ())?
) -> ()
}
local function observers_new(
world: PatchedWorld,
query: any,
callback: (<T, a>(jecs.Entity<T>, jecs.Id<a>, value: a?) -> ())?
)
query = query:cached()
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
observers_on_create[#observers_on_create].callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
observers_on_delete[#observers_on_delete].callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
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, id, value)
end
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
return function()
local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end
end
end
local function join(world, component)
local sparse_array = {}
local dense_array = {}
local values = {}
local max_id = 0
world:added(component, function(entity, id, value)
max_id += 1
sparse_array[entity] = max_id
dense_array[max_id] = entity
values[max_id] = value
end)
world:removed(component, function(entity, id)
local e_swap = dense_array[max_id]
local v_swap = values[max_id]
local dense = sparse_array[entity]
dense_array[dense] = e_swap
values[dense] = v_swap
sparse_array[entity] = nil
dense_array[max_id] = nil
values[max_id] = nil
max_id -= 1
end)
world:changed(component, function(entity, id, value)
values[sparse_array[entity]] = value
end)
return function()
local i = max_id
return function(): ...any
i -= 1
if i == 0 then
return nil
end
local e = dense_array[i]
return e, values[i]
end
end
end
local function monitors_new(world, query, callback)
query = query:cached()
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
observers_on_create[#observers_on_create].callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
observers_on_delete[#observers_on_delete].callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
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, id, value)
end
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local EcsOnRemove = jecs.OnRemove :: jecs.Id
if callback ~= nil then
callback(entity, EcsOnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
return function()
local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end
end
end
local function observers_add(world: jecs.World): PatchedWorld
type Signal = { [jecs.Entity]: { (...any) -> () } }
local world_mut = world :: jecs.World & {[string]: any}
local signals = {
added = {} :: Signal,
emplaced = {} :: Signal,
removed = {} :: Signal
}
world_mut.added = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local function on_add(entity, id, value)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnAdd)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.on_add = on_add
else
world:set(component, jecs.OnAdd, on_add)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.changed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local function on_change(entity, id, value: any)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnChange)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.on_change = on_change
else
world:set(component, jecs.OnChange, on_change)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.removed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id) -> ()
)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity, id)
for _, listener in listeners :: any do
listener(entity, id)
end
end
local existing_hook = world:get(component, jecs.OnRemove)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.on_remove = on_remove
else
world:set(component, jecs.OnRemove, on_remove)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.signals = signals
world_mut.observer = observers_new
world_mut.monitor = monitors_new
world_mut.trackers = {}
return world_mut :: PatchedWorld
end
return observers_add

View file

@ -1,65 +1,61 @@
--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.Lib:Clone())
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.world()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local C1 = ecs:component()
local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component()
local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity()
local m = mcs:entity()
local j = ecs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
return {
ParameterGenerator = function()
local j = ecs:entity()
ecs:set(j, C1, true)
local m = mcs:entity()
mcs:set(m, E1, true)
for i = 1, 1000 do
local friend1 = ecs:entity()
local friend2 = mcs:entity()
ecs:add(friend1, pair(C2, j))
ecs:add(friend1, pair(C3, j))
ecs:add(friend1, pair(C4, j))
mcs:add(friend2, pair(E2, m))
mcs:add(friend2, pair(E3, m))
mcs:add(friend2, pair(E4, m))
end
return {
m = m,
j = j,
}
end,
Functions = {
Mirror = function()
for i = 1, 10 do
local friend2 = mcs:entity()
mcs:add(friend2, pair(E2, m))
mcs:add(friend2, pair(E3, m))
mcs:add(friend2, pair(E4, m))
-- local r = mirror.entity_index_try_get_fast(mcs.entity_index, friend2)
-- local archetype = r.archetype
-- mirror.archetype_destroy(mcs, archetype)
mcs:delete(m)
m = mcs:entity(m)
end
Mirror = function(_, a)
mcs:delete(a.m)
end,
Jecs = function()
for i = 1, 10 do
local friend1 = ecs:entity()
ecs:add(friend1, pair(C2, j))
ecs:add(friend1, pair(C3, j))
ecs:add(friend1, pair(C4, j))
-- local r = jecs.entity_index_try_get_fast(ecs.entity_index, friend1)
-- local archetype = r.archetype
-- jecs.archetype_destroy(ecs, archetype)
ecs:delete(j)
j = ecs:entity()
end
Jecs = function(_, a)
ecs:delete(a.j)
end,
},
}

View file

@ -1,11 +1,13 @@
--!strict
local function collect(signal)
local function collect<T...>(
signal: {
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
): () -> (T...)
local enqueued = {}
local i = 0
local connection = signal:Connect(function(...)
local connection = (signal :: any):Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
@ -23,11 +25,4 @@ local function collect(signal)
end, connection
end
type Signal<T... = ...any> = {
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
ConnectParallel: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Once: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Wait: (self: Signal<T...>) -> (T...)
}
return collect :: <T...>(Signal<T...>) -> (() -> (T...), RBXScriptConnection)
return collect

View file

@ -1,11 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = jecs.world()
local world = observers_add(jecs.world())
local systems = ReplicatedStorage.systems
SYSTEM(world, systems.receive_replication)

View file

@ -0,0 +1,190 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
type Observer<T...> = {
callback: (jecs.Entity) -> (),
query: jecs.Query<T...>,
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (),
monitor: (PatchedWorld, Observer<any>) -> (),
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
local signals = {
added = {},
emplaced = {},
removed = {},
deleted = {}
}
world = world :: jecs.World & {[string]: any}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
idr.hooks.on_add = on_add :: any
idr_r.hooks.on_add = on_add :: any
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_change(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnChange, on_change)
idr.hooks.on_change = on_change :: any
idr_r.hooks.on_change = on_change :: any
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_remove(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnRemove, on_remove)
idr.hooks.on_remove = on_remove :: any
idr_r.hooks.on_remove = on_remove :: any
end
table.insert(listeners, fn)
end
world.signals = signals
world.observer = observers_new
world.monitor = monitors_new
-- local world_delete = world.delete
-- world.deleted = function(_, fn)
-- local listeners = signals.deleted
-- table.insert(listeners, fn)
-- end
-- world.delete = function(world, entity)
-- world_delete(world, entity)
-- for _, fn in signals.deleted do
-- fn(entity)
-- end
-- end
return world :: PatchedWorld
end
return observers_add

View file

@ -1,32 +1,40 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = {
FireClient: (Remote<T...>, Player, T...) -> (),
FireClient: (Remote<T...>, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>, T...) -> (),
OnServerEvent: RBXScriptSignal<(Player, T...)>,
OnClientEvent: RBXScriptSignal<T...>
FireServer: (Remote<T...>) -> (),
OnServerEvent: {
Connect: (any, fn: (Player, T...) -> () ) -> ()
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
}
local function stream_ensure(name)
local function stream_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote
return remote :: any
end
local function datagram_ensure(name)
local function datagram_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote
return remote :: any
end
return {

View file

@ -88,6 +88,9 @@ return function(world: types.World)
if removed then
for _, entity in removed do
entity = ecs_ensure_entity(world, entity)
if not world:contains(entity) then
continue
end
world:remove(entity, id)
end
end

View file

@ -1,6 +1,7 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local observers_add = require("../ReplicatedStorage/observers_add")
export type World = typeof(jecs.world())
export type World = typeof(observers_add(jecs.world()))
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
export type Snapshot = {

View file

@ -2,12 +2,13 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = jecs.world()
local world = observers_add(jecs.world())
local systems = ServerScriptService.systems

View file

@ -1,3 +1,4 @@
--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
@ -176,7 +177,7 @@ return function(world: ty.World)
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif world:contains(e) then
elseif not world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end

View file

@ -2,7 +2,7 @@
Jecs. Just an Entity Component System.
# Members
# Properties
## World
```luau
@ -12,93 +12,27 @@ A world is a container of all ECS data. Games can have multiple worlds but compo
## Wildcard
```luau
jecs.Wildcard: Id
jecs.Wildcard: Entity
```
Builtin component type. This ID is used for wildcard queries.
## Component
```luau
jecs.Component: Id
jecs.Component: Entity
```
Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
## ChildOf
```luau
jecs.ChildOf: Id
jecs.ChildOf: Entity
```
Builtin component type. This ID is for creating parent-child hierarchies.
## OnAdd
```luau
jecs.OnAdd: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is added.
## OnRemove
```luau
jecs.OnRemove: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is removed.
## OnChange
```luau
jecs.OnChange: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is changed.
## Exclusive
```lua
jecs.Exclusive: Id
```
Builtin component type. This ID is for encoding that an ID is Exclusive meaning that an entity can never have more than one target for that exclusive relation.
:::code-group
```luau [luau]
local ChildOf = world:entity()
world:add(ChildOf, jecs.Exclusive)
local pop = world:entity()
local dad = world:entity()
local kid = world:entity()
world:add(kid, pair(ChildOf, dad))
print(world:target(kid, ChildOf, 0) == dad)
world:add(kid, pair(ChildOf, pop))
print(world:target(kid, ChildOf, 1) == dad) -- If ChildOf was not exclusive this would have been true
print(world:target(kid, ChildOf, 0) == pop)
-- Output:
-- true
-- false
-- true
```
:::info
By default, jecs.ChildOf is already an exclusive relationship and this is just a demonstration of how to use it.
In some cases you can use Exclusive relationships as a performance optimization as you can guarantee there will only be one target, therefore
retrieving the data from a wildcard pair with that exclusive relationship can be deterministic.
:::
## Name
```luau
jecs.Name: Id
```
Builtin component type. This ID is for naming components, but realistically you could use any component to do that.
## Rest
```luau
jecs.Rest: Id
jecs.Rest: Entity
```
Builtin component type. This ID is simply for denoting the end of the range for builtin component IDs.
# Functions
## pair()
@ -114,30 +48,3 @@ function jecs.pair(
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
:::
## pair_first()
```luau
function jecs.pair_first(
pair: Id, -- A full pair ID encoded using a relation-target pair.
): Entity -- The ID of the first element. Returns 0 if the ID is not alive.
```
Returns the first element (the relation part) of a pair ID.
**Example:**
```luau
local Likes = world:component()
local alice = world:entity()
local bob = world:entity()
local pair_id = pair(Likes, alice)
local relation = jecs.pair_first(pair_id)
print(relation == Likes) -- true
```
## pair_second()
```luau
function jecs.pair_second(
pair: Id, -- A full pair ID encoded using a relation-target pair.
): Entity -- The ID of the second element. Returns 0 if the ID is not alive.
```
Returns the second element (the target part) of a pair ID.

View file

@ -1,196 +0,0 @@
# Observers
The observers addon extends the World with signal-based reactivity and query-based observers. This addon provides a more ergonomic way to handle component lifecycle events and query changes.
## Installation
The observers addon is included with jecs and can be imported directly:
```luau
local jecs = require(path/to/jecs)
local observers_add = require(path/to/jecs/addons/observers)
local world = observers_add(jecs.world())
```
## Methods
### added
Registers a callback that is invoked when a component is added to any entity.
```luau
function World:added<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>, value: T?) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for additions
- `callback` - Function called when component is added, receives entity, component ID, and value
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:added(Health, function(entity, id, value)
print("Health component added to entity", entity, "with value", value)
end)
-- Later, to stop listening:
unsubscribe()
```
### removed
Registers a callback that is invoked when a component is removed from any entity.
```luau
function World:removed<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for removals
- `callback` - Function called when component is removed, receives entity and component ID
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:removed(Health, function(entity, id)
print("Health component removed from entity", entity)
end)
```
### changed
Registers a callback that is invoked when a component's value is changed on any entity.
```luau
function World:changed<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>, value: T) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for changes
- `callback` - Function called when component value changes, receives entity, component ID, and new value
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:changed(Health, function(entity, id, value)
print("Health changed to", value, "for entity", entity)
end)
```
### observer
Creates a query-based observer that triggers when entities match or stop matching a query.
```luau
function World:observer<T...>(
query: Query<T...>,
callback: ((entity: Entity, id: Id, value: any?) -> ())?
): () -> () -> Entity -- Returns an iterator function
```
**Parameters:**
- `query` - The query to observe for changes
- `callback` - Optional function called when entities match the query
**Returns:** An iterator function that returns entities that matched the query since last iteration
**Example:**
```luau
local Position = world:component() :: jecs.Id<Vector3>
local Velocity = world:component() :: jecs.Id<Vector3>
local moving_entities = world:observer(
world:query(Position, Velocity),
function(entity, id, value)
print("Entity", entity, "started moving")
end
)
-- In your game loop:
for entity in moving_entities() do
-- Process newly moving entities
end
```
### monitor
Creates a query-based monitor that triggers when entities are added to or removed from a query.
```luau
function World:monitor<T...>(
query: Query<T...>,
callback: ((entity: Entity, id: Id, value: any?) -> ())?
): () -> () -> Entity -- Returns an iterator function
```
**Parameters:**
- `query` - The query to monitor for additions/removals
- `callback` - Optional function called when entities are added or removed from the query
**Returns:** An iterator function that returns entities that were added or removed since last iteration
**Example:**
```luau
local Health = world:component() :: jecs.Id<number>
local health_changes = world:monitor(
world:query(Health),
function(entity, id, value)
print("Health component changed for entity", entity)
end
)
-- In your game loop:
for entity in health_changes() do
-- Process entities with health changes
end
```
## Usage Patterns
### Component Lifecycle Tracking
```luau
local Player = world:component()
local Health = world:component() :: jecs.Id<number>
-- Track when players are created
world:added(Player, function(entity, id, instance)
instance:SetAttribute("entityid", entity)
end)
world:removed(Player, function(entity, id)
world:add(entity, Destroy) -- process its deletion later!
end)
```
## Performance Considerations
- **Signal listeners** are called immediately when components are added/removed/changed
- **Query observers** cache the query for better performance
- **Multiple listeners** for the same component are supported and called in registration order
- **Unsubscribe functions** should be called when listeners are no longer needed to prevent memory leaks
- **Observer iterators** should be called regularly to clear the internal buffer
## Integration with Built-in Hooks
The observers addon integrates with the built-in component hooks (`OnAdd`, `OnRemove`, `OnChange`). If a component already has these hooks configured, the observers addon will preserve them and call both the original hook and any registered signal listeners.

View file

@ -4,38 +4,13 @@ A World contains entities which have components. The World is queryable and can
# Methods
## cached
## iter
Returns a cached version of the query. This is useful if you want to create a query that you can iterate multiple times.
Returns an iterator that can be used to iterate over the query.
```luau
function Query:cached(): Query -- Returns the cached Query
function Query:iter(): () -> (Entity, ...)
```
Example:
::: code-group
```luau [luau]
local lerps = world:query(Lerp):cached() -- Ensure that you cache this outside a system so you do not create a new cache for a query every frame
local function system(dt)
for entity, lerp in lerps do
-- Do something
end
end
```
```ts [typescript]
const lerps = world.query(Lerp).cached()
function system(dt) {
for (const [entity, lerp] of lerps) {
// Do something
}
}
```
:::
## with
@ -108,13 +83,15 @@ Example:
```luau [luau]
for i, archetype in world:query(Position, Velocity):archetypes() do
local field = archetype.columns_map
local positions = field[Position]
local velocities = field[Velocity]
local columns = archetype.columns
local field = archetype.records
local P = field[Position]
local V = field[Velocity]
for row, entity in archetype.entities do
local position = positions[row]
local velocity = velocities[row]
local position = columns[P][row]
local velocity = columns[V][row]
-- Do something
end
end
@ -124,27 +101,10 @@ end
This function is meant for people who want to really customize their query behaviour at the archetype-level
:::
## iter
In most cases, you can iterate over queries directly using `for entity, ... in query do`. The `:iter()` method is mainly useful if you are on the old solver, to get types for the returned values.
## cached
Returns a cached version of the query. This is useful if you want to iterate over the same query multiple times.
```luau
function Query:iter(): () -> (Entity, ...)
```
Example:
::: code-group
```luau [luau]
local query = world:query(Position, Velocity)
-- Direct iteration (recommended)
for entity, position, velocity in query do
-- Process entity
end
-- Using explicit iterator (when needed for the old solver)
local iterator = query:iter()
for entity, position, velocity in iterator do
-- Process entity
end
function Query:cached(): Query -- Returns the cached Query
```

View file

@ -6,7 +6,7 @@ A World contains entities which have components. The World is queryable and can
## new
`World` utilizes a class, meaning jecs allows you to create multiple worlds.
`World` utilizes a class, meaning JECS allows you to create multiple worlds.
```luau
function World.new(): World
@ -55,12 +55,12 @@ const entity = world.entity();
## component
Creates a new component. Do note components are entities as well, meaning jecs allows you to add other components onto them.
Creates a new component. Do note components are entities as well, meaning JECS allows you to add other components onto them.
These are meant to be added onto other entities through `add` and `set`
```luau
function World:component<T>(): Entity<T> -- The new component.
function World:component<T>(): Entity<T> -- The new componen.
```
Example:
@ -241,10 +241,7 @@ print(world.get(Entity, Health));
// 100
// 50
```
:::
:::info
`world:set(entity, component, value)` propagates that a change has happened for thes component on this entity, while mutating a value directly would not.
:::
## query
@ -292,52 +289,10 @@ If the index is larger than the total number of instances the entity has for the
```luau
function World:target(
entity: Entity, -- The entity
relation: Id, -- The relationship between the entity and the target
relation: Entity, -- The relationship between the entity and the target
nth: number, -- The index
): Id? -- The target for the relationship at the specified index.
): Entity? -- The target for the relationship at the specified index.
```
Example:
::: code-group
```luau [luau]
local function timers_count(world: types.World)
local timers = world
:query(jecs.pair(ct.Timer, jecs.w))
:without(ct.Destroy)
:cached()
return function(_, dt: number)
for entity in timers do
local index = 0
local nth = world:target(entity, ct.Timer, index)
while nth do
local timer = world:get(entity, jecs.pair(ct.Timer, nth))
local elapsed = timer.elapsed + dt
if elapsed >= timer.duration then
world:add(entity, ct.Destroy)
end
timer.elapsed = elapsed
end
end
end
end
```
```ts [typescript]
const entity = world.entity();
print(world.contains(entity));
print(world.contains(1));
print(world.contains(2));
// Outputs:
// true
// true
// false
```
:::
## parent
@ -400,9 +355,9 @@ print(world.contains(2));
Removes a component (ID) from an entity
```luau
function World:remove<T>(
function World:remove(
entity: Entity,
component: Id<T>
component: Entity<T>
): void
```
@ -503,20 +458,20 @@ Useful when you only need the entity for a specific ID and you want to avoid cre
```luau
function World:each(
component: Id -- The component ID
id: Entity -- The component ID
): () -> Entity
```
Example:
::: code-group
```luau [luau]
local id = world:component()
local id = world:entity()
for entity in world:each(id) do
-- Do something
end
```
```ts [typescript]
const id = world.component();
const id = world.entity();
for (const entity of world.each(id)) {
// Do something
}
@ -545,122 +500,6 @@ Enforces a check for entities to be created within a desired range.
```luau
function World:range(
range_begin: number -- The starting point,
range_end: number? -- The end point (optional)
range_begin: number? -- The end point (optional)
)
```
Example:
::: code-group
```luau [luau]
world:range(1000, 5000) -- Entities will be created with IDs 1000-5000
local entity = world:entity()
print(entity) -- Will be >= 1000 and < 5000
```
```ts [typescript]
world.range(1000, 5000) // Entities will be created with IDs 1000-5000
const entity = world.entity()
print(entity) // Will be >= 1000 and < 5000
```
:::
## parent
Gets the parent entity of the specified entity using the built-in `ChildOf` relationship.
```luau
function World:parent(
entity: Entity
): Entity? -- Returns the parent entity or nil if no parent
```
Example:
::: code-group
```luau [luau]
local parent = world:entity()
local child = world:entity()
world:add(child, pair(jecs.ChildOf, parent))
local retrieved_parent = world:parent(child)
print(retrieved_parent == parent) -- true
```
```ts [typescript]
const parent = world.entity()
const child = world.entity()
world.add(child, pair(jecs.ChildOf, parent))
const retrievedParent = world.parent(child)
print(retrievedParent === parent) // true
```
:::
## contains
Checks if an entity exists and is alive in the world.
```luau
function World:contains(
entity: Entity
): boolean
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
print(world:contains(entity)) -- true
world:delete(entity)
print(world:contains(entity)) -- false
```
```ts [typescript]
const entity = world.entity()
print(world.contains(entity)) // true
world.delete(entity)
print(world.contains(entity)) // false
```
:::
## exists
Checks if the entity ID exists regardless of whether it is alive or not. Useful to know if the ID is occupied in the entity index.
```luau
function World:exists(
entity: Entity
): boolean
```
## cleanup
Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed.
```luau
function World:cleanup(): void
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
world:delete(entity)
-- Cleanup is usually automatic, but can be called manually
world:cleanup()
```
```ts [typescript]
const entity = world.entity()
world.delete(entity)
// Cleanup is usually automatic, but can be called manually
world.cleanup()
```
:::

View file

@ -27,8 +27,8 @@ local jecs = require(path/to/jecs)
local world = jecs.world()
```
```typescript [typescript]
import * as jecs from "@rbxts/jecs"
const world = jecs.world()
import { World } from "@rbxts/jecs"
const world = new World()
// creates a new entity with no components and returns its identifier
const entity = world.entity()
@ -156,13 +156,6 @@ world.set(Transform, OnChange, (entity, id, data) => {
```
:::
:::info
Children are cleaned up before parents
When a parent and its children are deleted, OnRemove hooks will be invoked for children first, under the condition that there are no cycles in the relationship graph of the deleted entities. This order is maintained for any relationship that has the (OnDeleteTarget, Delete) trait (see Component Traits for more details).
When an entity graph contains cycles, order is undefined. This includes cycles that can be formed using different relationships.
:::
### Cleanup Traits
When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references.
@ -173,22 +166,15 @@ This is what cleanup traits are for: to specify which action needs to be execute
To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed.
#### Cleanup Traits Summary
There are two cleanup actions:
| Condition | Action | Description | Use Case |
|-----------|--------|-------------|----------|
| `OnDelete` | `Remove` | Removes the component from all entities when the component is deleted | Default behavior, safe cleanup |
| `OnDelete` | `Delete` | Deletes all entities that have the component when the component is deleted | Cascading deletion, dangerous |
| `OnDeleteTarget` | `Remove` | Removes the relationship from all entities when the target is deleted | Safe relationship cleanup |
| `OnDeleteTarget` | `Delete` | Deletes all entities that have the relationship when the target is deleted | Hierarchical deletion (e.g., parent-child) |
- `Remove`: removes instances of the specified (component) id from all entities (default)
- `Delete`: deletes all entities with specified id
**Cleanup Actions:**
- `Remove`: removes instances of the specified (component) id from all entities (default)
- `Delete`: deletes all entities with specified id
There are two cleanup conditions:
**Cleanup Conditions:**
- `OnDelete`: the component, tag or relationship is deleted
- `OnDeleteTarget`: a target used with the relationship is deleted
- `OnDelete`: the component, tag or relationship is deleted
- `OnDeleteTarget`: a target used with the relationship is deleted
#### (OnDelete, Remove)
::: code-group
@ -299,10 +285,9 @@ jecs.world() -- Position gets registered here
```
```typescript [typescript]
import { world } from "@rbxts/jecs"
const Position = jecs.component<Vector3>();
world() // Position gets registered here
new World() // Position gets registered here
```
:::
@ -316,11 +301,9 @@ jecs.world() -- Position gets registered here with its name "Position"
```
```typescript [typescript]
import { world } from "@rbxts/jecs"
jecs.meta(Position, jecs.Name, "Position")
world() // Position gets registered here with its name "Position"
new World() // Position gets registered here with its name "Position"
```
:::
@ -649,7 +632,7 @@ world:set(e, pair(Eats, Apples), { amount = 1 })
world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0))
world:set(e, pair(End, Position), Vector3.new(10, 20, 30))
world:add(e, pair(jecs.ChildOf, Position))
world:add(e, jecs.ChildOf, Position)
```
```typescript [typescript]
@ -665,7 +648,7 @@ world.set(e, pair(Eats, Apples), { amount: 1 })
world.set(e, pair(Begin, Position), new Vector3(0, 0, 0))
world.set(e, pair(End, Position), new Vector3(10, 20, 30))
world.add(e, pair(jecs.ChildOf, Position))
world.add(e, jecs.ChildOf, Position)
```
:::
@ -712,7 +695,3 @@ To improve the speed of evaluating queries, Jecs has indices that store all arch
While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships.
This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html).
## Next Steps
- [API Reference](../api/jecs.md) - Complete API documentation

View file

@ -49,8 +49,8 @@ A tool for inspecting entity lifetimes
### Helpers
#### [jecs_ob](https://github.com/Ukendio/jecs/blob/main/addons/ob.luau)
Observers & Monitors for queries
#### [jecs_observers](https://github.com/Ukendio/jecs/blob/main/addons/observers.luau)
Observers for queries and signals for components
### [hammer](https://github.com/Mark-Marks/hammer)
A set of utilities for Jecs

14
jecs.d.ts vendored
View file

@ -49,7 +49,7 @@ export type Archetype<T extends unknown[]> = {
type: string;
entities: number[];
columns: Column<unknown>[];
columns_map: Record<Id, Column<T[number]>>
columns_map: { [K in keyof T]: Column<T[K]> }
};
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
@ -156,8 +156,8 @@ export class World {
* @param hook The hook to install.
* @param value The hook callback.
*/
set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity, id: Id<T>, data: T) => void): void;
set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity, id: Id<T>) => void): void;
set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity<T>, id: Id<T>, data: T) => void): void;
set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity<T>, id: Id<T>) => void): void;
/**
* Assigns a value to a component on the given entity.
* @param entity The target entity.
@ -247,10 +247,6 @@ export class World {
* @returns A Query object to iterate over results.
*/
query<T extends Id[]>(...components: T): Query<InferComponents<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
}
export function world(): World;
@ -307,7 +303,6 @@ export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook;
export declare const OnChange: StatefulHook;
export declare const ChildOf: Tag;
export declare const Component: Tag;
export declare const Wildcard: Entity;
export declare const w: Entity;
export declare const OnDelete: Tag;
@ -325,6 +320,3 @@ export type 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

1087
jecs.luau

File diff suppressed because it is too large Load diff

View file

@ -42,12 +42,14 @@ export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
export type Query<T...> = typeof(setmetatable(
{} :: {
iter: Iter<T...>,
with: (<a>(Query<T...>, Id<a>) -> Query<T...>)
with:
(<a>(Query<T...>, Id<a>) -> Query<T...>)
& (<a, b>(Query<T...>, Id<a>, Id<b>) -> Query<T...>)
& (<a, b, c>(Query<T...>, Id<a>, Id<b>, Id<c>) -> Query<T...>)
& (<a, b, c>(Query<T...>, Id<a>, Id<b>, Id<c>) -> Query<T...>)
& (<a, b, c, d>(Query<T...>, Id<a>, Id<b>, Id<c>, Id) -> Query<T...>),
without: (<a>(Query<T...>, Id<a>) -> Query<T...>)
without:
(<a>(Query<T...>, Id<a>) -> Query<T...>)
& (<a, b>(Query<T...>, Id<a>, Id<b>) -> Query<T...>)
& (<a, b, c>(Query<T...>, Id<a>, Id<b>, Id<c>) -> Query<T...>)
& (<a, b, c>(Query<T...>, Id<a>, Id<b>, Id<c>) -> Query<T...>)
@ -170,10 +172,11 @@ export type ComponentRecord = {
counts: { [Id]: number },
flags: number,
size: number,
on_add: (<T>(entity: Entity, id: Entity<T>, value: T?) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T) -> ())?,
on_remove: ((entity: Entity, id: Entity) -> ())?,
hooks: {
on_add: (<T>(entity: Entity, id: Entity<T>, value: T?) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T) -> ())?,
on_remove: ((entity: Entity, id: Entity) -> ())?,
},
}
export type ComponentIndex = Map<Id, ComponentRecord>
export type Archetypes = { [Id]: Archetype }
@ -193,10 +196,9 @@ local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48
local ECS_ID_DELETE = 0b0001
local ECS_ID_IS_TAG = 0b0010
local ECS_ID_IS_EXCLUSIVE = 0b0100
local ECS_ID_MASK = 0b0000
local ECS_ID_DELETE = 0b01
local ECS_ID_IS_TAG = 0b10
local ECS_ID_MASK = 0b00
local HI_COMPONENT_ID = 256
local EcsOnAdd = HI_COMPONENT_ID + 1
@ -212,8 +214,7 @@ local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11
local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsExclusive = HI_COMPONENT_ID + 14
local EcsRest = HI_COMPONENT_ID + 15
local EcsRest = HI_COMPONENT_ID + 14
local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false)
@ -318,9 +319,9 @@ end
local function entity_index_try_get_any(
entity_index: EntityIndex,
entity: Entity
entity: number
): Record?
local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity::number)]
local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
if not r or r.dense == 0 then
return nil
@ -343,20 +344,6 @@ local function entity_index_try_get(entity_index: EntityIndex, entity: Entity):
return r
end
local function entity_index_try_get_fast(entity_index: EntityIndex, entity: Entity): Record?
local r = entity_index_try_get_any(entity_index, entity)
if r then
local r_dense = r.dense
-- if r_dense > entity_index.alive_count then
-- return nil
-- end
if entity_index.dense_array[r_dense] ~= entity then
return nil
end
end
return r
end
local function entity_index_is_alive<T>(entity_index: EntityIndex, entity: Entity<T>): boolean
return entity_index_try_get(entity_index, entity) ~= nil
end
@ -696,7 +683,6 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
local is_pair = ECS_IS_PAIR(id :: number)
local has_delete = false
local is_exclusive = false
if is_pair then
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53
@ -711,10 +697,6 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
if cleanup_policy_target == EcsDelete then
has_delete = true
end
if world_has_one_inline(world, relation, EcsExclusive) then
is_exclusive = true
end
else
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
@ -736,8 +718,7 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
flags = bit32.bor(
flags,
if has_delete then ECS_ID_DELETE else 0,
if is_tag then ECS_ID_IS_TAG else 0,
if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0
if is_tag then ECS_ID_IS_TAG else 0
)
idr = {
@ -745,10 +726,11 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
records = {},
counts = {},
flags = flags,
on_add = on_add,
on_change = on_change,
on_remove = on_remove,
hooks = {
on_add = on_add,
on_change = on_change,
on_remove = on_remove,
},
} :: ComponentRecord
component_index[id] = idr
@ -781,7 +763,6 @@ local function archetype_register(world: World, archetype: Archetype)
local archetype_id = archetype.id
local columns_map = archetype.columns_map
local columns = archetype.columns
for i, component_id in archetype.types do
local idr = id_record_ensure(world, component_id)
local is_tag = bit32.btest(idr.flags, ECS_ID_IS_TAG)
@ -804,10 +785,6 @@ local function archetype_register(world: World, archetype: Archetype)
archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column)
end
end
world.archetype_index[archetype.type] = archetype
world.archetypes[archetype_id] = archetype
world.archetype_edges[archetype.id] = {} :: Map<Id, Archetype>
end
local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?): Archetype
@ -829,7 +806,7 @@ local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?):
dead = false,
}
archetype_register(world, archetype, false)
archetype_register(world, archetype)
for id in columns_map do
local observer_list = find_observers(world, EcsOnArchetypeCreate, id)
@ -843,6 +820,9 @@ local function archetype_create(world: World, id_types: { Id }, ty, prev: i53?):
end
end
world.archetype_index[ty] = archetype
world.archetypes[archetype_id] = archetype
world.archetype_edges[archetype.id] = {} :: Map<Id, Archetype>
return archetype
end
@ -949,10 +929,9 @@ end
local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype
local id_types = from.types
local dst = table.clone(id_types)
local at = find_insert(id_types :: { number } , id :: number)
local dst = table.clone(id_types)
table.insert(dst, at, id)
return archetype_ensure(world, dst)
@ -988,6 +967,8 @@ local function world_component(world: World): i53
return id
end
local function archetype_fast_delete_last(columns: { Column }, column_count: number)
for i, column in columns do
if column ~= NULL_ARRAY then
@ -1029,7 +1010,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number)
for _, id in id_types do
local idr = component_index[id]
local on_remove = idr.on_remove
local on_remove = idr.hooks.on_remove
if on_remove then
on_remove(delete, id)
end
@ -1052,25 +1033,17 @@ local function archetype_destroy(world: World, archetype: Archetype)
local component_index = world.component_index
local archetype_edges = world.archetype_edges
local edges = archetype_edges[archetype.id]
for id, node in edges do
archetype_edges[node.id][id] = nil
edges[id] = nil
for id, edge in archetype_edges[archetype.id] do
archetype_edges[edge.id][id] = nil
end
local archetype_id = archetype.id
-- world.archetypes[archetype_id] = nil :: any
-- world.archetype_index[archetype.type] = nil :: any
world.archetypes[archetype_id] = nil :: any
world.archetype_index[archetype.type] = nil :: any
local columns_map = archetype.columns_map
for id in columns_map do
local idr = component_index[id]
idr.records[archetype_id] = nil :: any
idr.counts[archetype_id] = nil
idr.size -= 1
if idr.size == 0 then
component_index[id] = nil :: any
end
local observer_list = find_observers(world, EcsOnArchetypeDelete, id)
if not observer_list then
continue
@ -1082,7 +1055,15 @@ local function archetype_destroy(world: World, archetype: Archetype)
end
end
archetype.dead = true
for id in columns_map do
local idr = component_index[id]
idr.records[archetype_id] = nil :: any
idr.counts[archetype_id] = nil
idr.size -= 1
if idr.size == 0 then
component_index[id] = nil :: any
end
end
end
local function NOOP() end
@ -2028,7 +2009,7 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va
local value = values[i]
local cdr = component_index[id]
local on_add = cdr.on_add
local on_add = cdr.hooks.on_add
if value then
columns_map[id][row] = value
if on_add then
@ -2073,11 +2054,11 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va
local value = values[i] :: any
local on_add = idr.on_add
local on_add = idr.hooks.on_add
if value ~= nil then
columns_map[id][row] = value
local on_change = idr.on_change
local on_change = idr.hooks.on_change
local hook = if set then on_change else on_add
if hook then
hook(entity, id, value :: any)
@ -2112,7 +2093,7 @@ local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity })
remove[id] = true
local idr = component_index[id]
local on_remove = idr.on_remove
local on_remove = idr.hooks.on_remove
if on_remove then
on_remove(entity, id)
end
@ -2186,76 +2167,6 @@ local function world_new()
return r
end
local function inner_archetype_move(
entity: Entity,
to: Archetype,
dst_row: i24,
from: Archetype,
src_row: i24
)
local src_columns = from.columns
local dst_entities = to.entities
local src_entities = from.entities
local last = #src_entities
local id_types = from.types
local columns_map = to.columns_map
if src_row ~= last then
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
local dst_column = columns_map[id_types[i]]
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[src_row] = column[last]
column[last] = nil
end
local e2 = src_entities[last]
src_entities[src_row] = e2
local record2 = eindex_sparse_array[ECS_ENTITY_T_LO(e2 :: number)]
record2.row = src_row
else
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[last] = nil
end
end
src_entities[last] = nil :: any
dst_entities[dst_row] = entity
end
local function inner_entity_move(
entity_index: EntityIndex,
entity: Entity,
record: Record,
to: Archetype
)
local sourceRow = record.row
local from = record.archetype
local dst_row = archetype_append(entity, to)
inner_archetype_move(entity, to, dst_row, from, sourceRow)
record.archetype = to
record.row = dst_row
end
-- local function inner_entity_index_try_get(entity: number): Record?
-- local r = inner_entity_index_try_get_any(entity)
-- if r then
@ -2296,80 +2207,20 @@ local function world_new()
end
local from = record.archetype
if ECS_IS_PAIR(id::number) then
local src = from or ROOT_ARCHETYPE
local edge = archetype_edges[src.id]
local to = edge[id]
local idr: ComponentRecord
if not to then
local first = ECS_PAIR_FIRST(id::number)
local wc = ECS_PAIR(first, EcsWildcard)
idr = component_index[wc]
if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local cr = idr.records[src.id]
if cr then
local on_remove = idr.on_remove
local id_types = src.types
if on_remove then
on_remove(entity, id_types[cr])
src = record.archetype
id_types = src.types
cr = idr.records[src.id]
end
local dst = table.clone(id_types)
dst[cr] = id
to = archetype_ensure(world, dst)
else
to = find_archetype_with(world, id, src)
idr = component_index[id]
end
else
to = find_archetype_with(world, id, src)
idr = component_index[id]
end
edge[id] = to
else
if to.dead then
archetype_register(world, to)
edge[id] = to
archetype_edges[to.id][id] = src
to.dead = false
end
idr = component_index[id]
end
if from == to then
return
end
if from then
inner_entity_move(entity_index, entity, record, to)
else
if #to.types > 0 then
new_entity(entity, record, to)
end
end
local on_add = idr.on_add
if on_add then
on_add(entity, id)
end
return
end
local to = archetype_traverse_add(world, id, from)
if from == to then
return
end
if from then
inner_entity_move(entity_index, entity, record, to)
entity_move(entity_index, entity, record, to)
else
if #to.types > 0 then
new_entity(entity, record, to)
end
end
local idr = component_index[id]
local on_add = idr.on_add
local idr = world.component_index[id]
local on_add = idr.hooks.on_add
if on_add then
on_add(entity, id)
@ -2497,74 +2348,36 @@ local function world_new()
end
local from: Archetype = record.archetype
local src = from or ROOT_ARCHETYPE
local column = src.columns_map[id]
if column then
local idr = component_index[id]
local to: Archetype = inner_archetype_traverse_add(id, from)
local idr = component_index[id]
local idr_hooks = idr.hooks
if from == to then
local column = to.columns_map[id]
column[record.row] = data
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
local on_change = idr.on_change
local on_change = idr_hooks.on_change
if on_change then
on_change(entity, id, data)
end
return
end
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
entity_move(entity_index, entity, record, to)
else
local to: Archetype
local idr: ComponentRecord
if ECS_IS_PAIR(id::number) then
local edge = archetype_edges[src.id]
to = edge[id]
if not to then
local first = ECS_PAIR_FIRST(id::number)
local wc = ECS_PAIR(first, EcsWildcard)
idr = component_index[wc]
if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local cr = idr.records[src.id]
if cr then
local on_remove = idr.on_remove
local id_types = src.types
if on_remove then
on_remove(entity, id_types[cr])
src = record.archetype
id_types = src.types
cr = idr.records[src.id]
end
local dst = table.clone(id_types)
dst[cr] = id
to = archetype_ensure(world, dst)
else
to = find_archetype_with(world, id, src)
idr = component_index[id]
end
else
to = find_archetype_with(world, id, src)
idr = component_index[id]
end
edge[id] = to
archetype_edges[to.id][id] = src
else
idr = component_index[id]
end
else
to = inner_archetype_traverse_add(id, from)
idr = component_index[id]
end
new_entity(entity, record, to)
end
local column = to.columns_map[id]
column[record.row] = data
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
inner_entity_move(entity_index, entity, record, to)
else
new_entity(entity, record, to)
end
column = to.columns_map[id]
column[record.row] = data
local on_add = idr.on_add
if on_add then
on_add(entity, id, data)
end
local on_add = idr_hooks.on_add
if on_add then
on_add(entity, id, data)
end
end
@ -2579,42 +2392,30 @@ local function world_new()
if not dense or r.dense == 0 then
r.dense = index
dense = index
local any = eindex_dense_array[dense]
if any == entity then
local e_swap = eindex_dense_array[dense]
local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record
r_swap.dense = dense
alive_count += 1
entity_index.alive_count = alive_count
r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
end
return entity
end
local any = eindex_dense_array[dense]
if any ~= entity then
if alive_count <= dense then
local e_swap = eindex_dense_array[dense]
local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record
r_swap.dense = dense
alive_count += 1
entity_index.alive_count = alive_count
r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
if dense <= alive_count then
if any ~= entity then
error("Entity ID is already in use with a different generation")
else
return entity
end
end
local e_swap = eindex_dense_array[dense]
local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record
alive_count += 1
entity_index.alive_count = alive_count
r_swap.dense = dense
r.dense = alive_count
eindex_dense_array[dense] = e_swap
eindex_dense_array[alive_count] = entity
return entity
else
for i = eindex_max_id + 1, index do
eindex_sparse_array[i] = { dense = i } :: Record
eindex_sparse_array[i]= { dense = i } :: Record
eindex_dense_array[i] = i
end
entity_index.max_id = index
@ -2654,14 +2455,14 @@ local function world_new()
if from.columns_map[id] then
local idr = world.component_index[id]
local on_remove = idr.on_remove
local on_remove = idr.hooks.on_remove
if on_remove then
on_remove(entity, id)
end
local to = archetype_traverse_remove(world, id, record.archetype)
inner_entity_move(entity_index, entity, record, to)
entity_move(entity_index, entity, record, to)
end
end
@ -2688,13 +2489,16 @@ local function world_new()
end
if idr_t then
local queue: { i53 }
local ids: Map<i53, boolean>
local count = 0
local archetype_ids = idr_t.records
for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id]
local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities
local node = idr_t_archetype
local removal_queued = false
for _, id in idr_t_types do
if not ECS_IS_PAIR(id::number) then
@ -2705,54 +2509,65 @@ local function world_new()
if object ~= entity then
continue
end
node = archetype_traverse_remove(world, id, node)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
if not ids then
ids = {} :: { [i53]: boolean }
end
ids[id] = true
removal_queued = true
end
for i = #entities, 1, -1 do
local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
if not removal_queued then
continue
end
if not queue then
queue = {} :: { i53 }
end
local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n
end
for id in ids do
for _, child in queue do
inner_world_remove(world, child, id)
end
end
end
if idr_r then
local count = 0
local archetype_ids = idr_r.records
local ids = {}
local queue = {}
local records = idr_r.records
local counts = idr_r.counts
for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id]
local node = idr_r_archetype
local entities = idr_r_archetype.entities
local tr = records[archetype_id]
local tr_count = counts[archetype_id]
local types = idr_r_archetype.types
for i = tr, tr + tr_count - 1 do
local id = types[i]
node = archetype_traverse_remove(world, id, idr_r_archetype)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
end
ids[types[i]] = true
end
for i = #entities, 1, -1 do
local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n
end
for _, e in queue do
for id in ids do
inner_world_remove(world, e, id)
end
end
end
end
local function inner_world_delete<T>(world: World, entity: Entity<T>)
local entity_index = world.entity_index
local record = inner_entity_index_try_get_unsafe(entity::number)
if not record then
return
@ -2791,7 +2606,7 @@ local function world_new()
archetype_destroy(world, idr_archetype)
end
else
local on_remove = idr.on_remove
local on_remove = idr.hooks.on_remove
if on_remove then
for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id]
@ -2808,7 +2623,7 @@ local function world_new()
-- this is hypothetically not that expensive of an operation anyways
to = archetype_traverse_remove(world, entity, from)
end
inner_entity_move(entity_index, e, r, to)
entity_move(entity_index, e, r, to)
end
archetype_destroy(world, idr_archetype)
@ -2829,15 +2644,19 @@ local function world_new()
end
end
end
if idr_t then
local children: { i53 }
local ids: Map<i53, boolean>
local count = 0
local archetype_ids = idr_t.records
for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id]
local node = idr_t_archetype
local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities
local removal_queued = false
local deleted = false
for _, id in idr_t_types do
if not ECS_IS_PAIR(id::number) then
continue
@ -2855,24 +2674,31 @@ local function world_new()
local child = entities[i]
inner_world_delete(world, child)
end
deleted = true
break
else
node = archetype_traverse_remove(world, id, node)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
if not ids then
ids = {} :: { [i53]: boolean }
end
ids[id] = true
removal_queued = true
end
end
if not deleted then
for i = #entities, 1, -1 do
local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
if not removal_queued then
continue
end
if not children then
children = {} :: { i53 }
end
local n = #entities
table.move(entities, 1, n, count + 1, children)
count += n
end
if ids then
for _, child in children do
for id in ids do
inner_world_remove(world, child, id)
end
end
end
@ -2896,29 +2722,28 @@ local function world_new()
archetype_destroy(world, idr_r_archetype)
end
else
local children = {}
local count = 0
local ids = {}
local counts = idr_r.counts
local records = idr_r.records
for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id]
local node = idr_r_archetype
local entities = idr_r_archetype.entities
local tr = records[archetype_id]
local tr_count = counts[archetype_id]
local types = idr_r_archetype.types
for i = tr, tr + tr_count - 1 do
local id = types[i]
node = archetype_traverse_remove(world, id, node)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
end
ids[types[i]] = true
end
for i = #entities, 1, -1 do
local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
local n = #entities
table.move(entities, 1, n, count + 1, children)
count += n
end
for _, child in children do
for id in ids do
inner_world_remove(world, child, id)
end
end
@ -2928,19 +2753,21 @@ local function world_new()
end
end
local dense_array = entity_index.dense_array
local dense = record.dense
local i_swap = entity_index.alive_count
entity_index.alive_count = i_swap - 1
local e_swap = eindex_dense_array[i_swap]
local e_swap = dense_array[i_swap]
local r_swap = inner_entity_index_try_get_any(e_swap :: number) :: Record
r_swap.dense = dense
record.archetype = nil :: any
record.row = nil :: any
record.dense = i_swap
eindex_dense_array[dense] = e_swap
eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity :: number)
dense_array[dense] = e_swap
dense_array[i_swap] = ECS_GENERATION_INC(entity :: number)
end
local function inner_world_exists<T>(world: World, entity: Entity<T>): boolean
@ -3023,7 +2850,6 @@ local function world_new()
inner_world_set(world, EcsRest, EcsRest, "jecs.Rest")
inner_world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
inner_world_add(world, EcsChildOf, EcsExclusive)
for i = EcsRest + 1, ecs_max_tag_id do
entity_index_new_id(entity_index)
@ -3087,9 +2913,6 @@ return {
Delete = (EcsDelete :: any) :: Entity,
Remove = (EcsRemove :: any) :: Entity,
Name = (EcsName :: any) :: Entity<string>,
Exclusive = EcsExclusive :: Entity,
ArchetypeCreate = EcsOnArchetypeCreate,
ArchetypeDelete = EcsOnArchetypeDelete,
Rest = (EcsRest :: any) :: Entity,
pair = (ECS_PAIR :: any) :: <P, O>(first: Id<P>, second: Id<O>) -> Pair<P, O>,
@ -3126,7 +2949,6 @@ return {
entity_move = entity_move,
entity_index_try_get = entity_index_try_get,
entity_index_try_get_fast = entity_index_try_get_fast,
entity_index_try_get_any = entity_index_try_get_any,
entity_index_is_alive = entity_index_is_alive,
entity_index_new_id = entity_index_new_id,

View file

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

View file

@ -2,28 +2,43 @@ 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")
local observers_add = require("@addons/observers")
TEST("addons/observers", function()
local world = observers_add(jecs.world())
local world = jecs.world()
do CASE "monitors should accept pairs"
do CASE "Should work even if set after the component has been used"
local A = world:component()
local B = world:component()
local c = 1
ob.monitor(world:query(jecs.pair(A, B)), function (_, event)
c += 1
world:set(world:entity(), A, 2)
local ran = false
world:added(A, function()
ran = true
end)
local child = world:entity()
world:add(child, jecs.pair(A, B))
CHECK(c == 2)
local entity = world:entity()
world:set(entity, A, 3)
world:remove(child, jecs.pair(A, B))
CHECK(c == 3)
CHECK(ran)
end
do CASE "Should not override hook"
local A = world:component()
local count = 1
local function counter()
count += 1
end
world:set(A, jecs.OnAdd, counter)
world:added(A, counter)
world:set(world:entity(), A, false)
CHECK(count == (1 + 2))
world:set(world:entity(), A, false)
CHECK(count == (1 + (2 * 2)))
end
do CASE "Ensure ordering between signals and observers"
local A = world:component()
local B = world:component()
@ -33,15 +48,11 @@ TEST("addons/observers", function()
count += 1
end
ob.observer(world:query(A, B), counter)
world: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)
@ -58,7 +69,7 @@ TEST("addons/observers", function()
count += 1
end
ob.observer(world:query(A), counter)
world:observer(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)
@ -78,7 +89,7 @@ TEST("addons/observers", function()
count += 1
end
ob.monitor(world:query(A), counter)
world:monitor(world:query(A), counter)
local e = world:entity()
world:set(e, A, false)

View file

@ -24,145 +24,8 @@ 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()
local world = jecs.world()
local edges = world.archetype_edges
local e = world:entity()
local r = jecs.record(world, e)
world:set(e, A, true)
world:add(e, A)
local arch_a = r.archetype
world:set(e, B, true)
world:add(e, B)
local arch_ab = r.archetype
CHECK(edges[arch_a.id][B] == arch_ab)
CHECK(edges[arch_ab.id][B] == arch_a)
world:delete(B)
CHECK(edges[arch_a.id][B] == nil)
CHECK(edges[arch_ab.id][A] == nil)
for _ in edges[arch_ab.id] do
CHECK(false)
end
world:delete(A)
CHECK(edges[arch_a.id][B] == nil)
CHECK(edges[arch_a.id][A] == nil)
for _ in edges[arch_a.id] do
CHECK(false)
end
end)
TEST("repeated entity cached query", function()
local pair = jecs.pair
local world = jecs.world()
local rel = world:entity()
local cmp = world:component()
local query = world:query(cmp):cached()
local t1 = world:entity()
local p1 = pair(rel, t1)
local e1 = world:entity()
world:add(e1, p1)
world:set(e1, cmp, true)
CHECK(query:iter()() == e1)
world:delete(e1)
world:delete(t1)
local t2 = world:entity()
local p2 = pair(rel, t2)
local e2 = world:entity()
world:add(e2, p2)
world:set(e2, cmp, true)
CHECK(query:iter()() == e2) -- Fails
end)
TEST("repeated pairs", function()
local pair = jecs.pair
local world = jecs.world()
local rel = world:component() -- Does not error if this is just a tag
-- Does not happen if we delete manually instead of using this
world:add(rel, pair(jecs.OnDeleteTarget, jecs.Delete))
local t1 = world:entity()
local p1 = pair(rel, t1)
local e1 = world:entity()
world:set(e1, p1, true)
CHECK(world:get(e1, p1))
CHECK(world:each(p1)() == e1)
world:delete(t1)
local t2 = world:entity()
local p2 = pair(rel, t2)
local e2 = world:entity()
print("-----")
world:set(e2, p2, true)
CHECK(world:get(e2, p2))
CHECK(p1 == p2)
local count = 0
CHECK(world:has(e2, p2))
for _ in world:query(p2) do
count += 1
end
CHECK(count == 1)
CHECK(world:each(p2)() == e2) -- Fails
end)
TEST("repro", function()
FOCUS()
TEST("", function()
local world = jecs.world()
local data = world:component()
local relation = world:component()
@ -175,9 +38,9 @@ TEST("repro", function()
world:delete(e1)
local e1v1 = world:entity()
CHECK(ECS_ID(e1v1) == e1::any)
CHECK(ECS_ID(e1v1) == e1)
local e2v1 = world:entity()
CHECK(ECS_ID(e2v1) == e2::any)
CHECK(ECS_ID(e2v1) == e2)
world:set(e2v1, data, 456)
CHECK(world:contains(e1v1))
@ -190,7 +53,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,7 +60,6 @@ TEST("repro", function()
count += 1
end
print(count)
CHECK(count==1)
end)
TEST("bulk", function()
@ -341,17 +202,28 @@ TEST("world:add()", function()
local B = world:component()
local C = world:component()
local e_ptr: jecs.Entity = (jecs.Rest :: any) + 1
local e_ptr = jecs.Rest :: number + 1
world:add(A, jecs.Exclusive)
local on_remove_call = false
world:set(A, jecs.OnRemove, function(e, id)
CHECK(e == e_ptr)
CHECK(id == jecs.pair(A, B))
on_remove_call = true
end)
local on_add_call_count = 0
world:set(A, jecs.OnAdd, function(e, id)
on_add_call_count += 1
if on_add_call_count == 1 then
CHECK(e == e_ptr)
CHECK(id == jecs.pair(A, B))
elseif on_add_call_count == 2 then
CHECK(e == e_ptr)
CHECK(id == jecs.pair(A, C))
else
CHECK(false)
end
end)
@ -366,17 +238,6 @@ TEST("world:add()", function()
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 "idempotent"
@ -634,9 +495,9 @@ TEST("world:delete()", function()
world:add(e1, ct)
world:add(e2, jecs.pair(ct, dummy))
-- world:delete(dummy)
world:delete(dummy)
-- CHECK(world:contains(e2))
CHECK(world:contains(e2))
world:delete(ct)
@ -1062,81 +923,6 @@ TEST("world:each()", function()
end
end)
TEST("world:added", function()
local world = jecs.world()
do CASE "Should work even if set after the component has been used"
local A = world:component()
world:set(world:entity(), A, 2)
local ran = false
world:added(A, function()
ran = true
end)
local entity = world:entity()
world:set(entity, A, 3)
CHECK(ran)
end
do CASE "Should work even if set after the pair has been used"
local A = world:component()
local B = world:component()
world:set(world:entity(), A, 2)
world:set(world:entity(), pair(A, B), 2)
world:added(A, function()
ran = true
end)
local entity = world:entity()
world:set(entity, pair(A, B), 3)
CHECK(ran)
end
do CASE "Should allow setting signal after Relation has been used as a component"
local A = world:component()
local B = world:component()
world:add(world:entity(), A)
world:added(A, function()
ran = true
end)
world:add(world:entity(), pair(A, B))
CHECK(ran)
end
do CASE "Should invoke signal for the Relation being set as a key despite a pair with Relation having been cached"
local A = world:component()
local B = world:component()
world:add(world:entity(), pair(A, B))
world:added(A, function()
ran = true
end)
world:add(world:entity(), A)
CHECK(ran)
end
do CASE "Should not override hook"
local A = world:component()
local count = 1
local function counter()
count += 1
end
world:set(A, jecs.OnAdd, counter)
world:added(A, counter)
world:set(world:entity(), A, false)
CHECK(count == (1 + 2))
world:set(world:entity(), A, false)
CHECK(count == (1 + (2 * 2)))
end
end)
TEST("world:range()", function()
do CASE "spawn entity under min range"
@ -1202,7 +988,7 @@ TEST("world:range()", function()
client:range(1000, 5000)
local e1 = server:entity()
CHECK((e1::any)< 1000)
CHECK((e1::number)< 1000)
server:delete(e1)
local e2 = client:entity(e1)
CHECK(e2 == e1)
@ -1212,12 +998,12 @@ TEST("world:range()", function()
client:delete(e2)
local e3 = client:entity()
CHECK(ECS_ID(e3) == 1000)
CHECK(ECS_ID(e3::number) == 1000)
local e1v1 = server:entity()
local e4 = client:entity(e1v1)
CHECK(ECS_ID(e4) == e1::any)
CHECK(ECS_GENERATION(e4) == 1)
CHECK(ECS_ID(e4::number) == e1)
CHECK(ECS_GENERATION(e4::number) == 1)
CHECK(not client:contains(e2))
CHECK(client:contains(e4))
end
@ -1226,16 +1012,15 @@ TEST("world:range()", function()
local world = jecs.world()
world:range(400, 1000)
local id = world:entity()
local e = world:entity(id::any + 5)
CHECK(e::any == (id::any) + 5)
local id = world:entity() :: number
local e = world:entity(id + 5)
CHECK(e == id + 5)
CHECK(world:contains(e))
local e2 = world:entity(399)
CHECK(world:contains(e2))
world:delete(e2)
CHECK(not world:contains(e2))
local e2v1 = world:entity(399)
local e2v1 = world:entity(399) :: number
CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 399)
CHECK(ECS_GENERATION(e2v1) == 0)
@ -1248,13 +1033,13 @@ TEST("world:range()", function()
CHECK(world:contains(e2))
world:delete(e2)
CHECK(not world:contains(e2))
local e2v1 = world:entity(405)
local e2v1 = world:entity(405) :: number
CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 405)
CHECK(ECS_GENERATION(e2v1) == 0)
world:delete(e2v1)
local e2v2 = world:entity(e2v1)
local e2v2 = world:entity(e2v1) :: number
CHECK(ECS_ID(e2v2) == 405)
CHECK(ECS_GENERATION(e2v2) == 0)
end
@ -1263,10 +1048,9 @@ end)
TEST("world:entity()", function()
do CASE "desired id"
local world = jecs.world()
local id = world:entity()
local offset: jecs.Entity = (id ::any) + 5
local e = world:entity(offset)
CHECK(e == offset)
local id = world:entity() :: number
local e = world:entity(id + 5)
CHECK(e == id + 5)
CHECK(world:contains(e))
local e2 = world:entity(399)
CHECK(world:contains(e2))
@ -1284,7 +1068,7 @@ TEST("world:entity()", function()
end
do CASE "generations"
local world = jecs.world()
local e = world:entity()
local e = world:entity() :: any
CHECK(ECS_ID(e) == 1 + jecs.Rest :: any)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
@ -1334,11 +1118,11 @@ TEST("world:entity()", function()
local e = world:entity()
world:delete(e)
end
local e = world:entity()
local e = world:entity() :: number
CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 2^16-1)
world:delete(e)
e = world:entity()
e = world:entity() :: number
CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 0)
end
@ -1613,7 +1397,6 @@ TEST("world:query()", function()
for i = 1, 9 do
local id = world:component()
world:component() -- make the components sparsely interleaved
components[i] = id
end
local e1 = world:entity()
@ -1857,7 +1640,7 @@ TEST("world:query()", function()
world:add(e2, B)
local count = 0
for id in world:query(A) do
for id in world:query(A) :: any do
world:clear(id)
count += 1
end
@ -2297,14 +2080,14 @@ TEST("change tracking", function()
world:set(e2, Foo, 2)
local i = 0
for e, new in q1 do
for e, new in q1 :: any do
i += 1
world:set(e, pair(Previous, Foo), new)
end
CHECK(i == 2)
local j = 0
for e, new in q1 do
for e, new in q1 :: any do
j += 1
world:set(e, pair(Previous, Foo), new)
end
@ -2325,14 +2108,14 @@ TEST("change tracking", function()
world:set(testEntity, component, 10)
local i = 0
for entity, number in q1 do
for entity, number in q1 :: any do
i += 1
world:add(testEntity, tag)
end
CHECK(i == 1)
for e, n in q1 do
for e, n in q1 :: any do
world:set(e, pair(previous, component), n)
end
end

View file

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