From 10344637dfd82ee06a9f99583b6a6969c3acb8b2 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:14:59 +0100 Subject: [PATCH] feat: implement CloseContext remapper to handle PlayerQuitEvent correctly --- gradle.properties | 2 +- .../framework/CloseContextRemapper.kt | 237 ++++++++++++++++++ .../inventory/framework/InventoryLoader.kt | 1 + 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/CloseContextRemapper.kt diff --git a/gradle.properties b/gradle.properties index 2256470e..690b7d39 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.70.1 +version=1.21.11-2.70.2 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/CloseContextRemapper.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/CloseContextRemapper.kt new file mode 100644 index 00000000..79d728a1 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/CloseContextRemapper.kt @@ -0,0 +1,237 @@ +package dev.slne.surf.surfapi.bukkit.server.inventory.framework + +import net.bytebuddy.ByteBuddy +import net.bytebuddy.asm.AsmVisitorWrapper +import net.bytebuddy.description.field.FieldDescription +import net.bytebuddy.description.field.FieldList +import net.bytebuddy.description.method.MethodList +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.dynamic.ClassFileLocator +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy +import net.bytebuddy.implementation.Implementation +import net.bytebuddy.jar.asm.* +import net.bytebuddy.pool.TypePool +import net.bytebuddy.utility.OpenedClassReader + +/** + * Remaps the `CloseContext` class from the inventory-framework to accept `Object` instead of + * `InventoryCloseEvent` as the close origin parameter. + * + * This is necessary because `IFInventoryListener.onPlayerQuit()` calls + * `ElementFactory.createCloseContext(viewer, context, event)` where `event` is a + * `PlayerQuitEvent`, **not** an `InventoryCloseEvent`. The original `BukkitElementFactory` + * casts `closeOrigin` to `InventoryCloseEvent`, and the `CloseContext` constructor also + * expects `InventoryCloseEvent`, causing a `ClassCastException` at runtime. + * + * This remapper rewrites the bytecode of both `BukkitElementFactory` and `CloseContext` so that: + * 1. `BukkitElementFactory.createCloseContext()` no longer casts the origin to `InventoryCloseEvent`. + * 2. `CloseContext`'s constructor accepts `Object` instead of `InventoryCloseEvent`. + * 3. `CloseContext`'s `closeOrigin` field is typed as `Object` instead of `InventoryCloseEvent`. + */ +object CloseContextRemapper { + + private const val INVENTORY_CLOSE_EVENT_INTERNAL = "org/bukkit/event/inventory/InventoryCloseEvent" + private const val OBJECT_INTERNAL = "java/lang/Object" + + private const val CLOSE_CONTEXT_INTERNAL = + "dev/slne/surf/surfapi/libs/devnatan/inventoryframework/context/CloseContext" + private const val BUKKIT_ELEMENT_FACTORY_INTERNAL = + "dev/slne/surf/surfapi/libs/devnatan/inventoryframework/internal/BukkitElementFactory" + + fun remap() { + remapCloseContext() + remapBukkitElementFactory() + } + + /** + * Remaps `CloseContext`: + * - Field `closeOrigin`: `InventoryCloseEvent` → `Object` + * - Constructor descriptor: `(…, InventoryCloseEvent)` → `(…, Object)` + * - Method bodies: removes `CHECKCAST InventoryCloseEvent` instructions + */ + private fun remapCloseContext() { + val locator = ClassFileLocator.ForClassLoader.of(javaClass.classLoader) + val typePool = TypePool.Default.of(locator) + val typeDescription = typePool.describe(CLOSE_CONTEXT_INTERNAL.replace("/", ".")).resolve() + + ByteBuddy() + .redefine(typeDescription, locator) + .visit(CloseContextClassVisitorWrapper()) + .make() + .load(javaClass.classLoader, ClassLoadingStrategy.Default.INJECTION) + } + + private class CloseContextClassVisitorWrapper : AsmVisitorWrapper { + override fun mergeWriter(flags: Int): Int = flags + override fun mergeReader(flags: Int): Int = flags + + override fun wrap( + instrumentedType: TypeDescription, + classVisitor: ClassVisitor, + implementationContext: Implementation.Context, + typePool: TypePool, + fields: FieldList, + methods: MethodList<*>, + writerFlags: Int, + readerFlags: Int + ): ClassVisitor { + return CloseContextClassVisitor(classVisitor) + } + } + + private class CloseContextClassVisitor( + visitor: ClassVisitor + ) : ClassVisitor(OpenedClassReader.ASM_API, visitor) { + + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any? + ): FieldVisitor? { + // Change field type: InventoryCloseEvent -> Object + val remappedDescriptor = remapDescriptor(descriptor) + return super.visitField(access, name, remappedDescriptor, signature, value) + } + + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + // Change method descriptor: replace InventoryCloseEvent with Object + val remappedDescriptor = remapDescriptor(descriptor) + val mv = super.visitMethod(access, name, remappedDescriptor, signature, exceptions) + return CloseContextMethodVisitor(mv) + } + } + + private class CloseContextMethodVisitor( + visitor: MethodVisitor + ) : MethodVisitor(OpenedClassReader.ASM_API, visitor) { + + override fun visitTypeInsn(opcode: Int, type: String) { + // Remove CHECKCAST to InventoryCloseEvent + if (opcode == Opcodes.CHECKCAST && type == INVENTORY_CLOSE_EVENT_INTERNAL) { + return // skip the cast entirely + } + super.visitTypeInsn(opcode, type) + } + + override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { + // Remap field access descriptors + val remappedDescriptor = remapDescriptor(descriptor) + super.visitFieldInsn(opcode, owner, name, remappedDescriptor) + } + + override fun visitMethodInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean + ) { + // Remap method invocation descriptors (e.g. constructor calls within CloseContext) + val remappedDescriptor = remapDescriptor(descriptor) + super.visitMethodInsn(opcode, owner, name, remappedDescriptor, isInterface) + } + } + + /** + * Remaps `BukkitElementFactory.createCloseContext()`: + * - Removes the `CHECKCAST InventoryCloseEvent` instruction + * - Rewrites the `CloseContext.` invocation descriptor to use `Object` + */ + private fun remapBukkitElementFactory() { + val locator = ClassFileLocator.ForClassLoader.of(javaClass.classLoader) + val typePool = TypePool.Default.of(locator) + val typeDescription = typePool.describe(BUKKIT_ELEMENT_FACTORY_INTERNAL.replace("/", ".")).resolve() + + ByteBuddy() + .redefine(typeDescription, locator) + .visit(BukkitElementFactoryClassVisitorWrapper()) + .make() + .load(javaClass.classLoader, ClassLoadingStrategy.Default.INJECTION) + } + + private class BukkitElementFactoryClassVisitorWrapper : AsmVisitorWrapper { + override fun mergeWriter(flags: Int): Int = flags + override fun mergeReader(flags: Int): Int = flags + + override fun wrap( + instrumentedType: TypeDescription, + classVisitor: ClassVisitor, + implementationContext: Implementation.Context, + typePool: TypePool, + fields: FieldList, + methods: MethodList<*>, + writerFlags: Int, + readerFlags: Int + ): ClassVisitor { + return BukkitElementFactoryClassVisitor(classVisitor) + } + } + + private class BukkitElementFactoryClassVisitor( + visitor: ClassVisitor + ) : ClassVisitor(OpenedClassReader.ASM_API, visitor) { + + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + val mv = super.visitMethod(access, name, descriptor, signature, exceptions) + + // Only remap the createCloseContext method + if (name == "createCloseContext") { + return BukkitElementFactoryMethodVisitor(mv) + } + + return mv + } + } + + private class BukkitElementFactoryMethodVisitor( + visitor: MethodVisitor + ) : MethodVisitor(OpenedClassReader.ASM_API, visitor) { + + override fun visitTypeInsn(opcode: Int, type: String) { + // Remove CHECKCAST to InventoryCloseEvent + if (opcode == Opcodes.CHECKCAST && type == INVENTORY_CLOSE_EVENT_INTERNAL) { + return // skip the cast entirely + } + super.visitTypeInsn(opcode, type) + } + + override fun visitMethodInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean + ) { + // Remap the CloseContext constructor invocation descriptor + if (owner == CLOSE_CONTEXT_INTERNAL && name == "") { + val remappedDescriptor = remapDescriptor(descriptor) + super.visitMethodInsn(opcode, owner, name, remappedDescriptor, isInterface) + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + } + } + } + + /** + * Replaces all occurrences of `InventoryCloseEvent` with `Object` in a type descriptor. + */ + private fun remapDescriptor(descriptor: String): String { + val closeEventDescriptor = Type.getObjectType(INVENTORY_CLOSE_EVENT_INTERNAL).descriptor + val objectDescriptor = Type.getObjectType(OBJECT_INTERNAL).descriptor + return descriptor.replace(closeEventDescriptor, objectDescriptor) + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt index e21db76f..ca862825 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt @@ -6,6 +6,7 @@ import me.devnatan.inventoryframework.ViewFrame object InventoryLoader { init { InventoryViewRemapper.remap() + CloseContextRemapper.remap() } lateinit var viewFrame: ViewFrame