From 031010ba10b285b8466a2d6bf42547db62af6a89 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Tue, 28 Nov 2023 22:30:25 +0100 Subject: [PATCH] Validate non-iterating systems for family hook illegal interfaces. Merge family hook system tests to the main system test file. --- .../com/github/quillraven/fleks/exception.kt | 3 + .../com/github/quillraven/fleks/world.kt | 13 +++ .../quillraven/fleks/SystemFamilyHooksTest.kt | 74 ------------- .../com/github/quillraven/fleks/SystemTest.kt | 100 ++++++++++++++++++ 4 files changed, 116 insertions(+), 74 deletions(-) delete mode 100644 src/commonTest/kotlin/com/github/quillraven/fleks/SystemFamilyHooksTest.kt diff --git a/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index feb7de68..bda8c908 100644 --- a/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -35,3 +35,6 @@ class FleksWrongConfigurationUsageException : "The global functions 'inject' and 'family' must be used inside a WorldConfiguration scope." + "The same applies for 'compareEntityBy' and 'compareEntity' unless you specify the world parameter explicitly." ) + +class FleksWrongSystemInterfaceException(system: KClass<*>, `interface`: KClass<*>) : + FleksException("System $system cannot have interface $`interface`") diff --git a/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 856a9de0..68c6f22e 100644 --- a/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -187,6 +187,19 @@ class WorldConfiguration(@PublishedApi internal val world: World) { */ private fun setUpAggregatedFamilyHooks() { + // Validate systems against illegal interfaces. + world.systems.forEach { system -> + // FamilyOnAdd and FamilyOnRemove interfaces are only meant to be used by IteratingSystem. + if (system !is IteratingSystem) { + + if (system is IteratingSystem.FamilyOnAdd) + throw FleksWrongSystemInterfaceException(system::class, IteratingSystem.FamilyOnAdd::class) + + if (system is IteratingSystem.FamilyOnRemove) + throw FleksWrongSystemInterfaceException(system::class, IteratingSystem.FamilyOnRemove::class) + } + } + // Register family hooks for IteratingSystem.FamilyOnAdd containing systems. world.systems .mapNotNull { system -> if (system is IteratingSystem && system is IteratingSystem.FamilyOnAdd) system else null } diff --git a/src/commonTest/kotlin/com/github/quillraven/fleks/SystemFamilyHooksTest.kt b/src/commonTest/kotlin/com/github/quillraven/fleks/SystemFamilyHooksTest.kt deleted file mode 100644 index 13b63f17..00000000 --- a/src/commonTest/kotlin/com/github/quillraven/fleks/SystemFamilyHooksTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.quillraven.fleks - -import com.github.quillraven.fleks.World.Companion.family -import kotlin.test.Test -import kotlin.test.assertEquals - -private class NameComponent(val name: String) : Component { - companion object : ComponentType() - override fun type() = NameComponent -} - -private class SystemFamilyHooksTest { - @Test - fun iterationSystemFamilyHookFlow() { - - var check: Int = 0 - - val world = configureWorld { - families { - val family = family { all(NameComponent) } - onAdd(family) { entity -> - assertEquals(0, check++) // First, we expect the global add hook to be called. - println("$check. Global family onAdd() called with: entity = ${entity[NameComponent].name}") - } - onRemove(family) { entity -> - assertEquals(5, check++) // The global hook gets called before the systems. - println("$check. Global family onRemove() called with: entity = ${entity[NameComponent].name}") - } - } - - systems { - add(object : IteratingSystem(family { all(NameComponent) }), IteratingSystem.FamilyOnAdd, IteratingSystem.FamilyOnRemove { - val sysName = "System0" - - override fun onAddEntity(entity: Entity) { - assertEquals(1, check++) // onAdd hooks called in forward system order. Thus, "System0" gets called first. - println("$check. $sysName onAddEntity() called with: entity = ${entity[NameComponent].name}") - } - - override fun onTickEntity(entity: Entity) { - assertEquals(3, check++) - println("$check. $sysName onTickEntity() called with: entity = ${entity[NameComponent].name}") - } - - override fun onRemoveEntity(entity: Entity) { - assertEquals(6, check++) - println("$check. $sysName onRemoveEntity() called with: entity = ${entity[NameComponent].name}") - } - }) - add(object : IteratingSystem(family { all(NameComponent) }), IteratingSystem.FamilyOnAdd, IteratingSystem.FamilyOnRemove { - val sysName = "System1" - - override fun onAddEntity(entity: Entity) { - assertEquals(2, check++) - println("$check. $sysName onAddEntity() called with: entity = ${entity[NameComponent].name}") - } - - override fun onTickEntity(entity: Entity) { - assertEquals(4, check++) - println("$check. $sysName onTickEntity() called with: entity = ${entity[NameComponent].name}") - } - - override fun onRemoveEntity(entity: Entity) { - assertEquals(6, check++) - println("$check. $sysName onRemoveEntity() called with: entity = ${entity[NameComponent].name}") - } - }) - } - } - val entity = world.entity { it += NameComponent("Entity0") } - world.update(0f) - world -= entity - } -} diff --git a/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt b/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt index 009f17e5..05c34440 100644 --- a/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt +++ b/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt @@ -48,6 +48,13 @@ private data class SystemTestComponent( companion object : ComponentType() } +private data class NameComponent( + val name: String +) : Component { + companion object : ComponentType() + override fun type() = NameComponent +} + private class SystemTestIteratingSystem : IteratingSystem( family = family { all(SystemTestComponent) }, interval = Fixed(0.25f) @@ -431,4 +438,97 @@ internal class SystemTest { val system = world.system() assertEquals(1, system.numTicks) } + + + @Test + fun iterationSystemFamilyHookFlow() { + // this verifies that IterationSystems properly subscribe to their family's onAdd/onRemove hooks + // and that the order of hook calls is correct + + var check: Int = 0 + + val world = configureWorld { + families { + val family = family { all(NameComponent) } + onAdd(family) { entity -> + assertEquals(0, check++) // First, we expect the global add hook to be called. + println("$check. Global family onAdd() called with: entity = ${entity[NameComponent].name}") + } + onRemove(family) { entity -> + assertEquals(5, check++) // The global hook gets called before the systems. + println("$check. Global family onRemove() called with: entity = ${entity[NameComponent].name}") + } + } + + systems { + add(object : IteratingSystem(family { all(NameComponent) }), IteratingSystem.FamilyOnAdd, IteratingSystem.FamilyOnRemove { + val sysName = "System0" + + override fun onAddEntity(entity: Entity) { + assertEquals(1, check++) // onAdd hooks called in forward system order. Thus, "System0" gets called first. + println("$check. $sysName onAddEntity() called with: entity = ${entity[NameComponent].name}") + } + + override fun onTickEntity(entity: Entity) { + assertEquals(3, check++) + println("$check. $sysName onTickEntity() called with: entity = ${entity[NameComponent].name}") + } + + override fun onRemoveEntity(entity: Entity) { + assertEquals(6, check++) + println("$check. $sysName onRemoveEntity() called with: entity = ${entity[NameComponent].name}") + } + }) + add(object : IteratingSystem(family { all(NameComponent) }), IteratingSystem.FamilyOnAdd, IteratingSystem.FamilyOnRemove { + val sysName = "System1" + + override fun onAddEntity(entity: Entity) { + assertEquals(2, check++) + println("$check. $sysName onAddEntity() called with: entity = ${entity[NameComponent].name}") + } + + override fun onTickEntity(entity: Entity) { + assertEquals(4, check++) + println("$check. $sysName onTickEntity() called with: entity = ${entity[NameComponent].name}") + } + + override fun onRemoveEntity(entity: Entity) { + assertEquals(7, check++) // Lastly, the system with the highest order receives the hook call. + println("$check. $sysName onRemoveEntity() called with: entity = ${entity[NameComponent].name}") + } + }) + } + } + val entity = world.entity { it += NameComponent("Entity0") } + world.update(0f) + world -= entity + } + + @Test + fun illegalFamilyOnAddInterfaceOnIntervalSystem() { + assertFailsWith { + configureWorld { + systems { + add(object : IntervalSystem(), IteratingSystem.FamilyOnAdd { + override fun onTick() = Unit + override fun onAddEntity(entity: Entity) = Unit + }) + } + } + } + } + + @Test + fun illegalFamilyOnRemoveInterfaceOnIntervalSystem() { + assertFailsWith { + configureWorld { + systems { + add(object : IntervalSystem(), IteratingSystem.FamilyOnRemove { + override fun onTick() = Unit + override fun onRemoveEntity(entity: Entity) = Unit + }) + } + } + } + } }