Skip to content

Commit

Permalink
feat: Component remove event
Browse files Browse the repository at this point in the history
fix: Error when instantiating prefab that gets modified as it's being created
test: Add test for a component set listener running while an entity instantiates
test: Add tests for component removal event
  • Loading branch information
0ffz committed Mar 23, 2024
1 parent 03008f2 commit 4763b58
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ sealed class AddedComponent

sealed class SetComponent

sealed class RemovedComponent

sealed class UpdatedComponent

sealed class ExtendedEntity
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ value class Entity(val id: EntityId) {
remove(componentId(kClass))

/** Removes a component with id [component] from this entity. */
fun remove(component: ComponentId): Boolean =
write.removeComponentFor(this, component)
fun remove(component: ComponentId, noEvent: Boolean = false): Boolean =
write.removeComponentFor(this, component, noEvent)

/**
* Removes a list of [components] from this entity.
Expand Down Expand Up @@ -301,8 +301,8 @@ value class Entity(val id: EntityId) {
return removeRelation<K>(component<T>())
}

inline fun <reified K : Any> removeRelation(target: Entity): Boolean {
return geary.write.removeComponentFor(this, Relation.of<K>(target).id)
inline fun <reified K : Any> removeRelation(target: Entity, noEvent: Boolean = false): Boolean {
return geary.write.removeComponentFor(this, Relation.of<K>(target).id, noEvent)
}

// Events
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mineinabyss.geary.datatypes.family

import com.mineinabyss.geary.components.events.AddedComponent
import com.mineinabyss.geary.components.events.RemovedComponent
import com.mineinabyss.geary.components.events.SetComponent
import com.mineinabyss.geary.components.events.UpdatedComponent
import com.mineinabyss.geary.datatypes.*
Expand Down Expand Up @@ -125,6 +126,10 @@ sealed class MutableFamily : Family {
onAdd.hasRelation<SetComponent?>(id)
}

fun onRemove(id: ComponentId) {
onAdd.hasRelation<RemovedComponent?>(id)
}

fun onFirstSet(id: ComponentId) {
onAdd.hasRelation<SetComponent?>(id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Components {
val addedComponent = componentId<AddedComponent>()
val setComponent = componentId<SetComponent>()
val updatedComponent = componentId<UpdatedComponent>()
val removedComponent = componentId<RemovedComponent>()
val extendedEntity = componentId<ExtendedEntity>()
val entityRemoved = componentId<EntityRemoved>()
val childOf = componentId<ChildOf>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface EntityMutateOperations {
fun extendFor(entity: Entity, base: Entity)

/** Removes a [componentId] from an [entity] and clears any data previously associated with it. */
fun removeComponentFor(entity: Entity, componentId: ComponentId): Boolean
fun removeComponentFor(entity: Entity, componentId: ComponentId, noEvent: Boolean): Boolean

/** Removes all components from an entity. */
fun clearEntity(entity: Entity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,8 @@ class Archetype internal constructor(
val newRow = moveTo.moveOnlyAdding(this, row, entityId)
removeEntity(row)

if (callEvent) moveTo.callComponentModifyEvent(geary.components.addedComponent, newRow, componentId)

onUpdated(moveTo, newRow)
if (callEvent) moveTo.callComponentModifyEvent(geary.components.addedComponent, newRow, componentId, onUpdated)
else onUpdated(moveTo, newRow)
}

internal fun setComponent(
Expand Down Expand Up @@ -283,55 +282,73 @@ class Archetype internal constructor(
removeEntity(row)

// Component add listeners must query the target, this is an optimization
if (callEvent) moveTo.callComponentModifyEvent(geary.components.setComponent, newRow, componentId)
return onUpdated(moveTo, newRow)
if (callEvent) moveTo.callComponentModifyEvent(geary.components.setComponent, newRow, componentId, onUpdated)
else onUpdated(moveTo, newRow)
}

private inline fun callComponentModifyEvent(
eventType: ComponentId,
row: Int,
componentId: ComponentId,
onComplete: (Archetype, row: Int) -> Unit = { _, _ -> }
) {
val entity = getEntity(row)
callComponentModifyEvent(eventType, row, componentId)
// Don't have any way to know final archetype and row without re-reading
records.runOn(entity, onComplete)
}

fun callComponentModifyEvent(eventType: ComponentId, row: Int, componentId: ComponentId) {
private fun callComponentModifyEvent(
eventType: ComponentId,
row: Int,
componentId: ComponentId,
) {
// Archetype for the set event, if this doesn't have an event listener we skip
val eventArc = archetypeProvider.getArchetype(
GearyEntityType(ulongArrayOf(Relation.of(eventType, componentId).id))
)
if (eventArc.eventListeners.isNotEmpty()) {
val entity = getEntity(row)
temporaryEntity { event ->
event.add(geary.components.keepArchetype, noEvent = true)
event.addRelation(eventType, componentId, noEvent = true)
records.runOn(event) { eventArc, eventRow ->
eventRunner.callEvent(
getEntity(row),
eventArc.getEntity(eventRow),
null
)
}
eventRunner.callEvent(entity, event, source = null)
}
}
}

@Suppress("NAME_SHADOWING") // Want to make sure original arch/row is not accidentally accessed
fun instantiateTo(
baseRow: Int,
instanceArch: Archetype,
instanceRow: Int,
callEvent: Boolean = true,
) {
val baseEntity = this.getEntity(baseRow)
var currArch = instanceArch
var currRow = instanceRow
currArch.addComponent(currRow, Relation.of<InstanceOf?>(baseEntity).id, true) { arch, row ->
currArch = arch; currRow = row
var instanceArch = instanceArch
var instanceRow = instanceRow
instanceArch.addComponent(instanceRow, Relation.of<InstanceOf?>(baseEntity).id, true) { arch, row ->
instanceArch = arch; instanceRow = row
}

val noInheritComponents = EntityType(getRelationsByKind(componentId<NoInherit>()).map { it.target })
type.filter { !it.holdsData() && it !in noInheritComponents }.forEach {
currArch.addComponent(currRow, it, true) { arch, row -> currArch = arch; currRow = row }
instanceArch.addComponent(instanceRow, it, true) { arch, row -> instanceArch = arch; instanceRow = row }
}
dataHoldingType.forEach {
if (it.withoutRole(HOLDS_DATA) in noInheritComponents) return@forEach
currArch.setComponent(currRow, it, get(baseRow, it)!!, true) { arch, row -> currArch = arch; currRow = row }
instanceArch.setComponent(instanceRow, it, get(baseRow, it)!!, true) { arch, row ->
instanceArch = arch; instanceRow = row
}
}
baseEntity.children.fastForEach {
it.addParent(instanceArch.getEntity(instanceRow))
}
if (callEvent) currArch.callComponentModifyEvent(geary.components.extendedEntity, currRow, baseEntity.id)
if (callEvent) instanceArch.callComponentModifyEvent(
geary.components.extendedEntity,
instanceRow,
baseEntity.id
)
}

/**
Expand All @@ -341,12 +358,24 @@ class Archetype internal constructor(
*/
internal fun removeComponent(
row: Int,
component: ComponentId
component: ComponentId,
callEvent: Boolean,
): Boolean {
val entityId = ids[row]

if (component !in type) return false

if (callEvent) {
// Call event first, then ensure component is removed
callComponentModifyEvent(geary.components.removedComponent, row, component) { finalArch, finalRow ->
if (component !in finalArch.type) return true // Case where listeners manually removed component
val moveTo = finalArch - component
moveTo.moveWithoutComponent(finalArch, finalRow, component, entityId)
finalArch.removeEntity(row)
}
return true
}

val moveTo = this - component

moveTo.moveWithoutComponent(this, row, component, entityId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package com.mineinabyss.geary.engine.archetypes.operations

import com.mineinabyss.geary.datatypes.*
import com.mineinabyss.geary.datatypes.maps.ArrayTypeMap
import com.mineinabyss.geary.datatypes.maps.TypeMap
import com.mineinabyss.geary.engine.EntityMutateOperations
import com.mineinabyss.geary.engine.archetypes.ArchetypeProvider
import com.mineinabyss.geary.helpers.toGeary
import com.mineinabyss.geary.modules.archetypes

class ArchetypeMutateOperations : EntityMutateOperations {
Expand Down Expand Up @@ -44,12 +42,12 @@ class ArchetypeMutateOperations : EntityMutateOperations {
}
}

override fun removeComponentFor(entity: Entity, componentId: ComponentId): Boolean {
override fun removeComponentFor(entity: Entity, componentId: ComponentId, noEvent: Boolean): Boolean {
val a = records.runOn(entity) { archetype, row ->
archetype.removeComponent(row, componentId.withRole(HOLDS_DATA))
archetype.removeComponent(row, componentId.withRole(HOLDS_DATA), !noEvent)
}
val b = records.runOn(entity) { archetype, row ->
archetype.removeComponent(row, componentId.withoutRole(HOLDS_DATA))
archetype.removeComponent(row, componentId.withoutRole(HOLDS_DATA), !noEvent)
}
return a || b // return whether anything was changed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ abstract class ListenerQuery : Query() {
}
protected fun EventQueriedEntity.anySet(vararg props: KProperty<*>) = anySet(*getAccessorsFor(*props))

protected fun EventQueriedEntity.anyRemoved(vararg props: KProperty<*>) = anyRemoved(*getAccessorsFor(*props))

protected fun EventQueriedEntity.anyAdded(vararg props: KProperty<*>) = anyAdded(*getAccessorsFor(*props))

protected fun EventQueriedEntity.anyFirstSet(vararg props: KProperty<*>) = anyFirstSet(*getAccessorsFor(*props))
Expand All @@ -56,6 +58,17 @@ abstract class ListenerQuery : Query() {
forEachAccessorComponent(props.toSet()) { onSet(it) }
}


/**
* Listens to removal of any component read by any of [accessors].
* Entity will still have the component on it when the event is fire,
* the component will *always* be removed after all relevant listeners are fired.
*/
protected fun EventQueriedEntity.anyRemoved(vararg accessors: Accessor) {
forEachAccessorComponent(accessors.toSet()) { onRemove(it) }
}


/** Fires when an entity has a component of type [T] added, updates are not considered since no data changes. */
protected fun EventQueriedEntity.anyAdded(vararg props: Accessor) {
forEachAccessorComponent(props.toSet()) { onAdd(it) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.mineinabyss.geary.events

import com.mineinabyss.geary.datatypes.Entity
import com.mineinabyss.geary.helpers.entity
import com.mineinabyss.geary.helpers.tests.GearyTest
import com.mineinabyss.geary.modules.geary
import com.mineinabyss.geary.systems.builders.listener
import com.mineinabyss.geary.systems.query.ListenerQuery
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

internal class ComponentRemoveEventTest : GearyTest() {
@BeforeEach
fun reset() {
resetEngine()
}

@Test
fun `should fire remove listener and have access to removed component data when component removed`() {
// arrange
var called = 0
geary.listener(object : ListenerQuery() {
val string by get<String>()
override fun ensure() = event.anyRemoved(::string)
}).exec {
string shouldBe "data"
called++
}
val entity = entity {
set("data")
}

// act
entity.remove<String>()

// assert
called shouldBe 1
}

@Test
fun `should not fire remove listener when other component removed`() {
// arrange
var called = 0
geary.listener(object : ListenerQuery() {
val string by get<String>()
override fun ensure() = event.anyRemoved(::string)
}).exec {
called++
}
val entity = entity {
set("data")
set(1)
}

// act
entity.remove<Int>()

// assert
called shouldBe 0
}

@Test
fun `should not call remove listener when added but not set component removed`() {
// arrange
var called = 0
geary.listener(object : ListenerQuery() {
val string by get<String>()
override fun ensure() = event.anyRemoved(::string)
}).exec {
called++
}
val entity = entity {
add<String>()
}

// act
entity.remove<String>()

// assert
called shouldBe 0
}

@Test
fun `should still remove component after remove listener modifies it`() {
// arrange
var called = 0
geary.listener(object : ListenerQuery() {
val string by get<String>()
override fun ensure() = event.anyRemoved(::string)
}).exec {
called++
entity.set("new data")
}
val entity = entity {
add<String>()
}

// act
entity.remove<String>()

// assert
called shouldBe 0
entity.get<String>() shouldBe null
}

@Test
fun `should correctly fire listener that listens to several removed components`() {
// arrange
var called = mutableListOf<Entity>()
geary.listener(object : ListenerQuery() {
val string by get<String>()
val int by get<Int>()
override fun ensure() = event.anyRemoved(::string, ::int)
}).exec { called.add(entity) }
val entity1 = entity {
set("data")
set(1)
}
val entity2 = entity {
set("data")
set(1)
}
val entity3 = entity {
set("data")
}

// act
entity1.remove<Int>() // Fires
entity2.remove<String>() // Fires
entity3.remove<String>() // Doesn't fire

// assert
called.filter { it == entity1 }.size shouldBe 1
called.filter { it == entity2 }.size shouldBe 1
called.filter { it == entity3 }.size shouldBe 0
}
}
Loading

0 comments on commit 4763b58

Please sign in to comment.