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 0957252..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) @@ -38,3 +39,66 @@ end) When an entity graph contains cycles, order is undefined. This includes cycles that can be formed using different relationships. ]] + +--[[ + 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/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/rokit.toml b/rokit.toml index 6df2554..9488feb 100755 --- a/rokit.toml +++ b/rokit.toml @@ -1,5 +1,5 @@ [tools] wally = "upliftgames/wally@0.3.2" -rojo = "rojo-rbx/rojo@7.4.4" -luau = "luau-lang/luau@0.701" -zune = "scythe-technology/zune@0.5.1" +rojo = "rojo-rbx/rojo@7.7.0-rc.1" +luau = "luau-lang/luau@0.703.0" +zune = "scythe-technology/zune@0.5.1" \ No newline at end of file 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 fef3e92..25b3602 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?) @@ -1033,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 @@ -1312,359 +1313,508 @@ 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] + 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_u = #entities + if i_u == 0 then + continue + end + 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_u + i_u -= 1 + + return e, col0[row], col1[row] + end + elseif not id3 then + 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 + + 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] + 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] + return e, col0[row], col1[row], col2[row] end - elseif not D 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] + 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] + return e, col0[row], col1[row], col2[row], col3[row] end - elseif not E 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 = 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] + return e, col0[row], col1[row], col2[row], col3[row], col4[row] end - elseif not F 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] + 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] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row] end - elseif not G 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] + 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] + return e, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[row] end - elseif not H 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + 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 - elseif not I 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 - return nil - end - entities = archetype.entities - i = #entities - if i == 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] - end + local row = i_u + i_u -= 1 - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], 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 - return nil - end - - entities = archetype.entities - i = #entities - if i == 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] - end - - local row = i - i -= 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) - 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) @@ -1676,21 +1826,16 @@ local function query_iter(query): () -> (number, ...any) end local function query_cached(query: QueryInner) - local ids = query.ids - local lastArchetype = 1 + local ids_u = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids :: { Component }) - if not A then - A = query.filter_with[1] + local id0, id1, id2, id3, id4, id5, id6, id7, id8 = unpack(ids_u :: { Component }) + if not id0 then + 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 @@ -1699,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 @@ -1710,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] @@ -1721,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) @@ -1752,350 +1911,500 @@ 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() - lastArchetype = 1 - archetype = compatible_archetypes[lastArchetype] - 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 - if not A then - 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] + entities_u = archetype_u.entities + i_u = #entities_u + columns_map_u = archetype_u.columns_map + if not id0 then + 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 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] + entities_u = 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, col0[row], col1[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, col0[row], col1[row], col2[row], col3[row], col4[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, col0[row], col1[row], col2[row], col3[row], col4[row], col5[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, col0[row], col1[row], col2[row], col3[row], col4[row], col5[row], col6[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, 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 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 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 + 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_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 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 @@ -2358,7 +2667,7 @@ local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 }) end end -local function world_new() +local function world_new(DEBUG: boolean?) local eindex_dense_array = {} :: { i53 } local eindex_sparse_array = {} :: { record } @@ -2524,6 +2833,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 +2871,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 +2978,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 @@ -2989,62 +3302,51 @@ local function world_new() 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 @@ -3209,6 +3511,7 @@ local function world_new() 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 @@ -3216,18 +3519,12 @@ local function world_new() 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 @@ -3387,6 +3684,85 @@ 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 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 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 + 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 + 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) + DEBUG_DELETING_ENTITY = nil + 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) + 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) + 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_checked + world.add = world_add_checked + world.set = world_set_checked + end + for i = 1, EcsRest do entity_index_new_id(entity_index) end @@ -3453,7 +3829,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 }, @@ -3463,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, diff --git a/test.rbxl b/test.rbxl new file mode 100755 index 0000000..372eb6b Binary files /dev/null and b/test.rbxl differ 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..5cb2ab0 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -24,83 +24,8 @@ 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) TEST("reproduce idr_t nil archetype bug", function() - local world = jecs.world() + local world = jecs.world(true) local cts = { Humanoid = world:component(), @@ -121,46 +46,58 @@ 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 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, __)):cached() + + + world:delete(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(`current parent: {i}`) + print(`batch = {batchSize}, 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) TEST("Ensure archetype edges get cleaned", function() @@ -980,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() @@ -1599,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() @@ -1651,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() @@ -1668,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) @@ -1679,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() @@ -1746,6 +1971,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::any) + 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" @@ -1817,6 +2081,8 @@ TEST("world:entity()", function() CHECK(ECS_ID(e) == pin) CHECK(ECS_GENERATION(e) == 0) end + + end) TEST("world:has()", function()