From a2383201640b440ec7f8adb60dc0cba384198aa7 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 14 Oct 2026 06:36:45 +0700 Subject: [PATCH 01/48] monitors: dummy enter/leave fragments --- README.md | 30 ++++++++++++++++++++++++++++++ evolved.d.tl | 7 +++++++ evolved.lua | 27 +++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/README.md b/README.md index 5e97ce1..4466017 100644 --- a/README.md +++ b/README.md @@ -1601,6 +1601,10 @@ realloc :: {storage?, integer, integer -> storage?} compmove :: {storage, integer, integer, integer, storage} execute :: {chunk, entity[], integer, any...} + +enter :: {entity, system} +leave :: {entity, system} + prologue :: {any...} epilogue :: {any...} @@ -1651,6 +1655,9 @@ GROUP :: fragment QUERY :: fragment EXECUTE :: fragment +ENTER :: fragment +LEAVE :: fragment + PROLOGUE :: fragment EPILOGUE :: fragment @@ -1811,6 +1818,9 @@ builder_mt:group :: system -> builder builder_mt:query :: query -> builder builder_mt:execute :: {chunk, entity[], integer, any...} -> builder +builder_mt:enter :: {entity, system} -> builder +builder_mt:leave :: {entity, system} -> builder + builder_mt:prologue :: {any...} -> builder builder_mt:epilogue :: {any...} -> builder @@ -1952,6 +1962,10 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EXECUTE` +### `evolved.ENTER` + +### `evolved.LEAVE` + ### `evolved.PROLOGUE` ### `evolved.EPILOGUE` @@ -2864,6 +2878,22 @@ function evolved.builder_mt:query(query) end function evolved.builder_mt:execute(execute) end ``` +#### `evolved.builder_mt:enter` + +```lua +---@param enter evolved.enter +---@return evolved.builder builder +function evolved.builder_mt:enter(enter) end +``` + +#### `evolved.builder_mt:leave` + +```lua +---@param leave evolved.leave +---@return evolved.builder builder +function evolved.builder_mt:leave(leave) end +``` + #### `evolved.builder_mt:prologue` ```lua diff --git a/evolved.d.tl b/evolved.d.tl index 1742203..6153aa9 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -136,6 +136,9 @@ query: function(self: Builder, query: Query): Builder execute: function(self: Builder, execute: function(Chunk, {Entity}, integer, ...: any)): Builder + enter: function(self: Builder, enter: function(Entity, System)): Builder + leave: function(self: Builder, leave: function(Entity, System)): Builder + prologue: function(self: Builder, prologue: function(...: any)): Builder epilogue: function(self: Builder, epilogue: function(...: any)): Builder @@ -173,6 +176,10 @@ QUERY: Fragment EXECUTE: Fragment + + ENTER: Fragment + LEAVE: Fragment + PROLOGUE: Fragment EPILOGUE: Fragment diff --git a/evolved.lua b/evolved.lua index 5c66b4f..57b2f69 100644 --- a/evolved.lua +++ b/evolved.lua @@ -52,6 +52,9 @@ local evolved = { --- entity_count: integer, --- ...: any) +---@alias evolved.enter fun(entity: evolved.entity, system: evolved.system) +---@alias evolved.leave fun(entity: evolved.entity, system: evolved.system) + ---@alias evolved.prologue fun(...: any) ---@alias evolved.epilogue fun(...: any) @@ -1294,6 +1297,9 @@ local __GROUP = __acquire_id() local __QUERY = __acquire_id() local __EXECUTE = __acquire_id() +local __ENTER = __acquire_id() +local __LEAVE = __acquire_id() + local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() @@ -7839,6 +7845,18 @@ function __builder_mt:execute(execute) return self:set(__EXECUTE, execute) end +---@param enter evolved.enter +---@return evolved.builder builder +function __builder_mt:enter(enter) + return self:set(__ENTER, enter) +end + +---@param leave evolved.leave +---@return evolved.builder builder +function __builder_mt:leave(leave) + return self:set(__LEAVE, leave) +end + ---@param prologue evolved.prologue ---@return evolved.builder builder function __builder_mt:prologue(prologue) @@ -8221,6 +8239,9 @@ __evolved_set(__GROUP, __NAME, '__GROUP') __evolved_set(__QUERY, __NAME, '__QUERY') __evolved_set(__EXECUTE, __NAME, '__EXECUTE') +__evolved_set(__ENTER, __NAME, '__ENTER') +__evolved_set(__LEAVE, __NAME, '__LEAVE') + __evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') __evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') @@ -8266,6 +8287,9 @@ __evolved_set(__GROUP, __INTERNAL) __evolved_set(__QUERY, __INTERNAL) __evolved_set(__EXECUTE, __INTERNAL) +__evolved_set(__ENTER, __INTERNAL) +__evolved_set(__LEAVE, __INTERNAL) + __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) @@ -8311,6 +8335,9 @@ evolved.GROUP = __GROUP evolved.QUERY = __QUERY evolved.EXECUTE = __EXECUTE +evolved.ENTER = __ENTER +evolved.LEAVE = __LEAVE + evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE From e75e694f2d9dd2362f5d476487d3217e02f1a39b Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 14 Oct 2026 07:45:39 +0700 Subject: [PATCH 02/48] monitors: call monitors --- evolved.lua | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) diff --git a/evolved.lua b/evolved.lua index 57b2f69..268fc25 100644 --- a/evolved.lua +++ b/evolved.lua @@ -143,6 +143,9 @@ local evolved = { ---@field package __component_duplicates evolved.duplicate[] ---@field package __component_reallocs evolved.realloc[] ---@field package __component_compmoves evolved.compmove[] +---@field package __monitor_set table +---@field package __monitor_list evolved.system[] +---@field package __monitor_count integer ---@field package __with_fragment_edges table ---@field package __without_fragment_edges table ---@field package __with_required_fragments? evolved.chunk @@ -625,6 +628,10 @@ local __root_count = 0 ---@type integer local __major_chunks = {} ---@type table> local __minor_chunks = {} ---@type table> +local __monitor_set = {} ---@type table +local __monitor_list = {} ---@type evolved.system[] +local __monitor_count = 0 ---@type integer + local __query_chunks = {} ---@type table> local __major_queries = {} ---@type table> @@ -1477,6 +1484,11 @@ local __defer_multi_clone_entity --- local __safe_tbls = { + __EMPTY_ENTITY_SET = setmetatable({}, { + __tostring = function() return 'empty entity set' end, + __newindex = function() __error_fmt 'attempt to modify empty entity set' end + }) --[[@as table]], + __EMPTY_FRAGMENT_SET = setmetatable({}, { __tostring = function() return 'empty fragment set' end, __newindex = function() __error_fmt 'attempt to modify empty fragment set' end @@ -1561,6 +1573,9 @@ function __new_chunk(chunk_parent, chunk_fragment) __component_duplicates = {}, __component_reallocs = {}, __component_compmoves = {}, + __monitor_set = {}, + __monitor_list = {}, + __monitor_count = 0, __with_fragment_edges = {}, __without_fragment_edges = {}, __with_required_fragments = nil, @@ -3012,6 +3027,23 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) end end end + + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + monitor_enter(entity, monitor) + end + end + end end ---@param chunk? evolved.chunk @@ -3175,6 +3207,26 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co end end end + + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + for place = b_place, e_place do + local entity = chunk_entity_list[place] + monitor_enter(entity, monitor) + end + end + end + end end ---@param prefab evolved.entity @@ -3343,6 +3395,23 @@ function __clone_entity(prefab, entity, component_table, component_mapper) end end end + + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + monitor_enter(entity, monitor) + end + end + end end ---@param prefab evolved.entity @@ -3535,6 +3604,26 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c end end end + + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + for place = b_place, e_place do + local entity = chunk_entity_list[place] + monitor_enter(entity, monitor) + end + end + end + end end ---@param chunk evolved.chunk @@ -3760,6 +3849,23 @@ function __clear_entity_one(entity) local place = entity_places[entity_primary] if chunk then + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + monitor_leave(entity, monitor) + end + end + end + local chunk_remove_fragment_list = chunk.__remove_fragment_list if chunk_remove_fragment_list then @@ -3839,6 +3945,23 @@ function __destroy_entity_one(entity) local place = entity_places[entity_primary] if chunk then + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + monitor_leave(entity, monitor) + end + end + end + local chunk_remove_fragment_list = chunk.__remove_fragment_list if chunk_remove_fragment_list then @@ -4197,6 +4320,29 @@ function __chunk_set(old_chunk, fragment, component) end end + if old_chunk.__monitor_count > 0 then + local old_monitor_list = old_chunk.__monitor_list + local old_monitor_count = old_chunk.__monitor_count + + local new_monitor_set = new_chunk.__monitor_set + + for old_monitor_index = 1, old_monitor_count do + local monitor = old_monitor_list[old_monitor_index] + + if not new_monitor_set[monitor] then + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + for old_place = 1, old_entity_count do + local entity = old_entity_list[old_place] + monitor_leave(entity, monitor) + end + end + end + end + end + local new_component_indices = new_chunk.__component_indices local new_component_storages = new_chunk.__component_storages local new_component_reallocs = new_chunk.__component_reallocs @@ -4496,6 +4642,29 @@ function __chunk_set(old_chunk, fragment, component) end end + if new_chunk.__monitor_count > 0 then + local new_monitor_list = new_chunk.__monitor_list + local new_monitor_count = new_chunk.__monitor_count + + local old_monitor_set = old_chunk.__monitor_set + + for new_monitor_index = 1, new_monitor_count do + local monitor = new_monitor_list[new_monitor_index] + + if not old_monitor_set[monitor] then + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] + __defer_call_hook(monitor_enter, entity, monitor) + end + end + end + end + end + __structural_changes = __structural_changes + 1 end end @@ -4529,6 +4698,30 @@ function __chunk_remove(old_chunk, ...) return end + if old_chunk.__monitor_count > 0 then + local old_monitor_list = old_chunk.__monitor_list + local old_monitor_count = old_chunk.__monitor_count + + local new_monitor_set = new_chunk and new_chunk.__monitor_set + or __safe_tbls.__EMPTY_ENTITY_SET + + for old_monitor_index = 1, old_monitor_count do + local monitor = old_monitor_list[old_monitor_index] + + if not new_monitor_set[monitor] then + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + for old_place = 1, old_entity_count do + local entity = old_entity_list[old_place] + monitor_leave(entity, monitor) + end + end + end + end + end + local old_remove_fragment_list = old_chunk.__remove_fragment_list if old_remove_fragment_list then @@ -4653,6 +4846,29 @@ function __chunk_remove(old_chunk, ...) __detach_all_entities(old_chunk) end + + if new_chunk.__monitor_count > 0 then + local new_monitor_list = new_chunk.__monitor_list + local new_monitor_count = new_chunk.__monitor_count + + local old_monitor_set = old_chunk.__monitor_set + + for new_monitor_index = 1, new_monitor_count do + local monitor = new_monitor_list[new_monitor_index] + + if not old_monitor_set[monitor] then + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] + __defer_call_hook(monitor_enter, entity, monitor) + end + end + end + end + end else local entity_chunks = __entity_chunks local entity_places = __entity_places @@ -4683,6 +4899,26 @@ function __chunk_clear(chunk) return end + local chunk_monitor_count = chunk.__monitor_count + + if chunk_monitor_count > 0 then + local chunk_monitor_list = chunk.__monitor_list + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] + monitor_leave(entity, monitor) + end + end + end + end + local chunk_remove_fragment_list = chunk.__remove_fragment_list if chunk_remove_fragment_list then @@ -5925,6 +6161,26 @@ function __evolved_set(entity, fragment, component) end end + if old_chunk and old_chunk.__monitor_count > 0 then + local old_monitor_list = old_chunk.__monitor_list + local old_monitor_count = old_chunk.__monitor_count + + local new_monitor_set = new_chunk.__monitor_set + + for old_monitor_index = 1, old_monitor_count do + local monitor = old_monitor_list[old_monitor_index] + + if not new_monitor_set[monitor] then + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + monitor_leave(entity, monitor) + end + end + end + end + local new_component_indices = new_chunk.__component_indices local new_component_storages = new_chunk.__component_storages @@ -6079,6 +6335,27 @@ function __evolved_set(entity, fragment, component) end end end + + if new_chunk.__monitor_count > 0 then + local new_monitor_list = new_chunk.__monitor_list + local new_monitor_count = new_chunk.__monitor_count + + local old_monitor_set = old_chunk and old_chunk.__monitor_set + or __safe_tbls.__EMPTY_ENTITY_SET + + for new_monitor_index = 1, new_monitor_count do + local monitor = new_monitor_list[new_monitor_index] + + if not old_monitor_set[monitor] then + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + __defer_call_hook(monitor_enter, entity, monitor) + end + end + end + end end __evolved_commit() @@ -6119,6 +6396,27 @@ function __evolved_remove(entity, ...) local old_component_indices = old_chunk.__component_indices local old_component_storages = old_chunk.__component_storages + if old_chunk.__monitor_count > 0 then + local old_monitor_list = old_chunk.__monitor_list + local old_monitor_count = old_chunk.__monitor_count + + local new_monitor_set = new_chunk and new_chunk.__monitor_set + or __safe_tbls.__EMPTY_ENTITY_SET + + for old_monitor_index = 1, old_monitor_count do + local monitor = old_monitor_list[old_monitor_index] + + if not new_monitor_set[monitor] then + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) + + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + monitor_leave(entity, monitor) + end + end + end + end + local old_remove_fragment_list = old_chunk.__remove_fragment_list if old_remove_fragment_list then @@ -6188,6 +6486,26 @@ function __evolved_remove(entity, ...) new_cs[new_place] = old_cs[old_place] end + + if new_chunk.__monitor_count > 0 then + local new_monitor_list = new_chunk.__monitor_list + local new_monitor_count = new_chunk.__monitor_count + + local old_monitor_set = old_chunk.__monitor_set + + for new_monitor_index = 1, new_monitor_count do + local monitor = new_monitor_list[new_monitor_index] + + if not old_monitor_set[monitor] then + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + __defer_call_hook(monitor_enter, entity, monitor) + end + end + end + end end do From abbd358b4564f546ba4f8b93f24b0dff98374121 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 14 Oct 2026 09:44:55 +0700 Subject: [PATCH 03/48] monitors: first version --- evolved.lua | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/evolved.lua b/evolved.lua index 268fc25..e0a5fbf 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1409,6 +1409,7 @@ local __remove_child_chunk local __update_chunk_caches local __update_chunk_queries +local __update_chunk_monitors local __update_chunk_storages local __trace_major_chunks @@ -1642,6 +1643,7 @@ function __new_chunk(chunk_parent, chunk_fragment) __update_chunk_caches(chunk) __update_chunk_queries(chunk) + __update_chunk_monitors(chunk) __update_chunk_storages(chunk) return chunk @@ -1992,6 +1994,28 @@ function __update_chunk_queries(chunk) end end +---@param chunk evolved.chunk +function __update_chunk_monitors(chunk) + local chunk_monitor_set = chunk.__monitor_set + local chunk_monitor_list = chunk.__monitor_list + local chunk_monitor_count = 0 + + __lua_table_clear(chunk_monitor_set, true, false) + __lua_table_clear(chunk_monitor_list, false, true) + + for monitor_index = 1, __monitor_count do + local monitor = __monitor_list[monitor_index] + local monitor_query = __evolved_get(monitor, __QUERY) or monitor + + if __evolved_alive(monitor_query) and __query_minor_matches(chunk, monitor_query) then + chunk_monitor_count = __assoc_list_fns.insert_ex( + chunk_monitor_set, chunk_monitor_list, chunk_monitor_count, monitor) + end + end + + chunk.__monitor_count = chunk_monitor_count +end + ---@param chunk evolved.chunk function __update_chunk_storages(chunk) local entity_count = chunk.__entity_count @@ -2399,6 +2423,7 @@ end function __update_major_chunks_trace(chunk) __update_chunk_caches(chunk) __update_chunk_queries(chunk) + __update_chunk_monitors(chunk) __update_chunk_storages(chunk) end @@ -8338,6 +8363,15 @@ function __hook_fns.insert_query(query) __assoc_list_fns.insert(major_queries, query) end end + + for monitor_index = 1, __monitor_count do + local monitor = __monitor_list[monitor_index] + local monitor_query = __evolved_get(monitor, __QUERY) or monitor + + if monitor_query == query then + __hook_fns.update_monitor(monitor) + end + end end ---@param query evolved.query @@ -8374,6 +8408,86 @@ function __hook_fns.remove_query(query) __reset_query_chunks(query) end +---@param monitor evolved.system +function __hook_fns.insert_monitor(monitor) + if __defer_depth > 0 then + __defer_call_hook(__hook_fns.insert_monitor, monitor) + return + end + + if __monitor_set[monitor] or not __evolved_has_any(monitor, __ENTER, __LEAVE) then + return + end + + __monitor_count = __assoc_list_fns.insert_ex( + __monitor_set, __monitor_list, __monitor_count, monitor) + + __hook_fns.update_monitor(monitor) +end + +---@param monitor evolved.system +function __hook_fns.remove_monitor(monitor) + if __defer_depth > 0 then + __defer_call_hook(__hook_fns.remove_monitor, monitor) + return + end + + if not __monitor_set[monitor] or __evolved_has_any(monitor, __ENTER, __LEAVE) then + return + end + + __monitor_count = __assoc_list_fns.remove_ex( + __monitor_set, __monitor_list, __monitor_count, monitor) + + for _, major_chunks in __lua_next, __major_chunks do + local major_chunk_list = major_chunks.__item_list + local major_chunk_count = major_chunks.__item_count + + for major_chunk_index = 1, major_chunk_count do + local major_chunk = major_chunk_list[major_chunk_index] + + major_chunk.__monitor_count = __assoc_list_fns.remove_ex( + major_chunk.__monitor_set, major_chunk.__monitor_list, + major_chunk.__monitor_count, monitor) + end + end +end + +---@param monitor evolved.system +function __hook_fns.update_monitor(monitor) + if __defer_depth > 0 then + __defer_call_hook(__hook_fns.update_monitor, monitor) + return + end + + if not __monitor_set[monitor] then + return + end + + ---@type evolved.query + local monitor_query = __evolved_get(monitor, __QUERY) or monitor + local is_monitor_query_alive = __evolved_alive(monitor_query) + + for _, major_chunks in __lua_next, __major_chunks do + local major_chunk_list = major_chunks.__item_list + local major_chunk_count = major_chunks.__item_count + + for major_chunk_index = 1, major_chunk_count do + local major_chunk = major_chunk_list[major_chunk_index] + + if is_monitor_query_alive and __query_minor_matches(major_chunk, monitor_query) then + major_chunk.__monitor_count = __assoc_list_fns.insert_ex( + major_chunk.__monitor_set, major_chunk.__monitor_list, + major_chunk.__monitor_count, monitor) + else + major_chunk.__monitor_count = __assoc_list_fns.remove_ex( + major_chunk.__monitor_set, major_chunk.__monitor_list, + major_chunk.__monitor_count, monitor) + end + end + end +end + --- --- --- @@ -8525,6 +8639,21 @@ end) --- --- +__evolved_set(__QUERY, __ON_SET, __hook_fns.update_monitor) +__evolved_set(__QUERY, __ON_REMOVE, __hook_fns.update_monitor) + +__evolved_set(__ENTER, __ON_INSERT, __hook_fns.insert_monitor) +__evolved_set(__ENTER, __ON_REMOVE, __hook_fns.remove_monitor) + +__evolved_set(__LEAVE, __ON_INSERT, __hook_fns.insert_monitor) +__evolved_set(__LEAVE, __ON_REMOVE, __hook_fns.remove_monitor) + +--- +--- +--- +--- +--- + __evolved_set(__TAG, __NAME, '__TAG') __evolved_set(__NAME, __NAME, '__NAME') From 353f46cec398e014640df5d174987108b678baae Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 16 Oct 2026 06:14:48 +0700 Subject: [PATCH 04/48] free one top-level local variable --- evolved.lua | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/evolved.lua b/evolved.lua index e0a5fbf..adee1e2 100644 --- a/evolved.lua +++ b/evolved.lua @@ -734,29 +734,30 @@ local __table_pool_tag = { __count = 9, } ----@type table -local __table_pool_reserve = { - [__table_pool_tag.bytecode] = 16, - [__table_pool_tag.chunk_list] = 16, - [__table_pool_tag.system_list] = 16, - [__table_pool_tag.each_state] = 16, - [__table_pool_tag.execute_state] = 16, - [__table_pool_tag.entity_list] = 16, - [__table_pool_tag.fragment_set] = 16, - [__table_pool_tag.fragment_list] = 16, - [__table_pool_tag.component_table] = 16, -} - ---@class (exact) evolved.table_pool ---@field package __size integer +---@field package __reserve integer ---@field package [integer] table ---@type table local __tagged_table_pools = (function() + ---@type table + local table_pool_reserves = { + [__table_pool_tag.bytecode] = 16, + [__table_pool_tag.chunk_list] = 16, + [__table_pool_tag.system_list] = 16, + [__table_pool_tag.each_state] = 16, + [__table_pool_tag.execute_state] = 16, + [__table_pool_tag.entity_list] = 16, + [__table_pool_tag.fragment_set] = 16, + [__table_pool_tag.fragment_list] = 16, + [__table_pool_tag.component_table] = 16, + } + local table_pools = __lua_table_new(__table_pool_tag.__count) for table_pool_tag = 1, __table_pool_tag.__count do - local table_pool_reserve = __table_pool_reserve[table_pool_tag] + local table_pool_reserve = table_pool_reserves[table_pool_tag] ---@type evolved.table_pool local table_pool = __lua_table_new(table_pool_reserve) @@ -766,6 +767,7 @@ local __tagged_table_pools = (function() end table_pool.__size = table_pool_reserve + table_pool.__reserve = table_pool_reserve table_pools[table_pool_tag] = table_pool end @@ -7357,7 +7359,7 @@ function __evolved_collect_garbage(no_shrink) if not no_shrink then for table_pool_tag = 1, __table_pool_tag.__count do - local table_pool_reserve = __table_pool_reserve[table_pool_tag] + local table_pool_reserve = __tagged_table_pools[table_pool_tag].__reserve ---@type evolved.table_pool local new_table_pool = __lua_table_new(table_pool_reserve) @@ -7367,6 +7369,7 @@ function __evolved_collect_garbage(no_shrink) end new_table_pool.__size = table_pool_reserve + new_table_pool.__reserve = table_pool_reserve __tagged_table_pools[table_pool_tag] = new_table_pool end From 2ab45e57e9f7bc800db3415977d1f060427f10ce Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 16 Oct 2026 06:17:19 +0700 Subject: [PATCH 05/48] trace_all_chunks internal function --- evolved.lua | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/evolved.lua b/evolved.lua index adee1e2..ec0a52f 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1414,6 +1414,7 @@ local __update_chunk_queries local __update_chunk_monitors local __update_chunk_storages +local __trace_all_chunks local __trace_major_chunks local __trace_minor_chunks @@ -2182,6 +2183,55 @@ function __update_chunk_storages(chunk) end end +---@param trace fun(chunk: evolved.chunk, ...: any) +---@param ... any additional trace arguments +function __trace_all_chunks(trace, ...) + local chunk_stack ---@type evolved.chunk[]? + local chunk_stack_size = 0 ---@type integer + + do + local root_list = __root_list + local root_count = __root_count + + if root_count > 0 then + ---@type evolved.chunk[] + chunk_stack = __acquire_table(__table_pool_tag.chunk_list) + + __lua_table_move( + root_list, 1, root_count, + chunk_stack_size + 1, chunk_stack) + + chunk_stack_size = chunk_stack_size + root_count + end + end + + while chunk_stack_size > 0 do + ---@cast chunk_stack -? + local chunk = chunk_stack[chunk_stack_size] + + trace(chunk, ...) + + chunk_stack[chunk_stack_size] = nil + chunk_stack_size = chunk_stack_size - 1 + + do + local chunk_child_list = chunk.__child_list + local chunk_child_count = chunk.__child_count + + __lua_table_move( + chunk_child_list, 1, chunk_child_count, + chunk_stack_size + 1, chunk_stack) + + chunk_stack_size = chunk_stack_size + chunk_child_count + end + end + + if chunk_stack then + __release_table(__table_pool_tag.chunk_list, chunk_stack, + chunk_stack_size == 0, true) + end +end + ---@param major evolved.fragment ---@param trace fun(chunk: evolved.chunk, ...: any) ---@param ... any additional trace arguments From 131cecf773c31269b5e388c4e0ccea8976066c33 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 16 Oct 2026 16:41:04 +0700 Subject: [PATCH 06/48] monitors: some optimizations --- evolved.lua | 209 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 166 insertions(+), 43 deletions(-) diff --git a/evolved.lua b/evolved.lua index ec0a52f..833934f 100644 --- a/evolved.lua +++ b/evolved.lua @@ -634,6 +634,7 @@ local __monitor_count = 0 ---@type integer local __query_chunks = {} ---@type table> local __major_queries = {} ---@type table> +local __monitor_majors = {} ---@type table> local __entity_chunks = {} ---@type (evolved.chunk|false)[] local __entity_places = {} ---@type integer[] @@ -8370,6 +8371,8 @@ __evolved_set(__ON_REMOVE, __UNIQUE) __evolved_set(__GROUP, __INDEX) +__evolved_set(__QUERY, __INDEX) + --- --- --- @@ -8417,13 +8420,25 @@ function __hook_fns.insert_query(query) end end - for monitor_index = 1, __monitor_count do - local monitor = __monitor_list[monitor_index] - local monitor_query = __evolved_get(monitor, __QUERY) or monitor + if __monitor_set[query] and not __evolved_has(query, __QUERY) then + __hook_fns.attach_monitor(query) + end - if monitor_query == query then - __hook_fns.update_monitor(monitor) + if __evolved_search(__QUERY, query) then + ---@type evolved.system[] + local query_system_list = __acquire_table(__table_pool_tag.system_list) + local query_system_count = __evolved_multi_search_to(query_system_list, 1, __QUERY, query) + + for query_system_index = 1, query_system_count do + local query_system = query_system_list[query_system_index] + + if __monitor_set[query_system] then + __hook_fns.attach_monitor(query_system) + end end + + __release_table(__table_pool_tag.system_list, query_system_list, + query_system_count == 0, true) end end @@ -8458,6 +8473,27 @@ function __hook_fns.remove_query(query) end end + if __monitor_set[query] and not __evolved_has(query, __QUERY) then + __hook_fns.detach_monitor(query) + end + + if __evolved_search(__QUERY, query) then + ---@type evolved.system[] + local query_system_list = __acquire_table(__table_pool_tag.system_list) + local query_system_count = __evolved_multi_search_to(query_system_list, 1, __QUERY, query) + + for query_system_index = 1, query_system_count do + local query_system = query_system_list[query_system_index] + + if __monitor_set[query_system] then + __hook_fns.detach_monitor(query_system) + end + end + + __release_table(__table_pool_tag.system_list, query_system_list, + query_system_count == 0, true) + end + __reset_query_chunks(query) end @@ -8468,14 +8504,14 @@ function __hook_fns.insert_monitor(monitor) return end - if __monitor_set[monitor] or not __evolved_has_any(monitor, __ENTER, __LEAVE) then + if not __evolved_has_any(monitor, __ENTER, __LEAVE) then return end __monitor_count = __assoc_list_fns.insert_ex( __monitor_set, __monitor_list, __monitor_count, monitor) - __hook_fns.update_monitor(monitor) + __hook_fns.attach_monitor(monitor) end ---@param monitor evolved.system @@ -8485,31 +8521,20 @@ function __hook_fns.remove_monitor(monitor) return end - if not __monitor_set[monitor] or __evolved_has_any(monitor, __ENTER, __LEAVE) then + if not __monitor_set[monitor] then return end + __hook_fns.detach_monitor(monitor) + __monitor_count = __assoc_list_fns.remove_ex( __monitor_set, __monitor_list, __monitor_count, monitor) - - for _, major_chunks in __lua_next, __major_chunks do - local major_chunk_list = major_chunks.__item_list - local major_chunk_count = major_chunks.__item_count - - for major_chunk_index = 1, major_chunk_count do - local major_chunk = major_chunk_list[major_chunk_index] - - major_chunk.__monitor_count = __assoc_list_fns.remove_ex( - major_chunk.__monitor_set, major_chunk.__monitor_list, - major_chunk.__monitor_count, monitor) - end - end end ---@param monitor evolved.system -function __hook_fns.update_monitor(monitor) +function __hook_fns.attach_monitor(monitor) if __defer_depth > 0 then - __defer_call_hook(__hook_fns.update_monitor, monitor) + __defer_call_hook(__hook_fns.attach_monitor, monitor) return end @@ -8519,26 +8544,103 @@ function __hook_fns.update_monitor(monitor) ---@type evolved.query local monitor_query = __evolved_get(monitor, __QUERY) or monitor - local is_monitor_query_alive = __evolved_alive(monitor_query) - for _, major_chunks in __lua_next, __major_chunks do - local major_chunk_list = major_chunks.__item_list - local major_chunk_count = major_chunks.__item_count + if not __evolved_alive(monitor_query) then + return + end + + ---@type evolved.assoc_list + local monitor_majors = __assoc_list_fns.new(4) - for major_chunk_index = 1, major_chunk_count do - local major_chunk = major_chunk_list[major_chunk_index] + do + local query_includes = __sorted_includes[monitor_query] + local query_include_list = query_includes and query_includes.__item_list + local query_include_count = query_includes and query_includes.__item_count or 0 - if is_monitor_query_alive and __query_minor_matches(major_chunk, monitor_query) then - major_chunk.__monitor_count = __assoc_list_fns.insert_ex( - major_chunk.__monitor_set, major_chunk.__monitor_list, - major_chunk.__monitor_count, monitor) - else - major_chunk.__monitor_count = __assoc_list_fns.remove_ex( - major_chunk.__monitor_set, major_chunk.__monitor_list, - major_chunk.__monitor_count, monitor) + local query_variants = __sorted_variants[monitor_query] + local query_variant_list = query_variants and query_variants.__item_list + local query_variant_count = query_variants and query_variants.__item_count or 0 + + if query_include_count > 0 then + local query_major = query_include_list[query_include_count] + __assoc_list_fns.insert(monitor_majors, query_major) + end + + for query_variant_index = 1, query_variant_count do + local query_variant = query_variant_list[query_variant_index] + + if query_include_count == 0 or query_variant > query_include_list[query_include_count] then + __assoc_list_fns.insert(monitor_majors, query_variant) end end end + + __monitor_majors[monitor] = monitor_majors + + local monitor_major_list = monitor_majors.__item_list + local monitor_major_count = monitor_majors.__item_count + + if monitor_major_count == 0 then + __trace_all_chunks( + __hook_fns.insert_monitor_trace, monitor, monitor_query) + end + + for monitor_major_index = 1, monitor_major_count do + local monitor_major = monitor_major_list[monitor_major_index] + + __trace_major_chunks(monitor_major, + __hook_fns.insert_monitor_trace, monitor, monitor_query) + end +end + +---@param monitor evolved.system +function __hook_fns.detach_monitor(monitor) + if __defer_depth > 0 then + __defer_call_hook(__hook_fns.detach_monitor, monitor) + return + end + + local monitor_majors = __monitor_majors[monitor] + + if not monitor_majors then + return + end + + __monitor_majors[monitor] = nil + + local monitor_major_list = monitor_majors.__item_list + local monitor_major_count = monitor_majors.__item_count + + if monitor_major_count == 0 then + __trace_all_chunks( + __hook_fns.remove_monitor_trace, monitor) + end + + for monitor_major_index = 1, monitor_major_count do + local monitor_major = monitor_major_list[monitor_major_index] + + __trace_major_chunks(monitor_major, + __hook_fns.remove_monitor_trace, monitor) + end +end + +---@param chunk evolved.chunk +---@param monitor evolved.system +---@param monitor_query evolved.query +function __hook_fns.insert_monitor_trace(chunk, monitor, monitor_query) + if __query_minor_matches(chunk, monitor_query) then + chunk.__monitor_count = __assoc_list_fns.insert_ex( + chunk.__monitor_set, chunk.__monitor_list, + chunk.__monitor_count, monitor) + end +end + +---@param chunk evolved.chunk +---@param monitor evolved.system +function __hook_fns.remove_monitor_trace(chunk, monitor) + chunk.__monitor_count = __assoc_list_fns.remove_ex( + chunk.__monitor_set, chunk.__monitor_list, + chunk.__monitor_count, monitor) end --- @@ -8692,14 +8794,35 @@ end) --- --- -__evolved_set(__QUERY, __ON_SET, __hook_fns.update_monitor) -__evolved_set(__QUERY, __ON_REMOVE, __hook_fns.update_monitor) +__evolved_set(__QUERY, __ON_SET, function(monitor) + __hook_fns.detach_monitor(monitor) + __hook_fns.attach_monitor(monitor) +end) + +__evolved_set(__QUERY, __ON_REMOVE, function(monitor) + __hook_fns.detach_monitor(monitor) + __hook_fns.attach_monitor(monitor) +end) + +__evolved_set(__ENTER, __ON_INSERT, function(monitor) + __hook_fns.remove_monitor(monitor) + __hook_fns.insert_monitor(monitor) +end) -__evolved_set(__ENTER, __ON_INSERT, __hook_fns.insert_monitor) -__evolved_set(__ENTER, __ON_REMOVE, __hook_fns.remove_monitor) +__evolved_set(__ENTER, __ON_REMOVE, function(monitor) + __hook_fns.remove_monitor(monitor) + __hook_fns.insert_monitor(monitor) +end) -__evolved_set(__LEAVE, __ON_INSERT, __hook_fns.insert_monitor) -__evolved_set(__LEAVE, __ON_REMOVE, __hook_fns.remove_monitor) +__evolved_set(__LEAVE, __ON_INSERT, function(monitor) + __hook_fns.remove_monitor(monitor) + __hook_fns.insert_monitor(monitor) +end) + +__evolved_set(__LEAVE, __ON_REMOVE, function(monitor) + __hook_fns.remove_monitor(monitor) + __hook_fns.insert_monitor(monitor) +end) --- --- From c739c9130b1fb2c6e6484b026f60af4ff210923d Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 16 Oct 2026 23:20:18 +0700 Subject: [PATCH 07/48] huge hook/monitor order reworking --- develop/all.lua | 1 + develop/testing/main_tests.lua | 20 ++ develop/testing/monitor_tests.lua | 279 +++++++++++++++ develop/testing/realloc_tests.lua | 20 ++ evolved.lua | 540 ++++++++++++++---------------- 5 files changed, 565 insertions(+), 295 deletions(-) create mode 100644 develop/testing/monitor_tests.lua diff --git a/develop/all.lua b/develop/all.lua index ba4677b..f377594 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -10,6 +10,7 @@ require 'develop.testing.locate_tests' require 'develop.testing.lookup_tests' require 'develop.testing.main_tests' require 'develop.testing.mappers_tests' +require 'develop.testing.monitor_tests' require 'develop.testing.multi_spawn_tests' require 'develop.testing.name_tests' require 'develop.testing.process_with_tests' diff --git a/develop/testing/main_tests.lua b/develop/testing/main_tests.lua index ba33631..9b52ff0 100644 --- a/develop/testing/main_tests.lua +++ b/develop/testing/main_tests.lua @@ -7134,3 +7134,23 @@ do assert(tostring(b) == '') end end + +do + local o = '' + + local g = evo.builder():build() + local f = evo.builder() + :on_insert(function(_, _, nc) o = o .. 'i' .. nc end) + :on_assign(function(_, _, nc, oc) o = o .. 'a' .. oc .. nc end) + :build() + + local q = evo.builder():include(g):build() + + local e1 = evo.spawn({ [g] = true }) + local e2 = evo.spawn({ [g] = true, [f] = 1 }) + + o = '' + evo.batch_set(q, f, 2) + assert(o == 'a12i2') + assert(evo.get(e1, f) == 2 and evo.get(e2, f) == 2) +end diff --git a/develop/testing/monitor_tests.lua b/develop/testing/monitor_tests.lua new file mode 100644 index 0000000..5acae34 --- /dev/null +++ b/develop/testing/monitor_tests.lua @@ -0,0 +1,279 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + local o = '' + + local f = evo.builder() + :build() + + local m = evo.builder() + :include(f) + :enter(function() + o = o .. 'e' + end) + :leave(function() + o = o .. 'l' + end) + :build() + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'e') + evo.remove(e, f) + assert(o == 'el') + evo.destroy(e) + assert(o == 'el') + end + + do + o = '' + local e = evo.spawn({ [f] = true }) + assert(o == 'e') + evo.destroy(e) + assert(o == 'el') + end + + evo.destroy(m) + + do + o = '' + local e = evo.spawn({ [f] = true }) + evo.destroy(e) + assert(o == '') + end +end + +do + local o = '' + + local f = evo.builder() + :build() + + local m = evo.builder() + :include(f) + :enter(function() + o = o .. 'e' + end) + :leave(function() + o = o .. 'l' + end) + :build() + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'e') + evo.destroy(f) + assert(o == 'el') + evo.destroy(e) + assert(o == 'el') + end + + evo.destroy(m) +end + +do + local o = '' + + local f = evo.builder() + :build() + + local m = evo.builder() + :include(f) + :enter(function() + o = o .. 'e' + end) + :leave(function() + o = o .. 'l' + end) + :build() + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'e') + evo.destroy(m, e) + assert(o == 'e') + evo.destroy(f) + assert(o == 'e') + end +end + +do + local o = '' + + local f = evo.builder() + :on_insert(function() + o = o .. 'i' + end) + :on_remove(function() + o = o .. 'r' + end) + :build() + + local m = evo.builder() + :include(f) + :enter(function() + o = o .. 'e' + end) + :leave(function() + o = o .. 'l' + end) + :build() + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'ie') + evo.remove(e, f) + assert(o == 'ielr') + evo.destroy(e) + assert(o == 'ielr') + end + + do + o = '' + local e = evo.spawn({ [f] = true }) + assert(o == 'ie') + evo.destroy(e) + assert(o == 'ielr') + end + + evo.destroy(m) + + do + o = '' + local e = evo.spawn({ [f] = true }) + assert(o == 'i') + evo.destroy(e, f) + assert(o == 'ir') + end +end + +do + local o = '' + + local f = evo.builder() + :on_insert(function(e, f) + o = o .. 'i' + evo.remove(e, f) + end) + :on_remove(function() + o = o .. 'r' + end) + :build() + + local m = evo.builder() + :include(f) + :enter(function() + o = o .. 'e' + end) + :leave(function() + o = o .. 'l' + end) + :build() + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'ielr') + evo.destroy(e) + assert(o == 'ielr') + end + + do + o = '' + local e = evo.spawn({ [f] = true }) + assert(o == 'ielr') + evo.destroy(e, f) + assert(o == 'ielr') + end + + evo.destroy(m) +end + +do + local o = '' + + local r = evo.builder() + :on_insert(function() + o = o .. 'I' + end) + :on_remove(function() + o = o .. 'R' + end) + :build() + + local f = evo.builder() + :require(r) + :on_insert(function() + o = o .. 'i' + end) + :on_remove(function() + o = o .. 'r' + end) + :build() + + local m = evo.builder() + :include(r) + :enter(function() + o = o .. 'e' + end) + :leave(function() + o = o .. 'l' + end) + :build() + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'Iie') + evo.remove(e, f) + assert(o == 'Iier') + evo.destroy(e) + assert(o == 'IierlR') + end + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'Iie') + evo.remove(e, r) + assert(o == 'IielR') + evo.destroy(e) + assert(o == 'IielRr') + end + + do + o = '' + local e = evo.id() + evo.set(e, f) + assert(o == 'Iie') + evo.destroy(e) + assert(o == 'IielRr') + end + + do + o = '' + local e = evo.spawn({ [f] = true }) + assert(o == 'Iie') + evo.destroy(f) + assert(o == 'Iier') + evo.destroy(r) + assert(o == 'IierlR') + evo.destroy(e) + assert(o == 'IierlR') + end + + evo.destroy(m) +end diff --git a/develop/testing/realloc_tests.lua b/develop/testing/realloc_tests.lua index a54c6aa..c1fc0fa 100644 --- a/develop/testing/realloc_tests.lua +++ b/develop/testing/realloc_tests.lua @@ -594,3 +594,23 @@ do end end end + +do + local f1 = evo.builder():realloc(double_realloc) + :on_assign(function(_, _, nc, oc) assert(nc == 42 and oc == 21) end):build() + local f2 = evo.builder():realloc(double_realloc):compmove(double_compmove) + :on_assign(function(_, _, nc, oc) assert(nc == 42 and oc == 21) end):build() + + local q1 = evo.builder():include(f1):build() + local q2 = evo.builder():include(f2):build() + + local es, ec = evo.multi_spawn(10, { [f1] = 21, [f2] = 21 }) + + evo.batch_set(q1, f1, 42) + evo.batch_set(q2, f2, 42) + + for i = 1, ec do + assert(evo.get(es[i], f1) == 42 and evo.get(es[i], f2) == 42) + end +end + diff --git a/evolved.lua b/evolved.lua index 833934f..74d51a8 100644 --- a/evolved.lua +++ b/evolved.lua @@ -4272,6 +4272,8 @@ function __chunk_set(old_chunk, fragment, component) local old_component_indices = old_chunk.__component_indices local old_component_storages = old_chunk.__component_storages local old_component_fragments = old_chunk.__component_fragments + local old_component_reallocs = old_chunk.__component_reallocs + local old_component_compmoves = old_chunk.__component_compmoves if old_entity_count == 0 then return @@ -4297,91 +4299,86 @@ function __chunk_set(old_chunk, fragment, component) and __search_indices[fragment] or nil - if fragment_on_set or fragment_on_assign then - local old_component_index = old_component_indices[fragment] - - if old_component_index then - local old_component_storage = old_component_storages[old_component_index] - - if fragment_duplicate then - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component ~= nil then new_component = fragment_duplicate(new_component) end - if new_component == nil then new_component = true end + local old_component_index = old_component_indices[fragment] - local old_component = old_component_storage[old_place] - old_component_storage[old_place] = new_component + if old_component_index then + local old_component_storage = old_component_storages[old_component_index] + local old_component_realloc = old_component_reallocs[old_component_index] + local old_component_compmove = old_component_compmoves[old_component_index] - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component, old_component) - end + local old_component_storage2 ---@type evolved.storage? - if fragment_on_assign then - __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) - end - end + if fragment_on_set or fragment_on_assign then + if old_component_realloc then + old_component_storage2 = old_component_realloc(nil, 0, old_entity_count) else - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component == nil then new_component = true end - - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - - local old_component = old_component_storage[old_place] - old_component_storage[old_place] = new_component - - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component, old_component) - end + old_component_storage2 = __default_realloc(nil, 0, old_entity_count) + end - if fragment_on_assign then - __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) - end - end + if not old_component_storage2 then + __error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', + tostring(old_chunk), __id_name(fragment)) end - else - -- nothing - end - else - local old_component_index = old_component_indices[fragment] - if old_component_index then - local old_component_storage = old_component_storages[old_component_index] + ---@cast old_component_storage2 -? - if fragment_duplicate then + if old_component_compmove then + old_component_compmove(old_component_storage, 1, old_entity_count, 1, old_component_storage2) + elseif old_component_realloc then for old_place = 1, old_entity_count do - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component ~= nil then new_component = fragment_duplicate(new_component) end - if new_component == nil then new_component = true end - old_component_storage[old_place] = new_component + old_component_storage2[old_place] = old_component_storage[old_place] end else + __default_compmove(old_component_storage, 1, old_entity_count, 1, old_component_storage2) + end + end + + if fragment_duplicate then + for old_place = 1, old_entity_count do local new_component = component if new_component == nil then new_component = fragment_default end + if new_component ~= nil then new_component = fragment_duplicate(new_component) end if new_component == nil then new_component = true end - for old_place = 1, old_entity_count do - old_component_storage[old_place] = new_component - end + old_component_storage[old_place] = new_component end else - -- nothing + local new_component = component + if new_component == nil then new_component = fragment_default end + if new_component == nil then new_component = true end + for old_place = 1, old_entity_count do + old_component_storage[old_place] = new_component + end 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] + if fragment_search_index then __search_index_fns.multi_assign(fragment_search_index, old_entity_list, old_component_storage, 1, old_entity_count) end + + if old_component_storage2 then + for old_place = 1, old_entity_count do + local entity = old_entity_list[old_place] + + local new_component = old_component_storage[old_place] + local old_component = old_component_storage2[old_place] + + if fragment_on_set then + fragment_on_set(entity, fragment, new_component, old_component) + end + + if fragment_on_assign then + fragment_on_assign(entity, fragment, new_component, old_component) + end + end + + if old_component_realloc then + old_component_realloc(old_component_storage2, old_entity_count, 0) + else + __default_realloc(old_component_storage2, old_entity_count, 0) + end + end + else + -- nothing end else local ini_new_chunk = new_chunk @@ -4427,15 +4424,14 @@ function __chunk_set(old_chunk, fragment, component) local new_component_compmoves = new_chunk.__component_compmoves 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 + ---@type evolved.default?, evolved.duplicate? + local fragment_default, fragment_duplicate - if new_chunk_has_setup_hooks or new_chunk_has_insert_hooks then - fragment_default, fragment_duplicate, fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) + if new_chunk_has_setup_hooks then + fragment_default, fragment_duplicate = + __evolved_get(fragment, __DEFAULT, __DUPLICATE) end ---@type evolved.search_index? @@ -4508,7 +4504,7 @@ function __chunk_set(old_chunk, fragment, component) __detach_all_entities(old_chunk) end - if fragment_on_set or fragment_on_insert then + do local new_component_index = new_component_indices[fragment] if new_component_index then @@ -4516,90 +4512,25 @@ function __chunk_set(old_chunk, fragment, component) if fragment_duplicate then for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - local new_component = component if new_component == nil then new_component = fragment_default end if new_component ~= nil then new_component = fragment_duplicate(new_component) end if new_component == nil then new_component = true end - new_component_storage[new_place] = new_component - - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component) - end - - if fragment_on_insert then - __defer_call_hook(fragment_on_insert, entity, fragment, new_component) - end end else local new_component = component if new_component == nil then new_component = fragment_default end if new_component == nil then new_component = true end - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - new_component_storage[new_place] = new_component - - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component) - end - - if fragment_on_insert then - __defer_call_hook(fragment_on_insert, entity, fragment, new_component) - end - end - end - else - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment) - end - - if fragment_on_insert then - __defer_call_hook(fragment_on_insert, entity, fragment) end end - end - else - local new_component_index = new_component_indices[fragment] - - if new_component_index then - local new_component_storage = new_component_storages[new_component_index] - if fragment_duplicate then - for new_place = new_entity_count + 1, sum_entity_count do - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component ~= nil then new_component = fragment_duplicate(new_component) end - if new_component == nil then new_component = true end - new_component_storage[new_place] = new_component - end - else - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component == nil then new_component = true end - for new_place = new_entity_count + 1, sum_entity_count do - new_component_storage[new_place] = new_component - end + if fragment_search_index then + __search_index_fns.multi_insert(fragment_search_index, + new_entity_list, new_component_storage, new_entity_count + 1, sum_entity_count) end - else - -- nothing - 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 @@ -4613,12 +4544,12 @@ function __chunk_set(old_chunk, fragment, component) if ini_fragment_set[req_fragment] then -- this fragment has already been initialized else - ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.insert_hook? - local req_fragment_default, req_fragment_duplicate, req_fragment_on_set, req_fragment_on_insert + ---@type evolved.default?, evolved.duplicate? + local req_fragment_default, req_fragment_duplicate - if new_chunk_has_setup_hooks or new_chunk_has_insert_hooks then - req_fragment_default, req_fragment_duplicate, req_fragment_on_set, req_fragment_on_insert = - __evolved_get(req_fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) + if new_chunk_has_setup_hooks then + req_fragment_default, req_fragment_duplicate = + __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) end ---@type evolved.search_index? @@ -4626,94 +4557,79 @@ function __chunk_set(old_chunk, fragment, component) and __search_indices[req_fragment] or nil - if req_fragment_on_set or req_fragment_on_insert then - local req_component_index = new_component_indices[req_fragment] + local req_component_index = new_component_indices[req_fragment] - if req_component_index then - local req_component_storage = new_component_storages[req_component_index] + if req_component_index then + local req_component_storage = new_component_storages[req_component_index] - if req_fragment_duplicate then - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] + if req_fragment_duplicate then + for new_place = new_entity_count + 1, sum_entity_count do + local req_component = req_fragment_default + if req_component ~= nil then req_component = req_fragment_duplicate(req_component) end + if req_component == nil then req_component = true end + req_component_storage[new_place] = req_component + end + else + local req_component = req_fragment_default + if req_component == nil then req_component = true end + for new_place = new_entity_count + 1, sum_entity_count do + req_component_storage[new_place] = req_component + end + end - local req_component = req_fragment_default - if req_component ~= nil then req_component = req_fragment_duplicate(req_component) end - if req_component == nil then req_component = true end + if req_fragment_search_index then + __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 - req_component_storage[new_place] = req_component + local new_insert_fragment_list = new_chunk.__insert_fragment_list - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment, req_component) - end + if new_insert_fragment_list then + local old_fragment_set = old_chunk.__fragment_set - if req_fragment_on_insert then - __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) - end - end - else - local req_component = req_fragment_default - if req_component == nil then req_component = true end + for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do + local new_fragment = new_insert_fragment_list[new_insert_fragment_index] - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] + if not old_fragment_set[new_fragment] then + ---@type evolved.set_hook?, evolved.insert_hook? + local new_fragment_on_set, new_fragment_on_insert = + __evolved_get(new_fragment, __ON_SET, __ON_INSERT) - req_component_storage[new_place] = req_component + if new_fragment_on_set or new_fragment_on_insert then + local new_component_index = new_component_indices[new_fragment] - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment, req_component) - end + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] - if req_fragment_on_insert then - __defer_call_hook(req_fragment_on_insert, entity, req_fragment, req_component) - end - end - end - else for new_place = new_entity_count + 1, sum_entity_count do local entity = new_entity_list[new_place] - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment) - end + local new_component = new_component_storage[new_place] - if req_fragment_on_insert then - __defer_call_hook(req_fragment_on_insert, entity, req_fragment) + if new_fragment_on_set then + new_fragment_on_set(entity, new_fragment, new_component) end - end - end - else - local req_component_index = new_component_indices[req_fragment] - if req_component_index then - local req_component_storage = new_component_storages[req_component_index] - - if req_fragment_duplicate then - for new_place = new_entity_count + 1, sum_entity_count do - local req_component = req_fragment_default - if req_component ~= nil then req_component = req_fragment_duplicate(req_component) end - if req_component == nil then req_component = true end - req_component_storage[new_place] = req_component - end - else - local req_component = req_fragment_default - if req_component == nil then req_component = true end - for new_place = new_entity_count + 1, sum_entity_count do - req_component_storage[new_place] = req_component + if new_fragment_on_insert then + new_fragment_on_insert(entity, new_fragment, new_component) end end else - -- nothing - end - end - - if req_fragment_search_index then - local req_component_index = new_component_indices[req_fragment] + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] - if req_component_index then - local req_component_storage = new_component_storages[req_component_index] + if new_fragment_on_set then + new_fragment_on_set(entity, new_fragment) + end - __search_index_fns.multi_insert(req_fragment_search_index, - new_entity_list, req_component_storage, new_entity_count + 1, sum_entity_count) + if new_fragment_on_insert then + new_fragment_on_insert(entity, new_fragment) + end + end end end end @@ -4736,7 +4652,7 @@ function __chunk_set(old_chunk, fragment, component) if monitor_enter and not __evolved_has(monitor, __DISABLED) then for new_place = new_entity_count + 1, sum_entity_count do local entity = new_entity_list[new_place] - __defer_call_hook(monitor_enter, entity, monitor) + monitor_enter(entity, monitor) end end end @@ -4941,7 +4857,7 @@ function __chunk_remove(old_chunk, ...) if monitor_enter and not __evolved_has(monitor, __DISABLED) then for new_place = new_entity_count + 1, sum_entity_count do local entity = new_entity_list[new_place] - __defer_call_hook(monitor_enter, entity, monitor) + monitor_enter(entity, monitor) end end end @@ -6210,16 +6126,16 @@ function __evolved_set(entity, fragment, component) local old_component = old_component_storage[old_place] old_component_storage[old_place] = new_component - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component, old_component) + if fragment_search_index then + __search_index_fns.assign(fragment_search_index, entity, new_component) end - if fragment_on_assign then - __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) + if fragment_on_set then + fragment_on_set(entity, fragment, new_component, old_component) end - if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component) + if fragment_on_assign then + fragment_on_assign(entity, fragment, new_component, old_component) end else -- nothing @@ -6263,15 +6179,14 @@ function __evolved_set(entity, fragment, component) local new_component_storages = new_chunk.__component_storages 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 + ---@type evolved.default?, evolved.duplicate? + local fragment_default, fragment_duplicate - if new_chunk_has_setup_hooks or new_chunk_has_insert_hooks then - fragment_default, fragment_duplicate, fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) + if new_chunk_has_setup_hooks then + fragment_default, fragment_duplicate = + __evolved_get(fragment, __DEFAULT, __DUPLICATE) end ---@type evolved.search_index? @@ -6328,25 +6243,9 @@ function __evolved_set(entity, fragment, component) new_component_storage[new_place] = new_component - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component) - end - - 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) - end - - if fragment_on_insert then - __defer_call_hook(fragment_on_insert, entity, fragment) - end end end @@ -6360,12 +6259,12 @@ function __evolved_set(entity, fragment, component) if ini_fragment_set[req_fragment] then -- this fragment has already been initialized else - ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.insert_hook? - local req_fragment_default, req_fragment_duplicate, req_fragment_on_set, req_fragment_on_insert + ---@type evolved.default?, evolved.duplicate? + local req_fragment_default, req_fragment_duplicate - if new_chunk_has_setup_hooks or new_chunk_has_insert_hooks then - req_fragment_default, req_fragment_duplicate, req_fragment_on_set, req_fragment_on_insert = - __evolved_get(req_fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_INSERT) + if new_chunk_has_setup_hooks then + req_fragment_default, req_fragment_duplicate = + __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) end ---@type evolved.search_index? @@ -6390,24 +6289,51 @@ function __evolved_set(entity, fragment, component) req_component_storage[new_place] = req_component - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment, req_component) - end - - 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) - end + end + end + end + end + + local new_insert_fragment_list = new_chunk.__insert_fragment_list - if req_fragment_on_insert then - __defer_call_hook(req_fragment_on_insert, entity, req_fragment) + if new_insert_fragment_list then + local old_fragment_set = old_chunk and old_chunk.__fragment_set + or __safe_tbls.__EMPTY_FRAGMENT_SET + + for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do + local new_fragment = new_insert_fragment_list[new_insert_fragment_index] + + if not old_fragment_set[new_fragment] then + ---@type evolved.set_hook?, evolved.insert_hook? + local new_fragment_on_set, new_fragment_on_insert = + __evolved_get(new_fragment, __ON_SET, __ON_INSERT) + + if new_fragment_on_set or new_fragment_on_insert then + local new_component_index = new_component_indices[new_fragment] + + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] + + local new_component = new_component_storage[new_place] + + if new_fragment_on_set then + new_fragment_on_set(entity, new_fragment, new_component) + end + + if new_fragment_on_insert then + new_fragment_on_insert(entity, new_fragment, new_component) + end + else + if new_fragment_on_set then + new_fragment_on_set(entity, new_fragment) + end + + if new_fragment_on_insert then + new_fragment_on_insert(entity, new_fragment) + end end end end @@ -6429,7 +6355,7 @@ function __evolved_set(entity, fragment, component) local monitor_enter = __evolved_get(monitor, __ENTER) if monitor_enter and not __evolved_has(monitor, __DISABLED) then - __defer_call_hook(monitor_enter, entity, monitor) + monitor_enter(entity, monitor) end end end @@ -6564,26 +6490,6 @@ function __evolved_remove(entity, ...) new_cs[new_place] = old_cs[old_place] end - - if new_chunk.__monitor_count > 0 then - local new_monitor_list = new_chunk.__monitor_list - local new_monitor_count = new_chunk.__monitor_count - - local old_monitor_set = old_chunk.__monitor_set - - for new_monitor_index = 1, new_monitor_count do - local monitor = new_monitor_list[new_monitor_index] - - if not old_monitor_set[monitor] then - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - __defer_call_hook(monitor_enter, entity, monitor) - end - end - end - end end do @@ -6594,6 +6500,26 @@ function __evolved_remove(entity, ...) __structural_changes = __structural_changes + 1 end + + if new_chunk and new_chunk.__monitor_count > 0 then + local new_monitor_list = new_chunk.__monitor_list + local new_monitor_count = new_chunk.__monitor_count + + local old_monitor_set = old_chunk.__monitor_set + + for new_monitor_index = 1, new_monitor_count do + local monitor = new_monitor_list[new_monitor_index] + + if not old_monitor_set[monitor] then + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) + + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + monitor_enter(entity, monitor) + end + end + end + end end __evolved_commit() @@ -6802,28 +6728,52 @@ function __evolved_batch_set(query, fragment, component) __evolved_defer() do - local chunk_list ---@type evolved.chunk[]? - local chunk_count = 0 ---@type integer + local assign_chunk_list ---@type evolved.chunk[]? + local assign_chunk_count = 0 ---@type integer + + local insert_chunk_list ---@type evolved.chunk[]? + local insert_chunk_count = 0 ---@type integer for chunk in __evolved_execute(query) do - if not chunk_list then - ---@type evolved.chunk[] - chunk_list = __acquire_table(__table_pool_tag.chunk_list) + if chunk.__fragment_set[fragment] then + if not assign_chunk_list then + ---@type evolved.chunk[] + assign_chunk_list = __acquire_table(__table_pool_tag.chunk_list) + end + + assign_chunk_count = assign_chunk_count + 1 + assign_chunk_list[assign_chunk_count] = chunk + else + if not insert_chunk_list then + ---@type evolved.chunk[] + insert_chunk_list = __acquire_table(__table_pool_tag.chunk_list) + end + + insert_chunk_count = insert_chunk_count + 1 + insert_chunk_list[insert_chunk_count] = chunk end + end - chunk_count = chunk_count + 1 - chunk_list[chunk_count] = chunk + for assign_chunk_index = 1, assign_chunk_count do + ---@cast assign_chunk_list -? + local assign_chunk = assign_chunk_list[assign_chunk_index] + __chunk_set(assign_chunk, fragment, component) end - for chunk_index = 1, chunk_count do - ---@cast chunk_list -? - local chunk = chunk_list[chunk_index] - __chunk_set(chunk, fragment, component) + for insert_chunk_index = 1, insert_chunk_count do + ---@cast insert_chunk_list -? + local insert_chunk = insert_chunk_list[insert_chunk_index] + __chunk_set(insert_chunk, fragment, component) end - if chunk_list then - __release_table(__table_pool_tag.chunk_list, chunk_list, - chunk_count == 0, true) + if assign_chunk_list then + __release_table(__table_pool_tag.chunk_list, assign_chunk_list, + assign_chunk_count == 0, true) + end + + if insert_chunk_list then + __release_table(__table_pool_tag.chunk_list, insert_chunk_list, + insert_chunk_count == 0, true) end end From 10519fcddfb9a2bfd7dc0ae379ce4054d37847ef Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 17 Oct 2026 03:25:37 +0700 Subject: [PATCH 08/48] free up another top-level local variable --- evolved.lua | 152 ++++++++++++++++++++++++++-------------------------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/evolved.lua b/evolved.lua index 74d51a8..4c08361 100644 --- a/evolved.lua +++ b/evolved.lua @@ -589,16 +589,18 @@ end)() --- --- +local __debug_fns = {} + ---@param fmt string ---@param ... any -local function __error_fmt(fmt, ...) +function __debug_fns.error_fmt(fmt, ...) error(string.format('| evolved.lua (e) | %s', string.format(fmt, ...))) end ---@param fmt string ---@param ... any -local function __warning_fmt(fmt, ...) +function __debug_fns.warning_fmt(fmt, ...) print(__lua_debug_traceback(string.format('| evolved.lua (w) | %s', string.format(fmt, ...)))) end @@ -677,7 +679,7 @@ local function __acquire_id() local acquired_count = __acquired_count if acquired_count == 2 ^ 20 - 1 then - __error_fmt('id index overflow') + __debug_fns.error_fmt('id index overflow') end acquired_count = acquired_count + 1 @@ -704,7 +706,7 @@ local function __release_id(id) local freelist_ids = __freelist_ids if freelist_ids[acquired_primary] ~= id then - __error_fmt('the id (%d) is not acquired or already released', id) + __debug_fns.error_fmt('the id (%d) is not acquired or already released', id) end shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 @@ -1491,17 +1493,17 @@ local __defer_multi_clone_entity local __safe_tbls = { __EMPTY_ENTITY_SET = setmetatable({}, { __tostring = function() return 'empty entity set' end, - __newindex = function() __error_fmt 'attempt to modify empty entity set' end + __newindex = function() __debug_fns.error_fmt 'attempt to modify empty entity set' end }) --[[@as table]], __EMPTY_FRAGMENT_SET = setmetatable({}, { __tostring = function() return 'empty fragment set' end, - __newindex = function() __error_fmt 'attempt to modify empty fragment set' end + __newindex = function() __debug_fns.error_fmt 'attempt to modify empty fragment set' end }) --[[@as table]], __EMPTY_COMPONENT_STORAGE = setmetatable({}, { __tostring = function() return 'empty component storage' end, - __newindex = function() __error_fmt 'attempt to modify empty component storage' end + __newindex = function() __debug_fns.error_fmt 'attempt to modify empty component storage' end }) --[=[@as evolved.component[]]=], } @@ -1534,7 +1536,7 @@ function __new_chunk(chunk_parent, chunk_fragment) local chunk_fragment_primary = chunk_fragment % 2 ^ 20 if __freelist_ids[chunk_fragment_primary] ~= chunk_fragment then - __error_fmt('the fragment (%s) is not alive and cannot be used for a new chunk', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used for a new chunk', __id_name(chunk_fragment)) end @@ -1704,7 +1706,7 @@ end ---@param root evolved.chunk function __remove_root_chunk(root) if root.__parent then - __error_fmt('unexpected root chunk: (%s)', + __debug_fns.error_fmt('unexpected root chunk: (%s)', tostring(root)) return end @@ -1712,7 +1714,7 @@ function __remove_root_chunk(root) local root_index = __root_set[root.__fragment] if not root_index or __root_list[root_index] ~= root then - __error_fmt('unexpected root chunk: (%s)', + __debug_fns.error_fmt('unexpected root chunk: (%s)', tostring(root)) return end @@ -1756,7 +1758,7 @@ function __remove_child_chunk(child) local parent = child.__parent if not parent then - __error_fmt('unexpected child chunk: (%s)', + __debug_fns.error_fmt('unexpected child chunk: (%s)', tostring(child)) return end @@ -1764,7 +1766,7 @@ function __remove_child_chunk(child) local child_index = parent.__child_set[child] if not child_index or parent.__child_list[child_index] ~= child then - __error_fmt('unexpected child chunk: (%s)', + __debug_fns.error_fmt('unexpected child chunk: (%s)', tostring(child)) return end @@ -2113,7 +2115,7 @@ function __update_chunk_storages(chunk) end if not new_component_storage then - __error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', + __debug_fns.error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', tostring(chunk), __id_name(fragment)) end @@ -2146,7 +2148,7 @@ function __update_chunk_storages(chunk) end if not new_component_storage then - __error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', + __debug_fns.error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', tostring(chunk), __id_name(fragment)) end @@ -2972,7 +2974,7 @@ end ---@param component_mapper? evolved.component_mapper function __spawn_entity(chunk, entity, component_table, component_mapper) if __defer_depth <= 0 then - __error_fmt('spawn entity operations should be deferred') + __debug_fns.error_fmt('spawn entity operations should be deferred') end if not chunk or chunk.__unreachable_or_collected then @@ -3132,7 +3134,7 @@ end ---@param component_mapper? evolved.component_mapper function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, component_table, component_mapper) if __defer_depth <= 0 then - __error_fmt('spawn entity operations should be deferred') + __debug_fns.error_fmt('spawn entity operations should be deferred') end if not chunk or chunk.__unreachable_or_collected then @@ -3313,7 +3315,7 @@ end ---@param component_mapper? evolved.component_mapper function __clone_entity(prefab, entity, component_table, component_mapper) if __defer_depth <= 0 then - __error_fmt('clone entity operations should be deferred') + __debug_fns.error_fmt('clone entity operations should be deferred') end local prefab_chunk, prefab_place = __evolved_locate(prefab) @@ -3500,7 +3502,7 @@ end ---@param component_mapper? evolved.component_mapper function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, component_table, component_mapper) if __defer_depth <= 0 then - __error_fmt('clone entity operations should be deferred') + __debug_fns.error_fmt('clone entity operations should be deferred') end local prefab_chunk, prefab_place = __evolved_locate(prefab) @@ -3707,11 +3709,11 @@ end ---@param chunk evolved.chunk function __purge_chunk(chunk) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end if chunk.__child_count > 0 or chunk.__entity_count > 0 then - __error_fmt('chunk should be empty before purging') + __debug_fns.error_fmt('chunk should be empty before purging') end if chunk.__entity_capacity > 0 then @@ -3759,7 +3761,7 @@ end ---@param min_capacity integer function __expand_chunk(chunk, min_capacity) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end local entity_count = chunk.__entity_count @@ -3804,11 +3806,11 @@ function __expand_chunk(chunk, min_capacity) end if min_capacity > 0 and not new_component_storage then - __error_fmt( + __debug_fns.error_fmt( 'component storage reallocation failed: chunk (%s), fragment (%s)', tostring(chunk), __id_name(chunk.__component_fragments[component_index])) elseif min_capacity == 0 and new_component_storage then - __warning_fmt( + __debug_fns.warning_fmt( 'component storage reallocation for zero capacity should return nil: chunk (%s), fragment (%s)', tostring(chunk), __id_name(chunk.__component_fragments[component_index])) new_component_storage = nil @@ -3825,7 +3827,7 @@ end ---@param min_capacity integer function __shrink_chunk(chunk, min_capacity) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end local entity_count = chunk.__entity_count @@ -3875,11 +3877,11 @@ function __shrink_chunk(chunk, min_capacity) end if min_capacity > 0 and not new_component_storage then - __error_fmt( + __debug_fns.error_fmt( 'component storage reallocation failed: chunk (%s), fragment (%s)', tostring(chunk), __id_name(chunk.__component_fragments[component_index])) elseif min_capacity == 0 and new_component_storage then - __warning_fmt( + __debug_fns.warning_fmt( 'component storage reallocation for zero capacity should return nil: chunk (%s), fragment (%s)', tostring(chunk), __id_name(chunk.__component_fragments[component_index])) new_component_storage = nil @@ -3896,7 +3898,7 @@ end ---@param chunk_count integer function __clear_chunk_list(chunk_list, chunk_count) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end if chunk_count == 0 then @@ -3912,7 +3914,7 @@ end ---@param entity evolved.entity function __clear_entity_one(entity) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end local entity_chunks = __entity_chunks @@ -3996,7 +3998,7 @@ end ---@param entity_count integer function __clear_entity_list(entity_list, entity_count) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end for entity_index = 1, entity_count do @@ -4008,7 +4010,7 @@ end ---@param entity evolved.entity function __destroy_entity_one(entity) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end local entity_chunks = __entity_chunks @@ -4094,7 +4096,7 @@ end ---@param entity_count integer function __destroy_entity_list(entity_list, entity_count) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end for entity_index = 1, entity_count do @@ -4106,7 +4108,7 @@ end ---@param fragment evolved.fragment function __destroy_fragment_one(fragment) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end ---@type evolved.fragment[] @@ -4130,7 +4132,7 @@ end ---@param fragment_count integer function __destroy_fragment_list(fragment_list, fragment_count) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end ---@type evolved.fragment[] @@ -4157,7 +4159,7 @@ end ---@param processing_fragment_stack_size integer function __destroy_fragment_stack(processing_fragment_stack, processing_fragment_stack_size) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end ---@type table @@ -4220,7 +4222,7 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment remove_fragment_policy_fragment_count = remove_fragment_policy_fragment_count + 1 remove_fragment_policy_fragment_list[remove_fragment_policy_fragment_count] = processing_fragment else - __error_fmt('unknown DESTRUCTION_POLICY (%s) on (%s)', + __debug_fns.error_fmt('unknown DESTRUCTION_POLICY (%s) on (%s)', __id_name(processing_fragment_destruction_policy), __id_name(processing_fragment)) end end @@ -4262,7 +4264,7 @@ end ---@param component evolved.component function __chunk_set(old_chunk, fragment, component) if __defer_depth <= 0 then - __error_fmt('batched chunk operations should be deferred') + __debug_fns.error_fmt('batched chunk operations should be deferred') end local old_entity_list = old_chunk.__entity_list @@ -4316,7 +4318,7 @@ function __chunk_set(old_chunk, fragment, component) end if not old_component_storage2 then - __error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', + __debug_fns.error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', tostring(old_chunk), __id_name(fragment)) end @@ -4667,7 +4669,7 @@ end ---@param ... evolved.fragment fragments function __chunk_remove(old_chunk, ...) if __defer_depth <= 0 then - __error_fmt('batched chunk operations should be deferred') + __debug_fns.error_fmt('batched chunk operations should be deferred') end local fragment_count = __lua_select('#', ...) @@ -4883,7 +4885,7 @@ end ---@param chunk evolved.chunk function __chunk_clear(chunk) if __defer_depth <= 0 then - __error_fmt('batched chunk operations should be deferred') + __debug_fns.error_fmt('batched chunk operations should be deferred') end local chunk_entity_list = chunk.__entity_list @@ -5310,7 +5312,7 @@ function __iterator_fns.__each_iterator(each_state) local chunk_fragment_index = each_state[4] if structural_changes ~= __structural_changes then - __error_fmt('structural changes are prohibited during iteration') + __debug_fns.error_fmt('structural changes are prohibited during iteration') end local chunk_fragment_list = entity_chunk.__fragment_list @@ -5341,7 +5343,7 @@ function __iterator_fns.__execute_iterator(execute_state) local variant_set = execute_state[6] if structural_changes ~= __structural_changes then - __error_fmt('structural changes are prohibited during iteration') + __debug_fns.error_fmt('structural changes are prohibited during iteration') end while chunk_stack_size > 0 do @@ -5402,7 +5404,7 @@ local function __system_process(system, ...) local success, result = __lua_xpcall(prologue, __error_handler, ...) if not success then - __error_fmt('system prologue failed: %s', result) + __debug_fns.error_fmt('system prologue failed: %s', result) end end @@ -5413,7 +5415,7 @@ local function __system_process(system, ...) if not success then __evolved_cancel() - __error_fmt('system execution failed: %s', result) + __debug_fns.error_fmt('system execution failed: %s', result) end end __evolved_commit() @@ -5439,7 +5441,7 @@ local function __system_process(system, ...) local success, result = __lua_xpcall(epilogue, __error_handler, ...) if not success then - __error_fmt('system epilogue failed: %s', result) + __debug_fns.error_fmt('system epilogue failed: %s', result) end end end @@ -5525,11 +5527,11 @@ end ---@nodiscard function __evolved_pack(primary, secondary) if primary < 1 or primary > 2 ^ 20 - 1 then - __error_fmt('the primary (%d) is out of range [1, 2 ^ 20 - 1]', primary) + __debug_fns.error_fmt('the primary (%d) is out of range [1, 2 ^ 20 - 1]', primary) end if secondary < 1 or secondary > 2 ^ 20 - 1 then - __error_fmt('the secondary (%d) is out of range [1, 2 ^ 20 - 1]', secondary) + __debug_fns.error_fmt('the secondary (%d) is out of range [1, 2 ^ 20 - 1]', secondary) end return primary + secondary * 2 ^ 20 --[[@as evolved.id]] @@ -5560,7 +5562,7 @@ end ---@return boolean committed function __evolved_commit() if __defer_depth <= 0 then - __error_fmt('unbalanced defer/commit/cancel') + __debug_fns.error_fmt('unbalanced defer/commit/cancel') end __defer_depth = __defer_depth - 1 @@ -5592,7 +5594,7 @@ end ---@return boolean cancelled function __evolved_cancel() if __defer_depth <= 0 then - __error_fmt('unbalanced defer/commit/cancel') + __debug_fns.error_fmt('unbalanced defer/commit/cancel') end __defer_length = __defer_points[__defer_depth] @@ -5608,7 +5610,7 @@ function __evolved_spawn(component_table, component_mapper) if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -5686,7 +5688,7 @@ function __evolved_multi_spawn_to(out_entity_list, out_entity_first, if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -5723,14 +5725,14 @@ end function __evolved_clone(prefab, component_table, component_mapper) if __debug_mode then if not __evolved_alive(prefab) then - __error_fmt('the prefab (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the prefab (%s) is not alive and cannot be used', __id_name(prefab)) end if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -5805,14 +5807,14 @@ function __evolved_multi_clone_to(out_entity_list, out_entity_first, if __debug_mode then if not __evolved_alive(prefab) then - __error_fmt('the prefab (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the prefab (%s) is not alive and cannot be used', __id_name(prefab)) end if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -6064,7 +6066,7 @@ function __evolved_set(entity, fragment, component) local entity_primary = entity % 2 ^ 20 if __freelist_ids[entity_primary] ~= entity then - __error_fmt('the entity (%s) is not alive and cannot be changed', + __debug_fns.error_fmt('the entity (%s) is not alive and cannot be changed', __id_name(entity)) end @@ -6072,7 +6074,7 @@ function __evolved_set(entity, fragment, component) local fragment_primary = fragment % 2 ^ 20 if __freelist_ids[fragment_primary] ~= fragment then - __error_fmt('the fragment (%s) is not alive and cannot be set', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be set', __id_name(fragment)) end end @@ -6707,7 +6709,7 @@ function __evolved_batch_set(query, fragment, component) local query_primary = query % 2 ^ 20 if __freelist_ids[query_primary] ~= query then - __error_fmt('the query (%s) is not alive and cannot be executed', + __debug_fns.error_fmt('the query (%s) is not alive and cannot be executed', __id_name(query)) end @@ -6715,7 +6717,7 @@ function __evolved_batch_set(query, fragment, component) local fragment_primary = fragment % 2 ^ 20 if __freelist_ids[fragment_primary] ~= fragment then - __error_fmt('the fragment (%s) is not alive and cannot be set', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be set', __id_name(fragment)) end end @@ -6792,7 +6794,7 @@ function __evolved_batch_remove(query, ...) local query_primary = query % 2 ^ 20 if __freelist_ids[query_primary] ~= query then - __error_fmt('the query (%s) is not alive and cannot be executed', + __debug_fns.error_fmt('the query (%s) is not alive and cannot be executed', __id_name(query)) end @@ -6857,7 +6859,7 @@ function __evolved_batch_clear(...) local query_primary = query % 2 ^ 20 if __freelist_ids[query_primary] ~= query then - __warning_fmt('the query (%s) is not alive and cannot be executed', + __debug_fns.warning_fmt('the query (%s) is not alive and cannot be executed', __id_name(query)) else for chunk in __evolved_execute(query) do @@ -6915,7 +6917,7 @@ function __evolved_batch_destroy(...) local query_primary = query % 2 ^ 20 if __freelist_ids[query_primary] ~= query then - __warning_fmt('the query (%s) is not alive and cannot be executed', + __debug_fns.warning_fmt('the query (%s) is not alive and cannot be executed', __id_name(query)) else for chunk, entity_list, entity_count in __evolved_execute(query) do @@ -6986,7 +6988,7 @@ function __evolved_each(entity) local entity_primary = entity % 2 ^ 20 if __freelist_ids[entity_primary] ~= entity then - __error_fmt('the entity (%s) is not alive and cannot be iterated', + __debug_fns.error_fmt('the entity (%s) is not alive and cannot be iterated', __id_name(entity)) end @@ -7015,7 +7017,7 @@ function __evolved_execute(query) local query_primary = query % 2 ^ 20 if __freelist_ids[query_primary] ~= query then - __error_fmt('the query (%s) is not alive and cannot be executed', + __debug_fns.error_fmt('the query (%s) is not alive and cannot be executed', __id_name(query)) end @@ -7140,7 +7142,7 @@ function __evolved_search(fragment, component) local fragment_search_index = __search_indices[fragment] if not fragment_search_index then - __error_fmt('the fragment (%s) is not indexed and cannot be searched', + __debug_fns.error_fmt('the fragment (%s) is not indexed and cannot be searched', __id_name(fragment)) end @@ -7167,7 +7169,7 @@ 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 - __error_fmt('the fragment (%s) is not indexed and cannot be searched', + __debug_fns.error_fmt('the fragment (%s) is not indexed and cannot be searched', __id_name(fragment)) end @@ -7210,7 +7212,7 @@ function __evolved_reindex(...) local fragment = __lua_select(fragment_index, ...) if not __search_indices[fragment] then - __error_fmt('the fragment (%s) is not indexed and cannot be reindexed', + __debug_fns.error_fmt('the fragment (%s) is not indexed and cannot be reindexed', __id_name(fragment)) end end @@ -7237,7 +7239,7 @@ function __evolved_process(...) local system_primary = system % 2 ^ 20 if __freelist_ids[system_primary] ~= system then - __warning_fmt('the system (%s) is not alive and cannot be processed', + __debug_fns.warning_fmt('the system (%s) is not alive and cannot be processed', __id_name(system)) else __system_process(system) @@ -7251,7 +7253,7 @@ function __evolved_process_with(system, ...) local system_primary = system % 2 ^ 20 if __freelist_ids[system_primary] ~= system then - __error_fmt('the system (%s) is not alive and cannot be processed', + __debug_fns.error_fmt('the system (%s) is not alive and cannot be processed', __id_name(system)) end @@ -7618,7 +7620,7 @@ function __builder_mt:spawn(component_mapper) if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -7692,7 +7694,7 @@ function __builder_mt:multi_spawn_to(out_entity_list, out_entity_first, if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -7730,14 +7732,14 @@ function __builder_mt:clone(prefab, component_mapper) if __debug_mode then if not __evolved_alive(prefab) then - __error_fmt('the prefab (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the prefab (%s) is not alive and cannot be used', __id_name(prefab)) end if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -7807,14 +7809,14 @@ function __builder_mt:multi_clone_to(out_entity_list, out_entity_first, if __debug_mode then if not __evolved_alive(prefab) then - __error_fmt('the prefab (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the prefab (%s) is not alive and cannot be used', __id_name(prefab)) end if component_table then for fragment in __lua_next, component_table do if not __evolved_alive(fragment) then - __error_fmt('the fragment (%s) is not alive and cannot be used', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used', __id_name(fragment)) end end @@ -7926,7 +7928,7 @@ function __builder_mt:set(fragment, component) if __debug_mode then if __freelist_ids[fragment_primary] ~= fragment then - __error_fmt('the fragment (%s) is not alive and cannot be set', + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be set', __id_name(fragment)) end end From 6ab56478c7219da968d3f4c352aed1ff93f19b49 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 17 Oct 2026 04:04:10 +0700 Subject: [PATCH 09/48] split chunk_set to chunk_assign/insert --- evolved.lua | 602 +++++++++++++++++++++++++++------------------------- 1 file changed, 311 insertions(+), 291 deletions(-) diff --git a/evolved.lua b/evolved.lua index 4c08361..bdeaa70 100644 --- a/evolved.lua +++ b/evolved.lua @@ -733,8 +733,9 @@ local __table_pool_tag = { entity_list = 6, fragment_set = 7, fragment_list = 8, - component_table = 9, - __count = 9, + component_list = 9, + component_table = 10, + __count = 10, } ---@class (exact) evolved.table_pool @@ -754,6 +755,7 @@ local __tagged_table_pools = (function() [__table_pool_tag.entity_list] = 16, [__table_pool_tag.fragment_set] = 16, [__table_pool_tag.fragment_list] = 16, + [__table_pool_tag.component_list] = 16, [__table_pool_tag.component_table] = 16, } @@ -1472,7 +1474,8 @@ local __destroy_fragment_one local __destroy_fragment_list local __destroy_fragment_stack -local __chunk_set +local __chunk_assign +local __chunk_insert local __chunk_remove local __chunk_clear @@ -4259,410 +4262,427 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment true, false) end ----@param old_chunk evolved.chunk +---@param chunk evolved.chunk ---@param fragment evolved.fragment ---@param component evolved.component -function __chunk_set(old_chunk, fragment, component) +function __chunk_assign(chunk, fragment, component) if __defer_depth <= 0 then __debug_fns.error_fmt('batched chunk operations should be deferred') end - local old_entity_list = old_chunk.__entity_list - local old_entity_count = old_chunk.__entity_count + local chunk_entity_list = chunk.__entity_list + local chunk_entity_count = chunk.__entity_count - local old_component_count = old_chunk.__component_count - local old_component_indices = old_chunk.__component_indices - local old_component_storages = old_chunk.__component_storages - local old_component_fragments = old_chunk.__component_fragments - local old_component_reallocs = old_chunk.__component_reallocs - local old_component_compmoves = old_chunk.__component_compmoves + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages + local chunk_component_reallocs = chunk.__component_reallocs + local chunk_component_compmoves = chunk.__component_compmoves - if old_entity_count == 0 then + if chunk_entity_count == 0 then return end - local new_chunk = __chunk_with_fragment(old_chunk, fragment) + local component_index = chunk_component_indices[fragment] - 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 + if not component_index then + return + end - ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.assign_hook? - local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign + local chunk_has_setup_hooks = chunk.__has_setup_hooks + local chunk_has_assign_hooks = chunk.__has_assign_hooks + local chunk_has_index_fragments = chunk.__has_index_fragments - if old_chunk_has_setup_hooks or old_chunk_has_assign_hooks then - fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign = - __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) - end + ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.assign_hook? + local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign - ---@type evolved.search_index? - local fragment_search_index = old_chunk_has_index_fragments - and __search_indices[fragment] - or nil + if chunk_has_setup_hooks or chunk_has_assign_hooks then + fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign = + __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) + end - local old_component_index = old_component_indices[fragment] + ---@type evolved.search_index? + local fragment_search_index = chunk_has_index_fragments + and __search_indices[fragment] + or nil - if old_component_index then - local old_component_storage = old_component_storages[old_component_index] - local old_component_realloc = old_component_reallocs[old_component_index] - local old_component_compmove = old_component_compmoves[old_component_index] + local component_storage = chunk_component_storages[component_index] + local component_realloc = chunk_component_reallocs[component_index] + local component_compmove = chunk_component_compmoves[component_index] - local old_component_storage2 ---@type evolved.storage? + local old_component_storage_for_hooks ---@type evolved.storage? - if fragment_on_set or fragment_on_assign then - if old_component_realloc then - old_component_storage2 = old_component_realloc(nil, 0, old_entity_count) - else - old_component_storage2 = __default_realloc(nil, 0, old_entity_count) - end + if fragment_on_set or fragment_on_assign then + if component_realloc then + old_component_storage_for_hooks = component_realloc(nil, 0, chunk_entity_count) + else + old_component_storage_for_hooks = __acquire_table(__table_pool_tag.component_list) + end - if not old_component_storage2 then - __debug_fns.error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', - tostring(old_chunk), __id_name(fragment)) - end + if not old_component_storage_for_hooks then + __debug_fns.error_fmt('component storage allocation failed: chunk (%s), fragment (%s)', + tostring(chunk), __id_name(fragment)) + end - ---@cast old_component_storage2 -? + ---@cast old_component_storage_for_hooks -? - if old_component_compmove then - old_component_compmove(old_component_storage, 1, old_entity_count, 1, old_component_storage2) - elseif old_component_realloc then - for old_place = 1, old_entity_count do - old_component_storage2[old_place] = old_component_storage[old_place] - end - else - __default_compmove(old_component_storage, 1, old_entity_count, 1, old_component_storage2) - end - end - - if fragment_duplicate then - for old_place = 1, old_entity_count do - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component ~= nil then new_component = fragment_duplicate(new_component) end - if new_component == nil then new_component = true end - old_component_storage[old_place] = new_component - end - else - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component == nil then new_component = true end - for old_place = 1, old_entity_count do - old_component_storage[old_place] = new_component - end + if component_compmove then + component_compmove(component_storage, 1, chunk_entity_count, 1, old_component_storage_for_hooks) + elseif not component_realloc then + __default_compmove(component_storage, 1, chunk_entity_count, 1, old_component_storage_for_hooks) + else + for place = 1, chunk_entity_count do + old_component_storage_for_hooks[place] = component_storage[place] end + end + end - if fragment_search_index then - __search_index_fns.multi_assign(fragment_search_index, - old_entity_list, old_component_storage, 1, old_entity_count) - end + if fragment_duplicate then + for place = 1, chunk_entity_count do + local new_component = component + if new_component == nil then new_component = fragment_default end + if new_component ~= nil then new_component = fragment_duplicate(new_component) end + if new_component == nil then new_component = true end + component_storage[place] = new_component + end + else + local new_component = component + if new_component == nil then new_component = fragment_default end + if new_component == nil then new_component = true end + for place = 1, chunk_entity_count do + component_storage[place] = new_component + end + end - if old_component_storage2 then - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] + if fragment_search_index then + __search_index_fns.multi_assign(fragment_search_index, + chunk_entity_list, component_storage, 1, chunk_entity_count) + end - local new_component = old_component_storage[old_place] - local old_component = old_component_storage2[old_place] + if old_component_storage_for_hooks then + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component, old_component) - end + local new_component = component_storage[place] + local old_component = old_component_storage_for_hooks[place] - if fragment_on_assign then - fragment_on_assign(entity, fragment, new_component, old_component) - end - end + if fragment_on_set then + fragment_on_set(entity, fragment, new_component, old_component) + end - if old_component_realloc then - old_component_realloc(old_component_storage2, old_entity_count, 0) - else - __default_realloc(old_component_storage2, old_entity_count, 0) - end + if fragment_on_assign then + fragment_on_assign(entity, fragment, new_component, old_component) end + end + + if component_realloc then + component_realloc(old_component_storage_for_hooks, chunk_entity_count, 0) else - -- nothing + __release_table(__table_pool_tag.component_list, old_component_storage_for_hooks, + chunk_entity_count == 0, true) end - else - local ini_new_chunk = new_chunk - local ini_fragment_set = ini_new_chunk.__fragment_set + end +end - while new_chunk and new_chunk.__has_required_fragments do - local required_chunk = new_chunk.__with_required_fragments +---@param old_chunk evolved.chunk +---@param fragment evolved.fragment +---@param component evolved.component +function __chunk_insert(old_chunk, fragment, component) + if __defer_depth <= 0 then + __debug_fns.error_fmt('batched chunk operations should be deferred') + end - if required_chunk and not required_chunk.__unreachable_or_collected then - new_chunk = required_chunk - else - required_chunk = __chunk_requires(new_chunk) - new_chunk.__with_required_fragments, new_chunk = required_chunk, required_chunk - end + local old_entity_list = old_chunk.__entity_list + local old_entity_count = old_chunk.__entity_count + + local old_component_count = old_chunk.__component_count + local old_component_storages = old_chunk.__component_storages + local old_component_fragments = old_chunk.__component_fragments + + if old_entity_count == 0 then + return + end + + local new_chunk = __chunk_with_fragment(old_chunk, fragment) + + local ini_new_chunk = new_chunk + local ini_fragment_set = ini_new_chunk.__fragment_set + + while new_chunk and new_chunk.__has_required_fragments do + local required_chunk = new_chunk.__with_required_fragments + + if required_chunk and not required_chunk.__unreachable_or_collected then + new_chunk = required_chunk + else + required_chunk = __chunk_requires(new_chunk) + new_chunk.__with_required_fragments, new_chunk = required_chunk, required_chunk end + end - if old_chunk.__monitor_count > 0 then - local old_monitor_list = old_chunk.__monitor_list - local old_monitor_count = old_chunk.__monitor_count + if old_chunk.__monitor_count > 0 then + local old_monitor_list = old_chunk.__monitor_list + local old_monitor_count = old_chunk.__monitor_count - local new_monitor_set = new_chunk.__monitor_set + local new_monitor_set = new_chunk.__monitor_set - for old_monitor_index = 1, old_monitor_count do - local monitor = old_monitor_list[old_monitor_index] + for old_monitor_index = 1, old_monitor_count do + local monitor = old_monitor_list[old_monitor_index] - if not new_monitor_set[monitor] then - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) + if not new_monitor_set[monitor] then + ---@type evolved.leave? + local monitor_leave = __evolved_get(monitor, __LEAVE) - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - monitor_leave(entity, monitor) - end + if monitor_leave and not __evolved_has(monitor, __DISABLED) then + for old_place = 1, old_entity_count do + local entity = old_entity_list[old_place] + monitor_leave(entity, monitor) end end end end + end - local new_component_indices = new_chunk.__component_indices - local new_component_storages = new_chunk.__component_storages - local new_component_reallocs = new_chunk.__component_reallocs - local new_component_compmoves = new_chunk.__component_compmoves - - local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks - local new_chunk_has_index_fragments = new_chunk.__has_index_fragments - - ---@type evolved.default?, evolved.duplicate? - local fragment_default, fragment_duplicate + local new_component_indices = new_chunk.__component_indices + local new_component_storages = new_chunk.__component_storages + local new_component_reallocs = new_chunk.__component_reallocs + local new_component_compmoves = new_chunk.__component_compmoves - if new_chunk_has_setup_hooks then - fragment_default, fragment_duplicate = - __evolved_get(fragment, __DEFAULT, __DUPLICATE) - end + local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks + local new_chunk_has_index_fragments = new_chunk.__has_index_fragments - ---@type evolved.search_index? - local fragment_search_index = new_chunk_has_index_fragments - and __search_indices[fragment] - or nil + ---@type evolved.default?, evolved.duplicate? + local fragment_default, fragment_duplicate - local sum_entity_count = old_entity_count + new_chunk.__entity_count + if new_chunk_has_setup_hooks then + fragment_default, fragment_duplicate = + __evolved_get(fragment, __DEFAULT, __DUPLICATE) + end - if sum_entity_count > new_chunk.__entity_capacity then - __expand_chunk(new_chunk, sum_entity_count) - end + ---@type evolved.search_index? + local fragment_search_index = new_chunk_has_index_fragments + and __search_indices[fragment] + or nil - local new_entity_list = new_chunk.__entity_list - local new_entity_count = new_chunk.__entity_count + local sum_entity_count = old_entity_count + new_chunk.__entity_count - do - for old_ci = 1, old_component_count do - local old_f = old_component_fragments[old_ci] - local old_cs = old_component_storages[old_ci] + if sum_entity_count > new_chunk.__entity_capacity then + __expand_chunk(new_chunk, sum_entity_count) + end - local new_ci = new_component_indices[old_f] - local new_cs = new_component_storages[new_ci] - local new_cr = new_component_reallocs[new_ci] - local new_cm = new_component_compmoves[new_ci] + local new_entity_list = new_chunk.__entity_list + local new_entity_count = new_chunk.__entity_count - if new_cm then - new_cm(old_cs, 1, old_entity_count, new_entity_count + 1, new_cs) - elseif new_cr then - for old_place = 1, old_entity_count do - local new_place = new_entity_count + old_place - new_cs[new_place] = old_cs[old_place] - end + do + for old_ci = 1, old_component_count do + local old_f = old_component_fragments[old_ci] + local old_cs = old_component_storages[old_ci] + + local new_ci = new_component_indices[old_f] + local new_cs = new_component_storages[new_ci] + local new_cr = new_component_reallocs[new_ci] + local new_cm = new_component_compmoves[new_ci] + + if new_cm then + new_cm(old_cs, 1, old_entity_count, new_entity_count + 1, new_cs) + elseif new_cr then + for old_place = 1, old_entity_count do + local new_place = new_entity_count + old_place + new_cs[new_place] = old_cs[old_place] + end + else + if new_entity_count > 0 then + __default_compmove(old_cs, 1, old_entity_count, new_entity_count + 1, new_cs) else - if new_entity_count > 0 then - __default_compmove(old_cs, 1, old_entity_count, new_entity_count + 1, new_cs) - else - old_component_storages[old_ci], new_component_storages[new_ci] = - new_component_storages[new_ci], old_component_storages[old_ci] - end + old_component_storages[old_ci], new_component_storages[new_ci] = + new_component_storages[new_ci], old_component_storages[old_ci] end end + end - if new_entity_count > 0 then - __lua_table_move( - old_entity_list, 1, old_entity_count, - new_entity_count + 1, new_entity_list) - else - old_chunk.__entity_list, new_chunk.__entity_list = - new_entity_list, old_entity_list - - old_entity_list, new_entity_list = - new_entity_list, old_entity_list - end + if new_entity_count > 0 then + __lua_table_move( + old_entity_list, 1, old_entity_count, + new_entity_count + 1, new_entity_list) + else + old_chunk.__entity_list, new_chunk.__entity_list = + new_entity_list, old_entity_list - new_chunk.__entity_count = sum_entity_count + old_entity_list, new_entity_list = + new_entity_list, old_entity_list end - do - local entity_chunks = __entity_chunks - local entity_places = __entity_places + new_chunk.__entity_count = sum_entity_count + end - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - local entity_primary = entity % 2 ^ 20 - entity_chunks[entity_primary] = new_chunk - entity_places[entity_primary] = new_place - end + do + local entity_chunks = __entity_chunks + local entity_places = __entity_places - __detach_all_entities(old_chunk) + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] + local entity_primary = entity % 2 ^ 20 + entity_chunks[entity_primary] = new_chunk + entity_places[entity_primary] = new_place end - do - local new_component_index = new_component_indices[fragment] + __detach_all_entities(old_chunk) + end - if new_component_index then - local new_component_storage = new_component_storages[new_component_index] + do + local new_component_index = new_component_indices[fragment] - if fragment_duplicate then - for new_place = new_entity_count + 1, sum_entity_count do - local new_component = component - if new_component == nil then new_component = fragment_default end - if new_component ~= nil then new_component = fragment_duplicate(new_component) end - if new_component == nil then new_component = true end - new_component_storage[new_place] = new_component - end - else + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] + + if fragment_duplicate then + for new_place = new_entity_count + 1, sum_entity_count do local new_component = component if new_component == nil then new_component = fragment_default end + if new_component ~= nil then new_component = fragment_duplicate(new_component) end if new_component == nil then new_component = true end - for new_place = new_entity_count + 1, sum_entity_count do - new_component_storage[new_place] = new_component - end + new_component_storage[new_place] = new_component end - - if fragment_search_index then - __search_index_fns.multi_insert(fragment_search_index, - new_entity_list, new_component_storage, new_entity_count + 1, sum_entity_count) + else + local new_component = component + if new_component == nil then new_component = fragment_default end + if new_component == nil then new_component = true end + for new_place = new_entity_count + 1, sum_entity_count do + new_component_storage[new_place] = new_component end end + + if fragment_search_index then + __search_index_fns.multi_insert(fragment_search_index, + new_entity_list, new_component_storage, new_entity_count + 1, sum_entity_count) + end 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 + if ini_new_chunk.__has_required_fragments then + local req_fragment_list = new_chunk.__fragment_list + local req_fragment_count = new_chunk.__fragment_count - for req_fragment_index = 1, req_fragment_count do - local req_fragment = req_fragment_list[req_fragment_index] + for req_fragment_index = 1, req_fragment_count do + local req_fragment = req_fragment_list[req_fragment_index] - if ini_fragment_set[req_fragment] then - -- this fragment has already been initialized - else - ---@type evolved.default?, evolved.duplicate? - local req_fragment_default, req_fragment_duplicate + if ini_fragment_set[req_fragment] then + -- this fragment has already been initialized + else + ---@type evolved.default?, evolved.duplicate? + local req_fragment_default, req_fragment_duplicate - if new_chunk_has_setup_hooks then - req_fragment_default, req_fragment_duplicate = - __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) - end + if new_chunk_has_setup_hooks then + req_fragment_default, req_fragment_duplicate = + __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) + end - ---@type evolved.search_index? - local req_fragment_search_index = new_chunk_has_index_fragments - and __search_indices[req_fragment] - or nil + ---@type evolved.search_index? + 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] + local req_component_index = new_component_indices[req_fragment] - if req_component_index then - local req_component_storage = new_component_storages[req_component_index] + if req_component_index then + local req_component_storage = new_component_storages[req_component_index] - if req_fragment_duplicate then - for new_place = new_entity_count + 1, sum_entity_count do - local req_component = req_fragment_default - if req_component ~= nil then req_component = req_fragment_duplicate(req_component) end - if req_component == nil then req_component = true end - req_component_storage[new_place] = req_component - end - else + if req_fragment_duplicate then + for new_place = new_entity_count + 1, sum_entity_count do local req_component = req_fragment_default + if req_component ~= nil then req_component = req_fragment_duplicate(req_component) end if req_component == nil then req_component = true end - for new_place = new_entity_count + 1, sum_entity_count do - req_component_storage[new_place] = req_component - end + req_component_storage[new_place] = req_component end - - if req_fragment_search_index then - __search_index_fns.multi_insert(req_fragment_search_index, - new_entity_list, req_component_storage, new_entity_count + 1, sum_entity_count) + else + local req_component = req_fragment_default + if req_component == nil then req_component = true end + for new_place = new_entity_count + 1, sum_entity_count do + req_component_storage[new_place] = req_component end end + + if req_fragment_search_index then + __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 - local new_insert_fragment_list = new_chunk.__insert_fragment_list + local new_insert_fragment_list = new_chunk.__insert_fragment_list - if new_insert_fragment_list then - local old_fragment_set = old_chunk.__fragment_set + if new_insert_fragment_list then + local old_fragment_set = old_chunk.__fragment_set - for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do - local new_fragment = new_insert_fragment_list[new_insert_fragment_index] + for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do + local new_fragment = new_insert_fragment_list[new_insert_fragment_index] - if not old_fragment_set[new_fragment] then - ---@type evolved.set_hook?, evolved.insert_hook? - local new_fragment_on_set, new_fragment_on_insert = - __evolved_get(new_fragment, __ON_SET, __ON_INSERT) + if not old_fragment_set[new_fragment] then + ---@type evolved.set_hook?, evolved.insert_hook? + local new_fragment_on_set, new_fragment_on_insert = + __evolved_get(new_fragment, __ON_SET, __ON_INSERT) - if new_fragment_on_set or new_fragment_on_insert then - local new_component_index = new_component_indices[new_fragment] + if new_fragment_on_set or new_fragment_on_insert then + local new_component_index = new_component_indices[new_fragment] - if new_component_index then - local new_component_storage = new_component_storages[new_component_index] + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] - local new_component = new_component_storage[new_place] + local new_component = new_component_storage[new_place] - if new_fragment_on_set then - new_fragment_on_set(entity, new_fragment, new_component) - end + if new_fragment_on_set then + new_fragment_on_set(entity, new_fragment, new_component) + end - if new_fragment_on_insert then - new_fragment_on_insert(entity, new_fragment, new_component) - end + if new_fragment_on_insert then + new_fragment_on_insert(entity, new_fragment, new_component) end - else - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] + end + else + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] - if new_fragment_on_set then - new_fragment_on_set(entity, new_fragment) - end + if new_fragment_on_set then + new_fragment_on_set(entity, new_fragment) + end - if new_fragment_on_insert then - new_fragment_on_insert(entity, new_fragment) - end + if new_fragment_on_insert then + new_fragment_on_insert(entity, new_fragment) end end end end end end + end - if new_chunk.__monitor_count > 0 then - local new_monitor_list = new_chunk.__monitor_list - local new_monitor_count = new_chunk.__monitor_count + if new_chunk.__monitor_count > 0 then + local new_monitor_list = new_chunk.__monitor_list + local new_monitor_count = new_chunk.__monitor_count - local old_monitor_set = old_chunk.__monitor_set + local old_monitor_set = old_chunk.__monitor_set - for new_monitor_index = 1, new_monitor_count do - local monitor = new_monitor_list[new_monitor_index] + for new_monitor_index = 1, new_monitor_count do + local monitor = new_monitor_list[new_monitor_index] - if not old_monitor_set[monitor] then - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) + if not old_monitor_set[monitor] then + ---@type evolved.enter? + local monitor_enter = __evolved_get(monitor, __ENTER) - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - monitor_enter(entity, monitor) - end + if monitor_enter and not __evolved_has(monitor, __DISABLED) then + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] + monitor_enter(entity, monitor) end end end end - - __structural_changes = __structural_changes + 1 end + + __structural_changes = __structural_changes + 1 end ---@param old_chunk evolved.chunk @@ -6759,13 +6779,13 @@ function __evolved_batch_set(query, fragment, component) for assign_chunk_index = 1, assign_chunk_count do ---@cast assign_chunk_list -? local assign_chunk = assign_chunk_list[assign_chunk_index] - __chunk_set(assign_chunk, fragment, component) + __chunk_assign(assign_chunk, fragment, component) end for insert_chunk_index = 1, insert_chunk_count do ---@cast insert_chunk_list -? local insert_chunk = insert_chunk_list[insert_chunk_index] - __chunk_set(insert_chunk, fragment, component) + __chunk_insert(insert_chunk, fragment, component) end if assign_chunk_list then From 99369fbc1bb6d5f6f89c3fa6eab97cc0a39ba5c1 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 02:33:32 +0700 Subject: [PATCH 10/48] from monitors to observers wip --- README.md | 42 ++- develop/all.lua | 2 + develop/fuzzing/monitor_fuzz.lua | 400 ++++++++++++++++++++++ develop/testing/monitor_tests.lua | 371 ++++++++++++++++++-- evolved.d.tl | 14 +- evolved.lua | 539 +++++++++++++----------------- 6 files changed, 1013 insertions(+), 355 deletions(-) create mode 100644 develop/fuzzing/monitor_fuzz.lua diff --git a/README.md b/README.md index 4466017..0a94f49 100644 --- a/README.md +++ b/README.md @@ -1587,6 +1587,7 @@ entity :: id fragment :: id query :: id system :: id +event :: id component :: any storage :: component[] @@ -1602,8 +1603,7 @@ compmove :: {storage, integer, integer, integer, storage} execute :: {chunk, entity[], integer, any...} -enter :: {entity, system} -leave :: {entity, system} +callback :: {event, entity, any...} prologue :: {any...} epilogue :: {any...} @@ -1655,8 +1655,11 @@ GROUP :: fragment QUERY :: fragment EXECUTE :: fragment -ENTER :: fragment -LEAVE :: fragment +OBSERVES :: fragment +CALLBACK :: fragment + +ENTER :: event +LEAVE :: event PROLOGUE :: fragment EPILOGUE :: fragment @@ -1729,6 +1732,8 @@ multi_search_to :: entity[], integer, fragment, component -> integer reindex :: fragment... -> () +emit :: event, entity, any... -> () + process :: system... -> () process_with :: system, ... -> () @@ -1818,8 +1823,8 @@ builder_mt:group :: system -> builder builder_mt:query :: query -> builder builder_mt:execute :: {chunk, entity[], integer, any...} -> builder -builder_mt:enter :: {entity, system} -> builder -builder_mt:leave :: {entity, system} -> builder +builder_mt:observe :: event... -> builder +builder_mt:callback :: {event, entity, any...} -> builder builder_mt:prologue :: {any...} -> builder builder_mt:epilogue :: {any...} -> builder @@ -1962,6 +1967,10 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EXECUTE` +### `evolved.OBSERVES` + +### `evolved.CALLBACK` + ### `evolved.ENTER` ### `evolved.LEAVE` @@ -2387,6 +2396,15 @@ function evolved.multi_search_to(out_entity_list, out_entity_first, fragment, co function evolved.reindex(...) end ``` +### `evolved.emit` + +```lua +---@param event evolved.event +---@param entity evolved.entity +---@param ... any event payload +function evolved.emit(event, entity, ...) end +``` + ### `evolved.process` ```lua @@ -2878,20 +2896,20 @@ function evolved.builder_mt:query(query) end function evolved.builder_mt:execute(execute) end ``` -#### `evolved.builder_mt:enter` +#### `evolved.builder_mt:observe` ```lua ----@param enter evolved.enter +---@param ... evolved.event events ---@return evolved.builder builder -function evolved.builder_mt:enter(enter) end +function evolved.builder_mt:observe(...) end ``` -#### `evolved.builder_mt:leave` +#### `evolved.builder_mt:callback` ```lua ----@param leave evolved.leave +---@param callback evolved.callback ---@return evolved.builder builder -function evolved.builder_mt:leave(leave) end +function evolved.builder_mt:callback(callback) end ``` #### `evolved.builder_mt:prologue` diff --git a/develop/all.lua b/develop/all.lua index f377594..112f670 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -39,6 +39,8 @@ basics.describe_fuzz 'develop.fuzzing.batch_destroy_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.explicit_fuzz' print '----------------------------------------' +basics.describe_fuzz 'develop.fuzzing.monitor_fuzz' +print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.pack_unpack_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.requires_fuzz' diff --git a/develop/fuzzing/monitor_fuzz.lua b/develop/fuzzing/monitor_fuzz.lua new file mode 100644 index 0000000..4004c57 --- /dev/null +++ b/develop/fuzzing/monitor_fuzz.lua @@ -0,0 +1,400 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +--- +--- +--- +--- +--- + +local __table_unpack = (function() + ---@diagnostic disable-next-line: deprecated + return table.unpack or unpack +end)() + +--- +--- +--- +--- +--- + +-- entities created before and during monitor setup can leave monitors without +-- entering them first, because monitors do not emit ENTER for existing entities +local setup_entity_set = {} ---@type table + +local HIT = evo.id() +setup_entity_set[HIT] = true + +local alive_fragment_list = {} ---@type evolved.fragment[] + +for i = 1, math.random(2, 8) do + local fragment = evo.id() + alive_fragment_list[i] = fragment + setup_entity_set[fragment] = true +end + +for _, fragment in ipairs(alive_fragment_list) do + if math.random(1, 4) == 1 then + local require = alive_fragment_list[math.random(1, #alive_fragment_list)] + + if require ~= fragment then + evo.set(fragment, evo.REQUIRES, { require }) + end + end + + if math.random(1, 4) == 1 then + evo.set(fragment, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + end +end + +---@return evolved.fragment +local function random_fragment() + return alive_fragment_list[math.random(1, #alive_fragment_list)] +end + +---@param min_count integer +---@return evolved.fragment[] +local function random_fragment_list(min_count) + local fragment_set = {} ---@type table + local fragment_list = {} ---@type evolved.fragment[] + + for _ = 1, math.random(min_count, #alive_fragment_list) do + local fragment = random_fragment() + + if not fragment_set[fragment] then + fragment_set[fragment] = true + fragment_list[#fragment_list + 1] = fragment + end + end + + return fragment_list +end + +---@return evolved.component_table +local function random_component_table() + local component_table = {} ---@type evolved.component_table + + for _, fragment in ipairs(random_fragment_list(0)) do + component_table[fragment] = true + end + + return component_table +end + +---@param query evolved.query +---@param with_includes boolean +local function generate_query(query, with_includes) + local include_list = random_fragment_list(with_includes and 1 or 0) + local exclude_list = random_fragment_list(0) + local variant_list = random_fragment_list(0) + + if #include_list > 0 and (with_includes or math.random(1, 4) ~= 1) then + evo.set(query, evo.INCLUDES, include_list) + end + + if #exclude_list > 0 and math.random(1, 2) == 1 then + evo.set(query, evo.EXCLUDES, exclude_list) + end + + if #variant_list > 0 and math.random(1, 3) == 1 then + evo.set(query, evo.VARIANTS, variant_list) + end +end + +--- +--- +--- +--- +--- + +local monitor_list = {} ---@type evolved.system[] +local monitor_inside_sets = {} ---@type table> +local monitor_observe_hits = {} ---@type table + +---@type table> +local monitor_received_hits = {} + +for i = 1, math.random(1, 6) do + local monitor = evo.id() + setup_entity_set[monitor] = true + + local inside_set = {} ---@type table + local received_hits = {} ---@type table + + monitor_list[i] = monitor + monitor_inside_sets[monitor] = inside_set + monitor_received_hits[monitor] = received_hits + monitor_observe_hits[monitor] = math.random(1, 3) ~= 1 + + if math.random(1, 3) == 1 then + local query = evo.id() + setup_entity_set[query] = true + generate_query(query, false) + evo.set(monitor, evo.QUERY, query) + else + generate_query(monitor, false) + end + + if monitor_observe_hits[monitor] then + evo.set(monitor, evo.OBSERVES, { evo.ENTER, evo.LEAVE, HIT }) + else + evo.set(monitor, evo.OBSERVES, { evo.ENTER, evo.LEAVE }) + end + + evo.set(monitor, evo.CALLBACK, function(event, entity, payload) + if setup_entity_set[entity] then + assert(event == evo.ENTER or event == evo.LEAVE) + elseif event == evo.ENTER then + assert(not inside_set[entity]) + inside_set[entity] = true + elseif event == evo.LEAVE then + assert(inside_set[entity]) + inside_set[entity] = nil + elseif event == HIT then + assert(monitor_observe_hits[monitor]) + assert(inside_set[entity]) + assert(payload == entity) + received_hits[entity] = (received_hits[entity] or 0) + 1 + else + assert(false, 'unexpected event') + end + end) +end + +--- +--- +--- +--- +--- + +local tracked_entity_set = {} ---@type table +local tracked_entity_list = {} ---@type evolved.entity[] + +---@param entity evolved.entity +local function track_entity(entity) + if not tracked_entity_set[entity] then + tracked_entity_set[entity] = true + tracked_entity_list[#tracked_entity_list + 1] = entity + end +end + +---@return evolved.entity? +local function random_alive_entity() + local alive_entity_list = {} ---@type evolved.entity[] + + for _, entity in ipairs(tracked_entity_list) do + if evo.alive(entity) then + alive_entity_list[#alive_entity_list + 1] = entity + end + end + + if #alive_entity_list > 0 then + return alive_entity_list[math.random(1, #alive_entity_list)] + end +end + +local function check_monitors() + assert(evo.depth() == 0) + + for _, monitor in ipairs(monitor_list) do + local inside_set = monitor_inside_sets[monitor] + local expected_set = {} ---@type table + + for _, entity_list, entity_count in evo.execute(evo.get(monitor, evo.QUERY) or monitor) do + for entity_index = 1, entity_count do + local entity = entity_list[entity_index] + + if tracked_entity_set[entity] then + expected_set[entity] = true + end + end + end + + for entity in pairs(expected_set) do + assert(inside_set[entity]) + end + + for entity in pairs(inside_set) do + assert(evo.alive(entity)) + + if tracked_entity_set[entity] then + assert(expected_set[entity]) + end + end + end +end + +--- +--- +--- +--- +--- + +---@param no_destroy boolean +---@return boolean destroyed +local function random_operation(no_destroy) + local operation = math.random(1, no_destroy and 3 or 15) + + if operation == 1 then + track_entity(evo.spawn(random_component_table())) + elseif operation == 2 then + local entity_list = evo.multi_spawn(math.random(1, 4), random_component_table()) + + for _, entity in ipairs(entity_list) do + track_entity(entity) + end + elseif operation == 3 then + local entity = random_alive_entity() + + if entity then + evo.emit(HIT, entity, entity) + end + elseif operation == 4 then + local prefab = random_alive_entity() + + if prefab then + track_entity(evo.clone(prefab, random_component_table())) + end + elseif operation == 5 then + local prefab = random_alive_entity() + + if prefab then + local entity_list = evo.multi_clone(math.random(1, 4), prefab, random_component_table()) + + for _, entity in ipairs(entity_list) do + track_entity(entity) + end + end + elseif operation == 6 or operation == 7 then + local entity = random_alive_entity() + + if entity then + evo.set(entity, random_fragment()) + end + elseif operation == 8 then + local entity = random_alive_entity() + + if entity then + evo.remove(entity, __table_unpack(random_fragment_list(1))) + end + elseif operation == 9 then + local query = evo.id() + generate_query(query, true) + evo.batch_set(query, random_fragment()) + evo.destroy(query) + elseif operation == 10 then + local query = evo.id() + generate_query(query, true) + evo.batch_remove(query, __table_unpack(random_fragment_list(1))) + evo.destroy(query) + elseif operation == 11 then + local entity = random_alive_entity() + + if entity then + evo.clear(entity) + end + elseif operation == 12 then + local query = evo.id() + generate_query(query, true) + evo.batch_clear(query) + evo.destroy(query) + elseif operation == 13 then + local entity = random_alive_entity() + + if entity then + evo.destroy(entity) + return true + end + elseif operation == 14 then + local query = evo.id() + generate_query(query, true) + evo.batch_destroy(query) + evo.destroy(query) + return true + elseif operation == 15 then + if #alive_fragment_list > 1 then + local fragment_index = math.random(1, #alive_fragment_list) + local fragment = table.remove(alive_fragment_list, fragment_index) + + for _, other_fragment in ipairs(alive_fragment_list) do + local require_list = evo.get(other_fragment, evo.REQUIRES) + + if require_list and require_list[1] == fragment then + evo.remove(other_fragment, evo.REQUIRES) + end + end + + evo.destroy(fragment) + return true + end + end + + return false +end + +for _ = 1, math.random(20, 80) do + if math.random(1, 4) == 1 then + evo.defer() + + local destroyed = false + + for _ = 1, math.random(1, 10) do + if random_operation(destroyed) then + destroyed = true + end + end + + evo.commit() + else + random_operation(false) + end + + if math.random(1, 10) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) + end + + check_monitors() + + do + local entity = random_alive_entity() + + if entity then + for _, monitor in ipairs(monitor_list) do + monitor_received_hits[monitor][entity] = nil + end + + evo.emit(HIT, entity, entity) + + for _, monitor in ipairs(monitor_list) do + local expected_hits = monitor_observe_hits[monitor] + and monitor_inside_sets[monitor][entity] and 1 or nil + + assert(monitor_received_hits[monitor][entity] == expected_hits) + end + end + end +end + +--- +--- +--- +--- +--- + +evo.destroy(__table_unpack(tracked_entity_list)) + +check_monitors() + +for _, monitor in ipairs(monitor_list) do + for entity in pairs(monitor_inside_sets[monitor]) do + assert(not tracked_entity_set[entity]) + end +end + +evo.destroy(__table_unpack(monitor_list)) +evo.destroy(__table_unpack(alive_fragment_list)) + +if math.random(1, 2) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) +end diff --git a/develop/testing/monitor_tests.lua b/develop/testing/monitor_tests.lua index 5acae34..d14506d 100644 --- a/develop/testing/monitor_tests.lua +++ b/develop/testing/monitor_tests.lua @@ -10,11 +10,13 @@ do local m = evo.builder() :include(f) - :enter(function() - o = o .. 'e' - end) - :leave(function() - o = o .. 'l' + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + if ev == evo.ENTER then + o = o .. 'e' + elseif ev == evo.LEAVE then + o = o .. 'l' + end end) :build() @@ -55,11 +57,13 @@ do local m = evo.builder() :include(f) - :enter(function() - o = o .. 'e' - end) - :leave(function() - o = o .. 'l' + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + if ev == evo.ENTER then + o = o .. 'e' + elseif ev == evo.LEAVE then + o = o .. 'l' + end end) :build() @@ -85,11 +89,13 @@ do local m = evo.builder() :include(f) - :enter(function() - o = o .. 'e' - end) - :leave(function() - o = o .. 'l' + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + if ev == evo.ENTER then + o = o .. 'e' + elseif ev == evo.LEAVE then + o = o .. 'l' + end end) :build() @@ -119,11 +125,13 @@ do local m = evo.builder() :include(f) - :enter(function() - o = o .. 'e' - end) - :leave(function() - o = o .. 'l' + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + if ev == evo.ENTER then + o = o .. 'e' + elseif ev == evo.LEAVE then + o = o .. 'l' + end end) :build() @@ -172,11 +180,13 @@ do local m = evo.builder() :include(f) - :enter(function() - o = o .. 'e' - end) - :leave(function() - o = o .. 'l' + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + if ev == evo.ENTER then + o = o .. 'e' + elseif ev == evo.LEAVE then + o = o .. 'l' + end end) :build() @@ -224,11 +234,13 @@ do local m = evo.builder() :include(r) - :enter(function() - o = o .. 'e' - end) - :leave(function() - o = o .. 'l' + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + if ev == evo.ENTER then + o = o .. 'e' + elseif ev == evo.LEAVE then + o = o .. 'l' + end end) :build() @@ -277,3 +289,302 @@ do evo.destroy(m) end + +do + local o = '' + + local f1, f2 = evo.id(2) + local hit, heal = evo.id(2) + + local m = evo.builder() + :include(f1) + :observe(hit) + :callback(function(ev, e, a, b) + assert(ev == hit) + assert(evo.has(e, f1)) + o = o .. 'h' .. tostring(a) .. tostring(b) + end) + :build() + + do + o = '' + local e = evo.spawn({ [f1] = true }) + assert(o == '') + evo.emit(hit, e, 1, 2) + assert(o == 'h12') + evo.emit(heal, e, 3, 4) + assert(o == 'h12') + evo.emit(hit, e) + assert(o == 'h12hnilnil') + evo.emit(hit, e, nil, 5) + assert(o == 'h12hnilnilhnil5') + evo.destroy(e) + assert(o == 'h12hnilnilhnil5') + end + + do + o = '' + local e = evo.spawn({ [f2] = true }) + evo.emit(hit, e, 1, 2) + assert(o == '') + evo.set(e, f1) + evo.emit(hit, e, 1, 2) + assert(o == 'h12') + evo.remove(e, f1) + evo.emit(hit, e, 1, 2) + assert(o == 'h12') + end + + do + o = '' + local e = evo.id() + evo.emit(hit, e, 1, 2) + assert(o == '') + evo.destroy(e) + evo.emit(hit, e, 1, 2) + assert(o == '') + end + + evo.destroy(m) + + do + o = '' + local e = evo.spawn({ [f1] = true }) + evo.emit(hit, e, 1, 2) + assert(o == '') + end +end + +do + local o = '' + + local f = evo.id() + local hit, heal = evo.id(2) + + local m1 = evo.builder() + :include(f) + :observe(hit) + :callback(function(ev) + o = o .. (ev == hit and 'h' or '?') + end) + :build() + + local m2 = evo.builder() + :include(f) + :observe(heal, evo.ENTER) + :callback(function(ev) + o = o .. (ev == heal and 'H' or ev == evo.ENTER and 'E' or '?') + end) + :build() + + do + o = '' + local e = evo.spawn({ [f] = true }) + assert(o == 'E') + evo.emit(hit, e) + assert(o == 'Eh') + evo.emit(heal, e) + assert(o == 'EhH') + evo.destroy(e) + assert(o == 'EhH') + end + + evo.destroy(m1, m2) +end + +do + local o = '' + + local f = evo.id() + local hit = evo.id() + + local m = evo.builder() + :include(f) + :observe(hit) + :callback(function() + o = o .. 'h' + end) + :build() + + local e = evo.spawn({ [f] = true }) + + do + o = '' + evo.set(m, evo.DISABLED) + evo.emit(hit, e) + assert(o == '') + evo.remove(m, evo.DISABLED) + evo.emit(hit, e) + assert(o == 'h') + end + + do + o = '' + evo.remove(m, evo.CALLBACK) + evo.emit(hit, e) + assert(o == '') + evo.set(m, evo.CALLBACK, function() o = o .. 'H' end) + evo.emit(hit, e) + assert(o == 'H') + end + + do + o = '' + evo.remove(m, evo.OBSERVES) + evo.emit(hit, e) + assert(o == '') + evo.set(m, evo.OBSERVES, { hit }) + evo.emit(hit, e) + assert(o == 'H') + evo.set(m, evo.OBSERVES, {}) + evo.emit(hit, e) + assert(o == 'H') + evo.set(m, evo.OBSERVES, { evo.ENTER, hit }) + evo.emit(hit, e) + assert(o == 'HH') + end + + evo.destroy(m) +end + +do + local o = '' + + local f1, f2 = evo.id(2) + local hit = evo.id() + + local m = evo.builder() + :include(f2) + :observe(hit, evo.LEAVE) + :callback(function(ev, e, a) + if ev == hit then + o = o .. 'h' .. a + evo.remove(e, f2) + assert(evo.has(e, f2)) + elseif ev == evo.LEAVE then + o = o .. 'l' + end + end) + :build() + + do + o = '' + local e = evo.spawn({ [f1] = true }) + + evo.defer() + evo.set(e, f2) + evo.emit(hit, e, 1) + assert(o == '') + evo.commit() + + assert(o == 'h1l') + assert(not evo.has(e, f2)) + end + + do + o = '' + local e = evo.spawn({ [f1] = true, [f2] = true }) + + evo.defer() + evo.emit(hit, e, 1) + evo.destroy(e) + evo.emit(hit, e, 2) + assert(o == '') + evo.commit() + + assert(o == 'h1l') + assert(not evo.alive(e)) + end + + evo.destroy(m) +end + +do + local o = '' + + local f, g = evo.id(2) + local hit = evo.id() + + local q = evo.builder() + :include(f) + :build() + + local m = evo.builder() + :include(g) + :query(q) + :observe(hit) + :callback(function(_, _, a) + o = o .. a + end) + :build() + + do + o = '' + local e = evo.spawn({ [f] = true }) + evo.emit(hit, e, 'a') + assert(o == 'a') + evo.remove(m, evo.QUERY) + evo.emit(hit, e, 'b') + assert(o == 'a') + evo.set(m, evo.QUERY, q) + evo.emit(hit, e, 'c') + assert(o == 'ac') + end + + evo.destroy(m, q) +end + +do + local o = '' + + local f1, f2 = evo.id(2) + + local m1 = evo.builder() + :include(f1) + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + o = o .. (ev == evo.ENTER and 'e' or 'l') + end) + :build() + + local m2 = evo.builder() + :include(f1, f2) + :observe(evo.ENTER, evo.LEAVE) + :callback(function(ev) + o = o .. (ev == evo.ENTER and 'E' or 'L') + end) + :build() + + local q1 = evo.builder():include(f1):build() + local q2 = evo.builder():include(f2):build() + + do + o = '' + local es = evo.multi_spawn(3, { [f1] = true }) + assert(#es == 3 and o == 'eee') + evo.batch_set(q1, f2) + assert(o == 'eeeEEE') + evo.batch_remove(q2, f2) + assert(o == 'eeeEEELLL') + evo.batch_set(q1, f2) + assert(o == 'eeeEEELLLEEE') + evo.batch_remove(q1, f1) + assert(o == 'eeeEEELLLEEElllLLL') + evo.batch_set(q2, f1) + assert(o == 'eeeEEELLLEEElllLLLeeeEEE') + evo.batch_destroy(q2) + assert(o == 'eeeEEELLLEEElllLLLeeeEEElllLLL') + end + + do + o = '' + local es = evo.multi_clone(2, evo.builder():set(f1):set(f2):build()) + assert(#es == 2 and o == 'eEeeEE') + evo.destroy(f2) + assert(o == 'eEeeEELLL') + evo.destroy(f1) + assert(o == 'eEeeEELLLlll') + end + + evo.destroy(m1, m2, q1, q2) +end diff --git a/evolved.d.tl b/evolved.d.tl index 6153aa9..bf29a46 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -5,6 +5,7 @@ type Fragment = Id type Query = Id type System = Id + type Event = Id interface EachState end interface ExecuteState end @@ -136,8 +137,8 @@ query: function(self: Builder, query: Query): Builder execute: function(self: Builder, execute: function(Chunk, {Entity}, integer, ...: any)): Builder - enter: function(self: Builder, enter: function(Entity, System)): Builder - leave: function(self: Builder, leave: function(Entity, System)): Builder + observe: function(self: Builder, ...: Event): Builder + callback: function(self: Builder, callback: function(Event, Entity, ...: any)): Builder prologue: function(self: Builder, prologue: function(...: any)): Builder epilogue: function(self: Builder, epilogue: function(...: any)): Builder @@ -177,8 +178,11 @@ QUERY: Fragment EXECUTE: Fragment - ENTER: Fragment - LEAVE: Fragment + OBSERVES: Fragment + CALLBACK: Fragment + + ENTER: Event + LEAVE: Event PROLOGUE: Fragment EPILOGUE: Fragment @@ -287,6 +291,8 @@ reindex: function(...: Fragment) + emit: function(event: Event, entity: Entity, ...: any) + process: function(...: System) process_with: function(system: System, ...: any) diff --git a/evolved.lua b/evolved.lua index bdeaa70..e2ccbce 100644 --- a/evolved.lua +++ b/evolved.lua @@ -33,6 +33,7 @@ local evolved = { ---@alias evolved.fragment evolved.id ---@alias evolved.query evolved.id ---@alias evolved.system evolved.id +---@alias evolved.event evolved.id ---@alias evolved.component any ---@alias evolved.storage evolved.component[] @@ -52,8 +53,10 @@ local evolved = { --- entity_count: integer, --- ...: any) ----@alias evolved.enter fun(entity: evolved.entity, system: evolved.system) ----@alias evolved.leave fun(entity: evolved.entity, system: evolved.system) +---@alias evolved.callback fun( +--- event: evolved.event, +--- entity: evolved.entity, +--- ...: any) ---@alias evolved.prologue fun(...: any) ---@alias evolved.epilogue fun(...: any) @@ -630,9 +633,7 @@ local __root_count = 0 ---@type integer local __major_chunks = {} ---@type table> local __minor_chunks = {} ---@type table> -local __monitor_set = {} ---@type table -local __monitor_list = {} ---@type evolved.system[] -local __monitor_count = 0 ---@type integer +local __monitors = { __item_set = {}, __item_list = {}, __item_count = 0 } ---@type evolved.assoc_list local __query_chunks = {} ---@type table> local __major_queries = {} ---@type table> @@ -647,6 +648,7 @@ local __sorted_includes = {} ---@type table> local __sorted_variants = {} ---@type table> local __sorted_requires = {} ---@type table> +local __sorted_observes = {} ---@type table> local __structural_changes = 0 ---@type integer @@ -1311,6 +1313,9 @@ local __GROUP = __acquire_id() local __QUERY = __acquire_id() local __EXECUTE = __acquire_id() +local __OBSERVES = __acquire_id() +local __CALLBACK = __acquire_id() + local __ENTER = __acquire_id() local __LEAVE = __acquire_id() @@ -1389,6 +1394,8 @@ local __evolved_multi_search_to local __evolved_reindex +local __evolved_emit + local __evolved_process local __evolved_process_with @@ -1481,11 +1488,9 @@ local __chunk_clear local __defer_call_hook -local __defer_spawn_entity -local __defer_multi_spawn_entity +local __defer_fns = {} -local __defer_clone_entity -local __defer_multi_clone_entity +local __event_fns = {} --- --- @@ -2012,8 +2017,11 @@ function __update_chunk_monitors(chunk) __lua_table_clear(chunk_monitor_set, true, false) __lua_table_clear(chunk_monitor_list, false, true) - for monitor_index = 1, __monitor_count do - local monitor = __monitor_list[monitor_index] + local monitor_list = __monitors.__item_list + local monitor_count = __monitors.__item_count + + for monitor_index = 1, monitor_count do + local monitor = monitor_list[monitor_index] local monitor_query = __evolved_get(monitor, __QUERY) or monitor if __evolved_alive(monitor_query) and __query_minor_matches(chunk, monitor_query) then @@ -2971,6 +2979,72 @@ function __detach_all_entities(chunk) chunk.__entity_count = 0 end +---@param event evolved.event +---@param chunk evolved.chunk +---@param skip_monitor_set? table +---@param entity evolved.entity +---@param ... any event payload +function __event_fns.emit_one(event, chunk, skip_monitor_set, entity, ...) + if __defer_depth <= 0 then + __debug_fns.error_fmt('event emitting should be deferred') + end + + local chunk_monitor_list = chunk.__monitor_list + local chunk_monitor_count = chunk.__monitor_count + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + if not skip_monitor_set or not skip_monitor_set[monitor] then + local monitor_observes = __sorted_observes[monitor] + + ---@type evolved.callback? + local monitor_callback = monitor_observes and monitor_observes.__item_set[event] + and __evolved_get(monitor, __CALLBACK) + + if monitor_callback and not __evolved_has(monitor, __DISABLED) then + monitor_callback(event, entity, ...) + end + end + end +end + +---@param event evolved.event +---@param chunk evolved.chunk +---@param skip_monitor_set? table +---@param entity_list evolved.entity[] +---@param entity_first integer +---@param entity_count integer +---@param ... any event payload +function __event_fns.emit_list(event, chunk, skip_monitor_set, entity_list, entity_first, entity_count, ...) + if __defer_depth <= 0 then + __debug_fns.error_fmt('event emitting should be deferred') + end + + local chunk_monitor_list = chunk.__monitor_list + local chunk_monitor_count = chunk.__monitor_count + + local entity_last = entity_first + entity_count - 1 + + for chunk_monitor_index = 1, chunk_monitor_count do + local monitor = chunk_monitor_list[chunk_monitor_index] + + if not skip_monitor_set or not skip_monitor_set[monitor] then + local monitor_observes = __sorted_observes[monitor] + + ---@type evolved.callback? + local monitor_callback = monitor_observes and monitor_observes.__item_set[event] + and __evolved_get(monitor, __CALLBACK) + + if monitor_callback and not __evolved_has(monitor, __DISABLED) then + for entity_index = entity_first, entity_last do + monitor_callback(event, entity_list[entity_index], ...) + end + end + end + end +end + ---@param chunk? evolved.chunk ---@param entity evolved.entity ---@param component_table? evolved.component_table @@ -3111,21 +3185,8 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) end end - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - monitor_enter(entity, monitor) - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_one(__ENTER, chunk, nil, entity) end end @@ -3291,24 +3352,9 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co end end - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - for place = b_place, e_place do - local entity = chunk_entity_list[place] - monitor_enter(entity, monitor) - end - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_list(__ENTER, chunk, nil, + chunk_entity_list, b_place, entity_count) end end @@ -3479,21 +3525,8 @@ function __clone_entity(prefab, entity, component_table, component_mapper) end end - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - monitor_enter(entity, monitor) - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_one(__ENTER, chunk, nil, entity) end end @@ -3688,24 +3721,9 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c end end - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - for place = b_place, e_place do - local entity = chunk_entity_list[place] - monitor_enter(entity, monitor) - end - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_list(__ENTER, chunk, nil, + chunk_entity_list, b_place, entity_count) end end @@ -3932,21 +3950,8 @@ function __clear_entity_one(entity) local place = entity_places[entity_primary] if chunk then - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - monitor_leave(entity, monitor) - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_one(__LEAVE, chunk, nil, entity) end local chunk_remove_fragment_list = chunk.__remove_fragment_list @@ -4028,21 +4033,8 @@ function __destroy_entity_one(entity) local place = entity_places[entity_primary] if chunk then - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - monitor_leave(entity, monitor) - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_one(__LEAVE, chunk, nil, entity) end local chunk_remove_fragment_list = chunk.__remove_fragment_list @@ -4419,26 +4411,8 @@ function __chunk_insert(old_chunk, fragment, component) end if old_chunk.__monitor_count > 0 then - local old_monitor_list = old_chunk.__monitor_list - local old_monitor_count = old_chunk.__monitor_count - - local new_monitor_set = new_chunk.__monitor_set - - for old_monitor_index = 1, old_monitor_count do - local monitor = old_monitor_list[old_monitor_index] - - if not new_monitor_set[monitor] then - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - monitor_leave(entity, monitor) - end - end - end - end + __event_fns.emit_list(__LEAVE, old_chunk, new_chunk.__monitor_set, + old_entity_list, 1, old_entity_count) end local new_component_indices = new_chunk.__component_indices @@ -4660,26 +4634,8 @@ function __chunk_insert(old_chunk, fragment, component) end if new_chunk.__monitor_count > 0 then - local new_monitor_list = new_chunk.__monitor_list - local new_monitor_count = new_chunk.__monitor_count - - local old_monitor_set = old_chunk.__monitor_set - - for new_monitor_index = 1, new_monitor_count do - local monitor = new_monitor_list[new_monitor_index] - - if not old_monitor_set[monitor] then - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - monitor_enter(entity, monitor) - end - end - end - end + __event_fns.emit_list(__ENTER, new_chunk, old_chunk.__monitor_set, + new_entity_list, new_entity_count + 1, old_entity_count) end __structural_changes = __structural_changes + 1 @@ -4715,27 +4671,8 @@ function __chunk_remove(old_chunk, ...) end if old_chunk.__monitor_count > 0 then - local old_monitor_list = old_chunk.__monitor_list - local old_monitor_count = old_chunk.__monitor_count - - local new_monitor_set = new_chunk and new_chunk.__monitor_set - or __safe_tbls.__EMPTY_ENTITY_SET - - for old_monitor_index = 1, old_monitor_count do - local monitor = old_monitor_list[old_monitor_index] - - if not new_monitor_set[monitor] then - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - for old_place = 1, old_entity_count do - local entity = old_entity_list[old_place] - monitor_leave(entity, monitor) - end - end - end - end + __event_fns.emit_list(__LEAVE, old_chunk, new_chunk and new_chunk.__monitor_set, + old_entity_list, 1, old_entity_count) end local old_remove_fragment_list = old_chunk.__remove_fragment_list @@ -4864,26 +4801,8 @@ function __chunk_remove(old_chunk, ...) end if new_chunk.__monitor_count > 0 then - local new_monitor_list = new_chunk.__monitor_list - local new_monitor_count = new_chunk.__monitor_count - - local old_monitor_set = old_chunk.__monitor_set - - for new_monitor_index = 1, new_monitor_count do - local monitor = new_monitor_list[new_monitor_index] - - if not old_monitor_set[monitor] then - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] - monitor_enter(entity, monitor) - end - end - end - end + __event_fns.emit_list(__ENTER, new_chunk, old_chunk.__monitor_set, + new_entity_list, new_entity_count + 1, old_entity_count) end else local entity_chunks = __entity_chunks @@ -4915,24 +4834,9 @@ function __chunk_clear(chunk) return end - local chunk_monitor_count = chunk.__monitor_count - - if chunk_monitor_count > 0 then - local chunk_monitor_list = chunk.__monitor_list - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - for place = 1, chunk_entity_count do - local entity = chunk_entity_list[place] - monitor_leave(entity, monitor) - end - end - end + if chunk.__monitor_count > 0 then + __event_fns.emit_list(__LEAVE, chunk, nil, + chunk_entity_list, 1, chunk_entity_count) end local chunk_remove_fragment_list = chunk.__remove_fragment_list @@ -5096,7 +5000,7 @@ end ---@param entity evolved.entity ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_spawn_entity(chunk, entity, component_table, component_mapper) +function __defer_fns.spawn_entity(chunk, entity, component_table, component_mapper) ---@type evolved.component_table? local component_table2 @@ -5145,7 +5049,7 @@ end ---@param entity_count integer ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_multi_spawn_entity(chunk, entity_list, entity_first, entity_count, component_table, component_mapper) +function __defer_fns.multi_spawn_entity(chunk, entity_list, entity_first, entity_count, component_table, component_mapper) ---@type evolved.entity[] local entity_list2 = __acquire_table(__table_pool_tag.entity_list) @@ -5207,7 +5111,7 @@ end ---@param entity evolved.entity ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_clone_entity(prefab, entity, component_table, component_mapper) +function __defer_fns.clone_entity(prefab, entity, component_table, component_mapper) ---@type evolved.component_table? local component_table2 @@ -5256,7 +5160,7 @@ end ---@param entity_count integer ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_multi_clone_entity(prefab, entity_list, entity_first, entity_count, component_table, component_mapper) +function __defer_fns.multi_clone_entity(prefab, entity_list, entity_first, entity_count, component_table, component_mapper) ---@type evolved.entity[] local entity_list2 = __acquire_table(__table_pool_tag.entity_list) @@ -5644,7 +5548,7 @@ function __evolved_spawn(component_table, component_mapper) end if __defer_depth > 0 then - __defer_spawn_entity(nil, entity, component_table, component_mapper) + __defer_fns.spawn_entity(nil, entity, component_table, component_mapper) else __evolved_defer() do @@ -5724,7 +5628,7 @@ function __evolved_multi_spawn_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_multi_spawn_entity(nil, + __defer_fns.multi_spawn_entity(nil, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -5762,7 +5666,7 @@ function __evolved_clone(prefab, component_table, component_mapper) local entity = __acquire_id() if __defer_depth > 0 then - __defer_clone_entity(prefab, entity, component_table, component_mapper) + __defer_fns.clone_entity(prefab, entity, component_table, component_mapper) else __evolved_defer() do @@ -5846,7 +5750,7 @@ function __evolved_multi_clone_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_multi_clone_entity(prefab, + __defer_fns.multi_clone_entity(prefab, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -6178,23 +6082,7 @@ function __evolved_set(entity, fragment, component) end if old_chunk and old_chunk.__monitor_count > 0 then - local old_monitor_list = old_chunk.__monitor_list - local old_monitor_count = old_chunk.__monitor_count - - local new_monitor_set = new_chunk.__monitor_set - - for old_monitor_index = 1, old_monitor_count do - local monitor = old_monitor_list[old_monitor_index] - - if not new_monitor_set[monitor] then - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - monitor_leave(entity, monitor) - end - end - end + __event_fns.emit_one(__LEAVE, old_chunk, new_chunk.__monitor_set, entity) end local new_component_indices = new_chunk.__component_indices @@ -6363,24 +6251,7 @@ function __evolved_set(entity, fragment, component) end if new_chunk.__monitor_count > 0 then - local new_monitor_list = new_chunk.__monitor_list - local new_monitor_count = new_chunk.__monitor_count - - local old_monitor_set = old_chunk and old_chunk.__monitor_set - or __safe_tbls.__EMPTY_ENTITY_SET - - for new_monitor_index = 1, new_monitor_count do - local monitor = new_monitor_list[new_monitor_index] - - if not old_monitor_set[monitor] then - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - monitor_enter(entity, monitor) - end - end - end + __event_fns.emit_one(__ENTER, new_chunk, old_chunk and old_chunk.__monitor_set, entity) end end @@ -6423,24 +6294,7 @@ function __evolved_remove(entity, ...) local old_component_storages = old_chunk.__component_storages if old_chunk.__monitor_count > 0 then - local old_monitor_list = old_chunk.__monitor_list - local old_monitor_count = old_chunk.__monitor_count - - local new_monitor_set = new_chunk and new_chunk.__monitor_set - or __safe_tbls.__EMPTY_ENTITY_SET - - for old_monitor_index = 1, old_monitor_count do - local monitor = old_monitor_list[old_monitor_index] - - if not new_monitor_set[monitor] then - ---@type evolved.leave? - local monitor_leave = __evolved_get(monitor, __LEAVE) - - if monitor_leave and not __evolved_has(monitor, __DISABLED) then - monitor_leave(entity, monitor) - end - end - end + __event_fns.emit_one(__LEAVE, old_chunk, new_chunk and new_chunk.__monitor_set, entity) end local old_remove_fragment_list = old_chunk.__remove_fragment_list @@ -6524,23 +6378,7 @@ function __evolved_remove(entity, ...) end if new_chunk and new_chunk.__monitor_count > 0 then - local new_monitor_list = new_chunk.__monitor_list - local new_monitor_count = new_chunk.__monitor_count - - local old_monitor_set = old_chunk.__monitor_set - - for new_monitor_index = 1, new_monitor_count do - local monitor = new_monitor_list[new_monitor_index] - - if not old_monitor_set[monitor] then - ---@type evolved.enter? - local monitor_enter = __evolved_get(monitor, __ENTER) - - if monitor_enter and not __evolved_has(monitor, __DISABLED) then - monitor_enter(entity, monitor) - end - end - end + __event_fns.emit_one(__ENTER, new_chunk, old_chunk.__monitor_set, entity) end end @@ -7245,6 +7083,42 @@ function __evolved_reindex(...) end end +---@param event evolved.event +---@param entity evolved.entity +---@param ... any event payload +function __evolved_emit(event, entity, ...) + local entity_primary = entity % 2 ^ 20 + + if __freelist_ids[entity_primary] ~= entity then + -- nothing to emit to non-alive entities + return + end + + if __debug_mode then + local event_primary = event % 2 ^ 20 + + if __freelist_ids[event_primary] ~= event then + __debug_fns.error_fmt('the event (%s) is not alive and cannot be emitted', + __id_name(event)) + end + end + + if __defer_depth > 0 then + __defer_call_hook(__evolved_emit, event, entity, ...) + return + end + + local chunk = __entity_chunks[entity_primary] + + if not chunk or chunk.__monitor_count == 0 then + return + end + + __evolved_defer() + __event_fns.emit_one(event, chunk, nil, entity, ...) + __evolved_commit() +end + ---@param ... evolved.system systems function __evolved_process(...) local argument_count = __lua_select('#', ...) @@ -7654,7 +7528,7 @@ function __builder_mt:spawn(component_mapper) end if __defer_depth > 0 then - __defer_spawn_entity(chunk, entity, component_table, component_mapper) + __defer_fns.spawn_entity(chunk, entity, component_table, component_mapper) else __evolved_defer() do @@ -7730,7 +7604,7 @@ function __builder_mt:multi_spawn_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_multi_spawn_entity(chunk, + __defer_fns.multi_spawn_entity(chunk, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -7769,7 +7643,7 @@ function __builder_mt:clone(prefab, component_mapper) local entity = __acquire_id() if __defer_depth > 0 then - __defer_clone_entity(prefab, entity, component_table, component_mapper) + __defer_fns.clone_entity(prefab, entity, component_table, component_mapper) else __evolved_defer() do @@ -7848,7 +7722,7 @@ function __builder_mt:multi_clone_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_multi_clone_entity(prefab, + __defer_fns.multi_clone_entity(prefab, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -8214,16 +8088,35 @@ function __builder_mt:execute(execute) return self:set(__EXECUTE, execute) end ----@param enter evolved.enter +---@param ... evolved.event events ---@return evolved.builder builder -function __builder_mt:enter(enter) - return self:set(__ENTER, enter) +function __builder_mt:observe(...) + local argument_count = __lua_select('#', ...) + + if argument_count == 0 then + return self + end + + local observe_list = self:get(__OBSERVES) + local observe_count = observe_list and #observe_list or 0 + + if observe_count == 0 then + observe_list = __list_fns.new(argument_count) + end + + for argument_index = 1, argument_count do + ---@type evolved.event + local event = __lua_select(argument_index, ...) + observe_list[observe_count + argument_index] = event + end + + return self:set(__OBSERVES, observe_list) end ----@param leave evolved.leave +---@param callback evolved.callback ---@return evolved.builder builder -function __builder_mt:leave(leave) - return self:set(__LEAVE, leave) +function __builder_mt:callback(callback) + return self:set(__CALLBACK, callback) end ---@param prologue evolved.prologue @@ -8336,6 +8229,9 @@ __evolved_set(__VARIANTS, __DUPLICATE, __list_fns.dup) __evolved_set(__REQUIRES, __DEFAULT, __list_fns.new()) __evolved_set(__REQUIRES, __DUPLICATE, __list_fns.dup) +__evolved_set(__OBSERVES, __DEFAULT, __list_fns.new()) +__evolved_set(__OBSERVES, __DUPLICATE, __list_fns.dup) + __evolved_set(__ON_SET, __UNIQUE) __evolved_set(__ON_ASSIGN, __UNIQUE) __evolved_set(__ON_INSERT, __UNIQUE) @@ -8392,7 +8288,7 @@ function __hook_fns.insert_query(query) end end - if __monitor_set[query] and not __evolved_has(query, __QUERY) then + if __monitors.__item_set[query] and not __evolved_has(query, __QUERY) then __hook_fns.attach_monitor(query) end @@ -8404,7 +8300,7 @@ function __hook_fns.insert_query(query) for query_system_index = 1, query_system_count do local query_system = query_system_list[query_system_index] - if __monitor_set[query_system] then + if __monitors.__item_set[query_system] then __hook_fns.attach_monitor(query_system) end end @@ -8445,7 +8341,7 @@ function __hook_fns.remove_query(query) end end - if __monitor_set[query] and not __evolved_has(query, __QUERY) then + if __monitors.__item_set[query] and not __evolved_has(query, __QUERY) then __hook_fns.detach_monitor(query) end @@ -8457,7 +8353,7 @@ function __hook_fns.remove_query(query) for query_system_index = 1, query_system_count do local query_system = query_system_list[query_system_index] - if __monitor_set[query_system] then + if __monitors.__item_set[query_system] then __hook_fns.detach_monitor(query_system) end end @@ -8476,12 +8372,11 @@ function __hook_fns.insert_monitor(monitor) return end - if not __evolved_has_any(monitor, __ENTER, __LEAVE) then + if not __sorted_observes[monitor] or not __evolved_has(monitor, __CALLBACK) then return end - __monitor_count = __assoc_list_fns.insert_ex( - __monitor_set, __monitor_list, __monitor_count, monitor) + __assoc_list_fns.insert(__monitors, monitor) __hook_fns.attach_monitor(monitor) end @@ -8493,14 +8388,13 @@ function __hook_fns.remove_monitor(monitor) return end - if not __monitor_set[monitor] then + if not __monitors.__item_set[monitor] then return end __hook_fns.detach_monitor(monitor) - __monitor_count = __assoc_list_fns.remove_ex( - __monitor_set, __monitor_list, __monitor_count, monitor) + __assoc_list_fns.remove(__monitors, monitor) end ---@param monitor evolved.system @@ -8510,7 +8404,7 @@ function __hook_fns.attach_monitor(monitor) return end - if not __monitor_set[monitor] then + if not __monitors.__item_set[monitor] then return end @@ -8776,22 +8670,38 @@ __evolved_set(__QUERY, __ON_REMOVE, function(monitor) __hook_fns.attach_monitor(monitor) end) -__evolved_set(__ENTER, __ON_INSERT, function(monitor) +__evolved_set(__OBSERVES, __ON_SET, function(monitor, _, observe_list) + local observe_count = #observe_list + + if observe_count > 0 then + ---@type evolved.assoc_list + local sorted_observes = __assoc_list_fns.new(observe_count) + + __assoc_list_fns.move(observe_list, 1, observe_count, sorted_observes) + __assoc_list_fns.sort(sorted_observes) + + __sorted_observes[monitor] = sorted_observes + else + __sorted_observes[monitor] = nil + end + __hook_fns.remove_monitor(monitor) __hook_fns.insert_monitor(monitor) end) -__evolved_set(__ENTER, __ON_REMOVE, function(monitor) +__evolved_set(__OBSERVES, __ON_REMOVE, function(monitor) + __sorted_observes[monitor] = nil + __hook_fns.remove_monitor(monitor) __hook_fns.insert_monitor(monitor) end) -__evolved_set(__LEAVE, __ON_INSERT, function(monitor) +__evolved_set(__CALLBACK, __ON_INSERT, function(monitor) __hook_fns.remove_monitor(monitor) __hook_fns.insert_monitor(monitor) end) -__evolved_set(__LEAVE, __ON_REMOVE, function(monitor) +__evolved_set(__CALLBACK, __ON_REMOVE, function(monitor) __hook_fns.remove_monitor(monitor) __hook_fns.insert_monitor(monitor) end) @@ -8834,6 +8744,9 @@ __evolved_set(__GROUP, __NAME, '__GROUP') __evolved_set(__QUERY, __NAME, '__QUERY') __evolved_set(__EXECUTE, __NAME, '__EXECUTE') +__evolved_set(__OBSERVES, __NAME, '__OBSERVES') +__evolved_set(__CALLBACK, __NAME, '__CALLBACK') + __evolved_set(__ENTER, __NAME, '__ENTER') __evolved_set(__LEAVE, __NAME, '__LEAVE') @@ -8882,6 +8795,9 @@ __evolved_set(__GROUP, __INTERNAL) __evolved_set(__QUERY, __INTERNAL) __evolved_set(__EXECUTE, __INTERNAL) +__evolved_set(__OBSERVES, __INTERNAL) +__evolved_set(__CALLBACK, __INTERNAL) + __evolved_set(__ENTER, __INTERNAL) __evolved_set(__LEAVE, __INTERNAL) @@ -8930,6 +8846,9 @@ evolved.GROUP = __GROUP evolved.QUERY = __QUERY evolved.EXECUTE = __EXECUTE +evolved.OBSERVES = __OBSERVES +evolved.CALLBACK = __CALLBACK + evolved.ENTER = __ENTER evolved.LEAVE = __LEAVE @@ -9006,6 +8925,8 @@ evolved.multi_search_to = __evolved_multi_search_to evolved.reindex = __evolved_reindex +evolved.emit = __evolved_emit + evolved.process = __evolved_process evolved.process_with = __evolved_process_with From cc0733e569f188ce850c923a3fc2d947368763a8 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 02:43:03 +0700 Subject: [PATCH 11/48] remove monitors temporary --- README.md | 48 --- develop/all.lua | 3 - develop/fuzzing/monitor_fuzz.lua | 400 -------------------- develop/testing/monitor_tests.lua | 590 ----------------------------- evolved.d.tl | 13 - evolved.lua | 595 +----------------------------- 6 files changed, 16 insertions(+), 1633 deletions(-) delete mode 100644 develop/fuzzing/monitor_fuzz.lua delete mode 100644 develop/testing/monitor_tests.lua diff --git a/README.md b/README.md index 0a94f49..5e97ce1 100644 --- a/README.md +++ b/README.md @@ -1587,7 +1587,6 @@ entity :: id fragment :: id query :: id system :: id -event :: id component :: any storage :: component[] @@ -1602,9 +1601,6 @@ realloc :: {storage?, integer, integer -> storage?} compmove :: {storage, integer, integer, integer, storage} execute :: {chunk, entity[], integer, any...} - -callback :: {event, entity, any...} - prologue :: {any...} epilogue :: {any...} @@ -1655,12 +1651,6 @@ GROUP :: fragment QUERY :: fragment EXECUTE :: fragment -OBSERVES :: fragment -CALLBACK :: fragment - -ENTER :: event -LEAVE :: event - PROLOGUE :: fragment EPILOGUE :: fragment @@ -1732,8 +1722,6 @@ multi_search_to :: entity[], integer, fragment, component -> integer reindex :: fragment... -> () -emit :: event, entity, any... -> () - process :: system... -> () process_with :: system, ... -> () @@ -1823,9 +1811,6 @@ builder_mt:group :: system -> builder builder_mt:query :: query -> builder builder_mt:execute :: {chunk, entity[], integer, any...} -> builder -builder_mt:observe :: event... -> builder -builder_mt:callback :: {event, entity, any...} -> builder - builder_mt:prologue :: {any...} -> builder builder_mt:epilogue :: {any...} -> builder @@ -1967,14 +1952,6 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EXECUTE` -### `evolved.OBSERVES` - -### `evolved.CALLBACK` - -### `evolved.ENTER` - -### `evolved.LEAVE` - ### `evolved.PROLOGUE` ### `evolved.EPILOGUE` @@ -2396,15 +2373,6 @@ function evolved.multi_search_to(out_entity_list, out_entity_first, fragment, co function evolved.reindex(...) end ``` -### `evolved.emit` - -```lua ----@param event evolved.event ----@param entity evolved.entity ----@param ... any event payload -function evolved.emit(event, entity, ...) end -``` - ### `evolved.process` ```lua @@ -2896,22 +2864,6 @@ function evolved.builder_mt:query(query) end function evolved.builder_mt:execute(execute) end ``` -#### `evolved.builder_mt:observe` - -```lua ----@param ... evolved.event events ----@return evolved.builder builder -function evolved.builder_mt:observe(...) end -``` - -#### `evolved.builder_mt:callback` - -```lua ----@param callback evolved.callback ----@return evolved.builder builder -function evolved.builder_mt:callback(callback) end -``` - #### `evolved.builder_mt:prologue` ```lua diff --git a/develop/all.lua b/develop/all.lua index 112f670..ba4677b 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -10,7 +10,6 @@ require 'develop.testing.locate_tests' require 'develop.testing.lookup_tests' require 'develop.testing.main_tests' require 'develop.testing.mappers_tests' -require 'develop.testing.monitor_tests' require 'develop.testing.multi_spawn_tests' require 'develop.testing.name_tests' require 'develop.testing.process_with_tests' @@ -39,8 +38,6 @@ basics.describe_fuzz 'develop.fuzzing.batch_destroy_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.explicit_fuzz' print '----------------------------------------' -basics.describe_fuzz 'develop.fuzzing.monitor_fuzz' -print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.pack_unpack_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.requires_fuzz' diff --git a/develop/fuzzing/monitor_fuzz.lua b/develop/fuzzing/monitor_fuzz.lua deleted file mode 100644 index 4004c57..0000000 --- a/develop/fuzzing/monitor_fuzz.lua +++ /dev/null @@ -1,400 +0,0 @@ -local evo = require 'evolved' - -evo.debug_mode(true) - ---- ---- ---- ---- ---- - -local __table_unpack = (function() - ---@diagnostic disable-next-line: deprecated - return table.unpack or unpack -end)() - ---- ---- ---- ---- ---- - --- entities created before and during monitor setup can leave monitors without --- entering them first, because monitors do not emit ENTER for existing entities -local setup_entity_set = {} ---@type table - -local HIT = evo.id() -setup_entity_set[HIT] = true - -local alive_fragment_list = {} ---@type evolved.fragment[] - -for i = 1, math.random(2, 8) do - local fragment = evo.id() - alive_fragment_list[i] = fragment - setup_entity_set[fragment] = true -end - -for _, fragment in ipairs(alive_fragment_list) do - if math.random(1, 4) == 1 then - local require = alive_fragment_list[math.random(1, #alive_fragment_list)] - - if require ~= fragment then - evo.set(fragment, evo.REQUIRES, { require }) - end - end - - if math.random(1, 4) == 1 then - evo.set(fragment, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) - end -end - ----@return evolved.fragment -local function random_fragment() - return alive_fragment_list[math.random(1, #alive_fragment_list)] -end - ----@param min_count integer ----@return evolved.fragment[] -local function random_fragment_list(min_count) - local fragment_set = {} ---@type table - local fragment_list = {} ---@type evolved.fragment[] - - for _ = 1, math.random(min_count, #alive_fragment_list) do - local fragment = random_fragment() - - if not fragment_set[fragment] then - fragment_set[fragment] = true - fragment_list[#fragment_list + 1] = fragment - end - end - - return fragment_list -end - ----@return evolved.component_table -local function random_component_table() - local component_table = {} ---@type evolved.component_table - - for _, fragment in ipairs(random_fragment_list(0)) do - component_table[fragment] = true - end - - return component_table -end - ----@param query evolved.query ----@param with_includes boolean -local function generate_query(query, with_includes) - local include_list = random_fragment_list(with_includes and 1 or 0) - local exclude_list = random_fragment_list(0) - local variant_list = random_fragment_list(0) - - if #include_list > 0 and (with_includes or math.random(1, 4) ~= 1) then - evo.set(query, evo.INCLUDES, include_list) - end - - if #exclude_list > 0 and math.random(1, 2) == 1 then - evo.set(query, evo.EXCLUDES, exclude_list) - end - - if #variant_list > 0 and math.random(1, 3) == 1 then - evo.set(query, evo.VARIANTS, variant_list) - end -end - ---- ---- ---- ---- ---- - -local monitor_list = {} ---@type evolved.system[] -local monitor_inside_sets = {} ---@type table> -local monitor_observe_hits = {} ---@type table - ----@type table> -local monitor_received_hits = {} - -for i = 1, math.random(1, 6) do - local monitor = evo.id() - setup_entity_set[monitor] = true - - local inside_set = {} ---@type table - local received_hits = {} ---@type table - - monitor_list[i] = monitor - monitor_inside_sets[monitor] = inside_set - monitor_received_hits[monitor] = received_hits - monitor_observe_hits[monitor] = math.random(1, 3) ~= 1 - - if math.random(1, 3) == 1 then - local query = evo.id() - setup_entity_set[query] = true - generate_query(query, false) - evo.set(monitor, evo.QUERY, query) - else - generate_query(monitor, false) - end - - if monitor_observe_hits[monitor] then - evo.set(monitor, evo.OBSERVES, { evo.ENTER, evo.LEAVE, HIT }) - else - evo.set(monitor, evo.OBSERVES, { evo.ENTER, evo.LEAVE }) - end - - evo.set(monitor, evo.CALLBACK, function(event, entity, payload) - if setup_entity_set[entity] then - assert(event == evo.ENTER or event == evo.LEAVE) - elseif event == evo.ENTER then - assert(not inside_set[entity]) - inside_set[entity] = true - elseif event == evo.LEAVE then - assert(inside_set[entity]) - inside_set[entity] = nil - elseif event == HIT then - assert(monitor_observe_hits[monitor]) - assert(inside_set[entity]) - assert(payload == entity) - received_hits[entity] = (received_hits[entity] or 0) + 1 - else - assert(false, 'unexpected event') - end - end) -end - ---- ---- ---- ---- ---- - -local tracked_entity_set = {} ---@type table -local tracked_entity_list = {} ---@type evolved.entity[] - ----@param entity evolved.entity -local function track_entity(entity) - if not tracked_entity_set[entity] then - tracked_entity_set[entity] = true - tracked_entity_list[#tracked_entity_list + 1] = entity - end -end - ----@return evolved.entity? -local function random_alive_entity() - local alive_entity_list = {} ---@type evolved.entity[] - - for _, entity in ipairs(tracked_entity_list) do - if evo.alive(entity) then - alive_entity_list[#alive_entity_list + 1] = entity - end - end - - if #alive_entity_list > 0 then - return alive_entity_list[math.random(1, #alive_entity_list)] - end -end - -local function check_monitors() - assert(evo.depth() == 0) - - for _, monitor in ipairs(monitor_list) do - local inside_set = monitor_inside_sets[monitor] - local expected_set = {} ---@type table - - for _, entity_list, entity_count in evo.execute(evo.get(monitor, evo.QUERY) or monitor) do - for entity_index = 1, entity_count do - local entity = entity_list[entity_index] - - if tracked_entity_set[entity] then - expected_set[entity] = true - end - end - end - - for entity in pairs(expected_set) do - assert(inside_set[entity]) - end - - for entity in pairs(inside_set) do - assert(evo.alive(entity)) - - if tracked_entity_set[entity] then - assert(expected_set[entity]) - end - end - end -end - ---- ---- ---- ---- ---- - ----@param no_destroy boolean ----@return boolean destroyed -local function random_operation(no_destroy) - local operation = math.random(1, no_destroy and 3 or 15) - - if operation == 1 then - track_entity(evo.spawn(random_component_table())) - elseif operation == 2 then - local entity_list = evo.multi_spawn(math.random(1, 4), random_component_table()) - - for _, entity in ipairs(entity_list) do - track_entity(entity) - end - elseif operation == 3 then - local entity = random_alive_entity() - - if entity then - evo.emit(HIT, entity, entity) - end - elseif operation == 4 then - local prefab = random_alive_entity() - - if prefab then - track_entity(evo.clone(prefab, random_component_table())) - end - elseif operation == 5 then - local prefab = random_alive_entity() - - if prefab then - local entity_list = evo.multi_clone(math.random(1, 4), prefab, random_component_table()) - - for _, entity in ipairs(entity_list) do - track_entity(entity) - end - end - elseif operation == 6 or operation == 7 then - local entity = random_alive_entity() - - if entity then - evo.set(entity, random_fragment()) - end - elseif operation == 8 then - local entity = random_alive_entity() - - if entity then - evo.remove(entity, __table_unpack(random_fragment_list(1))) - end - elseif operation == 9 then - local query = evo.id() - generate_query(query, true) - evo.batch_set(query, random_fragment()) - evo.destroy(query) - elseif operation == 10 then - local query = evo.id() - generate_query(query, true) - evo.batch_remove(query, __table_unpack(random_fragment_list(1))) - evo.destroy(query) - elseif operation == 11 then - local entity = random_alive_entity() - - if entity then - evo.clear(entity) - end - elseif operation == 12 then - local query = evo.id() - generate_query(query, true) - evo.batch_clear(query) - evo.destroy(query) - elseif operation == 13 then - local entity = random_alive_entity() - - if entity then - evo.destroy(entity) - return true - end - elseif operation == 14 then - local query = evo.id() - generate_query(query, true) - evo.batch_destroy(query) - evo.destroy(query) - return true - elseif operation == 15 then - if #alive_fragment_list > 1 then - local fragment_index = math.random(1, #alive_fragment_list) - local fragment = table.remove(alive_fragment_list, fragment_index) - - for _, other_fragment in ipairs(alive_fragment_list) do - local require_list = evo.get(other_fragment, evo.REQUIRES) - - if require_list and require_list[1] == fragment then - evo.remove(other_fragment, evo.REQUIRES) - end - end - - evo.destroy(fragment) - return true - end - end - - return false -end - -for _ = 1, math.random(20, 80) do - if math.random(1, 4) == 1 then - evo.defer() - - local destroyed = false - - for _ = 1, math.random(1, 10) do - if random_operation(destroyed) then - destroyed = true - end - end - - evo.commit() - else - random_operation(false) - end - - if math.random(1, 10) == 1 then - evo.collect_garbage(math.random(1, 2) == 1) - end - - check_monitors() - - do - local entity = random_alive_entity() - - if entity then - for _, monitor in ipairs(monitor_list) do - monitor_received_hits[monitor][entity] = nil - end - - evo.emit(HIT, entity, entity) - - for _, monitor in ipairs(monitor_list) do - local expected_hits = monitor_observe_hits[monitor] - and monitor_inside_sets[monitor][entity] and 1 or nil - - assert(monitor_received_hits[monitor][entity] == expected_hits) - end - end - end -end - ---- ---- ---- ---- ---- - -evo.destroy(__table_unpack(tracked_entity_list)) - -check_monitors() - -for _, monitor in ipairs(monitor_list) do - for entity in pairs(monitor_inside_sets[monitor]) do - assert(not tracked_entity_set[entity]) - end -end - -evo.destroy(__table_unpack(monitor_list)) -evo.destroy(__table_unpack(alive_fragment_list)) - -if math.random(1, 2) == 1 then - evo.collect_garbage(math.random(1, 2) == 1) -end diff --git a/develop/testing/monitor_tests.lua b/develop/testing/monitor_tests.lua deleted file mode 100644 index d14506d..0000000 --- a/develop/testing/monitor_tests.lua +++ /dev/null @@ -1,590 +0,0 @@ -local evo = require 'evolved' - -evo.debug_mode(true) - -do - local o = '' - - local f = evo.builder() - :build() - - local m = evo.builder() - :include(f) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - if ev == evo.ENTER then - o = o .. 'e' - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'e') - evo.remove(e, f) - assert(o == 'el') - evo.destroy(e) - assert(o == 'el') - end - - do - o = '' - local e = evo.spawn({ [f] = true }) - assert(o == 'e') - evo.destroy(e) - assert(o == 'el') - end - - evo.destroy(m) - - do - o = '' - local e = evo.spawn({ [f] = true }) - evo.destroy(e) - assert(o == '') - end -end - -do - local o = '' - - local f = evo.builder() - :build() - - local m = evo.builder() - :include(f) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - if ev == evo.ENTER then - o = o .. 'e' - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'e') - evo.destroy(f) - assert(o == 'el') - evo.destroy(e) - assert(o == 'el') - end - - evo.destroy(m) -end - -do - local o = '' - - local f = evo.builder() - :build() - - local m = evo.builder() - :include(f) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - if ev == evo.ENTER then - o = o .. 'e' - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'e') - evo.destroy(m, e) - assert(o == 'e') - evo.destroy(f) - assert(o == 'e') - end -end - -do - local o = '' - - local f = evo.builder() - :on_insert(function() - o = o .. 'i' - end) - :on_remove(function() - o = o .. 'r' - end) - :build() - - local m = evo.builder() - :include(f) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - if ev == evo.ENTER then - o = o .. 'e' - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'ie') - evo.remove(e, f) - assert(o == 'ielr') - evo.destroy(e) - assert(o == 'ielr') - end - - do - o = '' - local e = evo.spawn({ [f] = true }) - assert(o == 'ie') - evo.destroy(e) - assert(o == 'ielr') - end - - evo.destroy(m) - - do - o = '' - local e = evo.spawn({ [f] = true }) - assert(o == 'i') - evo.destroy(e, f) - assert(o == 'ir') - end -end - -do - local o = '' - - local f = evo.builder() - :on_insert(function(e, f) - o = o .. 'i' - evo.remove(e, f) - end) - :on_remove(function() - o = o .. 'r' - end) - :build() - - local m = evo.builder() - :include(f) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - if ev == evo.ENTER then - o = o .. 'e' - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'ielr') - evo.destroy(e) - assert(o == 'ielr') - end - - do - o = '' - local e = evo.spawn({ [f] = true }) - assert(o == 'ielr') - evo.destroy(e, f) - assert(o == 'ielr') - end - - evo.destroy(m) -end - -do - local o = '' - - local r = evo.builder() - :on_insert(function() - o = o .. 'I' - end) - :on_remove(function() - o = o .. 'R' - end) - :build() - - local f = evo.builder() - :require(r) - :on_insert(function() - o = o .. 'i' - end) - :on_remove(function() - o = o .. 'r' - end) - :build() - - local m = evo.builder() - :include(r) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - if ev == evo.ENTER then - o = o .. 'e' - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'Iie') - evo.remove(e, f) - assert(o == 'Iier') - evo.destroy(e) - assert(o == 'IierlR') - end - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'Iie') - evo.remove(e, r) - assert(o == 'IielR') - evo.destroy(e) - assert(o == 'IielRr') - end - - do - o = '' - local e = evo.id() - evo.set(e, f) - assert(o == 'Iie') - evo.destroy(e) - assert(o == 'IielRr') - end - - do - o = '' - local e = evo.spawn({ [f] = true }) - assert(o == 'Iie') - evo.destroy(f) - assert(o == 'Iier') - evo.destroy(r) - assert(o == 'IierlR') - evo.destroy(e) - assert(o == 'IierlR') - end - - evo.destroy(m) -end - -do - local o = '' - - local f1, f2 = evo.id(2) - local hit, heal = evo.id(2) - - local m = evo.builder() - :include(f1) - :observe(hit) - :callback(function(ev, e, a, b) - assert(ev == hit) - assert(evo.has(e, f1)) - o = o .. 'h' .. tostring(a) .. tostring(b) - end) - :build() - - do - o = '' - local e = evo.spawn({ [f1] = true }) - assert(o == '') - evo.emit(hit, e, 1, 2) - assert(o == 'h12') - evo.emit(heal, e, 3, 4) - assert(o == 'h12') - evo.emit(hit, e) - assert(o == 'h12hnilnil') - evo.emit(hit, e, nil, 5) - assert(o == 'h12hnilnilhnil5') - evo.destroy(e) - assert(o == 'h12hnilnilhnil5') - end - - do - o = '' - local e = evo.spawn({ [f2] = true }) - evo.emit(hit, e, 1, 2) - assert(o == '') - evo.set(e, f1) - evo.emit(hit, e, 1, 2) - assert(o == 'h12') - evo.remove(e, f1) - evo.emit(hit, e, 1, 2) - assert(o == 'h12') - end - - do - o = '' - local e = evo.id() - evo.emit(hit, e, 1, 2) - assert(o == '') - evo.destroy(e) - evo.emit(hit, e, 1, 2) - assert(o == '') - end - - evo.destroy(m) - - do - o = '' - local e = evo.spawn({ [f1] = true }) - evo.emit(hit, e, 1, 2) - assert(o == '') - end -end - -do - local o = '' - - local f = evo.id() - local hit, heal = evo.id(2) - - local m1 = evo.builder() - :include(f) - :observe(hit) - :callback(function(ev) - o = o .. (ev == hit and 'h' or '?') - end) - :build() - - local m2 = evo.builder() - :include(f) - :observe(heal, evo.ENTER) - :callback(function(ev) - o = o .. (ev == heal and 'H' or ev == evo.ENTER and 'E' or '?') - end) - :build() - - do - o = '' - local e = evo.spawn({ [f] = true }) - assert(o == 'E') - evo.emit(hit, e) - assert(o == 'Eh') - evo.emit(heal, e) - assert(o == 'EhH') - evo.destroy(e) - assert(o == 'EhH') - end - - evo.destroy(m1, m2) -end - -do - local o = '' - - local f = evo.id() - local hit = evo.id() - - local m = evo.builder() - :include(f) - :observe(hit) - :callback(function() - o = o .. 'h' - end) - :build() - - local e = evo.spawn({ [f] = true }) - - do - o = '' - evo.set(m, evo.DISABLED) - evo.emit(hit, e) - assert(o == '') - evo.remove(m, evo.DISABLED) - evo.emit(hit, e) - assert(o == 'h') - end - - do - o = '' - evo.remove(m, evo.CALLBACK) - evo.emit(hit, e) - assert(o == '') - evo.set(m, evo.CALLBACK, function() o = o .. 'H' end) - evo.emit(hit, e) - assert(o == 'H') - end - - do - o = '' - evo.remove(m, evo.OBSERVES) - evo.emit(hit, e) - assert(o == '') - evo.set(m, evo.OBSERVES, { hit }) - evo.emit(hit, e) - assert(o == 'H') - evo.set(m, evo.OBSERVES, {}) - evo.emit(hit, e) - assert(o == 'H') - evo.set(m, evo.OBSERVES, { evo.ENTER, hit }) - evo.emit(hit, e) - assert(o == 'HH') - end - - evo.destroy(m) -end - -do - local o = '' - - local f1, f2 = evo.id(2) - local hit = evo.id() - - local m = evo.builder() - :include(f2) - :observe(hit, evo.LEAVE) - :callback(function(ev, e, a) - if ev == hit then - o = o .. 'h' .. a - evo.remove(e, f2) - assert(evo.has(e, f2)) - elseif ev == evo.LEAVE then - o = o .. 'l' - end - end) - :build() - - do - o = '' - local e = evo.spawn({ [f1] = true }) - - evo.defer() - evo.set(e, f2) - evo.emit(hit, e, 1) - assert(o == '') - evo.commit() - - assert(o == 'h1l') - assert(not evo.has(e, f2)) - end - - do - o = '' - local e = evo.spawn({ [f1] = true, [f2] = true }) - - evo.defer() - evo.emit(hit, e, 1) - evo.destroy(e) - evo.emit(hit, e, 2) - assert(o == '') - evo.commit() - - assert(o == 'h1l') - assert(not evo.alive(e)) - end - - evo.destroy(m) -end - -do - local o = '' - - local f, g = evo.id(2) - local hit = evo.id() - - local q = evo.builder() - :include(f) - :build() - - local m = evo.builder() - :include(g) - :query(q) - :observe(hit) - :callback(function(_, _, a) - o = o .. a - end) - :build() - - do - o = '' - local e = evo.spawn({ [f] = true }) - evo.emit(hit, e, 'a') - assert(o == 'a') - evo.remove(m, evo.QUERY) - evo.emit(hit, e, 'b') - assert(o == 'a') - evo.set(m, evo.QUERY, q) - evo.emit(hit, e, 'c') - assert(o == 'ac') - end - - evo.destroy(m, q) -end - -do - local o = '' - - local f1, f2 = evo.id(2) - - local m1 = evo.builder() - :include(f1) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - o = o .. (ev == evo.ENTER and 'e' or 'l') - end) - :build() - - local m2 = evo.builder() - :include(f1, f2) - :observe(evo.ENTER, evo.LEAVE) - :callback(function(ev) - o = o .. (ev == evo.ENTER and 'E' or 'L') - end) - :build() - - local q1 = evo.builder():include(f1):build() - local q2 = evo.builder():include(f2):build() - - do - o = '' - local es = evo.multi_spawn(3, { [f1] = true }) - assert(#es == 3 and o == 'eee') - evo.batch_set(q1, f2) - assert(o == 'eeeEEE') - evo.batch_remove(q2, f2) - assert(o == 'eeeEEELLL') - evo.batch_set(q1, f2) - assert(o == 'eeeEEELLLEEE') - evo.batch_remove(q1, f1) - assert(o == 'eeeEEELLLEEElllLLL') - evo.batch_set(q2, f1) - assert(o == 'eeeEEELLLEEElllLLLeeeEEE') - evo.batch_destroy(q2) - assert(o == 'eeeEEELLLEEElllLLLeeeEEElllLLL') - end - - do - o = '' - local es = evo.multi_clone(2, evo.builder():set(f1):set(f2):build()) - assert(#es == 2 and o == 'eEeeEE') - evo.destroy(f2) - assert(o == 'eEeeEELLL') - evo.destroy(f1) - assert(o == 'eEeeEELLLlll') - end - - evo.destroy(m1, m2, q1, q2) -end diff --git a/evolved.d.tl b/evolved.d.tl index bf29a46..1742203 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -5,7 +5,6 @@ type Fragment = Id type Query = Id type System = Id - type Event = Id interface EachState end interface ExecuteState end @@ -137,9 +136,6 @@ query: function(self: Builder, query: Query): Builder execute: function(self: Builder, execute: function(Chunk, {Entity}, integer, ...: any)): Builder - observe: function(self: Builder, ...: Event): Builder - callback: function(self: Builder, callback: function(Event, Entity, ...: any)): Builder - prologue: function(self: Builder, prologue: function(...: any)): Builder epilogue: function(self: Builder, epilogue: function(...: any)): Builder @@ -177,13 +173,6 @@ QUERY: Fragment EXECUTE: Fragment - - OBSERVES: Fragment - CALLBACK: Fragment - - ENTER: Event - LEAVE: Event - PROLOGUE: Fragment EPILOGUE: Fragment @@ -291,8 +280,6 @@ reindex: function(...: Fragment) - emit: function(event: Event, entity: Entity, ...: any) - process: function(...: System) process_with: function(system: System, ...: any) diff --git a/evolved.lua b/evolved.lua index e2ccbce..d29851e 100644 --- a/evolved.lua +++ b/evolved.lua @@ -33,7 +33,6 @@ local evolved = { ---@alias evolved.fragment evolved.id ---@alias evolved.query evolved.id ---@alias evolved.system evolved.id ----@alias evolved.event evolved.id ---@alias evolved.component any ---@alias evolved.storage evolved.component[] @@ -53,11 +52,6 @@ local evolved = { --- entity_count: integer, --- ...: any) ----@alias evolved.callback fun( ---- event: evolved.event, ---- entity: evolved.entity, ---- ...: any) - ---@alias evolved.prologue fun(...: any) ---@alias evolved.epilogue fun(...: any) @@ -146,9 +140,6 @@ local evolved = { ---@field package __component_duplicates evolved.duplicate[] ---@field package __component_reallocs evolved.realloc[] ---@field package __component_compmoves evolved.compmove[] ----@field package __monitor_set table ----@field package __monitor_list evolved.system[] ----@field package __monitor_count integer ---@field package __with_fragment_edges table ---@field package __without_fragment_edges table ---@field package __with_required_fragments? evolved.chunk @@ -633,11 +624,8 @@ local __root_count = 0 ---@type integer local __major_chunks = {} ---@type table> local __minor_chunks = {} ---@type table> -local __monitors = { __item_set = {}, __item_list = {}, __item_count = 0 } ---@type evolved.assoc_list - local __query_chunks = {} ---@type table> local __major_queries = {} ---@type table> -local __monitor_majors = {} ---@type table> local __entity_chunks = {} ---@type (evolved.chunk|false)[] local __entity_places = {} ---@type integer[] @@ -648,7 +636,6 @@ local __sorted_includes = {} ---@type table> local __sorted_variants = {} ---@type table> local __sorted_requires = {} ---@type table> -local __sorted_observes = {} ---@type table> local __structural_changes = 0 ---@type integer @@ -1313,12 +1300,6 @@ local __GROUP = __acquire_id() local __QUERY = __acquire_id() local __EXECUTE = __acquire_id() -local __OBSERVES = __acquire_id() -local __CALLBACK = __acquire_id() - -local __ENTER = __acquire_id() -local __LEAVE = __acquire_id() - local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() @@ -1394,8 +1375,6 @@ local __evolved_multi_search_to local __evolved_reindex -local __evolved_emit - local __evolved_process local __evolved_process_with @@ -1423,10 +1402,8 @@ local __remove_child_chunk local __update_chunk_caches local __update_chunk_queries -local __update_chunk_monitors local __update_chunk_storages -local __trace_all_chunks local __trace_major_chunks local __trace_minor_chunks @@ -1488,9 +1465,11 @@ local __chunk_clear local __defer_call_hook -local __defer_fns = {} +local __defer_spawn_entity +local __defer_multi_spawn_entity -local __event_fns = {} +local __defer_clone_entity +local __defer_multi_clone_entity --- --- @@ -1588,9 +1567,6 @@ function __new_chunk(chunk_parent, chunk_fragment) __component_duplicates = {}, __component_reallocs = {}, __component_compmoves = {}, - __monitor_set = {}, - __monitor_list = {}, - __monitor_count = 0, __with_fragment_edges = {}, __without_fragment_edges = {}, __with_required_fragments = nil, @@ -1657,7 +1633,6 @@ function __new_chunk(chunk_parent, chunk_fragment) __update_chunk_caches(chunk) __update_chunk_queries(chunk) - __update_chunk_monitors(chunk) __update_chunk_storages(chunk) return chunk @@ -2008,31 +1983,6 @@ function __update_chunk_queries(chunk) end end ----@param chunk evolved.chunk -function __update_chunk_monitors(chunk) - local chunk_monitor_set = chunk.__monitor_set - local chunk_monitor_list = chunk.__monitor_list - local chunk_monitor_count = 0 - - __lua_table_clear(chunk_monitor_set, true, false) - __lua_table_clear(chunk_monitor_list, false, true) - - local monitor_list = __monitors.__item_list - local monitor_count = __monitors.__item_count - - for monitor_index = 1, monitor_count do - local monitor = monitor_list[monitor_index] - local monitor_query = __evolved_get(monitor, __QUERY) or monitor - - if __evolved_alive(monitor_query) and __query_minor_matches(chunk, monitor_query) then - chunk_monitor_count = __assoc_list_fns.insert_ex( - chunk_monitor_set, chunk_monitor_list, chunk_monitor_count, monitor) - end - end - - chunk.__monitor_count = chunk_monitor_count -end - ---@param chunk evolved.chunk function __update_chunk_storages(chunk) local entity_count = chunk.__entity_count @@ -2197,55 +2147,6 @@ function __update_chunk_storages(chunk) end end ----@param trace fun(chunk: evolved.chunk, ...: any) ----@param ... any additional trace arguments -function __trace_all_chunks(trace, ...) - local chunk_stack ---@type evolved.chunk[]? - local chunk_stack_size = 0 ---@type integer - - do - local root_list = __root_list - local root_count = __root_count - - if root_count > 0 then - ---@type evolved.chunk[] - chunk_stack = __acquire_table(__table_pool_tag.chunk_list) - - __lua_table_move( - root_list, 1, root_count, - chunk_stack_size + 1, chunk_stack) - - chunk_stack_size = chunk_stack_size + root_count - end - end - - while chunk_stack_size > 0 do - ---@cast chunk_stack -? - local chunk = chunk_stack[chunk_stack_size] - - trace(chunk, ...) - - chunk_stack[chunk_stack_size] = nil - chunk_stack_size = chunk_stack_size - 1 - - do - local chunk_child_list = chunk.__child_list - local chunk_child_count = chunk.__child_count - - __lua_table_move( - chunk_child_list, 1, chunk_child_count, - chunk_stack_size + 1, chunk_stack) - - chunk_stack_size = chunk_stack_size + chunk_child_count - end - end - - if chunk_stack then - __release_table(__table_pool_tag.chunk_list, chunk_stack, - chunk_stack_size == 0, true) - end -end - ---@param major evolved.fragment ---@param trace fun(chunk: evolved.chunk, ...: any) ---@param ... any additional trace arguments @@ -2489,7 +2390,6 @@ end function __update_major_chunks_trace(chunk) __update_chunk_caches(chunk) __update_chunk_queries(chunk) - __update_chunk_monitors(chunk) __update_chunk_storages(chunk) end @@ -2979,72 +2879,6 @@ function __detach_all_entities(chunk) chunk.__entity_count = 0 end ----@param event evolved.event ----@param chunk evolved.chunk ----@param skip_monitor_set? table ----@param entity evolved.entity ----@param ... any event payload -function __event_fns.emit_one(event, chunk, skip_monitor_set, entity, ...) - if __defer_depth <= 0 then - __debug_fns.error_fmt('event emitting should be deferred') - end - - local chunk_monitor_list = chunk.__monitor_list - local chunk_monitor_count = chunk.__monitor_count - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - if not skip_monitor_set or not skip_monitor_set[monitor] then - local monitor_observes = __sorted_observes[monitor] - - ---@type evolved.callback? - local monitor_callback = monitor_observes and monitor_observes.__item_set[event] - and __evolved_get(monitor, __CALLBACK) - - if monitor_callback and not __evolved_has(monitor, __DISABLED) then - monitor_callback(event, entity, ...) - end - end - end -end - ----@param event evolved.event ----@param chunk evolved.chunk ----@param skip_monitor_set? table ----@param entity_list evolved.entity[] ----@param entity_first integer ----@param entity_count integer ----@param ... any event payload -function __event_fns.emit_list(event, chunk, skip_monitor_set, entity_list, entity_first, entity_count, ...) - if __defer_depth <= 0 then - __debug_fns.error_fmt('event emitting should be deferred') - end - - local chunk_monitor_list = chunk.__monitor_list - local chunk_monitor_count = chunk.__monitor_count - - local entity_last = entity_first + entity_count - 1 - - for chunk_monitor_index = 1, chunk_monitor_count do - local monitor = chunk_monitor_list[chunk_monitor_index] - - if not skip_monitor_set or not skip_monitor_set[monitor] then - local monitor_observes = __sorted_observes[monitor] - - ---@type evolved.callback? - local monitor_callback = monitor_observes and monitor_observes.__item_set[event] - and __evolved_get(monitor, __CALLBACK) - - if monitor_callback and not __evolved_has(monitor, __DISABLED) then - for entity_index = entity_first, entity_last do - monitor_callback(event, entity_list[entity_index], ...) - end - end - end - end -end - ---@param chunk? evolved.chunk ---@param entity evolved.entity ---@param component_table? evolved.component_table @@ -3184,10 +3018,6 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) end end end - - if chunk.__monitor_count > 0 then - __event_fns.emit_one(__ENTER, chunk, nil, entity) - end end ---@param chunk? evolved.chunk @@ -3351,11 +3181,6 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co end end end - - if chunk.__monitor_count > 0 then - __event_fns.emit_list(__ENTER, chunk, nil, - chunk_entity_list, b_place, entity_count) - end end ---@param prefab evolved.entity @@ -3524,10 +3349,6 @@ function __clone_entity(prefab, entity, component_table, component_mapper) end end end - - if chunk.__monitor_count > 0 then - __event_fns.emit_one(__ENTER, chunk, nil, entity) - end end ---@param prefab evolved.entity @@ -3720,11 +3541,6 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c end end end - - if chunk.__monitor_count > 0 then - __event_fns.emit_list(__ENTER, chunk, nil, - chunk_entity_list, b_place, entity_count) - end end ---@param chunk evolved.chunk @@ -3950,10 +3766,6 @@ function __clear_entity_one(entity) local place = entity_places[entity_primary] if chunk then - if chunk.__monitor_count > 0 then - __event_fns.emit_one(__LEAVE, chunk, nil, entity) - end - local chunk_remove_fragment_list = chunk.__remove_fragment_list if chunk_remove_fragment_list then @@ -4033,10 +3845,6 @@ function __destroy_entity_one(entity) local place = entity_places[entity_primary] if chunk then - if chunk.__monitor_count > 0 then - __event_fns.emit_one(__LEAVE, chunk, nil, entity) - end - local chunk_remove_fragment_list = chunk.__remove_fragment_list if chunk_remove_fragment_list then @@ -4410,11 +4218,6 @@ function __chunk_insert(old_chunk, fragment, component) end end - if old_chunk.__monitor_count > 0 then - __event_fns.emit_list(__LEAVE, old_chunk, new_chunk.__monitor_set, - old_entity_list, 1, old_entity_count) - end - local new_component_indices = new_chunk.__component_indices local new_component_storages = new_chunk.__component_storages local new_component_reallocs = new_chunk.__component_reallocs @@ -4633,11 +4436,6 @@ function __chunk_insert(old_chunk, fragment, component) end end - if new_chunk.__monitor_count > 0 then - __event_fns.emit_list(__ENTER, new_chunk, old_chunk.__monitor_set, - new_entity_list, new_entity_count + 1, old_entity_count) - end - __structural_changes = __structural_changes + 1 end @@ -4670,11 +4468,6 @@ function __chunk_remove(old_chunk, ...) return end - if old_chunk.__monitor_count > 0 then - __event_fns.emit_list(__LEAVE, old_chunk, new_chunk and new_chunk.__monitor_set, - old_entity_list, 1, old_entity_count) - end - local old_remove_fragment_list = old_chunk.__remove_fragment_list if old_remove_fragment_list then @@ -4799,11 +4592,6 @@ function __chunk_remove(old_chunk, ...) __detach_all_entities(old_chunk) end - - if new_chunk.__monitor_count > 0 then - __event_fns.emit_list(__ENTER, new_chunk, old_chunk.__monitor_set, - new_entity_list, new_entity_count + 1, old_entity_count) - end else local entity_chunks = __entity_chunks local entity_places = __entity_places @@ -4834,11 +4622,6 @@ function __chunk_clear(chunk) return end - if chunk.__monitor_count > 0 then - __event_fns.emit_list(__LEAVE, chunk, nil, - chunk_entity_list, 1, chunk_entity_count) - end - local chunk_remove_fragment_list = chunk.__remove_fragment_list if chunk_remove_fragment_list then @@ -5000,7 +4783,7 @@ end ---@param entity evolved.entity ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_fns.spawn_entity(chunk, entity, component_table, component_mapper) +function __defer_spawn_entity(chunk, entity, component_table, component_mapper) ---@type evolved.component_table? local component_table2 @@ -5049,7 +4832,7 @@ end ---@param entity_count integer ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_fns.multi_spawn_entity(chunk, entity_list, entity_first, entity_count, component_table, component_mapper) +function __defer_multi_spawn_entity(chunk, entity_list, entity_first, entity_count, component_table, component_mapper) ---@type evolved.entity[] local entity_list2 = __acquire_table(__table_pool_tag.entity_list) @@ -5111,7 +4894,7 @@ end ---@param entity evolved.entity ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_fns.clone_entity(prefab, entity, component_table, component_mapper) +function __defer_clone_entity(prefab, entity, component_table, component_mapper) ---@type evolved.component_table? local component_table2 @@ -5160,7 +4943,7 @@ end ---@param entity_count integer ---@param component_table? evolved.component_table ---@param component_mapper? evolved.component_mapper -function __defer_fns.multi_clone_entity(prefab, entity_list, entity_first, entity_count, component_table, component_mapper) +function __defer_multi_clone_entity(prefab, entity_list, entity_first, entity_count, component_table, component_mapper) ---@type evolved.entity[] local entity_list2 = __acquire_table(__table_pool_tag.entity_list) @@ -5548,7 +5331,7 @@ function __evolved_spawn(component_table, component_mapper) end if __defer_depth > 0 then - __defer_fns.spawn_entity(nil, entity, component_table, component_mapper) + __defer_spawn_entity(nil, entity, component_table, component_mapper) else __evolved_defer() do @@ -5628,7 +5411,7 @@ function __evolved_multi_spawn_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_fns.multi_spawn_entity(nil, + __defer_multi_spawn_entity(nil, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -5666,7 +5449,7 @@ function __evolved_clone(prefab, component_table, component_mapper) local entity = __acquire_id() if __defer_depth > 0 then - __defer_fns.clone_entity(prefab, entity, component_table, component_mapper) + __defer_clone_entity(prefab, entity, component_table, component_mapper) else __evolved_defer() do @@ -5750,7 +5533,7 @@ function __evolved_multi_clone_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_fns.multi_clone_entity(prefab, + __defer_multi_clone_entity(prefab, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -6081,10 +5864,6 @@ function __evolved_set(entity, fragment, component) end end - if old_chunk and old_chunk.__monitor_count > 0 then - __event_fns.emit_one(__LEAVE, old_chunk, new_chunk.__monitor_set, entity) - end - local new_component_indices = new_chunk.__component_indices local new_component_storages = new_chunk.__component_storages @@ -6249,10 +6028,6 @@ function __evolved_set(entity, fragment, component) end end end - - if new_chunk.__monitor_count > 0 then - __event_fns.emit_one(__ENTER, new_chunk, old_chunk and old_chunk.__monitor_set, entity) - end end __evolved_commit() @@ -6293,10 +6068,6 @@ function __evolved_remove(entity, ...) local old_component_indices = old_chunk.__component_indices local old_component_storages = old_chunk.__component_storages - if old_chunk.__monitor_count > 0 then - __event_fns.emit_one(__LEAVE, old_chunk, new_chunk and new_chunk.__monitor_set, entity) - end - local old_remove_fragment_list = old_chunk.__remove_fragment_list if old_remove_fragment_list then @@ -6376,10 +6147,6 @@ function __evolved_remove(entity, ...) __structural_changes = __structural_changes + 1 end - - if new_chunk and new_chunk.__monitor_count > 0 then - __event_fns.emit_one(__ENTER, new_chunk, old_chunk.__monitor_set, entity) - end end __evolved_commit() @@ -7083,42 +6850,6 @@ function __evolved_reindex(...) end end ----@param event evolved.event ----@param entity evolved.entity ----@param ... any event payload -function __evolved_emit(event, entity, ...) - local entity_primary = entity % 2 ^ 20 - - if __freelist_ids[entity_primary] ~= entity then - -- nothing to emit to non-alive entities - return - end - - if __debug_mode then - local event_primary = event % 2 ^ 20 - - if __freelist_ids[event_primary] ~= event then - __debug_fns.error_fmt('the event (%s) is not alive and cannot be emitted', - __id_name(event)) - end - end - - if __defer_depth > 0 then - __defer_call_hook(__evolved_emit, event, entity, ...) - return - end - - local chunk = __entity_chunks[entity_primary] - - if not chunk or chunk.__monitor_count == 0 then - return - end - - __evolved_defer() - __event_fns.emit_one(event, chunk, nil, entity, ...) - __evolved_commit() -end - ---@param ... evolved.system systems function __evolved_process(...) local argument_count = __lua_select('#', ...) @@ -7528,7 +7259,7 @@ function __builder_mt:spawn(component_mapper) end if __defer_depth > 0 then - __defer_fns.spawn_entity(chunk, entity, component_table, component_mapper) + __defer_spawn_entity(chunk, entity, component_table, component_mapper) else __evolved_defer() do @@ -7604,7 +7335,7 @@ function __builder_mt:multi_spawn_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_fns.multi_spawn_entity(chunk, + __defer_multi_spawn_entity(chunk, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -7643,7 +7374,7 @@ function __builder_mt:clone(prefab, component_mapper) local entity = __acquire_id() if __defer_depth > 0 then - __defer_fns.clone_entity(prefab, entity, component_table, component_mapper) + __defer_clone_entity(prefab, entity, component_table, component_mapper) else __evolved_defer() do @@ -7722,7 +7453,7 @@ function __builder_mt:multi_clone_to(out_entity_list, out_entity_first, end if __defer_depth > 0 then - __defer_fns.multi_clone_entity(prefab, + __defer_multi_clone_entity(prefab, out_entity_list, out_entity_first, entity_count, component_table, component_mapper) else @@ -8088,37 +7819,6 @@ function __builder_mt:execute(execute) return self:set(__EXECUTE, execute) end ----@param ... evolved.event events ----@return evolved.builder builder -function __builder_mt:observe(...) - local argument_count = __lua_select('#', ...) - - if argument_count == 0 then - return self - end - - local observe_list = self:get(__OBSERVES) - local observe_count = observe_list and #observe_list or 0 - - if observe_count == 0 then - observe_list = __list_fns.new(argument_count) - end - - for argument_index = 1, argument_count do - ---@type evolved.event - local event = __lua_select(argument_index, ...) - observe_list[observe_count + argument_index] = event - end - - return self:set(__OBSERVES, observe_list) -end - ----@param callback evolved.callback ----@return evolved.builder builder -function __builder_mt:callback(callback) - return self:set(__CALLBACK, callback) -end - ---@param prologue evolved.prologue ---@return evolved.builder builder function __builder_mt:prologue(prologue) @@ -8229,9 +7929,6 @@ __evolved_set(__VARIANTS, __DUPLICATE, __list_fns.dup) __evolved_set(__REQUIRES, __DEFAULT, __list_fns.new()) __evolved_set(__REQUIRES, __DUPLICATE, __list_fns.dup) -__evolved_set(__OBSERVES, __DEFAULT, __list_fns.new()) -__evolved_set(__OBSERVES, __DUPLICATE, __list_fns.dup) - __evolved_set(__ON_SET, __UNIQUE) __evolved_set(__ON_ASSIGN, __UNIQUE) __evolved_set(__ON_INSERT, __UNIQUE) @@ -8239,8 +7936,6 @@ __evolved_set(__ON_REMOVE, __UNIQUE) __evolved_set(__GROUP, __INDEX) -__evolved_set(__QUERY, __INDEX) - --- --- --- @@ -8287,27 +7982,6 @@ function __hook_fns.insert_query(query) __assoc_list_fns.insert(major_queries, query) end end - - if __monitors.__item_set[query] and not __evolved_has(query, __QUERY) then - __hook_fns.attach_monitor(query) - end - - if __evolved_search(__QUERY, query) then - ---@type evolved.system[] - local query_system_list = __acquire_table(__table_pool_tag.system_list) - local query_system_count = __evolved_multi_search_to(query_system_list, 1, __QUERY, query) - - for query_system_index = 1, query_system_count do - local query_system = query_system_list[query_system_index] - - if __monitors.__item_set[query_system] then - __hook_fns.attach_monitor(query_system) - end - end - - __release_table(__table_pool_tag.system_list, query_system_list, - query_system_count == 0, true) - end end ---@param query evolved.query @@ -8341,174 +8015,9 @@ function __hook_fns.remove_query(query) end end - if __monitors.__item_set[query] and not __evolved_has(query, __QUERY) then - __hook_fns.detach_monitor(query) - end - - if __evolved_search(__QUERY, query) then - ---@type evolved.system[] - local query_system_list = __acquire_table(__table_pool_tag.system_list) - local query_system_count = __evolved_multi_search_to(query_system_list, 1, __QUERY, query) - - for query_system_index = 1, query_system_count do - local query_system = query_system_list[query_system_index] - - if __monitors.__item_set[query_system] then - __hook_fns.detach_monitor(query_system) - end - end - - __release_table(__table_pool_tag.system_list, query_system_list, - query_system_count == 0, true) - end - __reset_query_chunks(query) end ----@param monitor evolved.system -function __hook_fns.insert_monitor(monitor) - if __defer_depth > 0 then - __defer_call_hook(__hook_fns.insert_monitor, monitor) - return - end - - if not __sorted_observes[monitor] or not __evolved_has(monitor, __CALLBACK) then - return - end - - __assoc_list_fns.insert(__monitors, monitor) - - __hook_fns.attach_monitor(monitor) -end - ----@param monitor evolved.system -function __hook_fns.remove_monitor(monitor) - if __defer_depth > 0 then - __defer_call_hook(__hook_fns.remove_monitor, monitor) - return - end - - if not __monitors.__item_set[monitor] then - return - end - - __hook_fns.detach_monitor(monitor) - - __assoc_list_fns.remove(__monitors, monitor) -end - ----@param monitor evolved.system -function __hook_fns.attach_monitor(monitor) - if __defer_depth > 0 then - __defer_call_hook(__hook_fns.attach_monitor, monitor) - return - end - - if not __monitors.__item_set[monitor] then - return - end - - ---@type evolved.query - local monitor_query = __evolved_get(monitor, __QUERY) or monitor - - if not __evolved_alive(monitor_query) then - return - end - - ---@type evolved.assoc_list - local monitor_majors = __assoc_list_fns.new(4) - - do - local query_includes = __sorted_includes[monitor_query] - local query_include_list = query_includes and query_includes.__item_list - local query_include_count = query_includes and query_includes.__item_count or 0 - - local query_variants = __sorted_variants[monitor_query] - local query_variant_list = query_variants and query_variants.__item_list - local query_variant_count = query_variants and query_variants.__item_count or 0 - - if query_include_count > 0 then - local query_major = query_include_list[query_include_count] - __assoc_list_fns.insert(monitor_majors, query_major) - end - - for query_variant_index = 1, query_variant_count do - local query_variant = query_variant_list[query_variant_index] - - if query_include_count == 0 or query_variant > query_include_list[query_include_count] then - __assoc_list_fns.insert(monitor_majors, query_variant) - end - end - end - - __monitor_majors[monitor] = monitor_majors - - local monitor_major_list = monitor_majors.__item_list - local monitor_major_count = monitor_majors.__item_count - - if monitor_major_count == 0 then - __trace_all_chunks( - __hook_fns.insert_monitor_trace, monitor, monitor_query) - end - - for monitor_major_index = 1, monitor_major_count do - local monitor_major = monitor_major_list[monitor_major_index] - - __trace_major_chunks(monitor_major, - __hook_fns.insert_monitor_trace, monitor, monitor_query) - end -end - ----@param monitor evolved.system -function __hook_fns.detach_monitor(monitor) - if __defer_depth > 0 then - __defer_call_hook(__hook_fns.detach_monitor, monitor) - return - end - - local monitor_majors = __monitor_majors[monitor] - - if not monitor_majors then - return - end - - __monitor_majors[monitor] = nil - - local monitor_major_list = monitor_majors.__item_list - local monitor_major_count = monitor_majors.__item_count - - if monitor_major_count == 0 then - __trace_all_chunks( - __hook_fns.remove_monitor_trace, monitor) - end - - for monitor_major_index = 1, monitor_major_count do - local monitor_major = monitor_major_list[monitor_major_index] - - __trace_major_chunks(monitor_major, - __hook_fns.remove_monitor_trace, monitor) - end -end - ----@param chunk evolved.chunk ----@param monitor evolved.system ----@param monitor_query evolved.query -function __hook_fns.insert_monitor_trace(chunk, monitor, monitor_query) - if __query_minor_matches(chunk, monitor_query) then - chunk.__monitor_count = __assoc_list_fns.insert_ex( - chunk.__monitor_set, chunk.__monitor_list, - chunk.__monitor_count, monitor) - end -end - ----@param chunk evolved.chunk ----@param monitor evolved.system -function __hook_fns.remove_monitor_trace(chunk, monitor) - chunk.__monitor_count = __assoc_list_fns.remove_ex( - chunk.__monitor_set, chunk.__monitor_list, - chunk.__monitor_count, monitor) -end - --- --- --- @@ -8660,58 +8169,6 @@ end) --- --- -__evolved_set(__QUERY, __ON_SET, function(monitor) - __hook_fns.detach_monitor(monitor) - __hook_fns.attach_monitor(monitor) -end) - -__evolved_set(__QUERY, __ON_REMOVE, function(monitor) - __hook_fns.detach_monitor(monitor) - __hook_fns.attach_monitor(monitor) -end) - -__evolved_set(__OBSERVES, __ON_SET, function(monitor, _, observe_list) - local observe_count = #observe_list - - if observe_count > 0 then - ---@type evolved.assoc_list - local sorted_observes = __assoc_list_fns.new(observe_count) - - __assoc_list_fns.move(observe_list, 1, observe_count, sorted_observes) - __assoc_list_fns.sort(sorted_observes) - - __sorted_observes[monitor] = sorted_observes - else - __sorted_observes[monitor] = nil - end - - __hook_fns.remove_monitor(monitor) - __hook_fns.insert_monitor(monitor) -end) - -__evolved_set(__OBSERVES, __ON_REMOVE, function(monitor) - __sorted_observes[monitor] = nil - - __hook_fns.remove_monitor(monitor) - __hook_fns.insert_monitor(monitor) -end) - -__evolved_set(__CALLBACK, __ON_INSERT, function(monitor) - __hook_fns.remove_monitor(monitor) - __hook_fns.insert_monitor(monitor) -end) - -__evolved_set(__CALLBACK, __ON_REMOVE, function(monitor) - __hook_fns.remove_monitor(monitor) - __hook_fns.insert_monitor(monitor) -end) - ---- ---- ---- ---- ---- - __evolved_set(__TAG, __NAME, '__TAG') __evolved_set(__NAME, __NAME, '__NAME') @@ -8744,12 +8201,6 @@ __evolved_set(__GROUP, __NAME, '__GROUP') __evolved_set(__QUERY, __NAME, '__QUERY') __evolved_set(__EXECUTE, __NAME, '__EXECUTE') -__evolved_set(__OBSERVES, __NAME, '__OBSERVES') -__evolved_set(__CALLBACK, __NAME, '__CALLBACK') - -__evolved_set(__ENTER, __NAME, '__ENTER') -__evolved_set(__LEAVE, __NAME, '__LEAVE') - __evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') __evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') @@ -8795,12 +8246,6 @@ __evolved_set(__GROUP, __INTERNAL) __evolved_set(__QUERY, __INTERNAL) __evolved_set(__EXECUTE, __INTERNAL) -__evolved_set(__OBSERVES, __INTERNAL) -__evolved_set(__CALLBACK, __INTERNAL) - -__evolved_set(__ENTER, __INTERNAL) -__evolved_set(__LEAVE, __INTERNAL) - __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) @@ -8846,12 +8291,6 @@ evolved.GROUP = __GROUP evolved.QUERY = __QUERY evolved.EXECUTE = __EXECUTE -evolved.OBSERVES = __OBSERVES -evolved.CALLBACK = __CALLBACK - -evolved.ENTER = __ENTER -evolved.LEAVE = __LEAVE - evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE @@ -8925,8 +8364,6 @@ evolved.multi_search_to = __evolved_multi_search_to evolved.reindex = __evolved_reindex -evolved.emit = __evolved_emit - evolved.process = __evolved_process evolved.process_with = __evolved_process_with From d7facf9f04dd1aace5f403a2ecf544271d04f9d0 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 04:27:49 +0700 Subject: [PATCH 12/48] remove query major chunks cache --- evolved.lua | 399 ++++++++++++++-------------------------------------- 1 file changed, 109 insertions(+), 290 deletions(-) diff --git a/evolved.lua b/evolved.lua index d29851e..e630ae3 100644 --- a/evolved.lua +++ b/evolved.lua @@ -624,9 +624,6 @@ local __root_count = 0 ---@type integer local __major_chunks = {} ---@type table> local __minor_chunks = {} ---@type table> -local __query_chunks = {} ---@type table> -local __major_queries = {} ---@type table> - local __entity_chunks = {} ---@type (evolved.chunk|false)[] local __entity_places = {} ---@type integer[] @@ -1401,18 +1398,11 @@ local __add_child_chunk local __remove_child_chunk local __update_chunk_caches -local __update_chunk_queries local __update_chunk_storages local __trace_major_chunks local __trace_minor_chunks -local __cache_query_chunks -local __reset_query_chunks - -local __query_major_matches -local __query_minor_matches - local __update_major_chunks local __update_major_chunks_trace @@ -1632,7 +1622,6 @@ function __new_chunk(chunk_parent, chunk_fragment) end __update_chunk_caches(chunk) - __update_chunk_queries(chunk) __update_chunk_storages(chunk) return chunk @@ -1963,26 +1952,6 @@ function __update_chunk_caches(chunk) end end ----@param chunk evolved.chunk -function __update_chunk_queries(chunk) - local major_queries = __major_queries[chunk.__fragment] - local major_query_list = major_queries and major_queries.__item_list - local major_query_count = major_queries and major_queries.__item_count or 0 - - for major_query_index = 1, major_query_count do - local major_query = major_query_list[major_query_index] - local major_query_chunks = __query_chunks[major_query] - - if major_query_chunks then - if __query_major_matches(chunk, major_query) then - __assoc_list_fns.insert(major_query_chunks, chunk) - else - __assoc_list_fns.remove(major_query_chunks, chunk) - end - end - end -end - ---@param chunk evolved.chunk function __update_chunk_storages(chunk) local entity_count = chunk.__entity_count @@ -2238,144 +2207,6 @@ function __trace_minor_chunks(minor, trace, ...) end end ----@param query evolved.query ----@return evolved.assoc_list -function __cache_query_chunks(query) - __reset_query_chunks(query) - - local query_includes = __sorted_includes[query] - local query_include_list = query_includes and query_includes.__item_list - local query_include_count = query_includes and query_includes.__item_count or 0 - - local query_variants = __sorted_variants[query] - local query_variant_list = query_variants and query_variants.__item_list - local query_variant_count = query_variants and query_variants.__item_count or 0 - - ---@type evolved.assoc_list - local query_chunks = __assoc_list_fns.new(4) - __query_chunks[query] = query_chunks - - if query_include_count > 0 then - local query_major = query_include_list[query_include_count] - - local major_chunks = __major_chunks[query_major] - local major_chunk_list = major_chunks and major_chunks.__item_list - local major_chunk_count = major_chunks and major_chunks.__item_count or 0 - - for major_chunk_index = 1, major_chunk_count do - local major_chunk = major_chunk_list[major_chunk_index] - - if __query_major_matches(major_chunk, query) then - __assoc_list_fns.insert(query_chunks, major_chunk) - end - end - end - - for query_variant_index = 1, query_variant_count do - local query_variant = query_variant_list[query_variant_index] - - if query_include_count == 0 or query_variant > query_include_list[query_include_count] then - local major_chunks = __major_chunks[query_variant] - local major_chunk_list = major_chunks and major_chunks.__item_list - local major_chunk_count = major_chunks and major_chunks.__item_count or 0 - - for major_chunk_index = 1, major_chunk_count do - local major_chunk = major_chunk_list[major_chunk_index] - - if __query_major_matches(major_chunk, query) then - __assoc_list_fns.insert(query_chunks, major_chunk) - end - end - end - end - - return query_chunks -end - ----@param query evolved.query -function __reset_query_chunks(query) - __query_chunks[query] = nil -end - ----@param chunk evolved.chunk ----@param query evolved.query ----@return boolean ----@nodiscard -function __query_major_matches(chunk, query) - local query_includes = __sorted_includes[query] - local query_include_set = query_includes and query_includes.__item_set - local query_include_count = query_includes and query_includes.__item_count or 0 - - local query_variants = __sorted_variants[query] - local query_variant_set = query_variants and query_variants.__item_set - local query_variant_list = query_variants and query_variants.__item_list - local query_variant_count = query_variants and query_variants.__item_count or 0 - - local query_include_index = query_include_count > 0 and query_include_set[chunk.__fragment] or nil - local query_variant_index = query_variant_count > 0 and query_variant_set[chunk.__fragment] or nil - - return ( - (query_include_index ~= nil and query_include_index == query_include_count) or - (query_variant_index ~= nil and not __chunk_has_any_fragment_list(chunk, query_variant_list, query_variant_index - 1)) - ) and __query_minor_matches(chunk, query) -end - ----@param chunk evolved.chunk ----@param query evolved.query ----@return boolean ----@nodiscard -function __query_minor_matches(chunk, query) - local query_includes = __sorted_includes[query] - local query_include_set = query_includes and query_includes.__item_set - local query_include_list = query_includes and query_includes.__item_list - local query_include_count = query_includes and query_includes.__item_count or 0 - - if query_include_count > 0 then - if not __chunk_has_all_fragment_list(chunk, query_include_list, query_include_count) then - return false - end - end - - local query_excludes = __sorted_excludes[query] - local query_exclude_list = query_excludes and query_excludes.__item_list - local query_exclude_count = query_excludes and query_excludes.__item_count or 0 - - if query_exclude_count > 0 then - if __chunk_has_any_fragment_list(chunk, query_exclude_list, query_exclude_count) then - return false - end - end - - local query_variants = __sorted_variants[query] - local query_variant_set = query_variants and query_variants.__item_set - local query_variant_list = query_variants and query_variants.__item_list - local query_variant_count = query_variants and query_variants.__item_count or 0 - - if query_variant_count > 0 then - if not __chunk_has_any_fragment_list(chunk, query_variant_list, query_variant_count) then - return false - end - end - - local chunk_explicit_fragment_list = chunk.__explicit_fragment_list - - if chunk_explicit_fragment_list then - for chunk_explicit_fragment_index = 1, chunk.__explicit_fragment_count do - local fragment = chunk_explicit_fragment_list[chunk_explicit_fragment_index] - - local is_chunk_fragment_matched = - (query_variant_count > 0 and query_variant_set[fragment]) or - (query_include_count > 0 and query_include_set[fragment]) - - if not is_chunk_fragment_matched then - return false - end - end - end - - return true -end - ---@param major evolved.fragment function __update_major_chunks(major) if __defer_depth > 0 then @@ -2389,7 +2220,6 @@ end ---@param chunk evolved.chunk function __update_major_chunks_trace(chunk) __update_chunk_caches(chunk) - __update_chunk_queries(chunk) __update_chunk_storages(chunk) end @@ -6652,27 +6482,128 @@ function __evolved_execute(query) local query_includes = __sorted_includes[query] local query_include_set = query_includes and query_includes.__item_set + local query_include_list = query_includes and query_includes.__item_list local query_include_count = query_includes and query_includes.__item_count or 0 local query_excludes = __sorted_excludes[query] local query_exclude_set = query_excludes and query_excludes.__item_set + local query_exclude_list = query_excludes and query_excludes.__item_list local query_exclude_count = query_excludes and query_excludes.__item_count or 0 local query_variants = __sorted_variants[query] local query_variant_set = query_variants and query_variants.__item_set + local query_variant_list = query_variants and query_variants.__item_list local query_variant_count = query_variants and query_variants.__item_count or 0 if query_include_count > 0 or query_variant_count > 0 then - local query_chunks = __query_chunks[query] or __cache_query_chunks(query) - local query_chunk_list = query_chunks and query_chunks.__item_list - local query_chunk_count = query_chunks and query_chunks.__item_count or 0 + if query_include_count > 0 then + local query_major = query_include_list[query_include_count] - if query_chunk_count > 0 then - __lua_table_move( - query_chunk_list, 1, query_chunk_count, - chunk_stack_size + 1, chunk_stack) + local major_chunks = __major_chunks[query_major] + local major_chunk_list = major_chunks and major_chunks.__item_list + local major_chunk_count = major_chunks and major_chunks.__item_count or 0 + + for major_chunk_index = 1, major_chunk_count do + local major_chunk = major_chunk_list[major_chunk_index] + + local is_major_chunk_matched = true + + if is_major_chunk_matched and query_include_count > 1 then + is_major_chunk_matched = __chunk_has_all_fragment_list( + major_chunk, query_include_list, query_include_count - 1) + end + + if is_major_chunk_matched and query_exclude_count > 0 then + is_major_chunk_matched = not __chunk_has_any_fragment_list( + major_chunk, query_exclude_list, query_exclude_count) + end + + if is_major_chunk_matched and query_variant_count > 0 then + is_major_chunk_matched = __chunk_has_any_fragment_list( + major_chunk, query_variant_list, query_variant_count) + end + + if is_major_chunk_matched and major_chunk.__has_explicit_fragments then + local major_chunk_explicit_fragment_list = major_chunk.__explicit_fragment_list + local major_chunk_explicit_fragment_count = major_chunk.__explicit_fragment_count + + ---@cast major_chunk_explicit_fragment_list -? + + for major_chunk_explicit_fragment_index = 1, major_chunk_explicit_fragment_count do + local fragment = major_chunk_explicit_fragment_list[major_chunk_explicit_fragment_index] + + local is_explicit_fragment_matched = + (query_include_set and query_include_set[fragment]) or + (query_variant_set and query_variant_set[fragment]) + + if not is_explicit_fragment_matched then + is_major_chunk_matched = false + break + end + end + end + + if is_major_chunk_matched then + chunk_stack_size = chunk_stack_size + 1 + chunk_stack[chunk_stack_size] = major_chunk + end + end + end + + for query_variant_index = 1, query_variant_count do + local query_variant = query_variant_list[query_variant_index] + + if query_include_count == 0 or query_variant > query_include_list[query_include_count] then + local major_chunks = __major_chunks[query_variant] + local major_chunk_list = major_chunks and major_chunks.__item_list + local major_chunk_count = major_chunks and major_chunks.__item_count or 0 + + for major_chunk_index = 1, major_chunk_count do + local major_chunk = major_chunk_list[major_chunk_index] - chunk_stack_size = chunk_stack_size + query_chunk_count + local is_major_chunk_matched = true + + if is_major_chunk_matched and query_include_count > 0 then + is_major_chunk_matched = __chunk_has_all_fragment_list( + major_chunk, query_include_list, query_include_count) + end + + if is_major_chunk_matched and query_exclude_count > 0 then + is_major_chunk_matched = not __chunk_has_any_fragment_list( + major_chunk, query_exclude_list, query_exclude_count) + end + + if is_major_chunk_matched and query_variant_index > 1 then + is_major_chunk_matched = not __chunk_has_any_fragment_list( + major_chunk, query_variant_list, query_variant_index - 1) + end + + if is_major_chunk_matched and major_chunk.__has_explicit_fragments then + local major_chunk_explicit_fragment_list = major_chunk.__explicit_fragment_list + local major_chunk_explicit_fragment_count = major_chunk.__explicit_fragment_count + + ---@cast major_chunk_explicit_fragment_list -? + + for major_chunk_explicit_fragment_index = 1, major_chunk_explicit_fragment_count do + local fragment = major_chunk_explicit_fragment_list[major_chunk_explicit_fragment_index] + + local is_explicit_fragment_matched = + (query_include_set and query_include_set[fragment]) or + (query_variant_set and query_variant_set[fragment]) + + if not is_explicit_fragment_matched then + is_major_chunk_matched = false + break + end + end + end + + if is_major_chunk_matched then + chunk_stack_size = chunk_stack_size + 1 + chunk_stack[chunk_stack_size] = major_chunk + end + end + end end elseif query_exclude_count > 0 then for root_index = 1, __root_count do @@ -7942,93 +7873,9 @@ __evolved_set(__GROUP, __INDEX) --- --- -local __hook_fns = {} - ----@param query evolved.query -function __hook_fns.insert_query(query) - local query_includes = __sorted_includes[query] - local query_include_list = query_includes and query_includes.__item_list - local query_include_count = query_includes and query_includes.__item_count or 0 - - local query_variants = __sorted_variants[query] - local query_variant_list = query_variants and query_variants.__item_list - local query_variant_count = query_variants and query_variants.__item_count or 0 - - if query_include_count > 0 then - local query_major = query_include_list[query_include_count] - local major_queries = __major_queries[query_major] - - if not major_queries then - ---@type evolved.assoc_list - major_queries = __assoc_list_fns.new(4) - __major_queries[query_major] = major_queries - end - - __assoc_list_fns.insert(major_queries, query) - end - - for query_variant_index = 1, query_variant_count do - local query_variant = query_variant_list[query_variant_index] - - if query_include_count == 0 or query_variant > query_include_list[query_include_count] then - local major_queries = __major_queries[query_variant] - - if not major_queries then - ---@type evolved.assoc_list - major_queries = __assoc_list_fns.new(4) - __major_queries[query_variant] = major_queries - end - - __assoc_list_fns.insert(major_queries, query) - end - end -end - ----@param query evolved.query -function __hook_fns.remove_query(query) - local query_includes = __sorted_includes[query] - local query_include_list = query_includes and query_includes.__item_list - local query_include_count = query_includes and query_includes.__item_count or 0 - - local query_variants = __sorted_variants[query] - local query_variant_list = query_variants and query_variants.__item_list - local query_variant_count = query_variants and query_variants.__item_count or 0 - - if query_include_count > 0 then - local query_major = query_include_list[query_include_count] - local major_queries = __major_queries[query_major] - - if major_queries and __assoc_list_fns.remove(major_queries, query) == 0 then - __major_queries[query_major] = nil - end - end - - for query_variant_index = 1, query_variant_count do - local query_variant = query_variant_list[query_variant_index] - - if query_include_count == 0 or query_variant > query_include_list[query_include_count] then - local major_queries = __major_queries[query_variant] - - if major_queries and __assoc_list_fns.remove(major_queries, query) == 0 then - __major_queries[query_variant] = nil - end - end - end - - __reset_query_chunks(query) -end - ---- ---- ---- ---- ---- - ---@param query evolved.query ---@param include_list evolved.fragment[] __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) - __hook_fns.remove_query(query) - local include_count = #include_list if include_count > 0 then @@ -8042,18 +7889,10 @@ __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) else __sorted_includes[query] = nil end - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) __evolved_set(__INCLUDES, __ON_REMOVE, function(query) - __hook_fns.remove_query(query) - __sorted_includes[query] = nil - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) --- @@ -8065,8 +7904,6 @@ end) ---@param query evolved.query ---@param exclude_list evolved.fragment[] __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) - __hook_fns.remove_query(query) - local exclude_count = #exclude_list if exclude_count > 0 then @@ -8080,18 +7917,10 @@ __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) else __sorted_excludes[query] = nil end - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) __evolved_set(__EXCLUDES, __ON_REMOVE, function(query) - __hook_fns.remove_query(query) - __sorted_excludes[query] = nil - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) --- @@ -8103,8 +7932,6 @@ end) ---@param query evolved.query ---@param variant_list evolved.fragment[] __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) - __hook_fns.remove_query(query) - local variant_count = #variant_list if variant_count > 0 then @@ -8118,18 +7945,10 @@ __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) else __sorted_variants[query] = nil end - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) __evolved_set(__VARIANTS, __ON_REMOVE, function(query) - __hook_fns.remove_query(query) - __sorted_variants[query] = nil - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) --- From f3193ec4bb6cb2df685dc05d4648df7930fbf978 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 09:52:45 +0700 Subject: [PATCH 13/48] dummy link and link policy fragments --- evolved.d.tl | 7 +++++++ evolved.lua | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/evolved.d.tl b/evolved.d.tl index 1742203..f34d169 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -108,6 +108,7 @@ name: function(self: Builder, name: string): Builder index: function(self: Builder): Builder + link: function(self: Builder): Builder unique: function(self: Builder): Builder explicit: function(self: Builder): Builder internal: function(self: Builder): Builder @@ -139,6 +140,7 @@ prologue: function(self: Builder, prologue: function(...: any)): Builder epilogue: function(self: Builder, epilogue: function(...: any)): Builder + link_policy: function(self: Builder, link_policy: Id): Builder destruction_policy: function(self: Builder, destruction_policy: Id): Builder end @@ -146,6 +148,7 @@ NAME: Fragment INDEX: Fragment + LINK: Fragment UNIQUE: Fragment EXPLICIT: Fragment INTERNAL: Fragment @@ -176,6 +179,10 @@ PROLOGUE: Fragment EPILOGUE: Fragment + LINK_POLICY: Fragment + LINK_POLICY_DESTROY_ENTITY: Id + LINK_POLICY_REMOVE_FRAGMENT: Id + DESTRUCTION_POLICY: Fragment DESTRUCTION_POLICY_DESTROY_ENTITY: Id DESTRUCTION_POLICY_REMOVE_FRAGMENT: Id diff --git a/evolved.lua b/evolved.lua index e630ae3..f3a2c5f 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1269,6 +1269,7 @@ local __TAG = __acquire_id() local __NAME = __acquire_id() local __INDEX = __acquire_id() +local __LINK = __acquire_id() local __UNIQUE = __acquire_id() local __EXPLICIT = __acquire_id() local __INTERNAL = __acquire_id() @@ -1300,6 +1301,10 @@ local __EXECUTE = __acquire_id() local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() +local __LINK_POLICY = __acquire_id() +local __LINK_POLICY_DESTROY_ENTITY = __acquire_id() +local __LINK_POLICY_REMOVE_FRAGMENT = __acquire_id() + local __DESTRUCTION_POLICY = __acquire_id() local __DESTRUCTION_POLICY_DESTROY_ENTITY = __acquire_id() local __DESTRUCTION_POLICY_REMOVE_FRAGMENT = __acquire_id() @@ -7559,6 +7564,11 @@ function __builder_mt:index() return self:set(__INDEX) end +---@return evolved.builder builder +function __builder_mt:link() + return self:set(__LINK) +end + ---@return evolved.builder builder function __builder_mt:unique() return self:set(__UNIQUE) @@ -7762,6 +7772,12 @@ function __builder_mt:epilogue(epilogue) return self:set(__EPILOGUE, epilogue) end +---@param link_policy evolved.id +---@return evolved.builder builder +function __builder_mt:link_policy(link_policy) + return self:set(__LINK_POLICY, link_policy) +end + ---@param destruction_policy evolved.id ---@return evolved.builder builder function __builder_mt:destruction_policy(destruction_policy) @@ -7798,6 +7814,9 @@ __evolved_set(__TAG, __ON_REMOVE, __update_search_index) __evolved_set(__INDEX, __ON_INSERT, __update_search_index) __evolved_set(__INDEX, __ON_REMOVE, __update_search_index) +__evolved_set(__LINK, __ON_INSERT, __update_search_index) +__evolved_set(__LINK, __ON_REMOVE, __update_search_index) + __evolved_set(__UNIQUE, __ON_INSERT, __update_major_chunks) __evolved_set(__UNIQUE, __ON_REMOVE, __update_major_chunks) @@ -7832,6 +7851,9 @@ __evolved_set(__NAME, __INDEX) __evolved_set(__INDEX, __TAG) __evolved_set(__INDEX, __UNIQUE) +__evolved_set(__LINK, __TAG) +__evolved_set(__LINK, __UNIQUE) + __evolved_set(__UNIQUE, __TAG) __evolved_set(__EXPLICIT, __TAG) @@ -7866,6 +7888,12 @@ __evolved_set(__ON_INSERT, __UNIQUE) __evolved_set(__ON_REMOVE, __UNIQUE) __evolved_set(__GROUP, __INDEX) +__evolved_set(__GROUP, __LINK) +__evolved_set(__GROUP, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) + +__evolved_set(__QUERY, __INDEX) +__evolved_set(__QUERY, __LINK) +__evolved_set(__QUERY, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) --- --- @@ -7992,6 +8020,7 @@ __evolved_set(__TAG, __NAME, '__TAG') __evolved_set(__NAME, __NAME, '__NAME') __evolved_set(__INDEX, __NAME, '__INDEX') +__evolved_set(__LINK, __NAME, '__LINK') __evolved_set(__UNIQUE, __NAME, '__UNIQUE') __evolved_set(__EXPLICIT, __NAME, '__EXPLICIT') __evolved_set(__INTERNAL, __NAME, '__INTERNAL') @@ -8023,6 +8052,10 @@ __evolved_set(__EXECUTE, __NAME, '__EXECUTE') __evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') __evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') +__evolved_set(__LINK_POLICY, __NAME, '__LINK_POLICY') +__evolved_set(__LINK_POLICY_DESTROY_ENTITY, __NAME, '__LINK_POLICY_DESTROY_ENTITY') +__evolved_set(__LINK_POLICY_REMOVE_FRAGMENT, __NAME, '__LINK_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') @@ -8037,6 +8070,7 @@ __evolved_set(__TAG, __INTERNAL) __evolved_set(__NAME, __INTERNAL) __evolved_set(__INDEX, __INTERNAL) +__evolved_set(__LINK, __INTERNAL) __evolved_set(__UNIQUE, __INTERNAL) __evolved_set(__EXPLICIT, __INTERNAL) __evolved_set(__INTERNAL, __INTERNAL) @@ -8068,6 +8102,10 @@ __evolved_set(__EXECUTE, __INTERNAL) __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) +__evolved_set(__LINK_POLICY, __INTERNAL) +__evolved_set(__LINK_POLICY_DESTROY_ENTITY, __INTERNAL) +__evolved_set(__LINK_POLICY_REMOVE_FRAGMENT, __INTERNAL) + __evolved_set(__DESTRUCTION_POLICY, __INTERNAL) __evolved_set(__DESTRUCTION_POLICY_DESTROY_ENTITY, __INTERNAL) __evolved_set(__DESTRUCTION_POLICY_REMOVE_FRAGMENT, __INTERNAL) @@ -8082,6 +8120,7 @@ evolved.TAG = __TAG evolved.NAME = __NAME evolved.INDEX = __INDEX +evolved.LINK = __LINK evolved.UNIQUE = __UNIQUE evolved.EXPLICIT = __EXPLICIT evolved.INTERNAL = __INTERNAL @@ -8113,6 +8152,10 @@ evolved.EXECUTE = __EXECUTE evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE +evolved.LINK_POLICY = __LINK_POLICY +evolved.LINK_POLICY_DESTROY_ENTITY = __LINK_POLICY_DESTROY_ENTITY +evolved.LINK_POLICY_REMOVE_FRAGMENT = __LINK_POLICY_REMOVE_FRAGMENT + evolved.DESTRUCTION_POLICY = __DESTRUCTION_POLICY evolved.DESTRUCTION_POLICY_DESTROY_ENTITY = __DESTRUCTION_POLICY_DESTROY_ENTITY evolved.DESTRUCTION_POLICY_REMOVE_FRAGMENT = __DESTRUCTION_POLICY_REMOVE_FRAGMENT From 5455cf473c9130d7e017d8eacf7ce97cf0e679c1 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 09:55:27 +0700 Subject: [PATCH 14/48] first impl of links --- develop/all.lua | 3 + develop/fuzzing/link_fuzz.lua | 241 +++++++++++++++ develop/testing/link_tests.lua | 541 +++++++++++++++++++++++++++++++++ evolved.lua | 424 ++++++++++++++++++-------- 4 files changed, 1076 insertions(+), 133 deletions(-) create mode 100644 develop/fuzzing/link_fuzz.lua create mode 100644 develop/testing/link_tests.lua diff --git a/develop/all.lua b/develop/all.lua index ba4677b..734bd50 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -6,6 +6,7 @@ require 'develop.testing.cancel_tests' require 'develop.testing.clone_tests' require 'develop.testing.depth_tests' require 'develop.testing.destroy_tests' +require 'develop.testing.link_tests' require 'develop.testing.locate_tests' require 'develop.testing.lookup_tests' require 'develop.testing.main_tests' @@ -38,6 +39,8 @@ basics.describe_fuzz 'develop.fuzzing.batch_destroy_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.explicit_fuzz' print '----------------------------------------' +basics.describe_fuzz 'develop.fuzzing.link_fuzz' +print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.pack_unpack_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.requires_fuzz' diff --git a/develop/fuzzing/link_fuzz.lua b/develop/fuzzing/link_fuzz.lua new file mode 100644 index 0000000..b3fb489 --- /dev/null +++ b/develop/fuzzing/link_fuzz.lua @@ -0,0 +1,241 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +--- +--- +--- +--- +--- + +local __table_unpack = (function() + ---@diagnostic disable-next-line: deprecated + return table.unpack or unpack +end)() + +--- +--- +--- +--- +--- + +local destroying_mark = evo.builder():tag():build() +local destroying_query = evo.builder():include(destroying_mark):build() + +local all_link_fragment_list = {} ---@type evolved.fragment[] + +for i = 1, math.random(1, 4) do + local builder = evo.builder():link() + + local r = math.random(1, 3) + + if r == 1 then + builder:link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + elseif r == 2 then + builder:link_policy(evo.LINK_POLICY_REMOVE_FRAGMENT) + end + + if math.random(1, 2) == 1 then + builder:index() + end + + all_link_fragment_list[i] = builder:build() +end + +local all_entity_list = {} ---@type evolved.entity[] + +for i = 1, math.random(1, 20) do + local entity = evo.id() + all_entity_list[i] = entity +end + +--- +--- +--- +--- +--- + +local function random_entity() + return all_entity_list[math.random(1, #all_entity_list)] +end + +for _, entity in ipairs(all_entity_list) do + for _, link_fragment in ipairs(all_link_fragment_list) do + if math.random(1, 2) == 1 then + evo.set(entity, link_fragment, random_entity()) + end + end +end + +for _ = 1, math.random(0, #all_entity_list) do + local entity = random_entity() + local link_fragment = all_link_fragment_list[math.random(1, #all_link_fragment_list)] + + local r = math.random(1, 3) + + if r == 1 then + evo.set(entity, link_fragment, random_entity()) + elseif r == 2 then + evo.remove(entity, link_fragment) + else + evo.remove(link_fragment, evo.LINK) + evo.set(link_fragment, evo.LINK) + end +end + +for _, link_fragment in ipairs(all_link_fragment_list) do + if math.random(1, 4) == 1 then + if evo.has(link_fragment, evo.INDEX) then + evo.remove(link_fragment, evo.INDEX) + else + evo.set(link_fragment, evo.INDEX) + end + end +end + +--- +--- +--- +--- +--- + +local should_be_destroyed_entity_set = {} ---@type table + +local destroying_entity_list = {} ---@type evolved.entity[] + +for i = 1, math.random(1, #all_entity_list) do + local destroying_entity = random_entity() + destroying_entity_list[i] = destroying_entity + should_be_destroyed_entity_set[destroying_entity] = true +end + +repeat + local has_new_destroyed_entities = false + + for _, entity in ipairs(all_entity_list) do + if not should_be_destroyed_entity_set[entity] then + for _, link_fragment in ipairs(all_link_fragment_list) do + local link_policy = evo.get(link_fragment, evo.LINK_POLICY) + or evo.LINK_POLICY_REMOVE_FRAGMENT + + if link_policy == evo.LINK_POLICY_DESTROY_ENTITY then + local link_target = evo.get(entity, link_fragment) + + if link_target and should_be_destroyed_entity_set[link_target] then + should_be_destroyed_entity_set[entity] = true + has_new_destroyed_entities = true + break + end + end + end + end + end +until not has_new_destroyed_entities + +--- +--- +--- +--- +--- + +do + local r = math.random(1, 6) + + if r == 1 then + evo.destroy(__table_unpack(destroying_entity_list)) + elseif r == 2 then + assert(evo.defer()) + evo.destroy(__table_unpack(destroying_entity_list)) + assert(evo.commit()) + elseif r == 3 then + for _, destroying_entity in ipairs(destroying_entity_list) do + evo.destroy(destroying_entity) + end + elseif r == 4 then + assert(evo.defer()) + for _, destroying_entity in ipairs(destroying_entity_list) do + evo.destroy(destroying_entity) + end + assert(evo.commit()) + elseif r == 5 then + for _, destroying_entity in ipairs(destroying_entity_list) do + evo.set(destroying_entity, destroying_mark) + end + evo.batch_destroy(destroying_query) + elseif r == 6 then + for _, destroying_entity in ipairs(destroying_entity_list) do + evo.set(destroying_entity, destroying_mark) + end + assert(evo.defer()) + evo.batch_destroy(destroying_query) + assert(evo.commit()) + end +end + +--- +--- +--- +--- +--- + +for _, entity in ipairs(all_entity_list) do + assert(evo.alive(entity) == not should_be_destroyed_entity_set[entity]) +end + +for _, entity in ipairs(all_entity_list) do + if evo.alive(entity) then + for _, link_fragment in ipairs(all_link_fragment_list) do + local link_target = evo.get(entity, link_fragment) + assert(link_target == nil or evo.alive(link_target)) + end + end +end + +for _, entity in ipairs(all_entity_list) do + if not evo.alive(entity) then + for _, link_fragment in ipairs(all_link_fragment_list) do + assert(evo.search(link_fragment, entity) == nil) + end + end +end + +for _, link_fragment in ipairs(all_link_fragment_list) do + for _, entity in ipairs(all_entity_list) do + if evo.alive(entity) then + local link_target = evo.get(entity, link_fragment) + + if link_target then + local referrer_list, referrer_count = evo.multi_search(link_fragment, link_target) + + local is_referrer_found = false + + for i = 1, referrer_count do + if referrer_list[i] == entity then + is_referrer_found = true + break + end + end + + assert(is_referrer_found) + end + end + end +end + +--- +--- +--- +--- +--- + +if math.random(1, 2) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) +end + +evo.destroy(__table_unpack(all_entity_list)) +evo.destroy(__table_unpack(all_link_fragment_list)) +evo.destroy(destroying_query, destroying_mark) + +if math.random(1, 2) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) +end diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua new file mode 100644 index 0000000..053e25a --- /dev/null +++ b/develop/testing/link_tests.lua @@ -0,0 +1,541 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + assert(evo.has_all(evo.GROUP, evo.LINK, evo.INDEX)) + assert(evo.get(evo.GROUP, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) + + assert(evo.has_all(evo.QUERY, evo.LINK, evo.INDEX)) + assert(evo.get(evo.QUERY, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) +end + +do + local f = evo.builder():link():build() + assert(evo.has(f, evo.LINK) and not evo.has(f, evo.INDEX)) + assert(not evo.has(f, evo.LINK_POLICY)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + assert(evo.has(f, evo.LINK) and not evo.has(f, evo.INDEX)) + assert(evo.get(f, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) +end + +do + local f, g = evo.id(2) + local t1, t2 = evo.id(2) + + evo.set(f, evo.LINK) + assert(evo.has(f, evo.LINK) and not evo.has(f, evo.INDEX)) + + local e1 = evo.builder():set(f, t1):set(g, 42):build() + local e2 = evo.builder():set(f, t1):build() + local e3 = evo.builder():set(f, t2):set(g, 21):build() + + evo.destroy(t1) + assert(evo.alive_all(e1, e2, e3)) + + assert(not evo.has(e1, f) and evo.get(e1, g) == 42) + assert(not evo.has(e2, f)) + assert(evo.get(e3, f) == t2 and evo.get(e3, g) == 21) + + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == e3) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local g = evo.id() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f, t1):set(g, 42):build() + local e2 = evo.builder():set(f, t1):build() + local e3 = evo.builder():set(f, t2):build() + local e4 = evo.builder():set(g, t1):build() + + evo.destroy(t1) + assert(not evo.alive_any(e1, e2)) + assert(evo.alive_all(e3, e4)) + assert(evo.get(e3, f) == t2 and evo.get(e4, g) == t1) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local e1 = evo.id() + local e2 = evo.builder():set(f, e1):build() + local e3 = evo.builder():set(f, e2):build() + local e4 = evo.builder():set(f, e3):build() + + evo.destroy(e1) + assert(not evo.alive_any(e1, e2, e3, e4)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local e1, e2, e3 = evo.id(3) + evo.set(e1, f, e2) + evo.set(e2, f, e3) + evo.set(e3, f, e1) + + evo.destroy(e2) + assert(not evo.alive_any(e1, e2, e3)) +end + +do + local f1 = evo.builder():link():build() + local f2 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local e1 = evo.id() + evo.set(e1, f1, e1) + + local e2 = evo.id() + evo.set(e2, f2, e2) + + evo.destroy(e1, e2) + assert(not evo.alive_any(e1, e2)) +end + +do + local f1, f2 = evo.builder():link():build(), evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f1, t):set(f2, t):build() + + evo.destroy(t) + assert(evo.alive(e) and not evo.has_any(e, f1, f2)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, t):build() + + evo.destroy(t, e1) + assert(not evo.alive_any(t, e1)) + assert(evo.alive(e2) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, t1):build() + + evo.defer() + do + evo.destroy(t1) + evo.set(e, f, t2) + end + evo.commit() + + assert(evo.get(e, f) == t2) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, t1):build() + + evo.defer() + do + evo.set(e, f, t2) + evo.destroy(t1) + end + evo.commit() + + assert(evo.alive(e) and evo.get(e, f) == t2) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.defer() + do + evo.destroy(t) + assert(evo.alive(e)) + end + evo.commit() + + assert(not evo.alive(e)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.defer() + do + evo.destroy(t) + assert(evo.get(e, f) == t) + end + evo.commit() + + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.builder():tag():build() + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(t):build() + + evo.destroy(t) + assert(evo.alive_all(e1, e2)) + assert(not evo.has(e1, f) and not evo.has(e2, t)) +end + +do + local f = evo.builder():link():build() + local g = evo.builder():tag():build() + + local t1 = evo.builder():set(g):build() + local t2 = evo.builder():set(g):build() + + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + + local q = evo.builder():include(g):build() + + evo.batch_destroy(q) + assert(not evo.alive_any(t1, t2)) + assert(evo.alive_all(e1, e2)) + assert(not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + local p = evo.builder():prefab():build() + + local t = evo.id() + evo.set(p, f, t) + + local e1 = evo.clone(p) + local e2 = evo.clone(p) + assert(evo.get(e1, f) == t and evo.get(e2, f) == t) + + evo.destroy(t) + assert(not evo.has_any(p, f) and not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.remove(f, evo.LINK) + assert(not evo.has_any(f, evo.LINK, evo.INDEX)) + + evo.destroy(t) + assert(evo.get(e, f) == t) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + assert(evo.search(f, t) == e) +end + +do + local f = evo.builder():index():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.remove(f, evo.INDEX) + assert(evo.has(f, evo.LINK) and not evo.has(f, evo.INDEX)) + assert(evo.search(f, t) == e) + + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():index():build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + + evo.destroy(t1) + assert(evo.get(e1, f) == t1) + + evo.remove(e1, f) + evo.set(f, evo.LINK) + + evo.destroy(t2) + assert(evo.alive(e2) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + + evo.remove(f, evo.LINK) + + evo.destroy(t1) + assert(evo.get(e1, f) == t1) + + evo.remove(e1, f) + evo.set(f, evo.LINK) + + evo.destroy(t2) + assert(evo.alive(e2) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.set(f, evo.LINK_POLICY, evo.LINK_POLICY_DESTROY_ENTITY) + + evo.destroy(t) + assert(not evo.alive(e)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_REMOVE_FRAGMENT):build() + assert(evo.get(f, evo.LINK_POLICY) == evo.LINK_POLICY_REMOVE_FRAGMENT) + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.remove(f, evo.LINK_POLICY) + assert(evo.has(f, evo.LINK) and not evo.has(f, evo.LINK_POLICY)) + + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():index():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + assert(not evo.has(f, evo.LINK) and evo.has(f, evo.LINK_POLICY)) + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.destroy(t) + assert(evo.alive(e) and evo.get(e, f) == t) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, t1):build() + + evo.destroy(f) + assert(evo.alive(e) and not evo.has(e, f)) + + evo.destroy(t1, t2) + assert(evo.alive(e)) +end + +do + local group = evo.builder():build() + + local s1 = evo.builder():group(group):build() + local s2 = evo.builder():group(group):build() + local s3 = evo.builder():build() + local s4 = evo.builder():group(s1):build() + + evo.destroy(group) + assert(not evo.alive_any(group, s1, s2, s4)) + assert(evo.alive(s3)) +end + +do + local f = evo.builder():build() + + local e = evo.builder():set(f, 42):build() + local q = evo.builder():include(f):build() + + local executed = 0 + + local s = evo.builder() + :query(q) + :execute(function(chunk, entity_list, entity_count) + for i = 1, entity_count do + assert(chunk:components(f)[i] == 42) + assert(entity_list[i] == e) + end + executed = executed + entity_count + end) + :build() + + evo.process(s) + assert(executed == 1) + + evo.destroy(q) + assert(not evo.alive(s)) + assert(evo.alive(e) and evo.get(e, f) == 42) +end + +do + local q = evo.builder():build() + + local s1 = evo.builder():query(q):build() + local s2 = evo.builder():group(s1):build() + local s3 = evo.builder():query(q):group(s1):build() + + evo.destroy(q) + assert(not evo.alive_any(q, s1, s2, s3)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t1):build() + + evo.set(e1, f, t2) + evo.set(e2, f, t2) + + evo.destroy(t1) + assert(evo.get(e1, f) == t2 and evo.get(e2, f) == t2) + + evo.destroy(t2) + assert(not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, t1):build() + + do + local chunk, place = evo.locate(e) + assert(chunk):components(f)[place] = t2 + evo.reindex(f) + end + + evo.destroy(t1) + assert(evo.get(e, f) == t2) + + evo.destroy(t2) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f1 = evo.builder():link():build() + local f2 = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f1, t):set(f2, t):build() + + evo.remove(f1, evo.LINK) + + evo.destroy(t) + assert(evo.get(e, f1) == t and not evo.has(e, f2)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.remove(f, evo.INDEX) + evo.set(f, evo.INDEX) + + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, t):build() + + evo.remove(e1, f) + evo.destroy(e2) + + local e3 = evo.builder():set(f, t):build() + + evo.destroy(t) + assert(evo.alive_all(e1, e3) and not evo.has_any(e1, f) and not evo.has(e3, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.remove(f, evo.LINK) + evo.set(f, evo.LINK) + + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, t1):build() + + evo.remove(f, evo.LINK) + evo.set(e, f, t2) + evo.set(f, evo.LINK) + + evo.destroy(t1) + assert(evo.get(e, f) == t2) + + evo.destroy(t2) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f1 = evo.builder():link():build() + local f2 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t = evo.id() + local e1 = evo.builder():set(f1, t):build() + local e2 = evo.builder():set(f2, t):build() + + evo.remove(f1, evo.INDEX) + evo.set(f1, evo.INDEX) + + evo.remove(f2, evo.LINK) + evo.set(f2, evo.LINK) + + evo.destroy(t) + assert(evo.alive(e1) and not evo.has(e1, f1)) + assert(not evo.alive(e2)) +end diff --git a/evolved.lua b/evolved.lua index f3a2c5f..c1a1dad 100644 --- a/evolved.lua +++ b/evolved.lua @@ -628,6 +628,7 @@ local __entity_chunks = {} ---@type (evolved.chunk|false)[] local __entity_places = {} ---@type integer[] local __search_indices = {} ---@type table +local __target_fragments = {} ---@type table> local __sorted_includes = {} ---@type table> local __sorted_excludes = {} ---@type table> @@ -1114,21 +1115,61 @@ end ---@field package __prev_entities table ---@field package __next_entities table ---@field package __entity_components table +---@field package __target_fragment? evolved.fragment local __search_index_fns = {} +---@param target_fragment? evolved.fragment ---@return evolved.search_index ---@nodiscard -function __search_index_fns.new() +function __search_index_fns.new(target_fragment) ---@type evolved.search_index return { __head_entities = {}, __prev_entities = {}, __next_entities = {}, __entity_components = {}, + __target_fragment = target_fragment, } end +---@param target evolved.entity +---@param fragment evolved.fragment +function __search_index_fns.link(target, fragment) + if __debug_mode then + local target_primary = target % 2 ^ 20 + + if __freelist_ids[target_primary] ~= target then + __debug_fns.error_fmt('the target (%s) of the fragment (%s) is not alive', + tostring(target), tostring(fragment)) + end + end + + local target_fragments = __target_fragments[target] + + if not target_fragments then + ---@type evolved.assoc_list + target_fragments = __assoc_list_fns.new(1) + __target_fragments[target] = target_fragments + end + + __assoc_list_fns.insert(target_fragments, fragment) +end + +---@param target evolved.entity +---@param fragment evolved.fragment +function __search_index_fns.unlink(target, fragment) + local target_fragments = __target_fragments[target] + + if not target_fragments then + return + end + + if __assoc_list_fns.unordered_remove(target_fragments, fragment) == 0 then + __target_fragments[target] = nil + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -1163,6 +1204,12 @@ function __search_index_fns.insert(index, entity, component) prev_entities[entity_primary] = entity next_entities[entity_primary] = entity + + local target_fragment = index.__target_fragment + + if target_fragment then + __search_index_fns.link(component, target_fragment) + end end index.__entity_components[entity_primary] = component @@ -1189,6 +1236,12 @@ function __search_index_fns.remove(index, entity) if next_entity == entity then head_entities[component] = nil + + local target_fragment = index.__target_fragment + + if target_fragment then + __search_index_fns.unlink(component, target_fragment) + end else local prev_entity_primary = prev_entity % 2 ^ 20 local next_entity_primary = next_entity % 2 ^ 20 @@ -1452,6 +1505,7 @@ local __destroy_entity_list local __destroy_fragment_one local __destroy_fragment_list local __destroy_fragment_stack +local __remove_entity_fragments local __chunk_assign local __chunk_insert @@ -2238,8 +2292,9 @@ function __update_search_index(fragment) __search_indices[fragment] = nil __update_major_chunks(fragment) - if __evolved_has(fragment, __INDEX) then - local fragment_search_index = __search_index_fns.new() + if __evolved_has_any(fragment, __INDEX, __LINK) then + local target_fragment = __evolved_has(fragment, __LINK) and fragment or nil + local fragment_search_index = __search_index_fns.new(target_fragment) __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) @@ -3813,6 +3868,10 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment local remove_fragment_policy_fragment_list ---@type evolved.fragment[]? local remove_fragment_policy_fragment_count = 0 ---@type integer + local remove_fragment_link_fragment_list ---@type evolved.fragment[]? + local remove_fragment_link_target_list ---@type evolved.entity[]? + local remove_fragment_link_count = 0 ---@type integer + while processing_fragment_stack_size > 0 do local processing_fragment = processing_fragment_stack[processing_fragment_stack_size] @@ -3829,61 +3888,139 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment releasing_fragment_list[releasing_fragment_count] = processing_fragment end - local processing_fragment_destruction_policy = __evolved_get(processing_fragment, __DESTRUCTION_POLICY) - or __DESTRUCTION_POLICY_REMOVE_FRAGMENT + if __minor_chunks[processing_fragment] then + local processing_fragment_destruction_policy = __evolved_get(processing_fragment, __DESTRUCTION_POLICY) + or __DESTRUCTION_POLICY_REMOVE_FRAGMENT - if processing_fragment_destruction_policy == __DESTRUCTION_POLICY_DESTROY_ENTITY then - if not destroy_entity_policy_fragment_list then - ---@type evolved.fragment[] - destroy_entity_policy_fragment_list = __acquire_table(__table_pool_tag.fragment_list) - end + if processing_fragment_destruction_policy == __DESTRUCTION_POLICY_DESTROY_ENTITY then + if not destroy_entity_policy_fragment_list then + ---@type evolved.fragment[] + destroy_entity_policy_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + end - destroy_entity_policy_fragment_count = destroy_entity_policy_fragment_count + 1 - destroy_entity_policy_fragment_list[destroy_entity_policy_fragment_count] = processing_fragment + destroy_entity_policy_fragment_count = destroy_entity_policy_fragment_count + 1 + destroy_entity_policy_fragment_list[destroy_entity_policy_fragment_count] = processing_fragment - __trace_minor_chunks(processing_fragment, function(chunk) - local chunk_entity_list = chunk.__entity_list - local chunk_entity_count = chunk.__entity_count + __trace_minor_chunks(processing_fragment, function(chunk) + local chunk_entity_list = chunk.__entity_list + local chunk_entity_count = chunk.__entity_count - __lua_table_move( - chunk_entity_list, 1, chunk_entity_count, - processing_fragment_stack_size + 1, processing_fragment_stack) - - processing_fragment_stack_size = processing_fragment_stack_size + chunk_entity_count - end) - elseif processing_fragment_destruction_policy == __DESTRUCTION_POLICY_REMOVE_FRAGMENT then - if not remove_fragment_policy_fragment_list then - ---@type evolved.fragment[] - remove_fragment_policy_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + __lua_table_move( + chunk_entity_list, 1, chunk_entity_count, + processing_fragment_stack_size + 1, processing_fragment_stack) + + processing_fragment_stack_size = processing_fragment_stack_size + chunk_entity_count + end) + elseif processing_fragment_destruction_policy == __DESTRUCTION_POLICY_REMOVE_FRAGMENT then + if not remove_fragment_policy_fragment_list then + ---@type evolved.fragment[] + remove_fragment_policy_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + end + + remove_fragment_policy_fragment_count = remove_fragment_policy_fragment_count + 1 + remove_fragment_policy_fragment_list[remove_fragment_policy_fragment_count] = processing_fragment + else + __debug_fns.error_fmt('unknown DESTRUCTION_POLICY (%s) on (%s)', + __id_name(processing_fragment_destruction_policy), __id_name(processing_fragment)) end + end - remove_fragment_policy_fragment_count = remove_fragment_policy_fragment_count + 1 - remove_fragment_policy_fragment_list[remove_fragment_policy_fragment_count] = processing_fragment - else - __debug_fns.error_fmt('unknown DESTRUCTION_POLICY (%s) on (%s)', - __id_name(processing_fragment_destruction_policy), __id_name(processing_fragment)) + local target_fragments = __target_fragments[processing_fragment] + + if target_fragments then + __target_fragments[processing_fragment] = nil + + local target_fragment_list = target_fragments.__item_list + local target_fragment_count = target_fragments.__item_count + + for target_fragment_index = 1, target_fragment_count do + local target_fragment = target_fragment_list[target_fragment_index] + local target_fragment_search_index = __search_indices[target_fragment] + + if target_fragment_search_index + and target_fragment_search_index.__target_fragment == target_fragment + and target_fragment_search_index.__head_entities[processing_fragment] + then + local target_fragment_link_policy = __evolved_get(target_fragment, __LINK_POLICY) + or __LINK_POLICY_REMOVE_FRAGMENT + + if target_fragment_link_policy == __LINK_POLICY_DESTROY_ENTITY then + processing_fragment_stack_size = processing_fragment_stack_size + __evolved_multi_search_to( + processing_fragment_stack, processing_fragment_stack_size + 1, + target_fragment, processing_fragment) + elseif target_fragment_link_policy == __LINK_POLICY_REMOVE_FRAGMENT then + if not remove_fragment_link_target_list then + ---@type evolved.entity[] + remove_fragment_link_target_list = __acquire_table(__table_pool_tag.entity_list) + end + + if not remove_fragment_link_fragment_list then + ---@type evolved.fragment[] + remove_fragment_link_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + end + + remove_fragment_link_count = remove_fragment_link_count + 1 + remove_fragment_link_target_list[remove_fragment_link_count] = processing_fragment + remove_fragment_link_fragment_list[remove_fragment_link_count] = target_fragment + else + __debug_fns.error_fmt('unknown LINK_POLICY (%s) on (%s)', + __id_name(target_fragment_link_policy), __id_name(target_fragment)) + end + end + end end end end - if destroy_entity_policy_fragment_list then + if destroy_entity_policy_fragment_count > 0 then + ---@cast destroy_entity_policy_fragment_list -? + for i = 1, destroy_entity_policy_fragment_count do local minor = destroy_entity_policy_fragment_list[i] __trace_minor_chunks(minor, __chunk_clear) end - __release_table(__table_pool_tag.fragment_list, destroy_entity_policy_fragment_list, - destroy_entity_policy_fragment_count == 0, true) + __release_table(__table_pool_tag.fragment_list, destroy_entity_policy_fragment_list, false, true) end - if remove_fragment_policy_fragment_list then + if remove_fragment_policy_fragment_count > 0 then + ---@cast remove_fragment_policy_fragment_list -? + for i = 1, remove_fragment_policy_fragment_count do local minor = remove_fragment_policy_fragment_list[i] __trace_minor_chunks(minor, __chunk_remove, minor) end - __release_table(__table_pool_tag.fragment_list, remove_fragment_policy_fragment_list, - remove_fragment_policy_fragment_count == 0, true) + __release_table(__table_pool_tag.fragment_list, remove_fragment_policy_fragment_list, false, true) + end + + if remove_fragment_link_count > 0 then + ---@cast remove_fragment_link_fragment_list -? + ---@cast remove_fragment_link_target_list -? + + ---@type evolved.entity[] + local referrer_list = __acquire_table(__table_pool_tag.entity_list) + + for i = 1, remove_fragment_link_count do + local link_fragment = remove_fragment_link_fragment_list[i] + local link_target = remove_fragment_link_target_list[i] + + local referrer_count = __search_indices[link_fragment] + and __evolved_multi_search_to(referrer_list, 1, link_fragment, link_target) + or 0 + + for referrer_index = 1, referrer_count do + local referrer = referrer_list[referrer_index] + + if not processed_fragment_set[referrer] then + __remove_entity_fragments(referrer, link_fragment) + end + end + end + + __release_table(__table_pool_tag.entity_list, referrer_list, false, true) + __release_table(__table_pool_tag.fragment_list, remove_fragment_link_fragment_list, false, true) + __release_table(__table_pool_tag.entity_list, remove_fragment_link_target_list, false, true) end if releasing_fragment_count > 0 then @@ -3897,6 +4034,114 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment true, false) end +---@param entity evolved.entity +---@param ... evolved.fragment fragments +function __remove_entity_fragments(entity, ...) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') + end + + local entity_primary = entity % 2 ^ 20 + + if __freelist_ids[entity_primary] ~= entity then + -- nothing to remove from non-alive entities + return + end + + local entity_chunks = __entity_chunks + local entity_places = __entity_places + + local old_chunk = entity_chunks[entity_primary] + local old_place = entity_places[entity_primary] + + local new_chunk = __chunk_without_fragments(old_chunk or nil, ...) + + if old_chunk and old_chunk ~= new_chunk then + local old_component_indices = old_chunk.__component_indices + local old_component_storages = old_chunk.__component_storages + + local old_remove_fragment_list = old_chunk.__remove_fragment_list + + if old_remove_fragment_list then + local new_fragment_set = new_chunk and new_chunk.__fragment_set + or __safe_tbls.__EMPTY_FRAGMENT_SET + + for old_remove_fragment_index = 1, old_chunk.__remove_fragment_count do + local fragment = old_remove_fragment_list[old_remove_fragment_index] + + if not new_fragment_set[fragment] then + ---@type evolved.remove_hook? + local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + + if fragment_on_remove then + local old_component_index = old_component_indices[fragment] + + if old_component_index then + local old_component_storage = old_component_storages[old_component_index] + local old_component = old_component_storage[old_place] + fragment_on_remove(entity, fragment, old_component) + else + fragment_on_remove(entity, fragment) + end + end + end + 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 fragment_search_index = __search_indices[fragment] + __search_index_fns.remove(fragment_search_index, entity) + end + end + end + + if new_chunk then + local new_component_count = new_chunk.__component_count + local new_component_storages = new_chunk.__component_storages + local new_component_fragments = new_chunk.__component_fragments + + local new_place = new_chunk.__entity_count + 1 + + if new_place > new_chunk.__entity_capacity then + __expand_chunk(new_chunk, new_place) + end + + local new_entity_list = new_chunk.__entity_list + + new_entity_list[new_place] = entity + new_chunk.__entity_count = new_place + + for new_ci = 1, new_component_count do + local new_f = new_component_fragments[new_ci] + local new_cs = new_component_storages[new_ci] + + local old_ci = old_component_indices[new_f] + local old_cs = old_component_storages[old_ci] + + new_cs[new_place] = old_cs[old_place] + end + end + + do + __detach_entity(old_chunk, old_place) + + entity_chunks[entity_primary] = new_chunk or false + entity_places[entity_primary] = new_chunk and new_chunk.__entity_count or 0 + + __structural_changes = __structural_changes + 1 + end + end +end + ---@param chunk evolved.chunk ---@param fragment evolved.fragment ---@param component evolved.component @@ -5889,99 +6134,10 @@ function __evolved_remove(entity, ...) return end - local entity_chunks = __entity_chunks - local entity_places = __entity_places - - local old_chunk = entity_chunks[entity_primary] - local old_place = entity_places[entity_primary] - - local new_chunk = __chunk_without_fragments(old_chunk or nil, ...) - __evolved_defer() - if old_chunk and old_chunk ~= new_chunk then - local old_component_indices = old_chunk.__component_indices - local old_component_storages = old_chunk.__component_storages - - local old_remove_fragment_list = old_chunk.__remove_fragment_list - - if old_remove_fragment_list then - local new_fragment_set = new_chunk and new_chunk.__fragment_set - or __safe_tbls.__EMPTY_FRAGMENT_SET - - for old_remove_fragment_index = 1, old_chunk.__remove_fragment_count do - local fragment = old_remove_fragment_list[old_remove_fragment_index] - - if not new_fragment_set[fragment] then - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) - - if fragment_on_remove then - local old_component_index = old_component_indices[fragment] - - if old_component_index then - local old_component_storage = old_component_storages[old_component_index] - local old_component = old_component_storage[old_place] - fragment_on_remove(entity, fragment, old_component) - else - fragment_on_remove(entity, fragment) - end - end - end - 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 fragment_search_index = __search_indices[fragment] - __search_index_fns.remove(fragment_search_index, entity) - end - end - end - - if new_chunk then - local new_component_count = new_chunk.__component_count - local new_component_storages = new_chunk.__component_storages - local new_component_fragments = new_chunk.__component_fragments - - local new_place = new_chunk.__entity_count + 1 - - if new_place > new_chunk.__entity_capacity then - __expand_chunk(new_chunk, new_place) - end - - local new_entity_list = new_chunk.__entity_list - - new_entity_list[new_place] = entity - new_chunk.__entity_count = new_place - - for new_ci = 1, new_component_count do - local new_f = new_component_fragments[new_ci] - local new_cs = new_component_storages[new_ci] - - local old_ci = old_component_indices[new_f] - local old_cs = old_component_storages[old_ci] - - new_cs[new_place] = old_cs[old_place] - end - end - - do - __detach_entity(old_chunk, old_place) - - entity_chunks[entity_primary] = new_chunk or false - entity_places[entity_primary] = new_chunk and new_chunk.__entity_count or 0 - - __structural_changes = __structural_changes + 1 - end + do + __remove_entity_fragments(entity, ...) end __evolved_commit() @@ -6081,6 +6237,7 @@ function __evolved_destroy(...) do local minor_chunks = __minor_chunks + local target_fragments = __target_fragments local purging_entity_list ---@type evolved.entity[]? local purging_entity_count = 0 ---@type integer @@ -6096,9 +6253,9 @@ function __evolved_destroy(...) if __freelist_ids[entity_primary] ~= entity then -- nothing to destroy from non-alive entities else - local is_fragment = minor_chunks[entity] + local is_fragment_or_target = minor_chunks[entity] or target_fragments[entity] - if not is_fragment then + if not is_fragment_or_target then if not purging_entity_list then ---@type evolved.entity[] purging_entity_list = __acquire_table(__table_pool_tag.entity_list) @@ -6149,9 +6306,9 @@ function __evolved_destroy_one(entity) if __freelist_ids[entity_primary] ~= entity then -- nothing to destroy from non-alive entities else - local is_fragment = __minor_chunks[entity] + local is_fragment_or_target = __minor_chunks[entity] or __target_fragments[entity] - if not is_fragment then + if not is_fragment_or_target then __destroy_entity_one(entity) else __destroy_fragment_one(entity) @@ -6361,6 +6518,7 @@ function __evolved_batch_destroy(...) do local minor_chunks = __minor_chunks + local target_fragments = __target_fragments local clearing_chunk_list ---@type evolved.chunk[]? local clearing_chunk_count = 0 ---@type integer @@ -6394,9 +6552,9 @@ function __evolved_batch_destroy(...) for i = 1, entity_count do local entity = entity_list[i] - local is_fragment = minor_chunks[entity] + local is_fragment_or_target = minor_chunks[entity] or target_fragments[entity] - if not is_fragment then + if not is_fragment_or_target then if not purging_entity_list then ---@type evolved.entity[] purging_entity_list = __acquire_table(__table_pool_tag.entity_list) From 04a3897aeceb50f78305072341c836fad09d008f Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 10:10:54 +0700 Subject: [PATCH 15/48] update readme --- README.md | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e97ce1..980849b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ - [Internal Fragments](#internal-fragments) - [Shared Components](#shared-components) - [Fragment Requirements](#fragment-requirements) + - [Linked Fragments](#linked-fragments) - [Destruction Policies](#destruction-policies) - [Custom Component Storages](#custom-component-storages) - [Error Handling](#error-handling) @@ -77,6 +78,7 @@ - [Chunk](#chunk) - [Builder](#builder) - [Changelog](#changelog) + - [vX.Y.Z](#vxyz) - [v1.12.0](#v1120) - [v1.11.1](#v1111) - [v1.11.0](#v1110) @@ -975,6 +977,8 @@ evolved.builder() evolved.process(physics_group) ``` +The [`evolved.GROUP`](#evolvedgroup) and [`evolved.QUERY`](#evolvedquery) fragments are [linked](#linked-fragments) to the entities they refer to. So when a group is destroyed, all its systems are destroyed too, including nested groups and their systems. And when a query is destroyed, all systems that use it are destroyed as well. + Systems and groups also can have the [`evolved.PROLOGUE`](#evolvedprologue) and [`evolved.EPILOGUE`](#evolvedepilogue) fragments. These fragments are used to specify callbacks that will be executed before and after the system or group is processed. This is useful for setting up and tearing down systems or groups, or for performing some additional processing before or after the main processing. ```lua @@ -1179,7 +1183,7 @@ 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 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. +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. [Linked fragments](#linked-fragments) are always indexed, even without the [`evolved.INDEX`](#evolvedindex) trait. 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. @@ -1210,7 +1214,7 @@ assert(evolved.search(team, 'blue') == unit) ``` > [!NOTE] -> Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) trait is an error. +> Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) or [`evolved.LINK`](#evolvedlink) trait is an error. > [!NOTE] > 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. @@ -1354,6 +1358,93 @@ local enemy = evolved.builder() assert(evolved.has_all(enemy, position, velocity)) ``` +#### Linked Fragments + +Components often refer to other entities. For example, an item can store its owner, or a node can store its parent. When such an entity is destroyed, these components would keep referring to a dead entity. To avoid this, mark the fragment with the [`evolved.LINK`](#evolvedlink) fragment trait. Components of such fragments are treated as links to other entities, and when an entity is destroyed, the library resolves all links to it. By default, the linked fragment is removed from all entities that link to the destroyed entity. + +```lua +local evolved = require 'evolved' + +local owner = evolved.builder() + :link() + :build() + +local player = evolved.builder() + :build() + +local sword = evolved.builder() + :set(owner, player) + :build() + +-- destroy the player that the sword links to +evolved.destroy(player) + +-- the sword is still alive, but it no longer has the owner fragment +assert(evolved.alive(sword) and not evolved.has(sword, owner)) +``` + +You can change this behavior by using the [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment trait. This trait expects one of the following predefined identifiers: + +- [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) will destroy any entity that links to the destroyed entity. Links of destroyed entities are resolved too, so this is useful for hierarchies, where all descendants should be destroyed together with their root. + +- [`evolved.LINK_POLICY_REMOVE_FRAGMENT`](#evolvedlink_policy_remove_fragment) will remove the linked fragment from all entities that link to the destroyed entity. 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 parent = evolved.builder() + :link() + :link_policy(evolved.LINK_POLICY_DESTROY_ENTITY) + :build() + +local root = evolved.builder() + :build() + +local child = evolved.builder() + :set(parent, root) + :build() + +local grandchild = evolved.builder() + :set(parent, child) + :build() + +-- destroy the root that the child links to +evolved.destroy(root) + +-- the child and the grandchild are destroyed together with the root +assert(not evolved.alive(child) and not evolved.alive(grandchild)) +``` + +Linked fragments are always [indexed](#indexed-fragments), even without the [`evolved.INDEX`](#evolvedindex) trait, because the library uses the index to find the links to destroyed entities. So you can also use the [`evolved.search`](#evolvedsearch) functions to find all entities that link to some entity, for example, all children of a parent. + +```lua +local evolved = require 'evolved' + +local parent = evolved.builder() + :link() + :build() + +local root = evolved.builder() + :build() + +local child1 = evolved.builder() + :set(parent, root) + :build() + +local child2 = evolved.builder() + :set(parent, root) + :build() + +local child_list, child_count = evolved.multi_search(parent, root) +assert(child_count == 2 and child_list[1] == child1 and child_list[2] == child2) +``` + +> [!NOTE] +> Links must refer to alive entities. In [debug mode](#debug-mode), linking to an entity that is not alive is an error. + +> [!NOTE] +> Components changed directly in chunk storages bypass the index, and so the links too. After such changes, update the index using the [`evolved.reindex`](#evolvedreindex) function, otherwise these links will not be resolved when their entities are destroyed. + #### Destruction Policies Typically, fragments remain alive for the entire lifetime of the program. However, in some cases, you might want to destroy fragments when they are no longer needed. For example, you can use some runtime entities as fragments for other entities. In this case, you might want to destroy such fragments even while they are still attached to other entities. Since entities cannot have destroyed fragments, a destruction policy must be applied to resolve this. By default, the library will remove the destroyed fragment from all entities that have it. @@ -1623,6 +1714,7 @@ TAG :: fragment NAME :: fragment INDEX :: fragment +LINK :: fragment UNIQUE :: fragment EXPLICIT :: fragment INTERNAL :: fragment @@ -1654,6 +1746,10 @@ EXECUTE :: fragment PROLOGUE :: fragment EPILOGUE :: fragment +LINK_POLICY :: fragment +LINK_POLICY_DESTROY_ENTITY :: id +LINK_POLICY_REMOVE_FRAGMENT :: id + DESTRUCTION_POLICY :: fragment DESTRUCTION_POLICY_DESTROY_ENTITY :: id DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id @@ -1783,6 +1879,7 @@ builder_mt:tag :: builder builder_mt:name :: string -> builder builder_mt:index :: builder +builder_mt:link :: builder builder_mt:unique :: builder builder_mt:explicit :: builder builder_mt:internal :: builder @@ -1814,11 +1911,17 @@ builder_mt:execute :: {chunk, entity[], integer, any...} -> builder builder_mt:prologue :: {any...} -> builder builder_mt:epilogue :: {any...} -> builder +builder_mt:link_policy :: id -> builder builder_mt:destruction_policy :: id -> builder ``` ## Changelog +### vX.Y.Z + +- Added the new [`evolved.LINK`](#evolvedlink) and [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section +- The [`evolved.GROUP`](#evolvedgroup) and [`evolved.QUERY`](#evolvedquery) fragments are now linked, so destroying a group or a query also destroys the systems that use them + ### v1.12.0 - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) @@ -1912,6 +2015,8 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.INDEX` +### `evolved.LINK` + ### `evolved.UNIQUE` ### `evolved.EXPLICIT` @@ -1956,6 +2061,12 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EPILOGUE` +### `evolved.LINK_POLICY` + +### `evolved.LINK_POLICY_DESTROY_ENTITY` + +### `evolved.LINK_POLICY_REMOVE_FRAGMENT` + ### `evolved.DESTRUCTION_POLICY` ### `evolved.DESTRUCTION_POLICY_DESTROY_ENTITY` @@ -2709,6 +2820,13 @@ function evolved.builder_mt:name(name) end function evolved.builder_mt:index() end ``` +#### `evolved.builder_mt:link` + +```lua +---@return evolved.builder builder +function evolved.builder_mt:link() end +``` + #### `evolved.builder_mt:unique` ```lua @@ -2880,6 +2998,14 @@ function evolved.builder_mt:prologue(prologue) end function evolved.builder_mt:epilogue(epilogue) end ``` +#### `evolved.builder_mt:link_policy` + +```lua +---@param link_policy evolved.id +---@return evolved.builder builder +function evolved.builder_mt:link_policy(link_policy) end +``` + #### `evolved.builder_mt:destruction_policy` ```lua From e86397b2846ca9fae8eef38ff0ae2e984373b9f9 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 11:25:57 +0700 Subject: [PATCH 16/48] fix destroying links order, clear search indices on update --- develop/testing/link_tests.lua | 84 +++++++ evolved.lua | 447 +++++++++++++++++---------------- 2 files changed, 314 insertions(+), 217 deletions(-) diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua index 053e25a..d93c711 100644 --- a/develop/testing/link_tests.lua +++ b/develop/testing/link_tests.lua @@ -539,3 +539,87 @@ do assert(evo.alive(e1) and not evo.has(e1, f1)) assert(not evo.alive(e2)) end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, t):build() + + evo.set(f, evo.INDEX) + evo.remove(f, evo.INDEX) + + evo.destroy(t) + assert(evo.alive_all(e1, e2) and not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local g = evo.builder():build() + + local removed_list = {} + + local f = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + :on_remove(function(e, _, t) + assert(evo.alive(t) and evo.get(t, g) == 42) + removed_list[#removed_list + 1] = e + end) + :build() + + local e1 = evo.builder():set(g, 42):build() + local e2 = evo.builder():set(f, e1):set(g, 42):build() + local e3 = evo.builder():set(f, e2):set(g, 42):build() + + evo.destroy(e1) + assert(not evo.alive_any(e1, e2, e3)) + assert(#removed_list == 2 and removed_list[1] == e3 and removed_list[2] == e2) +end + +do + local g = evo.builder():build() + + local removed_count = 0 + + local f = evo.builder() + :link() + :on_remove(function(_, _, t) + assert(evo.alive(t) and evo.get(t, g) == 42) + removed_count = removed_count + 1 + end) + :build() + + local t = evo.builder():set(g, 42):build() + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, t):build() + + evo.destroy(t) + assert(removed_count == 2) + assert(evo.alive_all(e1, e2) and not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local g = evo.builder():tag():build() + + local removed_count = 0 + + local f = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + :on_remove(function(_, _, t) + assert(evo.alive(t)) + removed_count = removed_count + 1 + end) + :build() + + local t = evo.builder():set(g):build() + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, e1):build() + + local q = evo.builder():include(g):build() + + evo.batch_destroy(q) + assert(removed_count == 2) + assert(not evo.alive_any(t, e1, e2)) +end diff --git a/evolved.lua b/evolved.lua index c1a1dad..ac97609 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1110,214 +1110,6 @@ end --- --- ----@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 ----@field package __target_fragment? evolved.fragment - -local __search_index_fns = {} - ----@param target_fragment? evolved.fragment ----@return evolved.search_index ----@nodiscard -function __search_index_fns.new(target_fragment) - ---@type evolved.search_index - return { - __head_entities = {}, - __prev_entities = {}, - __next_entities = {}, - __entity_components = {}, - __target_fragment = target_fragment, - } -end - ----@param target evolved.entity ----@param fragment evolved.fragment -function __search_index_fns.link(target, fragment) - if __debug_mode then - local target_primary = target % 2 ^ 20 - - if __freelist_ids[target_primary] ~= target then - __debug_fns.error_fmt('the target (%s) of the fragment (%s) is not alive', - tostring(target), tostring(fragment)) - end - end - - local target_fragments = __target_fragments[target] - - if not target_fragments then - ---@type evolved.assoc_list - target_fragments = __assoc_list_fns.new(1) - __target_fragments[target] = target_fragments - end - - __assoc_list_fns.insert(target_fragments, fragment) -end - ----@param target evolved.entity ----@param fragment evolved.fragment -function __search_index_fns.unlink(target, fragment) - local target_fragments = __target_fragments[target] - - if not target_fragments then - return - end - - if __assoc_list_fns.unordered_remove(target_fragments, fragment) == 0 then - __target_fragments[target] = nil - end -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 - - local entity_primary = entity % 2 ^ 20 - - local head_entities = index.__head_entities - local prev_entities = index.__prev_entities - local next_entities = index.__next_entities - - ---@type evolved.entity? - local head_entity = head_entities[component] - - if head_entity then - local head_entity_primary = head_entity % 2 ^ 20 - - local tail_entity = prev_entities[head_entity_primary] - local tail_entity_primary = tail_entity % 2 ^ 20 - - prev_entities[entity_primary] = tail_entity - next_entities[entity_primary] = head_entity - - 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 - - local target_fragment = index.__target_fragment - - if target_fragment then - __search_index_fns.link(component, target_fragment) - end - end - - index.__entity_components[entity_primary] = component -end - ----@param index evolved.search_index ----@param entity evolved.entity -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 head_entities = index.__head_entities - local prev_entities = index.__prev_entities - local next_entities = index.__next_entities - - local prev_entity = prev_entities[entity_primary] - local next_entity = next_entities[entity_primary] - - if next_entity == entity then - head_entities[component] = nil - - local target_fragment = index.__target_fragment - - if target_fragment then - __search_index_fns.unlink(component, target_fragment) - end - else - local prev_entity_primary = prev_entity % 2 ^ 20 - local next_entity_primary = next_entity % 2 ^ 20 - - 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 - - prev_entities[entity_primary] = nil - next_entities[entity_primary] = nil - - 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 - ----@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) - for place = first, last do - __search_index_fns.insert(index, entity_list[place], component_list[place]) - 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) - 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_assign(index, entity_list, component_list, first, last) - local entity_components = index.__entity_components - - for place = first, last do - local entity = entity_list[place] - local component = component_list[place] - - if entity_components[entity % 2 ^ 20] ~= component then - __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, component) - end - end -end - ---- ---- ---- ---- ---- - local __TAG = __acquire_id() local __NAME = __acquire_id() @@ -1549,6 +1341,212 @@ local __safe_tbls = { --- --- +---@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 +---@field package __target_fragment? evolved.fragment + +local __search_index_fns = {} + +---@param fragment evolved.fragment +---@return evolved.search_index +---@nodiscard +function __search_index_fns.new(fragment) + ---@type evolved.search_index + return { + __head_entities = {}, + __prev_entities = {}, + __next_entities = {}, + __entity_components = {}, + __target_fragment = __evolved_has(fragment, __LINK) and fragment or nil, + } +end + +---@param target evolved.entity +---@param fragment evolved.fragment +function __search_index_fns.link(target, fragment) + local target_fragments = __target_fragments[target] + + if not target_fragments then + ---@type evolved.assoc_list + target_fragments = __assoc_list_fns.new(1) + __target_fragments[target] = target_fragments + end + + __assoc_list_fns.insert(target_fragments, fragment) +end + +---@param target evolved.entity +---@param fragment evolved.fragment +function __search_index_fns.unlink(target, fragment) + local target_fragments = __target_fragments[target] + + if not target_fragments then + return + end + + if __assoc_list_fns.unordered_remove(target_fragments, fragment) == 0 then + __target_fragments[target] = nil + end +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 + + local entity_primary = entity % 2 ^ 20 + + local head_entities = index.__head_entities + local prev_entities = index.__prev_entities + local next_entities = index.__next_entities + + ---@type evolved.entity? + local head_entity = head_entities[component] + + if head_entity then + local head_entity_primary = head_entity % 2 ^ 20 + + local tail_entity = prev_entities[head_entity_primary] + local tail_entity_primary = tail_entity % 2 ^ 20 + + prev_entities[entity_primary] = tail_entity + next_entities[entity_primary] = head_entity + + prev_entities[head_entity_primary] = entity + next_entities[tail_entity_primary] = entity + else + local target_fragment = index.__target_fragment + + if __debug_mode and target_fragment then + if __freelist_ids[component % 2 ^ 20] ~= component then + __debug_fns.error_fmt('the target (%s) of the linked fragment (%s) is not alive', + __id_name(component), __id_name(target_fragment)) + end + end + + head_entities[component] = entity + + prev_entities[entity_primary] = entity + next_entities[entity_primary] = entity + + if target_fragment then + __search_index_fns.link(component, target_fragment) + end + end + + index.__entity_components[entity_primary] = component +end + +---@param index evolved.search_index +---@param entity evolved.entity +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 head_entities = index.__head_entities + local prev_entities = index.__prev_entities + local next_entities = index.__next_entities + + local prev_entity = prev_entities[entity_primary] + local next_entity = next_entities[entity_primary] + + if next_entity == entity then + head_entities[component] = nil + + local target_fragment = index.__target_fragment + + if target_fragment then + __search_index_fns.unlink(component, target_fragment) + end + else + local prev_entity_primary = prev_entity % 2 ^ 20 + local next_entity_primary = next_entity % 2 ^ 20 + + 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 + + prev_entities[entity_primary] = nil + next_entities[entity_primary] = nil + + 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 + +---@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) + for place = first, last do + __search_index_fns.insert(index, entity_list[place], component_list[place]) + 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) + 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_assign(index, entity_list, component_list, first, last) + local entity_components = index.__entity_components + + for place = first, last do + local entity = entity_list[place] + local component = component_list[place] + + if entity_components[entity % 2 ^ 20] ~= component then + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, component) + end + end +end + +--- +--- +--- +--- +--- + ---@param id evolved.id ---@return string ---@nodiscard @@ -2289,14 +2287,21 @@ function __update_search_index(fragment) return end + local old_search_index = __search_indices[fragment] + + if old_search_index and old_search_index.__target_fragment then + for target in __lua_next, old_search_index.__head_entities do + __search_index_fns.unlink(target, fragment) + end + end + __search_indices[fragment] = nil __update_major_chunks(fragment) if __evolved_has_any(fragment, __INDEX, __LINK) then - local target_fragment = __evolved_has(fragment, __LINK) and fragment or nil - local fragment_search_index = __search_index_fns.new(target_fragment) - __search_indices[fragment] = fragment_search_index - __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) + local new_search_index = __search_index_fns.new(fragment) + __search_indices[fragment] = new_search_index + __trace_minor_chunks(fragment, __update_search_index_trace, fragment, new_search_index) __trace_major_chunks(fragment, __update_chunk_caches) end end @@ -3787,14 +3792,22 @@ end ---@param entity_list evolved.entity[] ---@param entity_count integer -function __destroy_entity_list(entity_list, entity_count) +---@param reverse? boolean +function __destroy_entity_list(entity_list, entity_count, reverse) if __defer_depth <= 0 then __debug_fns.error_fmt('this operation should be deferred') end - for entity_index = 1, entity_count do - local entity = entity_list[entity_index] - __destroy_entity_one(entity) + if reverse then + for entity_index = entity_count, 1, -1 do + local entity = entity_list[entity_index] + __destroy_entity_one(entity) + end + else + for entity_index = 1, entity_count do + local entity = entity_list[entity_index] + __destroy_entity_one(entity) + end end end @@ -4024,7 +4037,7 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment end if releasing_fragment_count > 0 then - __destroy_entity_list(releasing_fragment_list, releasing_fragment_count) + __destroy_entity_list(releasing_fragment_list, releasing_fragment_count, true) end __release_table(__table_pool_tag.fragment_list, releasing_fragment_list, From c51db29d8d5a151cf4e8a46a86a2e290266b03fe Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 16:57:21 +0700 Subject: [PATCH 17/48] support target lists for linked fragments --- README.md | 60 ++- develop/ROADMAP.md | 2 + develop/fuzzing/link_fuzz.lua | 119 +++++- develop/testing/link_tests.lua | 731 +++++++++++++++++++++++++++++++++ evolved.lua | 226 ++++++++-- 5 files changed, 1076 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 980849b..3823ef0 100644 --- a/README.md +++ b/README.md @@ -728,6 +728,8 @@ local query = evolved.builder() We don't have to set all of [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), and [`evolved.VARIANTS`](#evolvedvariants) fragments, we can even do it without filters at all, then the query will match all chunks in the world. +These fragments are [linked](#linked-fragments) to the fragments they list. So when a fragment used by a query is destroyed, the query is destroyed too, and so are all [systems](#systems) that use it. + After the query is created, we are ready to process our filtered by this query entities. You can do this by using the [`evolved.execute`](#evolvedexecute) function. This function takes a query as an argument and returns an iterator that can be used to iterate over all matching with the query chunks. ```lua @@ -1358,6 +1360,8 @@ local enemy = evolved.builder() assert(evolved.has_all(enemy, position, velocity)) ``` +The [`evolved.REQUIRES`](#evolvedrequires) fragment is [linked](#linked-fragments) to the fragments it lists. So when a required fragment is destroyed, all fragments that require it are destroyed too, and their [destruction policies](#destruction-policies) are applied to the entities that have them. + #### Linked Fragments Components often refer to other entities. For example, an item can store its owner, or a node can store its parent. When such an entity is destroyed, these components would keep referring to a dead entity. To avoid this, mark the fragment with the [`evolved.LINK`](#evolvedlink) fragment trait. Components of such fragments are treated as links to other entities, and when an entity is destroyed, the library resolves all links to it. By default, the linked fragment is removed from all entities that link to the destroyed entity. @@ -1439,11 +1443,61 @@ local child_list, child_count = evolved.multi_search(parent, root) assert(child_count == 2 and child_list[1] == child1 and child_list[2] == child2) ``` +A component of a linked fragment can also be a list of entities. Unlike regular [indexed fragments](#indexed-fragments), where table components are indexed by reference, such a list links to every entity in it. So you can find the entity that holds the list by any entity from this list, and the link policy is applied as soon as any of these entities is destroyed. With the default policy, the whole list is removed, and with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy, the entity that holds the list is destroyed. + +```lua +local evolved = require 'evolved' + +local attached_to = evolved.builder() + :link() + :build() + +local pillar1 = evolved.builder() + :build() + +local pillar2 = evolved.builder() + :build() + +local rope = evolved.builder() + :set(attached_to, { pillar1, pillar2 }) + :build() + +-- the rope can be found by any of its pillars +assert(evolved.search(attached_to, pillar1) == rope) +assert(evolved.search(attached_to, pillar2) == rope) + +-- destroy one of the pillars that the rope is attached to +evolved.destroy(pillar1) + +-- the rope is still alive, but it is no longer attached to anything +assert(evolved.alive(rope) and not evolved.has(rope, attached_to)) +``` + +The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are linked this way with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy. So you can also find all queries that use some fragment, or all fragments that require it. + +```lua +local evolved = require 'evolved' + +local position, velocity = evolved.id(2) + +local query = evolved.builder() + :include(position, velocity) + :build() + +assert(evolved.search(evolved.INCLUDES, velocity) == query) + +-- destroy one of the fragments that the query includes +evolved.destroy(velocity) + +-- the query is destroyed together with the fragment +assert(not evolved.alive(query)) +``` + > [!NOTE] -> Links must refer to alive entities. In [debug mode](#debug-mode), linking to an entity that is not alive is an error. +> Links must refer to alive entities. In [debug mode](#debug-mode), linking to an entity that is not alive is an error. Otherwise, such links are not indexed, so they cannot be found by the search functions and are never resolved. > [!NOTE] -> Components changed directly in chunk storages bypass the index, and so the links too. After such changes, update the index using the [`evolved.reindex`](#evolvedreindex) function, otherwise these links will not be resolved when their entities are destroyed. +> Components changed directly in chunk storages bypass the index, and so the links too. The same applies to lists changed in place. After such changes, set the components again or update the index using the [`evolved.reindex`](#evolvedreindex) function, otherwise these links will not be resolved when their entities are destroyed. #### Destruction Policies @@ -1921,6 +1975,8 @@ builder_mt:destruction_policy :: id -> builder - Added the new [`evolved.LINK`](#evolvedlink) and [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section - The [`evolved.GROUP`](#evolvedgroup) and [`evolved.QUERY`](#evolvedquery) fragments are now linked, so destroying a group or a query also destroys the systems that use them +- Components of [linked fragments](#linked-fragments) can be lists of entities, which link to every entity in them +- The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are now linked, so destroying a fragment also destroys the queries and fragments that use it ### v1.12.0 diff --git a/develop/ROADMAP.md b/develop/ROADMAP.md index 057aff1..4e0205d 100644 --- a/develop/ROADMAP.md +++ b/develop/ROADMAP.md @@ -2,6 +2,8 @@ ## Backlog +- custom indexing function +- custom linking function - observers and events - use compact prefix-tree for chunks diff --git a/develop/fuzzing/link_fuzz.lua b/develop/fuzzing/link_fuzz.lua index b3fb489..1dafa92 100644 --- a/develop/fuzzing/link_fuzz.lua +++ b/develop/fuzzing/link_fuzz.lua @@ -59,10 +59,35 @@ local function random_entity() return all_entity_list[math.random(1, #all_entity_list)] end +---@return evolved.entity | evolved.entity[] +local function random_link_component() + if math.random(1, 2) == 1 then + return random_entity() + end + + local link_target_list = {} + + for i = 1, math.random(0, 3) do + link_target_list[i] = random_entity() + end + + return link_target_list +end + +---@param link_component evolved.entity | evolved.entity[] +---@return evolved.entity[] +local function link_component_targets(link_component) + if type(link_component) == 'table' then + return link_component + end + + return { link_component } +end + for _, entity in ipairs(all_entity_list) do for _, link_fragment in ipairs(all_link_fragment_list) do if math.random(1, 2) == 1 then - evo.set(entity, link_fragment, random_entity()) + evo.set(entity, link_fragment, random_link_component()) end end end @@ -71,15 +96,27 @@ for _ = 1, math.random(0, #all_entity_list) do local entity = random_entity() local link_fragment = all_link_fragment_list[math.random(1, #all_link_fragment_list)] - local r = math.random(1, 3) + local r = math.random(1, 4) if r == 1 then - evo.set(entity, link_fragment, random_entity()) + evo.set(entity, link_fragment, random_link_component()) elseif r == 2 then evo.remove(entity, link_fragment) - else + elseif r == 3 then evo.remove(link_fragment, evo.LINK) evo.set(link_fragment, evo.LINK) + else + local link_component = evo.get(entity, link_fragment) + + if type(link_component) == 'table' then + if math.random(1, 2) == 1 then + link_component[#link_component + 1] = random_entity() + else + link_component[#link_component] = nil + end + + evo.reindex(link_fragment) + end end end @@ -119,14 +156,22 @@ repeat or evo.LINK_POLICY_REMOVE_FRAGMENT if link_policy == evo.LINK_POLICY_DESTROY_ENTITY then - local link_target = evo.get(entity, link_fragment) - - if link_target and should_be_destroyed_entity_set[link_target] then - should_be_destroyed_entity_set[entity] = true - has_new_destroyed_entities = true - break + local link_component = evo.get(entity, link_fragment) + + if link_component then + for _, link_target in ipairs(link_component_targets(link_component)) do + if should_be_destroyed_entity_set[link_target] then + should_be_destroyed_entity_set[entity] = true + has_new_destroyed_entities = true + break + end + end end end + + if should_be_destroyed_entity_set[entity] then + break + end end end end @@ -185,8 +230,13 @@ end for _, entity in ipairs(all_entity_list) do if evo.alive(entity) then for _, link_fragment in ipairs(all_link_fragment_list) do - local link_target = evo.get(entity, link_fragment) - assert(link_target == nil or evo.alive(link_target)) + local link_component = evo.get(entity, link_fragment) + + if link_component then + for _, link_target in ipairs(link_component_targets(link_component)) do + assert(evo.alive(link_target)) + end + end end end end @@ -202,21 +252,48 @@ end for _, link_fragment in ipairs(all_link_fragment_list) do for _, entity in ipairs(all_entity_list) do if evo.alive(entity) then - local link_target = evo.get(entity, link_fragment) + local link_component = evo.get(entity, link_fragment) - if link_target then - local referrer_list, referrer_count = evo.multi_search(link_fragment, link_target) + if link_component then + for _, link_target in ipairs(link_component_targets(link_component)) do + local referrer_list, referrer_count = evo.multi_search(link_fragment, link_target) - local is_referrer_found = false + local referrer_found_count = 0 - for i = 1, referrer_count do - if referrer_list[i] == entity then - is_referrer_found = true - break + for i = 1, referrer_count do + if referrer_list[i] == entity then + referrer_found_count = referrer_found_count + 1 + end + end + + assert(referrer_found_count == 1) + end + end + end + end +end + +for _, link_fragment in ipairs(all_link_fragment_list) do + for _, link_target in ipairs(all_entity_list) do + if evo.alive(link_target) then + local referrer_list, referrer_count = evo.multi_search(link_fragment, link_target) + + for i = 1, referrer_count do + local referrer = referrer_list[i] + local referrer_component = evo.get(referrer, link_fragment) + + local is_target_found = false + + if referrer_component then + for _, referrer_target in ipairs(link_component_targets(referrer_component)) do + if referrer_target == link_target then + is_target_found = true + break + end end end - assert(is_referrer_found) + assert(is_target_found) end end end diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua index d93c711..7d38208 100644 --- a/develop/testing/link_tests.lua +++ b/develop/testing/link_tests.lua @@ -8,6 +8,18 @@ do assert(evo.has_all(evo.QUERY, evo.LINK, evo.INDEX)) assert(evo.get(evo.QUERY, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) + + assert(evo.has(evo.INCLUDES, evo.LINK) and not evo.has(evo.INCLUDES, evo.INDEX)) + assert(evo.get(evo.INCLUDES, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) + + assert(evo.has(evo.EXCLUDES, evo.LINK) and not evo.has(evo.EXCLUDES, evo.INDEX)) + assert(evo.get(evo.EXCLUDES, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) + + assert(evo.has(evo.VARIANTS, evo.LINK) and not evo.has(evo.VARIANTS, evo.INDEX)) + assert(evo.get(evo.VARIANTS, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) + + assert(evo.has(evo.REQUIRES, evo.LINK) and not evo.has(evo.REQUIRES, evo.INDEX)) + assert(evo.get(evo.REQUIRES, evo.LINK_POLICY) == evo.LINK_POLICY_DESTROY_ENTITY) end do @@ -623,3 +635,722 @@ do assert(removed_count == 2) assert(not evo.alive_any(t, e1, e2)) end + +do + local f = evo.builder():link():build() + local g = evo.id() + + local t1, t2, t3 = evo.id(3) + + local e1 = evo.builder():set(f, { t1, t2 }):set(g, 42):build() + local e2 = evo.builder():set(f, { t2, t3 }):build() + local e3 = evo.builder():set(f, { t3 }):build() + local e4 = evo.builder():set(f, t2):build() + local e5 = evo.builder():set(f, {}):build() + + evo.destroy(t2) + assert(evo.alive_all(e1, e2, e3, e4, e5)) + + assert(not evo.has(e1, f) and evo.get(e1, g) == 42) + assert(not evo.has(e2, f)) + assert(not evo.has(e4, f)) + + do + local l3 = evo.get(e3, f) + assert(#l3 == 1 and l3[1] == t3) + + local l5 = evo.get(e5, f) + assert(#l5 == 0) + end + + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == nil) + assert(evo.search(f, t3) == e3) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f, { t1, t2 }):build() + local e2 = evo.builder():set(f, { t2 }):build() + local e3 = evo.builder():set(f, { t2, e1 }):build() + local e4 = evo.builder():set(f, e3):build() + local e5 = evo.builder():set(f, {}):build() + + evo.destroy(t1) + assert(not evo.alive_any(t1, e1, e3, e4)) + assert(evo.alive_all(t2, e2, e5)) + + evo.destroy(t2) + assert(not evo.alive_any(t2, e2)) + assert(evo.alive(e5)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local e1, e2, e3 = evo.id(3) + evo.set(e1, f, { e2 }) + evo.set(e2, f, { e3, e3 }) + evo.set(e3, f, { e1, e2 }) + + evo.destroy(e2) + assert(not evo.alive_any(e1, e2, e3)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f, { t1, t2 }):build() + local e2 = evo.builder():set(f, { t2 }):build() + local e3 = evo.builder():set(f, { t2, t1 }):build() + + assert(evo.search(f, t1) == e1) + assert(evo.search(f, t2) == e1) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 2 and l[1] == e1 and l[2] == e3) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end + + evo.remove(e1, f) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 1 and l[1] == e3) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 2 and l[1] == e2 and l[2] == e3) + end +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, { t }):build() + + do + local l, c = evo.multi_search(f, t) + assert(c == 2 and l[1] == e1 and l[2] == e2) + end + + evo.remove(e1, f) + assert(evo.search(f, t) == e2) + + evo.remove(e2, f) + assert(evo.search(f, t) == nil) + + evo.set(e1, f, { t }) + evo.set(e2, f, t) + + evo.destroy(t) + assert(evo.alive_all(e1, e2) and not evo.has_any(e1, f) and not evo.has_any(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + local e3 = evo.builder():set(f, t1):build() + + local e4 = evo.builder():set(f, { t2, t1 }):build() + + do + local l, c = evo.multi_search(f, t1) + assert(c == 3 and l[1] == e1 and l[2] == e3 and l[3] == e4) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 2 and l[1] == e2 and l[2] == e4) + end + + evo.remove(e1, f) + evo.set(e2, f, t1) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 3 and l[1] == e3 and l[2] == e4 and l[3] == e2) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 1 and l[1] == e4) + end + + evo.destroy(t2) + assert(evo.alive_all(e2, e3, e4) and not evo.has(e4, f)) + assert(evo.get(e2, f) == t1 and evo.get(e3, f) == t1) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 2 and l[1] == e3 and l[2] == e2) + end + + evo.destroy(t1) + assert(evo.alive_all(e2, e3) and not evo.has(e2, f) and not evo.has(e3, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + local e = evo.builder():set(f, { t, t, t }):build() + + do + local l, c = evo.multi_search(f, t) + assert(c == 1 and l[1] == e) + end + + evo.remove(e, f) + assert(evo.search(f, t) == nil) + + evo.set(e, f, { t, t }) + assert(evo.search(f, t) == e) + + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + + local e1 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, { t, t }):build() + local e3 = evo.builder():set(f, { t }):build() + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end + + evo.remove(e2, f) + + do + local l, c = evo.multi_search(f, t) + assert(c == 2 and l[1] == e1 and l[2] == e3) + end + + evo.set(e2, f, { t, t }) + evo.set(e2, f, { t }) + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == e1 and l[2] == e3 and l[3] == e2) + end + + evo.set(e2, f, { t, t }) + evo.destroy(e2) + + do + local l, c = evo.multi_search(f, t) + assert(c == 2 and l[1] == e1 and l[2] == e3) + end + + evo.destroy(t) + assert(evo.alive_all(e1, e3) and not evo.has(e1, f) and not evo.has(e3, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2, t3 = evo.id(3) + local e = evo.builder():set(f, { t1, t2 }):build() + + evo.set(e, f, { t2, t3 }) + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == e) + assert(evo.search(f, t3) == e) + + evo.destroy(t1) + assert(evo.has(e, f)) + + evo.destroy(t3) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, t1):build() + + evo.set(e, f, { t2 }) + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == e) + + evo.set(e, f, t1) + assert(evo.search(f, t1) == e) + assert(evo.search(f, t2) == nil) + + evo.destroy(t2) + assert(evo.get(e, f) == t1) + + evo.destroy(t1) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t = evo.id() + + local e1 = evo.builder():set(f, { t }):build() + local e2 = evo.builder():set(f, t):build() + local e3 = evo.builder():set(f, { t, t }):build() + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end + + evo.set(e1, f, { t }) + evo.set(e3, f, { t, t }) + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end + + evo.set(e1, f, { t, t }) + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == e2 and l[2] == e3 and l[3] == e1) + end + + evo.set(e2, f, { t }) + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == e3 and l[2] == e1 and l[3] == e2) + end +end + +do + local f = evo.builder():link():build() + + local t1, t2, t3 = evo.id(3) + + local e1 = evo.builder():set(f, { t1, t2 }):build() + local e2 = evo.builder():set(f, { t1, t2 }):build() + + evo.set(e1, f, { t2, t3 }) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 1 and l[1] == e2) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 2 and l[1] == e2 and l[2] == e1) + end + + assert(evo.search(f, t3) == e1) +end + +do + local f = evo.builder():link():build() + local g = evo.id() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f, { t1 }):build() + local e2 = evo.builder():set(f, { t1 }):set(g, 42):build() + local e3 = evo.builder():set(f, { t1 }):build() + + evo.reindex(f) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end + + do + local l = evo.get(e2, f) + l[2] = t2 + evo.reindex(f) + end + + do + local l, c = evo.multi_search(f, t1) + assert(c == 3 and l[1] == e1 and l[2] == e3 and l[3] == e2) + end + + assert(evo.search(f, t2) == e2) + + evo.destroy(t2) + assert(evo.alive(e2) and not evo.has(e2, f)) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 2 and l[1] == e1 and l[2] == e3) + end +end + +do + local f = evo.builder():link():build() + + local t1, t2, t3 = evo.id(3) + local e = evo.builder():set(f, { t1, t2 }):build() + + do + local l = evo.get(e, f) + l[1], l[2] = t2, t3 + evo.reindex(f) + end + + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == e) + assert(evo.search(f, t3) == e) + + evo.destroy(t1) + assert(evo.has(e, f)) + + evo.destroy(t2) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + + local l = { t1 } + local e = evo.builder():set(f, l):build() + + l[1] = t2 + evo.set(e, f, l) + + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == e) + + evo.destroy(t2) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():index():build() + + local t = evo.id() + + local l = { t } + local e = evo.builder():set(f, l):build() + + assert(evo.search(f, l) == e) + assert(evo.search(f, t) == nil) + + evo.set(f, evo.LINK) + + assert(evo.search(f, l) == nil) + assert(evo.search(f, t) == e) + + evo.remove(f, evo.LINK) + + assert(evo.search(f, l) == e) + assert(evo.search(f, t) == nil) + + evo.destroy(t) + assert(evo.get(e, f) == l) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, { t1, t2 }):build() + + evo.remove(f, evo.LINK) + + evo.destroy(t1) + assert(evo.has(e, f)) + + evo.set(e, f, { t2 }) + evo.set(f, evo.LINK) + assert(evo.search(f, t2) == e) + + evo.destroy(t2) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, { t1 }):build() + + evo.defer() + do + evo.destroy(t1) + evo.set(e, f, { t2 }) + end + evo.commit() + + do + local l = evo.get(e, f) + assert(#l == 1 and l[1] == t2) + end +end + +do + local f = evo.builder():link():build() + local p = evo.builder():prefab():build() + + local t = evo.id() + evo.set(p, f, { t }) + + local e1 = evo.clone(p) + local e2 = evo.clone(p) + + do + local l, c = evo.multi_search(f, t) + assert(c == 3 and l[1] == p and l[2] == e1 and l[3] == e2) + end + + evo.destroy(t) + assert(not evo.has(p, f) and not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t1, t2 = evo.id(2) + + local l1, c1 = evo.multi_spawn(2, { [f] = { t1 } }) + local l2, c2 = evo.multi_spawn(2, { [f] = { t2, t1 } }) + assert(c1 == 2 and c2 == 2) + + evo.destroy(t2) + assert(evo.alive_all(l1[1], l1[2])) + assert(not evo.alive_any(l2[1], l2[2])) + + evo.destroy(t1) + assert(not evo.alive_any(l1[1], l1[2])) +end + +do + local f = evo.builder():link():build() + local g = evo.builder():tag():build() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(g):build() + local e2 = evo.builder():set(g):set(f, t1):build() + + local q = evo.builder():include(g):build() + + evo.batch_set(q, f, { t1, t2 }) + + do + local l, c = evo.multi_search(f, t1) + assert(c == 2) + assert((l[1] == e1 and l[2] == e2) or (l[1] == e2 and l[2] == e1)) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 2) + assert((l[1] == e1 and l[2] == e2) or (l[1] == e2 and l[2] == e1)) + end + + evo.destroy(t2) + assert(evo.alive_all(e1, e2) and not evo.has(e1, f) and not evo.has(e2, f)) +end + +do + local f1, f2 = evo.id(2) + + local q = evo.builder():include(f1, f2):build() + local s = evo.builder():query(q):build() + + local e = evo.builder():set(f1, 42):set(f2, 21):build() + + evo.destroy(f2) + assert(not evo.alive_any(f2, q, s)) + assert(evo.alive(e) and evo.get(e, f1) == 42) +end + +do + local f1, f2, f3 = evo.id(3) + + local q1 = evo.builder():include(f1):exclude(f2):build() + local q2 = evo.builder():include(f1):variant(f2, f3):build() + local q3 = evo.builder():include(f1):exclude(f3):build() + local q4 = evo.builder():variant(f1, f3):build() + + evo.destroy(f2) + assert(not evo.alive_any(q1, q2)) + assert(evo.alive_all(q3, q4)) + + evo.destroy(f3) + assert(not evo.alive_any(q3, q4)) +end + +do + local f1, f2 = evo.id(2) + + local s = evo.builder():include(f1):exclude(f2):execute(function() end):build() + + evo.destroy(f2) + assert(not evo.alive(s)) +end + +do + local f1, f2, f3 = evo.id(3) + + local q1 = evo.builder():include(f1):build() + local q2 = evo.builder():include(f2, f1):exclude(f3):build() + local q3 = evo.builder():variant(f1):build() + + do + local l, c = evo.multi_search(evo.INCLUDES, f1) + assert(c == 2 and l[1] == q1 and l[2] == q2) + end + + assert(evo.search(evo.EXCLUDES, f3) == q2) + assert(evo.search(evo.VARIANTS, f1) == q3) + + evo.set(q2, evo.INCLUDES, { f2 }) + + do + local l, c = evo.multi_search(evo.INCLUDES, f1) + assert(c == 1 and l[1] == q1) + end + + evo.destroy(f1) + assert(not evo.alive_any(q1, q3)) + assert(evo.alive(q2)) +end + +do + local f1, f2 = evo.id(2) + local f3 = evo.builder():require(f1, f2):build() + + local e = evo.builder():set(f3, 42):build() + assert(evo.has_all(e, f1, f2, f3)) + + evo.destroy(f2) + assert(not evo.alive_any(f2, f3)) + assert(evo.alive(e) and evo.has(e, f1) and not evo.has_any(e, f2, f3)) + + local f4 = evo.builder():require(f1):build() + evo.set(e, f4) + assert(evo.has_all(e, f1, f4)) +end + +do + local f1 = evo.id() + local f2 = evo.builder():require(f1):destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + + local e1 = evo.builder():set(f2):build() + local e2 = evo.builder():set(f1):build() + + evo.destroy(f1) + assert(not evo.alive_any(f1, f2, e1)) + assert(evo.alive(e2) and not evo.has(e2, f1)) +end + +do + local f1 = evo.id() + local f2 = evo.builder():require(f1):build() + local f3 = evo.builder():require(f2):build() + local f4 = evo.builder():require(f3):destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + + local e1 = evo.builder():set(f3):build() + local e2 = evo.builder():set(f4):build() + + assert(evo.has_all(e1, f1, f2, f3)) + assert(evo.has_all(e2, f1, f2, f3, f4)) + + do + local l, c = evo.multi_search(evo.REQUIRES, f2) + assert(c == 1 and l[1] == f3) + end + + evo.destroy(f1) + assert(not evo.alive_any(f1, f2, f3, f4, e2)) + assert(evo.alive(e1) and evo.empty(e1)) +end + +do + local f1, f2 = evo.id(2) + + local q = evo.builder():include(f1, f2):build() + local s = evo.builder():query(q):build() + + evo.remove(q, evo.INCLUDES) + assert(evo.search(evo.INCLUDES, f1) == nil) + + evo.destroy(f1) + assert(evo.alive_all(q, s)) + + evo.set(q, evo.INCLUDES, { f2 }) + + evo.destroy(f2) + assert(not evo.alive_any(q, s)) +end + +do + evo.debug_mode(false) + + local f = evo.builder():link():build() + + local dead = evo.id() + evo.destroy(dead) + + local alive = evo.id() + assert(alive % 2 ^ 20 == dead % 2 ^ 20) + + local e1 = evo.builder():set(f, { dead, alive }):build() + local e2 = evo.builder():set(f, alive):build() + + assert(evo.search(f, dead) == nil) + + do + local l, c = evo.multi_search(f, alive) + assert(c == 2 and l[1] == e1 and l[2] == e2) + end + + evo.remove(e1, f) + assert(evo.search(f, alive) == e2) + + do + local l, c = evo.multi_search(f, alive) + assert(c == 1 and l[1] == e2) + end + + evo.destroy(e2) + assert(evo.search(f, alive) == nil) + + local e3 = evo.builder():set(f, { dead, alive }):build() + + evo.destroy(e3) + assert(evo.search(f, alive) == nil) + + local e4 = evo.builder():set(f, alive):build() + + evo.destroy(alive) + assert(evo.alive(e4) and not evo.has(e4, f)) + + evo.debug_mode(true) +end diff --git a/evolved.lua b/evolved.lua index ac97609..94f2848 100644 --- a/evolved.lua +++ b/evolved.lua @@ -187,6 +187,7 @@ __builder_mt.__index = __builder_mt local __lua_next = next local __lua_select = select +local __lua_type = type ---@type fun(nseq?: integer): table local __lua_table_new = (function() @@ -842,6 +843,27 @@ function __list_fns.dup(list, size) return dup_list, list_size end +---@generic V +---@param list_a V[] +---@param list_b V[] +---@return boolean +---@nodiscard +function __list_fns.eq(list_a, list_b) + local list_size = #list_a + + if list_size ~= #list_b then + return false + end + + for list_index = 1, list_size do + if list_a[list_index] ~= list_b[list_index] then + return false + end + end + + return true +end + ---@generic V ---@param list V[] ---@param item V @@ -1347,6 +1369,7 @@ local __safe_tbls = { ---@field package __next_entities table ---@field package __entity_components table ---@field package __target_fragment? evolved.fragment +---@field package __combined_keys boolean local __search_index_fns = {} @@ -1361,9 +1384,40 @@ function __search_index_fns.new(fragment) __next_entities = {}, __entity_components = {}, __target_fragment = __evolved_has(fragment, __LINK) and fragment or nil, + __combined_keys = false, } end +---@param index evolved.search_index +function __search_index_fns.combine_keys(index) + if index.__combined_keys then + return + end + + local entity_components = index.__entity_components + + local old_prev_entities = index.__prev_entities + local old_next_entities = index.__next_entities + + local new_prev_entities = {} ---@type table + local new_next_entities = {} ---@type table + + for entity_primary, prev_entity in __lua_next, old_prev_entities do + local component = entity_components[entity_primary] + + local key_offset = (component % 2 ^ 20) * 2 ^ 20 + local entity_key = key_offset + entity_primary + + new_prev_entities[entity_key] = prev_entity + new_next_entities[entity_key] = old_next_entities[entity_primary] + end + + index.__prev_entities = new_prev_entities + index.__next_entities = new_next_entities + + index.__combined_keys = true +end + ---@param target evolved.entity ---@param fragment evolved.fragment function __search_index_fns.link(target, fragment) @@ -1403,6 +1457,34 @@ function __search_index_fns.insert(index, entity, component) local entity_primary = entity % 2 ^ 20 + if index.__target_fragment and __lua_type(component) == 'table' then + if not index.__combined_keys then + __search_index_fns.combine_keys(index) + end + + ---@type evolved.entity[], integer + local target_list, target_count = __list_fns.dup(component) + + for target_index = 1, target_count do + __search_index_fns.insert_node(index, entity, target_list[target_index]) + end + + index.__entity_components[entity_primary] = target_list + else + __search_index_fns.insert_node(index, entity, component) + index.__entity_components[entity_primary] = component + end +end + +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.insert_node(index, entity, component) + local target_fragment = index.__target_fragment + + local key_offset = index.__combined_keys and (component % 2 ^ 20) * 2 ^ 20 or 0 + local entity_key = key_offset + entity % 2 ^ 20 + local head_entities = index.__head_entities local prev_entities = index.__prev_entities local next_entities = index.__next_entities @@ -1411,37 +1493,34 @@ function __search_index_fns.insert(index, entity, component) local head_entity = head_entities[component] if head_entity then - local head_entity_primary = head_entity % 2 ^ 20 + if prev_entities[entity_key] then + -- this target is duplicated in the list and has already been linked + return + end - local tail_entity = prev_entities[head_entity_primary] - local tail_entity_primary = tail_entity % 2 ^ 20 + local head_entity_key = key_offset + head_entity % 2 ^ 20 - prev_entities[entity_primary] = tail_entity - next_entities[entity_primary] = head_entity + local tail_entity = prev_entities[head_entity_key] + local tail_entity_key = key_offset + tail_entity % 2 ^ 20 - prev_entities[head_entity_primary] = entity - next_entities[tail_entity_primary] = entity - else - local target_fragment = index.__target_fragment - - if __debug_mode and target_fragment then - if __freelist_ids[component % 2 ^ 20] ~= component then - __debug_fns.error_fmt('the target (%s) of the linked fragment (%s) is not alive', - __id_name(component), __id_name(target_fragment)) - end - end + prev_entities[entity_key] = tail_entity + next_entities[entity_key] = head_entity + prev_entities[head_entity_key] = entity + next_entities[tail_entity_key] = entity + elseif not target_fragment or __freelist_ids[component % 2 ^ 20] == component then head_entities[component] = entity - prev_entities[entity_primary] = entity - next_entities[entity_primary] = entity + prev_entities[entity_key] = entity + next_entities[entity_key] = entity if target_fragment then __search_index_fns.link(component, target_fragment) end + elseif __debug_mode and target_fragment then + __debug_fns.error_fmt('the target (%s) of the linked fragment (%s) is not alive', + __id_name(component), __id_name(target_fragment)) end - - index.__entity_components[entity_primary] = component end ---@param index evolved.search_index @@ -1456,49 +1535,98 @@ function __search_index_fns.remove(index, entity) return end + if index.__target_fragment and __lua_type(component) == 'table' then + ---@type evolved.entity[], integer + local target_list, target_count = component, #component + + for target_index = 1, target_count do + __search_index_fns.remove_node(index, entity, target_list[target_index]) + end + + index.__entity_components[entity_primary] = nil + else + __search_index_fns.remove_node(index, entity, component) + index.__entity_components[entity_primary] = nil + end +end + +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.remove_node(index, entity, component) + local target_fragment = index.__target_fragment + + local key_offset = index.__combined_keys and (component % 2 ^ 20) * 2 ^ 20 or 0 + local entity_key = key_offset + entity % 2 ^ 20 + local head_entities = index.__head_entities local prev_entities = index.__prev_entities local next_entities = index.__next_entities - local prev_entity = prev_entities[entity_primary] - local next_entity = next_entities[entity_primary] + ---@type evolved.entity? + local head_entity = head_entities[component] + + if not head_entity then + -- this target is dead or has no links anymore + return + end + + ---@type evolved.entity? + local next_entity = next_entities[entity_key] + + if not next_entity then + -- this target is duplicated in the list and has already been unlinked + return + end if next_entity == entity then head_entities[component] = nil - local target_fragment = index.__target_fragment - if target_fragment then __search_index_fns.unlink(component, target_fragment) end else - local prev_entity_primary = prev_entity % 2 ^ 20 - local next_entity_primary = next_entity % 2 ^ 20 + local prev_entity = prev_entities[entity_key] - prev_entities[next_entity_primary] = prev_entity - next_entities[prev_entity_primary] = next_entity + local prev_entity_key = key_offset + prev_entity % 2 ^ 20 + local next_entity_key = key_offset + next_entity % 2 ^ 20 - if head_entities[component] == entity then + prev_entities[next_entity_key] = prev_entity + next_entities[prev_entity_key] = next_entity + + if head_entity == entity then head_entities[component] = next_entity end end - prev_entities[entity_primary] = nil - next_entities[entity_primary] = nil - - index.__entity_components[entity_primary] = nil + prev_entities[entity_key] = nil + next_entities[entity_key] = nil end ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component function __search_index_fns.assign(index, entity, component) + local target_fragment = index.__target_fragment local entity_components = index.__entity_components + local entity_component = entity_components[entity % 2 ^ 20] + + if component == entity_component then + -- nothing to reassign + return + end - if entity_components[entity % 2 ^ 20] ~= component then - __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, component) + if target_fragment + and __lua_type(component) == 'table' + and __lua_type(entity_component) == 'table' + and __list_fns.eq(component, entity_component) + then + -- the list has the same targets, nothing to reassign + return end + + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, component) end ---@param index evolved.search_index @@ -1528,13 +1656,23 @@ end ---@param first integer ---@param last integer function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) + local target_fragment = index.__target_fragment local entity_components = index.__entity_components for place = first, last do local entity = entity_list[place] local component = component_list[place] - - if entity_components[entity % 2 ^ 20] ~= component then + local entity_component = entity_components[entity % 2 ^ 20] + + if component == entity_component then + -- nothing to reassign + elseif target_fragment + and __lua_type(component) == 'table' + and __lua_type(entity_component) == 'table' + and __list_fns.eq(component, entity_component) + then + -- the list has the same targets, nothing to reassign + else __search_index_fns.remove(index, entity) __search_index_fns.insert(index, entity, component) end @@ -6914,11 +7052,13 @@ function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, local head_entity = head_entities[component] if head_entity then + local key_offset = fragment_search_index.__combined_keys and (component % 2 ^ 20) * 2 ^ 20 or 0 + local entity = head_entity repeat out_entity_list[out_entity_first + entity_count] = entity - entity = next_entities[entity % 2 ^ 20] + entity = next_entities[key_offset + entity % 2 ^ 20] entity_count = entity_count + 1 until entity == head_entity end @@ -8043,15 +8183,23 @@ __evolved_set(__DISABLED, __EXPLICIT) __evolved_set(__INCLUDES, __DEFAULT, __list_fns.new()) __evolved_set(__INCLUDES, __DUPLICATE, __list_fns.dup) +__evolved_set(__INCLUDES, __LINK) +__evolved_set(__INCLUDES, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__EXCLUDES, __DEFAULT, __list_fns.new()) __evolved_set(__EXCLUDES, __DUPLICATE, __list_fns.dup) +__evolved_set(__EXCLUDES, __LINK) +__evolved_set(__EXCLUDES, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__VARIANTS, __DEFAULT, __list_fns.new()) __evolved_set(__VARIANTS, __DUPLICATE, __list_fns.dup) +__evolved_set(__VARIANTS, __LINK) +__evolved_set(__VARIANTS, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__REQUIRES, __DEFAULT, __list_fns.new()) __evolved_set(__REQUIRES, __DUPLICATE, __list_fns.dup) +__evolved_set(__REQUIRES, __LINK) +__evolved_set(__REQUIRES, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__ON_SET, __UNIQUE) __evolved_set(__ON_ASSIGN, __UNIQUE) From a7169a4cd27928ecb2b864a90ff76032e63dc50e Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 18 Oct 2026 18:39:24 +0700 Subject: [PATCH 18/48] custom index extraction --- README.md | 69 +++++- develop/ROADMAP.md | 2 - develop/all.lua | 1 + develop/testing/extract_tests.lua | 376 ++++++++++++++++++++++++++++++ evolved.d.tl | 2 + evolved.lua | 149 +++++++----- 6 files changed, 544 insertions(+), 55 deletions(-) create mode 100644 develop/testing/extract_tests.lua diff --git a/README.md b/README.md index 3823ef0..fb65c94 100644 --- a/README.md +++ b/README.md @@ -1215,11 +1215,38 @@ evolved.reindex(team) assert(evolved.search(team, 'blue') == unit) ``` +Sometimes the component itself is not a good key. For example, a component can be a table, and you want to find entities by one of its fields, or names should be found regardless of their case. In such cases, use the [`evolved.EXTRACT`](#evolvedextract) fragment trait. This trait expects a function that takes a component and returns a value to be indexed instead of it. Components themselves are stored as is, only the index uses the extracted values. If the function returns `nil`, the entity is not indexed at all. + +```lua +local evolved = require 'evolved' + +local unit_info = evolved.builder() + :index() + :extract(function(info) return info.team end) + :build() + +local unit1 = evolved.builder() + :set(unit_info, { team = 'red', health = 100 }) + :build() + +local unit2 = evolved.builder() + :set(unit_info, { team = 'blue', health = 50 }) + :build() + +assert(evolved.search(unit_info, 'red') == unit1) +assert(evolved.search(unit_info, 'blue') == unit2) +``` + +The function is called every time a component of the fragment is inserted or assigned, including reindexing, and the index remembers the extracted values. Removals and destructions use these remembered values, so the function is not called for them. The trait can be added, changed, or removed at any time, and the index is rebuilt each time. + +> [!NOTE] +> The extracted value must depend only on the component itself. The index is not updated when other fragments of the entity change, so a function that reads them will leave the index outdated. For the same reason, the function should not call any library functions. Tables changed in place bypass the index too, so set them again or use the [`evolved.reindex`](#evolvedreindex) function after such changes. + > [!NOTE] > Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) or [`evolved.LINK`](#evolvedlink) trait is an error. > [!NOTE] -> 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. +> Components, or values extracted from them, are used as keys in the index, so table keys are indexed by reference, and searching by another table with the same content will not find anything. Also, [Fragment Tags](#fragment-tags) are not indexed at all, and neither are `nil` and `NaN` keys. > [!NOTE] > 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. @@ -1473,6 +1500,32 @@ evolved.destroy(pillar1) assert(evolved.alive(rope) and not evolved.has(rope, attached_to)) ``` +The [`evolved.EXTRACT`](#evolvedextract) fragment trait works with linked fragments too, so components can store links among other data. In this case, the function should return an entity or a list of entities to link to, or `nil` if the component does not link to anything. Link policies are still applied to whole components, so with the default policy, the whole component is removed, not only the link inside it. + +```lua +local evolved = require 'evolved' + +local equipment = evolved.builder() + :link() + :extract(function(item) return item.owner end) + :build() + +local player = evolved.builder() + :build() + +local sword = evolved.builder() + :set(equipment, { owner = player, slot = 'hand' }) + :build() + +assert(evolved.search(equipment, player) == sword) + +-- destroy the player that the sword links to +evolved.destroy(player) + +-- the whole equipment component is removed, not only the owner field +assert(evolved.alive(sword) and not evolved.has(sword, equipment)) +``` + The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are linked this way with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy. So you can also find all queries that use some fragment, or all fragments that require it. ```lua @@ -1741,6 +1794,7 @@ component_mapper :: {chunk, integer, integer} default :: component duplicate :: {component -> component} +extract :: {component -> component?} realloc :: {storage?, integer, integer -> storage?} compmove :: {storage, integer, integer, integer, storage} @@ -1775,6 +1829,7 @@ INTERNAL :: fragment DEFAULT :: fragment DUPLICATE :: fragment +EXTRACT :: fragment REALLOC :: fragment COMPMOVE :: fragment @@ -1940,6 +1995,7 @@ builder_mt:internal :: builder builder_mt:default :: component -> builder builder_mt:duplicate :: {component -> component} -> builder +builder_mt:extract :: {component -> component?} -> builder builder_mt:realloc :: {storage?, integer, integer -> storage?} -> builder builder_mt:compmove :: {storage, integer, integer, integer, storage} -> builder @@ -1977,6 +2033,7 @@ builder_mt:destruction_policy :: id -> builder - The [`evolved.GROUP`](#evolvedgroup) and [`evolved.QUERY`](#evolvedquery) fragments are now linked, so destroying a group or a query also destroys the systems that use them - Components of [linked fragments](#linked-fragments) can be lists of entities, which link to every entity in them - The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are now linked, so destroying a fragment also destroys the queries and fragments that use it +- Added the new [`evolved.EXTRACT`](#evolvedextract) fragment trait that allows indexing and linking by values extracted from components ### v1.12.0 @@ -2083,6 +2140,8 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.DUPLICATE` +### `evolved.EXTRACT` + ### `evolved.REALLOC` ### `evolved.COMPMOVE` @@ -2920,6 +2979,14 @@ function evolved.builder_mt:default(default) end function evolved.builder_mt:duplicate(duplicate) end ``` +#### `evolved.builder_mt:extract` + +```lua +---@param extract evolved.extract +---@return evolved.builder builder +function evolved.builder_mt:extract(extract) end +``` + #### `evolved.builder_mt:realloc` ```lua diff --git a/develop/ROADMAP.md b/develop/ROADMAP.md index 4e0205d..057aff1 100644 --- a/develop/ROADMAP.md +++ b/develop/ROADMAP.md @@ -2,8 +2,6 @@ ## Backlog -- custom indexing function -- custom linking function - observers and events - use compact prefix-tree for chunks diff --git a/develop/all.lua b/develop/all.lua index 734bd50..97a4e8e 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -6,6 +6,7 @@ require 'develop.testing.cancel_tests' require 'develop.testing.clone_tests' require 'develop.testing.depth_tests' require 'develop.testing.destroy_tests' +require 'develop.testing.extract_tests' require 'develop.testing.link_tests' require 'develop.testing.locate_tests' require 'develop.testing.lookup_tests' diff --git a/develop/testing/extract_tests.lua b/develop/testing/extract_tests.lua new file mode 100644 index 0000000..5ed5726 --- /dev/null +++ b/develop/testing/extract_tests.lua @@ -0,0 +1,376 @@ +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 +end + +---@param c table +local function extract_team(c) + return c.team +end + +do + local f = evo.builder():extract(extract_team):build() + assert(evo.get(f, evo.EXTRACT) == extract_team) + assert(not evo.has_any(f, evo.INDEX, evo.LINK)) +end + +do + local f = evo.builder():index():extract(string.lower):build() + + local e1 = evo.spawn { [f] = 'Hello' } + local e2 = evo.spawn { [f] = 'HELLO' } + local e3 = evo.spawn { [f] = 'World' } + + check_search(f, 'hello', e1, e2) + check_search(f, 'world', e3) + check_search(f, 'Hello') + + evo.set(e3, f, 'hElLo') + check_search(f, 'hello', e1, e2, e3) + check_search(f, 'world') + + -- components are stored as is, only the index uses extracted values + assert(evo.get(e3, f) == 'hElLo') +end + +do + local f = evo.builder():index():extract(extract_team):build() + + local e1 = evo.spawn { [f] = { team = 'red' } } + local e2 = evo.spawn { [f] = { team = 'red' } } + local e3 = evo.spawn { [f] = { team = 'blue' } } + + check_search(f, 'red', e1, e2) + check_search(f, 'blue', e3) + + -- a new table with the same extracted value keeps the entity position + evo.set(e1, f, { team = 'red' }) + check_search(f, 'red', e1, e2) + + evo.set(e1, f, { team = 'blue' }) + check_search(f, 'red', e2) + check_search(f, 'blue', e3, e1) + + evo.remove(e3, f) + check_search(f, 'blue', e1) + + evo.destroy(e1) + check_search(f, 'blue') + + evo.clear(e2) + check_search(f, 'red') +end + +do + local f = evo.builder():index():extract(extract_team):build() + + -- nil and NaN extracted values are not indexed + local e1 = evo.spawn { [f] = {} } + local e2 = evo.spawn { [f] = { team = 'red' } } + local e3 = evo.spawn { [f] = { team = 0 / 0 } } + check_search(f, 'red', e2) + + evo.set(e1, f, { team = 'red' }) + evo.set(e3, f, { team = 'red' }) + check_search(f, 'red', e2, e1, e3) + + evo.set(e2, f, {}) + evo.set(e3, f, { team = 0 / 0 }) + check_search(f, 'red', e1) + + evo.set(e2, f, {}) + check_search(f, 'red', e1) + + evo.destroy(e1, e2, e3) + check_search(f, 'red') +end + +do + local f = evo.builder():index():extract(extract_team):build() + + local c1, c2 = { team = 'red' }, { team = 'red' } + local e1 = evo.spawn { [f] = c1 } + local e2 = evo.spawn { [f] = c2 } + + -- in-place changes bypass the index + c1.team = 'blue' + check_search(f, 'red', e1, e2) + check_search(f, 'blue') + + -- setting the same table again updates the index + evo.set(e1, f, c1) + check_search(f, 'red', e2) + check_search(f, 'blue', e1) + + -- reindexing updates the index too + c2.team = 'blue' + evo.reindex(f) + check_search(f, 'red') + check_search(f, 'blue', e1, e2) + + -- removing uses the indexed value, not the changed component + c1.team = 'green' + evo.remove(e1, f) + check_search(f, 'blue', e2) + check_search(f, 'green') +end + +do + local f = evo.builder():index():build() + + local c1, c2 = { team = 'red' }, { team = 'blue' } + local e1 = evo.spawn { [f] = c1 } + local e2 = evo.spawn { [f] = c2 } + + check_search(f, c1, e1) + check_search(f, 'red') + + -- setting the trait rebuilds the index + evo.set(f, evo.EXTRACT, extract_team) + check_search(f, c1) + check_search(f, 'red', e1) + check_search(f, 'blue', e2) + + -- changing the trait rebuilds the index + evo.set(f, evo.EXTRACT, function(c) return c.team == 'red' end) + check_search(f, 'red') + check_search(f, true, e1) + check_search(f, false, e2) + + -- removing the trait rebuilds the index + evo.remove(f, evo.EXTRACT) + check_search(f, true) + check_search(f, c1, e1) + check_search(f, c2, e2) +end + +do + local f = evo.builder():extract(extract_team):build() + + local e1 = evo.spawn { [f] = { team = 'red' } } + local e2 = evo.spawn { [f] = { team = 'blue' } } + + -- the trait is used as soon as the fragment is indexed + evo.set(f, evo.INDEX) + check_search(f, 'red', e1) + check_search(f, 'blue', e2) + + evo.remove(f, evo.INDEX) + evo.set(e1, f, { team = 'blue' }) + + evo.set(f, evo.INDEX) + check_search(f, 'red') + check_search(f, 'blue', e1, e2) +end + +do + local extract_count = 0 + + local f = evo.builder():index():extract(function(c) + extract_count = extract_count + 1 + return c.team + end):build() + + local g = evo.builder():build() + + local e = evo.spawn { [f] = { team = 'red' } } + assert(extract_count == 1) + + evo.set(e, f, { team = 'blue' }) + assert(extract_count == 2) + + -- moving between chunks does not extract values again + evo.set(e, g, 42) + evo.remove(e, g) + assert(extract_count == 2) + + -- removing uses the indexed value, so nothing is extracted + evo.remove(e, f) + assert(extract_count == 2) + + evo.set(e, f, { team = 'red' }) + assert(extract_count == 3) + + evo.destroy(e) + assert(extract_count == 3) + check_search(f, 'red') +end + +do + local f = evo.builder():index():extract(extract_team):build() + + local prefab = evo.spawn { [f] = { team = 'red' } } + + local clone = evo.clone(prefab) + check_search(f, 'red', prefab, clone) + + local clone_list = evo.multi_clone(2, prefab, { [f] = { team = 'blue' } }) + check_search(f, 'blue', clone_list[1], clone_list[2]) + + local spawn_list = evo.multi_spawn(2, { [f] = { team = 'green' } }) + check_search(f, 'green', spawn_list[1], spawn_list[2]) + + local q = evo.builder():include(f):build() + evo.batch_set(q, f, { team = 'yellow' }) + check_search(f, 'red') + check_search(f, 'blue') + check_search(f, 'green') + assert(select(2, evo.multi_search(f, 'yellow')) == 6) + + evo.batch_destroy(q) + check_search(f, 'yellow') +end + +do + local f = evo.builder() + :index() + :default({ team = 'none' }) + :duplicate(function(c) return { team = c.team } end) + :extract(extract_team) + :build() + + local e1 = evo.spawn { [f] = { team = 'red' } } + local e2 = evo.builder():set(f):build() + check_search(f, 'red', e1) + check_search(f, 'none', e2) + + evo.defer() + local e3 = evo.spawn { [f] = { team = 'red' } } + evo.set(e2, f, { team = 'red' }) + check_search(f, 'red', e1) + evo.commit() + check_search(f, 'red', e1, e3, e2) + check_search(f, 'none') +end + +do + local owner = evo.builder():link():extract(function(c) return c.owner end):build() + + local player1, player2 = evo.id(2) + + local sword = evo.spawn { [owner] = { owner = player1, slot = 1 } } + local shield = evo.spawn { [owner] = { owner = player1, slot = 2 } } + local bow = evo.spawn { [owner] = { owner = player2, slot = 1 } } + + check_search(owner, player1, sword, shield) + check_search(owner, player2, bow) + + evo.destroy(player1) + assert(evo.alive_all(sword, shield, bow)) + assert(not evo.has(sword, owner) and not evo.has(shield, owner)) + assert(evo.get(bow, owner).owner == player2) + + check_search(owner, player1) + check_search(owner, player2, bow) +end + +do + local owner = evo.builder():link():extract(function(c) return c.owner end):build() + + local player = evo.id() + + -- nil extracted values do not link to anything + local item = evo.spawn { [owner] = { slot = 1 } } + check_search(owner, player) + + evo.set(item, owner, { owner = player, slot = 1 }) + check_search(owner, player, item) + + evo.set(item, owner, { slot = 1 }) + check_search(owner, player) + + evo.destroy(player) + assert(evo.has(item, owner)) +end + +do + local parent = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + :extract(function(c) return c.parent end) + :build() + + local root = evo.id() + + local child = evo.spawn { [parent] = { parent = root } } + local grandchild = evo.spawn { [parent] = { parent = child } } + local orphan = evo.spawn { [parent] = {} } + + evo.destroy(root) + assert(not evo.alive_any(child, grandchild)) + assert(evo.alive(orphan) and evo.has(orphan, parent)) +end + +do + local attached_to = evo.builder():link():extract(function(c) return c.pillars end):build() + + local p1, p2, p3 = evo.id(3) + + local c = { pillars = { p1, p2 } } + local rope = evo.spawn { [attached_to] = c } + + check_search(attached_to, p1, rope) + check_search(attached_to, p2, rope) + check_search(attached_to, p3) + + -- the extracted list is changed in place, so set the component again + c.pillars[2] = p3 + evo.set(rope, attached_to, c) + check_search(attached_to, p1, rope) + check_search(attached_to, p2) + check_search(attached_to, p3, rope) + + evo.destroy(p2) + assert(evo.has(rope, attached_to)) + + evo.destroy(p1) + assert(evo.alive(rope) and not evo.has(rope, attached_to)) + check_search(attached_to, p3) +end + +do + local f = evo.builder():link():build() + + local t1, t2, t3 = evo.id(3) + + local e = evo.spawn { [f] = { t1, t2, t3 } } + check_search(f, t1, e) + check_search(f, t2, e) + + -- setting the trait relinks existing components + evo.set(f, evo.EXTRACT, function(c) return c[1] end) + check_search(f, t1, e) + check_search(f, t2) + + evo.destroy(t2) + assert(evo.has(e, f)) + + evo.set(e, f, { t1, t3 }) + + -- removing the trait relinks existing components + evo.remove(f, evo.EXTRACT) + check_search(f, t1, e) + check_search(f, t3, e) + + evo.destroy(t3) + assert(evo.alive(e) and not evo.has(e, f)) +end diff --git a/evolved.d.tl b/evolved.d.tl index f34d169..acb2656 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -115,6 +115,7 @@ default: function(self: Builder, default: Component): Builder duplicate: function(self: Builder, duplicate: function(Component): Component): Builder + extract: function(self: Builder, extract: function(Component): Extract | nil): Builder realloc: function(self: Builder, realloc: function({ Component } | nil, integer, integer): { Component } | nil): Builder compmove: function(self: Builder, compmove: function({ Component }, integer, integer, integer, { Component })): Builder @@ -155,6 +156,7 @@ DEFAULT: Fragment DUPLICATE: Fragment + EXTRACT: Fragment REALLOC: Fragment COMPMOVE: Fragment diff --git a/evolved.lua b/evolved.lua index 94f2848..1995658 100644 --- a/evolved.lua +++ b/evolved.lua @@ -42,6 +42,7 @@ local evolved = { ---@alias evolved.default evolved.component ---@alias evolved.duplicate fun(component: evolved.component): evolved.component +---@alias evolved.extract fun(component: evolved.component): evolved.component? ---@alias evolved.realloc fun(src?: evolved.storage, src_size: integer, dst_size: integer): evolved.storage? ---@alias evolved.compmove fun(src: evolved.storage, f: integer, e: integer, t: integer, dst: evolved.storage) @@ -1143,6 +1144,7 @@ local __INTERNAL = __acquire_id() local __DEFAULT = __acquire_id() local __DUPLICATE = __acquire_id() +local __EXTRACT = __acquire_id() local __REALLOC = __acquire_id() local __COMPMOVE = __acquire_id() @@ -1364,12 +1366,13 @@ local __safe_tbls = { --- ---@class (exact) evolved.search_index +---@field package __combined_keys boolean ---@field package __head_entities table ---@field package __prev_entities table ---@field package __next_entities table ----@field package __entity_components table +---@field package __entity_extracts table ---@field package __target_fragment? evolved.fragment ----@field package __combined_keys boolean +---@field package __extract_component? evolved.extract local __search_index_fns = {} @@ -1379,12 +1382,13 @@ local __search_index_fns = {} function __search_index_fns.new(fragment) ---@type evolved.search_index return { + __combined_keys = false, __head_entities = {}, __prev_entities = {}, __next_entities = {}, - __entity_components = {}, + __entity_extracts = {}, __target_fragment = __evolved_has(fragment, __LINK) and fragment or nil, - __combined_keys = false, + __extract_component = __evolved_get(fragment, __EXTRACT), } end @@ -1394,7 +1398,7 @@ function __search_index_fns.combine_keys(index) return end - local entity_components = index.__entity_components + local entity_extracts = index.__entity_extracts local old_prev_entities = index.__prev_entities local old_next_entities = index.__next_entities @@ -1403,9 +1407,9 @@ function __search_index_fns.combine_keys(index) local new_next_entities = {} ---@type table for entity_primary, prev_entity in __lua_next, old_prev_entities do - local component = entity_components[entity_primary] + local extracted = entity_extracts[entity_primary] - local key_offset = (component % 2 ^ 20) * 2 ^ 20 + local key_offset = (extracted % 2 ^ 20) * 2 ^ 20 local entity_key = key_offset + entity_primary new_prev_entities[entity_key] = prev_entity @@ -1450,39 +1454,52 @@ end ---@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 + local extract_component = index.__extract_component + + local extracted = component + + if extract_component then + extracted = extract_component(extracted) end - local entity_primary = entity % 2 ^ 20 + __search_index_fns.insert_extracted(index, entity, extracted) +end - if index.__target_fragment and __lua_type(component) == 'table' then +---@param index evolved.search_index +---@param entity evolved.entity +---@param extracted? evolved.component +function __search_index_fns.insert_extracted(index, entity, extracted) + if extracted == nil or extracted ~= extracted then + -- nil and NaN values cannot be indexed + return + end + + if index.__target_fragment and __lua_type(extracted) == 'table' then if not index.__combined_keys then __search_index_fns.combine_keys(index) end ---@type evolved.entity[], integer - local target_list, target_count = __list_fns.dup(component) + local target_list, target_count = __list_fns.dup(extracted) for target_index = 1, target_count do __search_index_fns.insert_node(index, entity, target_list[target_index]) end - index.__entity_components[entity_primary] = target_list + index.__entity_extracts[entity % 2 ^ 20] = target_list else - __search_index_fns.insert_node(index, entity, component) - index.__entity_components[entity_primary] = component + __search_index_fns.insert_node(index, entity, extracted) + index.__entity_extracts[entity % 2 ^ 20] = extracted end end ---@param index evolved.search_index ---@param entity evolved.entity ----@param component evolved.component -function __search_index_fns.insert_node(index, entity, component) +---@param extracted evolved.component +function __search_index_fns.insert_node(index, entity, extracted) local target_fragment = index.__target_fragment - local key_offset = index.__combined_keys and (component % 2 ^ 20) * 2 ^ 20 or 0 + local key_offset = index.__combined_keys and (extracted % 2 ^ 20) * 2 ^ 20 or 0 local entity_key = key_offset + entity % 2 ^ 20 local head_entities = index.__head_entities @@ -1490,7 +1507,7 @@ function __search_index_fns.insert_node(index, entity, component) local next_entities = index.__next_entities ---@type evolved.entity? - local head_entity = head_entities[component] + local head_entity = head_entities[extracted] if head_entity then if prev_entities[entity_key] then @@ -1508,18 +1525,18 @@ function __search_index_fns.insert_node(index, entity, component) prev_entities[head_entity_key] = entity next_entities[tail_entity_key] = entity - elseif not target_fragment or __freelist_ids[component % 2 ^ 20] == component then - head_entities[component] = entity + elseif not target_fragment or __freelist_ids[extracted % 2 ^ 20] == extracted then + head_entities[extracted] = entity prev_entities[entity_key] = entity next_entities[entity_key] = entity if target_fragment then - __search_index_fns.link(component, target_fragment) + __search_index_fns.link(extracted, target_fragment) end elseif __debug_mode and target_fragment then __debug_fns.error_fmt('the target (%s) of the linked fragment (%s) is not alive', - __id_name(component), __id_name(target_fragment)) + __id_name(extracted), __id_name(target_fragment)) end end @@ -1529,34 +1546,34 @@ function __search_index_fns.remove(index, entity) local entity_primary = entity % 2 ^ 20 ---@type evolved.component? - local component = index.__entity_components[entity_primary] + local extracted = index.__entity_extracts[entity_primary] - if component == nil then + if extracted == nil then return end - if index.__target_fragment and __lua_type(component) == 'table' then + if index.__target_fragment and __lua_type(extracted) == 'table' then ---@type evolved.entity[], integer - local target_list, target_count = component, #component + local target_list, target_count = extracted, #extracted for target_index = 1, target_count do __search_index_fns.remove_node(index, entity, target_list[target_index]) end - index.__entity_components[entity_primary] = nil + index.__entity_extracts[entity_primary] = nil else - __search_index_fns.remove_node(index, entity, component) - index.__entity_components[entity_primary] = nil + __search_index_fns.remove_node(index, entity, extracted) + index.__entity_extracts[entity_primary] = nil end end ---@param index evolved.search_index ---@param entity evolved.entity ----@param component evolved.component -function __search_index_fns.remove_node(index, entity, component) +---@param extracted evolved.component +function __search_index_fns.remove_node(index, entity, extracted) local target_fragment = index.__target_fragment - local key_offset = index.__combined_keys and (component % 2 ^ 20) * 2 ^ 20 or 0 + local key_offset = index.__combined_keys and (extracted % 2 ^ 20) * 2 ^ 20 or 0 local entity_key = key_offset + entity % 2 ^ 20 local head_entities = index.__head_entities @@ -1564,7 +1581,7 @@ function __search_index_fns.remove_node(index, entity, component) local next_entities = index.__next_entities ---@type evolved.entity? - local head_entity = head_entities[component] + local head_entity = head_entities[extracted] if not head_entity then -- this target is dead or has no links anymore @@ -1580,10 +1597,10 @@ function __search_index_fns.remove_node(index, entity, component) end if next_entity == entity then - head_entities[component] = nil + head_entities[extracted] = nil if target_fragment then - __search_index_fns.unlink(component, target_fragment) + __search_index_fns.unlink(extracted, target_fragment) end else local prev_entity = prev_entities[entity_key] @@ -1595,7 +1612,7 @@ function __search_index_fns.remove_node(index, entity, component) next_entities[prev_entity_key] = next_entity if head_entity == entity then - head_entities[component] = next_entity + head_entities[extracted] = next_entity end end @@ -1608,25 +1625,33 @@ end ---@param component evolved.component function __search_index_fns.assign(index, entity, component) local target_fragment = index.__target_fragment - local entity_components = index.__entity_components - local entity_component = entity_components[entity % 2 ^ 20] + local extract_component = index.__extract_component + local entity_extracts = index.__entity_extracts + + local new_extracted = component - if component == entity_component then + if extract_component then + new_extracted = extract_component(new_extracted) + end + + local old_extracted = entity_extracts[entity % 2 ^ 20] + + if new_extracted == old_extracted then -- nothing to reassign return end if target_fragment - and __lua_type(component) == 'table' - and __lua_type(entity_component) == 'table' - and __list_fns.eq(component, entity_component) + and __lua_type(new_extracted) == 'table' + and __lua_type(old_extracted) == 'table' + and __list_fns.eq(new_extracted, old_extracted) then -- the list has the same targets, nothing to reassign return end __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, component) + __search_index_fns.insert_extracted(index, entity, new_extracted) end ---@param index evolved.search_index @@ -1657,24 +1682,32 @@ end ---@param last integer function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) local target_fragment = index.__target_fragment - local entity_components = index.__entity_components + local extract_component = index.__extract_component + local entity_extracts = index.__entity_extracts for place = first, last do local entity = entity_list[place] local component = component_list[place] - local entity_component = entity_components[entity % 2 ^ 20] - if component == entity_component then + local new_extracted = component + + if extract_component then + new_extracted = extract_component(new_extracted) + end + + local old_extracted = entity_extracts[entity % 2 ^ 20] + + if new_extracted == old_extracted then -- nothing to reassign elseif target_fragment - and __lua_type(component) == 'table' - and __lua_type(entity_component) == 'table' - and __list_fns.eq(component, entity_component) + and __lua_type(new_extracted) == 'table' + and __lua_type(old_extracted) == 'table' + and __list_fns.eq(new_extracted, old_extracted) then -- the list has the same targets, nothing to reassign else __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, component) + __search_index_fns.insert_extracted(index, entity, new_extracted) end end end @@ -7907,6 +7940,12 @@ function __builder_mt:duplicate(duplicate) return self:set(__DUPLICATE, duplicate) end +---@param extract evolved.extract +---@return evolved.builder builder +function __builder_mt:extract(extract) + return self:set(__EXTRACT, extract) +end + ---@param realloc evolved.realloc ---@return evolved.builder builder function __builder_mt:realloc(realloc) @@ -8143,6 +8182,9 @@ __evolved_set(__DEFAULT, __ON_REMOVE, __update_major_chunks) __evolved_set(__DUPLICATE, __ON_INSERT, __update_major_chunks) __evolved_set(__DUPLICATE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__EXTRACT, __ON_SET, __update_search_index) +__evolved_set(__EXTRACT, __ON_REMOVE, __update_search_index) + __evolved_set(__REALLOC, __ON_SET, __update_major_chunks) __evolved_set(__REALLOC, __ON_REMOVE, __update_major_chunks) @@ -8346,6 +8388,7 @@ __evolved_set(__INTERNAL, __NAME, '__INTERNAL') __evolved_set(__DEFAULT, __NAME, '__DEFAULT') __evolved_set(__DUPLICATE, __NAME, '__DUPLICATE') +__evolved_set(__EXTRACT, __NAME, '__EXTRACT') __evolved_set(__REALLOC, __NAME, '__REALLOC') __evolved_set(__COMPMOVE, __NAME, '__COMPMOVE') @@ -8396,6 +8439,7 @@ __evolved_set(__INTERNAL, __INTERNAL) __evolved_set(__DEFAULT, __INTERNAL) __evolved_set(__DUPLICATE, __INTERNAL) +__evolved_set(__EXTRACT, __INTERNAL) __evolved_set(__REALLOC, __INTERNAL) __evolved_set(__COMPMOVE, __INTERNAL) @@ -8446,6 +8490,7 @@ evolved.INTERNAL = __INTERNAL evolved.DEFAULT = __DEFAULT evolved.DUPLICATE = __DUPLICATE +evolved.EXTRACT = __EXTRACT evolved.REALLOC = __REALLOC evolved.COMPMOVE = __COMPMOVE From 107bd57ce769f4ea11f7c73050e4d61e34d88dcc Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 19 Oct 2026 17:00:19 +0700 Subject: [PATCH 19/48] fix tests exit code on failure --- develop/all.lua | 8 +++++++- develop/basics.lua | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/develop/all.lua b/develop/all.lua index 97a4e8e..d1cff60 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -47,5 +47,11 @@ print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.requires_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.unique_fuzz' +print '----------------------------------------' -print 'All tests passed.' +if basics.failure_count() == 0 then + print 'All tests passed.' +else + print(string.format('%d test(s) failed.', basics.failure_count())) + os.exit(1) +end diff --git a/develop/basics.lua b/develop/basics.lua index 55ccc4c..cc556d6 100644 --- a/develop/basics.lua +++ b/develop/basics.lua @@ -20,6 +20,14 @@ local __table_unpack = (function() return table.unpack or unpack end)() +local __failure_count = 0 + +---@return integer +---@nodiscard +function basics.failure_count() + return __failure_count +end + ---@param pattern string function basics.unload(pattern) for name, _ in pairs(package.loaded) do @@ -63,6 +71,7 @@ function basics.describe_fuzz(modname) iters)) else print('|-- FUZZ FAIL: ' .. result) + __failure_count = __failure_count + 1 end end @@ -95,6 +104,7 @@ function basics.describe_bench(name, loop, init, fini) if not success then print('|-- WARMUP FAIL: ' .. result) + __failure_count = __failure_count + 1 return end end @@ -126,6 +136,7 @@ function basics.describe_bench(name, loop, init, fini) iters)) else print('|-- LOOP FAIL: ' .. result) + __failure_count = __failure_count + 1 end end @@ -136,6 +147,7 @@ function basics.describe_bench(name, loop, init, fini) if not success then print('|-- FINI FAIL: ' .. result) + __failure_count = __failure_count + 1 end end From aa5088bd207709793bb51abdaba3648ec8f803b5 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 19 Oct 2026 19:59:50 +0700 Subject: [PATCH 20/48] return query major chunks cache --- develop/benchmarks/process_bmarks.lua | 94 +++++++ develop/fuzzing/execute_fuzz.lua | 30 +++ develop/testing/main_tests.lua | 302 +++++++++++++++++++++ evolved.lua | 368 ++++++++++++++++++-------- 4 files changed, 685 insertions(+), 109 deletions(-) diff --git a/develop/benchmarks/process_bmarks.lua b/develop/benchmarks/process_bmarks.lua index 287a8dd..e23c755 100644 --- a/develop/benchmarks/process_bmarks.lua +++ b/develop/benchmarks/process_bmarks.lua @@ -100,3 +100,97 @@ basics.describe_bench(string.format('Process Benchmarks: Evolved SoA Processing function(w) evo.destroy(w) end) + +basics.describe_bench(string.format('Process Benchmarks: Evolved Narrow Processing | %d archetypes', N / 10), + function(w) + evo.process(w) + end, + + function() + local wf = evo.builder() + :set(evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + :spawn() + + local tfs = {} + + for i = 1, N / 10 do + tfs[i] = evo.builder():set(wf):tag():spawn() + end + + local pf = evo.builder():set(wf):spawn() + + for i = 1, N / 10 do + evo.multi_spawn_nr(10, { + [wf] = true, + [tfs[i]] = true, + [pf] = 0, + }) + end + + for i = 1, N / 100 do + evo.builder() + :set(wf) + :set(evo.GROUP, wf) + :set(evo.QUERY, evo.builder():set(wf):include(tfs[i], pf):spawn()) + :set(evo.EXECUTE, function(chunk, _, entity_count) + local ps = chunk:components(pf) + + for j = 1, entity_count do + ps[j] = ps[j] + 1 + end + end) + :spawn() + end + + return wf + end, + + function(w) + evo.destroy(w) + end) + +basics.describe_bench(string.format('Process Benchmarks: Evolved Wide Processing | %d archetypes', N / 10), + function(w) + evo.process(w) + end, + + function() + local wf = evo.builder() + :set(evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + :spawn() + + local tfs = {} + + for i = 1, N / 10 do + tfs[i] = evo.builder():set(wf):tag():spawn() + end + + local pf = evo.builder():set(wf):spawn() + + for i = 1, N / 10 do + evo.multi_spawn_nr(10, { + [wf] = true, + [tfs[i]] = true, + [pf] = 0, + }) + end + + evo.builder() + :set(wf) + :set(evo.GROUP, wf) + :set(evo.QUERY, evo.builder():set(wf):include(pf):spawn()) + :set(evo.EXECUTE, function(chunk, _, entity_count) + local ps = chunk:components(pf) + + for j = 1, entity_count do + ps[j] = ps[j] + 1 + end + end) + :spawn() + + return wf + end, + + function(w) + evo.destroy(w) + end) diff --git a/develop/fuzzing/execute_fuzz.lua b/develop/fuzzing/execute_fuzz.lua index 25fa377..8e39909 100644 --- a/develop/fuzzing/execute_fuzz.lua +++ b/develop/fuzzing/execute_fuzz.lua @@ -295,6 +295,36 @@ end --- --- +local cleared_entity_list = {} +local cleared_entity_count = 0 + +for _ = 1, math.random(1, 5) do + local entity = all_entity_list[math.random(1, all_entity_count)] + + evo.clear(entity) + + cleared_entity_count = cleared_entity_count + 1 + cleared_entity_list[cleared_entity_count] = entity +end + +if math.random(1, 2) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) +end + +for i = 1, cleared_entity_count do + generate_entity(cleared_entity_list[i]) +end + +for i = 1, all_query_count do + execute_query(all_query_list[i]) +end + +--- +--- +--- +--- +--- + if math.random(1, 2) == 1 then evo.collect_garbage(math.random(1, 2) == 1) end diff --git a/develop/testing/main_tests.lua b/develop/testing/main_tests.lua index 9b52ff0..2106081 100644 --- a/develop/testing/main_tests.lua +++ b/develop/testing/main_tests.lua @@ -7154,3 +7154,305 @@ do assert(o == 'a12i2') assert(evo.get(e1, f) == 2 and evo.get(e2, f) == 2) end + +do + ---@param query evolved.query + ---@return table entity_set + ---@return integer entity_count + local function execute_entities(query) + local entity_set, entity_count = {}, 0 + + for _, chunk_entity_list, chunk_entity_count in evo.execute(query) do + for i = 1, chunk_entity_count do + local entity = chunk_entity_list[i] + entity_set[entity] = (entity_set[entity] or 0) + 1 + entity_count = entity_count + 1 + end + end + + return entity_set, entity_count + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q = evo.builder():include(f1, f2):spawn() + + local e1 = evo.builder():set(f1, 1):set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e1] == 1) + end + + local e2 = evo.builder():set(f0, 0):set(f1, 1):set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e1] == 1 and es[e2] == 1) + end + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q1 = evo.builder():include(f2):spawn() + local q2 = evo.builder():include(f1, f2):spawn() + local q3 = evo.builder():include(f0, f2):spawn() + + local e1 = evo.builder():set(f1, 1):set(f2, 2):spawn() + + do + local es1, ec1 = execute_entities(q1) + assert(ec1 == 1 and es1[e1] == 1) + + local es2, ec2 = execute_entities(q2) + assert(ec2 == 1 and es2[e1] == 1) + + local _, ec3 = execute_entities(q3) + assert(ec3 == 0) + end + + local e2 = evo.builder():set(f0, 0):set(f1, 1):set(f2, 2):spawn() + + do + local es1, ec1 = execute_entities(q1) + assert(ec1 == 2 and es1[e1] == 1 and es1[e2] == 1) + + local es2, ec2 = execute_entities(q2) + assert(ec2 == 2 and es2[e1] == 1 and es2[e2] == 1) + + local es3, ec3 = execute_entities(q3) + assert(ec3 == 1 and es3[e2] == 1) + end + + local e3 = evo.builder():set(f0, 0):set(f2, 2):spawn() + + do + local es1, ec1 = execute_entities(q1) + assert(ec1 == 3 and es1[e1] == 1 and es1[e2] == 1 and es1[e3] == 1) + + local es2, ec2 = execute_entities(q2) + assert(ec2 == 2 and es2[e1] == 1 and es2[e2] == 1) + + local es3, ec3 = execute_entities(q3) + assert(ec3 == 2 and es3[e2] == 1 and es3[e3] == 1) + end + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q = evo.builder():variant(f1, f2):spawn() + + local e1 = evo.builder():set(f1, 1):set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e1] == 1) + end + + local e2 = evo.builder():set(f0, 0):set(f1, 1):spawn() + local e3 = evo.builder():set(f0, 0):set(f2, 2):spawn() + local e4 = evo.builder():set(f0, 0):set(f1, 1):set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 4 and es[e1] == 1 and es[e2] == 1 and es[e3] == 1 and es[e4] == 1) + end + end + + do + local fs = { evo.id(2) } + table.sort(fs) + local f1, f2 = fs[1], fs[2] + + local q = evo.builder():include(f2):spawn() + + local e1 = evo.builder():set(f1, 1):set(f2, 2):spawn() + local e2 = evo.builder():set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e1] == 1 and es[e2] == 1) + end + + evo.set(f1, evo.EXPLICIT) + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e2] == 1) + end + + evo.remove(f1, evo.EXPLICIT) + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e1] == 1 and es[e2] == 1) + end + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q = evo.builder():include(f1):spawn() + + local e1 = evo.builder():set(f1, 1):spawn() + local e2 = evo.builder():set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e1] == 1) + end + + evo.set(q, evo.INCLUDES, { f2 }) + + local e3 = evo.builder():set(f0, 0):set(f1, 1):spawn() + local e4 = evo.builder():set(f0, 0):set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e2] == 1 and es[e4] == 1) + end + + evo.remove(q, evo.INCLUDES) + evo.set(q, evo.VARIANTS, { f1 }) + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e1] == 1 and es[e3] == 1) + end + + evo.set(q, evo.EXCLUDES, { f0 }) + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e1] == 1) + end + + evo.set(q, evo.VARIANTS, { f2 }) + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e2] == 1) + end + end + + do + local fs = { evo.id(2) } + table.sort(fs) + local f1, f2 = fs[1], fs[2] + + local q = evo.builder():include(f2):spawn() + + local e = evo.builder():set(f1, 1):set(f2, 2):spawn() + local c = evo.chunk(f1, f2) + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e] == 1) + end + + evo.clear(e) + evo.collect_garbage() + + do + local _, ec = execute_entities(q) + assert(ec == 0) + end + + evo.set(e, f1, 1) + evo.set(e, f2, 2) + assert(evo.chunk(f1, f2) ~= c) + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e] == 1) + end + + evo.clear(e) + evo.collect_garbage() + + evo.set(e, f1, 1) + evo.set(e, f2, 2) + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e] == 1) + end + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q = evo.builder():include(f2):variant(f1):spawn() + + local e1 = evo.builder():set(f1, 1):set(f2, 2):spawn() + local e2 = evo.builder():set(f2, 2):spawn() + local e3 = evo.builder():set(f1, 1):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e1] == 1) + end + + local e4 = evo.builder():set(f0, 0):set(f2, 2):spawn() + local e5 = evo.builder():set(f0, 0):set(f1, 1):set(f2, 2):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e1] == 1 and es[e5] == 1) + end + + evo.remove(q, evo.VARIANTS) + + do + local es, ec = execute_entities(q) + assert(ec == 4 and es[e1] == 1 and es[e2] == 1 and es[e4] == 1 and es[e5] == 1) + end + + evo.set(q, evo.VARIANTS, { f1 }) + + do + local es, ec = execute_entities(q) + assert(ec == 2 and es[e1] == 1 and es[e5] == 1) + end + + evo.remove(q, evo.INCLUDES) + + do + local es, ec = execute_entities(q) + assert(ec == 3 and es[e1] == 1 and es[e3] == 1 and es[e5] == 1) + end + end + + do + local fs = { evo.id(2) } + table.sort(fs) + local f0, f1 = fs[1], fs[2] + + local q = evo.builder():include(f1):spawn() + + local e1 = evo.builder():set(f1, 1):spawn() + + do + local es, ec = execute_entities(q) + assert(ec == 1 and es[e1] == 1) + end + + evo.destroy(q) + + local e2 = evo.builder():set(f0, 0):set(f1, 1):spawn() + assert(evo.has_all(e2, f0, f1)) + end +end diff --git a/evolved.lua b/evolved.lua index 1995658..6bffe47 100644 --- a/evolved.lua +++ b/evolved.lua @@ -626,6 +626,9 @@ local __root_count = 0 ---@type integer local __major_chunks = {} ---@type table> local __minor_chunks = {} ---@type table> +local __query_chunks = {} ---@type table> +local __major_queries = {} ---@type table> + local __entity_chunks = {} ---@type (evolved.chunk|false)[] local __entity_places = {} ---@type integer[] @@ -1272,11 +1275,18 @@ local __add_child_chunk local __remove_child_chunk local __update_chunk_caches +local __update_chunk_queries local __update_chunk_storages local __trace_major_chunks local __trace_minor_chunks +local __cache_query_chunks +local __reset_query_chunks + +local __query_major_matches +local __query_minor_matches + local __update_major_chunks local __update_major_chunks_trace @@ -1850,6 +1860,7 @@ function __new_chunk(chunk_parent, chunk_fragment) end __update_chunk_caches(chunk) + __update_chunk_queries(chunk) __update_chunk_storages(chunk) return chunk @@ -2180,6 +2191,26 @@ function __update_chunk_caches(chunk) end end +---@param chunk evolved.chunk +function __update_chunk_queries(chunk) + local major_queries = __major_queries[chunk.__fragment] + local major_query_list = major_queries and major_queries.__item_list + local major_query_count = major_queries and major_queries.__item_count or 0 + + for major_query_index = 1, major_query_count do + local major_query = major_query_list[major_query_index] + local major_query_chunks = __query_chunks[major_query] + + if major_query_chunks then + if __query_major_matches(chunk, major_query) then + __assoc_list_fns.insert(major_query_chunks, chunk) + else + __assoc_list_fns.remove(major_query_chunks, chunk) + end + end + end +end + ---@param chunk evolved.chunk function __update_chunk_storages(chunk) local entity_count = chunk.__entity_count @@ -2435,6 +2466,196 @@ function __trace_minor_chunks(minor, trace, ...) end end +---@param query evolved.query +---@return evolved.assoc_list +---@nodiscard +function __cache_query_chunks(query) + local query_includes = __sorted_includes[query] + local query_include_list = query_includes and query_includes.__item_list + local query_include_count = query_includes and query_includes.__item_count or 0 + + local query_variants = __sorted_variants[query] + local query_variant_list = query_variants and query_variants.__item_list + local query_variant_count = query_variants and query_variants.__item_count or 0 + + ---@type evolved.assoc_list + local query_chunks = __assoc_list_fns.new(4) + __query_chunks[query] = query_chunks + + if query_include_count > 0 then + local query_major = query_include_list[query_include_count] + + local major_chunks = __major_chunks[query_major] + local major_chunk_list = major_chunks and major_chunks.__item_list + local major_chunk_count = major_chunks and major_chunks.__item_count or 0 + + for major_chunk_index = 1, major_chunk_count do + local major_chunk = major_chunk_list[major_chunk_index] + + if __query_major_matches(major_chunk, query) then + __assoc_list_fns.insert(query_chunks, major_chunk) + end + end + + local major_queries = __major_queries[query_major] + + if not major_queries then + ---@type evolved.assoc_list + major_queries = __assoc_list_fns.new(4) + __major_queries[query_major] = major_queries + end + + __assoc_list_fns.insert(major_queries, query) + end + + for query_variant_index = 1, query_variant_count do + local query_variant = query_variant_list[query_variant_index] + + if query_include_count == 0 or query_variant > query_include_list[query_include_count] then + local major_chunks = __major_chunks[query_variant] + local major_chunk_list = major_chunks and major_chunks.__item_list + local major_chunk_count = major_chunks and major_chunks.__item_count or 0 + + for major_chunk_index = 1, major_chunk_count do + local major_chunk = major_chunk_list[major_chunk_index] + + if __query_major_matches(major_chunk, query) then + __assoc_list_fns.insert(query_chunks, major_chunk) + end + end + + local major_queries = __major_queries[query_variant] + + if not major_queries then + ---@type evolved.assoc_list + major_queries = __assoc_list_fns.new(4) + __major_queries[query_variant] = major_queries + end + + __assoc_list_fns.insert(major_queries, query) + end + end + + return query_chunks +end + +---@param query evolved.query +function __reset_query_chunks(query) + if not __query_chunks[query] then + return + end + + __query_chunks[query] = nil + + local query_includes = __sorted_includes[query] + local query_include_list = query_includes and query_includes.__item_list + local query_include_count = query_includes and query_includes.__item_count or 0 + + local query_variants = __sorted_variants[query] + local query_variant_list = query_variants and query_variants.__item_list + local query_variant_count = query_variants and query_variants.__item_count or 0 + + if query_include_count > 0 then + local query_major = query_include_list[query_include_count] + local major_queries = __major_queries[query_major] + + if major_queries and __assoc_list_fns.remove(major_queries, query) == 0 then + __major_queries[query_major] = nil + end + end + + for query_variant_index = 1, query_variant_count do + local query_variant = query_variant_list[query_variant_index] + + if query_include_count == 0 or query_variant > query_include_list[query_include_count] then + local major_queries = __major_queries[query_variant] + + if major_queries and __assoc_list_fns.remove(major_queries, query) == 0 then + __major_queries[query_variant] = nil + end + end + end +end + +---@param chunk evolved.chunk +---@param query evolved.query +---@return boolean +---@nodiscard +function __query_major_matches(chunk, query) + local query_includes = __sorted_includes[query] + local query_include_set = query_includes and query_includes.__item_set + local query_include_count = query_includes and query_includes.__item_count or 0 + + local query_variants = __sorted_variants[query] + local query_variant_set = query_variants and query_variants.__item_set + local query_variant_list = query_variants and query_variants.__item_list + local query_variant_count = query_variants and query_variants.__item_count or 0 + + local query_include_index = query_include_count > 0 and query_include_set[chunk.__fragment] or nil + local query_variant_index = query_variant_count > 0 and query_variant_set[chunk.__fragment] or nil + + return ( + (query_include_index ~= nil and query_include_index == query_include_count) or + (query_variant_index ~= nil and not __chunk_has_any_fragment_list(chunk, query_variant_list, query_variant_index - 1)) + ) and __query_minor_matches(chunk, query) +end + +---@param chunk evolved.chunk +---@param query evolved.query +---@return boolean +---@nodiscard +function __query_minor_matches(chunk, query) + local query_includes = __sorted_includes[query] + local query_include_set = query_includes and query_includes.__item_set + local query_include_list = query_includes and query_includes.__item_list + local query_include_count = query_includes and query_includes.__item_count or 0 + + if query_include_count > 0 then + if not __chunk_has_all_fragment_list(chunk, query_include_list, query_include_count) then + return false + end + end + + local query_excludes = __sorted_excludes[query] + local query_exclude_list = query_excludes and query_excludes.__item_list + local query_exclude_count = query_excludes and query_excludes.__item_count or 0 + + if query_exclude_count > 0 then + if __chunk_has_any_fragment_list(chunk, query_exclude_list, query_exclude_count) then + return false + end + end + + local query_variants = __sorted_variants[query] + local query_variant_set = query_variants and query_variants.__item_set + local query_variant_list = query_variants and query_variants.__item_list + local query_variant_count = query_variants and query_variants.__item_count or 0 + + if query_variant_count > 0 then + if not __chunk_has_any_fragment_list(chunk, query_variant_list, query_variant_count) then + return false + end + end + + local chunk_explicit_fragment_list = chunk.__explicit_fragment_list + + if chunk_explicit_fragment_list then + for chunk_explicit_fragment_index = 1, chunk.__explicit_fragment_count do + local fragment = chunk_explicit_fragment_list[chunk_explicit_fragment_index] + + local is_chunk_fragment_matched = + (query_variant_count > 0 and query_variant_set[fragment]) or + (query_include_count > 0 and query_include_set[fragment]) + + if not is_chunk_fragment_matched then + return false + end + end + end + + return true +end + ---@param major evolved.fragment function __update_major_chunks(major) if __defer_depth > 0 then @@ -2448,6 +2669,7 @@ end ---@param chunk evolved.chunk function __update_major_chunks_trace(chunk) __update_chunk_caches(chunk) + __update_chunk_queries(chunk) __update_chunk_storages(chunk) end @@ -3638,6 +3860,22 @@ function __purge_chunk(chunk) end end + do + local major = chunk.__fragment + local major_queries = __major_queries[major] + local major_query_list = major_queries and major_queries.__item_list + local major_query_count = major_queries and major_queries.__item_count or 0 + + for major_query_index = 1, major_query_count do + local major_query = major_query_list[major_query_index] + local major_query_chunks = __query_chunks[major_query] + + if major_query_chunks then + __assoc_list_fns.remove(major_query_chunks, chunk) + end + end + end + for chunk_fragment_index = 1, chunk.__fragment_count do local minor = chunk.__fragment_list[chunk_fragment_index] local minor_chunks = __minor_chunks[minor] @@ -6829,128 +7067,27 @@ function __evolved_execute(query) local query_includes = __sorted_includes[query] local query_include_set = query_includes and query_includes.__item_set - local query_include_list = query_includes and query_includes.__item_list local query_include_count = query_includes and query_includes.__item_count or 0 local query_excludes = __sorted_excludes[query] local query_exclude_set = query_excludes and query_excludes.__item_set - local query_exclude_list = query_excludes and query_excludes.__item_list local query_exclude_count = query_excludes and query_excludes.__item_count or 0 local query_variants = __sorted_variants[query] local query_variant_set = query_variants and query_variants.__item_set - local query_variant_list = query_variants and query_variants.__item_list local query_variant_count = query_variants and query_variants.__item_count or 0 if query_include_count > 0 or query_variant_count > 0 then - if query_include_count > 0 then - local query_major = query_include_list[query_include_count] - - local major_chunks = __major_chunks[query_major] - local major_chunk_list = major_chunks and major_chunks.__item_list - local major_chunk_count = major_chunks and major_chunks.__item_count or 0 - - for major_chunk_index = 1, major_chunk_count do - local major_chunk = major_chunk_list[major_chunk_index] - - local is_major_chunk_matched = true - - if is_major_chunk_matched and query_include_count > 1 then - is_major_chunk_matched = __chunk_has_all_fragment_list( - major_chunk, query_include_list, query_include_count - 1) - end + local query_chunks = __query_chunks[query] or __cache_query_chunks(query) + local query_chunk_list = query_chunks and query_chunks.__item_list + local query_chunk_count = query_chunks and query_chunks.__item_count or 0 - if is_major_chunk_matched and query_exclude_count > 0 then - is_major_chunk_matched = not __chunk_has_any_fragment_list( - major_chunk, query_exclude_list, query_exclude_count) - end - - if is_major_chunk_matched and query_variant_count > 0 then - is_major_chunk_matched = __chunk_has_any_fragment_list( - major_chunk, query_variant_list, query_variant_count) - end - - if is_major_chunk_matched and major_chunk.__has_explicit_fragments then - local major_chunk_explicit_fragment_list = major_chunk.__explicit_fragment_list - local major_chunk_explicit_fragment_count = major_chunk.__explicit_fragment_count - - ---@cast major_chunk_explicit_fragment_list -? - - for major_chunk_explicit_fragment_index = 1, major_chunk_explicit_fragment_count do - local fragment = major_chunk_explicit_fragment_list[major_chunk_explicit_fragment_index] - - local is_explicit_fragment_matched = - (query_include_set and query_include_set[fragment]) or - (query_variant_set and query_variant_set[fragment]) - - if not is_explicit_fragment_matched then - is_major_chunk_matched = false - break - end - end - end - - if is_major_chunk_matched then - chunk_stack_size = chunk_stack_size + 1 - chunk_stack[chunk_stack_size] = major_chunk - end - end - end - - for query_variant_index = 1, query_variant_count do - local query_variant = query_variant_list[query_variant_index] - - if query_include_count == 0 or query_variant > query_include_list[query_include_count] then - local major_chunks = __major_chunks[query_variant] - local major_chunk_list = major_chunks and major_chunks.__item_list - local major_chunk_count = major_chunks and major_chunks.__item_count or 0 - - for major_chunk_index = 1, major_chunk_count do - local major_chunk = major_chunk_list[major_chunk_index] - - local is_major_chunk_matched = true - - if is_major_chunk_matched and query_include_count > 0 then - is_major_chunk_matched = __chunk_has_all_fragment_list( - major_chunk, query_include_list, query_include_count) - end - - if is_major_chunk_matched and query_exclude_count > 0 then - is_major_chunk_matched = not __chunk_has_any_fragment_list( - major_chunk, query_exclude_list, query_exclude_count) - end - - if is_major_chunk_matched and query_variant_index > 1 then - is_major_chunk_matched = not __chunk_has_any_fragment_list( - major_chunk, query_variant_list, query_variant_index - 1) - end - - if is_major_chunk_matched and major_chunk.__has_explicit_fragments then - local major_chunk_explicit_fragment_list = major_chunk.__explicit_fragment_list - local major_chunk_explicit_fragment_count = major_chunk.__explicit_fragment_count - - ---@cast major_chunk_explicit_fragment_list -? - - for major_chunk_explicit_fragment_index = 1, major_chunk_explicit_fragment_count do - local fragment = major_chunk_explicit_fragment_list[major_chunk_explicit_fragment_index] - - local is_explicit_fragment_matched = - (query_include_set and query_include_set[fragment]) or - (query_variant_set and query_variant_set[fragment]) - - if not is_explicit_fragment_matched then - is_major_chunk_matched = false - break - end - end - end + if query_chunk_count > 0 then + __lua_table_move( + query_chunk_list, 1, query_chunk_count, + chunk_stack_size + 1, chunk_stack) - if is_major_chunk_matched then - chunk_stack_size = chunk_stack_size + 1 - chunk_stack[chunk_stack_size] = major_chunk - end - end - end + chunk_stack_size = chunk_stack_size + query_chunk_count end elseif query_exclude_count > 0 then for root_index = 1, __root_count do @@ -8265,6 +8402,8 @@ __evolved_set(__QUERY, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) ---@param query evolved.query ---@param include_list evolved.fragment[] __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) + __reset_query_chunks(query) + local include_count = #include_list if include_count > 0 then @@ -8281,6 +8420,8 @@ __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) end) __evolved_set(__INCLUDES, __ON_REMOVE, function(query) + __reset_query_chunks(query) + __sorted_includes[query] = nil end) @@ -8293,6 +8434,8 @@ end) ---@param query evolved.query ---@param exclude_list evolved.fragment[] __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) + __reset_query_chunks(query) + local exclude_count = #exclude_list if exclude_count > 0 then @@ -8309,6 +8452,8 @@ __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) end) __evolved_set(__EXCLUDES, __ON_REMOVE, function(query) + __reset_query_chunks(query) + __sorted_excludes[query] = nil end) @@ -8321,6 +8466,8 @@ end) ---@param query evolved.query ---@param variant_list evolved.fragment[] __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) + __reset_query_chunks(query) + local variant_count = #variant_list if variant_count > 0 then @@ -8337,6 +8484,8 @@ __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) end) __evolved_set(__VARIANTS, __ON_REMOVE, function(query) + __reset_query_chunks(query) + __sorted_variants[query] = nil end) @@ -8368,6 +8517,7 @@ end) __evolved_set(__REQUIRES, __ON_REMOVE, function(fragment) __sorted_requires[fragment] = nil + __update_major_chunks(fragment) end) From e3a90a5ed84389fcbe247fe1c7b4ea20fd23aae1 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 19 Oct 2026 21:56:06 +0700 Subject: [PATCH 21/48] fix cached chunk ordering --- develop/testing/main_tests.lua | 118 +++++++++++++++++++++++++++ evolved.lua | 141 ++++++++++++++++++++++++--------- 2 files changed, 222 insertions(+), 37 deletions(-) diff --git a/develop/testing/main_tests.lua b/develop/testing/main_tests.lua index 2106081..08c4ef4 100644 --- a/develop/testing/main_tests.lua +++ b/develop/testing/main_tests.lua @@ -7455,4 +7455,122 @@ do local e2 = evo.builder():set(f0, 0):set(f1, 1):spawn() assert(evo.has_all(e2, f0, f1)) end + + ---@param query evolved.query + ---@return evolved.chunk[] chunk_list + ---@return integer chunk_count + local function execute_chunks(query) + local chunk_list, chunk_count = {}, 0 + + for chunk in evo.execute(query) do + chunk_count = chunk_count + 1 + chunk_list[chunk_count] = chunk + end + + return chunk_list, chunk_count + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q = evo.builder():variant(f1, f2):spawn() + + evo.builder():set(f1, 1):spawn() + evo.builder():set(f2, 2):spawn() + + do + local _, cc = execute_chunks(q) + assert(cc == 2) + end + + evo.builder():set(f0, 0):set(f1, 1):spawn() + + do + local cs, cc = execute_chunks(q) + assert(cc == 3) + assert(cs[1] == evo.chunk(f2) and cs[2] == evo.chunk(f1) and cs[3] == evo.chunk(f0, f1)) + end + + evo.set(q, evo.VARIANTS, { f1, f2 }) + + do + local cs, cc = execute_chunks(q) + assert(cc == 3) + assert(cs[1] == evo.chunk(f2) and cs[2] == evo.chunk(f1) and cs[3] == evo.chunk(f0, f1)) + end + end + + do + local fs = { evo.id(4) } + table.sort(fs) + local f0, f1, f2, f3 = fs[1], fs[2], fs[3], fs[4] + + local q = evo.builder():include(f3):spawn() + + evo.builder():set(f1, 1):set(f3, 3):spawn() + + do + local _, cc = execute_chunks(q) + assert(cc == 1) + end + + evo.builder():set(f2, 2):set(f3, 3):spawn() + evo.builder():set(f0, 0):set(f3, 3):spawn() + + do + local cs, cc = execute_chunks(q) + assert(cc == 3) + assert(cs[1] == evo.chunk(f2, f3) and cs[2] == evo.chunk(f1, f3) and cs[3] == evo.chunk(f0, f3)) + end + + evo.set(f1, evo.EXPLICIT) + + do + local cs, cc = execute_chunks(q) + assert(cc == 2) + assert(cs[1] == evo.chunk(f2, f3) and cs[2] == evo.chunk(f0, f3)) + end + + evo.remove(f1, evo.EXPLICIT) + + do + local cs, cc = execute_chunks(q) + assert(cc == 3) + assert(cs[1] == evo.chunk(f2, f3) and cs[2] == evo.chunk(f1, f3) and cs[3] == evo.chunk(f0, f3)) + end + + evo.set(f3, evo.DEFAULT, 3) + + do + local cs, cc = execute_chunks(q) + assert(cc == 3) + assert(cs[1] == evo.chunk(f2, f3) and cs[2] == evo.chunk(f1, f3) and cs[3] == evo.chunk(f0, f3)) + end + end + + do + local fs = { evo.id(3) } + table.sort(fs) + local f0, f1, f2 = fs[1], fs[2], fs[3] + + local q = evo.builder():exclude(f0):spawn() + + evo.builder():set(f2, 2):spawn() + evo.builder():set(f1, 1):spawn() + + local c1_index, c2_index + + do + local cs, cc = execute_chunks(q) + + for i = 1, cc do + if cs[i] == evo.chunk(f1) then c1_index = i end + if cs[i] == evo.chunk(f2) then c2_index = i end + end + end + + assert(c1_index and c2_index and c2_index < c1_index) + end end diff --git a/evolved.lua b/evolved.lua index 6bffe47..f46db1e 100644 --- a/evolved.lua +++ b/evolved.lua @@ -850,16 +850,19 @@ end ---@generic V ---@param list_a V[] ---@param list_b V[] +---@param size_a? integer +---@param size_b? integer ---@return boolean ---@nodiscard -function __list_fns.eq(list_a, list_b) - local list_size = #list_a +function __list_fns.eq(list_a, list_b, size_a, size_b) + size_a = size_a or #list_a + size_b = size_b or #list_b - if list_size ~= #list_b then + if size_a ~= size_b then return false end - for list_index = 1, list_size do + for list_index = 1, size_a do if list_a[list_index] ~= list_b[list_index] then return false end @@ -868,14 +871,37 @@ function __list_fns.eq(list_a, list_b) return true end +---@generic V +---@param list_a V[] +---@param list_b V[] +---@param size_a? integer +---@param size_b? integer +---@return boolean +---@nodiscard +function __list_fns.lt(list_a, list_b, size_a, size_b) + size_a = size_a or #list_a + size_b = size_b or #list_b + + for list_index = 1, size_a < size_b and size_a or size_b do + local item_a = list_a[list_index] + local item_b = list_b[list_index] + + if item_a ~= item_b then + return item_a < item_b + end + end + + return size_a < size_b +end + ---@generic V ---@param list V[] ---@param item V ---@param comp? fun(a: V, b: V): boolean ---@param size? integer ----@return integer lower_bound_index +---@return integer upper_bound_index ---@nodiscard -function __list_fns.lwr(list, item, comp, size) +function __list_fns.upr(list, item, comp, size) local lower, upper = 1, size or #list if comp then @@ -1050,6 +1076,50 @@ function __assoc_list_fns.insert_ex(al_item_set, al_item_list, al_item_count, it return al_item_count end +---@generic K +---@param al evolved.assoc_list +---@param item K +---@param comp? fun(a: K, b: K): boolean +---@return integer new_al_count +function __assoc_list_fns.sorted_insert(al, item, comp) + local new_al_count = __assoc_list_fns.sorted_insert_ex( + al.__item_set, al.__item_list, al.__item_count, + item, comp) + + al.__item_count = new_al_count + return new_al_count +end + +---@generic K +---@param al_item_set table +---@param al_item_list K[] +---@param al_item_count integer +---@param item K +---@param comp? fun(a: K, b: K): boolean +---@return integer new_al_count +---@nodiscard +function __assoc_list_fns.sorted_insert_ex(al_item_set, al_item_list, al_item_count, item, comp) + local item_index = al_item_set[item] + + if item_index then + return al_item_count + end + + item_index = __list_fns.upr(al_item_list, item, comp, al_item_count) + + for al_item_index = al_item_count, item_index, -1 do + local al_item = al_item_list[al_item_index] + al_item_set[al_item] = al_item_index + 1 + al_item_list[al_item_index + 1] = al_item + end + + al_item_count = al_item_count + 1 + al_item_set[item] = item_index + al_item_list[item_index] = item + + return al_item_count +end + ---@generic K ---@param al evolved.assoc_list ---@param item K @@ -1899,9 +1969,7 @@ end ---@param root evolved.chunk function __add_root_chunk(root) - local root_index = __list_fns.lwr(__root_list, root, function(a, b) - return a.__fragment < b.__fragment - end, __root_count) + local root_index = __list_fns.upr(__root_list, root, __chunk_mt.__lt, __root_count) for sib_root_index = __root_count, root_index, -1 do local sib_root = __root_list[sib_root_index] @@ -1944,19 +2012,9 @@ end ---@param child evolved.chunk ---@param parent evolved.chunk function __add_child_chunk(child, parent) - local child_index = __list_fns.lwr(parent.__child_list, child, function(a, b) - return a.__fragment < b.__fragment - end, parent.__child_count) - - for sib_child_index = parent.__child_count, child_index, -1 do - local sib_child = parent.__child_list[sib_child_index] - parent.__child_set[sib_child] = sib_child_index + 1 - parent.__child_list[sib_child_index + 1] = sib_child - end - - parent.__child_set[child] = child_index - parent.__child_list[child_index] = child - parent.__child_count = parent.__child_count + 1 + parent.__child_count = __assoc_list_fns.sorted_insert_ex( + parent.__child_set, parent.__child_list, parent.__child_count, + child, __chunk_mt.__lt) parent.__with_fragment_edges[child.__fragment] = child child.__without_fragment_edges[child.__fragment] = parent @@ -1982,15 +2040,9 @@ function __remove_child_chunk(child) return end - for sib_child_index = child_index, parent.__child_count - 1 do - local next_sib_child = parent.__child_list[sib_child_index + 1] - parent.__child_set[next_sib_child] = sib_child_index - parent.__child_list[sib_child_index] = next_sib_child - end - - parent.__child_set[child] = nil - parent.__child_list[parent.__child_count] = nil - parent.__child_count = parent.__child_count - 1 + parent.__child_count = __assoc_list_fns.remove_ex( + parent.__child_set, parent.__child_list, parent.__child_count, + child) parent.__with_fragment_edges[child.__fragment] = nil child.__without_fragment_edges[child.__fragment] = nil @@ -2203,7 +2255,7 @@ function __update_chunk_queries(chunk) if major_query_chunks then if __query_major_matches(chunk, major_query) then - __assoc_list_fns.insert(major_query_chunks, chunk) + __assoc_list_fns.sorted_insert(major_query_chunks, chunk, __chunk_mt.__lt) else __assoc_list_fns.remove(major_query_chunks, chunk) end @@ -2536,6 +2588,8 @@ function __cache_query_chunks(query) end end + __assoc_list_fns.sort(query_chunks, __chunk_mt.__lt) + return query_chunks end @@ -2559,7 +2613,7 @@ function __reset_query_chunks(query) local query_major = query_include_list[query_include_count] local major_queries = __major_queries[query_major] - if major_queries and __assoc_list_fns.remove(major_queries, query) == 0 then + if major_queries and __assoc_list_fns.unordered_remove(major_queries, query) == 0 then __major_queries[query_major] = nil end end @@ -2570,7 +2624,7 @@ function __reset_query_chunks(query) if query_include_count == 0 or query_variant > query_include_list[query_include_count] then local major_queries = __major_queries[query_variant] - if major_queries and __assoc_list_fns.remove(major_queries, query) == 0 then + if major_queries and __assoc_list_fns.unordered_remove(major_queries, query) == 0 then __major_queries[query_variant] = nil end end @@ -7450,16 +7504,29 @@ function evolved.chunk(fragment, ...) return chunk, chunk.__entity_list, chunk.__entity_count end -function __chunk_mt:__tostring() +---@param chunk evolved.chunk +---@return string +---@nodiscard +function __chunk_mt.__tostring(chunk) local fragment_names = {} ---@type string[] - for fragment_index = 1, self.__fragment_count do - fragment_names[fragment_index] = __id_name(self.__fragment_list[fragment_index]) + for fragment_index = 1, chunk.__fragment_count do + fragment_names[fragment_index] = __id_name(chunk.__fragment_list[fragment_index]) end return string.format('<%s>', table.concat(fragment_names, ', ')) end +---@param chunk_a evolved.chunk +---@param chunk_b evolved.chunk +---@return boolean +---@nodiscard +function __chunk_mt.__lt(chunk_a, chunk_b) + return __list_fns.lt( + chunk_a.__fragment_list, chunk_b.__fragment_list, + chunk_a.__fragment_count, chunk_b.__fragment_count) +end + ---@return boolean ---@nodiscard function __chunk_mt:alive() From 9b4c161771d8beda878608bdb9f1f977f445ab69 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 19 Oct 2026 23:55:30 +0700 Subject: [PATCH 22/48] minor cleanup --- evolved.lua | 70 ++++++++++++++++------------------------------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/evolved.lua b/evolved.lua index f46db1e..d7e260a 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1423,11 +1423,6 @@ local __defer_multi_clone_entity --- local __safe_tbls = { - __EMPTY_ENTITY_SET = setmetatable({}, { - __tostring = function() return 'empty entity set' end, - __newindex = function() __debug_fns.error_fmt 'attempt to modify empty entity set' end - }) --[[@as table]], - __EMPTY_FRAGMENT_SET = setmetatable({}, { __tostring = function() return 'empty fragment set' end, __newindex = function() __debug_fns.error_fmt 'attempt to modify empty fragment set' end @@ -2473,8 +2468,7 @@ function __trace_major_chunks(major, trace, ...) end if chunk_stack then - __release_table(__table_pool_tag.chunk_list, chunk_stack, - chunk_stack_size == 0, true) + __release_table(__table_pool_tag.chunk_list, chunk_stack, true, true) end end @@ -2513,8 +2507,7 @@ function __trace_minor_chunks(minor, trace, ...) end if chunk_stack then - __release_table(__table_pool_tag.chunk_list, chunk_stack, - chunk_stack_size == 0, true) + __release_table(__table_pool_tag.chunk_list, chunk_stack, true, true) end end @@ -4293,8 +4286,7 @@ function __destroy_fragment_one(fragment) processing_fragment_stack, processing_fragment_stack_size) - __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, - true, true) + __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, true, true) end ---@param fragment_list evolved.fragment[] @@ -4320,8 +4312,7 @@ function __destroy_fragment_list(fragment_list, fragment_count) processing_fragment_stack, processing_fragment_stack_size) - __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, - true, true) + __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, true, true) end ---@param processing_fragment_stack evolved.fragment[] @@ -4499,15 +4490,10 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment __release_table(__table_pool_tag.entity_list, remove_fragment_link_target_list, false, true) end - if releasing_fragment_count > 0 then - __destroy_entity_list(releasing_fragment_list, releasing_fragment_count, true) - end - - __release_table(__table_pool_tag.fragment_list, releasing_fragment_list, - releasing_fragment_count == 0, true) + __destroy_entity_list(releasing_fragment_list, releasing_fragment_count, true) - __release_table(__table_pool_tag.fragment_set, processed_fragment_set, - true, false) + __release_table(__table_pool_tag.fragment_list, releasing_fragment_list, false, true) + __release_table(__table_pool_tag.fragment_set, processed_fragment_set, true, false) end ---@param entity evolved.entity @@ -4733,8 +4719,7 @@ function __chunk_assign(chunk, fragment, component) if component_realloc then component_realloc(old_component_storage_for_hooks, chunk_entity_count, 0) else - __release_table(__table_pool_tag.component_list, old_component_storage_for_hooks, - chunk_entity_count == 0, true) + __release_table(__table_pool_tag.component_list, old_component_storage_for_hooks, false, true) end end end @@ -5696,8 +5681,7 @@ local function __system_process(system, ...) end end - __release_table(__table_pool_tag.system_list, subsystem_list, - subsystem_count == 0, true) + __release_table(__table_pool_tag.system_list, subsystem_list, false, true) end if epilogue then @@ -6662,8 +6646,7 @@ function __evolved_clear(...) if purging_entity_list then __clear_entity_list(purging_entity_list, purging_entity_count) - __release_table(__table_pool_tag.entity_list, purging_entity_list, - purging_entity_count == 0, true) + __release_table(__table_pool_tag.entity_list, purging_entity_list, false, true) end end @@ -6753,14 +6736,12 @@ function __evolved_destroy(...) if purging_fragment_list then __destroy_fragment_list(purging_fragment_list, purging_fragment_count) - __release_table(__table_pool_tag.fragment_list, purging_fragment_list, - purging_fragment_count == 0, true) + __release_table(__table_pool_tag.fragment_list, purging_fragment_list, false, true) end if purging_entity_list then __destroy_entity_list(purging_entity_list, purging_entity_count) - __release_table(__table_pool_tag.entity_list, purging_entity_list, - purging_entity_count == 0, true) + __release_table(__table_pool_tag.entity_list, purging_entity_list, false, true) end end @@ -6862,13 +6843,11 @@ function __evolved_batch_set(query, fragment, component) end if assign_chunk_list then - __release_table(__table_pool_tag.chunk_list, assign_chunk_list, - assign_chunk_count == 0, true) + __release_table(__table_pool_tag.chunk_list, assign_chunk_list, false, true) end if insert_chunk_list then - __release_table(__table_pool_tag.chunk_list, insert_chunk_list, - insert_chunk_count == 0, true) + __release_table(__table_pool_tag.chunk_list, insert_chunk_list, false, true) end end @@ -6919,8 +6898,7 @@ function __evolved_batch_remove(query, ...) end if chunk_list then - __release_table(__table_pool_tag.chunk_list, chunk_list, - chunk_count == 0, true) + __release_table(__table_pool_tag.chunk_list, chunk_list, false, true) end end @@ -6969,8 +6947,7 @@ function __evolved_batch_clear(...) if chunk_list then __clear_chunk_list(chunk_list, chunk_count) - __release_table(__table_pool_tag.chunk_list, chunk_list, - chunk_count == 0, true) + __release_table(__table_pool_tag.chunk_list, chunk_list, false, true) end end @@ -7054,20 +7031,17 @@ function __evolved_batch_destroy(...) if purging_fragment_list then __destroy_fragment_list(purging_fragment_list, purging_fragment_count) - __release_table(__table_pool_tag.fragment_list, purging_fragment_list, - purging_fragment_count == 0, true) + __release_table(__table_pool_tag.fragment_list, purging_fragment_list, false, true) end if clearing_chunk_list then __clear_chunk_list(clearing_chunk_list, clearing_chunk_count) - __release_table(__table_pool_tag.chunk_list, clearing_chunk_list, - clearing_chunk_count == 0, true) + __release_table(__table_pool_tag.chunk_list, clearing_chunk_list, false, true) end if purging_entity_list then __destroy_entity_list(purging_entity_list, purging_entity_count) - __release_table(__table_pool_tag.entity_list, purging_entity_list, - purging_entity_count == 0, true) + __release_table(__table_pool_tag.entity_list, purging_entity_list, false, true) end end @@ -7446,13 +7420,11 @@ function __evolved_collect_garbage(no_shrink) end if working_chunk_stack then - __release_table(__table_pool_tag.chunk_list, working_chunk_stack, - working_chunk_stack_size == 0, true) + __release_table(__table_pool_tag.chunk_list, working_chunk_stack, true, true) end if postorder_chunk_stack then - __release_table(__table_pool_tag.chunk_list, postorder_chunk_stack, - postorder_chunk_stack_size == 0, true) + __release_table(__table_pool_tag.chunk_list, postorder_chunk_stack, true, true) end end From 363df21b9e0aa6f7466135e01730241c42d6a48c Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 20 Oct 2026 02:53:21 +0700 Subject: [PATCH 23/48] update changelog and issues --- README.md | 7 ++++--- develop/all.lua | 2 ++ develop/issues/issue_54.lua | 31 +++++++++++++++++++++++++++++++ develop/issues/issue_55.lua | 25 +++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 develop/issues/issue_54.lua create mode 100644 develop/issues/issue_55.lua diff --git a/README.md b/README.md index fb65c94..e4d2ae1 100644 --- a/README.md +++ b/README.md @@ -2030,10 +2030,11 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z - Added the new [`evolved.LINK`](#evolvedlink) and [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section -- The [`evolved.GROUP`](#evolvedgroup) and [`evolved.QUERY`](#evolvedquery) fragments are now linked, so destroying a group or a query also destroys the systems that use them -- Components of [linked fragments](#linked-fragments) can be lists of entities, which link to every entity in them -- The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are now linked, so destroying a fragment also destroys the queries and fragments that use it - Added the new [`evolved.EXTRACT`](#evolvedextract) fragment trait that allows indexing and linking by values extracted from components +- The [`GROUP`](#evolvedgroup), [`QUERY`](#evolvedquery), [`INCLUDES`](#evolvedincludes), [`EXCLUDES`](#evolvedexcludes), [`VARIANTS`](#evolvedvariants), and [`REQUIRES`](#evolvedrequires) fragments are now linked, see the [Linked Fragments](#linked-fragments) section +- Query iteration order no longer depends on the order in which chunks were created +- Fixed [`evolved.batch_set`](#evolvedbatch_set) invoking hooks twice: [#54](https://github.com/BlackMATov/evolved.lua/issues/54) +- Fixed query caches keeping collected chunks: [#55](https://github.com/BlackMATov/evolved.lua/issues/55) ### v1.12.0 diff --git a/develop/all.lua b/develop/all.lua index d1cff60..b5ba323 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -1,4 +1,6 @@ require 'develop.issues.issue_52' +require 'develop.issues.issue_54' +require 'develop.issues.issue_55' require 'develop.testing.build_tests' require 'develop.testing.cached_hooks_tests' diff --git a/develop/issues/issue_54.lua b/develop/issues/issue_54.lua new file mode 100644 index 0000000..b3d3255 --- /dev/null +++ b/develop/issues/issue_54.lua @@ -0,0 +1,31 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + local insert_count, assign_count = 0, 0 + + local g, f = evo.id(2) + + if g > f then + -- the bug shows up when the fragment is greater than the group + g, f = f, g + end + + evo.set(f, evo.ON_INSERT, function() insert_count = insert_count + 1 end) + evo.set(f, evo.ON_ASSIGN, function() assign_count = assign_count + 1 end) + + local q = evo.builder():include(g):build() + + evo.spawn({ [g] = true }) + evo.spawn({ [g] = true, [f] = 1 }) + + insert_count, assign_count = 0, 0 + + -- the first entity receives the fragment, the second one already has it, + -- so each hook should be invoked only once + evo.batch_set(q, f, 2) + + assert(insert_count == 1) + assert(assign_count == 1) +end diff --git a/develop/issues/issue_55.lua b/develop/issues/issue_55.lua new file mode 100644 index 0000000..51b99f4 --- /dev/null +++ b/develop/issues/issue_55.lua @@ -0,0 +1,25 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + local f1, f2 = evo.id(2) + + local q = evo.builder():include(f1, f2):build() + local e = evo.spawn({ [f1] = true, [f2] = true }) + + -- the query caches the chunk of the entity + for _ in evo.execute(q) do end + + local weak_chunks = setmetatable({}, { __mode = 'v' }) + weak_chunks[1] = evo.chunk(f1, f2) + + -- the chunk becomes empty and is removed by the garbage collector + evo.clear(e) + evo.collect_garbage() + + collectgarbage('collect') + + -- nothing should keep a reference to the removed chunk + assert(weak_chunks[1] == nil) +end From 046bc23f337753dae312d24e7e469b8b55f4d81d Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 20 Oct 2026 04:49:10 +0700 Subject: [PATCH 24/48] NAME is now marked as UNIQUE --- README.md | 31 ++++++++++++++++++++++++++----- evolved.lua | 1 + 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e4d2ae1..9cbcc5e 100644 --- a/README.md +++ b/README.md @@ -1056,7 +1056,25 @@ local player = evolved.builder() assert(evolved.name(player) == 'Player') ``` -Names are not unique, so multiple entities can have the same name. Also, the name of an entity can be changed at any time by setting a new name using the [`evolved.NAME`](#evolvedname) fragment as a usual component. +The same name can be used by multiple entities at once, nothing prevents two entities from being called `'Player'`. Also, the name of an entity can be changed at any time by setting a new name using the [`evolved.NAME`](#evolvedname) fragment as a usual component. + +However, names are not copied when cloning entities, because the [`evolved.NAME`](#evolvedname) fragment is marked with the [`evolved.UNIQUE`](#evolvedunique) fragment trait. Note that this trait only affects cloning, it doesn't mean that the name value must be unique among entities. So clones of a named prefab are unnamed, unless you pass a name in the component table. See the [Unique Fragments](#unique-fragments) section for more details. + +```lua +local evolved = require 'evolved' + +local enemy_prefab = evolved.builder() + :name('Enemy') + :prefab() + :build() + +local enemy1 = evolved.clone(enemy_prefab) +local enemy2 = evolved.clone(enemy_prefab, { [evolved.NAME] = 'Boss' }) + +assert(evolved.name(enemy_prefab) == 'Enemy') +assert(not evolved.has(enemy1, evolved.NAME)) +assert(evolved.name(enemy2) == 'Boss') +``` You can find entities by their names using the [`evolved.lookup`](#evolvedlookup) and [`evolved.multi_lookup`](#evolvedmulti_lookup) functions. The [`evolved.lookup`](#evolvedlookup) function returns the first entity with the specified name, while the [`evolved.multi_lookup`](#evolvedmulti_lookup) function returns a list of all entities with the specified name. @@ -1275,6 +1293,8 @@ assert(evolved.has(enemy_prefab, evolved.PREFAB)) assert(not evolved.has(enemy_clone, evolved.PREFAB)) ``` +The [`evolved.NAME`](#evolvedname) fragment is marked as unique too, so clones don't inherit the names of their prefabs. Otherwise, every clone of a named prefab would also be added to the name index, which is costly when spawning many entities. + #### Explicit Fragments In some cases, you might want to hide chunks with certain fragments from queries by default. For example, the library has a special fragment called [`evolved.DISABLED`](#evolveddisabled) that behaves this way. This fragment is marked with the [`evolved.EXPLICIT`](#evolvedexplicit) fragment trait, which means it will be hidden from queries unless you explicitly include it. This is useful for fragments that are used for internal or editor purposes and should not be exposed to queries by default. @@ -2029,11 +2049,12 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z -- Added the new [`evolved.LINK`](#evolvedlink) and [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section -- Added the new [`evolved.EXTRACT`](#evolvedextract) fragment trait that allows indexing and linking by values extracted from components -- The [`GROUP`](#evolvedgroup), [`QUERY`](#evolvedquery), [`INCLUDES`](#evolvedincludes), [`EXCLUDES`](#evolvedexcludes), [`VARIANTS`](#evolvedvariants), and [`REQUIRES`](#evolvedrequires) fragments are now linked, see the [Linked Fragments](#linked-fragments) section +- Added the new [`LINK`](#evolvedlink) and [`LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section +- The [`GROUP`](#evolvedgroup), [`QUERY`](#evolvedquery), [`INCLUDES`](#evolvedincludes), [`EXCLUDES`](#evolvedexcludes), [`VARIANTS`](#evolvedvariants), and [`REQUIRES`](#evolvedrequires) fragments are now linked +- Added the new [`EXTRACT`](#evolvedextract) fragment trait that allows indexing and linking by values extracted from components - Query iteration order no longer depends on the order in which chunks were created -- Fixed [`evolved.batch_set`](#evolvedbatch_set) invoking hooks twice: [#54](https://github.com/BlackMATov/evolved.lua/issues/54) +- The [`NAME`](#evolvedname) fragment is now marked as [UNIQUE](#unique-fragments), so clones no longer inherit the names of their prefabs +- Fixed [`batch_set`](#evolvedbatch_set) invoking hooks twice: [#54](https://github.com/BlackMATov/evolved.lua/issues/54) - Fixed query caches keeping collected chunks: [#55](https://github.com/BlackMATov/evolved.lua/issues/55) ### v1.12.0 diff --git a/evolved.lua b/evolved.lua index d7e260a..8c3f924 100644 --- a/evolved.lua +++ b/evolved.lua @@ -8376,6 +8376,7 @@ __evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) __evolved_set(__TAG, __TAG) __evolved_set(__NAME, __INDEX) +__evolved_set(__NAME, __UNIQUE) __evolved_set(__INDEX, __TAG) __evolved_set(__INDEX, __UNIQUE) From c26abea0d9ab6395620c54b9eac39e406ad6429f Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 21 Oct 2026 04:13:07 +0700 Subject: [PATCH 25/48] warnings instead of errors when searching/indexing non-indexed fragments --- README.md | 3 +- develop/ROADMAP.md | 2 ++ evolved.lua | 78 ++++++++++++++++++++-------------------------- 3 files changed, 37 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 9cbcc5e..941b3ca 100644 --- a/README.md +++ b/README.md @@ -1261,7 +1261,7 @@ The function is called every time a component of the fragment is inserted or ass > The extracted value must depend only on the component itself. The index is not updated when other fragments of the entity change, so a function that reads them will leave the index outdated. For the same reason, the function should not call any library functions. Tables changed in place bypass the index too, so set them again or use the [`evolved.reindex`](#evolvedreindex) function after such changes. > [!NOTE] -> Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) or [`evolved.LINK`](#evolvedlink) trait is an error. +> Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) or [`evolved.LINK`](#evolvedlink) trait finds nothing and prints a warning. The [`evolved.reindex`](#evolvedreindex) function prints a warning for such fragments too and skips them. > [!NOTE] > Components, or values extracted from them, are used as keys in the index, so table keys are indexed by reference, and searching by another table with the same content will not find anything. Also, [Fragment Tags](#fragment-tags) are not indexed at all, and neither are `nil` and `NaN` keys. @@ -2054,6 +2054,7 @@ builder_mt:destruction_policy :: id -> builder - Added the new [`EXTRACT`](#evolvedextract) fragment trait that allows indexing and linking by values extracted from components - Query iteration order no longer depends on the order in which chunks were created - The [`NAME`](#evolvedname) fragment is now marked as [UNIQUE](#unique-fragments), so clones no longer inherit the names of their prefabs +- The [`search`](#evolvedsearch) and [`reindex`](#evolvedreindex) functions now print a warning instead of throwing an error for non-indexed fragments - Fixed [`batch_set`](#evolvedbatch_set) invoking hooks twice: [#54](https://github.com/BlackMATov/evolved.lua/issues/54) - Fixed query caches keeping collected chunks: [#55](https://github.com/BlackMATov/evolved.lua/issues/55) diff --git a/develop/ROADMAP.md b/develop/ROADMAP.md index 057aff1..1e3862b 100644 --- a/develop/ROADMAP.md +++ b/develop/ROADMAP.md @@ -3,6 +3,8 @@ ## Backlog - observers and events +- add batch hooks for fragments +- add component mapper for batch_set - use compact prefix-tree for chunks ## Thoughts diff --git a/evolved.lua b/evolved.lua index 8c3f924..ca3ed59 100644 --- a/evolved.lua +++ b/evolved.lua @@ -3464,7 +3464,6 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co if component_index then local fragment_search_index = __search_indices[fragment] local component_storage = chunk_component_storages[component_index] - __search_index_fns.multi_insert(fragment_search_index, chunk_entity_list, component_storage, b_place, e_place) end @@ -3824,7 +3823,6 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c if component_index then local fragment_search_index = __search_indices[fragment] local component_storage = chunk_component_storages[component_index] - __search_index_fns.multi_insert(fragment_search_index, chunk_entity_list, component_storage, b_place, e_place) end @@ -4642,11 +4640,6 @@ function __chunk_assign(chunk, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) end - ---@type evolved.search_index? - local fragment_search_index = chunk_has_index_fragments - and __search_indices[fragment] - or nil - local component_storage = chunk_component_storages[component_index] local component_realloc = chunk_component_reallocs[component_index] local component_compmove = chunk_component_compmoves[component_index] @@ -4695,6 +4688,10 @@ function __chunk_assign(chunk, fragment, component) end end + local fragment_search_index = chunk_has_index_fragments + and __search_indices[fragment] + or nil + if fragment_search_index then __search_index_fns.multi_assign(fragment_search_index, chunk_entity_list, component_storage, 1, chunk_entity_count) @@ -4775,11 +4772,6 @@ function __chunk_insert(old_chunk, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE) end - ---@type evolved.search_index? - 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 if sum_entity_count > new_chunk.__entity_capacity then @@ -4868,6 +4860,10 @@ function __chunk_insert(old_chunk, fragment, component) end end + local fragment_search_index = new_chunk_has_index_fragments + and __search_indices[fragment] + or nil + if fragment_search_index then __search_index_fns.multi_insert(fragment_search_index, new_entity_list, new_component_storage, new_entity_count + 1, sum_entity_count) @@ -4893,11 +4889,6 @@ function __chunk_insert(old_chunk, fragment, component) __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) end - ---@type evolved.search_index? - 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] if req_component_index then @@ -4918,6 +4909,10 @@ function __chunk_insert(old_chunk, fragment, component) end end + local req_fragment_search_index = new_chunk_has_index_fragments + and __search_indices[req_fragment] + or nil + if req_fragment_search_index then __search_index_fns.multi_insert(req_fragment_search_index, new_entity_list, req_component_storage, new_entity_count + 1, sum_entity_count) @@ -5055,7 +5050,6 @@ function __chunk_remove(old_chunk, ...) if not new_fragment_set[fragment] then local fragment_search_index = __search_indices[fragment] - __search_index_fns.multi_remove(fragment_search_index, old_entity_list, 1, old_entity_count) end @@ -5202,7 +5196,6 @@ function __chunk_clear(chunk) for chunk_index_fragment_index = 1, chunk.__index_fragment_count do local fragment = chunk_index_fragment_list[chunk_index_fragment_index] local fragment_search_index = __search_indices[fragment] - __search_index_fns.multi_remove(fragment_search_index, chunk_entity_list, 1, chunk_entity_count) end @@ -6357,11 +6350,6 @@ function __evolved_set(entity, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) end - ---@type evolved.search_index? - local fragment_search_index = old_chunk_has_index_fragments - and __search_indices[fragment] - or nil - local old_component_index = old_component_indices[fragment] if old_component_index then @@ -6375,6 +6363,10 @@ function __evolved_set(entity, fragment, component) local old_component = old_component_storage[old_place] old_component_storage[old_place] = new_component + local fragment_search_index = old_chunk_has_index_fragments + and __search_indices[fragment] + or nil + if fragment_search_index then __search_index_fns.assign(fragment_search_index, entity, new_component) end @@ -6418,11 +6410,6 @@ function __evolved_set(entity, fragment, component) __evolved_get(fragment, __DEFAULT, __DUPLICATE) end - ---@type evolved.search_index? - local fragment_search_index = new_chunk_has_index_fragments - and __search_indices[fragment] - or nil - local new_place = new_chunk.__entity_count + 1 if new_place > new_chunk.__entity_capacity then @@ -6472,6 +6459,10 @@ function __evolved_set(entity, fragment, component) new_component_storage[new_place] = new_component + local fragment_search_index = new_chunk_has_index_fragments + and __search_indices[fragment] + or nil + if fragment_search_index then __search_index_fns.insert(fragment_search_index, entity, new_component) end @@ -6496,11 +6487,6 @@ function __evolved_set(entity, fragment, component) __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) end - ---@type evolved.search_index? - 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] if req_component_index then @@ -6518,6 +6504,10 @@ function __evolved_set(entity, fragment, component) req_component_storage[new_place] = req_component + local req_fragment_search_index = new_chunk_has_index_fragments + and __search_indices[req_fragment] + or nil + if req_fragment_search_index then __search_index_fns.insert(req_fragment_search_index, entity, req_component) end @@ -7210,8 +7200,9 @@ function __evolved_search(fragment, component) local fragment_search_index = __search_indices[fragment] if not fragment_search_index then - __debug_fns.error_fmt('the fragment (%s) is not indexed and cannot be searched', + __debug_fns.warning_fmt('the fragment (%s) is not indexed and cannot be searched', __id_name(fragment)) + return end return fragment_search_index.__head_entities[component] @@ -7237,8 +7228,9 @@ 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 - __debug_fns.error_fmt('the fragment (%s) is not indexed and cannot be searched', + __debug_fns.warning_fmt('the fragment (%s) is not indexed and cannot be searched', __id_name(fragment)) + return 0 end local entity_count = 0 @@ -7280,19 +7272,15 @@ function __evolved_reindex(...) for fragment_index = 1, fragment_count do ---@type evolved.fragment local fragment = __lua_select(fragment_index, ...) + local fragment_search_index = __search_indices[fragment] - if not __search_indices[fragment] then - __debug_fns.error_fmt('the fragment (%s) is not indexed and cannot be reindexed', + if not fragment_search_index then + __debug_fns.warning_fmt('the fragment (%s) is not indexed and cannot be reindexed', __id_name(fragment)) + else + __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) 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 From caac11609aa3afbb15a77e4f008a709410022bab Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 21 Oct 2026 19:40:21 +0700 Subject: [PATCH 26/48] bug report --- develop/all.lua | 1 + develop/issues/issue_56.lua | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 develop/issues/issue_56.lua diff --git a/develop/all.lua b/develop/all.lua index b5ba323..3f08133 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -1,6 +1,7 @@ require 'develop.issues.issue_52' require 'develop.issues.issue_54' require 'develop.issues.issue_55' +require 'develop.issues.issue_56' require 'develop.testing.build_tests' require 'develop.testing.cached_hooks_tests' diff --git a/develop/issues/issue_56.lua b/develop/issues/issue_56.lua new file mode 100644 index 0000000..37257e6 --- /dev/null +++ b/develop/issues/issue_56.lua @@ -0,0 +1,18 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + local remove_count = 0 + + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local g = evo.builder():set(f):on_remove(function() remove_count = remove_count + 1 end):build() + + local e = evo.spawn({ [g] = true }) + + -- destroying the first fragment also destroys the second one, + -- so the second fragment should be removed from the entity with its hook + evo.destroy(f) + + assert(evo.alive(e) and not evo.has(e, g) and remove_count == 1) +end From b58851afcb60728d99d21972a40ae95b5456d42d Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 23 Oct 2026 07:08:32 +0700 Subject: [PATCH 27/48] rewrite destruction logic --- README.md | 1 + develop/fuzzing/batch_destroy_fuzz.lua | 155 +++++-- develop/fuzzing/destroy_fuzz.lua | 149 +++++-- develop/fuzzing/link_fuzz.lua | 82 ++++ develop/testing/destroy_tests.lua | 261 +++++++++++ evolved.lua | 583 +++++++++++++------------ 6 files changed, 902 insertions(+), 329 deletions(-) diff --git a/README.md b/README.md index 941b3ca..7bc2592 100644 --- a/README.md +++ b/README.md @@ -2057,6 +2057,7 @@ builder_mt:destruction_policy :: id -> builder - The [`search`](#evolvedsearch) and [`reindex`](#evolvedreindex) functions now print a warning instead of throwing an error for non-indexed fragments - Fixed [`batch_set`](#evolvedbatch_set) invoking hooks twice: [#54](https://github.com/BlackMATov/evolved.lua/issues/54) - Fixed query caches keeping collected chunks: [#55](https://github.com/BlackMATov/evolved.lua/issues/55) +- Fixed skipped remove hooks of fragments destroyed in a cascade: [#56](https://github.com/BlackMATov/evolved.lua/issues/56) ### v1.12.0 diff --git a/develop/fuzzing/batch_destroy_fuzz.lua b/develop/fuzzing/batch_destroy_fuzz.lua index c1eba25..c78ae36 100644 --- a/develop/fuzzing/batch_destroy_fuzz.lua +++ b/develop/fuzzing/batch_destroy_fuzz.lua @@ -19,66 +19,126 @@ end)() --- --- -local all_entity_list = {} ---@type evolved.entity[] +local all_fragment_list = {} ---@type evolved.fragment[] for i = 1, math.random(1, 10) do - local entity = evo.id() - all_entity_list[i] = entity + local fragment = evo.id() + all_fragment_list[i] = fragment end -for _, entity in ipairs(all_entity_list) do - for _ = 0, math.random(0, #all_entity_list) do - local fragment = all_entity_list[math.random(1, #all_entity_list)] - evo.set(entity, fragment) +local remove_hook_calls = {} ---@type table> +local hooked_fragment_set = {} ---@type table + +---@param entity evolved.entity +---@param fragment evolved.fragment +local function remove_hook(entity, fragment) + assert(evo.alive(entity) and evo.alive(fragment)) + + local entity_calls = remove_hook_calls[entity] + + if not entity_calls then + entity_calls = {} + remove_hook_calls[entity] = entity_calls + end + + entity_calls[fragment] = (entity_calls[fragment] or 0) + 1 +end + +for _, fragment in ipairs(all_fragment_list) do + for _ = 0, math.random(0, #all_fragment_list) do + local other_fragment = all_fragment_list[math.random(1, #all_fragment_list)] + evo.set(fragment, other_fragment) end if math.random(1, 5) == 1 then - evo.set(entity, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + evo.set(fragment, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) end if math.random(1, 5) == 1 then - evo.set(entity, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT) + evo.set(fragment, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT) + end + + if math.random(1, 3) == 1 then + evo.set(fragment, evo.ON_REMOVE, remove_hook) + hooked_fragment_set[fragment] = true end end +local all_entity_list = {} ---@type evolved.entity[] + +for i, fragment in ipairs(all_fragment_list) do + all_entity_list[i] = fragment +end + +for _ = 1, math.random(0, #all_fragment_list) do + local prefab = all_fragment_list[math.random(1, #all_fragment_list)] + all_entity_list[#all_entity_list + 1] = evo.clone(prefab) +end + --- --- --- --- --- -local should_be_destroyed_entity_set = {} ---@type table -local should_be_destroyed_entity_list = {} ---@type evolved.entity[] -local should_be_destroyed_entity_count = 0 ---@type integer +local entity_fragment_sets = {} ---@type table> +local fragment_destruction_policies = {} ---@type table -local function collect_destroyed_entities_with(entity) - local entity_destruction_policy = evo.get(entity, evo.DESTRUCTION_POLICY) - or evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT +for _, entity in ipairs(all_entity_list) do + local entity_fragment_set = {} - if entity_destruction_policy == evo.DESTRUCTION_POLICY_DESTROY_ENTITY then - for _, other_entity in ipairs(all_entity_list) do - if evo.has(other_entity, entity) and not should_be_destroyed_entity_set[other_entity] then - should_be_destroyed_entity_count = should_be_destroyed_entity_count + 1 - should_be_destroyed_entity_list[should_be_destroyed_entity_count] = other_entity - should_be_destroyed_entity_set[other_entity] = should_be_destroyed_entity_count - end + for _, fragment in ipairs(all_fragment_list) do + if evo.has(entity, fragment) then + entity_fragment_set[fragment] = true end end + + entity_fragment_sets[entity] = entity_fragment_set +end + +for _, fragment in ipairs(all_fragment_list) do + fragment_destruction_policies[fragment] = evo.get(fragment, evo.DESTRUCTION_POLICY) + or evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT end local destroying_include_list = {} ---@type evolved.entity[] +local should_be_destroyed_entity_set = {} ---@type table -for i = 1, math.random(1, #all_entity_list) do - local destroying_include = all_entity_list[math.random(1, #all_entity_list)] +for i = 1, math.random(1, #all_fragment_list) do + local destroying_include = all_fragment_list[math.random(1, #all_fragment_list)] destroying_include_list[i] = destroying_include end for _, entity in ipairs(all_entity_list) do if evo.has_all(entity, __table_unpack(destroying_include_list)) then - collect_destroyed_entities_with(entity) + should_be_destroyed_entity_set[entity] = true end end +repeat + local has_new_destroyed_entities = false + + for _, entity in ipairs(all_entity_list) do + if not should_be_destroyed_entity_set[entity] then + for fragment in pairs(entity_fragment_sets[entity]) do + if should_be_destroyed_entity_set[fragment] + and fragment_destruction_policies[fragment] == evo.DESTRUCTION_POLICY_DESTROY_ENTITY + then + should_be_destroyed_entity_set[entity] = true + has_new_destroyed_entities = true + break + end + end + end + end +until not has_new_destroyed_entities + +--- +--- +--- +--- +--- + do local r = math.random(1, 2) local q = evo.builder():include(__table_unpack(destroying_include_list)):spawn() @@ -90,6 +150,8 @@ do evo.batch_destroy(q) assert(evo.commit()) end + + evo.destroy(q) end --- @@ -101,14 +163,51 @@ end local all_chunk_query = evo.spawn() for chunk in evo.execute(all_chunk_query) do - assert(not chunk:has_any(__table_unpack(should_be_destroyed_entity_list))) + for _, fragment in ipairs(all_fragment_list) do + if should_be_destroyed_entity_set[fragment] then + assert(not chunk:has(fragment)) + end + end + for _, fragment in ipairs(chunk:fragments()) do assert(not evo.has_all(fragment, __table_unpack(destroying_include_list))) end end -for _, destroyed_entity in ipairs(should_be_destroyed_entity_list) do - assert(not evo.alive(destroyed_entity)) +for _, entity in ipairs(all_entity_list) do + assert(evo.alive(entity) == not should_be_destroyed_entity_set[entity]) +end + +for _, entity in ipairs(all_entity_list) do + if evo.alive(entity) then + for fragment in pairs(entity_fragment_sets[entity]) do + assert(evo.has(entity, fragment) == not should_be_destroyed_entity_set[fragment]) + end + end +end + +for _, entity in ipairs(all_entity_list) do + local entity_calls = remove_hook_calls[entity] or {} + + for fragment in pairs(entity_fragment_sets[entity]) do + local expected_call_count = 0 + + if hooked_fragment_set[fragment] then + if should_be_destroyed_entity_set[entity] or should_be_destroyed_entity_set[fragment] then + expected_call_count = 1 + end + end + + assert((entity_calls[fragment] or 0) == expected_call_count) + end + + for fragment in pairs(entity_calls) do + assert(entity_fragment_sets[entity][fragment]) + end +end + +for entity in pairs(remove_hook_calls) do + assert(entity_fragment_sets[entity]) end --- diff --git a/develop/fuzzing/destroy_fuzz.lua b/develop/fuzzing/destroy_fuzz.lua index a274c10..e1d261e 100644 --- a/develop/fuzzing/destroy_fuzz.lua +++ b/develop/fuzzing/destroy_fuzz.lua @@ -19,61 +19,121 @@ end)() --- --- -local all_entity_list = {} ---@type evolved.entity[] +local all_fragment_list = {} ---@type evolved.fragment[] for i = 1, math.random(1, 10) do - local entity = evo.id() - all_entity_list[i] = entity + local fragment = evo.id() + all_fragment_list[i] = fragment end -for _, entity in ipairs(all_entity_list) do - for _ = 0, math.random(0, #all_entity_list) do - local fragment = all_entity_list[math.random(1, #all_entity_list)] - evo.set(entity, fragment) +local remove_hook_calls = {} ---@type table> +local hooked_fragment_set = {} ---@type table + +---@param entity evolved.entity +---@param fragment evolved.fragment +local function remove_hook(entity, fragment) + assert(evo.alive(entity) and evo.alive(fragment)) + + local entity_calls = remove_hook_calls[entity] + + if not entity_calls then + entity_calls = {} + remove_hook_calls[entity] = entity_calls + end + + entity_calls[fragment] = (entity_calls[fragment] or 0) + 1 +end + +for _, fragment in ipairs(all_fragment_list) do + for _ = 0, math.random(0, #all_fragment_list) do + local other_fragment = all_fragment_list[math.random(1, #all_fragment_list)] + evo.set(fragment, other_fragment) end if math.random(1, 5) == 1 then - evo.set(entity, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + evo.set(fragment, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_DESTROY_ENTITY) end if math.random(1, 5) == 1 then - evo.set(entity, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT) + evo.set(fragment, evo.DESTRUCTION_POLICY, evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT) + end + + if math.random(1, 3) == 1 then + evo.set(fragment, evo.ON_REMOVE, remove_hook) + hooked_fragment_set[fragment] = true end end +local all_entity_list = {} ---@type evolved.entity[] + +for i, fragment in ipairs(all_fragment_list) do + all_entity_list[i] = fragment +end + +for _ = 1, math.random(0, #all_fragment_list) do + local prefab = all_fragment_list[math.random(1, #all_fragment_list)] + all_entity_list[#all_entity_list + 1] = evo.clone(prefab) +end + --- --- --- --- --- -local should_be_destroyed_entity_set = {} ---@type table -local should_be_destroyed_entity_list = {} ---@type evolved.entity[] -local should_be_destroyed_entity_count = 0 ---@type integer +local entity_fragment_sets = {} ---@type table> +local fragment_destruction_policies = {} ---@type table -local function collect_destroyed_entities_with(entity) - local entity_destruction_policy = evo.get(entity, evo.DESTRUCTION_POLICY) - or evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT +for _, entity in ipairs(all_entity_list) do + local entity_fragment_set = {} - if entity_destruction_policy == evo.DESTRUCTION_POLICY_DESTROY_ENTITY then - for _, other_entity in ipairs(all_entity_list) do - if evo.has(other_entity, entity) and not should_be_destroyed_entity_set[other_entity] then - should_be_destroyed_entity_count = should_be_destroyed_entity_count + 1 - should_be_destroyed_entity_list[should_be_destroyed_entity_count] = other_entity - should_be_destroyed_entity_set[other_entity] = should_be_destroyed_entity_count - end + for _, fragment in ipairs(all_fragment_list) do + if evo.has(entity, fragment) then + entity_fragment_set[fragment] = true end end + + entity_fragment_sets[entity] = entity_fragment_set +end + +for _, fragment in ipairs(all_fragment_list) do + fragment_destruction_policies[fragment] = evo.get(fragment, evo.DESTRUCTION_POLICY) + or evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT end local destroying_entity_list = {} ---@type evolved.entity[] +local should_be_destroyed_entity_set = {} ---@type table for i = 1, math.random(1, #all_entity_list) do local destroying_entity = all_entity_list[math.random(1, #all_entity_list)] destroying_entity_list[i] = destroying_entity - collect_destroyed_entities_with(destroying_entity) + should_be_destroyed_entity_set[destroying_entity] = true end +repeat + local has_new_destroyed_entities = false + + for _, entity in ipairs(all_entity_list) do + if not should_be_destroyed_entity_set[entity] then + for fragment in pairs(entity_fragment_sets[entity]) do + if should_be_destroyed_entity_set[fragment] + and fragment_destruction_policies[fragment] == evo.DESTRUCTION_POLICY_DESTROY_ENTITY + then + should_be_destroyed_entity_set[entity] = true + has_new_destroyed_entities = true + break + end + end + end + end +until not has_new_destroyed_entities + +--- +--- +--- +--- +--- + do local r = math.random(1, 4) @@ -105,16 +165,47 @@ end local all_chunk_query = evo.spawn() for chunk in evo.execute(all_chunk_query) do - assert(not chunk:has_any(__table_unpack(destroying_entity_list))) - assert(not chunk:has_any(__table_unpack(should_be_destroyed_entity_list))) + for _, fragment in ipairs(all_fragment_list) do + if should_be_destroyed_entity_set[fragment] then + assert(not chunk:has(fragment)) + end + end +end + +for _, entity in ipairs(all_entity_list) do + assert(evo.alive(entity) == not should_be_destroyed_entity_set[entity]) +end + +for _, entity in ipairs(all_entity_list) do + if evo.alive(entity) then + for fragment in pairs(entity_fragment_sets[entity]) do + assert(evo.has(entity, fragment) == not should_be_destroyed_entity_set[fragment]) + end + end end -for _, destroying_entity in ipairs(destroying_entity_list) do - assert(not evo.alive(destroying_entity)) +for _, entity in ipairs(all_entity_list) do + local entity_calls = remove_hook_calls[entity] or {} + + for fragment in pairs(entity_fragment_sets[entity]) do + local expected_call_count = 0 + + if hooked_fragment_set[fragment] then + if should_be_destroyed_entity_set[entity] or should_be_destroyed_entity_set[fragment] then + expected_call_count = 1 + end + end + + assert((entity_calls[fragment] or 0) == expected_call_count) + end + + for fragment in pairs(entity_calls) do + assert(entity_fragment_sets[entity][fragment]) + end end -for _, destroyed_entity in ipairs(should_be_destroyed_entity_list) do - assert(not evo.alive(destroyed_entity)) +for entity in pairs(remove_hook_calls) do + assert(entity_fragment_sets[entity]) end --- diff --git a/develop/fuzzing/link_fuzz.lua b/develop/fuzzing/link_fuzz.lua index 1dafa92..6c61e93 100644 --- a/develop/fuzzing/link_fuzz.lua +++ b/develop/fuzzing/link_fuzz.lua @@ -22,11 +22,33 @@ end)() local destroying_mark = evo.builder():tag():build() local destroying_query = evo.builder():include(destroying_mark):build() +local remove_hook_calls = {} ---@type table> +local hooked_link_fragment_set = {} ---@type table + +---@param entity evolved.entity +---@param fragment evolved.fragment +local function remove_hook(entity, fragment) + assert(evo.alive(entity) and evo.alive(fragment)) + + local entity_calls = remove_hook_calls[entity] + + if not entity_calls then + entity_calls = {} + remove_hook_calls[entity] = entity_calls + end + + entity_calls[fragment] = (entity_calls[fragment] or 0) + 1 +end + local all_link_fragment_list = {} ---@type evolved.fragment[] for i = 1, math.random(1, 4) do local builder = evo.builder():link() + if math.random(1, 2) == 1 then + builder:on_remove(remove_hook) + end + local r = math.random(1, 3) if r == 1 then @@ -40,6 +62,10 @@ for i = 1, math.random(1, 4) do end all_link_fragment_list[i] = builder:build() + + if evo.has(all_link_fragment_list[i], evo.ON_REMOVE) then + hooked_link_fragment_set[all_link_fragment_list[i]] = true + end end local all_entity_list = {} ---@type evolved.entity[] @@ -177,6 +203,30 @@ repeat end until not has_new_destroyed_entities +local entity_link_target_lists = {} ---@type table> + +for _, entity in ipairs(all_entity_list) do + local entity_link_target_list = {} + + for _, link_fragment in ipairs(all_link_fragment_list) do + local link_component = evo.get(entity, link_fragment) + + if link_component then + local link_target_list = {} + + for i, link_target in ipairs(link_component_targets(link_component)) do + link_target_list[i] = link_target + end + + entity_link_target_list[link_fragment] = link_target_list + end + end + + entity_link_target_lists[entity] = entity_link_target_list +end + +remove_hook_calls = {} + --- --- --- @@ -299,6 +349,38 @@ for _, link_fragment in ipairs(all_link_fragment_list) do end end +for _, entity in ipairs(all_entity_list) do + local entity_calls = remove_hook_calls[entity] or {} + + for link_fragment, link_target_list in pairs(entity_link_target_lists[entity]) do + local expected_call_count = 0 + + if hooked_link_fragment_set[link_fragment] then + local is_link_removed = should_be_destroyed_entity_set[entity] + + for _, link_target in ipairs(link_target_list) do + if should_be_destroyed_entity_set[link_target] then + is_link_removed = true + end + end + + if is_link_removed then + expected_call_count = 1 + end + end + + assert((entity_calls[link_fragment] or 0) == expected_call_count) + end + + for link_fragment in pairs(entity_calls) do + assert(entity_link_target_lists[entity][link_fragment]) + end +end + +for entity in pairs(remove_hook_calls) do + assert(entity_link_target_lists[entity]) +end + --- --- --- diff --git a/develop/testing/destroy_tests.lua b/develop/testing/destroy_tests.lua index 6b4c72a..624db63 100644 --- a/develop/testing/destroy_tests.lua +++ b/develop/testing/destroy_tests.lua @@ -104,3 +104,264 @@ do assert(not evo.alive(e)) end end + +do + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + + local remove_count = 0 + local g = evo.builder():set(f):on_remove(function(e, g, c) + remove_count = remove_count + 1 + assert(evo.alive_all(e, g) and c == 42) + end):build() + + local e = evo.spawn({ [g] = 42 }) + + evo.destroy(f) + assert(not evo.alive_any(f, g)) + assert(evo.alive(e) and not evo.has(e, g)) + assert(remove_count == 1) +end + +do + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + + local remove_count = 0 + local g = evo.builder():set(f):destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + :on_remove(function(e, g, c) + remove_count = remove_count + 1 + assert(evo.alive_all(f, e, g) and c == 42) + end):build() + + local e1 = evo.spawn({ [g] = 42 }) + local e2 = evo.spawn({ [f] = true, [g] = 42 }) + + evo.destroy(f) + assert(not evo.alive_any(f, g, e1, e2)) + assert(remove_count == 2) +end + +do + local q = evo.id() + + local remove_count = 0 + local g = evo.builder():set(q):on_remove(function(e, g, c) + remove_count = remove_count + 1 + assert(evo.alive_all(e, g) and c == 42) + end):build() + + local e1 = evo.spawn({ [g] = 42 }) + local e2 = evo.spawn({ [q] = true }) + + evo.batch_destroy(evo.builder():include(q):build()) + assert(not evo.alive_any(g, e2)) + assert(evo.alive(e1) and not evo.has(e1, g)) + assert(remove_count == 1) +end + +for _, f_first in ipairs({ true, false }) do + local remove_count = 0 + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + :on_remove(function(e, f) + remove_count = remove_count + 1 + assert(evo.alive_all(e, f)) + end):build() + + local g = evo.spawn({ [f] = true }) + local e = evo.spawn({ [g] = 42 }) + + if f_first then + evo.destroy(f, g) + else + evo.destroy(g, f) + end + + assert(not evo.alive_any(f, g)) + assert(evo.alive(e) and not evo.has(e, g)) + assert(remove_count == 1) +end + +do + local DE = evo.DESTRUCTION_POLICY_DESTROY_ENTITY + local RF = evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT + + for _, f_policy in ipairs({ DE, RF }) do + for _, g_policy in ipairs({ DE, RF }) do + for _, destroy_case in ipairs({ 'f', 'g', 'f,g', 'g,f' }) do + local f_remove_count, g_remove_count = 0, 0 + + local f, g = evo.id(2) + + evo.set(f, evo.DESTRUCTION_POLICY, f_policy) + evo.set(g, evo.DESTRUCTION_POLICY, g_policy) + + evo.set(f, evo.ON_REMOVE, function(e, ff, c) + f_remove_count = f_remove_count + 1 + assert(e == g and ff == f and c == 21) + assert(evo.alive_all(f, g)) + end) + + evo.set(g, evo.ON_REMOVE, function(e, gg, c) + g_remove_count = g_remove_count + 1 + assert(e == f and gg == g and c == 42) + assert(evo.alive_all(f, g)) + end) + + evo.set(f, g, 42) + evo.set(g, f, 21) + + if destroy_case == 'f' then + evo.destroy(f) + assert(not evo.alive(f)) + assert(evo.alive(g) == (f_policy == RF)) + elseif destroy_case == 'g' then + evo.destroy(g) + assert(not evo.alive(g)) + assert(evo.alive(f) == (g_policy == RF)) + elseif destroy_case == 'f,g' then + evo.destroy(f, g) + assert(not evo.alive_any(f, g)) + else + evo.destroy(g, f) + assert(not evo.alive_any(f, g)) + end + + assert(not evo.alive(f) or not evo.has(f, g)) + assert(not evo.alive(g) or not evo.has(g, f)) + + assert(f_remove_count == 1) + assert(g_remove_count == 1) + end + end + end +end + +for g_place = 1, 4 do + local remove_count = 0 + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY) + :on_remove(function(e, f) + remove_count = remove_count + 1 + assert(evo.alive_all(e, f)) + end):build() + + local x = evo.id() + evo.set(x, evo.INDEX) + + local g, plain_list = nil, {} + + for i = 1, 4 do + local e = evo.spawn({ [f] = true, [x] = i }) + + if i == g_place then + g = e + else + plain_list[#plain_list + 1] = e + end + end + + ---@cast g -? + local u = evo.spawn({ [g] = 42 }) + + evo.destroy(f) + assert(not evo.alive_any(f, g)) + assert(not evo.alive_any(plain_list[1], plain_list[2], plain_list[3])) + assert(evo.alive(u) and not evo.has(u, g)) + assert(remove_count == 4) + + for i = 1, 4 do + assert(evo.search(x, i) == nil) + end + + local e = evo.spawn({ [x] = 5 }) + assert(evo.search(x, 5) == e) +end + +do + local l1 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local l2 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t1, t2 = evo.id(2) + + local r = evo.spawn({ [l1] = t1, [l2] = t2 }) + + evo.destroy(t1, t2) + assert(not evo.alive_any(t1, t2, r)) + + local ids = { evo.id(8) } + + for i = 1, #ids do + for j = i + 1, #ids do + assert(ids[i] % 2 ^ 20 ~= ids[j] % 2 ^ 20) + end + end + + evo.destroy(ids[1], ids[2], ids[3], ids[4], ids[5], ids[6], ids[7], ids[8]) +end + +do + local l1 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local l2 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t = evo.id() + + local r = evo.spawn({ [l1] = t, [l2] = t }) + + evo.destroy(t) + assert(not evo.alive_any(t, r)) + + local ids = { evo.id(8) } + + for i = 1, #ids do + for j = i + 1, #ids do + assert(ids[i] % 2 ^ 20 ~= ids[j] % 2 ^ 20) + end + end + + evo.destroy(ids[1], ids[2], ids[3], ids[4], ids[5], ids[6], ids[7], ids[8]) +end + +do + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local l = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t = evo.id() + + local r = evo.spawn({ [f] = true, [l] = t }) + + for _, f_first in ipairs({ true, false }) do + if f_first then + evo.destroy(f, t) + else + evo.destroy(t, f) + end + + assert(not evo.alive_any(f, t, r)) + assert(evo.search(l, t) == nil) + + f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + t = evo.id() + r = evo.spawn({ [f] = true, [l] = t }) + end +end + +do + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local l = evo.builder():link():build() + + local t = evo.id() + + local r1 = evo.spawn({ [f] = true, [l] = t }) + local r2 = evo.spawn({ [f] = true, [l] = t }) + + evo.destroy(t, f) + assert(not evo.alive_any(f, t, r1, r2)) + assert(evo.search(l, t) == nil) +end + +do + local e = evo.id() + + evo.destroy() + assert(evo.alive(e)) + + evo.destroy(e) +end diff --git a/evolved.lua b/evolved.lua index ca3ed59..b89502e 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1396,11 +1396,10 @@ local __shrink_chunk local __clear_chunk_list local __clear_entity_one local __clear_entity_list +local __destroy_chunk_list local __destroy_entity_one local __destroy_entity_list -local __destroy_fragment_one -local __destroy_fragment_list -local __destroy_fragment_stack +local __destroy_entity_stack local __remove_entity_fragments local __chunk_assign @@ -4176,6 +4175,30 @@ function __clear_entity_list(entity_list, entity_count) end end +---@param chunk_list evolved.chunk[] +---@param chunk_count integer +function __destroy_chunk_list(chunk_list, chunk_count) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') + end + + if chunk_count == 0 then + return + end + + ---@type evolved.chunk[] + local chunk_stack = __acquire_table(__table_pool_tag.chunk_list) + + ---@type evolved.entity[] + local entity_stack = __acquire_table(__table_pool_tag.entity_list) + + __lua_table_move(chunk_list, 1, chunk_count, 1, chunk_stack) + __destroy_entity_stack(chunk_stack, chunk_count, entity_stack, 0) + + __release_table(__table_pool_tag.chunk_list, chunk_stack, true, true) + __release_table(__table_pool_tag.entity_list, entity_stack, true, true) +end + ---@param entity evolved.entity function __destroy_entity_one(entity) if __defer_depth <= 0 then @@ -4246,190 +4269,200 @@ end ---@param entity_list evolved.entity[] ---@param entity_count integer ----@param reverse? boolean -function __destroy_entity_list(entity_list, entity_count, reverse) +function __destroy_entity_list(entity_list, entity_count) if __defer_depth <= 0 then __debug_fns.error_fmt('this operation should be deferred') end - if reverse then - for entity_index = entity_count, 1, -1 do - local entity = entity_list[entity_index] - __destroy_entity_one(entity) - end - else - for entity_index = 1, entity_count do - local entity = entity_list[entity_index] - __destroy_entity_one(entity) - end - end -end - ----@param fragment evolved.fragment -function __destroy_fragment_one(fragment) - if __defer_depth <= 0 then - __debug_fns.error_fmt('this operation should be deferred') + if entity_count == 0 then + return end - ---@type evolved.fragment[] - local processing_fragment_stack = __acquire_table(__table_pool_tag.fragment_list) - local processing_fragment_stack_size = 0 + ---@type evolved.chunk[] + local chunk_stack = __acquire_table(__table_pool_tag.chunk_list) - do - processing_fragment_stack_size = processing_fragment_stack_size + 1 - processing_fragment_stack[processing_fragment_stack_size] = fragment - end + ---@type evolved.entity[] + local entity_stack = __acquire_table(__table_pool_tag.entity_list) - __destroy_fragment_stack( - processing_fragment_stack, - processing_fragment_stack_size) + __lua_table_move(entity_list, 1, entity_count, 1, entity_stack) + __destroy_entity_stack(chunk_stack, 0, entity_stack, entity_count) - __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, true, true) + __release_table(__table_pool_tag.entity_list, entity_stack, true, true) + __release_table(__table_pool_tag.chunk_list, chunk_stack, true, true) end ----@param fragment_list evolved.fragment[] ----@param fragment_count integer -function __destroy_fragment_list(fragment_list, fragment_count) +---@param chunk_stack evolved.chunk[] +---@param chunk_stack_size integer +---@param entity_stack evolved.entity[] +---@param entity_stack_size integer +function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, entity_stack_size) if __defer_depth <= 0 then __debug_fns.error_fmt('this operation should be deferred') end - ---@type evolved.fragment[] - local processing_fragment_stack = __acquire_table(__table_pool_tag.fragment_list) - local processing_fragment_stack_size = 0 + local freelist_ids = __freelist_ids - do - __lua_table_move( - fragment_list, 1, fragment_count, - processing_fragment_stack_size + 1, processing_fragment_stack) + local entity_chunks = __entity_chunks + local entity_places = __entity_places - processing_fragment_stack_size = processing_fragment_stack_size + fragment_count - end + local minor_chunks = __minor_chunks + local search_indices = __search_indices + local target_fragments = __target_fragments - __destroy_fragment_stack( - processing_fragment_stack, - processing_fragment_stack_size) + ---@type table + local processed_entity_set = __acquire_table(__table_pool_tag.fragment_set) - __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, true, true) -end + ---@type evolved.entity[] + local processed_entity_list = __acquire_table(__table_pool_tag.entity_list) + local processed_entity_count = 0 ----@param processing_fragment_stack evolved.fragment[] ----@param processing_fragment_stack_size integer -function __destroy_fragment_stack(processing_fragment_stack, processing_fragment_stack_size) - if __defer_depth <= 0 then - __debug_fns.error_fmt('this operation should be deferred') - end + ---@type evolved.entity[] + local releasing_entity_list = __acquire_table(__table_pool_tag.entity_list) + local releasing_entity_count = 0 - ---@type table - local processed_fragment_set = __acquire_table(__table_pool_tag.fragment_set) + local removing_count = 0 ---@type integer + local removing_target_list ---@type (evolved.entity|false)[]? + local removing_fragment_list ---@type evolved.fragment[]? - ---@type evolved.fragment[] - local releasing_fragment_list = __acquire_table(__table_pool_tag.fragment_list) - local releasing_fragment_count = 0 ---@type integer + while chunk_stack_size > 0 or entity_stack_size > 0 do + if chunk_stack_size > 0 then + local chunk = chunk_stack[chunk_stack_size] - local destroy_entity_policy_fragment_list ---@type evolved.fragment[]? - local destroy_entity_policy_fragment_count = 0 ---@type integer + chunk_stack[chunk_stack_size] = nil + chunk_stack_size = chunk_stack_size - 1 - local remove_fragment_policy_fragment_list ---@type evolved.fragment[]? - local remove_fragment_policy_fragment_count = 0 ---@type integer + local entity_stack_base = entity_stack_size - local remove_fragment_link_fragment_list ---@type evolved.fragment[]? - local remove_fragment_link_target_list ---@type evolved.entity[]? - local remove_fragment_link_count = 0 ---@type integer + local chunk_entity_list = chunk.__entity_list + local chunk_entity_count = chunk.__entity_count - while processing_fragment_stack_size > 0 do - local processing_fragment = processing_fragment_stack[processing_fragment_stack_size] + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] + + if minor_chunks[entity] or target_fragments[entity] then + entity_stack_size = entity_stack_size + 1 + entity_stack[entity_stack_size] = entity + end + end - processing_fragment_stack[processing_fragment_stack_size] = nil - processing_fragment_stack_size = processing_fragment_stack_size - 1 + if entity_stack_base == entity_stack_size then + __lua_table_move( + chunk_entity_list, 1, chunk_entity_count, + releasing_entity_count + 1, releasing_entity_list) - if processed_fragment_set[processing_fragment] then - -- this fragment has already beed processed - else - processed_fragment_set[processing_fragment] = true + releasing_entity_count = releasing_entity_count + chunk_entity_count - do - releasing_fragment_count = releasing_fragment_count + 1 - releasing_fragment_list[releasing_fragment_count] = processing_fragment - end + __chunk_clear(chunk) + else + for place = chunk_entity_count, 1, -1 do + local entity = chunk_entity_list[place] - if __minor_chunks[processing_fragment] then - local processing_fragment_destruction_policy = __evolved_get(processing_fragment, __DESTRUCTION_POLICY) - or __DESTRUCTION_POLICY_REMOVE_FRAGMENT + if not minor_chunks[entity] and not target_fragments[entity] then + releasing_entity_count = releasing_entity_count + 1 + releasing_entity_list[releasing_entity_count] = entity - if processing_fragment_destruction_policy == __DESTRUCTION_POLICY_DESTROY_ENTITY then - if not destroy_entity_policy_fragment_list then - ---@type evolved.fragment[] - destroy_entity_policy_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + __clear_entity_one(entity) end + end + end + else + local entity = entity_stack[entity_stack_size] - destroy_entity_policy_fragment_count = destroy_entity_policy_fragment_count + 1 - destroy_entity_policy_fragment_list[destroy_entity_policy_fragment_count] = processing_fragment - - __trace_minor_chunks(processing_fragment, function(chunk) - local chunk_entity_list = chunk.__entity_list - local chunk_entity_count = chunk.__entity_count + entity_stack[entity_stack_size] = nil + entity_stack_size = entity_stack_size - 1 - __lua_table_move( - chunk_entity_list, 1, chunk_entity_count, - processing_fragment_stack_size + 1, processing_fragment_stack) + if processed_entity_set[entity] then + -- this entity has already been processed + else + local entity_minor_chunks = minor_chunks[entity] + local entity_target_fragments = target_fragments[entity] - processing_fragment_stack_size = processing_fragment_stack_size + chunk_entity_count - end) - elseif processing_fragment_destruction_policy == __DESTRUCTION_POLICY_REMOVE_FRAGMENT then - if not remove_fragment_policy_fragment_list then - ---@type evolved.fragment[] - remove_fragment_policy_fragment_list = __acquire_table(__table_pool_tag.fragment_list) - end + if not entity_minor_chunks and not entity_target_fragments then + releasing_entity_count = releasing_entity_count + 1 + releasing_entity_list[releasing_entity_count] = entity - remove_fragment_policy_fragment_count = remove_fragment_policy_fragment_count + 1 - remove_fragment_policy_fragment_list[remove_fragment_policy_fragment_count] = processing_fragment + __clear_entity_one(entity) else - __debug_fns.error_fmt('unknown DESTRUCTION_POLICY (%s) on (%s)', - __id_name(processing_fragment_destruction_policy), __id_name(processing_fragment)) - end - end + processed_entity_set[entity] = true + + processed_entity_count = processed_entity_count + 1 + processed_entity_list[processed_entity_count] = entity + + releasing_entity_count = releasing_entity_count + 1 + releasing_entity_list[releasing_entity_count] = entity + + if entity_minor_chunks then + local destruction_policy = __evolved_get(entity, __DESTRUCTION_POLICY) + or __DESTRUCTION_POLICY_REMOVE_FRAGMENT + + if destruction_policy == __DESTRUCTION_POLICY_DESTROY_ENTITY then + local minor_chunk_list = entity_minor_chunks.__item_list + local minor_chunk_count = entity_minor_chunks.__item_count + + __lua_table_move( + minor_chunk_list, 1, minor_chunk_count, + chunk_stack_size + 1, chunk_stack) - local target_fragments = __target_fragments[processing_fragment] - - if target_fragments then - __target_fragments[processing_fragment] = nil - - local target_fragment_list = target_fragments.__item_list - local target_fragment_count = target_fragments.__item_count - - for target_fragment_index = 1, target_fragment_count do - local target_fragment = target_fragment_list[target_fragment_index] - local target_fragment_search_index = __search_indices[target_fragment] - - if target_fragment_search_index - and target_fragment_search_index.__target_fragment == target_fragment - and target_fragment_search_index.__head_entities[processing_fragment] - then - local target_fragment_link_policy = __evolved_get(target_fragment, __LINK_POLICY) - or __LINK_POLICY_REMOVE_FRAGMENT - - if target_fragment_link_policy == __LINK_POLICY_DESTROY_ENTITY then - processing_fragment_stack_size = processing_fragment_stack_size + __evolved_multi_search_to( - processing_fragment_stack, processing_fragment_stack_size + 1, - target_fragment, processing_fragment) - elseif target_fragment_link_policy == __LINK_POLICY_REMOVE_FRAGMENT then - if not remove_fragment_link_target_list then - ---@type evolved.entity[] - remove_fragment_link_target_list = __acquire_table(__table_pool_tag.entity_list) + chunk_stack_size = chunk_stack_size + minor_chunk_count + else + if destruction_policy ~= __DESTRUCTION_POLICY_REMOVE_FRAGMENT then + __debug_fns.warning_fmt('unknown DESTRUCTION_POLICY (%s) on (%s), %s is used instead', + __id_name(destruction_policy), __id_name(entity), + __id_name(__DESTRUCTION_POLICY_REMOVE_FRAGMENT)) end - if not remove_fragment_link_fragment_list then + if removing_count == 0 then + ---@type (evolved.entity|false)[] + removing_target_list = __acquire_table(__table_pool_tag.entity_list) ---@type evolved.fragment[] - remove_fragment_link_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + removing_fragment_list = __acquire_table(__table_pool_tag.fragment_list) end - remove_fragment_link_count = remove_fragment_link_count + 1 - remove_fragment_link_target_list[remove_fragment_link_count] = processing_fragment - remove_fragment_link_fragment_list[remove_fragment_link_count] = target_fragment - else - __debug_fns.error_fmt('unknown LINK_POLICY (%s) on (%s)', - __id_name(target_fragment_link_policy), __id_name(target_fragment)) + removing_count = removing_count + 1 + removing_target_list[removing_count] = false + removing_fragment_list[removing_count] = entity + end + end + + if entity_target_fragments then + target_fragments[entity] = nil + + local target_fragment_list = entity_target_fragments.__item_list + local target_fragment_count = entity_target_fragments.__item_count + + for target_fragment_index = 1, target_fragment_count do + local target_fragment = target_fragment_list[target_fragment_index] + local target_fragment_search_index = search_indices[target_fragment] + + if target_fragment_search_index + and target_fragment_search_index.__target_fragment == target_fragment + and target_fragment_search_index.__head_entities[entity] + then + local link_policy = __evolved_get(target_fragment, __LINK_POLICY) + or __LINK_POLICY_REMOVE_FRAGMENT + + if link_policy == __LINK_POLICY_DESTROY_ENTITY then + entity_stack_size = entity_stack_size + __evolved_multi_search_to( + entity_stack, entity_stack_size + 1, target_fragment, entity) + else + if link_policy ~= __LINK_POLICY_REMOVE_FRAGMENT then + __debug_fns.warning_fmt('unknown LINK_POLICY (%s) on (%s), %s is used instead', + __id_name(link_policy), __id_name(target_fragment), + __id_name(__LINK_POLICY_REMOVE_FRAGMENT)) + end + + if removing_count == 0 then + ---@type (evolved.entity|false)[] + removing_target_list = __acquire_table(__table_pool_tag.entity_list) + ---@type evolved.fragment[] + removing_fragment_list = __acquire_table(__table_pool_tag.fragment_list) + end + + removing_count = removing_count + 1 + removing_target_list[removing_count] = entity + removing_fragment_list[removing_count] = target_fragment + end + end end end end @@ -4437,61 +4470,126 @@ function __destroy_fragment_stack(processing_fragment_stack, processing_fragment end end - if destroy_entity_policy_fragment_count > 0 then - ---@cast destroy_entity_policy_fragment_list -? + if removing_count > 0 then + ---@cast removing_target_list -? + ---@cast removing_fragment_list -? - for i = 1, destroy_entity_policy_fragment_count do - local minor = destroy_entity_policy_fragment_list[i] - __trace_minor_chunks(minor, __chunk_clear) - end + for removing_index = 1, removing_count do + local target = removing_target_list[removing_index] + local fragment = removing_fragment_list[removing_index] - __release_table(__table_pool_tag.fragment_list, destroy_entity_policy_fragment_list, false, true) - end + if target then + local referrer_count = __evolved_multi_search_to(entity_stack, 1, fragment, target) + + while referrer_count > 0 do + local referrer = entity_stack[referrer_count] - if remove_fragment_policy_fragment_count > 0 then - ---@cast remove_fragment_policy_fragment_list -? + entity_stack[referrer_count] = nil + referrer_count = referrer_count - 1 - for i = 1, remove_fragment_policy_fragment_count do - local minor = remove_fragment_policy_fragment_list[i] - __trace_minor_chunks(minor, __chunk_remove, minor) + if not processed_entity_set[referrer] then + __remove_entity_fragments(referrer, fragment) + end + end + else + local fragment_chunks = minor_chunks[fragment] + local fragment_chunk_list = fragment_chunks.__item_list + local fragment_chunk_count = fragment_chunks.__item_count + + __lua_table_move( + fragment_chunk_list, 1, fragment_chunk_count, + 1, chunk_stack) + + while fragment_chunk_count > 0 do + local chunk = chunk_stack[fragment_chunk_count] + + chunk_stack[fragment_chunk_count] = nil + fragment_chunk_count = fragment_chunk_count - 1 + + __chunk_remove(chunk, fragment) + end + end end - __release_table(__table_pool_tag.fragment_list, remove_fragment_policy_fragment_list, false, true) + __release_table(__table_pool_tag.entity_list, removing_target_list, false, true) + __release_table(__table_pool_tag.fragment_list, removing_fragment_list, false, true) end - if remove_fragment_link_count > 0 then - ---@cast remove_fragment_link_fragment_list -? - ---@cast remove_fragment_link_target_list -? + for processed_entity_index = 1, processed_entity_count do + local entity = processed_entity_list[processed_entity_index] + local entity_primary = entity % 2 ^ 20 + + local chunk = entity_chunks[entity_primary] + local place = entity_places[entity_primary] + + if chunk then + local chunk_remove_fragment_list = chunk.__remove_fragment_list + + if chunk_remove_fragment_list then + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages + + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do + local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - ---@type evolved.entity[] - local referrer_list = __acquire_table(__table_pool_tag.entity_list) + ---@type evolved.remove_hook? + local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) - for i = 1, remove_fragment_link_count do - local link_fragment = remove_fragment_link_fragment_list[i] - local link_target = remove_fragment_link_target_list[i] + if fragment_on_remove then + local component_index = chunk_component_indices[fragment] - local referrer_count = __search_indices[link_fragment] - and __evolved_multi_search_to(referrer_list, 1, link_fragment, link_target) - or 0 + if component_index then + local component_storage = chunk_component_storages[component_index] + local old_component = component_storage[place] + fragment_on_remove(entity, fragment, old_component) + else + fragment_on_remove(entity, fragment) + end + end + end + end - for referrer_index = 1, referrer_count do - local referrer = referrer_list[referrer_index] + local chunk_index_fragment_list = chunk.__index_fragment_list - if not processed_fragment_set[referrer] then - __remove_entity_fragments(referrer, link_fragment) + 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 fragment_search_index = search_indices[fragment] + __search_index_fns.remove(fragment_search_index, entity) end end end + end + + for processed_entity_index = 1, processed_entity_count do + local entity = processed_entity_list[processed_entity_index] + local entity_primary = entity % 2 ^ 20 + + local chunk = entity_chunks[entity_primary] + local place = entity_places[entity_primary] + + if chunk then + __detach_entity(chunk, place) + + entity_chunks[entity_primary] = false + entity_places[entity_primary] = 0 - __release_table(__table_pool_tag.entity_list, referrer_list, false, true) - __release_table(__table_pool_tag.fragment_list, remove_fragment_link_fragment_list, false, true) - __release_table(__table_pool_tag.entity_list, remove_fragment_link_target_list, false, true) + __structural_changes = __structural_changes + 1 + end end - __destroy_entity_list(releasing_fragment_list, releasing_fragment_count, true) + for releasing_entity_index = 1, releasing_entity_count do + local entity = releasing_entity_list[releasing_entity_index] + local entity_primary = entity % 2 ^ 20 + + if freelist_ids[entity_primary] == entity then + __release_id(entity) + end + end - __release_table(__table_pool_tag.fragment_list, releasing_fragment_list, false, true) - __release_table(__table_pool_tag.fragment_set, processed_fragment_set, true, false) + __release_table(__table_pool_tag.entity_list, releasing_entity_list, false, true) + __release_table(__table_pool_tag.entity_list, processed_entity_list, false, true) + __release_table(__table_pool_tag.fragment_set, processed_entity_set, true, false) end ---@param entity evolved.entity @@ -6688,11 +6786,8 @@ function __evolved_destroy(...) local minor_chunks = __minor_chunks local target_fragments = __target_fragments - local purging_entity_list ---@type evolved.entity[]? - local purging_entity_count = 0 ---@type integer - - local purging_fragment_list ---@type evolved.fragment[]? - local purging_fragment_count = 0 ---@type integer + local destroying_entity_list ---@type evolved.entity[]? + local destroying_entity_count = 0 ---@type integer for argument_index = 1, argument_count do ---@type evolved.entity @@ -6701,37 +6796,22 @@ function __evolved_destroy(...) if __freelist_ids[entity_primary] ~= entity then -- nothing to destroy from non-alive entities + elseif not minor_chunks[entity] and not target_fragments[entity] then + __destroy_entity_one(entity) else - local is_fragment_or_target = minor_chunks[entity] or target_fragments[entity] - - if not is_fragment_or_target then - if not purging_entity_list then - ---@type evolved.entity[] - purging_entity_list = __acquire_table(__table_pool_tag.entity_list) - end - - purging_entity_count = purging_entity_count + 1 - purging_entity_list[purging_entity_count] = entity - else - if not purging_fragment_list then - ---@type evolved.fragment[] - purging_fragment_list = __acquire_table(__table_pool_tag.fragment_list) - end - - purging_fragment_count = purging_fragment_count + 1 - purging_fragment_list[purging_fragment_count] = entity + if not destroying_entity_list then + ---@type evolved.entity[] + destroying_entity_list = __acquire_table(__table_pool_tag.entity_list) end - end - end - if purging_fragment_list then - __destroy_fragment_list(purging_fragment_list, purging_fragment_count) - __release_table(__table_pool_tag.fragment_list, purging_fragment_list, false, true) + destroying_entity_count = destroying_entity_count + 1 + destroying_entity_list[destroying_entity_count] = entity + end end - if purging_entity_list then - __destroy_entity_list(purging_entity_list, purging_entity_count) - __release_table(__table_pool_tag.entity_list, purging_entity_list, false, true) + if destroying_entity_list then + __destroy_entity_list(destroying_entity_list, destroying_entity_count) + __release_table(__table_pool_tag.entity_list, destroying_entity_list, false, true) end end @@ -6752,14 +6832,18 @@ function __evolved_destroy_one(entity) if __freelist_ids[entity_primary] ~= entity then -- nothing to destroy from non-alive entities + elseif not __minor_chunks[entity] and not __target_fragments[entity] then + __destroy_entity_one(entity) else - local is_fragment_or_target = __minor_chunks[entity] or __target_fragments[entity] + ---@type evolved.entity[] + local destroying_entity_list = __acquire_table(__table_pool_tag.entity_list) + local destroying_entity_count = 0 - if not is_fragment_or_target then - __destroy_entity_one(entity) - else - __destroy_fragment_one(entity) - end + destroying_entity_count = destroying_entity_count + 1 + destroying_entity_list[destroying_entity_count] = entity + + __destroy_entity_list(destroying_entity_list, destroying_entity_count) + __release_table(__table_pool_tag.entity_list, destroying_entity_list, false, true) end end @@ -6960,17 +7044,8 @@ function __evolved_batch_destroy(...) __evolved_defer() do - local minor_chunks = __minor_chunks - local target_fragments = __target_fragments - - local clearing_chunk_list ---@type evolved.chunk[]? - local clearing_chunk_count = 0 ---@type integer - - local purging_entity_list ---@type evolved.entity[]? - local purging_entity_count = 0 ---@type integer - - local purging_fragment_list ---@type evolved.fragment[]? - local purging_fragment_count = 0 ---@type integer + local destroying_chunk_list ---@type evolved.chunk[]? + local destroying_chunk_count = 0 ---@type integer for argument_index = 1, argument_count do ---@type evolved.query @@ -6981,57 +7056,21 @@ function __evolved_batch_destroy(...) __debug_fns.warning_fmt('the query (%s) is not alive and cannot be executed', __id_name(query)) else - for chunk, entity_list, entity_count in __evolved_execute(query) do - do - if not clearing_chunk_list then - ---@type evolved.chunk[] - clearing_chunk_list = __acquire_table(__table_pool_tag.chunk_list) - end - - clearing_chunk_count = clearing_chunk_count + 1 - clearing_chunk_list[clearing_chunk_count] = chunk + for chunk in __evolved_execute(query) do + if not destroying_chunk_list then + ---@type evolved.chunk[] + destroying_chunk_list = __acquire_table(__table_pool_tag.chunk_list) end - for i = 1, entity_count do - local entity = entity_list[i] - - local is_fragment_or_target = minor_chunks[entity] or target_fragments[entity] - - if not is_fragment_or_target then - if not purging_entity_list then - ---@type evolved.entity[] - purging_entity_list = __acquire_table(__table_pool_tag.entity_list) - end - - purging_entity_count = purging_entity_count + 1 - purging_entity_list[purging_entity_count] = entity - else - if not purging_fragment_list then - ---@type evolved.fragment[] - purging_fragment_list = __acquire_table(__table_pool_tag.fragment_list) - end - - purging_fragment_count = purging_fragment_count + 1 - purging_fragment_list[purging_fragment_count] = entity - end - end + destroying_chunk_count = destroying_chunk_count + 1 + destroying_chunk_list[destroying_chunk_count] = chunk end end end - if purging_fragment_list then - __destroy_fragment_list(purging_fragment_list, purging_fragment_count) - __release_table(__table_pool_tag.fragment_list, purging_fragment_list, false, true) - end - - if clearing_chunk_list then - __clear_chunk_list(clearing_chunk_list, clearing_chunk_count) - __release_table(__table_pool_tag.chunk_list, clearing_chunk_list, false, true) - end - - if purging_entity_list then - __destroy_entity_list(purging_entity_list, purging_entity_count) - __release_table(__table_pool_tag.entity_list, purging_entity_list, false, true) + if destroying_chunk_list then + __destroy_chunk_list(destroying_chunk_list, destroying_chunk_count) + __release_table(__table_pool_tag.chunk_list, destroying_chunk_list, false, true) end end From 327aac2cfc2da8aebbd52b24c44a7f6e14d860f6 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Fri, 23 Oct 2026 17:24:39 +0700 Subject: [PATCH 28/48] inline version of releasing id lists --- evolved.lua | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/evolved.lua b/evolved.lua index b89502e..59c9717 100644 --- a/evolved.lua +++ b/evolved.lua @@ -695,18 +695,34 @@ local function __release_id(id) local acquired_primary = id % 2 ^ 20 local shifted_secondary = id - acquired_primary - local freelist_ids = __freelist_ids + if __freelist_ids[acquired_primary] == id then + shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 + and 2 ^ 20 + or shifted_secondary + 2 ^ 20 - if freelist_ids[acquired_primary] ~= id then - __debug_fns.error_fmt('the id (%d) is not acquired or already released', id) + __freelist_ids[acquired_primary] = __available_primary + shifted_secondary + __available_primary = acquired_primary end +end + +---@param id_list evolved.id[] +---@param id_count integer +local function __release_id_list(id_list, id_count) + for id_index = 1, id_count do + local id = id_list[id_index] - shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 - and 2 ^ 20 - or shifted_secondary + 2 ^ 20 + local acquired_primary = id % 2 ^ 20 + local shifted_secondary = id - acquired_primary - freelist_ids[acquired_primary] = __available_primary + shifted_secondary - __available_primary = acquired_primary + if __freelist_ids[acquired_primary] == id then + shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 + and 2 ^ 20 + or shifted_secondary + 2 ^ 20 + + __freelist_ids[acquired_primary] = __available_primary + shifted_secondary + __available_primary = acquired_primary + end + end end --- @@ -4300,8 +4316,6 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent __debug_fns.error_fmt('this operation should be deferred') end - local freelist_ids = __freelist_ids - local entity_chunks = __entity_chunks local entity_places = __entity_places @@ -4578,14 +4592,7 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent end end - for releasing_entity_index = 1, releasing_entity_count do - local entity = releasing_entity_list[releasing_entity_index] - local entity_primary = entity % 2 ^ 20 - - if freelist_ids[entity_primary] == entity then - __release_id(entity) - end - end + __release_id_list(releasing_entity_list, releasing_entity_count) __release_table(__table_pool_tag.entity_list, releasing_entity_list, false, true) __release_table(__table_pool_tag.entity_list, processed_entity_list, false, true) From 313922004f3b8637851a9e205fda38475690afdd Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 24 Oct 2026 00:33:50 +0700 Subject: [PATCH 29/48] inline version of acquiring id lists --- evolved.lua | 82 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/evolved.lua b/evolved.lua index 59c9717..17dee50 100644 --- a/evolved.lua +++ b/evolved.lua @@ -658,14 +658,14 @@ local function __acquire_id() local acquired_primary = available_primary local freelist_id = freelist_ids[acquired_primary] - local next_available_primary = freelist_id % 2 ^ 20 - local shifted_secondary = freelist_id - next_available_primary - - __available_primary = next_available_primary + available_primary = freelist_id % 2 ^ 20 + local shifted_secondary = freelist_id - available_primary local acquired_id = acquired_primary + shifted_secondary freelist_ids[acquired_primary] = acquired_id + __available_primary = available_primary + return acquired_id --[[@as evolved.id]] else local acquired_count = __acquired_count @@ -674,18 +674,16 @@ local function __acquire_id() __debug_fns.error_fmt('id index overflow') end - acquired_count = acquired_count + 1 - __acquired_count = acquired_count - - local acquired_primary = acquired_count - local shifted_secondary = 2 ^ 20 + local acquired_primary = acquired_count + 1 - local acquired_id = acquired_primary + shifted_secondary + local acquired_id = acquired_primary + 2 ^ 20 freelist_ids[acquired_primary] = acquired_id __entity_chunks[acquired_primary] = false __entity_places[acquired_primary] = 0 + __acquired_count = acquired_count + 1 + return acquired_id --[[@as evolved.id]] end end @@ -705,6 +703,54 @@ local function __release_id(id) end end +---@param id_list evolved.id[] +---@param id_first integer +---@param id_count integer +local function __acquire_id_list(id_list, id_first, id_count) + local freelist_ids = __freelist_ids + local available_primary = __available_primary + + while available_primary ~= 0 and id_count > 0 do + local acquired_primary = available_primary + local freelist_id = freelist_ids[acquired_primary] + + available_primary = freelist_id % 2 ^ 20 + local shifted_secondary = freelist_id - available_primary + + local acquired_id = acquired_primary + shifted_secondary + freelist_ids[acquired_primary] = acquired_id + + id_list[id_first] = acquired_id --[[@as evolved.id]] + id_first, id_count = id_first + 1, id_count - 1 + end + + __available_primary = available_primary + + if id_count > 0 then + local acquired_count = __acquired_count + + if acquired_count + id_count > 2 ^ 20 - 1 then + __debug_fns.error_fmt('id index overflow') + end + + local entity_chunks = __entity_chunks + local entity_places = __entity_places + + for acquired_primary = acquired_count + 1, acquired_count + id_count do + local acquired_id = acquired_primary + 2 ^ 20 + freelist_ids[acquired_primary] = acquired_id + + entity_chunks[acquired_primary] = false + entity_places[acquired_primary] = 0 + + id_list[id_first] = acquired_id --[[@as evolved.id]] + id_first = id_first + 1 + end + + __acquired_count = acquired_count + id_count + end +end + ---@param id_list evolved.id[] ---@param id_count integer local function __release_id_list(id_list, id_count) @@ -6040,9 +6086,7 @@ function __evolved_multi_spawn_to(out_entity_list, out_entity_first, end end - for entity_index = out_entity_first, out_entity_first + entity_count - 1 do - out_entity_list[entity_index] = __acquire_id() - end + __acquire_id_list(out_entity_list, out_entity_first, entity_count) if not component_table or not __lua_next(component_table) then return @@ -6166,9 +6210,7 @@ function __evolved_multi_clone_to(out_entity_list, out_entity_first, end end - for entity_index = out_entity_first, out_entity_first + entity_count - 1 do - out_entity_list[entity_index] = __acquire_id() - end + __acquire_id_list(out_entity_list, out_entity_first, entity_count) if __defer_depth > 0 then __defer_multi_clone_entity(prefab, @@ -7816,9 +7858,7 @@ function __builder_mt:multi_spawn_to(out_entity_list, out_entity_first, end end - for entity_index = out_entity_first, out_entity_first + entity_count - 1 do - out_entity_list[entity_index] = __acquire_id() - end + __acquire_id_list(out_entity_list, out_entity_first, entity_count) if not component_table or not __lua_next(component_table) then return @@ -7938,9 +7978,7 @@ function __builder_mt:multi_clone_to(out_entity_list, out_entity_first, end end - for entity_index = out_entity_first, out_entity_first + entity_count - 1 do - out_entity_list[entity_index] = __acquire_id() - end + __acquire_id_list(out_entity_list, out_entity_first, entity_count) if __defer_depth > 0 then __defer_multi_clone_entity(prefab, From 053a163b4674e68673991f8454603bfddc67b9fb Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 24 Oct 2026 00:43:29 +0700 Subject: [PATCH 30/48] free up twp top-level local variables --- evolved.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/evolved.lua b/evolved.lua index 17dee50..4fa622e 100644 --- a/evolved.lua +++ b/evolved.lua @@ -5779,7 +5779,7 @@ end ---@param query evolved.query ---@param execute evolved.execute ---@param ... any processing payload -local function __query_execute(query, execute, ...) +function __iterator_fns.__query_execute(query, execute, ...) for chunk, entity_list, entity_count in __evolved_execute(query) do execute(chunk, entity_list, entity_count, ...) end @@ -5787,7 +5787,7 @@ end ---@param system evolved.system ---@param ... any processing payload -local function __system_process(system, ...) +function __iterator_fns.__system_process(system, ...) ---@type evolved.query?, evolved.execute?, evolved.prologue?, evolved.epilogue? local query, execute, prologue, epilogue = __evolved_get(system, __QUERY, __EXECUTE, __PROLOGUE, __EPILOGUE) @@ -5803,7 +5803,8 @@ local function __system_process(system, ...) if execute then __evolved_defer() do - local success, result = __lua_xpcall(__query_execute, __error_handler, query or system, execute, ...) + local success, result = __lua_xpcall(__iterator_fns.__query_execute, __error_handler, + query or system, execute, ...) if not success then __evolved_cancel() @@ -5821,7 +5822,7 @@ local function __system_process(system, ...) for subsystem_index = 1, subsystem_count do local subsystem = subsystem_list[subsystem_index] if not __evolved_has(subsystem, __DISABLED) then - __system_process(subsystem, ...) + __iterator_fns.__system_process(subsystem, ...) end end @@ -7388,7 +7389,7 @@ function __evolved_process(...) __debug_fns.warning_fmt('the system (%s) is not alive and cannot be processed', __id_name(system)) else - __system_process(system) + __iterator_fns.__system_process(system) end end end @@ -7403,7 +7404,7 @@ function __evolved_process_with(system, ...) __id_name(system)) end - __system_process(system, ...) + __iterator_fns.__system_process(system, ...) end ---@param yesno boolean From af3991b564397de841060e9c94e34355c80cbe17 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sat, 24 Oct 2026 06:49:40 +0700 Subject: [PATCH 31/48] use constants instead 2 ^ 20 and 2 ^ 40 to avoid floating indexing on lua 5.4 --- evolved.lua | 151 ++++++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 75 deletions(-) diff --git a/evolved.lua b/evolved.lua index 4fa622e..cbef7f1 100644 --- a/evolved.lua +++ b/evolved.lua @@ -658,7 +658,7 @@ local function __acquire_id() local acquired_primary = available_primary local freelist_id = freelist_ids[acquired_primary] - available_primary = freelist_id % 2 ^ 20 + available_primary = freelist_id % 1048576 local shifted_secondary = freelist_id - available_primary local acquired_id = acquired_primary + shifted_secondary @@ -670,13 +670,13 @@ local function __acquire_id() else local acquired_count = __acquired_count - if acquired_count == 2 ^ 20 - 1 then + if acquired_count == 1048576 - 1 then __debug_fns.error_fmt('id index overflow') end local acquired_primary = acquired_count + 1 - local acquired_id = acquired_primary + 2 ^ 20 + local acquired_id = acquired_primary + 1048576 freelist_ids[acquired_primary] = acquired_id __entity_chunks[acquired_primary] = false @@ -690,13 +690,13 @@ end ---@param id evolved.id local function __release_id(id) - local acquired_primary = id % 2 ^ 20 + local acquired_primary = id % 1048576 local shifted_secondary = id - acquired_primary if __freelist_ids[acquired_primary] == id then - shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 - and 2 ^ 20 - or shifted_secondary + 2 ^ 20 + shifted_secondary = shifted_secondary == 1099511627776 - 1048576 + and 1048576 + or shifted_secondary + 1048576 __freelist_ids[acquired_primary] = __available_primary + shifted_secondary __available_primary = acquired_primary @@ -714,7 +714,7 @@ local function __acquire_id_list(id_list, id_first, id_count) local acquired_primary = available_primary local freelist_id = freelist_ids[acquired_primary] - available_primary = freelist_id % 2 ^ 20 + available_primary = freelist_id % 1048576 local shifted_secondary = freelist_id - available_primary local acquired_id = acquired_primary + shifted_secondary @@ -729,7 +729,7 @@ local function __acquire_id_list(id_list, id_first, id_count) if id_count > 0 then local acquired_count = __acquired_count - if acquired_count + id_count > 2 ^ 20 - 1 then + if acquired_count + id_count > 1048576 - 1 then __debug_fns.error_fmt('id index overflow') end @@ -737,7 +737,7 @@ local function __acquire_id_list(id_list, id_first, id_count) local entity_places = __entity_places for acquired_primary = acquired_count + 1, acquired_count + id_count do - local acquired_id = acquired_primary + 2 ^ 20 + local acquired_id = acquired_primary + 1048576 freelist_ids[acquired_primary] = acquired_id entity_chunks[acquired_primary] = false @@ -757,13 +757,13 @@ local function __release_id_list(id_list, id_count) for id_index = 1, id_count do local id = id_list[id_index] - local acquired_primary = id % 2 ^ 20 + local acquired_primary = id % 1048576 local shifted_secondary = id - acquired_primary if __freelist_ids[acquired_primary] == id then - shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 - and 2 ^ 20 - or shifted_secondary + 2 ^ 20 + shifted_secondary = shifted_secondary == 1099511627776 - 1048576 + and 1048576 + or shifted_secondary + 1048576 __freelist_ids[acquired_primary] = __available_primary + shifted_secondary __available_primary = acquired_primary @@ -1545,7 +1545,7 @@ function __search_index_fns.combine_keys(index) for entity_primary, prev_entity in __lua_next, old_prev_entities do local extracted = entity_extracts[entity_primary] - local key_offset = (extracted % 2 ^ 20) * 2 ^ 20 + local key_offset = extracted % 1048576 * 1048576 local entity_key = key_offset + entity_primary new_prev_entities[entity_key] = prev_entity @@ -1622,10 +1622,10 @@ function __search_index_fns.insert_extracted(index, entity, extracted) __search_index_fns.insert_node(index, entity, target_list[target_index]) end - index.__entity_extracts[entity % 2 ^ 20] = target_list + index.__entity_extracts[entity % 1048576] = target_list else __search_index_fns.insert_node(index, entity, extracted) - index.__entity_extracts[entity % 2 ^ 20] = extracted + index.__entity_extracts[entity % 1048576] = extracted end end @@ -1635,8 +1635,8 @@ end function __search_index_fns.insert_node(index, entity, extracted) local target_fragment = index.__target_fragment - local key_offset = index.__combined_keys and (extracted % 2 ^ 20) * 2 ^ 20 or 0 - local entity_key = key_offset + entity % 2 ^ 20 + local key_offset = index.__combined_keys and extracted % 1048576 * 1048576 or 0 + local entity_key = key_offset + entity % 1048576 local head_entities = index.__head_entities local prev_entities = index.__prev_entities @@ -1651,17 +1651,17 @@ function __search_index_fns.insert_node(index, entity, extracted) return end - local head_entity_key = key_offset + head_entity % 2 ^ 20 + local head_entity_key = key_offset + head_entity % 1048576 local tail_entity = prev_entities[head_entity_key] - local tail_entity_key = key_offset + tail_entity % 2 ^ 20 + local tail_entity_key = key_offset + tail_entity % 1048576 prev_entities[entity_key] = tail_entity next_entities[entity_key] = head_entity prev_entities[head_entity_key] = entity next_entities[tail_entity_key] = entity - elseif not target_fragment or __freelist_ids[extracted % 2 ^ 20] == extracted then + elseif not target_fragment or __freelist_ids[extracted % 1048576] == extracted then head_entities[extracted] = entity prev_entities[entity_key] = entity @@ -1679,7 +1679,7 @@ end ---@param index evolved.search_index ---@param entity evolved.entity function __search_index_fns.remove(index, entity) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 ---@type evolved.component? local extracted = index.__entity_extracts[entity_primary] @@ -1709,8 +1709,8 @@ end function __search_index_fns.remove_node(index, entity, extracted) local target_fragment = index.__target_fragment - local key_offset = index.__combined_keys and (extracted % 2 ^ 20) * 2 ^ 20 or 0 - local entity_key = key_offset + entity % 2 ^ 20 + local key_offset = index.__combined_keys and extracted % 1048576 * 1048576 or 0 + local entity_key = key_offset + entity % 1048576 local head_entities = index.__head_entities local prev_entities = index.__prev_entities @@ -1741,8 +1741,8 @@ function __search_index_fns.remove_node(index, entity, extracted) else local prev_entity = prev_entities[entity_key] - local prev_entity_key = key_offset + prev_entity % 2 ^ 20 - local next_entity_key = key_offset + next_entity % 2 ^ 20 + local prev_entity_key = key_offset + prev_entity % 1048576 + local next_entity_key = key_offset + next_entity % 1048576 prev_entities[next_entity_key] = prev_entity next_entities[prev_entity_key] = next_entity @@ -1770,7 +1770,7 @@ function __search_index_fns.assign(index, entity, component) new_extracted = extract_component(new_extracted) end - local old_extracted = entity_extracts[entity % 2 ^ 20] + local old_extracted = entity_extracts[entity % 1048576] if new_extracted == old_extracted then -- nothing to reassign @@ -1831,7 +1831,7 @@ function __search_index_fns.multi_assign(index, entity_list, component_list, fir new_extracted = extract_component(new_extracted) end - local old_extracted = entity_extracts[entity % 2 ^ 20] + local old_extracted = entity_extracts[entity % 1048576] if new_extracted == old_extracted then -- nothing to reassign @@ -1874,7 +1874,7 @@ end ---@return evolved.chunk ---@nodiscard function __new_chunk(chunk_parent, chunk_fragment) - local chunk_fragment_primary = chunk_fragment % 2 ^ 20 + local chunk_fragment_primary = chunk_fragment % 1048576 if __freelist_ids[chunk_fragment_primary] ~= chunk_fragment then __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used for a new chunk', @@ -3258,7 +3258,7 @@ function __detach_entity(chunk, place) local last_entity = entity_list[entity_count] entity_list[place] = last_entity - local last_entity_primary = last_entity % 2 ^ 20 + local last_entity_primary = last_entity % 1048576 __entity_places[last_entity_primary] = place for component_index = 1, component_count do @@ -3324,7 +3324,7 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) chunk_entity_list[place] = entity - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 __entity_chunks[entity_primary] = chunk __entity_places[entity_primary] = place end @@ -3473,7 +3473,7 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co local entity = entity_list[place - b_place + entity_first] chunk_entity_list[place] = entity - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 entity_chunks[entity_primary] = chunk entity_places[entity_primary] = place end @@ -3644,7 +3644,7 @@ function __clone_entity(prefab, entity, component_table, component_mapper) chunk_entity_list[place] = entity - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 __entity_chunks[entity_primary] = chunk __entity_places[entity_primary] = place end @@ -3822,7 +3822,7 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c local entity = entity_list[place - b_place + entity_first] chunk_entity_list[place] = entity - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 entity_chunks[entity_primary] = chunk entity_places[entity_primary] = place end @@ -4167,7 +4167,7 @@ function __clear_entity_one(entity) local entity_chunks = __entity_chunks local entity_places = __entity_places - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to clear from non-alive entities @@ -4270,7 +4270,7 @@ function __destroy_entity_one(entity) local entity_chunks = __entity_chunks local entity_places = __entity_places - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- this entity is not alive, nothing to purge @@ -4577,7 +4577,7 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent for processed_entity_index = 1, processed_entity_count do local entity = processed_entity_list[processed_entity_index] - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 local chunk = entity_chunks[entity_primary] local place = entity_places[entity_primary] @@ -4623,7 +4623,7 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent for processed_entity_index = 1, processed_entity_count do local entity = processed_entity_list[processed_entity_index] - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 local chunk = entity_chunks[entity_primary] local place = entity_places[entity_primary] @@ -4652,7 +4652,7 @@ function __remove_entity_fragments(entity, ...) __debug_fns.error_fmt('this operation should be deferred') end - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to remove from non-alive entities @@ -4980,7 +4980,7 @@ function __chunk_insert(old_chunk, fragment, component) for new_place = new_entity_count + 1, sum_entity_count do local entity = new_entity_list[new_place] - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 entity_chunks[entity_primary] = new_chunk entity_places[entity_primary] = new_place end @@ -5271,7 +5271,7 @@ function __chunk_remove(old_chunk, ...) for new_place = new_entity_count + 1, sum_entity_count do local entity = new_entity_list[new_place] - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 entity_chunks[entity_primary] = new_chunk entity_places[entity_primary] = new_place end @@ -5284,7 +5284,7 @@ function __chunk_remove(old_chunk, ...) for old_place = 1, old_entity_count do local entity = old_entity_list[old_place] - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 entity_chunks[entity_primary] = false entity_places[entity_primary] = 0 end @@ -5358,7 +5358,7 @@ function __chunk_clear(chunk) for place = 1, chunk_entity_count do local entity = chunk_entity_list[place] - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 entity_chunks[entity_primary] = false entity_places[entity_primary] = 0 end @@ -5918,15 +5918,15 @@ end ---@return evolved.id id ---@nodiscard function __evolved_pack(primary, secondary) - if primary < 1 or primary > 2 ^ 20 - 1 then + if primary < 1 or primary > 1048576 - 1 then __debug_fns.error_fmt('the primary (%d) is out of range [1, 2 ^ 20 - 1]', primary) end - if secondary < 1 or secondary > 2 ^ 20 - 1 then + if secondary < 1 or secondary > 1048576 - 1 then __debug_fns.error_fmt('the secondary (%d) is out of range [1, 2 ^ 20 - 1]', secondary) end - return primary + secondary * 2 ^ 20 --[[@as evolved.id]] + return primary + secondary * 1048576 --[[@as evolved.id]] end ---@param id evolved.id @@ -5934,8 +5934,7 @@ end ---@return integer secondary ---@nodiscard function __evolved_unpack(id) - return id % 2 ^ 20, - (id - id % 2 ^ 20) / 2 ^ 20 % 2 ^ 20 + return id % 1048576, (id - id % 1048576) / 1048576 % 1048576 end ---@return boolean started @@ -6232,7 +6231,7 @@ end ---@return boolean ---@nodiscard function __evolved_alive(entity) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then return false @@ -6287,7 +6286,7 @@ end ---@return boolean ---@nodiscard function __evolved_empty(entity) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- non-alive entities are empty @@ -6351,7 +6350,7 @@ end ---@return boolean ---@nodiscard function __evolved_has(entity, fragment) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- non-alive entities have no fragments @@ -6379,7 +6378,7 @@ function __evolved_has_all(entity, ...) return true end - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- non-alive entities have no fragments @@ -6407,7 +6406,7 @@ function __evolved_has_any(entity, ...) return false end - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- non-alive entities have no fragments @@ -6429,7 +6428,7 @@ end ---@return evolved.component ... components ---@nodiscard function __evolved_get(entity, ...) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- non-alive entities have no fragments @@ -6451,7 +6450,7 @@ end ---@param fragment evolved.fragment ---@param component evolved.component function __evolved_set(entity, fragment, component) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then __debug_fns.error_fmt('the entity (%s) is not alive and cannot be changed', @@ -6459,7 +6458,7 @@ function __evolved_set(entity, fragment, component) end if __debug_mode then - local fragment_primary = fragment % 2 ^ 20 + local fragment_primary = fragment % 1048576 if __freelist_ids[fragment_primary] ~= fragment then __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be set', @@ -6720,7 +6719,7 @@ function __evolved_remove(entity, ...) return end - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to remove from non-alive entities @@ -6767,7 +6766,7 @@ function __evolved_clear(...) for argument_index = 1, argument_count do ---@type evolved.entity local entity = __lua_select(argument_index, ...) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to clear from non-alive entities @@ -6801,7 +6800,7 @@ function __evolved_clear_one(entity) __evolved_defer() do - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to clear from non-alive entities @@ -6842,7 +6841,7 @@ function __evolved_destroy(...) for argument_index = 1, argument_count do ---@type evolved.entity local entity = __lua_select(argument_index, ...) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to destroy from non-alive entities @@ -6878,7 +6877,7 @@ function __evolved_destroy_one(entity) __evolved_defer() do - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- nothing to destroy from non-alive entities @@ -6904,7 +6903,7 @@ end ---@param fragment evolved.fragment ---@param component evolved.component function __evolved_batch_set(query, fragment, component) - local query_primary = query % 2 ^ 20 + local query_primary = query % 1048576 if __freelist_ids[query_primary] ~= query then __debug_fns.error_fmt('the query (%s) is not alive and cannot be executed', @@ -6912,7 +6911,7 @@ function __evolved_batch_set(query, fragment, component) end if __debug_mode then - local fragment_primary = fragment % 2 ^ 20 + local fragment_primary = fragment % 1048576 if __freelist_ids[fragment_primary] ~= fragment then __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be set', @@ -6987,7 +6986,7 @@ function __evolved_batch_remove(query, ...) return end - local query_primary = query % 2 ^ 20 + local query_primary = query % 1048576 if __freelist_ids[query_primary] ~= query then __debug_fns.error_fmt('the query (%s) is not alive and cannot be executed', @@ -7051,7 +7050,7 @@ function __evolved_batch_clear(...) for argument_index = 1, argument_count do ---@type evolved.query local query = __lua_select(argument_index, ...) - local query_primary = query % 2 ^ 20 + local query_primary = query % 1048576 if __freelist_ids[query_primary] ~= query then __debug_fns.warning_fmt('the query (%s) is not alive and cannot be executed', @@ -7100,7 +7099,7 @@ function __evolved_batch_destroy(...) for argument_index = 1, argument_count do ---@type evolved.query local query = __lua_select(argument_index, ...) - local query_primary = query % 2 ^ 20 + local query_primary = query % 1048576 if __freelist_ids[query_primary] ~= query then __debug_fns.warning_fmt('the query (%s) is not alive and cannot be executed', @@ -7132,7 +7131,7 @@ end ---@return evolved.each_state? iterator_state ---@nodiscard function __evolved_each(entity) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then __debug_fns.error_fmt('the entity (%s) is not alive and cannot be iterated', @@ -7161,7 +7160,7 @@ end ---@return evolved.execute_state? iterator_state ---@nodiscard function __evolved_execute(query) - local query_primary = query % 2 ^ 20 + local query_primary = query % 1048576 if __freelist_ids[query_primary] ~= query then __debug_fns.error_fmt('the query (%s) is not alive and cannot be executed', @@ -7241,7 +7240,7 @@ end ---@return integer place ---@nodiscard function __evolved_locate(entity) - local entity_primary = entity % 2 ^ 20 + local entity_primary = entity % 1048576 if __freelist_ids[entity_primary] ~= entity then -- non-alive entities have no chunks @@ -7331,13 +7330,15 @@ function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, local head_entity = head_entities[component] if head_entity then - local key_offset = fragment_search_index.__combined_keys and (component % 2 ^ 20) * 2 ^ 20 or 0 + local key_offset = fragment_search_index.__combined_keys + and component % 1048576 * 1048576 + or 0 local entity = head_entity repeat out_entity_list[out_entity_first + entity_count] = entity - entity = next_entities[key_offset + entity % 2 ^ 20] + entity = next_entities[key_offset + entity % 1048576] entity_count = entity_count + 1 until entity == head_entity end @@ -7383,7 +7384,7 @@ function __evolved_process(...) for argument_index = 1, argument_count do ---@type evolved.system local system = __lua_select(argument_index, ...) - local system_primary = system % 2 ^ 20 + local system_primary = system % 1048576 if __freelist_ids[system_primary] ~= system then __debug_fns.warning_fmt('the system (%s) is not alive and cannot be processed', @@ -7397,7 +7398,7 @@ end ---@param system evolved.system ---@param ... any processing payload function __evolved_process_with(system, ...) - local system_primary = system % 2 ^ 20 + local system_primary = system % 1048576 if __freelist_ids[system_primary] ~= system then __debug_fns.error_fmt('the system (%s) is not alive and cannot be processed', @@ -8078,7 +8079,7 @@ end ---@param component evolved.component ---@return evolved.builder builder function __builder_mt:set(fragment, component) - local fragment_primary = fragment % 2 ^ 20 + local fragment_primary = fragment % 1048576 if __debug_mode then if __freelist_ids[fragment_primary] ~= fragment then From b958feba18404f56a1e6556275897b7dad9dd4fa Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 25 Oct 2026 05:56:36 +0700 Subject: [PATCH 32/48] more destruction tests --- develop/testing/destroy_tests.lua | 198 ++++++++++++++++++++++++++++++ evolved.lua | 6 +- 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/develop/testing/destroy_tests.lua b/develop/testing/destroy_tests.lua index 624db63..7af8106 100644 --- a/develop/testing/destroy_tests.lua +++ b/develop/testing/destroy_tests.lua @@ -365,3 +365,201 @@ do evo.destroy(e) end + +do + local f1, f2 = evo.id(2) + + local e = evo.spawn({ [f1] = true, [f2] = true }) + local c12 = evo.locate(e) + local c1 = evo.chunk(f1) + + evo.destroy(e) + assert(c1:alive() and c12:alive()) + + evo.destroy(f1) + assert(not evo.alive(f1) and evo.alive(f2)) + + evo.collect_garbage() + assert(not c1:alive() and not c12:alive()) + + evo.destroy(f2) + assert(not evo.alive(f2)) +end + +do + local p = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local f1 = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local f2, f3 = evo.id(2) + + evo.set(f1, p) + evo.set(f2, p) + evo.set(f3, p) + + local e1 = evo.spawn({ [f1] = true }) + local e3 = evo.spawn({ [f3] = true }) + + local c1 = evo.locate(e1) + local c3 = evo.locate(e3) + + evo.destroy(e1) + assert(c1:alive() and c3:alive()) + + evo.destroy(p) + assert(not evo.alive_any(p, f1, f2, f3)) + assert(evo.alive(e3) and evo.empty(e3)) + + evo.collect_garbage() + assert(not c1:alive() and not c3:alive()) + + evo.destroy(e3) +end + +do + local p = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local f1 = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local f2, f3 = evo.id(2) + + evo.set(f1, p) + evo.set(f2, p) + evo.set(f3, p) + + local e1 = evo.spawn({ [f1] = true, [f2] = true }) + local e2 = evo.spawn({ [f2] = true }) + local e3 = evo.spawn({ [f3] = true }) + + local c12 = evo.locate(e1) + local c2 = evo.locate(e2) + + evo.destroy(e2, e3) + assert(c12:alive() and c2:alive()) + + evo.destroy(p) + assert(not evo.alive_any(p, f1, f2, f3, e1, e2, e3)) + + evo.collect_garbage() + assert(not c12:alive() and not c2:alive()) +end + +do + local p = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local f1, f2, f3 = evo.id(3) + + evo.set(f1, p) + evo.set(f2, p) + evo.set(f3, p) + + local e2 = evo.spawn({ [f2] = true }) + evo.destroy(evo.spawn({ [f3] = true })) + + evo.destroy(f1, f3, p) + assert(not evo.alive_any(p, f1, f2, f3)) + assert(evo.alive(e2) and evo.empty(e2)) + + evo.destroy(e2) + evo.collect_garbage() +end + +do + local f1, f2 = evo.id(2) + + local e = evo.spawn({ [f1] = true, [f2] = true }) + + evo.destroy(f1) + assert(evo.alive(e) and not evo.has(e, f1) and evo.has(e, f2)) + assert(evo.locate(e) == evo.chunk(f2)) + + evo.destroy(f2) + assert(evo.alive(e) and evo.empty(e)) + + evo.destroy(e) + evo.collect_garbage() +end + +do + local f = evo.id() + + evo.set(f, f) + local c = evo.locate(f) + + evo.destroy(f) + assert(not evo.alive(f)) + + evo.collect_garbage() + assert(not c:alive()) +end + +do + local f = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local l = evo.builder():link():build() + + local e = evo.spawn({ [f] = true }) + local c = evo.locate(e) + + local r = evo.spawn({ [l] = f }) + + evo.destroy(e) + assert(c:alive()) + + evo.destroy(f) + assert(not evo.alive(f) and evo.alive(r)) + assert(not evo.has(r, l)) + + evo.collect_garbage() + assert(not c:alive()) + + evo.destroy(r, l) +end + +do + for _, e_first in ipairs({ true, false }) do + local f1 = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_DESTROY_ENTITY):build() + local f2 = evo.builder():destruction_policy(evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT):build() + + local e1 = evo.spawn({ [f1] = true }) + local e2 = evo.spawn({ [f2] = true }) + local e3 = evo.spawn({ [f1] = true, [f2] = true }) + + evo.destroy(e3) + + if e_first then + evo.destroy(e1, e2, f1, f2) + else + evo.destroy(f1, f2, e1, e2) + end + + assert(not evo.alive_any(f1, f2, e1, e2, e3)) + evo.collect_garbage() + end +end + +do + for _, policy in ipairs({ evo.DESTRUCTION_POLICY_DESTROY_ENTITY, evo.DESTRUCTION_POLICY_REMOVE_FRAGMENT }) do + local f = evo.builder():destruction_policy(policy):build() + local f1, f2, f3 = evo.id(3) + + local e1 = evo.spawn({ [f] = true, [f1] = true }) + local e2 = evo.spawn({ [f] = true, [f2] = true }) + local e3 = evo.spawn({ [f] = true, [f3] = true }) + + local c1, c2, c3 = evo.locate(e1), evo.locate(e2), evo.locate(e3) + + evo.destroy(e1, e2) + evo.destroy(f) + + if policy == evo.DESTRUCTION_POLICY_DESTROY_ENTITY then + assert(not evo.alive_any(f, e3)) + else + assert(not evo.alive(f) and evo.alive(e3)) + assert(not evo.has(e3, f) and evo.has(e3, f3)) + end + + evo.collect_garbage() + assert(not c1:alive() and not c2:alive() and not c3:alive()) + + local e4 = evo.spawn({ [f1] = true, [f2] = true, [f3] = true }) + assert(evo.has_all(e4, f1, f2, f3)) + + evo.destroy(e3, e4, f1, f2, f3) + evo.collect_garbage() + end +end diff --git a/evolved.lua b/evolved.lua index cbef7f1..9ba8c64 100644 --- a/evolved.lua +++ b/evolved.lua @@ -4414,10 +4414,14 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent __chunk_clear(chunk) else + local entity_stack_curr = entity_stack_size + for place = chunk_entity_count, 1, -1 do local entity = chunk_entity_list[place] - if not minor_chunks[entity] and not target_fragments[entity] then + if entity_stack_curr > entity_stack_base and entity == entity_stack[entity_stack_curr] then + entity_stack_curr = entity_stack_curr - 1 + else releasing_entity_count = releasing_entity_count + 1 releasing_entity_list[releasing_entity_count] = entity From 598cf7674dca8e9286e163285b4f94bcb30c5b24 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 25 Oct 2026 06:28:52 +0700 Subject: [PATCH 33/48] cache link policy --- develop/testing/link_tests.lua | 71 ++++++++++++++++++++++++++++++++++ evolved.lua | 7 +++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua index 7d38208..aa5f7c8 100644 --- a/develop/testing/link_tests.lua +++ b/develop/testing/link_tests.lua @@ -361,6 +361,77 @@ do assert(evo.alive(e) and evo.get(e, f) == t) end +do + local f = evo.builder():index():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t = evo.id() + local e = evo.builder():set(f, t):build() + + evo.set(f, evo.LINK) + + evo.destroy(t) + assert(not evo.alive(e)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + + evo.remove(f, evo.LINK) + evo.set(f, evo.LINK) + + evo.destroy(t1) + assert(not evo.alive(e1)) + + evo.remove(f, evo.LINK_POLICY) + evo.remove(f, evo.LINK) + evo.set(f, evo.LINK) + + evo.destroy(t2) + assert(evo.alive(e2) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + + evo.defer() + do + evo.set(f, evo.LINK_POLICY, evo.LINK_POLICY_DESTROY_ENTITY) + evo.destroy(t1) + evo.remove(f, evo.LINK_POLICY) + evo.destroy(t2) + end + evo.commit() + + assert(not evo.alive(e1)) + assert(evo.alive(e2) and not evo.has(e2, f)) +end + +do + local f = evo.builder():link():build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, t1):build() + local e2 = evo.builder():set(f, t2):build() + + evo.set(f, evo.LINK_POLICY, evo.LINK_POLICY_DESTROY_ENTITY) + + evo.destroy(t1) + assert(not evo.alive(e1)) + + evo.set(f, evo.LINK_POLICY, evo.LINK_POLICY_REMOVE_FRAGMENT) + + evo.destroy(t2) + assert(evo.alive(e2) and not evo.has(e2, f)) +end + do local f = evo.builder():link():build() diff --git a/evolved.lua b/evolved.lua index 9ba8c64..2e78430 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1507,6 +1507,7 @@ local __safe_tbls = { ---@field package __prev_entities table ---@field package __next_entities table ---@field package __entity_extracts table +---@field package __link_policy? evolved.id ---@field package __target_fragment? evolved.fragment ---@field package __extract_component? evolved.extract @@ -1523,6 +1524,7 @@ function __search_index_fns.new(fragment) __prev_entities = {}, __next_entities = {}, __entity_extracts = {}, + __link_policy = __evolved_get(fragment, __LINK_POLICY), __target_fragment = __evolved_has(fragment, __LINK) and fragment or nil, __extract_component = __evolved_get(fragment, __EXTRACT), } @@ -4502,7 +4504,7 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent and target_fragment_search_index.__target_fragment == target_fragment and target_fragment_search_index.__head_entities[entity] then - local link_policy = __evolved_get(target_fragment, __LINK_POLICY) + local link_policy = target_fragment_search_index.__link_policy or __LINK_POLICY_REMOVE_FRAGMENT if link_policy == __LINK_POLICY_DESTROY_ENTITY then @@ -8421,6 +8423,9 @@ __evolved_set(__INDEX, __ON_REMOVE, __update_search_index) __evolved_set(__LINK, __ON_INSERT, __update_search_index) __evolved_set(__LINK, __ON_REMOVE, __update_search_index) +__evolved_set(__LINK_POLICY, __ON_SET, __update_search_index) +__evolved_set(__LINK_POLICY, __ON_REMOVE, __update_search_index) + __evolved_set(__UNIQUE, __ON_INSERT, __update_major_chunks) __evolved_set(__UNIQUE, __ON_REMOVE, __update_major_chunks) From 573ca0045bcab2c7417b991db3764b0d3dc204cc Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 25 Oct 2026 07:38:58 +0700 Subject: [PATCH 34/48] bug report --- develop/all.lua | 1 + develop/issues/issue_57.lua | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 develop/issues/issue_57.lua diff --git a/develop/all.lua b/develop/all.lua index 3f08133..99e8d6c 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -2,6 +2,7 @@ require 'develop.issues.issue_52' require 'develop.issues.issue_54' require 'develop.issues.issue_55' require 'develop.issues.issue_56' +require 'develop.issues.issue_57' require 'develop.testing.build_tests' require 'develop.testing.cached_hooks_tests' diff --git a/develop/issues/issue_57.lua b/develop/issues/issue_57.lua new file mode 100644 index 0000000..d547f94 --- /dev/null +++ b/develop/issues/issue_57.lua @@ -0,0 +1,38 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +do + local f1, f2 = evo.id(2) + + evo.set(f1, evo.DEFAULT, 1) + evo.set(f2, evo.REQUIRES, { f1 }) + + -- the chunk with both fragments caches the default value of the first fragment + local e1 = evo.spawn({ [f2] = true }) + assert(evo.get(e1, f1) == 1) + + evo.set(f1, evo.DEFAULT, 2) + + -- the cached default value should be updated after reassignment + local e2 = evo.spawn({ [f2] = true }) + assert(evo.get(e2, f1) == 2) +end + +do + local f1, f2 = evo.id(2) + + evo.set(f1, evo.DEFAULT, 1) + evo.set(f1, evo.DUPLICATE, function(v) return v + 10 end) + evo.set(f2, evo.REQUIRES, { f1 }) + + -- the chunk with both fragments caches the duplicate function of the first fragment + local e1 = evo.spawn({ [f2] = true }) + assert(evo.get(e1, f1) == 11) + + evo.set(f1, evo.DUPLICATE, function(v) return v + 20 end) + + -- the cached duplicate function should be updated after reassignment + local e2 = evo.spawn({ [f2] = true }) + assert(evo.get(e2, f1) == 21) +end From 3a9db9f7993951df05a20bac281702347fcadcfb Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 25 Oct 2026 09:55:10 +0700 Subject: [PATCH 35/48] cache chunk hooks --- README.md | 2 + develop/testing/cached_hooks_tests.lua | 258 +++++++++++++ evolved.lua | 483 +++++++++++++++---------- 3 files changed, 554 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index 7bc2592..2a2392a 100644 --- a/README.md +++ b/README.md @@ -2055,9 +2055,11 @@ builder_mt:destruction_policy :: id -> builder - Query iteration order no longer depends on the order in which chunks were created - The [`NAME`](#evolvedname) fragment is now marked as [UNIQUE](#unique-fragments), so clones no longer inherit the names of their prefabs - The [`search`](#evolvedsearch) and [`reindex`](#evolvedreindex) functions now print a warning instead of throwing an error for non-indexed fragments +- Improved performance of modifying operations for fragments with [`ON_SET`](#evolvedon_set), [`ON_ASSIGN`](#evolvedon_assign), [`ON_INSERT`](#evolvedon_insert), and [`ON_REMOVE`](#evolvedon_remove) hooks - Fixed [`batch_set`](#evolvedbatch_set) invoking hooks twice: [#54](https://github.com/BlackMATov/evolved.lua/issues/54) - Fixed query caches keeping collected chunks: [#55](https://github.com/BlackMATov/evolved.lua/issues/55) - Fixed skipped remove hooks of fragments destroyed in a cascade: [#56](https://github.com/BlackMATov/evolved.lua/issues/56) +- Fixed stale [`DEFAULT`](#evolveddefault) and [`DUPLICATE`](#evolvedduplicate) values after their reassignment: [#57](https://github.com/BlackMATov/evolved.lua/issues/57) ### v1.12.0 diff --git a/develop/testing/cached_hooks_tests.lua b/develop/testing/cached_hooks_tests.lua index 3881111..594a889 100644 --- a/develop/testing/cached_hooks_tests.lua +++ b/develop/testing/cached_hooks_tests.lua @@ -201,3 +201,261 @@ do assert(remove_hook_calls == 2) end end + +do + local f1, f2 = evo.id(2) + + local remove_hook_calls = 0 + + evo.set(f1, evo.ON_REMOVE, function() + remove_hook_calls = remove_hook_calls + 1 + end) + + evo.set(f2, evo.ON_REMOVE, function() + remove_hook_calls = remove_hook_calls + 10 + end) + + do + remove_hook_calls = 0 + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.destroy(e) + assert(remove_hook_calls == 11) + end + + evo.set(f1, evo.ON_REMOVE, function() + remove_hook_calls = remove_hook_calls + 2 + end) + + evo.set(f2, evo.ON_REMOVE, function() + remove_hook_calls = remove_hook_calls + 20 + end) + + do + remove_hook_calls = 0 + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.destroy(e) + assert(remove_hook_calls == 22) + end + + do + remove_hook_calls = 0 + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.remove(e, f1) + assert(remove_hook_calls == 2) + evo.clear(e) + assert(remove_hook_calls == 22) + evo.destroy(e) + end + + do + local q = evo.builder():include(f1, f2):build() + + remove_hook_calls = 0 + local e1 = evo.spawn { [f1] = 42, [f2] = 'hello' } + local e2 = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.batch_remove(q, f2) + assert(remove_hook_calls == 40) + evo.destroy(e1, e2) + assert(remove_hook_calls == 44) + + remove_hook_calls = 0 + local e3 = evo.spawn { [f1] = 42, [f2] = 'hello' } + local e4 = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.batch_clear(q) + assert(remove_hook_calls == 44) + evo.destroy(e3, e4) + + remove_hook_calls = 0 + local e5 = evo.spawn { [f1] = 42, [f2] = 'hello' } + local e6 = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.batch_destroy(q) + assert(remove_hook_calls == 44) + assert(not evo.alive(e5) and not evo.alive(e6)) + + evo.destroy(q) + end + + evo.set(f1, evo.ON_REMOVE, false) + + do + remove_hook_calls = 0 + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.destroy(e) + assert(remove_hook_calls == 20) + end + + evo.set(f2, evo.ON_REMOVE, false) + + do + remove_hook_calls = 0 + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.destroy(e) + assert(remove_hook_calls == 0) + end +end + +do + local f1, f2 = evo.id(2) + + local calls = '' + + local function hooks(tag) + evo.set(f1, evo.ON_SET, function() calls = calls .. tag .. 's' end) + evo.set(f1, evo.ON_ASSIGN, function() calls = calls .. tag .. 'a' end) + evo.set(f1, evo.ON_INSERT, function() calls = calls .. tag .. 'i' end) + end + + hooks '1' + + do + calls = '' + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.set(e, f1, 43) + evo.destroy(e) + assert(calls == '1s1i1s1a') + end + + hooks '2' + + do + calls = '' + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + assert(calls == '2s2i') + evo.set(e, f1, 43) + assert(calls == '2s2i2s2a') + evo.destroy(e) + end + + do + calls = '' + local e = evo.spawn { [f2] = 'hello' } + evo.set(e, f1, 42) + assert(calls == '2s2i') + evo.destroy(e) + end + + do + calls = '' + local es = evo.multi_spawn(2, { [f1] = 42, [f2] = 'hello' }) + assert(calls == '2s2i2s2i') + evo.destroy(es[1], es[2]) + end + + do + local p = evo.spawn { [f1] = 42, [f2] = 'hello' } + + calls = '' + local e1 = evo.clone(p) + assert(calls == '2s2i') + + calls = '' + local es = evo.multi_clone(2, p) + assert(calls == '2s2i2s2i') + + evo.destroy(p, e1, es[1], es[2]) + end + + do + local q1 = evo.builder():include(f1, f2):build() + local q2 = evo.builder():include(f2):exclude(f1):build() + + local e1 = evo.spawn { [f1] = 42, [f2] = 'hello' } + + calls = '' + evo.batch_set(q1, f1, 43) + assert(calls == '2s2a') + + local e2 = evo.spawn { [f2] = 'hello' } + + calls = '' + evo.batch_set(q2, f1, 42) + assert(calls == '2s2i') + + evo.destroy(e1, e2, q1, q2) + end + + evo.set(f1, evo.ON_SET, false) + evo.set(f1, evo.ON_ASSIGN, false) + + do + calls = '' + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.set(e, f1, 43) + evo.destroy(e) + assert(calls == '2i') + end + + evo.set(f1, evo.ON_INSERT, false) + + do + calls = '' + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.set(e, f1, 43) + evo.destroy(e) + assert(calls == '') + end +end + +do + local f1, f2, f3 = evo.id(3) + + evo.set(f3, evo.REQUIRES, { f1 }) + + evo.set(f1, evo.DEFAULT, 1) + + do + local e = evo.spawn { [f2] = 'hello', [f3] = true } + assert(evo.get(e, f1) == 1) + evo.destroy(e) + end + + evo.set(f1, evo.DEFAULT, 2) + + do + local e = evo.spawn { [f2] = 'hello', [f3] = true } + assert(evo.get(e, f1) == 2) + evo.destroy(e) + end + + do + local es = evo.multi_spawn(2, { [f2] = 'hello', [f3] = true }) + assert(evo.get(es[1], f1) == 2 and evo.get(es[2], f1) == 2) + evo.destroy(es[1], es[2]) + end + + do + local e = evo.spawn { [f1] = 42, [f2] = 'hello' } + evo.set(e, f1) + assert(evo.get(e, f1) == 2) + evo.destroy(e) + end + + do + local q = evo.builder():include(f2):build() + local e = evo.spawn { [f2] = 'hello' } + evo.batch_set(q, f1) + assert(evo.get(e, f1) == 2) + evo.destroy(e, q) + end + + evo.set(f1, evo.DEFAULT, { 3 }) + evo.set(f1, evo.DUPLICATE, function(v) return { v[1] } end) + + do + local e1 = evo.spawn { [f2] = 'hello', [f3] = true } + local e2 = evo.spawn { [f2] = 'hello', [f3] = true } + assert(evo.get(e1, f1)[1] == 3 and evo.get(e2, f1)[1] == 3) + assert(evo.get(e1, f1) ~= evo.get(e2, f1)) + evo.destroy(e1, e2) + end + + evo.set(f1, evo.DUPLICATE, function(v) return { v[1] + 1 } end) + + do + local e = evo.spawn { [f2] = 'hello', [f3] = true } + assert(evo.get(e, f1)[1] == 4) + evo.set(e, f1) + assert(evo.get(e, f1)[1] == 4) + evo.destroy(e) + end +end diff --git a/evolved.lua b/evolved.lua index 2e78430..a8971ec 100644 --- a/evolved.lua +++ b/evolved.lua @@ -137,6 +137,8 @@ local evolved = { ---@field package __component_indices table ---@field package __component_storages evolved.storage[] ---@field package __component_fragments evolved.fragment[] +---@field package __component_sets evolved.set_hook[] +---@field package __component_assigns evolved.assign_hook[] ---@field package __component_defaults evolved.default[] ---@field package __component_duplicates evolved.duplicate[] ---@field package __component_reallocs evolved.realloc[] @@ -164,8 +166,11 @@ local evolved = { ---@field package __has_internal_fragments boolean ---@field package __has_required_fragments boolean ---@field package __insert_fragment_list? evolved.fragment[] +---@field package __insert_fragment_sets? evolved.set_hook[] +---@field package __insert_fragment_inserts? evolved.insert_hook[] ---@field package __insert_fragment_count integer ---@field package __remove_fragment_list? evolved.fragment[] +---@field package __remove_fragment_removes? evolved.remove_hook[] ---@field package __remove_fragment_count integer ---@field package __index_fragment_list? evolved.fragment[] ---@field package __index_fragment_count integer @@ -1919,6 +1924,8 @@ function __new_chunk(chunk_parent, chunk_fragment) __component_indices = {}, __component_storages = {}, __component_fragments = {}, + __component_sets = {}, + __component_assigns = {}, __component_defaults = {}, __component_duplicates = {}, __component_reallocs = {}, @@ -1946,8 +1953,11 @@ function __new_chunk(chunk_parent, chunk_fragment) __has_internal_fragments = false, __has_required_fragments = false, __insert_fragment_list = nil, + __insert_fragment_sets = nil, + __insert_fragment_inserts = nil, __insert_fragment_count = 0, __remove_fragment_list = nil, + __remove_fragment_removes = nil, __remove_fragment_count = 0, __index_fragment_list = nil, __index_fragment_count = 0, @@ -2117,16 +2127,20 @@ function __update_chunk_caches(chunk) local chunk_fragment_list = chunk.__fragment_list local chunk_fragment_count = chunk.__fragment_count + ---@type evolved.set_hook?, evolved.assign_hook?, evolved.insert_hook?, evolved.remove_hook? + local major_set, major_assign, major_insert, major_remove = + __evolved_get(chunk_fragment, __ON_SET, __ON_ASSIGN, __ON_INSERT, __ON_REMOVE) + local has_setup_major = __evolved_has_any(chunk_fragment, __DEFAULT, __DUPLICATE) local has_setup_minors = chunk_parent ~= nil and chunk_parent.__has_setup_hooks - local has_assign_major = __evolved_has_any(chunk_fragment, __ON_SET, __ON_ASSIGN) + local has_assign_major = (major_set or major_assign) and true or false local has_assign_minors = chunk_parent ~= nil and chunk_parent.__has_assign_hooks - local has_insert_major = __evolved_has_any(chunk_fragment, __ON_SET, __ON_INSERT) + local has_insert_major = (major_set or major_insert) and true or false local has_insert_minors = chunk_parent ~= nil and chunk_parent.__has_insert_hooks - local has_remove_major = __evolved_has(chunk_fragment, __ON_REMOVE) + local has_remove_major = major_remove and true or false local has_remove_minors = chunk_parent ~= nil and chunk_parent.__has_remove_hooks local has_setup_hooks = has_setup_major or has_setup_minors @@ -2210,47 +2224,74 @@ function __update_chunk_caches(chunk) if has_insert_hooks then local parent_insert_fragment_list = chunk_parent and chunk_parent.__insert_fragment_list + local parent_insert_fragment_sets = chunk_parent and chunk_parent.__insert_fragment_sets + local parent_insert_fragment_inserts = chunk_parent and chunk_parent.__insert_fragment_inserts local parent_insert_fragment_count = chunk_parent and chunk_parent.__insert_fragment_count or 0 if has_insert_major then - local insert_fragment_list, insert_fragment_count = parent_insert_fragment_list + local insert_fragment_list = parent_insert_fragment_list and __list_fns.dup(parent_insert_fragment_list, parent_insert_fragment_count) - or __list_fns.new(1), 0 + or __list_fns.new(1) + + local insert_fragment_sets = parent_insert_fragment_sets + and __list_fns.dup(parent_insert_fragment_sets, parent_insert_fragment_count) + or __list_fns.new(1) - insert_fragment_count = parent_insert_fragment_count + 1 + local insert_fragment_inserts = parent_insert_fragment_inserts + and __list_fns.dup(parent_insert_fragment_inserts, parent_insert_fragment_count) + or __list_fns.new(1) + + local insert_fragment_count = parent_insert_fragment_count + 1 insert_fragment_list[insert_fragment_count] = chunk_fragment + insert_fragment_sets[insert_fragment_count] = major_set + insert_fragment_inserts[insert_fragment_count] = major_insert chunk.__insert_fragment_list = insert_fragment_list + chunk.__insert_fragment_sets = insert_fragment_sets + chunk.__insert_fragment_inserts = insert_fragment_inserts chunk.__insert_fragment_count = insert_fragment_count else chunk.__insert_fragment_list = parent_insert_fragment_list + chunk.__insert_fragment_sets = parent_insert_fragment_sets + chunk.__insert_fragment_inserts = parent_insert_fragment_inserts chunk.__insert_fragment_count = parent_insert_fragment_count end else chunk.__insert_fragment_list = nil + chunk.__insert_fragment_sets = nil + chunk.__insert_fragment_inserts = nil chunk.__insert_fragment_count = 0 end if has_remove_hooks then local parent_remove_fragment_list = chunk_parent and chunk_parent.__remove_fragment_list + local parent_remove_fragment_removes = chunk_parent and chunk_parent.__remove_fragment_removes local parent_remove_fragment_count = chunk_parent and chunk_parent.__remove_fragment_count or 0 if has_remove_major then - local remove_fragment_list, remove_fragment_count = parent_remove_fragment_list + local remove_fragment_list = parent_remove_fragment_list and __list_fns.dup(parent_remove_fragment_list, parent_remove_fragment_count) - or __list_fns.new(1), 0 + or __list_fns.new(1) - remove_fragment_count = parent_remove_fragment_count + 1 + local remove_fragment_removes = parent_remove_fragment_removes + and __list_fns.dup(parent_remove_fragment_removes, parent_remove_fragment_count) + or __list_fns.new(1) + + local remove_fragment_count = parent_remove_fragment_count + 1 remove_fragment_list[remove_fragment_count] = chunk_fragment + remove_fragment_removes[remove_fragment_count] = major_remove chunk.__remove_fragment_list = remove_fragment_list + chunk.__remove_fragment_removes = remove_fragment_removes chunk.__remove_fragment_count = remove_fragment_count else chunk.__remove_fragment_list = parent_remove_fragment_list + chunk.__remove_fragment_removes = parent_remove_fragment_removes chunk.__remove_fragment_count = parent_remove_fragment_count end else chunk.__remove_fragment_list = nil + chunk.__remove_fragment_removes = nil chunk.__remove_fragment_count = 0 end @@ -2333,6 +2374,8 @@ function __update_chunk_storages(chunk) local component_indices = chunk.__component_indices local component_storages = chunk.__component_storages local component_fragments = chunk.__component_fragments + local component_sets = chunk.__component_sets + local component_assigns = chunk.__component_assigns local component_defaults = chunk.__component_defaults local component_duplicates = chunk.__component_duplicates local component_reallocs = chunk.__component_reallocs @@ -2344,6 +2387,10 @@ function __update_chunk_storages(chunk) local component_index = component_indices[fragment] local component_realloc = component_index and component_reallocs[component_index] + ---@type evolved.set_hook?, evolved.assign_hook? + local assign_fragment_set, assign_fragment_assign = + __evolved_get(fragment, __ON_SET, __ON_ASSIGN) + ---@type evolved.default?, evolved.duplicate?, evolved.realloc?, evolved.compmove? local fragment_default, fragment_duplicate, fragment_realloc, fragment_compmove = __evolved_get(fragment, __DEFAULT, __DUPLICATE, __REALLOC, __COMPMOVE) @@ -2366,6 +2413,8 @@ function __update_chunk_storages(chunk) if component_index ~= component_count then local last_component_storage = component_storages[component_count] local last_component_fragment = component_fragments[component_count] + local last_component_set = component_sets[component_count] + local last_component_assign = component_assigns[component_count] local last_component_default = component_defaults[component_count] local last_component_duplicate = component_duplicates[component_count] local last_component_realloc = component_reallocs[component_count] @@ -2374,6 +2423,8 @@ function __update_chunk_storages(chunk) component_indices[last_component_fragment] = component_index component_storages[component_index] = last_component_storage component_fragments[component_index] = last_component_fragment + component_sets[component_index] = last_component_set + component_assigns[component_index] = last_component_assign component_defaults[component_index] = last_component_default component_duplicates[component_index] = last_component_duplicate component_reallocs[component_index] = last_component_realloc @@ -2383,6 +2434,8 @@ function __update_chunk_storages(chunk) component_indices[fragment] = nil component_storages[component_count] = nil component_fragments[component_count] = nil + component_sets[component_count] = nil + component_assigns[component_count] = nil component_defaults[component_count] = nil component_duplicates[component_count] = nil component_reallocs[component_count] = nil @@ -2399,6 +2452,8 @@ function __update_chunk_storages(chunk) component_indices[fragment] = component_index component_storages[component_index] = nil component_fragments[component_index] = fragment + component_sets[component_index] = assign_fragment_set + component_assigns[component_index] = assign_fragment_assign component_defaults[component_index] = fragment_default component_duplicates[component_index] = fragment_duplicate component_reallocs[component_index] = fragment_realloc @@ -2479,6 +2534,8 @@ function __update_chunk_storages(chunk) component_defaults[component_index] = fragment_default component_duplicates[component_index] = fragment_duplicate + component_sets[component_index] = assign_fragment_set + component_assigns[component_index] = assign_fragment_assign component_reallocs[component_index] = fragment_realloc component_compmoves[component_index] = fragment_compmove end @@ -3382,14 +3439,21 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then + local chunk_insert_fragment_sets = chunk.__insert_fragment_sets + local chunk_insert_fragment_inserts = chunk.__insert_fragment_inserts + + ---@cast chunk_insert_fragment_sets -? + ---@cast chunk_insert_fragment_inserts -? + for chunk_insert_fragment_index = 1, chunk.__insert_fragment_count do local fragment = chunk_insert_fragment_list[chunk_insert_fragment_index] ---@type evolved.set_hook?, evolved.insert_hook? - local fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __ON_SET, __ON_INSERT) + local insert_fragment_set, insert_fragment_insert = + chunk_insert_fragment_sets[chunk_insert_fragment_index], + chunk_insert_fragment_inserts[chunk_insert_fragment_index] - if fragment_on_set or fragment_on_insert then + do local component_index = chunk_component_indices[fragment] if component_index then @@ -3397,20 +3461,20 @@ function __spawn_entity(chunk, entity, component_table, component_mapper) local new_component = component_storage[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component) + if insert_fragment_set then + insert_fragment_set(entity, fragment, new_component) end - if fragment_on_insert then - fragment_on_insert(entity, fragment, new_component) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment, new_component) end else - if fragment_on_set then - fragment_on_set(entity, fragment) + if insert_fragment_set then + insert_fragment_set(entity, fragment) end - if fragment_on_insert then - fragment_on_insert(entity, fragment) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment) end end end @@ -3536,14 +3600,21 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then + local chunk_insert_fragment_sets = chunk.__insert_fragment_sets + local chunk_insert_fragment_inserts = chunk.__insert_fragment_inserts + + ---@cast chunk_insert_fragment_sets -? + ---@cast chunk_insert_fragment_inserts -? + for chunk_insert_fragment_index = 1, chunk.__insert_fragment_count do local fragment = chunk_insert_fragment_list[chunk_insert_fragment_index] ---@type evolved.set_hook?, evolved.insert_hook? - local fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __ON_SET, __ON_INSERT) + local insert_fragment_set, insert_fragment_insert = + chunk_insert_fragment_sets[chunk_insert_fragment_index], + chunk_insert_fragment_inserts[chunk_insert_fragment_index] - if fragment_on_set or fragment_on_insert then + do local component_index = chunk_component_indices[fragment] if component_index then @@ -3554,24 +3625,24 @@ function __multi_spawn_entity(chunk, entity_list, entity_first, entity_count, co local new_component = component_storage[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component) + if insert_fragment_set then + insert_fragment_set(entity, fragment, new_component) end - if fragment_on_insert then - fragment_on_insert(entity, fragment, new_component) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment, new_component) end end else for place = b_place, e_place do local entity = chunk_entity_list[place] - if fragment_on_set then - fragment_on_set(entity, fragment) + if insert_fragment_set then + insert_fragment_set(entity, fragment) end - if fragment_on_insert then - fragment_on_insert(entity, fragment) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment) end end end @@ -3712,14 +3783,21 @@ function __clone_entity(prefab, entity, component_table, component_mapper) local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then + local chunk_insert_fragment_sets = chunk.__insert_fragment_sets + local chunk_insert_fragment_inserts = chunk.__insert_fragment_inserts + + ---@cast chunk_insert_fragment_sets -? + ---@cast chunk_insert_fragment_inserts -? + for chunk_insert_fragment_index = 1, chunk.__insert_fragment_count do local fragment = chunk_insert_fragment_list[chunk_insert_fragment_index] ---@type evolved.set_hook?, evolved.insert_hook? - local fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __ON_SET, __ON_INSERT) + local insert_fragment_set, insert_fragment_insert = + chunk_insert_fragment_sets[chunk_insert_fragment_index], + chunk_insert_fragment_inserts[chunk_insert_fragment_index] - if fragment_on_set or fragment_on_insert then + do local component_index = chunk_component_indices[fragment] if component_index then @@ -3727,20 +3805,20 @@ function __clone_entity(prefab, entity, component_table, component_mapper) local new_component = component_storage[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component) + if insert_fragment_set then + insert_fragment_set(entity, fragment, new_component) end - if fragment_on_insert then - fragment_on_insert(entity, fragment, new_component) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment, new_component) end else - if fragment_on_set then - fragment_on_set(entity, fragment) + if insert_fragment_set then + insert_fragment_set(entity, fragment) end - if fragment_on_insert then - fragment_on_insert(entity, fragment) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment) end end end @@ -3895,14 +3973,21 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c local chunk_insert_fragment_list = chunk.__insert_fragment_list if chunk_insert_fragment_list then + local chunk_insert_fragment_sets = chunk.__insert_fragment_sets + local chunk_insert_fragment_inserts = chunk.__insert_fragment_inserts + + ---@cast chunk_insert_fragment_sets -? + ---@cast chunk_insert_fragment_inserts -? + for chunk_insert_fragment_index = 1, chunk.__insert_fragment_count do local fragment = chunk_insert_fragment_list[chunk_insert_fragment_index] ---@type evolved.set_hook?, evolved.insert_hook? - local fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __ON_SET, __ON_INSERT) + local insert_fragment_set, insert_fragment_insert = + chunk_insert_fragment_sets[chunk_insert_fragment_index], + chunk_insert_fragment_inserts[chunk_insert_fragment_index] - if fragment_on_set or fragment_on_insert then + do local component_index = chunk_component_indices[fragment] if component_index then @@ -3913,24 +3998,24 @@ function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, c local new_component = component_storage[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component) + if insert_fragment_set then + insert_fragment_set(entity, fragment, new_component) end - if fragment_on_insert then - fragment_on_insert(entity, fragment, new_component) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment, new_component) end end else for place = b_place, e_place do local entity = chunk_entity_list[place] - if fragment_on_set then - fragment_on_set(entity, fragment) + if insert_fragment_set then + insert_fragment_set(entity, fragment) end - if fragment_on_insert then - fragment_on_insert(entity, fragment) + if insert_fragment_insert then + insert_fragment_insert(entity, fragment) end end end @@ -4184,21 +4269,23 @@ function __clear_entity_one(entity) local chunk_component_indices = chunk.__component_indices local chunk_component_storages = chunk.__component_storages + local chunk_remove_fragment_removes = chunk.__remove_fragment_removes + ---@cast chunk_remove_fragment_removes -? + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local remove_fragment_remove = chunk_remove_fragment_removes[chunk_remove_fragment_index] - if fragment_on_remove then + do local component_index = chunk_component_indices[fragment] if component_index then local component_storage = chunk_component_storages[component_index] local old_component = component_storage[place] - fragment_on_remove(entity, fragment, old_component) + remove_fragment_remove(entity, fragment, old_component) else - fragment_on_remove(entity, fragment) + remove_fragment_remove(entity, fragment) end end end @@ -4287,21 +4374,23 @@ function __destroy_entity_one(entity) local chunk_component_indices = chunk.__component_indices local chunk_component_storages = chunk.__component_storages + local chunk_remove_fragment_removes = chunk.__remove_fragment_removes + ---@cast chunk_remove_fragment_removes -? + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local remove_fragment_remove = chunk_remove_fragment_removes[chunk_remove_fragment_index] - if fragment_on_remove then + do local component_index = chunk_component_indices[fragment] if component_index then local component_storage = chunk_component_storages[component_index] local old_component = component_storage[place] - fragment_on_remove(entity, fragment, old_component) + remove_fragment_remove(entity, fragment, old_component) else - fragment_on_remove(entity, fragment) + remove_fragment_remove(entity, fragment) end end end @@ -4595,21 +4684,23 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent local chunk_component_indices = chunk.__component_indices local chunk_component_storages = chunk.__component_storages + local chunk_remove_fragment_removes = chunk.__remove_fragment_removes + ---@cast chunk_remove_fragment_removes -? + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local remove_fragment_remove = chunk_remove_fragment_removes[chunk_remove_fragment_index] - if fragment_on_remove then + do local component_index = chunk_component_indices[fragment] if component_index then local component_storage = chunk_component_storages[component_index] local old_component = component_storage[place] - fragment_on_remove(entity, fragment, old_component) + remove_fragment_remove(entity, fragment, old_component) else - fragment_on_remove(entity, fragment) + remove_fragment_remove(entity, fragment) end end end @@ -4683,22 +4774,24 @@ function __remove_entity_fragments(entity, ...) local new_fragment_set = new_chunk and new_chunk.__fragment_set or __safe_tbls.__EMPTY_FRAGMENT_SET + local old_remove_fragment_removes = old_chunk.__remove_fragment_removes + ---@cast old_remove_fragment_removes -? + for old_remove_fragment_index = 1, old_chunk.__remove_fragment_count do local fragment = old_remove_fragment_list[old_remove_fragment_index] if not new_fragment_set[fragment] then - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local remove_fragment_remove = old_remove_fragment_removes[old_remove_fragment_index] - if fragment_on_remove then + do local old_component_index = old_component_indices[fragment] if old_component_index then local old_component_storage = old_component_storages[old_component_index] local old_component = old_component_storage[old_place] - fragment_on_remove(entity, fragment, old_component) + remove_fragment_remove(entity, fragment, old_component) else - fragment_on_remove(entity, fragment) + remove_fragment_remove(entity, fragment) end end end @@ -4772,6 +4865,9 @@ function __chunk_assign(chunk, fragment, component) local chunk_component_indices = chunk.__component_indices local chunk_component_storages = chunk.__component_storages + + local chunk_component_defaults = chunk.__component_defaults + local chunk_component_duplicates = chunk.__component_duplicates local chunk_component_reallocs = chunk.__component_reallocs local chunk_component_compmoves = chunk.__component_compmoves @@ -4785,25 +4881,21 @@ function __chunk_assign(chunk, fragment, component) return end - local chunk_has_setup_hooks = chunk.__has_setup_hooks - local chunk_has_assign_hooks = chunk.__has_assign_hooks - local chunk_has_index_fragments = 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 - - if chunk_has_setup_hooks or chunk_has_assign_hooks then - fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign = - __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) - end - local component_storage = chunk_component_storages[component_index] local component_realloc = chunk_component_reallocs[component_index] local component_compmove = chunk_component_compmoves[component_index] + ---@type evolved.set_hook?, evolved.assign_hook? + local assign_fragment_set, assign_fragment_assign + + if chunk.__has_assign_hooks then + assign_fragment_set = chunk.__component_sets[component_index] + assign_fragment_assign = chunk.__component_assigns[component_index] + end + local old_component_storage_for_hooks ---@type evolved.storage? - if fragment_on_set or fragment_on_assign then + if assign_fragment_set or assign_fragment_assign then if component_realloc then old_component_storage_for_hooks = component_realloc(nil, 0, chunk_entity_count) else @@ -4828,6 +4920,9 @@ function __chunk_assign(chunk, fragment, component) end end + local fragment_default = chunk_component_defaults[component_index] + local fragment_duplicate = chunk_component_duplicates[component_index] + if fragment_duplicate then for place = 1, chunk_entity_count do local new_component = component @@ -4845,7 +4940,7 @@ function __chunk_assign(chunk, fragment, component) end end - local fragment_search_index = chunk_has_index_fragments + local fragment_search_index = chunk.__has_index_fragments and __search_indices[fragment] or nil @@ -4861,12 +4956,12 @@ function __chunk_assign(chunk, fragment, component) local new_component = component_storage[place] local old_component = old_component_storage_for_hooks[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component, old_component) + if assign_fragment_set then + assign_fragment_set(entity, fragment, new_component, old_component) end - if fragment_on_assign then - fragment_on_assign(entity, fragment, new_component, old_component) + if assign_fragment_assign then + assign_fragment_assign(entity, fragment, new_component, old_component) end end @@ -4915,20 +5010,11 @@ function __chunk_insert(old_chunk, fragment, component) local new_component_indices = new_chunk.__component_indices local new_component_storages = new_chunk.__component_storages + local new_component_defaults = new_chunk.__component_defaults + local new_component_duplicates = new_chunk.__component_duplicates local new_component_reallocs = new_chunk.__component_reallocs local new_component_compmoves = new_chunk.__component_compmoves - local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks - local new_chunk_has_index_fragments = new_chunk.__has_index_fragments - - ---@type evolved.default?, evolved.duplicate? - local fragment_default, fragment_duplicate - - if new_chunk_has_setup_hooks then - fragment_default, fragment_duplicate = - __evolved_get(fragment, __DEFAULT, __DUPLICATE) - end - local sum_entity_count = old_entity_count + new_chunk.__entity_count if sum_entity_count > new_chunk.__entity_capacity then @@ -4998,6 +5084,9 @@ function __chunk_insert(old_chunk, fragment, component) local new_component_index = new_component_indices[fragment] if new_component_index then + local fragment_default = new_component_defaults[new_component_index] + local fragment_duplicate = new_component_duplicates[new_component_index] + local new_component_storage = new_component_storages[new_component_index] if fragment_duplicate then @@ -5017,7 +5106,7 @@ function __chunk_insert(old_chunk, fragment, component) end end - local fragment_search_index = new_chunk_has_index_fragments + local fragment_search_index = new_chunk.__has_index_fragments and __search_indices[fragment] or nil @@ -5038,17 +5127,12 @@ function __chunk_insert(old_chunk, fragment, component) if ini_fragment_set[req_fragment] then -- this fragment has already been initialized else - ---@type evolved.default?, evolved.duplicate? - local req_fragment_default, req_fragment_duplicate - - if new_chunk_has_setup_hooks then - req_fragment_default, req_fragment_duplicate = - __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) - end - local req_component_index = new_component_indices[req_fragment] if req_component_index then + local req_fragment_default = new_component_defaults[req_component_index] + local req_fragment_duplicate = new_component_duplicates[req_component_index] + local req_component_storage = new_component_storages[req_component_index] if req_fragment_duplicate then @@ -5066,7 +5150,7 @@ function __chunk_insert(old_chunk, fragment, component) end end - local req_fragment_search_index = new_chunk_has_index_fragments + local req_fragment_search_index = new_chunk.__has_index_fragments and __search_indices[req_fragment] or nil @@ -5084,15 +5168,20 @@ function __chunk_insert(old_chunk, fragment, component) if new_insert_fragment_list then local old_fragment_set = old_chunk.__fragment_set + local new_insert_fragment_sets = new_chunk.__insert_fragment_sets + local new_insert_fragment_inserts = new_chunk.__insert_fragment_inserts + + ---@cast new_insert_fragment_sets -? + ---@cast new_insert_fragment_inserts -? + for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do local new_fragment = new_insert_fragment_list[new_insert_fragment_index] if not old_fragment_set[new_fragment] then - ---@type evolved.set_hook?, evolved.insert_hook? - local new_fragment_on_set, new_fragment_on_insert = - __evolved_get(new_fragment, __ON_SET, __ON_INSERT) + local new_insert_fragment_set = new_insert_fragment_sets[new_insert_fragment_index] + local new_insert_fragment_insert = new_insert_fragment_inserts[new_insert_fragment_index] - if new_fragment_on_set or new_fragment_on_insert then + do local new_component_index = new_component_indices[new_fragment] if new_component_index then @@ -5103,24 +5192,24 @@ function __chunk_insert(old_chunk, fragment, component) local new_component = new_component_storage[new_place] - if new_fragment_on_set then - new_fragment_on_set(entity, new_fragment, new_component) + if new_insert_fragment_set then + new_insert_fragment_set(entity, new_fragment, new_component) end - if new_fragment_on_insert then - new_fragment_on_insert(entity, new_fragment, new_component) + if new_insert_fragment_insert then + new_insert_fragment_insert(entity, new_fragment, new_component) end end else for new_place = new_entity_count + 1, sum_entity_count do local entity = new_entity_list[new_place] - if new_fragment_on_set then - new_fragment_on_set(entity, new_fragment) + if new_insert_fragment_set then + new_insert_fragment_set(entity, new_fragment) end - if new_fragment_on_insert then - new_fragment_on_insert(entity, new_fragment) + if new_insert_fragment_insert then + new_insert_fragment_insert(entity, new_fragment) end end end @@ -5167,14 +5256,16 @@ function __chunk_remove(old_chunk, ...) local new_fragment_set = new_chunk and new_chunk.__fragment_set or __safe_tbls.__EMPTY_FRAGMENT_SET + local old_remove_fragment_removes = old_chunk.__remove_fragment_removes + ---@cast old_remove_fragment_removes -? + for old_remove_fragment_index = 1, old_chunk.__remove_fragment_count do local fragment = old_remove_fragment_list[old_remove_fragment_index] if not new_fragment_set[fragment] then - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local remove_fragment_remove = old_remove_fragment_removes[old_remove_fragment_index] - if fragment_on_remove then + do local old_component_index = old_component_indices[fragment] if old_component_index then @@ -5183,12 +5274,12 @@ function __chunk_remove(old_chunk, ...) for old_place = 1, old_entity_count do local entity = old_entity_list[old_place] local old_component = old_component_storage[old_place] - fragment_on_remove(entity, fragment, old_component) + remove_fragment_remove(entity, fragment, old_component) end else for old_place = 1, old_entity_count do local entity = old_entity_list[old_place] - fragment_on_remove(entity, fragment) + remove_fragment_remove(entity, fragment) end end end @@ -5320,13 +5411,15 @@ function __chunk_clear(chunk) local chunk_component_indices = chunk.__component_indices local chunk_component_storages = chunk.__component_storages + local chunk_remove_fragment_removes = chunk.__remove_fragment_removes + ---@cast chunk_remove_fragment_removes -? + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local remove_fragment_remove = chunk_remove_fragment_removes[chunk_remove_fragment_index] - if fragment_on_remove then + do local component_index = chunk_component_indices[fragment] if component_index then @@ -5335,12 +5428,12 @@ function __chunk_clear(chunk) for place = 1, chunk_entity_count do local entity = chunk_entity_list[place] local old_component = component_storage[place] - fragment_on_remove(entity, fragment, old_component) + remove_fragment_remove(entity, fragment, old_component) end else for place = 1, chunk_entity_count do local entity = chunk_entity_list[place] - fragment_on_remove(entity, fragment) + remove_fragment_remove(entity, fragment) end end end @@ -6491,23 +6584,27 @@ function __evolved_set(entity, fragment, component) local old_component_indices = old_chunk.__component_indices local old_component_storages = old_chunk.__component_storages - 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 - - if old_chunk_has_setup_hooks or old_chunk_has_assign_hooks then - fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign = - __evolved_get(fragment, __DEFAULT, __DUPLICATE, __ON_SET, __ON_ASSIGN) - end - local old_component_index = old_component_indices[fragment] if old_component_index then local old_component_storage = old_component_storages[old_component_index] + ---@type evolved.set_hook?, evolved.assign_hook? + local assign_fragment_set, assign_fragment_assign + + if old_chunk.__has_assign_hooks then + assign_fragment_set = old_chunk.__component_sets[old_component_index] + assign_fragment_assign = old_chunk.__component_assigns[old_component_index] + end + + ---@type evolved.default?, evolved.duplicate? + local fragment_default, fragment_duplicate + + if old_chunk.__has_setup_hooks then + fragment_default = old_chunk.__component_defaults[old_component_index] + fragment_duplicate = old_chunk.__component_duplicates[old_component_index] + end + local new_component = component if new_component == nil then new_component = fragment_default end if new_component ~= nil and fragment_duplicate then new_component = fragment_duplicate(new_component) end @@ -6516,7 +6613,7 @@ function __evolved_set(entity, fragment, component) local old_component = old_component_storage[old_place] old_component_storage[old_place] = new_component - local fragment_search_index = old_chunk_has_index_fragments + local fragment_search_index = old_chunk.__has_index_fragments and __search_indices[fragment] or nil @@ -6524,12 +6621,12 @@ function __evolved_set(entity, fragment, component) __search_index_fns.assign(fragment_search_index, entity, new_component) end - if fragment_on_set then - fragment_on_set(entity, fragment, new_component, old_component) + if assign_fragment_set then + assign_fragment_set(entity, fragment, new_component, old_component) end - if fragment_on_assign then - fragment_on_assign(entity, fragment, new_component, old_component) + if assign_fragment_assign then + assign_fragment_assign(entity, fragment, new_component, old_component) end else -- nothing @@ -6552,17 +6649,6 @@ function __evolved_set(entity, fragment, component) local new_component_indices = new_chunk.__component_indices local new_component_storages = new_chunk.__component_storages - local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks - local new_chunk_has_index_fragments = new_chunk.__has_index_fragments - - ---@type evolved.default?, evolved.duplicate? - local fragment_default, fragment_duplicate - - if new_chunk_has_setup_hooks then - fragment_default, fragment_duplicate = - __evolved_get(fragment, __DEFAULT, __DUPLICATE) - end - local new_place = new_chunk.__entity_count + 1 if new_place > new_chunk.__entity_capacity then @@ -6605,6 +6691,14 @@ function __evolved_set(entity, fragment, component) if new_component_index then local new_component_storage = new_component_storages[new_component_index] + ---@type evolved.default?, evolved.duplicate? + local fragment_default, fragment_duplicate + + if new_chunk.__has_setup_hooks then + fragment_default = new_chunk.__component_defaults[new_component_index] + fragment_duplicate = new_chunk.__component_duplicates[new_component_index] + end + local new_component = component if new_component == nil then new_component = fragment_default end if new_component ~= nil and fragment_duplicate then new_component = fragment_duplicate(new_component) end @@ -6612,7 +6706,7 @@ function __evolved_set(entity, fragment, component) new_component_storage[new_place] = new_component - local fragment_search_index = new_chunk_has_index_fragments + local fragment_search_index = new_chunk.__has_index_fragments and __search_indices[fragment] or nil @@ -6626,25 +6720,31 @@ function __evolved_set(entity, fragment, component) local req_fragment_list = new_chunk.__fragment_list local req_fragment_count = new_chunk.__fragment_count + local new_chunk_has_setup_hooks = new_chunk.__has_setup_hooks + local new_chunk_has_index_fragments = new_chunk.__has_index_fragments + + local new_chunk_component_defaults = new_chunk.__component_defaults + local new_chunk_component_duplicates = new_chunk.__component_duplicates + for req_fragment_index = 1, req_fragment_count do local req_fragment = req_fragment_list[req_fragment_index] if ini_fragment_set[req_fragment] then -- this fragment has already been initialized else - ---@type evolved.default?, evolved.duplicate? - local req_fragment_default, req_fragment_duplicate - - if new_chunk_has_setup_hooks then - req_fragment_default, req_fragment_duplicate = - __evolved_get(req_fragment, __DEFAULT, __DUPLICATE) - end - local req_component_index = new_component_indices[req_fragment] if req_component_index then local req_component_storage = new_component_storages[req_component_index] + ---@type evolved.default?, evolved.duplicate? + local req_fragment_default, req_fragment_duplicate + + if new_chunk_has_setup_hooks then + req_fragment_default = new_chunk_component_defaults[req_component_index] + req_fragment_duplicate = new_chunk_component_duplicates[req_component_index] + end + local req_component = req_fragment_default if req_component ~= nil and req_fragment_duplicate then @@ -6675,15 +6775,20 @@ function __evolved_set(entity, fragment, component) local old_fragment_set = old_chunk and old_chunk.__fragment_set or __safe_tbls.__EMPTY_FRAGMENT_SET + local new_insert_fragment_sets = new_chunk.__insert_fragment_sets + local new_insert_fragment_inserts = new_chunk.__insert_fragment_inserts + + ---@cast new_insert_fragment_sets -? + ---@cast new_insert_fragment_inserts -? + for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do local new_fragment = new_insert_fragment_list[new_insert_fragment_index] if not old_fragment_set[new_fragment] then - ---@type evolved.set_hook?, evolved.insert_hook? - local new_fragment_on_set, new_fragment_on_insert = - __evolved_get(new_fragment, __ON_SET, __ON_INSERT) + local new_fragment_set = new_insert_fragment_sets[new_insert_fragment_index] + local new_fragment_insert = new_insert_fragment_inserts[new_insert_fragment_index] - if new_fragment_on_set or new_fragment_on_insert then + do local new_component_index = new_component_indices[new_fragment] if new_component_index then @@ -6691,20 +6796,20 @@ function __evolved_set(entity, fragment, component) local new_component = new_component_storage[new_place] - if new_fragment_on_set then - new_fragment_on_set(entity, new_fragment, new_component) + if new_fragment_set then + new_fragment_set(entity, new_fragment, new_component) end - if new_fragment_on_insert then - new_fragment_on_insert(entity, new_fragment, new_component) + if new_fragment_insert then + new_fragment_insert(entity, new_fragment, new_component) end else - if new_fragment_on_set then - new_fragment_on_set(entity, new_fragment) + if new_fragment_set then + new_fragment_set(entity, new_fragment) end - if new_fragment_on_insert then - new_fragment_on_insert(entity, new_fragment) + if new_fragment_insert then + new_fragment_insert(entity, new_fragment) end end end @@ -8396,16 +8501,16 @@ end --- --- -__evolved_set(__ON_SET, __ON_INSERT, __update_major_chunks) +__evolved_set(__ON_SET, __ON_SET, __update_major_chunks) __evolved_set(__ON_SET, __ON_REMOVE, __update_major_chunks) -__evolved_set(__ON_ASSIGN, __ON_INSERT, __update_major_chunks) +__evolved_set(__ON_ASSIGN, __ON_SET, __update_major_chunks) __evolved_set(__ON_ASSIGN, __ON_REMOVE, __update_major_chunks) -__evolved_set(__ON_INSERT, __ON_INSERT, __update_major_chunks) +__evolved_set(__ON_INSERT, __ON_SET, __update_major_chunks) __evolved_set(__ON_INSERT, __ON_REMOVE, __update_major_chunks) -__evolved_set(__ON_REMOVE, __ON_INSERT, __update_major_chunks) +__evolved_set(__ON_REMOVE, __ON_SET, __update_major_chunks) __evolved_set(__ON_REMOVE, __ON_REMOVE, __update_major_chunks) --- @@ -8435,10 +8540,10 @@ __evolved_set(__EXPLICIT, __ON_REMOVE, __update_major_chunks) __evolved_set(__INTERNAL, __ON_INSERT, __update_major_chunks) __evolved_set(__INTERNAL, __ON_REMOVE, __update_major_chunks) -__evolved_set(__DEFAULT, __ON_INSERT, __update_major_chunks) +__evolved_set(__DEFAULT, __ON_SET, __update_major_chunks) __evolved_set(__DEFAULT, __ON_REMOVE, __update_major_chunks) -__evolved_set(__DUPLICATE, __ON_INSERT, __update_major_chunks) +__evolved_set(__DUPLICATE, __ON_SET, __update_major_chunks) __evolved_set(__DUPLICATE, __ON_REMOVE, __update_major_chunks) __evolved_set(__EXTRACT, __ON_SET, __update_search_index) From 2e9e5d3a6b46c37daa50e5fdef74851d6083303c Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 25 Oct 2026 12:19:37 +0700 Subject: [PATCH 36/48] a little optimization --- evolved.lua | 247 +++++++++++++++++++--------------------------------- 1 file changed, 89 insertions(+), 158 deletions(-) diff --git a/evolved.lua b/evolved.lua index a8971ec..73339a7 100644 --- a/evolved.lua +++ b/evolved.lua @@ -759,21 +759,26 @@ end ---@param id_list evolved.id[] ---@param id_count integer local function __release_id_list(id_list, id_count) + local freelist_ids = __freelist_ids + local available_primary = __available_primary + for id_index = 1, id_count do local id = id_list[id_index] local acquired_primary = id % 1048576 local shifted_secondary = id - acquired_primary - if __freelist_ids[acquired_primary] == id then + if freelist_ids[acquired_primary] == id then shifted_secondary = shifted_secondary == 1099511627776 - 1048576 and 1048576 or shifted_secondary + 1048576 - __freelist_ids[acquired_primary] = __available_primary + shifted_secondary - __available_primary = acquired_primary + freelist_ids[acquired_primary] = available_primary + shifted_secondary + available_primary = acquired_primary end end + + __available_primary = available_primary end --- @@ -1460,12 +1465,8 @@ local __multi_clone_entity local __purge_chunk local __expand_chunk local __shrink_chunk -local __clear_chunk_list local __clear_entity_one -local __clear_entity_list -local __destroy_chunk_list local __destroy_entity_one -local __destroy_entity_list local __destroy_entity_stack local __remove_entity_fragments @@ -4228,23 +4229,6 @@ function __shrink_chunk(chunk, min_capacity) chunk.__entity_capacity = min_capacity end ----@param chunk_list evolved.chunk[] ----@param chunk_count integer -function __clear_chunk_list(chunk_list, chunk_count) - if __defer_depth <= 0 then - __debug_fns.error_fmt('this operation should be deferred') - end - - if chunk_count == 0 then - return - end - - for chunk_index = 1, chunk_count do - local chunk = chunk_list[chunk_index] - __chunk_clear(chunk) - end -end - ---@param entity evolved.entity function __clear_entity_one(entity) if __defer_depth <= 0 then @@ -4313,43 +4297,6 @@ function __clear_entity_one(entity) end end ----@param entity_list evolved.entity[] ----@param entity_count integer -function __clear_entity_list(entity_list, entity_count) - if __defer_depth <= 0 then - __debug_fns.error_fmt('this operation should be deferred') - end - - for entity_index = 1, entity_count do - local entity = entity_list[entity_index] - __clear_entity_one(entity) - end -end - ----@param chunk_list evolved.chunk[] ----@param chunk_count integer -function __destroy_chunk_list(chunk_list, chunk_count) - if __defer_depth <= 0 then - __debug_fns.error_fmt('this operation should be deferred') - end - - if chunk_count == 0 then - return - end - - ---@type evolved.chunk[] - local chunk_stack = __acquire_table(__table_pool_tag.chunk_list) - - ---@type evolved.entity[] - local entity_stack = __acquire_table(__table_pool_tag.entity_list) - - __lua_table_move(chunk_list, 1, chunk_count, 1, chunk_stack) - __destroy_entity_stack(chunk_stack, chunk_count, entity_stack, 0) - - __release_table(__table_pool_tag.chunk_list, chunk_stack, true, true) - __release_table(__table_pool_tag.entity_list, entity_stack, true, true) -end - ---@param entity evolved.entity function __destroy_entity_one(entity) if __defer_depth <= 0 then @@ -4420,30 +4367,6 @@ function __destroy_entity_one(entity) end end ----@param entity_list evolved.entity[] ----@param entity_count integer -function __destroy_entity_list(entity_list, entity_count) - if __defer_depth <= 0 then - __debug_fns.error_fmt('this operation should be deferred') - end - - if entity_count == 0 then - return - end - - ---@type evolved.chunk[] - local chunk_stack = __acquire_table(__table_pool_tag.chunk_list) - - ---@type evolved.entity[] - local entity_stack = __acquire_table(__table_pool_tag.entity_list) - - __lua_table_move(entity_list, 1, entity_count, 1, entity_stack) - __destroy_entity_stack(chunk_stack, 0, entity_stack, entity_count) - - __release_table(__table_pool_tag.entity_list, entity_stack, true, true) - __release_table(__table_pool_tag.chunk_list, chunk_stack, true, true) -end - ---@param chunk_stack evolved.chunk[] ---@param chunk_stack_size integer ---@param entity_stack evolved.entity[] @@ -6871,9 +6794,6 @@ function __evolved_clear(...) __evolved_defer() do - local purging_entity_list ---@type evolved.entity[]? - local purging_entity_count = 0 ---@type integer - for argument_index = 1, argument_count do ---@type evolved.entity local entity = __lua_select(argument_index, ...) @@ -6882,20 +6802,9 @@ function __evolved_clear(...) if __freelist_ids[entity_primary] ~= entity then -- nothing to clear from non-alive entities else - if not purging_entity_list then - ---@type evolved.entity[] - purging_entity_list = __acquire_table(__table_pool_tag.entity_list) - end - - purging_entity_count = purging_entity_count + 1 - purging_entity_list[purging_entity_count] = entity + __clear_entity_one(entity) end end - - if purging_entity_list then - __clear_entity_list(purging_entity_list, purging_entity_count) - __release_table(__table_pool_tag.entity_list, purging_entity_list, false, true) - end end __evolved_commit() @@ -6946,8 +6855,8 @@ function __evolved_destroy(...) local minor_chunks = __minor_chunks local target_fragments = __target_fragments - local destroying_entity_list ---@type evolved.entity[]? - local destroying_entity_count = 0 ---@type integer + local destroying_entity_stack ---@type evolved.entity[]? + local destroying_entity_stack_size = 0 ---@type integer for argument_index = 1, argument_count do ---@type evolved.entity @@ -6959,19 +6868,26 @@ function __evolved_destroy(...) elseif not minor_chunks[entity] and not target_fragments[entity] then __destroy_entity_one(entity) else - if not destroying_entity_list then + if not destroying_entity_stack then ---@type evolved.entity[] - destroying_entity_list = __acquire_table(__table_pool_tag.entity_list) + destroying_entity_stack = __acquire_table(__table_pool_tag.entity_list) end - destroying_entity_count = destroying_entity_count + 1 - destroying_entity_list[destroying_entity_count] = entity + destroying_entity_stack_size = destroying_entity_stack_size + 1 + destroying_entity_stack[destroying_entity_stack_size] = entity end end - if destroying_entity_list then - __destroy_entity_list(destroying_entity_list, destroying_entity_count) - __release_table(__table_pool_tag.entity_list, destroying_entity_list, false, true) + if destroying_entity_stack then + ---@type evolved.chunk[] + local destroying_chunk_stack = __acquire_table(__table_pool_tag.chunk_list) + + __destroy_entity_stack( + destroying_chunk_stack, 0, + destroying_entity_stack, destroying_entity_stack_size) + + __release_table(__table_pool_tag.chunk_list, destroying_chunk_stack, true, true) + __release_table(__table_pool_tag.entity_list, destroying_entity_stack, true, true) end end @@ -6995,15 +6911,20 @@ function __evolved_destroy_one(entity) elseif not __minor_chunks[entity] and not __target_fragments[entity] then __destroy_entity_one(entity) else + ---@type evolved.chunk[] + local destroying_chunk_stack = __acquire_table(__table_pool_tag.chunk_list) + ---@type evolved.entity[] - local destroying_entity_list = __acquire_table(__table_pool_tag.entity_list) - local destroying_entity_count = 0 + local destroying_entity_stack = __acquire_table(__table_pool_tag.entity_list) + + destroying_entity_stack[1] = entity - destroying_entity_count = destroying_entity_count + 1 - destroying_entity_list[destroying_entity_count] = entity + __destroy_entity_stack( + destroying_chunk_stack, 0, + destroying_entity_stack, 1) - __destroy_entity_list(destroying_entity_list, destroying_entity_count) - __release_table(__table_pool_tag.entity_list, destroying_entity_list, false, true) + __release_table(__table_pool_tag.chunk_list, destroying_chunk_stack, true, true) + __release_table(__table_pool_tag.entity_list, destroying_entity_stack, true, true) end end @@ -7064,23 +6985,23 @@ function __evolved_batch_set(query, fragment, component) end end - for assign_chunk_index = 1, assign_chunk_count do - ---@cast assign_chunk_list -? - local assign_chunk = assign_chunk_list[assign_chunk_index] - __chunk_assign(assign_chunk, fragment, component) - end - - for insert_chunk_index = 1, insert_chunk_count do - ---@cast insert_chunk_list -? - local insert_chunk = insert_chunk_list[insert_chunk_index] - __chunk_insert(insert_chunk, fragment, component) - end - if assign_chunk_list then + for assign_chunk_index = 1, assign_chunk_count do + ---@cast assign_chunk_list -? + local assign_chunk = assign_chunk_list[assign_chunk_index] + __chunk_assign(assign_chunk, fragment, component) + end + __release_table(__table_pool_tag.chunk_list, assign_chunk_list, false, true) end if insert_chunk_list then + for insert_chunk_index = 1, insert_chunk_count do + ---@cast insert_chunk_list -? + local insert_chunk = insert_chunk_list[insert_chunk_index] + __chunk_insert(insert_chunk, fragment, component) + end + __release_table(__table_pool_tag.chunk_list, insert_chunk_list, false, true) end end @@ -7112,27 +7033,26 @@ function __evolved_batch_remove(query, ...) __evolved_defer() do - local chunk_list ---@type evolved.chunk[]? - local chunk_count = 0 ---@type integer + local remove_chunk_list ---@type evolved.chunk[]? + local remove_chunk_count = 0 ---@type integer for chunk in __evolved_execute(query) do - if not chunk_list then + if not remove_chunk_list then ---@type evolved.chunk[] - chunk_list = __acquire_table(__table_pool_tag.chunk_list) + remove_chunk_list = __acquire_table(__table_pool_tag.chunk_list) end - chunk_count = chunk_count + 1 - chunk_list[chunk_count] = chunk + remove_chunk_count = remove_chunk_count + 1 + remove_chunk_list[remove_chunk_count] = chunk end - for chunk_index = 1, chunk_count do - ---@cast chunk_list -? - local chunk = chunk_list[chunk_index] - __chunk_remove(chunk, ...) - end + if remove_chunk_list then + for chunk_index = 1, remove_chunk_count do + local chunk = remove_chunk_list[chunk_index] + __chunk_remove(chunk, ...) + end - if chunk_list then - __release_table(__table_pool_tag.chunk_list, chunk_list, false, true) + __release_table(__table_pool_tag.chunk_list, remove_chunk_list, false, true) end end @@ -7155,8 +7075,8 @@ function __evolved_batch_clear(...) __evolved_defer() do - local chunk_list ---@type evolved.chunk[]? - local chunk_count = 0 ---@type integer + local clear_chunk_list ---@type evolved.chunk[]? + local clear_chunk_count = 0 ---@type integer for argument_index = 1, argument_count do ---@type evolved.query @@ -7168,20 +7088,24 @@ function __evolved_batch_clear(...) __id_name(query)) else for chunk in __evolved_execute(query) do - if not chunk_list then + if not clear_chunk_list then ---@type evolved.chunk[] - chunk_list = __acquire_table(__table_pool_tag.chunk_list) + clear_chunk_list = __acquire_table(__table_pool_tag.chunk_list) end - chunk_count = chunk_count + 1 - chunk_list[chunk_count] = chunk + clear_chunk_count = clear_chunk_count + 1 + clear_chunk_list[clear_chunk_count] = chunk end end end - if chunk_list then - __clear_chunk_list(chunk_list, chunk_count) - __release_table(__table_pool_tag.chunk_list, chunk_list, false, true) + if clear_chunk_list then + for chunk_index = 1, clear_chunk_count do + local chunk = clear_chunk_list[chunk_index] + __chunk_clear(chunk) + end + + __release_table(__table_pool_tag.chunk_list, clear_chunk_list, false, true) end end @@ -7204,8 +7128,8 @@ function __evolved_batch_destroy(...) __evolved_defer() do - local destroying_chunk_list ---@type evolved.chunk[]? - local destroying_chunk_count = 0 ---@type integer + local destroy_chunk_stack ---@type evolved.chunk[]? + local destroy_chunk_stack_size = 0 ---@type integer for argument_index = 1, argument_count do ---@type evolved.query @@ -7217,20 +7141,27 @@ function __evolved_batch_destroy(...) __id_name(query)) else for chunk in __evolved_execute(query) do - if not destroying_chunk_list then + if not destroy_chunk_stack then ---@type evolved.chunk[] - destroying_chunk_list = __acquire_table(__table_pool_tag.chunk_list) + destroy_chunk_stack = __acquire_table(__table_pool_tag.chunk_list) end - destroying_chunk_count = destroying_chunk_count + 1 - destroying_chunk_list[destroying_chunk_count] = chunk + destroy_chunk_stack_size = destroy_chunk_stack_size + 1 + destroy_chunk_stack[destroy_chunk_stack_size] = chunk end end end - if destroying_chunk_list then - __destroy_chunk_list(destroying_chunk_list, destroying_chunk_count) - __release_table(__table_pool_tag.chunk_list, destroying_chunk_list, false, true) + if destroy_chunk_stack then + ---@type evolved.entity[] + local destroying_entity_stack = __acquire_table(__table_pool_tag.entity_list) + + __destroy_entity_stack( + destroy_chunk_stack, destroy_chunk_stack_size, + destroying_entity_stack, 0) + + __release_table(__table_pool_tag.chunk_list, destroy_chunk_stack, true, true) + __release_table(__table_pool_tag.entity_list, destroying_entity_stack, true, true) end end From 5c40dbbc38b9b6f46d2a228e186defd22468428c Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 26 Oct 2026 01:08:34 +0700 Subject: [PATCH 37/48] a little defer optimization --- evolved.lua | 241 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 167 insertions(+), 74 deletions(-) diff --git a/evolved.lua b/evolved.lua index 73339a7..b973276 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1475,7 +1475,7 @@ local __chunk_insert local __chunk_remove local __chunk_clear -local __defer_call_hook +local __defer_call_func local __defer_spawn_entity local __defer_multi_spawn_entity @@ -2827,7 +2827,7 @@ end ---@param major evolved.fragment function __update_major_chunks(major) if __defer_depth > 0 then - __defer_call_hook(__update_major_chunks, major) + __defer_call_func(__update_major_chunks, 1, major) return end @@ -2844,7 +2844,7 @@ end ---@param fragment evolved.fragment function __update_search_index(fragment) if __defer_depth > 0 then - __defer_call_hook(__update_search_index, fragment) + __defer_call_func(__update_search_index, 1, fragment) return end @@ -5399,93 +5399,155 @@ end ---@enum evolved.defer_op local __defer_op = { - call_hook = 1, + call_func_0 = 1, + call_func_1 = 2, + call_func_2 = 3, + call_func_3 = 4, + call_func_4 = 5, + call_func_N = 6, - spawn_entity = 2, - multi_spawn_entity = 3, + set = 7, + remove_one = 8, + clear_one = 9, + destroy_one = 10, - clone_entity = 4, - multi_clone_entity = 5, + spawn_entity = 11, + multi_spawn_entity = 12, - __count = 5, + clone_entity = 13, + multi_clone_entity = 14, + + __count = 14, } ---@type table local __defer_ops = __lua_table_new(__defer_op.__count) ----@param hook fun(...) ----@param ... any hook arguments -function __defer_call_hook(hook, ...) +---@param func fun(...) +---@param argument_count integer +---@param a1? any +---@param a2? any +---@param a3? any +---@param a4? any +---@param ... any function arguments +function __defer_call_func(func, argument_count, a1, a2, a3, a4, ...) local length = __defer_length local bytecode = __defer_bytecode - local argument_count = __lua_select('#', ...) - - bytecode[length + 1] = __defer_op.call_hook - bytecode[length + 2] = hook - bytecode[length + 3] = argument_count - if argument_count == 0 then - -- nothing + bytecode[length + 1] = __defer_op.call_func_0 + bytecode[length + 2] = func + __defer_length = length + 2 elseif argument_count == 1 then - local a1 = ... - bytecode[length + 4] = a1 + bytecode[length + 1] = __defer_op.call_func_1 + bytecode[length + 2] = func + bytecode[length + 3] = a1 + __defer_length = length + 3 elseif argument_count == 2 then - local a1, a2 = ... - bytecode[length + 4] = a1 - bytecode[length + 5] = a2 + bytecode[length + 1] = __defer_op.call_func_2 + bytecode[length + 2] = func + bytecode[length + 3] = a1 + bytecode[length + 4] = a2 + __defer_length = length + 4 elseif argument_count == 3 then - local a1, a2, a3 = ... - bytecode[length + 4] = a1 - bytecode[length + 5] = a2 - bytecode[length + 6] = a3 + bytecode[length + 1] = __defer_op.call_func_3 + bytecode[length + 2] = func + bytecode[length + 3] = a1 + bytecode[length + 4] = a2 + bytecode[length + 5] = a3 + __defer_length = length + 5 elseif argument_count == 4 then - local a1, a2, a3, a4 = ... - bytecode[length + 4] = a1 - bytecode[length + 5] = a2 - bytecode[length + 6] = a3 - bytecode[length + 7] = a4 + bytecode[length + 1] = __defer_op.call_func_4 + bytecode[length + 2] = func + bytecode[length + 3] = a1 + bytecode[length + 4] = a2 + bytecode[length + 5] = a3 + bytecode[length + 6] = a4 + __defer_length = length + 6 else - local a1, a2, a3, a4 = ... + bytecode[length + 1] = __defer_op.call_func_N + bytecode[length + 2] = func + bytecode[length + 3] = argument_count bytecode[length + 4] = a1 bytecode[length + 5] = a2 bytecode[length + 6] = a3 bytecode[length + 7] = a4 for i = 5, argument_count do - bytecode[length + 3 + i] = __lua_select(i, ...) + bytecode[length + 3 + i] = __lua_select(i - 4, ...) end + __defer_length = length + 3 + argument_count end +end - __defer_length = length + 3 + argument_count +__defer_ops[__defer_op.call_func_0] = function(bytes, index) + local func = bytes[index] + func() + return 1 end -__defer_ops[__defer_op.call_hook] = function(bytes, index) - local hook = bytes[index + 0] - local argument_count = bytes[index + 1] +__defer_ops[__defer_op.call_func_1] = function(bytes, index) + local func = bytes[index] + local a1 = bytes[index + 1] + func(a1) + return 2 +end - if argument_count == 0 then - hook() - elseif argument_count == 1 then - local a1 = bytes[index + 2] - hook(a1) - elseif argument_count == 2 then - local a1, a2 = bytes[index + 2], bytes[index + 3] - hook(a1, a2) - elseif argument_count == 3 then - local a1, a2, a3 = bytes[index + 2], bytes[index + 3], bytes[index + 4] - hook(a1, a2, a3) - elseif argument_count == 4 then - local a1, a2, a3, a4 = bytes[index + 2], bytes[index + 3], bytes[index + 4], bytes[index + 5] - hook(a1, a2, a3, a4) - else - local a1, a2, a3, a4 = bytes[index + 2], bytes[index + 3], bytes[index + 4], bytes[index + 5] - hook(a1, a2, a3, a4, - __lua_table_unpack(bytes, index + 6, index + 1 + argument_count)) - end +__defer_ops[__defer_op.call_func_2] = function(bytes, index) + local func = bytes[index] + local a1, a2 = bytes[index + 1], bytes[index + 2] + func(a1, a2) + return 3 +end +__defer_ops[__defer_op.call_func_3] = function(bytes, index) + local func = bytes[index] + local a1, a2, a3 = bytes[index + 1], bytes[index + 2], bytes[index + 3] + func(a1, a2, a3) + return 4 +end + +__defer_ops[__defer_op.call_func_4] = function(bytes, index) + local func = bytes[index] + local a1, a2, a3, a4 = bytes[index + 1], bytes[index + 2], bytes[index + 3], bytes[index + 4] + func(a1, a2, a3, a4) + return 5 +end + +__defer_ops[__defer_op.call_func_N] = function(bytes, index) + local func = bytes[index] + local argument_count = bytes[index + 1] + local a1, a2, a3, a4 = bytes[index + 2], bytes[index + 3], bytes[index + 4], bytes[index + 5] + func(a1, a2, a3, a4, __lua_table_unpack(bytes, index + 6, index + 1 + argument_count)) return 2 + argument_count end +__defer_ops[__defer_op.set] = function(bytes, index) + local entity = bytes[index] + local fragment = bytes[index + 1] + local component = bytes[index + 2] + __evolved_set(entity, fragment, component) + return 3 +end + +__defer_ops[__defer_op.remove_one] = function(bytes, index) + local entity = bytes[index] + local fragment = bytes[index + 1] + __evolved_remove(entity, fragment) + return 2 +end + +__defer_ops[__defer_op.clear_one] = function(bytes, index) + local entity = bytes[index] + __evolved_clear_one(entity) + return 1 +end + +__defer_ops[__defer_op.destroy_one] = function(bytes, index) + local entity = bytes[index] + __evolved_destroy_one(entity) + return 1 +end + ---@param chunk? evolved.chunk ---@param entity evolved.entity ---@param component_table? evolved.component_table @@ -5515,7 +5577,7 @@ function __defer_spawn_entity(chunk, entity, component_table, component_mapper) end __defer_ops[__defer_op.spawn_entity] = function(bytes, index) - local chunk = bytes[index + 0] ---@type evolved.chunk + local chunk = bytes[index] ---@type evolved.chunk local entity = bytes[index + 1] ---@type evolved.entity local component_table2 = bytes[index + 2] ---@type evolved.component_table? local component_mapper = bytes[index + 3] ---@type evolved.component_mapper? @@ -5572,7 +5634,7 @@ function __defer_multi_spawn_entity(chunk, entity_list, entity_first, entity_cou end __defer_ops[__defer_op.multi_spawn_entity] = function(bytes, index) - local chunk = bytes[index + 0] ---@type evolved.chunk + local chunk = bytes[index] ---@type evolved.chunk local entity_count = bytes[index + 1] ---@type integer local entity_list2 = bytes[index + 2] ---@type evolved.entity[] local component_table2 = bytes[index + 3] ---@type evolved.component_table? @@ -5626,7 +5688,7 @@ function __defer_clone_entity(prefab, entity, component_table, component_mapper) end __defer_ops[__defer_op.clone_entity] = function(bytes, index) - local prefab = bytes[index + 0] ---@type evolved.entity + local prefab = bytes[index] ---@type evolved.entity local entity = bytes[index + 1] ---@type evolved.entity local component_table2 = bytes[index + 2] ---@type evolved.component_table? local component_mapper = bytes[index + 3] ---@type evolved.component_mapper? @@ -5683,7 +5745,7 @@ function __defer_multi_clone_entity(prefab, entity_list, entity_first, entity_co end __defer_ops[__defer_op.multi_clone_entity] = function(bytes, index) - local prefab = bytes[index + 0] ---@type evolved.entity + local prefab = bytes[index] ---@type evolved.entity local entity_count = bytes[index + 1] ---@type integer local entity_list2 = bytes[index + 2] ---@type evolved.entity[] local component_table2 = bytes[index + 3] ---@type evolved.component_table? @@ -6489,7 +6551,15 @@ function __evolved_set(entity, fragment, component) end if __defer_depth > 0 then - __defer_call_hook(__evolved_set, entity, fragment, component) + local length = __defer_length + local bytecode = __defer_bytecode + + bytecode[length + 1] = __defer_op.set + bytecode[length + 2] = entity + bytecode[length + 3] = fragment + bytecode[length + 4] = component + + __defer_length = length + 4 return end @@ -6761,7 +6831,18 @@ function __evolved_remove(entity, ...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_remove, entity, ...) + if fragment_count == 1 then + local length = __defer_length + local bytecode = __defer_bytecode + + bytecode[length + 1] = __defer_op.remove_one + bytecode[length + 2] = entity + bytecode[length + 3] = ... + + __defer_length = length + 3 + else + __defer_call_func(__evolved_remove, fragment_count + 1, entity, ...) + end return end @@ -6787,7 +6868,7 @@ function __evolved_clear(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_clear, ...) + __defer_call_func(__evolved_clear, argument_count, ...) return end @@ -6813,7 +6894,13 @@ end ---@param entity evolved.entity function __evolved_clear_one(entity) if __defer_depth > 0 then - __defer_call_hook(__evolved_clear_one, entity) + local length = __defer_length + local bytecode = __defer_bytecode + + bytecode[length + 1] = __defer_op.clear_one + bytecode[length + 2] = entity + + __defer_length = length + 2 return end @@ -6845,7 +6932,7 @@ function __evolved_destroy(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_destroy, ...) + __defer_call_func(__evolved_destroy, argument_count, ...) return end @@ -6897,7 +6984,13 @@ end ---@param entity evolved.entity function __evolved_destroy_one(entity) if __defer_depth > 0 then - __defer_call_hook(__evolved_destroy_one, entity) + local length = __defer_length + local bytecode = __defer_bytecode + + bytecode[length + 1] = __defer_op.destroy_one + bytecode[length + 2] = entity + + __defer_length = length + 2 return end @@ -6952,7 +7045,7 @@ function __evolved_batch_set(query, fragment, component) end if __defer_depth > 0 then - __defer_call_hook(__evolved_batch_set, query, fragment, component) + __defer_call_func(__evolved_batch_set, 3, query, fragment, component) return end @@ -7026,7 +7119,7 @@ function __evolved_batch_remove(query, ...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_batch_remove, query, ...) + __defer_call_func(__evolved_batch_remove, fragment_count + 1, query, ...) return end @@ -7068,7 +7161,7 @@ function __evolved_batch_clear(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_batch_clear, ...) + __defer_call_func(__evolved_batch_clear, argument_count, ...) return end @@ -7121,7 +7214,7 @@ function __evolved_batch_destroy(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_batch_destroy, ...) + __defer_call_func(__evolved_batch_destroy, argument_count, ...) return end @@ -7397,7 +7490,7 @@ function __evolved_reindex(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_reindex, ...) + __defer_call_func(__evolved_reindex, fragment_count, ...) return end @@ -7463,7 +7556,7 @@ end ---@param no_shrink? boolean function __evolved_collect_garbage(no_shrink) if __defer_depth > 0 then - __defer_call_hook(__evolved_collect_garbage) + __defer_call_func(__evolved_collect_garbage, 1, no_shrink) return end From 4c440a69f513cc0496ea189b2d28f4de7f0383d2 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 26 Oct 2026 17:25:48 +0700 Subject: [PATCH 38/48] free up top-level local variables --- evolved.lua | 296 +++++++++++++++++++++++++--------------------------- 1 file changed, 141 insertions(+), 155 deletions(-) diff --git a/evolved.lua b/evolved.lua index b973276..f7ae5f1 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1406,35 +1406,9 @@ local __evolved_collect_garbage local __id_name -local __new_chunk - local __default_realloc local __default_compmove -local __add_root_chunk -local __remove_root_chunk -local __add_child_chunk -local __remove_child_chunk - -local __update_chunk_caches -local __update_chunk_queries -local __update_chunk_storages - -local __trace_major_chunks -local __trace_minor_chunks - -local __cache_query_chunks -local __reset_query_chunks - -local __query_major_matches -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 @@ -1862,26 +1836,13 @@ end --- --- ----@param id evolved.id ----@return string ----@nodiscard -function __id_name(id) - ---@type string? - local id_name = __evolved_get(id, __NAME) - - if id_name then - return id_name - end - - local id_primary, id_secondary = __evolved_unpack(id) - return string.format('$%d#%d:%d', id, id_primary, id_secondary) -end +local __chunk_fns = {} ---@param chunk_parent? evolved.chunk ---@param chunk_fragment evolved.fragment ---@return evolved.chunk ---@nodiscard -function __new_chunk(chunk_parent, chunk_fragment) +function __chunk_fns.new_chunk(chunk_parent, chunk_fragment) local chunk_fragment_primary = chunk_fragment % 1048576 if __freelist_ids[chunk_fragment_primary] ~= chunk_fragment then @@ -1967,9 +1928,9 @@ function __new_chunk(chunk_parent, chunk_fragment) }, __chunk_mt) if not chunk_parent then - __add_root_chunk(chunk) + __chunk_fns.add_root_chunk(chunk) else - __add_child_chunk(chunk, chunk_parent) + __chunk_fns.add_child_chunk(chunk, chunk_parent) end do @@ -1998,46 +1959,15 @@ function __new_chunk(chunk_parent, chunk_fragment) __assoc_list_fns.insert(minor_chunks, chunk) end - __update_chunk_caches(chunk) - __update_chunk_queries(chunk) - __update_chunk_storages(chunk) + __chunk_fns.update_chunk_caches(chunk) + __chunk_fns.update_chunk_queries(chunk) + __chunk_fns.update_chunk_storages(chunk) return chunk end ----@param src? evolved.storage ----@param src_size integer ----@param dst_size integer ----@return evolved.storage? -function __default_realloc(src, src_size, dst_size) - if dst_size == 0 then - return - end - - if src and dst_size >= src_size then - return src - end - - local dst = __lua_table_new(dst_size) - - if src then - __lua_table_move(src, 1, dst_size, 1, dst) - end - - return dst -end - ----@param src evolved.storage ----@param f integer ----@param e integer ----@param t integer ----@param dst evolved.storage -function __default_compmove(src, f, e, t, dst) - __lua_table_move(src, f, e, t, dst) -end - ---@param root evolved.chunk -function __add_root_chunk(root) +function __chunk_fns.add_root_chunk(root) local root_index = __list_fns.upr(__root_list, root, __chunk_mt.__lt, __root_count) for sib_root_index = __root_count, root_index, -1 do @@ -2052,7 +1982,7 @@ function __add_root_chunk(root) end ---@param root evolved.chunk -function __remove_root_chunk(root) +function __chunk_fns.remove_root_chunk(root) if root.__parent then __debug_fns.error_fmt('unexpected root chunk: (%s)', tostring(root)) @@ -2080,7 +2010,7 @@ end ---@param child evolved.chunk ---@param parent evolved.chunk -function __add_child_chunk(child, parent) +function __chunk_fns.add_child_chunk(child, parent) parent.__child_count = __assoc_list_fns.sorted_insert_ex( parent.__child_set, parent.__child_list, parent.__child_count, child, __chunk_mt.__lt) @@ -2092,7 +2022,7 @@ function __add_child_chunk(child, parent) end ---@param child evolved.chunk -function __remove_child_chunk(child) +function __chunk_fns.remove_child_chunk(child) local parent = child.__parent if not parent then @@ -2120,7 +2050,7 @@ function __remove_child_chunk(child) end ---@param chunk evolved.chunk -function __update_chunk_caches(chunk) +function __chunk_fns.update_chunk_caches(chunk) local chunk_parent = chunk.__parent local chunk_fragment = chunk.__fragment @@ -2344,7 +2274,7 @@ function __update_chunk_caches(chunk) end ---@param chunk evolved.chunk -function __update_chunk_queries(chunk) +function __chunk_fns.update_chunk_queries(chunk) local major_queries = __major_queries[chunk.__fragment] local major_query_list = major_queries and major_queries.__item_list local major_query_count = major_queries and major_queries.__item_count or 0 @@ -2354,7 +2284,7 @@ function __update_chunk_queries(chunk) local major_query_chunks = __query_chunks[major_query] if major_query_chunks then - if __query_major_matches(chunk, major_query) then + if __chunk_fns.query_major_matches(chunk, major_query) then __assoc_list_fns.sorted_insert(major_query_chunks, chunk, __chunk_mt.__lt) else __assoc_list_fns.remove(major_query_chunks, chunk) @@ -2364,7 +2294,7 @@ function __update_chunk_queries(chunk) end ---@param chunk evolved.chunk -function __update_chunk_storages(chunk) +function __chunk_fns.update_chunk_storages(chunk) local entity_count = chunk.__entity_count local entity_capacity = chunk.__entity_capacity @@ -2546,7 +2476,7 @@ end ---@param major evolved.fragment ---@param trace fun(chunk: evolved.chunk, ...: any) ---@param ... any additional trace arguments -function __trace_major_chunks(major, trace, ...) +function __chunk_fns.trace_major_chunks(major, trace, ...) local chunk_stack ---@type evolved.chunk[]? local chunk_stack_size = 0 ---@type integer @@ -2596,7 +2526,7 @@ end ---@param minor evolved.fragment ---@param trace fun(chunk: evolved.chunk, ...: any) ---@param ... any additional trace arguments -function __trace_minor_chunks(minor, trace, ...) +function __chunk_fns.trace_minor_chunks(minor, trace, ...) local chunk_stack ---@type evolved.chunk[]? local chunk_stack_size = 0 @@ -2635,7 +2565,7 @@ end ---@param query evolved.query ---@return evolved.assoc_list ---@nodiscard -function __cache_query_chunks(query) +function __chunk_fns.cache_query_chunks(query) local query_includes = __sorted_includes[query] local query_include_list = query_includes and query_includes.__item_list local query_include_count = query_includes and query_includes.__item_count or 0 @@ -2658,7 +2588,7 @@ function __cache_query_chunks(query) for major_chunk_index = 1, major_chunk_count do local major_chunk = major_chunk_list[major_chunk_index] - if __query_major_matches(major_chunk, query) then + if __chunk_fns.query_major_matches(major_chunk, query) then __assoc_list_fns.insert(query_chunks, major_chunk) end end @@ -2685,7 +2615,7 @@ function __cache_query_chunks(query) for major_chunk_index = 1, major_chunk_count do local major_chunk = major_chunk_list[major_chunk_index] - if __query_major_matches(major_chunk, query) then + if __chunk_fns.query_major_matches(major_chunk, query) then __assoc_list_fns.insert(query_chunks, major_chunk) end end @@ -2708,7 +2638,7 @@ function __cache_query_chunks(query) end ---@param query evolved.query -function __reset_query_chunks(query) +function __chunk_fns.reset_query_chunks(query) if not __query_chunks[query] then return end @@ -2749,7 +2679,7 @@ end ---@param query evolved.query ---@return boolean ---@nodiscard -function __query_major_matches(chunk, query) +function __chunk_fns.query_major_matches(chunk, query) local query_includes = __sorted_includes[query] local query_include_set = query_includes and query_includes.__item_set local query_include_count = query_includes and query_includes.__item_count or 0 @@ -2765,14 +2695,14 @@ function __query_major_matches(chunk, query) return ( (query_include_index ~= nil and query_include_index == query_include_count) or (query_variant_index ~= nil and not __chunk_has_any_fragment_list(chunk, query_variant_list, query_variant_index - 1)) - ) and __query_minor_matches(chunk, query) + ) and __chunk_fns.query_minor_matches(chunk, query) end ---@param chunk evolved.chunk ---@param query evolved.query ---@return boolean ---@nodiscard -function __query_minor_matches(chunk, query) +function __chunk_fns.query_minor_matches(chunk, query) local query_includes = __sorted_includes[query] local query_include_set = query_includes and query_includes.__item_set local query_include_list = query_includes and query_includes.__item_list @@ -2825,26 +2755,26 @@ function __query_minor_matches(chunk, query) end ---@param major evolved.fragment -function __update_major_chunks(major) +function __chunk_fns.update_major_chunks(major) if __defer_depth > 0 then - __defer_call_func(__update_major_chunks, 1, major) + __defer_call_func(__chunk_fns.update_major_chunks, 1, major) return end - __trace_major_chunks(major, __update_major_chunks_trace) + __chunk_fns.trace_major_chunks(major, __chunk_fns.update_major_chunks_trace) end ---@param chunk evolved.chunk -function __update_major_chunks_trace(chunk) - __update_chunk_caches(chunk) - __update_chunk_queries(chunk) - __update_chunk_storages(chunk) +function __chunk_fns.update_major_chunks_trace(chunk) + __chunk_fns.update_chunk_caches(chunk) + __chunk_fns.update_chunk_queries(chunk) + __chunk_fns.update_chunk_storages(chunk) end ---@param fragment evolved.fragment -function __update_search_index(fragment) +function __chunk_fns.update_search_index(fragment) if __defer_depth > 0 then - __defer_call_func(__update_search_index, 1, fragment) + __defer_call_func(__chunk_fns.update_search_index, 1, fragment) return end @@ -2857,20 +2787,23 @@ function __update_search_index(fragment) end __search_indices[fragment] = nil - __update_major_chunks(fragment) + __chunk_fns.update_major_chunks(fragment) if __evolved_has_any(fragment, __INDEX, __LINK) then local new_search_index = __search_index_fns.new(fragment) __search_indices[fragment] = new_search_index - __trace_minor_chunks(fragment, __update_search_index_trace, fragment, new_search_index) - __trace_major_chunks(fragment, __update_chunk_caches) + + __chunk_fns.trace_minor_chunks(fragment, __chunk_fns.update_search_index_trace, + fragment, new_search_index) + + __chunk_fns.trace_major_chunks(fragment, __chunk_fns.update_chunk_caches) end 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) +function __chunk_fns.update_search_index_trace(chunk, fragment, fragment_search_index) local chunk_component_index = chunk.__component_indices[fragment] if chunk_component_index then @@ -2882,6 +2815,58 @@ function __update_search_index_trace(chunk, fragment, fragment_search_index) end end +--- +--- +--- +--- +--- + +---@param id evolved.id +---@return string +---@nodiscard +function __id_name(id) + ---@type string? + local id_name = __evolved_get(id, __NAME) + + if id_name then + return id_name + end + + local id_primary, id_secondary = __evolved_unpack(id) + return string.format('$%d#%d:%d', id, id_primary, id_secondary) +end + +---@param src? evolved.storage +---@param src_size integer +---@param dst_size integer +---@return evolved.storage? +function __default_realloc(src, src_size, dst_size) + if dst_size == 0 then + return + end + + if src and dst_size >= src_size then + return src + end + + local dst = __lua_table_new(dst_size) + + if src then + __lua_table_move(src, 1, dst_size, 1, dst) + end + + return dst +end + +---@param src evolved.storage +---@param f integer +---@param e integer +---@param t integer +---@param dst evolved.storage +function __default_compmove(src, f, e, t, dst) + __lua_table_move(src, f, e, t, dst) +end + ---@param chunk? evolved.chunk ---@param fragment evolved.fragment ---@return evolved.chunk @@ -2889,7 +2874,7 @@ end function __chunk_with_fragment(chunk, fragment) if not chunk then local root_chunk = __root_list[__root_set[fragment]] - return root_chunk or __new_chunk(nil, fragment) + return root_chunk or __chunk_fns.new_chunk(nil, fragment) end if chunk.__fragment_set[fragment] then @@ -2926,7 +2911,7 @@ function __chunk_with_fragment(chunk, fragment) return sib_chunk end - return __new_chunk(chunk, fragment) + return __chunk_fns.new_chunk(chunk, fragment) end ---@param chunk? evolved.chunk @@ -3075,7 +3060,7 @@ end ---@nodiscard function __chunk_fragments(head_fragment, ...) local chunk = __root_list[__root_set[head_fragment]] - or __new_chunk(nil, head_fragment) + or __chunk_fns.new_chunk(nil, head_fragment) for tail_fragment_index = 1, __lua_select('#', ...) do ---@type evolved.fragment @@ -3098,7 +3083,7 @@ function __chunk_components(components) end local chunk = __root_list[__root_set[head_fragment]] - or __new_chunk(nil, head_fragment) + or __chunk_fns.new_chunk(nil, head_fragment) for tail_fragment in __lua_next, components, head_fragment do chunk = chunk.__with_fragment_edges[tail_fragment] @@ -4040,9 +4025,9 @@ function __purge_chunk(chunk) end if not chunk.__parent then - __remove_root_chunk(chunk) + __chunk_fns.remove_root_chunk(chunk) else - __remove_child_chunk(chunk) + __chunk_fns.remove_child_chunk(chunk) end do @@ -7319,7 +7304,7 @@ function __evolved_execute(query) local query_variant_count = query_variants and query_variants.__item_count or 0 if query_include_count > 0 or query_variant_count > 0 then - local query_chunks = __query_chunks[query] or __cache_query_chunks(query) + local query_chunks = __query_chunks[query] or __chunk_fns.cache_query_chunks(query) local query_chunk_list = query_chunks and query_chunks.__item_list local query_chunk_count = query_chunks and query_chunks.__item_count or 0 @@ -7503,7 +7488,8 @@ function __evolved_reindex(...) __debug_fns.warning_fmt('the fragment (%s) is not indexed and cannot be reindexed', __id_name(fragment)) else - __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) + __chunk_fns.trace_minor_chunks(fragment, __chunk_fns.update_search_index_trace, + fragment, fragment_search_index) end end end @@ -8525,17 +8511,17 @@ end --- --- -__evolved_set(__ON_SET, __ON_SET, __update_major_chunks) -__evolved_set(__ON_SET, __ON_REMOVE, __update_major_chunks) +__evolved_set(__ON_SET, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__ON_SET, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__ON_ASSIGN, __ON_SET, __update_major_chunks) -__evolved_set(__ON_ASSIGN, __ON_REMOVE, __update_major_chunks) +__evolved_set(__ON_ASSIGN, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__ON_ASSIGN, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__ON_INSERT, __ON_SET, __update_major_chunks) -__evolved_set(__ON_INSERT, __ON_REMOVE, __update_major_chunks) +__evolved_set(__ON_INSERT, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__ON_INSERT, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__ON_REMOVE, __ON_SET, __update_major_chunks) -__evolved_set(__ON_REMOVE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__ON_REMOVE, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__ON_REMOVE, __ON_REMOVE, __chunk_fns.update_major_chunks) --- --- @@ -8543,41 +8529,41 @@ __evolved_set(__ON_REMOVE, __ON_REMOVE, __update_major_chunks) --- --- -__evolved_set(__TAG, __ON_INSERT, __update_search_index) -__evolved_set(__TAG, __ON_REMOVE, __update_search_index) +__evolved_set(__TAG, __ON_INSERT, __chunk_fns.update_search_index) +__evolved_set(__TAG, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__INDEX, __ON_INSERT, __update_search_index) -__evolved_set(__INDEX, __ON_REMOVE, __update_search_index) +__evolved_set(__INDEX, __ON_INSERT, __chunk_fns.update_search_index) +__evolved_set(__INDEX, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__LINK, __ON_INSERT, __update_search_index) -__evolved_set(__LINK, __ON_REMOVE, __update_search_index) +__evolved_set(__LINK, __ON_INSERT, __chunk_fns.update_search_index) +__evolved_set(__LINK, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__LINK_POLICY, __ON_SET, __update_search_index) -__evolved_set(__LINK_POLICY, __ON_REMOVE, __update_search_index) +__evolved_set(__LINK_POLICY, __ON_SET, __chunk_fns.update_search_index) +__evolved_set(__LINK_POLICY, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__UNIQUE, __ON_INSERT, __update_major_chunks) -__evolved_set(__UNIQUE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__UNIQUE, __ON_INSERT, __chunk_fns.update_major_chunks) +__evolved_set(__UNIQUE, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__EXPLICIT, __ON_INSERT, __update_major_chunks) -__evolved_set(__EXPLICIT, __ON_REMOVE, __update_major_chunks) +__evolved_set(__EXPLICIT, __ON_INSERT, __chunk_fns.update_major_chunks) +__evolved_set(__EXPLICIT, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__INTERNAL, __ON_INSERT, __update_major_chunks) -__evolved_set(__INTERNAL, __ON_REMOVE, __update_major_chunks) +__evolved_set(__INTERNAL, __ON_INSERT, __chunk_fns.update_major_chunks) +__evolved_set(__INTERNAL, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__DEFAULT, __ON_SET, __update_major_chunks) -__evolved_set(__DEFAULT, __ON_REMOVE, __update_major_chunks) +__evolved_set(__DEFAULT, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__DEFAULT, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__DUPLICATE, __ON_SET, __update_major_chunks) -__evolved_set(__DUPLICATE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__DUPLICATE, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__DUPLICATE, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__EXTRACT, __ON_SET, __update_search_index) -__evolved_set(__EXTRACT, __ON_REMOVE, __update_search_index) +__evolved_set(__EXTRACT, __ON_SET, __chunk_fns.update_search_index) +__evolved_set(__EXTRACT, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__REALLOC, __ON_SET, __update_major_chunks) -__evolved_set(__REALLOC, __ON_REMOVE, __update_major_chunks) +__evolved_set(__REALLOC, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__REALLOC, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__COMPMOVE, __ON_SET, __update_major_chunks) -__evolved_set(__COMPMOVE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__COMPMOVE, __ON_SET, __chunk_fns.update_major_chunks) +__evolved_set(__COMPMOVE, __ON_REMOVE, __chunk_fns.update_major_chunks) --- --- @@ -8654,7 +8640,7 @@ __evolved_set(__QUERY, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) ---@param query evolved.query ---@param include_list evolved.fragment[] __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) - __reset_query_chunks(query) + __chunk_fns.reset_query_chunks(query) local include_count = #include_list @@ -8672,7 +8658,7 @@ __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) end) __evolved_set(__INCLUDES, __ON_REMOVE, function(query) - __reset_query_chunks(query) + __chunk_fns.reset_query_chunks(query) __sorted_includes[query] = nil end) @@ -8686,7 +8672,7 @@ end) ---@param query evolved.query ---@param exclude_list evolved.fragment[] __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) - __reset_query_chunks(query) + __chunk_fns.reset_query_chunks(query) local exclude_count = #exclude_list @@ -8704,7 +8690,7 @@ __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) end) __evolved_set(__EXCLUDES, __ON_REMOVE, function(query) - __reset_query_chunks(query) + __chunk_fns.reset_query_chunks(query) __sorted_excludes[query] = nil end) @@ -8718,7 +8704,7 @@ end) ---@param query evolved.query ---@param variant_list evolved.fragment[] __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) - __reset_query_chunks(query) + __chunk_fns.reset_query_chunks(query) local variant_count = #variant_list @@ -8736,7 +8722,7 @@ __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) end) __evolved_set(__VARIANTS, __ON_REMOVE, function(query) - __reset_query_chunks(query) + __chunk_fns.reset_query_chunks(query) __sorted_variants[query] = nil end) @@ -8764,13 +8750,13 @@ __evolved_set(__REQUIRES, __ON_SET, function(fragment, _, require_list) __sorted_requires[fragment] = nil end - __update_major_chunks(fragment) + __chunk_fns.update_major_chunks(fragment) end) __evolved_set(__REQUIRES, __ON_REMOVE, function(fragment) __sorted_requires[fragment] = nil - __update_major_chunks(fragment) + __chunk_fns.update_major_chunks(fragment) end) --- From 646e5c97bd7cb3a42a7123b6625b2ba35a507438 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 26 Oct 2026 20:53:07 +0700 Subject: [PATCH 39/48] remove EXTRACT --- develop/all.lua | 1 - develop/testing/extract_tests.lua | 376 ------------------------------ evolved.d.tl | 2 - evolved.lua | 190 ++++++--------- 4 files changed, 71 insertions(+), 498 deletions(-) delete mode 100644 develop/testing/extract_tests.lua diff --git a/develop/all.lua b/develop/all.lua index 99e8d6c..a9a5daa 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -10,7 +10,6 @@ require 'develop.testing.cancel_tests' require 'develop.testing.clone_tests' require 'develop.testing.depth_tests' require 'develop.testing.destroy_tests' -require 'develop.testing.extract_tests' require 'develop.testing.link_tests' require 'develop.testing.locate_tests' require 'develop.testing.lookup_tests' diff --git a/develop/testing/extract_tests.lua b/develop/testing/extract_tests.lua deleted file mode 100644 index 5ed5726..0000000 --- a/develop/testing/extract_tests.lua +++ /dev/null @@ -1,376 +0,0 @@ -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 -end - ----@param c table -local function extract_team(c) - return c.team -end - -do - local f = evo.builder():extract(extract_team):build() - assert(evo.get(f, evo.EXTRACT) == extract_team) - assert(not evo.has_any(f, evo.INDEX, evo.LINK)) -end - -do - local f = evo.builder():index():extract(string.lower):build() - - local e1 = evo.spawn { [f] = 'Hello' } - local e2 = evo.spawn { [f] = 'HELLO' } - local e3 = evo.spawn { [f] = 'World' } - - check_search(f, 'hello', e1, e2) - check_search(f, 'world', e3) - check_search(f, 'Hello') - - evo.set(e3, f, 'hElLo') - check_search(f, 'hello', e1, e2, e3) - check_search(f, 'world') - - -- components are stored as is, only the index uses extracted values - assert(evo.get(e3, f) == 'hElLo') -end - -do - local f = evo.builder():index():extract(extract_team):build() - - local e1 = evo.spawn { [f] = { team = 'red' } } - local e2 = evo.spawn { [f] = { team = 'red' } } - local e3 = evo.spawn { [f] = { team = 'blue' } } - - check_search(f, 'red', e1, e2) - check_search(f, 'blue', e3) - - -- a new table with the same extracted value keeps the entity position - evo.set(e1, f, { team = 'red' }) - check_search(f, 'red', e1, e2) - - evo.set(e1, f, { team = 'blue' }) - check_search(f, 'red', e2) - check_search(f, 'blue', e3, e1) - - evo.remove(e3, f) - check_search(f, 'blue', e1) - - evo.destroy(e1) - check_search(f, 'blue') - - evo.clear(e2) - check_search(f, 'red') -end - -do - local f = evo.builder():index():extract(extract_team):build() - - -- nil and NaN extracted values are not indexed - local e1 = evo.spawn { [f] = {} } - local e2 = evo.spawn { [f] = { team = 'red' } } - local e3 = evo.spawn { [f] = { team = 0 / 0 } } - check_search(f, 'red', e2) - - evo.set(e1, f, { team = 'red' }) - evo.set(e3, f, { team = 'red' }) - check_search(f, 'red', e2, e1, e3) - - evo.set(e2, f, {}) - evo.set(e3, f, { team = 0 / 0 }) - check_search(f, 'red', e1) - - evo.set(e2, f, {}) - check_search(f, 'red', e1) - - evo.destroy(e1, e2, e3) - check_search(f, 'red') -end - -do - local f = evo.builder():index():extract(extract_team):build() - - local c1, c2 = { team = 'red' }, { team = 'red' } - local e1 = evo.spawn { [f] = c1 } - local e2 = evo.spawn { [f] = c2 } - - -- in-place changes bypass the index - c1.team = 'blue' - check_search(f, 'red', e1, e2) - check_search(f, 'blue') - - -- setting the same table again updates the index - evo.set(e1, f, c1) - check_search(f, 'red', e2) - check_search(f, 'blue', e1) - - -- reindexing updates the index too - c2.team = 'blue' - evo.reindex(f) - check_search(f, 'red') - check_search(f, 'blue', e1, e2) - - -- removing uses the indexed value, not the changed component - c1.team = 'green' - evo.remove(e1, f) - check_search(f, 'blue', e2) - check_search(f, 'green') -end - -do - local f = evo.builder():index():build() - - local c1, c2 = { team = 'red' }, { team = 'blue' } - local e1 = evo.spawn { [f] = c1 } - local e2 = evo.spawn { [f] = c2 } - - check_search(f, c1, e1) - check_search(f, 'red') - - -- setting the trait rebuilds the index - evo.set(f, evo.EXTRACT, extract_team) - check_search(f, c1) - check_search(f, 'red', e1) - check_search(f, 'blue', e2) - - -- changing the trait rebuilds the index - evo.set(f, evo.EXTRACT, function(c) return c.team == 'red' end) - check_search(f, 'red') - check_search(f, true, e1) - check_search(f, false, e2) - - -- removing the trait rebuilds the index - evo.remove(f, evo.EXTRACT) - check_search(f, true) - check_search(f, c1, e1) - check_search(f, c2, e2) -end - -do - local f = evo.builder():extract(extract_team):build() - - local e1 = evo.spawn { [f] = { team = 'red' } } - local e2 = evo.spawn { [f] = { team = 'blue' } } - - -- the trait is used as soon as the fragment is indexed - evo.set(f, evo.INDEX) - check_search(f, 'red', e1) - check_search(f, 'blue', e2) - - evo.remove(f, evo.INDEX) - evo.set(e1, f, { team = 'blue' }) - - evo.set(f, evo.INDEX) - check_search(f, 'red') - check_search(f, 'blue', e1, e2) -end - -do - local extract_count = 0 - - local f = evo.builder():index():extract(function(c) - extract_count = extract_count + 1 - return c.team - end):build() - - local g = evo.builder():build() - - local e = evo.spawn { [f] = { team = 'red' } } - assert(extract_count == 1) - - evo.set(e, f, { team = 'blue' }) - assert(extract_count == 2) - - -- moving between chunks does not extract values again - evo.set(e, g, 42) - evo.remove(e, g) - assert(extract_count == 2) - - -- removing uses the indexed value, so nothing is extracted - evo.remove(e, f) - assert(extract_count == 2) - - evo.set(e, f, { team = 'red' }) - assert(extract_count == 3) - - evo.destroy(e) - assert(extract_count == 3) - check_search(f, 'red') -end - -do - local f = evo.builder():index():extract(extract_team):build() - - local prefab = evo.spawn { [f] = { team = 'red' } } - - local clone = evo.clone(prefab) - check_search(f, 'red', prefab, clone) - - local clone_list = evo.multi_clone(2, prefab, { [f] = { team = 'blue' } }) - check_search(f, 'blue', clone_list[1], clone_list[2]) - - local spawn_list = evo.multi_spawn(2, { [f] = { team = 'green' } }) - check_search(f, 'green', spawn_list[1], spawn_list[2]) - - local q = evo.builder():include(f):build() - evo.batch_set(q, f, { team = 'yellow' }) - check_search(f, 'red') - check_search(f, 'blue') - check_search(f, 'green') - assert(select(2, evo.multi_search(f, 'yellow')) == 6) - - evo.batch_destroy(q) - check_search(f, 'yellow') -end - -do - local f = evo.builder() - :index() - :default({ team = 'none' }) - :duplicate(function(c) return { team = c.team } end) - :extract(extract_team) - :build() - - local e1 = evo.spawn { [f] = { team = 'red' } } - local e2 = evo.builder():set(f):build() - check_search(f, 'red', e1) - check_search(f, 'none', e2) - - evo.defer() - local e3 = evo.spawn { [f] = { team = 'red' } } - evo.set(e2, f, { team = 'red' }) - check_search(f, 'red', e1) - evo.commit() - check_search(f, 'red', e1, e3, e2) - check_search(f, 'none') -end - -do - local owner = evo.builder():link():extract(function(c) return c.owner end):build() - - local player1, player2 = evo.id(2) - - local sword = evo.spawn { [owner] = { owner = player1, slot = 1 } } - local shield = evo.spawn { [owner] = { owner = player1, slot = 2 } } - local bow = evo.spawn { [owner] = { owner = player2, slot = 1 } } - - check_search(owner, player1, sword, shield) - check_search(owner, player2, bow) - - evo.destroy(player1) - assert(evo.alive_all(sword, shield, bow)) - assert(not evo.has(sword, owner) and not evo.has(shield, owner)) - assert(evo.get(bow, owner).owner == player2) - - check_search(owner, player1) - check_search(owner, player2, bow) -end - -do - local owner = evo.builder():link():extract(function(c) return c.owner end):build() - - local player = evo.id() - - -- nil extracted values do not link to anything - local item = evo.spawn { [owner] = { slot = 1 } } - check_search(owner, player) - - evo.set(item, owner, { owner = player, slot = 1 }) - check_search(owner, player, item) - - evo.set(item, owner, { slot = 1 }) - check_search(owner, player) - - evo.destroy(player) - assert(evo.has(item, owner)) -end - -do - local parent = evo.builder() - :link() - :link_policy(evo.LINK_POLICY_DESTROY_ENTITY) - :extract(function(c) return c.parent end) - :build() - - local root = evo.id() - - local child = evo.spawn { [parent] = { parent = root } } - local grandchild = evo.spawn { [parent] = { parent = child } } - local orphan = evo.spawn { [parent] = {} } - - evo.destroy(root) - assert(not evo.alive_any(child, grandchild)) - assert(evo.alive(orphan) and evo.has(orphan, parent)) -end - -do - local attached_to = evo.builder():link():extract(function(c) return c.pillars end):build() - - local p1, p2, p3 = evo.id(3) - - local c = { pillars = { p1, p2 } } - local rope = evo.spawn { [attached_to] = c } - - check_search(attached_to, p1, rope) - check_search(attached_to, p2, rope) - check_search(attached_to, p3) - - -- the extracted list is changed in place, so set the component again - c.pillars[2] = p3 - evo.set(rope, attached_to, c) - check_search(attached_to, p1, rope) - check_search(attached_to, p2) - check_search(attached_to, p3, rope) - - evo.destroy(p2) - assert(evo.has(rope, attached_to)) - - evo.destroy(p1) - assert(evo.alive(rope) and not evo.has(rope, attached_to)) - check_search(attached_to, p3) -end - -do - local f = evo.builder():link():build() - - local t1, t2, t3 = evo.id(3) - - local e = evo.spawn { [f] = { t1, t2, t3 } } - check_search(f, t1, e) - check_search(f, t2, e) - - -- setting the trait relinks existing components - evo.set(f, evo.EXTRACT, function(c) return c[1] end) - check_search(f, t1, e) - check_search(f, t2) - - evo.destroy(t2) - assert(evo.has(e, f)) - - evo.set(e, f, { t1, t3 }) - - -- removing the trait relinks existing components - evo.remove(f, evo.EXTRACT) - check_search(f, t1, e) - check_search(f, t3, e) - - evo.destroy(t3) - assert(evo.alive(e) and not evo.has(e, f)) -end diff --git a/evolved.d.tl b/evolved.d.tl index acb2656..f34d169 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -115,7 +115,6 @@ default: function(self: Builder, default: Component): Builder duplicate: function(self: Builder, duplicate: function(Component): Component): Builder - extract: function(self: Builder, extract: function(Component): Extract | nil): Builder realloc: function(self: Builder, realloc: function({ Component } | nil, integer, integer): { Component } | nil): Builder compmove: function(self: Builder, compmove: function({ Component }, integer, integer, integer, { Component })): Builder @@ -156,7 +155,6 @@ DEFAULT: Fragment DUPLICATE: Fragment - EXTRACT: Fragment REALLOC: Fragment COMPMOVE: Fragment diff --git a/evolved.lua b/evolved.lua index f7ae5f1..288cc9f 100644 --- a/evolved.lua +++ b/evolved.lua @@ -42,7 +42,6 @@ local evolved = { ---@alias evolved.default evolved.component ---@alias evolved.duplicate fun(component: evolved.component): evolved.component ----@alias evolved.extract fun(component: evolved.component): evolved.component? ---@alias evolved.realloc fun(src?: evolved.storage, src_size: integer, dst_size: integer): evolved.storage? ---@alias evolved.compmove fun(src: evolved.storage, f: integer, e: integer, t: integer, dst: evolved.storage) @@ -1289,7 +1288,6 @@ local __INTERNAL = __acquire_id() local __DEFAULT = __acquire_id() local __DUPLICATE = __acquire_id() -local __EXTRACT = __acquire_id() local __REALLOC = __acquire_id() local __COMPMOVE = __acquire_id() @@ -1486,10 +1484,9 @@ local __safe_tbls = { ---@field package __head_entities table ---@field package __prev_entities table ---@field package __next_entities table ----@field package __entity_extracts table +---@field package __entity_components table ---@field package __link_policy? evolved.id ---@field package __target_fragment? evolved.fragment ----@field package __extract_component? evolved.extract local __search_index_fns = {} @@ -1503,10 +1500,9 @@ function __search_index_fns.new(fragment) __head_entities = {}, __prev_entities = {}, __next_entities = {}, - __entity_extracts = {}, + __entity_components = {}, __link_policy = __evolved_get(fragment, __LINK_POLICY), __target_fragment = __evolved_has(fragment, __LINK) and fragment or nil, - __extract_component = __evolved_get(fragment, __EXTRACT), } end @@ -1516,7 +1512,7 @@ function __search_index_fns.combine_keys(index) return end - local entity_extracts = index.__entity_extracts + local entity_components = index.__entity_components local old_prev_entities = index.__prev_entities local old_next_entities = index.__next_entities @@ -1525,9 +1521,9 @@ function __search_index_fns.combine_keys(index) local new_next_entities = {} ---@type table for entity_primary, prev_entity in __lua_next, old_prev_entities do - local extracted = entity_extracts[entity_primary] + local component = entity_components[entity_primary] - local key_offset = extracted % 1048576 * 1048576 + local key_offset = component % 1048576 * 1048576 local entity_key = key_offset + entity_primary new_prev_entities[entity_key] = prev_entity @@ -1572,52 +1568,37 @@ end ---@param entity evolved.entity ---@param component evolved.component function __search_index_fns.insert(index, entity, component) - local extract_component = index.__extract_component - - local extracted = component - - if extract_component then - extracted = extract_component(extracted) - end - - __search_index_fns.insert_extracted(index, entity, extracted) -end - ----@param index evolved.search_index ----@param entity evolved.entity ----@param extracted? evolved.component -function __search_index_fns.insert_extracted(index, entity, extracted) - if extracted == nil or extracted ~= extracted then - -- nil and NaN values cannot be indexed + if component ~= component then + -- NaN components cannot be indexed return end - if index.__target_fragment and __lua_type(extracted) == 'table' then + if index.__target_fragment and __lua_type(component) == 'table' then if not index.__combined_keys then __search_index_fns.combine_keys(index) end ---@type evolved.entity[], integer - local target_list, target_count = __list_fns.dup(extracted) + local target_list, target_count = __list_fns.dup(component) for target_index = 1, target_count do __search_index_fns.insert_node(index, entity, target_list[target_index]) end - index.__entity_extracts[entity % 1048576] = target_list + index.__entity_components[entity % 1048576] = target_list else - __search_index_fns.insert_node(index, entity, extracted) - index.__entity_extracts[entity % 1048576] = extracted + __search_index_fns.insert_node(index, entity, component) + index.__entity_components[entity % 1048576] = component end end ---@param index evolved.search_index ---@param entity evolved.entity ----@param extracted evolved.component -function __search_index_fns.insert_node(index, entity, extracted) +---@param component evolved.component +function __search_index_fns.insert_node(index, entity, component) local target_fragment = index.__target_fragment - local key_offset = index.__combined_keys and extracted % 1048576 * 1048576 or 0 + local key_offset = index.__combined_keys and component % 1048576 * 1048576 or 0 local entity_key = key_offset + entity % 1048576 local head_entities = index.__head_entities @@ -1625,7 +1606,7 @@ function __search_index_fns.insert_node(index, entity, extracted) local next_entities = index.__next_entities ---@type evolved.entity? - local head_entity = head_entities[extracted] + local head_entity = head_entities[component] if head_entity then if prev_entities[entity_key] then @@ -1643,18 +1624,18 @@ function __search_index_fns.insert_node(index, entity, extracted) prev_entities[head_entity_key] = entity next_entities[tail_entity_key] = entity - elseif not target_fragment or __freelist_ids[extracted % 1048576] == extracted then - head_entities[extracted] = entity + elseif not target_fragment or __freelist_ids[component % 1048576] == component then + head_entities[component] = entity prev_entities[entity_key] = entity next_entities[entity_key] = entity if target_fragment then - __search_index_fns.link(extracted, target_fragment) + __search_index_fns.link(component, target_fragment) end elseif __debug_mode and target_fragment then __debug_fns.error_fmt('the target (%s) of the linked fragment (%s) is not alive', - __id_name(extracted), __id_name(target_fragment)) + __id_name(component), __id_name(target_fragment)) end end @@ -1664,34 +1645,34 @@ function __search_index_fns.remove(index, entity) local entity_primary = entity % 1048576 ---@type evolved.component? - local extracted = index.__entity_extracts[entity_primary] + local component = index.__entity_components[entity_primary] - if extracted == nil then + if component == nil then return end - if index.__target_fragment and __lua_type(extracted) == 'table' then + if index.__target_fragment and __lua_type(component) == 'table' then ---@type evolved.entity[], integer - local target_list, target_count = extracted, #extracted + local target_list, target_count = component, #component for target_index = 1, target_count do __search_index_fns.remove_node(index, entity, target_list[target_index]) end - index.__entity_extracts[entity_primary] = nil + index.__entity_components[entity_primary] = nil else - __search_index_fns.remove_node(index, entity, extracted) - index.__entity_extracts[entity_primary] = nil + __search_index_fns.remove_node(index, entity, component) + index.__entity_components[entity_primary] = nil end end ---@param index evolved.search_index ---@param entity evolved.entity ----@param extracted evolved.component -function __search_index_fns.remove_node(index, entity, extracted) +---@param component evolved.component +function __search_index_fns.remove_node(index, entity, component) local target_fragment = index.__target_fragment - local key_offset = index.__combined_keys and extracted % 1048576 * 1048576 or 0 + local key_offset = index.__combined_keys and component % 1048576 * 1048576 or 0 local entity_key = key_offset + entity % 1048576 local head_entities = index.__head_entities @@ -1699,7 +1680,7 @@ function __search_index_fns.remove_node(index, entity, extracted) local next_entities = index.__next_entities ---@type evolved.entity? - local head_entity = head_entities[extracted] + local head_entity = head_entities[component] if not head_entity then -- this target is dead or has no links anymore @@ -1715,10 +1696,10 @@ function __search_index_fns.remove_node(index, entity, extracted) end if next_entity == entity then - head_entities[extracted] = nil + head_entities[component] = nil if target_fragment then - __search_index_fns.unlink(extracted, target_fragment) + __search_index_fns.unlink(component, target_fragment) end else local prev_entity = prev_entities[entity_key] @@ -1730,7 +1711,7 @@ function __search_index_fns.remove_node(index, entity, extracted) next_entities[prev_entity_key] = next_entity if head_entity == entity then - head_entities[extracted] = next_entity + head_entities[component] = next_entity end end @@ -1743,33 +1724,23 @@ end ---@param component evolved.component function __search_index_fns.assign(index, entity, component) local target_fragment = index.__target_fragment - local extract_component = index.__extract_component - local entity_extracts = index.__entity_extracts + local entity_components = index.__entity_components - local new_extracted = component + local new_component = component + local old_component = entity_components[entity % 1048576] - if extract_component then - new_extracted = extract_component(new_extracted) - end - - local old_extracted = entity_extracts[entity % 1048576] - - if new_extracted == old_extracted then + if new_component == old_component then -- nothing to reassign - return - end - - if target_fragment - and __lua_type(new_extracted) == 'table' - and __lua_type(old_extracted) == 'table' - and __list_fns.eq(new_extracted, old_extracted) + elseif target_fragment + and __lua_type(new_component) == 'table' + and __lua_type(old_component) == 'table' + and __list_fns.eq(new_component, old_component) then -- the list has the same targets, nothing to reassign - return + else + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, new_component) end - - __search_index_fns.remove(index, entity) - __search_index_fns.insert_extracted(index, entity, new_extracted) end ---@param index evolved.search_index @@ -1777,56 +1748,49 @@ end ---@param component_list evolved.component[] ---@param first integer ---@param last integer -function __search_index_fns.multi_insert(index, entity_list, component_list, first, last) +function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) + local target_fragment = index.__target_fragment + local entity_components = index.__entity_components + for place = first, last do - __search_index_fns.insert(index, entity_list[place], component_list[place]) + local entity = entity_list[place] + + local new_component = component_list[place] + local old_component = entity_components[entity % 1048576] + + if new_component == old_component then + -- nothing to reassign + elseif target_fragment + and __lua_type(new_component) == 'table' + and __lua_type(old_component) == 'table' + and __list_fns.eq(new_component, old_component) + then + -- the list has the same targets, nothing to reassign + else + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, new_component) + end 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) +function __search_index_fns.multi_insert(index, entity_list, component_list, first, last) for place = first, last do - __search_index_fns.remove(index, entity_list[place]) + __search_index_fns.insert(index, entity_list[place], component_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_assign(index, entity_list, component_list, first, last) - local target_fragment = index.__target_fragment - local extract_component = index.__extract_component - local entity_extracts = index.__entity_extracts - +function __search_index_fns.multi_remove(index, entity_list, first, last) for place = first, last do - local entity = entity_list[place] - local component = component_list[place] - - local new_extracted = component - - if extract_component then - new_extracted = extract_component(new_extracted) - end - - local old_extracted = entity_extracts[entity % 1048576] - - if new_extracted == old_extracted then - -- nothing to reassign - elseif target_fragment - and __lua_type(new_extracted) == 'table' - and __lua_type(old_extracted) == 'table' - and __list_fns.eq(new_extracted, old_extracted) - then - -- the list has the same targets, nothing to reassign - else - __search_index_fns.remove(index, entity) - __search_index_fns.insert_extracted(index, entity, new_extracted) - end + __search_index_fns.remove(index, entity_list[place]) end end @@ -8311,12 +8275,6 @@ function __builder_mt:duplicate(duplicate) return self:set(__DUPLICATE, duplicate) end ----@param extract evolved.extract ----@return evolved.builder builder -function __builder_mt:extract(extract) - return self:set(__EXTRACT, extract) -end - ---@param realloc evolved.realloc ---@return evolved.builder builder function __builder_mt:realloc(realloc) @@ -8556,9 +8514,6 @@ __evolved_set(__DEFAULT, __ON_REMOVE, __chunk_fns.update_major_chunks) __evolved_set(__DUPLICATE, __ON_SET, __chunk_fns.update_major_chunks) __evolved_set(__DUPLICATE, __ON_REMOVE, __chunk_fns.update_major_chunks) -__evolved_set(__EXTRACT, __ON_SET, __chunk_fns.update_search_index) -__evolved_set(__EXTRACT, __ON_REMOVE, __chunk_fns.update_search_index) - __evolved_set(__REALLOC, __ON_SET, __chunk_fns.update_major_chunks) __evolved_set(__REALLOC, __ON_REMOVE, __chunk_fns.update_major_chunks) @@ -8776,7 +8731,6 @@ __evolved_set(__INTERNAL, __NAME, '__INTERNAL') __evolved_set(__DEFAULT, __NAME, '__DEFAULT') __evolved_set(__DUPLICATE, __NAME, '__DUPLICATE') -__evolved_set(__EXTRACT, __NAME, '__EXTRACT') __evolved_set(__REALLOC, __NAME, '__REALLOC') __evolved_set(__COMPMOVE, __NAME, '__COMPMOVE') @@ -8827,7 +8781,6 @@ __evolved_set(__INTERNAL, __INTERNAL) __evolved_set(__DEFAULT, __INTERNAL) __evolved_set(__DUPLICATE, __INTERNAL) -__evolved_set(__EXTRACT, __INTERNAL) __evolved_set(__REALLOC, __INTERNAL) __evolved_set(__COMPMOVE, __INTERNAL) @@ -8878,7 +8831,6 @@ evolved.INTERNAL = __INTERNAL evolved.DEFAULT = __DEFAULT evolved.DUPLICATE = __DUPLICATE -evolved.EXTRACT = __EXTRACT evolved.REALLOC = __REALLOC evolved.COMPMOVE = __COMPMOVE From 3c76150c67330af09abd98f48656a380a776ed87 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 27 Oct 2026 02:33:07 +0700 Subject: [PATCH 40/48] code style fixes --- evolved.lua | 150 ++++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/evolved.lua b/evolved.lua index 288cc9f..de9522a 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1564,6 +1564,60 @@ function __search_index_fns.unlink(target, fragment) end end +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.assign(index, entity, component) + local target_fragment = index.__target_fragment + local entity_components = index.__entity_components + + local new_component = component + local old_component = entity_components[entity % 1048576] + + if new_component == old_component then + -- nothing to reassign + elseif target_fragment + and __lua_type(new_component) == 'table' + and __lua_type(old_component) == 'table' + and __list_fns.eq(new_component, old_component) + then + -- the list has the same targets, nothing to reassign + else + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, new_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 target_fragment = index.__target_fragment + local entity_components = index.__entity_components + + for place = first, last do + local entity = entity_list[place] + + local new_component = component_list[place] + local old_component = entity_components[entity % 1048576] + + if new_component == old_component then + -- nothing to reassign + elseif target_fragment + and __lua_type(new_component) == 'table' + and __lua_type(old_component) == 'table' + and __list_fns.eq(new_component, old_component) + then + -- the list has the same targets, nothing to reassign + else + __search_index_fns.remove(index, entity) + __search_index_fns.insert(index, entity, new_component) + end + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -1592,6 +1646,17 @@ function __search_index_fns.insert(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_insert(index, entity_list, component_list, first, last) + for place = first, last do + __search_index_fns.insert(index, entity_list[place], component_list[place]) + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -1666,6 +1731,16 @@ 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) + for place = first, last do + __search_index_fns.remove(index, entity_list[place]) + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -1719,81 +1794,6 @@ function __search_index_fns.remove_node(index, entity, component) next_entities[entity_key] = nil end ----@param index evolved.search_index ----@param entity evolved.entity ----@param component evolved.component -function __search_index_fns.assign(index, entity, component) - local target_fragment = index.__target_fragment - local entity_components = index.__entity_components - - local new_component = component - local old_component = entity_components[entity % 1048576] - - if new_component == old_component then - -- nothing to reassign - elseif target_fragment - and __lua_type(new_component) == 'table' - and __lua_type(old_component) == 'table' - and __list_fns.eq(new_component, old_component) - then - -- the list has the same targets, nothing to reassign - else - __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, new_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 target_fragment = index.__target_fragment - local entity_components = index.__entity_components - - for place = first, last do - local entity = entity_list[place] - - local new_component = component_list[place] - local old_component = entity_components[entity % 1048576] - - if new_component == old_component then - -- nothing to reassign - elseif target_fragment - and __lua_type(new_component) == 'table' - and __lua_type(old_component) == 'table' - and __list_fns.eq(new_component, old_component) - then - -- the list has the same targets, nothing to reassign - else - __search_index_fns.remove(index, entity) - __search_index_fns.insert(index, entity, new_component) - end - 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_insert(index, entity_list, component_list, first, last) - for place = first, last do - __search_index_fns.insert(index, entity_list[place], component_list[place]) - 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) - for place = first, last do - __search_index_fns.remove(index, entity_list[place]) - end -end - --- --- --- From d9eb15654b814108445b8e3bedc4f74b160d146c Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 27 Oct 2026 03:47:04 +0700 Subject: [PATCH 41/48] proof of concept unlink_target policy --- README.md | 86 +++--- develop/fuzzing/link_fuzz.lua | 125 ++++++++- develop/testing/link_tests.lua | 468 +++++++++++++++++++++++++++++++++ evolved.d.tl | 1 + evolved.lua | 165 ++++++++++-- 5 files changed, 771 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 2a2392a..678bd31 100644 --- a/README.md +++ b/README.md @@ -1233,38 +1233,11 @@ evolved.reindex(team) assert(evolved.search(team, 'blue') == unit) ``` -Sometimes the component itself is not a good key. For example, a component can be a table, and you want to find entities by one of its fields, or names should be found regardless of their case. In such cases, use the [`evolved.EXTRACT`](#evolvedextract) fragment trait. This trait expects a function that takes a component and returns a value to be indexed instead of it. Components themselves are stored as is, only the index uses the extracted values. If the function returns `nil`, the entity is not indexed at all. - -```lua -local evolved = require 'evolved' - -local unit_info = evolved.builder() - :index() - :extract(function(info) return info.team end) - :build() - -local unit1 = evolved.builder() - :set(unit_info, { team = 'red', health = 100 }) - :build() - -local unit2 = evolved.builder() - :set(unit_info, { team = 'blue', health = 50 }) - :build() - -assert(evolved.search(unit_info, 'red') == unit1) -assert(evolved.search(unit_info, 'blue') == unit2) -``` - -The function is called every time a component of the fragment is inserted or assigned, including reindexing, and the index remembers the extracted values. Removals and destructions use these remembered values, so the function is not called for them. The trait can be added, changed, or removed at any time, and the index is rebuilt each time. - -> [!NOTE] -> The extracted value must depend only on the component itself. The index is not updated when other fragments of the entity change, so a function that reads them will leave the index outdated. For the same reason, the function should not call any library functions. Tables changed in place bypass the index too, so set them again or use the [`evolved.reindex`](#evolvedreindex) function after such changes. - > [!NOTE] > Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) or [`evolved.LINK`](#evolvedlink) trait finds nothing and prints a warning. The [`evolved.reindex`](#evolvedreindex) function prints a warning for such fragments too and skips them. > [!NOTE] -> Components, or values extracted from them, are used as keys in the index, so table keys are indexed by reference, and searching by another table with the same content will not find anything. Also, [Fragment Tags](#fragment-tags) are not indexed at all, and neither are `nil` and `NaN` keys. +> 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 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. @@ -1436,6 +1409,8 @@ assert(evolved.alive(sword) and not evolved.has(sword, owner)) You can change this behavior by using the [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment trait. This trait expects one of the following predefined identifiers: +- [`evolved.LINK_POLICY_UNLINK_TARGET`](#evolvedlink_policy_unlink_target) will remove only the destroyed entity from lists of entities that link to it, see the example with children below. Single links are removed together with the linked fragment, as with the default policy. + - [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) will destroy any entity that links to the destroyed entity. Links of destroyed entities are resolved too, so this is useful for hierarchies, where all descendants should be destroyed together with their root. - [`evolved.LINK_POLICY_REMOVE_FRAGMENT`](#evolvedlink_policy_remove_fragment) will remove the linked fragment from all entities that link to the destroyed entity. This is the default behavior, so you don't have to set it explicitly, but you can if you want. @@ -1520,30 +1495,46 @@ evolved.destroy(pillar1) assert(evolved.alive(rope) and not evolved.has(rope, attached_to)) ``` -The [`evolved.EXTRACT`](#evolvedextract) fragment trait works with linked fragments too, so components can store links among other data. In this case, the function should return an entity or a list of entities to link to, or `nil` if the component does not link to anything. Link policies are still applied to whole components, so with the default policy, the whole component is removed, not only the link inside it. +Removing the whole list is not always what you want. For example, a parent can store the list of its children, and destroying one child should not take the other children away from it. In such cases, use the [`evolved.LINK_POLICY_UNLINK_TARGET`](#evolvedlink_policy_unlink_target) policy. With this policy, the destroyed entity is removed from the list, and the other entities stay there in the same order. If the list becomes empty, the linked fragment is removed, as if it were a single link. ```lua local evolved = require 'evolved' -local equipment = evolved.builder() +local parent = evolved.builder() :link() - :extract(function(item) return item.owner end) + :link_policy(evolved.LINK_POLICY_DESTROY_ENTITY) :build() -local player = evolved.builder() +local children = evolved.builder() + :link() + :link_policy(evolved.LINK_POLICY_UNLINK_TARGET) :build() -local sword = evolved.builder() - :set(equipment, { owner = player, slot = 'hand' }) +local root = evolved.builder() :build() -assert(evolved.search(equipment, player) == sword) +local child1 = evolved.builder() + :set(parent, root) + :build() --- destroy the player that the sword links to -evolved.destroy(player) +local child2 = evolved.builder() + :set(parent, root) + :build() --- the whole equipment component is removed, not only the owner field -assert(evolved.alive(sword) and not evolved.has(sword, equipment)) +evolved.set(root, children, { child1, child2 }) + +-- destroy one of the children of the root +evolved.destroy(child1) + +-- the root is still alive and still has the other child +local child_list = evolved.get(root, children) +assert(#child_list == 1 and child_list[1] == child2) + +-- destroy the last child of the root +evolved.destroy(child2) + +-- the empty list is removed together with the children fragment +assert(evolved.alive(root) and not evolved.has(root, children)) ``` The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are linked this way with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy. So you can also find all queries that use some fragment, or all fragments that require it. @@ -1814,7 +1805,6 @@ component_mapper :: {chunk, integer, integer} default :: component duplicate :: {component -> component} -extract :: {component -> component?} realloc :: {storage?, integer, integer -> storage?} compmove :: {storage, integer, integer, integer, storage} @@ -1849,7 +1839,6 @@ INTERNAL :: fragment DEFAULT :: fragment DUPLICATE :: fragment -EXTRACT :: fragment REALLOC :: fragment COMPMOVE :: fragment @@ -1876,6 +1865,7 @@ PROLOGUE :: fragment EPILOGUE :: fragment LINK_POLICY :: fragment +LINK_POLICY_UNLINK_TARGET :: id LINK_POLICY_DESTROY_ENTITY :: id LINK_POLICY_REMOVE_FRAGMENT :: id @@ -2015,7 +2005,6 @@ builder_mt:internal :: builder builder_mt:default :: component -> builder builder_mt:duplicate :: {component -> component} -> builder -builder_mt:extract :: {component -> component?} -> builder builder_mt:realloc :: {storage?, integer, integer -> storage?} -> builder builder_mt:compmove :: {storage, integer, integer, integer, storage} -> builder @@ -2051,7 +2040,6 @@ builder_mt:destruction_policy :: id -> builder - Added the new [`LINK`](#evolvedlink) and [`LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section - The [`GROUP`](#evolvedgroup), [`QUERY`](#evolvedquery), [`INCLUDES`](#evolvedincludes), [`EXCLUDES`](#evolvedexcludes), [`VARIANTS`](#evolvedvariants), and [`REQUIRES`](#evolvedrequires) fragments are now linked -- Added the new [`EXTRACT`](#evolvedextract) fragment trait that allows indexing and linking by values extracted from components - Query iteration order no longer depends on the order in which chunks were created - The [`NAME`](#evolvedname) fragment is now marked as [UNIQUE](#unique-fragments), so clones no longer inherit the names of their prefabs - The [`search`](#evolvedsearch) and [`reindex`](#evolvedreindex) functions now print a warning instead of throwing an error for non-indexed fragments @@ -2166,8 +2154,6 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.DUPLICATE` -### `evolved.EXTRACT` - ### `evolved.REALLOC` ### `evolved.COMPMOVE` @@ -2204,6 +2190,8 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.LINK_POLICY` +### `evolved.LINK_POLICY_UNLINK_TARGET` + ### `evolved.LINK_POLICY_DESTROY_ENTITY` ### `evolved.LINK_POLICY_REMOVE_FRAGMENT` @@ -3005,14 +2993,6 @@ function evolved.builder_mt:default(default) end function evolved.builder_mt:duplicate(duplicate) end ``` -#### `evolved.builder_mt:extract` - -```lua ----@param extract evolved.extract ----@return evolved.builder builder -function evolved.builder_mt:extract(extract) end -``` - #### `evolved.builder_mt:realloc` ```lua diff --git a/develop/fuzzing/link_fuzz.lua b/develop/fuzzing/link_fuzz.lua index 6c61e93..24ad7e0 100644 --- a/develop/fuzzing/link_fuzz.lua +++ b/develop/fuzzing/link_fuzz.lua @@ -25,6 +25,9 @@ local destroying_query = evo.builder():include(destroying_mark):build() local remove_hook_calls = {} ---@type table> local hooked_link_fragment_set = {} ---@type table +local assign_hook_calls = {} ---@type table> +local assign_hooked_link_fragment_set = {} ---@type table + ---@param entity evolved.entity ---@param fragment evolved.fragment local function remove_hook(entity, fragment) @@ -40,6 +43,21 @@ local function remove_hook(entity, fragment) entity_calls[fragment] = (entity_calls[fragment] or 0) + 1 end +---@param entity evolved.entity +---@param fragment evolved.fragment +local function assign_hook(entity, fragment) + assert(evo.alive(entity) and evo.alive(fragment)) + + local entity_calls = assign_hook_calls[entity] + + if not entity_calls then + entity_calls = {} + assign_hook_calls[entity] = entity_calls + end + + entity_calls[fragment] = (entity_calls[fragment] or 0) + 1 +end + local all_link_fragment_list = {} ---@type evolved.fragment[] for i = 1, math.random(1, 4) do @@ -49,11 +67,17 @@ for i = 1, math.random(1, 4) do builder:on_remove(remove_hook) end - local r = math.random(1, 3) + if math.random(1, 2) == 1 then + builder:on_assign(assign_hook) + end + + local r = math.random(1, 4) if r == 1 then - builder:link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + builder:link_policy(evo.LINK_POLICY_UNLINK_TARGET) elseif r == 2 then + builder:link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + elseif r == 3 then builder:link_policy(evo.LINK_POLICY_REMOVE_FRAGMENT) end @@ -66,6 +90,10 @@ for i = 1, math.random(1, 4) do if evo.has(all_link_fragment_list[i], evo.ON_REMOVE) then hooked_link_fragment_set[all_link_fragment_list[i]] = true end + + if evo.has(all_link_fragment_list[i], evo.ON_ASSIGN) then + assign_hooked_link_fragment_set[all_link_fragment_list[i]] = true + end end local all_entity_list = {} ---@type evolved.entity[] @@ -204,9 +232,11 @@ repeat until not has_new_destroyed_entities local entity_link_target_lists = {} ---@type table> +local entity_link_components = {} ---@type table> for _, entity in ipairs(all_entity_list) do local entity_link_target_list = {} + local entity_link_component_list = {} for _, link_fragment in ipairs(all_link_fragment_list) do local link_component = evo.get(entity, link_fragment) @@ -219,13 +249,16 @@ for _, entity in ipairs(all_entity_list) do end entity_link_target_list[link_fragment] = link_target_list + entity_link_component_list[link_fragment] = link_component end end entity_link_target_lists[entity] = entity_link_target_list + entity_link_components[entity] = entity_link_component_list end remove_hook_calls = {} +assign_hook_calls = {} --- --- @@ -233,8 +266,10 @@ remove_hook_calls = {} --- --- +local destroying_mode = math.random(1, 6) + do - local r = math.random(1, 6) + local r = destroying_mode if r == 1 then evo.destroy(__table_unpack(destroying_entity_list)) @@ -349,6 +384,49 @@ for _, link_fragment in ipairs(all_link_fragment_list) do end end +---@param entity evolved.entity +---@param link_fragment evolved.fragment +---@return evolved.entity[] alive_target_list, boolean is_unlinked_list +local function expected_link_targets(entity, link_fragment) + local link_target_list = entity_link_target_lists[entity][link_fragment] + local link_component = entity_link_components[entity][link_fragment] + + local alive_target_list = {} + + for _, link_target in ipairs(link_target_list) do + if not should_be_destroyed_entity_set[link_target] then + alive_target_list[#alive_target_list + 1] = link_target + end + end + + local is_unlinked_list = type(link_component) == 'table' + and evo.get(link_fragment, evo.LINK_POLICY) == evo.LINK_POLICY_UNLINK_TARGET + and #alive_target_list < #link_target_list + + return alive_target_list, is_unlinked_list +end + +for _, entity in ipairs(all_entity_list) do + if evo.alive(entity) then + for link_fragment, link_target_list in pairs(entity_link_target_lists[entity]) do + local alive_target_list, is_unlinked_list = expected_link_targets(entity, link_fragment) + + if #alive_target_list == #link_target_list then + assert(evo.get(entity, link_fragment) == entity_link_components[entity][link_fragment]) + elseif is_unlinked_list and #alive_target_list > 0 then + local link_component = evo.get(entity, link_fragment) + assert(type(link_component) == 'table' and #link_component == #alive_target_list) + + for i, link_target in ipairs(alive_target_list) do + assert(link_component[i] == link_target) + end + else + assert(not evo.has(entity, link_fragment)) + end + end + end +end + for _, entity in ipairs(all_entity_list) do local entity_calls = remove_hook_calls[entity] or {} @@ -356,10 +434,12 @@ for _, entity in ipairs(all_entity_list) do local expected_call_count = 0 if hooked_link_fragment_set[link_fragment] then + local alive_target_list, is_unlinked_list = expected_link_targets(entity, link_fragment) + local is_link_removed = should_be_destroyed_entity_set[entity] - for _, link_target in ipairs(link_target_list) do - if should_be_destroyed_entity_set[link_target] then + if #alive_target_list < #link_target_list then + if not is_unlinked_list or #alive_target_list == 0 then is_link_removed = true end end @@ -381,6 +461,41 @@ for entity in pairs(remove_hook_calls) do assert(entity_link_target_lists[entity]) end +for _, entity in ipairs(all_entity_list) do + local entity_calls = assign_hook_calls[entity] or {} + + for link_fragment in pairs(entity_link_target_lists[entity]) do + if assign_hooked_link_fragment_set[link_fragment] then + local alive_target_list, is_unlinked_list = expected_link_targets(entity, link_fragment) + + local call_count = entity_calls[link_fragment] or 0 + + local is_link_assigned = is_unlinked_list + and #alive_target_list > 0 + and not should_be_destroyed_entity_set[entity] + + if destroying_mode == 3 or destroying_mode == 4 then + -- each target is destroyed separately, so lists can be assigned several times + if is_link_assigned then + assert(call_count >= 1) + elseif not is_unlinked_list then + assert(call_count == 0) + end + else + assert(call_count == (is_link_assigned and 1 or 0)) + end + end + end + + for link_fragment in pairs(entity_calls) do + assert(entity_link_target_lists[entity][link_fragment]) + end +end + +for entity in pairs(assign_hook_calls) do + assert(entity_link_target_lists[entity]) +end + --- --- --- diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua index aa5f7c8..997a2f9 100644 --- a/develop/testing/link_tests.lua +++ b/develop/testing/link_tests.lua @@ -1425,3 +1425,471 @@ do evo.debug_mode(true) end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + assert(evo.get(f, evo.LINK_POLICY) == evo.LINK_POLICY_UNLINK_TARGET) + + local t1, t2, t3 = evo.id(3) + + local l = { t1, t2, t3 } + local e = evo.builder():set(f, l):build() + + evo.destroy(t2) + assert(evo.alive(e)) + + do + local nl = evo.get(e, f) + assert(nl ~= l and #nl == 2 and nl[1] == t1 and nl[2] == t3) + assert(#l == 3 and l[1] == t1 and l[2] == t2 and l[3] == t3) + end + + assert(evo.search(f, t1) == e) + assert(evo.search(f, t2) == nil) + assert(evo.search(f, t3) == e) + + evo.destroy(t1) + + do + local nl = evo.get(e, f) + assert(#nl == 1 and nl[1] == t3) + end + + evo.destroy(t3) + assert(evo.alive(e) and not evo.has(e, f)) + assert(evo.search(f, t3) == nil) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local g = evo.id() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f, t1):set(g, 42):build() + local e2 = evo.builder():set(f, { t1, t2 }):build() + local e3 = evo.builder():set(f, { t1 }):build() + local e4 = evo.builder():set(f, {}):build() + + evo.destroy(t1) + assert(evo.alive_all(e1, e2, e3, e4)) + + assert(not evo.has(e1, f) and evo.get(e1, g) == 42) + assert(not evo.has(e3, f)) + + do + local l2 = evo.get(e2, f) + assert(#l2 == 1 and l2[1] == t2) + + local l4 = evo.get(e4, f) + assert(#l4 == 0) + end + + assert(evo.search(f, t1) == nil) + assert(evo.search(f, t2) == e2) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2, t3, t4 = evo.id(4) + local e = evo.builder():set(f, { t1, t2, t3, t4 }):build() + + evo.destroy(t1, t3) + + do + local l = evo.get(e, f) + assert(#l == 2 and l[1] == t2 and l[2] == t4) + end + + evo.destroy(t2, t4) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, { t1, t2, t1, t2, t1 }):build() + + evo.destroy(t1) + + do + local l = evo.get(e, f) + assert(#l == 2 and l[1] == t2 and l[2] == t2) + end + + assert(evo.search(f, t2) == e) + + evo.destroy(t2) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2 = evo.id(2) + + local l = { t1, t2 } + local e1 = evo.builder():set(f, l):build() + local e2 = evo.builder():set(f, l):build() + + evo.destroy(t1) + + do + local l1, l2 = evo.get(e1, f), evo.get(e2, f) + assert(l1 ~= l and #l1 == 1 and l1[1] == t2) + assert(l2 ~= l and #l2 == 1 and l2[1] == t2) + assert(#l == 2 and l[1] == t1 and l[2] == t2) + end + + do + local rl, rc = evo.multi_search(f, t2) + assert(rc == 2 and rl[1] == e1 and rl[2] == e2) + end +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2 = evo.id(2) + local e1 = evo.builder():set(f, { t1, t2 }):build() + local e2 = evo.builder():set(f, { t1, t2 }):build() + + evo.destroy(t1, e1) + assert(not evo.alive_any(t1, e1)) + assert(evo.alive(e2)) + + do + local l = evo.get(e2, f) + assert(#l == 1 and l[1] == t2) + end + + assert(evo.search(f, t2) == e2) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2 = evo.id(2) + local e = evo.builder():set(f, { t1, t2 }):build() + + evo.destroy(f, t1) + assert(not evo.alive_any(f, t1)) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local g = evo.builder():build() + + local set_count, assign_count, remove_count = 0, 0, 0 + + local f = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_UNLINK_TARGET) + :on_set(function(e, _, nl, ol) + assert(evo.alive(e)) + if ol then + assert(#ol == 3 and #nl == 1) + for _, t in ipairs(ol) do assert(evo.alive(t) and evo.get(t, g) == 42) end + set_count = set_count + 1 + end + end) + :on_assign(function(_, _, nl, ol) + assert(#ol == 3 and #nl == 1) + assign_count = assign_count + 1 + end) + :on_remove(function(_, _, ol) + assert(#ol == 1) + remove_count = remove_count + 1 + end) + :build() + + local t1 = evo.builder():set(g, 42):build() + local t2 = evo.builder():set(g, 42):build() + local t3 = evo.builder():set(g, 42):build() + + local e = evo.builder():set(f, { t1, t2, t3 }):build() + + evo.destroy(t1, t3) + assert(set_count == 1 and assign_count == 1 and remove_count == 0) + + evo.destroy(t2) + assert(set_count == 1 and assign_count == 1 and remove_count == 1) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2, t3 = evo.id(3) + local e = evo.builder():set(f, { t1, t2, t3 }):build() + + evo.defer() + do + evo.destroy(t1) + evo.destroy(t3) + + local l = evo.get(e, f) + assert(#l == 3) + end + evo.commit() + + do + local l = evo.get(e, f) + assert(#l == 1 and l[1] == t2) + end +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2, t3 = evo.id(3) + local e = evo.builder():set(f, { t1, t2 }):build() + + evo.defer() + do + evo.destroy(t1) + evo.set(e, f, { t2, t3 }) + end + evo.commit() + + do + local l = evo.get(e, f) + assert(#l == 2 and l[1] == t2 and l[2] == t3) + end +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local g = evo.builder():tag():build() + + local t1 = evo.builder():set(g):build() + local t2 = evo.builder():build() + local t3 = evo.builder():set(g):build() + + local e = evo.builder():set(f, { t1, t2, t3 }):build() + + local q = evo.builder():include(g):build() + + evo.batch_destroy(q) + assert(not evo.alive_any(t1, t3)) + + do + local l = evo.get(e, f) + assert(#l == 1 and l[1] == t2) + end +end + +do + local f = evo.builder():link():build() + + local t1, t2, t3 = evo.id(3) + local e = evo.builder():set(f, { t1, t2, t3 }):build() + + evo.set(f, evo.LINK_POLICY, evo.LINK_POLICY_UNLINK_TARGET) + + evo.destroy(t2) + + do + local l = evo.get(e, f) + assert(#l == 2 and l[1] == t1 and l[2] == t3) + end + + evo.remove(f, evo.LINK_POLICY) + + evo.destroy(t3) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local parent = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_DESTROY_ENTITY) + :build() + + local children = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_UNLINK_TARGET) + :build() + + local root = evo.id() + + local c1 = evo.builder():set(parent, root):build() + local c2 = evo.builder():set(parent, root):build() + local c3 = evo.builder():set(parent, root):build() + + evo.set(root, children, { c1, c2, c3 }) + + local gc1 = evo.builder():set(parent, c2):build() + local gc2 = evo.builder():set(parent, c2):build() + + evo.set(c2, children, { gc1, gc2 }) + + evo.destroy(c2) + assert(not evo.alive_any(c2, gc1, gc2)) + assert(evo.alive_all(root, c1, c3)) + + do + local l = evo.get(root, children) + assert(#l == 2 and l[1] == c1 and l[2] == c3) + end + + evo.destroy(c1, c3) + assert(evo.alive(root) and not evo.has(root, children)) + + local c4 = evo.builder():set(parent, root):build() + evo.set(root, children, { c4 }) + + evo.destroy(root) + assert(not evo.alive_any(root, c4)) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local e1, e2, e3 = evo.id(3) + evo.set(e1, f, { e2, e3 }) + evo.set(e2, f, { e1, e3 }) + evo.set(e3, f, { e1, e2 }) + + evo.destroy(e2) + assert(evo.alive_all(e1, e3)) + + do + local l1, l3 = evo.get(e1, f), evo.get(e3, f) + assert(#l1 == 1 and l1[1] == e3) + assert(#l3 == 1 and l3[1] == e1) + end + + evo.destroy(e1) + assert(evo.alive(e3) and not evo.has(e3, f)) +end + +do + local removed_list = {} + local assigned_list = {} + + local f1 = evo.builder() + :link() + :on_remove(function(e) removed_list[#removed_list + 1] = e end) + :build() + + local f2 = evo.builder() + :link() + :link_policy(evo.LINK_POLICY_UNLINK_TARGET) + :on_assign(function(e) assigned_list[#assigned_list + 1] = e end) + :build() + + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f1, t1):set(f2, { t1, t2 }):build() + local e2 = evo.builder():set(f1, t1):set(f2, { t1, t2 }):build() + local e3 = evo.builder():set(f1, t1):set(f2, { t1, t2 }):build() + + evo.destroy(t1) + + assert(#removed_list == 3) + assert(removed_list[1] == e1 and removed_list[2] == e2 and removed_list[3] == e3) + + assert(#assigned_list == 3) + assert(assigned_list[1] == e1 and assigned_list[2] == e2 and assigned_list[3] == e3) + + do + local l, c = evo.multi_search(f2, t2) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t1, t2, t3 = evo.id(3) + + local e1 = evo.builder():set(f, { t1, t2, t3 }):build() + local e2 = evo.builder():set(f, { t2, t3 }):build() + local e3 = evo.builder():set(f, { t3, t1, t2 }):build() + local e4 = evo.builder():set(f, { t2 }):build() + + evo.destroy(t1) + + do + local l1, l3 = evo.get(e1, f), evo.get(e3, f) + assert(#l1 == 2 and l1[1] == t2 and l1[2] == t3) + assert(#l3 == 2 and l3[1] == t3 and l3[2] == t2) + end + + do + local l, c = evo.multi_search(f, t2) + assert(c == 4 and l[1] == e1 and l[2] == e2 and l[3] == e3 and l[4] == e4) + end + + do + local l, c = evo.multi_search(f, t3) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) + end + + evo.destroy(t3) + + do + local l, c = evo.multi_search(f, t2) + assert(c == 4 and l[1] == e1 and l[2] == e2 and l[3] == e3 and l[4] == e4) + end +end + +do + local f1 = evo.builder():link():build() + local f2 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local f3 = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + do + local t1, t2 = evo.id(2) + + local e1 = evo.builder():set(f1, t1):build() + local e2 = evo.builder():set(f2, t1):build() + local e3 = evo.builder():set(f3, { t1, t2 }):build() + local e4 = evo.builder():set(f2, t1):build() + + evo.remove(e2, f2) + + evo.destroy(t1) + assert(evo.alive_all(e1, e2, e3) and not evo.alive(e4)) + assert(not evo.has(e1, f1) and not evo.has(e2, f2)) + + do + local l = evo.get(e3, f3) + assert(#l == 1 and l[1] == t2) + end + end + + do + local t = evo.id() + + local e1 = evo.builder():set(f1, t):build() + local e2 = evo.builder():set(f2, t):build() + + evo.remove(e1, f1) + evo.remove(e2, f2) + + local e3 = evo.builder():set(f2, t):build() + local e4 = evo.builder():set(f1, t):build() + + evo.remove(e3, f2) + + evo.destroy(t) + assert(evo.alive_all(e1, e2, e3, e4)) + assert(not evo.has(e4, f1)) + end + + do + local t = evo.id() + + local e1 = evo.builder():set(f1, t):build() + evo.remove(e1, f1) + + local e2 = evo.builder():set(f2, t):build() + + evo.destroy(t) + assert(evo.alive(e1) and not evo.alive(e2)) + end +end diff --git a/evolved.d.tl b/evolved.d.tl index f34d169..62d0364 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -180,6 +180,7 @@ EPILOGUE: Fragment LINK_POLICY: Fragment + LINK_POLICY_UNLINK_TARGET: Id LINK_POLICY_DESTROY_ENTITY: Id LINK_POLICY_REMOVE_FRAGMENT: Id diff --git a/evolved.lua b/evolved.lua index de9522a..2700e32 100644 --- a/evolved.lua +++ b/evolved.lua @@ -637,7 +637,7 @@ local __entity_chunks = {} ---@type (evolved.chunk|false)[] local __entity_places = {} ---@type integer[] local __search_indices = {} ---@type table -local __target_fragments = {} ---@type table> +local __target_fragments = {} ---@type table> local __sorted_includes = {} ---@type table> local __sorted_excludes = {} ---@type table> @@ -1314,6 +1314,7 @@ local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() local __LINK_POLICY = __acquire_id() +local __LINK_POLICY_UNLINK_TARGET = __acquire_id() local __LINK_POLICY_DESTROY_ENTITY = __acquire_id() local __LINK_POLICY_REMOVE_FRAGMENT = __acquire_id() @@ -1440,6 +1441,7 @@ local __shrink_chunk local __clear_entity_one local __destroy_entity_one local __destroy_entity_stack +local __unlink_entity_targets local __remove_entity_fragments local __chunk_assign @@ -1542,12 +1544,12 @@ function __search_index_fns.link(target, fragment) local target_fragments = __target_fragments[target] if not target_fragments then - ---@type evolved.assoc_list - target_fragments = __assoc_list_fns.new(1) - __target_fragments[target] = target_fragments + __target_fragments[target] = fragment + elseif __lua_type(target_fragments) == 'table' then + __assoc_list_fns.insert(target_fragments, fragment) + elseif target_fragments ~= fragment then + __target_fragments[target] = __assoc_list_fns.from(target_fragments, fragment) end - - __assoc_list_fns.insert(target_fragments, fragment) end ---@param target evolved.entity @@ -1559,11 +1561,43 @@ function __search_index_fns.unlink(target, fragment) return end - if __assoc_list_fns.unordered_remove(target_fragments, fragment) == 0 then + if __lua_type(target_fragments) == 'table' then + if __assoc_list_fns.remove(target_fragments, fragment) == 0 then + __target_fragments[target] = nil + end + elseif target_fragments == fragment then __target_fragments[target] = nil end end +---@param index evolved.search_index +---@param entity evolved.entity +---@param remove table +function __search_index_fns.filter(index, entity, remove) + local entity_primary = entity % 1048576 + + ---@type evolved.entity[] + local target_list = index.__entity_components[entity_primary] + local target_count = #target_list + + local kept_count = 0 + + for target_index = 1, target_count do + local target = target_list[target_index] + + if remove[target] then + __search_index_fns.remove_node(index, entity, target) + else + kept_count = kept_count + 1 + target_list[kept_count] = target + end + end + + for target_index = kept_count + 1, target_count do + target_list[target_index] = nil + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component @@ -4454,11 +4488,18 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent if entity_target_fragments then target_fragments[entity] = nil - local target_fragment_list = entity_target_fragments.__item_list - local target_fragment_count = entity_target_fragments.__item_count + ---@type evolved.fragment[]?, integer + local target_fragment_list, target_fragment_count = nil, 1 + + if __lua_type(entity_target_fragments) == 'table' then + target_fragment_list = entity_target_fragments.__item_list + target_fragment_count = entity_target_fragments.__item_count + end for target_fragment_index = 1, target_fragment_count do - local target_fragment = target_fragment_list[target_fragment_index] + local target_fragment = target_fragment_list + and target_fragment_list[target_fragment_index] + or entity_target_fragments local target_fragment_search_index = search_indices[target_fragment] if target_fragment_search_index @@ -4472,7 +4513,9 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent entity_stack_size = entity_stack_size + __evolved_multi_search_to( entity_stack, entity_stack_size + 1, target_fragment, entity) else - if link_policy ~= __LINK_POLICY_REMOVE_FRAGMENT then + if link_policy ~= __LINK_POLICY_UNLINK_TARGET + and link_policy ~= __LINK_POLICY_REMOVE_FRAGMENT + then __debug_fns.warning_fmt('unknown LINK_POLICY (%s) on (%s), %s is used instead', __id_name(link_policy), __id_name(target_fragment), __id_name(__LINK_POLICY_REMOVE_FRAGMENT)) @@ -4506,16 +4549,20 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent local fragment = removing_fragment_list[removing_index] if target then + local link_policy = search_indices[fragment].__link_policy local referrer_count = __evolved_multi_search_to(entity_stack, 1, fragment, target) - while referrer_count > 0 do - local referrer = entity_stack[referrer_count] + for referrer_index = 1, referrer_count do + local referrer = entity_stack[referrer_index] - entity_stack[referrer_count] = nil - referrer_count = referrer_count - 1 + entity_stack[referrer_index] = nil if not processed_entity_set[referrer] then - __remove_entity_fragments(referrer, fragment) + if link_policy == __LINK_POLICY_UNLINK_TARGET then + __unlink_entity_targets(referrer, fragment, processed_entity_set) + else + __remove_entity_fragments(referrer, fragment) + end end end else @@ -4614,6 +4661,89 @@ function __destroy_entity_stack(chunk_stack, chunk_stack_size, entity_stack, ent __release_table(__table_pool_tag.fragment_set, processed_entity_set, true, false) end +---@param entity evolved.entity +---@param fragment evolved.fragment +---@param destroying_entity_set table +function __unlink_entity_targets(entity, fragment, destroying_entity_set) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') + end + + local entity_primary = entity % 1048576 + + if __freelist_ids[entity_primary] ~= entity then + -- nothing to unlink from non-alive entities + return + end + + local chunk = __entity_chunks[entity_primary] + local place = __entity_places[entity_primary] + + if not chunk then + return + end + + local component_indices = chunk.__component_indices + local component_index = component_indices[fragment] + + if not component_index then + return + end + + local component_storages = chunk.__component_storages + local component_storage = component_storages[component_index] + + ---@type evolved.entity[] + local old_target_list = component_storage[place] + local old_target_count = __lua_type(old_target_list) == 'table' and #old_target_list or 0 + + if old_target_count == 0 then + __remove_entity_fragments(entity, fragment) + return + end + + ---@type evolved.entity[] + local new_target_list = __lua_table_new(old_target_count) + local new_target_count = 0 + + for old_target_index = 1, old_target_count do + local old_target = old_target_list[old_target_index] + + if not destroying_entity_set[old_target] then + new_target_count = new_target_count + 1 + new_target_list[new_target_count] = old_target + end + end + + if new_target_count == 0 then + __remove_entity_fragments(entity, fragment) + return + end + + component_storage[place] = new_target_list + + local fragment_search_index = chunk.__has_index_fragments + and __search_indices[fragment] + or nil + + if fragment_search_index then + __search_index_fns.filter(fragment_search_index, entity, destroying_entity_set) + end + + if chunk.__has_assign_hooks then + local fragment_set = chunk.__component_sets[component_index] + local fragment_assign = chunk.__component_assigns[component_index] + + if fragment_set then + fragment_set(entity, fragment, new_target_list, old_target_list) + end + + if fragment_assign then + fragment_assign(entity, fragment, new_target_list, old_target_list) + end + end +end + ---@param entity evolved.entity ---@param ... evolved.fragment fragments function __remove_entity_fragments(entity, ...) @@ -8757,6 +8887,7 @@ __evolved_set(__PROLOGUE, __NAME, '__PROLOGUE') __evolved_set(__EPILOGUE, __NAME, '__EPILOGUE') __evolved_set(__LINK_POLICY, __NAME, '__LINK_POLICY') +__evolved_set(__LINK_POLICY_UNLINK_TARGET, __NAME, '__LINK_POLICY_UNLINK_TARGET') __evolved_set(__LINK_POLICY_DESTROY_ENTITY, __NAME, '__LINK_POLICY_DESTROY_ENTITY') __evolved_set(__LINK_POLICY_REMOVE_FRAGMENT, __NAME, '__LINK_POLICY_REMOVE_FRAGMENT') @@ -8807,6 +8938,7 @@ __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) __evolved_set(__LINK_POLICY, __INTERNAL) +__evolved_set(__LINK_POLICY_UNLINK_TARGET, __INTERNAL) __evolved_set(__LINK_POLICY_DESTROY_ENTITY, __INTERNAL) __evolved_set(__LINK_POLICY_REMOVE_FRAGMENT, __INTERNAL) @@ -8857,6 +8989,7 @@ evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE evolved.LINK_POLICY = __LINK_POLICY +evolved.LINK_POLICY_UNLINK_TARGET = __LINK_POLICY_UNLINK_TARGET evolved.LINK_POLICY_DESTROY_ENTITY = __LINK_POLICY_DESTROY_ENTITY evolved.LINK_POLICY_REMOVE_FRAGMENT = __LINK_POLICY_REMOVE_FRAGMENT From a6ae64afd71976c93799489338073329fa143628 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 27 Oct 2026 06:59:13 +0700 Subject: [PATCH 42/48] multi link optimizations --- develop/testing/link_tests.lua | 122 +++++++++++++++++++++++++++++++-- evolved.lua | 116 +++++++++++++++++++++++-------- 2 files changed, 206 insertions(+), 32 deletions(-) diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua index 997a2f9..5c0da96 100644 --- a/develop/testing/link_tests.lua +++ b/develop/testing/link_tests.lua @@ -1006,14 +1006,14 @@ do do local l, c = evo.multi_search(f, t) - assert(c == 3 and l[1] == e2 and l[2] == e3 and l[3] == e1) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) end evo.set(e2, f, { t }) do local l, c = evo.multi_search(f, t) - assert(c == 3 and l[1] == e3 and l[2] == e1 and l[3] == e2) + assert(c == 3 and l[1] == e1 and l[2] == e3 and l[3] == e2) end end @@ -1034,7 +1034,7 @@ do do local l, c = evo.multi_search(f, t2) - assert(c == 2 and l[1] == e2 and l[2] == e1) + assert(c == 2 and l[1] == e1 and l[2] == e2) end assert(evo.search(f, t3) == e1) @@ -1065,7 +1065,7 @@ do do local l, c = evo.multi_search(f, t1) - assert(c == 3 and l[1] == e1 and l[2] == e3 and l[3] == e2) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) end assert(evo.search(f, t2) == e2) @@ -1893,3 +1893,117 @@ do assert(evo.alive(e1) and not evo.alive(e2)) end end + +do + local f = evo.builder():link():build() + + local t1, t2, t3, t4, t5, t6, t7 = evo.id(7) + local targets = { t1, t2, t3, t4, t5, t6, t7 } + + local e1 = evo.builder():set(f, { t1, t2, t3 }):build() + local e2 = evo.builder():set(f, { t1, t2, t3 }):build() + + local function has_target(e, t) + local l = evo.get(e, f) + for i = 1, #l do + if l[i] == t then + return true + end + end + return false + end + + local function check() + for _, t in ipairs(targets) do + local expected_count = 0 + if has_target(e1, t) then expected_count = expected_count + 1 end + if has_target(e2, t) then expected_count = expected_count + 1 end + + local l, c = evo.multi_search(f, t) + assert(c == expected_count) + + for i = 1, c do + assert(has_target(l[i], t)) + end + + if c == 2 then + assert(l[1] ~= l[2]) + end + end + end + + check() + + evo.set(e1, f, { t1, t2, t3, t4 }) + check() + assert(evo.search(f, t1) == e1 and evo.search(f, t3) == e1) + + evo.set(e1, f, { t5, t1, t2, t3, t4 }) + check() + assert(evo.search(f, t1) == e1 and evo.search(f, t3) == e1) + + evo.set(e1, f, { t5, t1, t3, t4 }) + check() + assert(evo.search(f, t1) == e1 and evo.search(f, t2) == e2) + + evo.set(e1, f, { t5, t1, t3, t4, t1 }) + check() + assert(evo.search(f, t1) == e1) + + evo.set(e1, f, { t5, t3, t4, t1 }) + check() + assert(evo.search(f, t1) == e1) + + evo.set(e1, f, { t4, t3 }) + check() + assert(evo.search(f, t1) == e2 and evo.search(f, t3) == e1) + + evo.set(e1, f, { t1, t2, t3, t4, t5, t6, t7 }) + check() + assert(evo.search(f, t3) == e1) + + evo.set(e1, f, { t7, t6, t5, t4, t3, t2, t1 }) + check() + assert(evo.search(f, t1) == e2 and evo.search(f, t3) == e2) + + evo.set(e1, f, { t6, t6, t6, t6, t6, t6 }) + check() + assert(evo.search(f, t6) == e1) + + evo.set(e1, f, { t1, t2, t3, t4, t5, t6, t1, t2, t3, t4, t5, t6 }) + check() + + evo.set(e1, f, { t7 }) + check() + + evo.set(e1, f, { t1, t2, t3, t4, t5, t1 }) + check() + + evo.set(e1, f, { t1, t6, t7, t5, t4, t3 }) + check() + + evo.set(e1, f, { t1, t2, t3, t4, t6, t5, t6 }) + check() + + evo.set(e1, f, { t5, t4, t3, t2, t7, t6 }) + check() + + do + local q = evo.builder():include(f):build() + + evo.batch_set(q, f, { t2, t3, t4 }) + check() + + evo.batch_set(q, f, { t2, t4, t5, t6, t7, t1 }) + check() + end + + do + local l = evo.get(e1, f) + l[#l + 1] = t3 + l[1] = t1 + + evo.reindex(f) + check() + end +end diff --git a/evolved.lua b/evolved.lua index 2700e32..22ef71b 100644 --- a/evolved.lua +++ b/evolved.lua @@ -918,30 +918,6 @@ function __list_fns.dup(list, size) return dup_list, list_size end ----@generic V ----@param list_a V[] ----@param list_b V[] ----@param size_a? integer ----@param size_b? integer ----@return boolean ----@nodiscard -function __list_fns.eq(list_a, list_b, size_a, size_b) - size_a = size_a or #list_a - size_b = size_b or #list_b - - if size_a ~= size_b then - return false - end - - for list_index = 1, size_a do - if list_a[list_index] ~= list_b[list_index] then - return false - end - end - - return true -end - ---@generic V ---@param list_a V[] ---@param list_b V[] @@ -1613,9 +1589,8 @@ function __search_index_fns.assign(index, entity, component) elseif target_fragment and __lua_type(new_component) == 'table' and __lua_type(old_component) == 'table' - and __list_fns.eq(new_component, old_component) then - -- the list has the same targets, nothing to reassign + __search_index_fns.assign_targets(index, entity, new_component) else __search_index_fns.remove(index, entity) __search_index_fns.insert(index, entity, new_component) @@ -1642,9 +1617,8 @@ function __search_index_fns.multi_assign(index, entity_list, component_list, fir elseif target_fragment and __lua_type(new_component) == 'table' and __lua_type(old_component) == 'table' - and __list_fns.eq(new_component, old_component) then - -- the list has the same targets, nothing to reassign + __search_index_fns.assign_targets(index, entity, new_component) else __search_index_fns.remove(index, entity) __search_index_fns.insert(index, entity, new_component) @@ -1652,6 +1626,92 @@ function __search_index_fns.multi_assign(index, entity_list, component_list, fir end end +---@param index evolved.search_index +---@param entity evolved.entity +---@param new_target_list evolved.entity[] +function __search_index_fns.assign_targets(index, entity, new_target_list) + local entity_primary = entity % 1048576 + + ---@type evolved.entity[] + local old_target_list = index.__entity_components[entity_primary] + + local old_target_count = #old_target_list + local new_target_count = #new_target_list + + local min_target_count = old_target_count < new_target_count + and old_target_count + or new_target_count + + local prefix_count = 0 + + while prefix_count < min_target_count + and old_target_list[prefix_count + 1] == new_target_list[prefix_count + 1] + do + prefix_count = prefix_count + 1 + end + + if prefix_count == old_target_count and prefix_count == new_target_count then + return + end + + local suffix_count = 0 + + while prefix_count + suffix_count < min_target_count + and old_target_list[old_target_count - suffix_count] == new_target_list[new_target_count - suffix_count] + do + suffix_count = suffix_count + 1 + end + + local old_middle_last = old_target_count - suffix_count + local new_middle_last = new_target_count - suffix_count + + if old_middle_last - prefix_count <= 4 then + for old_target_index = prefix_count + 1, old_middle_last do + local old_target = old_target_list[old_target_index] + + local new_target_index = 1 + + while new_target_index <= new_target_count + and new_target_list[new_target_index] ~= old_target + do + new_target_index = new_target_index + 1 + end + + if new_target_index > new_target_count then + __search_index_fns.remove_node(index, entity, old_target) + end + end + + for new_target_index = prefix_count + 1, new_middle_last do + __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) + end + else + for old_target_index = prefix_count + 1, old_middle_last do + __search_index_fns.remove_node(index, entity, old_target_list[old_target_index]) + end + + for new_target_index = prefix_count + 1, new_middle_last do + __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) + end + + for new_target_index = 1, prefix_count do + __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) + end + + for new_target_index = new_middle_last + 1, new_target_count do + __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) + end + end + + __lua_table_move( + new_target_list, prefix_count + 1, new_target_count, + prefix_count + 1, old_target_list) + + for old_target_index = new_target_count + 1, old_target_count do + old_target_list[old_target_index] = nil + end +end + ---@param index evolved.search_index ---@param entity evolved.entity ---@param component evolved.component From 2c91560612486999e757e8b6af1a6d56ea0e8568 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 27 Oct 2026 23:14:13 +0700 Subject: [PATCH 43/48] index policy ids --- README.md | 25 +++++++++++++++++++++++++ evolved.d.tl | 7 +++++++ evolved.lua | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/README.md b/README.md index 678bd31..e41104a 100644 --- a/README.md +++ b/README.md @@ -1864,6 +1864,12 @@ EXECUTE :: fragment PROLOGUE :: fragment EPILOGUE :: fragment +INDEX_POLICY :: fragment +INDEX_POLICY_SELF :: id +INDEX_POLICY_LIST :: id +INDEX_POLICY_KEYS :: id +INDEX_POLICY_VALUES :: id + LINK_POLICY :: fragment LINK_POLICY_UNLINK_TARGET :: id LINK_POLICY_DESTROY_ENTITY :: id @@ -2030,6 +2036,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:link_policy :: id -> builder builder_mt:destruction_policy :: id -> builder ``` @@ -2188,6 +2195,16 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EPILOGUE` +### `evolved.INDEX_POLICY` + +### `evolved.INDEX_POLICY_SELF` + +### `evolved.INDEX_POLICY_LIST` + +### `evolved.INDEX_POLICY_KEYS` + +### `evolved.INDEX_POLICY_VALUES` + ### `evolved.LINK_POLICY` ### `evolved.LINK_POLICY_UNLINK_TARGET` @@ -3127,6 +3144,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:link_policy` ```lua diff --git a/evolved.d.tl b/evolved.d.tl index 62d0364..b8743f7 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -140,6 +140,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 link_policy: function(self: Builder, link_policy: Id): Builder destruction_policy: function(self: Builder, destruction_policy: Id): Builder end @@ -179,6 +180,12 @@ PROLOGUE: Fragment EPILOGUE: Fragment + INDEX_POLICY: Fragment + INDEX_POLICY_SELF: Id + INDEX_POLICY_LIST: Id + INDEX_POLICY_KEYS: Id + INDEX_POLICY_VALUES: Id + LINK_POLICY: Fragment LINK_POLICY_UNLINK_TARGET: Id LINK_POLICY_DESTROY_ENTITY: Id diff --git a/evolved.lua b/evolved.lua index 22ef71b..cf73b53 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1289,6 +1289,12 @@ local __EXECUTE = __acquire_id() local __PROLOGUE = __acquire_id() local __EPILOGUE = __acquire_id() +local __INDEX_POLICY = __acquire_id() +local __INDEX_POLICY_SELF = __acquire_id() +local __INDEX_POLICY_LIST = __acquire_id() +local __INDEX_POLICY_KEYS = __acquire_id() +local __INDEX_POLICY_VALUES = __acquire_id() + local __LINK_POLICY = __acquire_id() local __LINK_POLICY_UNLINK_TARGET = __acquire_id() local __LINK_POLICY_DESTROY_ENTITY = __acquire_id() @@ -8641,6 +8647,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 link_policy evolved.id ---@return evolved.builder builder function __builder_mt:link_policy(link_policy) @@ -8686,6 +8698,9 @@ __evolved_set(__INDEX, __ON_REMOVE, __chunk_fns.update_search_index) __evolved_set(__LINK, __ON_INSERT, __chunk_fns.update_search_index) __evolved_set(__LINK, __ON_REMOVE, __chunk_fns.update_search_index) +__evolved_set(__INDEX_POLICY, __ON_SET, __chunk_fns.update_search_index) +__evolved_set(__INDEX_POLICY, __ON_REMOVE, __chunk_fns.update_search_index) + __evolved_set(__LINK_POLICY, __ON_SET, __chunk_fns.update_search_index) __evolved_set(__LINK_POLICY, __ON_REMOVE, __chunk_fns.update_search_index) @@ -8746,21 +8761,25 @@ __evolved_set(__DISABLED, __EXPLICIT) __evolved_set(__INCLUDES, __DEFAULT, __list_fns.new()) __evolved_set(__INCLUDES, __DUPLICATE, __list_fns.dup) __evolved_set(__INCLUDES, __LINK) +__evolved_set(__INCLUDES, __INDEX_POLICY, __INDEX_POLICY_LIST) __evolved_set(__INCLUDES, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__EXCLUDES, __DEFAULT, __list_fns.new()) __evolved_set(__EXCLUDES, __DUPLICATE, __list_fns.dup) __evolved_set(__EXCLUDES, __LINK) +__evolved_set(__EXCLUDES, __INDEX_POLICY, __INDEX_POLICY_LIST) __evolved_set(__EXCLUDES, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__VARIANTS, __DEFAULT, __list_fns.new()) __evolved_set(__VARIANTS, __DUPLICATE, __list_fns.dup) __evolved_set(__VARIANTS, __LINK) +__evolved_set(__VARIANTS, __INDEX_POLICY, __INDEX_POLICY_LIST) __evolved_set(__VARIANTS, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__REQUIRES, __DEFAULT, __list_fns.new()) __evolved_set(__REQUIRES, __DUPLICATE, __list_fns.dup) __evolved_set(__REQUIRES, __LINK) +__evolved_set(__REQUIRES, __INDEX_POLICY, __INDEX_POLICY_LIST) __evolved_set(__REQUIRES, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) __evolved_set(__ON_SET, __UNIQUE) @@ -8946,6 +8965,12 @@ __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_SELF, __NAME, '__INDEX_POLICY_SELF') +__evolved_set(__INDEX_POLICY_LIST, __NAME, '__INDEX_POLICY_LIST') +__evolved_set(__INDEX_POLICY_KEYS, __NAME, '__INDEX_POLICY_KEYS') +__evolved_set(__INDEX_POLICY_VALUES, __NAME, '__INDEX_POLICY_VALUES') + __evolved_set(__LINK_POLICY, __NAME, '__LINK_POLICY') __evolved_set(__LINK_POLICY_UNLINK_TARGET, __NAME, '__LINK_POLICY_UNLINK_TARGET') __evolved_set(__LINK_POLICY_DESTROY_ENTITY, __NAME, '__LINK_POLICY_DESTROY_ENTITY') @@ -8997,6 +9022,12 @@ __evolved_set(__EXECUTE, __INTERNAL) __evolved_set(__PROLOGUE, __INTERNAL) __evolved_set(__EPILOGUE, __INTERNAL) +__evolved_set(__INDEX_POLICY, __INTERNAL) +__evolved_set(__INDEX_POLICY_SELF, __INTERNAL) +__evolved_set(__INDEX_POLICY_LIST, __INTERNAL) +__evolved_set(__INDEX_POLICY_KEYS, __INTERNAL) +__evolved_set(__INDEX_POLICY_VALUES, __INTERNAL) + __evolved_set(__LINK_POLICY, __INTERNAL) __evolved_set(__LINK_POLICY_UNLINK_TARGET, __INTERNAL) __evolved_set(__LINK_POLICY_DESTROY_ENTITY, __INTERNAL) @@ -9048,6 +9079,12 @@ evolved.EXECUTE = __EXECUTE evolved.PROLOGUE = __PROLOGUE evolved.EPILOGUE = __EPILOGUE +evolved.INDEX_POLICY = __INDEX_POLICY +evolved.INDEX_POLICY_SELF = __INDEX_POLICY_SELF +evolved.INDEX_POLICY_LIST = __INDEX_POLICY_LIST +evolved.INDEX_POLICY_KEYS = __INDEX_POLICY_KEYS +evolved.INDEX_POLICY_VALUES = __INDEX_POLICY_VALUES + evolved.LINK_POLICY = __LINK_POLICY evolved.LINK_POLICY_UNLINK_TARGET = __LINK_POLICY_UNLINK_TARGET evolved.LINK_POLICY_DESTROY_ENTITY = __LINK_POLICY_DESTROY_ENTITY From dd8e3c752aa7ab602483bd5eac26f352eda2e8e4 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 28 Oct 2026 21:38:27 +0700 Subject: [PATCH 44/48] indexing optimization and tests --- develop/all.lua | 3 + develop/fuzzing/index_fuzz.lua | 255 +++++++++++ develop/fuzzing/link_fuzz.lua | 180 ++++++-- develop/testing/link_tests.lua | 176 +++++--- develop/testing/multi_index_tests.lua | 625 ++++++++++++++++++++++++++ evolved.lua | 570 ++++++++++++++++------- 6 files changed, 1563 insertions(+), 246 deletions(-) create mode 100644 develop/fuzzing/index_fuzz.lua create mode 100644 develop/testing/multi_index_tests.lua diff --git a/develop/all.lua b/develop/all.lua index a9a5daa..6b5e2b8 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -15,6 +15,7 @@ require 'develop.testing.locate_tests' require 'develop.testing.lookup_tests' require 'develop.testing.main_tests' require 'develop.testing.mappers_tests' +require 'develop.testing.multi_index_tests' require 'develop.testing.multi_spawn_tests' require 'develop.testing.name_tests' require 'develop.testing.process_with_tests' @@ -43,6 +44,8 @@ basics.describe_fuzz 'develop.fuzzing.batch_destroy_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.explicit_fuzz' print '----------------------------------------' +basics.describe_fuzz 'develop.fuzzing.index_fuzz' +print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.link_fuzz' print '----------------------------------------' basics.describe_fuzz 'develop.fuzzing.pack_unpack_fuzz' diff --git a/develop/fuzzing/index_fuzz.lua b/develop/fuzzing/index_fuzz.lua new file mode 100644 index 0000000..0efc0c6 --- /dev/null +++ b/develop/fuzzing/index_fuzz.lua @@ -0,0 +1,255 @@ +local evo = require 'evolved' + +evo.debug_mode(true) + +--- +--- +--- +--- +--- + +local __table_unpack = (function() + ---@diagnostic disable-next-line: deprecated + return table.unpack or unpack +end)() + +--- +--- +--- +--- +--- + +local all_value_list = { + 1, 2, 1048577, 2 ^ 40 + 1, 0.5, -1, + 'a', 'b', 'c', true, false, {}, {}, +} + +local all_map_key_list = { 1, 2, 3, 'x', 'y' } + +local all_index_policy_list = { + evo.INDEX_POLICY_SELF, + evo.INDEX_POLICY_LIST, + evo.INDEX_POLICY_KEYS, + evo.INDEX_POLICY_VALUES, +} + +local function random_value() + return all_value_list[math.random(1, #all_value_list)] +end + +local function random_map_key() + return all_map_key_list[math.random(1, #all_map_key_list)] +end + +local all_index_fragment_list = {} ---@type evolved.fragment[] + +for i = 1, math.random(1, 3) do + local builder = evo.builder():index() + + local index_policy = all_index_policy_list[math.random(1, #all_index_policy_list)] + + if index_policy ~= evo.INDEX_POLICY_SELF or math.random(1, 2) == 1 then + builder:index_policy(index_policy) + end + + all_index_fragment_list[i] = builder:build() +end + +local all_entity_list = {} ---@type evolved.entity[] + +for i = 1, math.random(1, 20) do + all_entity_list[i] = evo.id() +end + +--- +--- +--- +--- +--- + +local function random_entity() + return all_entity_list[math.random(1, #all_entity_list)] +end + +local function random_index_fragment() + return all_index_fragment_list[math.random(1, #all_index_fragment_list)] +end + +---@param fragment evolved.fragment +---@return evolved.id +local function index_policy_of(fragment) + return evo.get(fragment, evo.INDEX_POLICY) or evo.INDEX_POLICY_SELF +end + +---@param fragment evolved.fragment +---@return evolved.component +local function random_component(fragment) + local index_policy = index_policy_of(fragment) + + if index_policy == evo.INDEX_POLICY_SELF then + return math.random(1, 10) == 1 and 0 / 0 or random_value() + end + + local component = {} + + for i = 1, math.random(0, 8) do + if index_policy == evo.INDEX_POLICY_LIST then + component[i] = math.random(1, 10) == 1 and 0 / 0 or random_value() + elseif index_policy == evo.INDEX_POLICY_KEYS then + component[random_value()] = math.random(1, 2) == 1 + else + component[random_map_key()] = math.random(1, 10) == 1 and 0 / 0 or random_value() + end + end + + return component +end + +---@param fragment evolved.fragment +---@param component table +local function mutate_component(fragment, component) + local index_policy = index_policy_of(fragment) + + if index_policy == evo.INDEX_POLICY_LIST then + if math.random(1, 2) == 1 then + component[#component + 1] = random_value() + else + component[#component] = nil + end + elseif index_policy == evo.INDEX_POLICY_KEYS then + component[random_value()] = math.random(1, 2) == 1 or nil + else + component[random_map_key()] = math.random(1, 2) == 1 and random_value() or nil + end +end + +---@param fragment evolved.fragment +---@param component evolved.component +---@param value evolved.component +---@return boolean +local function component_has_value(fragment, component, value) + local index_policy = index_policy_of(fragment) + + if index_policy == evo.INDEX_POLICY_SELF then + return component == value + end + + if index_policy == evo.INDEX_POLICY_KEYS then + return component[value] ~= nil + end + + for _, component_value in pairs(component) do + if component_value == value then + return true + end + end + + return false +end + +--- +--- +--- +--- +--- + +for _, entity in ipairs(all_entity_list) do + for _, fragment in ipairs(all_index_fragment_list) do + if math.random(1, 2) == 1 then + evo.set(entity, fragment, random_component(fragment)) + end + end +end + +for _ = 1, math.random(0, 4 * #all_entity_list) do + local entity = random_entity() + local fragment = random_index_fragment() + + local r = math.random(1, 8) + + if not evo.alive(entity) and r ~= 6 and r ~= 7 then + -- nothing to do with destroyed entities + elseif r <= 3 then + evo.set(entity, fragment, random_component(fragment)) + elseif r == 4 then + evo.remove(entity, fragment) + elseif r == 5 then + local component = evo.get(entity, fragment) + + if component ~= nil and index_policy_of(fragment) ~= evo.INDEX_POLICY_SELF then + mutate_component(fragment, component) + evo.reindex(fragment) + end + elseif r == 6 then + if math.random(1, 2) == 1 then + evo.set(fragment, evo.INDEX_POLICY, index_policy_of(fragment)) + else + evo.remove(fragment, evo.INDEX) + evo.set(fragment, evo.INDEX) + end + elseif r == 7 then + local q = evo.builder():include(fragment):build() + evo.batch_set(q, fragment, random_component(fragment)) + evo.destroy(q) + else + evo.destroy(entity) + end +end + +--- +--- +--- +--- +--- + +for _, fragment in ipairs(all_index_fragment_list) do + for _, value in ipairs(all_value_list) do + local expected_set = {} ---@type table + local expected_count = 0 + + for _, entity in ipairs(all_entity_list) do + if evo.alive(entity) and evo.has(entity, fragment) + and component_has_value(fragment, evo.get(entity, fragment), value) + then + expected_set[entity] = true + expected_count = expected_count + 1 + end + end + + local entity_list, entity_count = evo.multi_search(fragment, value) + assert(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, value) == entity_list[1]) + end + + assert(evo.search(fragment, 0 / 0) == nil) +end + +--- +--- +--- +--- +--- + +if math.random(1, 2) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) +end + +evo.destroy(__table_unpack(all_entity_list)) + +for _, fragment in ipairs(all_index_fragment_list) do + for _, value in ipairs(all_value_list) do + assert(evo.search(fragment, value) == nil) + end +end + +evo.destroy(__table_unpack(all_index_fragment_list)) + +if math.random(1, 2) == 1 then + evo.collect_garbage(math.random(1, 2) == 1) +end diff --git a/develop/fuzzing/link_fuzz.lua b/develop/fuzzing/link_fuzz.lua index 24ad7e0..24ec762 100644 --- a/develop/fuzzing/link_fuzz.lua +++ b/develop/fuzzing/link_fuzz.lua @@ -85,6 +85,18 @@ for i = 1, math.random(1, 4) do builder:index() end + r = math.random(1, 5) + + if r == 1 then + builder:index_policy(evo.INDEX_POLICY_SELF) + elseif r == 2 then + builder:index_policy(evo.INDEX_POLICY_LIST) + elseif r == 3 then + builder:index_policy(evo.INDEX_POLICY_KEYS) + elseif r == 4 then + builder:index_policy(evo.INDEX_POLICY_VALUES) + end + all_link_fragment_list[i] = builder:build() if evo.has(all_link_fragment_list[i], evo.ON_REMOVE) then @@ -113,35 +125,113 @@ local function random_entity() return all_entity_list[math.random(1, #all_entity_list)] end ----@return evolved.entity | evolved.entity[] -local function random_link_component() - if math.random(1, 2) == 1 then +local all_map_key_list = { 1, 2, 3, 'a', 'b', 'c' } + +local function random_map_key() + return all_map_key_list[math.random(1, #all_map_key_list)] +end + +---@param link_fragment evolved.fragment +---@return evolved.id +local function link_index_policy(link_fragment) + return evo.get(link_fragment, evo.INDEX_POLICY) or evo.INDEX_POLICY_SELF +end + +---@param link_fragment evolved.fragment +---@return evolved.entity | table +local function random_link_component(link_fragment) + local index_policy = link_index_policy(link_fragment) + + if index_policy == evo.INDEX_POLICY_SELF then return random_entity() end + local link_component = {} + + for i = 1, math.random(0, 8) do + if index_policy == evo.INDEX_POLICY_LIST then + link_component[i] = random_entity() + elseif index_policy == evo.INDEX_POLICY_KEYS then + link_component[random_entity()] = true + else + link_component[random_map_key()] = random_entity() + end + end + + return link_component +end + +---@param link_fragment evolved.fragment +---@param link_component table +local function mutate_link_component(link_fragment, link_component) + local index_policy = link_index_policy(link_fragment) + + if index_policy == evo.INDEX_POLICY_LIST then + if math.random(1, 2) == 1 then + link_component[#link_component + 1] = random_entity() + else + link_component[#link_component] = nil + end + elseif index_policy == evo.INDEX_POLICY_KEYS then + if math.random(1, 2) == 1 then + link_component[random_entity()] = true + else + link_component[random_entity()] = nil + end + else + if math.random(1, 2) == 1 then + link_component[random_map_key()] = random_entity() + else + link_component[random_map_key()] = nil + end + end +end + +---@param link_fragment evolved.fragment +---@param link_component evolved.entity | table +---@return evolved.entity[] +local function link_component_targets(link_fragment, link_component) + local index_policy = link_index_policy(link_fragment) + + if index_policy == evo.INDEX_POLICY_SELF then + return { link_component } + end + + if index_policy == evo.INDEX_POLICY_LIST then + return link_component + end + local link_target_list = {} - for i = 1, math.random(0, 3) do - link_target_list[i] = random_entity() + for key, value in pairs(link_component) do + link_target_list[#link_target_list + 1] = + index_policy == evo.INDEX_POLICY_KEYS and key or value end return link_target_list end ----@param link_component evolved.entity | evolved.entity[] ----@return evolved.entity[] -local function link_component_targets(link_component) - if type(link_component) == 'table' then +---@param link_fragment evolved.fragment +---@param link_component evolved.entity | table +---@return evolved.entity | table +local function copy_link_component(link_fragment, link_component) + if link_index_policy(link_fragment) == evo.INDEX_POLICY_SELF then return link_component end - return { link_component } + local link_component_copy = {} + + for key, value in pairs(link_component) do + link_component_copy[key] = value + end + + return link_component_copy end for _, entity in ipairs(all_entity_list) do for _, link_fragment in ipairs(all_link_fragment_list) do if math.random(1, 2) == 1 then - evo.set(entity, link_fragment, random_link_component()) + evo.set(entity, link_fragment, random_link_component(link_fragment)) end end end @@ -153,22 +243,23 @@ for _ = 1, math.random(0, #all_entity_list) do local r = math.random(1, 4) if r == 1 then - evo.set(entity, link_fragment, random_link_component()) + evo.set(entity, link_fragment, random_link_component(link_fragment)) elseif r == 2 then evo.remove(entity, link_fragment) elseif r == 3 then - evo.remove(link_fragment, evo.LINK) - evo.set(link_fragment, evo.LINK) + local index_policy = evo.get(link_fragment, evo.INDEX_POLICY) + + if index_policy and math.random(1, 2) == 1 then + evo.set(link_fragment, evo.INDEX_POLICY, index_policy) + else + evo.remove(link_fragment, evo.LINK) + evo.set(link_fragment, evo.LINK) + end else local link_component = evo.get(entity, link_fragment) - if type(link_component) == 'table' then - if math.random(1, 2) == 1 then - link_component[#link_component + 1] = random_entity() - else - link_component[#link_component] = nil - end - + if link_component and link_index_policy(link_fragment) ~= evo.INDEX_POLICY_SELF then + mutate_link_component(link_fragment, link_component) evo.reindex(link_fragment) end end @@ -213,7 +304,7 @@ repeat local link_component = evo.get(entity, link_fragment) if link_component then - for _, link_target in ipairs(link_component_targets(link_component)) do + for _, link_target in ipairs(link_component_targets(link_fragment, link_component)) do if should_be_destroyed_entity_set[link_target] then should_be_destroyed_entity_set[entity] = true has_new_destroyed_entities = true @@ -232,11 +323,13 @@ repeat until not has_new_destroyed_entities local entity_link_target_lists = {} ---@type table> -local entity_link_components = {} ---@type table> +local entity_link_components = {} ---@type table> +local entity_link_component_copies = {} ---@type table> for _, entity in ipairs(all_entity_list) do local entity_link_target_list = {} local entity_link_component_list = {} + local entity_link_component_copy_list = {} for _, link_fragment in ipairs(all_link_fragment_list) do local link_component = evo.get(entity, link_fragment) @@ -244,17 +337,19 @@ for _, entity in ipairs(all_entity_list) do if link_component then local link_target_list = {} - for i, link_target in ipairs(link_component_targets(link_component)) do + for i, link_target in ipairs(link_component_targets(link_fragment, link_component)) do link_target_list[i] = link_target end entity_link_target_list[link_fragment] = link_target_list entity_link_component_list[link_fragment] = link_component + entity_link_component_copy_list[link_fragment] = copy_link_component(link_fragment, link_component) end end entity_link_target_lists[entity] = entity_link_target_list entity_link_components[entity] = entity_link_component_list + entity_link_component_copies[entity] = entity_link_component_copy_list end remove_hook_calls = {} @@ -318,7 +413,7 @@ for _, entity in ipairs(all_entity_list) do local link_component = evo.get(entity, link_fragment) if link_component then - for _, link_target in ipairs(link_component_targets(link_component)) do + for _, link_target in ipairs(link_component_targets(link_fragment, link_component)) do assert(evo.alive(link_target)) end end @@ -340,7 +435,7 @@ for _, link_fragment in ipairs(all_link_fragment_list) do local link_component = evo.get(entity, link_fragment) if link_component then - for _, link_target in ipairs(link_component_targets(link_component)) do + for _, link_target in ipairs(link_component_targets(link_fragment, link_component)) do local referrer_list, referrer_count = evo.multi_search(link_fragment, link_target) local referrer_found_count = 0 @@ -370,7 +465,7 @@ for _, link_fragment in ipairs(all_link_fragment_list) do local is_target_found = false if referrer_component then - for _, referrer_target in ipairs(link_component_targets(referrer_component)) do + for _, referrer_target in ipairs(link_component_targets(link_fragment, referrer_component)) do if referrer_target == link_target then is_target_found = true break @@ -389,7 +484,6 @@ end ---@return evolved.entity[] alive_target_list, boolean is_unlinked_list local function expected_link_targets(entity, link_fragment) local link_target_list = entity_link_target_lists[entity][link_fragment] - local link_component = entity_link_components[entity][link_fragment] local alive_target_list = {} @@ -399,7 +493,7 @@ local function expected_link_targets(entity, link_fragment) end end - local is_unlinked_list = type(link_component) == 'table' + local is_unlinked_list = link_index_policy(link_fragment) ~= evo.INDEX_POLICY_SELF and evo.get(link_fragment, evo.LINK_POLICY) == evo.LINK_POLICY_UNLINK_TARGET and #alive_target_list < #link_target_list @@ -415,10 +509,32 @@ for _, entity in ipairs(all_entity_list) do assert(evo.get(entity, link_fragment) == entity_link_components[entity][link_fragment]) elseif is_unlinked_list and #alive_target_list > 0 then local link_component = evo.get(entity, link_fragment) - assert(type(link_component) == 'table' and #link_component == #alive_target_list) + assert(type(link_component) == 'table') + + local index_policy = link_index_policy(link_fragment) - for i, link_target in ipairs(alive_target_list) do - assert(link_component[i] == link_target) + if index_policy == evo.INDEX_POLICY_LIST then + assert(#link_component == #alive_target_list) + + for i, link_target in ipairs(alive_target_list) do + assert(link_component[i] == link_target) + end + else + local old_link_component = entity_link_component_copies[entity][link_fragment] + + for key, value in pairs(old_link_component) do + local link_target = index_policy == evo.INDEX_POLICY_KEYS and key or value + + if should_be_destroyed_entity_set[link_target] then + assert(link_component[key] == nil) + else + assert(link_component[key] == value) + end + end + + for key in pairs(link_component) do + assert(old_link_component[key] ~= nil) + end end else assert(not evo.has(entity, link_fragment)) diff --git a/develop/testing/link_tests.lua b/develop/testing/link_tests.lua index 5c0da96..80c83bb 100644 --- a/develop/testing/link_tests.lua +++ b/develop/testing/link_tests.lua @@ -708,7 +708,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local g = evo.id() local t1, t2, t3 = evo.id(3) @@ -716,7 +716,7 @@ do local e1 = evo.builder():set(f, { t1, t2 }):set(g, 42):build() local e2 = evo.builder():set(f, { t2, t3 }):build() local e3 = evo.builder():set(f, { t3 }):build() - local e4 = evo.builder():set(f, t2):build() + local e4 = evo.builder():set(f, { t2 }):build() local e5 = evo.builder():set(f, {}):build() evo.destroy(t2) @@ -740,14 +740,14 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() local t1, t2 = evo.id(2) local e1 = evo.builder():set(f, { t1, t2 }):build() local e2 = evo.builder():set(f, { t2 }):build() local e3 = evo.builder():set(f, { t2, e1 }):build() - local e4 = evo.builder():set(f, e3):build() + local e4 = evo.builder():set(f, { e3 }):build() local e5 = evo.builder():set(f, {}):build() evo.destroy(t1) @@ -760,7 +760,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() local e1, e2, e3 = evo.id(3) evo.set(e1, f, { e2 }) @@ -772,7 +772,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2 = evo.id(2) @@ -807,12 +807,12 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t = evo.id() - local e1 = evo.builder():set(f, t):build() - local e2 = evo.builder():set(f, { t }):build() + local e1 = evo.builder():set(f, { t }):build() + local e2 = evo.builder():set(f, { t, t }):build() do local l, c = evo.multi_search(f, t) @@ -826,20 +826,20 @@ do assert(evo.search(f, t) == nil) evo.set(e1, f, { t }) - evo.set(e2, f, t) + evo.set(e2, f, { t }) evo.destroy(t) assert(evo.alive_all(e1, e2) and not evo.has_any(e1, f) and not evo.has_any(e2, f)) end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2 = evo.id(2) - local e1 = evo.builder():set(f, t1):build() - local e2 = evo.builder():set(f, t2):build() - local e3 = evo.builder():set(f, t1):build() + local e1 = evo.builder():set(f, { t1 }):build() + local e2 = evo.builder():set(f, { t2 }):build() + local e3 = evo.builder():set(f, { t1 }):build() local e4 = evo.builder():set(f, { t2, t1 }):build() @@ -854,7 +854,7 @@ do end evo.remove(e1, f) - evo.set(e2, f, t1) + evo.set(e2, f, { t1 }) do local l, c = evo.multi_search(f, t1) @@ -868,7 +868,7 @@ do evo.destroy(t2) assert(evo.alive_all(e2, e3, e4) and not evo.has(e4, f)) - assert(evo.get(e2, f) == t1 and evo.get(e3, f) == t1) + assert(evo.get(e2, f)[1] == t1 and evo.get(e3, f)[1] == t1) do local l, c = evo.multi_search(f, t1) @@ -880,7 +880,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t = evo.id() local e = evo.builder():set(f, { t, t, t }):build() @@ -901,11 +901,11 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t = evo.id() - local e1 = evo.builder():set(f, t):build() + local e1 = evo.builder():set(f, { t }):build() local e2 = evo.builder():set(f, { t, t }):build() local e3 = evo.builder():set(f, { t }):build() @@ -942,7 +942,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2, t3 = evo.id(3) local e = evo.builder():set(f, { t1, t2 }):build() @@ -960,33 +960,33 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2 = evo.id(2) - local e = evo.builder():set(f, t1):build() + local e = evo.builder():set(f, { t1 }):build() evo.set(e, f, { t2 }) assert(evo.search(f, t1) == nil) assert(evo.search(f, t2) == e) - evo.set(e, f, t1) + evo.set(e, f, { t1 }) assert(evo.search(f, t1) == e) assert(evo.search(f, t2) == nil) evo.destroy(t2) - assert(evo.get(e, f) == t1) + assert(evo.get(e, f)[1] == t1) evo.destroy(t1) assert(evo.alive(e) and not evo.has(e, f)) end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t = evo.id() local e1 = evo.builder():set(f, { t }):build() - local e2 = evo.builder():set(f, t):build() + local e2 = evo.builder():set(f, { t, t, t }):build() local e3 = evo.builder():set(f, { t, t }):build() do @@ -1013,12 +1013,12 @@ do do local l, c = evo.multi_search(f, t) - assert(c == 3 and l[1] == e1 and l[2] == e3 and l[3] == e2) + assert(c == 3 and l[1] == e1 and l[2] == e2 and l[3] == e3) end end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2, t3 = evo.id(3) @@ -1041,7 +1041,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local g = evo.id() local t1, t2 = evo.id(2) @@ -1080,7 +1080,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2, t3 = evo.id(3) local e = evo.builder():set(f, { t1, t2 }):build() @@ -1103,7 +1103,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2 = evo.id(2) @@ -1131,6 +1131,11 @@ do assert(evo.search(f, l) == e) assert(evo.search(f, t) == nil) + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_LIST) + + assert(evo.search(f, l) == nil) + assert(evo.search(f, t) == e) + evo.set(f, evo.LINK) assert(evo.search(f, l) == nil) @@ -1138,6 +1143,11 @@ do evo.remove(f, evo.LINK) + assert(evo.search(f, l) == nil) + assert(evo.search(f, t) == e) + + evo.remove(f, evo.INDEX_POLICY) + assert(evo.search(f, l) == e) assert(evo.search(f, t) == nil) @@ -1146,7 +1156,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2 = evo.id(2) local e = evo.builder():set(f, { t1, t2 }):build() @@ -1165,7 +1175,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2 = evo.id(2) local e = evo.builder():set(f, { t1 }):build() @@ -1184,7 +1194,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local p = evo.builder():prefab():build() local t = evo.id() @@ -1203,7 +1213,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() local t1, t2 = evo.id(2) @@ -1220,13 +1230,13 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local g = evo.builder():tag():build() local t1, t2 = evo.id(2) local e1 = evo.builder():set(g):build() - local e2 = evo.builder():set(g):set(f, t1):build() + local e2 = evo.builder():set(g):set(f, { t1 }):build() local q = evo.builder():include(g):build() @@ -1382,9 +1392,11 @@ do end do - evo.debug_mode(false) + -- links to dead targets are reported with warnings, they are counted here + local warning_count, original_print = 0, print + print = function() warning_count = warning_count + 1 end - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local dead = evo.id() evo.destroy(dead) @@ -1393,7 +1405,7 @@ do assert(alive % 2 ^ 20 == dead % 2 ^ 20) local e1 = evo.builder():set(f, { dead, alive }):build() - local e2 = evo.builder():set(f, alive):build() + local e2 = evo.builder():set(f, { alive }):build() assert(evo.search(f, dead) == nil) @@ -1418,16 +1430,58 @@ do evo.destroy(e3) assert(evo.search(f, alive) == nil) - local e4 = evo.builder():set(f, alive):build() + local e4 = evo.builder():set(f, { alive }):build() evo.destroy(alive) assert(evo.alive(e4) and not evo.has(e4, f)) - evo.debug_mode(true) + print = original_print + assert(warning_count == 2) end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + -- links to dead targets are reported with warnings, they are counted here + local warning_count, original_print = 0, print + print = function() warning_count = warning_count + 1 end + + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() + + local d1, d2 = evo.id(2) + evo.destroy(d1, d2) + + local ts = { evo.id(8) } + + local e1 = evo.builder():set(f, { ts[1], ts[2], d1, ts[3], ts[4], ts[5], ts[6], ts[8] }):build() + local e2 = evo.builder():set(f, { ts[6], ts[5], ts[4], ts[3], ts[2] }):build() + + -- dead targets in long middles are skipped, and other targets keep their order + evo.set(e1, f, { ts[1], d2, ts[6], ts[7], d1, ts[4], ts[2], ts[8] }) + + assert(evo.search(f, d1) == nil and evo.search(f, d2) == nil) + assert(evo.search(f, ts[1]) == e1 and evo.search(f, ts[3]) == e2 and evo.search(f, ts[7]) == e1) + + for _, t in ipairs { ts[2], ts[4], ts[6] } do + local l, c = evo.multi_search(f, t) + assert(c == 2 and l[1] == e1 and l[2] == e2) + end + + do + local l, c = evo.multi_search(f, ts[5]) + assert(c == 1 and l[1] == e2) + end + + evo.set(e1, f, { d1, d2 }) + assert(evo.search(f, ts[1]) == nil and evo.search(f, ts[7]) == nil and evo.search(f, ts[2]) == e2) + + evo.destroy(ts[2]) + assert(evo.alive(e1) and evo.has(e1, f) and evo.alive(e2) and not evo.has(e2, f)) + + print = original_print + assert(warning_count == 5) +end + +do + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() assert(evo.get(f, evo.LINK_POLICY) == evo.LINK_POLICY_UNLINK_TARGET) local t1, t2, t3 = evo.id(3) @@ -1461,12 +1515,12 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local g = evo.id() local t1, t2 = evo.id(2) - local e1 = evo.builder():set(f, t1):set(g, 42):build() + local e1 = evo.builder():set(f, { t1, t1 }):set(g, 42):build() local e2 = evo.builder():set(f, { t1, t2 }):build() local e3 = evo.builder():set(f, { t1 }):build() local e4 = evo.builder():set(f, {}):build() @@ -1490,7 +1544,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2, t3, t4 = evo.id(4) local e = evo.builder():set(f, { t1, t2, t3, t4 }):build() @@ -1507,7 +1561,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2 = evo.id(2) local e = evo.builder():set(f, { t1, t2, t1, t2, t1 }):build() @@ -1526,7 +1580,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2 = evo.id(2) @@ -1550,7 +1604,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2 = evo.id(2) local e1 = evo.builder():set(f, { t1, t2 }):build() @@ -1569,7 +1623,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2 = evo.id(2) local e = evo.builder():set(f, { t1, t2 }):build() @@ -1586,6 +1640,7 @@ do local f = evo.builder() :link() + :index_policy(evo.INDEX_POLICY_LIST) :link_policy(evo.LINK_POLICY_UNLINK_TARGET) :on_set(function(e, _, nl, ol) assert(evo.alive(e)) @@ -1620,7 +1675,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2, t3 = evo.id(3) local e = evo.builder():set(f, { t1, t2, t3 }):build() @@ -1642,7 +1697,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2, t3 = evo.id(3) local e = evo.builder():set(f, { t1, t2 }):build() @@ -1661,7 +1716,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local g = evo.builder():tag():build() local t1 = evo.builder():set(g):build() @@ -1682,7 +1737,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2, t3 = evo.id(3) local e = evo.builder():set(f, { t1, t2, t3 }):build() @@ -1710,6 +1765,7 @@ do local children = evo.builder() :link() + :index_policy(evo.INDEX_POLICY_LIST) :link_policy(evo.LINK_POLICY_UNLINK_TARGET) :build() @@ -1746,7 +1802,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local e1, e2, e3 = evo.id(3) evo.set(e1, f, { e2, e3 }) @@ -1777,6 +1833,7 @@ do local f2 = evo.builder() :link() + :index_policy(evo.INDEX_POLICY_LIST) :link_policy(evo.LINK_POLICY_UNLINK_TARGET) :on_assign(function(e) assigned_list[#assigned_list + 1] = e end) :build() @@ -1802,7 +1859,7 @@ do end do - local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() local t1, t2, t3 = evo.id(3) @@ -1840,7 +1897,8 @@ end do local f1 = evo.builder():link():build() local f2 = evo.builder():link():link_policy(evo.LINK_POLICY_DESTROY_ENTITY):build() - local f3 = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + local f3 = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST) + :link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() do local t1, t2 = evo.id(2) @@ -1895,7 +1953,7 @@ do end do - local f = evo.builder():link():build() + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() local t1, t2, t3, t4, t5, t6, t7 = evo.id(7) local targets = { t1, t2, t3, t4, t5, t6, t7 } @@ -1964,7 +2022,7 @@ do evo.set(e1, f, { t7, t6, t5, t4, t3, t2, t1 }) check() - assert(evo.search(f, t1) == e2 and evo.search(f, t3) == e2) + assert(evo.search(f, t1) == e2 and evo.search(f, t3) == e1) evo.set(e1, f, { t6, t6, t6, t6, t6, t6 }) check() diff --git a/develop/testing/multi_index_tests.lua b/develop/testing/multi_index_tests.lua new file mode 100644 index 0000000..6dbb0d7 --- /dev/null +++ b/develop/testing/multi_index_tests.lua @@ -0,0 +1,625 @@ +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 +end + +do + assert(evo.get(evo.INCLUDES, evo.INDEX_POLICY) == evo.INDEX_POLICY_LIST) + assert(evo.get(evo.EXCLUDES, evo.INDEX_POLICY) == evo.INDEX_POLICY_LIST) + assert(evo.get(evo.VARIANTS, evo.INDEX_POLICY) == evo.INDEX_POLICY_LIST) + assert(evo.get(evo.REQUIRES, evo.INDEX_POLICY) == evo.INDEX_POLICY_LIST) + + assert(not evo.has(evo.NAME, evo.INDEX_POLICY)) + assert(not evo.has(evo.GROUP, evo.INDEX_POLICY)) + assert(not evo.has(evo.QUERY, evo.INDEX_POLICY)) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + assert(evo.has(f, evo.INDEX) and evo.get(f, evo.INDEX_POLICY) == evo.INDEX_POLICY_LIST) + + local e1 = evo.spawn { [f] = { 'red', 'big' } } + local e2 = evo.spawn { [f] = { 'blue', 'big' } } + local e3 = evo.spawn { [f] = { 'red' } } + local e4 = evo.spawn { [f] = {} } + + check_search(f, 'red', e1, e3) + check_search(f, 'blue', e2) + check_search(f, 'big', e1, e2) + check_search(f, 'small') + + evo.set(e3, f, { 'small', 'red' }) + check_search(f, 'red', e1, e3) + check_search(f, 'small', e3) + + evo.set(e1, f, { 'blue' }) + check_search(f, 'red', e3) + check_search(f, 'blue', e2, e1) + check_search(f, 'big', e2) + + evo.set(e4, f, { 'big', 'big', 'big' }) + check_search(f, 'big', e2, e4) + + evo.remove(e2, f) + check_search(f, 'blue', e1) + check_search(f, 'big', e4) + + evo.destroy(e1, e3, e4) + check_search(f, 'red') + check_search(f, 'blue') + check_search(f, 'big') + check_search(f, 'small') +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_KEYS):build() + + local e1 = evo.spawn { [f] = { red = true, big = true } } + local e2 = evo.spawn { [f] = { blue = true, big = true } } + local e3 = evo.spawn { [f] = { red = false } } + + -- every key of a set is indexed, whatever its value is + check_search(f, 'red', e1, e3) + check_search(f, 'blue', e2) + check_search(f, 'big', e1, e2) + check_search(f, true) + check_search(f, false) + + evo.set(e1, f, { big = true, small = true }) + check_search(f, 'red', e3) + check_search(f, 'big', e1, e2) + check_search(f, 'small', e1) + + evo.remove(e1, f) + check_search(f, 'big', e2) + check_search(f, 'small') +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_VALUES):build() + + local e1 = evo.spawn { [f] = { left = 'sword', right = 'shield' } } + local e2 = evo.spawn { [f] = { left = 'shield', right = 'shield' } } + local e3 = evo.spawn { [f] = { 'sword', 'sword' } } + + -- only values of a map are indexed, keys are not + check_search(f, 'sword', e1, e3) + check_search(f, 'shield', e1, e2) + check_search(f, 'left') + check_search(f, 1) + + evo.set(e2, f, { left = 'sword' }) + check_search(f, 'sword', e1, e3, e2) + check_search(f, 'shield', e1) + + evo.destroy(e1) + check_search(f, 'sword', e3, e2) + check_search(f, 'shield') +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + -- values with equal primary parts must not be mixed up + local e1 = evo.spawn { [f] = { 1, 1048577, 2 ^ 40 + 1 } } + local e2 = evo.spawn { [f] = { 1048577 } } + local e3 = evo.spawn { [f] = { 1, 0.5, -1 } } + + check_search(f, 1, e1, e3) + check_search(f, 1048577, e1, e2) + check_search(f, 2 ^ 40 + 1, e1) + check_search(f, 0.5, e3) + check_search(f, -1, e3) + + evo.remove(e1, f) + check_search(f, 1, e3) + check_search(f, 1048577, e2) + check_search(f, 2 ^ 40 + 1) + + evo.destroy(e2, e3) + check_search(f, 1) + check_search(f, 1048577) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local nan = 0 / 0 + + local t1, t2 = {}, {} + + local e1 = evo.spawn { [f] = { nan, true, false, t1 } } + local e2 = evo.spawn { [f] = { false, t2, nan } } + + -- NaN values are not indexed, tables are indexed by reference + check_search(f, nan) + check_search(f, true, e1) + check_search(f, false, e1, e2) + check_search(f, t1, e1) + check_search(f, t2, e2) + + evo.set(e1, f, { nan }) + check_search(f, true) + check_search(f, false, e2) + check_search(f, t1) + + evo.destroy(e1, e2) + check_search(f, false) + check_search(f, t2) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local e1 = evo.spawn { [f] = { 1, 2, 3, 4, 5, 6, 7, 8 } } + local e2 = evo.spawn { [f] = { 8, 7, 6, 5, 4, 3, 2, 1 } } + + for i = 1, 8 do + check_search(f, i, e1, e2) + end + + evo.set(e1, f, { 9, 10, 11, 12, 13, 14, 1, 8 }) + + for i = 2, 7 do + check_search(f, i, e2) + end + + for i = 9, 14 do + check_search(f, i, e1) + end + + check_search(f, 1, e1, e2) + check_search(f, 8, e1, e2) + + evo.set(e2, f, { 15 }) + + for i = 1, 8 do + if i == 1 or i == 8 then + check_search(f, i, e1) + else + check_search(f, i) + end + end + + check_search(f, 15, e2) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local nan = 0 / 0 + + local e1 = evo.spawn { [f] = { 1, 2, 3, 4, 5, 6, 7, 8 } } + local e2 = evo.spawn { [f] = { 1, 2, 3, 4, 5, 6, 7, 8 } } + + -- the long middle is changed, but its retained values keep their order + evo.set(e1, f, { 1, 7, 6, nan, 5, 4, 3, 9, 1, 8 }) + + for i = 1, 8 do + if i == 2 then + check_search(f, i, e2) + else + check_search(f, i, e1, e2) + end + end + + check_search(f, 9, e1) + check_search(f, nan) + + -- values duplicated outside of the long middle are not unlinked + evo.set(e1, f, { 1, 10, 11, 12, 13, 14, 1 }) + + check_search(f, 1, e1, e2) + + for i = 3, 9 do + if i == 9 then + check_search(f, i) + else + check_search(f, i, e2) + end + end + + for i = 10, 14 do + check_search(f, i, e1) + end + + evo.set(e1, f, { 14, 13, 12, 11, 10, 9, 8, 7 }) + + check_search(f, 1, e2) + check_search(f, 7, e2, e1) + check_search(f, 8, e2, e1) + check_search(f, 9, e1) + + for i = 10, 14 do + check_search(f, i, e1) + end +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_KEYS):build() + + local s1 = {} + local s2 = {} + + for i = 1, 16 do + s1['k' .. i] = true + s2['k' .. i] = true + end + + local e1 = evo.spawn { [f] = s1 } + local e2 = evo.spawn { [f] = s2 } + + -- sets with other key orders keep the search order of their retained keys + local s3 = {} + + for i = 32, 1, -1 do + if i % 3 ~= 0 then + s3['k' .. i] = true + end + end + + evo.set(e1, f, s3) + + for i = 1, 32 do + if i % 3 == 0 and i <= 16 then + check_search(f, 'k' .. i, e2) + elseif i % 3 == 0 then + check_search(f, 'k' .. i) + elseif i <= 16 then + check_search(f, 'k' .. i, e1, e2) + else + check_search(f, 'k' .. i, e1) + end + end +end + +do + local f = evo.builder() + :link() + :index_policy(evo.INDEX_POLICY_LIST) + :link_policy(evo.LINK_POLICY_UNLINK_TARGET) + :build() + + local ts = { evo.id(8) } + + local e1 = evo.spawn { [f] = { ts[1], ts[2], ts[3], ts[4], ts[5], ts[6], ts[7], ts[8] } } + local e2 = evo.spawn { [f] = { ts[2], ts[4], ts[6] } } + + -- unlinking many targets keeps the search order of the other ones + evo.destroy(ts[1], ts[3], ts[5], ts[7], ts[8]) + + local c1 = evo.get(e1, f) + assert(#c1 == 3 and c1[1] == ts[2] and c1[2] == ts[4] and c1[3] == ts[6]) + + check_search(f, ts[2], e1, e2) + check_search(f, ts[4], e1, e2) + check_search(f, ts[6], e1, e2) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local weak_values = setmetatable({}, { __mode = 'v' }) + + do + local t = {} + weak_values[1] = t + + local e = evo.spawn { [f] = { 1, 2, 3, 4, 5, 6 } } + + -- long middles mark their new values, these marks must not hold them + evo.set(e, f, { 7, t, 8, 9, 10, 11 }) + check_search(f, t, e) + + evo.set(e, f, { 12, 13, 14, 15, 16, 17 }) + check_search(f, t) + + evo.destroy(e) + end + + collectgarbage('collect') + assert(weak_values[1] == nil) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local e = evo.spawn { [f] = {} } + + -- value keys are reused after values are not indexed anymore + for i = 1, 100 do + evo.set(e, f, { 'v' .. i, 'w' .. i, i }) + check_search(f, 'v' .. i, e) + check_search(f, 'w' .. i, e) + check_search(f, i, e) + check_search(f, 'v' .. (i - 1)) + check_search(f, i - 1) + end + + local e2 = evo.spawn { [f] = { 'v100', 100 } } + check_search(f, 'v100', e, e2) + check_search(f, 100, e, e2) + + evo.destroy(e) + check_search(f, 'v100', e2) + check_search(f, 'w100') +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_KEYS):build() + local g = evo.builder():index():index_policy(evo.INDEX_POLICY_VALUES):build() + + local s = { red = true } + local m = { left = 'sword' } + + local e = evo.spawn { [f] = s, [g] = m } + + s.blue, s.red = true, nil + m.right, m.left = 'shield', nil + + -- tables changed in place are not reindexed until reindex is called + check_search(f, 'red', e) + check_search(f, 'blue') + check_search(g, 'sword', e) + check_search(g, 'shield') + + evo.reindex(f, g) + + check_search(f, 'red') + check_search(f, 'blue', e) + check_search(g, 'sword') + check_search(g, 'shield', e) +end + +do + local f = evo.builder():index():build() + + local l = { 'red', 'big' } + local s = { red = true } + + local e1 = evo.spawn { [f] = l } + local e2 = evo.spawn { [f] = s } + + check_search(f, l, e1) + check_search(f, 'red') + + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_VALUES) + check_search(f, l) + check_search(f, 'red', e1) + check_search(f, true, e2) + + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_KEYS) + check_search(f, 'red', e2) + check_search(f, 1, e1) + check_search(f, true) + + evo.set(f, evo.INDEX_POLICY, evo.INDEX_POLICY_LIST) + check_search(f, 'red', e1) + check_search(f, 'big', e1) + + evo.remove(f, evo.INDEX_POLICY) + check_search(f, 'red') + check_search(f, l, e1) + check_search(f, s, e2) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local p = evo.builder():prefab():set(f, { 'red' }):build() + + local e1 = evo.clone(p) + local l, c = evo.multi_clone(2, p) + assert(c == 2) + + check_search(f, 'red', p, e1, l[1], l[2]) + + local sl, sc = evo.multi_spawn(2, { [f] = { 'blue', 'red' } }) + assert(sc == 2) + + check_search(f, 'red', p, e1, l[1], l[2], sl[1], sl[2]) + check_search(f, 'blue', sl[1], sl[2]) + + local q = evo.builder():include(f):build() + + evo.batch_set(q, f, { 'green' }) + check_search(f, 'red', p) + check_search(f, 'blue') + + do + local gl, gc = evo.multi_search(f, 'green') + assert(gc == 5) + + local gs = {} + for i = 1, gc do gs[gl[i]] = true end + assert(gs[e1] and gs[l[1]] and gs[l[2]] and gs[sl[1]] and gs[sl[2]]) + end + + evo.batch_destroy(q) + check_search(f, 'green') +end + +do + local f = evo.builder() + :index() + :index_policy(evo.INDEX_POLICY_KEYS) + :link() + :link_policy(evo.LINK_POLICY_UNLINK_TARGET) + :build() + + local t1, t2, t3 = evo.id(3) + + local s = { [t1] = 'a', [t2] = 'b', [t3] = 'c' } + local e = evo.spawn { [f] = s } + + check_search(f, t1, e) + check_search(f, t2, e) + + evo.destroy(t2) + + do + local ns = evo.get(e, f) + assert(ns ~= s and ns[t1] == 'a' and ns[t2] == nil and ns[t3] == 'c') + assert(s[t1] == 'a' and s[t2] == 'b' and s[t3] == 'c') + end + + check_search(f, t1, e) + check_search(f, t2) + + evo.destroy(t1, t3) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder() + :link() + :index_policy(evo.INDEX_POLICY_VALUES) + :link_policy(evo.LINK_POLICY_UNLINK_TARGET) + :build() + + local sword, shield = evo.id(2) + + local e1 = evo.spawn { [f] = { left = sword, right = shield } } + local e2 = evo.spawn { [f] = { left = shield, right = shield } } + + check_search(f, sword, e1) + check_search(f, shield, e1, e2) + + evo.destroy(shield) + + do + local m1 = evo.get(e1, f) + assert(m1.left == sword and m1.right == nil) + end + + assert(evo.alive(e2) and not evo.has(e2, f)) + + check_search(f, sword, e1) + check_search(f, shield) +end + +do + local f = evo.builder():link():link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local t = evo.id() + local e = evo.spawn { [f] = t } + + -- single links are removed together with the fragment + evo.destroy(t) + assert(evo.alive(e) and not evo.has(e, f)) +end + +do + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):build() + + local t1, t2 = evo.id(2) + local e = evo.spawn { [f] = { t1, t2 } } + + check_search(f, t1, e) + check_search(f, t2, e) + + evo.set(f, evo.INDEX) + evo.remove(f, evo.LINK) + check_search(f, t1, e) + + -- not linked anymore, so nothing is resolved + evo.destroy(t1) + check_search(f, t1, e) + assert(evo.get(e, f)[1] == t1) +end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_KEYS):build() + + local weak_values = setmetatable({}, { __mode = 'v' }) + + do + local t1, t2 = {}, {} + weak_values[1], weak_values[2] = t1, t2 + + local e = evo.spawn { [f] = { [t1] = true } } + evo.set(e, f, { [t1] = true, [t2] = true }) + check_search(f, t2, e) + + evo.destroy(e) + check_search(f, t1) + check_search(f, t2) + end + + -- collected components of the last assignment are kept until garbage collection + evo.collect_garbage() + collectgarbage('collect') + assert(weak_values[1] == nil and weak_values[2] == nil) +end + +do + local fk = evo.builder():index():index_policy(evo.INDEX_POLICY_KEYS):build() + local fl = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):link_policy(evo.LINK_POLICY_UNLINK_TARGET):build() + + local ts = { evo.id(8) } + + local es = {} + + for i = 1, 1000 do + es[i] = evo.spawn { + [fk] = { ['k' .. i] = true, shared = true }, + [fl] = { ts[i % 8 + 1], ts[(i + 1) % 8 + 1] }, + } + end + + for i = 3, 1000 do + evo.destroy(es[i]) + end + + -- indices are shrunk here, and they must keep working after that + evo.collect_garbage() + + check_search(fk, 'shared', es[1], es[2]) + check_search(fk, 'k1', es[1]) + check_search(fk, 'k3') + check_search(fl, ts[3], es[1], es[2]) + + local e3 = evo.spawn { [fk] = { k1 = true, new = true }, [fl] = { ts[3], ts[5] } } + evo.set(es[1], fk, { shared = true, new = true }) + + check_search(fk, 'k1', e3) + check_search(fk, 'new', e3, es[1]) + check_search(fk, 'shared', es[1], es[2]) + check_search(fl, ts[3], es[1], es[2], e3) + + evo.destroy(ts[3]) + assert(evo.get(es[1], fl)[1] == ts[2] and #evo.get(es[1], fl) == 1) + assert(evo.get(e3, fl)[1] == ts[5] and #evo.get(e3, fl) == 1) + + evo.collect_garbage() + evo.destroy(es[1], es[2], e3) + + check_search(fk, 'shared') + check_search(fk, 'new') + check_search(fl, ts[2]) + check_search(fl, ts[5]) + + evo.collect_garbage() + + local e4 = evo.spawn { [fk] = { shared = true } } + check_search(fk, 'shared', e4) +end diff --git a/evolved.lua b/evolved.lua index cf73b53..42e9d39 100644 --- a/evolved.lua +++ b/evolved.lua @@ -984,6 +984,82 @@ end --- --- +local __table_fns = {} + +---@generic K, V +---@param table table +---@return table +---@nodiscard +function __table_fns.dup(table) + local dup_table = {} + + for key, value in __lua_next, table do + dup_table[key] = value + end + + return dup_table +end + +---@generic K +---@param table table +---@return K[] key_list +---@return integer key_count +---@nodiscard +function __table_fns.keys(table) + local key_list = {} + local key_count = __table_fns.keys_to(key_list, 1, table) + return key_list, key_count +end + +---@generic K +---@param out_key_list K[] +---@param out_key_first integer +---@param table table +---@return integer key_count +function __table_fns.keys_to(out_key_list, out_key_first, table) + local out_key_last = out_key_first - 1 + + for key in __lua_next, table do + out_key_last = out_key_last + 1 + out_key_list[out_key_last] = key + end + + return out_key_last - out_key_first + 1 +end + +---@generic V +---@param table table +---@return V[] value_list +---@return integer value_count +---@nodiscard +function __table_fns.values(table) + local value_list = {} + local value_count = __table_fns.values_to(value_list, 1, table) + return value_list, value_count +end + +---@generic V +---@param out_value_list V[] +---@param out_value_first integer +---@param table table +---@return integer value_count +function __table_fns.values_to(out_value_list, out_value_first, table) + local out_value_last = out_value_first - 1 + + for _, value in __lua_next, table do + out_value_last = out_value_last + 1 + out_value_list[out_value_last] = value + end + + return out_value_last - out_value_first + 1 +end + +--- +--- +--- +--- +--- + ---@class (exact) evolved.assoc_list: { --- __item_set: { [K]: integer }, --- __item_list: K[], @@ -1464,11 +1540,20 @@ local __safe_tbls = { --- ---@class (exact) evolved.search_index ----@field package __combined_keys boolean +---@field package __fragment evolved.fragment +---@field package __index_policy evolved.id +---@field package __multi_components boolean ---@field package __head_entities table ---@field package __prev_entities table ---@field package __next_entities table ---@field package __entity_components table +---@field package __collected_components? evolved.component[] +---@field package __component_keys? table +---@field package __component_key_count integer +---@field package __free_component_keys? integer[] +---@field package __free_component_key_count integer +---@field package __component_epochs? table +---@field package __component_epoch integer ---@field package __link_policy? evolved.id ---@field package __target_fragment? evolved.fragment @@ -1478,46 +1563,90 @@ local __search_index_fns = {} ---@return evolved.search_index ---@nodiscard function __search_index_fns.new(fragment) + ---@type evolved.id + local index_policy = __evolved_get(fragment, __INDEX_POLICY) + or __INDEX_POLICY_SELF + + if index_policy ~= __INDEX_POLICY_SELF + and index_policy ~= __INDEX_POLICY_LIST + and index_policy ~= __INDEX_POLICY_KEYS + and index_policy ~= __INDEX_POLICY_VALUES + then + __debug_fns.warning_fmt('unknown INDEX_POLICY (%s) on (%s), %s is used instead', + __id_name(index_policy), __id_name(fragment), + __id_name(__INDEX_POLICY_SELF)) + index_policy = __INDEX_POLICY_SELF + end + + local multi_components = index_policy ~= __INDEX_POLICY_SELF + local collect_components = multi_components and index_policy ~= __INDEX_POLICY_LIST + local target_fragment = __evolved_has(fragment, __LINK) and fragment or nil + local intern_components = multi_components and not target_fragment + ---@type evolved.search_index return { - __combined_keys = false, + __fragment = fragment, + __index_policy = index_policy, + __multi_components = multi_components, __head_entities = {}, __prev_entities = {}, __next_entities = {}, __entity_components = {}, + __collected_components = collect_components and {} or nil, + __component_keys = intern_components and {} or nil, + __component_key_count = 0, + __free_component_keys = intern_components and {} or nil, + __free_component_key_count = 0, + __component_epochs = multi_components and {} or nil, + __component_epoch = 0, __link_policy = __evolved_get(fragment, __LINK_POLICY), - __target_fragment = __evolved_has(fragment, __LINK) and fragment or nil, + __target_fragment = target_fragment, } end ---@param index evolved.search_index -function __search_index_fns.combine_keys(index) - if index.__combined_keys then - return - end +---@param component evolved.component +---@return integer +---@nodiscard +function __search_index_fns.acquire_component_key(index, component) + local component_keys = index.__component_keys + local free_component_keys = index.__free_component_keys + ---@cast component_keys -? + ---@cast free_component_keys -? - local entity_components = index.__entity_components + local free_component_key_count = index.__free_component_key_count - local old_prev_entities = index.__prev_entities - local old_next_entities = index.__next_entities + local component_key ---@type integer - local new_prev_entities = {} ---@type table - local new_next_entities = {} ---@type table + if free_component_key_count > 0 then + component_key = free_component_keys[free_component_key_count] + + free_component_keys[free_component_key_count] = nil + index.__free_component_key_count = free_component_key_count - 1 + else + component_key = index.__component_key_count + 1 + index.__component_key_count = component_key + end - for entity_primary, prev_entity in __lua_next, old_prev_entities do - local component = entity_components[entity_primary] + component_keys[component] = component_key - local key_offset = component % 1048576 * 1048576 - local entity_key = key_offset + entity_primary + return component_key +end - new_prev_entities[entity_key] = prev_entity - new_next_entities[entity_key] = old_next_entities[entity_primary] - end +---@param index evolved.search_index +---@param component evolved.component +function __search_index_fns.release_component_key(index, component) + local component_keys = index.__component_keys + local free_component_keys = index.__free_component_keys + ---@cast component_keys -? + ---@cast free_component_keys -? - index.__prev_entities = new_prev_entities - index.__next_entities = new_next_entities + local free_component_key_count = index.__free_component_key_count + 1 - index.__combined_keys = true + free_component_keys[free_component_key_count] = component_keys[component] + index.__free_component_key_count = free_component_key_count + + component_keys[component] = nil end ---@param target evolved.entity @@ -1584,22 +1713,20 @@ end ---@param entity evolved.entity ---@param component evolved.component function __search_index_fns.assign(index, entity, component) - local target_fragment = index.__target_fragment + local entity_primary = entity % 1048576 local entity_components = index.__entity_components local new_component = component - local old_component = entity_components[entity % 1048576] + local old_component = entity_components[entity_primary] - if new_component == old_component then - -- nothing to reassign - elseif target_fragment - and __lua_type(new_component) == 'table' - and __lua_type(old_component) == 'table' - then - __search_index_fns.assign_targets(index, entity, new_component) - else - __search_index_fns.remove(index, entity) + if old_component == nil then __search_index_fns.insert(index, entity, new_component) + elseif index.__multi_components then + __search_index_fns.assign_components(index, entity, new_component) + elseif old_component ~= new_component then + __search_index_fns.remove_node(index, entity, old_component) + __search_index_fns.insert_node(index, entity, new_component) + entity_components[entity_primary] = new_component end end @@ -1609,112 +1736,147 @@ end ---@param first integer ---@param last integer function __search_index_fns.multi_assign(index, entity_list, component_list, first, last) - local target_fragment = index.__target_fragment + local multi_components = index.__multi_components local entity_components = index.__entity_components for place = first, last do local entity = entity_list[place] + local entity_primary = entity % 1048576 local new_component = component_list[place] - local old_component = entity_components[entity % 1048576] - - if new_component == old_component then - -- nothing to reassign - elseif target_fragment - and __lua_type(new_component) == 'table' - and __lua_type(old_component) == 'table' - then - __search_index_fns.assign_targets(index, entity, new_component) - else - __search_index_fns.remove(index, entity) + local old_component = entity_components[entity_primary] + + if old_component == nil then __search_index_fns.insert(index, entity, new_component) + elseif multi_components then + __search_index_fns.assign_components(index, entity, new_component) + elseif old_component ~= new_component then + __search_index_fns.remove_node(index, entity, old_component) + __search_index_fns.insert_node(index, entity, new_component) + entity_components[entity_primary] = new_component end end end ---@param index evolved.search_index ---@param entity evolved.entity ----@param new_target_list evolved.entity[] -function __search_index_fns.assign_targets(index, entity, new_target_list) +---@param component evolved.component +function __search_index_fns.assign_components(index, entity, component) local entity_primary = entity % 1048576 + local entity_components = index.__entity_components - ---@type evolved.entity[] - local old_target_list = index.__entity_components[entity_primary] - - local old_target_count = #old_target_list - local new_target_count = #new_target_list + ---@type evolved.component[] + local old_component_list = entity_components[entity_primary] + local old_component_count = #old_component_list + + ---@type evolved.component[], integer + local new_component_list, new_component_count + local index_policy = index.__index_policy + + if index_policy == __INDEX_POLICY_LIST then + new_component_list = component + new_component_count = #component + elseif index_policy == __INDEX_POLICY_KEYS then + new_component_list = index.__collected_components ---@cast new_component_list -? + new_component_count = __table_fns.keys_to(new_component_list, 1, component) + elseif index_policy == __INDEX_POLICY_VALUES then + new_component_list = index.__collected_components ---@cast new_component_list -? + new_component_count = __table_fns.values_to(new_component_list, 1, component) + else + __debug_fns.error_fmt('unknown INDEX_POLICY (%s) on (%s)', + __id_name(index_policy), __id_name(index.__fragment)) + end - local min_target_count = old_target_count < new_target_count - and old_target_count - or new_target_count + local min_component_count = old_component_count < new_component_count + and old_component_count or new_component_count local prefix_count = 0 - while prefix_count < min_target_count - and old_target_list[prefix_count + 1] == new_target_list[prefix_count + 1] + while prefix_count < min_component_count + and old_component_list[prefix_count + 1] == new_component_list[prefix_count + 1] do prefix_count = prefix_count + 1 end - if prefix_count == old_target_count and prefix_count == new_target_count then - return - end - local suffix_count = 0 - while prefix_count + suffix_count < min_target_count - and old_target_list[old_target_count - suffix_count] == new_target_list[new_target_count - suffix_count] + while prefix_count + suffix_count < min_component_count + and old_component_list[old_component_count - suffix_count] == new_component_list[new_component_count - suffix_count] do suffix_count = suffix_count + 1 end - local old_middle_last = old_target_count - suffix_count - local new_middle_last = new_target_count - suffix_count + if prefix_count == old_component_count and prefix_count == new_component_count then + return + end + + local old_middle_last = old_component_count - suffix_count + local new_middle_last = new_component_count - suffix_count if old_middle_last - prefix_count <= 4 then - for old_target_index = prefix_count + 1, old_middle_last do - local old_target = old_target_list[old_target_index] + for old_component_index = prefix_count + 1, old_middle_last do + local old_component = old_component_list[old_component_index] - local new_target_index = 1 + local new_component_index = 1 - while new_target_index <= new_target_count - and new_target_list[new_target_index] ~= old_target + while new_component_index <= new_component_count + and new_component_list[new_component_index] ~= old_component do - new_target_index = new_target_index + 1 + new_component_index = new_component_index + 1 end - if new_target_index > new_target_count then - __search_index_fns.remove_node(index, entity, old_target) + if new_component_index > new_component_count then + __search_index_fns.remove_node(index, entity, old_component) end end - - for new_target_index = prefix_count + 1, new_middle_last do - __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) - end else - for old_target_index = prefix_count + 1, old_middle_last do - __search_index_fns.remove_node(index, entity, old_target_list[old_target_index]) - end + local component_epochs = index.__component_epochs + ---@cast component_epochs -? - for new_target_index = prefix_count + 1, new_middle_last do - __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) - end + local component_epoch = index.__component_epoch + 1 + index.__component_epoch = component_epoch + + if index.__target_fragment then + for new_component_index = 1, new_component_count do + local new_component = new_component_list[new_component_index] + + if __freelist_ids[new_component % 1048576] ~= new_component then + -- dead targets are not linked, so they are not marked + else + component_epochs[new_component] = component_epoch + end + end + else + for new_component_index = 1, new_component_count do + local new_component = new_component_list[new_component_index] - for new_target_index = 1, prefix_count do - __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) + if new_component ~= new_component then + -- NaN components cannot be indexed, so they are not marked + else + component_epochs[new_component] = component_epoch + end + end end - for new_target_index = new_middle_last + 1, new_target_count do - __search_index_fns.insert_node(index, entity, new_target_list[new_target_index]) + for old_component_index = prefix_count + 1, old_middle_last do + local old_component = old_component_list[old_component_index] + + if component_epochs[old_component] ~= component_epoch then + __search_index_fns.remove_node(index, entity, old_component) + end end end + for new_component_index = prefix_count + 1, new_middle_last do + __search_index_fns.insert_node(index, entity, new_component_list[new_component_index]) + end + __lua_table_move( - new_target_list, prefix_count + 1, new_target_count, - prefix_count + 1, old_target_list) + new_component_list, prefix_count + 1, new_component_count, + prefix_count + 1, old_component_list) - for old_target_index = new_target_count + 1, old_target_count do - old_target_list[old_target_index] = nil + for old_component_index = new_component_count + 1, old_component_count do + old_component_list[old_component_index] = nil end end @@ -1722,24 +1884,27 @@ end ---@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 - - if index.__target_fragment and __lua_type(component) == 'table' then - if not index.__combined_keys then - __search_index_fns.combine_keys(index) + if index.__multi_components then + ---@type evolved.component[], integer + local component_list, component_count + local index_policy = index.__index_policy + + if index_policy == __INDEX_POLICY_LIST then + component_list, component_count = __list_fns.dup(component) + elseif index_policy == __INDEX_POLICY_KEYS then + component_list, component_count = __table_fns.keys(component) + elseif index_policy == __INDEX_POLICY_VALUES then + component_list, component_count = __table_fns.values(component) + else + __debug_fns.error_fmt('unknown INDEX_POLICY (%s) on (%s)', + __id_name(index_policy), __id_name(index.__fragment)) end - ---@type evolved.entity[], integer - local target_list, target_count = __list_fns.dup(component) - - for target_index = 1, target_count do - __search_index_fns.insert_node(index, entity, target_list[target_index]) + for component_index = 1, component_count do + __search_index_fns.insert_node(index, entity, component_list[component_index]) end - index.__entity_components[entity % 1048576] = target_list + index.__entity_components[entity % 1048576] = component_list else __search_index_fns.insert_node(index, entity, component) index.__entity_components[entity % 1048576] = component @@ -1761,10 +1926,12 @@ end ---@param entity evolved.entity ---@param component evolved.component function __search_index_fns.insert_node(index, entity, component) - local target_fragment = index.__target_fragment + if component ~= component then + -- NaN components cannot be indexed + return + end - local key_offset = index.__combined_keys and component % 1048576 * 1048576 or 0 - local entity_key = key_offset + entity % 1048576 + local target_fragment = index.__target_fragment local head_entities = index.__head_entities local prev_entities = index.__prev_entities @@ -1774,22 +1941,38 @@ function __search_index_fns.insert_node(index, entity, component) local head_entity = head_entities[component] if head_entity then + local component_keys = index.__component_keys + + local component_offset = not index.__multi_components and 0 + or component_keys and component_keys[component] * 1048576 + or component % 1048576 * 1048576 + + local entity_key = component_offset + entity % 1048576 + if prev_entities[entity_key] then - -- this target is duplicated in the list and has already been linked return end - local head_entity_key = key_offset + head_entity % 1048576 + local head_entity_key = component_offset + head_entity % 1048576 local tail_entity = prev_entities[head_entity_key] - local tail_entity_key = key_offset + tail_entity % 1048576 + local tail_entity_key = component_offset + tail_entity % 1048576 prev_entities[entity_key] = tail_entity next_entities[entity_key] = head_entity prev_entities[head_entity_key] = entity next_entities[tail_entity_key] = entity - elseif not target_fragment or __freelist_ids[component % 1048576] == component then + elseif target_fragment and __freelist_ids[component % 1048576] ~= component then + __debug_fns.warning_fmt('the target (%s) of the linked fragment (%s) is not alive and cannot be linked', + __id_name(component), __id_name(target_fragment)) + else + local component_offset = not index.__multi_components and 0 + or target_fragment and component % 1048576 * 1048576 + or __search_index_fns.acquire_component_key(index, component) * 1048576 + + local entity_key = component_offset + entity % 1048576 + head_entities[component] = entity prev_entities[entity_key] = entity @@ -1798,9 +1981,6 @@ function __search_index_fns.insert_node(index, entity, component) if target_fragment then __search_index_fns.link(component, target_fragment) end - elseif __debug_mode and target_fragment then - __debug_fns.error_fmt('the target (%s) of the linked fragment (%s) is not alive', - __id_name(component), __id_name(target_fragment)) end end @@ -1808,27 +1988,27 @@ end ---@param entity evolved.entity function __search_index_fns.remove(index, entity) local entity_primary = entity % 1048576 + local entity_components = index.__entity_components ---@type evolved.component? - local component = index.__entity_components[entity_primary] + local component = entity_components[entity_primary] if component == nil then return end - if index.__target_fragment and __lua_type(component) == 'table' then - ---@type evolved.entity[], integer - local target_list, target_count = component, #component + if index.__multi_components then + ---@type evolved.component[], integer + local component_list, component_count = component, #component - for target_index = 1, target_count do - __search_index_fns.remove_node(index, entity, target_list[target_index]) + for component_index = 1, component_count do + __search_index_fns.remove_node(index, entity, component_list[component_index]) end - - index.__entity_components[entity_primary] = nil else __search_index_fns.remove_node(index, entity, component) - index.__entity_components[entity_primary] = nil end + + entity_components[entity_primary] = nil end ---@param index evolved.search_index @@ -1845,11 +2025,6 @@ end ---@param entity evolved.entity ---@param component evolved.component function __search_index_fns.remove_node(index, entity, component) - local target_fragment = index.__target_fragment - - local key_offset = index.__combined_keys and component % 1048576 * 1048576 or 0 - local entity_key = key_offset + entity % 1048576 - local head_entities = index.__head_entities local prev_entities = index.__prev_entities local next_entities = index.__next_entities @@ -1858,29 +2033,45 @@ function __search_index_fns.remove_node(index, entity, component) local head_entity = head_entities[component] if not head_entity then - -- this target is dead or has no links anymore return end + local component_keys = index.__component_keys + + local component_offset = not index.__multi_components and 0 + or component_keys and component_keys[component] * 1048576 + or component % 1048576 * 1048576 + + local entity_key = component_offset + entity % 1048576 + ---@type evolved.entity? local next_entity = next_entities[entity_key] if not next_entity then - -- this target is duplicated in the list and has already been unlinked return end if next_entity == entity then head_entities[component] = nil + local component_epochs = index.__component_epochs + + if component_epochs then + component_epochs[component] = nil + end + + local target_fragment = index.__target_fragment + if target_fragment then __search_index_fns.unlink(component, target_fragment) + elseif component_keys then + __search_index_fns.release_component_key(index, component) end else local prev_entity = prev_entities[entity_key] - local prev_entity_key = key_offset + prev_entity % 1048576 - local next_entity_key = key_offset + next_entity % 1048576 + local prev_entity_key = component_offset + prev_entity % 1048576 + local next_entity_key = component_offset + next_entity % 1048576 prev_entities[next_entity_key] = prev_entity next_entities[prev_entity_key] = next_entity @@ -4759,25 +4950,59 @@ function __unlink_entity_targets(entity, fragment, destroying_entity_set) local component_storages = chunk.__component_storages local component_storage = component_storages[component_index] - ---@type evolved.entity[] - local old_target_list = component_storage[place] - local old_target_count = __lua_type(old_target_list) == 'table' and #old_target_list or 0 + local fragment_search_index = chunk.__has_index_fragments + and __search_indices[fragment] + or nil + + local index_policy = fragment_search_index + and fragment_search_index.__index_policy + or __INDEX_POLICY_SELF - if old_target_count == 0 then + if index_policy == __INDEX_POLICY_SELF then __remove_entity_fragments(entity, fragment) return end - ---@type evolved.entity[] - local new_target_list = __lua_table_new(old_target_count) + local new_component ---@type evolved.component? + local old_component = component_storage[place] + local new_target_count = 0 - for old_target_index = 1, old_target_count do - local old_target = old_target_list[old_target_index] + if index_policy == __INDEX_POLICY_LIST then + ---@type evolved.component[] + local old_target_list = old_component + local old_target_count = #old_target_list + + ---@type evolved.component[] + new_component = __lua_table_new(old_target_count) + + for old_target_index = 1, old_target_count do + local old_target = old_target_list[old_target_index] + + if not destroying_entity_set[old_target] then + new_target_count = new_target_count + 1 + new_component[new_target_count] = old_target + end + end + elseif index_policy == __INDEX_POLICY_KEYS then + ---@type table + new_component = {} + + for old_target, old_value in __lua_next, old_component do + if not destroying_entity_set[old_target] then + new_target_count = new_target_count + 1 + new_component[old_target] = old_value + end + end + elseif index_policy == __INDEX_POLICY_VALUES then + ---@type table + new_component = {} - if not destroying_entity_set[old_target] then - new_target_count = new_target_count + 1 - new_target_list[new_target_count] = old_target + for old_key, old_target in __lua_next, old_component do + if not destroying_entity_set[old_target] then + new_target_count = new_target_count + 1 + new_component[old_key] = old_target + end end end @@ -4786,11 +5011,7 @@ function __unlink_entity_targets(entity, fragment, destroying_entity_set) return end - component_storage[place] = new_target_list - - local fragment_search_index = chunk.__has_index_fragments - and __search_indices[fragment] - or nil + component_storage[place] = new_component if fragment_search_index then __search_index_fns.filter(fragment_search_index, entity, destroying_entity_set) @@ -4801,11 +5022,11 @@ function __unlink_entity_targets(entity, fragment, destroying_entity_set) local fragment_assign = chunk.__component_assigns[component_index] if fragment_set then - fragment_set(entity, fragment, new_target_list, old_target_list) + fragment_set(entity, fragment, new_component, old_component) end if fragment_assign then - fragment_assign(entity, fragment, new_target_list, old_target_list) + fragment_assign(entity, fragment, new_component, old_component) end end end @@ -7610,15 +7831,17 @@ function __evolved_multi_search_to(out_entity_list, out_entity_first, fragment, local head_entity = head_entities[component] if head_entity then - local key_offset = fragment_search_index.__combined_keys - and component % 1048576 * 1048576 - or 0 + local component_keys = fragment_search_index.__component_keys + + local component_offset = not fragment_search_index.__multi_components and 0 + or component_keys and component_keys[component] * 1048576 + or component % 1048576 * 1048576 local entity = head_entity repeat out_entity_list[out_entity_first + entity_count] = entity - entity = next_entities[key_offset + entity % 1048576] + entity = next_entities[component_offset + entity % 1048576] entity_count = entity_count + 1 until entity == head_entity end @@ -7787,6 +8010,43 @@ function __evolved_collect_garbage(no_shrink) end end + for _, search_index in __lua_next, __search_indices do + if not no_shrink then + search_index.__head_entities = __table_fns.dup(search_index.__head_entities) + search_index.__prev_entities = __table_fns.dup(search_index.__prev_entities) + search_index.__next_entities = __table_fns.dup(search_index.__next_entities) + search_index.__entity_components = __table_fns.dup(search_index.__entity_components) + end + + if search_index.__collected_components then + if not no_shrink then + search_index.__collected_components = {} + else + __lua_table_clear(search_index.__collected_components, false, true) + end + end + + if search_index.__component_keys and not no_shrink then + if __lua_next(search_index.__component_keys) then + search_index.__component_keys = __table_fns.dup( + search_index.__component_keys) + search_index.__free_component_keys = __list_fns.dup( + search_index.__free_component_keys, + search_index.__free_component_key_count) + else + search_index.__component_keys = {} + search_index.__component_key_count = 0 + search_index.__free_component_keys = {} + search_index.__free_component_key_count = 0 + end + end + + if search_index.__component_epochs and not no_shrink then + search_index.__component_epochs = {} + search_index.__component_epoch = 0 + end + end + if not no_shrink then for table_pool_tag = 1, __table_pool_tag.__count do local table_pool_reserve = __tagged_table_pools[table_pool_tag].__reserve From bd3c3d4a0a5447477a1db51e4bbba9612ed0329c Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 28 Oct 2026 21:38:36 +0700 Subject: [PATCH 45/48] update readme --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e41104a..c7af22b 100644 --- a/README.md +++ b/README.md @@ -1233,11 +1233,48 @@ evolved.reindex(team) assert(evolved.search(team, 'blue') == unit) ``` +By default, the whole component is used as a single value in the index. But a component can also hold several values, for example, a list of tags or a set of grid cells that a unit occupies. To index every value of such a component, use the [`evolved.INDEX_POLICY`](#evolvedindex_policy) fragment trait. This trait expects one of the following predefined identifiers: + +- [`evolved.INDEX_POLICY_SELF`](#evolvedindex_policy_self) will index the component itself as a single value. This is the default behavior, so you don't have to set it explicitly, but you can if you want. + +- [`evolved.INDEX_POLICY_LIST`](#evolvedindex_policy_list) will index every item of a list, like `{ 'red', 'big' }`. + +- [`evolved.INDEX_POLICY_KEYS`](#evolvedindex_policy_keys) will index every key of a table, like `{ red = true, big = true }`. The values of such tables are not indexed. + +- [`evolved.INDEX_POLICY_VALUES`](#evolvedindex_policy_values) will index every value of a table, like `{ left_hand = 'sword', right_hand = 'shield' }`. The keys of such tables are not indexed. + +```lua +local evolved = require 'evolved' + +local tags = evolved.builder() + :index() + :index_policy(evolved.INDEX_POLICY_KEYS) + :build() + +local unit1 = evolved.builder() + :set(tags, { flying = true, enemy = true }) + :build() + +local unit2 = evolved.builder() + :set(tags, { enemy = true }) + :build() + +assert(evolved.search(tags, 'flying') == unit1) + +local enemy_list, enemy_count = evolved.multi_search(tags, 'enemy') +assert(enemy_count == 2 and enemy_list[1] == unit1 and enemy_list[2] == unit2) +``` + +With these policies, components of the indexed fragment must be tables of the corresponding shape. An entity is found once by every value of its component, even if this value occurs in the component several times. When such a component changes, the entity keeps its place among the entities with the values that are still in the component, and it is moved to the end only for the new values. The index policy can be changed at any time, and the index is rebuilt from the current components when it happens. + > [!NOTE] > Searching by a fragment without the [`evolved.INDEX`](#evolvedindex) or [`evolved.LINK`](#evolvedlink) trait finds nothing and prints a warning. The [`evolved.reindex`](#evolvedreindex) function prints a warning for such fragments too and skips them. > [!NOTE] -> 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. +> Components and their values are used as keys in the index, so tables are indexed by reference, and searching by another table with the same content will not find anything. Also, [Fragment Tags](#fragment-tags) and `NaN` values are not indexed at all. + +> [!NOTE] +> Tables of multi-valued components changed in place bypass the index just like components changed in chunk storages. After such changes, set the components again or update the index using the [`evolved.reindex`](#evolvedreindex) function. > [!NOTE] > 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. @@ -1409,7 +1446,7 @@ assert(evolved.alive(sword) and not evolved.has(sword, owner)) You can change this behavior by using the [`evolved.LINK_POLICY`](#evolvedlink_policy) fragment trait. This trait expects one of the following predefined identifiers: -- [`evolved.LINK_POLICY_UNLINK_TARGET`](#evolvedlink_policy_unlink_target) will remove only the destroyed entity from lists of entities that link to it, see the example with children below. Single links are removed together with the linked fragment, as with the default policy. +- [`evolved.LINK_POLICY_UNLINK_TARGET`](#evolvedlink_policy_unlink_target) will remove only the destroyed entity from lists, sets, and maps of entities that link to it, see the example with children below. Single links are removed together with the linked fragment, as with the default policy. - [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) will destroy any entity that links to the destroyed entity. Links of destroyed entities are resolved too, so this is useful for hierarchies, where all descendants should be destroyed together with their root. @@ -1465,13 +1502,14 @@ local child_list, child_count = evolved.multi_search(parent, root) assert(child_count == 2 and child_list[1] == child1 and child_list[2] == child2) ``` -A component of a linked fragment can also be a list of entities. Unlike regular [indexed fragments](#indexed-fragments), where table components are indexed by reference, such a list links to every entity in it. So you can find the entity that holds the list by any entity from this list, and the link policy is applied as soon as any of these entities is destroyed. With the default policy, the whole list is removed, and with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy, the entity that holds the list is destroyed. +A component of a linked fragment can also hold several entities. Just like with [indexed fragments](#indexed-fragments), use the [`evolved.INDEX_POLICY`](#evolvedindex_policy) fragment trait for that, and the component will link to every entity in its list, set, or map. So you can find the entity that holds such a component by any entity in it, and the link policy is applied as soon as any of these entities is destroyed. With the default policy, the whole component is removed, and with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy, the entity that holds it is destroyed. ```lua local evolved = require 'evolved' local attached_to = evolved.builder() :link() + :index_policy(evolved.INDEX_POLICY_LIST) :build() local pillar1 = evolved.builder() @@ -1495,7 +1533,7 @@ evolved.destroy(pillar1) assert(evolved.alive(rope) and not evolved.has(rope, attached_to)) ``` -Removing the whole list is not always what you want. For example, a parent can store the list of its children, and destroying one child should not take the other children away from it. In such cases, use the [`evolved.LINK_POLICY_UNLINK_TARGET`](#evolvedlink_policy_unlink_target) policy. With this policy, the destroyed entity is removed from the list, and the other entities stay there in the same order. If the list becomes empty, the linked fragment is removed, as if it were a single link. +Removing the whole list is not always what you want. For example, a parent can store the list of its children, and destroying one child should not take the other children away from it. In such cases, use the [`evolved.LINK_POLICY_UNLINK_TARGET`](#evolvedlink_policy_unlink_target) policy. With this policy, the destroyed entity is removed from the list, and the other entities stay there in the same order. Sets and maps lose only their entries with the destroyed entity. The component is not changed in place, a new table is assigned instead, so the [`evolved.ON_SET`](#evolvedon_set) and [`evolved.ON_ASSIGN`](#evolvedon_assign) hooks of the fragment receive both tables. If the component becomes empty, the linked fragment is removed, as if it were a single link. ```lua local evolved = require 'evolved' @@ -1507,6 +1545,7 @@ local parent = evolved.builder() local children = evolved.builder() :link() + :index_policy(evolved.INDEX_POLICY_LIST) :link_policy(evolved.LINK_POLICY_UNLINK_TARGET) :build() @@ -1537,7 +1576,7 @@ evolved.destroy(child2) assert(evolved.alive(root) and not evolved.has(root, children)) ``` -The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are linked this way with the [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policy. So you can also find all queries that use some fragment, or all fragments that require it. +The [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), [`evolved.VARIANTS`](#evolvedvariants), and [`evolved.REQUIRES`](#evolvedrequires) fragments are linked this way with the [`evolved.INDEX_POLICY_LIST`](#evolvedindex_policy_list) and [`evolved.LINK_POLICY_DESTROY_ENTITY`](#evolvedlink_policy_destroy_entity) policies. So you can also find all queries that use some fragment, or all fragments that require it. ```lua local evolved = require 'evolved' @@ -1558,10 +1597,10 @@ assert(not evolved.alive(query)) ``` > [!NOTE] -> Links must refer to alive entities. In [debug mode](#debug-mode), linking to an entity that is not alive is an error. Otherwise, such links are not indexed, so they cannot be found by the search functions and are never resolved. +> Links must refer to alive entities. Links to entities that are not alive are not indexed, so they cannot be found by the search functions and are never resolved. Such links are reported with a warning. > [!NOTE] -> Components changed directly in chunk storages bypass the index, and so the links too. The same applies to lists changed in place. After such changes, set the components again or update the index using the [`evolved.reindex`](#evolvedreindex) function, otherwise these links will not be resolved when their entities are destroyed. +> Components changed directly in chunk storages bypass the index, and so the links too. The same applies to lists, sets, and maps changed in place. After such changes, set the components again or update the index using the [`evolved.reindex`](#evolvedreindex) function, otherwise these links will not be resolved when their entities are destroyed. #### Destruction Policies @@ -1776,7 +1815,7 @@ While using the library, some internal data structures can become obsolete and s function evolved.collect_garbage(no_shrink) end ``` -By default, [`evolved.collect_garbage`](#evolvedcollect_garbage) cleans up obsolete data structures and shrinks component storages to fit their current size. If you pass `true`, it only cleans up obsolete data structures and skips shrinking. This avoids the overhead of resizing storages, which can be expensive. +By default, [`evolved.collect_garbage`](#evolvedcollect_garbage) cleans up obsolete data structures and shrinks component storages and [indices](#indexed-fragments) to fit their current size. If you pass `true`, it only cleans up obsolete data structures and skips shrinking. This avoids the overhead of resizing storages and rebuilding indices, which can be expensive. Call this function periodically to keep memory usage under control. It is best to call it between levels or during loading screens when performance is not critical. Also, call Lua's built-in garbage collector afterward to ensure all unused memory is freed. @@ -2046,6 +2085,7 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z - Added the new [`LINK`](#evolvedlink) and [`LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section +- Added the new [`INDEX_POLICY`](#evolvedindex_policy) fragment trait that allows indexing and linking every value of lists, sets, and maps, see the [Indexed Fragments](#indexed-fragments) section - The [`GROUP`](#evolvedgroup), [`QUERY`](#evolvedquery), [`INCLUDES`](#evolvedincludes), [`EXCLUDES`](#evolvedexcludes), [`VARIANTS`](#evolvedvariants), and [`REQUIRES`](#evolvedrequires) fragments are now linked - Query iteration order no longer depends on the order in which chunks were created - The [`NAME`](#evolvedname) fragment is now marked as [UNIQUE](#unique-fragments), so clones no longer inherit the names of their prefabs From 71c395efa6843f966dd5db6fa44149386f7e44b9 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 28 Oct 2026 21:48:37 +0700 Subject: [PATCH 46/48] fix gc of indicies --- develop/testing/multi_index_tests.lua | 17 +++++++++++++++++ evolved.lua | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/develop/testing/multi_index_tests.lua b/develop/testing/multi_index_tests.lua index 6dbb0d7..4cf8feb 100644 --- a/develop/testing/multi_index_tests.lua +++ b/develop/testing/multi_index_tests.lua @@ -623,3 +623,20 @@ do local e4 = evo.spawn { [fk] = { shared = true } } check_search(fk, 'shared', e4) end + +do + local f = evo.builder():index():index_policy(evo.INDEX_POLICY_LIST):build() + + local e = evo.spawn { [f] = { false } } + + -- false is the only interned value here, so interned values must not look empty + evo.collect_garbage() + check_search(f, false, e) + + evo.set(e, f, { true }) + check_search(f, false) + check_search(f, true, e) + + evo.destroy(e) + check_search(f, true) +end diff --git a/evolved.lua b/evolved.lua index 42e9d39..bf2faad 100644 --- a/evolved.lua +++ b/evolved.lua @@ -8027,7 +8027,7 @@ function __evolved_collect_garbage(no_shrink) end if search_index.__component_keys and not no_shrink then - if __lua_next(search_index.__component_keys) then + if __lua_next(search_index.__component_keys) ~= nil then search_index.__component_keys = __table_fns.dup( search_index.__component_keys) search_index.__free_component_keys = __list_fns.dup( From 2fab28329748627fccb8fe56ef36464c5535a2e5 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 28 Oct 2026 22:14:15 +0700 Subject: [PATCH 47/48] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7af22b..8ea1f38 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ An identifier is a packed 40-bit integer. The first 20 bits represent the index, function evolved.id(count) end ``` -The `count` parameter is optional and defaults to `1`. The function returns one or more identifiers depending on the `count` parameter. The maximum number of alive identifiers is `2^20-1` (1048575). After that, the function will throw an error: `| evolved.lua | id index overflow`. +The `count` parameter is optional and defaults to `1`. The function returns one or more identifiers depending on the `count` parameter. The maximum number of alive identifiers is `2^20-1` (1048575). After that, the function will throw an error: `| evolved.lua (e) | id index overflow`. Identifiers can be recycled. When an identifier is no longer needed, use the [`evolved.destroy`](#evolveddestroy) function to destroy it. This will free the identifier for reuse. @@ -691,7 +691,7 @@ evolved.destroy(fragment) -- try to use the destroyed fragment evolved.set(entity, fragment, 42) --- [error] | evolved.lua | the fragment ($1048599#23:1) is not alive and cannot be used +-- [error] | evolved.lua (e) | the fragment ($1048599#23:1) is not alive and cannot be set ``` ### Queries From 48f81b16f91aae3f100065c4fea976acacb4e4e4 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Wed, 28 Oct 2026 22:19:39 +0700 Subject: [PATCH 48/48] v1.13.0 --- README.md | 4 +-- evolved.lua | 2 +- rockspecs/evolved.lua-1.13.0-0.rockspec | 34 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 rockspecs/evolved.lua-1.13.0-0.rockspec diff --git a/README.md b/README.md index 8ea1f38..8e8b7e5 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ - [Chunk](#chunk) - [Builder](#builder) - [Changelog](#changelog) - - [vX.Y.Z](#vxyz) + - [v1.13.0](#v1130) - [v1.12.0](#v1120) - [v1.11.1](#v1111) - [v1.11.0](#v1110) @@ -2082,7 +2082,7 @@ builder_mt:destruction_policy :: id -> builder ## Changelog -### vX.Y.Z +### v1.13.0 - Added the new [`LINK`](#evolvedlink) and [`LINK_POLICY`](#evolvedlink_policy) fragment traits that resolve links to destroyed entities, see the [Linked Fragments](#linked-fragments) section - Added the new [`INDEX_POLICY`](#evolvedindex_policy) fragment trait that allows indexing and linking every value of lists, sets, and maps, see the [Indexed Fragments](#indexed-fragments) section diff --git a/evolved.lua b/evolved.lua index bf2faad..c1a8f33 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.12.0', + __VERSION = '1.13.0', __LICENSE = [[ MIT License diff --git a/rockspecs/evolved.lua-1.13.0-0.rockspec b/rockspecs/evolved.lua-1.13.0-0.rockspec new file mode 100644 index 0000000..a78230d --- /dev/null +++ b/rockspecs/evolved.lua-1.13.0-0.rockspec @@ -0,0 +1,34 @@ +rockspec_format = "3.0" +package = "evolved.lua" +version = "1.13.0-0" +source = { + url = "git://github.com/BlackMATov/evolved.lua", + tag = "v1.13.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", + } +}