summaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/api/event
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/api/event')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/event/CancellableSkyHanniEvent.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/event/EventHandler.kt134
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/event/GenericSkyHanniEvent.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt35
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvent.kt19
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt64
6 files changed, 258 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/CancellableSkyHanniEvent.kt b/src/main/java/at/hannibal2/skyhanni/api/event/CancellableSkyHanniEvent.kt
new file mode 100644
index 000000000..048630b84
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/api/event/CancellableSkyHanniEvent.kt
@@ -0,0 +1,3 @@
+package at.hannibal2.skyhanni.api.event
+
+abstract class CancellableSkyHanniEvent : SkyHanniEvent(), SkyHanniEvent.Cancellable
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/EventHandler.kt b/src/main/java/at/hannibal2/skyhanni/api/event/EventHandler.kt
new file mode 100644
index 000000000..cfc8e63b8
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/api/event/EventHandler.kt
@@ -0,0 +1,134 @@
+package at.hannibal2.skyhanni.api.event
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.data.IslandType
+import at.hannibal2.skyhanni.test.command.ErrorManager
+import at.hannibal2.skyhanni.utils.ChatUtils
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland
+import at.hannibal2.skyhanni.utils.chat.Text
+import java.lang.invoke.LambdaMetafactory
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
+import java.lang.reflect.Method
+import java.lang.reflect.ParameterizedType
+import java.util.function.Consumer
+
+class EventHandler<T : SkyHanniEvent> private constructor(val name: String, private val isGeneric: Boolean) {
+
+ private val listeners: MutableList<Listener> = mutableListOf()
+
+ private var isFrozen = false
+ private var canReceiveCancelled = false
+
+ var invokeCount: Long = 0L
+ private set
+
+ constructor(event: Class<T>) : this(
+ (event.name.split(".").lastOrNull() ?: event.name).replace("$", "."),
+ GenericSkyHanniEvent::class.java.isAssignableFrom(event)
+ )
+
+ fun addListener(method: Method, instance: Any, options: HandleEvent) {
+ if (isFrozen) throw IllegalStateException("Cannot add listener to frozen event handler")
+ val generic: Class<*>? = if (isGeneric) {
+ method.genericParameterTypes
+ .firstNotNullOfOrNull { it as? ParameterizedType }
+ ?.let { it.actualTypeArguments.firstOrNull() as? Class<*> }
+ ?: throw IllegalArgumentException("Generic event handler must have a generic type")
+ } else {
+ null
+ }
+ val name = "${method.declaringClass.name}.${method.name}${
+ method.parameterTypes.joinTo(
+ StringBuilder(),
+ prefix = "(",
+ postfix = ")",
+ separator = ", ",
+ transform = Class<*>::getTypeName
+ )
+ }"
+ listeners.add(Listener(name, createEventConsumer(name, instance, method), options, generic))
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun createEventConsumer(name: String, instance: Any, method: Method): Consumer<Any> {
+ try {
+ val handle = MethodHandles.lookup().unreflect(method)
+ return LambdaMetafactory.metafactory(
+ MethodHandles.lookup(),
+ "accept",
+ MethodType.methodType(Consumer::class.java, instance::class.java),
+ MethodType.methodType(Nothing::class.javaPrimitiveType, Object::class.java),
+ handle,
+ MethodType.methodType(Nothing::class.javaPrimitiveType, method.parameterTypes[0])
+ ).target.bindTo(instance).invokeExact() as Consumer<Any>
+ } catch (e: Throwable) {
+ throw IllegalArgumentException("Method $name is not a valid consumer", e)
+ }
+ }
+
+ fun freeze() {
+ isFrozen = true
+ listeners.sortBy { it.options.priority }
+ canReceiveCancelled = listeners.any { it.options.receiveCancelled }
+ }
+
+ fun post(event: T, onError: ((Throwable) -> Unit)? = null): Boolean {
+ invokeCount++
+ if (this.listeners.isEmpty()) return false
+ if (!isFrozen) error("Cannot invoke event on unfrozen event handler")
+
+ if (SkyHanniEvents.isDisabledHandler(name)) return false
+
+ var errors = 0
+
+ for (listener in listeners) {
+ if (!shouldInvoke(event, listener)) continue
+ try {
+ listener.invoker.accept(event)
+ } catch (throwable: Throwable) {
+ errors++
+ if (errors <= 3) {
+ val errorName = throwable::class.simpleName ?: "error"
+ val message = "Caught an $errorName in ${listener.name} at $name: ${throwable.message}"
+ ErrorManager.logErrorWithData(throwable, message, ignoreErrorCache = onError != null)
+ }
+ onError?.invoke(throwable)
+ }
+ if (event.isCancelled && !canReceiveCancelled) break
+ }
+
+ if (errors > 3) {
+ val hiddenErrors = errors - 3
+ ChatUtils.chat(
+ Text.text(
+ "§c[SkyHanni/${SkyHanniMod.version}] $hiddenErrors more errors in $name are hidden!"
+ )
+ )
+ }
+ return event.isCancelled
+ }
+
+ private fun shouldInvoke(event: SkyHanniEvent, listener: Listener): Boolean {
+ if (SkyHanniEvents.isDisabledInvoker(listener.name)) return false
+ if (listener.options.onlyOnSkyblock && !LorenzUtils.inSkyBlock) return false
+ if (listener.options.onlyOnIsland != IslandType.ANY && !listener.options.onlyOnIsland.isInIsland()) return false
+ if (event.isCancelled && !listener.options.receiveCancelled) return false
+ if (
+ event is GenericSkyHanniEvent<*> &&
+ listener.generic != null &&
+ !listener.generic.isAssignableFrom(event.type)
+ ) {
+ return false
+ }
+ return true
+ }
+
+ private class Listener(
+ val name: String,
+ val invoker: Consumer<Any>,
+ val options: HandleEvent,
+ val generic: Class<*>?
+ )
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/GenericSkyHanniEvent.kt b/src/main/java/at/hannibal2/skyhanni/api/event/GenericSkyHanniEvent.kt
new file mode 100644
index 000000000..1b761e1ae
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/api/event/GenericSkyHanniEvent.kt
@@ -0,0 +1,3 @@
+package at.hannibal2.skyhanni.api.event
+
+abstract class GenericSkyHanniEvent<T>(val type: Class<T>) : SkyHanniEvent(), SkyHanniEvent.Cancellable
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt b/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt
new file mode 100644
index 000000000..39b047a6e
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt
@@ -0,0 +1,35 @@
+package at.hannibal2.skyhanni.api.event
+
+import at.hannibal2.skyhanni.data.IslandType
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION)
+annotation class HandleEvent(
+ /**
+ * If the event should only be received while on SkyBlock.
+ */
+ val onlyOnSkyblock: Boolean = false,
+
+ /**
+ * If the event should only be received while on a specific skyblock island.
+ */
+ val onlyOnIsland: IslandType = IslandType.ANY,
+
+ /**
+ * The priority of when the event will be called, lower priority will be called first, see the companion object.
+ */
+ val priority: Int = 0,
+
+ /**
+ * If the event is cancelled & receiveCancelled is true, then the method will still invoke.
+ */
+ val receiveCancelled: Boolean = false,
+) {
+
+ companion object {
+ const val HIGHEST = -2
+ const val HIGH = -1
+ const val LOW = 1
+ const val LOWEST = 2
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvent.kt b/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvent.kt
new file mode 100644
index 000000000..4f3685c8b
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvent.kt
@@ -0,0 +1,19 @@
+package at.hannibal2.skyhanni.api.event
+
+abstract class SkyHanniEvent protected constructor() {
+
+ var isCancelled: Boolean = false
+ private set
+
+ fun post() = SkyHanniEvents.getEventHandler(javaClass).post(this)
+
+ fun post(onError: (Throwable) -> Unit = {}) = SkyHanniEvents.getEventHandler(javaClass).post(this, onError)
+
+ interface Cancellable {
+
+ fun cancel() {
+ val event = this as SkyHanniEvent
+ event.isCancelled = true
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt b/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt
new file mode 100644
index 000000000..e3dff2014
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt
@@ -0,0 +1,64 @@
+package at.hannibal2.skyhanni.api.event
+
+import at.hannibal2.skyhanni.data.MinecraftData
+import at.hannibal2.skyhanni.data.jsonobjects.repo.DisabledEventsJson
+import at.hannibal2.skyhanni.events.DebugDataCollectEvent
+import at.hannibal2.skyhanni.events.RepositoryReloadEvent
+import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.lang.reflect.Method
+
+@SkyHanniModule
+object SkyHanniEvents {
+
+ private val handlers: MutableMap<Class<*>, EventHandler<*>> = mutableMapOf()
+ private var disabledHandlers = emptySet<String>()
+ private var disabledHandlerInvokers = emptySet<String>()
+
+ fun init(instances: List<Any>) {
+ instances.forEach { instance ->
+ instance.javaClass.declaredMethods.forEach {
+ registerMethod(it, instance)
+ }
+ }
+ handlers.values.forEach { it.freeze() }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun <T : SkyHanniEvent> getEventHandler(event: Class<T>): EventHandler<T> = handlers.getOrPut(event) {
+ EventHandler(event)
+ } as EventHandler<T>
+
+ fun isDisabledHandler(handler: String): Boolean = handler in disabledHandlers
+ fun isDisabledInvoker(invoker: String): Boolean = invoker in disabledHandlerInvokers
+
+ @Suppress("UNCHECKED_CAST")
+ private fun registerMethod(method: Method, instance: Any) {
+ if (method.parameterCount != 1) return
+ val options = method.getAnnotation(HandleEvent::class.java) ?: return
+ val event = method.parameterTypes[0]
+ if (!SkyHanniEvent::class.java.isAssignableFrom(event)) return
+ val handler = getEventHandler(event as Class<SkyHanniEvent>)
+ handler.addListener(method, instance, options)
+ }
+
+ @SubscribeEvent
+ fun onRepoLoad(event: RepositoryReloadEvent) {
+ val data = event.getConstant<DisabledEventsJson>("DisabledEvents")
+ disabledHandlers = data.disabledHandlers
+ disabledHandlerInvokers = data.disabledInvokers
+ }
+
+ @SubscribeEvent
+ fun onDebug(event: DebugDataCollectEvent) {
+ event.title("Events")
+ event.addIrrelevant {
+ handlers.values.toMutableList()
+ .filter { it.invokeCount > 0 }
+ .sortedWith(compareBy({ -it.invokeCount }, { it.name }))
+ .forEach {
+ add("- ${it.name} (${it.invokeCount} ${it.invokeCount / (MinecraftData.totalTicks / 20)}/s)")
+ }
+ }
+ }
+}