diff --git a/benches/visual/remove.bench.luau b/benches/visual/remove.bench.luau index 4762575..b62a64b 100755 --- a/benches/visual/remove.bench.luau +++ b/benches/visual/remove.bench.luau @@ -21,7 +21,6 @@ local C4 = ecs:entity() ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) local E1 = mcs:component() local E2 = mcs:entity() -mcs:add(E2, mirror.Exclusive) mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) local E3 = mcs:entity() mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) @@ -35,17 +34,17 @@ return { Functions = { Mirror = function() local m = mcs:entity() - for i = 1, 1000 do - mcs:add(m, pair(E2, E3)) - mcs:add(m, pair(E2, E4)) + for i = 1, 100 do + mcs:add(m, pair(E4, E4)) + mcs:add(m, pair(E4, E4)) end end, Jecs = function() local j = ecs:entity() - for i = 1, 1000 do - ecs:add(j, pair(C2, C3)) - ecs:add(j, pair(C2, C4)) + for i = 1, 100 do + ecs:add(j, pair(C4, C4)) + ecs:add(j, pair(C4, C4)) end end, }, diff --git a/jecs.luau b/jecs.luau index 356626b..d26384b 100755 --- a/jecs.luau +++ b/jecs.luau @@ -2269,7 +2269,7 @@ local function world_new() end end - local idr = world.component_index[id] + local idr = component_index[id] local on_add = idr.hooks.on_add if on_add then @@ -2398,6 +2398,74 @@ local function world_new() end local from: Archetype = record.archetype + if ECS_IS_PAIR(id::number) then + local src = from or ROOT_ARCHETYPE + local edge = archetype_edges[src.id] + local to = edge[id] + local idr: ComponentRecord + if not to then + local first = ECS_PAIR_FIRST(id::number) + local wc = ECS_PAIR(first, EcsWildcard) + idr = component_index[wc] + if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then + local cr = idr.records[src.id] + if cr then + local on_remove = idr.hooks.on_remove + local id_types = src.types + if on_remove then + on_remove(entity, id_types[cr]) + src = record.archetype + id_types = src.types + cr = idr.records[src.id] + end + local dst = table.clone(id_types) + dst[cr] = id + to = archetype_ensure(world, dst) + else + to = find_archetype_with(world, id, src) + idr = component_index[id] + end + else + to = find_archetype_with(world, id, src) + idr = component_index[id] + end + edge[id] = to + else + idr = component_index[id] + end + local idr_hooks = idr.hooks + if from == to then + local column = to.columns_map[id] + column[record.row] = data + + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local on_change = idr_hooks.on_change + if on_change then + on_change(entity, id, data) + end + + return + end + + if from then + entity_move(entity_index, entity, record, to) + else + if #to.types > 0 then + new_entity(entity, record, to) + end + end + + local column = to.columns_map[id] + column[record.row] = data + + local on_add = idr.hooks.on_add + + if on_add then + on_add(entity, id) + end + return + end local to: Archetype = inner_archetype_traverse_add(id, from) local idr = component_index[id] local idr_hooks = idr.hooks diff --git a/mirror.luau b/mirror.luau index 8d7d4a3..6ca77f9 100755 --- a/mirror.luau +++ b/mirror.luau @@ -42,8 +42,18 @@ export type Iter = (query: Query) -> () -> (Entity, T...) export type Query = typeof(setmetatable( {} :: { iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, + with: + ((Query, Id) -> Query) + & ((Query, Id, Id) -> Query) + & ((Query, Id, Id, Id) -> Query) + & ((Query, Id, Id, Id) -> Query) + & ((Query, Id, Id, Id, Id) -> Query), + without: + ((Query, Id) -> Query) + & ((Query, Id, Id) -> Query) + & ((Query, Id, Id, Id) -> Query) + & ((Query, Id, Id, Id) -> Query) + & ((Query, Id, Id, Id, Id) -> Query), archetypes: (self: Query) -> { Archetype }, cached: (self: Query) -> Query, }, @@ -186,10 +196,9 @@ local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_PAIR_OFFSET = 2^48 -local ECS_ID_DELETE = 0b0001 -local ECS_ID_IS_TAG = 0b0010 -local ECS_ID_IS_EXCLUSIVE = 0b0100 -local ECS_ID_MASK = 0b0000 +local ECS_ID_DELETE = 0b01 +local ECS_ID_IS_TAG = 0b10 +local ECS_ID_MASK = 0b00 local HI_COMPONENT_ID = 256 local EcsOnAdd = HI_COMPONENT_ID + 1 @@ -205,8 +214,7 @@ local EcsRemove = HI_COMPONENT_ID + 10 local EcsName = HI_COMPONENT_ID + 11 local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 -local EcsExclusive = HI_COMPONENT_ID + 14 -local EcsRest = HI_COMPONENT_ID + 15 +local EcsRest = HI_COMPONENT_ID + 14 local NULL_ARRAY = table.freeze({}) :: Column local NULL = newproxy(false) @@ -675,7 +683,6 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord local is_pair = ECS_IS_PAIR(id :: number) local has_delete = false - local is_exclusive = false if is_pair then relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53 @@ -690,10 +697,6 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord if cleanup_policy_target == EcsDelete then has_delete = true end - - if world_has_one_inline(world, relation, EcsExclusive) then - is_exclusive = true - end else local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) @@ -715,8 +718,7 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord flags = bit32.bor( flags, if has_delete then ECS_ID_DELETE else 0, - if is_tag then ECS_ID_IS_TAG else 0, - if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0 + if is_tag then ECS_ID_IS_TAG else 0 ) idr = { @@ -927,22 +929,9 @@ end local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype local id_types = from.types - local dst = table.clone(id_types) - - if ECS_IS_PAIR(id::number) then - local first = ECS_PAIR_FIRST(id::number) - local idr = world.component_index[ECS_PAIR(first, EcsWildcard)] - if idr and bit32.btest(idr.flags, EcsExclusive) then - local cr = idr.records[from.id] - if cr then - dst[cr] = id - return archetype_ensure(world, dst) - end - end - end local at = find_insert(id_types :: { number } , id :: number) - + local dst = table.clone(id_types) table.insert(dst, at, id) return archetype_ensure(world, dst) @@ -2426,7 +2415,7 @@ local function world_new() return entity else for i = eindex_max_id + 1, index do - eindex_sparse_array[i] = { dense = i } :: Record + eindex_sparse_array[i]= { dense = i } :: Record eindex_dense_array[i] = i end entity_index.max_id = index @@ -2909,7 +2898,7 @@ return { world = world_new :: () -> World, component = (ECS_COMPONENT :: any) :: () -> Entity, tag = (ECS_TAG :: any) :: () -> Entity, - meta = (ECS_META :: any) :: (id: Entity, id: Id, value: T) -> Entity, + meta = (ECS_META :: any) :: (id: Entity, id: Id, value: a?) -> Entity, is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, OnAdd = (EcsOnAdd :: any) :: Entity<(entity: Entity, id: Id, data: T) -> ()>, @@ -2924,7 +2913,6 @@ return { Delete = (EcsDelete :: any) :: Entity, Remove = (EcsRemove :: any) :: Entity, Name = (EcsName :: any) :: Entity, - Exclusive = EcsExclusive :: Entity, Rest = (EcsRest :: any) :: Entity, pair = (ECS_PAIR :: any) :: (first: Id

, second: Id) -> Pair, diff --git a/test/tests.luau b/test/tests.luau index b9c09dd..005d785 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -25,20 +25,7 @@ local entity_visualiser = require("@tools/entity_visualiser") local dwi = entity_visualiser.stringify TEST("exclusive", function() - local world = jecs.world() - local A = world:component() - world:add(A, jecs.Exclusive) - - local B = world:component() - local C = world:component() - - local e = world:entity() - world:add(e, pair(A, B)) - world:add(e, pair(A, C)) - - CHECK(world:has(e, pair(A, B)) == false) - CHECK(world:has(e, pair(A, C)) == true) - -- print(jecs.entity_index_try_get(world.entity_index, e).archetype.type) + -- print(jecs.entity_index_try_get(world.entity_index, e).archetype.type) end) TEST("bulk", function() @@ -159,6 +146,66 @@ TEST("repro", function() end) TEST("world:add()", function() + do CASE "exclusive relations" + local world = jecs.world() + local A = world:component() + world:add(A, jecs.Exclusive) + + local B = world:component() + local C = world:component() + + local e = world:entity() + world:add(e, pair(A, B)) + world:add(e, pair(A, C)) + + CHECK(world:has(e, pair(A, B)) == false) + CHECK(world:has(e, pair(A, C)) == true) + end + + do CASE "exclusive relations invoke hooks" + local world = jecs.world() + local A = world:component() + local B = world:component() + local C = world:component() + + local e_ptr = jecs.Rest :: number + 1 + + world:add(A, jecs.Exclusive) + local on_remove_call = false + world:set(A, jecs.OnRemove, function(e, id) + CHECK(e == e_ptr) + CHECK(id == jecs.pair(A, B)) + on_remove_call = true + end) + + local on_add_call_count = 0 + world:set(A, jecs.OnAdd, function(e, id) + on_add_call_count += 1 + if on_add_call_count == 1 then + CHECK(e == e_ptr) + CHECK(id == jecs.pair(A, B)) + elseif on_add_call_count == 2 then + CHECK(e == e_ptr) + CHECK(id == jecs.pair(A, C)) + else + CHECK(false) + end + end) + + + local e = world:entity() + CHECK(e == e_ptr) + world:add(e, pair(A, B)) + CHECK(on_add_call_count == 1) + world:add(e, pair(A, C)) + CHECK(on_add_call_count == 2) + CHECK(on_remove_call) + + CHECK(world:has(e, pair(A, B)) == false) + CHECK(world:has(e, pair(A, C)) == true) + + end + do CASE "idempotent" local world = jecs.world() local d = dwi(world)