1+ package dev.slne.surf.api.paper.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.ClassVisitor
13+ import net.bytebuddy.jar.asm.MethodVisitor
14+ import net.bytebuddy.jar.asm.Opcodes
15+ import net.bytebuddy.pool.TypePool
16+ import net.bytebuddy.utility.OpenedClassReader
17+ import org.bukkit.Bukkit
18+
19+ /* *
20+ * Remaps inventory-framework's McVersion parser to support Paper's new version format:
21+ *
22+ * 26.1.2.build.23-alpha
23+ *
24+ * inventory-framework 3.7.1 detects whether a patch version exists via a private method named
25+ * `countColons`, which actually counts dots and expects exactly 3 dots for versions like:
26+ *
27+ * 1.21.11-R0.1-SNAPSHOT
28+ *
29+ * With the new Paper format, `26.1.2.build.23-alpha` contains 4 dots, so IF incorrectly parses it
30+ * as `26.1` instead of `26.1.2`.
31+ *
32+ * This remapper replaces only the private static `countColons(String): Int` method with a parser
33+ * that returns `3` whenever the Minecraft version part has a numeric patch component.
34+ */
35+ object McVersionRemapper {
36+
37+ private const val MC_VERSION_INTERNAL =
38+ " dev/slne/surf/api/libs/devnatan/inventoryframework/runtime/thirdparty/McVersion"
39+
40+ private const val REMAPPER_INTERNAL =
41+ " dev/slne/surf/api/paper/server/inventory/framework/McVersionRemapper"
42+
43+ fun isRemapNecessary (): Boolean {
44+ return try {
45+ val version = Bukkit .getBukkitVersion()
46+ version.contains(" .build." ) || hasNumericPatchVersionPart(version)
47+ } catch (_: Throwable ) {
48+ false
49+ }
50+ }
51+
52+ fun remap () {
53+ if (! isRemapNecessary()) return
54+
55+ val locator = ClassFileLocator .ForClassLoader .of(javaClass.classLoader)
56+ val typePool = TypePool .Default .of(locator)
57+ val typeDescription = typePool.describe(MC_VERSION_INTERNAL .replace(" /" , " ." )).resolve()
58+
59+ ByteBuddy ()
60+ .redefine<Any >(typeDescription, locator)
61+ .visit(McVersionClassVisitorWrapper ())
62+ .make()
63+ .load(javaClass.classLoader, ClassLoadingStrategy .Default .INJECTION )
64+ }
65+
66+ /* *
67+ * Replacement for inventory-framework's private static `countColons(String): Int`.
68+ *
69+ * Important: The original code checks `countColons(version) == 3` before parsing
70+ * `version.split(".")[2]` as the patch number.
71+ *
72+ * Therefore this method deliberately returns:
73+ * - `3` if the Minecraft version part has a numeric patch, e.g. `1.21.11` or `26.1.2`
74+ * - `2` for patchless Paper build versions, e.g. `26.1.build.23-alpha`
75+ * - the original dot count for legacy/non-build versions
76+ */
77+ @JvmStatic
78+ @Suppress(" unused" )
79+ fun countVersionDotsForPatchDetection (version : String ): Int {
80+ if (version.contains(" .build." )) {
81+ val mcVersionPart = version.substringBefore(" .build." )
82+ return if (hasNumericPatchVersionPart(mcVersionPart)) 3 else 2
83+ }
84+
85+ return if (hasNumericPatchVersionPart(version)) {
86+ 3
87+ } else {
88+ version.count { it == ' .' }
89+ }
90+ }
91+
92+ private fun hasNumericPatchVersionPart (version : String ): Boolean {
93+ val mcVersionPart = version.substringBefore(' -' )
94+ val parts = mcVersionPart.split(' .' )
95+
96+ return parts.size >= 3 &&
97+ parts[0 ].isNotEmpty() &&
98+ parts[1 ].isNotEmpty() &&
99+ parts[2 ].isNotEmpty() &&
100+ parts[0 ].all(Char ::isDigit) &&
101+ parts[1 ].all(Char ::isDigit) &&
102+ parts[2 ].all(Char ::isDigit)
103+ }
104+
105+ private class McVersionClassVisitorWrapper : AsmVisitorWrapper {
106+ override fun mergeWriter (flags : Int ): Int = flags
107+ override fun mergeReader (flags : Int ): Int = flags
108+
109+ override fun wrap (
110+ instrumentedType : TypeDescription ,
111+ classVisitor : ClassVisitor ,
112+ implementationContext : Implementation .Context ,
113+ typePool : TypePool ,
114+ fields : FieldList <FieldDescription .InDefinedShape ?>,
115+ methods : MethodList <* >,
116+ writerFlags : Int ,
117+ readerFlags : Int
118+ ): ClassVisitor {
119+ return McVersionClassVisitor (classVisitor)
120+ }
121+ }
122+
123+ private class McVersionClassVisitor (
124+ visitor : ClassVisitor
125+ ) : ClassVisitor(OpenedClassReader .ASM_API , visitor) {
126+
127+ override fun visitMethod (
128+ access : Int ,
129+ name : String ,
130+ descriptor : String ,
131+ signature : String? ,
132+ exceptions : Array <String >?
133+ ): MethodVisitor ? {
134+ if (name == " countColons" && descriptor == " (Ljava/lang/String;)I" ) {
135+ val mv = super .visitMethod(access, name, descriptor, signature, exceptions)
136+
137+ mv.visitCode()
138+ mv.visitVarInsn(Opcodes .ALOAD , 0 )
139+ mv.visitMethodInsn(
140+ Opcodes .INVOKESTATIC ,
141+ REMAPPER_INTERNAL ,
142+ " countVersionDotsForPatchDetection" ,
143+ " (Ljava/lang/String;)I" ,
144+ false
145+ )
146+ mv.visitInsn(Opcodes .IRETURN )
147+ mv.visitMaxs(1 , 1 )
148+ mv.visitEnd()
149+
150+ return null
151+ }
152+
153+ return super .visitMethod(access, name, descriptor, signature, exceptions)
154+ }
155+ }
156+ }
0 commit comments