diff --git a/README.md b/README.md index 5e97ce1..8e8b7e5 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) + - [v1.13.0](#v1130) - [v1.12.0](#v1120) - [v1.11.1](#v1111) - [v1.11.0](#v1110) @@ -153,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. @@ -689,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 @@ -726,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 @@ -975,6 +979,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 @@ -1050,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. @@ -1179,7 +1203,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. @@ -1209,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) 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 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. @@ -1242,6 +1303,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. @@ -1354,6 +1417,191 @@ 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. + +```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_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. + +- [`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) +``` + +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() + :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)) +``` + +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' + +local parent = evolved.builder() + :link() + :link_policy(evolved.LINK_POLICY_DESTROY_ENTITY) + :build() + +local children = evolved.builder() + :link() + :index_policy(evolved.INDEX_POLICY_LIST) + :link_policy(evolved.LINK_POLICY_UNLINK_TARGET) + :build() + +local root = evolved.builder() + :build() + +local child1 = evolved.builder() + :set(parent, root) + :build() + +local child2 = evolved.builder() + :set(parent, root) + :build() + +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.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' + +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. 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, 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 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. @@ -1567,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. @@ -1623,6 +1871,7 @@ TAG :: fragment NAME :: fragment INDEX :: fragment +LINK :: fragment UNIQUE :: fragment EXPLICIT :: fragment INTERNAL :: fragment @@ -1654,6 +1903,17 @@ 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 +LINK_POLICY_REMOVE_FRAGMENT :: id + DESTRUCTION_POLICY :: fragment DESTRUCTION_POLICY_DESTROY_ENTITY :: id DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id @@ -1783,6 +2043,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 +2075,27 @@ 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 ``` ## Changelog +### 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 +- 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 +- 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 - Lookup can now find internal fragments by their names: [#52](https://github.com/BlackMATov/evolved.lua/issues/52) @@ -1912,6 +2189,8 @@ builder_mt:destruction_policy :: id -> builder ### `evolved.INDEX` +### `evolved.LINK` + ### `evolved.UNIQUE` ### `evolved.EXPLICIT` @@ -1956,6 +2235,24 @@ 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` + +### `evolved.LINK_POLICY_DESTROY_ENTITY` + +### `evolved.LINK_POLICY_REMOVE_FRAGMENT` + ### `evolved.DESTRUCTION_POLICY` ### `evolved.DESTRUCTION_POLICY_DESTROY_ENTITY` @@ -2709,6 +3006,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 +3184,22 @@ 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 +---@param link_policy evolved.id +---@return evolved.builder builder +function evolved.builder_mt:link_policy(link_policy) end +``` + #### `evolved.builder_mt:destruction_policy` ```lua 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/develop/all.lua b/develop/all.lua index ba4677b..6b5e2b8 100644 --- a/develop/all.lua +++ b/develop/all.lua @@ -1,4 +1,8 @@ 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' @@ -6,10 +10,12 @@ 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' 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' @@ -38,10 +44,20 @@ 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' 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 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/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/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/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 new file mode 100644 index 0000000..24ec762 --- /dev/null +++ b/develop/fuzzing/link_fuzz.lua @@ -0,0 +1,631 @@ +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 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) + 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 + +---@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 + local builder = evo.builder():link() + + if math.random(1, 2) == 1 then + builder:on_remove(remove_hook) + end + + 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_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 + + if math.random(1, 2) == 1 then + 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 + 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[] + +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 + +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 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_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 + + 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(link_fragment)) + 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, 4) + + if r == 1 then + evo.set(entity, link_fragment, random_link_component(link_fragment)) + elseif r == 2 then + evo.remove(entity, link_fragment) + elseif r == 3 then + 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 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 +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_component = evo.get(entity, link_fragment) + + if link_component then + 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 + break + end + end + end + end + + if should_be_destroyed_entity_set[entity] then + break + end + end + end + end +until not has_new_destroyed_entities + +local entity_link_target_lists = {} ---@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) + + if link_component then + local link_target_list = {} + + 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 = {} +assign_hook_calls = {} + +--- +--- +--- +--- +--- + +local destroying_mode = math.random(1, 6) + +do + local r = destroying_mode + + 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_component = evo.get(entity, link_fragment) + + if link_component then + for _, link_target in ipairs(link_component_targets(link_fragment, link_component)) do + assert(evo.alive(link_target)) + end + end + 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_component = evo.get(entity, link_fragment) + + if link_component then + 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 + + 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(link_fragment, referrer_component)) do + if referrer_target == link_target then + is_target_found = true + break + end + end + end + + assert(is_target_found) + end + end + 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 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 = 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 + + 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') + + local index_policy = link_index_policy(link_fragment) + + 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)) + end + end + 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 alive_target_list, is_unlinked_list = expected_link_targets(entity, link_fragment) + + local is_link_removed = should_be_destroyed_entity_set[entity] + + 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 + + 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 + +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 + +--- +--- +--- +--- +--- + +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/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 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 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 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/develop/testing/destroy_tests.lua b/develop/testing/destroy_tests.lua index 6b4c72a..7af8106 100644 --- a/develop/testing/destroy_tests.lua +++ b/develop/testing/destroy_tests.lua @@ -104,3 +104,462 @@ 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 + +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/develop/testing/link_tests.lua b/develop/testing/link_tests.lua new file mode 100644 index 0000000..80c83bb --- /dev/null +++ b/develop/testing/link_tests.lua @@ -0,0 +1,2067 @@ +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) + + 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 + 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():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() + + 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 + +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 + +do + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):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():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 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():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 }) + 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():index_policy(evo.INDEX_POLICY_LIST):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():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, 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():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 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)[1] == t1 and evo.get(e3, f)[1] == 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():index_policy(evo.INDEX_POLICY_LIST):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():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, 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():index_policy(evo.INDEX_POLICY_LIST):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():index_policy(evo.INDEX_POLICY_LIST):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)[1] == t1) + + evo.destroy(t1) + 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 t = evo.id() + + local e1 = 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 + 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] == 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] == e1 and l[2] == e2 and l[3] == e3) + end +end + +do + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):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] == e1 and l[2] == e2) + end + + assert(evo.search(f, t3) == e1) +end + +do + local f = evo.builder():link():index_policy(evo.INDEX_POLICY_LIST):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] == e2 and l[3] == e3) + 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():index_policy(evo.INDEX_POLICY_LIST):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():index_policy(evo.INDEX_POLICY_LIST):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.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) + assert(evo.search(f, t) == e) + + 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) + + evo.destroy(t) + assert(evo.get(e, f) == l) +end + +do + 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() + + 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():index_policy(evo.INDEX_POLICY_LIST):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():index_policy(evo.INDEX_POLICY_LIST):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():index_policy(evo.INDEX_POLICY_LIST):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():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 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 + -- 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 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)) + + print = original_print + assert(warning_count == 2) +end + +do + -- 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) + + 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():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, 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():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() + + 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():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() + + 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():index_policy(evo.INDEX_POLICY_LIST):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():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() + 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():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() + + 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() + :index_policy(evo.INDEX_POLICY_LIST) + :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():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() + + 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():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() + + 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():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() + 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():index_policy(evo.INDEX_POLICY_LIST):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() + :index_policy(evo.INDEX_POLICY_LIST) + :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():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 }) + 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() + :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() + + 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():index_policy(evo.INDEX_POLICY_LIST):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():index_policy(evo.INDEX_POLICY_LIST) + :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 + +do + 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 } + + 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) == e1) + + 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/develop/testing/main_tests.lua b/develop/testing/main_tests.lua index ba33631..08c4ef4 100644 --- a/develop/testing/main_tests.lua +++ b/develop/testing/main_tests.lua @@ -7134,3 +7134,443 @@ 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 + +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 + + ---@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/develop/testing/multi_index_tests.lua b/develop/testing/multi_index_tests.lua new file mode 100644 index 0000000..4cf8feb --- /dev/null +++ b/develop/testing/multi_index_tests.lua @@ -0,0 +1,642 @@ +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 + +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/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.d.tl b/evolved.d.tl index 1742203..b8743f7 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,8 @@ 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 @@ -146,6 +149,7 @@ NAME: Fragment INDEX: Fragment + LINK: Fragment UNIQUE: Fragment EXPLICIT: Fragment INTERNAL: Fragment @@ -176,6 +180,17 @@ 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 + 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 5c66b4f..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 @@ -136,6 +136,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[] @@ -163,8 +165,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 @@ -187,6 +192,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() @@ -583,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 @@ -629,6 +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 __sorted_includes = {} ---@type table> local __sorted_excludes = {} ---@type table> @@ -653,55 +662,122 @@ 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 % 1048576 + 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 - if acquired_count == 2 ^ 20 - 1 then - __error_fmt('id index overflow') + if acquired_count == 1048576 - 1 then + __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 + 1048576 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 ---@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 == 1099511627776 - 1048576 + and 1048576 + or shifted_secondary + 1048576 + + __freelist_ids[acquired_primary] = __available_primary + shifted_secondary + __available_primary = acquired_primary + 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 % 1048576 + 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 > 1048576 - 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 + 1048576 + 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 - if freelist_ids[acquired_primary] ~= id then - __error_fmt('the id (%d) is not acquired or already released', id) + __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) + local freelist_ids = __freelist_ids + local available_primary = __available_primary - shifted_secondary = shifted_secondary == 2 ^ 40 - 2 ^ 20 - and 2 ^ 20 - or shifted_secondary + 2 ^ 20 + 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 + shifted_secondary = shifted_secondary == 1099511627776 - 1048576 + and 1048576 + or shifted_secondary + 1048576 + + freelist_ids[acquired_primary] = available_primary + shifted_secondary + available_primary = acquired_primary + end + end - freelist_ids[acquired_primary] = __available_primary + shifted_secondary - __available_primary = acquired_primary + __available_primary = available_primary end --- @@ -720,33 +796,36 @@ local __table_pool_tag = { entity_list = 6, fragment_set = 7, fragment_list = 8, - component_table = 9, - __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, + component_list = 9, + component_table = 10, + __count = 10, } ---@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_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) @@ -756,6 +835,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 @@ -838,14 +918,37 @@ 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.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 @@ -881,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[], @@ -1020,6 +1199,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 @@ -1106,166 +1329,11 @@ 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 - -local __search_index_fns = {} - ----@return evolved.search_index ----@nodiscard -function __search_index_fns.new() - ---@type evolved.search_index - return { - __head_entities = {}, - __prev_entities = {}, - __next_entities = {}, - __entity_components = {}, - } -end - ----@param index evolved.search_index ----@param entity evolved.entity ----@param component evolved.component -function __search_index_fns.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 - 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 - 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() local __INDEX = __acquire_id() +local __LINK = __acquire_id() local __UNIQUE = __acquire_id() local __EXPLICIT = __acquire_id() local __INTERNAL = __acquire_id() @@ -1297,6 +1365,17 @@ 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() +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() @@ -1384,40 +1463,14 @@ 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 -local __chunk_without_fragments -local __chunk_without_unique_fragments +local __chunk_with_fragment +local __chunk_with_components +local __chunk_without_fragment +local __chunk_without_fragments +local __chunk_without_unique_fragments local __chunk_requires local __chunk_fragments @@ -1443,20 +1496,18 @@ 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_entity_one -local __destroy_entity_list -local __destroy_fragment_one -local __destroy_fragment_list -local __destroy_fragment_stack +local __destroy_entity_stack +local __unlink_entity_targets +local __remove_entity_fragments -local __chunk_set +local __chunk_assign +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 @@ -1473,12 +1524,12 @@ local __defer_multi_clone_entity local __safe_tbls = { __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[]]=], } @@ -1488,295 +1539,794 @@ local __safe_tbls = { --- --- ----@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 +---@class (exact) evolved.search_index +---@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 - local id_primary, id_secondary = __evolved_unpack(id) - return string.format('$%d#%d:%d', id, id_primary, id_secondary) -end +local __search_index_fns = {} ----@param chunk_parent? evolved.chunk ----@param chunk_fragment evolved.fragment ----@return evolved.chunk +---@param fragment evolved.fragment +---@return evolved.search_index ---@nodiscard -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', - __id_name(chunk_fragment)) - end +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 - local chunk_fragment_set = {} ---@type table - local chunk_fragment_list = {} ---@type evolved.fragment[] - local chunk_fragment_count = 0 ---@type integer + ---@type evolved.search_index + return { + __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 = target_fragment, + } +end - if chunk_parent then - local chunk_parent_fragment_list = chunk_parent.__fragment_list - local chunk_parent_fragment_count = chunk_parent.__fragment_count +---@param index evolved.search_index +---@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 -? - chunk_fragment_count = __assoc_list_fns.move_ex( - chunk_parent_fragment_list, 1, chunk_parent_fragment_count, - chunk_fragment_set, chunk_fragment_list, chunk_fragment_count) - end + local free_component_key_count = index.__free_component_key_count - do - chunk_fragment_count = chunk_fragment_count + 1 - chunk_fragment_set[chunk_fragment] = chunk_fragment_count - chunk_fragment_list[chunk_fragment_count] = chunk_fragment - end + local component_key ---@type integer - ---@type evolved.chunk - local chunk = setmetatable({ - __parent = nil, - __child_set = {}, - __child_list = {}, - __child_count = 0, - __entity_list = {}, - __entity_count = 0, - __entity_capacity = 0, - __fragment = chunk_fragment, - __fragment_set = chunk_fragment_set, - __fragment_list = chunk_fragment_list, - __fragment_count = chunk_fragment_count, - __component_count = 0, - __component_indices = {}, - __component_storages = {}, - __component_fragments = {}, - __component_defaults = {}, - __component_duplicates = {}, - __component_reallocs = {}, - __component_compmoves = {}, - __with_fragment_edges = {}, - __without_fragment_edges = {}, - __with_required_fragments = nil, - __without_unique_fragments = nil, - __unreachable_or_collected = false, - __has_setup_hooks = false, - __has_assign_hooks = false, - __has_insert_hooks = false, - __has_remove_hooks = false, - __has_index_major = false, - __has_index_minors = false, - __has_index_fragments = false, - __has_unique_major = false, - __has_unique_minors = false, - __has_unique_fragments = false, - __has_explicit_major = false, - __has_explicit_minors = false, - __has_explicit_fragments = false, - __has_internal_major = false, - __has_internal_minors = false, - __has_internal_fragments = false, - __has_required_fragments = false, - __insert_fragment_list = nil, - __insert_fragment_count = 0, - __remove_fragment_list = nil, - __remove_fragment_count = 0, - __index_fragment_list = nil, - __index_fragment_count = 0, - __explicit_fragment_list = nil, - __explicit_fragment_count = 0, - }, __chunk_mt) + if free_component_key_count > 0 then + component_key = free_component_keys[free_component_key_count] - if not chunk_parent then - __add_root_chunk(chunk) + free_component_keys[free_component_key_count] = nil + index.__free_component_key_count = free_component_key_count - 1 else - __add_child_chunk(chunk, chunk_parent) + component_key = index.__component_key_count + 1 + index.__component_key_count = component_key end - do - local major = chunk_fragment - local major_chunks = __major_chunks[major] + component_keys[component] = component_key - if not major_chunks then - ---@type evolved.assoc_list - major_chunks = __assoc_list_fns.new(4) - __major_chunks[major] = major_chunks - end + return component_key +end - __assoc_list_fns.insert(major_chunks, chunk) - 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 -? - for chunk_fragment_index = 1, chunk_fragment_count do - local minor = chunk_fragment_list[chunk_fragment_index] - local minor_chunks = __minor_chunks[minor] + local free_component_key_count = index.__free_component_key_count + 1 - if not minor_chunks then - ---@type evolved.assoc_list - minor_chunks = __assoc_list_fns.new(4) - __minor_chunks[minor] = minor_chunks - end + free_component_keys[free_component_key_count] = component_keys[component] + index.__free_component_key_count = free_component_key_count - __assoc_list_fns.insert(minor_chunks, chunk) - end + component_keys[component] = nil +end - __update_chunk_caches(chunk) - __update_chunk_queries(chunk) - __update_chunk_storages(chunk) +---@param target evolved.entity +---@param fragment evolved.fragment +function __search_index_fns.link(target, fragment) + local target_fragments = __target_fragments[target] - return chunk + if not target_fragments then + __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 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 +---@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 src and dst_size >= src_size then - return src + 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 - local dst = __lua_table_new(dst_size) - - if src then - __lua_table_move(src, 1, dst_size, 1, dst) - 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 - return dst -end + ---@type evolved.entity[] + local target_list = index.__entity_components[entity_primary] + local target_count = #target_list ----@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 + local kept_count = 0 ----@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) + for target_index = 1, target_count do + local target = target_list[target_index] - for sib_root_index = __root_count, root_index, -1 do - local sib_root = __root_list[sib_root_index] - __root_set[sib_root.__fragment] = sib_root_index + 1 - __root_list[sib_root_index + 1] = sib_root + 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 - __root_set[root.__fragment] = root_index - __root_list[root_index] = root - __root_count = __root_count + 1 + for target_index = kept_count + 1, target_count do + target_list[target_index] = nil + end end ----@param root evolved.chunk -function __remove_root_chunk(root) - if root.__parent then - __error_fmt('unexpected root chunk: (%s)', - tostring(root)) - return - end +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.assign(index, entity, component) + local entity_primary = entity % 1048576 + local entity_components = index.__entity_components - local root_index = __root_set[root.__fragment] + local new_component = component + local old_component = entity_components[entity_primary] - if not root_index or __root_list[root_index] ~= root then - __error_fmt('unexpected root chunk: (%s)', - tostring(root)) - return + 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 - for sib_root_index = root_index, __root_count - 1 do - local sib_root = __root_list[sib_root_index + 1] - __root_set[sib_root.__fragment] = sib_root_index - __root_list[sib_root_index] = sib_root - 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 multi_components = index.__multi_components + local entity_components = index.__entity_components - __root_set[root.__fragment] = nil - __root_list[__root_count] = nil - __root_count = __root_count - 1 -end + for place = first, last do + local entity = entity_list[place] + local entity_primary = entity % 1048576 ----@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) + local new_component = component_list[place] + local old_component = entity_components[entity_primary] - 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 + 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 - parent.__child_set[child] = child_index - parent.__child_list[child_index] = child - parent.__child_count = parent.__child_count + 1 +---@param index evolved.search_index +---@param entity evolved.entity +---@param component evolved.component +function __search_index_fns.assign_components(index, entity, component) + local entity_primary = entity % 1048576 + local entity_components = index.__entity_components - parent.__with_fragment_edges[child.__fragment] = child - child.__without_fragment_edges[child.__fragment] = parent + ---@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 - child.__parent = parent -end + local min_component_count = old_component_count < new_component_count + and old_component_count or new_component_count ----@param child evolved.chunk -function __remove_child_chunk(child) - local parent = child.__parent + local prefix_count = 0 - if not parent then - __error_fmt('unexpected child chunk: (%s)', - tostring(child)) - return + 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 - local child_index = parent.__child_set[child] + local suffix_count = 0 - if not child_index or parent.__child_list[child_index] ~= child then - __error_fmt('unexpected child chunk: (%s)', - tostring(child)) - return + 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 - 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 + if prefix_count == old_component_count and prefix_count == new_component_count then + return end - parent.__child_set[child] = nil - parent.__child_list[parent.__child_count] = nil - parent.__child_count = parent.__child_count - 1 + local old_middle_last = old_component_count - suffix_count + local new_middle_last = new_component_count - suffix_count - parent.__with_fragment_edges[child.__fragment] = nil - child.__without_fragment_edges[child.__fragment] = nil + if old_middle_last - prefix_count <= 4 then + for old_component_index = prefix_count + 1, old_middle_last do + local old_component = old_component_list[old_component_index] - child.__parent = nil -end + local new_component_index = 1 ----@param chunk evolved.chunk -function __update_chunk_caches(chunk) - local chunk_parent = chunk.__parent - local chunk_fragment = chunk.__fragment + while new_component_index <= new_component_count + and new_component_list[new_component_index] ~= old_component + do + new_component_index = new_component_index + 1 + end - local chunk_fragment_set = chunk.__fragment_set + if new_component_index > new_component_count then + __search_index_fns.remove_node(index, entity, old_component) + end + end + else + local component_epochs = index.__component_epochs + ---@cast component_epochs -? + + 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] + + 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 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_component_list, prefix_count + 1, new_component_count, + prefix_count + 1, old_component_list) + + for old_component_index = new_component_count + 1, old_component_count do + old_component_list[old_component_index] = 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 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 + + for component_index = 1, component_count do + __search_index_fns.insert_node(index, entity, component_list[component_index]) + end + + index.__entity_components[entity % 1048576] = component_list + else + __search_index_fns.insert_node(index, entity, component) + index.__entity_components[entity % 1048576] = 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 +function __search_index_fns.insert_node(index, entity, component) + if component ~= component then + -- NaN components cannot be indexed + return + end + + local target_fragment = index.__target_fragment + + 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 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 + return + end + + local head_entity_key = component_offset + head_entity % 1048576 + + local tail_entity = prev_entities[head_entity_key] + 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 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 + next_entities[entity_key] = entity + + if target_fragment then + __search_index_fns.link(component, target_fragment) + end + end +end + +---@param index evolved.search_index +---@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 = entity_components[entity_primary] + + if component == nil then + return + end + + if index.__multi_components then + ---@type evolved.component[], integer + local component_list, component_count = component, #component + + for component_index = 1, component_count do + __search_index_fns.remove_node(index, entity, component_list[component_index]) + end + else + __search_index_fns.remove_node(index, entity, component) + end + + entity_components[entity_primary] = nil +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 +function __search_index_fns.remove_node(index, entity, component) + 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 not head_entity then + 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 + 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 = 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 + + if head_entity == entity then + head_entities[component] = next_entity + end + end + + prev_entities[entity_key] = nil + next_entities[entity_key] = nil +end + +--- +--- +--- +--- +--- + +local __chunk_fns = {} + +---@param chunk_parent? evolved.chunk +---@param chunk_fragment evolved.fragment +---@return evolved.chunk +---@nodiscard +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 + __debug_fns.error_fmt('the fragment (%s) is not alive and cannot be used for a new chunk', + __id_name(chunk_fragment)) + end + + local chunk_fragment_set = {} ---@type table + local chunk_fragment_list = {} ---@type evolved.fragment[] + local chunk_fragment_count = 0 ---@type integer + + if chunk_parent then + local chunk_parent_fragment_list = chunk_parent.__fragment_list + local chunk_parent_fragment_count = chunk_parent.__fragment_count + + chunk_fragment_count = __assoc_list_fns.move_ex( + chunk_parent_fragment_list, 1, chunk_parent_fragment_count, + chunk_fragment_set, chunk_fragment_list, chunk_fragment_count) + end + + do + chunk_fragment_count = chunk_fragment_count + 1 + chunk_fragment_set[chunk_fragment] = chunk_fragment_count + chunk_fragment_list[chunk_fragment_count] = chunk_fragment + end + + ---@type evolved.chunk + local chunk = setmetatable({ + __parent = nil, + __child_set = {}, + __child_list = {}, + __child_count = 0, + __entity_list = {}, + __entity_count = 0, + __entity_capacity = 0, + __fragment = chunk_fragment, + __fragment_set = chunk_fragment_set, + __fragment_list = chunk_fragment_list, + __fragment_count = chunk_fragment_count, + __component_count = 0, + __component_indices = {}, + __component_storages = {}, + __component_fragments = {}, + __component_sets = {}, + __component_assigns = {}, + __component_defaults = {}, + __component_duplicates = {}, + __component_reallocs = {}, + __component_compmoves = {}, + __with_fragment_edges = {}, + __without_fragment_edges = {}, + __with_required_fragments = nil, + __without_unique_fragments = nil, + __unreachable_or_collected = false, + __has_setup_hooks = false, + __has_assign_hooks = false, + __has_insert_hooks = false, + __has_remove_hooks = false, + __has_index_major = false, + __has_index_minors = false, + __has_index_fragments = false, + __has_unique_major = false, + __has_unique_minors = false, + __has_unique_fragments = false, + __has_explicit_major = false, + __has_explicit_minors = false, + __has_explicit_fragments = false, + __has_internal_major = false, + __has_internal_minors = false, + __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, + __explicit_fragment_list = nil, + __explicit_fragment_count = 0, + }, __chunk_mt) + + if not chunk_parent then + __chunk_fns.add_root_chunk(chunk) + else + __chunk_fns.add_child_chunk(chunk, chunk_parent) + end + + do + local major = chunk_fragment + local major_chunks = __major_chunks[major] + + if not major_chunks then + ---@type evolved.assoc_list + major_chunks = __assoc_list_fns.new(4) + __major_chunks[major] = major_chunks + end + + __assoc_list_fns.insert(major_chunks, chunk) + end + + for chunk_fragment_index = 1, chunk_fragment_count do + local minor = chunk_fragment_list[chunk_fragment_index] + local minor_chunks = __minor_chunks[minor] + + if not minor_chunks then + ---@type evolved.assoc_list + minor_chunks = __assoc_list_fns.new(4) + __minor_chunks[minor] = minor_chunks + end + + __assoc_list_fns.insert(minor_chunks, chunk) + end + + __chunk_fns.update_chunk_caches(chunk) + __chunk_fns.update_chunk_queries(chunk) + __chunk_fns.update_chunk_storages(chunk) + + return chunk +end + +---@param root evolved.chunk +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 + local sib_root = __root_list[sib_root_index] + __root_set[sib_root.__fragment] = sib_root_index + 1 + __root_list[sib_root_index + 1] = sib_root + end + + __root_set[root.__fragment] = root_index + __root_list[root_index] = root + __root_count = __root_count + 1 +end + +---@param root evolved.chunk +function __chunk_fns.remove_root_chunk(root) + if root.__parent then + __debug_fns.error_fmt('unexpected root chunk: (%s)', + tostring(root)) + return + end + + local root_index = __root_set[root.__fragment] + + if not root_index or __root_list[root_index] ~= root then + __debug_fns.error_fmt('unexpected root chunk: (%s)', + tostring(root)) + return + end + + for sib_root_index = root_index, __root_count - 1 do + local sib_root = __root_list[sib_root_index + 1] + __root_set[sib_root.__fragment] = sib_root_index + __root_list[sib_root_index] = sib_root + end + + __root_set[root.__fragment] = nil + __root_list[__root_count] = nil + __root_count = __root_count - 1 +end + +---@param child evolved.chunk +---@param parent evolved.chunk +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) + + parent.__with_fragment_edges[child.__fragment] = child + child.__without_fragment_edges[child.__fragment] = parent + + child.__parent = parent +end + +---@param child evolved.chunk +function __chunk_fns.remove_child_chunk(child) + local parent = child.__parent + + if not parent then + __debug_fns.error_fmt('unexpected child chunk: (%s)', + tostring(child)) + return + end + + local child_index = parent.__child_set[child] + + if not child_index or parent.__child_list[child_index] ~= child then + __debug_fns.error_fmt('unexpected child chunk: (%s)', + tostring(child)) + return + end + + 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 + + child.__parent = nil +end + +---@param chunk evolved.chunk +function __chunk_fns.update_chunk_caches(chunk) + local chunk_parent = chunk.__parent + local chunk_fragment = chunk.__fragment + + local chunk_fragment_set = chunk.__fragment_set 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 @@ -1860,47 +2410,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) + + 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) - insert_fragment_count = parent_insert_fragment_count + 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) + + 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) - remove_fragment_count = parent_remove_fragment_count + 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 @@ -1952,7 +2529,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 @@ -1962,8 +2539,8 @@ 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 - __assoc_list_fns.insert(major_query_chunks, chunk) + 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) end @@ -1972,7 +2549,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 @@ -1983,6 +2560,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 @@ -1994,6 +2573,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) @@ -2016,6 +2599,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] @@ -2024,6 +2609,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 @@ -2033,6 +2620,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 @@ -2049,6 +2638,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 @@ -2064,7 +2655,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 @@ -2097,7 +2688,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 @@ -2129,6 +2720,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 @@ -2138,7 +2731,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 @@ -2181,15 +2774,14 @@ 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 ---@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 @@ -2221,16 +2813,14 @@ 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 ---@param query evolved.query ---@return evolved.assoc_list -function __cache_query_chunks(query) - __reset_query_chunks(query) - +---@nodiscard +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 @@ -2246,16 +2836,84 @@ function __cache_query_chunks(query) 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 + 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 __chunk_fns.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 __chunk_fns.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 + + __assoc_list_fns.sort(query_chunks, __chunk_mt.__lt) + + return query_chunks +end + +---@param query evolved.query +function __chunk_fns.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.unordered_remove(major_queries, query) == 0 then + __major_queries[query_major] = nil end end @@ -2263,33 +2921,20 @@ function __cache_query_chunks(query) 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 major_queries = __major_queries[query_variant] - if __query_major_matches(major_chunk, query) then - __assoc_list_fns.insert(query_chunks, major_chunk) - end + if major_queries and __assoc_list_fns.unordered_remove(major_queries, query) == 0 then + __major_queries[query_variant] = nil 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) +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 @@ -2305,14 +2950,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 @@ -2365,44 +3010,55 @@ 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_hook(__update_major_chunks, 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_hook(__update_search_index, fragment) + __defer_call_func(__chunk_fns.update_search_index, 1, 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) + __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 + + __chunk_fns.trace_minor_chunks(fragment, __chunk_fns.update_search_index_trace, + fragment, new_search_index) - if __evolved_has(fragment, __INDEX) then - local fragment_search_index = __search_index_fns.new() - __search_indices[fragment] = fragment_search_index - __trace_minor_chunks(fragment, __update_search_index_trace, fragment, fragment_search_index) - __trace_major_chunks(fragment, __update_chunk_caches) + __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 @@ -2414,6 +3070,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 @@ -2421,7 +3129,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 @@ -2458,7 +3166,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 @@ -2607,7 +3315,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 @@ -2630,7 +3338,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] @@ -2850,7 +3558,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 @@ -2873,7 +3581,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 @@ -2916,7 +3624,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 @@ -2972,14 +3680,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 @@ -2987,20 +3702,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 @@ -3016,7 +3731,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 @@ -3065,7 +3780,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 @@ -3117,7 +3832,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 @@ -3127,14 +3841,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 @@ -3145,24 +3866,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 @@ -3177,7 +3898,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) @@ -3223,23 +3944,209 @@ function __clone_entity(prefab, entity, component_table, component_mapper) local prefab_component_indices = prefab_chunk.__component_indices local prefab_component_storages = prefab_chunk.__component_storages - local place = chunk.__entity_count + 1 + local place = chunk.__entity_count + 1 + + if place > chunk.__entity_capacity then + __expand_chunk(chunk, place) + end + + local chunk_entity_list = chunk.__entity_list + + do + chunk.__entity_count = place + __structural_changes = __structural_changes + 1 + + chunk_entity_list[place] = entity + + local entity_primary = entity % 1048576 + __entity_chunks[entity_primary] = chunk + __entity_places[entity_primary] = place + end + + for component_index = 1, chunk_component_count do + local component_storage = chunk_component_storages[component_index] + local component_fragment = chunk_component_fragments[component_index] + local component_duplicate = chunk_component_duplicates[component_index] + + local ini_component = component_table and component_table[component_fragment] + + if ini_component == nil then + if chunk == prefab_chunk then + ini_component = component_storage[prefab_place] + else + local prefab_component_index = prefab_component_indices[component_fragment] + if prefab_component_index then + local prefab_component_storage = prefab_component_storages[prefab_component_index] + ini_component = prefab_component_storage[prefab_place] + else + ini_component = chunk_component_defaults[component_index] + end + end + end + + if ini_component ~= nil and component_duplicate then + local new_component = component_duplicate(ini_component) + + if new_component == nil then + new_component = true + end + + component_storage[place] = new_component + else + if ini_component == nil then + ini_component = true + end + + component_storage[place] = ini_component + end + end + + if component_mapper then + component_mapper(chunk, place, place) + end + + local chunk_index_fragment_list = chunk.__index_fragment_list + + if chunk_index_fragment_list then + for chunk_index_fragment_index = 1, chunk.__index_fragment_count do + local fragment = chunk_index_fragment_list[chunk_index_fragment_index] + local component_index = chunk_component_indices[fragment] + + if component_index then + local fragment_search_index = __search_indices[fragment] + local component_storage = chunk_component_storages[component_index] + local new_component = component_storage[place] + __search_index_fns.insert(fragment_search_index, entity, new_component) + end + end + end + + local chunk_insert_fragment_list = chunk.__insert_fragment_list + + if chunk_insert_fragment_list then + 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 insert_fragment_set, insert_fragment_insert = + chunk_insert_fragment_sets[chunk_insert_fragment_index], + chunk_insert_fragment_inserts[chunk_insert_fragment_index] + + do + local component_index = chunk_component_indices[fragment] + + if component_index then + local component_storage = chunk_component_storages[component_index] + + local new_component = component_storage[place] + + if insert_fragment_set then + insert_fragment_set(entity, fragment, new_component) + end + + if insert_fragment_insert then + insert_fragment_insert(entity, fragment, new_component) + end + else + if insert_fragment_set then + insert_fragment_set(entity, fragment) + end + + if insert_fragment_insert then + insert_fragment_insert(entity, fragment) + end + end + end + end + end +end + +---@param prefab evolved.entity +---@param entity_list evolved.entity[] +---@param entity_first integer +---@param entity_count integer +---@param component_table? evolved.component_table +---@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 + __debug_fns.error_fmt('clone entity operations should be deferred') + end + + local prefab_chunk, prefab_place = __evolved_locate(prefab) + + if prefab_chunk and prefab_chunk.__has_unique_fragments then + local without_unique_fragments = prefab_chunk.__without_unique_fragments + + if not without_unique_fragments or without_unique_fragments.__unreachable_or_collected then + prefab_chunk.__without_unique_fragments = __chunk_without_unique_fragments(prefab_chunk) + end + end + + if not prefab_chunk or not prefab_chunk.__without_unique_fragments then + return __multi_spawn_entity(nil, + entity_list, entity_first, entity_count, + component_table, component_mapper) + end + + local chunk = component_table + and __chunk_with_components(prefab_chunk.__without_unique_fragments, component_table) + or prefab_chunk.__without_unique_fragments + + while chunk and chunk.__has_required_fragments do + local required_chunk = chunk.__with_required_fragments + + if required_chunk and not required_chunk.__unreachable_or_collected then + chunk = required_chunk + else + required_chunk = __chunk_requires(chunk) + chunk.__with_required_fragments, chunk = required_chunk, required_chunk + end + end + + if not chunk then + return + end + + local chunk_component_count = chunk.__component_count + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages + local chunk_component_fragments = chunk.__component_fragments + local chunk_component_defaults = chunk.__component_defaults + local chunk_component_duplicates = chunk.__component_duplicates + + local prefab_component_indices = prefab_chunk.__component_indices + local prefab_component_storages = prefab_chunk.__component_storages + + local b_place = chunk.__entity_count + 1 + local e_place = b_place + entity_count - 1 - if place > chunk.__entity_capacity then - __expand_chunk(chunk, place) + if e_place > chunk.__entity_capacity then + __expand_chunk(chunk, e_place) end local chunk_entity_list = chunk.__entity_list do - chunk.__entity_count = place + chunk.__entity_count = e_place __structural_changes = __structural_changes + 1 - chunk_entity_list[place] = entity + local entity_chunks = __entity_chunks + local entity_places = __entity_places - local entity_primary = entity % 2 ^ 20 - __entity_chunks[entity_primary] = chunk - __entity_places[entity_primary] = place + for place = b_place, e_place do + local entity = entity_list[place - b_place + entity_first] + chunk_entity_list[place] = entity + + local entity_primary = entity % 1048576 + entity_chunks[entity_primary] = chunk + entity_places[entity_primary] = place + end end for component_index = 1, chunk_component_count do @@ -3264,24 +4171,28 @@ function __clone_entity(prefab, entity, component_table, component_mapper) end if ini_component ~= nil and component_duplicate then - local new_component = component_duplicate(ini_component) + for place = b_place, e_place do + local new_component = component_duplicate(ini_component) - if new_component == nil then - new_component = true - end + if new_component == nil then + new_component = true + end - component_storage[place] = new_component + component_storage[place] = new_component + end else if ini_component == nil then ini_component = true end - component_storage[place] = ini_component + for place = b_place, e_place do + component_storage[place] = ini_component + end end end if component_mapper then - component_mapper(chunk, place, place) + component_mapper(chunk, b_place, e_place) end local chunk_index_fragment_list = chunk.__index_fragment_list @@ -3294,8 +4205,8 @@ function __clone_entity(prefab, entity, component_table, component_mapper) if component_index then local fragment_search_index = __search_indices[fragment] local component_storage = chunk_component_storages[component_index] - local new_component = component_storage[place] - __search_index_fns.insert(fragment_search_index, entity, new_component) + __search_index_fns.multi_insert(fragment_search_index, + chunk_entity_list, component_storage, b_place, e_place) end end end @@ -3303,35 +4214,50 @@ 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 local component_storage = chunk_component_storages[component_index] - local new_component = component_storage[place] + for place = b_place, e_place do + local entity = chunk_entity_list[place] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component) - end + local new_component = component_storage[place] - if fragment_on_insert then - fragment_on_insert(entity, fragment, new_component) + if insert_fragment_set then + insert_fragment_set(entity, fragment, new_component) + end + + if insert_fragment_insert then + insert_fragment_insert(entity, fragment, new_component) + end end else - if fragment_on_set then - fragment_on_set(entity, fragment) - end + for place = b_place, e_place do + local entity = chunk_entity_list[place] + + 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 end @@ -3339,417 +4265,591 @@ function __clone_entity(prefab, entity, component_table, component_mapper) end end ----@param prefab evolved.entity ----@param entity_list evolved.entity[] ----@param entity_first integer ----@param entity_count integer ----@param component_table? evolved.component_table ----@param component_mapper? evolved.component_mapper -function __multi_clone_entity(prefab, entity_list, entity_first, entity_count, component_table, component_mapper) +---@param chunk evolved.chunk +function __purge_chunk(chunk) if __defer_depth <= 0 then - __error_fmt('clone entity operations should be deferred') + __debug_fns.error_fmt('this operation should be deferred') end - local prefab_chunk, prefab_place = __evolved_locate(prefab) + if chunk.__child_count > 0 or chunk.__entity_count > 0 then + __debug_fns.error_fmt('chunk should be empty before purging') + end - if prefab_chunk and prefab_chunk.__has_unique_fragments then - local without_unique_fragments = prefab_chunk.__without_unique_fragments + if chunk.__entity_capacity > 0 then + __shrink_chunk(chunk, 0) + end - if not without_unique_fragments or without_unique_fragments.__unreachable_or_collected then - prefab_chunk.__without_unique_fragments = __chunk_without_unique_fragments(prefab_chunk) + if not chunk.__parent then + __chunk_fns.remove_root_chunk(chunk) + else + __chunk_fns.remove_child_chunk(chunk) + end + + do + local major = chunk.__fragment + local major_chunks = __major_chunks[major] + + if major_chunks and __assoc_list_fns.remove(major_chunks, chunk) == 0 then + __major_chunks[major] = nil end end - if not prefab_chunk or not prefab_chunk.__without_unique_fragments then - return __multi_spawn_entity(nil, - entity_list, entity_first, entity_count, - component_table, component_mapper) + 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 - local chunk = component_table - and __chunk_with_components(prefab_chunk.__without_unique_fragments, component_table) - or prefab_chunk.__without_unique_fragments + for chunk_fragment_index = 1, chunk.__fragment_count do + local minor = chunk.__fragment_list[chunk_fragment_index] + local minor_chunks = __minor_chunks[minor] - while chunk and chunk.__has_required_fragments do - local required_chunk = chunk.__with_required_fragments + if minor_chunks and __assoc_list_fns.remove(minor_chunks, chunk) == 0 then + __minor_chunks[minor] = nil + end + end - if required_chunk and not required_chunk.__unreachable_or_collected then - chunk = required_chunk - else - required_chunk = __chunk_requires(chunk) - chunk.__with_required_fragments, chunk = required_chunk, required_chunk + for with_fragment, with_fragment_edge in __lua_next, chunk.__with_fragment_edges do + chunk.__with_fragment_edges[with_fragment] = nil + with_fragment_edge.__without_fragment_edges[with_fragment] = nil + end + + for without_fragment, without_fragment_edge in __lua_next, chunk.__without_fragment_edges do + chunk.__without_fragment_edges[without_fragment] = nil + without_fragment_edge.__with_fragment_edges[without_fragment] = nil + end + + chunk.__unreachable_or_collected = true +end + +---@param chunk evolved.chunk +---@param min_capacity integer +function __expand_chunk(chunk, min_capacity) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') + end + + local entity_count = chunk.__entity_count + + if min_capacity < entity_count then + min_capacity = entity_count + end + + local old_capacity = chunk.__entity_capacity + if old_capacity >= min_capacity then + -- no need to expand, the chunk is already large enough + return + end + + local new_capacity = old_capacity * 2 + + if new_capacity < min_capacity then + new_capacity = min_capacity + end + + if new_capacity < 4 then + new_capacity = 4 + end + + do + local component_count = chunk.__component_count + local component_storages = chunk.__component_storages + local component_reallocs = chunk.__component_reallocs + + for component_index = 1, component_count do + local component_realloc = component_reallocs[component_index] + + local new_component_storage ---@type evolved.storage? + local old_component_storage = component_storages[component_index] + + if component_realloc then + new_component_storage = component_realloc( + old_component_storage, old_capacity, new_capacity) + else + new_component_storage = __default_realloc( + old_component_storage, old_capacity, new_capacity) + end + + if min_capacity > 0 and not new_component_storage then + __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 + __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 + end + + component_storages[component_index] = new_component_storage + end + end + + chunk.__entity_capacity = new_capacity +end + +---@param chunk evolved.chunk +---@param min_capacity integer +function __shrink_chunk(chunk, min_capacity) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') + end + + local entity_count = chunk.__entity_count + + if min_capacity < entity_count then + min_capacity = entity_count + end + + if min_capacity > 0 and min_capacity < 4 then + min_capacity = 4 + end + + local old_capacity = chunk.__entity_capacity + if old_capacity <= min_capacity then + -- no need to shrink, the chunk is already small enough + return + end + + do + local old_entity_list = chunk.__entity_list + local new_entity_list = __lua_table_new(min_capacity) + + __lua_table_move( + old_entity_list, 1, entity_count, + 1, new_entity_list) + + chunk.__entity_list = new_entity_list + end + + do + local component_count = chunk.__component_count + local component_storages = chunk.__component_storages + local component_reallocs = chunk.__component_reallocs + + for component_index = 1, component_count do + local component_realloc = component_reallocs[component_index] + + local new_component_storage ---@type evolved.storage? + local old_component_storage = component_storages[component_index] + + if component_realloc then + new_component_storage = component_realloc( + old_component_storage, old_capacity, min_capacity) + else + new_component_storage = __default_realloc( + old_component_storage, old_capacity, min_capacity) + end + + if min_capacity > 0 and not new_component_storage then + __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 + __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 + end + + component_storages[component_index] = new_component_storage end end - if not chunk then - return + chunk.__entity_capacity = min_capacity +end + +---@param entity evolved.entity +function __clear_entity_one(entity) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') end - local chunk_component_count = chunk.__component_count - local chunk_component_indices = chunk.__component_indices - local chunk_component_storages = chunk.__component_storages - local chunk_component_fragments = chunk.__component_fragments - local chunk_component_defaults = chunk.__component_defaults - local chunk_component_duplicates = chunk.__component_duplicates + local entity_chunks = __entity_chunks + local entity_places = __entity_places - local prefab_component_indices = prefab_chunk.__component_indices - local prefab_component_storages = prefab_chunk.__component_storages + local entity_primary = entity % 1048576 - local b_place = chunk.__entity_count + 1 - local e_place = b_place + entity_count - 1 + if __freelist_ids[entity_primary] ~= entity then + -- nothing to clear from non-alive entities + else + local chunk = entity_chunks[entity_primary] + local place = entity_places[entity_primary] - if e_place > chunk.__entity_capacity then - __expand_chunk(chunk, e_place) - end + if chunk then + local chunk_remove_fragment_list = chunk.__remove_fragment_list - local chunk_entity_list = chunk.__entity_list + if chunk_remove_fragment_list then + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages - do - chunk.__entity_count = e_place - __structural_changes = __structural_changes + 1 + local chunk_remove_fragment_removes = chunk.__remove_fragment_removes + ---@cast chunk_remove_fragment_removes -? - local entity_chunks = __entity_chunks - local entity_places = __entity_places + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do + local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - for place = b_place, e_place do - local entity = entity_list[place - b_place + entity_first] - chunk_entity_list[place] = entity + local remove_fragment_remove = chunk_remove_fragment_removes[chunk_remove_fragment_index] - local entity_primary = entity % 2 ^ 20 - entity_chunks[entity_primary] = chunk - entity_places[entity_primary] = place - end - end + do + local component_index = chunk_component_indices[fragment] - for component_index = 1, chunk_component_count do - local component_storage = chunk_component_storages[component_index] - local component_fragment = chunk_component_fragments[component_index] - local component_duplicate = chunk_component_duplicates[component_index] + if component_index then + local component_storage = chunk_component_storages[component_index] + local old_component = component_storage[place] + remove_fragment_remove(entity, fragment, old_component) + else + remove_fragment_remove(entity, fragment) + end + end + end + end - local ini_component = component_table and component_table[component_fragment] + local chunk_index_fragment_list = chunk.__index_fragment_list - if ini_component == nil then - if chunk == prefab_chunk then - ini_component = component_storage[prefab_place] - else - local prefab_component_index = prefab_component_indices[component_fragment] - if prefab_component_index then - local prefab_component_storage = prefab_component_storages[prefab_component_index] - ini_component = prefab_component_storage[prefab_place] - else - ini_component = chunk_component_defaults[component_index] + 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 - if ini_component ~= nil and component_duplicate then - for place = b_place, e_place do - local new_component = component_duplicate(ini_component) - - if new_component == nil then - new_component = true - end + if chunk then + __detach_entity(chunk, place) - component_storage[place] = new_component - end - else - if ini_component == nil then - ini_component = true - end + entity_chunks[entity_primary] = false + entity_places[entity_primary] = 0 - for place = b_place, e_place do - component_storage[place] = ini_component - end + __structural_changes = __structural_changes + 1 end end +end - if component_mapper then - component_mapper(chunk, b_place, e_place) +---@param entity evolved.entity +function __destroy_entity_one(entity) + if __defer_depth <= 0 then + __debug_fns.error_fmt('this operation should be deferred') end - local chunk_index_fragment_list = chunk.__index_fragment_list - - if chunk_index_fragment_list then - for chunk_index_fragment_index = 1, chunk.__index_fragment_count do - local fragment = chunk_index_fragment_list[chunk_index_fragment_index] - local component_index = chunk_component_indices[fragment] - - if component_index then - local fragment_search_index = __search_indices[fragment] - local component_storage = chunk_component_storages[component_index] - - __search_index_fns.multi_insert(fragment_search_index, - chunk_entity_list, component_storage, b_place, e_place) - end - end - end + local entity_chunks = __entity_chunks + local entity_places = __entity_places - local chunk_insert_fragment_list = chunk.__insert_fragment_list + local entity_primary = entity % 1048576 - if chunk_insert_fragment_list then - for chunk_insert_fragment_index = 1, chunk.__insert_fragment_count do - local fragment = chunk_insert_fragment_list[chunk_insert_fragment_index] + if __freelist_ids[entity_primary] ~= entity then + -- this entity is not alive, nothing to purge + else + local chunk = entity_chunks[entity_primary] + local place = entity_places[entity_primary] - ---@type evolved.set_hook?, evolved.insert_hook? - local fragment_on_set, fragment_on_insert = - __evolved_get(fragment, __ON_SET, __ON_INSERT) + if chunk then + local chunk_remove_fragment_list = chunk.__remove_fragment_list - if fragment_on_set or fragment_on_insert then - local component_index = chunk_component_indices[fragment] + if chunk_remove_fragment_list then + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages - if component_index then - local component_storage = chunk_component_storages[component_index] + local chunk_remove_fragment_removes = chunk.__remove_fragment_removes + ---@cast chunk_remove_fragment_removes -? - for place = b_place, e_place do - local entity = chunk_entity_list[place] + for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do + local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] - local new_component = component_storage[place] + local remove_fragment_remove = chunk_remove_fragment_removes[chunk_remove_fragment_index] - if fragment_on_set then - fragment_on_set(entity, fragment, new_component) - end + do + local component_index = chunk_component_indices[fragment] - if fragment_on_insert then - fragment_on_insert(entity, fragment, new_component) + if component_index then + local component_storage = chunk_component_storages[component_index] + local old_component = component_storage[place] + remove_fragment_remove(entity, fragment, old_component) + else + remove_fragment_remove(entity, fragment) end end - else - for place = b_place, e_place do - local entity = chunk_entity_list[place] + end + end - if fragment_on_set then - fragment_on_set(entity, fragment) - end + local chunk_index_fragment_list = chunk.__index_fragment_list - if fragment_on_insert then - fragment_on_insert(entity, fragment) - end - end + 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 + + if chunk then + __detach_entity(chunk, place) + + entity_chunks[entity_primary] = false + entity_places[entity_primary] = 0 + + __structural_changes = __structural_changes + 1 + end + + __release_id(entity) end end ----@param chunk evolved.chunk -function __purge_chunk(chunk) +---@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 - __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('this operation should be deferred') end - if chunk.__entity_capacity > 0 then - __shrink_chunk(chunk, 0) - end + local entity_chunks = __entity_chunks + local entity_places = __entity_places - if not chunk.__parent then - __remove_root_chunk(chunk) - else - __remove_child_chunk(chunk) - end + local minor_chunks = __minor_chunks + local search_indices = __search_indices + local target_fragments = __target_fragments - do - local major = chunk.__fragment - local major_chunks = __major_chunks[major] + ---@type table + local processed_entity_set = __acquire_table(__table_pool_tag.fragment_set) - if major_chunks and __assoc_list_fns.remove(major_chunks, chunk) == 0 then - __major_chunks[major] = nil - end - end + ---@type evolved.entity[] + local processed_entity_list = __acquire_table(__table_pool_tag.entity_list) + local processed_entity_count = 0 - for chunk_fragment_index = 1, chunk.__fragment_count do - local minor = chunk.__fragment_list[chunk_fragment_index] - local minor_chunks = __minor_chunks[minor] + ---@type evolved.entity[] + local releasing_entity_list = __acquire_table(__table_pool_tag.entity_list) + local releasing_entity_count = 0 - if minor_chunks and __assoc_list_fns.remove(minor_chunks, chunk) == 0 then - __minor_chunks[minor] = nil - end - end + local removing_count = 0 ---@type integer + local removing_target_list ---@type (evolved.entity|false)[]? + local removing_fragment_list ---@type evolved.fragment[]? - for with_fragment, with_fragment_edge in __lua_next, chunk.__with_fragment_edges do - chunk.__with_fragment_edges[with_fragment] = nil - with_fragment_edge.__without_fragment_edges[with_fragment] = nil - end + while chunk_stack_size > 0 or entity_stack_size > 0 do + if chunk_stack_size > 0 then + local chunk = chunk_stack[chunk_stack_size] - for without_fragment, without_fragment_edge in __lua_next, chunk.__without_fragment_edges do - chunk.__without_fragment_edges[without_fragment] = nil - without_fragment_edge.__with_fragment_edges[without_fragment] = nil - end + chunk_stack[chunk_stack_size] = nil + chunk_stack_size = chunk_stack_size - 1 - chunk.__unreachable_or_collected = true -end + local entity_stack_base = entity_stack_size ----@param chunk evolved.chunk ----@param min_capacity integer -function __expand_chunk(chunk, min_capacity) - if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') - end + local chunk_entity_list = chunk.__entity_list + local chunk_entity_count = chunk.__entity_count - local entity_count = chunk.__entity_count + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] - if min_capacity < entity_count then - min_capacity = entity_count - end + if minor_chunks[entity] or target_fragments[entity] then + entity_stack_size = entity_stack_size + 1 + entity_stack[entity_stack_size] = entity + end + end - local old_capacity = chunk.__entity_capacity - if old_capacity >= min_capacity then - -- no need to expand, the chunk is already large enough - return - end + 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) - local new_capacity = old_capacity * 2 + releasing_entity_count = releasing_entity_count + chunk_entity_count - if new_capacity < min_capacity then - new_capacity = min_capacity - end + __chunk_clear(chunk) + else + local entity_stack_curr = entity_stack_size - if new_capacity < 4 then - new_capacity = 4 - end + for place = chunk_entity_count, 1, -1 do + local entity = chunk_entity_list[place] - do - local component_count = chunk.__component_count - local component_storages = chunk.__component_storages - local component_reallocs = chunk.__component_reallocs + 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 - for component_index = 1, component_count do - local component_realloc = component_reallocs[component_index] + __clear_entity_one(entity) + end + end + end + else + local entity = entity_stack[entity_stack_size] - local new_component_storage ---@type evolved.storage? - local old_component_storage = component_storages[component_index] + entity_stack[entity_stack_size] = nil + entity_stack_size = entity_stack_size - 1 - if component_realloc then - new_component_storage = component_realloc( - old_component_storage, old_capacity, new_capacity) + if processed_entity_set[entity] then + -- this entity has already been processed else - new_component_storage = __default_realloc( - old_component_storage, old_capacity, new_capacity) - end + local entity_minor_chunks = minor_chunks[entity] + local entity_target_fragments = target_fragments[entity] - if min_capacity > 0 and not new_component_storage then - __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( - '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 - 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 - component_storages[component_index] = new_component_storage - end - end + __clear_entity_one(entity) + else + processed_entity_set[entity] = true - chunk.__entity_capacity = new_capacity -end + processed_entity_count = processed_entity_count + 1 + processed_entity_list[processed_entity_count] = entity ----@param chunk evolved.chunk ----@param min_capacity integer -function __shrink_chunk(chunk, min_capacity) - if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') - end + releasing_entity_count = releasing_entity_count + 1 + releasing_entity_list[releasing_entity_count] = entity - local entity_count = chunk.__entity_count + if entity_minor_chunks then + local destruction_policy = __evolved_get(entity, __DESTRUCTION_POLICY) + or __DESTRUCTION_POLICY_REMOVE_FRAGMENT - if min_capacity < entity_count then - min_capacity = entity_count - end + 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 - if min_capacity > 0 and min_capacity < 4 then - min_capacity = 4 - end + __lua_table_move( + minor_chunk_list, 1, minor_chunk_count, + chunk_stack_size + 1, chunk_stack) - local old_capacity = chunk.__entity_capacity - if old_capacity <= min_capacity then - -- no need to shrink, the chunk is already small enough - return - end + 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 - do - local old_entity_list = chunk.__entity_list - local new_entity_list = __lua_table_new(min_capacity) + 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 - __lua_table_move( - old_entity_list, 1, entity_count, - 1, new_entity_list) + removing_count = removing_count + 1 + removing_target_list[removing_count] = false + removing_fragment_list[removing_count] = entity + end + end - chunk.__entity_list = new_entity_list - end + if entity_target_fragments then + target_fragments[entity] = nil - do - local component_count = chunk.__component_count - local component_storages = chunk.__component_storages - local component_reallocs = chunk.__component_reallocs + ---@type evolved.fragment[]?, integer + local target_fragment_list, target_fragment_count = nil, 1 - for component_index = 1, component_count do - local component_realloc = component_reallocs[component_index] + 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 - local new_component_storage ---@type evolved.storage? - local old_component_storage = component_storages[component_index] + for target_fragment_index = 1, target_fragment_count do + 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 + and target_fragment_search_index.__target_fragment == target_fragment + and target_fragment_search_index.__head_entities[entity] + then + local link_policy = target_fragment_search_index.__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_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)) + end - if component_realloc then - new_component_storage = component_realloc( - old_component_storage, old_capacity, min_capacity) - else - new_component_storage = __default_realloc( - old_component_storage, old_capacity, min_capacity) - 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 - if min_capacity > 0 and not new_component_storage then - __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( - '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 + removing_count = removing_count + 1 + removing_target_list[removing_count] = entity + removing_fragment_list[removing_count] = target_fragment + end + end + end + end + end end - - component_storages[component_index] = new_component_storage end end - chunk.__entity_capacity = min_capacity -end + if removing_count > 0 then + ---@cast removing_target_list -? + ---@cast removing_fragment_list -? ----@param chunk_list evolved.chunk[] ----@param chunk_count integer -function __clear_chunk_list(chunk_list, chunk_count) - if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') - end + for removing_index = 1, removing_count do + local target = removing_target_list[removing_index] + local fragment = removing_fragment_list[removing_index] - if chunk_count == 0 then - return - end + if target then + local link_policy = search_indices[fragment].__link_policy + local referrer_count = __evolved_multi_search_to(entity_stack, 1, fragment, target) - for chunk_index = 1, chunk_count do - local chunk = chunk_list[chunk_index] - __chunk_clear(chunk) - end -end + for referrer_index = 1, referrer_count do + local referrer = entity_stack[referrer_index] ----@param entity evolved.entity -function __clear_entity_one(entity) - if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') - end + entity_stack[referrer_index] = nil - local entity_chunks = __entity_chunks - local entity_places = __entity_places + if not processed_entity_set[referrer] then + 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 + local fragment_chunks = minor_chunks[fragment] + local fragment_chunk_list = fragment_chunks.__item_list + local fragment_chunk_count = fragment_chunks.__item_count - local entity_primary = entity % 2 ^ 20 + __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.entity_list, removing_target_list, false, true) + __release_table(__table_pool_tag.fragment_list, removing_fragment_list, false, true) + end + + for processed_entity_index = 1, processed_entity_count do + local entity = processed_entity_list[processed_entity_index] + local entity_primary = entity % 1048576 - if __freelist_ids[entity_primary] ~= entity then - -- nothing to clear from non-alive entities - else local chunk = entity_chunks[entity_primary] local place = entity_places[entity_primary] @@ -3760,21 +4860,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 @@ -3785,11 +4887,19 @@ function __clear_entity_one(entity) 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] + 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 % 1048576 + + local chunk = entity_chunks[entity_primary] + local place = entity_places[entity_primary] if chunk then __detach_entity(chunk, place) @@ -3800,269 +4910,370 @@ function __clear_entity_one(entity) __structural_changes = __structural_changes + 1 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) + __release_table(__table_pool_tag.fragment_set, processed_entity_set, true, false) end ----@param entity_list evolved.entity[] ----@param entity_count integer -function __clear_entity_list(entity_list, entity_count) +---@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 - __error_fmt('this operation should be deferred') + __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] + + 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 index_policy == __INDEX_POLICY_SELF then + __remove_entity_fragments(entity, fragment) + return + end + + local new_component ---@type evolved.component? + local old_component = component_storage[place] + + local new_target_count = 0 + + 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 = {} + + 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 + + if new_target_count == 0 then + __remove_entity_fragments(entity, fragment) + return + end + + component_storage[place] = new_component + + if fragment_search_index then + __search_index_fns.filter(fragment_search_index, entity, destroying_entity_set) end - for entity_index = 1, entity_count do - local entity = entity_list[entity_index] - __clear_entity_one(entity) + 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_component, old_component) + end + + if fragment_assign then + fragment_assign(entity, fragment, new_component, old_component) + end end end ---@param entity evolved.entity -function __destroy_entity_one(entity) +---@param ... evolved.fragment fragments +function __remove_entity_fragments(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_primary = entity % 1048576 + + 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 entity_primary = entity % 2 ^ 20 + local old_chunk = entity_chunks[entity_primary] + local old_place = entity_places[entity_primary] - if __freelist_ids[entity_primary] ~= entity then - -- this entity is not alive, nothing to purge - else - local chunk = entity_chunks[entity_primary] - local place = entity_places[entity_primary] + local new_chunk = __chunk_without_fragments(old_chunk or nil, ...) - if chunk then - local chunk_remove_fragment_list = chunk.__remove_fragment_list + 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 - if chunk_remove_fragment_list then - local chunk_component_indices = chunk.__component_indices - local chunk_component_storages = chunk.__component_storages + local old_remove_fragment_list = old_chunk.__remove_fragment_list - for chunk_remove_fragment_index = 1, chunk.__remove_fragment_count do - local fragment = chunk_remove_fragment_list[chunk_remove_fragment_index] + if old_remove_fragment_list then + local new_fragment_set = new_chunk and new_chunk.__fragment_set + or __safe_tbls.__EMPTY_FRAGMENT_SET - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + local old_remove_fragment_removes = old_chunk.__remove_fragment_removes + ---@cast old_remove_fragment_removes -? - if fragment_on_remove then - local component_index = chunk_component_indices[fragment] + for old_remove_fragment_index = 1, old_chunk.__remove_fragment_count do + local fragment = old_remove_fragment_list[old_remove_fragment_index] - 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) + if not new_fragment_set[fragment] then + local remove_fragment_remove = old_remove_fragment_removes[old_remove_fragment_index] + + 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] + remove_fragment_remove(entity, fragment, old_component) else - fragment_on_remove(entity, fragment) + remove_fragment_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 chunk_index_fragment_list = chunk.__index_fragment_list + local old_ci = old_component_indices[new_f] + local old_cs = old_component_storages[old_ci] - 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 + new_cs[new_place] = old_cs[old_place] end end - if chunk then - __detach_entity(chunk, place) + do + __detach_entity(old_chunk, old_place) - entity_chunks[entity_primary] = false - entity_places[entity_primary] = 0 + 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 - - __release_id(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 - __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) end end +---@param chunk evolved.chunk ---@param fragment evolved.fragment -function __destroy_fragment_one(fragment) +---@param component evolved.component +function __chunk_assign(chunk, fragment, component) if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + __debug_fns.error_fmt('batched chunk operations should be deferred') end - ---@type evolved.fragment[] - local processing_fragment_stack = __acquire_table(__table_pool_tag.fragment_list) - local processing_fragment_stack_size = 0 - - do - processing_fragment_stack_size = processing_fragment_stack_size + 1 - processing_fragment_stack[processing_fragment_stack_size] = fragment - end + local chunk_entity_list = chunk.__entity_list + local chunk_entity_count = chunk.__entity_count - __destroy_fragment_stack( - processing_fragment_stack, - processing_fragment_stack_size) + local chunk_component_indices = chunk.__component_indices + local chunk_component_storages = chunk.__component_storages - __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, - true, true) -end + 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 ----@param fragment_list evolved.fragment[] ----@param fragment_count integer -function __destroy_fragment_list(fragment_list, fragment_count) - if __defer_depth <= 0 then - __error_fmt('this operation should be deferred') + if chunk_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 - - do - __lua_table_move( - fragment_list, 1, fragment_count, - processing_fragment_stack_size + 1, processing_fragment_stack) + local component_index = chunk_component_indices[fragment] - processing_fragment_stack_size = processing_fragment_stack_size + fragment_count + if not component_index then + return end - __destroy_fragment_stack( - processing_fragment_stack, - processing_fragment_stack_size) + local component_storage = chunk_component_storages[component_index] + local component_realloc = chunk_component_reallocs[component_index] + local component_compmove = chunk_component_compmoves[component_index] - __release_table(__table_pool_tag.fragment_list, processing_fragment_stack, - true, true) -end + ---@type evolved.set_hook?, evolved.assign_hook? + local assign_fragment_set, assign_fragment_assign ----@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 - __error_fmt('this operation should be deferred') + if chunk.__has_assign_hooks then + assign_fragment_set = chunk.__component_sets[component_index] + assign_fragment_assign = chunk.__component_assigns[component_index] end - ---@type table - local processed_fragment_set = __acquire_table(__table_pool_tag.fragment_set) + local old_component_storage_for_hooks ---@type evolved.storage? - ---@type evolved.fragment[] - local releasing_fragment_list = __acquire_table(__table_pool_tag.fragment_list) - local releasing_fragment_count = 0 ---@type integer - - local destroy_entity_policy_fragment_list ---@type evolved.fragment[]? - local destroy_entity_policy_fragment_count = 0 ---@type integer - - local remove_fragment_policy_fragment_list ---@type evolved.fragment[]? - local remove_fragment_policy_fragment_count = 0 ---@type integer + 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 + old_component_storage_for_hooks = __acquire_table(__table_pool_tag.component_list) + end - while processing_fragment_stack_size > 0 do - local processing_fragment = processing_fragment_stack[processing_fragment_stack_size] + 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 - processing_fragment_stack[processing_fragment_stack_size] = nil - processing_fragment_stack_size = processing_fragment_stack_size - 1 + ---@cast old_component_storage_for_hooks -? - if processed_fragment_set[processing_fragment] then - -- this fragment has already beed processed + 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 - processed_fragment_set[processing_fragment] = true - - do - releasing_fragment_count = releasing_fragment_count + 1 - releasing_fragment_list[releasing_fragment_count] = processing_fragment + for place = 1, chunk_entity_count do + old_component_storage_for_hooks[place] = component_storage[place] end + end + end - 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 - - 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 - - __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 + local fragment_default = chunk_component_defaults[component_index] + local fragment_duplicate = chunk_component_duplicates[component_index] - 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)', - __id_name(processing_fragment_destruction_policy), __id_name(processing_fragment)) - 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 destroy_entity_policy_fragment_list then - for i = 1, destroy_entity_policy_fragment_count do - local minor = destroy_entity_policy_fragment_list[i] - __trace_minor_chunks(minor, __chunk_clear) - end + local fragment_search_index = chunk.__has_index_fragments + and __search_indices[fragment] + or nil - __release_table(__table_pool_tag.fragment_list, destroy_entity_policy_fragment_list, - destroy_entity_policy_fragment_count == 0, true) + if fragment_search_index then + __search_index_fns.multi_assign(fragment_search_index, + chunk_entity_list, component_storage, 1, chunk_entity_count) end - if remove_fragment_policy_fragment_list then - 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 + if old_component_storage_for_hooks then + for place = 1, chunk_entity_count do + local entity = chunk_entity_list[place] - __release_table(__table_pool_tag.fragment_list, remove_fragment_policy_fragment_list, - remove_fragment_policy_fragment_count == 0, true) - end + local new_component = component_storage[place] + local old_component = old_component_storage_for_hooks[place] - if releasing_fragment_count > 0 then - __destroy_entity_list(releasing_fragment_list, releasing_fragment_count) - end + if assign_fragment_set then + assign_fragment_set(entity, fragment, new_component, old_component) + end - __release_table(__table_pool_tag.fragment_list, releasing_fragment_list, - releasing_fragment_count == 0, true) + if assign_fragment_assign then + assign_fragment_assign(entity, fragment, new_component, old_component) + end + end - __release_table(__table_pool_tag.fragment_set, processed_fragment_set, - true, false) + 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, false, true) + end + end end ---@param old_chunk evolved.chunk ---@param fragment evolved.fragment ---@param component evolved.component -function __chunk_set(old_chunk, fragment, component) +function __chunk_insert(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 local old_entity_count = old_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 @@ -4072,433 +5283,238 @@ function __chunk_set(old_chunk, fragment, component) local new_chunk = __chunk_with_fragment(old_chunk, 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 - - ---@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 - - ---@type evolved.search_index? - local fragment_search_index = old_chunk_has_index_fragments - 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 = 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 - - if fragment_on_assign then - __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_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 + local ini_new_chunk = new_chunk + local ini_fragment_set = ini_new_chunk.__fragment_set - 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 + while new_chunk and new_chunk.__has_required_fragments do + local required_chunk = new_chunk.__with_required_fragments - if fragment_on_assign then - __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) - end - end - end - else - -- nothing - end + if required_chunk and not required_chunk.__unreachable_or_collected then + new_chunk = required_chunk else - 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 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 - end - else - -- nothing - end + required_chunk = __chunk_requires(new_chunk) + new_chunk.__with_required_fragments, new_chunk = required_chunk, required_chunk end + end - if fragment_search_index then - local old_component_index = old_component_indices[fragment] + 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 - if old_component_index then - local old_component_storage = old_component_storages[old_component_index] + local sum_entity_count = old_entity_count + new_chunk.__entity_count - __search_index_fns.multi_assign(fragment_search_index, - old_entity_list, old_component_storage, 1, old_entity_count) - end - end - else - local ini_new_chunk = new_chunk - local ini_fragment_set = ini_new_chunk.__fragment_set + if sum_entity_count > new_chunk.__entity_capacity then + __expand_chunk(new_chunk, sum_entity_count) + end - while new_chunk and new_chunk.__has_required_fragments do - local required_chunk = new_chunk.__with_required_fragments + local new_entity_list = new_chunk.__entity_list + local new_entity_count = new_chunk.__entity_count - if required_chunk and not required_chunk.__unreachable_or_collected then - new_chunk = required_chunk + 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 - required_chunk = __chunk_requires(new_chunk) - new_chunk.__with_required_fragments, new_chunk = required_chunk, required_chunk - 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_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 - - 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) - 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 - __expand_chunk(new_chunk, sum_entity_count) - end - - local new_entity_list = new_chunk.__entity_list - local new_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] - - 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 + 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 - - new_chunk.__entity_count = sum_entity_count - end - - do - local entity_chunks = __entity_chunks - local entity_places = __entity_places - - 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 - - __detach_all_entities(old_chunk) - end - - if fragment_on_set or fragment_on_insert then - 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 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] + 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_component_storage[new_place] = new_component + old_entity_list, new_entity_list = + new_entity_list, old_entity_list + end - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment, new_component) - end + new_chunk.__entity_count = sum_entity_count + 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] + do + local entity_chunks = __entity_chunks + local entity_places = __entity_places - if fragment_on_set then - __defer_call_hook(fragment_on_set, entity, fragment) - end + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] + local entity_primary = entity % 1048576 + entity_chunks[entity_primary] = new_chunk + entity_places[entity_primary] = new_place + 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] + __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 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 + 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 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 new_place = new_entity_count + 1, sum_entity_count do + new_component_storage[new_place] = new_component + end 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] + 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) 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?, evolved.set_hook?, evolved.insert_hook? - local req_fragment_default, req_fragment_duplicate, req_fragment_on_set, req_fragment_on_insert + if ini_fragment_set[req_fragment] then + -- this fragment has already been initialized + else + 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] - 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) + 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 + end end - ---@type evolved.search_index? - 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 - if req_fragment_on_set or req_fragment_on_insert then - local req_component_index = new_component_indices[req_fragment] + 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 - if req_component_index then - local req_component_storage = new_component_storages[req_component_index] + local new_insert_fragment_list = new_chunk.__insert_fragment_list - if req_fragment_duplicate then - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] + if new_insert_fragment_list then + local old_fragment_set = old_chunk.__fragment_set - 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 + local new_insert_fragment_sets = new_chunk.__insert_fragment_sets + local new_insert_fragment_inserts = new_chunk.__insert_fragment_inserts - req_component_storage[new_place] = req_component + ---@cast new_insert_fragment_sets -? + ---@cast new_insert_fragment_inserts -? - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment, req_component) - end + for new_insert_fragment_index = 1, new_chunk.__insert_fragment_count do + local new_fragment = new_insert_fragment_list[new_insert_fragment_index] - 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 + if not old_fragment_set[new_fragment] then + 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] - for new_place = new_entity_count + 1, sum_entity_count do - local entity = new_entity_list[new_place] + do + local new_component_index = new_component_indices[new_fragment] - req_component_storage[new_place] = req_component + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment, req_component) - end + for new_place = new_entity_count + 1, sum_entity_count do + local entity = new_entity_list[new_place] - 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] + local new_component = new_component_storage[new_place] - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment) - end + if new_insert_fragment_set then + new_insert_fragment_set(entity, new_fragment, new_component) + end - if req_fragment_on_insert then - __defer_call_hook(req_fragment_on_insert, entity, req_fragment) - end + if new_insert_fragment_insert then + new_insert_fragment_insert(entity, new_fragment, new_component) 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] + 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 + if new_insert_fragment_set then + new_insert_fragment_set(entity, new_fragment) end - else - -- nothing - end - end - - if req_fragment_search_index then - local req_component_index = new_component_indices[req_fragment] - - if req_component_index then - local req_component_storage = new_component_storages[req_component_index] - __search_index_fns.multi_insert(req_fragment_search_index, - new_entity_list, req_component_storage, new_entity_count + 1, sum_entity_count) + if new_insert_fragment_insert then + new_insert_fragment_insert(entity, new_fragment) + end end end end end end - - __structural_changes = __structural_changes + 1 end + + __structural_changes = __structural_changes + 1 end ---@param old_chunk evolved.chunk ---@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('#', ...) @@ -4529,14 +5545,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 @@ -4545,12 +5563,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 @@ -4569,7 +5587,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 @@ -4640,7 +5657,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 @@ -4653,7 +5670,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 @@ -4667,7 +5684,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 @@ -4683,13 +5700,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 @@ -4698,12 +5717,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 @@ -4716,7 +5735,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 @@ -4728,7 +5746,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 @@ -4747,93 +5765,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, + + set = 7, + remove_one = 8, + clear_one = 9, + destroy_one = 10, - spawn_entity = 2, - multi_spawn_entity = 3, + spawn_entity = 11, + multi_spawn_entity = 12, - clone_entity = 4, - multi_clone_entity = 5, + clone_entity = 13, + multi_clone_entity = 14, - __count = 5, + __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 @@ -4863,7 +5943,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? @@ -4920,7 +6000,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? @@ -4974,7 +6054,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? @@ -5031,7 +6111,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? @@ -5074,7 +6154,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 @@ -5105,7 +6185,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 @@ -5149,7 +6229,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 @@ -5157,7 +6237,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) @@ -5166,18 +6246,19 @@ 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 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() - __error_fmt('system execution failed: %s', result) + __debug_fns.error_fmt('system execution failed: %s', result) end end __evolved_commit() @@ -5191,19 +6272,18 @@ 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 - __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 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 @@ -5288,15 +6368,15 @@ end ---@return evolved.id id ---@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) + 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 - __error_fmt('the secondary (%d) is out of range [1, 2 ^ 20 - 1]', secondary) + 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 @@ -5304,8 +6384,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 @@ -5324,7 +6403,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 @@ -5356,7 +6435,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] @@ -5372,7 +6451,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 @@ -5450,16 +6529,14 @@ 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 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 @@ -5487,14 +6564,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 @@ -5569,23 +6646,21 @@ 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 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, @@ -5606,7 +6681,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 @@ -5661,7 +6736,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 @@ -5725,7 +6800,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 @@ -5753,7 +6828,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 @@ -5781,7 +6856,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 @@ -5803,7 +6878,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 @@ -5825,24 +6900,32 @@ 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 - __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 if __debug_mode then - local fragment_primary = fragment % 2 ^ 20 + local fragment_primary = fragment % 1048576 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 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 @@ -5860,27 +6943,26 @@ 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 + local old_component_index = old_component_indices[fragment] - ---@type evolved.default?, evolved.duplicate?, evolved.set_hook?, evolved.assign_hook? - local fragment_default, fragment_duplicate, fragment_on_set, fragment_on_assign + if old_component_index then + local old_component_storage = old_component_storages[old_component_index] - 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.set_hook?, evolved.assign_hook? + local assign_fragment_set, assign_fragment_assign - ---@type evolved.search_index? - local fragment_search_index = old_chunk_has_index_fragments - and __search_indices[fragment] - or nil + 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 - local old_component_index = old_component_indices[fragment] + ---@type evolved.default?, evolved.duplicate? + local fragment_default, fragment_duplicate - if old_component_index then - local old_component_storage = old_component_storages[old_component_index] + 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 @@ -5890,16 +6972,20 @@ 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) + 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 - if fragment_on_assign then - __defer_call_hook(fragment_on_assign, entity, fragment, new_component, old_component) + if assign_fragment_set then + assign_fragment_set(entity, fragment, new_component, old_component) end - if fragment_search_index then - __search_index_fns.assign(fragment_search_index, entity, new_component) + if assign_fragment_assign then + assign_fragment_assign(entity, fragment, new_component, old_component) end else -- nothing @@ -5922,23 +7008,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_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 - - 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) - 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 @@ -5981,6 +7050,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 @@ -5988,25 +7065,13 @@ 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 + 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 - 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 @@ -6014,30 +7079,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?, evolved.set_hook?, evolved.insert_hook? - local req_fragment_default, req_fragment_duplicate, req_fragment_on_set, req_fragment_on_insert - - 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) - 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 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 @@ -6050,148 +7116,106 @@ 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 + 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 - else - if req_fragment_on_set then - __defer_call_hook(req_fragment_on_set, entity, req_fragment) - end - - if req_fragment_on_insert then - __defer_call_hook(req_fragment_on_insert, entity, req_fragment) - end end end end end - end - - __evolved_commit() -end - ----@param entity evolved.entity ----@param ... evolved.fragment fragments -function __evolved_remove(entity, ...) - local fragment_count = __lua_select('#', ...) - - if fragment_count == 0 then - return - end - - local entity_primary = entity % 2 ^ 20 - - if __freelist_ids[entity_primary] ~= entity then - -- nothing to remove from non-alive entities - return - end - - if __defer_depth > 0 then - __defer_call_hook(__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() + local new_insert_fragment_list = new_chunk.__insert_fragment_list - 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 + if new_insert_fragment_list then + local old_fragment_set = old_chunk and old_chunk.__fragment_set + or __safe_tbls.__EMPTY_FRAGMENT_SET - local old_remove_fragment_list = old_chunk.__remove_fragment_list + local new_insert_fragment_sets = new_chunk.__insert_fragment_sets + local new_insert_fragment_inserts = new_chunk.__insert_fragment_inserts - if old_remove_fragment_list then - local new_fragment_set = new_chunk and new_chunk.__fragment_set - or __safe_tbls.__EMPTY_FRAGMENT_SET + ---@cast new_insert_fragment_sets -? + ---@cast new_insert_fragment_inserts -? - for old_remove_fragment_index = 1, old_chunk.__remove_fragment_count do - local fragment = old_remove_fragment_list[old_remove_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 new_fragment_set[fragment] then - ---@type evolved.remove_hook? - local fragment_on_remove = __evolved_get(fragment, __ON_REMOVE) + if not old_fragment_set[new_fragment] then + 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 fragment_on_remove then - local old_component_index = old_component_indices[fragment] + do + local new_component_index = new_component_indices[new_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 + if new_component_index then + local new_component_storage = new_component_storages[new_component_index] - local old_index_fragment_list = old_chunk.__index_fragment_list + local new_component = new_component_storage[new_place] - if old_index_fragment_list then - local new_fragment_set = new_chunk and new_chunk.__fragment_set - or __safe_tbls.__EMPTY_FRAGMENT_SET + if new_fragment_set then + new_fragment_set(entity, new_fragment, new_component) + end - for old_index_fragment_index = 1, old_chunk.__index_fragment_count do - local fragment = old_index_fragment_list[old_index_fragment_index] + if new_fragment_insert then + new_fragment_insert(entity, new_fragment, new_component) + end + else + if new_fragment_set then + new_fragment_set(entity, new_fragment) + end - if not new_fragment_set[fragment] then - local fragment_search_index = __search_indices[fragment] - __search_index_fns.remove(fragment_search_index, entity) + if new_fragment_insert then + new_fragment_insert(entity, new_fragment) + end + end + end end end end + end - if new_chunk then - local new_component_count = new_chunk.__component_count - local new_component_storages = new_chunk.__component_storages - local new_component_fragments = new_chunk.__component_fragments + __evolved_commit() +end - local new_place = new_chunk.__entity_count + 1 +---@param entity evolved.entity +---@param ... evolved.fragment fragments +function __evolved_remove(entity, ...) + local fragment_count = __lua_select('#', ...) - if new_place > new_chunk.__entity_capacity then - __expand_chunk(new_chunk, new_place) - end + if fragment_count == 0 then + return + end - local new_entity_list = new_chunk.__entity_list + local entity_primary = entity % 1048576 - new_entity_list[new_place] = entity - new_chunk.__entity_count = new_place + if __freelist_ids[entity_primary] ~= entity then + -- nothing to remove from non-alive entities + return + end - for new_ci = 1, new_component_count do - local new_f = new_component_fragments[new_ci] - local new_cs = new_component_storages[new_ci] + if __defer_depth > 0 then + if fragment_count == 1 then + local length = __defer_length + local bytecode = __defer_bytecode - local old_ci = old_component_indices[new_f] - local old_cs = old_component_storages[old_ci] + bytecode[length + 1] = __defer_op.remove_one + bytecode[length + 2] = entity + bytecode[length + 3] = ... - new_cs[new_place] = old_cs[old_place] - end + __defer_length = length + 3 + else + __defer_call_func(__evolved_remove, fragment_count + 1, entity, ...) end + return + 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 + __evolved_defer() - __structural_changes = __structural_changes + 1 - end + do + __remove_entity_fragments(entity, ...) end __evolved_commit() @@ -6210,39 +7234,24 @@ function __evolved_clear(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_clear, ...) + __defer_call_func(__evolved_clear, argument_count, ...) return end __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, ...) - 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 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, - purging_entity_count == 0, true) - end end __evolved_commit() @@ -6251,14 +7260,20 @@ 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 __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 @@ -6283,7 +7298,7 @@ function __evolved_destroy(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_destroy, ...) + __defer_call_func(__evolved_destroy, argument_count, ...) return end @@ -6291,53 +7306,41 @@ 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 - - local purging_fragment_list ---@type evolved.fragment[]? - local purging_fragment_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 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 + elseif not minor_chunks[entity] and not target_fragments[entity] then + __destroy_entity_one(entity) else - local is_fragment = minor_chunks[entity] - - if not is_fragment 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_stack then + ---@type evolved.entity[] + destroying_entity_stack = __acquire_table(__table_pool_tag.entity_list) end + + destroying_entity_stack_size = destroying_entity_stack_size + 1 + destroying_entity_stack[destroying_entity_stack_size] = entity 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, - purging_fragment_count == 0, true) - end + 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) - 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.chunk_list, destroying_chunk_stack, true, true) + __release_table(__table_pool_tag.entity_list, destroying_entity_stack, true, true) end end @@ -6347,25 +7350,40 @@ 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 __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 + elseif not __minor_chunks[entity] and not __target_fragments[entity] then + __destroy_entity_one(entity) else - local is_fragment = __minor_chunks[entity] + ---@type evolved.chunk[] + local destroying_chunk_stack = __acquire_table(__table_pool_tag.chunk_list) - if not is_fragment then - __destroy_entity_one(entity) - else - __destroy_fragment_one(entity) - end + ---@type evolved.entity[] + local destroying_entity_stack = __acquire_table(__table_pool_tag.entity_list) + + destroying_entity_stack[1] = entity + + __destroy_entity_stack( + destroying_chunk_stack, 0, + destroying_entity_stack, 1) + + __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 @@ -6376,52 +7394,74 @@ 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 - __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 if __debug_mode then - local fragment_primary = fragment % 2 ^ 20 + local fragment_primary = fragment % 1048576 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 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 __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) - end + 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 - chunk_count = chunk_count + 1 - chunk_list[chunk_count] = chunk + 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 - for chunk_index = 1, chunk_count do - ---@cast chunk_list -? - local chunk = chunk_list[chunk_index] - __chunk_set(chunk, fragment, component) + 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 chunk_list then - __release_table(__table_pool_tag.chunk_list, chunk_list, - chunk_count == 0, true) + 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 @@ -6437,43 +7477,41 @@ 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 - __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 if __defer_depth > 0 then - __defer_call_hook(__evolved_batch_remove, query, ...) + __defer_call_func(__evolved_batch_remove, fragment_count + 1, query, ...) return end __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, - chunk_count == 0, true) + __release_table(__table_pool_tag.chunk_list, remove_chunk_list, false, true) end end @@ -6489,41 +7527,44 @@ 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 __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 local query = __lua_select(argument_index, ...) - local query_primary = query % 2 ^ 20 + local query_primary = query % 1048576 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 - 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, - chunk_count == 0, 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 @@ -6539,87 +7580,47 @@ 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 __evolved_defer() do - local minor_chunks = __minor_chunks - - 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 destroy_chunk_stack ---@type evolved.chunk[]? + local destroy_chunk_stack_size = 0 ---@type integer 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 - __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 - 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 destroy_chunk_stack then + ---@type evolved.chunk[] + destroy_chunk_stack = __acquire_table(__table_pool_tag.chunk_list) end - for i = 1, entity_count do - local entity = entity_list[i] - - local is_fragment = minor_chunks[entity] - - if not is_fragment 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 + destroy_chunk_stack_size = destroy_chunk_stack_size + 1 + destroy_chunk_stack[destroy_chunk_stack_size] = 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, - purging_fragment_count == 0, true) - end + if destroy_chunk_stack then + ---@type evolved.entity[] + local destroying_entity_stack = __acquire_table(__table_pool_tag.entity_list) - 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) - end + __destroy_entity_stack( + destroy_chunk_stack, destroy_chunk_stack_size, + destroying_entity_stack, 0) - 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.chunk_list, destroy_chunk_stack, true, true) + __release_table(__table_pool_tag.entity_list, destroying_entity_stack, true, true) end end @@ -6631,10 +7632,10 @@ 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 - __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 @@ -6660,10 +7661,10 @@ 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 - __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 @@ -6684,7 +7685,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 @@ -6740,7 +7741,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 @@ -6788,8 +7789,9 @@ 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.warning_fmt('the fragment (%s) is not indexed and cannot be searched', __id_name(fragment)) + return end return fragment_search_index.__head_entities[component] @@ -6815,8 +7817,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 - __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 @@ -6828,11 +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 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[entity % 2 ^ 20] + entity = next_entities[component_offset + entity % 1048576] entity_count = entity_count + 1 until entity == head_entity end @@ -6849,26 +7858,23 @@ function __evolved_reindex(...) end if __defer_depth > 0 then - __defer_call_hook(__evolved_reindex, ...) + __defer_call_func(__evolved_reindex, fragment_count, ...) return end 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 - __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 + __chunk_fns.trace_minor_chunks(fragment, __chunk_fns.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 @@ -6882,13 +7888,13 @@ 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 - __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) + __iterator_fns.__system_process(system) end end end @@ -6896,14 +7902,14 @@ 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 - __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 - __system_process(system, ...) + __iterator_fns.__system_process(system, ...) end ---@param yesno boolean @@ -6919,7 +7925,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 @@ -6996,19 +8002,54 @@ 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 + + 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) ~= nil 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 = __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) @@ -7018,6 +8059,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 @@ -7053,16 +8095,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() @@ -7265,7 +8320,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 @@ -7339,16 +8394,14 @@ 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 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 @@ -7377,14 +8430,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 @@ -7454,23 +8507,21 @@ 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 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, @@ -7569,11 +8620,11 @@ 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 - __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 @@ -7648,6 +8699,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) @@ -7851,6 +8907,18 @@ 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) + return self:set(__LINK_POLICY, link_policy) +end + ---@param destruction_policy evolved.id ---@return evolved.builder builder function __builder_mt:destruction_policy(destruction_policy) @@ -7863,17 +8931,17 @@ end --- --- -__evolved_set(__ON_SET, __ON_INSERT, __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_INSERT, __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_INSERT, __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_INSERT, __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) --- --- @@ -7881,32 +8949,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(__UNIQUE, __ON_INSERT, __update_major_chunks) -__evolved_set(__UNIQUE, __ON_REMOVE, __update_major_chunks) +__evolved_set(__LINK, __ON_INSERT, __chunk_fns.update_search_index) +__evolved_set(__LINK, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__EXPLICIT, __ON_INSERT, __update_major_chunks) -__evolved_set(__EXPLICIT, __ON_REMOVE, __update_major_chunks) +__evolved_set(__INDEX_POLICY, __ON_SET, __chunk_fns.update_search_index) +__evolved_set(__INDEX_POLICY, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__INTERNAL, __ON_INSERT, __update_major_chunks) -__evolved_set(__INTERNAL, __ON_REMOVE, __update_major_chunks) +__evolved_set(__LINK_POLICY, __ON_SET, __chunk_fns.update_search_index) +__evolved_set(__LINK_POLICY, __ON_REMOVE, __chunk_fns.update_search_index) -__evolved_set(__DEFAULT, __ON_INSERT, __update_major_chunks) -__evolved_set(__DEFAULT, __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(__DUPLICATE, __ON_INSERT, __update_major_chunks) -__evolved_set(__DUPLICATE, __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(__REALLOC, __ON_SET, __update_major_chunks) -__evolved_set(__REALLOC, __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(__COMPMOVE, __ON_SET, __update_major_chunks) -__evolved_set(__COMPMOVE, __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, __chunk_fns.update_major_chunks) +__evolved_set(__DUPLICATE, __ON_REMOVE, __chunk_fns.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, __chunk_fns.update_major_chunks) +__evolved_set(__COMPMOVE, __ON_REMOVE, __chunk_fns.update_major_chunks) --- --- @@ -7917,10 +8994,14 @@ __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) +__evolved_set(__LINK, __TAG) +__evolved_set(__LINK, __UNIQUE) + __evolved_set(__UNIQUE, __TAG) __evolved_set(__EXPLICIT, __TAG) @@ -7939,15 +9020,27 @@ __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) __evolved_set(__ON_ASSIGN, __UNIQUE) @@ -7955,88 +9048,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) ---- ---- ---- ---- ---- - -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 +__evolved_set(__QUERY, __INDEX) +__evolved_set(__QUERY, __LINK) +__evolved_set(__QUERY, __LINK_POLICY, __LINK_POLICY_DESTROY_ENTITY) --- --- @@ -8047,7 +9064,7 @@ end ---@param query evolved.query ---@param include_list evolved.fragment[] __evolved_set(__INCLUDES, __ON_SET, function(query, _, include_list) - __hook_fns.remove_query(query) + __chunk_fns.reset_query_chunks(query) local include_count = #include_list @@ -8062,18 +9079,12 @@ __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) + __chunk_fns.reset_query_chunks(query) __sorted_includes[query] = nil - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) --- @@ -8085,7 +9096,7 @@ end) ---@param query evolved.query ---@param exclude_list evolved.fragment[] __evolved_set(__EXCLUDES, __ON_SET, function(query, _, exclude_list) - __hook_fns.remove_query(query) + __chunk_fns.reset_query_chunks(query) local exclude_count = #exclude_list @@ -8100,18 +9111,12 @@ __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) + __chunk_fns.reset_query_chunks(query) __sorted_excludes[query] = nil - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) --- @@ -8123,7 +9128,7 @@ end) ---@param query evolved.query ---@param variant_list evolved.fragment[] __evolved_set(__VARIANTS, __ON_SET, function(query, _, variant_list) - __hook_fns.remove_query(query) + __chunk_fns.reset_query_chunks(query) local variant_count = #variant_list @@ -8138,18 +9143,12 @@ __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) + __chunk_fns.reset_query_chunks(query) __sorted_variants[query] = nil - - __hook_fns.insert_query(query) - __update_major_chunks(query) end) --- @@ -8175,12 +9174,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) --- @@ -8193,6 +9193,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') @@ -8224,6 +9225,17 @@ __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') +__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') @@ -8238,6 +9250,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) @@ -8269,6 +9282,17 @@ __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) +__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) @@ -8283,6 +9307,7 @@ evolved.TAG = __TAG evolved.NAME = __NAME evolved.INDEX = __INDEX +evolved.LINK = __LINK evolved.UNIQUE = __UNIQUE evolved.EXPLICIT = __EXPLICIT evolved.INTERNAL = __INTERNAL @@ -8314,6 +9339,17 @@ 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 +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 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", + } +}