From ef4d880b0aab7cc9043fe73f981f479b9eb5139b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 28 Dec 2025 11:07:51 +0100 Subject: [PATCH 1/8] Add DEBUG mode with special assertions --- rokit.toml | 4 +- src/jecs.luau | 1178 +++++++++++++++++--------- test/benches/default.project.json | 8 +- test/benches/query.luau | 87 +- test/benches/visual/query.bench.luau | 81 +- test/tests.luau | 149 +--- 6 files changed, 853 insertions(+), 654 deletions(-) diff --git a/rokit.toml b/rokit.toml index 907eb03..5f95fc2 100755 --- a/rokit.toml +++ b/rokit.toml @@ -1,4 +1,4 @@ [tools] wally = "upliftgames/wally@0.3.2" -rojo = "rojo-rbx/rojo@7.4.4" -luau = "luau-lang/luau@0.701" +rojo = "rojo-rbx/rojo@7.7.0-rc.1" +luau = "luau-lang/luau@0.703.0" diff --git a/src/jecs.luau b/src/jecs.luau index fef3e92..6d0ae17 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -378,10 +378,11 @@ local EcsRest = HI_COMPONENT_ID + 15 local NULL_ARRAY = table.freeze({}) :: Column local NULL = newproxy(false) -local ECS_INTERNAL_ERROR = [[ - This is an internal error, please file a bug report via the following link: - - https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md +local ECS_INTERNAL_ERROR_INVALID_ENTITIES = [[ + You tried passing a pair that has invalid entities that are either unalive + or non-existing entities. You can enable DEBUG mode by passing in true to + jecs.world(true) and try doing it again in order to get better assertions so + that you can understand what went wrong. ]] local function ecs_assert(condition, msg: string?) @@ -1312,354 +1313,503 @@ end local function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next - local compatible_archetypes = query_archetypes(query::any) :: { Archetype } - local lastArchetype = 1 - local archetype = compatible_archetypes[1] - if not archetype then + local compatible_archetypes_u = query_archetypes(query::any) :: { Archetype } + local last_archetype_u = 1 + local archetype_u = compatible_archetypes_u[1] + if not archetype_u then return NOOP :: () -> (number, ...any) end - local entities = archetype.entities - local i = #entities - local columns_map = archetype.columns_map + local entities_u = archetype_u.entities + local i_u = #entities_u + local columns_map_u = archetype_u.columns_map - local ids = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids :: { Component }) - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column + local ids_u = query.ids + local id0, id1, id2, id3, id4, id5, id6, id7, id8 = unpack(ids_u :: { Component }) + local col0_u: Column, col1_u: Column, col2_u: Column, col3_u: Column + local col4_u: Column, col5_u: Column, col6_u: Column, col7_u: Column - if not A then + if not id0 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] + e = entities[i_u] + entities_u = entities end - i -= 1 - return entity + i_u -= 1 + return e end query.next = world_query_iter_next return world_query_iter_next - elseif not B then - a = columns_map[A] - elseif not C then - a = columns_map[A] - b = columns_map[B] - elseif not D then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - elseif not E then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - elseif not F then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - elseif not G then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - elseif not H then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] + elseif not id1 then + col0_u = columns_map_u[id0] + elseif not id2 then + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + elseif not id3 then + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + elseif not id4 then + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + elseif not id5 then + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + elseif not id6 then + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + col5_u = columns_map_u[id5] + elseif not id7 then + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + col5_u = columns_map_u[id5] + col6_u = columns_map_u[id6] else - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] - h = columns_map[H] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + col5_u = columns_map_u[id5] + col6_u = columns_map_u[id6] + col7_u = columns_map_u[id7] end - if not B then + if not id1 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col0_u = col0 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row] + return e, col0[row] end - elseif not C then + elseif not id2 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col0_u = col0 + col1_u = col1 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row] + return e, captured_a[row], captured_b[row] end - elseif not D then + elseif not id3 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col0_u = col0 + col1_u = col1 + col2_u = col2 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row] + return e, col0[row], col1[row], col2[row] end - elseif not E then + elseif not id4 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row] + return e, col0[row], col1[row], col2[row], col3[row] end - elseif not F then + elseif not id5 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row] end - elseif not G then + elseif not id6 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row], f[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row] end - elseif not H then + elseif not id7 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row] end - elseif not I then + elseif not id8 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + local col7 = col7_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] - h = columns_map[H] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col7 = columns_map[id7] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 + col7_u = col7 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row], captured_h[row] end else local output = {} local ids_len = #ids function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + local col7 = col7_u + local captured_ids = ids + local captured_columns_map = columns_map + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] + e = entities[i_u] + entities_u = entities columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] - h = columns_map[H] + columns_map = captured_columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col7 = columns_map[id7] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 + col7_u = col7 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 for i = 9, ids_len do output[i - 8] = columns_map[ids[i]::any][row] end - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output) + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output) end end @@ -1677,10 +1827,10 @@ end local function query_cached(query: QueryInner) local ids = query.ids - local lastArchetype = 1 + local last_archetype = 1 local A, B, C, D, E, F, G, H, I = unpack(ids :: { Component }) - if not A then + if not id0 then A = query.filter_with[1] end local a: Column, b: Column, c: Column, d: Column @@ -1753,43 +1903,43 @@ local function query_cached(query: QueryInner) table.insert(query_cache_on_delete, observer_for_delete) local function cached_query_iter() - lastArchetype = 1 - archetype = compatible_archetypes[lastArchetype] + last_archetype = 1 + archetype = compatible_archetypes[last_archetype] if not archetype then return NOOP end entities = archetype.entities i = #entities columns_map = archetype.columns_map - if not A then - elseif not B then + if not id0 then + elseif not id1 then a = columns_map[A] - elseif not C then + elseif not id2 then a = columns_map[A] b = columns_map[B] - elseif not D then + elseif not id3 then a = columns_map[A] b = columns_map[B] c = columns_map[C] - elseif not E then + elseif not id4 then a = columns_map[A] b = columns_map[B] c = columns_map[C] d = columns_map[D] - elseif not F then + elseif not id5 then a = columns_map[A] b = columns_map[B] c = columns_map[C] d = columns_map[D] e = columns_map[E] - elseif not G then + elseif not id6 then a = columns_map[A] b = columns_map[B] c = columns_map[C] d = columns_map[D] e = columns_map[E] f = columns_map[F] - elseif not H then + elseif not id7 then a = columns_map[A] b = columns_map[B] c = columns_map[C] @@ -1811,291 +1961,440 @@ local function query_cached(query: QueryInner) return world_query_iter_next end - if not A then + if not id0 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] + e = entities[i_u] + captured_entities = entities end - i -= 1 - return entity + i_u -= 1 + return e end - elseif not B then + elseif not id1 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col0_u = col0 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row] + return e, col0[row] end - elseif not C then + elseif not id2 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col0_u = col0 + col1_u = col1 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row] + return e, captured_a[row], captured_b[row] end - elseif not D then + elseif not id3 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col0_u = col0 + col1_u = col1 + col2_u = col2 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row] + return e, captured_a[row], captured_b[row], captured_c[row] end - elseif not E then + elseif not id4 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + captured_a = captured_columns_map[A] + captured_b = captured_columns_map[B] + captured_c = captured_columns_map[C] + captured_d = captured_columns_map[D] + a = captured_a + b = captured_b + c = captured_c + d = captured_d end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row] end - elseif not F then + elseif not id5 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row] end - elseif not G then + elseif not id6 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row], f[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row] end - elseif not H then + elseif not id7 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row] end - elseif not I then + elseif not id8 then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + local col7 = col7_u + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] - columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] - h = columns_map[H] + e = entities[i_u] + entities_u = entities + local columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col7 = columns_map[id7] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 + col7_u = col7 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row], captured_h[row] end else local output = {} local ids_len = #ids function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + local col7 = col7_u + local captured_ids = ids + local captured_columns_map = columns_map + + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + + if not archetype then return nil end - entities = archetype.entities - i = #entities - if i == 0 then + i_u = #entities + if i_u == 0 then continue end - entity = entities[i] + e = entities[i_u] + entities_u = entities columns_map = archetype.columns_map - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] - h = columns_map[H] + columns_map = captured_columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col7 = columns_map[id7] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 + col7_u = col7 end local row = i i -= 1 for i = 9, ids_len do - output[i - 8] = columns_map[ids[i]::any][row] + output[i - 8] = captured_columns_map[captured_ids[i]::any][row] end - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row], unpack(output) + return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row], captured_h[row], unpack(output) end end @@ -2358,7 +2657,10 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 }) end end -local function world_new() +type Context = { + debug: boolean? +} +local function world_new(DEBUG: boolean?) local eindex_dense_array = {} :: { i53 } local eindex_sparse_array = {} :: { record } @@ -2524,6 +2826,8 @@ local function world_new() -- end local function entity_index_try_get_unsafe(entity: i53): record? + local eindex_sparse_array = eindex_sparse_array + local eindex_dense_array = eindex_dense_array local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] if r then local r_dense = r.dense @@ -2560,6 +2864,7 @@ local function world_new() end local from: archetype = record.archetype + local ROOT_ARCHETYPE = ROOT_ARCHETYPE local src = from or ROOT_ARCHETYPE local column = src.columns_map[id] if column then @@ -2666,6 +2971,7 @@ local function world_new() end local from = record.archetype + local ROOT_ARCHETYPE = ROOT_ARCHETYPE local src = from or ROOT_ARCHETYPE if src.columns_map[id] then return @@ -3387,6 +3693,58 @@ local function world_new() world.children = world_children world.range = world_range + if DEBUG then + -- NOTE(marcus): Make it easy to grep the debug functions and + -- being able to read the specification, without having to look + -- at the implementation to understand invariants. + local function world_remove_checked(world: world, entity: i53, id: i53) + local entity_id = ECS_ID(entity) + local wrong_entity = eindex_sparse_array[entity_id] ~= entity_id + if wrong_entity then + error([[ + This Entity handle has an outdated generation. You are + probably holding onto an entity that you got from outside the ECS + ]], 2) + end + + if ECS_ID_IS_WILDCARD(id) then + error([[ + You tried to pass in a wildcard pair. This is strictly + forbidden. You probably want to iterate the targets and + remove them one by one. You can also populate a list of + targets to remove and use jecs.bulk_remove. + ]], 2) + end + + world_remove(world, entity, id) + end + local function world_add_checked(world: world, entity: i53, id: i53) + local entity_id = ECS_ID(entity) + local wrong_entity = eindex_sparse_array[entity_id] ~= entity_id + if wrong_entity then + error([[ + This Entity handle has an outdated generation. You are + probably holding onto an entity that you got from outside the ECS + ]], 2) + end + + world_add(world, entity, id) + end + local function world_set_checked(world: world, entity: i53, id: i53, value: any) + local entity_id = ECS_ID(entity) + local wrong_entity = eindex_sparse_array[entity_id] ~= entity_id + if wrong_entity then + error([[ + This Entity handle has an outdated generation. You are + probably holding onto an entity that you got from outside the ECS + ]], 2) + end + world_set(world, entity, id, value) + end + world.remove = world_remove_unchecked + world.add = world_add_checked + end + for i = 1, EcsRest do entity_index_new_id(entity_index) end diff --git a/test/benches/default.project.json b/test/benches/default.project.json index 3f11dca..b2519ce 100755 --- a/test/benches/default.project.json +++ b/test/benches/default.project.json @@ -12,16 +12,16 @@ "ReplicatedStorage": { "$className": "ReplicatedStorage", "Lib": { - "$path": "../src/jecs.luau" + "$path": "../../src/jecs.luau" }, "benches": { - "$path": "benches" + "$path": "visual" }, "mirror": { - "$path": "mirror.luau" + "$path": "../../src/mirror.luau" }, "DevPackages": { - "$path": "benches/visual/DevPackages" + "$path": "visual/DevPackages" } } } diff --git a/test/benches/query.luau b/test/benches/query.luau index 32560a9..1a74bdb 100755 --- a/test/benches/query.luau +++ b/test/benches/query.luau @@ -27,48 +27,11 @@ do G: jecs.Id, H: jecs.Id ) - BENCH("1 component", function() - for _ in world:query(A) do - end - end) - - BENCH("2 component", function() - for _ in world:query(B, A) do - end - end) - BENCH("4 component", function() for _ in world:query(D, C, B, A) do end end) - BENCH("8 component", function() - for _ in world:query(H, G, F, E, D, C, B, A) do - end - end) - - local e = world:entity() - world:set(e, A, true) - world:set(e, B, true) - world:set(e, C, true) - world:set(e, D, true) - world:set(e, E, true) - world:set(e, F, true) - world:set(e, G, true) - world:set(e, H, true) - - BENCH("Update Data", function() - for _ = 1, 100 do - world:set(e, A, false) - world:set(e, B, false) - world:set(e, C, false) - world:set(e, D, false) - world:set(e, E, false) - world:set(e, F, false) - world:set(e, G, false) - world:set(e, H, false) - end - end) end local D1 = ecs:component() @@ -86,8 +49,9 @@ do local added = 0 local archetypes = {} - for i = 1, 2 ^ 16 - 2 do + for i = 1, 2 ^ 12 - 2 do local entity = ecs:entity() + ecs:add(entity, entity) local combination = "" @@ -153,48 +117,10 @@ do G: jecs.Id, H: jecs.Id ) - BENCH("1 component", function() - for _ in world:query(A) do - end - end) - - BENCH("2 component", function() - for _ in world:query(B, A) do - end - end) - BENCH("4 component", function() for _ in world:query(D, C, B, A) do end end) - - BENCH("8 component", function() - for _ in world:query(H, G, F, E, D, C, B, A) do - end - end) - - local e = world:entity() - world:set(e, A, true) - world:set(e, B, true) - world:set(e, C, true) - world:set(e, D, true) - world:set(e, E, true) - world:set(e, F, true) - world:set(e, G, true) - world:set(e, H, true) - - BENCH("Update Data", function() - for _ = 1, 100 do - world:set(e, A, false) - world:set(e, B, false) - world:set(e, C, false) - world:set(e, D, false) - world:set(e, E, false) - world:set(e, F, false) - world:set(e, G, false) - world:set(e, H, false) - end - end) end local D1 = ecs:component() @@ -207,13 +133,14 @@ do local D8 = ecs:component() local function flip() - return math.random() >= 0.15 + return math.random() >= 0.5 end local added = 0 local archetypes = {} - for i = 1, 2 ^ 16 - 2 do + for i = 1, 2 ^ 12 - 2 do local entity = ecs:entity() + ecs:add(entity, entity) local combination = "" @@ -246,11 +173,9 @@ do ecs:set(entity, D8, { value = true }) end - if #combination == 7 then - added += 1 + if flip() then ecs:set(entity, D1, { value = true }) end - archetypes[combination] = true end local a = 0 diff --git a/test/benches/visual/query.bench.luau b/test/benches/visual/query.bench.luau index f1df0da..66b54ac 100755 --- a/test/benches/visual/query.bench.luau +++ b/test/benches/visual/query.bench.luau @@ -50,85 +50,54 @@ local E8 = mcs:component() local registry2 = ecr.registry() local function flip() - return math.random() >= 0.25 + return math.random() >= 0.5 end -local N = 2 ^ 16 - 2 +local N = 2 ^ 12- 2 local archetypes = {} local hm = 0 for i = 1, N do - local id = registry2.create() - local combination = "" - local n = newWorld:spawn() local entity = ecs:entity() local m = mcs:entity() + if flip() then + ecs:add(entity, entity) + mcs:add(m, m) + end if flip() then - registry2:set(id, B1, { value = true }) - ecs:set(entity, D1, { value = true }) - newWorld:insert(n, A1({ value = true })) mcs:set(m, E1, { value = 2 }) + ecs:set(entity, D1, {value = true}) end if flip() then - combination ..= "B" - registry2:set(id, B2, { value = true }) ecs:set(entity, D2, { value = true }) mcs:set(m, E2, { value = 2 }) - newWorld:insert(n, A2({ value = true })) end if flip() then - combination ..= "C" - registry2:set(id, B3, { value = true }) ecs:set(entity, D3, { value = true }) mcs:set(m, E3, { value = 2 }) - newWorld:insert(n, A3({ value = true })) end if flip() then - combination ..= "D" - registry2:set(id, B4, { value = true }) ecs:set(entity, D4, { value = true }) mcs:set(m, E4, { value = 2 }) - - newWorld:insert(n, A4({ value = true })) end if flip() then - combination ..= "E" - registry2:set(id, B5, { value = true }) ecs:set(entity, D5, { value = true }) mcs:set(m, E5, { value = 2 }) - - newWorld:insert(n, A5({ value = true })) end if flip() then - combination ..= "F" - registry2:set(id, B6, { value = true }) ecs:set(entity, D6, { value = true }) mcs:set(m, E6, { value = 2 }) - newWorld:insert(n, A6({ value = true })) end if flip() then - combination ..= "G" - registry2:set(id, B7, { value = true }) ecs:set(entity, D7, { value = true }) mcs:set(m, E7, { value = 2 }) - newWorld:insert(n, A7({ value = true })) end if flip() then - combination ..= "H" - registry2:set(id, B8, { value = true }) - newWorld:insert(n, A8({ value = true })) ecs:set(entity, D8, { value = true }) mcs:set(m, E8, { value = 2 }) end - if combination:find("BCDF") then - if not archetypes[combination] then - print(combination) - end - hm += 1 - end - archetypes[combination] = true end print("TEST", hm) @@ -140,30 +109,38 @@ end print(count) +local mq = mcs:query(E1, E2, E3, E4) +local jq = ecs:query(D1, D2, D3, D4) + return { ParameterGenerator = function() return end, Functions = { - Matter = function() - for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do - end - end, - - ECR = function() - for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do - end - end, - - -- Mirror = function() - -- for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do + -- Matter = function() + -- for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do -- end -- end, - Jecs = function() - for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do + -- ECR = function() + -- for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do + -- end + -- end, + + Mirror = function() + for i = 1, 10 do + for entityId, firstComponent in mq:iter() do + end end end, + + Jecs = function() + for i = 1, 10 do + for entityId, firstComponent in jq:iter() do + end + end + + end, }, } diff --git a/test/tests.luau b/test/tests.luau index 5133342..6e233f4 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -24,81 +24,7 @@ type Id = jecs.Id local entity_visualiser = require("@modules/entity_visualiser") local dwi = entity_visualiser.stringify -TEST("optimize idr_r removal", function() - - local pair = jecs.pair - local world = jecs.world() - local rel = world:component() - local A = world:component() - local B = world:component() - - local t1 = world:entity() - local t2 = world:entity() - - local entities = {} :: { jecs.Entity } - - for i = 1, 10 do - - local e1 = world:entity() - local e2 = world:entity() - - world:set(e1, A, true) - world:set(e2, A, true) - world:add(e1, pair(B, t1)) - world:add(e1, pair(B, t2)) - world:add(e2, pair(B, t1)) - world:add(e2, pair(B, t2)) - - table.insert(entities, e1) - table.insert(entities, e2) - end - - local e1 = world:entity() - local e2 = world:entity() - - table.insert(entities, e1) - table.insert(entities, e2) - - world:set(e1, A, true) - world:set(e2, A, true) - world:add(e1, pair(B, t1)) - world:add(e1, pair(B, t2)) - world:add(e2, pair(B, t1)) - world:add(e2, pair(B, t2)) - - BENCH("delete B", function() - world:delete(B) - end) - - for _, e in entities do - CHECK(world:has(e, A)) - CHECK(not world:target(e, B)) - CHECK(not world:target(e, B)) - end - -end) -TEST("deleting t1's archetype before invoking its onremove hooks", function() - local pair = jecs.pair - local world = jecs.world() - local rel = world:component() - - local t1 = world:entity() - local t2 = world:entity() - - --[[ - weirdly enough if i do this (only when adding childof relation after adding (rel, t2) to t1) it does not error. Probably a red herring - - world:add(t2, pair(rel, t1)) - world:add(t1, pair(rel, t2)) - world:add(t2, pair(jecs.ChildOf, t1)) - --]] - - -- this causes world:delete to error - world:add(t2, pair(jecs.ChildOf, t1)) - world:add(t1, pair(rel, t2)) - - world:delete(t1) -end) +FOCUS() TEST("reproduce idr_t nil archetype bug", function() local world = jecs.world() @@ -124,43 +50,56 @@ TEST("reproduce idr_t nil archetype bug", function() local dst = src and jecs.archetype_traverse_remove(world, id, src) end) - local batches = 10 - local batchSize = 20 + local batchSize = 200 local trackedEntities: { [number]: { parentId: number? } } = {} - for batch = 1, batches do - for i = 1, batchSize do - local root = world:entity() - world:add(root, jecs.pair(jecs.ChildOf, char)) + for i = 1, batchSize do - -- Removing animator from trackEntity1 causes it to stop happening - local trackEntity1 = world:entity() - world:set(trackEntity1, cts.Animator, 0) - world:add(trackEntity1, jecs.pair(jecs.ChildOf, root)) - trackedEntities[trackEntity1] = { parentId = root } + local root = world:entity() + -- world:add(root, jecs.pair(jecs.ChildOf, char)) - -- Removing animator from trackEntity2 causes it to happen less frequently - local trackEntity2 = world:entity() - world:set(trackEntity2, cts.Animator, 0) - world:add(trackEntity2, jecs.pair(jecs.ChildOf, root)) - trackedEntities[trackEntity2] = { parentId = root } + -- Removing animator from trackEntity1 causes it to stop happening + local trackEntity1 = world:entity() + world:set(trackEntity1, cts.Animator, 0) + world:add(trackEntity1, jecs.pair(jecs.ChildOf, root)) + world:set(trackEntity1, jecs.Name, "trackEntity1v"..i) + trackedEntities[trackEntity1] = { parentId = root } - -- Removing this, but keeping Animator on the other 2 causes it to stop happening - world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0) + -- Removing animator from trackEntity2 causes it to happen less frequently + local trackEntity2 = world:entity() + world:set(trackEntity2, cts.Animator, 0) + world:add(trackEntity2, jecs.pair(jecs.ChildOf, root)) + world:set(trackEntity2, jecs.Name, "trackEntity2v"..i) + trackedEntities[trackEntity2] = { parentId = root } - for entityId, info in trackedEntities do - if world:contains(entityId) and not world:parent(entityId :: any) then - print(`bugged entity found: {entityId}`) - print(`original parent: {info.parentId}`) - print(`batch = {batch}, i = {i}`) - print("==========================================") - trackedEntities[entityId] = nil - world:delete(entityId) - end - end - end - end + -- Removing this, but keeping Animator on the other 2 causes it to stop happening + world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0) + + local q = world:query(jecs.pair(jecs.ChildOf, jecs.w)):cached() + + + print("---- delete start root") + print("--- root", world:contains(root), root, jecs.ECS_ID(root), jecs.ECS_GENERATION(root)) + world:delete(root) + print("---- delete end root") + + for entity in q do + local parent = world:parent(entity) :: jecs.Entity + print("--- root", world:contains(root), jecs.ECS_ID(root), jecs.ECS_GENERATION(root)) + print(world:get(entity, jecs.Name), jecs.ECS_ID(parent), jecs.ECS_GENERATION(parent), "root ->", root) + CHECK(world:parent(entity) == nil) + end + for entityId, info in trackedEntities do + if world:contains(entityId) and not world:parent(entityId :: any) then + print(`bugged entity found: {entityId}`) + print(`original parent: {info.parentId}`) + print(`batch = {batch}, i = {i}`) + print("==========================================") + trackedEntities[entityId] = nil + end + end + end end) TEST("Ensure archetype edges get cleaned", function() From 5208aa7749b709a8372f5363a6b24ee43dbb439b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 28 Dec 2025 11:07:51 +0100 Subject: [PATCH 2/8] Fix tmp references --- how_to/110_hooks.luau | 5 + src/jecs.luau | 328 ++++++++++++++++++++++-------------------- test/tests.luau | 18 +-- 3 files changed, 181 insertions(+), 170 deletions(-) diff --git a/how_to/110_hooks.luau b/how_to/110_hooks.luau index 0957252..fc75dd6 100755 --- a/how_to/110_hooks.luau +++ b/how_to/110_hooks.luau @@ -37,4 +37,9 @@ end) When an entity graph contains cycles, order is undefined. This includes cycles that can be formed using different relationships. + + However an important note to make is that structural changes are not + necessarily always safe in OnRemove hooks. For instance, when an entity is + being deleted and invokes all of the OnRemove hooks on its components. It + can cause a lot of issues with moving entities ]] diff --git a/src/jecs.luau b/src/jecs.luau index 6d0ae17..8c5b6bd 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -1468,7 +1468,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row] + return e, col0[row], col1[row] end elseif not id3 then function world_query_iter_next(): any @@ -1594,7 +1594,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row] end elseif not id6 then function world_query_iter_next(): any @@ -1642,7 +1642,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row] end elseif not id7 then function world_query_iter_next(): any @@ -1693,7 +1693,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], captured_g[row] end elseif not id8 then function world_query_iter_next(): any @@ -1741,80 +1741,80 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) col4_u = col4 col5_u = col5 col6_u = col6 - col7_u = col7 - end - - local row = i_u - i_u -= 1 - - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row], captured_h[row] + col7_u = col7 end - else - local output = {} - local ids_len = #ids - function world_query_iter_next(): any - local entities = entities_u - local e = entities[i_u] - local col0 = col0_u - local col1 = col1_u - local col2 = col2_u - local col3 = col3_u - local col4 = col4_u - local col5 = col5_u - local col6 = col6_u - local col7 = col7_u - local captured_ids = ids - local captured_columns_map = columns_map - while e == nil do - last_archetype_u += 1 - local compatible_archetypes = compatible_archetypes_u - local archetype = compatible_archetypes[last_archetype_u] - archetype_u = archetype + local row = i_u + i_u -= 1 - if not archetype then - return nil - end - entities = archetype.entities - i_u = #entities - if i_u == 0 then - continue - end - e = entities[i_u] - entities_u = entities - columns_map = archetype.columns_map - columns_map = captured_columns_map - col0 = columns_map[id0] - col1 = columns_map[id1] - col2 = columns_map[id2] - col3 = columns_map[id3] - col4 = columns_map[id4] - col5 = columns_map[id5] - col6 = columns_map[id6] - col7 = columns_map[id7] - col0_u = col0 - col1_u = col1 - col2_u = col2 - col3_u = col3 - col4_u = col4 - col5_u = col5 - col6_u = col6 - col7_u = col7 - end - - local row = i_u - i_u -= 1 - - for i = 9, ids_len do - output[i - 8] = columns_map[ids[i]::any][row] - end - - return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output) - end + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row] end +else + local output = {} + local ids_len = #ids_u + function world_query_iter_next(): any + local entities = entities_u + local e = entities[i_u] + local col0 = col0_u + local col1 = col1_u + local col2 = col2_u + local col3 = col3_u + local col4 = col4_u + local col5 = col5_u + local col6 = col6_u + local col7 = col7_u + local ids = ids_u + local columns_map = columns_map_u - query.next = world_query_iter_next - return world_query_iter_next + while e == nil do + last_archetype_u += 1 + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype + + if not archetype then + return nil + end + entities = archetype.entities + i_u = #entities + if i_u == 0 then + continue + end + e = entities[i_u] + entities_u = entities + columns_map = archetype.columns_map + columns_map_u = columns_map + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col4 = columns_map[id4] + col5 = columns_map[id5] + col6 = columns_map[id6] + col7 = columns_map[id7] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 + col4_u = col4 + col5_u = col5 + col6_u = col6 + col7_u = col7 + end + + local row = i_u + i_u -= 1 + + for i = 9, ids_len do + output[i - 8] = columns_map[ids[i]::any][row] + end + + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output) + end +end + +query.next = world_query_iter_next +return world_query_iter_next end local function query_iter(query): () -> (number, ...any) @@ -1826,21 +1826,16 @@ local function query_iter(query): () -> (number, ...any) end local function query_cached(query: QueryInner) - local ids = query.ids - local last_archetype = 1 + local ids_u = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids :: { Component }) + local id0, id1, id2, id3, id4, id5, id6, id7, id8 = unpack(ids_u :: { Component }) if not id0 then - A = query.filter_with[1] + id0 = query.filter_with[1] end - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column + local col0_u: Column, col1_u: Column, col2_u: Column, col3_u: Column + local col4_u: Column, col5_u: Column, col6_u: Column, col7_u: Column local world_query_iter_next - local entities: { Entity } - local i: number - local archetype: Archetype - local columns_map: { [Component]: Column } local archetypes = query_archetypes(query :: any) :: { Archetype } local archetypes_map = {} query.archetypes_map = archetypes_map @@ -1849,7 +1844,21 @@ local function query_cached(query: QueryInner) archetypes_map[arche.id] = j end - local compatible_archetypes = archetypes :: { Archetype } + local compatible_archetypes_u = archetypes :: { Archetype } + local last_archetype_u = 1 + local archetype_u = compatible_archetypes_u[1] + local entities_u: { Entity } + local i_u: number + local columns_map_u: { [Component]: Column } + if not archetype_u then + entities_u = {} + i_u = 0 + columns_map_u = {} + else + entities_u = archetype_u.entities + i_u = #entities_u + columns_map_u = archetype_u.columns_map + end local world = (query :: { world: World }).world -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively @@ -1860,10 +1869,10 @@ local function query_cached(query: QueryInner) on_create_action = {} :: Map observable[EcsOnArchetypeCreate::any] = on_create_action end - local query_cache_on_create: { Observer } = on_create_action[A] + local query_cache_on_create: { Observer } = on_create_action[id0] if not query_cache_on_create then query_cache_on_create = {} - on_create_action[A] = query_cache_on_create + on_create_action[id0] = query_cache_on_create end local on_delete_action = observable[EcsOnArchetypeDelete::any] @@ -1871,10 +1880,10 @@ local function query_cached(query: QueryInner) on_delete_action = {} :: Map observable[EcsOnArchetypeDelete::any] = on_delete_action end - local query_cache_on_delete: { Observer } = on_delete_action[A] + local query_cache_on_delete: { Observer } = on_delete_action[id0] if not query_cache_on_delete then query_cache_on_delete = {} - on_delete_action[A] = query_cache_on_delete + on_delete_action[id0] = query_cache_on_delete end local function on_create_callback(archetype: Archetype) @@ -1902,60 +1911,61 @@ local function query_cached(query: QueryInner) table.insert(query_cache_on_create, observer_for_create) table.insert(query_cache_on_delete, observer_for_delete) - local function cached_query_iter() - last_archetype = 1 - archetype = compatible_archetypes[last_archetype] - if not archetype then + local function cached_query_iter() + last_archetype_u = 1 + local compatible_archetypes = compatible_archetypes_u + archetype_u = compatible_archetypes[last_archetype_u] + if not archetype_u then return NOOP end - entities = archetype.entities - i = #entities - columns_map = archetype.columns_map + entities_u = archetype_u.entities + i_u = #entities_u + columns_map_u = archetype_u.columns_map if not id0 then elseif not id1 then - a = columns_map[A] + col0_u = columns_map_u[id0] elseif not id2 then - a = columns_map[A] - b = columns_map[B] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] elseif not id3 then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] elseif not id4 then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] elseif not id5 then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] elseif not id6 then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + col5_u = columns_map_u[id5] elseif not id7 then - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + col5_u = columns_map_u[id5] + col6_u = columns_map_u[id6] else - a = columns_map[A] - b = columns_map[B] - c = columns_map[C] - d = columns_map[D] - e = columns_map[E] - f = columns_map[F] - g = columns_map[G] - h = columns_map[H] + col0_u = columns_map_u[id0] + col1_u = columns_map_u[id1] + col2_u = columns_map_u[id2] + col3_u = columns_map_u[id3] + col4_u = columns_map_u[id4] + col5_u = columns_map_u[id5] + col6_u = columns_map_u[id6] + col7_u = columns_map_u[id7] end return world_query_iter_next @@ -1981,7 +1991,7 @@ local function query_cached(query: QueryInner) continue end e = entities[i_u] - captured_entities = entities + entities_u = entities end i_u -= 1 return e @@ -2053,7 +2063,7 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row] + return e, col0[row], col1[row] end elseif not id3 then function world_query_iter_next(): any @@ -2092,7 +2102,7 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row] + return e, col0[row], col1[row], col2[row] end elseif not id4 then function world_query_iter_next(): any @@ -2121,20 +2131,20 @@ local function query_cached(query: QueryInner) entities_u = entities local columns_map = archetype.columns_map columns_map_u = columns_map - captured_a = captured_columns_map[A] - captured_b = captured_columns_map[B] - captured_c = captured_columns_map[C] - captured_d = captured_columns_map[D] - a = captured_a - b = captured_b - c = captured_c - d = captured_d + col0 = columns_map[id0] + col1 = columns_map[id1] + col2 = columns_map[id2] + col3 = columns_map[id3] + col0_u = col0 + col1_u = col1 + col2_u = col2 + col3_u = col3 end local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row] + return e, col0[row], col1[row], col2[row], col3[row] end elseif not id5 then function world_query_iter_next(): any @@ -2179,7 +2189,7 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row] end elseif not id6 then function world_query_iter_next(): any @@ -2227,7 +2237,7 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row] end elseif not id7 then function world_query_iter_next(): any @@ -2278,7 +2288,7 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], captured_g[row] end elseif not id8 then function world_query_iter_next(): any @@ -2332,11 +2342,11 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row], captured_h[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row] end else local output = {} - local ids_len = #ids + local ids_len = #ids_u function world_query_iter_next(): any local entities = entities_u local e = entities[i_u] @@ -2348,16 +2358,16 @@ local function query_cached(query: QueryInner) local col5 = col5_u local col6 = col6_u local col7 = col7_u - local captured_ids = ids - local captured_columns_map = columns_map + local ids = ids_u + local columns_map = columns_map_u while e == nil do last_archetype_u += 1 - local compatible_archetypes = compatible_archetypes_u - local archetype = compatible_archetypes[last_archetype_u] - archetype_u = archetype + local compatible_archetypes = compatible_archetypes_u + local archetype = compatible_archetypes[last_archetype_u] + archetype_u = archetype - if not archetype then + if not archetype then return nil end entities = archetype.entities @@ -2368,7 +2378,7 @@ local function query_cached(query: QueryInner) e = entities[i_u] entities_u = entities columns_map = archetype.columns_map - columns_map = captured_columns_map + columns_map_u = columns_map col0 = columns_map[id0] col1 = columns_map[id1] col2 = columns_map[id2] @@ -2387,14 +2397,14 @@ local function query_cached(query: QueryInner) col7_u = col7 end - local row = i - i -= 1 + local row = i_u + i_u -= 1 for i = 9, ids_len do - output[i - 8] = captured_columns_map[captured_ids[i]::any][row] + output[i - 8] = columns_map[ids[i]::any][row] end - return e, captured_a[row], captured_b[row], captured_c[row], captured_d[row], captured_e[row], captured_f[row], captured_g[row], captured_h[row], unpack(output) + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row], col7[row], unpack(output) end end diff --git a/test/tests.luau b/test/tests.luau index 6e233f4..333503a 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -24,7 +24,6 @@ type Id = jecs.Id local entity_visualiser = require("@modules/entity_visualiser") local dwi = entity_visualiser.stringify -FOCUS() TEST("reproduce idr_t nil archetype bug", function() local world = jecs.world() @@ -47,7 +46,7 @@ TEST("reproduce idr_t nil archetype bug", function() local src = r.archetype --REMOVING THIS jecs.archetype_traverse_remove CALL STOPS IT FROM HAPPENING - local dst = src and jecs.archetype_traverse_remove(world, id, src) + -- local dst = src and jecs.archetype_traverse_remove(world, id, src) end) local batchSize = 200 @@ -79,26 +78,23 @@ TEST("reproduce idr_t nil archetype bug", function() local q = world:query(jecs.pair(jecs.ChildOf, jecs.w)):cached() - print("---- delete start root") - print("--- root", world:contains(root), root, jecs.ECS_ID(root), jecs.ECS_GENERATION(root)) world:delete(root) - print("---- delete end root") - for entity in q do - local parent = world:parent(entity) :: jecs.Entity - print("--- root", world:contains(root), jecs.ECS_ID(root), jecs.ECS_GENERATION(root)) - print(world:get(entity, jecs.Name), jecs.ECS_ID(parent), jecs.ECS_GENERATION(parent), "root ->", root) - CHECK(world:parent(entity) == nil) - end + for entityId, info in trackedEntities do if world:contains(entityId) and not world:parent(entityId :: any) then print(`bugged entity found: {entityId}`) print(`original parent: {info.parentId}`) + print(`current parent: {i}`) print(`batch = {batch}, i = {i}`) print("==========================================") trackedEntities[entityId] = nil end end + for entity in q do + local parent = world:parent(entity) :: jecs.Entity + CHECK(world:parent(entity) == nil) + end end end) From e4b12f4a28abe324639867fa129934f0078fbd90 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 28 Dec 2025 11:47:45 +0100 Subject: [PATCH 3/8] check cascaded deletion for structural changes within onremove hooks --- modules/ob.luau | 8 ++++- src/jecs.luau | 80 +++++++++++++++++++++++++++++++------------------ test/tests.luau | 6 ++-- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/modules/ob.luau b/modules/ob.luau index 781636d..edfe0c4 100755 --- a/modules/ob.luau +++ b/modules/ob.luau @@ -83,6 +83,9 @@ local function observers_new( local tgt = jecs.ECS_PAIR_SECOND(term) local wc = tgt == jecs.w local onremoved = world:removed(rel, function(entity, id, delete: boolean?) + if delete then + return + end if not wc and id ~= term then return end @@ -98,7 +101,10 @@ local function observers_new( table.insert(cleanup, onremoved) else - local onremoved = world:removed(term, function(entity, id) + local onremoved = world:removed(term, function(entity, id, delete: boolean?) + if delete then + return + end local r = jecs.record(world, entity) local archetype = r.archetype if archetype then diff --git a/src/jecs.luau b/src/jecs.luau index 8c5b6bd..7fb45fc 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -1693,7 +1693,7 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local row = i_u i_u -= 1 - return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], captured_g[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row] end elseif not id8 then function world_query_iter_next(): any @@ -2288,7 +2288,7 @@ local function query_cached(query: QueryInner) local row = i_u i_u -= 1 - return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], captured_g[row] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row] end elseif not id8 then function world_query_iter_next(): any @@ -3393,6 +3393,7 @@ local function world_new(DEBUG: boolean?) end local function world_delete(world: world, entity: i53) + DEBUG_DELETING_ENTITY = entity local record = entity_index_try_get_unsafe(entity) if not record then return @@ -3525,6 +3526,7 @@ local function world_new(DEBUG: boolean?) else local on_remove = id_record.on_remove + local to = archetype_traverse_remove(world, id, idr_t_archetype) for i = #entities, 1, -1 do local child = entities[i] if on_remove then @@ -3532,18 +3534,12 @@ local function world_new(DEBUG: boolean?) end local r = entity_index_try_get_unsafe(child) :: record - local to = archetype_traverse_remove(world, id, r.archetype) inner_entity_move(child, r, to) end end end - end - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - if idr_t_archetype then - archetype_destroy(world, idr_t_archetype) - end + archetype_destroy(world, idr_t_archetype) end end @@ -3707,16 +3703,33 @@ local function world_new(DEBUG: boolean?) -- NOTE(marcus): Make it easy to grep the debug functions and -- being able to read the specification, without having to look -- at the implementation to understand invariants. - local function world_remove_checked(world: world, entity: i53, id: i53) + + local DEBUG_DELETING_ENTITY + local function DEBUG_IS_DELETING_ENTITY(entity: i53) + if DEBUG_DELETING_ENTITY == entity then + error([[ + Tried to make structural changes while the entity is in process + of being deleted. You called this function inside of the + OnRemove hook, but the entity is going to remove all of its + components making this operation moot. + ]], 2) + end + end + + local function DEBUG_IS_INVALID_ENTITY(entity: i53) local entity_id = ECS_ID(entity) - local wrong_entity = eindex_sparse_array[entity_id] ~= entity_id - if wrong_entity then + local r = eindex_sparse_array[entity_id] + local canonical_entity = eindex_dense_array[r.dense] + + if canonical_entity ~= entity then error([[ This Entity handle has an outdated generation. You are probably holding onto an entity that you got from outside the ECS ]], 2) end + end + local function DEBUG_ID_IS_INVALID_PAIR(id: i53) if ECS_ID_IS_WILDCARD(id) then error([[ You tried to pass in a wildcard pair. This is strictly @@ -3725,34 +3738,43 @@ local function world_new(DEBUG: boolean?) targets to remove and use jecs.bulk_remove. ]], 2) end + end + + -- NOTE(marcus): I have to save the old function and overriding the + -- upvalue in order to actually allow cascaded deletions to also be + -- checked by our program because we use the world_delete ptr internally. + local canonical_world_delete = world_delete + local function world_delete_checked(world: world, entity: i53) + DEBUG_DELETING_ENTITY = entity + DEBUG_IS_INVALID_ENTITY(entity) + canonical_world_delete(world, entity, id) + end + world_delete = world_delete_checked + + local function world_remove_checked(world: world, entity: i53, id: i53) + DEBUG_IS_DELETING_ENTITY(entity) + DEBUG_IS_INVALID_ENTITY(entity) + DEBUG_ID_IS_INVALID_PAIR(id) world_remove(world, entity, id) end local function world_add_checked(world: world, entity: i53, id: i53) - local entity_id = ECS_ID(entity) - local wrong_entity = eindex_sparse_array[entity_id] ~= entity_id - if wrong_entity then - error([[ - This Entity handle has an outdated generation. You are - probably holding onto an entity that you got from outside the ECS - ]], 2) - end + DEBUG_IS_DELETING_ENTITY(entity) + DEBUG_IS_INVALID_ENTITY(entity) + DEBUG_ID_IS_INVALID_PAIR(id) world_add(world, entity, id) end local function world_set_checked(world: world, entity: i53, id: i53, value: any) - local entity_id = ECS_ID(entity) - local wrong_entity = eindex_sparse_array[entity_id] ~= entity_id - if wrong_entity then - error([[ - This Entity handle has an outdated generation. You are - probably holding onto an entity that you got from outside the ECS - ]], 2) - end + DEBUG_IS_DELETING_ENTITY(entity) + DEBUG_IS_INVALID_ENTITY(entity) + DEBUG_ID_IS_INVALID_PAIR(id) + world_set(world, entity, id, value) end - world.remove = world_remove_unchecked + world.remove = world_remove_checked world.add = world_add_checked + world.set = world_set_checked end for i = 1, EcsRest do diff --git a/test/tests.luau b/test/tests.luau index 333503a..1e2cf8c 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -25,7 +25,7 @@ local entity_visualiser = require("@modules/entity_visualiser") local dwi = entity_visualiser.stringify TEST("reproduce idr_t nil archetype bug", function() - local world = jecs.world() + local world = jecs.world(true) local cts = { Humanoid = world:component(), @@ -46,7 +46,9 @@ TEST("reproduce idr_t nil archetype bug", function() local src = r.archetype --REMOVING THIS jecs.archetype_traverse_remove CALL STOPS IT FROM HAPPENING - -- local dst = src and jecs.archetype_traverse_remove(world, id, src) + CHECK_EXPECT_ERR(function() + world:remove(entity, cts.Humanoid) + end) end) local batchSize = 200 From d5c9abc57fad7aeb612bf0e7edbe5e67666f2ba3 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 28 Dec 2025 12:35:08 +0100 Subject: [PATCH 4/8] Document delete flag in OnRemove hooks --- how_to/004_tags.luau | 4 +- how_to/022_query_caching.luau | 2 +- how_to/110_hooks.luau | 71 ++++++++++++++++++++++++++++++++--- src/jecs.luau | 1 + 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/how_to/004_tags.luau b/how_to/004_tags.luau index a50a987..53133fa 100755 --- a/how_to/004_tags.luau +++ b/how_to/004_tags.luau @@ -16,8 +16,8 @@ local jecs = require("@jecs") --[[ There are two ways to create tags: - 1. Using jecs.tag() - preregister a tag entity which will be allocated when you create the world. - 2. Using world:entity() - creates a regular entity id + 1. Using jecs.tag() to preregister a tag entity which will be allocated when you create the world. + 2. Using world:entity() to create a regular entity id The first method is the "proper" way to create tags but it hinges upon that you do remember to create the world after declaring all of your diff --git a/how_to/022_query_caching.luau b/how_to/022_query_caching.luau index 428f959..79d32a2 100755 --- a/how_to/022_query_caching.luau +++ b/how_to/022_query_caching.luau @@ -79,6 +79,6 @@ end -- Cached query (faster for repeated use) local cached_query = world:query(Position, Velocity):cached() for entity, pos, vel in cached_query do - -- Process entities - this is faster for repeated iterations + -- Process entities. This is faster for repeated iterations. end diff --git a/how_to/110_hooks.luau b/how_to/110_hooks.luau index fc75dd6..f6ccc5b 100755 --- a/how_to/110_hooks.luau +++ b/how_to/110_hooks.luau @@ -17,8 +17,9 @@ world:set(Transform, jecs.OnAdd, function(entity, id, data) print(`Transform added to entity {entity}`) end) -world:set(Transform, jecs.OnRemove, function(entity, id) +world:set(Transform, jecs.OnRemove, function(entity, id, delete) -- A transform component id has been removed from entity + -- delete is true if the entity is being deleted, false/nil otherwise print(`Transform removed from entity {entity}`) end) @@ -37,9 +38,67 @@ end) When an entity graph contains cycles, order is undefined. This includes cycles that can be formed using different relationships. - - However an important note to make is that structural changes are not - necessarily always safe in OnRemove hooks. For instance, when an entity is - being deleted and invokes all of the OnRemove hooks on its components. It - can cause a lot of issues with moving entities +]] + +--[[ + Structural changes in OnRemove hooks + + You can call world:add, world:remove, and world:set inside OnRemove hooks. + That's fine. But there's a catch. + + When an entity is being deleted, all of its components get removed. Each + removal triggers the OnRemove hook. If you try to make structural changes + to the entity during deletion, like removing more components or adding new + ones, you're fighting against the deletion process itself. The entity is + going to lose all its components anyway, so what's the point? + + This creates a conflict. On one hand, you might want to clean up related + components when a specific component is removed. On the other hand, during + deletion, you don't want to do that because the entity is already being + torn down. So you need a way to tell the difference. + + The solution is the delete boolean. Every OnRemove hook receives it as the + third parameter. It's true when the entity is being deleted, and false + (or nil) when you're just removing a single component normally. + + So you check it. If delete is true, you bail out early. If it's false, + you do your cleanup. Simple. + + Here's what it looks like in practice: +]] + +local Health = world:component() +local Dead = world:component() + +world:set(Health, jecs.OnRemove, function(entity, id, delete) + if delete then + -- Entity is being deleted, don't try to clean up + return + end + + -- Normal removal, do cleanup + world:remove(entity, Dead) +end) + +--[[ + The ob.luau module uses this pattern extensively. When you're building + observers or monitors that track component removals, you need to distinguish + between "component removed" and "entity deleted" because they mean different + things for your tracking logic. + + Now, about the DEBUG flag. If you create a world with DEBUG enabled: + + local world = jecs.world(true) + + Then the world will actively prevent you from calling world:add, world:remove, + or world:set inside OnRemove hooks when delete is true. It throws an error + that tells you exactly what went wrong. This is useful during development + to catch cases where you forgot to check the delete flag. + + But here's the important part: even with DEBUG enabled, you're still allowed + to call these functions when delete is false. The DEBUG mode only prevents + structural changes during deletion, not during normal component removal. + + So the pattern is always the same: check delete, bail if true, proceed if false. + The DEBUG flag just makes sure you don't forget to do the check. ]] diff --git a/src/jecs.luau b/src/jecs.luau index 7fb45fc..713ee6d 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -3748,6 +3748,7 @@ local function world_new(DEBUG: boolean?) DEBUG_DELETING_ENTITY = entity DEBUG_IS_INVALID_ENTITY(entity) canonical_world_delete(world, entity, id) + DEBUG_DELETING_ENTITY = nil end world_delete = world_delete_checked From 74a97bc54eefeecd2aa2f19d8ff273c05bf6bd9b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 28 Dec 2025 12:37:13 +0100 Subject: [PATCH 5/8] Add types for DEBUG parameter --- src/jecs.d.ts | 8 ++++---- src/jecs.luau | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/jecs.d.ts b/src/jecs.d.ts index 11e7068..2c6b7aa 100755 --- a/src/jecs.d.ts +++ b/src/jecs.d.ts @@ -121,7 +121,7 @@ export class World { /** * Creates a new World. */ - private constructor(); + private constructor(DEBUG?: boolean); /** * Enforces a check for entities to be created within a desired range. @@ -354,9 +354,9 @@ export type ComponentRecord = { export function component_record(world: World, id: Id): ComponentRecord; type TagToUndefined = T extends TagDiscriminator ? undefined : T -type TrimOptional = T extends [...infer L, infer R] - ? unknown extends R - ? L | T | TrimOptional +type TrimOptional = T extends [...infer L, infer R] + ? unknown extends R + ? L | T | TrimOptional : R extends undefined ? L | T | TrimOptional : T diff --git a/src/jecs.luau b/src/jecs.luau index 713ee6d..3cae329 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -2667,9 +2667,6 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 }) end end -type Context = { - debug: boolean? -} local function world_new(DEBUG: boolean?) local eindex_dense_array = {} :: { i53 } local eindex_sparse_array = {} :: { record } @@ -3844,7 +3841,7 @@ local function ecs_entity_record(world: world, entity: i53) end return { - world = world_new :: () -> World, + world = world_new :: (boolean?) -> World, World = { new = world_new }, From 0d3f1bd3aaf6d9ee2ec6a47bcc60cb2282b0c446 Mon Sep 17 00:00:00 2001 From: alicesaidhi <166900055+alicesaidhi@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:11:02 +0100 Subject: [PATCH 6/8] introduce tests for specific cases (#291) --- test/tests.luau | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/tests.luau b/test/tests.luau index 1e2cf8c..50a6b11 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -1683,6 +1683,45 @@ TEST("world:entity()", function() local e2 = world:entity(399) CHECK(world:contains(e2)) end + + do CASE "desired id does not overwrite old entity id" + local world = jecs.world() + local ctype = world:component() + local id = world:entity() + + local e = world:entity(id + 1) + + CHECK(world:contains(id)) + CHECK(world:contains(e)) + + -- also make sure that they don't share the same record + + world:set(id, ctype, 1) + world:set(e, ctype, 2) + + CHECK(world:get(id, ctype) == 1) + CHECK(world:get(e, ctype) == 2) + end + + do CASE "creating ids with a higher key first" + local world = jecs.world() + local ctype = world:component() + + local e = world:entity(1000) + local id = world:entity(999) + + CHECK(world:contains(id)) + CHECK(world:contains(e)) + + -- also make sure that they don't share the same record + + world:set(id, ctype, 1) + world:set(e, ctype, 2) + + CHECK(world:get(id, ctype) == 1) + CHECK(world:get(e, ctype) == 2) + end + local N = 2^8 do CASE "unique IDs" From 7f3946736b3e162d6ae9a8eada66ea3ed3e481df Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 2 Jan 2026 06:32:59 +0100 Subject: [PATCH 7/8] Fix desired id being overriden --- src/jecs.luau | 74 +++++------- test.rbxl | Bin 0 -> 79 bytes test/tests.luau | 314 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 333 insertions(+), 55 deletions(-) create mode 100755 test.rbxl diff --git a/src/jecs.luau b/src/jecs.luau index 3cae329..5a98984 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -1034,8 +1034,8 @@ local function world_range(world: world, range_begin: number, range_end: number? dense = 0 } :: record end - entity_index.max_id = range_begin - 1 - entity_index.alive_count = range_begin - 1 + entity_index.max_id = range_begin + entity_index.alive_count = range_begin end end @@ -3302,62 +3302,51 @@ local function world_new(DEBUG: boolean?) if r then local dense = r.dense - if not dense or r.dense == 0 then - r.dense = index - dense = index - local e_swap = eindex_dense_array[dense] - local r_swap = entity_index_try_get_any(e_swap) :: record - - r_swap.dense = dense + -- If dense == 0, this is a pre-populated entry from world:range() + -- Just add the entity to the end of the dense array + if dense == 0 then 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 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 = entity_index_try_get_any(e_swap) :: 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 + -- If dense > 0, check if there's an existing entity at that position + local existing_entity = eindex_dense_array[dense] + if existing_entity and existing_entity ~= entity then + alive_count += 1 + entity_index.alive_count = alive_count + r.dense = alive_count + eindex_dense_array[alive_count] = entity + return entity end return entity else - for i = entity_index.max_id + 1, index do - eindex_sparse_array[i] = { dense = i } :: record - eindex_dense_array[i] = i - end - entity_index.max_id = index + local max_id = entity_index.max_id - local e_swap = eindex_dense_array[alive_count] - local r_swap = eindex_sparse_array[alive_count] - r_swap.dense = index + if index > max_id then + -- Pre-populate all intermediate IDs to keep sparse_array as an array + for i = max_id + 1, index - 1 do + if not eindex_sparse_array[i] then + -- NOTE(marcus): We have to do this check to see if + -- they exist first because world:range() may have + -- pre-populated some slots already. + end + + eindex_sparse_array[i] = { dense = 0 } :: record + end + entity_index.max_id = index + end alive_count += 1 entity_index.alive_count = alive_count - - r = eindex_sparse_array[index] - - r.dense = alive_count - - eindex_sparse_array[index] = r - - eindex_dense_array[index] = e_swap eindex_dense_array[alive_count] = entity + r = { dense = alive_count } :: record + eindex_sparse_array[index] = r + return entity end end @@ -3390,7 +3379,6 @@ local function world_new(DEBUG: boolean?) end local function world_delete(world: world, entity: i53) - DEBUG_DELETING_ENTITY = entity local record = entity_index_try_get_unsafe(entity) if not record then return @@ -3744,7 +3732,7 @@ local function world_new(DEBUG: boolean?) local function world_delete_checked(world: world, entity: i53) DEBUG_DELETING_ENTITY = entity DEBUG_IS_INVALID_ENTITY(entity) - canonical_world_delete(world, entity, id) + canonical_world_delete(world, entity) DEBUG_DELETING_ENTITY = nil end world_delete = world_delete_checked diff --git a/test.rbxl b/test.rbxl new file mode 100755 index 0000000000000000000000000000000000000000..372eb6b3fcbb6a4cf0005adee7c0403b7d38547f GIT binary patch literal 79 wcmcC1%1_G4uTbp#&&wsn#ee|gNKcIiaF9`VeF60K>rwbpQYW literal 0 HcmV?d00001 diff --git a/test/tests.luau b/test/tests.luau index 50a6b11..5cb2ab0 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -77,7 +77,7 @@ TEST("reproduce idr_t nil archetype bug", function() -- Removing this, but keeping Animator on the other 2 causes it to stop happening world:set(trackEntity1, cts.VelocitizeAnimationWeight, 0) - local q = world:query(jecs.pair(jecs.ChildOf, jecs.w)):cached() + local q = world:query(jecs.pair(jecs.ChildOf, __)):cached() world:delete(root) @@ -88,7 +88,7 @@ TEST("reproduce idr_t nil archetype bug", function() print(`bugged entity found: {entityId}`) print(`original parent: {info.parentId}`) print(`current parent: {i}`) - print(`batch = {batch}, i = {i}`) + print(`batch = {batchSize}, i = {i}`) print("==========================================") trackedEntities[entityId] = nil end @@ -917,9 +917,6 @@ TEST("world:delete()", function() CHECK(destroyed) CHECK(not world:contains(child)) end - if true then - return - end do CASE "Should delete children in different archetypes if they have the same parent" local world = jecs.world() @@ -1536,12 +1533,10 @@ TEST("world:range()", function() do CASE "spawn entity under min range" local world = jecs.world() world:range(400, 1000) - CHECK(world.entity_index.alive_count == 399) local e = world:entity(300) - CHECK(world.entity_index.alive_count == 400) local e1 = world:entity(300) - CHECK(world.entity_index.alive_count == 400) - CHECK(e) + CHECK(world:contains(e)) + CHECK(world:contains(e1)) end do CASE "entity ID reuse works correctly across different world ranges" local base = jecs.world() @@ -1588,6 +1583,7 @@ TEST("world:range()", function() CHECK(bar == bar_mirror) end + print("") do CASE "delete outside partitioned range" local server = jecs.world() local client = jecs.world() @@ -1605,8 +1601,25 @@ TEST("world:range()", function() CHECK(client:get(e2, A)) client:delete(e2) + + + print("") + print(client.entity_index.max_id) + print("") + + print("") + print(client.entity_index.alive_count) + print("") + + print("") + print(client.entity_index.dense_array[client.entity_index.alive_count]) + print("") + print("") local e3 = client:entity() - CHECK(ECS_ID(e3) == 1000) + print("") + print("") + CHECK(ECS_ID(e3) == 1001) + print("") local e1v1 = server:entity() local e4 = client:entity(e1v1) @@ -1616,6 +1629,281 @@ TEST("world:range()", function() CHECK(client:contains(e4)) end + print("") + + do CASE "desired id within range does not overwrite existing entities" + local world = jecs.world() + world:range(1000, 2000) + + local ctype = world:component() + + local e1 = world:entity(1000) + local e2 = world:entity(1001) + local e3 = world:entity(1002) + + world:set(e1, ctype, "entity1") + world:set(e2, ctype, "entity2") + world:set(e3, ctype, "entity3") + + local e4 = world:entity(1500) + world:set(e4, ctype, "entity4") + + CHECK(world:contains(e1)) + CHECK(world:contains(e2)) + CHECK(world:contains(e3)) + CHECK(world:contains(e4)) + CHECK(world:get(e1, ctype) == "entity1") + CHECK(world:get(e2, ctype) == "entity2") + CHECK(world:get(e3, ctype) == "entity3") + CHECK(world:get(e4, ctype) == "entity4") + end + + do CASE "creating entity with range+offset does not conflict with existing" + local world = jecs.world() + world:range(500, 1000) + + local ctype = world:component() + + local base = world:entity(500) + world:set(base, ctype, "base") + + local offset1 = world:entity(501) + world:set(offset1, ctype, "offset1") + + local offset2 = world:entity(750) + world:set(offset2, ctype, "offset2") + + CHECK(world:contains(base)) + CHECK(world:contains(offset1)) + CHECK(world:contains(offset2)) + CHECK(world:get(base, ctype) == "base") + CHECK(world:get(offset1, ctype) == "offset1") + CHECK(world:get(offset2, ctype) == "offset2") + end + + do CASE "creating entities in reverse order within range" + local world = jecs.world() + world:range(100, 200) + + local ctype = world:component() + + local e3 = world:entity(150) + local e2 = world:entity(120) + local e1 = world:entity(110) + + world:set(e1, ctype, 1) + world:set(e2, ctype, 2) + world:set(e3, ctype, 3) + + -- All should exist independently + CHECK(world:contains(e1)) + CHECK(world:contains(e2)) + CHECK(world:contains(e3)) + CHECK(world:get(e1, ctype) == 1) + CHECK(world:get(e2, ctype) == 2) + CHECK(world:get(e3, ctype) == 3) + end + + do CASE "creating entity with desired ID after range pre-population" + local world = jecs.world() + world:range(400, 1000) + + local ctype = world:component() + + -- Range pre-populates 1-399 with dense=0 + -- Create an entity at a pre-populated position + local e1 = world:entity(300) + world:set(e1, ctype, "prepop") + + -- Create another entity at a different pre-populated position + local e2 = world:entity(250) + world:set(e2, ctype, "prepop2") + + -- Create entity within the range + local e3 = world:entity(500) + world:set(e3, ctype, "inrange") + + -- All should work correctly + CHECK(world:contains(e1)) + CHECK(world:contains(e2)) + CHECK(world:contains(e3)) + CHECK(world:get(e1, ctype) == "prepop") + CHECK(world:get(e2, ctype) == "prepop2") + CHECK(world:get(e3, ctype) == "inrange") + end + + do CASE "creating same entity twice returns existing entity" + local world = jecs.world() + world:range(1000, 2000) + + local ctype = world:component() + + -- Create entity + local e1 = world:entity(1500) + world:set(e1, ctype, "original") + + -- Try to create it again + local e2 = world:entity(1500) + + -- Should return the same entity + CHECK(e1 == e2) + CHECK(world:get(e1, ctype) == "original") + CHECK(world:get(e2, ctype) == "original") + + -- Should only be one entity + local count = 0 + for _ in world:query(ctype) do + count += 1 + end + CHECK(count == 1) + end + + do CASE "creating entities with gaps in range does not affect existing" + local world = jecs.world() + world:range(200, 300) + + local ctype = world:component() + + -- Create entities with gaps + local e1 = world:entity(200) + local e2 = world:entity(250) + local e3 = world:entity(299) + + world:set(e1, ctype, 1) + world:set(e2, ctype, 2) + world:set(e3, ctype, 3) + + -- Create entity in the gap + local e4 = world:entity(225) + world:set(e4, ctype, 4) + + -- All should still exist + CHECK(world:contains(e1)) + CHECK(world:contains(e2)) + CHECK(world:contains(e3)) + CHECK(world:contains(e4)) + CHECK(world:get(e1, ctype) == 1) + CHECK(world:get(e2, ctype) == 2) + CHECK(world:get(e3, ctype) == 3) + CHECK(world:get(e4, ctype) == 4) + end + + do CASE "creating many entities with desired IDs in range" + local world = jecs.world() + world:range(1000, 2000) + + local ctype = world:component() + local entities = {} + + -- Create 50 entities with specific IDs + for i = 1, 50 do + local id = 1000 + (i * 10) + local e = world:entity(id) + world:set(e, ctype, i) + entities[i] = e + end + + -- Verify all exist and have correct values + for i = 1, 50 do + CHECK(world:contains(entities[i])) + CHECK(world:get(entities[i], ctype) == i) + end + + -- Create more entities in between + for i = 1, 49 do + local id = 1000 + (i * 10) + 5 + local e = world:entity(id) + world:set(e, ctype, i + 100) + end + + -- Original entities should still be intact + for i = 1, 50 do + CHECK(world:contains(entities[i])) + CHECK(world:get(entities[i], ctype) == i) + end + end + + do CASE "sparse_array remains optimized with non-contiguous entity IDs" + local world = jecs.world() + local ctype = world:component() + + -- Create entities with large gaps to test sparse_array optimization + -- Start with entity 100, then jump to 1000 (gap: 101-999) + local e1 = world:entity(100) + local e2 = world:entity(1000) + -- Then jump to 5000 (gap: 1001-4999) + local e3 = world:entity(5000) + -- Then jump to 10000 (gap: 5001-9999) + local e4 = world:entity(10000) + + world:set(e1, ctype, 1) + world:set(e2, ctype, 2) + world:set(e3, ctype, 3) + world:set(e4, ctype, 4) + + -- Verify all entities exist and have correct values + CHECK(world:contains(e1)) + CHECK(world:contains(e2)) + CHECK(world:contains(e3)) + CHECK(world:contains(e4)) + CHECK(world:get(e1, ctype) == 1) + CHECK(world:get(e2, ctype) == 2) + CHECK(world:get(e3, ctype) == 3) + CHECK(world:get(e4, ctype) == 4) + + -- Verify intermediate IDs are pre-populated (they should exist but not be alive) + -- These are in the gaps between created entities + local sparse_array = world.entity_index.sparse_array + CHECK(sparse_array[500] ~= nil) -- Between 100 and 1000, should be pre-populated + CHECK(sparse_array[500].dense == 0) -- But not alive (dense=0) + CHECK(sparse_array[2500] ~= nil) -- Between 1000 and 5000, should be pre-populated + CHECK(sparse_array[2500].dense == 0) + CHECK(sparse_array[7500] ~= nil) -- Between 5000 and 10000, should be pre-populated + CHECK(sparse_array[7500].dense == 0) + + -- Verify max_id was updated correctly + CHECK(world.entity_index.max_id == 10000) + end + do CASE "desired id does not overwrite old entity id" + local world = jecs.world() + local ctype = world:component() + local id = world:entity() + + print("") + local e = world:entity((id::any) + 1) + print("") + + CHECK(world:contains(id)) + CHECK(world:contains(e)) + + -- also make sure that they don't share the same record + + world:set(id, ctype, 1) + world:set(e, ctype, 2) + + CHECK(world:get(id, ctype) == 1) + CHECK(world:get(e, ctype) == 2) + end + + do CASE "creating ids with a higher key first" + local world = jecs.world() + local ctype = world:component() + + local e = world:entity(1000) + local id = world:entity(999) + + CHECK(world:contains(id)) + CHECK(world:contains(e)) + + -- also make sure that they don't share the same record + + world:set(id, ctype, 1) + world:set(e, ctype, 2) + + CHECK(world:get(id, ctype) == 1) + CHECK(world:get(e, ctype) == 2) + end + do CASE "under range start" local world = jecs.world() @@ -1689,7 +1977,7 @@ TEST("world:entity()", function() local ctype = world:component() local id = world:entity() - local e = world:entity(id + 1) + local e = world:entity((id::any) + 1) CHECK(world:contains(id)) CHECK(world:contains(e)) @@ -1702,7 +1990,7 @@ TEST("world:entity()", function() CHECK(world:get(id, ctype) == 1) CHECK(world:get(e, ctype) == 2) end - + do CASE "creating ids with a higher key first" local world = jecs.world() local ctype = world:component() @@ -1793,6 +2081,8 @@ TEST("world:entity()", function() CHECK(ECS_ID(e) == pin) CHECK(ECS_GENERATION(e) == 0) end + + end) TEST("world:has()", function() From 64f8750f3c5e3904c575b3e5884edd1c7cc57a36 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 2 Jan 2026 06:37:58 +0100 Subject: [PATCH 8/8] Add missing delete parameter in types for OnRemove --- src/jecs.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jecs.luau b/src/jecs.luau index 5a98984..25b3602 100755 --- a/src/jecs.luau +++ b/src/jecs.luau @@ -3839,7 +3839,7 @@ return { is_tag = (ecs_is_tag :: any) :: (World, Component) -> boolean, OnAdd = (EcsOnAdd :: any) :: Component<(entity: Entity, id: Id, data: T) -> ()>, - OnRemove = (EcsOnRemove :: any) :: Component<(entity: Entity, id: Id) -> ()>, + OnRemove = (EcsOnRemove :: any) :: Component<(entity: Entity, id: Id, delete: boolean?) -> ()>, OnChange = (EcsOnChange :: any) :: Component<(entity: Entity, id: Id, data: T) -> ()>, ChildOf = (EcsChildOf :: any) :: Entity, Component = (EcsComponent :: any) :: Entity,