diff --git a/jecs/addons/observers.luau b/jecs/addons/observers.luau index a7a8191..cffce81 100644 --- a/jecs/addons/observers.luau +++ b/jecs/addons/observers.luau @@ -101,50 +101,45 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl world.added = function(_, component, fn) local listeners = signals.added[component] - local component_index = world.component_index :: jecs.ComponentIndex - assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with") if not listeners then listeners = {} signals.added[component] = listeners - local function on_add(entity: number, id: number, value: any) - for _, listener in listeners :: any do - listener(entity, id, value) + local idr = jecs.id_record_ensure(world, component) + idr.hooks.on_add = function(entity) + for _, listener in listeners do + listener(entity, component) end end - world:set(component, jecs.OnAdd, on_add) end + end table.insert(listeners, fn) end world.changed = function(_, component, fn) local listeners = signals.emplaced[component] - local component_index = world.component_index :: jecs.ComponentIndex - assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with") if not listeners then listeners = {} signals.emplaced[component] = listeners - local function on_change(entity: number, id: number, value: any) - for _, listener in listeners :: any do - listener(entity, id, value) + local idr = jecs.id_record_ensure(world, component) + idr.hooks.on_change = function(entity, value) + for _, listener in listeners do + listener(entity, component, value) end end - world:set(component, jecs.OnChange, on_change) end table.insert(listeners, fn) end world.removed = function(_, component, fn) local listeners = signals.removed[component] - local component_index = world.component_index :: jecs.ComponentIndex - assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with") if not listeners then listeners = {} signals.removed[component] = listeners - local function on_remove(entity: number, id: number, value: any) - for _, listener in listeners :: any do - listener(entity, id, value) + local idr = jecs.id_record_ensure(world, component) + idr.hooks.on_remove = function(entity) + for _, listener in listeners do + listener(entity, component) end end - world:set(component, jecs.OnRemove, on_remove) end table.insert(listeners, fn) end @@ -155,7 +150,7 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl world.monitor = monitors_new - return world :: PatchedWorld + return world end return observers_add diff --git a/jecs/build.txt b/jecs/build.txt index 5224a55..a79bfff 100644 --- a/jecs/build.txt +++ b/jecs/build.txt @@ -1,2 +1,2 @@ -modified = ["addons/observers.luau", "jecs.luau"] -version = "0.5.5-nightly.20250423T001100Z" +modified = ["jecs.luau"] +version = "0.5.5-nightly.20250414T001100Z" diff --git a/jecs/jecs.luau b/jecs/jecs.luau index 3f297da..ce9e63d 100644 --- a/jecs/jecs.luau +++ b/jecs/jecs.luau @@ -45,9 +45,9 @@ type ecs_id_record_t = { flags: number, size: number, hooks: { - on_add: ((entity: i53, id: i53, data: any?) -> ())?, - on_change: ((entity: i53, id: i53, data: any) -> ())?, - on_remove: ((entity: i53, id: i53) -> ())?, + on_add: ((entity: i53, data: any?) -> ())?, + on_change: ((entity: i53, data: any) -> ())?, + on_remove: ((entity: i53) -> ())?, }, } @@ -116,47 +116,13 @@ local ECS_ID_MASK = 0b00 local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_GENERATION_MASK = bit32.lshift(1, 16) -local NULL_ARRAY = table.freeze({}) :: Column -local NULL = newproxy(false) - +local NULL_ARRAY = table.freeze({}) 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_metadata: Map> = {} -local ecs_max_component_id = 0 -local ecs_max_tag_id = EcsRest - -local function ECS_COMPONENT() - ecs_max_component_id += 1 - if ecs_max_component_id > HI_COMPONENT_ID then - error("Too many components") - end - return ecs_max_component_id -end - -local function ECS_TAG() - ecs_max_tag_id += 1 - return ecs_max_tag_id -end - -local function ECS_META(id: i53, ty: i53, value: any?) - local bundle = ecs_metadata[id] - if bundle == nil then - bundle = {} - ecs_metadata[id] = bundle - end - bundle[ty] = if value == nil then NULL else value -end - -local function ECS_META_RESET() - ecs_metadata = {} - ecs_max_component_id = 0 - ecs_max_tag_id = EcsRest -end - local function ECS_COMBINE(id: number, generation: number): i53 return id + (generation * ECS_ENTITY_MASK) end @@ -500,14 +466,6 @@ local function world_has_one_inline(world: ecs_world_t, entity: i53, id: i53): b return records[id] ~= nil end -local function ecs_is_tag(world: ecs_world_t, entity: i53): boolean - local idr = world.component_index[entity] - if idr then - return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 - end - return not world_has_one_inline(world, entity, EcsComponent) -end - local function world_has(world: ecs_world_t, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean @@ -571,64 +529,62 @@ end local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t local component_index = world.component_index local entity_index = world.entity_index - local idr: ecs_id_record_t? = component_index[id] + local idr: ecs_id_record_t = component_index[id] - if idr then - return idr + if not idr then + local flags = ECS_ID_MASK + local relation = id + local target = 0 + local is_pair = ECS_IS_PAIR(id) + if is_pair then + relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 + assert(relation and entity_index_is_alive( + entity_index, relation), ECS_INTERNAL_ERROR) + target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 + assert(target and entity_index_is_alive( + entity_index, target), ECS_INTERNAL_ERROR) + end + + local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) + local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) + + local has_delete = false + + if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then + has_delete = true + end + + local on_add, on_change, on_remove = world_get(world, + relation, EcsOnAdd, EcsOnChange, EcsOnRemove) + + local is_tag = not world_has_one_inline(world, + relation, EcsComponent) + + if is_tag and is_pair then + is_tag = not world_has_one_inline(world, target, EcsComponent) + end + + flags = bit32.bor( + flags, + if has_delete then ECS_ID_DELETE else 0, + if is_tag then ECS_ID_IS_TAG else 0 + ) + + idr = { + size = 0, + cache = {}, + counts = {}, + flags = flags, + hooks = { + on_add = on_add, + on_change = on_change, + on_remove = on_remove, + }, + } + + component_index[id] = idr end - local flags = ECS_ID_MASK - local relation = id - local target = 0 - local is_pair = ECS_IS_PAIR(id) - if is_pair then - relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53 - assert(relation and entity_index_is_alive( - entity_index, relation), ECS_INTERNAL_ERROR) - target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53 - assert(target and entity_index_is_alive( - entity_index, target), ECS_INTERNAL_ERROR) - end - - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) - - local has_delete = false - - if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then - has_delete = true - end - - local on_add, on_change, on_remove = world_get(world, - relation, EcsOnAdd, EcsOnChange, EcsOnRemove) - - local is_tag = not world_has_one_inline(world, - relation, EcsComponent) - - if is_tag and is_pair then - is_tag = not world_has_one_inline(world, target, EcsComponent) - end - - flags = bit32.bor( - flags, - if has_delete then ECS_ID_DELETE else 0, - if is_tag then ECS_ID_IS_TAG else 0 - ) - - idr = { - size = 0, - cache = {}, - counts = {}, - flags = flags, - hooks = { - on_add = on_add, - on_change = on_change, - on_remove = on_remove, - }, - } :: ecs_id_record_t - - component_index[id] = idr - return idr end @@ -665,7 +621,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: local columns = (table.create(length) :: any) :: { Column } local records: { number } = {} - local counts: { number } = {} + local counts: {number} = {} local archetype: ecs_archetype_t = { columns = columns, @@ -714,7 +670,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev: world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} :: Map + world.archetype_edges[archetype.id] = {} return archetype end @@ -791,17 +747,17 @@ local function archetype_traverse_remove( local edges = world.archetype_edges local edge = edges[from.id] - local to: ecs_archetype_t = edge[id] - if to == nil then + local to = edge[id] + if not to then to = find_archetype_without(world, from, id) edge[id] = to edges[to.id][id] = from end - return to + return to :: ecs_archetype_t end -local function find_archetype_with(world, id, from): ecs_archetype_t +local function find_archetype_with(world, id, from) local id_types = from.types local at = find_insert(id_types, id) @@ -811,7 +767,7 @@ local function find_archetype_with(world, id, from): ecs_archetype_t return archetype_ensure(world, dst) end -local function archetype_traverse_add(world, id, from: ecs_archetype_t): ecs_archetype_t +local function archetype_traverse_add(world, id, from: ecs_archetype_t) from = from or world.ROOT_ARCHETYPE if from.records[id] then return from @@ -857,7 +813,7 @@ local function world_add( local on_add = idr.hooks.on_add if on_add then - on_add(entity, id) + on_add(entity) end end @@ -874,7 +830,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown local idr_hooks = idr.hooks if from == to then - local tr = (to :: ecs_archetype_t).records[id] + local tr = to.records[id] local column = from.columns[tr] column[record.row] = data @@ -882,7 +838,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown -- and just set the data directly. local on_change = idr_hooks.on_change if on_change then - on_change(entity, id, data) + on_change(entity, data) end return @@ -905,7 +861,7 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown local on_add = idr_hooks.on_add if on_add then - on_add(entity, id, data) + on_add(entity, data) end end @@ -937,7 +893,7 @@ local function world_remove(world: ecs_world_t, entity: i53, id: i53) local idr = world.component_index[id] local on_remove = idr.hooks.on_remove if on_remove then - on_remove(entity, id) + on_remove(entity) end local to = archetype_traverse_remove(world, id, record.archetype) @@ -989,7 +945,7 @@ local function archetype_delete(world: ecs_world_t, archetype: ecs_archetype_t, local idr = component_index[id] local on_remove = idr.hooks.on_remove if on_remove then - on_remove(delete, id) + on_remove(delete) end end @@ -1028,8 +984,8 @@ local function world_clear(world: ecs_world_t, entity: i53) end if idr_t then - local queue: { i53 } - local ids: Map + local queue + local ids local count = 0 local archetype_ids = idr_t.cache @@ -1049,7 +1005,7 @@ local function world_clear(world: ecs_world_t, entity: i53) continue end if not ids then - ids = {} :: { [i53]: boolean } + ids = {} end ids[id] = true removal_queued = true @@ -1060,7 +1016,7 @@ local function world_clear(world: ecs_world_t, entity: i53) end if not queue then - queue = {} :: { i53 } + queue = {} end local n = #entities @@ -1219,8 +1175,8 @@ local function world_delete(world: ecs_world_t, entity: i53) end if idr_t then - local children: { i53 } - local ids: Map + local children + local ids local count = 0 local archetype_ids = idr_t.cache @@ -1250,7 +1206,7 @@ local function world_delete(world: ecs_world_t, entity: i53) break else if not ids then - ids = {} :: { [i53]: boolean } + ids = {} end ids[id] = true removal_queued = true @@ -1261,7 +1217,7 @@ local function world_delete(world: ecs_world_t, entity: i53) continue end if not children then - children = {} :: { i53 } + children = {} end local n = #entities table.move(entities, 1, n, count + 1, children) @@ -1284,7 +1240,7 @@ local function world_delete(world: ecs_world_t, entity: i53) if idr_r then local archetype_ids = idr_r.cache local flags = idr_r.flags - if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then + if bit32.band(flags, ECS_ID_DELETE) ~= 0 then for archetype_id in archetype_ids do local idr_r_archetype = archetypes[archetype_id] local entities = idr_r_archetype.entities @@ -1798,7 +1754,7 @@ local function query_cached(query: ecs_query_data_t) local observable = world.observable :: ecs_observable_t local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then - on_create_action = {} :: Map + on_create_action = {} observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] @@ -1809,7 +1765,7 @@ local function query_cached(query: ecs_query_data_t) local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then - on_delete_action = {} :: Map + on_delete_action = {} observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] @@ -2212,12 +2168,12 @@ local function world_query(world: ecs_world_t, ...) return q end - if idr == nil or (map.size :: number) < (idr.size :: number) then + if idr == nil or map.size < idr.size then idr = map end end - if idr == nil then + if not idr then return q end @@ -2350,7 +2306,7 @@ local function world_new() ROOT_ARCHETYPE = (nil :: any) :: Archetype, max_archetype_id = 0, - max_component_id = ecs_max_component_id, + max_component_id = 0, observable = {} :: Observable, }, World) :: any @@ -2367,21 +2323,6 @@ local function world_new() entity_index_new_id(entity_index) end - for i = EcsRest + 1, ecs_max_tag_id do - -- Initialize built-in components - entity_index_new_id(entity_index) - end - - for i, bundle in ecs_metadata do - for ty, value in bundle do - if value == NULL then - world_add(self, i, ty) - else - world_set(self, i, ty, value) - end - end - end - world_add(self, EcsName, EcsComponent) world_add(self, EcsOnChange, EcsComponent) world_add(self, EcsOnAdd, EcsComponent) @@ -2409,25 +2350,22 @@ end World.new = world_new -export type Entity = { __T: T } -export type Id = { __T: T } +export type Entity = { __T: T } +export type Id = { __T: T } export type Pair = Id

type ecs_id_t = Id | Pair | Pair<"Tag", T> export type Item = (self: Query) -> (Entity, T...) export type Iter = (query: Query) -> () -> (Entity, T...) -export type Query = typeof(setmetatable( - {} :: { - iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, - }, - {} :: { - __iter: Iter - } -)) +export type Query = typeof(setmetatable({}, { + __iter = (nil :: any) :: Iter, +})) & { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, +} export type Observer = { callback: (archetype: Archetype) -> (), @@ -2461,20 +2399,20 @@ export type World = { component: (self: World) -> Entity, --- Gets the target of an relationship. For example, when a user calls --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. - target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, + target: (self: World, id: Entity, relation: Id, index: number?) -> Entity?, --- Deletes an entity and all it's related components and relationships. delete: (self: World, id: Entity) -> (), --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), + add: (self: World, id: Entity, component: Id) -> (), --- Assigns a value to a component on the given entity set: (self: World, id: Entity, component: Id, data: T) -> (), cleanup: (self: World) -> (), -- Clears an entity from the world - clear: (self: World, id: Id) -> (), + clear: (self: World, id: Entity) -> (), --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), + remove: (self: World, id: Entity, component: Id) -> (), --- Retrieves the value of up to 4 components. These values may be nil. get: ((self: World, id: Entity, Id) -> A?) & ((self: World, id: Entity, Id, Id) -> (A?, B?)) @@ -2493,9 +2431,9 @@ export type World = { --- Checks if the world contains the given entity contains:(self: World, entity: Entity) -> boolean, - each: (self: World, id: Id) -> () -> Entity, + each: (self: World, id: Id) -> () -> Entity, - children: (self: World, id: Id) -> () -> Entity, + children: (self: World, id: Id) -> () -> Entity, --- Searches the world for entities that match a given query query: ((World, Id) -> Query) @@ -2523,15 +2461,10 @@ export type World = { -- return first -- end -- end --- return { World = World :: { new: () -> World }, 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, - is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, @@ -2554,8 +2487,8 @@ return { ECS_GENERATION_INC = ECS_GENERATION_INC, ECS_GENERATION = ECS_GENERATION, ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, + ECS_ID_DELETE = ECS_ID_DELETE, - ECS_META_RESET = ECS_META_RESET, IS_PAIR = (ECS_IS_PAIR :: any) :: (pair: Pair) -> boolean, pair_first = (ecs_pair_first :: any) :: (world: World, pair: Pair) -> Id

, diff --git a/jecs/pesde-rbx.toml b/jecs/pesde-rbx.toml index bede318..490ef69 100644 --- a/jecs/pesde-rbx.toml +++ b/jecs/pesde-rbx.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/marked/jecs-nightly" -version = "0.5.5-nightly.20250423T001100Z" +version = "0.5.5-nightly.20250414T001100Z" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/jecs/pesde.toml b/jecs/pesde.toml index 21ecf1a..ac85098 100644 --- a/jecs/pesde.toml +++ b/jecs/pesde.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/marked/jecs-nightly" -version = "0.5.5-nightly.20250423T001100Z" +version = "0.5.5-nightly.20250414T001100Z" [indices] default = "https://github.com/pesde-pkg/index" diff --git a/jecs/test.txt b/jecs/test.txt index 3762876..47c96db 100644 --- a/jecs/test.txt +++ b/jecs/test.txt @@ -1,2 +1,2 @@ passed = true -timestamp = "20250423T001103Z" +timestamp = "20250422T001103Z" diff --git a/jecs/test_fulllog.txt b/jecs/test_fulllog.txt index 902e875..443dcfb 100644 --- a/jecs/test_fulllog.txt +++ b/jecs/test_fulllog.txt @@ -1,6 +1,6 @@ -7.7 us  2 kB│ delete children of entity -9.1 us  1 kB│ remove friends of entity -348 ns  0  B│ simple deletion of entity +7.2 us  3 kB│ delete children of entity +9.2 us  1 kB│ remove friends of entity +340 ns  0  B│ simple deletion of entity world:add() PASS│ idempotent PASS│ archetype move @@ -13,7 +13,6 @@ PASS│ remove cleared ID from entities world:component() -PASS│ allow IDs to be registered PASS│ only components should have EcsComponent trait PASS│ tag @@ -113,5 +112,5 @@ PASS│ #2 PASS│ #3 -70/70 test cases passed in 29.973 ms. +69/69 test cases passed in 29.775 ms. 0 fails diff --git a/jecs/wally.toml b/jecs/wally.toml index edcbfe1..c73b22a 100644 --- a/jecs/wally.toml +++ b/jecs/wally.toml @@ -5,4 +5,4 @@ license = "MIT" name = "mark-marks/jecs-nightly" realm = "shared" registry = "https://github.com/UpliftGames/wally-index" -version = "0.5.5-nightly.20250423T001100Z" +version = "0.5.5-nightly.20250414T001100Z"