Add replace method to query (#46)

* Add replace function

* Add next method

* Remove tostring

* Fix EmptyQuery

* add replace method

* merge conflicts

* return self in without

* Make aliases relative

* Add test

* add to changelog
This commit is contained in:
Marcus 2024-07-07 04:53:17 +02:00 committed by GitHub
parent b73d7e12b7
commit 0fe23e151c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 120 additions and 53 deletions

View file

@ -1,6 +1,6 @@
{ {
"aliases": { "aliases": {
"jecs": "C:/Users/Marcus/Documents/packages/jecs/src", "jecs": "src",
"testkit": "C:/Users/Marcus/Documents/packages/jecs/testkit" "testkit": "testkit"
} }
} }

View file

@ -10,6 +10,11 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
## [Unreleased] ## [Unreleased]
### Added
- Added `query:replace(function(...T) return ...U end)` for replacing components in place
- Method is fast pathed to replacing the data to the components for each corresponding entity
### Changed ### Changed
- Iterator now goes backwards instead to prevent common cases of iterator invalidation - Iterator now goes backwards instead to prevent common cases of iterator invalidation
@ -25,13 +30,13 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
### Added ### Added
- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships. - Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships.
- Give a parent to an entity with `world:add($source, pair(ChildOf, $target))` - Give a parent to an entity with `world:add($source, pair(ChildOf, $target))`
- Use `world:parent(entity)` to find the target of the relationship - Use `world:parent(entity)` to find the target of the relationship
- Added user-facing Luau types - Added user-facing Luau types
### Changed ### Changed
- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream: - Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream:
## [0.1.1] - 2024-05-19 ## [0.1.1] - 2024-05-19
@ -48,19 +53,19 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
## [0.1.0-rc.6] - 2024-05-13 ## [0.1.0-rc.6] - 2024-05-13
### Added ### Added
- Added a `jecs.Wildcard` term - Added a `jecs.Wildcard` term
- it lets you query any partially matched pairs - it lets you query any partially matched pairs
## [0.1.0-rc.5] - 2024-05-10 ## [0.1.0-rc.5] - 2024-05-10
### Added ### Added
- Added Entity relationships for creating logical connections between entities - Added Entity relationships for creating logical connections between entities
- Added `world:__iter method` which allows for iteration over the whole world to get every entity - Added `world:__iter method` which allows for iteration over the whole world to get every entity
- used for reconciling whole worlds such as via replication, saving/loading, etc - used for reconciling whole worlds such as via replication, saving/loading, etc
- Added `world:add(entity, component)` which adds a component to the entity - Added `world:add(entity, component)` which adds a component to the entity
- it is an idempotent function, so calling it twice and in any order should be fine - it is an idempotent function, so calling it twice and in any order should be fine
### Fixed ### Fixed
@ -89,7 +94,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
## [0.0.0-prototype.rc.2] - 2024-04-26 ## [0.0.0-prototype.rc.2] - 2024-04-26
### Changed ### Changed
- Optimized the creation of the query - Optimized the creation of the query
- It will now finds the smallest archetype map to iterate over - It will now finds the smallest archetype map to iterate over
- Optimized the query iterator - Optimized the query iterator
- It will now populates iterator with columns for faster indexing - It will now populates iterator with columns for faster indexing
@ -109,12 +114,3 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
[0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 [0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3
[0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2 [0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2
[0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1 [0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1

View file

@ -7,6 +7,7 @@ local function TITLE(title: string)
print() print()
print(testkit.color.white(title)) print(testkit.color.white(title))
end end
local jecs = require("@jecs") local jecs = require("@jecs")
local mirror = require("../mirror/init") local mirror = require("../mirror/init")

View file

@ -42,7 +42,7 @@ TODO:
index: number, index: number,
count: number, count: number,
column: number column: number
} }
]] ]]
@ -131,7 +131,7 @@ local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end end
local function STRIP_GENERATION(e: i53): i24 local function STRIP_GENERATION(e: i53): i24
return ECS_ENTITY_T_LO(e) return ECS_ENTITY_T_LO(e)
end end
@ -149,7 +149,7 @@ local function getAlive(index: EntityIndex, e: i24): i53
if id then if id then
local currentGeneration = ECS_GENERATION(id) local currentGeneration = ECS_GENERATION(id)
local gen = ECS_GENERATION(e) local gen = ECS_GENERATION(e)
if gen == currentGeneration then if gen == currentGeneration then
return id return id
end end
@ -159,7 +159,7 @@ local function getAlive(index: EntityIndex, e: i24): i53
error(ERROR_ENTITY_NOT_ALIVE) error(ERROR_ENTITY_NOT_ALIVE)
end end
local function sparseGet(entityIndex, id) local function sparseGet(entityIndex, id)
return entityIndex.sparse[getAlive(entityIndex, id)] return entityIndex.sparse[getAlive(entityIndex, id)]
end end
@ -353,7 +353,7 @@ end
-- TODO: -- TODO:
-- should have an additional `nth` parameter which selects the nth target -- should have an additional `nth` parameter which selects the nth target
-- this is important when an entity can have multiple relationships with the same target -- this is important when an entity can have multiple relationships with the same target
local function target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24? local function target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24?
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local record = entityIndex.sparse[entity] local record = entityIndex.sparse[entity]
local archetype = record.archetype local archetype = record.archetype
@ -374,7 +374,7 @@ local function target(world: World, entity: i53, relation: i24--[[, nth: number]
return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord])
end end
local function parent(world: World, entity: i53) local function parent(world: World, entity: i53)
return target(world, entity, EcsChildOf) return target(world, entity, EcsChildOf)
end end
@ -462,7 +462,7 @@ local function add(world: World, entityId: i53, componentId: i53)
end end
-- Symmetric like `World.add` but idempotent -- Symmetric like `World.add` but idempotent
local function set(world: World, entityId: i53, componentId: i53, data: unknown) local function set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = world.entityIndex.sparse[entityId] local record = world.entityIndex.sparse[entityId]
local from = record.archetype local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from) local to = archetypeTraverseAdd(world, componentId, from)
@ -600,17 +600,17 @@ local function delete(world: World, entityId: i53)
end end
local function clear(world: World, entityId: i53) local function clear(world: World, entityId: i53)
--TODO: use sparse_get (stashed) --TODO: use sparse_get (stashed)
local record = world.entityIndex.sparse[entityId] local record = world.entityIndex.sparse[entityId]
if not record then if not record then
return return
end end
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
local archetype = record.archetype local archetype = record.archetype
if archetype == nil or archetype == ROOT_ARCHETYPE then if archetype == nil or archetype == ROOT_ARCHETYPE then
return return
end end
@ -662,21 +662,29 @@ local function noop(_self: Query, ...): () -> ()
end end
local EmptyQuery = { local EmptyQuery = {
__iter = noop, __iter = iterNoop,
without = noop, next = noop,
replace = noop,
without = function(self)
return self
end
} }
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery) export type Query = typeof(EmptyQuery)
type CompatibleArchetype = { archetype: Archetype, indices: { number } } type CompatibleArchetype = { archetype: Archetype, indices: { number } }
local function preparedQuery(compatibleArchetypes: { Archetype }, local function replaceMult(row, columns, ...)
components: { i53? }, indices: { { number } }) for i, column in columns do
column[row] = select(i, ...)
end
end
local function preparedQuery(compatibleArchetypes: { Archetype },
components: { i53? }, indices: { { number } })
local queryLength = #components local queryLength = #components
local lastArchetype = 1 local lastArchetype = 1
local archetype: Archetype = compatibleArchetypes[lastArchetype] local archetype: Archetype = compatibleArchetypes[lastArchetype]
@ -686,16 +694,16 @@ local function preparedQuery(compatibleArchetypes: { Archetype },
local queryOutput = {} local queryOutput = {}
local entities = archetype.entities local entities = archetype.entities
local i = #entities local i = #entities
local function queryNext(): ...any local function queryNext(): ...any
local entityId = entities[i] local entityId = entities[i]
while entityId == nil do while entityId == nil do
lastArchetype += 1 lastArchetype += 1
archetype = compatibleArchetypes[lastArchetype] archetype = compatibleArchetypes[lastArchetype]
if not archetype then if not archetype then
return return
end end
@ -782,34 +790,77 @@ local function preparedQuery(compatibleArchetypes: { Archetype },
return self return self
end end
local it = {
__iter = function()
lastArchetype = 1
archetype = compatibleArchetypes[1]
entities = archetype.entities
i = #entities
return queryNext local function iter()
end, lastArchetype = 1
archetype = compatibleArchetypes[1]
entities = archetype.entities
i = #entities
return queryNext
end
local function replace(_, fn)
for i, archetype in compatibleArchetypes do
local tr = indices[i]
local columns = archetype.columns
for row in archetype.entities do
if queryLength == 1 then
local a = columns[tr[1]]
local pa = fn(a[row])
a[row] = pa
elseif queryLength == 2 then
local a = columns[tr[1]]
local b = columns[tr[2]]
a[row], b[row] = fn(a[row], b[row])
elseif queryLength == 3 then
local a = columns[tr[1]]
local b = columns[tr[2]]
local c = columns[tr[3]]
a[row], b[row], c[row] = fn(a[row], b[row], c[row])
elseif queryLength == 4 then
local a = columns[tr[1]]
local b = columns[tr[2]]
local c = columns[tr[3]]
local d = columns[tr[4]]
a[row], b[row], c[row], d[row] = fn(
a[row], b[row], c[row], d[row])
else
for i = 1, queryLength do
queryOutput[i] = columns[tr[i]][row]
end
replaceMult(row, columns, fn(unpack(queryOutput)))
end
end
end
end
local it = {
__iter = iter,
next = queryNext, next = queryNext,
without = without without = without,
replace = replace
} }
return setmetatable(it, it) :: any return setmetatable(it, it) :: any
end end
local function query(world: World, ...: number): Query local function query(world: World, ...: number): Query
-- breaking? -- breaking?
if (...) == nil then if (...) == nil then
error("Missing components") error("Missing components")
end end
local indices: { { number } } = {} local indices: { { number } } = {}
local compatibleArchetypes: { Archetype } = {} local compatibleArchetypes: { Archetype } = {}
local length = 0 local length = 0
local components: { number } = { ... } local components: { number } = { ... }
local archetypes: { Archetype } = world.archetypes :: any local archetypes: { Archetype } = world.archetypes :: any
local firstArchetypeMap: ArchetypeMap local firstArchetypeMap: ArchetypeMap
@ -989,7 +1040,7 @@ export type WorldShim = typeof(setmetatable(
local World = {} local World = {}
World.__index = World World.__index = World
function World.new() function World.new()
local self = setmetatable({ local self = setmetatable({
archetypeIndex = {} :: { [string]: Archetype }, archetypeIndex = {} :: { [string]: Archetype },
archetypes = {} :: Archetypes, archetypes = {} :: Archetypes,

View file

@ -449,9 +449,28 @@ TEST("world", function()
count += 1 count += 1
end end
print(count)
CHECK(count == 2) CHECK(count == 2)
end end
do CASE "should replace component data"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local e = world:entity()
world:set(e, A, 1)
world:set(e, B, true)
world:set(e, C, "hello ")
world:query(A, B, C):replace(function(a, b, c)
return a * 2, not b, c.."world"
end)
CHECK(world:get(e, A) == 2)
CHECK(world:get(e, B) == false)
CHECK(world:get(e, C) == "hello world")
end
end) end)
FINISH() FINISH()