From eca7805ab0ae0e0a37533fb1a52388c117eb12cf Mon Sep 17 00:00:00 2001 From: Kamigen <46357922+Edouard127@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:23:22 -0400 Subject: [PATCH 01/12] Destroyable listener --- .../com/lambda/event/listener/SafeListener.kt | 49 +++++++++++++++++++ .../lambda/event/listener/UnsafeListener.kt | 46 +++++++++++++++++ .../kotlin/com/lambda/util/SelfReference.kt | 10 ++++ 3 files changed, 105 insertions(+) create mode 100644 common/src/main/kotlin/com/lambda/util/SelfReference.kt diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index 4cd9b0cbc..67d149785 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -7,6 +7,7 @@ import com.lambda.event.Muteable import com.lambda.task.Task import com.lambda.threading.runConcurrent import com.lambda.threading.runSafe +import com.lambda.util.selfReference /** @@ -87,6 +88,54 @@ class SafeListener( return listener } + /** + * This function registers a new [SafeListener] for a generic [Event] type [T]. + * The [function] is executed on the same thread where the [Event] was dispatched. + * The [function] will only be executed when the context satisfies certain safety conditions. + * These conditions are met when none of the following [SafeContext] properties are null: + * - [SafeContext.world] + * - [SafeContext.player] + * - [SafeContext.interaction] + * - [SafeContext.connection] + * + * This typically occurs when the user is in-game. + * + * After the [function] is executed once, the [SafeListener] will be automatically unsubscribed. + * + * Usage: + * ```kotlin + * listenerOnce { event -> + * player.sendMessage("Event received: $event") + * } + * + * listenerOnce(priority = 1) { event -> + * player.sendMessage("Event received before the previous listener: $event") + * } + * ``` + * + * @param T The type of the event to listen for. This should be a subclass of Event. + * @param priority The priority of the listener. Listeners with higher priority will be executed first. The Default value is 0. + * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. + * @param function The function to be executed when the event is posted. This function should take a SafeContext and an event of type T as parameters. + * @return The newly created and registered [SafeListener]. + */ + inline fun Any.listenOnce( + priority: Int = 0, + alwaysListen: Boolean = false, + noinline function: SafeContext.(T) -> Unit, + ): SafeListener { + val destroyable by selfReference { + SafeListener(priority, this@listenOnce, alwaysListen) { event -> + function(event as T) + EventFlow.syncListeners.unsubscribe(self) + } + } + + EventFlow.syncListeners.subscribe(destroyable) + + return destroyable + } + /** * Registers a new [SafeListener] for a generic [Event] type [T] within the context of a [Task]. * The [function] is executed on the same thread where the [Event] was dispatched. diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index 26147cd9d..f139d1b94 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -5,7 +5,9 @@ import com.lambda.event.Event import com.lambda.event.EventFlow import com.lambda.event.Muteable import com.lambda.event.listener.SafeListener.Companion.concurrentListener +import com.lambda.event.listener.SafeListener.Companion.listenOnce import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.util.selfReference /** * An [UnsafeListener] is a specialized type of [Listener] that operates without a [SafeContext]. @@ -79,6 +81,50 @@ class UnsafeListener( return listener } + /** + * Registers a new [UnsafeListener] for a generic [Event] type [T]. + * The [function] is executed only once when the [Event] is dispatched. + * This function should only be used when the [function] performs read actions on the game data. + * For only in-game related contexts, use the [SafeListener.listenOnce] function instead. + * The listener will be automatically unsubscribed after the first execution. + * This function is useful for one-time event handling. + * + * Usage: + * ```kotlin + * unsafeListenOnce { event -> + * println("Unsafe event received only once: $event") + * } + * + * unsafeListenOnce(priority = 1) { event -> + * println("Unsafe event received only once before the previous listener: $event") + * } + * ``` + * + * After the [function] is executed once, the [SafeListener] will be automatically unsubscribed. + * + * @param T The type of the event to listen for. This should be a subclass of Event. + * @param priority The priority of the listener. Listeners with higher priority will be executed first. The Default value is 0. + * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. + * @param function The function to be executed when the event is posted. This function should take an event of type T as a parameter. + * @return The newly created and registered [UnsafeListener]. + */ + inline fun Any.unsafeListenOnce( + priority: Int = 0, + alwaysListen: Boolean = false, + noinline function: (T) -> Unit, + ): UnsafeListener { + val destroyable by selfReference { + UnsafeListener(priority, this@unsafeListenOnce, alwaysListen) { event -> + function(event as T) + EventFlow.syncListeners.unsubscribe(self) + } + } + + EventFlow.syncListeners.subscribe(destroyable) + + return destroyable + } + /** * Registers a new [UnsafeListener] for a generic [Event] type [T]. * The [function] is executed on a new thread running asynchronously to the game thread. diff --git a/common/src/main/kotlin/com/lambda/util/SelfReference.kt b/common/src/main/kotlin/com/lambda/util/SelfReference.kt new file mode 100644 index 000000000..c1cc3f018 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/SelfReference.kt @@ -0,0 +1,10 @@ +package com.lambda.util + +class SelfReference(initializer: SelfReference.() -> T) { + val self: T by lazy { inner ?: throw IllegalStateException("Do not use `self` until initialized.") } + + private val inner = initializer() + operator fun getValue(thisRef: Any?, property: Any?) = self +} + +fun selfReference(initializer: SelfReference.() -> T): SelfReference = SelfReference(initializer) From 8598570f76d36fc48313790ca3ded6f8ae436d4a Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:51:28 -0400 Subject: [PATCH 02/12] todo: delegate listeners --- .../main/kotlin/com/lambda/event/EventFlow.kt | 133 +++++++++++++++--- .../kotlin/com/lambda/event/Subscriber.kt | 34 ++--- .../com/lambda/event/listener/Listener.kt | 18 +-- .../com/lambda/event/listener/SafeListener.kt | 68 +++++---- .../lambda/event/listener/UnsafeListener.kt | 57 +++++--- 5 files changed, 216 insertions(+), 94 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/EventFlow.kt b/common/src/main/kotlin/com/lambda/event/EventFlow.kt index b14290673..9f5daecc1 100644 --- a/common/src/main/kotlin/com/lambda/event/EventFlow.kt +++ b/common/src/main/kotlin/com/lambda/event/EventFlow.kt @@ -5,9 +5,20 @@ import com.lambda.event.callback.ICancellable import com.lambda.event.listener.Listener import com.lambda.threading.runConcurrent import com.lambda.threading.runSafe -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout /** @@ -28,12 +39,35 @@ object EventFlow { * useful when you have multiple independent [Job]s running in parallel. */ val lambdaScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + /** + * [concurrentFlow] is a [MutableSharedFlow] of [Event]s with a buffer capacity to handle event emissions. + * + * Events emitted to this flow are processed by concurrent listeners, allowing for parallel event handling. + * + * The buffer overflow strategy is set to [BufferOverflow.DROP_OLDEST], meaning that when the buffer is full, + * the oldest event will be dropped to accommodate a new event. + */ val concurrentFlow = MutableSharedFlow( extraBufferCapacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST ) + /** + * [syncListeners] is a [Subscriber] that manages synchronous listeners. + * + * These listeners will be executed immediately when an event is posted, allowing for immediate responses to events. + * The [syncListeners] are stored in a [Subscriber] object, which is a specialized [ConcurrentHashMap] that manages sets of [Listener]s for different [Event] types. + */ val syncListeners = Subscriber() + + /** + * [concurrentListeners] is a [Subscriber] that manages asynchronous listeners. + * + * These listeners will be executed in parallel, each on a dedicated coroutine, + * allowing for concurrent processing of events. + * The [concurrentListeners] are stored in a [Subscriber] object, which is a specialized [ConcurrentHashMap] that manages sets of [Listener]s for different [Event] types. + */ val concurrentListeners = Subscriber() init { @@ -49,7 +83,14 @@ object EventFlow { } } - suspend inline fun awaitEvent( + /** + * Suspends until an event of type [E] is received that satisfies the given [predicate]. + * + * @param E The type of the event to wait for. This should be a subclass of [Event]. + * @param predicate A lambda to test if the event satisfies the condition. + * @return The first event that matches the predicate. + */ + suspend inline fun blockUntilEvent( noinline predicate: SafeContext.(E) -> Boolean = { true }, ) = concurrentFlow.filterIsInstance().first { runSafe { @@ -57,20 +98,45 @@ object EventFlow { } ?: false } - suspend inline fun awaitEventUnsafe( - noinline predicate: (E) -> Boolean = { true }, - ) = concurrentFlow.filterIsInstance().first(predicate) - - suspend inline fun awaitEvent( + /** + * Suspends until an event of type [E] is received that satisfies the given [predicate], + * or until the specified [timeout] occurs. + * + * @param E The type of the event to wait for. This should be a subclass of [Event]. + * @param timeout The maximum time to wait for the event, in milliseconds. + * @param predicate A lambda to test if the event satisfies the condition. + * @return The first event that matches the predicate or throws a timeout exception if not found. + */ + suspend inline fun blockUntilEvent( timeout: Long, noinline predicate: (E) -> Boolean = { true }, ) = runBlocking { - withTimeout(timeout) { - concurrentFlow.filterIsInstance().first(predicate) - } + withTimeout(timeout) { + concurrentFlow.filterIsInstance().first(predicate) } + } - suspend inline fun awaitEvents( + /** + * Suspends until an event of type [E] is received that satisfies the given [predicate]. + * + * This method is "unsafe" in the sense that it does not execute the predicate within a [SafeContext]. + * + * @param E The type of the event to wait for. This should be a subclass of [Event]. + * @param predicate A lambda to test if the event satisfies the condition. + * @return The first event that matches the predicate. + */ + suspend inline fun blockUntilUnsafeEvent( + noinline predicate: (E) -> Boolean = { true }, + ) = concurrentFlow.filterIsInstance().first(predicate) + + /** + * Returns a [Flow] of events of type [E] that satisfy the given [predicate]. + * + * @param E The type of the event to filter. This should be a subclass of [Event]. + * @param predicate A lambda to test if the event satisfies the condition. + * @return A [Flow] emitting events that match the predicate. + */ + suspend inline fun collectEvents( crossinline predicate: (E) -> Boolean = { true }, ): Flow = flow { concurrentFlow @@ -149,23 +215,58 @@ object EventFlow { concurrentListeners.remove(T::class) } - private fun Event.executeListenerSynchronous() { + /** + * Executes the listeners for the current event type synchronously. + * + * This method retrieves the list of synchronous listeners for the event's class + * and invokes their [Listener.execute] method if the listener should be notified. + * + * @receiver The current event for which listeners are to be executed. + * @param T The type of the event being handled. + */ + private fun T.executeListenerSynchronous() { syncListeners[this::class]?.forEach { listener -> + @Suppress("UNCHECKED_CAST") + listener as? Listener ?: return@forEach if (shouldNotNotify(listener, this)) return@forEach listener.execute(this) } } - private fun Event.executeListenerConcurrently() { + /** + * Executes the listeners for the current event type concurrently. + * + * This method retrieves the list of concurrent listeners for the event's class + * and invokes their [Listener.execute] method if the listener should be notified. + * Each listener is executed on the same coroutine scope. + * + * @receiver The current event for which listeners are to be executed. + * @param T The type of the event being handled. + */ + private fun T.executeListenerConcurrently() { concurrentListeners[this::class]?.forEach { listener -> + @Suppress("UNCHECKED_CAST") + listener as? Listener ?: return@forEach if (shouldNotNotify(listener, this)) return@forEach listener.execute(this) } } - private fun shouldNotNotify(listener: Listener, event: Event) = + /** + * Determines whether a given [listener] should be notified about an [event]. + * + * A listener should not be notified if: + * - The listener's owner is a [Muteable] and is currently muted, unless the listener is set to [alwaysListen]. + * - The event is cancellable and has been canceled. + * + * @param listener The listener to check. + * @param event The event being processed. + * @param T The type of the event. + * @return `true` if the listener should not be notified, `false` otherwise. + */ + private fun shouldNotNotify(listener: Listener, event: Event) = listener.owner is Muteable && (listener.owner as Muteable).isMuted && !listener.alwaysListen || event is ICancellable && event.isCanceled() -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/event/Subscriber.kt b/common/src/main/kotlin/com/lambda/event/Subscriber.kt index ae4a7b6f5..cd935ea15 100644 --- a/common/src/main/kotlin/com/lambda/event/Subscriber.kt +++ b/common/src/main/kotlin/com/lambda/event/Subscriber.kt @@ -14,26 +14,15 @@ import kotlin.reflect.KClass * * @property defaultListenerSet A [ConcurrentSkipListSet] of [Listener]s, sorted in reverse order. */ -class Subscriber : ConcurrentHashMap, ConcurrentSkipListSet>() { - val defaultListenerSet: ConcurrentSkipListSet - get() = ConcurrentSkipListSet(Comparator.reverseOrder()) +class Subscriber : ConcurrentHashMap, ConcurrentSkipListSet>>() { + val defaultListenerSet: ConcurrentSkipListSet> + get() = ConcurrentSkipListSet(compareByDescending> { it.priority }.thenBy { it.hashCode() }) // TODO: Fix this /** Allows a [Listener] to start receiving a specific type of [Event] */ - inline fun subscribe(listener: Listener) = + inline fun subscribe(listener: Listener) = getOrPut(T::class) { defaultListenerSet }.add(listener) - - /** Forgets about every [Listener]s association to [eventType] */ - fun unsubscribe(eventType: KClass<*>) = remove(eventType) - - /** Allows a [Listener] to stop receiving a specific type of [Event] */ - fun unsubscribe(listener: Listener) { - values.forEach { listeners -> - listeners.remove(listener) - } - } - /** Allows a [Subscriber] to start receiving all [Event]s of another [Subscriber]. */ infix fun subscribe(subscriber: Subscriber) { subscriber.forEach { (eventType, listeners) -> @@ -41,11 +30,18 @@ class Subscriber : ConcurrentHashMap, ConcurrentSkipListSet> } } + /** Forgets about every [Listener]'s association to [eventType] */ + fun unsubscribe(eventType: KClass) = + remove(eventType) + + /** Allows a [Listener] to stop receiving a specific type of [Event] */ + inline fun unsubscribe(listener: Listener) = + getOrElse(T::class) { defaultListenerSet }.remove(listener) + /** Allows a [Subscriber] to stop receiving all [Event]s of another [Subscriber] */ infix fun unsubscribe(subscriber: Subscriber) { - entries.removeAll { (eventType, listeners) -> - subscriber[eventType]?.let { listeners.removeAll(it) } - listeners.isEmpty() + subscriber.forEach { (eventType, listeners) -> + getOrElse(eventType) { defaultListenerSet }.removeAll(listeners) } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/event/listener/Listener.kt b/common/src/main/kotlin/com/lambda/event/listener/Listener.kt index ec4868b3e..a2f2ab050 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/Listener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/Listener.kt @@ -27,7 +27,7 @@ import com.lambda.module.Module * @property owner The owner of the [Listener]. This is typically the object that created the [Listener]. * @property alwaysListen If true, the [Listener] will always be triggered, even if the [owner] is [Muteable.isMuted]. */ -abstract class Listener : Comparable { +abstract class Listener : Comparable> { abstract val priority: Int abstract val owner: Any abstract val alwaysListen: Boolean @@ -37,21 +37,13 @@ abstract class Listener : Comparable { * * @param event The event that triggered this listener. */ - abstract fun execute(event: Event) + abstract fun execute(event: T) - /** - * Compares this listener with another listener. - * The comparison is based first on the priority, and then on the hash code of the listeners. - * - * @param other The other listener to compare with. - * @return A negative integer, zero, or a positive integer as this listener is less than, equal to, - * or greater than the specified listener. - */ - override fun compareTo(other: Listener) = - compareBy { + override fun compareTo(other: Listener) = + compareBy> { it.priority }.thenBy { // Needed because ConcurrentSkipListSet handles insertion based on compareTo it.hashCode() }.compare(this, other) -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index 67d149785..cda8cb5e2 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -20,18 +20,33 @@ import com.lambda.util.selfReference * The [SafeListener] class is used to create [Listener]s that execute a given [function] within a [SafeContext]. * This ensures that the [function] is executed in a context where certain safety conditions are met. * + * The [SafeListener] will keep a reference to the last signal processed by the listener. + * Allowing use cases where the last signal is needed. + * ```kotlin + * val lastPacketReceived by listener() + * + * listener { event -> + * println("Last packet received: ${lastPacketReceived?.packet}") + * // prints the last packet received + * // prints null if no packet was received + * } + * ``` + * * @property priority The priority of the listener. Listeners with higher priority are executed first. * @property owner The owner of the listener. This is typically the object that created the listener. * @property alwaysListen If true, the listener will always be triggered, even if the owner is not enabled. * @property function The function to be executed when the event occurs. This function operates within a [SafeContext]. */ -class SafeListener( +class SafeListener( override val priority: Int = 0, override val owner: Any, override val alwaysListen: Boolean = false, - val function: SafeContext.(Event) -> Unit, -) : Listener() { - override fun execute(event: Event) { + val function: SafeContext.(T) -> Unit, +) : Listener() { + private var lastSignal: T? = null + operator fun getValue(thisRef: Any?, property: Any?): T? = lastSignal + + override fun execute(event: T) { runSafe { // if (!mc.isOnThread) { // LOG.warn(""" @@ -40,6 +55,8 @@ class SafeListener( // Consider moving the execution to the game thread using runSafeOnGameThread { ... } or runOnGameThread { ... }. // """.trimIndent()) // } + + lastSignal = event function(event) } } @@ -77,13 +94,11 @@ class SafeListener( inline fun Any.listener( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: SafeContext.(T) -> Unit, - ): SafeListener { - val listener = SafeListener(priority, this, alwaysListen) { event -> - function(event as T) - } + noinline function: SafeContext.(T) -> Unit = {}, + ): SafeListener { + val listener = SafeListener(priority, this, alwaysListen) { event -> function(event) } - EventFlow.syncListeners.subscribe(listener) + EventFlow.syncListeners.subscribe(listener) return listener } @@ -119,15 +134,18 @@ class SafeListener( * @param function The function to be executed when the event is posted. This function should take a SafeContext and an event of type T as parameters. * @return The newly created and registered [SafeListener]. */ - inline fun Any.listenOnce( + inline fun Any.receiveNext( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: SafeContext.(T) -> Unit, - ): SafeListener { - val destroyable by selfReference { - SafeListener(priority, this@listenOnce, alwaysListen) { event -> - function(event as T) - EventFlow.syncListeners.unsubscribe(self) + noinline function: SafeContext.(T) -> Unit = {}, + noinline predicate: SafeContext.(T) -> Boolean = { true }, + ): SafeListener { + val destroyable by selfReference> { + SafeListener(priority, this@receiveNext, alwaysListen) { event -> + if (predicate(event)) { + function(event) + EventFlow.syncListeners.unsubscribe(self) + } } } @@ -170,10 +188,10 @@ class SafeListener( inline fun Task<*>.listener( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: SafeContext.(T) -> Unit, - ): SafeListener { - val listener = SafeListener(priority, this, alwaysListen) { event -> - function(event as T) // ToDo: run function always on game thread + noinline function: SafeContext.(T) -> Unit = {}, + ): SafeListener { + val listener = SafeListener(priority, this, alwaysListen) { event -> + function(event) // ToDo: run function always on game thread } syncListeners.subscribe(listener) @@ -209,11 +227,11 @@ class SafeListener( inline fun Any.concurrentListener( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: suspend SafeContext.(T) -> Unit, - ): SafeListener { - val listener = SafeListener(priority, this, alwaysListen) { event -> + noinline function: suspend SafeContext.(T) -> Unit = {}, + ): SafeListener { + val listener = SafeListener(priority, this, alwaysListen) { event -> runConcurrent { - function(event as T) + function(event) } } diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index f139d1b94..d6cdad17d 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -5,8 +5,8 @@ import com.lambda.event.Event import com.lambda.event.EventFlow import com.lambda.event.Muteable import com.lambda.event.listener.SafeListener.Companion.concurrentListener -import com.lambda.event.listener.SafeListener.Companion.listenOnce import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.event.listener.SafeListener.Companion.receiveNext import com.lambda.util.selfReference /** @@ -19,18 +19,33 @@ import com.lambda.util.selfReference * The [UnsafeListener] class is used to create [Listener]s that execute a given [function] without a [SafeContext]. * This means that the [function] is executed in a context where certain safety conditions may not be met. * + * The [SafeListener] will keep a reference to the last signal processed by the listener. + * Allowing use cases where the last signal is needed. + * ```kotlin + * val lastPacketReceived by unsafeListener() + * + * unsafeListener { event -> + * println("Last packet received: ${lastPacketReceived?.packet}") + * // prints the last packet received + * // prints null if no packet was received + * } + * ``` + * * @property priority The priority of the listener. Listeners with higher priority are executed first. * @property owner The owner of the listener. This is typically the object that created the listener. * @property alwaysListen If true, the listener will always be triggered, even if the owner is not enabled. * @property function The function to be executed when the event occurs. This function operates without a [SafeContext]. */ -class UnsafeListener( +class UnsafeListener( override val priority: Int, override val owner: Any, override val alwaysListen: Boolean = false, - val function: (Event) -> Unit, -) : Listener() { - override fun execute(event: Event) { + val function: (T) -> Unit, +) : Listener() { + private var lastSignal: T? = null + operator fun getValue(thisRef: Any?, property: Any?): T? = lastSignal + + override fun execute(event: T) { // if (!mc.isOnThread) { // LOG.warn(""" // Event ${this::class.simpleName} executed outside the game thread. @@ -38,6 +53,8 @@ class UnsafeListener( // Consider moving the execution to the game thread using runSafeOnGameThread { ... } or runOnGameThread { ... }. // """.trimIndent()) // } + + lastSignal = event function(event) } @@ -70,10 +87,10 @@ class UnsafeListener( inline fun Any.unsafeListener( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: (T) -> Unit, - ): UnsafeListener { - val listener = UnsafeListener(priority, this, alwaysListen) { event -> - function(event as T) + noinline function: (T) -> Unit = {}, + ): UnsafeListener { + val listener = UnsafeListener(priority, this, alwaysListen) { event -> + function(event) } EventFlow.syncListeners.subscribe(listener) @@ -85,7 +102,7 @@ class UnsafeListener( * Registers a new [UnsafeListener] for a generic [Event] type [T]. * The [function] is executed only once when the [Event] is dispatched. * This function should only be used when the [function] performs read actions on the game data. - * For only in-game related contexts, use the [SafeListener.listenOnce] function instead. + * For only in-game related contexts, use the [SafeListener.receiveNext] function instead. * The listener will be automatically unsubscribed after the first execution. * This function is useful for one-time event handling. * @@ -108,14 +125,14 @@ class UnsafeListener( * @param function The function to be executed when the event is posted. This function should take an event of type T as a parameter. * @return The newly created and registered [UnsafeListener]. */ - inline fun Any.unsafeListenOnce( + inline fun Any.unsafeReceiveNext( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: (T) -> Unit, - ): UnsafeListener { - val destroyable by selfReference { - UnsafeListener(priority, this@unsafeListenOnce, alwaysListen) { event -> - function(event as T) + noinline function: (T) -> Unit = {}, + ): UnsafeListener { + val destroyable by selfReference> { + UnsafeListener(priority, this@unsafeReceiveNext, alwaysListen) { event -> + function(event) EventFlow.syncListeners.unsubscribe(self) } } @@ -154,11 +171,9 @@ class UnsafeListener( inline fun Any.unsafeConcurrentListener( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: (T) -> Unit, - ): UnsafeListener { - val listener = UnsafeListener(priority, this, alwaysListen) { event -> - function(event as T) - } + noinline function: (T) -> Unit = {}, + ): UnsafeListener { + val listener = UnsafeListener(priority, this, alwaysListen) { event -> function(event) } EventFlow.concurrentListeners.subscribe(listener) From 9db010b6de22de98e0fe7666d51351a22cdf36e1 Mon Sep 17 00:00:00 2001 From: Kamigen <46357922+Edouard127@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:54:41 -0400 Subject: [PATCH 03/12] Better destroyable listeners --- .../kotlin/com/lambda/event/listener/SafeListener.kt | 10 ++++------ .../com/lambda/event/listener/UnsafeListener.kt | 11 ++++------- .../src/main/kotlin/com/lambda/util/SelfReference.kt | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index cda8cb5e2..63a575068 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -119,12 +119,10 @@ class SafeListener( * * Usage: * ```kotlin - * listenerOnce { event -> - * player.sendMessage("Event received: $event") - * } - * - * listenerOnce(priority = 1) { event -> - * player.sendMessage("Event received before the previous listener: $event") + * private val event by receiveNext { event -> + * player.sendMessage("Event received only once: $event") + * // event is stored in the value + * // event is unsubscribed after execution * } * ``` * diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index d6cdad17d..0b1cb2ddb 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -103,17 +103,14 @@ class UnsafeListener( * The [function] is executed only once when the [Event] is dispatched. * This function should only be used when the [function] performs read actions on the game data. * For only in-game related contexts, use the [SafeListener.receiveNext] function instead. - * The listener will be automatically unsubscribed after the first execution. - * This function is useful for one-time event handling. * * Usage: * ```kotlin - * unsafeListenOnce { event -> + * private val event by unsafeReceiveNext { event -> * println("Unsafe event received only once: $event") - * } - * - * unsafeListenOnce(priority = 1) { event -> - * println("Unsafe event received only once before the previous listener: $event") + * // no safe access to player or world + * // event is stored in the value + * // event is unsubscribed after execution * } * ``` * diff --git a/common/src/main/kotlin/com/lambda/util/SelfReference.kt b/common/src/main/kotlin/com/lambda/util/SelfReference.kt index c1cc3f018..2392316bd 100644 --- a/common/src/main/kotlin/com/lambda/util/SelfReference.kt +++ b/common/src/main/kotlin/com/lambda/util/SelfReference.kt @@ -1,7 +1,7 @@ package com.lambda.util class SelfReference(initializer: SelfReference.() -> T) { - val self: T by lazy { inner ?: throw IllegalStateException("Do not use `self` until initialized.") } + val self: T by lazy { inner } private val inner = initializer() operator fun getValue(thisRef: Any?, property: Any?) = self From aa2c22576a5a3702feb9dbae0276375809b74b3f Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:56:39 -0400 Subject: [PATCH 04/12] unsafe rcv next predicate --- .../kotlin/com/lambda/event/listener/UnsafeListener.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index 0b1cb2ddb..c04cf059b 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -126,11 +126,14 @@ class UnsafeListener( priority: Int = 0, alwaysListen: Boolean = false, noinline function: (T) -> Unit = {}, + noinline predicate: (T) -> Boolean = { true }, ): UnsafeListener { val destroyable by selfReference> { UnsafeListener(priority, this@unsafeReceiveNext, alwaysListen) { event -> - function(event) - EventFlow.syncListeners.unsubscribe(self) + if (predicate(event)) { + function(event) + EventFlow.syncListeners.unsubscribe(self) + } } } From 8c979291236a30ce1a81363bafb672bb04083415 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:26:36 -0400 Subject: [PATCH 05/12] Bump Forge New bootstrap method see https://forums.minecraftforge.net/topic/149456-forge-491-minecraft-1204/ --- .kotlin/errors/errors-1723508718868.log | 42 +++++++++++++++++++++++++ gradle.properties | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .kotlin/errors/errors-1723508718868.log diff --git a/.kotlin/errors/errors-1723508718868.log b/.kotlin/errors/errors-1723508718868.log new file mode 100644 index 000000000..7b2afe5f5 --- /dev/null +++ b/.kotlin/errors/errors-1723508718868.log @@ -0,0 +1,42 @@ +kotlin version: 2.0.0 +error message: java.io.FileNotFoundException: C:\Users\Kamigen\Desktop\BetterElytraBot\NeoLambda\common\src\main\kotlin\com\lambda\module\modules\movement\ElytraFly.kt (Le fichier spécifié est introuvable) + at java.base/java.io.FileInputStream.open0(Native Method) + at java.base/java.io.FileInputStream.open(FileInputStream.java:213) + at java.base/java.io.FileInputStream.(FileInputStream.java:152) + at org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalVirtualFile.getInputStream(CoreLocalVirtualFile.java:117) + at org.jetbrains.kotlin.KtVirtualFileSourceFile.getContentsAsStream(KtSourceFile.kt:39) + at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildFirViaLightTree(firUtils.kt:34) + at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:87) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:314) + at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:116) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:155) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:50) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:104) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:48) + at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:453) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:62) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:676) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92) + at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1661) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) + at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) + at java.base/java.lang.Thread.run(Thread.java:1583) + + diff --git a/gradle.properties b/gradle.properties index 853bead9e..49458b6cf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ fabricApiVersion=0.97.1 kotlinFabricVersion=1.11.0+kotlin.2.0.0 # Forge https://files.minecraftforge.net/ -forgeVersion=49.0.31 +forgeVersion=49.1.4 forgeVersionMin=44 forgeVersionMax=49.999.0 kotlinForgeVersion=4.11.0 From 16d2b128f53833ea1e3bc9c709f6bad17c9fd49f Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:28:56 -0400 Subject: [PATCH 06/12] Delete errors-1723508718868.log --- .kotlin/errors/errors-1723508718868.log | 42 ------------------------- 1 file changed, 42 deletions(-) delete mode 100644 .kotlin/errors/errors-1723508718868.log diff --git a/.kotlin/errors/errors-1723508718868.log b/.kotlin/errors/errors-1723508718868.log deleted file mode 100644 index 7b2afe5f5..000000000 --- a/.kotlin/errors/errors-1723508718868.log +++ /dev/null @@ -1,42 +0,0 @@ -kotlin version: 2.0.0 -error message: java.io.FileNotFoundException: C:\Users\Kamigen\Desktop\BetterElytraBot\NeoLambda\common\src\main\kotlin\com\lambda\module\modules\movement\ElytraFly.kt (Le fichier spécifié est introuvable) - at java.base/java.io.FileInputStream.open0(Native Method) - at java.base/java.io.FileInputStream.open(FileInputStream.java:213) - at java.base/java.io.FileInputStream.(FileInputStream.java:152) - at org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalVirtualFile.getInputStream(CoreLocalVirtualFile.java:117) - at org.jetbrains.kotlin.KtVirtualFileSourceFile.getContentsAsStream(KtSourceFile.kt:39) - at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildFirViaLightTree(firUtils.kt:34) - at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:87) - at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:314) - at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:116) - at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:155) - at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:50) - at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:104) - at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:48) - at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:453) - at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:62) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301) - at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:676) - at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92) - at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1661) - at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) - at java.base/java.lang.reflect.Method.invoke(Method.java:580) - at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) - at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:714) - at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) - at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721) - at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) - at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720) - at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) - at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) - at java.base/java.lang.Thread.run(Thread.java:1583) - - From 1593fde707e12366405e6c1c56bff0884af66de9 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:29:33 -0400 Subject: [PATCH 07/12] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 46d92844f..b427213a2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ build/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +.kotlin/ ### Eclipse ### .apt_generated From cb7cda4cced0d594443f856cbaf5951e3d789f98 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 17 Aug 2024 20:04:39 -0400 Subject: [PATCH 08/12] feature: delegate listeners --- .../com/lambda/event/listener/SafeListener.kt | 28 +++++++++++------ .../lambda/event/listener/UnsafeListener.kt | 31 ++++++++++++------- .../main/kotlin/com/lambda/util/Pointer.kt | 16 ++++++++++ .../kotlin/com/lambda/util/SelfReference.kt | 11 +++---- 4 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/util/Pointer.kt diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index 63a575068..e20016512 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -7,7 +7,9 @@ import com.lambda.event.Muteable import com.lambda.task.Task import com.lambda.threading.runConcurrent import com.lambda.threading.runSafe +import com.lambda.util.Pointer import com.lambda.util.selfReference +import kotlin.properties.ReadWriteProperty /** @@ -105,8 +107,8 @@ class SafeListener( /** * This function registers a new [SafeListener] for a generic [Event] type [T]. - * The [function] is executed on the same thread where the [Event] was dispatched. - * The [function] will only be executed when the context satisfies certain safety conditions. + * The [transform] is executed on the same thread where the [Event] was dispatched. + * The [transform] will only be executed when the context satisfies certain safety conditions. * These conditions are met when none of the following [SafeContext] properties are null: * - [SafeContext.world] * - [SafeContext.player] @@ -115,7 +117,7 @@ class SafeListener( * * This typically occurs when the user is in-game. * - * After the [function] is executed once, the [SafeListener] will be automatically unsubscribed. + * After the [transform] is executed once, the [SafeListener] will be automatically unsubscribed. * * Usage: * ```kotlin @@ -129,19 +131,25 @@ class SafeListener( * @param T The type of the event to listen for. This should be a subclass of Event. * @param priority The priority of the listener. Listeners with higher priority will be executed first. The Default value is 0. * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. - * @param function The function to be executed when the event is posted. This function should take a SafeContext and an event of type T as parameters. + * @param transform The function used to transform the event into a value. * @return The newly created and registered [SafeListener]. */ - inline fun Any.receiveNext( + inline fun Any.receiveNext( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: SafeContext.(T) -> Unit = {}, + noinline transform: SafeContext.(T) -> E? = { null }, noinline predicate: SafeContext.(T) -> Boolean = { true }, - ): SafeListener { + ): ReadWriteProperty { + val ptr = Pointer() + val destroyable by selfReference> { SafeListener(priority, this@receiveNext, alwaysListen) { event -> - if (predicate(event)) { - function(event) + ptr.value = transform(event) + + if (predicate(event) && + ptr.value != null + ) { + val self by this@selfReference EventFlow.syncListeners.unsubscribe(self) } } @@ -149,7 +157,7 @@ class SafeListener( EventFlow.syncListeners.subscribe(destroyable) - return destroyable + return ptr } /** diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index c04cf059b..cbd5bda47 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -7,7 +7,10 @@ import com.lambda.event.Muteable import com.lambda.event.listener.SafeListener.Companion.concurrentListener import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.event.listener.SafeListener.Companion.receiveNext +import com.lambda.util.Pointer import com.lambda.util.selfReference +import net.minecraft.advancement.AdvancementRewards.Builder.function +import kotlin.properties.ReadWriteProperty /** * An [UnsafeListener] is a specialized type of [Listener] that operates without a [SafeContext]. @@ -79,8 +82,8 @@ class UnsafeListener( * ``` * * @param T The type of the event to listen for. This should be a subclass of Event. - * @param priority The priority of the listener. Listeners with higher priority will be executed first. The Default value is 0. - * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. + * @param priority The priority of the listener. Listeners with higher priority will be executed first. + * @param alwaysListen If true, the listener will be executed even if it is muted. * @param function The function to be executed when the event is posted. This function should take an event of type T as a parameter. * @return The newly created and registered [UnsafeListener]. */ @@ -117,21 +120,27 @@ class UnsafeListener( * After the [function] is executed once, the [SafeListener] will be automatically unsubscribed. * * @param T The type of the event to listen for. This should be a subclass of Event. - * @param priority The priority of the listener. Listeners with higher priority will be executed first. The Default value is 0. - * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. - * @param function The function to be executed when the event is posted. This function should take an event of type T as a parameter. + * @param priority The priority of the listener. Listeners with higher priority will be executed first. + * @param alwaysListen If true, the listener will be executed even if it is muted. + * @param transform The function used to transform the event into a value. * @return The newly created and registered [UnsafeListener]. */ - inline fun Any.unsafeReceiveNext( + inline fun Any.unsafeReceiveNext( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: (T) -> Unit = {}, + noinline transform: (T) -> E? = { null }, noinline predicate: (T) -> Boolean = { true }, - ): UnsafeListener { + ): ReadWriteProperty { + val ptr = Pointer() + val destroyable by selfReference> { UnsafeListener(priority, this@unsafeReceiveNext, alwaysListen) { event -> - if (predicate(event)) { - function(event) + ptr.value = transform(event) + + if (predicate(event) && + ptr.value != null) + { + val self by this@selfReference EventFlow.syncListeners.unsubscribe(self) } } @@ -139,7 +148,7 @@ class UnsafeListener( EventFlow.syncListeners.subscribe(destroyable) - return destroyable + return ptr } /** diff --git a/common/src/main/kotlin/com/lambda/util/Pointer.kt b/common/src/main/kotlin/com/lambda/util/Pointer.kt new file mode 100644 index 000000000..9d01f5ea9 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/Pointer.kt @@ -0,0 +1,16 @@ +package com.lambda.util + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * A class representing a pointer to a value. + * + * It is a high-level abstraction over a mutable variable that allows for easy access to the value. + */ +data class Pointer(var value: T? = null) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = value + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + this.value = value + } +} diff --git a/common/src/main/kotlin/com/lambda/util/SelfReference.kt b/common/src/main/kotlin/com/lambda/util/SelfReference.kt index 2392316bd..fd22b86ea 100644 --- a/common/src/main/kotlin/com/lambda/util/SelfReference.kt +++ b/common/src/main/kotlin/com/lambda/util/SelfReference.kt @@ -1,10 +1,9 @@ package com.lambda.util -class SelfReference(initializer: SelfReference.() -> T) { - val self: T by lazy { inner } +import kotlin.properties.ReadOnlyProperty - private val inner = initializer() - operator fun getValue(thisRef: Any?, property: Any?) = self -} +inline fun selfReference(noinline initializer: ReadOnlyProperty.() -> T) = object : ReadOnlyProperty { + val value: T by lazy { initializer() } -fun selfReference(initializer: SelfReference.() -> T): SelfReference = SelfReference(initializer) + override fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>) = value +} From 26658714807773a0bf9f5fd29cefb8f31a257124 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 17 Aug 2024 20:13:44 -0400 Subject: [PATCH 09/12] refactor: readonly property listeners --- .../com/lambda/event/listener/SafeListener.kt | 24 +++++++++++-------- .../lambda/event/listener/UnsafeListener.kt | 21 ++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index e20016512..a7d66a6b1 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -9,7 +9,9 @@ import com.lambda.threading.runConcurrent import com.lambda.threading.runSafe import com.lambda.util.Pointer import com.lambda.util.selfReference +import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty /** @@ -44,20 +46,22 @@ class SafeListener( override val owner: Any, override val alwaysListen: Boolean = false, val function: SafeContext.(T) -> Unit, -) : Listener() { +) : Listener(), ReadOnlyProperty { + /** + * The last processed event signal. + */ private var lastSignal: T? = null - operator fun getValue(thisRef: Any?, property: Any?): T? = lastSignal + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = lastSignal + + /** + * Executes the actions defined by this listener when the event occurs. + * + * Note that running this function outside the game thread can + * lead to race conditions when manipulating shared data. + */ override fun execute(event: T) { runSafe { -// if (!mc.isOnThread) { -// LOG.warn(""" -// Event ${this::class.simpleName} executed outside the game thread. -// This can lead to race conditions when manipulating shared data. -// Consider moving the execution to the game thread using runSafeOnGameThread { ... } or runOnGameThread { ... }. -// """.trimIndent()) -// } - lastSignal = event function(event) } diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index cbd5bda47..6ebbf8a6d 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -10,7 +10,9 @@ import com.lambda.event.listener.SafeListener.Companion.receiveNext import com.lambda.util.Pointer import com.lambda.util.selfReference import net.minecraft.advancement.AdvancementRewards.Builder.function +import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty /** * An [UnsafeListener] is a specialized type of [Listener] that operates without a [SafeContext]. @@ -44,19 +46,18 @@ class UnsafeListener( override val owner: Any, override val alwaysListen: Boolean = false, val function: (T) -> Unit, -) : Listener() { +) : Listener(), ReadOnlyProperty { + /** + * The last processed event signal. + */ private var lastSignal: T? = null - operator fun getValue(thisRef: Any?, property: Any?): T? = lastSignal - override fun execute(event: T) { -// if (!mc.isOnThread) { -// LOG.warn(""" -// Event ${this::class.simpleName} executed outside the game thread. -// This can lead to race conditions when manipulating game data. -// Consider moving the execution to the game thread using runSafeOnGameThread { ... } or runOnGameThread { ... }. -// """.trimIndent()) -// } + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = lastSignal + /** + * Executes the actions defined by this listener when the event occurs. + */ + override fun execute(event: T) { lastSignal = event function(event) } From 1d0e2e6f896a3780680be337e7c0ea7b41f44751 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 17 Aug 2024 20:51:43 -0400 Subject: [PATCH 10/12] style: try cast listener --- common/src/main/kotlin/com/lambda/event/EventFlow.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/EventFlow.kt b/common/src/main/kotlin/com/lambda/event/EventFlow.kt index 9f5daecc1..bda889797 100644 --- a/common/src/main/kotlin/com/lambda/event/EventFlow.kt +++ b/common/src/main/kotlin/com/lambda/event/EventFlow.kt @@ -225,9 +225,9 @@ object EventFlow { * @param T The type of the event being handled. */ private fun T.executeListenerSynchronous() { - syncListeners[this::class]?.forEach { listener -> + syncListeners[this::class]?.forEach { @Suppress("UNCHECKED_CAST") - listener as? Listener ?: return@forEach + val listener = it as? Listener ?: return@forEach if (shouldNotNotify(listener, this)) return@forEach listener.execute(this) } @@ -244,9 +244,9 @@ object EventFlow { * @param T The type of the event being handled. */ private fun T.executeListenerConcurrently() { - concurrentListeners[this::class]?.forEach { listener -> + concurrentListeners[this::class]?.forEach { @Suppress("UNCHECKED_CAST") - listener as? Listener ?: return@forEach + val listener = it as? Listener ?: return@forEach if (shouldNotNotify(listener, this)) return@forEach listener.execute(this) } From e01fd36a09f7a7e2fc8f9f6eb9228378b632c24c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 17 Aug 2024 20:56:06 -0400 Subject: [PATCH 11/12] fix: unresolved reference --- .../src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index 2fd66d769..b089e00e0 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -5,7 +5,6 @@ import com.lambda.event.Event import com.lambda.event.EventFlow import com.lambda.event.Muteable import com.lambda.event.listener.SafeListener.Companion.concurrentListener -import com.lambda.event.listener.SafeListener.Companion.listenOnce import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.event.listener.SafeListener.Companion.receiveNext import com.lambda.util.Pointer From 366e53f84fd48fd054477f3f1d3d59d0a0dabe95 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 19 Aug 2024 01:06:49 +0200 Subject: [PATCH 12/12] Reduced redundancy and better naming --- .../main/kotlin/com/lambda/event/Subscriber.kt | 2 +- .../com/lambda/event/listener/Listener.kt | 10 +++++++--- .../com/lambda/event/listener/SafeListener.kt | 16 ++++++++-------- .../lambda/event/listener/UnsafeListener.kt | 18 +++++++++--------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/Subscriber.kt b/common/src/main/kotlin/com/lambda/event/Subscriber.kt index cd935ea15..563b7be35 100644 --- a/common/src/main/kotlin/com/lambda/event/Subscriber.kt +++ b/common/src/main/kotlin/com/lambda/event/Subscriber.kt @@ -16,7 +16,7 @@ import kotlin.reflect.KClass */ class Subscriber : ConcurrentHashMap, ConcurrentSkipListSet>>() { val defaultListenerSet: ConcurrentSkipListSet> - get() = ConcurrentSkipListSet(compareByDescending> { it.priority }.thenBy { it.hashCode() }) // TODO: Fix this + get() = ConcurrentSkipListSet(Listener.comparator.reversed()) /** Allows a [Listener] to start receiving a specific type of [Event] */ diff --git a/common/src/main/kotlin/com/lambda/event/listener/Listener.kt b/common/src/main/kotlin/com/lambda/event/listener/Listener.kt index a2f2ab050..7733e5000 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/Listener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/Listener.kt @@ -40,10 +40,14 @@ abstract class Listener : Comparable> { abstract fun execute(event: T) override fun compareTo(other: Listener) = - compareBy> { + comparator.compare(this, other) + + companion object { + val comparator = compareBy> { it.priority }.thenBy { - // Needed because ConcurrentSkipListSet handles insertion based on compareTo + // Hashcode is needed because ConcurrentSkipListSet handles insertion based on compareTo it.hashCode() - }.compare(this, other) + } + } } diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index a7d66a6b1..1546c98b1 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -125,7 +125,7 @@ class SafeListener( * * Usage: * ```kotlin - * private val event by receiveNext { event -> + * private val event by listenNext { event -> * player.sendMessage("Event received only once: $event") * // event is stored in the value * // event is unsubscribed after execution @@ -138,20 +138,20 @@ class SafeListener( * @param transform The function used to transform the event into a value. * @return The newly created and registered [SafeListener]. */ - inline fun Any.receiveNext( + inline fun Any.listenOnce( priority: Int = 0, alwaysListen: Boolean = false, - noinline transform: SafeContext.(T) -> E? = { null }, noinline predicate: SafeContext.(T) -> Boolean = { true }, + noinline transform: SafeContext.(T) -> E? = { null }, ): ReadWriteProperty { - val ptr = Pointer() + val pointer = Pointer() val destroyable by selfReference> { - SafeListener(priority, this@receiveNext, alwaysListen) { event -> - ptr.value = transform(event) + SafeListener(priority, this@listenOnce, alwaysListen) { event -> + pointer.value = transform(event) if (predicate(event) && - ptr.value != null + pointer.value != null ) { val self by this@selfReference EventFlow.syncListeners.unsubscribe(self) @@ -161,7 +161,7 @@ class SafeListener( EventFlow.syncListeners.subscribe(destroyable) - return ptr + return pointer } /** diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index b089e00e0..3b19a9866 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -6,7 +6,7 @@ import com.lambda.event.EventFlow import com.lambda.event.Muteable import com.lambda.event.listener.SafeListener.Companion.concurrentListener import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.event.listener.SafeListener.Companion.receiveNext +import com.lambda.event.listener.SafeListener.Companion.listenOnce import com.lambda.util.Pointer import com.lambda.util.selfReference import kotlin.properties.ReadOnlyProperty @@ -105,11 +105,11 @@ class UnsafeListener( * Registers a new [UnsafeListener] for a generic [Event] type [T]. * The [function] is executed only once when the [Event] is dispatched. * This function should only be used when the [function] performs read actions on the game data. - * For only in-game related contexts, use the [SafeListener.receiveNext] function instead. + * For only in-game related contexts, use the [SafeListener.listenOnce] function instead. * * Usage: * ```kotlin - * private val event by unsafeReceiveNext { event -> + * private val event by unsafeListenOnce { event -> * println("Unsafe event received only once: $event") * // no safe access to player or world * // event is stored in the value @@ -125,20 +125,20 @@ class UnsafeListener( * @param transform The function used to transform the event into a value. * @return The newly created and registered [UnsafeListener]. */ - inline fun Any.unsafeReceiveNext( + inline fun Any.unsafeListenOnce( priority: Int = 0, alwaysListen: Boolean = false, noinline transform: (T) -> E? = { null }, noinline predicate: (T) -> Boolean = { true }, ): ReadWriteProperty { - val ptr = Pointer() + val pointer = Pointer() val destroyable by selfReference> { - UnsafeListener(priority, this@unsafeReceiveNext, alwaysListen) { event -> - ptr.value = transform(event) + UnsafeListener(priority, this@unsafeListenOnce, alwaysListen) { event -> + pointer.value = transform(event) if (predicate(event) && - ptr.value != null) + pointer.value != null) { val self by this@selfReference EventFlow.syncListeners.unsubscribe(self) @@ -148,7 +148,7 @@ class UnsafeListener( EventFlow.syncListeners.subscribe(destroyable) - return ptr + return pointer } /**