Skip to content

Commit

Permalink
feat(queries): Allow iterating over entities gathered ahead of time, …
Browse files Browse the repository at this point in the history
…allows some degree of modifying archetypes while iterating a query
  • Loading branch information
0ffz committed Feb 3, 2025
1 parent 1279600 commit ea0b9a2
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class EntityArray(
for (i in ids.indices) action(GearyEntity(ids[i], world))
}

inline fun forEachId(action: (EntityId) -> Unit) {
for (i in ids.indices) action(ids[i])
}

inline fun flatMap(transform: (Entity) -> EntityArray): EntityArray {
return ids.flatMapTo(arrayListOf()) { transform(Entity(it, world)).ids }.toULongArray().toEntityArray(world)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ class CachedQuery<T : Query> internal constructor(val query: T) {
}
}

/**
* Runs this query on a list of entities gathered ahead of time.
*
* ### Unsafe constraints
* - Ensure all [entities] match this query.
* - Ensure [entities] belong to the same world as the query.
* - When iterating, ensure of all entities matching this query, only the current entity is modified.
*/
@UnsafeAccessors
inline fun forEachMutating(entities: EntityArray, run: (Entity, T) -> Unit) {
val accessors = cachingAccessors
val query = query
val world = query.world
require(entities.world == query.world) { "Entities must belong to the same world as the query" }
val records = world.records
entities.forEachId { id ->
records.runOn(id) { archetype, row ->
query.archetype = archetype
query.row = row
accessors.fastForEach { it.updateCache(archetype) }
run(Entity(id, world), query)
}
}
}

/**
* Matches entities ahead of time and iterates over the matched list, allowing for archetype modifications.
*
* ### Unsafe constraints
* - When iterating, ensure of all entities matching this query, only the current entity is modified.
*/
@UnsafeAccessors
inline fun forEachMutating(run: (Entity, T) -> Unit) {
forEachMutating(entities(), run)
}

fun Archetype.getData() = componentData

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mineinabyss.geary.queries

import com.mineinabyss.geary.annotations.optin.UnsafeAccessors
import com.mineinabyss.geary.helpers.entity
import com.mineinabyss.geary.systems.query.query
import com.mineinabyss.geary.test.GearyTest
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldHaveSize
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test

class QueryForEachMutatingTest : GearyTest() {
@BeforeEach
fun setup() {
resetEngine()
repeat(10) {
entity { set<Int>(it) }
}
}

@Test
fun `forEach should fail when modifying unsafeEntity's archetype`() {
val query = queryManager.trackQuery(query<Int>())
shouldThrowAny {
@OptIn(UnsafeAccessors::class)
query.forEach {
it.unsafeEntity.toGeary().remove<Int>()
}
}
}

@Test
fun `forEachEntity should allow modifying archetypes while iterating`() {
val query = queryManager.trackQuery(query<Int>())
val postQuery = queryManager.trackQuery(query<Long>())

@OptIn(UnsafeAccessors::class)
query.forEachMutating { entity, (integer) ->
entity.remove<Int>()
entity.set<Long>(integer.toLong())
}

query.entities().shouldBeEmpty()
postQuery.entities().shouldHaveSize(10)
}
}

0 comments on commit ea0b9a2

Please sign in to comment.