Skip to content

Commit 834cc99

Browse files
authored
Implement CloseContext remapper for PlayerQuitEvent handling (#260)
2 parents a52d04c + 9650c45 commit 834cc99

3 files changed

Lines changed: 239 additions & 1 deletion

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
77
javaVersion=25
88
mcVersion=1.21.11
99
group=dev.slne.surf
10-
version=1.21.11-2.70.1
10+
version=1.21.11-2.70.2
1111
relocationPrefix=dev.slne.surf.surfapi.libs
1212
snapshot=false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package dev.slne.surf.surfapi.bukkit.server.inventory.framework
2+
3+
import net.bytebuddy.ByteBuddy
4+
import net.bytebuddy.asm.AsmVisitorWrapper
5+
import net.bytebuddy.description.field.FieldDescription
6+
import net.bytebuddy.description.field.FieldList
7+
import net.bytebuddy.description.method.MethodList
8+
import net.bytebuddy.description.type.TypeDescription
9+
import net.bytebuddy.dynamic.ClassFileLocator
10+
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy
11+
import net.bytebuddy.implementation.Implementation
12+
import net.bytebuddy.jar.asm.*
13+
import net.bytebuddy.pool.TypePool
14+
import net.bytebuddy.utility.OpenedClassReader
15+
16+
/**
17+
* Remaps the `CloseContext` class from the inventory-framework to accept `Object` instead of
18+
* `InventoryCloseEvent` as the close origin parameter.
19+
*
20+
* This is necessary because `IFInventoryListener.onPlayerQuit()` calls
21+
* `ElementFactory.createCloseContext(viewer, context, event)` where `event` is a
22+
* `PlayerQuitEvent`, **not** an `InventoryCloseEvent`. The original `BukkitElementFactory`
23+
* casts `closeOrigin` to `InventoryCloseEvent`, and the `CloseContext` constructor also
24+
* expects `InventoryCloseEvent`, causing a `ClassCastException` at runtime.
25+
*
26+
* This remapper rewrites the bytecode of both `BukkitElementFactory` and `CloseContext` so that:
27+
* 1. `BukkitElementFactory.createCloseContext()` no longer casts the origin to `InventoryCloseEvent`.
28+
* 2. `CloseContext`'s constructor accepts `Object` instead of `InventoryCloseEvent`.
29+
* 3. `CloseContext`'s `closeOrigin` field is typed as `Object` instead of `InventoryCloseEvent`.
30+
*/
31+
object CloseContextRemapper {
32+
33+
private const val INVENTORY_CLOSE_EVENT_INTERNAL = "org/bukkit/event/inventory/InventoryCloseEvent"
34+
private const val OBJECT_INTERNAL = "java/lang/Object"
35+
36+
private const val CLOSE_CONTEXT_INTERNAL =
37+
"dev/slne/surf/surfapi/libs/devnatan/inventoryframework/context/CloseContext"
38+
private const val BUKKIT_ELEMENT_FACTORY_INTERNAL =
39+
"dev/slne/surf/surfapi/libs/devnatan/inventoryframework/internal/BukkitElementFactory"
40+
41+
fun remap() {
42+
remapCloseContext()
43+
remapBukkitElementFactory()
44+
}
45+
46+
/**
47+
* Remaps `CloseContext`:
48+
* - Field `closeOrigin`: `InventoryCloseEvent` → `Object`
49+
* - Constructor descriptor: `(…, InventoryCloseEvent)` → `(…, Object)`
50+
* - Method bodies: removes `CHECKCAST InventoryCloseEvent` instructions
51+
*/
52+
private fun remapCloseContext() {
53+
val locator = ClassFileLocator.ForClassLoader.of(javaClass.classLoader)
54+
val typePool = TypePool.Default.of(locator)
55+
val typeDescription = typePool.describe(CLOSE_CONTEXT_INTERNAL.replace("/", ".")).resolve()
56+
57+
ByteBuddy()
58+
.redefine<Any>(typeDescription, locator)
59+
.visit(CloseContextClassVisitorWrapper())
60+
.make()
61+
.load(javaClass.classLoader, ClassLoadingStrategy.Default.INJECTION)
62+
}
63+
64+
private class CloseContextClassVisitorWrapper : AsmVisitorWrapper {
65+
override fun mergeWriter(flags: Int): Int = flags
66+
override fun mergeReader(flags: Int): Int = flags
67+
68+
override fun wrap(
69+
instrumentedType: TypeDescription,
70+
classVisitor: ClassVisitor,
71+
implementationContext: Implementation.Context,
72+
typePool: TypePool,
73+
fields: FieldList<FieldDescription.InDefinedShape?>,
74+
methods: MethodList<*>,
75+
writerFlags: Int,
76+
readerFlags: Int
77+
): ClassVisitor {
78+
return CloseContextClassVisitor(classVisitor)
79+
}
80+
}
81+
82+
private class CloseContextClassVisitor(
83+
visitor: ClassVisitor
84+
) : ClassVisitor(OpenedClassReader.ASM_API, visitor) {
85+
86+
override fun visitField(
87+
access: Int,
88+
name: String,
89+
descriptor: String,
90+
signature: String?,
91+
value: Any?
92+
): FieldVisitor? {
93+
// Change field type: InventoryCloseEvent -> Object
94+
val remappedDescriptor = remapDescriptor(descriptor)
95+
return super.visitField(access, name, remappedDescriptor, signature, value)
96+
}
97+
98+
override fun visitMethod(
99+
access: Int,
100+
name: String,
101+
descriptor: String,
102+
signature: String?,
103+
exceptions: Array<String>?
104+
): MethodVisitor {
105+
// Change method descriptor: replace InventoryCloseEvent with Object
106+
val remappedDescriptor = remapDescriptor(descriptor)
107+
val mv = super.visitMethod(access, name, remappedDescriptor, signature, exceptions)
108+
return CloseContextMethodVisitor(mv)
109+
}
110+
}
111+
112+
private class CloseContextMethodVisitor(
113+
visitor: MethodVisitor
114+
) : MethodVisitor(OpenedClassReader.ASM_API, visitor) {
115+
116+
override fun visitTypeInsn(opcode: Int, type: String) {
117+
// Remove CHECKCAST to InventoryCloseEvent
118+
if (opcode == Opcodes.CHECKCAST && type == INVENTORY_CLOSE_EVENT_INTERNAL) {
119+
return // skip the cast entirely
120+
}
121+
super.visitTypeInsn(opcode, type)
122+
}
123+
124+
override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) {
125+
// Remap field access descriptors
126+
val remappedDescriptor = remapDescriptor(descriptor)
127+
super.visitFieldInsn(opcode, owner, name, remappedDescriptor)
128+
}
129+
130+
override fun visitMethodInsn(
131+
opcode: Int,
132+
owner: String,
133+
name: String,
134+
descriptor: String,
135+
isInterface: Boolean
136+
) {
137+
// Remap method invocation descriptors (e.g. constructor calls within CloseContext)
138+
val remappedDescriptor = remapDescriptor(descriptor)
139+
super.visitMethodInsn(opcode, owner, name, remappedDescriptor, isInterface)
140+
}
141+
}
142+
143+
/**
144+
* Remaps `BukkitElementFactory.createCloseContext()`:
145+
* - Removes the `CHECKCAST InventoryCloseEvent` instruction
146+
* - Rewrites the `CloseContext.<init>` invocation descriptor to use `Object`
147+
*/
148+
private fun remapBukkitElementFactory() {
149+
val locator = ClassFileLocator.ForClassLoader.of(javaClass.classLoader)
150+
val typePool = TypePool.Default.of(locator)
151+
val typeDescription = typePool.describe(BUKKIT_ELEMENT_FACTORY_INTERNAL.replace("/", ".")).resolve()
152+
153+
ByteBuddy()
154+
.redefine<Any>(typeDescription, locator)
155+
.visit(BukkitElementFactoryClassVisitorWrapper())
156+
.make()
157+
.load(javaClass.classLoader, ClassLoadingStrategy.Default.INJECTION)
158+
}
159+
160+
private class BukkitElementFactoryClassVisitorWrapper : AsmVisitorWrapper {
161+
override fun mergeWriter(flags: Int): Int = flags
162+
override fun mergeReader(flags: Int): Int = flags
163+
164+
override fun wrap(
165+
instrumentedType: TypeDescription,
166+
classVisitor: ClassVisitor,
167+
implementationContext: Implementation.Context,
168+
typePool: TypePool,
169+
fields: FieldList<FieldDescription.InDefinedShape?>,
170+
methods: MethodList<*>,
171+
writerFlags: Int,
172+
readerFlags: Int
173+
): ClassVisitor {
174+
return BukkitElementFactoryClassVisitor(classVisitor)
175+
}
176+
}
177+
178+
private class BukkitElementFactoryClassVisitor(
179+
visitor: ClassVisitor
180+
) : ClassVisitor(OpenedClassReader.ASM_API, visitor) {
181+
182+
override fun visitMethod(
183+
access: Int,
184+
name: String,
185+
descriptor: String,
186+
signature: String?,
187+
exceptions: Array<String>?
188+
): MethodVisitor {
189+
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
190+
191+
// Only remap the createCloseContext method
192+
if (name == "createCloseContext") {
193+
return BukkitElementFactoryMethodVisitor(mv)
194+
}
195+
196+
return mv
197+
}
198+
}
199+
200+
private class BukkitElementFactoryMethodVisitor(
201+
visitor: MethodVisitor
202+
) : MethodVisitor(OpenedClassReader.ASM_API, visitor) {
203+
204+
override fun visitTypeInsn(opcode: Int, type: String) {
205+
// Remove CHECKCAST to InventoryCloseEvent
206+
if (opcode == Opcodes.CHECKCAST && type == INVENTORY_CLOSE_EVENT_INTERNAL) {
207+
return // skip the cast entirely
208+
}
209+
super.visitTypeInsn(opcode, type)
210+
}
211+
212+
override fun visitMethodInsn(
213+
opcode: Int,
214+
owner: String,
215+
name: String,
216+
descriptor: String,
217+
isInterface: Boolean
218+
) {
219+
// Remap the CloseContext constructor invocation descriptor
220+
if (owner == CLOSE_CONTEXT_INTERNAL && name == "<init>") {
221+
val remappedDescriptor = remapDescriptor(descriptor)
222+
super.visitMethodInsn(opcode, owner, name, remappedDescriptor, isInterface)
223+
} else {
224+
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
225+
}
226+
}
227+
}
228+
229+
/**
230+
* Replaces all occurrences of `InventoryCloseEvent` with `Object` in a type descriptor.
231+
*/
232+
private fun remapDescriptor(descriptor: String): String {
233+
val closeEventDescriptor = Type.getObjectType(INVENTORY_CLOSE_EVENT_INTERNAL).descriptor
234+
val objectDescriptor = Type.getObjectType(OBJECT_INTERNAL).descriptor
235+
return descriptor.replace(closeEventDescriptor, objectDescriptor)
236+
}
237+
}

surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import me.devnatan.inventoryframework.ViewFrame
66
object InventoryLoader {
77
init {
88
InventoryViewRemapper.remap()
9+
CloseContextRemapper.remap()
910
}
1011

1112
lateinit var viewFrame: ViewFrame

0 commit comments

Comments
 (0)