Skip to content

Implement CloseContext remapper for PlayerQuitEvent handling#260

Merged
twisti-dev merged 2 commits intoversion/1.21.11from
fix/if-player-quit
Mar 21, 2026
Merged

Implement CloseContext remapper for PlayerQuitEvent handling#260
twisti-dev merged 2 commits intoversion/1.21.11from
fix/if-player-quit

Conversation

@twisti-dev
Copy link
Contributor

This pull request introduces a new runtime bytecode remapping solution to address a compatibility issue with the inventory-framework library, specifically around the handling of close events in inventory views. The main change is the addition of the CloseContextRemapper, which rewrites the bytecode of third-party classes so that the CloseContext class and related factory methods can accept any event object (not just InventoryCloseEvent), preventing runtime ClassCastExceptions when handling player quit events.

The most important changes are:

Bytecode Remapping for Inventory Framework Compatibility:

  • Added CloseContextRemapper in CloseContextRemapper.kt, which uses ByteBuddy and ASM to rewrite the CloseContext and BukkitElementFactory classes at runtime so that the closeOrigin parameter and field are typed as Object instead of InventoryCloseEvent. This removes unsafe casts and makes the code compatible with both InventoryCloseEvent and PlayerQuitEvent.
  • Updated InventoryLoader.kt to invoke CloseContextRemapper.remap() during initialization, ensuring the remapping is applied as soon as the inventory framework is loaded.

Versioning:

  • Bumped the project version in gradle.properties from 1.21.11-2.70.1 to 1.21.11-2.70.2 to reflect these changes.

@twisti-dev twisti-dev self-assigned this Mar 21, 2026
Copilot AI review requested due to automatic review settings March 21, 2026 22:38
@twisti-dev twisti-dev merged commit 834cc99 into version/1.21.11 Mar 21, 2026
3 of 4 checks passed
@twisti-dev twisti-dev deleted the fix/if-player-quit branch March 21, 2026 22:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a runtime ByteBuddy/ASM-based remap to make the shaded inventory-framework close handling tolerant to non-InventoryCloseEvent origins (notably PlayerQuitEvent), preventing runtime ClassCastExceptions during view close handling.

Changes:

  • Added CloseContextRemapper to rewrite CloseContext and BukkitElementFactory bytecode so the close-origin is treated as Object and the unsafe cast is removed.
  • Hooked the new remapper into InventoryLoader initialization so it runs as soon as the inventory framework is loaded.
  • Bumped project version to 1.21.11-2.70.2.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt Calls the new remapper during inventory framework bootstrap.
surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/CloseContextRemapper.kt Implements the ByteBuddy/ASM rewrite for CloseContext + BukkitElementFactory.
gradle.properties Version bump for the release containing the compatibility fix.

Comment on lines 7 to 10
init {
InventoryViewRemapper.remap()
CloseContextRemapper.remap()
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloseContextRemapper.remap() is invoked during InventoryLoader static initialization without any failure isolation. If the target classes are missing/renamed or injection fails, this will throw during object init and can prevent the plugin from enabling. Consider wrapping the remap call in a guarded/optional path (e.g., catch Throwable and log a warning) so startup doesn’t hard-fail on incompatible inventory-framework versions.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +62
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<Any>(typeDescription, locator)
.visit(CloseContextClassVisitorWrapper())
.make()
.load(javaClass.classLoader, ClassLoadingStrategy.Default.INJECTION)
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remapCloseContext()/remapBukkitElementFactory() unconditionally resolve types via TypePool.describe(...).resolve() and then inject with ClassLoadingStrategy.Default.INJECTION. Both steps can throw (type not found, class already loaded -> LinkageError, etc.), but the errors aren’t handled here. To make this safer in production, add a cheap “is remap necessary?” check (similar to InventoryViewRemapper.isRemapNecessary()), and/or catch Throwable and log/skip when remapping can’t be applied.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +109
override fun visitMethod(
access: Int,
name: String,
descriptor: String,
signature: String?,
exceptions: Array<String>?
): MethodVisitor {
// Change method descriptor: replace InventoryCloseEvent with Object
val remappedDescriptor = remapDescriptor(descriptor)
val mv = super.visitMethod(access, name, remappedDescriptor, signature, exceptions)
return CloseContextMethodVisitor(mv)
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloseContextClassVisitor.visitMethod() remaps the descriptor for every method in CloseContext. That widens the blast radius beyond the constructor/field and can cause linkage issues if any other inventory-framework classes (or already-loaded code) still reference the original method descriptors. Consider restricting descriptor rewriting to the specific members that need to change (e.g., <init> and the closeOrigin accessor/field), so the patch is as minimal as possible.

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +197
override fun visitMethod(
access: Int,
name: String,
descriptor: String,
signature: String?,
exceptions: Array<String>?
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)

// Only remap the createCloseContext method
if (name == "createCloseContext") {
return BukkitElementFactoryMethodVisitor(mv)
}

return mv
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BukkitElementFactoryClassVisitor targets methods solely by name == "createCloseContext". If the upstream library adds an overload with the same name, this visitor could unintentionally rewrite the wrong method. Matching on both name and descriptor (or verifying the method bytecode actually contains the InventoryCloseEvent cast / CloseContext.<init> call) would make this remap more robust across library updates.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants