diff --git a/.gitattributes b/.gitattributes deleted file mode 100755 index fdc9e36..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -*.luau text eol=lf -*.html linguist-vendored diff --git a/.gitignore b/.gitignore index 4d762af..5445cd0 100755 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,7 @@ profile.* *.patch genhtml.perl + +rokit.toml +package-lock.json +mirror.luau diff --git a/.luaurc b/.luaurc index 687382f..beb6617 100755 --- a/.luaurc +++ b/.luaurc @@ -1,10 +1,8 @@ { "aliases": { - "jecs": "./jecs", - "testkit": "./tools/testkit", - "mirror": "./mirror", - "tools": "./tools", - "addons": "./addons" + "jecs": "src/jecs", + "modules": "modules", + "mirror": "src/mirror", }, "languageMode": "strict" } diff --git a/.prettierrc b/.prettierrc deleted file mode 100755 index 537ac33..0000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 4, - "trailingComma": "all", - "useTabs": true -} diff --git a/.stylua.toml b/.stylua.toml deleted file mode 100755 index 554fc2f..0000000 --- a/.stylua.toml +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "All" -column_width = 120 -line_endings = "Unix" -indent_type = "Tabs" -indent_width = 4 -quote_style = "AutoPreferDouble" -call_parentheses = "Always" -space_after_function_names = "Never" -collapse_simple_statement = "Never" diff --git a/benches/100k.luau b/benches/100k.luau deleted file mode 100755 index 8f64d52..0000000 --- a/benches/100k.luau +++ /dev/null @@ -1,37 +0,0 @@ - ---!optimize 2 ---!native - -local testkit = require("@testkit") -local BENCH, START = testkit.benchmark() -local function TITLE(title: string) - print() - print(testkit.color.white(title)) -end - -local jecs = require("@jecs") -local mirror = require("@mirror") - -do - TITLE(testkit.color.white_underline("Jecs query")) - local world = jecs.world() :: jecs.World - - local A = world:component() - - for i = 1, 100_000 do - local e = world:entity() - world:set(e, A, true) - end - - local archetypes = world:query(A):archetypes() - - BENCH("", function() - for _, archetype in archetypes do - local column = archetype.columns[1] - for row, entity in archetype.entities do - local data = column[row] - end - end - end) - -end diff --git a/bench.project.json b/benches/default.project.json similarity index 93% rename from bench.project.json rename to benches/default.project.json index 04596de..3f11dca 100755 --- a/bench.project.json +++ b/benches/default.project.json @@ -12,7 +12,7 @@ "ReplicatedStorage": { "$className": "ReplicatedStorage", "Lib": { - "$path": "jecs.luau" + "$path": "../src/jecs.luau" }, "benches": { "$path": "benches" diff --git a/benches/general.luau b/benches/general.luau index 2902e95..362dbdb 100755 --- a/benches/general.luau +++ b/benches/general.luau @@ -1,5 +1,5 @@ local jecs = require("@jecs") -local testkit = require("@testkit") +local testkit = require("@modules/testkit") local BENCH, START = testkit.benchmark() diff --git a/benches/query.luau b/benches/query.luau index c79653a..32560a9 100755 --- a/benches/query.luau +++ b/benches/query.luau @@ -1,7 +1,7 @@ --!optimize 2 --!native -local testkit = require("@testkit") +local testkit = require("@modules/testkit") local BENCH, START = testkit.benchmark() local function TITLE(title: string) print() diff --git a/default.project.json b/default.project.json index d4531a0..385789b 100755 --- a/default.project.json +++ b/default.project.json @@ -1,6 +1,6 @@ { "name": "jecs", "tree": { - "$path": "jecs.luau" + "$path": "src/jecs.luau" } } diff --git a/howto/001_hello_world.luau b/how_to/001_hello_world.luau similarity index 100% rename from howto/001_hello_world.luau rename to how_to/001_hello_world.luau diff --git a/howto/002_entities.luau b/how_to/002_entities.luau similarity index 100% rename from howto/002_entities.luau rename to how_to/002_entities.luau diff --git a/howto/003_components.luau b/how_to/003_components.luau similarity index 100% rename from howto/003_components.luau rename to how_to/003_components.luau diff --git a/howto/004_tags.luau b/how_to/004_tags.luau similarity index 100% rename from howto/004_tags.luau rename to how_to/004_tags.luau diff --git a/howto/005_entity_singletons.luau b/how_to/005_entity_singletons.luau similarity index 100% rename from howto/005_entity_singletons.luau rename to how_to/005_entity_singletons.luau diff --git a/howto/010_how_components_works.luau b/how_to/010_how_components_works.luau similarity index 100% rename from howto/010_how_components_works.luau rename to how_to/010_how_components_works.luau diff --git a/howto/011_preregistering_components.luau b/how_to/011_preregistering_components.luau similarity index 100% rename from howto/011_preregistering_components.luau rename to how_to/011_preregistering_components.luau diff --git a/howto/013_pairs.luau b/how_to/013_pairs.luau similarity index 100% rename from howto/013_pairs.luau rename to how_to/013_pairs.luau diff --git a/howto/020_queries.luau b/how_to/020_queries.luau similarity index 100% rename from howto/020_queries.luau rename to how_to/020_queries.luau diff --git a/howto/021_query_operators.luau b/how_to/021_query_operators.luau similarity index 100% rename from howto/021_query_operators.luau rename to how_to/021_query_operators.luau diff --git a/howto/022_query_caching.luau b/how_to/022_query_caching.luau similarity index 100% rename from howto/022_query_caching.luau rename to how_to/022_query_caching.luau diff --git a/howto/030_archetypes.luau b/how_to/030_archetypes.luau similarity index 100% rename from howto/030_archetypes.luau rename to how_to/030_archetypes.luau diff --git a/howto/040_fragmentation.luau b/how_to/040_fragmentation.luau similarity index 100% rename from howto/040_fragmentation.luau rename to how_to/040_fragmentation.luau diff --git a/howto/041_entity_relationships.luau b/how_to/041_entity_relationships.luau similarity index 100% rename from howto/041_entity_relationships.luau rename to how_to/041_entity_relationships.luau diff --git a/howto/100_cleanup_traits.luau b/how_to/100_cleanup_traits.luau similarity index 100% rename from howto/100_cleanup_traits.luau rename to how_to/100_cleanup_traits.luau diff --git a/howto/110_hooks.luau b/how_to/110_hooks.luau similarity index 100% rename from howto/110_hooks.luau rename to how_to/110_hooks.luau diff --git a/mirror.luau b/mirror.luau deleted file mode 100755 index 35c8c6b..0000000 --- a/mirror.luau +++ /dev/null @@ -1,3415 +0,0 @@ - ---!optimize 2 ---!native ---!strict ---draft 4 - -type i53 = number -type i24 = number - -type Ty = { Entity } -type ArchetypeId = number - -type Column = { any } - -type Map = { [K]: V } - -export type Archetype = { - id: number, - types: Ty, - type: string, - entities: { Entity }, - columns: { Column }, - columns_map: { [Id]: Column } -} - -export type QueryInner = { - compatible_archetypes: { Archetype }, - ids: { Id }, - filter_with: { Id }, - filter_without: { Id }, - next: () -> (Entity, ...any), - world: World, -} - -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: ((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, - ids: { Id }, - -- world: World - }, - {} :: { - __iter: Iter, - } -)) - -type QueryArm = () -> () - -export type Observer = { - callback: (archetype: Archetype) -> (), - query: QueryInner, -} - -type query = { - compatible_archetypes: { archetype }, - ids: { i53 }, - filter_with: { i53 }, - filter_without: { i53 }, - next: () -> (i53, ...any), - world: World, -} - -export type observer = { - callback: (archetype: archetype) -> (), - query: query, -} - -type archetype = { - id: number, - types: { i53 }, - type: string, - entities: { i53 }, - columns: { Column }, - columns_map: { [i53]: Column } -} - -type componentrecord = { - records: { [number]: number }, - counts: { [i53]: number }, - flags: number, - size: number, - - on_add: ((entity: i53, id: i53, value: any?) -> ())?, - on_change: ((entity: i53, id: i53, value: any) -> ())?, - on_remove: ((entity: i53, id: i53) -> ())?, - - wildcard_pairs: { [number]: componentrecord }, -} -type record = { - archetype: archetype, - row: number, - dense: i24, -} -type entityindex = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, - range_begin: number?, - range_end: number?, -} -type world = { - archetype_edges: Map>, - archetype_index: { [string]: archetype }, - archetypes: { [i53]: archetype }, - component_index: Map, - entity_index: entityindex, - ROOT_ARCHETYPE: archetype, - - max_component_id: number, - max_archetype_id: number, - - observable: Map>, - - range: (self: world, range_begin: number, range_end: number?) -> (), - entity: (self: world, id: i53?) -> i53, - component: (self: world) -> i53, - target: (self: world, id: i53, relation: i53, index: number?) -> i53?, - delete: (self: world, id: i53) -> (), - add: (self: world, id: i53, component: i53) -> (), - set: (self: world, id: i53, component: i53, data: any) -> (), - cleanup: (self: world) -> (), - clear: (self: world, id: i53) -> (), - remove: (self: world, id: i53, component: i53) -> (), - get: (world, ...i53) -> (), - has: (world, ...i53) -> boolean, - parent: (self: world, entity: i53) -> i53?, - contains: (self: world, entity: i53) -> boolean, - exists: (self: world, entity: i53) -> boolean, - each: (self: world, id: i53) -> () -> i53, - children: (self: world, id: i53) -> () -> i53, - query: (world, ...i53) -> Query<...any>, - - added: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (), - changed: (world, i53, (e: i53, id: i53, value: any?) -> ()) -> () -> (), - removed: (world, i53, (e: i53, id: i53) -> ()) -> () -> (), -} - -export type World = { - archetype_edges: Map>, - archetype_index: { [string]: Archetype }, - archetypes: Archetypes, - component_index: ComponentIndex, - entity_index: EntityIndex, - ROOT_ARCHETYPE: Archetype, - - max_component_id: number, - max_archetype_id: number, - - observable: Map>, - - added: (World, Entity, (e: Entity, id: Id, value: T?) -> ()) -> () -> (), - removed: (World, Entity, (e: Entity, id: Id) -> ()) -> () -> (), - changed: (World, Entity, (e: Entity, id: Id, value: T) -> ()) -> () -> (), - - --- Enforce a check on entities to be created within desired range - range: (self: World, range_begin: number, range_end: number?) -> (), - - --- Creates a new entity - entity: (self: World, id: (number | Entity)?) -> Entity, - --- Creates a new entity located in the first 256 ids. - --- These should be used for static components for fast access. - 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?, - --- 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) -> (), - --- Assigns a value to a component on the given entity - set: (self: World, id: Entity, component: Id, data: a) -> (), - - cleanup: (self: World) -> (), - -- Clears an entity from the world - clear: (self: World, id: Id) -> (), - --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), - --- Retrieves the value of up to 4 components. These values may be nil. - get: & ((World, Entity, Id) -> a?) - & ((World, Entity, Id, Id) -> (a?, b?)) - & ((World, Entity, Id, Id, Id) -> (a?, b?, c?)) - & ((World, Entity, Id, Id, Id, Id) -> (a?, b?, c?, d?)), - - --- Returns whether the entity has the ID. - has: ((World, Entity, Id) -> boolean) - & ((World, Entity, Id, Id) -> boolean) - & ((World, Entity, Id, Id, Id) -> boolean) - & (World, Entity, Id, Id, Id, Id) -> boolean, - - --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity?, - - --- Checks if the world contains the given entity - contains: (self: World, entity: Entity) -> boolean, - - --- Checks if the entity exists - exists: (self: World, entity: Entity) -> boolean, - - each: (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) - & ((World, Id, Id) -> Query) - & ((World, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id) -> Query) - & (( - World, - Id, - Id, - Id, - Id, - Id, - Id, - Id - ) -> Query) - & (( - World, - Id, - Id, - Id, - Id, - Id, - Id, - Id, - Id, - ...Id - ) -> Query), -} - -export type Record = { - archetype: Archetype, - row: number, - dense: i24, -} -export type ComponentRecord = { - records: { [i24]: number }, - counts: { [i24]: number }, - flags: number, - size: number, - - on_add: ((entity: Entity, id: Entity, value: T?) -> ())?, - on_change: ((entity: Entity, id: Entity, value: T) -> ())?, - on_remove: ((entity: Entity, id: Entity) -> ())?, -} -export type ComponentIndex = Map -export type Archetypes = { [i24]: Archetype } - -export type EntityIndex = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, - range_begin: number?, - range_end: number?, -} - --- stylua: ignore start - -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 HI_COMPONENT_ID = 256 -local EcsOnAdd = HI_COMPONENT_ID + 1 -local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnChange = HI_COMPONENT_ID + 3 -local EcsWildcard = HI_COMPONENT_ID + 4 -local EcsChildOf = HI_COMPONENT_ID + 5 -local EcsComponent = HI_COMPONENT_ID + 6 -local EcsOnDelete = HI_COMPONENT_ID + 7 -local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 -local EcsDelete = HI_COMPONENT_ID + 9 -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 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 function ecs_assert(condition, msg: string?) - if not condition then - error(msg) - end -end - -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 = {} :: Map - 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 - -local function ECS_IS_PAIR(e: number): boolean - return e > ECS_PAIR_OFFSET -end - -local function ECS_GENERATION_INC(e: i53): i53 - if e > ECS_ENTITY_MASK then - local id = e % ECS_ENTITY_MASK - local generation = e // ECS_ENTITY_MASK - - local next_gen = generation + 1 - if next_gen >= ECS_GENERATION_MASK then - return id - end - - return ECS_COMBINE(id, next_gen) - end - return ECS_COMBINE(e, 1) -end - -local function ECS_ENTITY_T_LO(e: i53): i24 - return e % ECS_ENTITY_MASK -end - -local function ECS_ID(e: i53) - return e % ECS_ENTITY_MASK -end - -local function ECS_GENERATION(e: i53) - return e // ECS_ENTITY_MASK -end - -local function ECS_ENTITY_T_HI(e: i53): i24 - return e // ECS_ENTITY_MASK -end - -local function ECS_PAIR(pred: i53, obj: i53): i53 - pred %= ECS_ENTITY_MASK - obj %= ECS_ENTITY_MASK - - return obj + (pred * ECS_ENTITY_MASK) + ECS_PAIR_OFFSET -end - -local function ECS_PAIR_FIRST(e: i53): i24 - return (e - ECS_PAIR_OFFSET) // ECS_ENTITY_MASK -end - -local function ECS_PAIR_SECOND(e: i53): i24 - return (e - ECS_PAIR_OFFSET) % ECS_ENTITY_MASK -end - -local function entity_index_try_get_any( - entity_index: entityindex, - entity: i53 -): record? - local r = entity_index.sparse_array[ECS_ID(entity)] - - if not r or r.dense == 0 then - return nil - end - - return r -end - -local function entity_index_try_get(entity_index: entityindex, entity: i53): record? - local r = entity_index_try_get_any(entity_index, entity) - if r then - local r_dense = r.dense - if r_dense > entity_index.alive_count then - return nil - end - if entity_index.dense_array[r_dense] ~= entity then - return nil - end - end - return r -end - -local function entity_index_try_get_fast(entity_index: entityindex, entity: i53): record? - local r = entity_index_try_get_any(entity_index, entity) - if r then - local r_dense = r.dense - -- if r_dense > entity_index.alive_count then - -- return nil - -- end - if entity_index.dense_array[r_dense] ~= entity then - return nil - end - end - return r -end - -local function entity_index_is_alive(entity_index: entityindex, entity: i53): boolean - return entity_index_try_get(entity_index, entity) ~= nil -end - -local function entity_index_get_alive(entity_index: entityindex, entity: i53): i53? - local r = entity_index_try_get_any(entity_index, entity :: number) - if r then - return entity_index.dense_array[r.dense] - end - return nil -end - -local function ecs_get_alive(world: world, entity: i53): i53 - if entity == 0 then - return 0 - end - - local eindex = world.entity_index - - if entity_index_is_alive(eindex, entity) then - return entity - end - - if (entity :: number) > ECS_ENTITY_MASK then - return 0 - end - - local current = entity_index_get_alive(eindex, entity) - if not current or not entity_index_is_alive(eindex, current) then - return 0 - end - - return current -end - -local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range" - -local function entity_index_new_id(entity_index: entityindex): i53 - local dense_array = entity_index.dense_array - local alive_count = entity_index.alive_count - local sparse_array = entity_index.sparse_array - local max_id = entity_index.max_id - - if alive_count < max_id then - alive_count += 1 - entity_index.alive_count = alive_count - local id = dense_array[alive_count] - return id - end - - local id = max_id + 1 - local range_end = entity_index.range_end - ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY) - - entity_index.max_id = id - alive_count += 1 - entity_index.alive_count = alive_count - dense_array[alive_count] = id - sparse_array[id] = { dense = alive_count } :: record - - return id -end - -local function ecs_pair_first(world: world, e: i53) - local pred = ECS_PAIR_FIRST(e) - return ecs_get_alive(world, pred) -end - -local function ecs_pair_second(world: world, e: i53) - local obj = ECS_PAIR_SECOND(e) - return ecs_get_alive(world, obj) -end - -local function query_match(query: query, archetype: archetype) - local columns_map = archetype.columns_map - local with = query.filter_with - - for _, id in with do - if not columns_map[id] then - return false - end - end - - local without = query.filter_without - if without then - for _, id in without do - if columns_map[id] then - return false - end - end - end - - return true -end - -local function find_observers(world: world, event: i53, component: i53): { observer }? - local cache = world.observable[event] - if not cache then - return nil - end - return cache[component] :: any -end - -local function archetype_move( - entity_index: entityindex, - entity: i53, - to: archetype, - dst_row: i24, - from: archetype, - src_row: i24 -) - local src_columns = from.columns - local dst_entities = to.entities - local src_entities = from.entities - - local last = #src_entities - local id_types = from.types - local columns_map = to.columns_map - - if src_row ~= last then - -- If the entity is the last row in the archetype then swapping it would be meaningless. - - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - -- Retrieves the new column index from the source archetype's record from each component - -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local dst_column = columns_map[id_types[i]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if dst_column then - dst_column[dst_row] = column[src_row] - end - - -- Swap rempves columns to ensure there are no holes in the archetype. - column[src_row] = column[last] - column[last] = nil - end - - - -- Move the entity from the source to the destination archetype. - -- Because we have swapped columns we now have to update the records - -- corresponding to the entities' rows that were swapped. - - local e2 = src_entities[last] - src_entities[src_row] = e2 - - local sparse_array = entity_index.sparse_array - local record2 = sparse_array[ECS_ID(e2)] - record2.row = src_row - else - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - -- Retrieves the new column index from the source archetype's record from each component - -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local dst_column = columns_map[id_types[i]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if dst_column then - dst_column[dst_row] = column[src_row] - end - - column[last] = nil - end - end - - src_entities[last] = nil - dst_entities[dst_row] = entity -end - -local function archetype_append( - entity: i53, - archetype: archetype -): number - local entities = archetype.entities - local length = #entities + 1 - entities[length] = entity - return length -end - -local function new_entity( - entity: i53, - record: record, - archetype: archetype -): record - local row = archetype_append(entity, archetype) - record.archetype = archetype - record.row = row - return record -end - -local function entity_move( - entity_index: entityindex, - entity: i53, - record: record, - to: archetype -) - local sourceRow = record.row - local from = record.archetype - local dst_row = archetype_append(entity, to) - archetype_move(entity_index, entity, to, dst_row, from, sourceRow) - record.archetype = to - record.row = dst_row -end - -local function hash(arr: { i53 }): string - return table.concat(arr, "_") -end - -local function fetch(id: i53, columns_map: { [i53]: Column }, row: number): any - local column = columns_map[id] - - if not column then - return nil - end - - return column[row] -end - -local function world_get(world: world, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local record = entity_index_try_get(world.entity_index, entity) - if not record then - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - local columns_map = archetype.columns_map - local row = record.row - - local va = fetch(a, columns_map, row) - - if not b then - return va - elseif not c then - return va, fetch(b, columns_map, row) - elseif not d then - return va, fetch(b, columns_map, row), fetch(c, columns_map, row) - elseif not e then - return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row) - else - error("args exceeded") - end -end - -local function world_has_one_inline(world: world, entity: i53, id: i53): boolean - local record = entity_index_try_get(world.entity_index, entity) - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - return archetype.columns_map[id] ~= nil -end - -local function world_target(world: world, entity: i53, relation: i53, index: number?): i53? - local entity_index = world.entity_index - local record = entity_index_try_get(entity_index, entity) - if not record then - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - local r = ECS_PAIR(relation, EcsWildcard) - local idr = world.component_index[r] - - if not idr then - return nil - end - - local archetype_id = archetype.id - local count = idr.counts[archetype_id] - if not count then - return nil - end - - local nth = index or 0 - - if nth >= count then - return nil - end - - nth = archetype.types[nth + idr.records[archetype_id]] - - if not nth then - return nil - end - - return entity_index_get_alive(entity_index, - ECS_PAIR_SECOND(nth :: number)) -end - -local function ECS_ID_IS_WILDCARD(e: i53): boolean - local first = ECS_ENTITY_T_HI(e) - local second = ECS_ENTITY_T_LO(e) - return first == EcsWildcard or second == EcsWildcard -end - -local function id_record_get(world: World, id: Entity): ComponentRecord? - local component_index = world.component_index - local idr: ComponentRecord = component_index[id] - - if idr then - return idr - end - - return nil -end - -local function id_record_ensure(world: world, id: i53): componentrecord - local component_index = world.component_index - local entity_index = world.entity_index - local idr: componentrecord? = component_index[id] - - if idr then - return idr - end - - local flags = ECS_ID_MASK - local relation = id - local target = 0 - 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)) :: i53 - ecs_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 - ecs_assert(target and entity_index_is_alive( - entity_index, target), ECS_INTERNAL_ERROR) - - local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) - - if cleanup_policy_target == EcsDelete then - has_delete = true - end - - if world_has_one_inline(world, relation, EcsExclusive) then - is_exclusive = true - end - end - - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - - if cleanup_policy == 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, - if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0 - ) - - idr = { - size = 0, - records = {}, - counts = {}, - flags = flags, - - on_add = on_add, - on_change = on_change, - on_remove = on_remove, - } :: componentrecord - - component_index[id] = idr - - return idr -end - -local function archetype_append_to_records( - idr: componentrecord, - archetype_id: number, - columns_map: { [i53]: Column }, - id: i53, - index: number, - column: Column -) - local idr_records = idr.records - local idr_counts = idr.counts - local tr = idr_records[archetype_id] - if not tr then - idr_records[archetype_id] = index - idr_counts[archetype_id] = 1 - columns_map[id] = column - else - local max_count = idr_counts[archetype_id] + 1 - idr_counts[archetype_id] = max_count - end -end - -local function archetype_create(world: world, id_types: { i53 }, ty, prev: i53?): archetype - local archetype_id = (world.max_archetype_id :: number) + 1 - world.max_archetype_id = archetype_id - - local length = #id_types - local columns = (table.create(length) :: any) :: { Column } - - local columns_map: { [i53]: Column } = {} - - local archetype: archetype = { - columns = columns, - columns_map = columns_map, - entities = {}, - id = archetype_id, - type = ty, - types = id_types - } - - for i, component_id in archetype.types do - local idr = id_record_ensure(world, component_id) - idr.size += 1 - local is_tag = bit32.btest(idr.flags, ECS_ID_IS_TAG) - local column = if is_tag then NULL_ARRAY else {} - columns[i] = column - - archetype_append_to_records(idr, archetype_id, columns_map, component_id :: number, i, column) - - if ECS_IS_PAIR(component_id) then - local relation = ECS_PAIR_FIRST(component_id) - local object = ECS_PAIR_SECOND(component_id) - - local r = ECS_PAIR(relation, EcsWildcard) - local idr_r = id_record_ensure(world, r) - - idr_r.size += 1 - archetype_append_to_records(idr_r, archetype_id, columns_map, r, i, column) - local idr_r_wc_pairs = idr_r.wildcard_pairs - if not idr_r_wc_pairs then - idr_r_wc_pairs = {} :: {[i53]: componentrecord } - idr_r.wildcard_pairs = idr_r_wc_pairs - end - idr_r_wc_pairs[component_id] = idr - - local t = ECS_PAIR(EcsWildcard, object) - local idr_t = id_record_ensure(world, t) - - idr_t.size += 1 - archetype_append_to_records(idr_t, archetype_id, columns_map, t, i, column) - - -- Hypothetically this should only capture leaf component records - local idr_t_wc_pairs = idr_t.wildcard_pairs - if not idr_t_wc_pairs then - idr_t_wc_pairs = {} :: {[i53]: componentrecord } - idr_t.wildcard_pairs = idr_t_wc_pairs - end - idr_t_wc_pairs[component_id] = idr - end - end - - world.archetype_index[archetype.type] = archetype - world.archetypes[archetype_id] = archetype - world.archetype_edges[archetype.id] = {} :: Map - - for id in columns_map do - local observer_list = find_observers(world, EcsOnArchetypeCreate, id) - if not observer_list then - continue - end - for _, observer in observer_list do - if query_match(observer.query, archetype) then - observer.callback(archetype::any) - end - end - end - - return archetype -end - -local function world_range(world: world, range_begin: number, range_end: number?) - local entity_index = world.entity_index - - entity_index.range_begin = range_begin - entity_index.range_end = range_end - - local max_id = entity_index.max_id - - if range_begin > max_id then - local dense_array = entity_index.dense_array - local sparse_array = entity_index.sparse_array - - for i = max_id + 1, range_begin do - dense_array[i] = i - sparse_array[i] = { - dense = 0 - } :: record - end - entity_index.max_id = range_begin - 1 - entity_index.alive_count = range_begin - 1 - end -end - -local function archetype_ensure(world: world, id_types: { i53 }): archetype - if #id_types < 1 then - return world.ROOT_ARCHETYPE - end - - local ty = hash(id_types) - local archetype = world.archetype_index[ty] - if archetype then - return archetype - end - - return archetype_create(world, id_types, ty) -end - -local function find_insert(id_types: { i53 }, toAdd: i53): number - for i, id in id_types do - if id == toAdd then - return -1 - end - if id > toAdd then - return i - end - end - return #id_types + 1 -end - -local function find_archetype_without( - world: world, - node: archetype, - id: i53 -): archetype - local id_types = node.types - local at = table.find(id_types, id) - - local dst = table.clone(id_types) - table.remove(dst, at) - - return archetype_ensure(world, dst) -end - - -local function create_edge_for_remove( - world: world, - node: archetype, - edge: Map, - id: i53 -): archetype - local to = find_archetype_without(world, node, id) - local edges = world.archetype_edges - local archetype_id = node.id - edges[archetype_id][id] = to - edges[to.id][id] = node - return to -end - -local function archetype_traverse_remove( - world: world, - id: i53, - from: archetype -): archetype - local edges = world.archetype_edges - local edge = edges[from.id] - - local to: archetype = edge[id] - if to == nil then - to = find_archetype_without(world, from, id) - edge[id] = to - edges[to.id][id] = from - end - - return to -end - -local function find_archetype_with( - world: world, - id: i53, - from: archetype -): archetype - local id_types = from.types - local dst = table.clone(id_types) - - local at = find_insert(id_types :: { number } , id :: number) - - table.insert(dst, at, id) - - return archetype_ensure(world, dst) -end - -local function archetype_traverse_add( - world: world, - id: i53, - from: archetype -): archetype - from = from or world.ROOT_ARCHETYPE - if from.columns_map[id] then - return from - end - local edges = world.archetype_edges - local edge = edges[from.id] - - local to = edge[id] - if not to then - to = find_archetype_with(world, id, from) - edge[id] = to - edges[to.id][id] = from - end - - return to -end - -local function world_component(world: world): i53 - local id = (world.max_component_id :: number) + 1 - if id > HI_COMPONENT_ID then - -- IDs are partitioned into ranges because component IDs are not nominal, - -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") - end - world.max_component_id = id - - return id -end - -local function archetype_fast_delete_last(columns: { Column }, column_count: number) - for i, column in columns do - if column ~= NULL_ARRAY then - column[column_count] = nil - end - end -end - -local function archetype_fast_delete(columns: { Column }, column_count: number, row: number) - for i, column in columns do - if column ~= NULL_ARRAY then - column[row] = column[column_count] - column[column_count] = nil - end - end -end - -local function archetype_delete(world: world, archetype: archetype, row: number) - local entity_index = world.entity_index - local columns = archetype.columns - local entities = archetype.entities - local column_count = #entities - local last = #entities - local move = entities[last] - -- We assume first that the entity is the last in the archetype - - if row ~= last then - local record_to_move = entity_index_try_get_any(entity_index, move) - if record_to_move then - record_to_move.row = row - end - - entities[row] = move - end - - entities[last] = nil :: any - - if row == last then - archetype_fast_delete_last(columns, column_count) - else - archetype_fast_delete(columns, column_count, row) - end -end - - -local function archetype_destroy(world: world, archetype: archetype) - if archetype == world.ROOT_ARCHETYPE then - return - end - - local component_index = world.component_index - local archetype_edges = world.archetype_edges - local edges = archetype_edges[archetype.id] - for id, node in edges do - archetype_edges[node.id][id] = nil - edges[id] = nil - end - - local archetype_id = archetype.id - world.archetypes[archetype_id] = nil :: any - world.archetype_index[archetype.type] = nil :: any - local columns_map = archetype.columns_map - - for id in columns_map do - local idr = component_index[id] - idr.records[archetype_id] = nil :: any - idr.counts[archetype_id] = nil - idr.size -= 1 - if idr.size == 0 then - component_index[id] = nil :: any - end - local observer_list = find_observers(world, EcsOnArchetypeDelete, id) - if not observer_list then - continue - end - for _, observer in observer_list do - if query_match(observer.query, archetype) then - observer.callback(archetype::any) - end - end - end -end - -local function NOOP() end - - -local function query_iter_init(query: QueryInner): () -> (number, ...any) - local world_query_iter_next - - local compatible_archetypes = query.compatible_archetypes - local lastArchetype = 1 - local archetype = compatible_archetypes[1] - if not archetype then - return NOOP :: () -> (number, ...any) - end - local entities = archetype.entities - local i = #entities - local columns_map = archetype.columns_map - - local ids = query.ids - local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column - - if 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] - 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] - end - - if not B 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] - end - - local row = i - i -= 1 - - return entity, a[row] - end - elseif not C 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row] - end - elseif not D 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row] - end - elseif not E 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row] - end - elseif not F 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row] - end - elseif not G 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - end - elseif not H 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - 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 - 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 - end - - query.next = world_query_iter_next - return world_query_iter_next -end - -local function query_iter(query): () -> (number, ...any) - local query_next = query.next - if not query_next then - query_next = query_iter_init(query) - end - return query_next -end - -local function query_without(query: QueryInner, ...: Id) - local without = { ... } - query.filter_without = without :: any - local compatible_archetypes = query.compatible_archetypes - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local columns_map = archetype.columns_map - local matches = true - - for _, id in without do - if columns_map[id] then - matches = false - break - end - end - - if matches then - continue - end - - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any - end - - return query :: any -end - -local function query_with(query: QueryInner, ...: Id) - local compatible_archetypes = query.compatible_archetypes - local with = { ... } :: any - query.filter_with = with - - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local columns_map = archetype.columns_map - local matches = true - - for _, id in with do - if not columns_map[id] then - matches = false - break - end - end - - if matches then - continue - end - - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any - end - - return query :: any -end - --- Meant for directly iterating over archetypes to minimize --- function call overhead. Should not be used unless iterating over --- hundreds of thousands of entities in bulk. -local function query_archetypes(query) - return query.compatible_archetypes -end - -local function query_cached(query: QueryInner) - local with = query.filter_with - local ids = query.ids - if with then - table.move(ids, 1, #ids, #with + 1, with) - else - query.filter_with = ids - end - - local compatible_archetypes = query.compatible_archetypes - local lastArchetype = 1 - - local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column - - local world_query_iter_next - local entities: { Entity } - local i: number - local archetype: Archetype - local columns_map: { [Id]: Column } - local archetypes = query.compatible_archetypes - - local world = query.world - -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively - -- because the event will be emitted for all components of that Archetype. - local observable = world.observable - local on_create_action = observable[EcsOnArchetypeCreate::any] - if not on_create_action then - on_create_action = {} :: Map - observable[EcsOnArchetypeCreate::any] = on_create_action - end - local query_cache_on_create = on_create_action[A] - if not query_cache_on_create then - query_cache_on_create = {} - on_create_action[A] = query_cache_on_create - end - - local on_delete_action = observable[EcsOnArchetypeDelete::any] - if not on_delete_action then - on_delete_action = {} :: Map - observable[EcsOnArchetypeDelete::any] = on_delete_action - end - local query_cache_on_delete = on_delete_action[A] - if not query_cache_on_delete then - query_cache_on_delete = {} - on_delete_action[A] = query_cache_on_delete - end - - local function on_create_callback(archetype) - table.insert(archetypes, archetype) - end - - local function on_delete_callback(archetype) - local i = table.find(archetypes, archetype) :: number - if i == nil then - return - end - local n = #archetypes - archetypes[i] = archetypes[n] - archetypes[n] = nil - end - - local observer_for_create = { query = query, callback = on_create_callback } - local observer_for_delete = { query = query, callback = on_delete_callback } - - 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 - return NOOP - end - entities = archetype.entities - i = #entities - columns_map = archetype.columns_map - if 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] - 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] - end - - return world_query_iter_next - end - - if not B 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] - end - - local row = i - i -= 1 - - return entity, a[row] - end - elseif not C 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row] - end - elseif not D 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row] - end - elseif not E 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row] - end - elseif not F 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row] - end - elseif not G 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - end - elseif not H 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] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - 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 - 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 - end - - local cached_query = query :: any - cached_query.archetypes = query_archetypes - cached_query.__iter = cached_query_iter - cached_query.iter = cached_query_iter - setmetatable(cached_query, cached_query) - return cached_query -end - -local Query = {} -Query.__index = Query -Query.__iter = query_iter -Query.iter = query_iter_init -Query.without = query_without -Query.with = query_with -Query.archetypes = query_archetypes -Query.cached = query_cached - -local function world_query(world: World, ...) - local compatible_archetypes = {} - local length = 0 - - local ids = { ... } - - local archetypes = world.archetypes - - local idr: ComponentRecord? - local component_index = world.component_index - - local q = setmetatable({ - ids = ids, - compatible_archetypes = compatible_archetypes, - world = world, - }, Query) - - for _, id in ids do - local map = component_index[id] - if not map then - return q - end - - if idr == nil or (map.size :: number) < (idr.size :: number) then - idr = map - end - end - - if idr == nil then - return q - end - - for archetype_id in idr.records do - local compatibleArchetype = archetypes[archetype_id] - if #compatibleArchetype.entities == 0 then - continue - end - local columns_map = compatibleArchetype.columns_map - - local skip = false - - for i, id in ids do - local column = columns_map[id] - if not column then - skip = true - break - end - end - - if skip then - continue - end - - length += 1 - compatible_archetypes[length] = compatibleArchetype - end - - return q -end - -local function world_each(world: world, id: i53): () -> i53 - local idr = world.component_index[id] - if not idr then - return NOOP :: () -> i53 - end - - local records = idr.records - local archetypes = world.archetypes - local archetype_id = next(records, nil) :: number - local archetype = archetypes[archetype_id] - if not archetype then - return NOOP :: () -> i53 - end - - local entities = archetype.entities - local row = #entities - - return function() - local entity = entities[row] - while not entity do - archetype_id = next(records, archetype_id) :: number - if not archetype_id then - return nil :: any - end - archetype = archetypes[archetype_id] - entities = archetype.entities - row = #entities - entity = entities[row] - end - row -= 1 - return entity - end -end - -local function world_children(world: world, parent: i53) - return world_each(world, ECS_PAIR(EcsChildOf, parent)) -end - -local function ecs_bulk_insert(world: world, entity: i53, ids: { i53 }, values: { any }) - local entity_index = world.entity_index - local r = entity_index_try_get(entity_index, entity) - if not r then - return - end - local from = r.archetype - local component_index = world.component_index - if not from then - local dst_types = ids - local to = archetype_ensure(world, dst_types) - new_entity(entity, r, to) - local row = r.row - local columns_map = to.columns_map - for i, id in ids do - local value = values[i] - local cdr = component_index[id] - - local on_add = cdr.on_add - if value then - columns_map[id][row] = value - if on_add then - on_add(entity, id, value :: any) - end - else - if on_add then - on_add(entity, id) - end - end - end - return - end - - local dst_types = table.clone(from.types) - - local emplaced: { [number]: boolean } = {} - - for i, id in ids do - local at = find_insert(dst_types :: { number }, id :: number) - if at == -1 then - emplaced[i] = true - continue - end - - emplaced[i] = false - - table.insert(dst_types, at, id) - end - - local to = archetype_ensure(world, dst_types) - local columns_map = to.columns_map - - if from ~= to then - entity_move(entity_index, entity, r, to) - end - local row = r.row - - for i, set in emplaced do - local id = ids[i] - local idr = component_index[id] - - local value = values[i] :: any - - local on_add = idr.on_add - - if value ~= nil then - columns_map[id][row] = value - local on_change = idr.on_change - local hook = if set then on_change else on_add - if hook then - hook(entity, id, value :: any) - end - elseif on_add then - on_add(entity, id) - end - end -end - -local function ecs_bulk_remove(world: world, entity: i53, ids: { i53 }) - local entity_index = world.entity_index - local r = entity_index_try_get(entity_index, entity) - if not r then - return - end - local from = r.archetype - local component_index = world.component_index - if not from then - return - end - - local remove: { [i53]: boolean } = {} - - local columns_map = from.columns_map - - for i, id in ids do - if not columns_map[id] then - continue - end - - remove[id] = true - local idr = component_index[id] - - local on_remove = idr.on_remove - if on_remove then - on_remove(entity, id) - end - end - - local to = r.archetype - if from ~= to then - from = to - end - - local dst_types = table.clone(from.types) :: { i53 } - - for id in remove do - local at = table.find(dst_types, id) - table.remove(dst_types, at) - end - - to = archetype_ensure(world, dst_types) - - if from ~= to then - entity_move(entity_index, entity, r, to) - end -end - -local function world_new() - local eindex_dense_array = {} :: { i53 } - local eindex_sparse_array = {} :: { record } - - local entity_index = { - dense_array = eindex_dense_array, - sparse_array = eindex_sparse_array, - alive_count = 0, - max_id = 0, - } :: entityindex - - local component_index = {} :: Map - - local archetype_index = {} :: { [string]: archetype } - local archetypes = {} :: Map - local archetype_edges = {} :: { [number]: { [i53]: archetype } } - - local observable = {} - - type Signal = { [i53]: { Listener } } - - local signals = { - added = {} :: Signal, - changed = {} :: Signal, - removed = {} :: Signal - } - - local world = { - archetype_edges = archetype_edges, - - component_index = component_index, - entity_index = entity_index, - ROOT_ARCHETYPE = nil :: any, - - archetypes = archetypes, - archetype_index = archetype_index, - max_archetype_id = 0, - max_component_id = ecs_max_component_id, - - observable = observable, - signals = signals, - } :: world - - - - local ROOT_ARCHETYPE = archetype_create(world, {}, "") - world.ROOT_ARCHETYPE = ROOT_ARCHETYPE - - local function inner_entity_index_try_get_any(entity: i53): record? - local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] - return r - end - - local function inner_archetype_move( - entity: i53, - to: archetype, - dst_row: i24, - from: archetype, - src_row: i24 - ) - local src_columns = from.columns - local dst_entities = to.entities - local src_entities = from.entities - - local last = #src_entities - local id_types = from.types - local columns_map = to.columns_map - - if src_row ~= last then - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - local dst_column = columns_map[id_types[i]] - - if dst_column then - dst_column[dst_row] = column[src_row] - end - - column[src_row] = column[last] - column[last] = nil - end - - local e2 = src_entities[last] - src_entities[src_row] = e2 - - local record2 = eindex_sparse_array[ECS_ENTITY_T_LO(e2)] - record2.row = src_row - else - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - -- Retrieves the new column index from the source archetype's record from each component - -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local dst_column = columns_map[id_types[i]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if dst_column then - dst_column[dst_row] = column[src_row] - end - - column[last] = nil - end - end - src_entities[last] = nil :: any - dst_entities[dst_row] = entity - end - - local function inner_entity_move( - entity_index: entityindex, - entity: i53, - record: record, - to: archetype - ) - local sourceRow = record.row - local from = record.archetype - local dst_row = archetype_append(entity, to) - inner_archetype_move(entity, to, dst_row, from, sourceRow) - record.archetype = to - record.row = dst_row - end - - -- local function inner_entity_index_try_get(entity: number): Record? - -- local r = inner_entity_index_try_get_any(entity) - -- if r then - -- local r_dense = r.dense - -- if r_dense > entity_index.alive_count then - -- return nil - -- end - -- if eindex_dense_array[r_dense] ~= entity then - -- return nil - -- end - -- end - -- return r - -- end - - local function inner_entity_index_try_get_unsafe(entity: i53): record? - local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] - if r then - local r_dense = r.dense - -- if r_dense > entity_index.alive_count then - -- return nil - -- end - if eindex_dense_array[r_dense] ~= entity then - return nil - end - end - return r - end - - local function exclusive_traverse_add( - archetype: archetype, - cr: number, - id: i53 - ) - local edge = archetype_edges[archetype.id] - local to = edge[id] - if not to then - local dst = table.clone(archetype.types) - dst[cr] = id - to = archetype_ensure(world, dst) - edge[id] = to - end - return to - end - - local function inner_world_set(world: world, entity: i53, id: i53, data): () - local record = inner_entity_index_try_get_unsafe(entity) - if not record then - return - end - - local from: archetype = record.archetype - local src = from or ROOT_ARCHETYPE - local column = src.columns_map[id] - if column then - local idr = component_index[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.on_change - if on_change then - on_change(entity, id, data) - end - else - local to: archetype - local idr: componentrecord - if ECS_IS_PAIR(id) then - local first = ECS_PAIR_FIRST(id) - local wc = ECS_PAIR(first, EcsWildcard) - idr = component_index[wc] - - local edge = archetype_edges[src.id] - to = edge[id] - if to == nil then - if idr and (bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) == true) then - local cr = idr.records[src.id] - if cr then - local on_remove = idr.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 - - to = exclusive_traverse_add(src, cr, id) - end - end - - if not to then - to = find_archetype_with(world, id, src) - if not idr then - idr = component_index[wc] - end - edge[id] = to - archetype_edges[(to :: Archetype).id][id] = src - end - else - if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then - local on_remove = idr.on_remove - if on_remove then - local cr = idr.records[src.id] - if cr then - local id_types = src.types - on_remove(entity, id_types[cr]) - local arche = record.archetype - if src ~= arche then - id_types = arche.types - cr = idr.records[arche.id] - to = exclusive_traverse_add(arche, cr, id) - end - end - end - end - end - else - local edges = archetype_edges - local edge = edges[src.id] - - to = edge[id] - if not to then - to = find_archetype_with(world, id, src) - edge[id] = to - edges[to.id][id] = src - end - idr = component_index[id] - end - - if from then - -- If there was a previous archetype, then the entity needs to move the archetype - inner_entity_move(entity_index, entity, record, to) - else - new_entity(entity, record, to) - end - - column = to.columns_map[id] - column[record.row] = data - - local on_add = idr.on_add - if on_add then - on_add(entity, id, data) - end - end - end - - local function inner_world_add( - world: world, - entity: i53, - id: i53 - ): () - local entity_index = world.entity_index - local record = inner_entity_index_try_get_unsafe(entity :: number) - if not record then - return - end - - local from = record.archetype - local src = from or ROOT_ARCHETYPE - if src.columns_map[id] then - return - end - local to: archetype - local idr: componentrecord - - if ECS_IS_PAIR(id) then - local first = ECS_PAIR_FIRST(id) - local wc = ECS_PAIR(first, EcsWildcard) - idr = component_index[wc] - - local edge = archetype_edges[src.id] - to = edge[id] - if to == nil then - if idr and (bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) == true) then - local cr = idr.records[src.id] - if cr then - local on_remove = idr.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 - - to = exclusive_traverse_add(src, cr, id) - end - end - - if not to then - to = find_archetype_with(world, id, src) - if not idr then - idr = component_index[wc] - end - edge[id] = to - archetype_edges[(to :: Archetype).id][id] = src - end - else - if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then - local on_remove = idr.on_remove - if on_remove then - local cr = idr.records[src.id] - if cr then - local id_types = src.types - on_remove(entity, id_types[cr]) - local arche = record.archetype - if src ~= arche then - id_types = arche.types - cr = idr.records[arche.id] - to = exclusive_traverse_add(arche, cr, id) - end - end - end - end - end - else - local edges = archetype_edges - local edge = edges[src.id] - - to = edge[id] - if not to then - to = find_archetype_with(world, id, src) - edge[id] = to - edges[to.id][id] = src - end - idr = component_index[id] - end - - if from then - inner_entity_move(entity_index, entity, record, to) - else - if #to.types > 0 then - new_entity(entity, record, to) - end - end - - local on_add = idr.on_add - - if on_add then - on_add(entity, id) - end - end - - local function inner_world_get(world: world, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local record = inner_entity_index_try_get_unsafe(entity) - if not record then - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - local columns_map = archetype.columns_map - local row = record.row - - local va = fetch(a, columns_map, row) - - if not b then - return va - elseif not c then - return va, fetch(b, columns_map, row) - elseif not d then - return va, fetch(b, columns_map, row), fetch(c, columns_map, row) - elseif not e then - return va, fetch(b, columns_map, row), fetch(c, columns_map, row), fetch(d, columns_map, row) - else - error("args exceeded") - end - end - - type Listener = (e: i53, id: i53, value: T?) -> () - - world.added = function(_: world, component: i53, fn: Listener) - local listeners = signals.added[component] - if not listeners then - listeners = {} - signals.added[component] = listeners - - local function on_add(entity, id, value) - for _, listener in listeners :: { Listener } do - listener(entity, id, value) - end - end - local existing_hook = inner_world_get(world, component, EcsOnAdd) :: Listener - if existing_hook then - table.insert(listeners, existing_hook) - end - - local idr_pair = component_index[ECS_PAIR(component, EcsWildcard)] - - if idr_pair then - for id, cr in idr_pair.wildcard_pairs do - cr.on_add = on_add - end - idr_pair.on_add = on_add - else - local idr = component_index[component] - if idr then - idr.on_add = on_add - end - end - inner_world_set(world, component, EcsOnAdd, on_add) - end - table.insert(listeners, fn) - return function() - local n = #listeners - local i = table.find(listeners, fn) - listeners[i] = listeners[n] - listeners[n] = nil - end - end - - world.changed = function( - _: world, - component: i53, - fn: Listener - ) - local listeners = signals.changed[component] - if not listeners then - listeners = {} - signals.changed[component] = listeners - local function on_change(entity, id, value: any) - for _, listener in listeners :: { Listener } do - listener(entity, id, value) - end - end - local existing_hook = inner_world_get(world, component, EcsOnChange) :: Listener - if existing_hook then - table.insert(listeners, existing_hook) - end - - local idr_pair = component_index[ECS_PAIR(component, EcsWildcard)] - - if idr_pair then - for _, cr in idr_pair.wildcard_pairs do - cr.on_change = on_change - end - - idr_pair.on_change = on_change - else - local idr = component_index[component] - if idr then - idr.on_change = on_change - end - end - - inner_world_set(world, component, EcsOnChange, on_change) - end - table.insert(listeners, fn) - return function() - local n = #listeners - local i = table.find(listeners, fn) - listeners[i] = listeners[n] - listeners[n] = nil - end - end - - world.removed = function(_: world, component: i53, fn: (i53, i53) -> ()) - local listeners = signals.removed[component] - if not listeners then - listeners = {} - signals.removed[component] = listeners - local function on_remove(entity, id) - for _, listener in listeners :: { Listener } do - listener(entity, id) - end - end - - local existing_hook = inner_world_get(world, component, EcsOnRemove) :: Listener - if existing_hook then - table.insert(listeners, existing_hook) - end - - local idr_pair = component_index[ECS_PAIR(component, EcsWildcard)] - - if idr_pair then - for _, cr in idr_pair.wildcard_pairs do - cr.on_remove = on_remove - end - - idr_pair.on_remove = on_remove - else - local idr = component_index[component] - if idr then - idr.on_remove = on_remove - end - end - - inner_world_set(world, component, EcsOnRemove, on_remove) - end - - table.insert(listeners, fn) - - return function() - local n = #listeners - local i = table.find(listeners, fn) - listeners[i] = listeners[n] - listeners[n] = nil - end - end - - local function inner_world_has(world: World, entity: i53, - a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean - - local record = inner_entity_index_try_get_unsafe(entity) - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - local columns_map = archetype.columns_map - - return columns_map[a] ~= nil and - (b == nil or columns_map[b] ~= nil) and - (c == nil or columns_map[c] ~= nil) and - (d == nil or columns_map[d] ~= nil) and - (e == nil or error("args exceeded")) - end - - local function inner_world_target(world: world, entity: i53, relation: i53, index: number?): i53? - local record = inner_entity_index_try_get_unsafe(entity) - if not record then - return nil - end - - local archetype = record.archetype - if not archetype then - return nil - end - - local r = ECS_PAIR(relation, EcsWildcard) - local idr = world.component_index[r] - - if not idr then - return nil - end - - local archetype_id = archetype.id - local count = idr.counts[archetype_id] - if not count then - return nil - end - - local nth = index or 0 - - if nth >= count then - return nil - end - - nth = archetype.types[nth + idr.records[archetype_id]] - - if not nth then - return nil - end - - return entity_index_get_alive(world.entity_index, - ECS_PAIR_SECOND(nth)) - end - - local function inner_world_parent(world: world, entity: i53): i53? - return inner_world_target(world, entity, EcsChildOf, 0) - end - - local function inner_world_entity(world: world, entity: i53?): i53 - if entity then - local index = ECS_ID(entity) - local alive_count = entity_index.alive_count - local r = eindex_sparse_array[index] - 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 = inner_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 - 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 = inner_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 - 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 e_swap = eindex_dense_array[alive_count] - local r_swap = eindex_sparse_array[alive_count] - r_swap.dense = index - - 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 - - return entity - end - end - return entity_index_new_id(entity_index) - end - - local function inner_world_remove(world: world, entity: i53, id: i53) - local record = inner_entity_index_try_get_unsafe(entity) - if not record then - return - end - local from = record.archetype - - if not from then - return - end - - if from.columns_map[id] then - local idr = world.component_index[id] - local on_remove = idr.on_remove - - if on_remove then - on_remove(entity, id) - end - - local to = archetype_traverse_remove(world, id, record.archetype) - - inner_entity_move(entity_index, entity, record, to) - end - end - - local function inner_world_clear(world: world, entity: i53) - local tgt = ECS_PAIR(EcsWildcard, entity) - local idr_t = component_index[tgt] - local idr = component_index[entity] - local rel = ECS_PAIR(entity, EcsWildcard) - local idr_r = component_index[rel] - - if idr then - local count = 0 - local queue = {} - for archetype_id in idr.records do - local idr_archetype = archetypes[archetype_id] - local entities = idr_archetype.entities - local n = #entities - table.move(entities, 1, n, count + 1, queue) - count += n - end - for _, e in queue do - inner_world_remove(world, e, entity) - end - end - - if idr_t then - local archetype_ids = idr_t.records - for archetype_id in archetype_ids do - local idr_t_archetype = archetypes[archetype_id] - local idr_t_types = idr_t_archetype.types - local entities = idr_t_archetype.entities - - local node = idr_t_archetype - - for _, id in idr_t_types do - if not ECS_IS_PAIR(id) then - continue - end - local object = entity_index_get_alive( - entity_index, ECS_PAIR_SECOND(id)) - if object ~= entity then - continue - end - node = archetype_traverse_remove(world, id, node) - local on_remove = component_index[id].on_remove - if on_remove then - for _, entity in entities do - on_remove(entity, id) - end - end - end - - for i = #entities, 1, -1 do - local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e) :: record - inner_entity_move(entity_index, e, r, node) - end - end - end - - if idr_r then - local archetype_ids = idr_r.records - local records = idr_r.records - local counts = idr_r.counts - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local node = idr_r_archetype - local entities = idr_r_archetype.entities - local tr = records[archetype_id] - local tr_count = counts[archetype_id] - local types = idr_r_archetype.types - for i = tr, tr + tr_count - 1 do - local id = types[i] - node = archetype_traverse_remove(world, id, idr_r_archetype) - local on_remove = component_index[id].on_remove - if on_remove then - for _, entity in entities do - on_remove(entity, id) - end - end - end - for i = #entities, 1, -1 do - local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e) :: record - inner_entity_move(entity_index, e, r, node) - end - end - end - end - - local function inner_world_delete(world: world, entity: i53) - local record = inner_entity_index_try_get_unsafe(entity) - if not record then - return - end - - local archetype = record.archetype - - if archetype then - for _, id in archetype.types do - local idr = component_index[id] - local on_remove = idr.on_remove - if on_remove then - on_remove(entity, id) - end - end - archetype_delete(world, record.archetype, record.row) - end - - local component_index = world.component_index - local archetypes = world.archetypes - local tgt = ECS_PAIR(EcsWildcard, entity::number) - local rel = ECS_PAIR(entity::number, EcsWildcard) - - local idr_t = component_index[tgt] - local idr = component_index[entity::number] - local idr_r = component_index[rel] - - if idr then - local flags = idr.flags - if (bit32.btest(flags, ECS_ID_DELETE) == true) then - for archetype_id in idr.records do - local idr_archetype = archetypes[archetype_id] - - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - inner_world_delete(world, entities[i]) - end - - archetype_destroy(world, idr_archetype) - end - else - local on_remove = idr.on_remove - if on_remove then - for archetype_id in idr.records do - local idr_archetype = archetypes[archetype_id] - local to = archetype_traverse_remove(world, entity, idr_archetype) - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - local e = entities[i] - on_remove(e, entity) - local r = eindex_sparse_array[ECS_ID(e :: number)] - local from = r.archetype - if from ~= idr_archetype then - -- unfortunately the on_remove hook allows a window where `e` can have changed archetype - -- this is hypothetically not that expensive of an operation anyways - to = archetype_traverse_remove(world, entity, from) - end - inner_entity_move(entity_index, e, r, to) - end - - archetype_destroy(world, idr_archetype) - end - else - for archetype_id in idr.records do - local idr_archetype = archetypes[archetype_id] - local to = archetype_traverse_remove(world, entity, idr_archetype) - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - local e = entities[i] - entity_move(entity_index, e, eindex_sparse_array[ECS_ID(e :: number)], to) - end - - archetype_destroy(world, idr_archetype) - end - end - end - end - if idr_t then - for id, cr in idr_t.wildcard_pairs do - local flags = cr.flags - local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE) - local on_remove = cr.on_remove - if flags_delete_mask then - for archetype_id in cr.records do - local idr_t_archetype = archetypes[archetype_id] - local entities = idr_t_archetype.entities - for i = #entities, 1, -1 do - local child = entities[i] - inner_world_delete(world, child) - end - end - else - for archetype_id in cr.records do - local idr_t_archetype = archetypes[archetype_id] - local entities = idr_t_archetype.entities - -- archetype_traverse_remove is not idempotent meaning - -- this access is actually unsafe because it can - -- incorrectly cache an edge despite a node of the - -- component id on the archetype does not exist. This - -- requires careful testing to ensure correct values are - -- being passed to the arguments. - local to = archetype_traverse_remove(world, id, idr_t_archetype) - - for i = #entities, 1, -1 do - local e = entities[i] - local r = eindex_sparse_array[ECS_ID(e :: number)] - if on_remove then - on_remove(e, id) - - local from = r.archetype - if from ~= idr_t_archetype then - -- unfortunately the on_remove hook allows a window where `e` can have changed archetype - -- this is hypothetically not that expensive of an operation anyways - to = archetype_traverse_remove(world, id, from) - end - end - - inner_entity_move(entity_index, e, r, to) - end - end - end - - for archetype_id in cr.records do - archetype_destroy(world, archetypes[archetype_id]) - end - end - end - - if idr_r then - local archetype_ids = idr_r.records - local flags = idr_r.flags - local has_delete_policy = bit32.btest(flags, ECS_ID_DELETE) - if has_delete_policy then - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local entities = idr_r_archetype.entities - local n = #entities - for i = n, 1, -1 do - inner_world_delete(world, entities[i]) - end - archetype_destroy(world, idr_r_archetype) - end - else - local counts = idr_r.counts - local records = idr_r.records - for archetype_id in archetype_ids do - local idr_r_archetype = archetypes[archetype_id] - local node = idr_r_archetype - local entities = idr_r_archetype.entities - local tr = records[archetype_id] - local tr_count = counts[archetype_id] - local types = idr_r_archetype.types - for i = tr, tr + tr_count - 1 do - local id = types[i] - node = archetype_traverse_remove(world, id, node) - local on_remove = component_index[id].on_remove - if on_remove then - for _, entity in entities do - on_remove(entity, id) - end - end - end - - for i = #entities, 1, -1 do - local e = entities[i] - local r = inner_entity_index_try_get_unsafe(e) :: record - inner_entity_move(entity_index, e, r, node) - end - end - - for archetype_id in archetype_ids do - archetype_destroy(world, archetypes[archetype_id]) - end - end - end - - local dense = record.dense - local i_swap = entity_index.alive_count - entity_index.alive_count = i_swap - 1 - - local e_swap = eindex_dense_array[i_swap] - local r_swap = inner_entity_index_try_get_any(e_swap) :: record - r_swap.dense = dense - record.archetype = nil :: any - record.row = nil :: any - record.dense = i_swap - - eindex_dense_array[dense] = e_swap - eindex_dense_array[i_swap] = ECS_GENERATION_INC(entity) - end - - local function inner_world_exists(world: world, entity: i53): boolean - return inner_entity_index_try_get_any(entity) ~= nil - end - - local function inner_world_contains(world: world, entity: i53): boolean - return entity_index_is_alive(entity_index, entity) - end - - local function inner_world_cleanup(world: world) - for _, archetype in archetypes do - if #archetype.entities == 0 then - archetype_destroy(world, archetype) - end - end - - local new_archetypes = {} - local new_archetype_map = {} - - for index, archetype in archetypes do - new_archetypes[index] = archetype - new_archetype_map[archetype.type] = archetype - end - - archetypes = new_archetypes - archetype_index = new_archetype_map - - world.archetypes = new_archetypes - world.archetype_index = new_archetype_map - end - - world.entity = inner_world_entity - world.query = world_query :: any - world.remove = inner_world_remove - world.clear = inner_world_clear - world.delete = inner_world_delete - world.component = world_component - world.add = inner_world_add - world.set = inner_world_set - world.get = inner_world_get :: any - world.has = inner_world_has :: any - world.target = inner_world_target - world.parent = inner_world_parent - world.contains = inner_world_contains - world.exists = inner_world_exists - world.cleanup = inner_world_cleanup - world.each = world_each - world.children = world_children - world.range = world_range - - for i = 1, HI_COMPONENT_ID do - local e = entity_index_new_id(entity_index) - inner_world_add(world, e, EcsComponent) - end - - for i = HI_COMPONENT_ID + 1, EcsRest do - -- Initialize built-in components - entity_index_new_id(entity_index) - end - - inner_world_add(world, EcsName, EcsComponent) - inner_world_add(world, EcsOnChange, EcsComponent) - inner_world_add(world, EcsOnAdd, EcsComponent) - inner_world_add(world, EcsOnRemove, EcsComponent) - inner_world_add(world, EcsWildcard, EcsComponent) - inner_world_add(world, EcsRest, EcsComponent) - - inner_world_set(world, EcsOnAdd, EcsName, "jecs.OnAdd") - inner_world_set(world, EcsOnRemove, EcsName, "jecs.OnRemove") - inner_world_set(world, EcsOnChange, EcsName, "jecs.OnChange") - inner_world_set(world, EcsWildcard, EcsName, "jecs.Wildcard") - inner_world_set(world, EcsChildOf, EcsName, "jecs.ChildOf") - inner_world_set(world, EcsComponent, EcsName, "jecs.Component") - - inner_world_set(world, EcsOnDelete, EcsName, "jecs.OnDelete") - inner_world_set(world, EcsOnDeleteTarget, EcsName, "jecs.OnDeleteTarget") - - inner_world_set(world, EcsDelete, EcsName, "jecs.Delete") - inner_world_set(world, EcsRemove, EcsName, "jecs.Remove") - inner_world_set(world, EcsName, EcsName, "jecs.Name") - inner_world_set(world, EcsRest, EcsRest, "jecs.Rest") - - inner_world_add(world, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) - inner_world_add(world, EcsChildOf, EcsExclusive) - - inner_world_add(world, EcsOnDelete, EcsExclusive) - inner_world_add(world, EcsOnDeleteTarget, EcsExclusive) - - for i = EcsRest + 1, ecs_max_tag_id do - entity_index_new_id(entity_index) - end - - for i, bundle in ecs_metadata do - for ty, value in bundle do - if value == NULL then - inner_world_add(world, i, ty) - else - inner_world_set(world, i, ty, value) - end - end - end - - return world -end - --- type function ecs_id_t(entity) --- local ty = entity:components()[2] --- local __T = ty:readproperty(types.singleton("__T")) --- if not __T then --- return ty:readproperty(types.singleton("__jecs_pair_value")) --- end --- return __T --- end - --- type function ecs_pair_t(first, second) --- if ecs_id_t(first):is("nil") then --- return second --- else --- return first --- end --- end --- - -local function ecs_is_tag(world: world, entity: i53): boolean - local idr = world.component_index[entity] - if idr then - return bit32.btest(idr.flags, ECS_ID_IS_TAG) - end - return not world_has_one_inline(world, entity, EcsComponent) -end - -local function ecs_entity_record(world: world, entity: i53) - return entity_index_try_get(world.entity_index, entity) -end - -return { - world = world_new :: () -> World, - World = { - new = world_new - }, - component = (ECS_COMPONENT :: any) :: () -> Entity, - tag = (ECS_TAG :: any) :: () -> Entity, - meta = (ECS_META :: any) :: (id: Entity, id: Id, value: a?) -> Entity, - is_tag = (ecs_is_tag :: any) :: (World, Id) -> boolean, - - OnAdd = (EcsOnAdd :: any) :: Id<(entity: Entity, id: Id, data: T) -> ()>, - OnRemove = (EcsOnRemove :: any) :: Id<(entity: Entity, id: Id) -> ()>, - OnChange = (EcsOnChange :: any) :: Id<(entity: Entity, id: Id, data: T) -> ()>, - ChildOf = (EcsChildOf :: any) :: Entity, - Component = (EcsComponent :: any) :: Entity, - Wildcard = (EcsWildcard :: any) :: Id, - w = (EcsWildcard :: any) :: Id, - OnDelete = (EcsOnDelete :: any) :: Entity, - OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity, - Delete = (EcsDelete :: any) :: Entity, - Remove = (EcsRemove :: any) :: Entity, - Name = (EcsName :: any) :: Id, - Exclusive = (EcsExclusive :: any) :: Entity, - ArchetypeCreate = (EcsOnArchetypeCreate :: any) :: Entity, - ArchetypeDelete = (EcsOnArchetypeDelete :: any) :: Entity, - Rest = (EcsRest :: any) :: Entity, - - pair = ECS_PAIR :: (first: Id

, second: Id) -> Pair, - - IS_PAIR = ECS_IS_PAIR :: (pair: Pair) -> boolean, - ECS_PAIR_FIRST = ECS_PAIR_FIRST :: (pair: Pair) -> Id

, - ECS_PAIR_SECOND = ECS_PAIR_SECOND :: (pair: Pair) -> Id, - pair_first = ecs_pair_first :: (world: World, pair: Pair) -> Id

, - pair_second = ecs_pair_second :: (world: World, pair: Pair) -> Id, - entity_index_get_alive = entity_index_get_alive, - - archetype_append_to_records = archetype_append_to_records, - id_record_ensure = id_record_ensure :: (World, Id) -> ComponentRecord, - component_record = id_record_get :: (World, Id) -> ComponentRecord?, - record = ecs_entity_record :: (World, Entity) -> Record, - - archetype_create = archetype_create :: (World, { Id }, string) -> Archetype, - archetype_ensure = archetype_ensure :: (World, { Id }) -> Archetype, - find_insert = find_insert, - find_archetype_with = find_archetype_with :: (World, Id, Archetype) -> Archetype, - find_archetype_without = find_archetype_without :: (World, Id, Archetype) -> Archetype, - create_edge_for_remove = create_edge_for_remove, - archetype_traverse_add = archetype_traverse_add :: (World, Id, Archetype) -> Archetype, - archetype_traverse_remove = archetype_traverse_remove :: (World, Id, Archetype) -> Archetype, - bulk_insert = ecs_bulk_insert :: (World, Entity, { Id }, { any }) -> (), - bulk_remove = ecs_bulk_remove :: (World, Entity, { Id }) -> (), - - entity_move = entity_move :: (EntityIndex, Entity, Record, Archetype) -> (), - - entity_index_try_get = entity_index_try_get :: (EntityIndex, Entity) -> Record?, - entity_index_try_get_fast = entity_index_try_get_fast :: (EntityIndex, Entity) -> Record?, - entity_index_try_get_any = entity_index_try_get_any :: (EntityIndex, Entity) -> Record, - entity_index_is_alive = entity_index_is_alive :: (EntityIndex, Entity) -> boolean, - entity_index_new_id = entity_index_new_id :: (EntityIndex) -> Entity, - - Query = Query, - - query_iter = query_iter, - query_iter_init = query_iter_init, - query_with = query_with, - query_without = query_without, - query_archetypes = query_archetypes, - query_match = query_match, - - find_observers = find_observers :: (World, Id, Id) -> { Observer }, - - -- Inwards facing API for testing - ECS_ID = ECS_ENTITY_T_LO :: (Entity) -> number, - ECS_GENERATION_INC = ECS_GENERATION_INC :: (Entity) -> Entity, - ECS_GENERATION = ECS_GENERATION :: (Entity) -> number, - ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD, - ECS_ID_IS_EXCLUSIVE = ECS_ID_IS_EXCLUSIVE, - ECS_ID_DELETE = ECS_ID_DELETE, - ECS_META_RESET = ECS_META_RESET, - ECS_COMBINE = ECS_COMBINE :: (number, number) -> Entity, - ECS_ENTITY_MASK = ECS_ENTITY_MASK, -} diff --git a/tools/entity_visualiser.luau b/modules/entity_visualiser.luau similarity index 100% rename from tools/entity_visualiser.luau rename to modules/entity_visualiser.luau diff --git a/tools/lifetime_tracker.luau b/modules/lifetime_tracker.luau similarity index 100% rename from tools/lifetime_tracker.luau rename to modules/lifetime_tracker.luau diff --git a/modules/ob.luau b/modules/ob.luau new file mode 100755 index 0000000..781636d --- /dev/null +++ b/modules/ob.luau @@ -0,0 +1,425 @@ +--!strict +local jecs = require("@jecs") + +type World = jecs.World + +type Id = jecs.Id + + +export type Observer = { + disconnect: () -> (), +} + +export type Monitor = { + disconnect: () -> (), + added: ((jecs.Entity) -> ()) -> (), + removed: ((jecs.Entity) -> ()) -> () +} + +local function observers_new( + query: jecs.Query<...any>, + callback: (jecs.Entity) -> () +): Observer + local cachedquery = query:cached() + + local world = (cachedquery :: jecs.Query & { world: World }).world + callback = callback + + local archetypes = cachedquery.archetypes_map + local terms = query.filter_with :: { jecs.Id } + + local entity_index = world.entity_index + + local function emplaced( + entity: jecs.Entity, + id: jecs.Id, + value: a, + oldarchetype: jecs.Archetype + ) + local r = entity_index.sparse_array[jecs.ECS_ID(entity)] + + local archetype = r.archetype + + if archetypes[archetype.id] then + callback(entity) + end + end + + local cleanup = {} + + for _, term in terms do + if jecs.IS_PAIR(term) then + local rel = jecs.ECS_PAIR_FIRST(term) + local tgt = jecs.ECS_PAIR_SECOND(term) + local wc = tgt == jecs.w + + local function emplaced_w_pair(entity, id, value, oldarchetype: jecs.Archetype) + if not wc and id ~= term then + return + end + local r = jecs.record(world, entity) + if archetypes[r.archetype.id] then + callback(entity) + end + end + + local onadded = world:added(rel, emplaced_w_pair) + local onchanged = world:changed(rel, emplaced_w_pair) + table.insert(cleanup, onadded) + table.insert(cleanup, onchanged) + else + local onadded = world:added(term, emplaced) + local onchanged = world:changed(term, emplaced) + table.insert(cleanup, onadded) + table.insert(cleanup, onchanged) + end + end + + local without = query.filter_without + if without then + for _, term in without do + if jecs.IS_PAIR(term) then + local rel = jecs.ECS_PAIR_FIRST(term) + local tgt = jecs.ECS_PAIR_SECOND(term) + local wc = tgt == jecs.w + local onremoved = world:removed(rel, function(entity, id, delete: boolean?) + if not wc and id ~= term then + return + end + local r = jecs.record(world, entity) + local archetype = r.archetype + if archetype then + local dst = jecs.archetype_traverse_remove(world, id, archetype) + if archetypes[dst.id] then + callback(entity) + end + end + end) + + table.insert(cleanup, onremoved) + else + local onremoved = world:removed(term, function(entity, id) + local r = jecs.record(world, entity) + local archetype = r.archetype + if archetype then + local dst = jecs.archetype_traverse_remove(world, id, archetype) + if archetypes[dst.id] then + callback(entity) + end + end + end) + + table.insert(cleanup, onremoved) + end + end + end + + local function disconnect() + for _, disconnect in cleanup do + disconnect() + end + end + + local observer = { + disconnect = disconnect, + } + + return observer +end + +local function monitors_new(query: jecs.Query<...any>): Monitor + local cachedquery = query:cached() + + local world = (cachedquery :: jecs.Query<...any> & { world: World }).world :: jecs.World + + local archetypes = cachedquery.archetypes_map + local terms = cachedquery.filter_with :: { jecs.Id } + + local entity_index = world.entity_index :: any + + local terms_lookup: { [jecs.Id]: boolean } = {} + for _, term in terms do + terms_lookup[term] = true + end + + local callback_added: ((jecs.Entity) -> ())? + local callback_removed: ((jecs.Entity) -> ())? + + -- NOTE(marcus): Track the last (entity, old archetype) pair we processed to detect bulk operations. + -- During bulk_insert from ROOT_ARCHETYPE, the entity is moved to the target archetype first, + -- then all on_add callbacks fire sequentially with the same oldarchetype for the same entity. + -- We track both entity and old archetype to distinguish between: + -- 1. Same entity, same old archetype (bulk operation - skip) + -- 2. Different entity, same old archetype (separate operation - don't skip) + local last_old_archetype: jecs.Archetype? = nil + local last_entity: jecs.Entity? = nil + + local function emplaced( + entity: jecs.Entity, + id: jecs.Id, + value: a, + oldarchetype: jecs.Archetype + ) + if callback_added == nil then + return + end + + local r = jecs.entity_index_try_get_fast( + entity_index, entity :: any) :: jecs.Record + if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then + -- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before + -- AND this component is in the query's terms. This detects bulk operations where + -- the same entity transitions with multiple components, while allowing different + -- entities to trigger even if they share the same old archetype. + if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then + return + end + + last_old_archetype = oldarchetype + last_entity = entity + callback_added(entity) + else + -- NOTE(marcus): Clear tracking when we see a different transition pattern + last_old_archetype = nil + last_entity = nil + end + end + + -- Track which entity we've already processed for deletion to avoid duplicate callbacks + -- during bulk deletion where multiple components are removed with delete=true + local last_deleted_entity: jecs.Entity? = nil + + local function removed(entity: jecs.Entity, component: jecs.Component, delete:boolean?) + if callback_removed == nil then + return + end + + if delete then + -- Deletion is a bulk removal - all components are removed with delete=true + -- We should only trigger the callback once per entity, not once per component + if last_deleted_entity == entity then + return + end + + local r = jecs.record(world, entity) + if r and r.archetype and archetypes[r.archetype.id] then + -- Entity was in the monitor before deletion + last_deleted_entity = entity + -- Clear tracking when entity is deleted + last_old_archetype = nil + last_entity = nil + callback_removed(entity) + end + return + end + + local r = jecs.record(world, entity) + local src = r.archetype + + local dst = jecs.archetype_traverse_remove(world, component, src) + + if not archetypes[dst.id] then + -- Clear tracking when entity leaves the monitor to allow re-entry + last_old_archetype = nil + last_entity = nil + last_deleted_entity = nil + callback_removed(entity) + end + end + + local cleanup = {} + + for _, term in terms do + if jecs.IS_PAIR(term) then + local rel = jecs.ECS_PAIR_FIRST(term) + local tgt = jecs.ECS_PAIR_SECOND(term) + local wc = tgt == jecs.w + + local onadded = world:added(rel, function(entity, id, _, oldarchetype: jecs.Archetype) + if callback_added == nil then + return + end + + if not wc and id ~= term then + return + end + + local r = jecs.entity_index_try_get_fast( + entity_index, entity :: any) :: jecs.Record + + if not archetypes[oldarchetype.id] and archetypes[r.archetype.id] then + -- NOTE(marcus): Skip if we've seen this exact (entity, old archetype) combination before + -- AND this component is in the query's terms. + if last_old_archetype == oldarchetype and last_entity == entity and terms_lookup[id] then + return + end + + last_old_archetype = oldarchetype + last_entity = entity + callback_added(entity) + else + -- Clear tracking when we see a different transition pattern + last_old_archetype = nil + last_entity = nil + end + end) + local onremoved = world:removed(rel, function(entity, id, deleted) + if callback_removed == nil then + return + end + if not wc and id ~= term then + return + end + + local r = jecs.record(world, entity) + if archetypes[r.archetype.id] then + last_old_archetype = nil + callback_removed(entity) + end + end) + table.insert(cleanup, onadded) + table.insert(cleanup, onremoved) + else + local onadded = world:added(term, emplaced) + local onremoved = world:removed(term, removed) + table.insert(cleanup, onadded) + table.insert(cleanup, onremoved) + end + + end + + local without = query.filter_without + if without then + for _, term in without do + if jecs.IS_PAIR(term) then + local rel = jecs.ECS_PAIR_FIRST(term) + local tgt = jecs.ECS_PAIR_SECOND(term) + local wc = tgt == jecs.w + local onadded = world:added(rel, function(entity, id, _, oldarchetype: jecs.Archetype) + if callback_removed == nil then + return + end + if not wc and id ~= term then + return + end + local r = jecs.record(world, entity) + local archetype = r.archetype + if not archetype then + return + end + + -- NOTE(marcus): This check that it was presently in + -- the query but distinctively leaves is important as + -- sometimes it could be too eager to report that it + -- removed a component even though the entity is not + -- apart of the monitor + if archetypes[oldarchetype.id] and not archetypes[archetype.id] then + last_old_archetype = nil + callback_removed(entity) + end + end) + local onremoved = world:removed(rel, function(entity, id, delete) + if delete then + return + end + if callback_added == nil then + return + end + if not wc and id ~= term then + return + end + + local r = jecs.record(world, entity) + local archetype = r.archetype + if not archetype then + return + end + if last_old_archetype == archetype and terms_lookup[id] then + return + end + + local dst = jecs.archetype_traverse_remove(world, id, archetype) + + if archetypes[dst.id] then + last_old_archetype = archetype + callback_added(entity) + end + end) + table.insert(cleanup, onadded) + table.insert(cleanup, onremoved) + else + local onadded = world:added(term, function(entity, id, _, oldarchetype: jecs.Archetype) + if callback_removed == nil then + return + end + local r = jecs.record(world, entity) + local archetype = r.archetype + if not archetype then + return + end + + -- NOTE(marcus): Sometimes OnAdd listeners for excluded + -- terms are too eager to report that it is leaving the + -- monitor even though the entity is not apart of it + -- already. + if archetypes[oldarchetype.id] and not archetypes[archetype.id] then + callback_removed(entity) + end + end) + local onremoved = world:removed(term, function(entity, id, delete) + if delete then + return + end + if callback_added == nil then + return + end + local r = jecs.record(world, entity) + local archetype = r.archetype + if not archetype then + return + end + local dst = jecs.archetype_traverse_remove(world, id, archetype) + + -- NOTE(marcus): Inversely with the opposite operation, you + -- only need to check if it is going to enter the query once + -- because world:remove already stipulates that it is + -- idempotent so that this hook won't be invoked if it is + -- was already removed. + if archetypes[dst.id] then + callback_added(entity) + end + end) + table.insert(cleanup, onadded) + table.insert(cleanup, onremoved) + end + end + end + + local function disconnect() + for _, disconnect in cleanup do + disconnect() + end + end + + local function monitor_added(callback) + callback_added = callback + end + + local function monitor_removed(callback) + callback_removed = callback + end + + local monitor = { + disconnect = disconnect, + added = monitor_added, + removed = monitor_removed + } :: Monitor + + return monitor +end + +return { + monitor = monitors_new, + observer = observers_new, + test = function(q: jecs.Query<...any>) end +} diff --git a/tools/perfgraph.py b/modules/perfgraph.py similarity index 100% rename from tools/perfgraph.py rename to modules/perfgraph.py diff --git a/tools/read_lcov.py b/modules/read_lcov.py similarity index 100% rename from tools/read_lcov.py rename to modules/read_lcov.py diff --git a/tools/runtime_lints.luau b/modules/runtime_lints.luau similarity index 100% rename from tools/runtime_lints.luau rename to modules/runtime_lints.luau diff --git a/tools/svg.py b/modules/svg.py similarity index 100% rename from tools/svg.py rename to modules/svg.py diff --git a/tools/testkit.luau b/modules/testkit.luau similarity index 100% rename from tools/testkit.luau rename to modules/testkit.luau diff --git a/package.json b/package.json index c0a1b62..e04e227 100755 --- a/package.json +++ b/package.json @@ -15,10 +15,9 @@ ], "homepage": "https://github.com/ukendio/jecs", "license": "MIT", - "types": "jecs.d.ts", + "types": "src/jecs.d.ts", "files": [ - "jecs.luau", - "jecs.d.ts", + "src", "LICENSE.md", "README.md" ], diff --git a/rokit.toml b/rokit.toml index ec312ca..907eb03 100755 --- a/rokit.toml +++ b/rokit.toml @@ -1,7 +1,4 @@ [tools] wally = "upliftgames/wally@0.3.2" rojo = "rojo-rbx/rojo@7.4.4" -stylua = "johnnymorganz/stylua@2.0.1" -Blink = "1Axen/Blink@0.14.1" -wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2" luau = "luau-lang/luau@0.701" diff --git a/jecs.d.ts b/src/jecs.d.ts similarity index 100% rename from jecs.d.ts rename to src/jecs.d.ts diff --git a/jecs.luau b/src/jecs.luau similarity index 100% rename from jecs.luau rename to src/jecs.luau diff --git a/test/ecr.luau b/test/ecr.luau deleted file mode 100755 index b1892d0..0000000 --- a/test/ecr.luau +++ /dev/null @@ -1,11 +0,0 @@ -local function component() - local id = 1 - local v - local function instance() - return id, v - end - return function(value) - v = value - return instance - end -end diff --git a/test/tools/entity_visualiser.luau b/test/entity_visualiser.luau similarity index 100% rename from test/tools/entity_visualiser.luau rename to test/entity_visualiser.luau diff --git a/test/lol.luau b/test/lol.luau index 0749543..fbe7662 100755 --- a/test/lol.luau +++ b/test/lol.luau @@ -136,7 +136,7 @@ local function pa(e) print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`) end -local tprint = require("@testkit").print +local tprint = require("@modules/testkit").print local e1v0 = alloc() local e2v0 = alloc() local e3v0 = alloc() diff --git a/test/addons/ob.luau b/test/ob.luau similarity index 99% rename from test/addons/ob.luau rename to test/ob.luau index 18e0953..82bc23d 100755 --- a/test/addons/ob.luau +++ b/test/ob.luau @@ -1,11 +1,11 @@ local jecs = require("@jecs") -local testkit = require("@testkit") +local testkit = require("@modules/testkit") local test = testkit.test() local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK local FOCUS = test.FOCUS -local ob = require("@addons/ob") +local ob = require("@modules/ob") -TEST("addons/ob::observer", function() +TEST("modules/ob::observer", function() local world = jecs.world() do CASE [[should not invoke callbacks with a related but non-queried pair that while the entity still matches against the query]] @@ -289,7 +289,7 @@ TEST("addons/ob::observer", function() end end) -TEST("addons/ob::monitor", function() +TEST("modules/ob::monitor", function() local world = jecs.world() do CASE [[should not invoke monitor.added callback multiple times in a bulk_move diff --git a/test/tests.luau b/test/tests.luau index 845f902..a552b93 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,6 +1,6 @@ local jecs = require("@jecs") -local testkit = require("@testkit") +local testkit = require("@modules/testkit") local BENCH, START = testkit.benchmark() local __ = jecs.Wildcard local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION @@ -21,7 +21,7 @@ type World = jecs.World type Entity = jecs.Entity type Id = jecs.Id -local entity_visualiser = require("@tools/entity_visualiser") +local entity_visualiser = require("@modules/entity_visualiser") local dwi = entity_visualiser.stringify TEST("Ensure archetype edges get cleaned", function() diff --git a/wally.toml b/wally.toml index 7bba005..7f4d0a8 100755 --- a/wally.toml +++ b/wally.toml @@ -6,10 +6,10 @@ realm = "shared" license = "MIT" include = [ "default.project.json", - "jecs.luau", + "src", "wally.toml", "README.md", "CHANGELOG.md", "LICENSE", ] -exclude = ["**"] +exclude = ["src/mirror.luau"]