diff --git a/jecs/.luaurc b/jecs/.luaurc index f856eba..5518c4c 100644 --- a/jecs/.luaurc +++ b/jecs/.luaurc @@ -4,7 +4,7 @@ "testkit": "tools/testkit", "mirror": "mirror", "tools": "tools", - "addons": "addons" + "addons": "addons", }, "languageMode": "strict" } diff --git a/jecs/CHANGELOG.md b/jecs/CHANGELOG.md index 8fdd827..19a8ef1 100644 --- a/jecs/CHANGELOG.md +++ b/jecs/CHANGELOG.md @@ -23,8 +23,6 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - This should allow a more lenient window for modifying data - Changed `OnRemove` to lazily lookup which archetype the entity will move to - Can now have interior structural changes within `OnRemove` hooks - - Optimized `world:has` for both single component and multiple component presence. - - This comes at the cost that it cannot check the component presence for more than 4 components at a time. If this is important, consider calling to this function multiple times. ## [0.5.0] - 2024-12-26 diff --git a/jecs/addons/observers.luau b/jecs/addons/observers.luau index cffce81..a11f67c 100644 --- a/jecs/addons/observers.luau +++ b/jecs/addons/observers.luau @@ -1,32 +1,20 @@ local jecs = require("@jecs") - -type Observer = { - callback: (jecs.Entity) -> (), - query: jecs.Query, -} - -export type PatchedWorld = jecs.World & { - added: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: any) -> ()) -> (), - removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), - changed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), - observer: (PatchedWorld, Observer) -> (), - monitor: (PatchedWorld, Observer) -> (), -} +local testkit = require("@testkit") local function observers_new(world, description) local query = description.query local callback = description.callback - local terms = query.filter_with :: { jecs.Id } + local terms = query.filter_with 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 entity_index = world.entity_index + local function emplaced(entity) local r = jecs.entity_index_try_get_fast( - entity_index, entity :: any) + entity_index, entity) if not r then return @@ -45,20 +33,18 @@ local function observers_new(world, description) 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 function world_track(world, ...) + local entity_index = world.entity_index + local terms = { ... } + local q_shim = { filter_with = terms } - local entity_index = world.entity_index :: any - local function emplaced(entity: jecs.Entity) + local n = 0 + local dense_array = {} + local sparse_array = {} + + local function emplaced(entity) local r = jecs.entity_index_try_get_fast( - entity_index, entity :: any) + entity_index, entity) if not r then return @@ -66,39 +52,55 @@ local function monitors_new(world, description) local archetype = r.archetype - if jecs.query_match(query, archetype) then - callback(entity, jecs.OnAdd) + if jecs.query_match(q_shim :: any, archetype) then + n += 1 + dense_array[n] = entity + sparse_array[entity] = n 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 + local function removed(entity) + local i = sparse_array[entity] + if i ~= n then + dense_array[i] = dense_array[n] end - local archetype = r.archetype - - if jecs.query_match(query, archetype) then - callback(entity, jecs.OnRemove) - end + dense_array[n] = nil end for _, term in terms do world:added(term, emplaced) - world:removed(term, removed) + world:changed(term, emplaced) end + + local function iter() + local i = n + return function() + local row = i + if row == 0 then + return nil + end + i -= 1 + return dense_array[row] :: any + end + end + + local it = { + __iter = iter, + without = function(self, ...) + q_shim.filter_without = { ... } + return self + end + } + return setmetatable(it, it) end -local function observers_add(world: jecs.World & { [string]: any }): PatchedWorld +local function observers_add(world) local signals = { added = {}, emplaced = {}, removed = {} } - world.added = function(_, component, fn) local listeners = signals.added[component] if not listeners then @@ -107,7 +109,7 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl local idr = jecs.id_record_ensure(world, component) idr.hooks.on_add = function(entity) for _, listener in listeners do - listener(entity, component) + listener(entity) end end end @@ -122,7 +124,7 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl local idr = jecs.id_record_ensure(world, component) idr.hooks.on_change = function(entity, value) for _, listener in listeners do - listener(entity, component, value) + listener(entity, value) end end end @@ -137,7 +139,7 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl local idr = jecs.id_record_ensure(world, component) idr.hooks.on_remove = function(entity) for _, listener in listeners do - listener(entity, component) + listener(entity) end end end @@ -146,10 +148,9 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl world.signals = signals + world.track = world_track + world.observer = observers_new - - world.monitor = monitors_new - return world end diff --git a/jecs/build.txt b/jecs/build.txt index 15b7408..f37d24a 100644 --- a/jecs/build.txt +++ b/jecs/build.txt @@ -1,2 +1,2 @@ modified = ["addons/observers.luau", ".luaurc", "CHANGELOG.md", "jecs.luau"] -version = "0.5.5-nightly.20250413T001101Z" +version = "0.5.5-nightly.20250412T181729Z" diff --git a/jecs/jecs.luau b/jecs/jecs.luau index d4da4ed..cc342ce 100644 --- a/jecs/jecs.luau +++ b/jecs/jecs.luau @@ -466,9 +466,7 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b return records[id] ~= nil end -local function world_has(world: ecs_world_t, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean - +local function world_has(world: ecs_world_t, entity: i53, ...: i53): boolean local record = entity_index_try_get_fast(world.entity_index, entity) if not record then return false @@ -481,11 +479,13 @@ local function world_has(world: ecs_world_t, entity: i53, local records = archetype.records - return records[a] ~= nil and - (b == nil or records[b] ~= nil) and - (c == nil or records[c] ~= nil) and - (d == nil or records[d] ~= nil) and - (e == nil or error("args exceeded")) + for i = 1, select("#", ...) do + if not records[select(i, ...)] then + return false + end + end + + return true end local function world_target(world: ecs_world_t, entity: i53, relation: i24, index: number?): i24? @@ -2420,10 +2420,7 @@ export type World = { & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), --- Returns whether the entity has the ID. - has: ((World, Entity, A) -> boolean) - & ((World, Entity, A, B) -> boolean) - & ((World, Entity, A, B, C) -> boolean) - & (World, Entity, A, B, C, D) -> boolean, + has: (self: World, entity: Entity, ...Id) -> boolean, --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. parent:(self: World, entity: Entity) -> Entity, @@ -2490,9 +2487,9 @@ return { ECS_ID_DELETE = ECS_ID_DELETE, - IS_PAIR = (ECS_IS_PAIR :: any) :: (pair: Pair) -> boolean, - pair_first = (ecs_pair_first :: any) :: (world: World, pair: Pair) -> Id

, - pair_second = (ecs_pair_second :: any) :: (world: World, pair: Pair) -> Id, + IS_PAIR = ECS_IS_PAIR, + pair_first = ecs_pair_first, + pair_second = ecs_pair_second, entity_index_get_alive = entity_index_get_alive, archetype_append_to_records = archetype_append_to_records, diff --git a/jecs/pesde-rbx.toml b/jecs/pesde-rbx.toml index f93026b..f1afa27 100644 --- a/jecs/pesde-rbx.toml +++ b/jecs/pesde-rbx.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/marked/jecs-nightly" -version = "0.5.5-nightly.20250413T001101Z" +version = "0.5.5-nightly.20250412T181729Z" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/jecs/pesde.toml b/jecs/pesde.toml index 6b1846b..2d4fd9a 100644 --- a/jecs/pesde.toml +++ b/jecs/pesde.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/marked/jecs-nightly" -version = "0.5.5-nightly.20250413T001101Z" +version = "0.5.5-nightly.20250412T181729Z" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/jecs/test.txt b/jecs/test.txt index 60eabdb..6b18d81 100644 --- a/jecs/test.txt +++ b/jecs/test.txt @@ -1,2 +1,2 @@ passed = true -timestamp = "20250413T001103Z" +timestamp = "20250412T181730Z" diff --git a/jecs/test_fulllog.txt b/jecs/test_fulllog.txt index 9a47c03..f588cc4 100644 --- a/jecs/test_fulllog.txt +++ b/jecs/test_fulllog.txt @@ -1,39 +1,77 @@ +*created e271v0 +*created e272v0 +*created e273v0 +*created e274v0 +*created e275v0 +*created e276v0 +*created e277v0 +*created e278v0 +*created e279v0 +*created e280v0 +|-alive--| +| e271v0 | +|--------| +| e272v0 | +|--------| +| e273v0 | +|--------| +| e274v0 | +|--------| +| e275v0 | +|--------| +| e276v0 | +|--------| +| e277v0 | +|--------| +| e278v0 | +|--------| +| e279v0 | +|--------| +| e280v0 | +|--------| + + +*deleted e270v0 +*deleted e271v0 +*deleted e272v0 +*deleted e273v0 +*deleted e274v0 +*deleted e275v0 +*deleted e274v1 +*deleted e273v1 +*deleted e272v1 +*deleted e271v1 +*deleted e270v1 ----idempotent 1_2 -7.4 us  2 kB│ delete children of entity -9.5 us  1 kB│ remove friends of entity -352 ns  0  B│ simple deletion of entity -world:add() -PASS│ idempotent -PASS│ archetype move - -world:children() +1_2 +7.1 us  1 kB│ delete children of entity +9.2 us  1 kB│ remove friends of entity +346 ns  0  B│ simple deletion of entity +the great reset PASS│  -world:clear() -PASS│ should remove its components -PASS│ remove cleared ID from entities - -world:component() -PASS│ only components should have EcsComponent trait -PASS│ tag - -world:contains() +#repro3 +PASS│ should add the correct ModelBase for parts +PASS│ should add the correct ModelBase for parts PASS│  -PASS│ should not exist after delete -world:delete() -PASS│ invoke OnRemove hooks -PASS│ delete recycled entity id used as component -PASS│ bug: Empty entity does not respect cleanup policy -PASS│ should allow deleting components -PASS│ delete entities using another Entity as component with Delete cleanup action -PASS│ delete children -PASS│ remove deleted ID from entities -PASS│ fast delete -PASS│ cycle +#adding a recycled target +PASS│  -world:each() +#repro2 +PASS│  + +another +PASS│  + +#repro +PASS│  + +archetype +PASS│  + +world:cleanup() PASS│  world:entity() @@ -43,9 +81,16 @@ PASS│ Recycling PASS│ Recycling max generation -world:has() -PASS│ should find Tag on entity -PASS│ should return false when missing one tag +world:set() +PASS│ archetype move +PASS│ pairs + +world:remove() +PASS│ should allow remove a component that doesn't exist on entity + +world:add() +PASS│ idempotent +PASS│ archetype move world:query() PASS│ cached @@ -65,32 +110,49 @@ PASS│ should error when setting invalid pair PASS│ should find target for ChildOf PASS│ despawning while iterating +NONE│ iterator invalidation +SKIP│ adding +PASS│ spawning PASS│ should not find any entities -PASS│ world:query():without() +PASS│ without -world:remove() -PASS│ should allow remove a component that doesn't exist on entity +world:each +PASS│  -world:set() -PASS│ archetype move -PASS│ pairs +world:children +PASS│  + +world:clear() +PASS│ should remove its components +PASS│ remove cleared ID from entities + +world:has() +PASS│ should find Tag on entity +PASS│ should return false when missing one tag + +world:component() +PASS│ only components should have EcsComponent trait +PASS│ tag + +world:delete +PASS│ invoke OnRemove hooks +PASS│ delete recycled entity id used as component +PASS│ bug: Empty entity does not respect cleanup policy +PASS│ should allow deleting components +PASS│ delete entities using another Entity as component with Delete cleanup action +PASS│ delete children +PASS│ remove deleted ID from entities +PASS│ fast delete +PASS│ cycle world:target PASS│ nth index PASS│ infer index when unspecified PASS│ loop until no target -#adding a recycled target -PASS│  - -#repro2 -PASS│  - -another -PASS│  - -#repro +world:contains PASS│  +PASS│ should not exist after delete Hooks PASS│ OnAdd @@ -115,5 +177,5 @@ PASS│ #2 PASS│ #3 -68/68 test cases passed in 29.621 ms. +77/77 test cases passed in 29.411 ms. 0 fails diff --git a/jecs/wally.toml b/jecs/wally.toml index 982c9a3..544c077 100644 --- a/jecs/wally.toml +++ b/jecs/wally.toml @@ -5,4 +5,4 @@ license = "MIT" name = "mark-marks/jecs-nightly" realm = "shared" registry = "https://github.com/UpliftGames/wally-index" -version = "0.5.5-nightly.20250413T001101Z" +version = "0.5.5-nightly.20250412T181729Z"