diff --git a/README.md b/README.md index ddc1784..5e1f109 100644 --- a/README.md +++ b/README.md @@ -588,16 +588,22 @@ evolved.set(entity, fragment, 42) One of the most important features of any ECS library is the ability to process entities by filters or queries. `evolved.lua` provides a simple and efficient way to do this. -First, you need to create a query that describes which entities you want to process. You can specify fragments you want to include, and fragments you want to exclude. Queries are just identifiers with a special predefined fragments: [`evolved.INCLUDES`](#evolvedincludes) and [`evolved.EXCLUDES`](#evolvedexcludes). These fragments expect a list of fragments as their components. +First, you need to create a query that describes which entities you want to process. You can specify fragments you want to include, and fragments you want to exclude. Queries are just identifiers with a special predefined fragments: [`evolved.INCLUDES`](#evolvedincludes), [`evolved.EXCLUDES`](#evolvedexcludes), and [`evolved.VARIANTS`](#evolvedvariants). These fragments expect a list of fragments as their components. + +- [`evolved.INCLUDES`](#evolvedincludes) is used to specify fragments that must be present in the entity; +- [`evolved.EXCLUDES`](#evolvedexcludes) is used to specify fragments that must not be present in the entity; +- [`evolved.VARIANTS`](#evolvedvariants) is used to specify fragments where at least one must be present in the entity. ```lua local evolved = require 'evolved' local health, poisoned, resistant = evolved.id(3) +local alive, undead = evolved.id(2) local query = evolved.id() evolved.set(query, evolved.INCLUDES, { health, poisoned }) evolved.set(query, evolved.EXCLUDES, { resistant }) +evolved.set(query, evolved.VARIANTS, { alive, undead }) ``` The builder interface can be used to create queries too. It is more convenient to use, because the builder has special methods for including and excluding fragments. Here is a simple example of this: @@ -606,10 +612,11 @@ The builder interface can be used to create queries too. It is more convenient t local query = evolved.builder() :include(health, poisoned) :exclude(resistant) + :variant(alive, undead) :build() ``` -We don't have to set both [`evolved.INCLUDES`](#evolvedincludes) and [`evolved.EXCLUDES`](#evolvedexcludes) fragments, we can even do it without filters at all, then the query will match all chunks in the world. +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. 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. @@ -788,7 +795,7 @@ The [`evolved.process`](#evolvedprocess) function is used to process systems. It function evolved.process(...) end ``` -If you don't specify a query for the system, the system itself will be treated as a query. This means the system can contain `evolved.INCLUDES` and `evolved.EXCLUDES` fragments, and it will be processed according to them. This is useful for creating systems with unique queries that don't need to be reused in other systems. +If you don't specify a query for the system, the system itself will be treated as a query. This means the system can contain `evolved.INCLUDES`, `evolved.EXCLUDES`, and `evolved.VARIANTS` fragments, and it will be processed according to them. This is useful for creating systems with unique queries that don't need to be reused in other systems. ```lua local evolved = require 'evolved' @@ -1198,6 +1205,7 @@ DISABLED :: fragment INCLUDES :: fragment EXCLUDES :: fragment +VARIANTS :: fragment REQUIRES :: fragment ON_SET :: fragment @@ -1332,6 +1340,7 @@ builder_mt:disabled :: builder builder_mt:include :: fragment... -> builder builder_mt:exclude :: fragment... -> builder +builder_mt:variant :: fragment... -> builder builder_mt:require :: fragment... -> builder builder_mt:on_set :: {entity, fragment, component, component} -> builder @@ -1354,6 +1363,7 @@ builder_mt:destruction_policy :: id -> builder ### vX.Y.Z +- Added the new [`evolved.VARIANTS`](#evolvedvariants) query fragment that allows specifying any of multiple fragments in queries - Added the new [`evolved.process_with`](#evolvedprocess_with) function that allows passing payloads to processing systems ### v1.6.0 @@ -1428,6 +1438,8 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.EXCLUDES` +### `evolved.VARIANTS` + ### `evolved.REQUIRES` ### `evolved.ON_SET` @@ -2065,6 +2077,14 @@ function evolved.builder_mt:include(...) end function evolved.builder_mt:exclude(...) end ``` +#### `evolved.builder_mt:variant` + +```lua +---@param ... evolved.fragment fragments +---@return evolved.builder builder +function evolved.builder_mt:variant(...) end +``` + ### `evolved.builder_mt:require` ```lua diff --git a/develop/fuzzing/execute_fuzz.lua b/develop/fuzzing/execute_fuzz.lua index 11bf6c0..044c007 100644 --- a/develop/fuzzing/execute_fuzz.lua +++ b/develop/fuzzing/execute_fuzz.lua @@ -56,6 +56,20 @@ local function generate_query(query) end end + local variant_set = {} + local variant_list = {} + local variant_count = 0 + + for _ = 1, math.random(0, #all_fragment_list) do + local variant = all_fragment_list[math.random(1, #all_fragment_list)] + + if not variant_set[variant] then + variant_count = variant_count + 1 + variant_set[variant] = variant_count + variant_list[variant_count] = variant + end + end + if include_count > 0 then evo.set(query, evo.INCLUDES, include_list) end @@ -63,6 +77,10 @@ local function generate_query(query) if exclude_count > 0 then evo.set(query, evo.EXCLUDES, exclude_list) end + + if variant_count > 0 then + evo.set(query, evo.VARIANTS, variant_list) + end end ---@param query_count integer @@ -173,12 +191,22 @@ local function execute_query(query) local query_include_list = evo.get(query, evo.INCLUDES) or {} local query_exclude_list = evo.get(query, evo.EXCLUDES) or {} + local query_variant_list = evo.get(query, evo.VARIANTS) or {} + + local query_include_count = #query_include_list + local query_exclude_count = #query_exclude_list + local query_variant_count = #query_variant_list local query_include_set = {} for _, include in ipairs(query_include_list) do query_include_set[include] = true end + local query_variant_set = {} + for _, variant in ipairs(query_variant_list) do + query_variant_set[variant] = true + end + for chunk, entity_list, entity_count in evo.execute(query) do assert(not query_chunk_set[chunk]) query_chunk_set[chunk] = true @@ -189,19 +217,29 @@ local function execute_query(query) query_entity_set[entity] = true end - assert(chunk:has_all(__table_unpack(query_include_list))) - assert(not chunk:has_any(__table_unpack(query_exclude_list))) + if query_include_count > 0 then + assert(chunk:has_all(__table_unpack(query_include_list))) + end + + if query_exclude_count > 0 then + assert(not chunk:has_any(__table_unpack(query_exclude_list))) + end + + if query_variant_count > 0 then + assert(chunk:has_any(__table_unpack(query_variant_list))) + end end for i = 1, all_entity_count do local entity = all_entity_list[i] local is_entity_matched = - evo.has_all(entity, __table_unpack(query_include_list)) - and not evo.has_any(entity, __table_unpack(query_exclude_list)) + (query_variant_count == 0 or evo.has_any(entity, __table_unpack(query_variant_list))) and + (query_include_count == 0 or evo.has_all(entity, __table_unpack(query_include_list))) and + (query_exclude_count == 0 or not evo.has_any(entity, __table_unpack(query_exclude_list))) for fragment in evo.each(entity) do - if evo.has(fragment, evo.EXPLICIT) and not query_include_set[fragment] then + if evo.has(fragment, evo.EXPLICIT) and not query_variant_set[fragment] and not query_include_set[fragment] then is_entity_matched = false end end @@ -236,10 +274,13 @@ for _ = 1, math.random(1, 5) do if math.random(1, 2) == 1 then generate_query(query) else - if math.random(1, 2) == 1 then + local r = math.random(1, 3) + if r == 1 then evo.remove(query, evo.INCLUDES) - else + elseif r == 2 then evo.remove(query, evo.EXCLUDES) + else + evo.remove(query, evo.VARIANTS) end end end diff --git a/evolved.d.tl b/evolved.d.tl index d78e4a0..8e390b0 100644 --- a/evolved.d.tl +++ b/evolved.d.tl @@ -69,6 +69,7 @@ include: function(self: Builder, ...: Fragment): Builder exclude: function(self: Builder, ...: Fragment): Builder + variant: function(self: Builder, ...: Fragment): Builder require: function(self: Builder, ...: Fragment): Builder on_set: function(self: Builder, on_set: function(Entity, Fragment, ? Component, ? Component)): Builder @@ -102,6 +103,7 @@ INCLUDES: Fragment EXCLUDES: Fragment + VARIANTS: Fragment REQUIRES: Fragment ON_SET: Fragment diff --git a/evolved.lua b/evolved.lua index c7888c1..535df2a 100644 --- a/evolved.lua +++ b/evolved.lua @@ -81,7 +81,9 @@ local evolved = { ---@field package [1] integer structural_changes ---@field package [2] evolved.chunk[] chunk_stack ---@field package [3] integer chunk_stack_size ----@field package [4] table? exclude_set +---@field package [4] table? include_set +---@field package [5] table? exclude_set +---@field package [6] table? variant_set ---@alias evolved.each_iterator fun( --- state: evolved.each_state?): @@ -135,6 +137,7 @@ local __entity_places = {} ---@type table local __sorted_includes = {} ---@type table> local __sorted_excludes = {} ---@type table> +local __sorted_variants = {} ---@type table> local __sorted_requires = {} ---@type table> local __subsystem_groups = {} ---@type table @@ -973,6 +976,7 @@ local __DISABLED = __acquire_id() local __INCLUDES = __acquire_id() local __EXCLUDES = __acquire_id() +local __VARIANTS = __acquire_id() local __REQUIRES = __acquire_id() local __ON_SET = __acquire_id() @@ -1106,6 +1110,9 @@ 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 @@ -1115,7 +1122,6 @@ local __chunk_without_fragment local __chunk_without_fragments local __chunk_without_unique_fragments -local __chunk_matches local __chunk_requires local __chunk_fragments local __chunk_components @@ -1397,7 +1403,7 @@ function __update_chunk_queries(chunk) local major_query_chunks = __query_chunks[major_query] if major_query_chunks then - if __chunk_matches(chunk, major_query) then + if __query_major_matches(chunk, major_query) then __assoc_list_insert(major_query_chunks, chunk) else __assoc_list_remove(major_query_chunks, chunk) @@ -1574,16 +1580,15 @@ function __cache_query_chunks(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 query_include_count == 0 then - __error_fmt('the query (%s) has no include fragments and cannot be cached', - __id_name(query)) - end + 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_new(4) __query_chunks[query] = query_chunks - do + if query_include_count > 0 then local query_major = query_include_list[query_include_count] local major_chunks = __major_chunks[query_major] @@ -1593,12 +1598,30 @@ function __cache_query_chunks(query) for major_chunk_index = 1, major_chunk_count do local major_chunk = major_chunk_list[major_chunk_index] - if __chunk_matches(major_chunk, query) then + if __query_major_matches(major_chunk, query) then __assoc_list_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_insert(query_chunks, major_chunk) + end + end + end + end + return query_chunks end @@ -1607,6 +1630,87 @@ 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 + + if chunk.__has_explicit_fragments then + local chunk_fragment_list = chunk.__fragment_list + local chunk_fragment_count = chunk.__fragment_count + + for chunk_fragment_index = 1, chunk_fragment_count do + local chunk_fragment = chunk_fragment_list[chunk_fragment_index] + + local is_chunk_fragment_matched = + (not __evolved_has(chunk_fragment, __EXPLICIT)) or + (query_variant_count > 0 and query_variant_set[chunk_fragment]) or + (query_include_count > 0 and query_include_set[chunk_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 @@ -1787,50 +1891,6 @@ function __chunk_without_unique_fragments(chunk) return sib_chunk end ----@param chunk evolved.chunk ----@param query evolved.query ----@return boolean ----@nodiscard -function __chunk_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 - elseif chunk.__has_explicit_fragments then - return false - 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 - - if chunk.__has_explicit_fragments then - local chunk_fragment_list = chunk.__fragment_list - local chunk_fragment_count = chunk.__fragment_count - - for chunk_fragment_index = 1, chunk_fragment_count do - local chunk_fragment = chunk_fragment_list[chunk_fragment_index] - - if not query_include_set[chunk_fragment] and __evolved_has(chunk_fragment, __EXPLICIT) then - return false - end - end - end - - return true -end - ---@param chunk evolved.chunk ---@return evolved.chunk ---@nodiscard @@ -3862,7 +3922,9 @@ function __iterator_fns.__execute_iterator(execute_state) local structural_changes = execute_state[1] local chunk_stack = execute_state[2] local chunk_stack_size = execute_state[3] - local exclude_set = execute_state[4] + local include_set = execute_state[4] + local exclude_set = execute_state[5] + local variant_set = execute_state[6] if structural_changes ~= __structural_changes then __error_fmt('structural changes are prohibited during iteration') @@ -3882,7 +3944,9 @@ function __iterator_fns.__execute_iterator(execute_state) local chunk_child_fragment = chunk_child.__fragment local is_chunk_child_matched = - (not chunk_child.__has_explicit_major) and + (not chunk_child.__has_explicit_major or ( + (include_set and include_set[chunk_child_fragment]) or + (variant_set and variant_set[chunk_child_fragment]))) and (not exclude_set or not exclude_set[chunk_child_fragment]) if is_chunk_child_matched then @@ -5232,13 +5296,18 @@ function __evolved_execute(query) local chunk_stack_size = 0 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_excludes = __sorted_excludes[query] local query_exclude_set = query_excludes and query_excludes.__item_set local query_exclude_count = query_excludes and query_excludes.__item_count or 0 - if query_include_count > 0 then + local query_variants = __sorted_variants[query] + local query_variant_set = query_variants and query_variants.__item_set + 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 @@ -5279,7 +5348,9 @@ function __evolved_execute(query) execute_state[1] = __structural_changes execute_state[2] = chunk_stack execute_state[3] = chunk_stack_size - execute_state[4] = query_exclude_set + execute_state[4] = query_include_set + execute_state[5] = query_exclude_set + execute_state[6] = query_variant_set return __iterator_fns.__execute_iterator, execute_state end @@ -6040,6 +6111,31 @@ function __builder_mt:exclude(...) return self:set(__EXCLUDES, exclude_list) end +---@param ... evolved.fragment fragments +---@return evolved.builder builder +function __builder_mt:variant(...) + local argument_count = __lua_select('#', ...) + + if argument_count == 0 then + return self + end + + local variant_list = self:get(__VARIANTS) + local variant_count = variant_list and #variant_list or 0 + + if variant_count == 0 then + variant_list = __list_new(argument_count) + end + + for argument_index = 1, argument_count do + ---@type evolved.fragment + local fragment = __lua_select(argument_index, ...) + variant_list[variant_count + argument_index] = fragment + end + + return self:set(__VARIANTS, variant_list) +end + ---@param ... evolved.fragment fragments ---@return evolved.builder builder function __builder_mt:require(...) @@ -6188,6 +6284,7 @@ __evolved_set(__DISABLED, __NAME, 'DISABLED') __evolved_set(__INCLUDES, __NAME, 'INCLUDES') __evolved_set(__EXCLUDES, __NAME, 'EXCLUDES') +__evolved_set(__VARIANTS, __NAME, 'VARIANTS') __evolved_set(__REQUIRES, __NAME, 'REQUIRES') __evolved_set(__ON_SET, __NAME, 'ON_SET') @@ -6228,6 +6325,7 @@ __evolved_set(__DISABLED, __INTERNAL) __evolved_set(__INCLUDES, __INTERNAL) __evolved_set(__EXCLUDES, __INTERNAL) +__evolved_set(__VARIANTS, __INTERNAL) __evolved_set(__REQUIRES, __INTERNAL) __evolved_set(__ON_SET, __INTERNAL) @@ -6277,6 +6375,9 @@ __evolved_set(__INCLUDES, __DUPLICATE, __list_dup) __evolved_set(__EXCLUDES, __DEFAULT, __list_new()) __evolved_set(__EXCLUDES, __DUPLICATE, __list_dup) +__evolved_set(__VARIANTS, __DEFAULT, __list_new()) +__evolved_set(__VARIANTS, __DUPLICATE, __list_dup) + __evolved_set(__REQUIRES, __DEFAULT, __list_new()) __evolved_set(__REQUIRES, __DUPLICATE, __list_dup) @@ -6297,17 +6398,38 @@ local function __insert_query(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_new(4) __major_queries[query_major] = major_queries end __assoc_list_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_new(4) + __major_queries[query_variant] = major_queries + end + + __assoc_list_insert(major_queries, query) + end + end end ---@param query evolved.query @@ -6316,6 +6438,10 @@ local function __remove_query(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] @@ -6325,9 +6451,27 @@ local function __remove_query(query) 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_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) @@ -6404,6 +6548,44 @@ end) --- --- +---@param query evolved.query +---@param variant_list evolved.fragment[] +__evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) + __remove_query(query) + + local variant_count = #variant_list + + if variant_count > 0 then + ---@type evolved.assoc_list + local sorted_variants = __assoc_list_new(variant_count) + + __assoc_list_move(variant_list, 1, variant_count, sorted_variants) + __assoc_list_sort(sorted_variants) + + __sorted_variants[query] = sorted_variants + else + __sorted_variants[query] = nil + end + + __insert_query(query) + __update_major_chunks(query) +end) + +__evolved_set(__VARIANTS, __ON_REMOVE, function(query) + __remove_query(query) + + __sorted_variants[query] = nil + + __insert_query(query) + __update_major_chunks(query) +end) + +--- +--- +--- +--- +--- + ---@param fragment evolved.fragment ---@param require_list evolved.fragment[] __evolved_set(__REQUIRES, __ON_SET, function(fragment, _, require_list) @@ -6506,6 +6688,7 @@ evolved.DISABLED = __DISABLED evolved.INCLUDES = __INCLUDES evolved.EXCLUDES = __EXCLUDES +evolved.VARIANTS = __VARIANTS evolved.REQUIRES = __REQUIRES evolved.ON_SET = __ON_SET