Skip to content

Commit d7f7333

Browse files
committed
feat: Entity getAsFlow (closes #108)
feat: Basic support for removing observers after adding
1 parent 66ee74f commit d7f7333

File tree

5 files changed

+133
-5
lines changed

5 files changed

+133
-5
lines changed

geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Entity.kt

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@ import com.mineinabyss.geary.annotations.optin.DangerousComponentOperation
44
import com.mineinabyss.geary.components.EntityName
55
import com.mineinabyss.geary.datatypes.family.family
66
import com.mineinabyss.geary.engine.Engine
7-
import com.mineinabyss.geary.helpers.*
7+
import com.mineinabyss.geary.helpers.NO_COMPONENT
8+
import com.mineinabyss.geary.helpers.component
9+
import com.mineinabyss.geary.helpers.componentId
10+
import com.mineinabyss.geary.helpers.componentIdWithNullable
811
import com.mineinabyss.geary.modules.Geary
912
import com.mineinabyss.geary.modules.relationOf
13+
import com.mineinabyss.geary.observers.entity.observe
14+
import com.mineinabyss.geary.observers.entity.removeObserver
1015
import com.mineinabyss.geary.observers.events.OnAdd
16+
import com.mineinabyss.geary.observers.events.OnEntityRemoved
17+
import com.mineinabyss.geary.observers.events.OnRemove
18+
import com.mineinabyss.geary.observers.events.OnSet
1119
import com.mineinabyss.geary.systems.accessors.AccessorOperations
1220
import com.mineinabyss.geary.systems.accessors.RelationWithData
13-
import org.koin.core.Koin
14-
import org.koin.core.KoinApplication
21+
import com.mineinabyss.geary.systems.query.query
22+
import kotlinx.coroutines.channels.Channel
23+
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
24+
import kotlinx.coroutines.flow.Flow
25+
import kotlinx.coroutines.flow.flow
1526
import kotlin.reflect.KClass
1627

1728
typealias GearyEntity = Entity
@@ -181,13 +192,44 @@ class Entity(val id: EntityId, val world: Geary) {
181192
requireSameWorldAs(base)
182193
world.write.extendFor(id, base.id)
183194
}
184-
195+
185196
/** Removes a [prefab] from this entity. */
186197
fun removePrefab(prefab: Entity) {
187198
requireSameWorldAs(prefab)
188199
remove(Relation.of(comp.instanceOf, prefab.id).id)
189200
}
190201

202+
/**
203+
* Get a component as a [Flow], updates to the component will be emitted, including `null` when the component is removed.
204+
*
205+
* The flow stops when the entity is removed.
206+
*/
207+
inline fun <reified T : Any> getAsFlow(): Flow<T?> = with(world) {
208+
flow {
209+
val updates = Channel<T?>(CONFLATED)
210+
updates.trySend(get<T>())
211+
val onSetObserver = observe<OnSet>().involving<T>().exec(query<T>()) { (comp) ->
212+
updates.trySend(comp)
213+
}
214+
val onRemoveObserver = observe<OnRemove>().involving<T>().exec(query<T>()) { (comp) ->
215+
updates.trySend(null)
216+
}
217+
val onEntityRemoved = observe<OnEntityRemoved>().exec {
218+
updates.close()
219+
}
220+
221+
try {
222+
for (update in updates) {
223+
emit(update)
224+
}
225+
} finally {
226+
removeObserver(onSetObserver)
227+
removeObserver(onRemoveObserver)
228+
removeObserver(onEntityRemoved)
229+
}
230+
}
231+
}
232+
191233
// Relations
192234

193235
/** Gets the data stored under the relation of kind [K] and target [T]. */

geary-core/src/commonMain/kotlin/com/mineinabyss/geary/observers/EventToObserversMap.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ class EventToObserversMap(
1010
) {
1111
private val eventToObserverMap = LongSparseArray<ObserverList>()
1212

13+
val isEmpty get() = eventToObserverMap.isEmpty()
14+
1315
fun addObserver(observer: Observer) {
1416
observer.listenToEvents.forEach { event ->
1517
eventToObserverMap.getOrPut(event.toLong()) { ObserverList(records) }.add(observer)
1618
}
1719
}
1820

21+
fun removeObserver(observer: Observer) {
22+
observer.listenToEvents.forEach { event ->
23+
eventToObserverMap[event.toLong()]?.remove(observer)
24+
}
25+
}
26+
1927
operator fun get(event: ComponentId): ObserverList? = eventToObserverMap[event.toLong()]
2028
}

geary-core/src/commonMain/kotlin/com/mineinabyss/geary/observers/ObserverList.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ class ObserverList(
2323
}
2424
}
2525

26+
fun remove(observer: Observer) {
27+
if (observer.involvedComponents.size == 0) {
28+
involved2Observer[0L]?.remove(observer)
29+
} else observer.involvedComponents.forEach { componentId ->
30+
involved2Observer[componentId.toLong()]?.remove(observer)
31+
}
32+
}
33+
2634
inline fun forEach(involvedComp: ComponentId, entity: EntityId, exec: (Observer, Archetype, row: Int) -> Unit) {
2735
involved2Observer[0L]?.forEach {
2836
records.runOn(entity) { archetype, row -> exec(it, archetype, row) }

geary-core/src/commonMain/kotlin/com/mineinabyss/geary/observers/entity/EntityObserver.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import com.mineinabyss.geary.datatypes.ComponentId
55
import com.mineinabyss.geary.datatypes.GearyEntity
66
import com.mineinabyss.geary.helpers.componentId
77
import com.mineinabyss.geary.helpers.entity
8-
import com.mineinabyss.geary.modules.ArchetypeEngineModule
98
import com.mineinabyss.geary.observers.EventToObserversMap
109
import com.mineinabyss.geary.observers.Observer
1110
import com.mineinabyss.geary.observers.builders.*
@@ -18,6 +17,9 @@ inline fun <reified T : Any> GearyEntity.observeWithData(): ObserverEventsBuilde
1817
return observeWithData(world.componentId<T>())
1918
}
2019

20+
/**
21+
* Attaches an observer to fire on events emitted on this entity and its instances.
22+
*/
2123
fun GearyEntity.attachObserver(observer: Observer) {
2224
val observerEntity = world.entity {
2325
// TODO avoid cast
@@ -29,6 +31,20 @@ fun GearyEntity.attachObserver(observer: Observer) {
2931
addRelation<Observer>(observerEntity)
3032
}
3133

34+
/**
35+
* Removes an entity observer that was previously attached via [attachObserver]
36+
*/
37+
fun GearyEntity.removeObserver(observer: Observer) = with(world) {
38+
getRelations<Observer, Any?>().forEach {
39+
val observerEntity = it.target.toGeary()
40+
val map = observerEntity.get<EventToObserversMap>()
41+
if (map != null) {
42+
map.removeObserver(observer)
43+
if (map.isEmpty) removeRelation<Observer>(observerEntity)
44+
}
45+
}
46+
}
47+
3248
fun GearyEntity.observe(vararg events: ComponentId): ObserverEventsBuilder<ObserverContext> {
3349
return ObserverWithoutData(events.toList(), world, ::attachObserver)
3450
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.mineinabyss.geary.observers
2+
3+
import com.mineinabyss.geary.helpers.entity
4+
import com.mineinabyss.geary.test.GearyTest
5+
import io.kotest.matchers.collections.shouldContainExactly
6+
import kotlinx.coroutines.launch
7+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
8+
import kotlinx.coroutines.test.runTest
9+
import kotlin.test.Test
10+
11+
class EntityGetAsFlowTest : GearyTest() {
12+
@Test
13+
fun `getAsFlow should correctly listen to entity updates`() = runTest {
14+
val entity = entity()
15+
val collected = mutableListOf<Int>()
16+
17+
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
18+
entity.getAsFlow<Int>().collect {
19+
if(it != null) collected.add(it)
20+
else collected.add(0)
21+
}
22+
}
23+
entity.set(1)
24+
entity.set("other component")
25+
entity.set(2)
26+
entity.remove<Int>()
27+
entity.set(3)
28+
29+
collected shouldContainExactly listOf(0, 1, 2, 0, 3)
30+
}
31+
32+
@Test
33+
fun `getAsFlow should unregister itself when cancelled`() = runTest {
34+
val entity = entity()
35+
val collected = mutableListOf<Int>()
36+
37+
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
38+
entity.getAsFlow<Int>().collect {
39+
if(it != null) collected.add(it)
40+
else collected.add(0)
41+
}
42+
val collecting = launch(UnconfinedTestDispatcher(testScheduler)) {
43+
entity.getAsFlow<Int>().collect {
44+
if(it != null) collected.add(it)
45+
else collected.add(0)
46+
}
47+
}
48+
entity.set(1)
49+
collecting.cancel()
50+
entity.set(2)
51+
collected shouldContainExactly listOf(0, 1)
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)