diff --git a/build.gradle.kts b/build.gradle.kts index 156b4cb1..de8c8463 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -108,13 +108,14 @@ repositories { mavenCentral() maven("https://maven.neoforged.net/releases") maven("https://maven.minecraftforge.net/") + maven("https://libraries.minecraft.net") } dependencies { + // fakecraft (fake class/field/method signatures used in place of the full Minecraft/Forge dependencies) "fakecraftCompileOnly"(libs.joml) "fakecraftCompileOnly"(libs.forge.core) - "fakecraftCompileOnly"(libs.forge.bus) "fakecraftCompileOnly"(libs.night.config) // kfflib/common @@ -131,6 +132,8 @@ dependencies { // kffmod/forge "modForgeCompileOnly"(libs.log4j.core) + "modForgeCompileOnly"(sourceSets["langForge"].output) + sourceSets.forEach { sourceSet -> val name = sourceSet.name @@ -153,6 +156,8 @@ dependencies { if (name.contains("lang")) { dependencies.add(compileOnly, libs.asm) dependencies.add(compileOnly, libs.log4j.core) + dependencies.add(compileOnly, libs.secure.jar) + dependencies.add(compileOnly, libs.forge.bus) } } } diff --git a/gradle.properties b/gradle.properties index a7eab569..7183a05b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,5 +12,5 @@ min_mc_version = 1.20.6 unsupported_mc_version = 1.22 # KOTLIN FOR FORGE VERSION -kff_version=5.9.0 -kff_max_version=6.0.0 +kff_version=6.0.0 +kff_max_version=7.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 736ae616..cfd4147b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,19 +3,21 @@ # https://github.com/JetBrains/kotlin # https://github.com/Kotlin/kotlinx.coroutines # https://github.com/Kotlin/kotlinx.serialization -kotlin = "2.1.21" +kotlin = "2.2.0" coroutines = "1.10.2" serialization = "1.8.1" # Misc -neoforge = "21.6.11-beta" +neoforge = "21.7.20-beta" neoforge-fml = "[3,5)" neoforge-bus = "8.0.5" neoforge-mergetool = "2.0.7" -forge = "1.21-51.0.8" +forge = "1.21.7-57.0.2" forge-mergetool = "1.0" -forge-spi = "7.1.4" -forge-bus = "6.2.6" +unsafe = "0.9.2" +forge-spi = "7.1.5" +secureJar = "3.0.9" +forge-bus = "7.0-beta.10" joml = "1.10.5" log4j = "2.22.1" asm = "9.5" @@ -44,9 +46,11 @@ neoforge-mergetool = { module = "net.neoforged:mergetool", version.ref = "neofor forge-fml = { module = "net.minecraftforge:javafmllanguage", version.ref = "forge" } forge-bus = { module = "net.minecraftforge:eventbus", version.ref = "forge-bus" } forge-loader = { module = "net.minecraftforge:fmlloader", version.ref = "forge" } +forge-unsafe = { module = "net.minecraftforge:unsafe", version.ref = "unsafe" } forge-core = { module = "net.minecraftforge:fmlcore", version.ref = "forge" } forge-mergetool = { module = "net.minecraftforge:mergetool-api", version.ref = "forge-mergetool" } forge-spi = { module = "net.minecraftforge:forgespi", version.ref = "forge-spi" } +secure-jar = { module = "cpw.mods:securejarhandler", version.ref = "secureJar"} joml = { module = "org.joml:joml", version.ref = "joml" } asm = { module = "org.ow2.asm:asm", version.ref = "asm" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } @@ -56,7 +60,7 @@ night-config = { module = "com.electronwill.night-config:core", version.ref = "n # Kotlin Reflect, Stdlib, Coroutines, Serialization JSON kotlin = ["kotlin-reflect", "kotlin-stdlib", "kotlin-stdlib-jdk7", "kotlin-stdlib-jdk8", "kotlinx-coroutines-core", "kotlinx-coroutines-core-jvm", "kotlinx-coroutines-jdk8", "kotlinx-serialization-core", "kotlinx-serialization-json"] neoforge = ["neoforge", "neoforge-bus", "neoforge-fml", "neoforge-mergetool"] -forge = ["forge-core", "forge-fml", "forge-loader", "forge-mergetool", "forge-spi", "forge-bus"] +forge = ["forge-core", "forge-fml", "forge-loader", "forge-mergetool", "forge-spi", "forge-bus", "forge-unsafe"] [plugins] ideaext = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaext" } diff --git a/src/fakecraft/java/net/minecraftforge/common/MinecraftForge.java b/src/fakecraft/java/net/minecraftforge/common/MinecraftForge.java index 5ffdd58a..54f6c863 100644 --- a/src/fakecraft/java/net/minecraftforge/common/MinecraftForge.java +++ b/src/fakecraft/java/net/minecraftforge/common/MinecraftForge.java @@ -1,7 +1,7 @@ package net.minecraftforge.common; -import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.eventbus.api.bus.BusGroup; public class MinecraftForge { - public static final IEventBus EVENT_BUS = null; + public static final BusGroup EVENT_BUS = null; } diff --git a/src/kfflang/forge/kotlin/thedarkcolour/common/KotlinMod.kt b/src/kfflang/forge/kotlin/thedarkcolour/common/KotlinMod.kt new file mode 100644 index 00000000..70a70540 --- /dev/null +++ b/src/kfflang/forge/kotlin/thedarkcolour/common/KotlinMod.kt @@ -0,0 +1,51 @@ +package thedarkcolour.common + +import net.minecraftforge.api.distmarker.Dist +import net.minecraftforge.eventbus.api.bus.BusGroup +import net.minecraftforge.fml.Bindings +import net.minecraftforge.fml.common.Mod +import thedarkcolour.kotlinforforge.KotlinModLoadingContext +import java.util.function.Supplier + +/** + * This defines a Kotlin Mod Class + * Any class found with this annotation applied will be loaded as a Mod. The instance that is loaded will + * represent the mod to other Mods in the system. It will be sent various subclasses of [ModLifecycleEvent] + * at pre-defined times during the loading of the game. + * @author Chidoziealways + * @since 6.0.0 + */ + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +public annotation class KotlinMod(val value: String) { + + public annotation class KotlinEventBusSubscriber( + val value: Array = [Dist.CLIENT, Dist.DEDICATED_SERVER], + val modId: String = "", + val bus: KotlinBus = KotlinBus.BOTH + ) { + } +} + +public enum class KotlinBus(public val eventBusSupplier: Supplier) { + /** + * The main BusGroup that most game events are fired on. + */ + FORGE(Bindings.getForgeBus()), + + /** + * The Mod-Specific event BusGroup, usually for mod lifecycle events. + * @see KotlinModLoadingContext.getKBusGroup() + */ + MOD({ KotlinModLoadingContext.get().getKBusGroup() }), + + /** + * Both the [FORGE] and [MOD] buses. This is slower to register events in your class but + * allows you to listen to events from different BusGroup types without needing separate classes annotated + * with [thedarkcolour.common.KotlinMod.KotlinEventBusSubscriber]. + */ + BOTH({null}); + + public fun bus(): Supplier = eventBusSupplier +} diff --git a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt index df5291ef..379a7eae 100644 --- a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt +++ b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/AutoKotlinEventBusSubscriber.kt @@ -1,14 +1,38 @@ package thedarkcolour.kotlinforforge import net.minecraftforge.api.distmarker.Dist +import net.minecraftforge.api.distmarker.OnlyIn +import net.minecraftforge.eventbus.api.bus.BusGroup +import net.minecraftforge.eventbus.api.bus.CancellableEventBus +import net.minecraftforge.eventbus.api.bus.EventBus +import net.minecraftforge.eventbus.api.event.characteristic.Cancellable +import net.minecraftforge.eventbus.api.listener.EventListener +import net.minecraftforge.eventbus.api.listener.ObjBooleanBiConsumer +import net.minecraftforge.eventbus.api.listener.Priority +import net.minecraftforge.eventbus.api.listener.SubscribeEvent +import net.minecraftforge.eventbus.internal.Event import net.minecraftforge.fml.Logging import net.minecraftforge.fml.common.Mod +import net.minecraftforge.fml.event.IModBusEvent import net.minecraftforge.fml.loading.FMLEnvironment import net.minecraftforge.forgespi.language.ModFileScanData import net.minecraftforge.forgespi.language.ModFileScanData.EnumData +import net.minecraftforge.unsafe.UnsafeHacks import org.objectweb.asm.Type +import thedarkcolour.common.KotlinBus +import thedarkcolour.common.KotlinMod +import java.lang.invoke.LambdaMetafactory +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType import java.lang.reflect.Method -import java.util.* +import java.lang.reflect.Modifier +import java.util.function.Consumer +import java.util.function.Predicate +import kotlin.reflect.KClass +import kotlin.reflect.KVisibility +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.full.findAnnotation /** * Automatically registers `object` classes to @@ -34,18 +58,12 @@ import java.util.* */ public object AutoKotlinEventBusSubscriber { // EventBusSubscriber annotation - private val EVENT_BUS_SUBSCRIBER: Type = Type.getType(Mod.EventBusSubscriber::class.java) - - // Legacy EnumHolder - private val enumHolderGetValue: Method? = try { - val klass = Class.forName("net.minecraftforge.fml.loading.moddiscovery.ModAnnotation\$EnumHolder") - klass.getDeclaredMethod("getValue") - } catch (e: ClassNotFoundException) { - null - } + private val EVENT_BUS_SUBSCRIBER: Type = Type.getType(KotlinMod.KotlinEventBusSubscriber::class.java) + private val MOD_TYPE: Type = Type.getType(KotlinMod::class.java) + private val ONLY_IN_TYPE: Type = Type.getType(OnlyIn::class.java) /** - * Allows the [Mod.EventBusSubscriber] annotation + * Allows the [KotlinMod.KotlinEventBusSubscriber] annotation * to target member functions of an `object` class. * * You **must** be using an `object` class, or the @@ -59,82 +77,318 @@ public object AutoKotlinEventBusSubscriber { * or [thedarkcolour.kotlinforforge.forge.MOD_BUS]. */ public fun inject(mod: KotlinModContainer, 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 @KotlinEventBusSubscriber kotlin objects in to the event bus for ${mod.modId}") + + val targets = scanData.annotations.filter { data -> EVENT_BUS_SUBSCRIBER == data.annotationType }.toList() - val data = scanData.annotations.filter { annotationData -> - EVENT_BUS_SUBSCRIBER == annotationData.annotationType + val onlyIns: Set = if (FMLEnvironment.production) { + emptySet() + } else { + scanData.annotations + .asSequence() + .filter { it.annotationType() == ONLY_IN_TYPE } + .map { it.clazz().className } + .toSet() } - for (annotationData in data) { - val annotationMap = annotationData.annotationData - val sides = getSides(annotationMap) - val modid = annotationMap.getOrDefault("modid", mod.modId) - val busTarget = getBusTarget(annotationMap) + val modids: Map = scanData.annotations + .asSequence() + .filter { it.annotationType() == MOD_TYPE } + .associate { it.clazz().className to (it.annotationData()["value"] as String) } - if (mod.modId == modid && FMLEnvironment.dist in sides) { - val kClass = Class.forName(annotationData.clazz.className, true, classLoader).kotlin + val defaultSides = listOf(EnumData(null, "CLIENT"), EnumData(null, "DEDICATED_SERVER")) + val defaultBus = EnumData(null, "FORGE") + + for (annotationData in targets) { + if (!FMLEnvironment.production && onlyIns.contains(annotationData.clazz.className)) { + throw RuntimeException("Found @OnlyIn on @KotlinEventBusSubscriber class ${annotationData.clazz().className} - this is not allowed as it causes crashes. Remove the OnlyIn and set value=Dist.CLIENT in the EventBusSubscriber annotation instead") + } - var ktObject: Any? + var modId = modids.getOrDefault(annotationData.clazz.className, mod.modId) + modId = value(annotationData, "modId", modId) + val sidesValue = value(annotationData, "value", defaultSides) + val sides = sidesValue + .map(EnumData::value) + .map(Dist::valueOf) + .toSet() + val busName = value(annotationData, "bus", defaultBus).value() + val busTarget = KotlinBus.valueOf(busName) + if (mod.modId == modId && FMLEnvironment.dist in sides) { try { - ktObject = kClass.objectInstance - } catch (unsupported: UnsupportedOperationException) { - if (unsupported.message?.contains("file facades") == false) { - throw unsupported + LOGGER.debug(LOADING, "Auto-subscribing {} to {}", annotationData.clazz().className, busTarget) + val clazz = Class.forName(annotationData.clazz.className, true, classLoader) + val instance = try { + val field = clazz.getDeclaredField("INSTANCE") + field.isAccessible = true + field.get(null) + } catch (e: Exception) { + LOGGER.warn(LOADING, "⚠️ Failed to get INSTANCE from ${clazz.name}: ${e.message}") + null + } + + if (instance != null) { + EventBusSubscriberLogic.register(busTarget.bus().get(), instance) } else { - LOGGER.debug(Logging.LOADING, "Auto-subscribing kotlin file ${annotationData.annotationType.className} to $busTarget") - registerTo(kClass.java, busTarget, mod) - continue + LOGGER.warn(LOADING, "⚠️ Could not register ${clazz.name}, instance was null") } + } catch (e: ClassNotFoundException){ + LOGGER.fatal(LOADING, "Failed to load mod class {} for @EventBusSubscriber annotation", annotationData.clazz(), e) + throw RuntimeException(e) + } + } + } + } + + @Suppress("UNCHECKED_CAST") + private fun value(data: ModFileScanData.AnnotationData, key: String, default: R): R { + return data.annotationData.getOrDefault(key, default) as R + } + + private object EventBusSubscriberLogic { + private val STRICT_RUNTIME_CHECKS = java.lang.Boolean.getBoolean("eventbus.api.strictRuntimeChecks") + private val STRICT_REGISTRATION_CHECKS = + STRICT_RUNTIME_CHECKS || java.lang.Boolean.getBoolean("eventbus.api.strictRegistrationChecks") + + private val RETURNS_CONSUMER = MethodType.methodType(Consumer::class.java) + private val RETURNS_PREDICATE = MethodType.methodType(Predicate::class.java) + private val RETURNS_MONITOR = MethodType.methodType(ObjBooleanBiConsumer::class.java) + + private val CONSUMER_FI_TYPE = MethodType.methodType(Void.TYPE, Any::class.java) + private val PREDICATE_FI_TYPE = CONSUMER_FI_TYPE.changeReturnType(Boolean::class.javaPrimitiveType) + private val MONITOR_FI_TYPE = MethodType.methodType(Void.TYPE, Any::class.java, Boolean::class.javaPrimitiveType) + + @JvmStatic + fun register(busGroup: BusGroup?, listenerClass: Any) { + if (STRICT_REGISTRATION_CHECKS) registerStrict(busGroup, listenerClass) + else registerLenient(busGroup, listenerClass) + } + + fun registerLenient(busGroup: BusGroup?, listenerObject: Any) { + val kClass = listenerObject::class + val functions = kClass.declaredFunctions + var listeners = 0 + + LOGGER.debug("🔍 Inspecting Kotlin class: ${kClass.qualifiedName}") + for (function in functions) { + LOGGER.debug("→ Function: ${function.name}") + + val annotation = function.findAnnotation() + if (annotation == null) { + LOGGER.debug(" ⛔ Skipped: No @SubscribeEvent") + continue + } else { + LOGGER.debug(" ✅ Found @SubscribeEvent") + } + + val params = function.parameters + LOGGER.debug(" 🧩 Params count: ${params.size}") + LOGGER.debug(" 🧩 Params: ${params.joinToString { it.type.toString() }}") + LOGGER.debug(" 🧩 Return type: ${function.returnType}") + + if (params.size !in 2..3) { + LOGGER.warn(" ⛔ Skipped: Unexpected parameter count: ${params.size}") + continue + } + + val classifier = params[1].type.classifier + val eventParamKClass = classifier as? KClass<*> + if (eventParamKClass == null) { + LOGGER.warn(" ⛔ Skipped: Param[1] classifier is not a KClass: $classifier") + continue + } + + val eventParam = try { + eventParamKClass.java + } catch (e: Exception) { + LOGGER.warn(" ⛔ Skipped: Failed to get Java class for event param: ${e.message}") + continue } - if (ktObject != null) { - try { - LOGGER.debug(Logging.LOADING, "Auto-subscribing kotlin object ${annotationData.annotationType.className} to $busTarget") + LOGGER.debug(" 📦 Event param class: ${eventParam.name}") + + if (!Event::class.java.isAssignableFrom(eventParam)) { + LOGGER.warn(" ⛔ Skipped: ${eventParam.name} is not a subtype of Event") + continue + } + + val cancellable = Cancellable::class.java.isAssignableFrom(eventParam) + val eventClass = eventParam as Class + val bus = busGroup ?: if (IModBusEvent::class.java.isAssignableFrom(eventClass)) + KotlinModLoadingContext.get().getKBusGroup() + else BusGroup.DEFAULT + + val priority = annotation.priority + + try { + when (params.size) { + 2 -> { + when (function.returnType.classifier) { + Unit::class -> { + if (cancellable) { + val busC = CancellableEventBus.create(bus, castToCancellableEvent(eventClass)) + if (annotation.alwaysCancelling) + busC.addListener(priority, true) { function.call(listenerObject, it) } + else + busC.addListener(priority, Consumer { function.call(listenerObject, it) }) + } else { + EventBus.create(bus, eventClass) + .addListener(priority) { function.call(listenerObject, it) } + } + } + + Boolean::class -> { + if (!cancellable) error("Boolean return type only valid on cancellable events") + if (annotation.alwaysCancelling) error("Can't use alwaysCancelling with boolean return") + + val busC = CancellableEventBus.create(bus, castToCancellableEvent(eventClass)) + busC.addListener(priority, Predicate { + function.call(listenerObject, it) as Boolean + }) + } - registerTo(ktObject, busTarget, mod) - } catch (e: Throwable) { - LOGGER.fatal(Logging.LOADING, "Failed to load mod class ${annotationData.annotationType} for @EventBusSubscriber annotation", e) - throw RuntimeException(e) + else -> { + LOGGER.warn(" ⛔ Skipped: Unsupported return type: ${function.returnType}") + return + } + } + } + + 3 -> { + if (!cancellable) error("Two params only valid for cancellable events") + if (function.returnType.classifier != Unit::class) error("Third param requires void return") + if (params[2].type.classifier != Boolean::class) error("Third param must be boolean") + if (annotation.priority != Priority.MONITOR) error("Third param only allowed on MONITOR") + + val busC = CancellableEventBus.create(bus, castToCancellableEvent(eventClass)) + busC.addListener(ObjBooleanBiConsumer { event, cancelled -> + function.call(listenerObject, event, cancelled) + }) + } + + else -> { + LOGGER.warn(" ⛔ Skipped: Invalid param count ${params.size}") + return + } } + + LOGGER.info("✅ Registered: ${function.name} for ${eventClass.simpleName}") + listeners++ + + } catch (e: Exception) { + LOGGER.error("💥 Error registering function ${function.name}: ${e.message}", e) } } + + if (listeners == 0) { + LOGGER.error("❌ No valid listeners found in ${kClass.qualifiedName}") + error("❌ No valid listeners found in ${kClass.qualifiedName}") + } } - } - private fun getSides(annotationMap: Map): List { - val sidesHolders = annotationMap["value"] - return if (sidesHolders != null) { - if (enumHolderGetValue != null) { - (sidesHolders as List).map { data -> Dist.valueOf(enumHolderGetValue.invoke(data) as String) } - } else { - (sidesHolders as List).map { data -> Dist.valueOf(data.value()) } + fun registerStrict(busGroup: BusGroup?, listenerObject: Any) { + val kClass = listenerObject::class + val functions = kClass.declaredFunctions.filter { fn -> + // Filter out compiler-generated/internal stuff + fn.name !in setOf("equals", "hashCode", "toString") && + !fn.name.contains("\$") && // filters accessors/lambdas/inline$ methods + fn.visibility == KVisibility.PUBLIC + } + + if (functions.isEmpty()) { + val superClass = listenerObject::class.java.superclass + var msg = "No declared methods found in ${kClass.qualifiedName}" + if (superClass != null && superClass != Record::class.java && superClass != Enum::class.java) { + msg += ". Listener inheritance isn't supported. Use @Override + @SubscribeEvent on subclass." + } + error(msg) } - } else { - listOf(Dist.CLIENT, Dist.DEDICATED_SERVER) - } - } - private fun getBusTarget(annotationMap: Map): Mod.EventBusSubscriber.Bus { - val busTargetHolder = annotationMap["bus"] + var listeners = 0 - return if (busTargetHolder != null) { - if (enumHolderGetValue != null) { - Mod.EventBusSubscriber.Bus.valueOf(enumHolderGetValue.invoke(busTargetHolder) as String) - } else { - Mod.EventBusSubscriber.Bus.valueOf((busTargetHolder as EnumData).value) + for (func in functions) { + val annotation = func.findAnnotation() ?: continue + val params = func.parameters + val returnType = func.returnType + val isMonitor = annotation.priority == Priority.MONITOR + + // expect instance + 1 or 2 parameters (event, [boolean]) + if (params.size !in 2..3) { + error("Invalid parameter count on ${func.name}: ${params.size - 1}") + } + + val eventParamClass = params[1].type.classifier as? Class<*> ?: continue + if (!Event::class.java.isAssignableFrom(eventParamClass)) { + error("First parameter of ${func.name} must be Event") + } + + val cancellable = Cancellable::class.java.isAssignableFrom(eventParamClass) + val eventClass = eventParamClass as Class + val bus = busGroup ?: if (net.minecraftforge.fml.event.IModBusEvent::class.java.isAssignableFrom(eventClass)) + KotlinModLoadingContext.get().getKBusGroup() + else BusGroup.DEFAULT + + // Validate return type + if (returnType.classifier !in listOf(Void.TYPE.kotlin, Boolean::class)) { + error("Invalid return type on ${func.name}: $returnType") + } + + // Two-parameter (event, cancelled) validation + if (params.size == 3) { + if (!cancellable) error("Second param only valid for cancellable events in ${func.name}") + if (params[2].type.classifier != Boolean::class) error("Second param must be Boolean in ${func.name}") + if (!isMonitor) error("Second param only valid for MONITOR priority in ${func.name}") + if (returnType.classifier != Void.TYPE.kotlin) error("Cancellation-monitoring listener must return void in ${func.name}") + } + + // Cancel checks + if (!cancellable) { + if (annotation.alwaysCancelling) error("alwaysCancelling only valid on cancellable events in ${func.name}") + if (returnType.classifier == Boolean::class) error("boolean return type only valid on cancellable events in ${func.name}") + } + + // REGISTER + when (params.size) { + 2 -> { + val priority = annotation.priority + if (returnType.classifier == Void.TYPE.kotlin) { + if (cancellable) { + val busC = CancellableEventBus.create(bus, castToCancellableEvent(eventClass)) + if (annotation.alwaysCancelling) + busC.addListener(priority, true, Consumer { func.call(listenerObject, it) }) + else + busC.addListener(priority, Consumer { func.call(listenerObject, it) }) + } else { + EventBus.create(bus, eventClass).addListener(priority, Consumer { func.call(listenerObject, it) }) + } + } else { + if (!cancellable) error("Boolean return type only valid on cancellable events in ${func.name}") + if (annotation.alwaysCancelling) error("Cannot combine boolean return type with alwaysCancelling in ${func.name}") + val busC = CancellableEventBus.create(bus, castToCancellableEvent(eventClass)) + busC.addListener(priority, Predicate { func.call(listenerObject, it) as Boolean }) + } + } + + 3 -> { + val busC = CancellableEventBus.create(bus, castToCancellableEvent(eventClass)) + busC.addListener(ObjBooleanBiConsumer { event, cancelled -> + func.call(listenerObject, event, cancelled) + }) + } + } + + println("✅ Strictly registered: ${func.name} for ${eventClass.simpleName}") + listeners++ } - } else { - Mod.EventBusSubscriber.Bus.FORGE + + if (listeners == 0) error("❌ No valid listeners found in ${kClass.qualifiedName}") } - } - private fun registerTo(any: Any, target: Mod.EventBusSubscriber.Bus, mod: KotlinModContainer) { - if (target == Mod.EventBusSubscriber.Bus.FORGE) { - target.bus().get().register(any) - } else { - mod.eventBus.register(any) + @Suppress("UNCHECKED_CAST") + fun castToCancellableEvent(eventType: Class<*>): Class + where T : Event, T : Cancellable { + return eventType as Class } } } diff --git a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/EventBusMakers.kt b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/EventBusMakers.kt deleted file mode 100644 index 739cb8fb..00000000 --- a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/EventBusMakers.kt +++ /dev/null @@ -1,37 +0,0 @@ -package thedarkcolour.kotlinforforge - -import net.minecraftforge.eventbus.api.BusBuilder -import net.minecraftforge.eventbus.api.IEventBus -import net.minecraftforge.eventbus.api.IEventExceptionHandler -import net.minecraftforge.fml.event.IModBusEvent - -// todo remove in 5.x.x -internal sealed interface EventBusMaker { - fun make(exceptionHandler: IEventExceptionHandler): IEventBus -} - -// Reflection is needed since KFF compiled on 1.19.2 assumes BusBuilder methods are interface methods -// which produces bytecode incompatible with older versions that assume the methods are not interface -internal object OldEventBusMaker : EventBusMaker { - private val builderMethod = BusBuilder::class.java.getDeclaredMethod("builder") - private val setExceptionHandlerMethod = BusBuilder::class.java.getDeclaredMethod("setExceptionHandler", IEventExceptionHandler::class.java) - private val setTrackPhasesMethod = BusBuilder::class.java.getDeclaredMethod("setTrackPhases", Boolean::class.java) - private val markerTypeMethod = BusBuilder::class.java.getDeclaredMethod("markerType", Class::class.java) - private val buildMethod = BusBuilder::class.java.getDeclaredMethod("build") - - override fun make(exceptionHandler: IEventExceptionHandler): IEventBus { - val builder = builderMethod.invoke(null) - - setExceptionHandlerMethod.invoke(builder, exceptionHandler) - setTrackPhasesMethod.invoke(builder, false) - markerTypeMethod.invoke(builder, IModBusEvent::class.java) - - return buildMethod.invoke(builder) as IEventBus - } -} - -internal object NewEventBusMaker: EventBusMaker { - override fun make(exceptionHandler: IEventExceptionHandler): IEventBus { - return BusBuilder.builder().setExceptionHandler(exceptionHandler).setTrackPhases(false).markerType(IModBusEvent::class.java).useModLauncher().build() - } -} \ No newline at end of file diff --git a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt index a8d8578b..87091a63 100644 --- a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt +++ b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinLanguageProvider.kt @@ -21,9 +21,17 @@ public class KotlinLanguageProvider : IModLanguageProvider { override fun getFileVisitor(): Consumer { return Consumer { scanData -> + LOGGER.warn(">>> KotlinLanguageProvider scanning file: ${scanData.annotations.size} annotations total") + for (annotation in scanData.annotations) { + LOGGER.warn("📛 Annotation seen: ${annotation.annotationType} on ${annotation.clazz.className}") + if (annotation.annotationType.className.contains("KotlinMod")) { + LOGGER.error("⚠️ POTENTIAL MATCH: ${annotation.annotationType.className}") + } + } scanData.addLanguageLoader(scanData.annotations.filter { data -> data.annotationType == MOD_ANNOTATION }.associate { data -> + LOGGER.debug(Logging.SCAN, "Found annotations: ${scanData.annotations.map { it.annotationType }}") val modid = data.annotationData["value"] as String val modClass = data.clazz.className @@ -78,6 +86,6 @@ public class KotlinLanguageProvider : IModLanguageProvider { } private companion object { - private val MOD_ANNOTATION = Type.getType("Lnet/minecraftforge/fml/common/Mod;") + private val MOD_ANNOTATION = Type.getType("Lthedarkcolour/common/KotlinMod;") } } diff --git a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt index 8fc52e02..8f17db00 100644 --- a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt +++ b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModContainer.kt @@ -1,20 +1,23 @@ package thedarkcolour.kotlinforforge -import net.minecraftforge.eventbus.EventBusErrorMessage -import net.minecraftforge.eventbus.api.BusBuilder -import net.minecraftforge.eventbus.api.Event -import net.minecraftforge.eventbus.api.IEventBus -import net.minecraftforge.eventbus.api.IEventListener +import cpw.mods.jarhandling.SecureJar +import net.minecraftforge.eventbus.api.bus.BusGroup +import net.minecraftforge.eventbus.api.bus.EventBus +import net.minecraftforge.eventbus.api.event.InheritableEvent +import net.minecraftforge.eventbus.internal.Event import net.minecraftforge.fml.Logging import net.minecraftforge.fml.ModContainer import net.minecraftforge.fml.ModLoadingException import net.minecraftforge.fml.ModLoadingStage +import net.minecraftforge.fml.config.IConfigEvent import net.minecraftforge.fml.event.IModBusEvent import net.minecraftforge.forgespi.language.IModInfo import net.minecraftforge.forgespi.language.ModFileScanData -import java.util.* -import java.util.function.Consumer +import net.minecraftforge.unsafe.UnsafeHacks +import java.lang.reflect.Constructor +import java.lang.reflect.Method import java.util.function.Supplier +import java.util.jar.Attributes /** * Kotlin mod container @@ -26,48 +29,31 @@ public class KotlinModContainer( gameLayer: ModuleLayer, ) : ModContainer(info) { private var modInstance: Any? = null - internal val eventBus: IEventBus + internal var busGroup: BusGroup private val modClass: Class<*> + val ctx = KotlinModLoadingContext(this) + private var implAddExportsOrOpens: Method? = null init { LOGGER.debug(Logging.LOADING, "Creating KotlinModContainer instance for $className") - activityMap[ModLoadingStage.CONSTRUCT] = Runnable(::constructMod) - - eventBus = NewEventBusMaker.make(::onEventFailed) - - configHandler = Optional.of(Consumer { event -> - eventBus.post(event.self()) - }) - - val ctx = KotlinModLoadingContext(this) + busGroup = BusGroup.create("modBusFor ${info.getModId()}") contextExtension = Supplier {ctx} - + try { - val layer = gameLayer.findModule(info.owningFile.moduleName()).orElseThrow() + val moduleName = info.owningFile.moduleName() + val layer = gameLayer.findModule(moduleName).orElseThrow {IllegalStateException("Failed to find $moduleName in $gameLayer")} + openModules(gameLayer, layer, info.owningFile.file.secureJar) modClass = Class.forName(layer, className) - LOGGER.trace(Logging.LOADING, "Loaded modclass {} with {}", modClass.name, modClass.classLoader) + LOGGER.trace(Logging.LOADING, "Loaded modclass {}/{} with {}", modClass.module.name, modClass.name, modClass.classLoader) } catch (t: Throwable) { LOGGER.error(Logging.LOADING, "Failed to load class $className", t) throw ModLoadingException(info, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadmodclass", t) } } - private fun onEventFailed(iEventBus: IEventBus, event: Event, listeners: Array, busId: Int, throwable: Throwable) { - LOGGER.error(EventBusErrorMessage(event, busId, listeners, throwable)) - } - // Sets modInstance to a new instance of the mod class or the object instance private fun constructMod() { - try { - LOGGER.trace(Logging.LOADING, "Loading mod instance ${getModId()} of type ${modClass.name}") - modInstance = modClass.kotlin.objectInstance ?: modClass.getDeclaredConstructor().newInstance() - LOGGER.trace(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) - throw ModLoadingException(modInfo, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadmod", throwable, modClass) - } - try { LOGGER.trace(Logging.LOADING, "Injecting Automatic Kotlin event subscribers for ${getModId()}") // Inject into object EventBusSubscribers @@ -77,6 +63,15 @@ public class KotlinModContainer( LOGGER.error(Logging.LOADING, "Failed to register Automatic Kotlin subscribers. ModID: ${getModId()}, class ${modClass.name}", throwable) throw ModLoadingException(modInfo, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadmod", throwable, modClass) } + + try { + LOGGER.trace(Logging.LOADING, "Loading mod instance ${getModId()} of type ${modClass.name}") + modInstance = modClass.kotlin.objectInstance ?: modClass.getDeclaredConstructor().newInstance() + LOGGER.trace(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) + throw ModLoadingException(modInfo, ModLoadingStage.CONSTRUCT, "fml.modloading.failedtoloadmod", throwable, modClass) + } } override fun matches(mod: Any?): Boolean { @@ -85,9 +80,26 @@ public class KotlinModContainer( override fun getMod(): Any? = modInstance - public override fun acceptEvent(e: T) where T : Event, T : IModBusEvent { + public fun getModBusGroup(): BusGroup { + return busGroup + } + + override fun toString(): String { + val name = modInfo.modId + return "FMLModContainer[$name, ${javaClass.name}]" + } + + override fun dispatchConfigEvent(event: IConfigEvent) { + @Suppress("UNCHECKED_CAST") + val self = event.self() as InheritableEvent + val eventBus = EventBus.create( busGroup, self.javaClass) + eventBus.post(self) + } + + override fun acceptEvent(e: T){ try { LOGGER.trace("Firing event for modid $modId : $e") + var eventBus = IModBusEvent.getBus(busGroup, e.javaClass) eventBus.post(e) LOGGER.trace("Fired event for modid $modId : $e") } catch (t: Throwable) { @@ -95,4 +107,44 @@ public class KotlinModContainer( throw ModLoadingException(modInfo, modLoadingStage, "fml.modloading.errorduringevent", t) } } + + private fun openModules(layer: ModuleLayer, self: Module, jar: SecureJar) { + val manifest = jar.moduleDataProvider().manifest.mainAttributes + addOpenOrExports(layer, self, true, manifest) + addOpenOrExports(layer, self, false, manifest) + } + + private fun addOpenOrExports(layer: ModuleLayer, self: Module, open: Boolean, attrs: Attributes) { + val key = if (open) "Add-Opens" else "Add-Exports" + val entry = attrs.getValue(key) ?: return + entry.split(" ").forEach { pair -> + val pts = pair.trim().split("/") + if (pts.size == 2) { + val target = layer.findModule(pts[0]).orElse(null) + if (target != null && target.descriptor.packages().contains(pts[1])) { + addOpenOrExports(target, pts[1], self, open) + } + } else { + LOGGER.warn(LOADING, "Invalid {} entry in {}: {}", key, self.name, pair) + } + } + } + + private fun addOpenOrExports(target: Module, pkg: String, reader: Module, open: Boolean) { + if(implAddExportsOrOpens == null) { + implAddExportsOrOpens = Module::class.java.getDeclaredMethod("implAddExportsOrOpens", String::class.java, Module::class.java, java.lang.Boolean.TYPE, java.lang.Boolean.TYPE) + UnsafeHacks.setAccessible(implAddExportsOrOpens!!) + } + + LOGGER.info( + LOADING, + "{} {}/{} to {}", + if (open) "Opening" else "Exporting", + target.name, + pkg, + reader.name + ) + implAddExportsOrOpens?.invoke(target, pkg, reader, open, true) + } + } diff --git a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt index ac4a168d..ff871ab0 100644 --- a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt +++ b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/KotlinModLoadingContext.kt @@ -1,6 +1,6 @@ package thedarkcolour.kotlinforforge -import net.minecraftforge.eventbus.api.IEventBus +import net.minecraftforge.eventbus.api.bus.BusGroup import net.minecraftforge.fml.ModLoadingContext /** @@ -8,14 +8,18 @@ import net.minecraftforge.fml.ModLoadingContext */ public class KotlinModLoadingContext(private val container: KotlinModContainer) { /** Mods should access through [MOD_BUS] */ - public fun getKEventBus(): IEventBus { - return container.eventBus + public fun getKBusGroup(): BusGroup { + return container.busGroup + } + + public fun getContainer(): KotlinModContainer { + return container } public companion object { /** Mods should access through [MOD_CONTEXT] */ public fun get(): KotlinModLoadingContext { - return ModLoadingContext.get().extension() + return ModLoadingContext.get().extension() as KotlinModLoadingContext } } } diff --git a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/Logger.kt b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/Logger.kt index bc6f9d47..edfa9681 100644 --- a/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/Logger.kt +++ b/src/kfflang/forge/kotlin/thedarkcolour/kotlinforforge/Logger.kt @@ -1,6 +1,7 @@ package thedarkcolour.kotlinforforge import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.MarkerManager /** * Logger field for KotlinForForge. @@ -9,3 +10,4 @@ import org.apache.logging.log4j.LogManager * before [KotlinModContainer] should initialize. */ internal val LOGGER = LogManager.getLogger() +internal val LOADING = MarkerManager.getMarker("LOADING") diff --git a/src/kfflang/forge/resources/META-INF/services/net.minecraftforge.forgespi.language.IModLanguageProvider b/src/kfflang/forge/resources/META-INF/services/net.minecraftforge.forgespi.language.IModLanguageProvider index c78667ed..264af3bb 100644 --- a/src/kfflang/forge/resources/META-INF/services/net.minecraftforge.forgespi.language.IModLanguageProvider +++ b/src/kfflang/forge/resources/META-INF/services/net.minecraftforge.forgespi.language.IModLanguageProvider @@ -1 +1 @@ -thedarkcolour.kotlinforforge.KotlinLanguageProvider +thedarkcolour.kotlinforforge.KotlinLanguageProvider \ No newline at end of file diff --git a/src/kfflang/neoforge/kotlin/thedarkcolour/kotlinforforge/neoforge/KotlinModContainer.kt b/src/kfflang/neoforge/kotlin/thedarkcolour/kotlinforforge/neoforge/KotlinModContainer.kt index e56da118..b9e5800b 100644 --- a/src/kfflang/neoforge/kotlin/thedarkcolour/kotlinforforge/neoforge/KotlinModContainer.kt +++ b/src/kfflang/neoforge/kotlin/thedarkcolour/kotlinforforge/neoforge/KotlinModContainer.kt @@ -11,7 +11,6 @@ import net.neoforged.fml.ModContainer import net.neoforged.fml.ModLoadingException import net.neoforged.fml.ModLoadingIssue import net.neoforged.fml.event.IModBusEvent -import net.neoforged.fml.javafmlmod.AutomaticEventSubscriber import net.neoforged.fml.javafmlmod.FMLModContainer import net.neoforged.fml.loading.FMLLoader import net.neoforged.neoforgespi.language.IModInfo diff --git a/src/kfflib/forge/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt b/src/kfflib/forge/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt index 29e6b384..1814dad1 100644 --- a/src/kfflib/forge/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt +++ b/src/kfflib/forge/kotlin/thedarkcolour/kotlinforforge/forge/Forge.kt @@ -3,13 +3,16 @@ 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.EventPriority -import net.minecraftforge.eventbus.api.GenericEvent -import net.minecraftforge.eventbus.api.IEventBus +import net.minecraftforge.eventbus.api.bus.BusGroup +import net.minecraftforge.eventbus.api.listener.EventListener +import net.minecraftforge.eventbus.api.listener.Priority +import net.minecraftforge.eventbus.internal.Event +import net.minecraftforge.eventbus.internal.EventListenerImpl import net.minecraftforge.fml.ModLoadingContext import net.minecraftforge.fml.config.ModConfig import net.minecraftforge.fml.loading.FMLEnvironment import thedarkcolour.kotlinforforge.KotlinModLoadingContext +import java.lang.invoke.MethodHandles import java.util.function.Consumer import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -23,9 +26,11 @@ import kotlin.reflect.KProperty * @see net.minecraftforge.event.entity.living.LivingEvent * @see net.minecraftforge.event.world.BlockEvent */ -public inline val FORGE_BUS: IEventBus + +public inline val FORGE_BUS: BusGroup get() = MinecraftForge.EVENT_BUS + /** * Mod-specific event bus. * Mod lifecycle events are fired on this bus. @@ -35,8 +40,8 @@ public inline val FORGE_BUS: IEventBus * @see net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent * @see net.minecraftforge.registries.NewRegistryEvent */ -public inline val MOD_BUS: IEventBus - get() = KotlinModLoadingContext.get().getKEventBus() +public inline val MOD_BUS: BusGroup + get() = KotlinModLoadingContext.get().getKBusGroup() /** * Used in place of [net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext] @@ -95,12 +100,13 @@ public inline fun registerConfig(type: ModConfig.Type, spec: ForgeConfigSpec) { LOADING_CONTEXT.registerConfig(type, spec) } -public inline fun , reified F> IEventBus.addGenericListener( +public inline fun BusGroup.addGenericListener( listener: Consumer, - priority: EventPriority = EventPriority.NORMAL, + priority: Byte = Priority.NORMAL, receiveCancelled: Boolean = false ) { - addGenericListener(F::class.java, priority, receiveCancelled, listener) + val eventType = T::class.java + this.register(MethodHandles.lookup(), eventType) } /** diff --git a/src/kffmod/forge/kotlin/thedarkcolour/kotlinforforge/test/KotlinForForge.kt b/src/kffmod/forge/kotlin/thedarkcolour/kotlinforforge/test/KotlinForForge.kt index ca3ea6d6..8623fc00 100644 --- a/src/kffmod/forge/kotlin/thedarkcolour/kotlinforforge/test/KotlinForForge.kt +++ b/src/kffmod/forge/kotlin/thedarkcolour/kotlinforforge/test/KotlinForForge.kt @@ -1,10 +1,12 @@ package thedarkcolour.kotlinforforge.test -import net.minecraftforge.fml.common.Mod import org.apache.logging.log4j.LogManager +import thedarkcolour.common.KotlinMod +import thedarkcolour.kotlinforforge.KotlinModLoadingContext +import java.lang.annotation.ElementType -@Mod("kotlinforforge") -public object KotlinForForge { +@KotlinMod("kotlinforforge") +public class KotlinForForge(context: KotlinModLoadingContext) { private val LOGGER = LogManager.getLogger() init { LOGGER.info("Kotlin For Forge Enabled!")