Merge branch 'feature/temp_remove_pairs' into dev

This commit is contained in:
BlackMATov
2025-08-20 23:11:55 +07:00
15 changed files with 1005 additions and 613 deletions

5
.luacov Normal file
View File

@@ -0,0 +1,5 @@
modules = {
['evolved'] = 'evolved.lua'
}
reporter = 'html'
reportfile = 'luacov.report.html'

View File

@@ -35,6 +35,7 @@
- [Spawning Entities](#spawning-entities) - [Spawning Entities](#spawning-entities)
- [Entity Builders](#entity-builders) - [Entity Builders](#entity-builders)
- [Access Operations](#access-operations) - [Access Operations](#access-operations)
- [Iterating Over Fragments](#iterating-over-fragments)
- [Modifying Operations](#modifying-operations) - [Modifying Operations](#modifying-operations)
- [Debug Mode](#debug-mode) - [Debug Mode](#debug-mode)
- [Queries](#queries) - [Queries](#queries)
@@ -94,7 +95,7 @@ luarocks install evolved.lua
## Quick Start ## Quick Start
To get started with `evolved.lua`, read the [Overview](#overview) section to understand the basic concepts and how to use the library. After that, check the [Example](develop/example.lua), which demonstrates complex usage of the library. Finally, refer to the [Cheat Sheet](#cheat-sheet) for a quick reference of all the functions and classes provided by the library. To get started with `evolved.lua`, read the [Overview](#overview) section to understand the basic concepts and how to use the library. After that, check the [Samples](develop/samples), which demonstrate complex usage of the library. Finally, refer to the [Cheat Sheet](#cheat-sheet) for a quick reference of all the functions and classes provided by the library.
## Overview ## Overview
@@ -146,14 +147,17 @@ function evolved.alive_any(...) end
Sometimes (for debugging purposes, for example), it is necessary to extract the index and version from an identifier or to pack them back into an identifier. The [`evolved.pack`](#evolvedpack) and [`evolved.unpack`](#evolvedunpack) functions can be used for this purpose. Sometimes (for debugging purposes, for example), it is necessary to extract the index and version from an identifier or to pack them back into an identifier. The [`evolved.pack`](#evolvedpack) and [`evolved.unpack`](#evolvedunpack) functions can be used for this purpose.
```lua ```lua
---@param index integer ---@param primary integer
---@param version integer ---@param secondary integer
---@return evolved.id id ---@return evolved.id id
function evolved.pack(index, version) end ---@nodiscard
function evolved.pack(primary, secondary) end
---@param id evolved.id ---@param id evolved.id
---@return integer index ---@return integer primary
---@return integer version ---@return integer secondary
---@return integer options
---@nodiscard
function evolved.unpack(id) end function evolved.unpack(id) end
``` ```
@@ -447,6 +451,35 @@ The [`evolved.alive`](#evolvedalive) function checks whether an entity is alive.
All of these functions can be safely called on non-alive entities and non-alive fragments. Also, they do not cause any structural changes, because they do not modify anything. All of these functions can be safely called on non-alive entities and non-alive fragments. Also, they do not cause any structural changes, because they do not modify anything.
### Iterating Over Fragments
Sometimes, you may need to iterate over all fragments attached to an entity. You can use the [`evolved.each`](#evolvedeach) function for this purpose.
```lua
local evolved = require 'evolved'
local health = evolved.builder()
:name('health')
:spawn()
local stamina = evolved.builder()
:name('stamina')
:spawn()
local player = evolved.builder()
:set(health, 100)
:set(stamina, 50)
:spawn()
for fragment, component in evolved.each(player) do
print(string.format('Fragment (%s) has value %d',
evolved.name(fragment), component))
end
```
> [!NOTE]
> [Structural changes](#structural-changes) are not allowed during iteration. If you want to spawn new entities or insert/remove fragments while iterating, defer these operations until the iteration is complete. See the [Deferred Operations](#deferred-operations) section for more details.
### Modifying Operations ### Modifying Operations
The library provides a classic set of functions for modifying entities. These functions are used to insert, override, and remove fragments from entities. The library provides a classic set of functions for modifying entities. These functions are used to insert, override, and remove fragments from entities.
@@ -1028,6 +1061,7 @@ NAME :: fragment
UNIQUE :: fragment UNIQUE :: fragment
EXPLICIT :: fragment EXPLICIT :: fragment
INTERNAL :: fragment
DEFAULT :: fragment DEFAULT :: fragment
DUPLICATE :: fragment DUPLICATE :: fragment
@@ -1061,9 +1095,10 @@ DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id
``` ```
id :: integer? -> id... id :: integer? -> id...
name :: id... -> string...
pack :: integer, integer -> id pack :: integer, integer -> id
unpack :: id -> integer, integer unpack :: id -> integer, integer, integer
defer :: boolean defer :: boolean
commit :: boolean commit :: boolean
@@ -1146,6 +1181,7 @@ builder_mt:name :: string -> builder
builder_mt:unique :: builder builder_mt:unique :: builder
builder_mt:explicit :: builder builder_mt:explicit :: builder
builder_mt:internal :: builder
builder_mt:default :: component -> builder builder_mt:default :: component -> builder
builder_mt:duplicate :: {component -> component} -> builder builder_mt:duplicate :: {component -> component} -> builder
@@ -1200,6 +1236,8 @@ builder_mt:destruction_policy :: id -> builder
### `evolved.EXPLICIT` ### `evolved.EXPLICIT`
### `evolved.INTERNAL`
### `evolved.DEFAULT` ### `evolved.DEFAULT`
### `evolved.DUPLICATE` ### `evolved.DUPLICATE`
@@ -1249,22 +1287,32 @@ builder_mt:destruction_policy :: id -> builder
function evolved.id(count) end function evolved.id(count) end
``` ```
### `evolved.name`
```lua
---@param ... evolved.id ids
---@return string... names
---@nodiscard
function evolved.name(...) end
```
### `evolved.pack` ### `evolved.pack`
```lua ```lua
---@param index integer ---@param primary integer
---@param version integer ---@param secondary integer
---@return evolved.id id ---@return evolved.id id
---@nodiscard ---@nodiscard
function evolved.pack(index, version) end function evolved.pack(primary, secondary) end
``` ```
### `evolved.unpack` ### `evolved.unpack`
```lua ```lua
---@param id evolved.id ---@param id evolved.id
---@return integer index ---@return integer primary
---@return integer version ---@return integer secondary
---@return integer options
---@nodiscard ---@nodiscard
function evolved.unpack(id) end function evolved.unpack(id) end
``` ```
@@ -1696,6 +1744,13 @@ function evolved.builder_mt:unique() end
function evolved.builder_mt:explicit() end function evolved.builder_mt:explicit() end
``` ```
#### `evolved.builder_mt:internal`
```lua
---@return evolved.builder builder
function evolved.builder_mt:internal() end
```
#### `evolved.builder_mt:default` #### `evolved.builder_mt:default`
```lua ```lua

View File

@@ -5,7 +5,16 @@
- Improve the performance of required fragments by caching first-level required chunks. - Improve the performance of required fragments by caching first-level required chunks.
- Improve the performance of builders that are used multiple times by caching hint chunks. - Improve the performance of builders that are used multiple times by caching hint chunks.
- Queries can cache major chunks to avoid finding them every time. - Queries can cache major chunks to avoid finding them every time.
- Add multi-spawn to the builder to spawn multiple entities at once.
- Add a function to shrink storages to free unused memory.
- observers and events - observers and events
- add INDEX fragment trait - add INDEX fragment trait
- use compact prefix-tree for chunks - use compact prefix-tree for chunks
- optional ffi component storages - optional ffi component storages
- add EXCLUSIVE fragment trait
## Thoughts
- We can return deferred status from modifying operations and spawn/clone methods.
- Should we make one builder:build method instead of :spawn and :clone?
- Should we cache the result of without_unique_fragments to clone faster?

View File

@@ -1,9 +1,11 @@
require 'develop.example' require 'develop.samples.systems'
require 'develop.untests'
require 'develop.testing.name_tests'
require 'develop.testing.requires_fragment_tests' require 'develop.testing.requires_fragment_tests'
require 'develop.testing.system_as_query_tests' require 'develop.testing.system_as_query_tests'
require 'develop.untests'
require 'develop.unbench' require 'develop.unbench'
require 'develop.usbench' require 'develop.usbench'

View File

@@ -1,5 +1,13 @@
local basics = {} local basics = {}
local MIN_FUZZ_SECS = 0.5
local MIN_BENCH_SECS = 0.1
local MIN_WARMUP_SECS = 0.1
local MIN_FUZZ_ITERS = 100
local MIN_BENCH_ITERS = 100
local MIN_WARMUP_ITERS = 100
local __table_pack = (function() local __table_pack = (function()
---@diagnostic disable-next-line: deprecated ---@diagnostic disable-next-line: deprecated
return table.pack or function(...) return table.pack or function(...)
@@ -23,6 +31,8 @@ end
---@param modname string ---@param modname string
function basics.describe_fuzz(modname) function basics.describe_fuzz(modname)
basics.unload('evolved')
print(string.format('| %s ... |', modname)) print(string.format('| %s ... |', modname))
collectgarbage('collect') collectgarbage('collect')
@@ -39,7 +49,7 @@ function basics.describe_fuzz(modname)
iters = iters + 1 iters = iters + 1
basics.unload(modname) basics.unload(modname)
require(modname) require(modname)
until os.clock() - start_s > 0.5 until iters >= MIN_FUZZ_ITERS and os.clock() - start_s >= MIN_FUZZ_SECS
end) end)
local finish_s = os.clock() local finish_s = os.clock()
@@ -65,17 +75,22 @@ end
---@param init? fun(): ... ---@param init? fun(): ...
---@param fini? fun(...): ... ---@param fini? fun(...): ...
function basics.describe_bench(name, loop, init, fini) function basics.describe_bench(name, loop, init, fini)
basics.unload('evolved')
print(string.format('| %s ... |', name)) print(string.format('| %s ... |', name))
local state = init and __table_pack(init()) or {} local state = init and __table_pack(init()) or {}
do do
local iters = 0
local warmup_s = os.clock() local warmup_s = os.clock()
local success, result = pcall(function() local success, result = pcall(function()
repeat repeat
iters = iters + 1
loop(__table_unpack(state)) loop(__table_unpack(state))
until os.clock() - warmup_s > 0.1 until iters >= MIN_WARMUP_ITERS and os.clock() - warmup_s > MIN_WARMUP_SECS
end) end)
if not success then if not success then
@@ -97,7 +112,7 @@ function basics.describe_bench(name, loop, init, fini)
repeat repeat
iters = iters + 1 iters = iters + 1
loop(__table_unpack(state)) loop(__table_unpack(state))
until os.clock() - start_s > 0.1 until iters >= MIN_BENCH_ITERS and os.clock() - start_s > MIN_BENCH_SECS
end) end)
local finish_s = os.clock() local finish_s = os.clock()

View File

@@ -117,5 +117,12 @@ end
--- ---
--- ---
evo.destroy(__table_unpack(all_entity_list)) if math.random(1, 2) == 1 then
evo.collect_garbage() evo.collect_garbage()
end
evo.destroy(__table_unpack(all_entity_list))
if math.random(1, 2) == 1 then
evo.collect_garbage()
end

View File

@@ -123,5 +123,12 @@ end
--- ---
--- ---
evo.destroy(__table_unpack(all_entity_list)) if math.random(1, 2) == 1 then
evo.collect_garbage() evo.collect_garbage()
end
evo.destroy(__table_unpack(all_entity_list))
if math.random(1, 2) == 1 then
evo.collect_garbage()
end

View File

@@ -77,5 +77,12 @@ end
--- ---
--- ---
evo.destroy(__table_unpack(all_entity_list)) if math.random(1, 2) == 1 then
evo.collect_garbage() evo.collect_garbage()
end
evo.destroy(__table_unpack(all_entity_list))
if math.random(1, 2) == 1 then
evo.collect_garbage()
end

View File

@@ -9,12 +9,25 @@ evo.debug_mode(true)
--- ---
for _ = 1, 1000 do for _ = 1, 1000 do
local initial_index = math.random(1, 0xFFFFF) local initial_primary = math.random(1, 2 ^ 20 - 1)
local initial_version = math.random(1, 0xFFFFF) local initial_secondary = math.random(1, 2 ^ 20 - 1)
local packed_id = evo.pack(initial_index, initial_version) local packed_id = evo.pack(initial_primary, initial_secondary)
local unpacked_index, unpacked_version = evo.unpack(packed_id) local unpacked_primary, unpacked_secondary, unpacked_options = evo.unpack(packed_id)
assert(initial_index == unpacked_index) assert(initial_primary == unpacked_primary)
assert(initial_version == unpacked_version) assert(initial_secondary == unpacked_secondary)
assert(0 == unpacked_options)
end
for _ = 1, 1000 do
local initial_primary = math.random(1, 2 ^ 31 - 1)
local initial_secondary = math.random(1, 2 ^ 31 - 1)
local packed_id = evo.pack(initial_primary, initial_secondary)
local unpacked_primary, unpacked_secondary, unpacked_options = evo.unpack(packed_id)
assert(initial_primary % 2 ^ 20 == unpacked_primary)
assert(initial_secondary % 2 ^ 20 == unpacked_secondary)
assert(0 == unpacked_options)
end end

View File

@@ -108,6 +108,24 @@ end
--- ---
--- ---
evo.destroy(__table_unpack(all_entity_list)) if math.random(1, 2) == 1 then
evo.destroy(__table_unpack(all_fragment_list))
evo.collect_garbage() evo.collect_garbage()
end
if math.random(1, 2) == 1 then
evo.destroy(__table_unpack(all_entity_list))
if math.random(1, 2) == 1 then
evo.collect_garbage()
end
evo.destroy(__table_unpack(all_fragment_list))
else
evo.destroy(__table_unpack(all_fragment_list))
if math.random(1, 2) == 1 then
evo.collect_garbage()
end
evo.destroy(__table_unpack(all_entity_list))
end
if math.random(1, 2) == 1 then
evo.collect_garbage()
end

View File

@@ -63,5 +63,12 @@ end
--- ---
--- ---
evo.destroy(__table_unpack(all_entity_list)) if math.random(1, 2) == 1 then
evo.collect_garbage() evo.collect_garbage()
end
evo.destroy(__table_unpack(all_entity_list))
if math.random(1, 2) == 1 then
evo.collect_garbage()
end

View File

@@ -0,0 +1,43 @@
local evo = require 'evolved'
do
local id = evo.id()
local index, version, options = evo.unpack(id)
assert(evo.name(id) == string.format('$%d#%d:%d:%d', id, index, version, options))
evo.set(id, evo.NAME, 'hello')
assert(evo.name(id) == 'hello')
evo.set(id, evo.NAME, 'world')
assert(evo.name(id) == 'world')
evo.destroy(id)
assert(evo.name(id) == string.format('$%d#%d:%d:%d', id, index, version, options))
end
do
local id1, id2, id3, id4, id5 = evo.id(5)
evo.set(id1, evo.NAME, 'id1')
evo.set(id2, evo.NAME, 'id2')
evo.set(id3, evo.NAME, 'id3')
evo.set(id4, evo.NAME, 'id4')
evo.set(id5, evo.NAME, 'id5')
do
local id1_n, id3_n, id5_n = evo.name(id1, id3, id5)
assert(id1_n == 'id1')
assert(id3_n == 'id3')
assert(id5_n == 'id5')
end
do
local id1_n, id2_n, id3_n, id4_n, id5_n = evo.name(id1, id2, id3, id4, id5)
assert(id1_n == 'id1')
assert(id2_n == 'id2')
assert(id3_n == 'id3')
assert(id4_n == 'id4')
assert(id5_n == 'id5')
end
end

View File

@@ -6325,3 +6325,36 @@ do
assert(not chunk and not entity_list and not entity_count) assert(not chunk and not entity_list and not entity_count)
end end
end end
do
local function v2(x, y) return { x = x or 0, y = y or 0 } end
local function v2_clone(v) return { x = v.x, y = v.y } end
local v2_default = v2(1, 2)
do
local f = evo.builder():default(v2_default):spawn()
local b = evo.builder()
b:set(f)
evo.remove(f, evo.DEFAULT)
local e = b:spawn()
assert(evo.has(e, f) and evo.get(e, f).x == 1 and evo.get(e, f).y == 2)
assert(evo.get(e, f) == v2_default)
end
do
local f = evo.builder():default(v2_default):duplicate(v2_clone):spawn()
local b = evo.builder()
b:set(f)
evo.remove(f, evo.DEFAULT, evo.DUPLICATE)
local e = b:spawn()
assert(evo.has(e, f) and evo.get(e, f).x == 1 and evo.get(e, f).y == 2)
assert(evo.get(e, f) ~= v2_default)
end
end

File diff suppressed because it is too large Load Diff