From 2a30736db3ac5d065d9dd17f04aa10ca64e3d431 Mon Sep 17 00:00:00 2001 From: thedarkcolour <30441001+thedarkcolour@users.noreply.github.com> Date: Sat, 18 Apr 2020 00:14:00 -0700 Subject: Update 1.2.0 --- .../kotlinforforge/AutoKotlinEventBusSubscriber.kt | 6 +- .../thedarkcolour/kotlinforforge/KotlinForForge.kt | 17 +- .../kotlinforforge/KotlinLanguageProvider.kt | 4 +- .../kotlinforforge/KotlinModContainer.kt | 51 ++- .../kotlinforforge/KotlinModLoadingContext.kt | 17 +- .../kotlin/thedarkcolour/kotlinforforge/Logger.kt | 2 +- .../kotlinforforge/eventbus/KotlinEventBus.kt | 373 +++++++++++++++++++++ .../eventbus/KotlinEventBusWrapper.kt | 52 +++ .../thedarkcolour/kotlinforforge/forge/Forge.kt | 33 +- src/main/resources/META-INF/mods.toml | 2 +- 10 files changed, 504 insertions(+), 53 deletions(-) create mode 100644 src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBus.kt create mode 100644 src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBusWrapper.kt (limited to 'src') diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt index fa43f57..547918f 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt @@ -35,7 +35,7 @@ public object AutoKotlinEventBusSubscriber { * } */ public fun inject(mod: ModContainer, scanData: ModFileScanData, classLoader: ClassLoader) { - logger.debug(Logging.LOADING, "Attempting to inject @EventBusSubscriber kotlin objects in to the event bus for ${mod.modId}") + LOGGER.debug(Logging.LOADING, "Attempting to inject @EventBusSubscriber kotlin objects in to the event bus for ${mod.modId}") val data: ArrayList = scanData.annotations.stream() .filter { annotationData -> EVENT_BUS_SUBSCRIBER == annotationData.annotationType @@ -51,7 +51,7 @@ public object AutoKotlinEventBusSubscriber { val ktObject = Class.forName(annotationData.classType.className, true, classLoader).kotlin.objectInstance if (ktObject != null && mod.modId == modid && sides.contains(FMLEnvironment.dist)) { try { - logger.debug(Logging.LOADING, "Auto-subscribing kotlin object ${annotationData.classType.className} to $busTarget") + LOGGER.debug(Logging.LOADING, "Auto-subscribing kotlin object ${annotationData.classType.className} to $busTarget") if (busTarget == Mod.EventBusSubscriber.Bus.MOD) { // Gets the correct mod loading context MOD_BUS.register(ktObject) @@ -59,7 +59,7 @@ public object AutoKotlinEventBusSubscriber { FORGE_BUS.register(ktObject) } } catch (e: Throwable) { - logger.fatal(Logging.LOADING, "Failed to load mod class ${annotationData.classType} for @EventBusSubscriber annotation", e) + LOGGER.fatal(Logging.LOADING, "Failed to load mod class ${annotationData.classType} for @EventBusSubscriber annotation", e) throw RuntimeException(e) } } diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinForForge.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinForForge.kt index e5ec289..51840d9 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinForForge.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinForForge.kt @@ -1,9 +1,24 @@ package thedarkcolour.kotlinforforge +import net.minecraft.block.Block +import net.minecraft.block.material.Material +import net.minecraftforge.event.RegistryEvent import net.minecraftforge.fml.common.Mod +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext +import thedarkcolour.kotlinforforge.forge.MOD_BUS /** * Set 'modLoader' in mods.toml to "kotlinforforge" and loaderVersion to "[1,)". + * + * Make sure to use [KotlinModLoadingContext] instead of [FMLJavaModLoadingContext]. */ @Mod("kotlinforforge") -public object KotlinForForge \ No newline at end of file +object KotlinForForge { + init { + MOD_BUS.addGenericListener(::registerBlocks) + } + + private fun registerBlocks(event: RegistryEvent.Register) { + event.registry.register(Block(Block.Properties.create(Material.ROCK)).setRegistryName("bruh")) + } +} \ No newline at end of file diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt index 61705ef..d393cbd 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt @@ -15,7 +15,7 @@ public class KotlinLanguageProvider : FMLJavaModLanguageProvider() { return Consumer { scanResult -> val target = scanResult.annotations.stream() .filter { data -> data.annotationType == MODANNOTATION } - .peek { data -> logger.debug(Logging.SCAN, "Found @Mod class ${data.classType.className} with id ${data.annotationData["value"]}") } + .peek { data -> LOGGER.debug(Logging.SCAN, "Found @Mod class ${data.classType.className} with id ${data.annotationData["value"]}") } .map { data -> KotlinModTarget(data.classType.className, data.annotationData["value"] as String) } .collect(Collectors.toMap({ target: KotlinModTarget -> target.modId }, { it }, { a, _ -> a })) scanResult.addLanguageLoader(target) @@ -29,7 +29,7 @@ public class KotlinLanguageProvider : FMLJavaModLanguageProvider() { public class KotlinModTarget constructor(private val className: String, val modId: String) : IModLanguageProvider.IModLanguageLoader { override fun loadMod(info: IModInfo, modClassLoader: ClassLoader, modFileScanResults: ModFileScanData): T { val ktContainer = Class.forName("thedarkcolour.kotlinforforge.KotlinModContainer", true, Thread.currentThread().contextClassLoader) - logger.debug(Logging.LOADING, "Loading KotlinModContainer from classloader ${Thread.currentThread().contextClassLoader} - got ${ktContainer.classLoader}}") + LOGGER.debug(Logging.LOADING, "Loading KotlinModContainer from classloader ${Thread.currentThread().contextClassLoader} - got ${ktContainer.classLoader}}") val constructor = ktContainer.declaredConstructors[0]//(IModInfo::class.java, String::class.java, ClassLoader::class.java, ModFileScanData::class.java)!! return constructor.newInstance(info, className, modClassLoader, modFileScanResults) as T } diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt index 0ea150f..9802d90 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt @@ -6,21 +6,22 @@ import net.minecraftforge.eventbus.api.Event import net.minecraftforge.eventbus.api.IEventBus import net.minecraftforge.eventbus.api.IEventListener import net.minecraftforge.fml.* +import net.minecraftforge.fml.config.ModConfig import net.minecraftforge.forgespi.language.IModInfo import net.minecraftforge.forgespi.language.ModFileScanData -import java.util.* +import thedarkcolour.kotlinforforge.eventbus.KotlinEventBus import java.util.function.Consumer import java.util.function.Supplier /** * Functions as [net.minecraftforge.fml.javafmlmod.FMLModContainer] for Kotlin */ -public class KotlinModContainer(private val info: IModInfo, private val className: String, private val classLoader: ClassLoader, private val scanData: ModFileScanData) : ModContainer(info) { +class KotlinModContainer(private val info: IModInfo, private val className: String, private val classLoader: ClassLoader, private val scanData: ModFileScanData) : ModContainer(info) { private lateinit var modInstance: Any - public val eventBus: IEventBus + val eventBus: KotlinEventBus init { - logger.debug(Logging.LOADING, "Creating KotlinModContainer instance for {} with classLoader {} & {}", className, classLoader, javaClass.classLoader) + LOGGER.debug(Logging.LOADING, "Creating KotlinModContainer instance for {} with classLoader {} & {}", className, classLoader, javaClass.classLoader) triggerMap[ModLoadingStage.CONSTRUCT] = dummy().andThen(::constructMod).andThen(::afterEvent) triggerMap[ModLoadingStage.CREATE_REGISTRIES] = dummy().andThen(::fireEvent).andThen(::afterEvent) triggerMap[ModLoadingStage.LOAD_REGISTRIES] = dummy().andThen(::fireEvent).andThen(::afterEvent) @@ -30,8 +31,7 @@ public class KotlinModContainer(private val info: IModInfo, private val classNam triggerMap[ModLoadingStage.PROCESS_IMC] = dummy().andThen(::fireEvent).andThen(::afterEvent) triggerMap[ModLoadingStage.COMPLETE] = dummy().andThen(::fireEvent).andThen(::afterEvent) triggerMap[ModLoadingStage.GATHERDATA] = dummy().andThen(::fireEvent).andThen(::afterEvent) - eventBus = BusBuilder.builder().setExceptionHandler(::onEventFailed).setTrackPhases(false).build() - configHandler = Optional.of(Consumer { event -> eventBus.post(event) }) + eventBus = KotlinEventBus(BusBuilder.builder().setExceptionHandler(::onEventFailed).setTrackPhases(false)) val ctx = KotlinModLoadingContext(this) contextExtension = Supplier { ctx } } @@ -39,25 +39,25 @@ public class KotlinModContainer(private val info: IModInfo, private val classNam private fun dummy(): Consumer = Consumer {} private fun onEventFailed(iEventBus: IEventBus, event: Event, iEventListeners: Array, i: Int, throwable: Throwable) { - logger.error(EventBusErrorMessage(event, i, iEventListeners, throwable)) + LOGGER.error(EventBusErrorMessage(event, i, iEventListeners, throwable)) } private fun fireEvent(lifecycleEvent: LifecycleEventProvider.LifecycleEvent) { val event = lifecycleEvent.getOrBuildEvent(this) - logger.debug(Logging.LOADING, "Firing event for modid $modId : $event") + LOGGER.debug(Logging.LOADING, "Firing event for modid $modId : $event") try { eventBus.post(event) - logger.debug(Logging.LOADING, "Fired event for modid $modId : $event") + LOGGER.debug(Logging.LOADING, "Fired event for modid $modId : $event") } catch (throwable: Throwable) { - logger.error(Logging.LOADING,"An error occurred while dispatching event ${lifecycleEvent.fromStage()} to $modId") + LOGGER.error(Logging.LOADING,"An error occurred while dispatching event ${lifecycleEvent.fromStage()} to $modId") throw ModLoadingException(modInfo, lifecycleEvent.fromStage(), "fml.modloading.errorduringevent", throwable) } } private fun afterEvent(lifecycleEvent: LifecycleEventProvider.LifecycleEvent) { if (currentState == ModLoadingStage.ERROR) { - logger.error(Logging.LOADING, "An error occurred while dispatching event ${lifecycleEvent.fromStage()} to $modId") + LOGGER.error(Logging.LOADING, "An error occurred while dispatching event ${lifecycleEvent.fromStage()} to $modId") } } @@ -65,41 +65,40 @@ public class KotlinModContainer(private val info: IModInfo, private val classNam val modClass: Class<*> try { modClass = Class.forName(className, true, classLoader) - logger.debug(Logging.LOADING, "Loaded kotlin modclass ${modClass.name} with ${modClass.classLoader}") + LOGGER.debug(Logging.LOADING, "Loaded kotlin modclass ${modClass.name} with ${modClass.classLoader}") } catch (throwable: Throwable) { - logger.error(Logging.LOADING, "Failed to load kotlin class $className", throwable) + LOGGER.error(Logging.LOADING, "Failed to load kotlin class $className", throwable) throw ModLoadingException(info, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadmodclass", throwable) } try { - logger.debug(Logging.LOADING, "Loading mod instance ${getModId()} of type ${modClass.name}") + LOGGER.debug(Logging.LOADING, "Loading mod instance ${getModId()} of type ${modClass.name}") modInstance = modClass.kotlin.objectInstance ?: modClass.newInstance() - logger.debug(Logging.LOADING, "Loaded mod instance ${getModId()} of type ${modClass.name}") + LOGGER.debug(Logging.LOADING, "Loaded mod instance ${getModId()} of type ${modClass.name}") } catch (throwable: Throwable) { - logger.error(Logging.LOADING, "Failed to create mod instance. ModID: ${getModId()}, class ${modClass.name}", throwable) + LOGGER.error(Logging.LOADING, "Failed to create mod instance. ModID: ${getModId()}, class ${modClass.name}", throwable) throw ModLoadingException(modInfo, lifecycleEvent.fromStage(), "fml.modloading.failedtoloadmod", throwable, modClass) } try { - logger.debug(Logging.LOADING, "Injecting Automatic Kotlin event subscribers for ${getModId()}") + LOGGER.debug(Logging.LOADING, "Injecting Automatic Kotlin event subscribers for ${getModId()}") // Inject into object EventBusSubscribers AutoKotlinEventBusSubscriber.inject(this, scanData, modClass.classLoader) - logger.debug(Logging.LOADING, "Completed Automatic Kotlin event subscribers for ${getModId()}") + LOGGER.debug(Logging.LOADING, "Completed Automatic Kotlin event subscribers for ${getModId()}") } catch (throwable: Throwable) { - logger.error(Logging.LOADING, "Failed to register Automatic Kotlin subscribers. ModID: ${getModId()}, class ${modClass.name}", throwable) + LOGGER.error(Logging.LOADING, "Failed to register Automatic Kotlin subscribers. ModID: ${getModId()}, class ${modClass.name}", throwable) throw ModLoadingException(modInfo, lifecycleEvent.fromStage(), "fml.modloading.failedtoloadmod", throwable, modClass) } } - override fun matches(mod: Any?): Boolean { - return mod == modInstance - } - - override fun getMod(): Any { - return modInstance - } + override fun matches(mod: Any?) = mod == modInstance + override fun getMod() = modInstance override fun acceptEvent(e: Event) { eventBus.post(e) } + + override fun dispatchConfigEvent(event: ModConfig.ModConfigEvent) { + eventBus.post(event) + } } \ No newline at end of file diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt index af42d16..44aac2d 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt @@ -1,19 +1,22 @@ package thedarkcolour.kotlinforforge -import net.minecraftforge.eventbus.api.IEventBus -import net.minecraftforge.fml.ModLoadingContext +import thedarkcolour.kotlinforforge.eventbus.KotlinEventBus +import thedarkcolour.kotlinforforge.forge.LOADING_CONTEXT /** * Functions as [net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext] for Kotlin */ -public class KotlinModLoadingContext constructor(private val container: KotlinModContainer) { - public fun getEventBus(): IEventBus { +class KotlinModLoadingContext constructor(private val container: KotlinModContainer) { + /** + * @see thedarkcolour.kotlinforforge.forge.MOD_BUS + */ + fun getEventBus(): KotlinEventBus { return container.eventBus } - public companion object { - public fun get(): KotlinModLoadingContext { - return ModLoadingContext.get().extension() + companion object { + fun get(): KotlinModLoadingContext { + return LOADING_CONTEXT.extension() } } } \ No newline at end of file diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/Logger.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/Logger.kt index 8276903..3724657 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/Logger.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/Logger.kt @@ -8,4 +8,4 @@ import org.apache.logging.log4j.LogManager * Kept here instead of [KotlinForForge] because logger is used * before [KotlinModContainer] should initialize. */ -internal val logger = LogManager.getLogger() \ No newline at end of file +internal val LOGGER = LogManager.getLogger() \ No newline at end of file diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBus.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBus.kt new file mode 100644 index 0000000..7323f23 --- /dev/null +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBus.kt @@ -0,0 +1,373 @@ +package thedarkcolour.kotlinforforge.eventbus + +import net.jodah.typetools.TypeResolver +import net.minecraftforge.eventbus.ASMEventHandler +import net.minecraftforge.eventbus.EventBus +import net.minecraftforge.eventbus.ListenerList +import net.minecraftforge.eventbus.api.* +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.MarkerManager +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Consumer + +/** @since 1.2.0 + * Fixes [IEventBus.addListener] for Kotlin SAM interfaces. + */ +open class KotlinEventBus(builder: BusBuilder, synthetic: Boolean = false) : IEventBus, IEventExceptionHandler { + @Suppress("LeakingThis") + private val exceptionHandler = builder.exceptionHandler ?: this + private val trackPhases = builder.trackPhases + @Volatile + private var shutdown = builder.isStartingShutdown + protected open val busID = MAX_ID.getAndIncrement() + protected open val listeners = ConcurrentHashMap>() + + init { + // see companion object + if (!synthetic) { + RESIZE_LISTENER_LIST(busID + 1) + } + } + + override fun register(target: Any) { + if (!listeners.containsKey(target)) { + if (target.javaClass == Class::class.java) { + registerClass(target as Class<*>) + } else { + registerObject(target) + } + } + } + + protected fun registerClass(clazz: Class<*>) { + for (method in clazz.methods) { + if (Modifier.isStatic(method.modifiers) && method.isAnnotationPresent(SubscribeEvent::class.java)) { + registerListener(clazz, method, method) + } + } + } + + protected fun registerObject(target: Any) { + val classes = HashSet>() + typesFor(target.javaClass, classes) + Arrays.stream(target.javaClass.methods).filter { m -> + !Modifier.isStatic(m.modifiers) + }.forEach { m -> + classes.map { c -> + getDeclMethod(c, m) + }.firstOrNull { rm -> + rm?.isAnnotationPresent(SubscribeEvent::class.java) == true + }?.let { rm -> + registerListener(target, m, rm) + } + } + } + + private fun typesFor(clz: Class<*>, visited: MutableSet>) { + if (clz.superclass == null) return + typesFor(clz.superclass, visited) + Arrays.stream(clz.interfaces).forEach { typesFor(it, visited) } + visited.add(clz) + } + + private fun getDeclMethod(clz: Class<*>, m: Method): Method? { + return try { + clz.getDeclaredMethod(m.name, *m.parameterTypes) + } catch (nse: NoSuchMethodException) { + null + } + } + + private fun registerListener(target: Any, f: Method, real: Method) { + val params: Array> = f.parameterTypes + + if (params.size != 1) { + throw IllegalArgumentException(""" + Function $f has @SubscribeEvent annotation. + It has ${params.size} value parameters, + but event handler functions require1 value parameter. + """.trimIndent() + ) + } + + val type = params[0] + + if (!Event::class.java.isAssignableFrom(type)) { + throw IllegalArgumentException(""" + Function $f has @SubscribeEvent annotation, + but takes an argument that is not an Event subtype : $type + """.trimIndent()) + } + + register(type, target, real) + } + + private fun register(type: Class<*>, target: Any, f: Method) { + try { + val asm = ASMEventHandler(target, f, IGenericEvent::class.java.isAssignableFrom(type)) + + addToListeners(target, type, asm.priority, asm) + } catch (e: IllegalAccessException) { + LOGGER.error(EVENT_BUS, "Error registering event handler: $type $f", e) + } catch (e: InstantiationException) { + LOGGER.error(EVENT_BUS, "Error registering event handler: $type $f", e) + } catch (e: NoSuchMethodException) { + LOGGER.error(EVENT_BUS, "Error registering event handler: $type $f", e) + } catch (e: InvocationTargetException) { + LOGGER.error(EVENT_BUS, "Error registering event handler: $type $f", e) + } + } + + protected open fun addToListeners(target: Any, eventType: Class<*>, priority: EventPriority, listener: IEventListener) { + val listenerList = EventListenerHelper.getListenerList(eventType) + listenerList.register(busID, priority, listener) + val others = listeners.computeIfAbsent(target) { Collections.synchronizedList(ArrayList()) } + others.add(listener) + } + + /** + * Add a consumer listener with default [EventPriority.NORMAL] and not receiving cancelled events. + * + * @param consumer Callback to invoke when a matching event is received + * @param T The [Event] subclass to listen for + */ + override fun addListener(consumer: Consumer) { + addListener(EventPriority.NORMAL, consumer) + } + + /** + * Add a consumer listener with the specified [EventPriority] and not receiving cancelled events. + * + * @param priority [EventPriority] for this listener + * @param consumer Callback to invoke when a matching event is received + * @param T The [Event] subclass to listen for + */ + override fun addListener(priority: EventPriority, consumer: Consumer) { + addListener(priority, false, consumer) + } + + /** + * Add a consumer listener with the specified [EventPriority] and potentially cancelled events. + * + * @param priority [EventPriority] for this listener + * @param receiveCancelled Indicate if this listener should receive events that have been [Cancelable] cancelled + * @param consumer Callback to invoke when a matching event is received + * @param T The [Event] subclass to listen for + */ + override fun addListener(priority: EventPriority, receiveCancelled: Boolean, consumer: Consumer) { + addListener(priority, passCancelled(receiveCancelled), consumer) + } + + /** + * Add a consumer listener with the specified [EventPriority] and potentially cancelled events. + * + * Use this method when one of the other methods fails to determine the concrete [Event] subclass that is + * intended to be subscribed to. + * + * @param priority [EventPriority] for this listener + * @param receiveCancelled Indicate if this listener should receive events that have been [Cancelable] cancelled + * @param eventType The concrete [Event] subclass to subscribe to + * @param consumer Callback to invoke when a matching event is received + * @param T The [Event] subclass to listen for + */ + override fun addListener(priority: EventPriority, receiveCancelled: Boolean, eventType: Class, consumer: Consumer) { + addListener(priority, passCancelled(receiveCancelled), eventType, consumer) + } + + private fun addListener(priority: EventPriority, filter: (T) -> Boolean, eventType: Class, consumer: Consumer) { + addToListeners(consumer, eventType, priority) { e -> + if (filter(e as T)) { + consumer.accept(e) + } + } + } + + private fun passCancelled(receiveCancelled: Boolean): (Event) -> Boolean = { event -> + receiveCancelled || !event.isCancelable || !event.isCanceled + } + + /** + * Add a consumer listener for a [GenericEvent] subclass with generic type [F]. + * + * @param consumer Callback to invoke when a matching event is received + * @param T The [GenericEvent] subclass to listen for + * @param F The [Class] to filter the [GenericEvent] for + */ + inline fun , reified F> addGenericListener(consumer: Consumer) { + addGenericListener(F::class.java, consumer) + } + + /** + * Add a consumer listener for a [GenericEvent] subclass, filtered to only be called for the specified + * filter [Class]. + * + * @param genericClassFilter A [Class] which the [GenericEvent] should be filtered for + * @param consumer Callback to invoke when a matching event is received + * @param T The [GenericEvent] subclass to listen for + * @param F The [Class] to filter the [GenericEvent] for + */ + override fun , F> addGenericListener(genericClassFilter: Class, consumer: Consumer) { + addGenericListener(genericClassFilter, EventPriority.NORMAL, consumer) + } + + /** + * Add a consumer listener with the specified [EventPriority] and not receiving cancelled events, + * for a [GenericEvent] subclass, filtered to only be called for the specified + * filter [Class]. + * + * @param genericClassFilter A [Class] which the [GenericEvent] should be filtered for + * @param priority [EventPriority] for this listener + * @param consumer Callback to invoke when a matching event is received + * @param T The [GenericEvent] subclass to listen for + * @param F The [Class] to filter the [GenericEvent] for + */ + override fun , F> addGenericListener(genericClassFilter: Class, priority: EventPriority, consumer: Consumer) { + addGenericListener(genericClassFilter, priority, false, consumer) + } + + /** + * Add a consumer listener with the specified [EventPriority] and potentially cancelled events, + * for a [GenericEvent] subclass, filtered to only be called for the specified + * filter [Class]. + * + * @param genericClassFilter A [Class] which the [GenericEvent] should be filtered for + * @param priority [EventPriority] for this listener + * @param receiveCancelled Indicate if this listener should receive events that have been [Cancelable] cancelled + * @param consumer Callback to invoke when a matching event is received + * @param T The [GenericEvent] subclass to listen for + * @param F The [Class] to filter the [GenericEvent] for + */ + override fun , F> addGenericListener(genericClassFilter: Class, priority: EventPriority, receiveCancelled: Boolean, consumer: Consumer) { + addListener(priority, passGenericCancelled(genericClassFilter, receiveCancelled), consumer) + } + + private fun addListener(priority: EventPriority, filter: (T) -> Boolean, consumer: Consumer) { + val eventType = reflectKotlinSAM(consumer) as Class? + + if (eventType == null) { + LOGGER.error(EVENT_BUS, "Failed to resolve handler for \"$consumer\"") + throw IllegalStateException("Failed to resolve KFunction event type: $consumer") + } + if (eventType == Event::class.java) { + LOGGER.warn(EVENT_BUS, """ + Attempting to add a Lambda listener with computed generic type of Event. + Are you sure this is what you meant? NOTE : there are complex lambda forms where + the generic type information is erased and cannot be recovered at runtime. + """.trimIndent()) + } + + addListener(priority, filter, eventType, consumer) + } + + /** + * Fixes issue that crashes when trying to register Kotlin SAM interface + * for a [Consumer] using the Java [IEventBus.addListener] method + */ + private fun reflectKotlinSAM(consumer: Consumer<*>): Class<*>? { + val clazz = consumer.javaClass + + if (clazz.simpleName.contains("$\$Lambda$")) { + return TypeResolver.resolveRawArgument(Consumer::class.java, consumer.javaClass) + } else if (clazz.simpleName.contains("\$sam$")) { + try { + val functionField = clazz.getDeclaredField("function") + functionField.isAccessible = true + val function = functionField[consumer] + + // Function should have two type parameters (parameter type and return type) + return TypeResolver.resolveRawArguments(kotlin.jvm.functions.Function1::class.java, function.javaClass)[0] + } catch (e: NoSuchFieldException) { + // Kotlin SAM interfaces compile to classes with a "function" field + LOGGER.log(Level.FATAL, "Tried to register invalid Kotlin SAM interface: Missing 'function' field") + throw e + } + } else return null + } + + private fun , F> passGenericCancelled(genericClassFilter: Class, receiveCancelled: Boolean): (T) -> Boolean = { event -> + event.genericType == genericClassFilter && (receiveCancelled || !event.isCancelable || !event.isCanceled) + } + + /** + * Add a consumer listener with the specified [EventPriority] and potentially cancelled events, + * for a [GenericEvent] subclass, filtered to only be called for the specified + * filter [Class]. + * + * Use this method when one of the other methods fails to determine the concrete [GenericEvent] subclass that is + * intended to be subscribed to. + * + * @param genericClassFilter A [Class] which the [GenericEvent] should be filtered for + * @param priority [EventPriority] for this listener + * @param receiveCancelled Indicate if this listener should receive events that have been [Cancelable] cancelled + * @param eventType The concrete [GenericEvent] subclass to subscribe to + * @param consumer Callback to invoke when a matching event is received + * @param T The [GenericEvent] subclass to listen for + * @param F The [Class] to filter the [GenericEvent] for + */ + override fun , F> addGenericListener(genericClassFilter: Class, priority: EventPriority, receiveCancelled: Boolean, eventType: Class, consumer: Consumer) { + addListener(priority, passGenericCancelled(genericClassFilter, receiveCancelled), eventType, consumer) + } + + override fun unregister(any: Any?) { + val list = listeners.remove(any) ?: return + + for (listener in list) { + ListenerList.unregisterAll(busID, listener) + } + } + + override fun post(event: Event): Boolean { + if (shutdown) return false + + val listeners = event.listenerList.getListeners(busID) + + for (index in listeners.indices) { + try { + if (!trackPhases && listeners[index]::class.java == EventPriority::class.java) { + continue + } else { + listeners[index].invoke(event) + } + } catch (throwable: Throwable) { + exceptionHandler.handleException(this, event, listeners, index, throwable) + throw throwable + } + } + + return event.isCancelable && event.isCanceled + } + + override fun handleException(bus: IEventBus, event: Event, listeners: Array, index: Int, throwable: Throwable) { + LOGGER.error(EVENT_BUS) + } + + override fun shutdown() { + LOGGER.fatal(EVENT_BUS, "KotlinEventBus $busID shutting down - future events will not be posted.", Exception("stacktrace")) + } + + override fun start() { + shutdown = false + } + + companion object { + private val LOGGER = LogManager.getLogger() + private val EVENT_BUS = MarkerManager.getMarker("EVENTBUS") + private val MAX_ID: AtomicInteger + private val RESIZE_LISTENER_LIST: (Int) -> Unit + + init { + val maxIDField = EventBus::class.java.getDeclaredField("maxID") + maxIDField.isAccessible = true + MAX_ID = maxIDField.get(null) as AtomicInteger + val resizeMethod = ListenerList::class.java.getDeclaredMethod("resize", Int::class.java) + resizeMethod.isAccessible = true + RESIZE_LISTENER_LIST = { max -> resizeMethod.invoke(null, max) } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBusWrapper.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBusWrapper.kt new file mode 100644 index 0000000..b3cb9d5 --- /dev/null +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/eventbus/KotlinEventBusWrapper.kt @@ -0,0 +1,52 @@ +package thedarkcolour.kotlinforforge.eventbus + +import net.minecraftforge.eventbus.EventBus +import net.minecraftforge.eventbus.api.BusBuilder +import net.minecraftforge.eventbus.api.IEventBus +import net.minecraftforge.eventbus.api.IEventExceptionHandler +import net.minecraftforge.eventbus.api.IEventListener +import thedarkcolour.kotlinforforge.forge.FORGE_BUS +import java.util.concurrent.ConcurrentHashMap + +/** @since 1.2.0 + * Fixes [IEventBus.addListener] for Kotlin SAM interfaces + * when using [FORGE_BUS]. + */ +class KotlinEventBusWrapper(private val parent: EventBus) : KotlinEventBus(BusBuilder() + .setExceptionHandler(getExceptionHandler(parent)) + .setTrackPhases(getTrackPhases(parent)) + .also { if (getShutdown(parent)) it.startShutdown() } +) { + override val busID = getBusID(parent) + override val listeners = getListeners(parent) + + // reflection stuff + companion object { + private val GET_BUS_ID = EventBus::class.java.getDeclaredField("busID").also { it.isAccessible = true } + private val GET_LISTENERS = EventBus::class.java.getDeclaredField("listeners").also { it.isAccessible = true } + private val GET_EXCEPTION_HANDLER = EventBus::class.java.getDeclaredField("exceptionHandler").also { it.isAccessible = true } + private val GET_TRACK_PHASES = EventBus::class.java.getDeclaredField("trackPhases").also { it.isAccessible = true } + private val GET_SHUTDOWN = EventBus::class.java.getDeclaredField("shutdown").also { it.isAccessible = true } + + fun getBusID(eventBus: EventBus): Int { + return GET_BUS_ID[eventBus] as Int + } + + @Suppress("UNCHECKED_CAST") + fun getListeners(eventBus: EventBus): ConcurrentHashMap> { + return GET_LISTENERS[eventBus] as ConcurrentHashMap> + } + + fun getExceptionHandler(eventBus: EventBus): IEventExceptionHandler { + return GET_EXCEPTION_HANDLER[eventBus] as IEventExceptionHandler + } + + fun getTrackPhases(eventBus: EventBus): Boolean { + return GET_TRACK_PHASES[eventBus] as Boolean + } + + fun getShutdown(eventBus: EventBus): Boolean { + return GET_SHUTDOWN[eventBus] as Boolean + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt b/src/main/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt index d712c75..e0b9cd8 100644 --- a/src/main/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt +++ b/src/main/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt @@ -3,34 +3,43 @@ package thedarkcolour.kotlinforforge.forge import net.minecraftforge.api.distmarker.Dist import net.minecraftforge.common.ForgeConfigSpec import net.minecraftforge.common.MinecraftForge -import net.minecraftforge.eventbus.api.IEventBus +import net.minecraftforge.eventbus.EventBus import net.minecraftforge.fml.ModLoadingContext import net.minecraftforge.fml.config.ModConfig import net.minecraftforge.fml.loading.FMLEnvironment import thedarkcolour.kotlinforforge.KotlinModLoadingContext +import thedarkcolour.kotlinforforge.eventbus.KotlinEventBus +import thedarkcolour.kotlinforforge.eventbus.KotlinEventBusWrapper /** @since 1.0.0 - * The forge EventBus. + * The forge EventBus wrapped in a [KEventBus]. * Many events that occur during the game are fired on this bus. * + * @since 1.2.0 + * This event bus supports [EventBus.addListener] + * for Kotlin SAM interfaces. + * * Examples: * @see net.minecraftforge.event.entity.player.PlayerEvent * @see net.minecraftforge.event.entity.living.LivingEvent * @see net.minecraftforge.event.world.BlockEvent */ -public val FORGE_BUS: IEventBus - inline get() = MinecraftForge.EVENT_BUS +val FORGE_BUS = KotlinEventBusWrapper(MinecraftForge.EVENT_BUS as EventBus) /** @since 1.0.0 * The mod-specific EventBus. * Setup events are typically fired on this bus. * + * @since 1.2.0 + * This event bus supports [EventBus.addListener] + * for Kotlin SAM interfaces. + * * Examples: * @see net.minecraftforge.fml.event.lifecycle.InterModProcessEvent * @see net.minecraftforge.event.AttachCapabilitiesEvent * @see net.minecraftforge.event.RegistryEvent */ -public val MOD_BUS: IEventBus +val MOD_BUS: KotlinEventBus inline get() = KotlinModLoadingContext.get().getEventBus() /** @since 1.0.0 @@ -38,22 +47,22 @@ public val MOD_BUS: IEventBus * * Used in place of [net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext] */ -public val MOD_CONTEXT: KotlinModLoadingContext +val MOD_CONTEXT: KotlinModLoadingContext inline get() = KotlinModLoadingContext.get() -public val LOADING_CONTEXT: ModLoadingContext +val LOADING_CONTEXT: ModLoadingContext inline get() = ModLoadingContext.get() /** @since 1.0.0 * The current [Dist] of this environment. */ -public val DIST: Dist = FMLEnvironment.dist +val DIST: Dist = FMLEnvironment.dist /** @since 1.0.0 * An alternative to [net.minecraftforge.fml.DistExecutor.callWhenOn] * that inlines the callable. */ -public inline fun callWhenOn(dist: Dist, toRun: () -> T): T? { +inline fun callWhenOn(dist: Dist, toRun: () -> T): T? { return if (DIST == dist) { try { toRun() @@ -69,7 +78,7 @@ public inline fun callWhenOn(dist: Dist, toRun: () -> T): T? { * An alternative to [net.minecraftforge.fml.DistExecutor.runWhenOn] * that uses Kotlin functions instead of Java functional interfaces. */ -public inline fun runWhenOn(dist: Dist, toRun: () -> Unit) { +inline fun runWhenOn(dist: Dist, toRun: () -> Unit) { if (DIST == dist) { toRun() } @@ -79,7 +88,7 @@ public inline fun runWhenOn(dist: Dist, toRun: () -> Unit) { * An alternative to [net.minecraftforge.fml.DistExecutor.runForDist] * that inlines the method call. */ -public inline fun runForDist(clientTarget: () -> T, serverTarget: () -> T): T { +inline fun runForDist(clientTarget: () -> T, serverTarget: () -> T): T { return when (DIST) { Dist.CLIENT -> clientTarget() Dist.DEDICATED_SERVER -> serverTarget() @@ -89,7 +98,7 @@ public inline fun runForDist(clientTarget: () -> T, serverTarget: () -> T): /** @since 1.0.0 * Registers a config. */ -public fun registerConfig(type: ModConfig.Type, spec: ForgeConfigSpec, fileName: String? = null) { +fun registerConfig(type: ModConfig.Type, spec: ForgeConfigSpec, fileName: String? = null) { if (fileName == null) { LOADING_CONTEXT.registerConfig(type, spec) } else { diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 7f4e0be..a58fee0 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -33,6 +33,6 @@ Kotlin for Forge. Allows mods to use the Kotlin programming language. [[mods]] #mandatory displayName="Kotlin for Forge" # Name of mod modId="kotlinforforge" # Modid -version="1.0.1" # Version of kotlinforforge +version="1.2.0" # Version of kotlinforforge authors="TheDarkColour" # Author credits="Herobrine knows all." # Credits \ No newline at end of file -- cgit