From d8a4360c80598bd844846d66d40bcd5507fdb112 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 2 Oct 2026 07:23:04 +0700 Subject: [PATCH 01/13] proof of concept simple search/multi_search --- README.md | 4 +++ develop/all.lua | 1 + develop/testing/search_tests.lua | 56 ++++++++++++++++++++++++++++++++ evolved.d.tl | 4 +++ evolved.lua | 52 +++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 develop/testing/search_tests.lua diff --git a/README.md b/README.md index 211c94b..7d89661 100644 --- a/README.md +++ b/README.md @@ -1608,6 +1608,10 @@ lookup :: string -> entity? multi_lookup :: string -> entity[], integer multi_lookup_to :: entity[], integer, string -> integer +search :: query -> entity? +multi_search :: query -> entity[], integer +multi_search_to :: entity[], integer, query -> integer + process :: system... -> () process_with :: system, ... -> () diff --git a/develop/all.lua b/develop/all.lua index 0c8918d..e74cb50 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -13,6 +13,7 @@ require 'develop.testing.name_tests' require 'develop.testing.process_with_tests' require 'develop.testing.realloc_tests' require 'develop.testing.requires_fragment_tests' +require 'develop.testing.search_tests' require 'develop.testing.spawn_tests' require 'develop.testing.system_as_query_tests' diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua new file mode 100644 index 0000000..5f0dbaa --- /dev/null +++ b/develop/testing/search_tests.lua @@ -0,0 +1,56 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + local f1, f2 = evo.id(2) + local q = evo.builder():include(f1):spawn() + local q_ex = evo.builder():include(f1):exclude(f2):spawn() + + do + assert(evo.search(q) == nil) + + local entity_list, entity_count = evo.multi_search(q) + assert(entity_list and #entity_list == 0 and entity_count == 0) + end + + local e1 = evo.builder():set(f1, 1):spawn() + local e2 = evo.builder():set(f1, 2):set(f2, 3):spawn() + local e3 = evo.builder():set(f1, 4):spawn() + + do + local e = evo.search(q) + assert(e == e1 or e == e2 or e == e3) + assert(evo.search(q_ex) == e1 or evo.search(q_ex) == e3) + end + + do + local entity_list, entity_count = evo.multi_search(q) + assert(entity_count == 3 and #entity_list == 3) + + local entity_set = {} + for i = 1, entity_count do entity_set[entity_list[i]] = true end + assert(entity_set[e1] and entity_set[e2] and entity_set[e3]) + end + + do + local entity_list, entity_count = evo.multi_search(q_ex) + assert(entity_count == 2 and #entity_list == 2) + + local entity_set = {} + for i = 1, entity_count do entity_set[entity_list[i]] = true end + assert(entity_set[e1] and entity_set[e3] and not entity_set[e2]) + end + + do + local entity_list = { 'a', 'b' } + local entity_count = evo.multi_search_to(entity_list, 3, q) + + assert(entity_count == 3 and #entity_list == 5) + assert(entity_list[1] == 'a' and entity_list[2] == 'b') + + local entity_set = {} + for i = 3, 3 + entity_count - 1 do entity_set[entity_list[i]] = true end + assert(entity_set[e1] and entity_set[e2] and entity_set[e3]) + end +end diff --git a/evolved.d.tl b/evolved.d.tl index c3be05a..16b7ef1 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -272,6 +272,10 @@ multi_lookup: function(name: string): { Entity }, integer multi_lookup_to: function(out_entity_list: { Entity }, out_entity_first: integer, name: string): integer + search: function(query: Query): Entity | nil + multi_search: function(query: Query): { Entity }, integer + multi_search_to: function(out_entity_list: { Entity }, out_entity_first: integer, query: Query): integer + process: function(...: System) process_with: function(system: System, ...: any) diff --git a/evolved.lua b/evolved.lua index 4e92f63..b1d6926 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1205,6 +1205,10 @@ local __evolved_lookup local __evolved_multi_lookup local __evolved_multi_lookup_to +local __evolved_search +local __evolved_multi_search +local __evolved_multi_search_to + local __evolved_process local __evolved_process_with @@ -6365,6 +6369,50 @@ function __evolved_multi_lookup_to(out_entity_list, out_entity_first, name) return 0 end +---@param query evolved.query +---@return evolved.entity? entity +---@nodiscard +function __evolved_search(query) + local execute_iterator, execute_state = __evolved_execute(query) + if not execute_state then return end + + local chunk, chunk_entity_list = execute_iterator(execute_state) + if not chunk or not chunk_entity_list then return end + + __release_table(__table_pool_tag.chunk_list, execute_state[2], false, true) + __release_table(__table_pool_tag.execute_state, execute_state, true, true) + + return chunk_entity_list[1] +end + +---@param query evolved.query +---@return evolved.entity[] entity_list +---@return integer entity_count +---@nodiscard +function __evolved_multi_search(query) + local entity_list = {} + local entity_count = __evolved_multi_search_to(entity_list, 1, query) + return entity_list, entity_count +end + +---@param out_entity_list evolved.entity[] +---@param out_entity_first integer +---@param query evolved.query +---@return integer entity_count +function __evolved_multi_search_to(out_entity_list, out_entity_first, query) + local out_entity_count = 0 + + for _, chunk_entity_list, chunk_entity_count in __evolved_execute(query) do + __lua_table_move( + chunk_entity_list, 1, chunk_entity_count, + out_entity_first + out_entity_count, out_entity_list) + + out_entity_count = out_entity_count + chunk_entity_count + end + + return out_entity_count +end + ---@param ... evolved.system systems function __evolved_process(...) local argument_count = __lua_select('#', ...) @@ -7979,6 +8027,10 @@ evolved.lookup = __evolved_lookup evolved.multi_lookup = __evolved_multi_lookup evolved.multi_lookup_to = __evolved_multi_lookup_to +evolved.search = __evolved_search +evolved.multi_search = __evolved_multi_search +evolved.multi_search_to = __evolved_multi_search_to + evolved.process = __evolved_process evolved.process_with = __evolved_process_with From 418980ef09536f43a38525ef9319088a1ee5b571 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 2 Oct 2026 07:50:52 +0700 Subject: [PATCH 02/13] evolved.search/multi_search functions documentation --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 7d89661..4882fd4 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ - [Modifying Operations](#modifying-operations) - [Debug Mode](#debug-mode) - [Queries](#queries) + - [Searching Entities](#searching-entities) - [Deferred Operations](#deferred-operations) - [Batch Operations](#batch-operations) - [Systems](#systems) @@ -76,6 +77,7 @@ - [Chunk](#chunk) - [Builder](#builder) - [Changelog](#changelog) + - [vX.Y.Z](#vxyz) - [v1.11.1](#v1111) - [v1.11.0](#v1110) - [v1.10.0](#v1100) @@ -751,6 +753,29 @@ As you can see, `evolved.execute_iterator` returns a chunk, a list of entities i > [!NOTE] > But I haven't mentioned one important thing yet: [structural changes](#structural-changes) are not allowed during any iteration over chunks. This means that you cannot insert or remove fragments from entities while iterating. Also, you cannot destroy or spawn entities because this will cause structural changes too. This is done to avoid inconsistencies in the iteration process. If we allowed structural changes here, we might skip some entities during iteration, or process the same entity multiple times. The [debug mode](#debug-mode) can catch this kind of error. +#### Searching Entities + +If you just need to collect entities matching a query and don't need to work with chunks and components, you can use the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) and [`evolved.multi_search_to`](#evolvedmulti_search_to) functions. The [`evolved.search`](#evolvedsearch) function returns the first found entity matching the query, while the [`evolved.multi_search`](#evolvedmulti_search) function returns a list of all entities matching the query. The [`evolved.multi_search_to`](#evolvedmulti_search_to) function does the same, but appends the entities to the provided list starting from the specified index and returns only the number of found entities. + +```lua +---@param query evolved.query +---@return evolved.entity? entity +---@nodiscard +function evolved.search(query) end + +---@param query evolved.query +---@return evolved.entity[] entity_list +---@return integer entity_count +---@nodiscard +function evolved.multi_search(query) end + +---@param out_entity_list evolved.entity[] +---@param out_entity_first integer +---@param query evolved.query +---@return integer entity_count +function evolved.multi_search_to(out_entity_list, out_entity_first, query) end +``` + #### Deferred Operations Now we know that structural changes are not allowed during iteration, but what if we want to make some structural changes after the iteration is finished? For example, we might want to remove some fragments from entities after we have processed them, or we might want to spawn new entities while processing existing ones. To do all of this, we can use deferred operations. @@ -1708,6 +1733,10 @@ builder_mt:destruction_policy :: id -> builder ## Changelog +### vX.Y.Z + +- Added the new [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) functions that allow finding entities matching queries + ### v1.11.1 - Minor annotation fixes @@ -2215,6 +2244,35 @@ function evolved.multi_lookup(name) end function evolved.multi_lookup_to(out_entity_list, out_entity_first, name) end ``` +### `evolved.search` + +```lua +---@param query evolved.query +---@return evolved.entity? entity +---@nodiscard +function evolved.search(query) end +``` + +### `evolved.multi_search` + +```lua +---@param query evolved.query +---@return evolved.entity[] entity_list +---@return integer entity_count +---@nodiscard +function evolved.multi_search(query) end +``` + +### `evolved.multi_search_to` + +```lua +---@param out_entity_list evolved.entity[] +---@param out_entity_first integer +---@param query evolved.query +---@return integer entity_count +function evolved.multi_search_to(out_entity_list, out_entity_first, query) end +``` + ### `evolved.process` ```lua From 27ba1728a3a611247eabc59d4d88135910b1c94e Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 3 Oct 2026 08:14:56 +0700 Subject: [PATCH 03/13] fix issue #52 --- README.md | 1 + develop/all.lua | 2 + develop/issues/issue_52.lua | 23 +++++ evolved.lua | 176 ++++++++++++++++++------------------ 4 files changed, 114 insertions(+), 88 deletions(-) create mode 100644 develop/issues/issue_52.lua diff --git a/README.md b/README.md index 4882fd4..a027b9f 100644 --- a/README.md +++ b/README.md @@ -1735,6 +1735,7 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z +- Fixed issues: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) - Added the new [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) functions that allow finding entities matching queries ### v1.11.1 diff --git a/develop/all.lua b/develop/all.lua index e74cb50..ba4677b 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -1,3 +1,5 @@ +require 'develop.issues.issue_52' + require 'develop.testing.build_tests' require 'develop.testing.cached_hooks_tests' require 'develop.testing.cancel_tests' diff --git a/develop/issues/issue_52.lua b/develop/issues/issue_52.lua new file mode 100644 index 0000000..37e6607 --- /dev/null +++ b/develop/issues/issue_52.lua @@ -0,0 +1,23 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + assert(evo.name(evo.TAG) == 'TAG') + assert(evo.lookup('TAG') == evo.TAG) +end + +do + local ALL_INTERNAL_FRAGMENTS = evo.builder() + :include(evo.INTERNAL) + :build() + + for _, entity_list, entity_count in evo.execute(ALL_INTERNAL_FRAGMENTS) do + for i = 1, entity_count do + local entity = entity_list[i] + assert(type(evo.get(entity, evo.NAME)) == 'string') + assert(evo.name(entity) == evo.get(entity, evo.NAME)) + assert(evo.lookup(evo.name(entity)) == entity) + end + end +end diff --git a/evolved.lua b/evolved.lua index b1d6926..1dc236d 100644 --- a/evolved.lua +++ b/evolved.lua @@ -7448,94 +7448,6 @@ __evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) --- --- -__evolved_set(__TAG, __NAME, 'TAG') -__evolved_set(__NAME, __NAME, 'NAME') - -__evolved_set(__UNIQUE, __NAME, 'UNIQUE') -__evolved_set(__EXPLICIT, __NAME, 'EXPLICIT') -__evolved_set(__INTERNAL, __NAME, 'INTERNAL') - -__evolved_set(__DEFAULT, __NAME, 'DEFAULT') -__evolved_set(__DUPLICATE, __NAME, 'DUPLICATE') - -__evolved_set(__REALLOC, __NAME, 'REALLOC') -__evolved_set(__COMPMOVE, __NAME, 'COMPMOVE') - -__evolved_set(__PREFAB, __NAME, 'PREFAB') -__evolved_set(__DISABLED, __NAME, 'DISABLED') - -__evolved_set(__INCLUDES, __NAME, 'INCLUDES') -__evolved_set(__EXCLUDES, __NAME, 'EXCLUDES') -__evolved_set(__VARIANTS, __NAME, 'VARIANTS') -__evolved_set(__REQUIRES, __NAME, 'REQUIRES') - -__evolved_set(__ON_SET, __NAME, 'ON_SET') -__evolved_set(__ON_ASSIGN, __NAME, 'ON_ASSIGN') -__evolved_set(__ON_INSERT, __NAME, 'ON_INSERT') -__evolved_set(__ON_REMOVE, __NAME, 'ON_REMOVE') - -__evolved_set(__GROUP, __NAME, 'GROUP') - -__evolved_set(__QUERY, __NAME, 'QUERY') -__evolved_set(__EXECUTE, __NAME, 'EXECUTE') - -__evolved_set(__PROLOGUE, __NAME, 'PROLOGUE') -__evolved_set(__EPILOGUE, __NAME, 'EPILOGUE') - -__evolved_set(__DESTRUCTION_POLICY, __NAME, 'DESTRUCTION_POLICY') -__evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __NAME, 'DESTRUCTION_POLICY_DESTROY_ENTITY') -__evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, 'DESTRUCTION_POLICY_REMOVE_FRAGMENT') - ---- ---- ---- ---- ---- - -__evolved_set(__TAG, __INTERNAL) -__evolved_set(__NAME, __INTERNAL) - -__evolved_set(__UNIQUE, __INTERNAL) -__evolved_set(__EXPLICIT, __INTERNAL) -__evolved_set(__INTERNAL, __INTERNAL) - -__evolved_set(__DEFAULT, __INTERNAL) -__evolved_set(__DUPLICATE, __INTERNAL) - -__evolved_set(__REALLOC, __INTERNAL) -__evolved_set(__COMPMOVE, __INTERNAL) - -__evolved_set(__PREFAB, __INTERNAL) -__evolved_set(__DISABLED, __INTERNAL) - -__evolved_set(__INCLUDES, __INTERNAL) -__evolved_set(__EXCLUDES, __INTERNAL) -__evolved_set(__VARIANTS, __INTERNAL) -__evolved_set(__REQUIRES, __INTERNAL) - -__evolved_set(__ON_SET, __INTERNAL) -__evolved_set(__ON_ASSIGN, __INTERNAL) -__evolved_set(__ON_INSERT, __INTERNAL) -__evolved_set(__ON_REMOVE, __INTERNAL) - -__evolved_set(__GROUP, __INTERNAL) - -__evolved_set(__QUERY, __INTERNAL) -__evolved_set(__EXECUTE, __INTERNAL) - -__evolved_set(__PROLOGUE, __INTERNAL) -__evolved_set(__EPILOGUE, __INTERNAL) - -__evolved_set(__DESTRUCTION_POLICY, __INTERNAL) -__evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __INTERNAL) -__evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __INTERNAL) - ---- ---- ---- ---- ---- - __evolved_set(__TAG, __TAG) __evolved_set(__UNIQUE, __TAG) @@ -7923,6 +7835,94 @@ __evolved_set(__GROUP, __ON_REMOVE, function(system) __update_major_chunks(system) end) +--- +--- +--- +--- +--- + +__evolved_set(__TAG, __NAME, 'TAG') +__evolved_set(__NAME, __NAME, 'NAME') + +__evolved_set(__UNIQUE, __NAME, 'UNIQUE') +__evolved_set(__EXPLICIT, __NAME, 'EXPLICIT') +__evolved_set(__INTERNAL, __NAME, 'INTERNAL') + +__evolved_set(__DEFAULT, __NAME, 'DEFAULT') +__evolved_set(__DUPLICATE, __NAME, 'DUPLICATE') + +__evolved_set(__REALLOC, __NAME, 'REALLOC') +__evolved_set(__COMPMOVE, __NAME, 'COMPMOVE') + +__evolved_set(__PREFAB, __NAME, 'PREFAB') +__evolved_set(__DISABLED, __NAME, 'DISABLED') + +__evolved_set(__INCLUDES, __NAME, 'INCLUDES') +__evolved_set(__EXCLUDES, __NAME, 'EXCLUDES') +__evolved_set(__VARIANTS, __NAME, 'VARIANTS') +__evolved_set(__REQUIRES, __NAME, 'REQUIRES') + +__evolved_set(__ON_SET, __NAME, 'ON_SET') +__evolved_set(__ON_ASSIGN, __NAME, 'ON_ASSIGN') +__evolved_set(__ON_INSERT, __NAME, 'ON_INSERT') +__evolved_set(__ON_REMOVE, __NAME, 'ON_REMOVE') + +__evolved_set(__GROUP, __NAME, 'GROUP') + +__evolved_set(__QUERY, __NAME, 'QUERY') +__evolved_set(__EXECUTE, __NAME, 'EXECUTE') + +__evolved_set(__PROLOGUE, __NAME, 'PROLOGUE') +__evolved_set(__EPILOGUE, __NAME, 'EPILOGUE') + +__evolved_set(__DESTRUCTION_POLICY, __NAME, 'DESTRUCTION_POLICY') +__evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __NAME, 'DESTRUCTION_POLICY_DESTROY_ENTITY') +__evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, 'DESTRUCTION_POLICY_REMOVE_FRAGMENT') + +--- +--- +--- +--- +--- + +__evolved_set(__TAG, __INTERNAL) +__evolved_set(__NAME, __INTERNAL) + +__evolved_set(__UNIQUE, __INTERNAL) +__evolved_set(__EXPLICIT, __INTERNAL) +__evolved_set(__INTERNAL, __INTERNAL) + +__evolved_set(__DEFAULT, __INTERNAL) +__evolved_set(__DUPLICATE, __INTERNAL) + +__evolved_set(__REALLOC, __INTERNAL) +__evolved_set(__COMPMOVE, __INTERNAL) + +__evolved_set(__PREFAB, __INTERNAL) +__evolved_set(__DISABLED, __INTERNAL) + +__evolved_set(__INCLUDES, __INTERNAL) +__evolved_set(__EXCLUDES, __INTERNAL) +__evolved_set(__VARIANTS, __INTERNAL) +__evolved_set(__REQUIRES, __INTERNAL) + +__evolved_set(__ON_SET, __INTERNAL) +__evolved_set(__ON_ASSIGN, __INTERNAL) +__evolved_set(__ON_INSERT, __INTERNAL) +__evolved_set(__ON_REMOVE, __INTERNAL) + +__evolved_set(__GROUP, __INTERNAL) + +__evolved_set(__QUERY, __INTERNAL) +__evolved_set(__EXECUTE, __INTERNAL) + +__evolved_set(__PROLOGUE, __INTERNAL) +__evolved_set(__EPILOGUE, __INTERNAL) + +__evolved_set(__DESTRUCTION_POLICY, __INTERNAL) +__evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __INTERNAL) +__evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __INTERNAL) + --- --- --- Predefs From 00c3833b5846b68fe0f46e7db653f8fb4a6da1e1 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 4 Oct 2026 01:25:42 +0700 Subject: [PATCH 04/13] remove search functions for a while --- README.md | 58 -------------------------------- develop/all.lua | 1 - develop/testing/search_tests.lua | 56 ------------------------------ evolved.d.tl | 4 --- evolved.lua | 52 ---------------------------- 5 files changed, 171 deletions(-) delete mode 100644 develop/testing/search_tests.lua diff --git a/README.md b/README.md index a027b9f..ffc8b4a 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ - [Modifying Operations](#modifying-operations) - [Debug Mode](#debug-mode) - [Queries](#queries) - - [Searching Entities](#searching-entities) - [Deferred Operations](#deferred-operations) - [Batch Operations](#batch-operations) - [Systems](#systems) @@ -753,29 +752,6 @@ As you can see, `evolved.execute_iterator` returns a chunk, a list of entities i > [!NOTE] > But I haven't mentioned one important thing yet: [structural changes](#structural-changes) are not allowed during any iteration over chunks. This means that you cannot insert or remove fragments from entities while iterating. Also, you cannot destroy or spawn entities because this will cause structural changes too. This is done to avoid inconsistencies in the iteration process. If we allowed structural changes here, we might skip some entities during iteration, or process the same entity multiple times. The [debug mode](#debug-mode) can catch this kind of error. -#### Searching Entities - -If you just need to collect entities matching a query and don't need to work with chunks and components, you can use the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) and [`evolved.multi_search_to`](#evolvedmulti_search_to) functions. The [`evolved.search`](#evolvedsearch) function returns the first found entity matching the query, while the [`evolved.multi_search`](#evolvedmulti_search) function returns a list of all entities matching the query. The [`evolved.multi_search_to`](#evolvedmulti_search_to) function does the same, but appends the entities to the provided list starting from the specified index and returns only the number of found entities. - -```lua ----@param query evolved.query ----@return evolved.entity? entity ----@nodiscard -function evolved.search(query) end - ----@param query evolved.query ----@return evolved.entity[] entity_list ----@return integer entity_count ----@nodiscard -function evolved.multi_search(query) end - ----@param out_entity_list evolved.entity[] ----@param out_entity_first integer ----@param query evolved.query ----@return integer entity_count -function evolved.multi_search_to(out_entity_list, out_entity_first, query) end -``` - #### Deferred Operations Now we know that structural changes are not allowed during iteration, but what if we want to make some structural changes after the iteration is finished? For example, we might want to remove some fragments from entities after we have processed them, or we might want to spawn new entities while processing existing ones. To do all of this, we can use deferred operations. @@ -1633,10 +1609,6 @@ lookup :: string -> entity? multi_lookup :: string -> entity[], integer multi_lookup_to :: entity[], integer, string -> integer -search :: query -> entity? -multi_search :: query -> entity[], integer -multi_search_to :: entity[], integer, query -> integer - process :: system... -> () process_with :: system, ... -> () @@ -1736,7 +1708,6 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z - Fixed issues: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) -- Added the new [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) functions that allow finding entities matching queries ### v1.11.1 @@ -2245,35 +2216,6 @@ function evolved.multi_lookup(name) end function evolved.multi_lookup_to(out_entity_list, out_entity_first, name) end ``` -### `evolved.search` - -```lua ----@param query evolved.query ----@return evolved.entity? entity ----@nodiscard -function evolved.search(query) end -``` - -### `evolved.multi_search` - -```lua ----@param query evolved.query ----@return evolved.entity[] entity_list ----@return integer entity_count ----@nodiscard -function evolved.multi_search(query) end -``` - -### `evolved.multi_search_to` - -```lua ----@param out_entity_list evolved.entity[] ----@param out_entity_first integer ----@param query evolved.query ----@return integer entity_count -function evolved.multi_search_to(out_entity_list, out_entity_first, query) end -``` - ### `evolved.process` ```lua diff --git a/develop/all.lua b/develop/all.lua index ba4677b..ec3c417 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -15,7 +15,6 @@ require 'develop.testing.name_tests' require 'develop.testing.process_with_tests' require 'develop.testing.realloc_tests' require 'develop.testing.requires_fragment_tests' -require 'develop.testing.search_tests' require 'develop.testing.spawn_tests' require 'develop.testing.system_as_query_tests' diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua deleted file mode 100644 index 5f0dbaa..0000000 --- a/develop/testing/search_tests.lua +++ /dev/null @@ -1,56 +0,0 @@ -local evo = require 'evolved' - -evo.debug_mode(true) - -do - local f1, f2 = evo.id(2) - local q = evo.builder():include(f1):spawn() - local q_ex = evo.builder():include(f1):exclude(f2):spawn() - - do - assert(evo.search(q) == nil) - - local entity_list, entity_count = evo.multi_search(q) - assert(entity_list and #entity_list == 0 and entity_count == 0) - end - - local e1 = evo.builder():set(f1, 1):spawn() - local e2 = evo.builder():set(f1, 2):set(f2, 3):spawn() - local e3 = evo.builder():set(f1, 4):spawn() - - do - local e = evo.search(q) - assert(e == e1 or e == e2 or e == e3) - assert(evo.search(q_ex) == e1 or evo.search(q_ex) == e3) - end - - do - local entity_list, entity_count = evo.multi_search(q) - assert(entity_count == 3 and #entity_list == 3) - - local entity_set = {} - for i = 1, entity_count do entity_set[entity_list[i]] = true end - assert(entity_set[e1] and entity_set[e2] and entity_set[e3]) - end - - do - local entity_list, entity_count = evo.multi_search(q_ex) - assert(entity_count == 2 and #entity_list == 2) - - local entity_set = {} - for i = 1, entity_count do entity_set[entity_list[i]] = true end - assert(entity_set[e1] and entity_set[e3] and not entity_set[e2]) - end - - do - local entity_list = { 'a', 'b' } - local entity_count = evo.multi_search_to(entity_list, 3, q) - - assert(entity_count == 3 and #entity_list == 5) - assert(entity_list[1] == 'a' and entity_list[2] == 'b') - - local entity_set = {} - for i = 3, 3 + entity_count - 1 do entity_set[entity_list[i]] = true end - assert(entity_set[e1] and entity_set[e2] and entity_set[e3]) - end -end diff --git a/evolved.d.tl b/evolved.d.tl index 16b7ef1..c3be05a 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -272,10 +272,6 @@ multi_lookup: function(name: string): { Entity }, integer multi_lookup_to: function(out_entity_list: { Entity }, out_entity_first: integer, name: string): integer - search: function(query: Query): Entity | nil - multi_search: function(query: Query): { Entity }, integer - multi_search_to: function(out_entity_list: { Entity }, out_entity_first: integer, query: Query): integer - process: function(...: System) process_with: function(system: System, ...: any) diff --git a/evolved.lua b/evolved.lua index 1dc236d..d884ab6 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1205,10 +1205,6 @@ local __evolved_lookup local __evolved_multi_lookup local __evolved_multi_lookup_to -local __evolved_search -local __evolved_multi_search -local __evolved_multi_search_to - local __evolved_process local __evolved_process_with @@ -6369,50 +6365,6 @@ function __evolved_multi_lookup_to(out_entity_list, out_entity_first, name) return 0 end ----@param query evolved.query ----@return evolved.entity? entity ----@nodiscard -function __evolved_search(query) - local execute_iterator, execute_state = __evolved_execute(query) - if not execute_state then return end - - local chunk, chunk_entity_list = execute_iterator(execute_state) - if not chunk or not chunk_entity_list then return end - - __release_table(__table_pool_tag.chunk_list, execute_state[2], false, true) - __release_table(__table_pool_tag.execute_state, execute_state, true, true) - - return chunk_entity_list[1] -end - ----@param query evolved.query ----@return evolved.entity[] entity_list ----@return integer entity_count ----@nodiscard -function __evolved_multi_search(query) - local entity_list = {} - local entity_count = __evolved_multi_search_to(entity_list, 1, query) - return entity_list, entity_count -end - ----@param out_entity_list evolved.entity[] ----@param out_entity_first integer ----@param query evolved.query ----@return integer entity_count -function __evolved_multi_search_to(out_entity_list, out_entity_first, query) - local out_entity_count = 0 - - for _, chunk_entity_list, chunk_entity_count in __evolved_execute(query) do - __lua_table_move( - chunk_entity_list, 1, chunk_entity_count, - out_entity_first + out_entity_count, out_entity_list) - - out_entity_count = out_entity_count + chunk_entity_count - end - - return out_entity_count -end - ---@param ... evolved.system systems function __evolved_process(...) local argument_count = __lua_select('#', ...) @@ -8027,10 +7979,6 @@ evolved.lookup = __evolved_lookup evolved.multi_lookup = __evolved_multi_lookup evolved.multi_lookup_to = __evolved_multi_lookup_to -evolved.search = __evolved_search -evolved.multi_search = __evolved_multi_search -evolved.multi_search_to = __evolved_multi_search_to - evolved.process = __evolved_process evolved.process_with = __evolved_process_with From 4db6ac03781c9ab390b34b432b23c4f3a3461283 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 4 Oct 2026 05:59:12 +0700 Subject: [PATCH 05/13] all internal names now have a double underscore prefix --- README.md | 3 ++- develop/issues/issue_52.lua | 5 ++-- evolved.lua | 54 ++++++++++++++++++------------------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ffc8b4a..fb97075 100644 --- a/README.md +++ b/README.md @@ -1707,7 +1707,8 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z -- Fixed issues: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) +- Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) +- All internal names now have a double underscore prefix ### v1.11.1 diff --git a/develop/issues/issue_52.lua b/develop/issues/issue_52.lua index 37e6607..d6fb245 100644 --- a/develop/issues/issue_52.lua +++ b/develop/issues/issue_52.lua @@ -3,8 +3,8 @@ local evo = require 'evolved' evo.debug_mode(true) do - assert(evo.name(evo.TAG) == 'TAG') - assert(evo.lookup('TAG') == evo.TAG) + assert(evo.name(evo.TAG) == '__TAG') + assert(evo.lookup('__TAG') == evo.TAG) end do @@ -16,6 +16,7 @@ do for i = 1, entity_count do local entity = entity_list[i] assert(type(evo.get(entity, evo.NAME)) == 'string') + assert(evo.get(entity, evo.NAME):sub(1, 2) == '__') assert(evo.name(entity) == evo.get(entity, evo.NAME)) assert(evo.lookup(evo.name(entity)) == entity) end diff --git a/evolved.lua b/evolved.lua index d884ab6..741ecba 100644 --- a/evolved.lua +++ b/evolved.lua @@ -7793,43 +7793,43 @@ end) --- --- -__evolved_set(__TAG, __NAME, 'TAG') -__evolved_set(__NAME, __NAME, 'NAME') +__evolved_set(__TAG, __NAME, '__TAG') +__evolved_set(__NAME, __NAME, '__NAME') -__evolved_set(__UNIQUE, __NAME, 'UNIQUE') -__evolved_set(__EXPLICIT, __NAME, 'EXPLICIT') -__evolved_set(__INTERNAL, __NAME, 'INTERNAL') +__evolved_set(__UNIQUE, __NAME, '__UNIQUE') +__evolved_set(__EXPLICIT, __NAME, '__EXPLICIT') +__evolved_set(__INTERNAL, __NAME, '__INTERNAL') -__evolved_set(__DEFAULT, __NAME, 'DEFAULT') -__evolved_set(__DUPLICATE, __NAME, 'DUPLICATE') +__evolved_set(__DEFAULT, __NAME, '__DEFAULT') +__evolved_set(__DUPLICATE, __NAME, '__DUPLICATE') -__evolved_set(__REALLOC, __NAME, 'REALLOC') -__evolved_set(__COMPMOVE, __NAME, 'COMPMOVE') +__evolved_set(__REALLOC, __NAME, '__REALLOC') +__evolved_set(__COMPMOVE, __NAME, '__COMPMOVE') -__evolved_set(__PREFAB, __NAME, 'PREFAB') -__evolved_set(__DISABLED, __NAME, 'DISABLED') +__evolved_set(__PREFAB, __NAME, '__PREFAB') +__evolved_set(__DISABLED, __NAME, '__DISABLED') -__evolved_set(__INCLUDES, __NAME, 'INCLUDES') -__evolved_set(__EXCLUDES, __NAME, 'EXCLUDES') -__evolved_set(__VARIANTS, __NAME, 'VARIANTS') -__evolved_set(__REQUIRES, __NAME, 'REQUIRES') +__evolved_set(__INCLUDES, __NAME, '__INCLUDES') +__evolved_set(__EXCLUDES, __NAME, '__EXCLUDES') +__evolved_set(__VARIANTS, __NAME, '__VARIANTS') +__evolved_set(__REQUIRES, __NAME, '__REQUIRES') -__evolved_set(__ON_SET, __NAME, 'ON_SET') -__evolved_set(__ON_ASSIGN, __NAME, 'ON_ASSIGN') -__evolved_set(__ON_INSERT, __NAME, 'ON_INSERT') -__evolved_set(__ON_REMOVE, __NAME, 'ON_REMOVE') +__evolved_set(__ON_SET, __NAME, '__ON_SET') +__evolved_set(__ON_ASSIGN, __NAME, '__ON_ASSIGN') +__evolved_set(__ON_INSERT, __NAME, '__ON_INSERT') +__evolved_set(__ON_REMOVE, __NAME, '__ON_REMOVE') -__evolved_set(__GROUP, __NAME, 'GROUP') +__evolved_set(__GROUP, __NAME, '__GROUP') -__evolved_set(__QUERY, __NAME, 'QUERY') -__evolved_set(__EXECUTE, __NAME, 'EXECUTE') +__evolved_set(__QUERY, __NAME, '__QUERY') +__evolved_set(__EXECUTE, __NAME, '__EXECUTE') -__evolved_set(__PROLOGUE, __NAME, 'PROLOGUE') -__evolved_set(__EPILOGUE, __NAME, 'EPILOGUE') +__evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') +__evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') -__evolved_set(__DESTRUCTION_POLICY, __NAME, 'DESTRUCTION_POLICY') -__evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __NAME, 'DESTRUCTION_POLICY_DESTROY_ENTITY') -__evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, 'DESTRUCTION_POLICY_REMOVE_FRAGMENT') +__evolved_set(__DESTRUCTION_POLICY, __NAME, '__DESTRUCTION_POLICY') +__evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __NAME, '__DESTRUCTION_POLICY_DESTROY_ENTITY') +__evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, '__DESTRUCTION_POLICY_REMOVE_FRAGMENT') --- --- From e5d56d831a38161fe7ae423f0f3a18554936ea64 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 5 Oct 2026 06:46:09 +0700 Subject: [PATCH 06/13] indices: internal only __INDEX fragment --- evolved.lua | 675 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 507 insertions(+), 168 deletions(-) diff --git a/evolved.lua b/evolved.lua index 741ecba..ab02080 100644 --- a/evolved.lua +++ b/evolved.lua @@ -149,6 +149,9 @@ local evolved = { ---@field package __has_assign_hooks boolean ---@field package __has_insert_hooks boolean ---@field package __has_remove_hooks boolean +---@field package __has_index_major boolean +---@field package __has_index_minors boolean +---@field package __has_index_fragments boolean ---@field package __has_unique_major boolean ---@field package __has_unique_minors boolean ---@field package __has_unique_fragments boolean @@ -163,6 +166,8 @@ local evolved = { ---@field package __insert_fragment_count integer ---@field package __remove_fragment_list? evolved.fragment[] ---@field package __remove_fragment_count integer +---@field package __index_fragment_list? evolved.fragment[] +---@field package __index_fragment_count integer ---@field package __explicit_fragment_list? evolved.fragment[] ---@field package __explicit_fragment_count integer local __chunk_mt = {} @@ -623,17 +628,13 @@ local __major_queries = {} ---@type table -local __named_entities = {} ---@type table> +local __search_indices = {} ---@type table local __sorted_includes = {} ---@type table> local __sorted_excludes = {} ---@type table> local __sorted_variants = {} ---@type table> local __sorted_requires = {} ---@type table> -local __subsystem_groups = {} ---@type table -local __group_subsystems = {} ---@type table> - local __structural_changes = 0 ---@type integer --- @@ -1105,9 +1106,93 @@ end --- --- +---@class (exact) evolved.search_index: { +--- __entity_map: { [evolved.component]: evolved.entity }, +--- __entities_map: { [evolved.component]: evolved.assoc_list }, +--- } + +local __search_index_fns = {} + +---@return evolved.search_index +---@nodiscard +function __search_index_fns.new() + ---@type evolved.search_index + return { + __entity_map = __lua_table_new(), + __entities_map = __lua_table_new(), + } +end + +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.insert(index, entity, component) + if component ~= component then + -- NaN components cannot be indexed + return + end + + ---@type evolved.entity? + local indexed_entity = index.__entity_map[component] + + if not indexed_entity then + index.__entity_map[component] = entity + return + end + + ---@type evolved.assoc_list? + local indexed_entities = index.__entities_map[component] + + if not indexed_entities then + index.__entities_map[component] = __assoc_list_fns.from(indexed_entity, entity) + return + end + + __assoc_list_fns.insert(indexed_entities, entity) +end + +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.remove(index, entity, component) + ---@type evolved.assoc_list? + local indexed_entities = index.__entities_map[component] + + if indexed_entities then + if __assoc_list_fns.remove(indexed_entities, entity) == 0 then + index.__entities_map[component], indexed_entities = nil, nil + end + end + + ---@type evolved.entity? + local indexed_entity = index.__entity_map[component] + + if indexed_entity == entity then + index.__entity_map[component] = indexed_entities and indexed_entities.__item_list[1] or nil + end +end + +---@param index evolved.search_index +---@param entity evolved.entity +---@param new_component evolved.component +---@param old_component evolved.component +function __search_index_fns.assign(index, entity, new_component, old_component) + if new_component ~= old_component then + __search_index_fns.remove(index, entity, old_component) + __search_index_fns.insert(index, entity, new_component) + end +end + +--- +--- +--- +--- +--- + local __TAG = __acquire_id() local __NAME = __acquire_id() +local __INDEX = __acquire_id() local __UNIQUE = __acquire_id() local __EXPLICIT = __acquire_id() local __INTERNAL = __acquire_id() @@ -1205,6 +1290,10 @@ local __evolved_lookup local __evolved_multi_lookup local __evolved_multi_lookup_to +local __evolved_search +local __evolved_multi_search +local __evolved_multi_search_to + local __evolved_process local __evolved_process_with @@ -1246,6 +1335,9 @@ local __query_minor_matches local __update_major_chunks local __update_major_chunks_trace +local __update_search_index +local __update_search_index_trace + local __chunk_with_fragment local __chunk_with_components local __chunk_without_fragment @@ -1397,6 +1489,9 @@ function __new_chunk(chunk_parent, chunk_fragment) __has_assign_hooks = false, __has_insert_hooks = false, __has_remove_hooks = false, + __has_index_major = false, + __has_index_minors = false, + __has_index_fragments = false, __has_unique_major = false, __has_unique_minors = false, __has_unique_fragments = false, @@ -1411,6 +1506,8 @@ function __new_chunk(chunk_parent, chunk_fragment) __insert_fragment_count = 0, __remove_fragment_list = nil, __remove_fragment_count = 0, + __index_fragment_list = nil, + __index_fragment_count = 0, __explicit_fragment_list = nil, __explicit_fragment_count = 0, }, __chunk_mt) @@ -1612,6 +1709,10 @@ function __update_chunk_caches(chunk) local has_insert_hooks = has_insert_major or has_insert_minors local has_remove_hooks = has_remove_major or has_remove_minors + local has_index_major = __search_indices[chunk_fragment] ~= nil + local has_index_minors = chunk_parent ~= nil and chunk_parent.__has_index_fragments + local has_index_fragments = has_index_major or has_index_minors + local has_unique_major = __evolved_has(chunk_fragment, __UNIQUE) local has_unique_minors = chunk_parent ~= nil and chunk_parent.__has_unique_fragments local has_unique_fragments = has_unique_major or has_unique_minors @@ -1652,6 +1753,10 @@ function __update_chunk_caches(chunk) chunk.__has_insert_hooks = has_insert_hooks chunk.__has_remove_hooks = has_remove_hooks + chunk.__has_index_major = has_index_major + chunk.__has_index_minors = has_index_minors + chunk.__has_index_fragments = has_index_fragments + chunk.__has_unique_major = has_unique_major chunk.__has_unique_minors = has_unique_minors chunk.__has_unique_fragments = has_unique_fragments @@ -1724,6 +1829,29 @@ function __update_chunk_caches(chunk) chunk.__remove_fragment_count = 0 end + if has_index_fragments then + local parent_index_fragment_list = chunk_parent and chunk_parent.__index_fragment_list + local parent_index_fragment_count = chunk_parent and chunk_parent.__index_fragment_count or 0 + + if has_index_major then + local index_fragment_list, index_fragment_count = parent_index_fragment_list + and __list_fns.dup(parent_index_fragment_list, parent_index_fragment_count) + or __list_fns.new(1), 0 + + index_fragment_count = parent_index_fragment_count + 1 + index_fragment_list[index_fragment_count] = chunk_fragment + + chunk.__index_fragment_list = index_fragment_list + chunk.__index_fragment_count = index_fragment_count + else + chunk.__index_fragment_list = parent_index_fragment_list + chunk.__index_fragment_count = parent_index_fragment_count + end + else + chunk.__index_fragment_list = nil + chunk.__index_fragment_count = 0 + end + if has_explicit_fragments then local parent_explicit_fragment_list = chunk_parent and chunk_parent.__explicit_fragment_list local parent_explicit_fragment_count = chunk_parent and chunk_parent.__explicit_fragment_count or 0 @@ -2165,9 +2293,10 @@ end function __update_major_chunks(major) if __defer_depth > 0 then __defer_call_hook(__update_major_chunks, major) - else - __trace_major_chunks(major, __update_major_chunks_trace) + return end + + __trace_major_chunks(major, __update_major_chunks_trace) end ---@param chunk evolved.chunk @@ -2177,6 +2306,42 @@ function __update_major_chunks_trace(chunk) __update_chunk_storages(chunk) end +---@param fragment evolved.fragment +function __update_search_index(fragment) + if __defer_depth > 0 then + __defer_call_hook(__update_search_index, fragment) + return + end + + if __evolved_has(fragment, __INDEX) then + local fragment_search_index = __search_index_fns.new() + __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) + __search_indices[fragment] = fragment_search_index + else + __search_indices[fragment] = nil + end + + __update_major_chunks(fragment) +end + +---@param chunk evolved.chunk +---@param fragment evolved.fragment +---@param fragment_search_index evolved.search_index +function __update_search_index_trace(chunk, fragment, fragment_search_index) + local chunk_component_index = chunk.__component_indices[fragment] + + if chunk_component_index then + local chunk_entity_list = chunk.__entity_list + local chunk_component_storage = chunk.__component_storages[chunk_component_index] + + for place = 1, chunk.__entity_count do + local entity = chunk_entity_list[place] + local component = chunk_component_storage[place] + __search_index_fns.insert(fragment_search_index, entity, component) + end + end +end + ---@param chunk? evolved.chunk ---@param fragment evolved.fragment ---@return evolved.chunk @@ -2716,6 +2881,22 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) component_mapper(chunk, place, place) end + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + local new_component = component_storage[place] + __search_index_fns.insert(fragment_search_index, entity, new_component) + end + end + end + local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then @@ -2854,6 +3035,26 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co component_mapper(chunk, b_place, e_place) end + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + + for place = b_place, e_place do + local entity = chunk_entity_list[place] + local new_component = component_storage[place] + __search_index_fns.insert(fragment_search_index, entity, new_component) + end + end + end + end + local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then @@ -3014,6 +3215,22 @@ function __clone_entity(prefab, entity, component_table, component_mapper) component_mapper(chunk, place, place) end + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + local new_component = component_storage[place] + __search_index_fns.insert(fragment_search_index, entity, new_component) + end + end + end + local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then @@ -3181,6 +3398,26 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c component_mapper(chunk, b_place, e_place) end + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + + for place = b_place, e_place do + local entity = chunk_entity_list[place] + local new_component = component_storage[place] + __search_index_fns.insert(fragment_search_index, entity, new_component) + end + end + end + end + local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then @@ -3476,6 +3713,25 @@ function __clear_entity_one(entity) end end end + + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages + + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + local old_component = component_storage[place] + __search_index_fns.remove(fragment_search_index, entity, old_component) + end + end + end end if chunk then @@ -3545,6 +3801,25 @@ function __destroy_entity_one(entity) end end end + + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages + + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + local old_component = component_storage[place] + __search_index_fns.remove(fragment_search_index, entity, old_component) + end + end + end end if chunk then @@ -3752,6 +4027,7 @@ function __chunk_set(old_chunk, fragment, component) if old_chunk == new_chunk then local old_chunk_has_setup_hooks = old_chunk.__has_setup_hooks local old_chunk_has_assign_hooks = old_chunk.__has_assign_hooks + local old_chunk_has_index_fragments = old_chunk.__has_index_fragments ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.assign_hook? local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign @@ -3761,7 +4037,14 @@ function __chunk_set(old_chunk, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) end - if fragment_on_set or fragment_on_assign then + ---@type evolved.search_index? + local fragment_search_index + + if old_chunk_has_index_fragments then + fragment_search_index = __search_indices[fragment] + end + + if fragment_on_set or fragment_on_assign or fragment_search_index then local old_component_index = old_component_indices[fragment] if old_component_index then @@ -3786,6 +4069,10 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_assign then __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) end + + if fragment_search_index then + __search_index_fns.assign(fragment_search_index, entity, new_component, old_component) + end end else local new_component = component @@ -3805,6 +4092,10 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_assign then __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) end + + if fragment_search_index then + __search_index_fns.assign(fragment_search_index, entity, new_component, old_component) + end end end else @@ -3858,6 +4149,7 @@ function __chunk_set(old_chunk, fragment, component) local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks local new_chunk_has_insert_hooks = new_chunk.__has_insert_hooks + local new_chunk_has_index_fragments = new_chunk.__has_index_fragments ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.insert_hook? local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_insert @@ -3867,6 +4159,13 @@ function __chunk_set(old_chunk, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) end + ---@type evolved.search_index? + local fragment_search_index + + if new_chunk_has_index_fragments then + fragment_search_index = __search_indices[fragment] + end + local sum_entity_count = old_entity_count + new_chunk.__entity_count if sum_entity_count > new_chunk.__entity_capacity then @@ -3932,7 +4231,7 @@ function __chunk_set(old_chunk, fragment, component) __detach_all_entities(old_chunk) end - if fragment_on_set or fragment_on_insert then + if fragment_on_set or fragment_on_insert or fragment_search_index then local new_component_index = new_component_indices[fragment] if new_component_index then @@ -3956,6 +4255,10 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_insert then __defer_call_hook(fragment_on_insert, entity, fragment, new_component) end + + if fragment_search_index then + __search_index_fns.insert(fragment_search_index, entity, new_component) + end end else local new_component = component @@ -3974,6 +4277,10 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_insert then __defer_call_hook(fragment_on_insert, entity, fragment, new_component) end + + if fragment_search_index then + __search_index_fns.insert(fragment_search_index, entity, new_component) + end end end else @@ -4034,7 +4341,14 @@ function __chunk_set(old_chunk, fragment, component) __evolved_get(req_fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) end - if req_fragment_on_set or req_fragment_on_insert then + ---@type evolved.search_index? + local req_fragment_search_index + + if new_chunk_has_index_fragments then + req_fragment_search_index = __search_indices[req_fragment] + end + + if req_fragment_on_set or req_fragment_on_insert or req_fragment_search_index then local req_component_index = new_component_indices[req_fragment] if req_component_index then @@ -4057,6 +4371,10 @@ function __chunk_set(old_chunk, fragment, component) if req_fragment_on_insert then __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) end + + if req_fragment_search_index then + __search_index_fns.insert(req_fragment_search_index, entity, req_component) + end end else local req_component = req_fragment_default @@ -4074,6 +4392,10 @@ function __chunk_set(old_chunk, fragment, component) if req_fragment_on_insert then __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) end + + if req_fragment_search_index then + __search_index_fns.insert(req_fragment_search_index, entity, req_component) + end end end else @@ -4185,6 +4507,32 @@ function __chunk_remove(old_chunk, ...) end end + local old_index_fragment_list = old_chunk.__index_fragment_list + + if old_index_fragment_list then + local new_fragment_set = new_chunk and new_chunk.__fragment_set + or __safe_tbls.__EMPTY_FRAGMENT_SET + + for old_index_fragment_index = 1, old_chunk.__index_fragment_count do + local fragment = old_index_fragment_list[old_index_fragment_index] + + if not new_fragment_set[fragment] then + local old_component_index = old_component_indices[fragment] + + if old_component_index then + local fragment_search_index = __search_indices[fragment] + local old_component_storage = old_component_storages[old_component_index] + + for old_place = 1, old_entity_count do + local entity = old_entity_list[old_place] + local old_component = old_component_storage[old_place] + __search_index_fns.remove(fragment_search_index, entity, old_component) + end + end + end + end + end + if new_chunk then local new_component_count = new_chunk.__component_count local new_component_storages = new_chunk.__component_storages @@ -4319,6 +4667,29 @@ function __chunk_clear(chunk) end end + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages + + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] + local old_component = component_storage[place] + __search_index_fns.remove(fragment_search_index, entity, old_component) + end + end + end + end + do local entity_chunks = __entity_chunks local entity_places = __entity_places @@ -4780,29 +5151,20 @@ local function __system_process(system, ...) __evolved_commit() end - do - local group_subsystems = __group_subsystems[system] - local group_subsystem_list = group_subsystems and group_subsystems.__item_list - local group_subsystem_count = group_subsystems and group_subsystems.__item_count or 0 - - if group_subsystem_count > 0 then - ---@type evolved.system[] - local subsystem_list = __acquire_table(__table_pool_tag.system_list) - - __lua_table_move( - group_subsystem_list, 1, group_subsystem_count, - 1, subsystem_list) + if __evolved_search(__GROUP, system) then + ---@type evolved.system[] + local subsystem_list = __acquire_table(__table_pool_tag.system_list) + local subsystem_count = __evolved_multi_search_to(subsystem_list, 1, __GROUP, system) - for subsystem_index = 1, group_subsystem_count do - local subsystem = subsystem_list[subsystem_index] - if not __evolved_has(subsystem, __DISABLED) then - __system_process(subsystem, ...) - end + for subsystem_index = 1, subsystem_count do + local subsystem = subsystem_list[subsystem_index] + if not __evolved_has(subsystem, __DISABLED) then + __system_process(subsystem, ...) end - - __release_table(__table_pool_tag.system_list, subsystem_list, - group_subsystem_count == 0, true) end + + __release_table(__table_pool_tag.system_list, subsystem_list, + subsystem_count == 0, true) end if epilogue then @@ -5468,6 +5830,7 @@ function __evolved_set(entity, fragment, component) local old_chunk_has_setup_hooks = old_chunk.__has_setup_hooks local old_chunk_has_assign_hooks = old_chunk.__has_assign_hooks + local old_chunk_has_index_fragments = old_chunk.__has_index_fragments ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.assign_hook? local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign @@ -5477,6 +5840,13 @@ function __evolved_set(entity, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) end + ---@type evolved.search_index? + local fragment_search_index + + if old_chunk_has_index_fragments then + fragment_search_index = __search_indices[fragment] + end + local old_component_index = old_component_indices[fragment] if old_component_index then @@ -5497,6 +5867,10 @@ function __evolved_set(entity, fragment, component) if fragment_on_assign then __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) end + + if fragment_search_index then + __search_index_fns.assign(fragment_search_index, entity, new_component, old_component) + end else -- nothing end @@ -5520,6 +5894,7 @@ function __evolved_set(entity, fragment, component) local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks local new_chunk_has_insert_hooks = new_chunk.__has_insert_hooks + local new_chunk_has_index_fragments = new_chunk.__has_index_fragments ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.insert_hook? local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_insert @@ -5529,6 +5904,13 @@ function __evolved_set(entity, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) end + ---@type evolved.search_index? + local fragment_search_index + + if new_chunk_has_index_fragments then + fragment_search_index = __search_indices[fragment] + end + local new_place = new_chunk.__entity_count + 1 if new_place > new_chunk.__entity_capacity then @@ -5585,6 +5967,10 @@ function __evolved_set(entity, fragment, component) if fragment_on_insert then __defer_call_hook(fragment_on_insert, entity, fragment, new_component) end + + if fragment_search_index then + __search_index_fns.insert(fragment_search_index, entity, new_component) + end else if fragment_on_set then __defer_call_hook(fragment_on_set, entity, fragment) @@ -5614,6 +6000,13 @@ function __evolved_set(entity, fragment, component) __evolved_get(req_fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) end + ---@type evolved.search_index? + local req_fragment_search_index + + if new_chunk_has_index_fragments then + req_fragment_search_index = __search_indices[req_fragment] + end + local req_component_index = new_component_indices[req_fragment] if req_component_index then @@ -5638,6 +6031,10 @@ function __evolved_set(entity, fragment, component) if req_fragment_on_insert then __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) end + + if req_fragment_search_index then + __search_index_fns.insert(req_fragment_search_index, entity, req_component) + end else if req_fragment_on_set then __defer_call_hook(req_fragment_on_set, entity, req_fragment) @@ -5718,6 +6115,28 @@ function __evolved_remove(entity, ...) end end + local old_index_fragment_list = old_chunk.__index_fragment_list + + if old_index_fragment_list then + local new_fragment_set = new_chunk and new_chunk.__fragment_set + or __safe_tbls.__EMPTY_FRAGMENT_SET + + for old_index_fragment_index = 1, old_chunk.__index_fragment_count do + local fragment = old_index_fragment_list[old_index_fragment_index] + + if not new_fragment_set[fragment] then + local old_component_index = old_component_indices[fragment] + + if old_component_index then + local fragment_search_index = __search_indices[fragment] + local old_component_storage = old_component_storages[old_component_index] + local old_component = old_component_storage[old_place] + __search_index_fns.remove(fragment_search_index, entity, old_component) + end + end + end + end + if new_chunk then local new_component_count = new_chunk.__component_count local new_component_storages = new_chunk.__component_storages @@ -6322,7 +6741,7 @@ end ---@return evolved.entity? entity ---@nodiscard function __evolved_lookup(name) - return __named_entity[name] + return __evolved_search(__NAME, name) end ---@param name string @@ -6330,9 +6749,7 @@ end ---@return integer entity_count ---@nodiscard function __evolved_multi_lookup(name) - local entity_list = {} - local entity_count = __evolved_multi_lookup_to(entity_list, 1, name) - return entity_list, entity_count + return __evolved_multi_search(__NAME, name) end ---@param out_entity_list evolved.entity[] @@ -6340,24 +6757,59 @@ end ---@param name string ---@return integer entity_count function __evolved_multi_lookup_to(out_entity_list, out_entity_first, name) + return __evolved_multi_search_to(out_entity_list, out_entity_first, __NAME, name) +end + +---@param fragment evolved.fragment +---@param component evolved.component +---@return evolved.entity? entity +---@nodiscard +function __evolved_search(fragment, component) + local fragment_search_index = __search_indices[fragment] + return fragment_search_index and fragment_search_index.__entity_map[component] +end + +---@param fragment evolved.fragment +---@param component evolved.component +---@return evolved.entity[] entity_list +---@return integer entity_count +---@nodiscard +function __evolved_multi_search(fragment, component) + local entity_list = {} + local entity_count = __evolved_multi_search_to(entity_list, 1, fragment, component) + return entity_list, entity_count +end + +---@param out_entity_list evolved.entity[] +---@param out_entity_first integer +---@param fragment evolved.fragment +---@param component evolved.component +---@return integer entity_count +function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, component) + local fragment_search_index = __search_indices[fragment] + + if not fragment_search_index then + return 0 + end + do - local named_entities = __named_entities[name] - local named_entity_list = named_entities and named_entities.__item_list - local named_entity_count = named_entities and named_entities.__item_count or 0 + local indexed_entities = fragment_search_index.__entities_map[component] + local indexed_entity_list = indexed_entities and indexed_entities.__item_list + local indexed_entity_count = indexed_entities and indexed_entities.__item_count or 0 - if named_entity_count > 0 then + if indexed_entity_count > 0 then __lua_table_move( - named_entity_list, 1, named_entity_count, + indexed_entity_list, 1, indexed_entity_count, out_entity_first, out_entity_list) - return named_entity_count + return indexed_entity_count end end do - local named_entity = __named_entity[name] + local indexed_entity = fragment_search_index.__entity_map[component] - if named_entity then - out_entity_list[out_entity_first] = named_entity + if indexed_entity then + out_entity_list[out_entity_first] = indexed_entity return 1 end end @@ -7373,6 +7825,9 @@ __evolved_set(__ON_REMOVE, __ON_REMOVE, __update_major_chunks) __evolved_set(__TAG, __ON_INSERT, __update_major_chunks) __evolved_set(__TAG, __ON_REMOVE, __update_major_chunks) +__evolved_set(__INDEX, __ON_INSERT, __update_search_index) +__evolved_set(__INDEX, __ON_REMOVE, __update_search_index) + __evolved_set(__UNIQUE, __ON_INSERT, __update_major_chunks) __evolved_set(__UNIQUE, __ON_REMOVE, __update_major_chunks) @@ -7402,6 +7857,11 @@ __evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) __evolved_set(__TAG, __TAG) +__evolved_set(__NAME, __INDEX) + +__evolved_set(__INDEX, __TAG) +__evolved_set(__INDEX, __UNIQUE) + __evolved_set(__UNIQUE, __TAG) __evolved_set(__EXPLICIT, __TAG) @@ -7435,6 +7895,8 @@ __evolved_set(__ON_ASSIGN, __UNIQUE) __evolved_set(__ON_INSERT, __UNIQUE) __evolved_set(__ON_REMOVE, __UNIQUE) +__evolved_set(__GROUP, __INDEX) + --- --- --- @@ -7443,48 +7905,6 @@ __evolved_set(__ON_REMOVE, __UNIQUE) local __hook_fns = {} ----@param name string ----@param entity evolved.entity -function __hook_fns.insert_named_entity(name, entity) - ---@type evolved.entity? - local named_entity = __named_entity[name] - - if not named_entity then - __named_entity[name] = entity - return - end - - ---@type evolved.assoc_list? - local named_entities = __named_entities[name] - - if not named_entities then - __named_entities[name] = __assoc_list_fns.from(named_entity, entity) - return - end - - __assoc_list_fns.insert(named_entities, entity) -end - ----@param name string ----@param entity evolved.entity -function __hook_fns.remove_named_entity(name, entity) - ---@type evolved.assoc_list? - local named_entities = __named_entities[name] - - if named_entities then - if __assoc_list_fns.remove(named_entities, entity) == 0 then - __named_entities[name], named_entities = nil, nil - end - end - - ---@type evolved.entity? - local named_entity = __named_entity[name] - - if named_entity == entity then - __named_entity[name] = named_entities and named_entities.__item_list[1] or nil - end -end - ---@param query evolved.query function __hook_fns.insert_query(query) local query_includes = __sorted_includes[query] @@ -7559,63 +7979,6 @@ function __hook_fns.remove_query(query) __reset_query_chunks(query) end ----@param subsystem evolved.system -function __hook_fns.add_subsystem(subsystem) - local subsystem_group = __subsystem_groups[subsystem] - - if subsystem_group then - local group_subsystems = __group_subsystems[subsystem_group] - - if not group_subsystems then - ---@type evolved.assoc_list - group_subsystems = __assoc_list_fns.new(4) - __group_subsystems[subsystem_group] = group_subsystems - end - - __assoc_list_fns.insert(group_subsystems, subsystem) - end -end - ----@param subsystem evolved.system -function __hook_fns.remove_subsystem(subsystem) - local subsystem_group = __subsystem_groups[subsystem] - - if subsystem_group then - local group_subsystems = __group_subsystems[subsystem_group] - - if group_subsystems and __assoc_list_fns.remove(group_subsystems, subsystem) == 0 then - __group_subsystems[subsystem_group] = nil - end - end -end - ---- ---- ---- ---- ---- - ----@param entity evolved.entity ----@param new_name? string ----@param old_name? string -__evolved_set(__NAME, __ON_SET, function(entity, _, new_name, old_name) - if old_name then - __hook_fns.remove_named_entity(old_name, entity) - end - - if new_name then - __hook_fns.insert_named_entity(new_name, entity) - end -end) - ----@param entity evolved.entity ----@param old_name? string -__evolved_set(__NAME, __ON_REMOVE, function(entity, _, old_name) - if old_name then - __hook_fns.remove_named_entity(old_name, entity) - end -end) - --- --- --- @@ -7767,35 +8130,10 @@ end) --- --- ----@param system evolved.system -__evolved_set(__GROUP, __ON_SET, function(system, _, group) - __hook_fns.remove_subsystem(system) - - __subsystem_groups[system] = group - - __hook_fns.add_subsystem(system) - __update_major_chunks(system) -end) - ----@param system evolved.system -__evolved_set(__GROUP, __ON_REMOVE, function(system) - __hook_fns.remove_subsystem(system) - - __subsystem_groups[system] = nil - - __hook_fns.add_subsystem(system) - __update_major_chunks(system) -end) - ---- ---- ---- ---- ---- - __evolved_set(__TAG, __NAME, '__TAG') __evolved_set(__NAME, __NAME, '__NAME') +__evolved_set(__INDEX, __NAME, '__INDEX') __evolved_set(__UNIQUE, __NAME, '__UNIQUE') __evolved_set(__EXPLICIT, __NAME, '__EXPLICIT') __evolved_set(__INTERNAL, __NAME, '__INTERNAL') @@ -7840,6 +8178,7 @@ __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, '__DESTRUCTION_POLIC __evolved_set(__TAG, __INTERNAL) __evolved_set(__NAME, __INTERNAL) +__evolved_set(__INDEX, __INTERNAL) __evolved_set(__UNIQUE, __INTERNAL) __evolved_set(__EXPLICIT, __INTERNAL) __evolved_set(__INTERNAL, __INTERNAL) From 0ba301d1711c42adf1c7da52fadd575510bb775d Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 5 Oct 2026 07:13:50 +0700 Subject: [PATCH 07/13] indices: public functions, docs, tests --- README.md | 82 +++++++ develop/all.lua | 1 + develop/testing/search_tests.lua | 402 +++++++++++++++++++++++++++++++ evolved.d.tl | 6 + evolved.lua | 10 + 5 files changed, 501 insertions(+) create mode 100644 develop/testing/search_tests.lua diff --git a/README.md b/README.md index fb97075..eaa95a4 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ - [Entity Names](#entity-names) - [Fragment Tags](#fragment-tags) - [Fragment Hooks](#fragment-hooks) + - [Indexed Fragments](#indexed-fragments) - [Unique Fragments](#unique-fragments) - [Explicit Fragments](#explicit-fragments) - [Internal Fragments](#internal-fragments) @@ -1070,6 +1071,8 @@ local player_list, player_count = evolved.multi_lookup('Player') assert(player_count == 2 and player_list[1] == player1 and player_list[2] == player2) ``` +Under the hood, the [`evolved.NAME`](#evolvedname) fragment is marked with the [`evolved.INDEX`](#evolvedindex) fragment trait, so the [`evolved.lookup`](#evolvedlookup) function is just a shorthand for the [`evolved.search`](#evolvedsearch) function with the [`evolved.NAME`](#evolvedname) fragment. See the [Indexed Fragments](#indexed-fragments) section for more details. + #### Fragment Tags Sometimes you want to have a fragment without a component. For example, you might want to have some marks that will be used to mark entities for processing. Fragments without components are called `tags`. Such fragments take up less memory, because they do not require any components to be stored. Migration of entities with tags is faster, because the library does not need to migrate components, only the tags themselves. To create a tag, mark the fragment with the [`evolved.TAG`](#evolvedtag) fragment. @@ -1112,6 +1115,37 @@ Use [`evolved.ON_SET`](#evolvedon_set) for callbacks on fragment insert or overr > [!NOTE] > Because fragments marked with [`evolved.TAG`](#evolvedtag) (also called [Fragment Tags](#fragment-tags)) have no components, their [`evolved.ON_SET`](#evolvedon_set) hooks are invoked only when the tag is inserted, not when it is overridden, as there is nothing to override. Their [`evolved.ON_ASSIGN`](#evolvedon_assign) hooks are never invoked for such tags for the same reason. +#### Indexed Fragments + +Sometimes you need to find entities not just by the presence of a fragment, but by the value of its component. For example, you might want to find a player by its network identifier, or all units of a specific team. To do this without iterating over chunks, mark the fragment with the [`evolved.INDEX`](#evolvedindex) fragment trait. The library will maintain an index from component values to entities, and you will be able to find such entities using the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search), and [`evolved.multi_search_to`](#evolvedmulti_search_to) functions. + +```lua +local evolved = require 'evolved' + +local team = evolved.builder() + :index() + :build() + +local unit1 = evolved.builder():set(team, 'red'):build() +local unit2 = evolved.builder():set(team, 'blue'):build() +local unit3 = evolved.builder():set(team, 'red'):build() + +assert(evolved.search(team, 'blue') == unit2) + +local red_list, red_count = evolved.multi_search(team, 'red') +assert(red_count == 2 and red_list[1] == unit1 and red_list[2] == unit3) +``` + +The [`evolved.search`](#evolvedsearch) function returns the first entity with the specified component, while the [`evolved.multi_search`](#evolvedmulti_search) function returns a list of all such entities. The [`evolved.multi_search_to`](#evolvedmulti_search_to) function does the same, but appends the entities to the provided list starting from the specified index and returns only the number of found entities. + +The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, deferred, and batch operations. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups. + +> [!NOTE] +> Components are used as keys in the index, so table components are indexed by reference, and modifying such a table in place does not update the index. The same applies to components modified directly in chunk storages during iteration. Also, [Fragment Tags](#fragment-tags) and `NaN` components are not indexed at all. + +> [!NOTE] +> The index is not free. Every change of an indexed component updates the index, and removing an entity from a large group of entities with the same component value takes time proportional to the size of that group. So mark only the fragments you are actually going to search by. + #### Unique Fragments Some fragments should not be cloned when cloning entities. For example, `evolved.lua` has a special fragment called `evolved.PREFAB`, which marks entities used as sources for cloning. This fragment should not be present on the cloned entities. To prevent a fragment from being cloned, mark it as unique using the [`evolved.UNIQUE`](#evolvedunique) fragment trait. This ensures the fragment will not be copied when cloning entities. @@ -1516,6 +1550,7 @@ execute_iterator :: {execute_state? -> chunk?, entity[]?, integer?} TAG :: fragment NAME :: fragment +INDEX :: fragment UNIQUE :: fragment EXPLICIT :: fragment INTERNAL :: fragment @@ -1609,6 +1644,10 @@ lookup :: string -> entity? multi_lookup :: string -> entity[], integer multi_lookup_to :: entity[], integer, string -> integer +search :: fragment, component -> entity? +multi_search :: fragment, component -> entity[], integer +multi_search_to :: entity[], integer, fragment, component -> integer + process :: system... -> () process_with :: system, ... -> () @@ -1669,6 +1708,7 @@ builder_mt:clear :: builder builder_mt:tag :: builder builder_mt:name :: string -> builder +builder_mt:index :: builder builder_mt:unique :: builder builder_mt:explicit :: builder builder_mt:internal :: builder @@ -1709,6 +1749,7 @@ builder_mt:destruction_policy :: id -> builder - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) - All internal names now have a double underscore prefix +- Added the new [`evolved.INDEX`](#evolvedindex) fragment trait and the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) functions that allow finding entities by their component values ### v1.11.1 @@ -1795,6 +1836,8 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.NAME` +### `evolved.INDEX` + ### `evolved.UNIQUE` ### `evolved.EXPLICIT` @@ -2217,6 +2260,38 @@ function evolved.multi_lookup(name) end function evolved.multi_lookup_to(out_entity_list, out_entity_first, name) end ``` +### `evolved.search` + +```lua +---@param fragment evolved.fragment +---@param component evolved.component +---@return evolved.entity? entity +---@nodiscard +function evolved.search(fragment, component) end +``` + +### `evolved.multi_search` + +```lua +---@param fragment evolved.fragment +---@param component evolved.component +---@return evolved.entity[] entity_list +---@return integer entity_count +---@nodiscard +function evolved.multi_search(fragment, component) end +``` + +### `evolved.multi_search_to` + +```lua +---@param out_entity_list evolved.entity[] +---@param out_entity_first integer +---@param fragment evolved.fragment +---@param component evolved.component +---@return integer entity_count +function evolved.multi_search_to(out_entity_list, out_entity_first, fragment, component) end +``` + ### `evolved.process` ```lua @@ -2546,6 +2621,13 @@ function evolved.builder_mt:tag() end function evolved.builder_mt:name(name) end ``` +#### `evolved.builder_mt:index` + +```lua +---@return evolved.builder builder +function evolved.builder_mt:index() end +``` + #### `evolved.builder_mt:unique` ```lua diff --git a/develop/all.lua b/develop/all.lua index ec3c417..ba4677b 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -15,6 +15,7 @@ require 'develop.testing.name_tests' require 'develop.testing.process_with_tests' require 'develop.testing.realloc_tests' require 'develop.testing.requires_fragment_tests' +require 'develop.testing.search_tests' require 'develop.testing.spawn_tests' require 'develop.testing.system_as_query_tests' diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua new file mode 100644 index 0000000..53b5e67 --- /dev/null +++ b/develop/testing/search_tests.lua @@ -0,0 +1,402 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +---@param fragment evolved.fragment +---@param component evolved.component +---@param ... evolved.entity expected entities +local function check_search(fragment, component, ...) + local expected_count = select('#', ...) + + do + assert(evo.search(fragment, component) == ...) + end + + do + local entity_list, entity_count = evo.multi_search(fragment, component) + assert(entity_list and #entity_list == expected_count and entity_count == expected_count) + + for i = 1, expected_count do + assert(entity_list[i] == select(i, ...)) + end + end + + do + local entity_list = { 42, 21 } + local entity_count = evo.multi_search_to(entity_list, 3, fragment, component) + assert(#entity_list == expected_count + 2 and entity_count == expected_count) + assert(entity_list[1] == 42 and entity_list[2] == 21) + + for i = 1, expected_count do + assert(entity_list[i + 2] == select(i, ...)) + end + end +end + +---@param fragment evolved.fragment +---@param component evolved.component +---@param ... evolved.entity expected entities in any order +local function check_search_unordered(fragment, component, ...) + local expected_set = {} + local expected_count = select('#', ...) + + for i = 1, expected_count do + expected_set[select(i, ...)] = true + end + + local entity_list, entity_count = evo.multi_search(fragment, component) + assert(entity_list and #entity_list == expected_count and entity_count == expected_count) + + for i = 1, entity_count do + assert(expected_set[entity_list[i]]) + expected_set[entity_list[i]] = nil + end + + assert(evo.search(fragment, component) == entity_list[1]) +end + +do + local f = evo.id() + + local e1 = evo.spawn { [f] = 'hello' } + local e2 = evo.spawn { [f] = 'hello' } + + -- fragments without the INDEX trait are not searchable + check_search(f, 'hello') + + assert(evo.get(e1, f) == 'hello') + assert(evo.get(e2, f) == 'hello') +end + +do + local f = evo.builder():index():build() + assert(evo.has(f, evo.INDEX)) + + check_search(f, 'hello') + check_search(f, 'world') + + local e1, e2, e3 = evo.id(3) + + evo.set(e1, f, 'hello') + check_search(f, 'hello', e1) + check_search(f, 'world') + + evo.set(e2, f, 'hello') + evo.set(e3, f, 'hello') + check_search(f, 'hello', e1, e2, e3) + + evo.set(e2, f, 'world') + check_search(f, 'hello', e1, e3) + check_search(f, 'world', e2) + + -- assigning the same value keeps the entity position + evo.set(e1, f, 'hello') + check_search(f, 'hello', e1, e3) + + evo.set(e1, f, 'world') + check_search(f, 'hello', e3) + check_search(f, 'world', e2, e1) + + evo.remove(e2, f) + check_search(f, 'world', e1) + + evo.clear(e1) + check_search(f, 'world') + + evo.destroy(e3) + check_search(f, 'hello') +end + +do + local f1 = evo.builder():index():build() + local f2 = evo.builder():index():build() + + local e1 = evo.spawn { [f1] = 1, [f2] = 'a' } + local e2 = evo.spawn { [f1] = 1, [f2] = 'b' } + local e3 = evo.spawn { [f1] = 2, [f2] = 'a' } + + check_search(f1, 1, e1, e2) + check_search(f1, 2, e3) + check_search(f2, 'a', e1, e3) + check_search(f2, 'b', e2) + + -- components of different fragments are indexed separately + check_search(f1, 'a') + check_search(f2, 1) + + evo.remove(e1, f2) + check_search(f1, 1, e1, e2) + check_search(f2, 'a', e3) +end + +do + local f = evo.builder():index():build() + + do + local entity_list, entity_count = evo.multi_spawn(3, { [f] = 'multi' }) + check_search(f, 'multi', entity_list[1], entity_list[2], entity_list[3]) + assert(entity_count == 3) + evo.destroy(entity_list[1], entity_list[2], entity_list[3]) + check_search(f, 'multi') + end + + do + local prefab = evo.builder():prefab():set(f, 'prefab'):build() + check_search(f, 'prefab', prefab) + + local clone = evo.clone(prefab) + check_search(f, 'prefab', prefab, clone) + + local clone_list = evo.multi_clone(2, prefab) + check_search(f, 'prefab', prefab, clone, clone_list[1], clone_list[2]) + + local other_clone = evo.clone(prefab, { [f] = 'other' }) + check_search(f, 'prefab', prefab, clone, clone_list[1], clone_list[2]) + check_search(f, 'other', other_clone) + + evo.destroy(prefab, clone, clone_list[1], clone_list[2], other_clone) + check_search(f, 'prefab') + check_search(f, 'other') + end +end + +do + local f = evo.builder():index():default('def'):build() + local r = evo.builder():tag():require(f):build() + + local e1 = evo.id() + evo.set(e1, r) + check_search(f, 'def', e1) + + local q = evo.builder():include(r):build() + + local e2 = evo.spawn { [r] = true } + check_search(f, 'def', e1, e2) + + local e3 = evo.id() + evo.set(e3, f, 'other') + + local t = evo.id() + evo.set(e3, t) + + local qt = evo.builder():include(t):build() + evo.batch_set(qt, r) + check_search(f, 'other', e3) + check_search(f, 'def', e1, e2) + + evo.batch_set(q, f, 'batch') + check_search(f, 'def') + check_search(f, 'other') + check_search_unordered(f, 'batch', e1, e2, e3) + + evo.batch_remove(q, f) + check_search(f, 'batch') + + evo.batch_set(q, f, 'again') + check_search_unordered(f, 'again', e1, e2, e3) + + evo.batch_clear(qt) + check_search_unordered(f, 'again', e1, e2) + + evo.batch_destroy(q) + check_search(f, 'again') +end + +do + local f = evo.id() + + local e1 = evo.spawn { [f] = 'existing' } + local e2 = evo.spawn { [f] = 'existing' } + + check_search(f, 'existing') + + -- the index is built from existing entities when the trait is added + evo.set(f, evo.INDEX) + do + local entity_list, entity_count = evo.multi_search(f, 'existing') + assert(entity_count == 2) + assert(entity_list[1] == e1 or entity_list[1] == e2) + assert(entity_list[2] == e1 or entity_list[2] == e2) + assert(entity_list[1] ~= entity_list[2]) + end + + -- the index is dropped when the trait is removed + evo.remove(f, evo.INDEX) + check_search(f, 'existing') + + local e3 = evo.spawn { [f] = 'existing' } + check_search(f, 'existing') + + evo.set(f, evo.INDEX) + do + local _, entity_count = evo.multi_search(f, 'existing') + assert(entity_count == 3) + end + + evo.destroy(e1, e2, e3) + check_search(f, 'existing') +end + +do + local f = evo.id() + + evo.defer() + do + evo.set(f, evo.INDEX) + check_search(f, 'deferred') + end + evo.commit() + + local e1, e2 + + evo.defer() + do + e1 = evo.spawn { [f] = 'deferred' } + e2 = evo.spawn { [f] = 'deferred' } + check_search(f, 'deferred') + end + evo.commit() + + check_search(f, 'deferred', e1, e2) + + evo.defer() + do + evo.set(e1, f, 'changed') + evo.destroy(e2) + check_search(f, 'deferred', e1, e2) + end + evo.commit() + + check_search(f, 'deferred') + check_search(f, 'changed', e1) +end + +do + local set_count, insert_count, remove_count = 0, 0, 0 + + local f = evo.builder() + :index() + :on_set(function(e, ff, v) + set_count = set_count + 1 + -- the index is already updated when the set hook is called + local entity_list, entity_count = evo.multi_search(ff, v) + local found = false + for i = 1, entity_count do found = found or entity_list[i] == e end + assert(found) + end) + :on_insert(function() + insert_count = insert_count + 1 + end) + :on_remove(function(e, ff, v) + remove_count = remove_count + 1 + -- the index is still not updated when the remove hook is called + local entity_list, entity_count = evo.multi_search(ff, v) + local found = false + for i = 1, entity_count do found = found or entity_list[i] == e end + assert(found) + end) + :build() + + local e = evo.spawn { [f] = 'hooked' } + assert(set_count == 1 and insert_count == 1 and remove_count == 0) + check_search(f, 'hooked', e) + + evo.set(e, f, 'other') + assert(set_count == 2 and insert_count == 1 and remove_count == 0) + check_search(f, 'hooked') + check_search(f, 'other', e) + + evo.remove(e, f) + assert(set_count == 2 and insert_count == 1 and remove_count == 1) + check_search(f, 'other') +end + +do + local f = evo.builder():index():build() + + local nan = 0 / 0 + + -- NaN components cannot be indexed + local e = evo.spawn { [f] = nan } + check_search(f, nan) + + evo.set(e, f, 'number') + check_search(f, 'number', e) + + evo.set(e, f, nan) + check_search(f, 'number') + check_search(f, nan) + + evo.destroy(e) +end + +do + local t = evo.builder():tag():index():build() + + -- tags have no components, so they are not indexed + local e = evo.spawn { [t] = true } + check_search(t, true) + + evo.destroy(e) +end + +do + local f = evo.builder():index():build() + + local k1, k2 = {}, {} + + local e1 = evo.spawn { [f] = k1 } + local e2 = evo.spawn { [f] = k2 } + + -- table components are indexed by reference + check_search(f, k1, e1) + check_search(f, k2, e2) + check_search(f, {}) + + evo.destroy(e1, e2) +end + +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'gone' } + local e2 = evo.spawn { [f] = 'gone' } + + check_search(f, 'gone', e1, e2) + + evo.destroy(f) + + assert(evo.alive(e1) and not evo.has(e1, f)) + assert(evo.alive(e2) and not evo.has(e2, f)) + + check_search(f, 'gone') +end + +do + local f = evo.builder():index():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local g = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'a', [g] = 'b' } + local e2 = evo.spawn { [g] = 'b' } + + check_search(g, 'b', e1, e2) + + evo.destroy(f) + + assert(not evo.alive(e1) and evo.alive(e2)) + check_search(g, 'b', e2) +end + +do + -- names and groups are searchable through the generic functions too + local e = evo.builder():name('search_name'):build() + check_search(evo.NAME, 'search_name', e) + assert(evo.lookup('search_name') == e) + + local group = evo.id() + local s1 = evo.builder():group(group):build() + local s2 = evo.builder():group(group):build() + check_search(evo.GROUP, group, s1, s2) + + evo.destroy(e, group, s1, s2) +end diff --git a/evolved.d.tl b/evolved.d.tl index c3be05a..b09f28f 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -107,6 +107,7 @@ tag: function(self: Builder): Builder name: function(self: Builder, name: string): Builder + index: function(self: Builder): Builder unique: function(self: Builder): Builder explicit: function(self: Builder): Builder internal: function(self: Builder): Builder @@ -144,6 +145,7 @@ TAG: Fragment NAME: Fragment + INDEX: Fragment UNIQUE: Fragment EXPLICIT: Fragment INTERNAL: Fragment @@ -272,6 +274,10 @@ multi_lookup: function(name: string): { Entity }, integer multi_lookup_to: function(out_entity_list: { Entity }, out_entity_first: integer, name: string): integer + search: function(fragment: Fragment, component: Component): Entity | nil + multi_search: function(fragment: Fragment, component: Component): { Entity }, integer + multi_search_to: function(out_entity_list: { Entity }, out_entity_first: integer, fragment: Fragment, component: Component): integer + process: function(...: System) process_with: function(system: System, ...: any) diff --git a/evolved.lua b/evolved.lua index ab02080..673dc08 100644 --- a/evolved.lua +++ b/evolved.lua @@ -7589,6 +7589,11 @@ function __builder_mt:name(name) return self:set(__NAME, name) end +---@return evolved.builder builder +function __builder_mt:index() + return self:set(__INDEX) +end + ---@return evolved.builder builder function __builder_mt:unique() return self:set(__UNIQUE) @@ -8223,6 +8228,7 @@ __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __INTERNAL) evolved.TAG = __TAG evolved.NAME = __NAME +evolved.INDEX = __INDEX evolved.UNIQUE = __UNIQUE evolved.EXPLICIT = __EXPLICIT evolved.INTERNAL = __INTERNAL @@ -8318,6 +8324,10 @@ evolved.lookup = __evolved_lookup evolved.multi_lookup = __evolved_multi_lookup evolved.multi_lookup_to = __evolved_multi_lookup_to +evolved.search = __evolved_search +evolved.multi_search = __evolved_multi_search +evolved.multi_search_to = __evolved_multi_search_to + evolved.process = __evolved_process evolved.process_with = __evolved_process_with From ad526b39ef2bf04b5eade5b62083c3f625ec093a Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 5 Oct 2026 07:47:06 +0700 Subject: [PATCH 08/13] indices: searching without the INDEX trait is an error --- README.md | 3 ++ develop/testing/search_tests.lua | 53 +++++++++++++++++++------------- evolved.lua | 23 +++++++++----- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index eaa95a4..ff9be5c 100644 --- a/README.md +++ b/README.md @@ -1140,6 +1140,9 @@ The [`evolved.search`](#evolvedsearch) function returns the first entity with th The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, deferred, and batch operations. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups. +> [!NOTE] +> Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) trait is an error. + > [!NOTE] > Components are used as keys in the index, so table components are indexed by reference, and modifying such a table in place does not update the index. The same applies to components modified directly in chunk storages during iteration. Also, [Fragment Tags](#fragment-tags) and `NaN` components are not indexed at all. diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua index 53b5e67..1f30ff6 100644 --- a/develop/testing/search_tests.lua +++ b/develop/testing/search_tests.lua @@ -55,19 +55,6 @@ local function check_search_unordered(fragment, component, ...) assert(evo.search(fragment, component) == entity_list[1]) end -do - local f = evo.id() - - local e1 = evo.spawn { [f] = 'hello' } - local e2 = evo.spawn { [f] = 'hello' } - - -- fragments without the INDEX trait are not searchable - check_search(f, 'hello') - - assert(evo.get(e1, f) == 'hello') - assert(evo.get(e2, f) == 'hello') -end - do local f = evo.builder():index():build() assert(evo.has(f, evo.INDEX)) @@ -208,8 +195,6 @@ do local e1 = evo.spawn { [f] = 'existing' } local e2 = evo.spawn { [f] = 'existing' } - check_search(f, 'existing') - -- the index is built from existing entities when the trait is added evo.set(f, evo.INDEX) do @@ -220,12 +205,10 @@ do assert(entity_list[1] ~= entity_list[2]) end - -- the index is dropped when the trait is removed + -- the index is rebuilt with entities spawned while the trait was removed evo.remove(f, evo.INDEX) - check_search(f, 'existing') local e3 = evo.spawn { [f] = 'existing' } - check_search(f, 'existing') evo.set(f, evo.INDEX) do @@ -243,10 +226,11 @@ do evo.defer() do evo.set(f, evo.INDEX) - check_search(f, 'deferred') end evo.commit() + check_search(f, 'deferred') + local e1, e2 evo.defer() @@ -333,13 +317,40 @@ end do local t = evo.builder():tag():index():build() - -- tags have no components, so they are not indexed + -- tags have no components, so their indices are always empty local e = evo.spawn { [t] = true } check_search(t, true) evo.destroy(e) end +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'hello' } + local e2 = evo.spawn { [f] = 'hello' } + check_search(f, 'hello', e1, e2) + + -- the index becomes empty when an indexed fragment becomes a tag + evo.set(f, evo.TAG) + check_search(f, 'hello') + check_search(f, true) + + evo.remove(e2, f) + check_search(f, 'hello') + + -- and it is rebuilt when the fragment is no longer a tag + evo.remove(f, evo.TAG) + assert(evo.get(e1, f) == true) + check_search(f, true, e1) + check_search(f, 'hello') + + local e3 = evo.spawn { [f] = 'hello' } + check_search(f, 'hello', e3) + + evo.destroy(e1, e2, e3) +end + do local f = evo.builder():index():build() @@ -368,8 +379,6 @@ do assert(evo.alive(e1) and not evo.has(e1, f)) assert(evo.alive(e2) and not evo.has(e2, f)) - - check_search(f, 'gone') end do diff --git a/evolved.lua b/evolved.lua index 673dc08..6219953 100644 --- a/evolved.lua +++ b/evolved.lua @@ -2313,15 +2313,15 @@ function __update_search_index(fragment) return end + __search_indices[fragment] = nil + __update_major_chunks(fragment) + if __evolved_has(fragment, __INDEX) then local fragment_search_index = __search_index_fns.new() __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) __search_indices[fragment] = fragment_search_index - else - __search_indices[fragment] = nil + __trace_major_chunks(fragment, __update_chunk_caches) end - - __update_major_chunks(fragment) end ---@param chunk evolved.chunk @@ -6766,7 +6766,13 @@ end ---@nodiscard function __evolved_search(fragment, component) local fragment_search_index = __search_indices[fragment] - return fragment_search_index and fragment_search_index.__entity_map[component] + + if not fragment_search_index then + __error_fmt('the fragment (%s) is not indexed and cannot be searched', + __id_name(fragment)) + end + + return fragment_search_index.__entity_map[component] end ---@param fragment evolved.fragment @@ -6789,7 +6795,8 @@ function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, local fragment_search_index = __search_indices[fragment] if not fragment_search_index then - return 0 + __error_fmt('the fragment (%s) is not indexed and cannot be searched', + __id_name(fragment)) end do @@ -7827,8 +7834,8 @@ __evolved_set(__ON_REMOVE, __ON_REMOVE, __update_major_chunks) --- --- -__evolved_set(__TAG, __ON_INSERT, __update_major_chunks) -__evolved_set(__TAG, __ON_REMOVE, __update_major_chunks) +__evolved_set(__TAG, __ON_INSERT, __update_search_index) +__evolved_set(__TAG, __ON_REMOVE, __update_search_index) __evolved_set(__INDEX, __ON_INSERT, __update_search_index) __evolved_set(__INDEX, __ON_REMOVE, __update_search_index) From 8a178eddff5147bcf4864f0726eec3b2d51d3a5a Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 5 Oct 2026 19:39:17 +0700 Subject: [PATCH 09/13] indices: reindexing --- README.md | 61 +++++++++++-- develop/testing/search_tests.lua | 136 ++++++++++++++++++++++++++++ evolved.d.tl | 2 + evolved.lua | 149 ++++++++++++++++--------------- 4 files changed, 270 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index ff9be5c..9397916 100644 --- a/README.md +++ b/README.md @@ -1126,9 +1126,17 @@ local team = evolved.builder() :index() :build() -local unit1 = evolved.builder():set(team, 'red'):build() -local unit2 = evolved.builder():set(team, 'blue'):build() -local unit3 = evolved.builder():set(team, 'red'):build() +local unit1 = evolved.builder() + :set(team, 'red') + :build() + +local unit2 = evolved.builder() + :set(team, 'blue') + :build() + +local unit3 = evolved.builder() + :set(team, 'red') + :build() assert(evolved.search(team, 'blue') == unit2) @@ -1138,16 +1146,46 @@ assert(red_count == 2 and red_list[1] == unit1 and red_list[2] == unit3) The [`evolved.search`](#evolvedsearch) function returns the first entity with the specified component, while the [`evolved.multi_search`](#evolvedmulti_search) function returns a list of all such entities. The [`evolved.multi_search_to`](#evolvedmulti_search_to) function does the same, but appends the entities to the provided list starting from the specified index and returns only the number of found entities. -The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, deferred, and batch operations. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups. +Entities are found in the order in which they received their components, but this order is not preserved after removals. When an entity loses the component or changes its value, the last entity with the same value takes its place. So, if the order matters to you, for example, the order of systems in a [evolved.GROUP](#systems), avoid removing such components or changing their values. + +The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, and setting. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups. + +Components changed directly in chunk storages during iteration bypass the index, so after such changes you should update it manually using the [`evolved.reindex`](#evolvedreindex) function. It only updates entities whose components have changed, so it is much cheaper than rebuilding the whole index. + +```lua +local evolved = require 'evolved' + +local team = evolved.builder() + :index() + :build() + +local unit = evolved.builder() + :set(team, 'red') + :build() + +local all_units = evolved.builder() + :include(team) + :build() + +for chunk, entity_list, entity_count in evolved.execute(all_units) do + local teams = chunk:components(team) + for i = 1, entity_count do + teams[i] = 'blue' + end +end + +evolved.reindex(team) +assert(evolved.search(team, 'blue') == unit) +``` > [!NOTE] > Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) trait is an error. > [!NOTE] -> Components are used as keys in the index, so table components are indexed by reference, and modifying such a table in place does not update the index. The same applies to components modified directly in chunk storages during iteration. Also, [Fragment Tags](#fragment-tags) and `NaN` components are not indexed at all. +> Components are used as keys in the index, so table components are indexed by reference, and searching by another table with the same content will not find anything. Also, [Fragment Tags](#fragment-tags) and `NaN` components are not indexed at all. > [!NOTE] -> The index is not free. Every change of an indexed component updates the index, and removing an entity from a large group of entities with the same component value takes time proportional to the size of that group. So mark only the fragments you are actually going to search by. +> The index is not free. Every indexed entity takes some additional memory, and all operations that change indexed components, such as spawning, cloning, setting, removing, or destroying, become slower because they have to update the index too. So mark only the fragments you are actually going to search by. #### Unique Fragments @@ -1651,6 +1689,8 @@ search :: fragment, component -> entity? multi_search :: fragment, component -> entity[], integer multi_search_to :: entity[], integer, fragment, component -> integer +reindex :: fragment... -> () + process :: system... -> () process_with :: system, ... -> () @@ -1752,7 +1792,7 @@ builder_mt:destruction_policy :: id -> builder - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) - All internal names now have a double underscore prefix -- Added the new [`evolved.INDEX`](#evolvedindex) fragment trait and the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search) functions that allow finding entities by their component values +- Added the new [`evolved.INDEX`](#evolvedindex) fragment trait and the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search), [`evolved.reindex`](#evolvedreindex) functions that allow finding entities by their component values ### v1.11.1 @@ -2295,6 +2335,13 @@ function evolved.multi_search(fragment, component) end function evolved.multi_search_to(out_entity_list, out_entity_first, fragment, component) end ``` +### `evolved.reindex` + +```lua +---@param ... evolved.fragment fragments +function evolved.reindex(...) end +``` + ### `evolved.process` ```lua diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua index 1f30ff6..aa7e4c1 100644 --- a/develop/testing/search_tests.lua +++ b/develop/testing/search_tests.lua @@ -409,3 +409,139 @@ do evo.destroy(e, group, s1, s2) end + +---@param entity evolved.entity +---@param fragment evolved.fragment +---@param component evolved.component +local function set_directly(entity, fragment, component) + local chunk, place = evo.locate(entity) + assert(chunk, 'entity should be in a chunk') + local storage = chunk:components(fragment) + storage[place] = component +end + +do + local f = evo.builder():index():build() + local q = evo.builder():include(f):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'blue' } + + -- components can be changed directly in chunk storages during iteration + for chunk, entity_list, entity_count in evo.execute(q) do + local storage = chunk:components(f) + for place = 1, entity_count do + if entity_list[place] == e1 then + storage[place] = 'blue' + end + end + end + + -- and then the index should be updated manually + evo.reindex(f) + + -- only entities with changed components are moved between index groups + check_search(f, 'red', e2) + check_search(f, 'blue', e3, e1) + + evo.destroy(e1, e2, e3) +end + +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + + set_directly(e1, f, 'blue') + + -- removing entities does not leave stale index entries even without reindexing + evo.destroy(e1) + check_search(f, 'red', e2) + check_search(f, 'blue') + + set_directly(e2, f, 'blue') + + evo.remove(e2, f) + check_search(f, 'red') + check_search(f, 'blue') + + evo.destroy(e2) +end + +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + + set_directly(e1, f, 'blue') + + -- setting the same component through the API updates the index too + evo.set(e1, f, evo.get(e1, f)) + check_search(f, 'red', e2) + check_search(f, 'blue', e1) + + evo.destroy(e1, e2) +end + +do + local f1 = evo.builder():index():build() + local f2 = evo.builder():index():build() + + local e1 = evo.spawn { [f1] = 'a', [f2] = 1 } + local e2 = evo.spawn { [f1] = 'a', [f2] = 1 } + + set_directly(e1, f1, 'b') + set_directly(e2, f2, 2) + + -- reindexing can be deferred + evo.defer() + do + evo.reindex(f1, f2) + end + evo.commit() + + check_search(f1, 'a', e2) + check_search(f1, 'b', e1) + check_search(f2, 1, e1) + check_search(f2, 2, e2) + + evo.destroy(e1, e2) +end + +do + local f = evo.builder():index():build() + + local e = evo.spawn { [f] = 'red' } + + -- NaN components are removed from the index by reindexing + set_directly(e, f, 0 / 0) + evo.reindex(f) + check_search(f, 'red') + + set_directly(e, f, 'green') + evo.reindex(f) + check_search(f, 'green', e) + + evo.destroy(e) +end + +do + local f = evo.id() + + local e = evo.spawn { [f] = 'red' } + + -- the trait can be added and the fragment reindexed in the same deferred block + evo.defer() + do + evo.set(f, evo.INDEX) + evo.reindex(f) + end + evo.commit() + + check_search(f, 'red', e) + + evo.destroy(e) +end diff --git a/evolved.d.tl b/evolved.d.tl index b09f28f..1742203 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -278,6 +278,8 @@ multi_search: function(fragment: Fragment, component: Component): { Entity }, integer multi_search_to: function(out_entity_list: { Entity }, out_entity_first: integer, fragment: Fragment, component: Component): integer + reindex: function(...: Fragment) + process: function(...: System) process_with: function(system: System, ...: any) diff --git a/evolved.lua b/evolved.lua index 6219953..dad6f0d 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1109,6 +1109,7 @@ end ---@class (exact) evolved.search_index: { --- __entity_map: { [evolved.component]: evolved.entity }, --- __entities_map: { [evolved.component]: evolved.assoc_list }, +--- __component_map: { [evolved.entity]: evolved.component }, --- } local __search_index_fns = {} @@ -1120,9 +1121,20 @@ function __search_index_fns.new() return { __entity_map = __lua_table_new(), __entities_map = __lua_table_new(), + __component_map = __lua_table_new(), } end +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.assign(index, entity, component) + if index.__component_map[entity] ~= component then + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, component) + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -1132,6 +1144,8 @@ function __search_index_fns.insert(index, entity, component) return end + index.__component_map[entity] = component + ---@type evolved.entity? local indexed_entity = index.__entity_map[component] @@ -1153,13 +1167,21 @@ end ---@param index evolved.search_index ---@param entity evolved.entity ----@param component evolved.component -function __search_index_fns.remove(index, entity, component) +function __search_index_fns.remove(index, entity) + ---@type evolved.component? + local component = index.__component_map[entity] + + if component == nil then + return + end + + index.__component_map[entity] = nil + ---@type evolved.assoc_list? local indexed_entities = index.__entities_map[component] if indexed_entities then - if __assoc_list_fns.remove(indexed_entities, entity) == 0 then + if __assoc_list_fns.unordered_remove(indexed_entities, entity) == 0 then index.__entities_map[component], indexed_entities = nil, nil end end @@ -1172,17 +1194,6 @@ function __search_index_fns.remove(index, entity, component) end end ----@param index evolved.search_index ----@param entity evolved.entity ----@param new_component evolved.component ----@param old_component evolved.component -function __search_index_fns.assign(index, entity, new_component, old_component) - if new_component ~= old_component then - __search_index_fns.remove(index, entity, old_component) - __search_index_fns.insert(index, entity, new_component) - end -end - --- --- --- @@ -1294,6 +1305,8 @@ local __evolved_search local __evolved_multi_search local __evolved_multi_search_to +local __evolved_reindex + local __evolved_process local __evolved_process_with @@ -2337,7 +2350,7 @@ function __update_search_index_trace(chunk, fragment, fragment_search_index) for place = 1, chunk.__entity_count do local entity = chunk_entity_list[place] local component = chunk_component_storage[place] - __search_index_fns.insert(fragment_search_index, entity, component) + __search_index_fns.assign(fragment_search_index, entity, component) end end end @@ -3717,19 +3730,10 @@ function __clear_entity_one(entity) local chunk_index_fragment_list = chunk.__index_fragment_list if chunk_index_fragment_list then - local chunk_component_indices = chunk.__component_indices - local chunk_component_storages = chunk.__component_storages - for chunk_index_fragment_index = 1, chunk.__index_fragment_count do local fragment = chunk_index_fragment_list[chunk_index_fragment_index] - local component_index = chunk_component_indices[fragment] - - if component_index then - local fragment_search_index = __search_indices[fragment] - local component_storage = chunk_component_storages[component_index] - local old_component = component_storage[place] - __search_index_fns.remove(fragment_search_index, entity, old_component) - end + local fragment_search_index = __search_indices[fragment] + __search_index_fns.remove(fragment_search_index, entity) end end end @@ -3805,19 +3809,10 @@ function __destroy_entity_one(entity) local chunk_index_fragment_list = chunk.__index_fragment_list if chunk_index_fragment_list then - local chunk_component_indices = chunk.__component_indices - local chunk_component_storages = chunk.__component_storages - for chunk_index_fragment_index = 1, chunk.__index_fragment_count do local fragment = chunk_index_fragment_list[chunk_index_fragment_index] - local component_index = chunk_component_indices[fragment] - - if component_index then - local fragment_search_index = __search_indices[fragment] - local component_storage = chunk_component_storages[component_index] - local old_component = component_storage[place] - __search_index_fns.remove(fragment_search_index, entity, old_component) - end + local fragment_search_index = __search_indices[fragment] + __search_index_fns.remove(fragment_search_index, entity) end end end @@ -4071,7 +4066,7 @@ function __chunk_set(old_chunk, fragment, component) end if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component, old_component) + __search_index_fns.assign(fragment_search_index, entity, new_component) end end else @@ -4094,7 +4089,7 @@ function __chunk_set(old_chunk, fragment, component) end if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component, old_component) + __search_index_fns.assign(fragment_search_index, entity, new_component) end end end @@ -4517,17 +4512,11 @@ function __chunk_remove(old_chunk, ...) local fragment = old_index_fragment_list[old_index_fragment_index] if not new_fragment_set[fragment] then - local old_component_index = old_component_indices[fragment] - - if old_component_index then - local fragment_search_index = __search_indices[fragment] - local old_component_storage = old_component_storages[old_component_index] + local fragment_search_index = __search_indices[fragment] - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - local old_component = old_component_storage[old_place] - __search_index_fns.remove(fragment_search_index, entity, old_component) - end + for old_place = 1, old_entity_count do + local entity = old_entity_list[old_place] + __search_index_fns.remove(fragment_search_index, entity) end end end @@ -4670,22 +4659,13 @@ function __chunk_clear(chunk) local chunk_index_fragment_list = chunk.__index_fragment_list if chunk_index_fragment_list then - local chunk_component_indices = chunk.__component_indices - local chunk_component_storages = chunk.__component_storages - for chunk_index_fragment_index = 1, chunk.__index_fragment_count do local fragment = chunk_index_fragment_list[chunk_index_fragment_index] - local component_index = chunk_component_indices[fragment] - - if component_index then - local fragment_search_index = __search_indices[fragment] - local component_storage = chunk_component_storages[component_index] + local fragment_search_index = __search_indices[fragment] - for place = 1, chunk_entity_count do - local entity = chunk_entity_list[place] - local old_component = component_storage[place] - __search_index_fns.remove(fragment_search_index, entity, old_component) - end + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] + __search_index_fns.remove(fragment_search_index, entity) end end end @@ -5869,7 +5849,7 @@ function __evolved_set(entity, fragment, component) end if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component, old_component) + __search_index_fns.assign(fragment_search_index, entity, new_component) end else -- nothing @@ -6125,14 +6105,8 @@ function __evolved_remove(entity, ...) local fragment = old_index_fragment_list[old_index_fragment_index] if not new_fragment_set[fragment] then - local old_component_index = old_component_indices[fragment] - - if old_component_index then - local fragment_search_index = __search_indices[fragment] - local old_component_storage = old_component_storages[old_component_index] - local old_component = old_component_storage[old_place] - __search_index_fns.remove(fragment_search_index, entity, old_component) - end + local fragment_search_index = __search_indices[fragment] + __search_index_fns.remove(fragment_search_index, entity) end end end @@ -6824,6 +6798,37 @@ function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, return 0 end +---@param ... evolved.fragment fragments +function __evolved_reindex(...) + local fragment_count = __lua_select('#', ...) + + if fragment_count == 0 then + return + end + + if __defer_depth > 0 then + __defer_call_hook(__evolved_reindex, ...) + return + end + + for fragment_index = 1, fragment_count do + ---@type evolved.fragment + local fragment = __lua_select(fragment_index, ...) + + if not __search_indices[fragment] then + __error_fmt('the fragment (%s) is not indexed and cannot be reindexed', + __id_name(fragment)) + end + end + + for fragment_index = 1, fragment_count do + ---@type evolved.fragment + local fragment = __lua_select(fragment_index, ...) + local fragment_search_index = __search_indices[fragment] + __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) + end +end + ---@param ... evolved.system systems function __evolved_process(...) local argument_count = __lua_select('#', ...) @@ -8335,6 +8340,8 @@ evolved.search = __evolved_search evolved.multi_search = __evolved_multi_search evolved.multi_search_to = __evolved_multi_search_to +evolved.reindex = __evolved_reindex + evolved.process = __evolved_process evolved.process_with = __evolved_process_with From fb31bce34f57a1549d68a2190de7ae74b6ee236d Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 7 Oct 2026 05:39:13 +0700 Subject: [PATCH 10/13] indices: multi_insert/remove --- evolved.lua | 133 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 23 deletions(-) diff --git a/evolved.lua b/evolved.lua index dad6f0d..3563d33 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1144,27 +1144,79 @@ function __search_index_fns.insert(index, entity, component) return end - index.__component_map[entity] = component + local entity_map = index.__entity_map + local entities_map = index.__entities_map + local component_map = index.__component_map + + component_map[entity] = component ---@type evolved.entity? - local indexed_entity = index.__entity_map[component] + local indexed_entity = entity_map[component] if not indexed_entity then - index.__entity_map[component] = entity + entity_map[component] = entity return end ---@type evolved.assoc_list? - local indexed_entities = index.__entities_map[component] + local indexed_entities = entities_map[component] if not indexed_entities then - index.__entities_map[component] = __assoc_list_fns.from(indexed_entity, entity) + entities_map[component] = __assoc_list_fns.from(indexed_entity, entity) return end __assoc_list_fns.insert(indexed_entities, entity) end +---@param index evolved.search_index +---@param entity_list evolved.entity[] +---@param component_list evolved.component[] +---@param first integer +---@param last integer +function __search_index_fns.multi_insert(index, entity_list, component_list, first, last) + local entity_map = index.__entity_map + local entities_map = index.__entities_map + local component_map = index.__component_map + + local last_entities ---@type evolved.assoc_list? + local last_component ---@type evolved.component? + + for place = first, last do + local entity = entity_list[place] + local component = component_list[place] + + if component ~= component then + -- NaN components cannot be indexed + elseif last_entities and last_component == component then + component_map[entity] = component + __assoc_list_fns.insert(last_entities, entity) + else + component_map[entity] = component + + ---@type evolved.entity? + local indexed_entity = entity_map[component] + + if not indexed_entity then + entity_map[component] = entity + last_entities, last_component = nil, nil + else + ---@type evolved.assoc_list? + local indexed_entities = entities_map[component] + + if not indexed_entities then + indexed_entities = __assoc_list_fns.from(indexed_entity, entity) + entities_map[component] = indexed_entities + else + __assoc_list_fns.insert(indexed_entities, entity) + end + + last_entities, last_component = indexed_entities, component + end + end + end +end + ---@param index evolved.search_index ---@param entity evolved.entity function __search_index_fns.remove(index, entity) @@ -1194,6 +1246,51 @@ function __search_index_fns.remove(index, entity) end end +---@param index evolved.search_index +---@param entity_list evolved.entity[] +---@param first integer +---@param last integer +function __search_index_fns.multi_remove(index, entity_list, first, last) + local entity_map = index.__entity_map + local entities_map = index.__entities_map + local component_map = index.__component_map + + local last_entities ---@type evolved.assoc_list? + local last_component ---@type evolved.component? + + for place = first, last do + local entity = entity_list[place] + + ---@type evolved.component? + local component = component_map[entity] + + if component ~= nil then + component_map[entity] = nil + + ---@type evolved.assoc_list? + local indexed_entities + + if last_entities and last_component == component then + indexed_entities = last_entities + else + indexed_entities = entities_map[component] + last_entities, last_component = indexed_entities, component + end + + if indexed_entities then + if __assoc_list_fns.unordered_remove(indexed_entities, entity) == 0 then + entities_map[component], indexed_entities = nil, nil + last_entities, last_component = nil, nil + end + end + + if entity_map[component] == entity then + entity_map[component] = indexed_entities and indexed_entities.__item_list[1] or nil + end + end + end +end + --- --- --- @@ -3059,11 +3156,8 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co local fragment_search_index = __search_indices[fragment] local component_storage = chunk_component_storages[component_index] - for place = b_place, e_place do - local entity = chunk_entity_list[place] - local new_component = component_storage[place] - __search_index_fns.insert(fragment_search_index, entity, new_component) - end + __search_index_fns.multi_insert(fragment_search_index, + chunk_entity_list, component_storage, b_place, e_place) end end end @@ -3422,11 +3516,8 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c local fragment_search_index = __search_indices[fragment] local component_storage = chunk_component_storages[component_index] - for place = b_place, e_place do - local entity = chunk_entity_list[place] - local new_component = component_storage[place] - __search_index_fns.insert(fragment_search_index, entity, new_component) - end + __search_index_fns.multi_insert(fragment_search_index, + chunk_entity_list, component_storage, b_place, e_place) end end end @@ -4514,10 +4605,8 @@ function __chunk_remove(old_chunk, ...) if not new_fragment_set[fragment] then local fragment_search_index = __search_indices[fragment] - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - __search_index_fns.remove(fragment_search_index, entity) - end + __search_index_fns.multi_remove(fragment_search_index, + old_entity_list, 1, old_entity_count) end end end @@ -4663,10 +4752,8 @@ function __chunk_clear(chunk) local fragment = chunk_index_fragment_list[chunk_index_fragment_index] local fragment_search_index = __search_indices[fragment] - for place = 1, chunk_entity_count do - local entity = chunk_entity_list[place] - __search_index_fns.remove(fragment_search_index, entity) - end + __search_index_fns.multi_remove(fragment_search_index, + chunk_entity_list, 1, chunk_entity_count) end end From 7a5c44cf58c42f5c7d5d32d8c21fc10d70d43759 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 10 Oct 2026 03:49:29 +0700 Subject: [PATCH 11/13] indices: new INDEX_POLICY trait --- README.md | 55 +++++- develop/ROADMAP.md | 1 - develop/testing/search_tests.lua | 330 +++++++++++++++++++++++++++++++ evolved.d.tl | 5 + evolved.lua | 238 ++++++++++++++-------- 5 files changed, 545 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 9397916..37006a8 100644 --- a/README.md +++ b/README.md @@ -1146,7 +1146,37 @@ assert(red_count == 2 and red_list[1] == unit1 and red_list[2] == unit3) The [`evolved.search`](#evolvedsearch) function returns the first entity with the specified component, while the [`evolved.multi_search`](#evolvedmulti_search) function returns a list of all such entities. The [`evolved.multi_search_to`](#evolvedmulti_search_to) function does the same, but appends the entities to the provided list starting from the specified index and returns only the number of found entities. -Entities are found in the order in which they received their components, but this order is not preserved after removals. When an entity loses the component or changes its value, the last entity with the same value takes its place. So, if the order matters to you, for example, the order of systems in a [evolved.GROUP](#systems), avoid removing such components or changing their values. +Entities are found in the order in which they received their components, but by default this order is not preserved after removals. When an entity loses the component or changes its value, the last entity with the same value takes its place. This makes removals fast, but if the order matters to you, you can change this behavior by using the [`evolved.INDEX_POLICY`](#evolvedindex_policy) fragment trait. This trait expects one of the following predefined identifiers: + +- [`evolved.INDEX_POLICY_ORDERED`](#evolvedindex_policy_ordered) will preserve the order of entities after removals. Entities that change their values are moved to the end of the new value group. Removals take time proportional to the number of entities with the same value, so use this policy only when the order really matters. The [`evolved.GROUP`](#evolvedgroup) fragment uses this policy, so the order of [systems](#systems) in a group is preserved. + +- [`evolved.INDEX_POLICY_UNORDERED`](#evolvedindex_policy_unordered) will not preserve the order of entities after removals. Removals take constant time regardless of the number of entities with the same value. This is the default behavior, so you don't have to set it explicitly, but you can if you want. + +```lua +local evolved = require 'evolved' + +local team = evolved.builder() + :index() + :index_policy(evolved.INDEX_POLICY_ORDERED) + :build() + +local unit1 = evolved.builder() + :set(team, 'red') + :build() + +local unit2 = evolved.builder() + :set(team, 'red') + :build() + +local unit3 = evolved.builder() + :set(team, 'red') + :build() + +evolved.destroy(unit1) + +local red_list, red_count = evolved.multi_search(team, 'red') +assert(red_count == 2 and red_list[1] == unit2 and red_list[2] == unit3) +``` The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, and setting. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups. @@ -1345,7 +1375,7 @@ evolved.destroy(world) assert(evolved.alive(entity) and not evolved.has(entity, world)) ``` -The default behavior works well in most cases, but you can change it by using the [`evolved.DESTRUCTION_POLICY`](#evolveddestruction_policy) fragment. This fragment expects one of the following predefined identifiers: +The default behavior works well in most cases, but you can change it by using the [`evolved.DESTRUCTION_POLICY`](#evolveddestruction_policy) fragment trait. This trait expects one of the following predefined identifiers: - [`evolved.DESTRUCTION_POLICY_DESTROY_ENTITY`](#evolveddestruction_policy_destroy_entity) will destroy any entity that has the destroyed fragment. This is useful for cases like the one above, where you want to destroy all entities when their world is destroyed. @@ -1623,6 +1653,10 @@ EXECUTE :: fragment PROLOGUE :: fragment EPILOGUE :: fragment +INDEX_POLICY :: fragment +INDEX_POLICY_ORDERED :: id +INDEX_POLICY_UNORDERED :: id + DESTRUCTION_POLICY :: fragment DESTRUCTION_POLICY_DESTROY_ENTITY :: id DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id @@ -1783,6 +1817,7 @@ builder_mt:execute :: {chunk, entity[], integer, any...} -> builder builder_mt:prologue :: {any...} -> builder builder_mt:epilogue :: {any...} -> builder +builder_mt:index_policy :: id -> builder builder_mt:destruction_policy :: id -> builder ``` @@ -1792,7 +1827,7 @@ builder_mt:destruction_policy :: id -> builder - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) - All internal names now have a double underscore prefix -- Added the new [`evolved.INDEX`](#evolvedindex) fragment trait and the [`evolved.search`](#evolvedsearch), [`evolved.multi_search`](#evolvedmulti_search), [`evolved.reindex`](#evolvedreindex) functions that allow finding entities by their component values +- Added the new [`evolved.INDEX`](#evolvedindex) and [`evolved.INDEX_POLICY`](#evolvedindex_policy) fragment traits and the [`evolved.(multi_)search(_to)`](#evolvedsearch) and [`evolved.reindex`](#evolvedreindex) functions that allow finding entities by their component values ### v1.11.1 @@ -1925,6 +1960,12 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EPILOGUE` +### `evolved.INDEX_POLICY` + +### `evolved.INDEX_POLICY_ORDERED` + +### `evolved.INDEX_POLICY_UNORDERED` + ### `evolved.DESTRUCTION_POLICY` ### `evolved.DESTRUCTION_POLICY_DESTROY_ENTITY` @@ -2849,6 +2890,14 @@ function evolved.builder_mt:prologue(prologue) end function evolved.builder_mt:epilogue(epilogue) end ``` +#### `evolved.builder_mt:index_policy` + +```lua +---@param index_policy evolved.id +---@return evolved.builder builder +function evolved.builder_mt:index_policy(index_policy) end +``` + #### `evolved.builder_mt:destruction_policy` ```lua diff --git a/develop/ROADMAP.md b/develop/ROADMAP.md index 85b9616..057aff1 100644 --- a/develop/ROADMAP.md +++ b/develop/ROADMAP.md @@ -3,7 +3,6 @@ ## Backlog - observers and events -- add INDEX fragment trait - use compact prefix-tree for chunks ## Thoughts diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua index aa7e4c1..70bb286 100644 --- a/develop/testing/search_tests.lua +++ b/develop/testing/search_tests.lua @@ -545,3 +545,333 @@ do evo.destroy(e) end + +do + assert(evo.get(evo.NAME, evo.INDEX_POLICY) == evo.INDEX_POLICY_UNORDERED) + assert(evo.get(evo.GROUP, evo.INDEX_POLICY) == evo.INDEX_POLICY_ORDERED) + + assert(evo.lookup('__INDEX_POLICY') == evo.INDEX_POLICY) + assert(evo.lookup('__INDEX_POLICY_ORDERED') == evo.INDEX_POLICY_ORDERED) + assert(evo.lookup('__INDEX_POLICY_UNORDERED') == evo.INDEX_POLICY_UNORDERED) + + local f = evo.builder():index_policy(evo.INDEX_POLICY_ORDERED):build() + assert(evo.get(f, evo.INDEX_POLICY) == evo.INDEX_POLICY_ORDERED) +end + +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'red' } + + -- the default policy does not preserve the order after removals + evo.remove(e1, f) + check_search(f, 'red', e4, e2, e3) + + evo.set(e2, f, 'blue') + check_search(f, 'red', e4, e3) + + evo.destroy(e1, e2, e3, e4) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_UNORDERED):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + + evo.destroy(e1) + check_search(f, 'red', e3, e2) + + evo.destroy(e2, e3) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'red' } + local e5 = evo.spawn { [f] = 'red' } + local e6 = evo.spawn { [f] = 'red' } + + -- the ordered policy preserves the order after removals + evo.remove(e1, f) + check_search(f, 'red', e2, e3, e4, e5, e6) + + evo.clear(e3) + check_search(f, 'red', e2, e4, e5, e6) + + evo.destroy(e4) + check_search(f, 'red', e2, e5, e6) + + -- changing the value moves the entity to the end of the new value group + evo.set(e2, f, 'blue') + check_search(f, 'red', e5, e6) + check_search(f, 'blue', e2) + + evo.set(e2, f, 'red') + check_search(f, 'red', e5, e6, e2) + check_search(f, 'blue') + + evo.destroy(e1, e2, e3, e4, e5, e6) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local t = evo.builder():tag():build() + + local q = evo.builder():include(t):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red', [t] = true } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'red', [t] = true } + local e5 = evo.spawn { [f] = 'red' } + + -- batch operations preserve the order too + evo.batch_remove(q, f) + check_search(f, 'red', e1, e3, e5) + + evo.batch_set(q, f, 'red') + check_search(f, 'red', e1, e3, e5, e2, e4) + + evo.batch_clear(q) + check_search(f, 'red', e1, e3, e5) + + evo.destroy(e1, e2, e3, e4, e5) +end + +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'red' } + + -- changing the value moves the entity to the end of the value group in the index, + -- but the entity stays in the same place in the chunk (chunk: e1, e2, e3, e4) + evo.set(e1, f, 'blue') + evo.set(e1, f, 'red') + check_search(f, 'red', e4, e2, e3, e1) + + -- changing the policy rebuilds the index in the chunk order + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_ORDERED) + check_search(f, 'red', e1, e2, e3, e4) + + -- the ordered policy is applied after rebuilding (chunk: e1, e4, e3) + evo.remove(e2, f) + check_search(f, 'red', e1, e3, e4) + + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_UNORDERED) + check_search(f, 'red', e1, e4, e3) + + -- the unordered policy is applied after rebuilding + evo.set(e1, f, 'blue') + evo.set(e1, f, 'red') + check_search(f, 'red', e3, e4, e1) + + -- removing the policy rebuilds the index too + evo.remove(f, evo.INDEX_POLICY) + check_search(f, 'red', e1, e4, e3) + + evo.destroy(e1, e2, e3, e4) +end + +do + local f = evo.builder():index_policy(evo.INDEX_POLICY_ORDERED):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + + -- the policy is applied when the trait is added later + evo.set(f, evo.INDEX) + check_search(f, 'red', e1, e2, e3) + + evo.remove(e1, f) + check_search(f, 'red', e2, e3) + + evo.destroy(e1, e2, e3) +end + +do + local f = evo.builder():index():build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + + -- the policy can be changed in a deferred block + evo.defer() + do + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_ORDERED) + evo.remove(e1, f) + end + evo.commit() + + check_search(f, 'red', e2, e3) + + evo.destroy(e1, e2, e3) +end + +do + local f = evo.id() + local p = evo.id() + + local e = evo.spawn { [f] = 'red' } + + -- policies are checked only for indexed fragments + evo.set(f, evo.INDEX_POLICY, p) + + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_ORDERED) + evo.set(f, evo.INDEX) + check_search(f, 'red', e) + + evo.destroy(e, p) +end + +do + -- the order of systems in a group is preserved after removals + local group = evo.id() + + local s1 = evo.builder():group(group):build() + local s2 = evo.builder():group(group):build() + local s3 = evo.builder():group(group):build() + check_search(evo.GROUP, group, s1, s2, s3) + + evo.remove(s1, evo.GROUP) + check_search(evo.GROUP, group, s2, s3) + + evo.set(s1, evo.GROUP, group) + check_search(evo.GROUP, group, s2, s3, s1) + + evo.destroy(s2) + check_search(evo.GROUP, group, s3, s1) + + evo.destroy(group, s1, s2, s3) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local t = evo.builder():tag():build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red' } + local e3 = evo.spawn { [f] = 'red' } + + -- moving to another chunk does not change the order in the index + evo.set(e1, t) + check_search(f, 'red', e1, e2, e3) + + evo.remove(e2, f) + check_search(f, 'red', e1, e3) + + evo.destroy(e1, e2, e3) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local q = evo.builder():include(f):build() + + local list1 = evo.multi_spawn(4, { [f] = 'red' }) + local list2 = evo.multi_spawn(4, { [f] = 'blue' }) + + -- batch destroying preserves the order of the remaining entities + evo.destroy(list1[2], list1[3]) + check_search(f, 'red', list1[1], list1[4]) + check_search(f, 'blue', list2[1], list2[2], list2[3], list2[4]) + + evo.batch_destroy(q) + check_search(f, 'red') + check_search(f, 'blue') +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local w = evo.builder():tag():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'red', [w] = true } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'red', [w] = true } + local e5 = evo.spawn { [f] = 'red' } + + -- destroying a fragment with the destroy entity policy preserves the order too + evo.destroy(w) + assert(not evo.alive(e2) and not evo.alive(e4)) + check_search(f, 'red', e1, e3, e5) + + evo.destroy(e1, e3, e5) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local q = evo.builder():include(f):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'blue' } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'blue' } + + -- batch setting keeps unchanged entities in place + -- and appends changed ones to the end of the new value group in the chunk order + evo.batch_set(q, f, 'blue') + check_search(f, 'red') + check_search(f, 'blue', e2, e4, e1, e3) + + evo.batch_set(q, f, 'green') + check_search(f, 'blue') + check_search(f, 'green', e1, e2, e3, e4) + + evo.destroy(e1, e2, e3, e4) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local q = evo.builder():include(f):build() + + local e1 = evo.spawn { [f] = 'red' } + local e2 = evo.spawn { [f] = 'blue' } + local e3 = evo.spawn { [f] = 'red' } + local e4 = evo.spawn { [f] = 'blue' } + + for chunk, entity_list, entity_count in evo.execute(q) do + local storage = chunk:components(f) + for place = 1, entity_count do + if entity_list[place] == e1 or entity_list[place] == e3 then + storage[place] = 'blue' + end + end + end + + -- reindexing works the same way as batch setting + evo.reindex(f) + check_search(f, 'red') + check_search(f, 'blue', e2, e4, e1, e3) + + evo.destroy(e1, e2, e3, e4) +end + +do + local f = evo.builder():index():default('def'):build() + local r = evo.builder():tag():require(f):build() + local t = evo.builder():tag():build() + + local q = evo.builder():include(t):build() + + local e1 = evo.spawn { [f] = 'def' } + local e2 = evo.spawn { [t] = true } + local e3 = evo.spawn { [t] = true } + + -- required fragments are indexed by batch operations too + evo.batch_set(q, r) + check_search(f, 'def', e1, e2, e3) + + evo.destroy(e1, e2, e3) +end diff --git a/evolved.d.tl b/evolved.d.tl index 1742203..5c447dc 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -139,6 +139,7 @@ prologue: function(self: Builder, prologue: function(...: any)): Builder epilogue: function(self: Builder, epilogue: function(...: any)): Builder + index_policy: function(self: Builder, index_policy: Id): Builder destruction_policy: function(self: Builder, destruction_policy: Id): Builder end @@ -176,6 +177,10 @@ PROLOGUE: Fragment EPILOGUE: Fragment + INDEX_POLICY: Fragment + INDEX_POLICY_ORDERED: Id + INDEX_POLICY_UNORDERED: Id + DESTRUCTION_POLICY: Fragment DESTRUCTION_POLICY_DESTROY_ENTITY: Id DESTRUCTION_POLICY_REMOVE_FRAGMENT: Id diff --git a/evolved.lua b/evolved.lua index 3563d33..b163e76 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1107,6 +1107,7 @@ end --- ---@class (exact) evolved.search_index: { +--- __ordered: boolean, --- __entity_map: { [evolved.component]: evolved.entity }, --- __entities_map: { [evolved.component]: evolved.assoc_list }, --- __component_map: { [evolved.entity]: evolved.component }, @@ -1114,11 +1115,13 @@ end local __search_index_fns = {} +---@param ordered boolean ---@return evolved.search_index ---@nodiscard -function __search_index_fns.new() +function __search_index_fns.new(ordered) ---@type evolved.search_index return { + __ordered = ordered, __entity_map = __lua_table_new(), __entities_map = __lua_table_new(), __component_map = __lua_table_new(), @@ -1135,6 +1138,38 @@ function __search_index_fns.assign(index, entity, component) end end +---@param index evolved.search_index +---@param entity_list evolved.entity[] +---@param component_list evolved.component[] +---@param first integer +---@param last integer +function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) + local component_map = index.__component_map + + local changed_count = 0 + + for place = last, first, -1 do + local entity = entity_list[place] + + if component_map[entity] ~= component_list[place] then + __search_index_fns.remove(index, entity) + changed_count = changed_count + 1 + end + end + + if changed_count == 0 then + return + end + + for place = first, last do + local entity = entity_list[place] + + if component_map[entity] == nil then + __search_index_fns.insert(index, entity, component_list[place]) + end + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -1163,10 +1198,9 @@ function __search_index_fns.insert(index, entity, component) if not indexed_entities then entities_map[component] = __assoc_list_fns.from(indexed_entity, entity) - return + else + __assoc_list_fns.insert(indexed_entities, entity) end - - __assoc_list_fns.insert(indexed_entities, entity) end ---@param index evolved.search_index @@ -1220,29 +1254,34 @@ end ---@param index evolved.search_index ---@param entity evolved.entity function __search_index_fns.remove(index, entity) + local entity_map = index.__entity_map + local entities_map = index.__entities_map + local component_map = index.__component_map + ---@type evolved.component? - local component = index.__component_map[entity] + local component = component_map[entity] if component == nil then return end - index.__component_map[entity] = nil + component_map[entity] = nil ---@type evolved.assoc_list? - local indexed_entities = index.__entities_map[component] + local indexed_entities = entities_map[component] if indexed_entities then - if __assoc_list_fns.unordered_remove(indexed_entities, entity) == 0 then - index.__entities_map[component], indexed_entities = nil, nil + local remove_fn = index.__ordered + and __assoc_list_fns.remove + or __assoc_list_fns.unordered_remove + + if remove_fn(indexed_entities, entity) == 0 then + entities_map[component], indexed_entities = nil, nil end end - ---@type evolved.entity? - local indexed_entity = index.__entity_map[component] - - if indexed_entity == entity then - index.__entity_map[component] = indexed_entities and indexed_entities.__item_list[1] or nil + if entity_map[component] == entity then + entity_map[component] = indexed_entities and indexed_entities.__item_list[1] or nil end end @@ -1255,10 +1294,14 @@ function __search_index_fns.multi_remove(index, entity_list, first, last) local entities_map = index.__entities_map local component_map = index.__component_map + local remove_fn = index.__ordered + and __assoc_list_fns.remove + or __assoc_list_fns.unordered_remove + local last_entities ---@type evolved.assoc_list? local last_component ---@type evolved.component? - for place = first, last do + for place = last, first, -1 do local entity = entity_list[place] ---@type evolved.component? @@ -1278,7 +1321,7 @@ function __search_index_fns.multi_remove(index, entity_list, first, last) end if indexed_entities then - if __assoc_list_fns.unordered_remove(indexed_entities, entity) == 0 then + if remove_fn(indexed_entities, entity) == 0 then entities_map[component], indexed_entities = nil, nil last_entities, last_component = nil, nil end @@ -1332,6 +1375,10 @@ local __EXECUTE = __acquire_id() local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() +local __INDEX_POLICY = __acquire_id() +local __INDEX_POLICY_ORDERED = __acquire_id() +local __INDEX_POLICY_UNORDERED = __acquire_id() + local __DESTRUCTION_POLICY = __acquire_id() local __DESTRUCTION_POLICY_DESTROY_ENTITY = __acquire_id() local __DESTRUCTION_POLICY_REMOVE_FRAGMENT = __acquire_id() @@ -2427,9 +2474,23 @@ function __update_search_index(fragment) __update_major_chunks(fragment) if __evolved_has(fragment, __INDEX) then - local fragment_search_index = __search_index_fns.new() - __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) + local fragment_index_policy = __evolved_get(fragment, __INDEX_POLICY) + or __INDEX_POLICY_UNORDERED + + ---@type evolved.search_index? + local fragment_search_index + + if fragment_index_policy == __INDEX_POLICY_ORDERED then + fragment_search_index = __search_index_fns.new(true) + elseif fragment_index_policy == __INDEX_POLICY_UNORDERED then + fragment_search_index = __search_index_fns.new(false) + else + __error_fmt('unknown INDEX_POLICY (%s) on (%s)', + __id_name(fragment_index_policy), __id_name(fragment)) + end + __search_indices[fragment] = fragment_search_index + __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) __trace_major_chunks(fragment, __update_chunk_caches) end end @@ -2444,11 +2505,8 @@ function __update_search_index_trace(chunk, fragment, fragment_search_index) local chunk_entity_list = chunk.__entity_list local chunk_component_storage = chunk.__component_storages[chunk_component_index] - for place = 1, chunk.__entity_count do - local entity = chunk_entity_list[place] - local component = chunk_component_storage[place] - __search_index_fns.assign(fragment_search_index, entity, component) - end + __search_index_fns.multi_assign(fragment_search_index, + chunk_entity_list, chunk_component_storage, 1, chunk.__entity_count) end end @@ -4124,13 +4182,11 @@ function __chunk_set(old_chunk, fragment, component) end ---@type evolved.search_index? - local fragment_search_index - - if old_chunk_has_index_fragments then - fragment_search_index = __search_indices[fragment] - end + local fragment_search_index = old_chunk_has_index_fragments + and __search_indices[fragment] + or nil - if fragment_on_set or fragment_on_assign or fragment_search_index then + if fragment_on_set or fragment_on_assign then local old_component_index = old_component_indices[fragment] if old_component_index then @@ -4155,10 +4211,6 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_assign then __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) end - - if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component) - end end else local new_component = component @@ -4178,10 +4230,6 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_assign then __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) end - - if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component) - end end end else @@ -4213,6 +4261,17 @@ function __chunk_set(old_chunk, fragment, component) -- nothing end end + + if fragment_search_index then + local old_component_index = old_component_indices[fragment] + + if old_component_index then + local old_component_storage = old_component_storages[old_component_index] + + __search_index_fns.multi_assign(fragment_search_index, + old_entity_list, old_component_storage, 1, old_entity_count) + end + end else local ini_new_chunk = new_chunk local ini_fragment_set = ini_new_chunk.__fragment_set @@ -4246,11 +4305,9 @@ function __chunk_set(old_chunk, fragment, component) end ---@type evolved.search_index? - local fragment_search_index - - if new_chunk_has_index_fragments then - fragment_search_index = __search_indices[fragment] - end + local fragment_search_index = new_chunk_has_index_fragments + and __search_indices[fragment] + or nil local sum_entity_count = old_entity_count + new_chunk.__entity_count @@ -4317,7 +4374,7 @@ function __chunk_set(old_chunk, fragment, component) __detach_all_entities(old_chunk) end - if fragment_on_set or fragment_on_insert or fragment_search_index then + if fragment_on_set or fragment_on_insert then local new_component_index = new_component_indices[fragment] if new_component_index then @@ -4341,10 +4398,6 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_insert then __defer_call_hook(fragment_on_insert, entity, fragment, new_component) end - - if fragment_search_index then - __search_index_fns.insert(fragment_search_index, entity, new_component) - end end else local new_component = component @@ -4363,10 +4416,6 @@ function __chunk_set(old_chunk, fragment, component) if fragment_on_insert then __defer_call_hook(fragment_on_insert, entity, fragment, new_component) end - - if fragment_search_index then - __search_index_fns.insert(fragment_search_index, entity, new_component) - end end end else @@ -4409,6 +4458,17 @@ function __chunk_set(old_chunk, fragment, component) end end + if fragment_search_index then + local new_component_index = new_component_indices[fragment] + + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] + + __search_index_fns.multi_insert(fragment_search_index, + new_entity_list, new_component_storage, new_entity_count + 1, sum_entity_count) + end + end + if ini_new_chunk.__has_required_fragments then local req_fragment_list = new_chunk.__fragment_list local req_fragment_count = new_chunk.__fragment_count @@ -4428,13 +4488,11 @@ function __chunk_set(old_chunk, fragment, component) end ---@type evolved.search_index? - local req_fragment_search_index - - if new_chunk_has_index_fragments then - req_fragment_search_index = __search_indices[req_fragment] - end + local req_fragment_search_index = new_chunk_has_index_fragments + and __search_indices[req_fragment] + or nil - if req_fragment_on_set or req_fragment_on_insert or req_fragment_search_index then + if req_fragment_on_set or req_fragment_on_insert then local req_component_index = new_component_indices[req_fragment] if req_component_index then @@ -4457,10 +4515,6 @@ function __chunk_set(old_chunk, fragment, component) if req_fragment_on_insert then __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) end - - if req_fragment_search_index then - __search_index_fns.insert(req_fragment_search_index, entity, req_component) - end end else local req_component = req_fragment_default @@ -4478,10 +4532,6 @@ function __chunk_set(old_chunk, fragment, component) if req_fragment_on_insert then __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) end - - if req_fragment_search_index then - __search_index_fns.insert(req_fragment_search_index, entity, req_component) - end end end else @@ -4521,6 +4571,17 @@ function __chunk_set(old_chunk, fragment, component) -- nothing end end + + if req_fragment_search_index then + local req_component_index = new_component_indices[req_fragment] + + if req_component_index then + local req_component_storage = new_component_storages[req_component_index] + + __search_index_fns.multi_insert(req_fragment_search_index, + new_entity_list, req_component_storage, new_entity_count + 1, sum_entity_count) + end + end end end end @@ -5908,11 +5969,9 @@ function __evolved_set(entity, fragment, component) end ---@type evolved.search_index? - local fragment_search_index - - if old_chunk_has_index_fragments then - fragment_search_index = __search_indices[fragment] - end + local fragment_search_index = old_chunk_has_index_fragments + and __search_indices[fragment] + or nil local old_component_index = old_component_indices[fragment] @@ -5972,11 +6031,9 @@ function __evolved_set(entity, fragment, component) end ---@type evolved.search_index? - local fragment_search_index - - if new_chunk_has_index_fragments then - fragment_search_index = __search_indices[fragment] - end + local fragment_search_index = new_chunk_has_index_fragments + and __search_indices[fragment] + or nil local new_place = new_chunk.__entity_count + 1 @@ -6068,11 +6125,9 @@ function __evolved_set(entity, fragment, component) end ---@type evolved.search_index? - local req_fragment_search_index - - if new_chunk_has_index_fragments then - req_fragment_search_index = __search_indices[req_fragment] - end + local req_fragment_search_index = new_chunk_has_index_fragments + and __search_indices[req_fragment] + or nil local req_component_index = new_component_indices[req_fragment] @@ -7896,6 +7951,12 @@ function __builder_mt:epilogue(epilogue) return self:set(__EPILOGUE, epilogue) end +---@param index_policy evolved.id +---@return evolved.builder builder +function __builder_mt:index_policy(index_policy) + return self:set(__INDEX_POLICY, index_policy) +end + ---@param destruction_policy evolved.id ---@return evolved.builder builder function __builder_mt:destruction_policy(destruction_policy) @@ -7953,6 +8014,9 @@ __evolved_set(__REALLOC, __ON_REMOVE, __update_major_chunks) __evolved_set(__COMPMOVE, __ON_SET, __update_major_chunks) __evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__INDEX_POLICY, __ON_SET, __update_search_index) +__evolved_set(__INDEX_POLICY, __ON_REMOVE, __update_search_index) + --- --- --- @@ -7962,6 +8026,7 @@ __evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) __evolved_set(__TAG, __TAG) __evolved_set(__NAME, __INDEX) +__evolved_set(__NAME, __INDEX_POLICY, __INDEX_POLICY_UNORDERED) __evolved_set(__INDEX, __TAG) __evolved_set(__INDEX, __UNIQUE) @@ -8000,6 +8065,7 @@ __evolved_set(__ON_INSERT, __UNIQUE) __evolved_set(__ON_REMOVE, __UNIQUE) __evolved_set(__GROUP, __INDEX) +__evolved_set(__GROUP, __INDEX_POLICY, __INDEX_POLICY_ORDERED) --- --- @@ -8269,6 +8335,10 @@ __evolved_set(__EXECUTE, __NAME, '__EXECUTE') __evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') __evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') +__evolved_set(__INDEX_POLICY, __NAME, '__INDEX_POLICY') +__evolved_set(__INDEX_POLICY_ORDERED, __NAME, '__INDEX_POLICY_ORDERED') +__evolved_set(__INDEX_POLICY_UNORDERED, __NAME, '__INDEX_POLICY_UNORDERED') + __evolved_set(__DESTRUCTION_POLICY, __NAME, '__DESTRUCTION_POLICY') __evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __NAME, '__DESTRUCTION_POLICY_DESTROY_ENTITY') __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, '__DESTRUCTION_POLICY_REMOVE_FRAGMENT') @@ -8314,6 +8384,10 @@ __evolved_set(__EXECUTE, __INTERNAL) __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) +__evolved_set(__INDEX_POLICY, __INTERNAL) +__evolved_set(__INDEX_POLICY_ORDERED, __INTERNAL) +__evolved_set(__INDEX_POLICY_UNORDERED, __INTERNAL) + __evolved_set(__DESTRUCTION_POLICY, __INTERNAL) __evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __INTERNAL) __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __INTERNAL) @@ -8359,6 +8433,10 @@ evolved.EXECUTE = __EXECUTE evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE +evolved.INDEX_POLICY = __INDEX_POLICY +evolved.INDEX_POLICY_ORDERED = __INDEX_POLICY_ORDERED +evolved.INDEX_POLICY_UNORDERED = __INDEX_POLICY_UNORDERED + evolved.DESTRUCTION_POLICY = __DESTRUCTION_POLICY evolved.DESTRUCTION_POLICY_DESTROY_ENTITY = __DESTRUCTION_POLICY_DESTROY_ENTITY evolved.DESTRUCTION_POLICY_REMOVE_FRAGMENT = __DESTRUCTION_POLICY_REMOVE_FRAGMENT From 3a2b1b7b3b3cd4f1b8a926285a9731a386f6f131 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 11 Oct 2026 09:45:17 +0700 Subject: [PATCH 12/13] indices: linked list instead of policies --- README.md | 36 +--- develop/testing/search_tests.lua | 114 ++--------- evolved.d.tl | 5 - evolved.lua | 329 ++++++++++--------------------- 4 files changed, 125 insertions(+), 359 deletions(-) diff --git a/README.md b/README.md index 37006a8..8a7dc71 100644 --- a/README.md +++ b/README.md @@ -1146,18 +1146,13 @@ assert(red_count == 2 and red_list[1] == unit1 and red_list[2] == unit3) The [`evolved.search`](#evolvedsearch) function returns the first entity with the specified component, while the [`evolved.multi_search`](#evolvedmulti_search) function returns a list of all such entities. The [`evolved.multi_search_to`](#evolvedmulti_search_to) function does the same, but appends the entities to the provided list starting from the specified index and returns only the number of found entities. -Entities are found in the order in which they received their components, but by default this order is not preserved after removals. When an entity loses the component or changes its value, the last entity with the same value takes its place. This makes removals fast, but if the order matters to you, you can change this behavior by using the [`evolved.INDEX_POLICY`](#evolvedindex_policy) fragment trait. This trait expects one of the following predefined identifiers: - -- [`evolved.INDEX_POLICY_ORDERED`](#evolvedindex_policy_ordered) will preserve the order of entities after removals. Entities that change their values are moved to the end of the new value group. Removals take time proportional to the number of entities with the same value, so use this policy only when the order really matters. The [`evolved.GROUP`](#evolvedgroup) fragment uses this policy, so the order of [systems](#systems) in a group is preserved. - -- [`evolved.INDEX_POLICY_UNORDERED`](#evolvedindex_policy_unordered) will not preserve the order of entities after removals. Removals take constant time regardless of the number of entities with the same value. This is the default behavior, so you don't have to set it explicitly, but you can if you want. +Entities are found in the order in which they received their components, and this order is preserved after removals. When an entity changes the value of its component, it is moved to the end of the entities with the new value. Insertions and removals take constant time regardless of the number of entities with the same value. ```lua local evolved = require 'evolved' local team = evolved.builder() :index() - :index_policy(evolved.INDEX_POLICY_ORDERED) :build() local unit1 = evolved.builder() @@ -1176,9 +1171,15 @@ evolved.destroy(unit1) local red_list, red_count = evolved.multi_search(team, 'red') assert(red_count == 2 and red_list[1] == unit2 and red_list[2] == unit3) + +evolved.set(unit2, team, 'blue') +evolved.set(unit2, team, 'red') + +red_list, red_count = evolved.multi_search(team, 'red') +assert(red_count == 2 and red_list[1] == unit3 and red_list[2] == unit2) ``` -The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, and setting. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups. +The index is updated automatically by all operations that change components of the indexed fragment, including spawning, cloning, and setting. The trait can be added or removed at any time. When it is added to a fragment that is already used by some entities, the index is built from these entities in the order they are stored in chunks. The [`evolved.NAME`](#evolvedname) and [`evolved.GROUP`](#evolvedgroup) fragments are indexed too, so you can search for entities by names or for systems by groups, and the order of [systems](#systems) in a group is preserved. Components changed directly in chunk storages during iteration bypass the index, so after such changes you should update it manually using the [`evolved.reindex`](#evolvedreindex) function. It only updates entities whose components have changed, so it is much cheaper than rebuilding the whole index. @@ -1653,10 +1654,6 @@ EXECUTE :: fragment PROLOGUE :: fragment EPILOGUE :: fragment -INDEX_POLICY :: fragment -INDEX_POLICY_ORDERED :: id -INDEX_POLICY_UNORDERED :: id - DESTRUCTION_POLICY :: fragment DESTRUCTION_POLICY_DESTROY_ENTITY :: id DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id @@ -1817,7 +1814,6 @@ builder_mt:execute :: {chunk, entity[], integer, any...} -> builder builder_mt:prologue :: {any...} -> builder builder_mt:epilogue :: {any...} -> builder -builder_mt:index_policy :: id -> builder builder_mt:destruction_policy :: id -> builder ``` @@ -1827,7 +1823,7 @@ builder_mt:destruction_policy :: id -> builder - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) - All internal names now have a double underscore prefix -- Added the new [`evolved.INDEX`](#evolvedindex) and [`evolved.INDEX_POLICY`](#evolvedindex_policy) fragment traits and the [`evolved.(multi_)search(_to)`](#evolvedsearch) and [`evolved.reindex`](#evolvedreindex) functions that allow finding entities by their component values +- Added the new [`evolved.INDEX`](#evolvedindex) fragment trait and the [`evolved.(multi_)search(_to)`](#evolvedsearch) and [`evolved.reindex`](#evolvedreindex) functions that allow finding entities by their component values ### v1.11.1 @@ -1960,12 +1956,6 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EPILOGUE` -### `evolved.INDEX_POLICY` - -### `evolved.INDEX_POLICY_ORDERED` - -### `evolved.INDEX_POLICY_UNORDERED` - ### `evolved.DESTRUCTION_POLICY` ### `evolved.DESTRUCTION_POLICY_DESTROY_ENTITY` @@ -2890,14 +2880,6 @@ function evolved.builder_mt:prologue(prologue) end function evolved.builder_mt:epilogue(epilogue) end ``` -#### `evolved.builder_mt:index_policy` - -```lua ----@param index_policy evolved.id ----@return evolved.builder builder -function evolved.builder_mt:index_policy(index_policy) end -``` - #### `evolved.builder_mt:destruction_policy` ```lua diff --git a/develop/testing/search_tests.lua b/develop/testing/search_tests.lua index 70bb286..45ad6f5 100644 --- a/develop/testing/search_tests.lua +++ b/develop/testing/search_tests.lua @@ -546,52 +546,9 @@ do evo.destroy(e) end -do - assert(evo.get(evo.NAME, evo.INDEX_POLICY) == evo.INDEX_POLICY_UNORDERED) - assert(evo.get(evo.GROUP, evo.INDEX_POLICY) == evo.INDEX_POLICY_ORDERED) - - assert(evo.lookup('__INDEX_POLICY') == evo.INDEX_POLICY) - assert(evo.lookup('__INDEX_POLICY_ORDERED') == evo.INDEX_POLICY_ORDERED) - assert(evo.lookup('__INDEX_POLICY_UNORDERED') == evo.INDEX_POLICY_UNORDERED) - - local f = evo.builder():index_policy(evo.INDEX_POLICY_ORDERED):build() - assert(evo.get(f, evo.INDEX_POLICY) == evo.INDEX_POLICY_ORDERED) -end - do local f = evo.builder():index():build() - local e1 = evo.spawn { [f] = 'red' } - local e2 = evo.spawn { [f] = 'red' } - local e3 = evo.spawn { [f] = 'red' } - local e4 = evo.spawn { [f] = 'red' } - - -- the default policy does not preserve the order after removals - evo.remove(e1, f) - check_search(f, 'red', e4, e2, e3) - - evo.set(e2, f, 'blue') - check_search(f, 'red', e4, e3) - - evo.destroy(e1, e2, e3, e4) -end - -do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_UNORDERED):build() - - local e1 = evo.spawn { [f] = 'red' } - local e2 = evo.spawn { [f] = 'red' } - local e3 = evo.spawn { [f] = 'red' } - - evo.destroy(e1) - check_search(f, 'red', e3, e2) - - evo.destroy(e2, e3) -end - -do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() - local e1 = evo.spawn { [f] = 'red' } local e2 = evo.spawn { [f] = 'red' } local e3 = evo.spawn { [f] = 'red' } @@ -599,7 +556,7 @@ do local e5 = evo.spawn { [f] = 'red' } local e6 = evo.spawn { [f] = 'red' } - -- the ordered policy preserves the order after removals + -- the order of entities is preserved after removals evo.remove(e1, f) check_search(f, 'red', e2, e3, e4, e5, e6) @@ -622,7 +579,7 @@ do end do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local f = evo.builder():index():build() local t = evo.builder():tag():build() local q = evo.builder():include(t):build() @@ -658,48 +615,20 @@ do -- but the entity stays in the same place in the chunk (chunk: e1, e2, e3, e4) evo.set(e1, f, 'blue') evo.set(e1, f, 'red') - check_search(f, 'red', e4, e2, e3, e1) + check_search(f, 'red', e2, e3, e4, e1) - -- changing the policy rebuilds the index in the chunk order - evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_ORDERED) + -- adding the trait again rebuilds the index in the chunk order + evo.remove(f, evo.INDEX) + evo.set(f, evo.INDEX) check_search(f, 'red', e1, e2, e3, e4) - -- the ordered policy is applied after rebuilding (chunk: e1, e4, e3) + -- the order is preserved after rebuilding too (chunk: e1, e4, e3) evo.remove(e2, f) check_search(f, 'red', e1, e3, e4) - evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_UNORDERED) - check_search(f, 'red', e1, e4, e3) - - -- the unordered policy is applied after rebuilding - evo.set(e1, f, 'blue') - evo.set(e1, f, 'red') - check_search(f, 'red', e3, e4, e1) - - -- removing the policy rebuilds the index too - evo.remove(f, evo.INDEX_POLICY) - check_search(f, 'red', e1, e4, e3) - evo.destroy(e1, e2, e3, e4) end -do - local f = evo.builder():index_policy(evo.INDEX_POLICY_ORDERED):build() - - local e1 = evo.spawn { [f] = 'red' } - local e2 = evo.spawn { [f] = 'red' } - local e3 = evo.spawn { [f] = 'red' } - - -- the policy is applied when the trait is added later - evo.set(f, evo.INDEX) - check_search(f, 'red', e1, e2, e3) - - evo.remove(e1, f) - check_search(f, 'red', e2, e3) - - evo.destroy(e1, e2, e3) -end - do local f = evo.builder():index():build() @@ -707,10 +636,9 @@ do local e2 = evo.spawn { [f] = 'red' } local e3 = evo.spawn { [f] = 'red' } - -- the policy can be changed in a deferred block + -- the order is preserved after deferred removals too evo.defer() do - evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_ORDERED) evo.remove(e1, f) end evo.commit() @@ -720,22 +648,6 @@ do evo.destroy(e1, e2, e3) end -do - local f = evo.id() - local p = evo.id() - - local e = evo.spawn { [f] = 'red' } - - -- policies are checked only for indexed fragments - evo.set(f, evo.INDEX_POLICY, p) - - evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_ORDERED) - evo.set(f, evo.INDEX) - check_search(f, 'red', e) - - evo.destroy(e, p) -end - do -- the order of systems in a group is preserved after removals local group = evo.id() @@ -758,7 +670,7 @@ do end do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local f = evo.builder():index():build() local t = evo.builder():tag():build() local e1 = evo.spawn { [f] = 'red' } @@ -776,7 +688,7 @@ do end do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local f = evo.builder():index():build() local q = evo.builder():include(f):build() local list1 = evo.multi_spawn(4, { [f] = 'red' }) @@ -793,7 +705,7 @@ do end do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local f = evo.builder():index():build() local w = evo.builder():tag():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() local e1 = evo.spawn { [f] = 'red' } @@ -811,7 +723,7 @@ do end do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local f = evo.builder():index():build() local q = evo.builder():include(f):build() local e1 = evo.spawn { [f] = 'red' } @@ -833,7 +745,7 @@ do end do - local f = evo.builder():index():index_policy(evo.INDEX_POLICY_ORDERED):build() + local f = evo.builder():index():build() local q = evo.builder():include(f):build() local e1 = evo.spawn { [f] = 'red' } diff --git a/evolved.d.tl b/evolved.d.tl index 5c447dc..1742203 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -139,7 +139,6 @@ prologue: function(self: Builder, prologue: function(...: any)): Builder epilogue: function(self: Builder, epilogue: function(...: any)): Builder - index_policy: function(self: Builder, index_policy: Id): Builder destruction_policy: function(self: Builder, destruction_policy: Id): Builder end @@ -177,10 +176,6 @@ PROLOGUE: Fragment EPILOGUE: Fragment - INDEX_POLICY: Fragment - INDEX_POLICY_ORDERED: Id - INDEX_POLICY_UNORDERED: Id - DESTRUCTION_POLICY: Fragment DESTRUCTION_POLICY_DESTROY_ENTITY: Id DESTRUCTION_POLICY_REMOVE_FRAGMENT: Id diff --git a/evolved.lua b/evolved.lua index b163e76..f07a993 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1106,100 +1106,113 @@ end --- --- ----@class (exact) evolved.search_index: { ---- __ordered: boolean, ---- __entity_map: { [evolved.component]: evolved.entity }, ---- __entities_map: { [evolved.component]: evolved.assoc_list }, ---- __component_map: { [evolved.entity]: evolved.component }, ---- } +---@class (exact) evolved.search_index +---@field package __head_entities table +---@field package __prev_entities table +---@field package __next_entities table +---@field package __entity_components table local __search_index_fns = {} ----@param ordered boolean ---@return evolved.search_index ---@nodiscard -function __search_index_fns.new(ordered) +function __search_index_fns.new() ---@type evolved.search_index return { - __ordered = ordered, - __entity_map = __lua_table_new(), - __entities_map = __lua_table_new(), - __component_map = __lua_table_new(), + __head_entities = {}, + __prev_entities = {}, + __next_entities = {}, + __entity_components = {}, } end ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component -function __search_index_fns.assign(index, entity, component) - if index.__component_map[entity] ~= component then - __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, component) +function __search_index_fns.insert(index, entity, component) + if component ~= component then + -- NaN components cannot be indexed + return end -end ----@param index evolved.search_index ----@param entity_list evolved.entity[] ----@param component_list evolved.component[] ----@param first integer ----@param last integer -function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) - local component_map = index.__component_map + local entity_primary = entity % 2 ^ 20 - local changed_count = 0 + local head_entities = index.__head_entities + local prev_entities = index.__prev_entities + local next_entities = index.__next_entities - for place = last, first, -1 do - local entity = entity_list[place] + ---@type evolved.entity? + local head_entity = head_entities[component] - if component_map[entity] ~= component_list[place] then - __search_index_fns.remove(index, entity) - changed_count = changed_count + 1 - end - end + if head_entity then + local head_entity_primary = head_entity % 2 ^ 20 - if changed_count == 0 then - return - end + local tail_entity = prev_entities[head_entity_primary] + local tail_entity_primary = tail_entity % 2 ^ 20 - for place = first, last do - local entity = entity_list[place] + prev_entities[entity_primary] = tail_entity + next_entities[entity_primary] = head_entity - if component_map[entity] == nil then - __search_index_fns.insert(index, entity, component_list[place]) - end + prev_entities[head_entity_primary] = entity + next_entities[tail_entity_primary] = entity + else + head_entities[component] = entity + + prev_entities[entity_primary] = entity + next_entities[entity_primary] = entity end + + index.__entity_components[entity_primary] = component end ---@param index evolved.search_index ---@param entity evolved.entity ----@param component evolved.component -function __search_index_fns.insert(index, entity, component) - if component ~= component then - -- NaN components cannot be indexed +function __search_index_fns.remove(index, entity) + local entity_primary = entity % 2 ^ 20 + + ---@type evolved.component? + local component = index.__entity_components[entity_primary] + + if component == nil then return end - local entity_map = index.__entity_map - local entities_map = index.__entities_map - local component_map = index.__component_map + local head_entities = index.__head_entities + local prev_entities = index.__prev_entities + local next_entities = index.__next_entities - component_map[entity] = component + local prev_entity = prev_entities[entity_primary] + local next_entity = next_entities[entity_primary] - ---@type evolved.entity? - local indexed_entity = entity_map[component] + if next_entity == entity then + head_entities[component] = nil + else + local prev_entity_primary = prev_entity % 2 ^ 20 + local next_entity_primary = next_entity % 2 ^ 20 - if not indexed_entity then - entity_map[component] = entity - return + prev_entities[next_entity_primary] = prev_entity + next_entities[prev_entity_primary] = next_entity + + if head_entities[component] == entity then + head_entities[component] = next_entity + end end - ---@type evolved.assoc_list? - local indexed_entities = entities_map[component] + prev_entities[entity_primary] = nil + next_entities[entity_primary] = nil - if not indexed_entities then - entities_map[component] = __assoc_list_fns.from(indexed_entity, entity) - else - __assoc_list_fns.insert(indexed_entities, entity) + index.__entity_components[entity_primary] = nil +end + +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.assign(index, entity, component) + local entity_components = index.__entity_components + + if entity_components[entity % 2 ^ 20] ~= component then + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, component) end end @@ -1209,127 +1222,36 @@ end ---@param first integer ---@param last integer function __search_index_fns.multi_insert(index, entity_list, component_list, first, last) - local entity_map = index.__entity_map - local entities_map = index.__entities_map - local component_map = index.__component_map - - local last_entities ---@type evolved.assoc_list? - local last_component ---@type evolved.component? - for place = first, last do - local entity = entity_list[place] - local component = component_list[place] - - if component ~= component then - -- NaN components cannot be indexed - elseif last_entities and last_component == component then - component_map[entity] = component - __assoc_list_fns.insert(last_entities, entity) - else - component_map[entity] = component - - ---@type evolved.entity? - local indexed_entity = entity_map[component] - - if not indexed_entity then - entity_map[component] = entity - last_entities, last_component = nil, nil - else - ---@type evolved.assoc_list? - local indexed_entities = entities_map[component] - - if not indexed_entities then - indexed_entities = __assoc_list_fns.from(indexed_entity, entity) - entities_map[component] = indexed_entities - else - __assoc_list_fns.insert(indexed_entities, entity) - end - - last_entities, last_component = indexed_entities, component - end - end + __search_index_fns.insert(index, entity_list[place], component_list[place]) end end ---@param index evolved.search_index ----@param entity evolved.entity -function __search_index_fns.remove(index, entity) - local entity_map = index.__entity_map - local entities_map = index.__entities_map - local component_map = index.__component_map - - ---@type evolved.component? - local component = component_map[entity] - - if component == nil then - return - end - - component_map[entity] = nil - - ---@type evolved.assoc_list? - local indexed_entities = entities_map[component] - - if indexed_entities then - local remove_fn = index.__ordered - and __assoc_list_fns.remove - or __assoc_list_fns.unordered_remove - - if remove_fn(indexed_entities, entity) == 0 then - entities_map[component], indexed_entities = nil, nil - end - end - - if entity_map[component] == entity then - entity_map[component] = indexed_entities and indexed_entities.__item_list[1] or nil +---@param entity_list evolved.entity[] +---@param first integer +---@param last integer +function __search_index_fns.multi_remove(index, entity_list, first, last) + for place = first, last do + __search_index_fns.remove(index, entity_list[place]) end end ---@param index evolved.search_index ---@param entity_list evolved.entity[] +---@param component_list evolved.component[] ---@param first integer ---@param last integer -function __search_index_fns.multi_remove(index, entity_list, first, last) - local entity_map = index.__entity_map - local entities_map = index.__entities_map - local component_map = index.__component_map - - local remove_fn = index.__ordered - and __assoc_list_fns.remove - or __assoc_list_fns.unordered_remove - - local last_entities ---@type evolved.assoc_list? - local last_component ---@type evolved.component? +function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) + local entity_components = index.__entity_components - for place = last, first, -1 do + for place = first, last do local entity = entity_list[place] + local component = component_list[place] - ---@type evolved.component? - local component = component_map[entity] - - if component ~= nil then - component_map[entity] = nil - - ---@type evolved.assoc_list? - local indexed_entities - - if last_entities and last_component == component then - indexed_entities = last_entities - else - indexed_entities = entities_map[component] - last_entities, last_component = indexed_entities, component - end - - if indexed_entities then - if remove_fn(indexed_entities, entity) == 0 then - entities_map[component], indexed_entities = nil, nil - last_entities, last_component = nil, nil - end - end - - if entity_map[component] == entity then - entity_map[component] = indexed_entities and indexed_entities.__item_list[1] or nil - end + if entity_components[entity % 2 ^ 20] ~= component then + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, component) end end end @@ -1375,10 +1297,6 @@ local __EXECUTE = __acquire_id() local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() -local __INDEX_POLICY = __acquire_id() -local __INDEX_POLICY_ORDERED = __acquire_id() -local __INDEX_POLICY_UNORDERED = __acquire_id() - local __DESTRUCTION_POLICY = __acquire_id() local __DESTRUCTION_POLICY_DESTROY_ENTITY = __acquire_id() local __DESTRUCTION_POLICY_REMOVE_FRAGMENT = __acquire_id() @@ -2474,21 +2392,7 @@ function __update_search_index(fragment) __update_major_chunks(fragment) if __evolved_has(fragment, __INDEX) then - local fragment_index_policy = __evolved_get(fragment, __INDEX_POLICY) - or __INDEX_POLICY_UNORDERED - - ---@type evolved.search_index? - local fragment_search_index - - if fragment_index_policy == __INDEX_POLICY_ORDERED then - fragment_search_index = __search_index_fns.new(true) - elseif fragment_index_policy == __INDEX_POLICY_UNORDERED then - fragment_search_index = __search_index_fns.new(false) - else - __error_fmt('unknown INDEX_POLICY (%s) on (%s)', - __id_name(fragment_index_policy), __id_name(fragment)) - end - + local fragment_search_index = __search_index_fns.new() __search_indices[fragment] = fragment_search_index __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) __trace_major_chunks(fragment, __update_chunk_caches) @@ -6888,7 +6792,7 @@ function __evolved_search(fragment, component) __id_name(fragment)) end - return fragment_search_index.__entity_map[component] + return fragment_search_index.__head_entities[component] end ---@param fragment evolved.fragment @@ -6915,29 +6819,25 @@ function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, __id_name(fragment)) end - do - local indexed_entities = fragment_search_index.__entities_map[component] - local indexed_entity_list = indexed_entities and indexed_entities.__item_list - local indexed_entity_count = indexed_entities and indexed_entities.__item_count or 0 + local entity_count = 0 - if indexed_entity_count > 0 then - __lua_table_move( - indexed_entity_list, 1, indexed_entity_count, - out_entity_first, out_entity_list) - return indexed_entity_count - end - end + local head_entities = fragment_search_index.__head_entities + local next_entities = fragment_search_index.__next_entities - do - local indexed_entity = fragment_search_index.__entity_map[component] + ---@type evolved.entity? + local head_entity = head_entities[component] - if indexed_entity then - out_entity_list[out_entity_first] = indexed_entity - return 1 - end + if head_entity then + local entity = head_entity + + repeat + out_entity_list[out_entity_first + entity_count] = entity + entity = next_entities[entity % 2 ^ 20] + entity_count = entity_count + 1 + until entity == head_entity end - return 0 + return entity_count end ---@param ... evolved.fragment fragments @@ -7951,12 +7851,6 @@ function __builder_mt:epilogue(epilogue) return self:set(__EPILOGUE, epilogue) end ----@param index_policy evolved.id ----@return evolved.builder builder -function __builder_mt:index_policy(index_policy) - return self:set(__INDEX_POLICY, index_policy) -end - ---@param destruction_policy evolved.id ---@return evolved.builder builder function __builder_mt:destruction_policy(destruction_policy) @@ -8014,9 +7908,6 @@ __evolved_set(__REALLOC, __ON_REMOVE, __update_major_chunks) __evolved_set(__COMPMOVE, __ON_SET, __update_major_chunks) __evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) -__evolved_set(__INDEX_POLICY, __ON_SET, __update_search_index) -__evolved_set(__INDEX_POLICY, __ON_REMOVE, __update_search_index) - --- --- --- @@ -8026,7 +7917,6 @@ __evolved_set(__INDEX_POLICY, __ON_REMOVE, __update_search_index) __evolved_set(__TAG, __TAG) __evolved_set(__NAME, __INDEX) -__evolved_set(__NAME, __INDEX_POLICY, __INDEX_POLICY_UNORDERED) __evolved_set(__INDEX, __TAG) __evolved_set(__INDEX, __UNIQUE) @@ -8065,7 +7955,6 @@ __evolved_set(__ON_INSERT, __UNIQUE) __evolved_set(__ON_REMOVE, __UNIQUE) __evolved_set(__GROUP, __INDEX) -__evolved_set(__GROUP, __INDEX_POLICY, __INDEX_POLICY_ORDERED) --- --- @@ -8335,10 +8224,6 @@ __evolved_set(__EXECUTE, __NAME, '__EXECUTE') __evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') __evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') -__evolved_set(__INDEX_POLICY, __NAME, '__INDEX_POLICY') -__evolved_set(__INDEX_POLICY_ORDERED, __NAME, '__INDEX_POLICY_ORDERED') -__evolved_set(__INDEX_POLICY_UNORDERED, __NAME, '__INDEX_POLICY_UNORDERED') - __evolved_set(__DESTRUCTION_POLICY, __NAME, '__DESTRUCTION_POLICY') __evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __NAME, '__DESTRUCTION_POLICY_DESTROY_ENTITY') __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __NAME, '__DESTRUCTION_POLICY_REMOVE_FRAGMENT') @@ -8384,10 +8269,6 @@ __evolved_set(__EXECUTE, __INTERNAL) __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) -__evolved_set(__INDEX_POLICY, __INTERNAL) -__evolved_set(__INDEX_POLICY_ORDERED, __INTERNAL) -__evolved_set(__INDEX_POLICY_UNORDERED, __INTERNAL) - __evolved_set(__DESTRUCTION_POLICY, __INTERNAL) __evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __INTERNAL) __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __INTERNAL) @@ -8433,10 +8314,6 @@ evolved.EXECUTE = __EXECUTE evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE -evolved.INDEX_POLICY = __INDEX_POLICY -evolved.INDEX_POLICY_ORDERED = __INDEX_POLICY_ORDERED -evolved.INDEX_POLICY_UNORDERED = __INDEX_POLICY_UNORDERED - evolved.DESTRUCTION_POLICY = __DESTRUCTION_POLICY evolved.DESTRUCTION_POLICY_DESTROY_ENTITY = __DESTRUCTION_POLICY_DESTROY_ENTITY evolved.DESTRUCTION_POLICY_REMOVE_FRAGMENT = __DESTRUCTION_POLICY_REMOVE_FRAGMENT From 56aefb3fc623971625ab81b1b9a68d4e2c83dd49 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 11 Oct 2026 10:05:26 +0700 Subject: [PATCH 13/13] v1.12.0 --- README.md | 4 +-- evolved.lua | 2 +- rockspecs/evolved.lua-1.12.0-0.rockspec | 34 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 rockspecs/evolved.lua-1.12.0-0.rockspec diff --git a/README.md b/README.md index 8a7dc71..5e97ce1 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ - [Chunk](#chunk) - [Builder](#builder) - [Changelog](#changelog) - - [vX.Y.Z](#vxyz) + - [v1.12.0](#v1120) - [v1.11.1](#v1111) - [v1.11.0](#v1110) - [v1.10.0](#v1100) @@ -1819,7 +1819,7 @@ builder_mt:destruction_policy :: id -> builder ## Changelog -### vX.Y.Z +### v1.12.0 - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) - All internal names now have a double underscore prefix diff --git a/evolved.lua b/evolved.lua index f07a993..5c66b4f 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1,7 +1,7 @@ local evolved = { __HOMEPAGE = 'https://github.com/BlackMATov/evolved.lua', __DESCRIPTION = 'Evolved ECS (Entity-Component-System) for Lua', - __VERSION = '1.11.1', + __VERSION = '1.12.0', __LICENSE = [[ MIT License diff --git a/rockspecs/evolved.lua-1.12.0-0.rockspec b/rockspecs/evolved.lua-1.12.0-0.rockspec new file mode 100644 index 0000000..86c0782 --- /dev/null +++ b/rockspecs/evolved.lua-1.12.0-0.rockspec @@ -0,0 +1,34 @@ +rockspec_format = "3.0" +package = "evolved.lua" +version = "1.12.0-0" +source = { + url = "git://github.com/BlackMATov/evolved.lua", + tag = "v1.12.0", +} +description = { + homepage = "https://github.com/BlackMATov/evolved.lua", + summary = "Evolved ECS (Entity-Component-System) for Lua", + detailed = [[ + `evolved.lua` is a fast and flexible ECS (Entity-Component-System) library for Lua. + It is designed to be simple and easy to use, while providing all the features needed to create complex systems with blazing performance. + ]], + license = "MIT", + labels = { + "ecs", + "entity", + "entities", + "component", + "components", + "entity-component", + "entity-component-system", + }, +} +dependencies = { + "lua >= 5.1", +} +build = { + type = "builtin", + modules = { + evolved = "evolved.lua", + } +}