diff --git a/LICENSE b/LICENSE index 0790b9c08..a0e2033f9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Mine In Abyss +Copyright (c) 2020 Danielle Voznyy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b87f285f0..ae1ad9482 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,7 @@ ## Overview -
- :warning: Notice: Looking for a new maintainer - -> This project was built from the ground up by myself, but its scope has finally caught up and I wish to move on and leave it in the hands of a bigger team that can get Geary to a state where others outside of Mine in Abyss can use it.\ -> \ -> If you're interested in building an ECS and find Geary's syntax interesting, I'm currently working on some major cleanup, and documenting usage + backend decisions that will hopefully make it easier to take over or build your own. If you just want a working engine (not using Kotlin), I recommend looking at [flecs](https://github.com/SanderMertens/flecs), otherwise browse through some established Java engines! \ -> \ -> \- Offz - -
- -Geary is an Entity Component System (ECS) written in Kotlin. The engine design is inspired by [flecs](https://github.com/SanderMertens/flecs). It is currently NOT optimized for performance. We use Geary internally for our Minecraft plugins, see [geary-papermc](https://github.com/MineInAbyss/geary-papermc) for more info. +Geary is an Entity Component System (ECS) written in Kotlin. The engine design is inspired by [flecs](https://github.com/SanderMertens/flecs). Core parts of the engine (ex. system iteration, entity creation) are quite optimized, with the main exception being our event system. We use Geary internally for our Minecraft plugins, see [geary-papermc](https://github.com/MineInAbyss/geary-papermc) for more info. ## Features - Null safe component access @@ -32,16 +21,17 @@ Geary is an Entity Component System (ECS) written in Kotlin. The engine design i ## Example +A simple ssytem that iterates over all entities with a position and velocity, updating the position every engine tick. ```kotlin data class Position(var x: Double, var y: Double) data class Velocity(var x: Double, var y: Double) class UpdatePositionSystem : TickingSystem(interval = 20.milliseconds) { // Specify all components we want (Geary also supports branched AND/OR/NOT statements for selection) - val TargetScope.position by get() - val TargetScope.velocity by get() + val Pointer.position by get() + val Pointer.velocity by get() - override fun TargetScope.tick() { + override fun Pointer.tick() { // We can access our components like regular variables! position.x += velocity.x position.y += velocity.y @@ -61,17 +51,13 @@ fun main() { entity { setAll(Position(0.0, 0.0), Velocity(1.0, 0.0)) } - - // Systems are queries! - val positions: List = UpdatePositionSystem.run { - filter { it.velocity != Velocity(0.0, 0.0) } - .map { it.position } - } } ``` ## Usage +A WIP wiki can be found at [wiki.mineinabyss.com](https://wiki.mineinabyss.com/geary/) + ### Gradle ```kotlin repositories { @@ -85,13 +71,10 @@ dependencies { } ``` -### Wiki -A WIP wiki can be found at [wiki.mineinabyss.com](https://wiki.mineinabyss.com/geary/) - ## Roadmap As the project matures, our primary goal is to make it useful to more people. Here are a handful of features we hope to achieve: -- (Ongoing) Multiplatform support, with js, jvm, and native targets -- Optimize key bottlenecks and benchmark the engine +- Multiplatform support, with js, jvm, and native targets +- Publish numbers for benchmarks and cover more parts of the engine with them - Component data migrations - Complex queries (including relations like parent/child) diff --git a/addons/geary-autoscan/src/main/kotlin/com/mineinabyss/geary/autoscan/AutoScanner.kt b/addons/geary-autoscan/src/main/kotlin/com/mineinabyss/geary/autoscan/AutoScanner.kt index f2f5ce5e4..f6b48be97 100644 --- a/addons/geary-autoscan/src/main/kotlin/com/mineinabyss/geary/autoscan/AutoScanner.kt +++ b/addons/geary-autoscan/src/main/kotlin/com/mineinabyss/geary/autoscan/AutoScanner.kt @@ -34,7 +34,7 @@ interface AutoScanner { } override fun AutoScanner.install() { - geary.pipeline.intercept(GearyPhase.INIT_SYSTEMS) { + geary.pipeline.runOnOrAfter(GearyPhase.INIT_SYSTEMS) { installSystems() } } diff --git a/addons/geary-common-features/src/main/kotlin/com/mineinabyss/geary/game/systems/ExpiringComponentSystem.kt b/addons/geary-common-features/src/main/kotlin/com/mineinabyss/geary/game/systems/ExpiringComponentSystem.kt index 965fcca7f..0443c647f 100644 --- a/addons/geary-common-features/src/main/kotlin/com/mineinabyss/geary/game/systems/ExpiringComponentSystem.kt +++ b/addons/geary-common-features/src/main/kotlin/com/mineinabyss/geary/game/systems/ExpiringComponentSystem.kt @@ -1,19 +1,24 @@ package com.mineinabyss.geary.game.systems +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.game.components.Expiry import com.mineinabyss.geary.systems.RepeatingSystem -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointer + /** * Handles removing components when an [Expiry] relation exists with another component. */ class ExpiringComponentSystem : RepeatingSystem() { - private val TargetScope.expiry by getRelations() + private val Pointer.expiry by getRelationsWithData() - override fun TargetScope.tick() { - if (expiry.data.timeOver()) { - entity.remove(expiry.kind.id) - entity.remove(expiry.relation.id) + @OptIn(UnsafeAccessors::class) + override fun Pointer.tick() { + expiry.forEach { + if (it.data.timeOver()) { + entity.remove(it.kind.id) + entity.remove(it.relation.id) + } } } } diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/Prefabs.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/Prefabs.kt index dbacabd38..6228599d1 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/Prefabs.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/Prefabs.kt @@ -33,7 +33,7 @@ interface Prefabs { ParseRelationWithDataSystem(), TrackPrefabsByKeySystem(), ) - geary.pipeline.intercept(GearyPhase.INIT_ENTITIES) { + geary.pipeline.runOnOrAfter(GearyPhase.INIT_ENTITIES) { loader.loadPrefabs() } } diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseChildOnPrefab.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseChildOnPrefab.kt index 897e63d90..2e248e500 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseChildOnPrefab.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseChildOnPrefab.kt @@ -1,43 +1,44 @@ package com.mineinabyss.geary.prefabs.configuration.systems -import com.mineinabyss.geary.annotations.Handler import com.mineinabyss.geary.components.EntityName import com.mineinabyss.geary.components.relations.NoInherit +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.helpers.addParent import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.prefabs.configuration.components.ChildOnPrefab import com.mineinabyss.geary.prefabs.configuration.components.ChildrenOnPrefab import com.mineinabyss.geary.prefabs.configuration.components.Prefab import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers + class ParseChildOnPrefab : Listener() { - private val TargetScope.child by onSet().onTarget() + private var Pointers.child by get().removable().whenSetOnTarget() - @Handler - private fun TargetScope.convertToRelation() { + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { entity { - addParent(entity) - setAll(child.components) + addParent(target.entity) + setAll(child!!.components) } - entity.remove() + child = null } } class ParseChildrenOnPrefab : Listener() { - private val TargetScope.children by onSet().onTarget() + private var Pointers.children by get().removable().whenSetOnTarget() - @Handler - private fun TargetScope.convertToRelation() { - children.nameToComponents.forEach { (name, components) -> + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { + children!!.nameToComponents.forEach { (name, components) -> entity { set(EntityName(name)) set(Prefab()) - addParent(entity) + addParent(target.entity) addRelation() setAll(components) } } - entity.remove() + children = null } } diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationOnPrefab.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationOnPrefab.kt index 204245043..c21f15929 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationOnPrefab.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationOnPrefab.kt @@ -1,20 +1,19 @@ package com.mineinabyss.geary.prefabs.configuration.systems -import com.mineinabyss.geary.annotations.Handler import com.mineinabyss.geary.prefabs.configuration.components.RelationOnPrefab import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers + class ParseRelationOnPrefab : Listener() { - private val TargetScope.relation by onSet().onTarget() + private var Pointers.relation by get().removable().whenSetOnTarget() - @Handler - private fun TargetScope.convertToRelation() { + override fun Pointers.handle() { try { - val rel: RelationOnPrefab = relation + val rel: RelationOnPrefab = relation!! // entity.setRelation(relation.value, entity.parseEntity(relation.key).id) } finally { - entity.remove() + relation = null } } } diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationWithDataSystem.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationWithDataSystem.kt index 8b2a37d1c..530a383a4 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationWithDataSystem.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/configuration/systems/ParseRelationWithDataSystem.kt @@ -1,15 +1,18 @@ package com.mineinabyss.geary.prefabs.configuration.systems -import com.mineinabyss.geary.annotations.Handler +import com.mineinabyss.geary.datatypes.Records +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.systems.Listener +import com.mineinabyss.geary.systems.accessors.Pointers import com.mineinabyss.geary.systems.accessors.RelationWithData -import com.mineinabyss.geary.systems.accessors.TargetScope + class ParseRelationWithDataSystem : Listener() { - private val TargetScope.relationWithData by onSet>().onTarget() + private val Records.relationWithData by get>().whenSetOnTarget() - @Handler - private fun TargetScope.convertToRelation() { + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { + val entity = target.entity val data = relationWithData.data val targetData = relationWithData.targetData if (data != null) entity.set(data, relationWithData.relation.id) diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/InheritPrefabsOnLoad.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/InheritPrefabsOnLoad.kt index 02426c851..e73fc8c68 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/InheritPrefabsOnLoad.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/InheritPrefabsOnLoad.kt @@ -1,19 +1,19 @@ package com.mineinabyss.geary.prefabs.systems -import com.mineinabyss.geary.annotations.Handler +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.datatypes.family.family import com.mineinabyss.geary.prefabs.events.PrefabLoaded import com.mineinabyss.geary.prefabs.helpers.inheritPrefabs import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers + class InheritPrefabsOnLoad : Listener() { - private val EventScope.loaded by family { has() }.onEvent() + private val Pointers.loaded by family { has() }.on(event) - @Handler - private fun TargetScope.inheritOnLoad() { - entity.inheritPrefabs() + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { + target.entity.inheritPrefabs() } } diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/TrackPrefabsByKeySystem.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/TrackPrefabsByKeySystem.kt index 82e8ad578..105d5c869 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/TrackPrefabsByKeySystem.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/systems/TrackPrefabsByKeySystem.kt @@ -1,18 +1,20 @@ package com.mineinabyss.geary.prefabs.systems -import com.mineinabyss.geary.annotations.Handler import com.mineinabyss.geary.components.relations.NoInherit +import com.mineinabyss.geary.datatypes.Records +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.prefabs.PrefabKey import com.mineinabyss.geary.prefabs.prefabs import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers + class TrackPrefabsByKeySystem : Listener() { - private val TargetScope.key by onSet().onTarget() + private val Records.key by get().whenSetOnTarget() - @Handler - private fun TargetScope.registerOnSet() { - prefabs.manager.registerPrefab(key, entity) - entity.addRelation() + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { + prefabs.manager.registerPrefab(key, target.entity) + target.entity.addRelation() } } diff --git a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/SerializableComponents.kt b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/SerializableComponents.kt index b41020511..8a3e81db8 100644 --- a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/SerializableComponents.kt +++ b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/SerializableComponents.kt @@ -27,7 +27,7 @@ interface SerializableComponents { } override fun Builder.install() { - geary.pipeline.intercept(GearyPhase.ADDONS_CONFIGURED) { + geary.pipeline.runOnOrAfter(GearyPhase.ADDONS_CONFIGURED) { DI.add(object : SerializableComponents { override val serializers = serializersBuilder.build() override val formats = formatsBuilder.build(serializers) diff --git a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/builders/FormatsBuilder.kt b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/builders/FormatsBuilder.kt index da5fcd3ad..f9e7e5f94 100644 --- a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/builders/FormatsBuilder.kt +++ b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/dsl/builders/FormatsBuilder.kt @@ -21,6 +21,7 @@ class FormatsBuilder { binaryFormat = Cbor { serializersModule = serializers.module encodeDefaults = false + ignoreUnknownKeys = true }, formats = formats.mapValues { it.value(serializers.module) }, ) diff --git a/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/TrackUuidOnAdd.kt b/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/TrackUuidOnAdd.kt index 2f3181580..b16e6d672 100644 --- a/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/TrackUuidOnAdd.kt +++ b/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/TrackUuidOnAdd.kt @@ -2,25 +2,27 @@ package com.mineinabyss.geary.uuid.systems import com.benasher44.uuid.Uuid import com.benasher44.uuid.uuid4 -import com.mineinabyss.geary.annotations.Handler +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.systems.GearyListener -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers + import com.mineinabyss.geary.uuid.components.RegenerateUUIDOnClash import com.mineinabyss.geary.uuid.uuid2Geary class TrackUuidOnAdd : GearyListener() { - private val TargetScope.uuid by onSet().onTarget() + var Pointers.uuid by get().whenSetOnTarget() + val Pointers.regenerateUUIDOnClash by get().orNull().on(target) - @Handler - private fun TargetScope.track() { + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { if (uuid in uuid2Geary) - if (entity.has()) { + if (regenerateUUIDOnClash != null) { val newUuid = uuid4() - entity.set(newUuid) - uuid2Geary[newUuid] = entity - } else error("Tried tracking entity $entity with already existing uuid $uuid") + uuid = newUuid + uuid2Geary[newUuid] = target.entity + } else error("Tried tracking entity $target.entity with already existing uuid $uuid") else - uuid2Geary[uuid] = entity + uuid2Geary[uuid] = target.entity } } diff --git a/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/UnTrackUuidOnRemove.kt b/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/UnTrackUuidOnRemove.kt index 8e816c655..cb5cd0c12 100644 --- a/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/UnTrackUuidOnRemove.kt +++ b/addons/geary-uuid/src/commonMain/kotlin/com/mineinabyss/geary/uuid/systems/UnTrackUuidOnRemove.kt @@ -1,20 +1,19 @@ package com.mineinabyss.geary.uuid.systems import com.benasher44.uuid.Uuid -import com.mineinabyss.geary.annotations.Handler import com.mineinabyss.geary.components.events.EntityRemoved +import com.mineinabyss.geary.datatypes.Records import com.mineinabyss.geary.datatypes.family.family import com.mineinabyss.geary.systems.GearyListener -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers + import com.mineinabyss.geary.uuid.uuid2Geary class UnTrackUuidOnRemove : GearyListener() { - private val TargetScope.uuid by get().onTarget() - private val EventScope.removed by family { has() }.onEvent() + private val Pointers.uuid by get().on(target) + private val Pointers.removed by family { has() }.on(event) - @Handler - private fun TargetScope.untrack() { + override fun Pointers.handle() { uuid2Geary.remove(uuid) } } diff --git a/build.gradle.kts b/build.gradle.kts index ed24a32e3..87132dec0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ allprojects { subprojects { tasks.withType().configureEach { kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" +// kotlinOptions.freeCompilerArgs += "-Xno-param-assertions" } } diff --git a/geary-benchmarks/build.gradle.kts b/geary-benchmarks/build.gradle.kts new file mode 100644 index 000000000..b8b79e9cb --- /dev/null +++ b/geary-benchmarks/build.gradle.kts @@ -0,0 +1,62 @@ +import kotlinx.benchmark.gradle.JvmBenchmarkTarget +import org.jetbrains.kotlin.allopen.gradle.AllOpenExtension + +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id(libs.plugins.mia.kotlin.jvm.get().pluginId) +// id(libs.plugins.mia.publication.get().pluginId) +// alias(libs.plugins.kotlinx.serialization) + id("org.jetbrains.kotlinx.benchmark") version "0.4.9" + kotlin("plugin.allopen") version "1.9.10" +} + +configure { + annotation("org.openjdk.jmh.annotations.State") +} + +dependencies { + implementation(project(":geary-core")) + implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.9") +} + +benchmark { + configurations { + named("main") { + exclude("jvmTesting") + warmups = 3 + iterations = 3 + iterationTime = 5 + iterationTimeUnit = "sec" + } + + create("fast") { + exclude("jvmTesting") + warmups = 1 + iterations = 1 + iterationTime = 3 + iterationTimeUnit = "sec" + } + + create("fastest") { + exclude("jvmTesting") + warmups = 1 + iterations = 1 + iterationTime = 3 + iterationTimeUnit = "sec" + } + + create("specific") { + include("Unpack") + warmups = 1 + iterations = 1 + iterationTime = 3 + iterationTimeUnit = "sec" + } + } + targets { + register("main") { + this as JvmBenchmarkTarget + jmhVersion = "1.21" + } + } +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt new file mode 100644 index 000000000..eff2b1a89 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/VelocitySystemBenchmark.kt @@ -0,0 +1,56 @@ +package com.mineinabyss.geary.benchmarks + +import com.mineinabyss.geary.benchmarks.helpers.oneMil +import com.mineinabyss.geary.benchmarks.helpers.tenMil +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.RepeatingSystem +import com.mineinabyss.geary.systems.accessors.Pointer +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +class VelocitySystemBenchmark { + data class Velocity(val x: Float, val y: Float) + data class Position(var x: Float, var y: Float) + + object VelocitySystem : RepeatingSystem() { + private val Pointer.velocity by get() + private var Pointer.position by get() + + override fun Pointer.tick() { + position.x += velocity.x + position.y += velocity.y + } + } + + @Setup + fun setUp() { + geary(TestEngineModule) + + repeat(tenMil) { + entity { + set(Velocity(it.toFloat() / oneMil, it.toFloat() / oneMil)) + set(Position(0f, 0f)) + } + } + } + + @Benchmark + fun velocitySystem() { + VelocitySystem.tickAll() + } +} + +fun main() { + VelocitySystemBenchmark().apply { + setUp() + + repeat(400) { + velocitySystem() + } + } +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/TestComponents.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/TestComponents.kt new file mode 100644 index 000000000..0ce4ffd3b --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/TestComponents.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.geary.benchmarks.helpers + +data class Comp1(val id: Int) +data class Comp2(val id: Int) +data class Comp3(val id: Int) +data class Comp4(val id: Int) +data class Comp5(val id: Int) +data class Comp6(val id: Int) diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/Values.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/Values.kt new file mode 100644 index 000000000..95d53b592 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/helpers/Values.kt @@ -0,0 +1,4 @@ +package com.mineinabyss.geary.benchmarks.helpers + +const val oneMil = 1000000 +const val tenMil = 10000000 diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/ArchetypeBenchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/ArchetypeBenchmark.kt new file mode 100644 index 000000000..3e04b1b73 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/ArchetypeBenchmark.kt @@ -0,0 +1,13 @@ +package com.mineinabyss.geary.benchmarks.instantiation + +import com.mineinabyss.geary.datatypes.EntityType +import com.mineinabyss.geary.engine.archetypes.Archetype +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +class ArchetypeBenchmark { + var arch: Archetype = Archetype(EntityType(listOf(1uL, 1uL shl 10, 1uL shl 32 or 1uL)), 0) + val type = EntityType(listOf(1uL, 1uL shl 10, 1uL shl 32 or 1uL)) + +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/ManyComponentsBenchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/ManyComponentsBenchmark.kt new file mode 100644 index 000000000..95ed9bae4 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/ManyComponentsBenchmark.kt @@ -0,0 +1,42 @@ +package com.mineinabyss.geary.benchmarks.instantiation + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.helpers.toGeary +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.idofront.di.DI +import org.openjdk.jmh.annotations.* + +@State(Scope.Benchmark) +class ManyComponentsBenchmark { + @Setup + fun setLoggingLevel() { + Logger.setMinSeverity(Severity.Warn) + } + + @Setup(Level.Invocation) + fun setupPerInvocation() { + geary(TestEngineModule) + } + + @TearDown(Level.Invocation) + fun teardown() { + DI.clear() + } + + @Benchmark + fun create1MilEntitiesWithUniqueComponentEach() { + repeat(10000) { + entity { + addRelation(it.toLong().toGeary()) + } + } + } +} + +fun main() { + geary(TestEngineModule) + ManyComponentsBenchmark().create1MilEntitiesWithUniqueComponentEach() +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/NewEntityBenchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/NewEntityBenchmark.kt new file mode 100644 index 000000000..4bbbbe346 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/instantiation/NewEntityBenchmark.kt @@ -0,0 +1,94 @@ +package com.mineinabyss.geary.benchmarks.instantiation + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import com.mineinabyss.geary.benchmarks.helpers.* +import com.mineinabyss.geary.helpers.componentId +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.idofront.di.DI +import org.openjdk.jmh.annotations.* + +@State(Scope.Benchmark) +class NewEntityBenchmark { + @Setup + fun setLoggingLevel() { + Logger.setMinSeverity(Severity.Warn) + } + + @Setup(Level.Invocation) + fun setupPerInvocation() { + geary(TestEngineModule) + } + + @TearDown(Level.Invocation) + fun teardown() { + DI.clear() + } + + @Benchmark + fun create1MilEntitiesWith0Components() { + repeat(oneMil) { + entity() + } + } + + @Benchmark + fun create1MilEntitiesWith1ComponentNoEvent() { + repeat(oneMil) { + entity { + set(Comp1(0), noEvent = true) + } + } + } + + @Benchmark + fun create1MilEntitiesWith1ComponentYesEvent() { + repeat(oneMil) { + entity { + set(Comp1(0)) + } + } + } + + @Benchmark + fun create1MilEntitiesWith6Components() { + repeat(oneMil) { + entity { + set(Comp1(0), noEvent = true) + set(Comp2(0), noEvent = true) + set(Comp3(0), noEvent = true) + set(Comp4(0), noEvent = true) + set(Comp5(0), noEvent = true) + set(Comp6(0), noEvent = true) + } + } + } + + @Benchmark + fun create1MilEntitiesWith6ComponentsWithoutComponentIdCalls() { + val comp1Id = componentId() + val comp2Id = componentId() + val comp3Id = componentId() + val comp4Id = componentId() + val comp5Id = componentId() + val comp6Id = componentId() + + repeat(oneMil) { + entity { + set(Comp1(0), comp1Id) + set(Comp2(0), comp2Id) + set(Comp3(0), comp3Id) + set(Comp4(0), comp4Id) + set(Comp5(0), comp5Id) + set(Comp6(0), comp6Id) + } + } + } +} + +fun main() { + geary(TestEngineModule) + NewEntityBenchmark().create1MilEntitiesWith0Components() +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/jvmTesting/MemoryAccessBenchmarks.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/jvmTesting/MemoryAccessBenchmarks.kt new file mode 100644 index 000000000..b8ec84677 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/jvmTesting/MemoryAccessBenchmarks.kt @@ -0,0 +1,157 @@ +package com.mineinabyss.geary.benchmarks.jvmTesting + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import kotlin.experimental.or +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +data class TestData(val int: Int, val double: Double) + +@State(Scope.Benchmark) +class MemoryAccessBenchmarks { + private final val oneMil = 1000000 + var intArr = intArrayOf() + var byteArr = byteArrayOf() + var objArr = arrayOf() + var anyArr = mutableListOf() + var accessorWithScope = AccessorWithScope(intArrayOf()) + + var nDimensionalArr = Array(8) { IntArray(oneMil) { it } } + + @Setup + fun setup() { + intArr = IntArray(oneMil) { it } + byteArr = ByteArray(oneMil) { it.toByte() } + objArr = Array(oneMil) { TestData(it, it.toDouble()) } + anyArr = MutableList(oneMil) { TestData(it, it.toDouble()) } + accessorWithScope = AccessorWithScope(intArr) + } + + @Benchmark + fun emptyLoop() { + for (i in 0 until oneMil) { + } + } + + @Benchmark + fun readIntArrDirectly() { + for (i in 0 until oneMil) { + intArr[i] + 1 + } + } + + @Benchmark + fun readObjArrDirectly() { + for (i in 0 until oneMil) { + objArr[i] + } + } + + @Benchmark + fun readObjArrWithAny() { + for (i in 0 until oneMil) { + anyArr[i] + } + } + + @Benchmark + fun read2ArrDirectly() { + for (i in 0 until oneMil) { + intArr[i] + 1 + byteArr[i] or 1 + } + } + + @Benchmark + fun read8ArrDirectly() { + for (i in 0 until oneMil) { + nDimensionalArr[0][i] + 1 + nDimensionalArr[1][i] + 1 + nDimensionalArr[2][i] + 1 + nDimensionalArr[3][i] + 1 + nDimensionalArr[4][i] + 1 + nDimensionalArr[5][i] + 1 + nDimensionalArr[6][i] + 1 + nDimensionalArr[7][i] + 1 + } + } + + @Benchmark + fun readWriteSingleIntArrDirectly() { + for (i in 0 until oneMil) { + intArr[i] = intArr[i] + 1 + } + } + + + @Benchmark + fun readWriteTwoArrDirectly() { + for (i in 0 until oneMil) { + intArr[i] = intArr[i] + 1 + byteArr[i] = byteArr[i] or 1 + } + } + + @Benchmark + fun readObjPackToLocalArr() { + for (i in 0 until oneMil) { + val localArr = intArrayOf(intArr[i]) + } + } + + @Benchmark + fun readObjPackManyToLocalArr() { + for (i in 0 until oneMil) { + val localArr = arrayOf(intArr[i], byteArr[i], objArr[i]) + } + } + + @Benchmark + fun readPackToSeparateArr() { + val arr = IntArray(oneMil) + for (i in 0 until oneMil) { + arr[i] = intArr[i] + } + } + + @Benchmark + fun readIntArrWithIndirection() { + for (i in 0 until oneMil) { + val acc by Accessor(i) + acc + 1 + } + } + + + @Benchmark + fun readIntArrWithIndirectionAndScope() { + val accessors = object { + val Int.acc by accessorWithScope + } + + for (i in 0 until oneMil) { + with(accessors) { + with(i) { + acc + 1 + } + } + } + } + + inner class Accessor(val index: Int) : ReadOnlyProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Int { + return intArr[index] + } + } + + @JvmInline + value class AccessorWithScope(private val array: IntArray) : ReadOnlyProperty { + override fun getValue(thisRef: Int, property: KProperty<*>): Int { + return array[thisRef] + } + } +} + diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt new file mode 100644 index 000000000..9ffb45eb9 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/misc/ComponentIdTest.kt @@ -0,0 +1,41 @@ +package com.mineinabyss.geary.benchmarks.misc + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import com.mineinabyss.geary.benchmarks.helpers.* +import com.mineinabyss.geary.helpers.componentId +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +class ComponentIdTest { + @Setup + fun setup() { + Logger.setMinSeverity(Severity.Warn) + geary(TestEngineModule) + } + + @Benchmark + fun componentIdFor6Comp() { + repeat(tenMil) { + componentId() + componentId() + componentId() + componentId() + componentId() + componentId() + } + } +} + +fun main() { + geary(TestEngineModule) + ComponentIdTest().apply { + setup() + componentIdFor6Comp() + } +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt new file mode 100644 index 000000000..557a4c7c3 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack1Benchmark.kt @@ -0,0 +1,40 @@ +package com.mineinabyss.geary.benchmarks.unpacking + +import com.mineinabyss.geary.benchmarks.helpers.Comp1 +import com.mineinabyss.geary.benchmarks.helpers.tenMil +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.query.GearyQuery +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +class Unpack1Benchmark { + private object SystemOf1 : GearyQuery() { + val Pointer.comp1 by get() + } + + @Setup + fun setUp() { + geary(TestEngineModule) + + repeat(tenMil) { + entity { + set(Comp1(1)) + } + } + } + + @Benchmark + fun unpack1of1Comp() { + SystemOf1.run { + forEach { + it.comp1 + } + } + } +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt new file mode 100644 index 000000000..340e0a13f --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack2Benchmark.kt @@ -0,0 +1,58 @@ +package com.mineinabyss.geary.benchmarks.unpacking + +import com.mineinabyss.geary.benchmarks.helpers.Comp1 +import com.mineinabyss.geary.benchmarks.helpers.Comp2 +import com.mineinabyss.geary.benchmarks.helpers.tenMil +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.query.GearyQuery +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +class Unpack2Benchmark { + private object SystemOf2 : GearyQuery() { + val Pointer.comp1 by get() + val Pointer.comp2 by get() + } + + private object SystemOf1 : GearyQuery() { + val Pointer.comp1 by get() + } + + @Setup + fun setUp() { + geary(TestEngineModule) { + } + + repeat(tenMil) { + entity { + set(Comp1(1)) + set(Comp2(1)) + } + } + } + + @Benchmark + fun unpack1of2Comp() { + SystemOf1.run { + forEach { + it.comp1 + } + } + } + + @Benchmark + fun unpack2of2Comp() { + SystemOf2.run { + forEach { + it.comp1 + it.comp2 + } + } + } +} diff --git a/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt new file mode 100644 index 000000000..1c225b916 --- /dev/null +++ b/geary-benchmarks/src/main/kotlin/com/mineinabyss/geary/benchmarks/unpacking/Unpack6Benchmark.kt @@ -0,0 +1,112 @@ +package com.mineinabyss.geary.benchmarks.unpacking + +import com.mineinabyss.geary.benchmarks.helpers.* +import com.mineinabyss.geary.datatypes.family.family +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.modules.TestEngineModule +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.query.GearyQuery +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +class Unpack6Benchmark { + private object SystemOf6 : GearyQuery() { + val Pointer.comp1 by get() + val Pointer.comp2 by get() + val Pointer.comp3 by get() + val Pointer.comp4 by get() + val Pointer.comp5 by get() + val Pointer.comp6 by get() + + } + + private object SystemOf6WithoutDelegate : GearyQuery() { + val comp1 = get() + val comp2 = get() + val comp3 = get() + val comp4 = get() + val comp5 = get() + val comp6 = get() + + val test by family { + hasSet() + hasSet() + hasSet() + hasSet() + hasSet() + hasSet() + } + } + + private object SystemOf1 : GearyQuery() { + val Pointer.comp1 by get() + } + + @Setup + fun setUp() { + geary(TestEngineModule) { + } + + repeat(tenMil) { + entity { + set(Comp1(0)) + set(Comp2(0)) + set(Comp3(0)) + set(Comp4(0)) + set(Comp5(0)) + set(Comp6(0)) + } + } + } + + @Benchmark + fun unpack1of6Comp() { + SystemOf1.run { + forEach { + it.comp1 + } + } + } + + @Benchmark + fun unpack6of6Comp() { + SystemOf6.run { + forEach { + it.comp1 + it.comp2 + it.comp3 + it.comp4 + it.comp5 + it.comp6 + } + } + } + + @Benchmark + fun unpack6of6CompNoDelegate() { + SystemOf6WithoutDelegate.run { + forEach { + comp1[it] + comp2[it] + comp3[it] + comp4[it] + comp5[it] + comp6[it] + } + } + } +} + + +fun main() { + Unpack6Benchmark().apply { + setUp() + repeat(100) { + unpack6of6Comp() + } + } +} diff --git a/geary-core/build.gradle.kts b/geary-core/build.gradle.kts index 12e560ad9..65f01d8c2 100644 --- a/geary-core/build.gradle.kts +++ b/geary-core/build.gradle.kts @@ -30,8 +30,8 @@ kotlin { implementation(mylibs.atomicfu) implementation(libs.kotlin.reflect) implementation(libs.kotlinx.serialization.cbor) - implementation(libs.idofront.di) + api(libs.idofront.di) api(mylibs.kds) api(mylibs.kermit) api(libs.kotlinx.coroutines) @@ -52,6 +52,7 @@ kotlin { val jvmMain by getting { dependencies { implementation(libs.kotlinx.serialization.kaml) + implementation(libs.fastutil) implementation(mylibs.roaringbitmap) } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/annotations/Handler.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/annotations/Handler.kt deleted file mode 100644 index e45f3c83a..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/annotations/Handler.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.mineinabyss.geary.annotations - -import com.mineinabyss.geary.events.Handler -import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.SourceScope -import com.mineinabyss.geary.systems.accessors.TargetScope - -/** - * Indicates a function within a [Listener] should be registered as a [Handler] - * - * The function can read from different accessors by adding arguments [SourceScope], [TargetScope], [EventScope]. - * They may appear in any order, be omitted, or used as a receiver. - * - * If [SourceScope] is nullable or omitted, the handler will not be called when there is no source present on the event. - * - * Example: - * - * ```kotlin - * @Handler - * fun TargetScope.doSomething(source: SourceScope, event: EventScope) { - * // Within here, you may use accessors defined for all three. - * } - * ``` - */ -@Target(AnnotationTarget.FUNCTION) -annotation class Handler - diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/annotations/optin/UnsafeAccessors.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/annotations/optin/UnsafeAccessors.kt new file mode 100644 index 000000000..df20f9221 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/annotations/optin/UnsafeAccessors.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.geary.annotations.optin + +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "Reading and writing entity data without an accessor reduces speed and does not enforce null safety for accessors." + + " Be careful when manually removing components used by other accessors." +) +annotation class UnsafeAccessors diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/components/events/FailedCheck.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/components/events/FailedCheck.kt index 6321b2a20..965e36c4e 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/components/events/FailedCheck.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/components/events/FailedCheck.kt @@ -2,7 +2,5 @@ package com.mineinabyss.geary.components.events /** * A component that gets added to events that failed a check. - * - * @see RequestCheck */ sealed class FailedCheck diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Entity.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Entity.kt index 5ac92537c..8602de49e 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Entity.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Entity.kt @@ -2,16 +2,12 @@ package com.mineinabyss.geary.datatypes import com.mineinabyss.geary.annotations.optin.DangerousComponentOperation import com.mineinabyss.geary.components.events.AddedComponent -import com.mineinabyss.geary.components.relations.ChildOf import com.mineinabyss.geary.components.relations.InstanceOf import com.mineinabyss.geary.components.relations.Persists -import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.datatypes.family.family import com.mineinabyss.geary.engine.Engine -import com.mineinabyss.geary.helpers.component -import com.mineinabyss.geary.helpers.componentId -import com.mineinabyss.geary.helpers.componentIdWithNullable -import com.mineinabyss.geary.helpers.temporaryEntity +import com.mineinabyss.geary.helpers.* +import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.accessors.AccessorOperations import com.mineinabyss.geary.systems.accessors.RelationWithData import kotlinx.serialization.Serializable @@ -40,12 +36,12 @@ value class Entity(val id: EntityId) { val children: List get() = queryManager.getEntitiesMatching(family { - hasRelation(this@Entity) + hasRelation(geary.components.childOf, this@Entity.id) }) val instances: List get() = queryManager.getEntitiesMatching(family { - hasRelation(this@Entity) + hasRelation(geary.components.instanceOf, this@Entity.id) }) /** Remove this entity from the ECS. */ @@ -212,7 +208,6 @@ value class Entity(val id: EntityId) { inline fun has(kClass: KClass): Boolean = has(componentId(kClass)) - /** Checks whether this entity has a [component], regardless of it holding data. */ fun has(component: ComponentId): Boolean = read.hasComponentFor(this, component) @@ -274,6 +269,10 @@ value class Entity(val id: EntityId) { geary.write.addComponentFor(this, Relation.of(target).id, noEvent) } + fun addRelation(kind: ComponentId, target: EntityId, noEvent: Boolean = false) { + geary.write.addComponentFor(this, Relation.of(kind, target).id, noEvent) + } + /** Removes a relation key of type [K] and value of type [V]. */ inline fun removeRelation(): Boolean { return removeRelation(component()) diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityStack.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityStack.kt index f1c42aa58..1578181df 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityStack.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityStack.kt @@ -1,7 +1,7 @@ package com.mineinabyss.geary.datatypes import com.mineinabyss.geary.helpers.toGeary -import com.soywiz.kds.DoubleQueue +import korlibs.datastructure.DoubleQueue import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized @@ -13,7 +13,8 @@ class EntityStack(private val stack: DoubleQueue = DoubleQueue()) { } } - fun pop(): Entity = synchronized(removedEntitiesLock) { - stack.dequeue().toRawBits().toGeary() + fun pop(): Entity? = synchronized(removedEntitiesLock) { + if (stack.isEmpty()) null + else stack.dequeue().toRawBits().toGeary() } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityType.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityType.kt index 1957309ea..51e2537e6 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityType.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/EntityType.kt @@ -1,37 +1,31 @@ package com.mineinabyss.geary.datatypes -import com.mineinabyss.geary.components.relations.InstanceOf -import com.mineinabyss.geary.helpers.componentId import com.mineinabyss.geary.helpers.readableString -import kotlin.jvm.JvmInline + /** * An inlined class used for tracking the components an entity/archetype has. * * It provides fast (no boxing) functions backed by FastUtil sorted sets to do operations with [ComponentId]s. */ -@JvmInline -value class EntityType private constructor( +class EntityType private constructor( @PublishedApi internal val inner: ULongArray ) { constructor() : this(ULongArray(0)) - constructor(ids: Collection) : this(inner = ids.toULongArray().apply { sort() }) - val size: Int get() = inner.size + constructor(ids: Collection) : this(inner = ids.toULongArray().apply { sort() }) - val prefabs: EntityType - get() = EntityType(filter { contains(Relation.of(componentId(), it).id) } - .map { Relation.of(it).target }) + val size: Int = inner.size operator fun contains(id: ComponentId): Boolean = indexOf(id) != -1 fun indexOf(id: ComponentId): Int { - return binarySearch(id).coerceAtLeast(-1) + return inner.indexOf(id) } - tailrec fun binarySearch(id: ComponentId, fromIndex: Int = 0, toIndex: Int = inner.lastIndex): Int { + tailrec fun binarySearch(id: ComponentId, fromIndex: Int = 0, toIndex: Int = size - 1): Int { if (fromIndex > toIndex) return -fromIndex - 1 val mid = (fromIndex + toIndex) / 2 val found = inner[mid] @@ -46,11 +40,9 @@ value class EntityType private constructor( fun last(): ComponentId = inner.last() inline fun forEach(run: (ComponentId) -> Unit) { - inner.forEach(run) -// val iterator = inner.iterator() -// while (iterator.hasNext()) { -// run(iterator.nextLong().toULong()) -// } + for(i in 0..inner.lastIndex) { + run(inner[i]) + } } inline fun any(predicate: (ComponentId) -> Boolean): Boolean { @@ -60,16 +52,10 @@ value class EntityType private constructor( inline fun forEachIndexed(run: (Int, ComponentId) -> Unit) { inner.forEachIndexed(run) -// val iterator = inner.iterator() -// var i = 0 -// forEach { run(i++, iterator.nextLong().toULong()) } } inline fun filter(predicate: (ComponentId) -> Boolean): EntityType { return EntityType(inner.filter(predicate)) -// val type = LongAVLTreeSet() -// forEach { if (predicate(it)) type.add(it.toLong()) } -// return GearyType(type) } inline fun map(transform: (ULong) -> T): List { diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/GearyAliases.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/GearyAliases.kt index de7111323..5df043af2 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/GearyAliases.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/GearyAliases.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Polymorphic typealias GearyEntity = Entity typealias GearyEntityType = EntityType typealias GearyRecord = Record +typealias GearyRecords = Records typealias GearyRelation = Relation /** diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/IdList.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/IdList.kt index 53e37e12f..af1bc8afd 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/IdList.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/IdList.kt @@ -1,4 +1,33 @@ package com.mineinabyss.geary.datatypes -// TODO more efficient type, no boxing -typealias IdList = ArrayList +import com.mineinabyss.geary.helpers.toGeary + +private const val initialSize: Int = 4 +private const val growFactor: Int = 2 + +class IdList { + var backingArr = ULongArray(initialSize) + var size = 0 + val lastIndex get() = size - 1 + + operator fun get(index: Int): ULong = backingArr[index] + operator fun set(index: Int, value: ULong) { + backingArr[index] = value + } + + fun add(value: ULong) { + if (size == backingArr.size) { + backingArr = backingArr.copyOf(size * growFactor) + } + backingArr[size++] = value + } + + fun removeLastOrNull(): ULong? { + if (size == 0) return null + return backingArr[--size] + } + + fun getEntities(): Sequence { + return backingArr.asSequence().take(size).map { it.toGeary() } + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Record.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Record.kt index b8b6c56f6..40661d9f9 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Record.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Record.kt @@ -3,7 +3,8 @@ package com.mineinabyss.geary.datatypes import com.mineinabyss.geary.engine.archetypes.Archetype import kotlinx.atomicfu.locks.SynchronizedObject -class Record internal constructor( + +class Record @PublishedApi internal constructor( archetype: Archetype, row: Int ) : SynchronizedObject() { @@ -17,7 +18,7 @@ class Record internal constructor( this.row = row } - internal val entity: Entity get() = archetype.getEntity(row) + val entity: Entity get() = archetype.getEntity(row) operator fun component1(): Archetype = archetype operator fun component2(): Int = row diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/RecordPointer.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/RecordPointer.kt new file mode 100644 index 000000000..a1882bb3a --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/RecordPointer.kt @@ -0,0 +1,42 @@ +package com.mineinabyss.geary.datatypes + +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.engine.archetypes.Archetype +import com.mineinabyss.geary.modules.archetypes + +/** A record created in place that delegates to the real entity pointer the first time [entity] gets accessed. */ +class RecordPointer @PublishedApi internal constructor( + archetype: Archetype, + row: Int +) { + constructor(record: Record) : this(record.archetype, record.row) { + delegated = true + delegate = record + } + + private val originalArchetype = archetype + private val originalRow = row + + @UnsafeAccessors + val archetype: Archetype get() = if (delegated) delegate!!.archetype else originalArchetype + val row: Int get() = if (delegated) delegate!!.row else originalRow + + private var delegate: Record? = null + private var delegated = false + + @UnsafeAccessors + val entity: Entity + get() { + val entity = archetype.getEntity(row) + if (!delegated) { + delegate = archetypes.records[entity] + } + delegated = true + return entity + } + + @UnsafeAccessors + operator fun component1(): Archetype = archetype + + operator fun component2(): Int = row +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Records.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Records.kt new file mode 100644 index 000000000..d53647f08 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/Records.kt @@ -0,0 +1,23 @@ +package com.mineinabyss.geary.datatypes + +import com.mineinabyss.geary.systems.accessors.Pointer + +/** + * A collection of records used for queries involving multiple entities. + * + * Currently built for our event system but will support arbitrary entities once we improve the query system. + */ +class Records( + val target: Pointer, + val event: Pointer, + val source: Pointer?, +) { + fun getByIndex(index: Int): Pointer { + return when (index) { + 0 -> target + 1 -> event + 2 -> source ?: error("Source is null") + else -> error("Index out of bounds") + } + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/TinyBitSet.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/TinyBitSet.kt deleted file mode 100644 index 064d50763..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/TinyBitSet.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.mineinabyss.geary.datatypes - -// Extra Long operations to be able to have a really fast tiny bitset - -fun Long.pop1(): Long { - val c = countTrailingZeroBits() - if (c == 64) return this - return unsetBit(c) -} - -fun Long.setBit(index: Int): Long { - return this or (1L shl index) -} - -fun Long.unsetBit(index: Int): Long { - return this and (1L shl index).inv() -} - -inline fun Long.forEachBit(run: (Int) -> Unit) { - var remaining = this - while (remaining != 0L) { - val hi = remaining.takeHighestOneBit() - remaining = remaining and hi.inv() - run(hi.countTrailingZeroBits()) - } -} - -fun Long.toIntArray(): IntArray { - val arr = IntArray(countOneBits()) - var i = 0 - forEachBit { arr[i] = it; i++ } - return arr -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/family/Family.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/family/Family.kt index e4d8aee19..6df1d66dd 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/family/Family.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/family/Family.kt @@ -2,8 +2,13 @@ package com.mineinabyss.geary.datatypes.family import com.mineinabyss.geary.datatypes.ComponentId import com.mineinabyss.geary.datatypes.EntityId +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.accessors.FamilyMatching +import com.mineinabyss.geary.systems.accessors.ReadOnlyAccessor +import kotlin.reflect.KProperty + +sealed interface Family : ReadOnlyAccessor, FamilyMatching { -sealed interface Family { sealed class Leaf : Family { sealed interface Component : Family { val component: ComponentId @@ -36,4 +41,10 @@ sealed interface Family { val or: List } } + + // Helpers for writing queries + override val family: Family? get() = this + override fun getValue(thisRef: Pointer, property: KProperty<*>): Family { + return this + } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/HashTypeMap.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/ArrayTypeMap.kt similarity index 70% rename from geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/HashTypeMap.kt rename to geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/ArrayTypeMap.kt index 30cf620b4..49f2b1642 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/HashTypeMap.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/ArrayTypeMap.kt @@ -2,13 +2,12 @@ package com.mineinabyss.geary.datatypes.maps import com.mineinabyss.geary.datatypes.Entity import com.mineinabyss.geary.datatypes.Record -import com.soywiz.kds.* import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized -class HashTypeMap : TypeMap { +class ArrayTypeMap : TypeMap { private val lock = SynchronizedObject() - private val map: FastIntMap = FastIntMap() + private val map: ArrayList = arrayListOf() // We don't return nullable record to avoid boxing. // Accessing an entity that doesn't exist is indicative of a problem elsewhere and should be made obvious. @@ -16,15 +15,22 @@ class HashTypeMap : TypeMap { ?: error("Tried to access components on an entity that no longer exists (${entity.id})") override fun set(entity: Entity, record: Record): Unit = synchronized(lock) { + val id = entity.id.toInt() + if(map.size == id) { + map.add(record) + return@synchronized + } if (contains(entity)) error("Tried setting the record of an entity that already exists.") - map[entity.id.toInt()] = record + while(map.size <= id) map.add(null) + map[id] = record } override fun remove(entity: Entity): Unit = synchronized(lock) { - map.remove(entity.id.toInt()) + map[entity.id.toInt()] = null } override operator fun contains(entity: Entity): Boolean = synchronized(lock) { - map.contains(entity.id.toInt()) + val id = entity.id.toInt() + map.size < id && map[id] != null } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt index b68041b8a..b75f1ba11 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt @@ -6,12 +6,25 @@ import com.mineinabyss.geary.engine.archetypes.Archetype /** * Inlined class that acts as a map of components to archetypes. Uses archetype ids for better performance. */ -class CompId2ArchetypeMap { - val inner: MutableMap = mutableMapOf() - operator fun get(id: GearyComponentId): Archetype? = inner[id.toLong()] +expect class CompId2ArchetypeMap() { + operator fun get(id: GearyComponentId): Archetype? + operator fun set(id: GearyComponentId, archetype: Archetype) + + operator fun contains(id: GearyComponentId): Boolean + + fun getOrSet(id: GearyComponentId, put: () -> Archetype): Archetype +} + +class CompId2ArchetypeMapViaMutableMap { + val inner: MutableMap = mutableMapOf() + operator fun get(id: GearyComponentId): Archetype? = inner[id] operator fun set(id: GearyComponentId, archetype: Archetype) { - inner[id.toLong()] = archetype + inner[id] = archetype } - operator fun contains(id: GearyComponentId): Boolean = inner.containsKey(id.toLong()) + operator fun contains(id: GearyComponentId): Boolean = inner.containsKey(id) + + fun getOrSet(id: GearyComponentId, put: () -> Archetype): Archetype { + return inner[id] ?: put().also { inner[id] = it } + } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Components.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Components.kt index 392605210..f8c3ee0a6 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Components.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Components.kt @@ -1,5 +1,9 @@ package com.mineinabyss.geary.engine +import com.mineinabyss.geary.components.CouldHaveChildren +import com.mineinabyss.geary.components.events.* +import com.mineinabyss.geary.components.relations.ChildOf +import com.mineinabyss.geary.components.relations.InstanceOf import com.mineinabyss.geary.components.relations.Persists import com.mineinabyss.geary.datatypes.ComponentId import com.mineinabyss.geary.helpers.componentId @@ -7,4 +11,12 @@ import com.mineinabyss.geary.helpers.componentId class Components { val any: ComponentId = componentId() val persists: ComponentId = componentId() + val suppressRemoveEvent = componentId() + val couldHaveChildren = componentId() + val addedComponent = componentId() + val setComponent = componentId() + val updatedComponent = componentId() + val entityRemoved = componentId() + val childOf = componentId() + val instanceOf = componentId() } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Engine.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Engine.kt index 60087a208..a55daeda9 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Engine.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Engine.kt @@ -10,5 +10,5 @@ import kotlinx.coroutines.CoroutineScope interface Engine : CoroutineScope { /** Ticks the entire engine. Implementations may call at different speeds. */ - suspend fun tick(currentTick: Long) + fun tick(currentTick: Long) } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/EntityProvider.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/EntityProvider.kt index 5e6db8460..56c9ffcbc 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/EntityProvider.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/EntityProvider.kt @@ -6,7 +6,7 @@ import com.mineinabyss.geary.datatypes.EntityType interface EntityProvider { /** Creates a new entity. */ - fun create(initialComponents: Collection = emptyList()): Entity + fun create(): Entity /** Removes an entity, freeing up its entity id for later reuse. */ fun remove(entity: Entity) diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Pipeline.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Pipeline.kt index d4782d979..9cf6c3cb0 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Pipeline.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/Pipeline.kt @@ -5,7 +5,7 @@ import com.mineinabyss.geary.systems.RepeatingSystem import com.mineinabyss.geary.systems.System interface Pipeline { - fun intercept(phase: GearyPhase, block: () -> Unit) + fun runOnOrAfter(phase: GearyPhase, block: () -> Unit) fun interceptSystemAddition(run: (System) -> System?) fun runStartupTasks() diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/PipelineImpl.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/PipelineImpl.kt index f4dde38c0..188cfedc5 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/PipelineImpl.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/PipelineImpl.kt @@ -5,7 +5,7 @@ import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.Listener import com.mineinabyss.geary.systems.RepeatingSystem import com.mineinabyss.geary.systems.System -import com.soywiz.kds.sortedMapOf +import korlibs.datastructure.sortedMapOf class PipelineImpl : Pipeline { private val queryManager get() = geary.queryManager @@ -14,10 +14,12 @@ class PipelineImpl : Pipeline { private val registeredSystems: MutableSet = mutableSetOf() private val registeredListeners: MutableSet = mutableSetOf() - private val actions = sortedMapOf Unit>>() + private val scheduled = sortedMapOf Unit>>() + private var currentPhase = GearyPhase.entries.first() - override fun intercept(phase: GearyPhase, block: () -> Unit) { - actions.getOrPut(phase) { mutableListOf() }.add(block) + override fun runOnOrAfter(phase: GearyPhase, block: () -> Unit) { + if (currentPhase > phase) block() + else scheduled.getOrPut(phase) { mutableListOf() }.add(block) } override fun interceptSystemAddition(run: (System) -> System?) { @@ -25,10 +27,11 @@ class PipelineImpl : Pipeline { } override fun runStartupTasks() { - actions.values.forEach { actions -> + scheduled.values.forEach { actions -> actions.forEach { it() } } } + override fun addSystem(system: System) { val resultSystem = onSystemRegister.fold(system) { acc, func -> func(acc) ?: return } // Track systems right at startup since they are likely going to tick very soon anyway, and we don't care about diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt index 8107baab2..d9790aa6f 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/Archetype.kt @@ -1,21 +1,13 @@ package com.mineinabyss.geary.engine.archetypes -import com.mineinabyss.geary.components.events.AddedComponent -import com.mineinabyss.geary.components.events.SetComponent -import com.mineinabyss.geary.components.events.UpdatedComponent -import com.mineinabyss.geary.modules.archetypes -import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.datatypes.* import com.mineinabyss.geary.datatypes.maps.CompId2ArchetypeMap -import com.mineinabyss.geary.datatypes.maps.Long2ObjectMap -import com.mineinabyss.geary.engine.Engine -import com.mineinabyss.geary.events.Handler import com.mineinabyss.geary.helpers.temporaryEntity import com.mineinabyss.geary.helpers.toGeary +import com.mineinabyss.geary.modules.archetypes +import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.query.GearyQuery -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized +import com.mineinabyss.geary.systems.accessors.RelationWithData /** * Archetypes store a list of entities with the same [EntityType], and provide functions to @@ -32,15 +24,11 @@ data class Archetype( private val archetypeProvider get() = archetypes.archetypeProvider private val eventRunner get() = archetypes.eventRunner - /** A mutex for anything which needs the size of ids to remain unchanged. */ - private val entityAddition = SynchronizedObject() - val entities: List get() = ids.map { it.toGeary() } + val entities: Sequence get() = ids.getEntities() /** The entity ids in this archetype. Indices are the same as [componentData]'s sub-lists. */ private val ids: IdList = IdList() - private val queuedRemoval = mutableListOf() - private val queueRemoval = SynchronizedObject() @PublishedApi internal var isIterating: Boolean = false @@ -62,17 +50,12 @@ data class Archetype( internal val relations = type.inner.mapNotNull { it.toRelation() } internal val relationsWithData = relations.filter { it.id.holdsData() } - /** Map of relation [Relation.target] id to a list of relations with that [Relation.target]. */ - internal val relationsByTarget: Long2ObjectMap> = relations - .groupBy { it.target.toLong() } - - /** Map of relation [Relation.kind] id to a list of relations with that [Relation.kind]. */ - internal val relationsByKind: Long2ObjectMap> = relations - .groupBy { it.kind.toLong() } + fun getRelationsByTarget(target: EntityId): List { + return relations.filter { it.target.toLong() == target.toLong() } + } - /** A map of component ids to index used internally in this archetype (ex. in [componentData])*/ - private val comp2indices: Map = buildMap { - dataHoldingType.forEachIndexed { i, compId -> put(compId.toLong(), i) } + fun getRelationsByKind(kind: ComponentId): List { + return relations.filter { it.kind.toLong() == kind.toLong() } } /** The amount of entities stored in this archetype. */ @@ -84,23 +67,20 @@ data class Archetype( private val _targetListeners = mutableSetOf() val targetListeners: Set = _targetListeners - private val _eventHandlers = mutableSetOf() - - //TODO update doc - /** A map of event class type to a set of event handlers which fire on that event. */ - val eventHandlers: Set = _eventHandlers + private val _eventListeners = mutableSetOf() + val eventListeners: Set = _eventListeners // ==== Helper functions ==== - fun getEntity(row: Int): Entity = synchronized(entityAddition) { + fun getEntity(row: Int): Entity { return ids[row].toGeary() } /** * Used to pack data closer together and avoid having hashmaps throughout the archetype. * - * @return The internally used index for this component [id]. + * @return The internally used index for this component [id], or 0 if not present. Use contains to check for presence. */ - internal fun indexOf(id: ComponentId): Int = comp2indices[id.toLong()] ?: -1 + fun indexOf(id: ComponentId): Int = dataHoldingType.indexOf(id) /** * @return The data under a [componentId] for an entity at [row]. @@ -118,12 +98,18 @@ data class Archetype( /** Returns the archetype associated with adding [componentId] to this archetype's [type]. */ operator fun plus(componentId: ComponentId): Archetype = - componentAddEdges[componentId] ?: archetypeProvider.getArchetype(type.plus(componentId)) + componentAddEdges.getOrSet(componentId) { + archetypeProvider.getArchetype( + if (componentId.hasRole(HOLDS_DATA)) + type + componentId.withoutRole(HOLDS_DATA) + componentId + else type + componentId + ) + } /** Returns the archetype associated with removing [componentId] to this archetype's [type]. */ operator fun minus(componentId: ComponentId): Archetype = - componentRemoveEdges[componentId] ?: archetypeProvider.getArchetype(type.minus(componentId)).also { - componentRemoveEdges[componentId] = it + componentRemoveEdges.getOrSet(componentId) { + archetypeProvider.getArchetype(type.minus(componentId)) } // ==== Entity mutation ==== @@ -134,20 +120,88 @@ data class Archetype( * * @return The new [Record] to be associated with this entity from now on. */ - internal fun addEntityWithData( + private fun moveWithNewComponent( + record: Record, + newComponent: Component, + newComponentId: ComponentId, + entity: EntityId, + ) = move(record, entity) { + val (oldArc, oldRow) = record + val newCompIndex = indexOf(newComponentId) + + // Add before new comp + for (i in 0 until newCompIndex) { + componentData[i].add(oldArc.componentData[i][oldRow]) + } + + // Add new comp + componentData[newCompIndex].add(newComponent) + + // Add after new comp + for (i in newCompIndex + 1..componentData.lastIndex) { + // Offset by one since this new comp didn't exist + componentData[i].add(oldArc.componentData[i - 1][oldRow]) + } + } + + private fun moveOnlyAdding( record: Record, - data: Array, - entity: Entity, - ) = synchronized(entityAddition) { - synchronized(record) { - ids.add(entity.id.toLong()) - componentData.forEachIndexed { i, compArray -> - compArray.add(data[i]) + entity: EntityId + ) = move(record, entity) { + val (oldArc, oldRow) = record + for (i in 0..componentData.lastIndex) { + componentData[i].add(oldArc.componentData[i][oldRow]) + } + } + + private fun moveWithoutComponent( + record: Record, + withoutComponentId: ComponentId, + entity: EntityId, + ) = move(record, entity) { + val (oldArc, oldRow) = record + val withoutCompIndex = oldArc.indexOf(withoutComponentId) + + // If removing a component that's added and not set, we just copy all data + if (withoutCompIndex == -1) { + for (i in 0..componentData.lastIndex) { + componentData[i].add(oldArc.componentData[i][oldRow]) } - record.row = -1 - record.archetype = this - record.row = ids.lastIndex + return@move } + + // Add before without comp + for (i in 0 until withoutCompIndex) { + componentData[i].add(oldArc.componentData[i][oldRow]) + } + + // Add after without comp + for (i in withoutCompIndex + 1..oldArc.componentData.lastIndex) { + componentData[i - 1].add(oldArc.componentData[i][oldRow]) + } + } + + internal fun createWithoutData(entity: Entity, existingRecord: Record) { + move(existingRecord, entity.id) {} + } + + internal fun createWithoutData(entity: Entity): Record { + ids.add(entity.id) + return Record(this, ids.lastIndex) + } + + internal inline fun move( + record: Record, + entity: EntityId, + copyData: () -> Unit + ) { + ids.add(entity) + + copyData() + + record.row = -1 + record.archetype = this + record.row = ids.lastIndex } // For the following few functions, both entity and row are passed to avoid doing several array look-ups @@ -168,13 +222,13 @@ data class Archetype( val moveTo = this + (componentId.withoutRole(HOLDS_DATA)) - val componentData = getComponents(record.row) - val entity = record.entity - removeEntity(record.row) - moveTo.addEntityWithData(record, componentData, entity) + val row = record.row + val entityId = ids[row] + moveTo.moveOnlyAdding(record, entityId) + removeEntity(row) if (callEvent) temporaryEntity { componentAddEvent -> - componentAddEvent.addRelation(componentId.toGeary(), noEvent = true) + componentAddEvent.addRelation(geary.components.addedComponent, componentId, noEvent = true) eventRunner.callEvent(record, records[componentAddEvent], null) } return true @@ -200,27 +254,36 @@ data class Archetype( if (addIndex != -1) { componentData[addIndex][row] = data if (callEvent) temporaryEntity { componentAddEvent -> - componentAddEvent.addRelation(componentId.toGeary(), noEvent = true) + componentAddEvent.addRelation(geary.components.updatedComponent, componentId, noEvent = true) eventRunner.callEvent(record, records[componentAddEvent], null) } return false } //if component is not already added, add it, then set - val moveTo = - if (contains(dataComponent.withoutRole(HOLDS_DATA))) - this + dataComponent - else this + dataComponent.withoutRole(HOLDS_DATA) + dataComponent - val newCompIndex = moveTo.dataHoldingType.indexOf(dataComponent) - val componentData = getComponents(row, add = data to newCompIndex) - - val entity = record.entity + val entityId = ids[row] + val moveTo = this + dataComponent + moveTo.moveWithNewComponent(record, data, dataComponent, entityId) removeEntity(row) - moveTo.addEntityWithData(record, componentData, entity) - if (callEvent) temporaryEntity { componentAddEvent -> - componentAddEvent.addRelation(componentId.toGeary(), noEvent = true) - eventRunner.callEvent(record, records[componentAddEvent], null) + if (callEvent && moveTo.targetListeners.isNotEmpty()) { + // Archetype for the set event + val eventArc = archetypeProvider.getArchetype( + GearyEntityType( + ulongArrayOf( + Relation.of( + geary.components.setComponent, + componentId + ).id + ) + ) + ) + if (eventArc.eventListeners.isNotEmpty()) { + temporaryEntity { componentAddEvent -> + componentAddEvent.addRelation(geary.components.setComponent, componentId, noEvent = true) + eventRunner.callEvent(record, records[componentAddEvent], null) + } + } } return true } @@ -233,45 +296,34 @@ data class Archetype( internal fun removeComponent( record: Record, component: ComponentId - ): Boolean = synchronized(record) { + ): Boolean { with(record.archetype) { val row = record.row + val entityId = ids[row] - if (component !in type) return@synchronized false + if (component !in type) return false val moveTo = this - component - val skipData = indexOf(component) - val copiedData = - if (component.holdsData()) - (Array((componentData.size - 1).coerceAtLeast(0)) {}).also { data -> - for (i in 0 until skipData) data[i] = componentData[i][row] - for (i in (skipData + 1)..componentData.lastIndex) data[i - 1] = componentData[i][row] - } - else (Array(componentData.size) {}).also { data -> - for (i in 0..componentData.lastIndex) data[i] = componentData[i][row] - } - val entity = record.entity + moveTo.moveWithoutComponent(record, component, entityId) removeEntity(row) - moveTo.addEntityWithData(record, copiedData, entity) } - return@synchronized true + return true } /** Gets all the components associated with an entity at a [row]. */ - internal fun getComponents(row: Int, add: Pair? = null): Array = - synchronized(entityAddition) { - if (add != null) { - val arr = Array(componentData.size + 1) { null } - val (addElement, addIndex) = add - for (i in 0 until addIndex) arr[i] = componentData[i][row] - arr[addIndex] = addElement - for (i in addIndex..componentData.lastIndex) arr[i + 1] = componentData[i][row] - @Suppress("UNCHECKED_CAST") // For loop above ensures no nulls - return arr as Array - } else - return Array(componentData.size) { i: Int -> componentData[i][row] } - } + internal fun getComponents(row: Int, add: Pair? = null): Array { + if (add != null) { + val arr = Array(componentData.size + 1) { null } + val (addElement, addIndex) = add + for (i in 0 until addIndex) arr[i] = componentData[i][row] + arr[addIndex] = addElement + for (i in addIndex..componentData.lastIndex) arr[i + 1] = componentData[i][row] + @Suppress("UNCHECKED_CAST") // For loop above ensures no nulls + return arr as Array + } else + return Array(componentData.size) { i: Int -> componentData[i][row] } + } /** * Queries for specific relations or by kind/target. @@ -288,21 +340,30 @@ data class Archetype( val specificTarget = target and ENTITY_MASK != geary.components.any return when { specificKind && specificTarget -> listOf(Relation.of(kind, target)) - specificTarget -> relationsByTarget[target.toLong()] - specificKind -> relationsByKind[kind.toLong()] + specificTarget -> getRelationsByTarget(target) + specificKind -> getRelationsByKind(kind) else -> relations - }?.run { //TODO this technically doesnt need to run when specificKind is set + }.run { //TODO this technically doesnt need to run when specificKind is set if (kind.hasRole(HOLDS_DATA)) filter { it.hasRole(HOLDS_DATA) } else this - }?.run { + }.run { if (target.holdsData()) filter { it.target.withRole(HOLDS_DATA) in type } else this - } ?: emptyList() + } } -// internal fun scheduleRemoveRow(row: Int) { -// synchronized(queueRemoval) { -// queuedRemoval.add(row) -// } -// } + internal fun readRelationDataFor( + row: Int, + kind: ComponentId, + target: EntityId, + relations: List + ): List> { + return relations.map { relation -> + RelationWithData( + data = if (kind.hasRole(HOLDS_DATA)) this[row, relation.id] else null, + targetData = if (target.hasRole(HOLDS_DATA)) this[row, relation.target.withRole(HOLDS_DATA)] else null, + relation = relation + ) + } + } /** * Removes the entity at a [row] in this archetype, notifying running archetype iterators. @@ -331,8 +392,8 @@ data class Archetype( // ==== Event listeners ==== /** Adds an event [handler] that listens to certain events relating to entities in this archetype. */ - fun addEventHandler(handler: Handler) { - _eventHandlers += handler + fun addEventListener(handler: Listener) { + _eventListeners += handler } fun addSourceListener(handler: Listener) { @@ -342,32 +403,4 @@ data class Archetype( fun addTargetListener(handler: Listener) { _targetListeners += handler } - - // ==== Iterators ==== - -// /** Stops tracking a running [iterator]. */ -// internal fun finalizeIterator(iterator: ArchetypeIterator) { -// runningIterators.remove(iterator) -// } - - /** Creates and tracks an [ArchetypeIterator] for a query. */ - @PublishedApi - internal fun iteratorFor(query: GearyQuery): ArchetypeIterator { - return ArchetypeIterator(this, query) - } - -// /** Removes any queued up entity deletions. */ -// @PublishedApi -// internal fun cleanup() { -// synchronized(queueRemoval) { -// if (!isIterating) -// queuedRemoval.sort() -// // Since the rows were added in order while iterating, the list is always sorted, -// // so we don't worry about moving rows -// while (queuedRemoval.isNotEmpty()) { -// val last = queuedRemoval.removeLast() -// removeEntity(last) -// } -// } -// } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEngine.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEngine.kt index 6e490c7a3..4cb928452 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEngine.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEngine.kt @@ -24,8 +24,8 @@ open class ArchetypeEngine(override val tickDuration: Duration) : TickingEngine( (CoroutineScope(Dispatchers.Default) + CoroutineName("Geary Engine")).coroutineContext /** Describes how to individually tick each system */ - protected open suspend fun RepeatingSystem.runSystem() { - doTick() + protected open fun RepeatingSystem.runSystem() { + tickAll() } override fun scheduleSystemTicking() { @@ -38,23 +38,18 @@ open class ArchetypeEngine(override val tickDuration: Duration) : TickingEngine( } } - override suspend fun tick(currentTick: Long): Unit = coroutineScope { + override fun tick(currentTick: Long) { // Create a job but don't start it - val tickJob = launch(start = CoroutineStart.LAZY) { - pipeline.getRepeatingInExecutionOrder() - .filter { currentTick % (it.interval / tickDuration).toInt().coerceAtLeast(1) == 0L } - .also { logger.v("Ticking engine with systems $it") } - .forEach { system -> - runCatching { - system.runSystem() - }.onFailure { - logger.e("Error while running system ${system::class.simpleName}") - it.printStackTrace() - } + pipeline.getRepeatingInExecutionOrder() + .filter { currentTick % (it.interval / tickDuration).toInt().coerceAtLeast(1) == 0L } + .also { logger.v("Ticking engine with systems $it") } + .forEach { system -> + runCatching { + system.runSystem() + }.onFailure { + logger.e("Error while running system ${system::class.simpleName}") + it.printStackTrace() } - } - - // Tick all systems - tickJob.join() + } } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEventRunner.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEventRunner.kt index 09859f5a5..2704f6660 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEventRunner.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeEventRunner.kt @@ -1,12 +1,13 @@ package com.mineinabyss.geary.engine.archetypes -import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.datatypes.Entity import com.mineinabyss.geary.datatypes.Record +import com.mineinabyss.geary.datatypes.RecordPointer +import com.mineinabyss.geary.datatypes.Records import com.mineinabyss.geary.datatypes.maps.TypeMap import com.mineinabyss.geary.engine.EventRunner -import com.mineinabyss.geary.helpers.contains -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope +import com.mineinabyss.geary.modules.archetypes +import com.mineinabyss.geary.systems.Listener class ArchetypeEventRunner : EventRunner { private val records: TypeMap get() = archetypes.records @@ -20,49 +21,20 @@ class ArchetypeEventRunner : EventRunner { val origTargetArc = target.archetype val origSourceArc = source?.archetype - //TODO performance upgrade will come when we figure out a solution in QueryManager as well. - for (handler in origEventArc.eventHandlers) { - // If an event handler has moved the entity to a new archetype, make sure we follow it. - val (targetArc, targetRow) = target - val (eventArc, eventRow) = event - val sourceArc = source?.archetype - val sourceRow = source?.row - - // If there's no source but the handler needs a source, skip - if (source == null && !handler.parentListener.source.isEmpty) continue - - // Check that this handler has a listener associated with it. - if (!handler.parentListener.target.isEmpty && handler.parentListener !in targetArc.targetListeners) continue - if (sourceArc != null && !handler.parentListener.source.isEmpty && handler.parentListener !in sourceArc.sourceListeners) continue - - // Check that we still match the data if archetype of any involved entities changed. - if (targetArc != origTargetArc && targetArc.type !in handler.parentListener.target.family) continue - if (eventArc != origEventArc && eventArc.type !in handler.parentListener.event.family) continue - if (sourceArc != origSourceArc && eventArc.type !in handler.parentListener.source.family) continue + // triple intersection of listeners + val listeners: Set = origTargetArc.targetListeners.toMutableSet().apply { + retainAll(origEventArc.eventListeners) + retainAll { it.source.isEmpty || (origSourceArc != null && it in origSourceArc.sourceListeners) } + } - val listenerName = handler.parentListener::class.simpleName - val targetScope = runCatching { - RawAccessorDataScope( - archetype = targetArc, - perArchetypeData = handler.parentListener.target.cacheForArchetype(targetArc), - row = targetRow, - ) - }.getOrElse { throw IllegalStateException("Failed while reading target scope on $listenerName", it) } - val eventScope = runCatching { - RawAccessorDataScope( - archetype = eventArc, - perArchetypeData = handler.parentListener.event.cacheForArchetype(eventArc), - row = eventRow, - ) - }.getOrElse { throw IllegalStateException("Failed while reading event scope on $listenerName", it) } - val sourceScope = if (source == null) null else runCatching { - RawAccessorDataScope( - archetype = sourceArc!!, - perArchetypeData = handler.parentListener.source.cacheForArchetype(sourceArc), - row = sourceRow!!, - ) - }.getOrElse { throw IllegalStateException("Failed while reading source scope on $listenerName", it) } - handler.processAndHandle(sourceScope, targetScope, eventScope) + for (listener in listeners) { + val pointers: Records = when (source) { + null -> Records(RecordPointer(target), RecordPointer(event), null) + else -> Records(RecordPointer(target), RecordPointer(event), RecordPointer(source)) + } + with(listener) { + pointers.handle() + } } } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeIterator.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeIterator.kt deleted file mode 100644 index f6a3d31f6..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeIterator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.mineinabyss.geary.engine.archetypes - -import com.mineinabyss.geary.systems.accessors.AccessorHolder -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope -import com.mineinabyss.geary.systems.accessors.TargetScope - -@PublishedApi -internal data class ArchetypeIterator( - val archetype: Archetype, - val holder: AccessorHolder, -) { - val perArchCache = holder.cacheForArchetype(archetype) - var row: Int = 0 - - inline fun forEach(upTo: Int, crossinline run: (TargetScope) -> Unit) { - while (row < archetype.size && row <= upTo) { - val dataScope = RawAccessorDataScope( - archetype = archetype, - row = row++, - perArchetypeData = perArchCache - ) - holder.forEachCombination(dataScope) { data -> - run( - TargetScope( - entity = dataScope.entity, - data = data - ) - ) - } - } - //FIXME clean up removed components - } -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeQueryManager.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeQueryManager.kt index b45fe99c1..285a17d0b 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeQueryManager.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ArchetypeQueryManager.kt @@ -4,7 +4,6 @@ import com.mineinabyss.geary.datatypes.Entity import com.mineinabyss.geary.datatypes.family.Family import com.mineinabyss.geary.datatypes.maps.Family2ObjectArrayMap import com.mineinabyss.geary.engine.QueryManager -import com.mineinabyss.geary.events.Handler import com.mineinabyss.geary.helpers.contains import com.mineinabyss.geary.systems.Listener import com.mineinabyss.geary.systems.query.GearyQuery @@ -13,18 +12,26 @@ class ArchetypeQueryManager : QueryManager { private val queries = mutableListOf() private val sourceListeners = mutableListOf() private val targetListeners = mutableListOf() - private val eventHandlers = mutableListOf() + private val eventListeners = mutableListOf() private val archetypes = Family2ObjectArrayMap() override fun trackEventListener(listener: Listener) { - com.mineinabyss.geary.systems.trackEventListener( - listener, - sourceListeners, - targetListeners, - archetypes, - eventHandlers - ) + val eventFamilyMatch = archetypes.match(listener.event.family) + for (archetype in eventFamilyMatch) archetype.addEventListener(listener) + eventListeners.add(listener) + + // Only start tracking a listener for the parts it actually cares for + if (!listener.source.isEmpty) { + val sourcesMatched = archetypes.match(listener.source.family) + for (archetype in sourcesMatched) archetype.addSourceListener(listener) + sourceListeners.add(listener) + } + if (!listener.target.isEmpty) { + val targetsMatched = archetypes.match(listener.target.family) + for (archetype in targetsMatched) archetype.addTargetListener(listener) + targetListeners.add(listener) + } } override fun trackQuery(query: GearyQuery) { @@ -40,11 +47,11 @@ class ArchetypeQueryManager : QueryManager { val matched = queries.filter { archetype.type in it.family } val matchedSources = sourceListeners.filter { archetype.type in it.source.family } val matchedTargets = targetListeners.filter { archetype.type in it.target.family } - val matchedHandlers = eventHandlers.filter { archetype.type in it.parentListener.event.family } + val matchedEvents = eventListeners.filter { archetype.type in it.event.family } matchedSources.forEach { archetype.addSourceListener(it) } matchedTargets.forEach { archetype.addTargetListener(it) } - matchedHandlers.forEach { archetype.addEventHandler(it) } + matchedEvents.forEach { archetype.addEventListener(it) } matched.forEach { it.matchedArchetypes += archetype } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ComponentAsEntityProvider.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ComponentAsEntityProvider.kt index 45c7d40a6..5cc8d1013 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ComponentAsEntityProvider.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/ComponentAsEntityProvider.kt @@ -1,10 +1,10 @@ package com.mineinabyss.geary.engine.archetypes import com.mineinabyss.geary.components.ComponentInfo -import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.datatypes.ComponentId import com.mineinabyss.geary.datatypes.maps.ClassToComponentMap import com.mineinabyss.geary.engine.ComponentProvider +import com.mineinabyss.geary.modules.geary import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized import kotlin.reflect.KClass @@ -33,7 +33,8 @@ class ComponentAsEntityProvider : ComponentProvider { private fun registerComponentIdForClass(kClass: KClass<*>): ComponentId { logger.v("Registering new component: ${kClass.simpleName}") - val compEntity = entityProvider.create(initialComponents = listOf(ComponentInfo(kClass))) + val compEntity = entityProvider.create() + compEntity.set(ComponentInfo(kClass), noEvent = true) classToComponentMap[kClass] = compEntity.id return compEntity.id } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/EntityByArchetypeProvider.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/EntityByArchetypeProvider.kt index 67efa15e2..56a49ff9c 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/EntityByArchetypeProvider.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/EntityByArchetypeProvider.kt @@ -1,44 +1,43 @@ package com.mineinabyss.geary.engine.archetypes -import com.mineinabyss.geary.components.CouldHaveChildren -import com.mineinabyss.geary.components.events.EntityRemoved -import com.mineinabyss.geary.components.events.SuppressRemoveEvent -import com.mineinabyss.geary.datatypes.* +import com.mineinabyss.geary.datatypes.Entity +import com.mineinabyss.geary.datatypes.EntityStack +import com.mineinabyss.geary.datatypes.EntityType +import com.mineinabyss.geary.datatypes.GearyEntity import com.mineinabyss.geary.datatypes.maps.TypeMap import com.mineinabyss.geary.engine.EntityProvider -import com.mineinabyss.geary.helpers.componentId import com.mineinabyss.geary.helpers.parents import com.mineinabyss.geary.helpers.removeParent import com.mineinabyss.geary.helpers.toGeary import com.mineinabyss.geary.modules.archetypes +import com.mineinabyss.geary.modules.geary import kotlinx.atomicfu.atomic class EntityByArchetypeProvider( private val reuseIDsAfterRemoval: Boolean = true, ) : EntityProvider { - private val records: TypeMap get() = archetypes.records - private val archetypeProvider: ArchetypeProvider get() = archetypes.archetypeProvider + private val records: TypeMap by lazy { archetypes.records } + private val archetypeProvider: ArchetypeProvider by lazy { archetypes.archetypeProvider } private val removedEntities: EntityStack = EntityStack() private val currId = atomic(0L) - override fun create(initialComponents: Collection): GearyEntity { + override fun create(): GearyEntity { val entity: GearyEntity = if (reuseIDsAfterRemoval) { - runCatching { removedEntities.pop() } - .getOrElse { currId.getAndIncrement().toGeary() } + removedEntities.pop() ?: currId.getAndIncrement().toGeary() } else currId.getAndIncrement().toGeary() - createRecord(entity, initialComponents) + createRecord(entity) return entity } override fun remove(entity: Entity) { - if (!entity.has()) entity.callEvent { - add() + if (!entity.has(geary.components.suppressRemoveEvent)) entity.callEvent { + add(geary.components.entityRemoved) } // remove all children of this entity from the ECS as well - if (entity.has()) entity.apply { + if (entity.has(geary.components.couldHaveChildren)) entity.apply { children.forEach { // Remove self from the child's parents or remove the child if it no longer has parents if (it.parents == setOf(this)) it.removeEntity() @@ -55,18 +54,9 @@ class EntityByArchetypeProvider( override fun getType(entity: Entity): EntityType = records[entity].archetype.type - private fun createRecord(entity: Entity, initialComponents: Collection) { - val ids = - initialComponents.map { componentId(it::class) } + - initialComponents.map { componentId(it::class) or HOLDS_DATA } - - val addTo = archetypeProvider.getArchetype(EntityType(ids)) - val record = Record(archetypeProvider.rootArchetype, -1) - addTo.addEntityWithData( - record, - initialComponents.toTypedArray().apply { sortBy { addTo.indexOf(componentId(it::class)) } }, - entity, - ) - records[entity] = record + private fun createRecord(entity: Entity) { + val root = archetypeProvider.rootArchetype + val createdRecord = root.createWithoutData(entity) + records[entity] = createdRecord } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/SimpleArchetypeProvider.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/SimpleArchetypeProvider.kt index 3fe35f614..d0687a896 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/SimpleArchetypeProvider.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/SimpleArchetypeProvider.kt @@ -1,8 +1,8 @@ package com.mineinabyss.geary.engine.archetypes -import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.datatypes.ComponentId import com.mineinabyss.geary.datatypes.EntityType +import com.mineinabyss.geary.modules.archetypes import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeMutateOperations.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeMutateOperations.kt index b6330745e..7d23ddd40 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeMutateOperations.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeMutateOperations.kt @@ -1,10 +1,10 @@ package com.mineinabyss.geary.engine.archetypes.operations -import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.datatypes.* import com.mineinabyss.geary.datatypes.maps.TypeMap import com.mineinabyss.geary.engine.EntityMutateOperations import com.mineinabyss.geary.engine.archetypes.ArchetypeProvider +import com.mineinabyss.geary.modules.archetypes class ArchetypeMutateOperations : EntityMutateOperations { private val records: TypeMap get() = archetypes.records @@ -42,8 +42,8 @@ class ArchetypeMutateOperations : EntityMutateOperations { } override fun clearEntity(entity: Entity) { - val rec = records[entity] - rec.archetype.removeEntity(rec.row) - archetypeProvider.rootArchetype.addEntityWithData(rec, arrayOf(), entity) + val record = records[entity] + record.archetype.removeEntity(record.row) + archetypeProvider.rootArchetype.createWithoutData(entity, record) } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeReadOperations.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeReadOperations.kt index 63ea08834..8f5bb1a25 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeReadOperations.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/engine/archetypes/operations/ArchetypeReadOperations.kt @@ -1,9 +1,9 @@ package com.mineinabyss.geary.engine.archetypes.operations -import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.datatypes.* import com.mineinabyss.geary.datatypes.maps.TypeMap import com.mineinabyss.geary.engine.EntityReadOperations +import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.systems.accessors.RelationWithData class ArchetypeReadOperations : EntityReadOperations { @@ -30,13 +30,8 @@ class ArchetypeReadOperations : EntityReadOperations { target: EntityId, ): List> { val (arc, row) = records[entity] - return arc.getRelations(kind, target).map { relation -> - RelationWithData( - data = if (kind.hasRole(HOLDS_DATA)) arc[row, relation.id] else null, - targetData = if (target.hasRole(HOLDS_DATA)) arc[row, relation.target.withRole(HOLDS_DATA)] else null, - relation = relation - ) - } + + return arc.readRelationDataFor(row, kind, target, arc.getRelations(kind, target)) } override fun getRelationsFor(entity: Entity, kind: ComponentId, target: EntityId): List = diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/CheckHandler.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/CheckHandler.kt deleted file mode 100644 index 656023136..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/CheckHandler.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.mineinabyss.geary.events - -import com.mineinabyss.geary.components.RequestCheck -import com.mineinabyss.geary.components.events.FailedCheck -import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.SourceScope -import com.mineinabyss.geary.systems.accessors.TargetScope - -/** - * A handler which will run a check on an event that requests one. - */ -abstract class CheckHandler( - parentListener: Listener, - sourceNullable: Boolean -) : Handler(parentListener, sourceNullable) { - init { - parentListener.event._family.has() - } - - abstract fun check(source: SourceScope?, target: TargetScope, event: EventScope): Boolean - - override fun handle(source: SourceScope?, target: TargetScope, event: EventScope) { - if (!check(source, target, event)) event.entity.apply { - remove() - add() - } - } -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/CheckingListener.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/CheckingListener.kt new file mode 100644 index 000000000..17407836e --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/CheckingListener.kt @@ -0,0 +1,26 @@ +package com.mineinabyss.geary.events + +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.components.RequestCheck +import com.mineinabyss.geary.components.events.FailedCheck +import com.mineinabyss.geary.systems.Listener +import com.mineinabyss.geary.systems.accessors.Pointers + +/** + * A listener that runs a check on matched events, adding [FailedCheck] to the event when the check fails. + */ +abstract class CheckingListener() : Listener() { + init { + event.mutableFamily.has() + } + + abstract fun Pointers.check(): Boolean + + @OptIn(UnsafeAccessors::class) + override fun Pointers.handle() { + if (!check()) event.entity.apply { + remove() + add() + } + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/Handler.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/Handler.kt deleted file mode 100644 index 854893175..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/Handler.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.mineinabyss.geary.events - -import com.mineinabyss.geary.modules.geary -import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope -import com.mineinabyss.geary.systems.accessors.SourceScope -import com.mineinabyss.geary.systems.accessors.TargetScope - -/** - * Generated within a [Listener]. Will handle events matching specified components on source/target/event entities. - */ -abstract class Handler( - val parentListener: Listener, - val sourceNullable: Boolean, -) { - private val logger get() = geary.logger - - /** Runs when a matching event is fired. */ - abstract fun handle(source: SourceScope?, target: TargetScope, event: EventScope) - - /** Reads necessary data and iterates over combinations as appropriate, calling the [handle] function on each. */ - open fun processAndHandle( - sourceScope: RawAccessorDataScope?, - targetScope: RawAccessorDataScope, - eventScope: RawAccessorDataScope, - ) { - if (!sourceNullable && sourceScope == null) return - try { - // Handle all combinations of data as needed - parentListener.event.forEachCombination(eventScope) { eventData -> - parentListener.target.forEachCombination(targetScope) { targetData -> - val eventResult = EventScope(eventScope.entity, eventData) - val targetResult = TargetScope(targetScope.entity, targetData) - if (sourceScope != null) { - parentListener.source.forEachCombination(sourceScope) { sourceData -> - val sourceResult = SourceScope(sourceScope.entity, sourceData) - handle(sourceResult, targetResult, eventResult) - } - } else handle(null, targetResult, eventResult) - } - } - } catch (e: Exception) { - logger.e("Failed to run event ${parentListener::class.simpleName}") - e.printStackTrace() - } - } -} - - diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/HandlerGearyAliases.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/HandlerGearyAliases.kt deleted file mode 100644 index b9d5941e0..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/events/HandlerGearyAliases.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.mineinabyss.geary.events - -typealias GearyHandler = Handler -typealias GearyCheckHandler = CheckHandler diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/helpers/EngineHelpers.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/helpers/EngineHelpers.kt index c8749c3de..cf5877967 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/helpers/EngineHelpers.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/helpers/EngineHelpers.kt @@ -1,9 +1,8 @@ package com.mineinabyss.geary.helpers import com.mineinabyss.geary.components.ComponentInfo -import com.mineinabyss.geary.components.events.SuppressRemoveEvent -import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.datatypes.* +import com.mineinabyss.geary.modules.geary import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -19,7 +18,7 @@ inline fun temporaryEntity( run: (Entity) -> T ): T { val entity = entity { - add(noEvent = true) + add(geary.components.suppressRemoveEvent, noEvent = true) } return try { run(entity) @@ -56,12 +55,3 @@ fun componentId(kClass: KClass): Nothing = /** Gets the [ComponentInfo] component from a component's id. */ fun ComponentId.getComponentInfo(): ComponentInfo? = this.toGeary().get() -//@ExperimentalAsyncGearyAPI -//public inline fun runSafely( -// scope: CoroutineScope = globalContext.engine, -// crossinline run: suspend () -> T -//): Deferred { -// val deferred = globalContext.engine.async(start = CoroutineStart.LAZY) { run() } -// globalContext.engine.runSafely(scope, deferred) -// return deferred -//} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/ArchetypeEngineModule.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/ArchetypeEngineModule.kt index 7db5b1f35..4a8da2c04 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/ArchetypeEngineModule.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/ArchetypeEngineModule.kt @@ -2,7 +2,7 @@ package com.mineinabyss.geary.modules import co.touchlab.kermit.Logger import com.mineinabyss.geary.addons.GearyPhase -import com.mineinabyss.geary.datatypes.maps.HashTypeMap +import com.mineinabyss.geary.datatypes.maps.ArrayTypeMap import com.mineinabyss.geary.engine.Components import com.mineinabyss.geary.engine.PipelineImpl import com.mineinabyss.geary.engine.archetypes.* @@ -28,8 +28,9 @@ open class ArchetypeEngineModule( override val write = ArchetypeMutateOperations() override val entityProvider = EntityByArchetypeProvider() override val componentProvider = ComponentAsEntityProvider() + override val defaults: Defaults = Defaults() - val records = HashTypeMap() + val records = ArrayTypeMap() val archetypeProvider = SimpleArchetypeProvider() override val components by lazy { Components() } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/Defaults.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/Defaults.kt new file mode 100644 index 000000000..8c6db1774 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/Defaults.kt @@ -0,0 +1,8 @@ +package com.mineinabyss.geary.modules + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +class Defaults( + val repeatingSystemInterval: Duration = 50.milliseconds, +) diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyConfiguration.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyConfiguration.kt index 5cfcd7938..c93c82cdc 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyConfiguration.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyConfiguration.kt @@ -43,6 +43,6 @@ class GearyConfiguration { * ``` */ fun on(phase: GearyPhase, run: () -> Unit) { - geary.pipeline.intercept(phase, run) + geary.pipeline.runOnOrAfter(phase, run) } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyModule.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyModule.kt index 11bf57f35..63712e924 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyModule.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/GearyModule.kt @@ -41,6 +41,8 @@ interface GearyModule { val eventRunner: EventRunner val pipeline: Pipeline + val defaults: Defaults + operator fun invoke(configure: GearyConfiguration.() -> Unit) { GearyConfiguration().apply(configure) } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/TestEngineModule.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/TestEngineModule.kt index 3c7c28a8a..e99fc956d 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/TestEngineModule.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/modules/TestEngineModule.kt @@ -10,10 +10,10 @@ import com.mineinabyss.geary.engine.archetypes.EntityByArchetypeProvider */ class TestEngineModule( reuseIDsAfterRemoval: Boolean = true, -): ArchetypeEngineModule() { +) : ArchetypeEngineModule() { override val entityProvider = EntityByArchetypeProvider(reuseIDsAfterRemoval) - companion object: GearyModuleProviderWithDefault { + companion object : GearyModuleProviderWithDefault { override fun init(module: TestEngineModule) { ArchetypeEngineModule.init(module) } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/Listener.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/Listener.kt index 36693a187..a516c752a 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/Listener.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/Listener.kt @@ -1,14 +1,8 @@ package com.mineinabyss.geary.systems -import com.mineinabyss.geary.datatypes.Component -import com.mineinabyss.geary.datatypes.family.Family -import com.mineinabyss.geary.datatypes.family.family -import com.mineinabyss.geary.events.Handler import com.mineinabyss.geary.helpers.componentId import com.mineinabyss.geary.systems.accessors.* -import com.mineinabyss.geary.systems.accessors.types.ComponentAccessor -import com.mineinabyss.geary.systems.accessors.types.DirectAccessor -import kotlin.reflect.KProperty +import com.mineinabyss.geary.systems.accessors.type.ComponentAccessor /** * #### [Guide: Listeners](https://wiki.mineinabyss.com/geary/guide/listeners) @@ -18,58 +12,52 @@ import kotlin.reflect.KProperty * [Handler]s can be defined inside by annotating a function with [Handler], these * are the actual functions that run when a matching event is found. */ -abstract class Listener : AccessorOperations(), GearySystem, AccessorScopeSelector { - val source: AccessorHolder = AccessorHolder() +abstract class Listener : AccessorOperations(), System { val target: AccessorHolder = AccessorHolder() val event: AccessorHolder = AccessorHolder() + val source: AccessorHolder = AccessorHolder() fun start() { onStart() } - operator fun Accessor.getValue(thisRef: SourceScope, property: KProperty<*>): T = access(thisRef) - operator fun Accessor.getValue(thisRef: TargetScope, property: KProperty<*>): T = access(thisRef) - operator fun Accessor.getValue(thisRef: EventScope, property: KProperty<*>): T = access(thisRef) - - fun AccessorBuilder>.onSource(): ComponentAccessor = - source.addAccessor { build(source, it) } - - fun AccessorBuilder>.onTarget(): ComponentAccessor = - target.addAccessor { build(target, it) } - - fun AccessorBuilder>.onEvent(): ComponentAccessor = - event.addAccessor { build(event, it) } - - fun Family.onSource(): DirectAccessor = - source._family.add(this).let { DirectAccessor(this) } + private fun getIndexForHolder(holder: AccessorHolder): Int= when(holder) { + target -> 0 + event -> 1 + source -> 2 + else -> error("Holder is not a part of this listener: $holder") + } - fun Family.onTarget(): DirectAccessor = - target._family.add(this).let { DirectAccessor(this) } + fun , A> T.on(holder: AccessorHolder): ReadOnlyEntitySelectingAccessor { + val index = getIndexForHolder(holder) + if (this is FamilyMatching) this.family?.let { holder.mutableFamily.add(it) } + return ReadOnlyEntitySelectingAccessor(this, index) + } - fun Family.onEvent(): DirectAccessor = - event._family.add(this).let { DirectAccessor(this) } + fun , A> T.on(holder: AccessorHolder): ReadWriteEntitySelectingAccessor { + val index = getIndexForHolder(holder) + if (this is FamilyMatching) this.family?.let { holder.mutableFamily.add(it) } + return ReadWriteEntitySelectingAccessor(this, index) + } /** Fires when an entity has a component of type [T] set or updated. */ - inline fun onSet(): AccessorBuilder> { - return AccessorBuilder { holder, index -> - event._family.onSet(componentId()) - get().build(holder, index) - } + inline fun , reified A> T.whenSetOnTarget(): ReadWriteEntitySelectingAccessor { + event.mutableFamily.onSet(componentId()) + return this.on(target) } /** Fires when an entity has a component of type [T] set, only if it was not set before. */ - inline fun onFirstSet(): AccessorBuilder> { - return AccessorBuilder { holder, index -> - event._family.onFirstSet(componentId()) - get().build(holder, index) - } + inline fun , reified A> T.whenFirstSetOnTarget(): ReadWriteEntitySelectingAccessor { + event.mutableFamily.onFirstSet(componentId()) + return this.on(target) } - //TODO support onAdd for relations /** Fires when an entity has a component of type [T] added, updates are not considered since no data changes. */ - inline fun onAdd(): Family { - event._family.onAdd(componentId()) - return family { has() } + inline fun , reified A> T.whenAddedOnTarget(): ReadWriteEntitySelectingAccessor { + event.mutableFamily.onAdd(componentId()) + return this.on(event) } + + abstract fun Pointers.handle() } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/RepeatingSystem.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/RepeatingSystem.kt index 36161c8e8..cfbf0d887 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/RepeatingSystem.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/RepeatingSystem.kt @@ -1,10 +1,9 @@ package com.mineinabyss.geary.systems -import com.mineinabyss.geary.engine.archetypes.ArchetypeIterator -import com.mineinabyss.geary.systems.accessors.TargetScope -import com.mineinabyss.geary.systems.query.GearyQuery +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.query.Query import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds /** * #### [Guide: Ticking systems](https://wiki.mineinabyss.com/geary/guide/ticking-systems) @@ -12,34 +11,15 @@ import kotlin.time.Duration.Companion.milliseconds * A system for the ECS that will run every [interval] ticks. * * @param interval How often to run this system in ticks. - * - * @see [ArchetypeIterator] */ abstract class RepeatingSystem( - val interval: Duration = 50.milliseconds // 1 tick in Minecraft -) : GearyQuery(), GearySystem { - protected var iteration: Int = 0 - private set - + val interval: Duration = geary.defaults.repeatingSystemInterval +) : Query(), System { override fun onStart() {} - //TODO better name differentiation between this and tick - fun doTick() { - iteration++ - tick() - } - - protected open fun tick() { - fastForEach(run = { it.tick() }) + open fun tickAll() { + forEach(run = { it.tick() }) } - protected open fun TargetScope.tick() {} - - protected fun every(iterations: Int): Boolean = - iteration.mod(iterations) == 0 - - protected inline fun every(iterations: Int, run: () -> T): T? { - if (every(iterations)) return run() - return null - } + protected open fun Pointer.tick() {} } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/TrackEventListener.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/TrackEventListener.kt deleted file mode 100644 index 333c5616a..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/TrackEventListener.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mineinabyss.geary.systems - -import com.mineinabyss.geary.datatypes.maps.Family2ObjectArrayMap -import com.mineinabyss.geary.engine.archetypes.Archetype -import com.mineinabyss.geary.events.Handler - -internal expect fun trackEventListener( - listener: Listener, - sourceListeners: MutableList, - targetListeners: MutableList, - archetypes: Family2ObjectArrayMap, - eventHandlers: MutableList, -) diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/Accessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/Accessor.kt deleted file mode 100644 index 86692ee0f..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/Accessor.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -interface Accessor { - fun access(scope: ResultScope): T -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorBuilder.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorBuilder.kt deleted file mode 100644 index 9cc5475f0..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorBuilder.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -/** - * A builder that can provide an accessor for [AccessorHolder]s. - * - * @see Accessor - */ -fun interface AccessorBuilder> { - //TODO we shouldn't expose index here since not all Accessors have one - /** Provides an [Accessor] to a [holder] and [index] this accessor should be placed in for that holder. */ - fun build(holder: AccessorHolder, index: Int): T -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolder.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolder.kt index 454cd93c8..32c0e7e50 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolder.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolder.kt @@ -2,12 +2,6 @@ package com.mineinabyss.geary.systems.accessors import com.mineinabyss.geary.datatypes.family.Family import com.mineinabyss.geary.datatypes.family.MutableFamily -import com.mineinabyss.geary.engine.archetypes.Archetype -import com.mineinabyss.geary.systems.accessors.types.DirectAccessor -import com.mineinabyss.geary.systems.accessors.types.IndexedAccessor -import com.soywiz.kds.FastIntMap -import com.soywiz.kds.getOrPut -import kotlin.reflect.KProperty /** @@ -15,56 +9,12 @@ import kotlin.reflect.KProperty * * @property family A lazily built immutable family that represents all data this holder needs to function. */ -open class AccessorHolder: AccessorOperations() { - val family: Family.Selector.And get() = _family +open class AccessorHolder : AccessorOperations() { + val family: Family.Selector.And get() = mutableFamily @PublishedApi - internal val _family: MutableFamily.Selector.And = MutableFamily.Selector.And() + internal val mutableFamily: MutableFamily.Selector.And = MutableFamily.Selector.And() - @PublishedApi - internal open val accessors: MutableList> = mutableListOf() - private val perArchetypeCache = FastIntMap>>() - - operator fun > AccessorBuilder.provideDelegate( - thisRef: Any, - property: KProperty<*> - ): T = addAccessor { build(this@AccessorHolder, it) } - - inline fun > addAccessor(create: (index: Int) -> T): T { - val accessor = create(accessors.size) - when (accessor) { - is IndexedAccessor<*> -> accessors += accessor - is DirectAccessor<*> -> {} - else -> error("Accessor from unknown backend, ignoring.") - } - return accessor - } - - /** Calculates, or gets cached values for an [archetype] */ - fun cacheForArchetype(archetype: Archetype): List> = - perArchetypeCache.getOrPut(archetype.id) { - val accessorCache: List> = accessors.map { it.cached.mapTo(mutableListOf()) { null } } - val cache = ArchetypeCacheScope(archetype, accessorCache) - - for (accessor in accessors) - for (it in accessor.cached) - accessorCache[accessor.index][it.cacheIndex] = - it.run { cache.calculate() } - - accessorCache - } - - /** Iterates over data in [dataScope] with all possible combinations calculated by accessors in this holder. */ - @PublishedApi - internal inline fun forEachCombination(dataScope: RawAccessorDataScope, run: (List<*>) -> Unit) { - // All sets of data each accessor wants. Will iterate over all combinations of items from each list. - val data: List> = accessors.map { with(it) { dataScope.readData() } } - // The total number of combinations that can be made with all elements in each list. - val totalCombinations = data.fold(1) { acc, b -> acc * b.size } - for (permutation in 0 until totalCombinations) { - run(data.map { it[permutation % it.size] }) - } - } /** Is the family of this holder not restricted in any way? */ val isEmpty: Boolean get() = family.and.isEmpty() diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorOperations.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorOperations.kt index dc35b663a..6c17fafbe 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorOperations.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorOperations.kt @@ -2,39 +2,55 @@ package com.mineinabyss.geary.systems.accessors import com.mineinabyss.geary.datatypes.Component import com.mineinabyss.geary.datatypes.HOLDS_DATA +import com.mineinabyss.geary.datatypes.Relation import com.mineinabyss.geary.datatypes.withRole import com.mineinabyss.geary.helpers.componentId import com.mineinabyss.geary.helpers.componentIdWithNullable -import com.mineinabyss.geary.systems.accessors.types.ComponentAccessor -import com.mineinabyss.geary.systems.accessors.types.ComponentOrDefaultAccessor -import com.mineinabyss.geary.systems.accessors.types.RelationWithDataAccessor +import com.mineinabyss.geary.systems.accessors.type.* +import kotlin.reflect.KProperty -/** - * An empty interface that limits [AccessorBuilder] helper functions only to classes that use [Accessor]s. - */ open class AccessorOperations { - /** Gets a component, ensuring it is on the entity. */ - inline fun get(): AccessorBuilder> { - return AccessorBuilder { holder, index -> - val component = componentId().withRole(HOLDS_DATA) - holder._family.has(component) - ComponentAccessor(index, component) - } + /** Accesses a component, ensuring it is on the entity. */ + inline fun get(): ComponentAccessor { + return NonNullComponentAccessor(componentId().withRole(HOLDS_DATA)) + } + + /** Accesses a data stored in a relation with kind [K] and target type [T], ensuring it is on the entity. */ + inline fun getRelation(): ComponentAccessor { + return NonNullComponentAccessor(Relation.of().id) + } + + /** + * Accesses a component, allows removing it by setting to null. + * As a result, the type is nullable since it may be removed during system runtime. + */ + fun ComponentAccessor.removable(): RemovableComponentAccessor { + return RemovableComponentAccessor(id) } - /** Gets a component or provides a [default] if the entity doesn't have it. */ - inline fun getOrDefault( - default: T - ): AccessorBuilder> { - return AccessorBuilder { _, index -> - val component = componentId().withRole(HOLDS_DATA) - ComponentOrDefaultAccessor(index, component, default) + /** + * Accesses a component or provides a [default] if the entity doesn't have it. + * Default gets recalculated on every call to the accessor. + */ + fun ComponentAccessor.orDefault(default: () -> T): ComponentOrDefaultAccessor { + return ComponentOrDefaultAccessor(id, default) + } + + /** Maps an accessor, will recalculate on every call. */ + fun > A.map(mapping: (T) -> U): ReadOnlyAccessor { + return object : ReadOnlyAccessor, FamilyMatching { + override val family = (this@map as? FamilyMatching)?.family + + override fun getValue(thisRef: Pointer, property: KProperty<*>): U { + val value = this@map.getValue(thisRef, property) + return mapping(value) + } } } - /** Gets a component or `null` if the entity doesn't have it. */ - inline fun getOrNull(): AccessorBuilder> { - return getOrDefault(null) + /** Accesses a component or `null` if the entity doesn't have it. */ + fun ComponentAccessor.orNull(): ComponentOrDefaultAccessor { + return orDefault { null } } /** @@ -49,10 +65,12 @@ open class AccessorOperations { * - One of [K] or [T] is [Any] => gets all relations matching the other (specified) type. * - Note: nullability rules are still upheld with [Any]. */ - inline fun getRelations(): AccessorBuilder> { - return AccessorBuilder { holder, index -> - holder._family.hasRelation() - RelationWithDataAccessor(index, componentIdWithNullable(), componentIdWithNullable()) - } + inline fun getRelations(): RelationsAccessor { + return RelationsAccessor(componentIdWithNullable(), componentIdWithNullable()) + } + + /** @see getRelations */ + inline fun getRelationsWithData(): RelationsWithDataAccessor { + return RelationsWithDataAccessor(componentIdWithNullable(), componentIdWithNullable()) } } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt deleted file mode 100644 index 8b41e7970..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -expect interface AccessorScopeSelector diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/Aliases.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/Aliases.kt new file mode 100644 index 000000000..9c9250725 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/Aliases.kt @@ -0,0 +1,19 @@ +package com.mineinabyss.geary.systems.accessors + +import com.mineinabyss.geary.datatypes.RecordPointer +import com.mineinabyss.geary.datatypes.Records +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty + + +typealias ReadOnlyAccessor = ReadOnlyProperty +typealias ReadWriteAccessor = ReadWriteProperty + +/** A pointer to where a specific entity's data is stored for use by accessors. */ +typealias Pointer = RecordPointer + +/** A list of [Pointer]s, currently used to select between the target, source, and event entity in listeners. */ +typealias Pointers = Records + +typealias GearyPointer = RecordPointer +typealias GearyPointers = Records diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/FamilyMatching.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/FamilyMatching.kt new file mode 100644 index 000000000..0aed8ed13 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/FamilyMatching.kt @@ -0,0 +1,11 @@ +package com.mineinabyss.geary.systems.accessors + +import com.mineinabyss.geary.datatypes.family.Family + +/** + * Used for accessors that require a family to be matched against to work + * (ex a component accessor needs the component present on the entity.) + */ +interface FamilyMatching { + val family: Family? +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/PerArchetypeCache.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/PerArchetypeCache.kt deleted file mode 100644 index 66c4bc889..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/PerArchetypeCache.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -/** - * Handles retrieving and calculating values per archetype with an assigned index for an [Accessor]. - * - * @property index The accessor's index in its holder. - * @property cacheIndex An assigned index to store/read data from. - */ -abstract class PerArchetypeCache( - val index: Int, - val cacheIndex: Int -) : ReadOnlyProperty { - abstract fun ArchetypeCacheScope.calculate(): T - - @Suppress("UNCHECKED_CAST") - override fun getValue(thisRef: ArchetypeCacheScope, property: KProperty<*>): T = - thisRef.perArchetypeData[index][cacheIndex] as T -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RawDataScopes.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RawDataScopes.kt deleted file mode 100644 index 2fc7cba93..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RawDataScopes.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -import com.mineinabyss.geary.datatypes.Entity -import com.mineinabyss.geary.engine.archetypes.Archetype - -/** - * A scope provided to [Accessor]s to cache data per archetype. - */ -open class ArchetypeCacheScope( - val archetype: Archetype, - val perArchetypeData: List>, -) - -/** - * An [ArchetypeCacheScope] with a reference to a specific entity in that archetype. - * It will be processed by an [Accessor] into a [ResultScope]. - * - * Note: This extends [ArchetypeCacheScope] to allow accessors to read their per archetype cache when processing data. - * - * @see Accessor - */ -class RawAccessorDataScope( - archetype: Archetype, - perArchetypeData: List>, - val row: Int, -) : ArchetypeCacheScope(archetype, perArchetypeData) { - val entity: Entity = archetype.getEntity(row) -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ReadOnlyEntitySelectingAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ReadOnlyEntitySelectingAccessor.kt new file mode 100644 index 000000000..c319354ae --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ReadOnlyEntitySelectingAccessor.kt @@ -0,0 +1,15 @@ +package com.mineinabyss.geary.systems.accessors + +import com.mineinabyss.geary.datatypes.Records +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** Wrapper for a [ReadOnlyAccessor] that selects a specific entity when multiple can be chosen from. */ +open class ReadOnlyEntitySelectingAccessor, A>( + protected val accessor: T, + protected val pointerIndex: Int, +) : ReadOnlyProperty { + override fun getValue(thisRef: Records, property: KProperty<*>): A { + return accessor.getValue(thisRef.getByIndex(pointerIndex), property) + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ReadWriteEntitySelectingAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ReadWriteEntitySelectingAccessor.kt new file mode 100644 index 000000000..260351d8a --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ReadWriteEntitySelectingAccessor.kt @@ -0,0 +1,15 @@ +package com.mineinabyss.geary.systems.accessors + +import com.mineinabyss.geary.datatypes.Records +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** Wrapper for a [ReadWriteAccessor] that selects a specific entity when multiple can be chosen from. */ +class ReadWriteEntitySelectingAccessor, A>( + accessor: T, + pointerIndex: Int +) : ReadOnlyEntitySelectingAccessor(accessor, pointerIndex), ReadWriteProperty { + override fun setValue(thisRef: Records, property: KProperty<*>, value: A) { + accessor.setValue(thisRef.getByIndex(pointerIndex), property, value) + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RelationWithData.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RelationWithData.kt index 8e2aa8250..1a1b52ae2 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RelationWithData.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/RelationWithData.kt @@ -5,6 +5,9 @@ import com.mineinabyss.geary.datatypes.Entity import com.mineinabyss.geary.datatypes.Relation import com.mineinabyss.geary.helpers.toGeary +/** + * Helper class for getting a compact overview of data stored in a relation. + */ data class RelationWithData( val data: K, val targetData: T, diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ResultScopes.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ResultScopes.kt deleted file mode 100644 index 5f4c3bd53..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/ResultScopes.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -import com.mineinabyss.geary.datatypes.Entity -import com.mineinabyss.geary.systems.GearySystem -import com.mineinabyss.geary.systems.Listener - -/** - * A generic scope for processed data. - * - * Other scopes extend this scope without changing much so that we can get type safe access when - * building accessors. For instance, a [Listener] can have accessors for a source, target, - * or event entity which each contain different data. - * - * @see SourceScope - * @see TargetScope - * @see EventScope - */ -open class ResultScope( - val entity: Entity, - internal val data: List<*>, -) - -/** - * Stores processed data for the source entity in an event. - * - * This is the entity that caused the event. - * - * @see TargetScope - * @see EventScope - */ -open class SourceScope( - entity: Entity, - data: List<*>, -) : ResultScope(entity, data) - -/** - * Stores processed data for the target entity in any [GearySystem]. - * - * This is the entity being affected by in event or system. - * - * @see SourceScope - * @see EventScope - */ -open class TargetScope( - entity: Entity, - data: List<*>, -) : ResultScope(entity, data) - -/** - * Stores processed data for the event entity in an event. - * - * This entity stores instructions in the form of components that will typically affect the source or target. - * - * @see SourceScope - * @see TargetScope - */ -class EventScope( - entity: Entity, - data: List<*>, -) : ResultScope(entity, data) diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/building/FlatAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/building/FlatAccessor.kt deleted file mode 100644 index 6716fa740..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/building/FlatAccessor.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.building - -import com.mineinabyss.geary.systems.accessors.AccessorBuilder -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope -import com.mineinabyss.geary.systems.accessors.types.IndexedAccessor - -//TODO make test for this accessor -/** - * Implements the accessor flatten operation given a [wrapped] accessor. - * - * @see flatten - */ -open class FlatAccessor>( - private val wrapped: A -) : IndexedAccessor>(wrapped.index) { - init { - _cached.addAll(wrapped.cached) - } - - override fun RawAccessorDataScope.readData(): List> = listOf(wrapped.run { readData() }) -} - -/** - * If several combinations are possible (ex several relations present on an entity), will process them as one list - * instead of handling each individually. - */ -fun > AccessorBuilder.flatten(): AccessorBuilder> = - AccessorBuilder { holder, index -> - FlatAccessor(this.build(holder, index)) - } diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/building/TransformingAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/building/TransformingAccessor.kt deleted file mode 100644 index b42dae4d4..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/building/TransformingAccessor.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.building - -import com.mineinabyss.geary.systems.accessors.AccessorBuilder -import com.mineinabyss.geary.systems.accessors.types.IndexedAccessor -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope - -/** - * Implements the accessor [map] operation given a [wrapped] accessor. - * - * @see map - */ -class TransformingAccessor( - private val transform: (T) -> R, - private val wrapped: IndexedAccessor -) : IndexedAccessor(wrapped.index) { - init { - _cached.addAll(wrapped.cached) - } - - override fun RawAccessorDataScope.readData(): List { - return wrapped.run { readData() }.map(transform) - } -} - -/** Takes the result of another accessor and [transform]s it. */ -fun > AccessorBuilder.map( - transform: (T) -> R -): AccessorBuilder> = AccessorBuilder { holder, index -> - TransformingAccessor(transform, this.build(holder, index)) -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt new file mode 100644 index 000000000..9c0e2dc74 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentAccessor.kt @@ -0,0 +1,56 @@ +package com.mineinabyss.geary.systems.accessors.type + +import com.mineinabyss.geary.datatypes.ComponentId +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.datatypes.family.Family +import com.mineinabyss.geary.datatypes.family.family +import com.mineinabyss.geary.engine.archetypes.Archetype +import com.mineinabyss.geary.systems.accessors.FamilyMatching +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.accessors.ReadWriteAccessor +import kotlin.reflect.KProperty + +@OptIn(UnsafeAccessors::class) +abstract class ComponentAccessor( + val id: ComponentId +) : ReadWriteAccessor, FamilyMatching { + override val family: Family = family { hasSet(id) } + + protected var cachedIndex = -1 + protected var cachedDataArray: MutableList = mutableListOf() + protected var cachedArchetype: Archetype? = null + + abstract operator fun get(thisRef: Pointer): T + + internal inline fun get(thisRef: Pointer, beforeRead: () -> Unit): T { + val archetype = thisRef.archetype + if (archetype !== cachedArchetype) { + cachedArchetype = archetype + cachedIndex = archetype.indexOf(id) + if (cachedIndex != -1) cachedDataArray = archetype.componentData[cachedIndex] as MutableList + } + beforeRead() + return cachedDataArray[thisRef.row] + } + + abstract operator fun set(thisRef: Pointer, value: T) + + internal inline fun set(thisRef: Pointer, value: T, beforeWrite: () -> Unit) { + val archetype = thisRef.archetype + if (archetype !== cachedArchetype) { + cachedArchetype = archetype + cachedIndex = archetype.indexOf(id) + if (cachedIndex != -1) cachedDataArray = archetype.componentData[cachedIndex] as MutableList + } + beforeWrite() + cachedDataArray[thisRef.row] = value + } + + final override fun getValue(thisRef: Pointer, property: KProperty<*>): T { + return get(thisRef) + } + + final override fun setValue(thisRef: Pointer, property: KProperty<*>, value: T) { + return set(thisRef, value) + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentOrDefaultAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentOrDefaultAccessor.kt new file mode 100644 index 000000000..15f61048b --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/ComponentOrDefaultAccessor.kt @@ -0,0 +1,27 @@ +package com.mineinabyss.geary.systems.accessors.type + +import com.mineinabyss.geary.datatypes.ComponentId +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.engine.archetypes.Archetype +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.accessors.ReadOnlyAccessor +import kotlin.reflect.KProperty + +@OptIn(UnsafeAccessors::class) +class ComponentOrDefaultAccessor( + val id: ComponentId, + val default: () -> T, +) : ReadOnlyAccessor { + private var cachedIndex = -1 + private var cachedArchetype: Archetype? = null + + override fun getValue(thisRef: Pointer, property: KProperty<*>): T { + val archetype = thisRef.archetype + if (archetype !== cachedArchetype) { + cachedArchetype = archetype + cachedIndex = archetype.indexOf(id) + } + if (cachedIndex == -1) return default() + return archetype.componentData[cachedIndex][thisRef.row] as T + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/NonNullComponentAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/NonNullComponentAccessor.kt new file mode 100644 index 000000000..51a1c3dcd --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/NonNullComponentAccessor.kt @@ -0,0 +1,22 @@ +package com.mineinabyss.geary.systems.accessors.type + +import com.mineinabyss.geary.datatypes.ComponentId +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.systems.accessors.Pointer + +@OptIn(UnsafeAccessors::class) +class NonNullComponentAccessor( + id: ComponentId, +) : ComponentAccessor(id) { + override operator fun get(thisRef: Pointer): T = + get(thisRef, beforeRead = {}) + + override operator fun set(thisRef: Pointer, value: T) { + set(thisRef, value, beforeWrite = { + if (cachedIndex == -1) { + thisRef.entity.set(value, id) + return + } + }) + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RelationsAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RelationsAccessor.kt new file mode 100644 index 000000000..3ece96668 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RelationsAccessor.kt @@ -0,0 +1,34 @@ +package com.mineinabyss.geary.systems.accessors.type + +import com.mineinabyss.geary.datatypes.ComponentId +import com.mineinabyss.geary.datatypes.EntityId +import com.mineinabyss.geary.datatypes.Relation +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.datatypes.family.Family +import com.mineinabyss.geary.datatypes.family.family +import com.mineinabyss.geary.engine.archetypes.Archetype +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.accessors.FamilyMatching +import com.mineinabyss.geary.systems.accessors.ReadOnlyAccessor +import kotlin.reflect.KProperty + +@OptIn(UnsafeAccessors::class) +class RelationsAccessor( + val kind: ComponentId, + val target: EntityId, +) : ReadOnlyAccessor>, FamilyMatching { + override val family: Family = family { hasRelation(kind, target) } + + private var cachedRelations = emptyList() + private var cachedArchetype: Archetype? = null + + override fun getValue(thisRef: Pointer, property: KProperty<*>): List { + val archetype = thisRef.archetype + if (archetype != cachedArchetype) { + cachedArchetype = archetype + cachedRelations = archetype.getRelations(kind, target) + } + + return cachedRelations + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RelationsWithDataAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RelationsWithDataAccessor.kt new file mode 100644 index 000000000..e352750a8 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RelationsWithDataAccessor.kt @@ -0,0 +1,35 @@ +package com.mineinabyss.geary.systems.accessors.type + +import com.mineinabyss.geary.datatypes.ComponentId +import com.mineinabyss.geary.datatypes.EntityId +import com.mineinabyss.geary.datatypes.Relation +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.datatypes.family.Family +import com.mineinabyss.geary.datatypes.family.family +import com.mineinabyss.geary.engine.archetypes.Archetype +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.accessors.FamilyMatching +import com.mineinabyss.geary.systems.accessors.ReadOnlyAccessor +import com.mineinabyss.geary.systems.accessors.RelationWithData +import kotlin.reflect.KProperty + +@OptIn(UnsafeAccessors::class) +class RelationsWithDataAccessor( + val kind: ComponentId, + val target: EntityId, +) : ReadOnlyAccessor>>, FamilyMatching { + override val family: Family = family { hasRelation(kind, target) } + + private var cachedRelations = emptyList() + private var cachedArchetype: Archetype? = null + + override fun getValue(thisRef: Pointer, property: KProperty<*>): List> { + val archetype = thisRef.archetype + if (archetype != cachedArchetype) { + cachedArchetype = archetype + cachedRelations = archetype.getRelations(kind, target) + } + + return archetype.readRelationDataFor(thisRef.row, kind, target, cachedRelations) as List> + } +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RemovableComponentAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RemovableComponentAccessor.kt new file mode 100644 index 000000000..0d2ce7d84 --- /dev/null +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/type/RemovableComponentAccessor.kt @@ -0,0 +1,28 @@ +package com.mineinabyss.geary.systems.accessors.type + +import com.mineinabyss.geary.datatypes.ComponentId +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.systems.accessors.Pointer + +@OptIn(UnsafeAccessors::class) +class RemovableComponentAccessor( + id: ComponentId, +) : ComponentAccessor(id) { + override fun get(thisRef: Pointer): T? = + get(thisRef, beforeRead = { + if (cachedIndex == -1) return null + }) + + override fun set(thisRef: Pointer, value: T?) = + set(thisRef, value, beforeWrite = { + if (cachedIndex == -1) { + if (value == null) return + else thisRef.entity.set(value, id) + return + } + if (value == null) { + thisRef.entity.remove(id) + return + } + }) +} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/ComponentAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/ComponentAccessor.kt deleted file mode 100644 index 5c7c54b80..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/ComponentAccessor.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.types - -import com.mineinabyss.geary.datatypes.Component -import com.mineinabyss.geary.datatypes.ComponentId -import com.mineinabyss.geary.systems.accessors.ArchetypeCacheScope -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope - -/** - * Implements the accessor [get] operation. - * - * @see get - */ -//TODO is it possible to merge with ComponentOrDefaultAccessor -open class ComponentAccessor( - index: Int, - private val componentId: ComponentId, -) : IndexedAccessor(index) { - private val ArchetypeCacheScope.dataIndex by cached { archetype.indexOf(componentId) } - - override fun RawAccessorDataScope.readData(): List = - @Suppress("UNCHECKED_CAST") // Index assignment ensures this should always be true - listOf(archetype.componentData[dataIndex][row] as T) -} - diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/ComponentOrDefaultAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/ComponentOrDefaultAccessor.kt deleted file mode 100644 index 6d153016e..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/ComponentOrDefaultAccessor.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.types - -import com.mineinabyss.geary.datatypes.Component -import com.mineinabyss.geary.datatypes.ComponentId -import com.mineinabyss.geary.systems.accessors.ArchetypeCacheScope -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope - -/** - * Implements the accessor [getOrDefault] operation. - * - * @see getOrDefault - */ -class ComponentOrDefaultAccessor( - index: Int, - componentId: ComponentId, - private val default: T -) : ComponentAccessor(index, componentId) { - private val ArchetypeCacheScope.dataIndex by cached { archetype.indexOf(componentId) } - - override fun RawAccessorDataScope.readData(): List { - if (dataIndex == -1) return listOf(default) - @Suppress("UNCHECKED_CAST") // Index assignment ensures this should always be true - return listOf(archetype.componentData[dataIndex][row] as T) - } -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/DirectAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/DirectAccessor.kt deleted file mode 100644 index 4d9a00918..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/DirectAccessor.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.types - -import com.mineinabyss.geary.systems.accessors.Accessor -import com.mineinabyss.geary.systems.accessors.ResultScope - -class DirectAccessor(val value: T) : Accessor { - override fun access(scope: ResultScope): T = value -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/IndexedAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/IndexedAccessor.kt deleted file mode 100644 index 553935aac..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/IndexedAccessor.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.types - -import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.* - -/** - * Accessors allow us to read components off an entity after a data preprocessing step. - * - * Accessor system summary: - * - An [AccessorOperationsProvider] provides a DSL for creating accessor builders. - * - Other classes provide functions for building and adding those accessors onto an [AccessorHolderImpl]. - * This is done to allow registering multiple holders, ex in [Listener]. - * - A consumer provides an [RawAccessorDataScope] and uses it to create an iterator with [AccessorHolderImpl.iteratorFor]. - * - The [iterator][AccessorHolder.AccessorCombinationsIterator] requests each accessor - * to [parse][readData] the raw data. - * - The consumer creates [ResultScope]s, for each iteration. - * - The scope allows appropriate accessors to read parsed data without recalculating it each time. - */ -abstract class IndexedAccessor( - val index: Int -) : Accessor { - /** - * Processes a [RawAccessorDataScope] with an entity. - * - * If more than one item is returned, systems will individually handle each combination. - */ - @PublishedApi - internal abstract fun RawAccessorDataScope.readData(): List - - /** - * A list of indices and operations to calculate a cached value on this accessor. - * - * @see cached - */ - internal val cached: List> get() = _cached - protected val _cached: MutableList> = mutableListOf() - - /** Calculates an [operation] once per archetype this accessor gets matched against. */ - protected inline fun cached(crossinline operation: ArchetypeCacheScope.() -> T): PerArchetypeCache = - object : PerArchetypeCache(index, _cached.size) { - override fun ArchetypeCacheScope.calculate(): T = operation() - }.also { _cached += it } - - @Suppress("UNCHECKED_CAST") // Internal logic ensures cast always succeeds - override fun access(scope: ResultScope): T = scope.data[index] as T -} diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/RelationAccessor.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/RelationAccessor.kt deleted file mode 100644 index 02022bf20..000000000 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/accessors/types/RelationAccessor.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.mineinabyss.geary.systems.accessors.types - -import com.mineinabyss.geary.datatypes.* -import com.mineinabyss.geary.systems.accessors.ArchetypeCacheScope -import com.mineinabyss.geary.systems.accessors.RawAccessorDataScope -import com.mineinabyss.geary.systems.accessors.RelationWithData - -open class RelationWithDataAccessor( - index: Int, - private val kind: ComponentId, - private val target: EntityId, -) : IndexedAccessor>(index) { - private val ArchetypeCacheScope.matchedRelations: List by cached { - archetype.getRelations(kind, target) - } - - private val ArchetypeCacheScope.relationDataIndices: IntArray - by cached { matchedRelations.map { archetype.indexOf(it.id) }.toIntArray() } - - private val ArchetypeCacheScope.targetDataIndices: IntArray - by cached { matchedRelations.map { archetype.indexOf(it.target.withRole(HOLDS_DATA)) }.toIntArray() } - - - override fun RawAccessorDataScope.readData(): List> = - matchedRelations.mapIndexed { i, relation -> - @Suppress("UNCHECKED_CAST") // Index assignment ensures this should always be true - (RelationWithData( - data = archetype.componentData.getOrNull(relationDataIndices[i])?.get(row) as K, - targetData = archetype.componentData.getOrNull(targetDataIndices[i])?.get(row) as T, - relation = relation - )) - } -} - diff --git a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/Query.kt b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/Query.kt index d3b2dbb81..e72d255dc 100644 --- a/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/Query.kt +++ b/geary-core/src/commonMain/kotlin/com/mineinabyss/geary/systems/query/Query.kt @@ -1,65 +1,76 @@ package com.mineinabyss.geary.systems.query -import com.mineinabyss.geary.modules.geary -import com.mineinabyss.geary.datatypes.Component +import com.mineinabyss.geary.datatypes.GearyEntity import com.mineinabyss.geary.datatypes.family.Family import com.mineinabyss.geary.engine.archetypes.Archetype -import com.mineinabyss.geary.systems.accessors.Accessor +import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.accessors.AccessorHolder -import com.mineinabyss.geary.systems.accessors.TargetScope -import com.mineinabyss.geary.systems.accessors.types.ComponentAccessor -import com.mineinabyss.geary.systems.accessors.types.DirectAccessor -import com.soywiz.kds.iterators.fastForEachWithIndex -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow +import com.mineinabyss.geary.systems.accessors.FamilyMatching +import com.mineinabyss.geary.systems.accessors.Pointer +import korlibs.datastructure.iterators.fastForEachWithIndex +import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty /**com.mineinabyss.geary.ecs.engine.iteration.accessors * @property matchedArchetypes A set of archetypes which have been matched to this query. */ -abstract class Query : AccessorHolder(), Iterable { +abstract class Query : AccessorHolder() { @PublishedApi internal val matchedArchetypes: MutableSet = mutableSetOf() @PublishedApi internal var registered: Boolean = false - fun flow(): Flow { - return channelFlow { - forEach { targetScope -> - send(targetScope) - } + val matchedEntities + get(): List { + registerIfNotRegistered() + return matchedArchetypes.flatMap { it.entities } } - } - override fun iterator(): Iterator { - val items = mutableListOf() - fastForEach { items += it } - return items.iterator() - } - - inline fun fastForEach(crossinline run: (TargetScope) -> Unit) { + fun registerIfNotRegistered() { if (!registered) { geary.queryManager.trackQuery(this) } + } + + inline fun toList(crossinline map: (Pointer) -> T): List { + val list = mutableListOf() + forEach { list.add(map(it)) } + return list + } + + /** + * Quickly iterates over all matched entities, running [run] for each. + * + * Use [apply] on the query to use its accessors. + * */ + inline fun forEach(crossinline run: (Pointer) -> Unit) { + registerIfNotRegistered() val matched = matchedArchetypes.toList() - val sizes = matched.map { it.size - 1 } matched.fastForEachWithIndex { i, archetype -> archetype.isIterating = true - archetype.iteratorFor(this@Query).forEach(upTo = sizes[i]) { targetScope -> - run(targetScope) + val upTo = archetype.size + // TODO upTo isn't perfect for cases where entities may be added or removed in the same iteration + for (entityIndex in 0 until upTo) { + run(Pointer(archetype, entityIndex)) } archetype.isIterating = false } } - @Deprecated("Likely trying to access component off entity", ReplaceWith("entity.get()")) - protected inline fun TargetScope.get(): ComponentAccessor = - error("Cannot change query at runtime") - - operator fun Accessor.getValue(thisRef: TargetScope, property: KProperty<*>): T = - access(thisRef) + operator fun Family.provideDelegate(thisRef: GearyQuery, property: KProperty<*>): ReadOnlyProperty { + mutableFamily.add(this) + return ReadOnlyProperty { thisRef, prop -> + this@provideDelegate + } + } - operator fun Family.provideDelegate(thisRef: GearyQuery, property: KProperty<*>): DirectAccessor = - _family.add(this).run { DirectAccessor(family) } + /** Automatically matches families for any accessor that's supposed to match a family. */ + operator fun T.provideDelegate( + thisRef: Any, + prop: KProperty<*> + ): T { + family?.let { mutableFamily.add(it) } + return this + } } diff --git a/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt b/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt new file mode 100644 index 000000000..75da47620 --- /dev/null +++ b/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt @@ -0,0 +1,3 @@ +package com.mineinabyss.geary.datatypes.maps + +actual typealias CompId2ArchetypeMap = CompId2ArchetypeMapViaMutableMap diff --git a/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt b/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt deleted file mode 100644 index 2f4d95139..000000000 --- a/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -actual interface AccessorScopeSelector diff --git a/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/systems/trackEventListener.kt b/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/systems/trackEventListener.kt deleted file mode 100644 index a9ab33214..000000000 --- a/geary-core/src/jsMain/kotlin/com/mineinabyss/geary/systems/trackEventListener.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.mineinabyss.geary.systems - -import com.mineinabyss.geary.datatypes.maps.Family2ObjectArrayMap -import com.mineinabyss.geary.engine.archetypes.Archetype -import com.mineinabyss.geary.events.GearyHandler - -internal actual fun trackEventListener( - listener: GearyListener, - sourceListeners: MutableList, - targetListeners: MutableList, - archetypes: Family2ObjectArrayMap, - eventHandlers: MutableList -) { - TODO("Currently unsupported due to lack of full reflection on js") -} diff --git a/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt b/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt new file mode 100644 index 000000000..227030307 --- /dev/null +++ b/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/datatypes/maps/CompId2ArchetypeMap.kt @@ -0,0 +1,19 @@ +package com.mineinabyss.geary.datatypes.maps + +import com.mineinabyss.geary.datatypes.GearyComponentId +import com.mineinabyss.geary.engine.archetypes.Archetype +import it.unimi.dsi.fastutil.longs.Long2ObjectArrayMap + +actual class CompId2ArchetypeMap { + val inner = Long2ObjectArrayMap() + actual operator fun get(id: GearyComponentId): Archetype? = inner[id.toLong()] + actual operator fun set(id: GearyComponentId, archetype: Archetype) { + inner[id.toLong()] = archetype + } + + actual operator fun contains(id: GearyComponentId): Boolean = inner.containsKey(id.toLong()) + + actual inline fun getOrSet(id: GearyComponentId, put: () -> Archetype): Archetype { + return inner[id.toLong()] ?: put().also { set(id, it) } + } +} diff --git a/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt b/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt deleted file mode 100644 index 36ae5ed62..000000000 --- a/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/systems/accessors/AccessorScopeSelector.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.mineinabyss.geary.systems.accessors - -import com.mineinabyss.geary.datatypes.family.Family -import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.types.DirectAccessor -import kotlin.reflect.KProperty -import kotlin.reflect.full.extensionReceiverParameter -import kotlin.reflect.typeOf - -actual interface AccessorScopeSelector { - /** Automatically finds which [ResultScope] to select based on the receiver used on this [property]. */ - operator fun > AccessorBuilder.provideDelegate( - thisRef: Listener, - property: KProperty<*> - ): T { - val holder = property.getHolder(thisRef) - return holder.addAccessor { build(holder, it) } - } - - /** Ensures the [ResultScope] at the receiver of this [property] matches this family. */ - operator fun Family.provideDelegate(thisRef: Listener, property: KProperty<*>): Accessor { - val holder = property.getHolder(thisRef) - holder._family.add(this) - return holder.addAccessor { - DirectAccessor(this) - } - } - - companion object { - private fun KProperty<*>.getHolder(thisRef: Listener) = when (extensionReceiverParameter?.type) { - typeOf() -> thisRef.source - typeOf() -> thisRef.target - typeOf() -> thisRef.event - else -> error("Can only define accessors for source, target, or event.") - } - } -} diff --git a/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/systems/trackEventListener.kt b/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/systems/trackEventListener.kt deleted file mode 100644 index 8c667817e..000000000 --- a/geary-core/src/jvmMain/kotlin/com/mineinabyss/geary/systems/trackEventListener.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.mineinabyss.geary.systems - -import com.mineinabyss.geary.annotations.Handler -import com.mineinabyss.geary.datatypes.maps.Family2ObjectArrayMap -import com.mineinabyss.geary.engine.archetypes.Archetype -import com.mineinabyss.geary.events.CheckHandler -import com.mineinabyss.geary.events.GearyHandler -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.SourceScope -import com.mineinabyss.geary.systems.accessors.TargetScope -import kotlin.reflect.KClass -import kotlin.reflect.KFunction -import kotlin.reflect.full.functions -import kotlin.reflect.full.hasAnnotation -import kotlin.reflect.jvm.isAccessible -import kotlin.reflect.typeOf - -internal actual fun trackEventListener( - listener: Listener, - sourceListeners: MutableList, - targetListeners: MutableList, - archetypes: Family2ObjectArrayMap, - eventHandlers: MutableList -) { - listener::class.functions - .filter { runCatching { it.hasAnnotation() }.getOrDefault(false) } - .map { func -> - class FunctionCaller(val kFunction: KFunction<*>, params: List>) { - val types = kFunction.parameters.map { it.type.classifier } - - init { - // First param is the class itself, we only care about function params - if (types.drop(1).any { it !in params }) - error("Event handler on ${listener::class.simpleName} had parameters other than $params") - } - - val indices = params.map { types.indexOf(it) } - fun call(vararg args: Any?): Any? { - // Pass parameters in the order they need to be, allowing them to be omitted if desired. - // handle won't be called if a param is null when it shouldn't be unless misused by end user - val argArray = arrayOfNulls(types.size) - argArray[0] = listener - indices.forEachIndexed { i, arrIndex -> - if (arrIndex != -1) argArray[arrIndex] = args[i] - } - kFunction.isAccessible = true - return runCatching { kFunction.call(*argArray) } - // Don't print the whole reflection error, just the cause - .getOrElse { throw it.cause ?: it } - } - } - - val caller = FunctionCaller(func, listOf(SourceScope::class, TargetScope::class, EventScope::class)) - val sourceNullable = typeOf() !in func.parameters.map { it.type } - - if (func.returnType == typeOf()) - object : CheckHandler(listener, sourceNullable) { - override fun check(source: SourceScope?, target: TargetScope, event: EventScope): Boolean { - return caller.call(source, target, event) as Boolean - } - } - else - object : GearyHandler(listener, sourceNullable) { - override fun handle(source: SourceScope?, target: TargetScope, event: EventScope) { - caller.call(source, target, event) - } - } - } - .forEach { handler -> - // Add handlers to any matched event entities - eventHandlers += handler - val handlerMatched = archetypes.match(handler.parentListener.event.family) - for (archetype in handlerMatched) archetype.addEventHandler(handler) - } - - // Only start tracking a listener for the parts it actually cares for - if (!listener.source.isEmpty) { - sourceListeners += listener - val sourcesMatched = archetypes.match(listener.source.family) - for (archetype in sourcesMatched) archetype.addSourceListener(listener) - } - if (!listener.target.isEmpty) { - targetListeners += listener - val targetsMatched = archetypes.match(listener.target.family) - for (archetype in targetsMatched) archetype.addSourceListener(listener) - } - // Match source and target requirements -} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/AsyncArchetypeTests.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/AsyncArchetypeTests.kt deleted file mode 100644 index c13c95df8..000000000 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/AsyncArchetypeTests.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.mineinabyss.geary.async - -import com.mineinabyss.geary.datatypes.EntityType -import com.mineinabyss.geary.datatypes.HOLDS_DATA -import com.mineinabyss.geary.helpers.componentId -import com.mineinabyss.geary.helpers.entity -import com.mineinabyss.geary.helpers.tests.GearyTest -import com.mineinabyss.geary.helpers.toGeary -import com.mineinabyss.geary.modules.archetypes -import com.mineinabyss.geary.modules.geary -import io.kotest.matchers.collections.shouldBeUnique -import io.kotest.matchers.shouldBe -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import kotlin.time.measureTime - -class AsyncArchetypeTests : GearyTest() { - private val concurrentEntityAmount = 10000 - - @Test - fun `add entities concurrently`() = runTest { - val arc = archetypes.archetypeProvider.getArchetype(EntityType(ulongArrayOf(componentId() or HOLDS_DATA))) - concurrentOperation(concurrentEntityAmount) { - val rec = archetypes.records[geary.entityProvider.create()] - arc.addEntityWithData(rec, arrayOf("Test"), rec.entity) - }.awaitAll() - arc.entities.size shouldBe concurrentEntityAmount - arc.entities.shouldBeUnique() - } - - - // The two tests below are pretty beefy and more like benchmarks so they're disabled by default - //TODO move into benchmark and turn back into concurrent version when we actually support concurrency -// @Test - fun `set and remove concurrency`() = runTest { - println(measureTime { - repeat(1000000) { - val entity = entity() - repeat(0) { id -> - entity.setRelation("String", id.toULong().toGeary()) - }//.awaitAll() -// println("Finished for ${entity.id}, arc size ${engine.archetypeProvider.count}") - }//.awaitAll() - }) -// entity.getComponents().shouldBeEmpty() - } - - // @Test -// fun `mutliple locks`() { -// val a = entity() -//// val b = entity() -// concurrentOperation(10000) { -// engine.withLock(setOf(a/*, b*/)) { -// println("Locking") -// delay(100) -// } -// } -// } - - // @Test - fun `concurrent archetype creation`() = runTest { - clearEngine() - val iters = 10000 - println(measureTime { - for (i in 0 until iters) { -// concurrentOperation(iters) { i -> - archetypes.archetypeProvider.getArchetype(EntityType((0uL..i.toULong()).toList())) - println("Creating arc $i, total: ${archetypes.archetypeProvider.count}") -// }.awaitAll() - } - }) - archetypes.archetypeProvider.count shouldBe iters + 1 - } -} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/ConcurrentSystemModificationTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/ConcurrentSystemModificationTest.kt deleted file mode 100644 index 519c8ea17..000000000 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/ConcurrentSystemModificationTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.mineinabyss.geary.async - -import com.mineinabyss.geary.datatypes.family.family -import com.mineinabyss.geary.helpers.entity -import com.mineinabyss.geary.helpers.tests.GearyTest -import com.mineinabyss.geary.modules.geary -import com.mineinabyss.geary.systems.RepeatingSystem -import com.mineinabyss.geary.systems.accessors.TargetScope -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.shouldBe -import kotlinx.coroutines.test.runTest - -class ConcurrentSystemModificationTest : GearyTest() { - //TODO put back when we support concurrency lol -// @Test - fun `concurrent modification`() = runTest { - resetEngine() - var ran = 0 - val removingSystem = object : RepeatingSystem() { - val TargetScope.string by get() - - override fun TargetScope.tick() { - entity.remove() - ran++ - } - } - geary.pipeline.addSystem(removingSystem) - val entities = (0 until 10).map { entity { set("Test") } } - val total = - geary.queryManager.getEntitiesMatching(family { - hasSet() - }).count() - geary.engine.tick(0) - ran shouldBe total - entities.map { it.getAll() } shouldContainExactly entities.map { setOf() } - } -} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/RunSafelyTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/RunSafelyTest.kt deleted file mode 100644 index 1979d06ee..000000000 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/async/RunSafelyTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.mineinabyss.geary.async - -import com.mineinabyss.geary.helpers.tests.GearyTest -import com.mineinabyss.geary.systems.RepeatingSystem -import com.mineinabyss.geary.systems.accessors.TargetScope - -class RunSafelyTest: GearyTest() { - class CheckAsyncSystem : RepeatingSystem() { - val TargetScope.string by get() - override fun TargetScope.tick() { - error("Found entity with string when it should have been removed before iteration") - } - } - - //TODO figure out what's up here -// @Test -// fun runSafely() = runTest { -// clearEngine() -// geary.systems.add(CheckAsyncSystem()) -// launch { -// repeat(5000) { -// geary.engine.tick(it.toLong()) -// } -// } -// concurrentOperation(50000) { -// runSafely { -// entity { -// set("Hello world") -// }.removeEntity() -// }.await() -// }.awaitAll() -// } -} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyTypeTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/EntityTypeTest.kt similarity index 90% rename from geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyTypeTest.kt rename to geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/EntityTypeTest.kt index 75f5e28a1..1901d2f9f 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyTypeTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/EntityTypeTest.kt @@ -4,7 +4,7 @@ import com.mineinabyss.geary.helpers.tests.GearyTest import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test -internal class GearyTypeTest : GearyTest() { +internal class EntityTypeTest : GearyTest() { @Test fun typeSorting() { val type = EntityType(listOf(3u, 1u, 2u)) diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyEntityTests.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyEntityTests.kt index d57c122b7..2bb04095c 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyEntityTests.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/datatypes/GearyEntityTests.kt @@ -2,9 +2,7 @@ package com.mineinabyss.geary.datatypes import com.mineinabyss.geary.components.relations.InstanceOf import com.mineinabyss.geary.components.relations.Persists -import com.mineinabyss.geary.helpers.componentId -import com.mineinabyss.geary.helpers.entity -import com.mineinabyss.geary.helpers.getArchetype +import com.mineinabyss.geary.helpers.* import com.mineinabyss.geary.helpers.tests.GearyTest import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.systems.accessors.RelationWithData @@ -48,6 +46,38 @@ internal class GearyEntityTests : GearyTest() { }.type.getArchetype() shouldBe archetypes.archetypeProvider.rootArchetype } + @Test + fun `component removal with two components`() { + entity { + set(1) + set("Test") + remove() + }.type.getArchetype() shouldBe archetypes.archetypeProvider.rootArchetype + componentId() + (HOLDS_DATA or componentId()) + } + + @Test + fun `should remove correct component when components with higher component id are also set on entity`() { + // Arrange + data class Data1(val id: Int) + data class Data2(val id: Int) + + // Ensure component order + componentId() + componentId() + + val entity = entity { + set(Data1(0)) + set(Data2(0)) + } + + // Act + entity.remove() + + // Assert + entity.getAll() shouldBe setOf(Data2(0)) + + } + @Test fun `add then set`() { entity { @@ -120,13 +150,27 @@ internal class GearyEntityTests : GearyTest() { setPersisting("Test") } val relations = entity.type.getArchetype() - .relationsByKind[componentId().toLong()]!! + .getRelationsByKind(componentId()) relations.size shouldBe 1 // one for persisting relations.first().target shouldBe componentId() entity.getAllPersisting().shouldContainExactly("Test") } + @Nested + inner class ChildTest { + @Test + fun `should handle single parent child relation correctly when using addParent`() { + val parent = entity() + val child = entity { + addParent(parent) + } + + parent.children.shouldContainExactly(child) + child.parents.shouldContainExactly(parent) + } + } + @Nested inner class RelationTest { @Test diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/engine/ArchetypeTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/engine/ArchetypeTest.kt index 3f1adbbdc..b0dad6624 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/engine/ArchetypeTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/engine/ArchetypeTest.kt @@ -54,7 +54,7 @@ internal class ArchetypeTest : GearyTest() { val instanceOf = Relation.of(target) val instanceOf2 = Relation.of(target2) val arc = Archetype(EntityType(listOf(persists.id, instanceOf.id, instanceOf2.id)), 0) - arc.relationsByTarget[target.id.toLong()].shouldContainExactlyInAnyOrder(persists, instanceOf) - arc.relationsByKind[componentId().toLong()].shouldContainExactlyInAnyOrder(instanceOf, instanceOf2) + arc.getRelationsByTarget(target.id).shouldContainExactlyInAnyOrder(persists, instanceOf) + arc.getRelationsByKind(componentId()).shouldContainExactlyInAnyOrder(instanceOf, instanceOf2) } } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/ComponentAddEventTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/ComponentAddEventTest.kt index a409b543e..1952bc690 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/ComponentAddEventTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/ComponentAddEventTest.kt @@ -1,12 +1,12 @@ package com.mineinabyss.geary.events -import com.mineinabyss.geary.annotations.Handler +import com.mineinabyss.geary.datatypes.Records import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.helpers.getArchetype import com.mineinabyss.geary.helpers.tests.GearyTest import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test @@ -16,12 +16,11 @@ internal class ComponentAddEventTest : GearyTest() { //TODO write test for all methods of checking for added inner class OnStringAdd : Listener() { // All three get added - val TargetScope.string by onSet() - val TargetScope.int by onSet() - val TargetScope.double by onSet() + val Records.string by get().whenSetOnTarget() + val Records.int by get().whenSetOnTarget() + val Records.double by get().whenSetOnTarget() - @Handler - fun TargetScope.increment() { + override fun Pointers.handle() { inc++ } } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SimpleComponentAddListenerTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SimpleComponentAddListenerTest.kt new file mode 100644 index 000000000..8db93fbfb --- /dev/null +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SimpleComponentAddListenerTest.kt @@ -0,0 +1,35 @@ +package com.mineinabyss.geary.events + +import com.mineinabyss.geary.datatypes.Records +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.helpers.tests.GearyTest +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.Listener +import com.mineinabyss.geary.systems.accessors.Pointers +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class SimpleComponentAddListenerTest : GearyTest() { + class MyListener : Listener() { + var called = 0 + + val Records.data by get().whenSetOnTarget() + + override fun Pointers.handle() { + called += 1 + } + } + + @Test + fun `simple event listener`() { + val listener = MyListener() + geary.pipeline.addSystem(listener) + + val entity = entity() + listener.called shouldBe 0 + entity.set(1.0) + listener.called shouldBe 0 + entity.set(1) + listener.called shouldBe 1 + } +} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SimpleEventTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SimpleEventTest.kt new file mode 100644 index 000000000..26a0fa78a --- /dev/null +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SimpleEventTest.kt @@ -0,0 +1,46 @@ +package com.mineinabyss.geary.events + +import com.mineinabyss.geary.datatypes.Records +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.helpers.tests.GearyTest +import com.mineinabyss.geary.modules.geary +import com.mineinabyss.geary.systems.Listener +import com.mineinabyss.geary.systems.accessors.Pointers +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class SimpleEventTest : GearyTest() { + class MyEvent() + + class MyListener : Listener() { + var called = 0 + + val Records.data by get().on(target) + val Records.event by get().on(event) + + override fun Pointers.handle() { + called += 1 + } + } + + @Test + fun `simple set listener`() { + val listener = MyListener() + geary.pipeline.addSystem(listener) + + val entity = entity { + set(1) + } + val event = entity { + set(MyEvent()) + } + + listener.called shouldBe 0 + entity.callEvent(event) + listener.called shouldBe 1 + entity.callEvent(entity()) + listener.called shouldBe 1 + entity().callEvent(event) + listener.called shouldBe 1 + } +} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SourceTargetEventTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SourceTargetEventTest.kt index 7f171b1a8..2acae2260 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SourceTargetEventTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/events/SourceTargetEventTest.kt @@ -1,14 +1,12 @@ package com.mineinabyss.geary.events -import com.mineinabyss.geary.annotations.Handler +import com.mineinabyss.geary.datatypes.Records import com.mineinabyss.geary.datatypes.family.family import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.helpers.tests.GearyTest import com.mineinabyss.geary.modules.geary import com.mineinabyss.geary.systems.Listener -import com.mineinabyss.geary.systems.accessors.EventScope -import com.mineinabyss.geary.systems.accessors.SourceScope -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test @@ -18,13 +16,15 @@ class SourceTargetEventTest : GearyTest() { data class Health(val amount: Int) inner class Interaction : Listener() { - val SourceScope.strength by get() - val TargetScope.health by get() - val EventScope.attacked by family { has() } + val Records.strength by get().on(source) + var Records.health by get().on(target) - @Handler - fun damage(source: SourceScope, target: TargetScope, event: EventScope) { - target.entity.set(Health(target.health.amount - source.strength.amount)) + init { + event.mutableFamily.add(family { has() }) + } + + override fun Pointers.handle() { + health = Health(health.amount - strength.amount) } } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/helpers/Components.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/helpers/Components.kt new file mode 100644 index 000000000..40389901f --- /dev/null +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/helpers/Components.kt @@ -0,0 +1,6 @@ +package com.mineinabyss.geary.helpers + +data class Comp1(val id: Int) +data class Comp2(val id: Int) +data class Comp3(val id: Int) +data class Comp4(val id: Int) diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/FamilyMatchingTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/FamilyMatchingTest.kt index c0d664465..833a84865 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/FamilyMatchingTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/FamilyMatchingTest.kt @@ -1,15 +1,13 @@ package com.mineinabyss.geary.systems -import com.mineinabyss.geary.datatypes.EntityType import com.mineinabyss.geary.datatypes.HOLDS_DATA -import com.mineinabyss.geary.datatypes.family.family +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors import com.mineinabyss.geary.helpers.componentId import com.mineinabyss.geary.helpers.entity -import com.mineinabyss.geary.helpers.getArchetype import com.mineinabyss.geary.helpers.tests.GearyTest import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.modules.geary -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointer import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe @@ -20,10 +18,15 @@ class FamilyMatchingTest : GearyTest() { val intId = componentId() val system = object : RepeatingSystem() { - val TargetScope.string by get() - val TargetScope.int by family { has() } + val Pointer.string by get() - override fun TargetScope.tick() { + // val GearyRecord.int by family { has() } + init { + mutableFamily.has() + } + + @OptIn(UnsafeAccessors::class) + override fun Pointer.tick() { string shouldBe entity.get() entity.has() shouldBe true } @@ -38,7 +41,8 @@ class FamilyMatchingTest : GearyTest() { @Test fun `family type is correct`() { - EntityType(system.family.components).getArchetype() shouldBe root + stringId + // TODO families can are wrapped by accessors now, so components won't be directly on it +// EntityType(system.family.components).getArchetype() shouldBe root + stringId } @Test @@ -61,6 +65,6 @@ class FamilyMatchingTest : GearyTest() { @Test fun `accessors in system correctly read data`() { - system.doTick() + system.tickAll() } } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/QueryManagerTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/QueryManagerTest.kt index 8671559c1..6bd97a645 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/QueryManagerTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/QueryManagerTest.kt @@ -1,12 +1,12 @@ package com.mineinabyss.geary.systems -import com.mineinabyss.geary.annotations.Handler +import com.mineinabyss.geary.datatypes.Records import com.mineinabyss.geary.helpers.contains import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.helpers.tests.GearyTest import com.mineinabyss.geary.modules.archetypes import com.mineinabyss.geary.modules.geary -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointers import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test @@ -14,25 +14,25 @@ import org.junit.jupiter.api.Test internal class QueryManagerTest : GearyTest() { private class TestComponent - private object EventListener : Listener() { + private class EventListener : Listener() { var ran = 0 - private val TargetScope.testComponent by get() + private val Records.testComponent by get().on(target) - @Handler - fun handle() { + override fun Pointers.handle() { ran++ } } @Test fun `empty event handler`() { - geary.pipeline.addSystem(EventListener) - (archetypes.archetypeProvider.rootArchetype.type in EventListener.event.family) shouldBe true - archetypes.archetypeProvider.rootArchetype.eventHandlers.map { it.parentListener } shouldContain EventListener + val listener = EventListener() + geary.pipeline.addSystem(listener) + (archetypes.archetypeProvider.rootArchetype.type in listener.event.family) shouldBe true + archetypes.archetypeProvider.rootArchetype.eventListeners shouldContain listener entity { set(TestComponent()) }.callEvent() // 1 from setting, 1 from calling empty event - EventListener.ran shouldBe 2 + listener.ran shouldBe 2 } } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt index 448b13f4a..907131a1a 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/RelationMatchingSystemTest.kt @@ -2,35 +2,38 @@ package com.mineinabyss.geary.systems import com.mineinabyss.geary.components.relations.InstanceOf import com.mineinabyss.geary.components.relations.Persists +import com.mineinabyss.geary.datatypes.RecordPointer import com.mineinabyss.geary.helpers.contains import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.helpers.getArchetype import com.mineinabyss.geary.helpers.tests.GearyTest import com.mineinabyss.geary.modules.geary -import com.mineinabyss.geary.systems.accessors.TargetScope +import com.mineinabyss.geary.systems.accessors.Pointer +import io.kotest.inspectors.forAll import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldNotContain import io.kotest.matchers.collections.shouldNotContainAll import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test +import kotlin.test.Test class RelationMatchingSystemTest : GearyTest() { @Test - fun relations() = runTest { + fun relations() { resetEngine() - var ran = 0 - val systemPersists = object : RepeatingSystem() { - val TargetScope.persists by getRelations() + val system = object: RepeatingSystem() { + val Pointer.persists by getRelationsWithData() + + var ran = 0 - override fun TargetScope.tick() { + override fun Pointer.tick() { ran++ - persists.data.shouldBeInstanceOf() + persists.forAll { it.data.shouldBeInstanceOf() } } } - geary.pipeline.addSystem(systemPersists) + geary.pipeline.addSystem(system) + val entity = entity { addRelation() add() @@ -43,26 +46,31 @@ class RelationMatchingSystemTest : GearyTest() { setRelation(Persists()) add() } - (entity3.type in systemPersists.family) shouldBe true - systemPersists.matchedArchetypes.shouldNotContainAll(entity.type.getArchetype(), entity2.type.getArchetype()) - systemPersists.matchedArchetypes.shouldContain(entity3.type.getArchetype()) + (entity3.type in system.family) shouldBe true + system.matchedArchetypes.shouldNotContainAll(entity.type.getArchetype(), entity2.type.getArchetype()) + system.matchedArchetypes.shouldContain(entity3.type.getArchetype()) geary.engine.tick(0) - ran shouldBe 1 + system.ran shouldBe 1 } @Test - fun relationPermutations() = runTest { + fun relationPermutations() { resetEngine() var ran = 0 + var persistsCount = 0 + var instanceOfCount = 0 val system = object : RepeatingSystem() { - val TargetScope.persists by getRelations() - val TargetScope.instanceOf by getRelations() - override fun TargetScope.tick() { + val RecordPointer.persists by getRelationsWithData() + val RecordPointer.instanceOf by getRelationsWithData() + + override fun RecordPointer.tick() { ran++ - persists.data.shouldBeInstanceOf() - persists.targetData shouldNotBe null - instanceOf.data shouldBe null + persistsCount += persists.size + instanceOfCount += instanceOf.size + persists.forAll { it.data.shouldBeInstanceOf() } + persists.forAll { it.targetData shouldNotBe null } + instanceOf.forAll { it.data shouldBe null } } } geary.pipeline.addSystem(system) @@ -88,18 +96,20 @@ class RelationMatchingSystemTest : GearyTest() { geary.engine.tick(0) // Only two of the Persists relations are valid, times both InstanceOf are valid - ran shouldBe 2 * 2 + ran shouldBe 1 + persistsCount shouldBe 2 + instanceOfCount shouldBe 2 } @Test - fun relationsWithData() = runTest { + fun relationsWithData() { resetEngine() val system = object : RepeatingSystem() { - val TargetScope.withData by getRelations() + val RecordPointer.withData by getRelationsWithData() - override fun TargetScope.tick() { - withData.data shouldBe Persists() - withData.targetData shouldBe "Test" + override fun RecordPointer.tick() { + withData.forAll { it.data shouldBe Persists() } + withData.forAll { it.targetData shouldBe "Test" } } } diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/SimpleQueryTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/SimpleQueryTest.kt new file mode 100644 index 000000000..5b917745e --- /dev/null +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/SimpleQueryTest.kt @@ -0,0 +1,35 @@ +package com.mineinabyss.geary.systems + +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.helpers.tests.GearyTest +import com.mineinabyss.geary.systems.accessors.Pointer +import com.mineinabyss.geary.systems.query.Query +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class SimpleQueryTest : GearyTest() { + class MyQuery : Query() { + val Pointer.int by get() + } + + @Test + fun `simple query`() { + repeat(10) { + entity { + set(1) + } + entity { + set("Not this!") + } + } + + var count = 0 + MyQuery().run { + forEach { + it.int shouldBe 1 + count++ + } + } + count shouldBe 10 + } +} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolderTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolderTest.kt index f3ab9cb72..17d0e7632 100644 --- a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolderTest.kt +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/AccessorHolderTest.kt @@ -2,15 +2,14 @@ package com.mineinabyss.geary.systems.accessors import com.mineinabyss.geary.helpers.entity import com.mineinabyss.geary.helpers.tests.GearyTest -import com.mineinabyss.geary.systems.accessors.building.map import com.mineinabyss.geary.systems.query.GearyQuery import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.jupiter.api.Test internal class AccessorHolderTest : GearyTest() { - object FancyQuery : GearyQuery() { - val TargetScope.default by getOrDefault("empty!") - val TargetScope.mapped by get().map { it.toString() } + class FancyQuery : GearyQuery() { + val Pointer.default by get().orDefault { "empty!" } + val Pointer.mapped by get().map { it.toString() } } @ExperimentalCoroutinesApi diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/RemovableAccessorTest.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/RemovableAccessorTest.kt new file mode 100644 index 000000000..f727150ed --- /dev/null +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/RemovableAccessorTest.kt @@ -0,0 +1,36 @@ +package com.mineinabyss.geary.systems.accessors + +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.helpers.Comp1 +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.helpers.tests.GearyTest +import com.mineinabyss.geary.systems.query.Query +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class RemovableAccessorTest: GearyTest() { + class MyQueryRemovable : Query() { + var Pointer.data by get().removable() + } + + @OptIn(UnsafeAccessors::class) + @Test + fun `should allow removing component via removable accessor`() { + resetEngine() + entity { + set(Comp1(1)) + } + var count = 0 + + MyQueryRemovable().run { + forEach { + it.data shouldBe Comp1(1) + it.data = null + it.data shouldBe null + it.entity.has() shouldBe false + count++ + } + } + count shouldBe 1 + } +} diff --git a/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/UnsafeQueryAccessTests.kt b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/UnsafeQueryAccessTests.kt new file mode 100644 index 000000000..d73a70462 --- /dev/null +++ b/geary-core/src/jvmTest/kotlin/com/mineinabyss/geary/systems/accessors/UnsafeQueryAccessTests.kt @@ -0,0 +1,81 @@ +package com.mineinabyss.geary.systems.accessors + +import com.mineinabyss.geary.annotations.optin.UnsafeAccessors +import com.mineinabyss.geary.helpers.Comp1 +import com.mineinabyss.geary.helpers.entity +import com.mineinabyss.geary.helpers.tests.GearyTest +import com.mineinabyss.geary.systems.query.Query +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class UnsafeQueryAccessTests : GearyTest() { + class MyQuery : Query() { + var Pointer.data by get() + } + + + @Test + fun `should allow data modify via accessor`() { + resetEngine() + entity { + set(Comp1(1)) + } + + var count = 0 + MyQuery().run { + forEach { + it.data shouldBe Comp1(1) + it.data = Comp1(10) + it.data shouldBe Comp1(10) + count++ + } + } + count shouldBe 1 + } + + @OptIn(UnsafeAccessors::class) + @Test + fun `should allow data modify when entity changed archetype by setting`() { + resetEngine() + entity { + set(Comp1(1)) + } + + var count = 0 + MyQuery().run { + forEach { + it.data shouldBe Comp1(1) + it.data = Comp1(10) + it.entity.set("Other comp") + it.entity.add() + it.data shouldBe Comp1(10) + count++ + } + } + count shouldBe 1 + } + + @OptIn(UnsafeAccessors::class) + @Test + fun `should allow data modify when entity changed archetype by removing`() { + resetEngine() + entity { + set(Comp1(1)) + } + + var count = 0 + MyQuery().run { + forEach { + it.data shouldBe Comp1(1) + it.entity.remove() + it.data = Comp1(10) + it.data shouldBe Comp1(10) + it.entity.set("Other comp") + it.data shouldBe Comp1(10) + count++ + } + } + count shouldBe 1 + } + +} diff --git a/gradle.properties b/gradle.properties index 92674803b..ff53eb6fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.mineinabyss -version=0.22 +version=0.23 # Workaround for dokka builds failing on CI, see https://github.com/Kotlin/dokka/issues/1405 -org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m -idofrontVersion=0.18.10 +#org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m +idofrontVersion=0.18.26 diff --git a/gradle/mylibs.versions.toml b/gradle/mylibs.versions.toml index 7ffe223e1..fae9fb6f8 100644 --- a/gradle/mylibs.versions.toml +++ b/gradle/mylibs.versions.toml @@ -1,11 +1,11 @@ [versions] -atomicfu = "0.20.0" +atomicfu = "0.22.0" [libraries] atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } -uuid = "com.benasher44:uuid:0.5.0" -okio = "com.squareup.okio:okio:3.2.0" -kds = "com.soywiz.korlibs.kds:kds:4.0.0-alpha-1" +uuid = "com.benasher44:uuid:0.8.1" +okio = "com.squareup.okio:okio:3.5.0" +kds = "com.soywiz.korlibs.kds:kds:4.0.10" roaringbitmap = "org.roaringbitmap:RoaringBitmap:0.9.32" bitvector = "net.onedaybeard.bitvector:bitvector-js:0.1.4" kermit = "co.touchlab:kermit:1.2.2" diff --git a/settings.gradle.kts b/settings.gradle.kts index 9ade99fe2..3bea7cdca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ dependencyResolutionManagement { } include( + "geary-benchmarks", "geary-core", "geary-catalog", )