aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/api/event/EventHandler.kt
blob: cfc8e63b87a87160116f9c89ea00febae066a6ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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<*>?
    )
}