diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.kt index b57cc5345..aeb4d3e00 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.kt @@ -150,4 +150,7 @@ object PylonSerializers { @JvmSynthetic internal val TICKING_BLOCK_DATA = TickingBlockPersistentDataType + @JvmSynthetic + internal val TICKING_ENTITY_DATA = TickingEntityPersistentDataType + } diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/TickingEntityPersistentDataType.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/TickingEntityPersistentDataType.kt new file mode 100644 index 000000000..e33289dd6 --- /dev/null +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/TickingEntityPersistentDataType.kt @@ -0,0 +1,36 @@ +package io.github.pylonmc.pylon.core.datatypes + +import io.github.pylonmc.pylon.core.entity.base.PylonTickingEntity +import io.github.pylonmc.pylon.core.util.pylonKey +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType + +internal object TickingEntityPersistentDataType : PersistentDataType { + val tickIntervalKey = pylonKey("tick_interval") + val isAsyncKey = pylonKey("is_async") + + override fun getPrimitiveType(): Class = PersistentDataContainer::class.java + + override fun getComplexType(): Class = + PylonTickingEntity.Companion.TickingEntityData::class.java + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext + ): PylonTickingEntity.Companion.TickingEntityData { + val tickInterval = primitive.get(tickIntervalKey, PersistentDataType.INTEGER)!! + val isAsync = primitive.get(isAsyncKey, PersistentDataType.BOOLEAN)!! + return PylonTickingEntity.Companion.TickingEntityData(tickInterval, isAsync, null) + } + + override fun toPrimitive( + complex: PylonTickingEntity.Companion.TickingEntityData, + context: PersistentDataAdapterContext + ): PersistentDataContainer { + val pdc = context.newPersistentDataContainer() + pdc.set(tickIntervalKey, PersistentDataType.INTEGER, complex.tickInterval) + pdc.set(isAsyncKey, PersistentDataType.BOOLEAN, complex.isAsync) + return pdc + } +} \ No newline at end of file diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityListener.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityListener.kt index 7cd268bae..b43a21272 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityListener.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityListener.kt @@ -807,7 +807,17 @@ internal object EntityListener : Listener { @JvmSynthetic internal fun logEventHandleErr(event: Event, e: Exception, entity: PylonEntity<*>) { - PylonCore.logger.severe("Error when handling entity(${entity.key}, ${entity.uuid}, ${entity.entity.location}) event handler ${event.javaClass.simpleName}: ${e.localizedMessage}") + PylonCore.logger.severe("Error when handling entity(${entity.key}, ${entity.uuid}, ${entity.entity.location}) event handler ${event?.javaClass?.simpleName}: ${e.localizedMessage}") + e.printStackTrace() + entityErrMap[entity.uuid] = entityErrMap[entity.uuid]?.plus(1) ?: 1 + if (entityErrMap[entity.uuid]!! > PylonConfig.ALLOWED_ENTITY_ERRORS) { + entity.entity.remove() + } + } + + @JvmSynthetic + internal fun logEventHandleErrTicking(e: Exception, entity: PylonEntity<*>) { + PylonCore.logger.severe("Error when handling ticking entity(${entity.key}, ${entity.uuid}, ${entity.entity.location}): ${e.localizedMessage}") e.printStackTrace() entityErrMap[entity.uuid] = entityErrMap[entity.uuid]?.plus(1) ?: 1 if (entityErrMap[entity.uuid]!! > PylonConfig.ALLOWED_ENTITY_ERRORS) { diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityStorage.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityStorage.kt index 54960b2ca..7af47c1e5 100644 --- a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityStorage.kt +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/EntityStorage.kt @@ -7,6 +7,7 @@ import io.github.pylonmc.pylon.core.PylonCore import io.github.pylonmc.pylon.core.addon.PylonAddon import io.github.pylonmc.pylon.core.block.BlockStorage import io.github.pylonmc.pylon.core.config.PylonConfig +import io.github.pylonmc.pylon.core.event.PylonEntityAddEvent import io.github.pylonmc.pylon.core.event.PylonEntityDeathEvent import io.github.pylonmc.pylon.core.event.PylonEntityLoadEvent import io.github.pylonmc.pylon.core.event.PylonEntityUnloadEvent @@ -19,7 +20,7 @@ import org.bukkit.entity.Entity import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.world.EntitiesLoadEvent -import java.util.UUID +import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.function.Consumer @@ -194,6 +195,7 @@ object EntityStorage : Listener { delay(PylonConfig.ENTITY_DATA_AUTOSAVE_INTERVAL_SECONDS * 1000) } } + PylonEntityAddEvent(entity).callEvent() entity } diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/base/PylonTickingEntity.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/base/PylonTickingEntity.kt new file mode 100644 index 000000000..ba5899274 --- /dev/null +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/entity/base/PylonTickingEntity.kt @@ -0,0 +1,150 @@ +package io.github.pylonmc.pylon.core.entity.base + +import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher +import com.github.shynixn.mccoroutine.bukkit.launch +import com.github.shynixn.mccoroutine.bukkit.minecraftDispatcher +import com.github.shynixn.mccoroutine.bukkit.ticks +import io.github.pylonmc.pylon.core.PylonCore +import io.github.pylonmc.pylon.core.config.PylonConfig +import io.github.pylonmc.pylon.core.datatypes.PylonSerializers +import io.github.pylonmc.pylon.core.entity.EntityListener +import io.github.pylonmc.pylon.core.entity.PylonEntity +import io.github.pylonmc.pylon.core.event.PylonEntityAddEvent +import io.github.pylonmc.pylon.core.event.PylonEntityDeathEvent +import io.github.pylonmc.pylon.core.event.PylonEntityLoadEvent +import io.github.pylonmc.pylon.core.event.PylonEntityUnloadEvent +import io.github.pylonmc.pylon.core.util.pylonKey +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.jetbrains.annotations.ApiStatus +import java.util.* + +/** + * Represents an entity that 'ticks' (does something at a fixed time interval). + */ +interface PylonTickingEntity { + + private val tickingData: TickingEntityData + get() = tickingEntities.getOrPut(this) { TickingEntityData( + PylonConfig.DEFAULT_TICK_INTERVAL, + false, + null + )} + + /** + * The interval at which the [tick] function is called. You should generally use [setTickInterval] + * in your place constructor instead of overriding this. + */ + val tickInterval + get() = tickingData.tickInterval + + /** + * Whether the [tick] function should be called asynchronously. You should generally use + * [setAsync] in your place constructor instead of overriding this. + */ + val isAsync + get() = tickingData.isAsync + + /** + * Sets how often the [tick] function should be called (in Minecraft ticks) + */ + fun setTickInterval(tickInterval: Int) { + tickingData.tickInterval = tickInterval + } + + /** + * Sets whether the [tick] function should be called asynchronously. + * + * WARNING: Setting an entity to tick asynchronously could have unintended consequences. + * + * Only set this option if you understand what 'asynchronous' means, and note that you + * cannot interact with the world asynchronously. + */ + fun setAsync(isAsync: Boolean) { + tickingData.isAsync = isAsync + } + + /** + * The function that should be called periodically. + */ + fun tick() + + @ApiStatus.Internal + companion object : Listener { + + data class TickingEntityData( + var tickInterval: Int, + var isAsync: Boolean, + var job: Job?, + ) + + private val tickingEntityKey = pylonKey("ticking_entity_data") + + private val tickingEntities = IdentityHashMap() + + @EventHandler + private fun onSerialize(event: PylonEntityAddEvent) { //todo serialize + val entity = event.pylonEntity + if (entity is PylonTickingEntity) { + entity.entity.persistentDataContainer.set(tickingEntityKey, PylonSerializers.TICKING_ENTITY_DATA, tickingEntities[entity]!!) + } + } + + @EventHandler + private fun onUnload(event: PylonEntityUnloadEvent) { + val entity = event.pylonEntity + if (entity is PylonTickingEntity) { + tickingEntities.remove(entity)?.job?.cancel() + } + } + + @EventHandler + private fun onDeath(event: PylonEntityDeathEvent) { + val entity = event.pylonEntity + if (entity is PylonTickingEntity) { + tickingEntities.remove(entity)?.job?.cancel() + } + } + + @EventHandler + private fun onEntityLoad(event: PylonEntityLoadEvent) { + val entity = event.pylonEntity + if (entity is PylonTickingEntity) { + startTicker(entity) + } + } + + /** + * Returns true if the entity is still ticking, or false if the entity does + * not exist, is not a ticking entity, or has errored and been unloaded. + */ + @JvmStatic + @ApiStatus.Internal + fun isTicking(entity: PylonEntity<*>?): Boolean { + return entity is PylonTickingEntity && tickingEntities[entity]?.job?.isActive == true + } + + @JvmSynthetic + internal fun stopTicking(entity: PylonTickingEntity) { + tickingEntities[entity]?.job?.cancel() + } + + private fun startTicker(tickingEntity: PylonTickingEntity) { + val dispatcher = if (tickingEntity.isAsync) PylonCore.asyncDispatcher else PylonCore.minecraftDispatcher + tickingEntities[tickingEntity]?.job = PylonCore.launch(dispatcher) { + while (true) { + delay(tickingEntity.tickInterval.ticks) + try { + tickingEntity.tick() + } catch (e: Exception) { + PylonCore.launch(PylonCore.minecraftDispatcher) { + EntityListener.logEventHandleErrTicking(e, tickingEntity as PylonEntity<*>) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/event/PylonEntityAddEvent.kt b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/event/PylonEntityAddEvent.kt new file mode 100644 index 000000000..f6555202e --- /dev/null +++ b/pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/event/PylonEntityAddEvent.kt @@ -0,0 +1,21 @@ +package io.github.pylonmc.pylon.core.event + +import io.github.pylonmc.pylon.core.entity.PylonEntity +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when an entity is added to [io.github.pylonmc.pylon.core.entity.EntityStorage], + * when this is called, the entity already spawned in the world, this only tracks when the + * pylon entity is actually added in the [io.github.pylonmc.pylon.core.entity.EntityStorage] + */ +class PylonEntityAddEvent(val pylonEntity: PylonEntity<*>) : Event() { + + override fun getHandlers(): HandlerList + = handlerList + + companion object { + @JvmStatic + val handlerList: HandlerList = HandlerList() + } +} \ No newline at end of file