Bump versions (#259)

This commit is contained in:
Marcus 2025-07-27 14:39:43 +02:00 committed by GitHub
parent 998b1d3528
commit b521fe750a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 396 additions and 586 deletions

211
addons/ob.luau Executable file
View file

@ -0,0 +1,211 @@
--!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
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, 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
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
}

View file

@ -1,338 +0,0 @@
--!strict
local jecs = require("@jecs")
export type Iter<T...> = (query: Query<T...>) -> () -> (jecs.Entity, T...)
type Id<T=any> = jecs.Id<T>
type Query<T...> = typeof(setmetatable(
{} :: {
iter: Iter<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...>)
& (<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...>),
archetypes: (self: Query<T...>) -> { jecs.Archetype },
cached: (self: Query<T...>) -> Query<T...>,
},
{} :: {
__iter: Iter<T...>,
}
))
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, Query<T...>, callback: ((jecs.Entity, jecs.Id, any?) -> ())?) -> () -> () -> (jecs.Entity),
monitor: <T...>(PatchedWorld, Query<T...>, callback: ((jecs.Entity, jecs.Id, any?) -> ())?) -> () -> () -> (jecs.Entity)
}
local function observers_new<T...>(
world: PatchedWorld,
query: Query<T...>,
callback: (<T, a>(jecs.Entity<T>, jecs.Id<a>, value: a?) -> ())?
)
query = query:cached()
callback = callback
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 = 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
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,190 +0,0 @@
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

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

View file

@ -15,9 +15,6 @@ hero:
- theme: alt - theme: alt
text: API References text: API References
link: /api/jecs.md link: /api/jecs.md
- theme: alt
text: Observers
link: /api/observers.md
features: features:
- title: Stupidly Fast - title: Stupidly Fast

View file

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

2
jecs.d.ts vendored
View file

@ -49,7 +49,7 @@ export type Archetype<T extends unknown[]> = {
type: string; type: string;
entities: number[]; entities: number[];
columns: Column<unknown>[]; columns: Column<unknown>[];
columns_map: { [K in keyof T]: Column<T[K]> } columns_map: Record<Id, Column<T[number]>>
}; };
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>; type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;

141
jecs.luau
View file

@ -53,6 +53,8 @@ export type Query<T...> = typeof(setmetatable(
& (<a, b, c, d>(Query<T...>, Id<a>, Id<b>, Id<c>, Id) -> Query<T...>), & (<a, b, c, d>(Query<T...>, Id<a>, Id<b>, Id<c>, Id) -> Query<T...>),
archetypes: (self: Query<T...>) -> { Archetype }, archetypes: (self: Query<T...>) -> { Archetype },
cached: (self: Query<T...>) -> Query<T...>, cached: (self: Query<T...>) -> Query<T...>,
ids: { Id<any> },
-- world: World
}, },
{} :: { {} :: {
__iter: Iter<T...>, __iter: Iter<T...>,
@ -66,7 +68,6 @@ export type Observer = {
query: QueryInner, query: QueryInner,
} }
type archetype = { type archetype = {
id: number, id: number,
types: { i53 }, types: { i53 },
@ -129,7 +130,11 @@ type world = {
exists: (self: world, entity: i53) -> boolean, exists: (self: world, entity: i53) -> boolean,
each: (self: world, id: i53) -> () -> i53, each: (self: world, id: i53) -> () -> i53,
children: (self: world, id: i53) -> () -> i53, children: (self: world, id: i53) -> () -> i53,
query: (world, ...i53) -> Query<...any> query: (world, ...i53) -> Query<...any>,
added: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (),
changed: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (),
removed: (world, i53, (e: i53, id: i53) -> ()) -> () -> (),
} }
export type World = { export type World = {
@ -145,6 +150,10 @@ export type World = {
observable: Map<Id, Map<Id, { Observer }>>, observable: Map<Id, Map<Id, { Observer }>>,
added: <T>(World, Id<T>, <e>(e: Entity<e>, id: Id<T>, value: T?) -> ()) -> () -> (),
removed: <T>(World, Id<T>, (e: Entity, id: Id<T>) -> ()) -> () -> (),
changed: <T>(World, Id<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (),
--- Enforce a check on entities to be created within desired range --- Enforce a check on entities to be created within desired range
range: (self: World, range_begin: number, range_end: number?) -> (), range: (self: World, range_begin: number, range_end: number?) -> (),
@ -2213,6 +2222,14 @@ local function world_new()
local observable = {} local observable = {}
type Signal = { [i53]: { Listener<any> } }
local signals = {
added = {} :: Signal,
changed = {} :: Signal,
removed = {} :: Signal
}
local world = { local world = {
archetype_edges = archetype_edges, archetype_edges = archetype_edges,
@ -2226,9 +2243,11 @@ local function world_new()
max_component_id = ecs_max_component_id, max_component_id = ecs_max_component_id,
observable = observable, observable = observable,
signals = signals,
} :: world } :: world
local ROOT_ARCHETYPE = archetype_create(world, {}, "") local ROOT_ARCHETYPE = archetype_create(world, {}, "")
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
@ -2578,6 +2597,108 @@ local function world_new()
end end
end end
type Listener<T> = (e: i53, id: i53, value: T?) -> ()
world.added = function<T>(_: world, component: i53, fn: Listener<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 :: { Listener<T> } do
listener(entity, id, value)
end
end
local existing_hook = inner_world_get(world, component, EcsOnAdd) :: Listener<T>
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
inner_world_set(world, component, EcsOnAdd, 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.changed = function<T>(
_: world,
component: i53,
fn: Listener<T>
)
local listeners = signals.changed[component]
if not listeners then
listeners = {}
signals.changed[component] = listeners
local function on_change(entity, id, value: any)
for _, listener in listeners :: { Listener<T> } do
listener(entity, id, value)
end
end
local existing_hook = inner_world_get(world, component, EcsOnChange) :: Listener<T>
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
inner_world_set(world, component, EcsOnChange, 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.removed = function<T>(_: world, component: i53, fn: (i53, i53) -> ())
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity, id)
for _, listener in listeners :: { Listener<T> } do
listener(entity, id)
end
end
local existing_hook = inner_world_get(world, component, EcsOnRemove) :: Listener<T>
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
inner_world_set(world, component, EcsOnRemove, 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
local function inner_world_has(world: World, entity: i53, local function inner_world_has(world: World, entity: i53,
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean
@ -3159,21 +3280,21 @@ return {
meta = (ECS_META :: any) :: <T, a>(id: Entity<T>, id: Id<a>, value: a?) -> Entity<T>, meta = (ECS_META :: any) :: <T, a>(id: Entity<T>, id: Id<a>, value: a?) -> Entity<T>,
is_tag = (ecs_is_tag :: any) :: <T>(World, Id<T>) -> boolean, is_tag = (ecs_is_tag :: any) :: <T>(World, Id<T>) -> boolean,
OnAdd = (EcsOnAdd :: any) :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>, OnAdd = (EcsOnAdd :: any) :: Id<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
OnRemove = (EcsOnRemove :: any) :: Entity<(entity: Entity, id: Id) -> ()>, OnRemove = (EcsOnRemove :: any) :: Id<(entity: Entity, id: Id) -> ()>,
OnChange = (EcsOnChange :: any) :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>, OnChange = (EcsOnChange :: any) :: Id<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
ChildOf = (EcsChildOf :: any) :: Entity, ChildOf = (EcsChildOf :: any) :: Entity,
Component = (EcsComponent :: any) :: Entity, Component = (EcsComponent :: any) :: Entity,
Wildcard = (EcsWildcard :: any) :: Entity, Wildcard = (EcsWildcard :: any) :: Id,
w = (EcsWildcard :: any) :: Entity, w = (EcsWildcard :: any) :: Id,
OnDelete = (EcsOnDelete :: any) :: Entity, OnDelete = (EcsOnDelete :: any) :: Entity,
OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity, OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity,
Delete = (EcsDelete :: any) :: Entity, Delete = (EcsDelete :: any) :: Entity,
Remove = (EcsRemove :: any) :: Entity, Remove = (EcsRemove :: any) :: Entity,
Name = (EcsName :: any) :: Entity<string>, Name = (EcsName :: any) :: Id<string>,
Exclusive = (EcsExclusive :: any) :: Entity, Exclusive = (EcsExclusive :: any) :: Entity,
ArchetypeCreate = EcsOnArchetypeCreate, ArchetypeCreate = (EcsOnArchetypeCreate :: any) :: Entity,
ArchetypeDelete = EcsOnArchetypeDelete, ArchetypeDelete = (EcsOnArchetypeDelete :: any) :: Entity,
Rest = (EcsRest :: any) :: Entity, Rest = (EcsRest :: any) :: Entity,
pair = ECS_PAIR :: <P, O>(first: Id<P>, second: Id<O>) -> Pair<P, O>, pair = ECS_PAIR :: <P, O>(first: Id<P>, second: Id<O>) -> Pair<P, O>,

View file

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

View file

@ -2,42 +2,10 @@ local jecs = require("@jecs")
local testkit = require("@testkit") local testkit = require("@testkit")
local test = testkit.test() local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local observers_add = require("@addons/observers") local ob = require("@addons/ob")
TEST("addons/observers", function() TEST("addons/observers", function()
local world = observers_add(jecs.world()) 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 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" do CASE "Ensure ordering between signals and observers"
local A = world:component() local A = world:component()
@ -48,11 +16,15 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
world:observer(world:query(A, B), counter) ob.observer(world:query(A, B), counter)
world:added(A, counter) world:added(A, counter)
world:added(A, counter) world:added(A, counter)
for _ in world:query(A) do
end
local e = world:entity() local e = world:entity()
world:add(e, A) world:add(e, A)
CHECK(count == 3) CHECK(count == 3)
@ -69,7 +41,7 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
world:observer(world:query(A), counter) ob.observer(world:query(A), counter)
local e = world:entity() local e = world:entity()
world:set(e, A, false) world:set(e, A, false)
@ -89,7 +61,7 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
world:monitor(world:query(A), counter) ob.monitor(world:query(A), counter)
local e = world:entity() local e = world:entity()
world:set(e, A, false) world:set(e, A, false)

View file

@ -1062,6 +1062,44 @@ TEST("world:each()", function()
end end
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 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() TEST("world:range()", function()
do CASE "spawn entity under min range" do CASE "spawn entity under min range"

View file

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