Skip to content

Commit dda284c

Browse files
dmulloy2rkfsociety
andcommitted
Fix custom payload handling on modern servers
Co-authored-by: rkfsociety <romankuzminvital@gmail.com>
1 parent debdd6d commit dda284c

4 files changed

Lines changed: 254 additions & 64 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
3+
* Copyright (C) 2015 dmulloy2
4+
* <p>
5+
* This program is free software; you can redistribute it and/or modify it under the terms of the
6+
* GNU General Public License as published by the Free Software Foundation; either version 2 of
7+
* the License, or (at your option) any later version.
8+
* <p>
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10+
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11+
* See the GNU General Public License for more details.
12+
* <p>
13+
* You should have received a copy of the GNU General Public License along with this program;
14+
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
15+
* 02111-1307 USA
16+
*/
17+
package com.comphenix.protocol.updater;
18+
19+
import com.comphenix.protocol.ProtocolLibrary;
20+
import com.comphenix.protocol.error.Report;
21+
22+
/**
23+
* Update check worker (top-level class so the runtime always loads a single {@code .class} entry).
24+
*/
25+
final class SpigotUpdateRunnable implements Runnable {
26+
27+
private final SpigotUpdater updater;
28+
29+
SpigotUpdateRunnable(SpigotUpdater updater) {
30+
this.updater = updater;
31+
}
32+
33+
@Override
34+
public void run() {
35+
try {
36+
String version = this.updater.getSpigotVersion();
37+
this.updater.setRemoteVersion(version);
38+
39+
if (this.updater.versionCheck(version)) {
40+
this.updater.result = Updater.UpdateResult.SPIGOT_UPDATE_AVAILABLE;
41+
} else {
42+
this.updater.result = Updater.UpdateResult.NO_UPDATE;
43+
}
44+
} catch (Throwable ex) {
45+
if (ProtocolLibrary.getConfig().isDebug()) {
46+
ProtocolLibrary.getErrorReporter().reportDetailed(
47+
this.updater,
48+
Report.newBuilder(Updater.REPORT_CANNOT_UPDATE_PLUGIN).error(ex).callerParam(this));
49+
}
50+
51+
ProtocolLibrary.disableUpdates();
52+
} finally {
53+
for (Runnable listener : this.updater.listeners) {
54+
ProtocolLibrary.getScheduler().runTask(listener);
55+
}
56+
}
57+
}
58+
}

‎src/main/java/com/comphenix/protocol/updater/SpigotUpdater.java‎

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
*/
1717
package com.comphenix.protocol.updater;
1818

19-
import com.comphenix.protocol.ProtocolLibrary;
20-
import com.comphenix.protocol.error.Report;
2119
import com.comphenix.protocol.utility.Closer;
2220
import org.bukkit.plugin.Plugin;
2321

@@ -44,7 +42,7 @@ public void start(UpdateType type) {
4442
waitForThread();
4543

4644
this.type = type;
47-
this.thread = new Thread(new SpigotUpdateRunnable());
45+
this.thread = new Thread(new SpigotUpdateRunnable(this));
4846
this.thread.start();
4947
}
5048

@@ -54,36 +52,8 @@ public String getResult() {
5452
return String.format(result.toString(), remoteVersion, plugin.getDescription().getVersion(), RESOURCE_URL);
5553
}
5654

57-
private class SpigotUpdateRunnable implements Runnable {
58-
59-
@Override
60-
public void run() {
61-
try {
62-
String version = getSpigotVersion();
63-
remoteVersion = version;
64-
65-
if (versionCheck(version)) {
66-
result = UpdateResult.SPIGOT_UPDATE_AVAILABLE;
67-
} else {
68-
result = UpdateResult.NO_UPDATE;
69-
}
70-
} catch (Throwable ex) {
71-
if (ProtocolLibrary.getConfig().isDebug()) {
72-
ProtocolLibrary.getErrorReporter().reportDetailed(
73-
SpigotUpdater.this, Report.newBuilder(REPORT_CANNOT_UPDATE_PLUGIN).error(ex).callerParam(this));
74-
} else {
75-
// People don't care
76-
// plugin.getLogger().log(Level.WARNING, "Failed to check for updates: " + ex);
77-
}
78-
79-
ProtocolLibrary.disableUpdates();
80-
} finally {
81-
// Invoke the listeners on the main thread
82-
for (Runnable listener : listeners) {
83-
ProtocolLibrary.getScheduler().runTask(listener);
84-
}
85-
}
86-
}
55+
void setRemoteVersion(String remoteVersion) {
56+
this.remoteVersion = remoteVersion;
8757
}
8858

8959
private static final String RESOURCE_URL = "https://www.spigotmc.org/resources/protocollib.1997/";

‎src/main/java/com/comphenix/protocol/wrappers/CustomPacketPayloadWrapper.java‎

Lines changed: 129 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.lang.reflect.Method;
1919
import java.lang.reflect.Modifier;
2020
import java.util.Objects;
21+
import java.util.Optional;
22+
import java.util.concurrent.ConcurrentHashMap;
2123

2224
import net.bytebuddy.ByteBuddy;
2325
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
@@ -33,8 +35,8 @@
3335
* by default. Constructing a new wrapper instance will give out a handle to a completely new implemented type, that
3436
* allows to set a key and some kind of data of any choice.
3537
* <p>
36-
* Constructing this class from a generic handle requires a payload that retains its raw byte data, such as
37-
* {@code DiscardedPayload}. Typed payloads without raw bytes should be accessed through a structure modifier directly.
38+
* Constructing this class from a generic handle requires either retained raw byte data, such as {@code DiscardedPayload},
39+
* or a writable serialization method on the concrete payload type.
3840
*
3941
* @author Pasqual Koschmieder
4042
*/
@@ -46,11 +48,14 @@ public final class CustomPacketPayloadWrapper {
4648
private static final ConstructorAccessor PAYLOAD_WRAPPER_CONSTRUCTOR;
4749

4850
private static final MethodAccessor GET_ID_PAYLOAD_METHOD;
49-
private static final MethodAccessor GET_TYPE_PAYLOAD_METHOD;
50-
private static final MethodAccessor GET_ID_PAYLOAD_TYPE_METHOD;
51+
private static final MethodAccessor GET_ID_HOLDER_PAYLOAD_METHOD;
52+
private static final MethodAccessor GET_ID_PAYLOAD_HOLDER_METHOD;
5153
private static final MethodAccessor SERIALIZE_PAYLOAD_METHOD;
54+
private static final Class<?> PAYLOAD_SERIALIZER_CLASS;
55+
private static final ConcurrentHashMap<Class<?>, Optional<MethodAccessor>> SERIALIZE_PAYLOAD_METHODS =
56+
new ConcurrentHashMap<>();
5257

53-
/** True when running on MC 1.21.5+ where DiscardedPayload is used instead of the ByteBuddy proxy. */
58+
/** True when running on the nested-ID API where DiscardedPayload is used instead of the ByteBuddy proxy. */
5459
private static final boolean USE_DISCARDED_PAYLOAD;
5560
private static final boolean DISCARDED_PAYLOAD_USES_BYTE_ARRAY;
5661

@@ -61,11 +66,9 @@ public final class CustomPacketPayloadWrapper {
6166
MINECRAFT_KEY_CLASS = MinecraftReflection.getMinecraftKeyClass();
6267
CUSTOM_PACKET_PAYLOAD_CLASS = MinecraftReflection.getMinecraftClass("network.protocol.common.custom.CustomPacketPayload");
6368

64-
// MC 1.21.5+: CustomPacketPayload no longer has id(); DiscardedPayload is the fallback payload
65-
// MC < 1.21.5: CustomPacketPayload.id() returns Identifier directly
6669
Method getPayloadId = null;
67-
Method getPayloadType = null;
68-
Method getPayloadTypeId = null;
70+
Method getPayloadIdHolder = null;
71+
Method getPayloadIdFromHolder = null;
6972
boolean useDiscardedPayload = false;
7073
try {
7174
getPayloadId = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder()
@@ -74,17 +77,19 @@ public final class CustomPacketPayloadWrapper {
7477
.parameterCount(0)
7578
.build());
7679
} catch (IllegalArgumentException ignored) {
77-
// New API: CustomPacketPayload.type().id() exposes the identifier for every payload type.
78-
getPayloadType = CUSTOM_PACKET_PAYLOAD_CLASS.getMethod("type");
79-
getPayloadTypeId = getPayloadType.getReturnType().getMethod("id");
80+
// Paper 26: the payload exposes its identifier through one intermediate record.
81+
Method[] accessors = findChainedIdAccessors(CUSTOM_PACKET_PAYLOAD_CLASS, MINECRAFT_KEY_CLASS);
82+
getPayloadIdHolder = accessors[0];
83+
getPayloadIdFromHolder = accessors[1];
8084
useDiscardedPayload = true;
8185
}
8286
GET_ID_PAYLOAD_METHOD = getPayloadId != null ? Accessors.getMethodAccessor(getPayloadId) : null;
83-
GET_TYPE_PAYLOAD_METHOD = getPayloadType != null ? Accessors.getMethodAccessor(getPayloadType) : null;
84-
GET_ID_PAYLOAD_TYPE_METHOD = getPayloadTypeId != null ? Accessors.getMethodAccessor(getPayloadTypeId) : null;
87+
GET_ID_HOLDER_PAYLOAD_METHOD = getPayloadIdHolder != null
88+
? Accessors.getMethodAccessor(getPayloadIdHolder) : null;
89+
GET_ID_PAYLOAD_HOLDER_METHOD = getPayloadIdFromHolder != null
90+
? Accessors.getMethodAccessor(getPayloadIdFromHolder) : null;
8591
USE_DISCARDED_PAYLOAD = useDiscardedPayload;
8692

87-
// serialize method may not exist in new API
8893
Method serializePayloadData = null;
8994
try {
9095
serializePayloadData = FuzzyReflection.fromClass(CUSTOM_PACKET_PAYLOAD_CLASS).getMethod(FuzzyMethodContract.newBuilder()
@@ -94,15 +99,26 @@ public final class CustomPacketPayloadWrapper {
9499
.parameterDerivedOf(ByteBuf.class, 0)
95100
.build());
96101
} catch (IllegalArgumentException ignored) {
97-
// Not present in new API
102+
// Paper 26 declares the write method on each concrete payload instead.
98103
}
99104
SERIALIZE_PAYLOAD_METHOD = serializePayloadData != null
100105
? Accessors.getMethodAccessor(serializePayloadData) : null;
106+
if (serializePayloadData == null) {
107+
ByteBuf buffer = Unpooled.buffer();
108+
try {
109+
PAYLOAD_SERIALIZER_CLASS = MinecraftReflection.getPacketDataSerializer(buffer).getClass();
110+
} finally {
111+
if (buffer.refCnt() > 0) {
112+
buffer.release();
113+
}
114+
}
115+
} else {
116+
PAYLOAD_SERIALIZER_CLASS = null;
117+
}
101118

102119
Constructor<?> payloadWrapperConstructor;
103120
boolean discardedPayloadUsesByteArray = false;
104121
if (useDiscardedPayload) {
105-
// The discarded payload changed from ByteBuf to byte[] in 26.2.
106122
Class<?> discardedPayloadClass = MinecraftReflection.getMinecraftClass(
107123
"network.protocol.common.custom.DiscardedPayload");
108124
try {
@@ -140,6 +156,41 @@ public Class<CustomPacketPayloadWrapper> getSpecificType() {
140156
}
141157
}
142158

159+
private static Method[] findChainedIdAccessors(Class<?> payloadClass, Class<?> keyClass) {
160+
Method payloadAccessor = null;
161+
Method keyAccessor = null;
162+
FuzzyMethodContract keyAccessorContract = FuzzyMethodContract.newBuilder()
163+
.banModifier(Modifier.STATIC)
164+
.returnTypeExact(keyClass)
165+
.parameterCount(0)
166+
.build();
167+
168+
for (Method candidate : payloadClass.getMethods()) {
169+
if (Modifier.isStatic(candidate.getModifiers()) || candidate.getParameterCount() != 0) {
170+
continue;
171+
}
172+
173+
Method candidateKeyAccessor;
174+
try {
175+
candidateKeyAccessor = FuzzyReflection.fromClass(candidate.getReturnType(), true)
176+
.getMethod(keyAccessorContract);
177+
} catch (IllegalArgumentException ignored) {
178+
continue;
179+
}
180+
181+
if (payloadAccessor != null) {
182+
throw new IllegalArgumentException("Multiple CustomPacketPayload identifier accessors found");
183+
}
184+
payloadAccessor = candidate;
185+
keyAccessor = candidateKeyAccessor;
186+
}
187+
188+
if (payloadAccessor == null) {
189+
throw new IllegalArgumentException("Unable to find a CustomPacketPayload identifier accessor");
190+
}
191+
return new Method[]{payloadAccessor, keyAccessor};
192+
}
193+
143194
private static Constructor<?> makePayloadWrapper() throws Exception {
144195
return new ByteBuddy()
145196
.subclass(Object.class)
@@ -162,6 +213,64 @@ private static Constructor<?> makePayloadWrapper() throws Exception {
162213
.getConstructor(MinecraftReflection.getMinecraftKeyClass(), byte[].class);
163214
}
164215

216+
private static Optional<MethodAccessor> findSerializePayloadMethod(Class<?> payloadClass) {
217+
Method serializeMethod = null;
218+
for (Method candidate : FuzzyReflection.fromClass(payloadClass, true).getMethods()) {
219+
if (Modifier.isStatic(candidate.getModifiers())
220+
|| candidate.getReturnType() != void.class
221+
|| candidate.getParameterCount() != 1) {
222+
continue;
223+
}
224+
225+
Class<?> parameterClass = candidate.getParameterTypes()[0];
226+
if (parameterClass == Object.class || !parameterClass.isAssignableFrom(PAYLOAD_SERIALIZER_CLASS)) {
227+
continue;
228+
}
229+
if (serializeMethod != null) {
230+
throw new IllegalArgumentException("Multiple CustomPacketPayload serialization methods found on "
231+
+ payloadClass.getName());
232+
}
233+
serializeMethod = candidate;
234+
}
235+
236+
if (serializeMethod == null) {
237+
return Optional.empty();
238+
}
239+
serializeMethod.setAccessible(true);
240+
return Optional.of(Accessors.getMethodAccessor(serializeMethod));
241+
}
242+
243+
private static MethodAccessor getSerializePayloadMethod(Object payload) {
244+
if (SERIALIZE_PAYLOAD_METHOD != null) {
245+
return SERIALIZE_PAYLOAD_METHOD;
246+
}
247+
return SERIALIZE_PAYLOAD_METHODS
248+
.computeIfAbsent(payload.getClass(), CustomPacketPayloadWrapper::findSerializePayloadMethod)
249+
.orElse(null);
250+
}
251+
252+
private static byte[] serializePayload(Object payload) {
253+
MethodAccessor serializeMethod = getSerializePayloadMethod(payload);
254+
if (serializeMethod == null) {
255+
throw new UnsupportedOperationException(
256+
"Cannot extract raw custom payload bytes from " + payload.getClass().getName()
257+
+ ": payload has no byte array, ByteBuf, or serialization method");
258+
}
259+
260+
ByteBuf buffer = Unpooled.buffer();
261+
try {
262+
Object serializer = MinecraftReflection.getPacketDataSerializer(buffer);
263+
serializeMethod.invoke(payload, serializer);
264+
byte[] result = new byte[buffer.readableBytes()];
265+
buffer.getBytes(buffer.readerIndex(), result);
266+
return result;
267+
} finally {
268+
if (buffer.refCnt() > 0) {
269+
buffer.release();
270+
}
271+
}
272+
}
273+
165274
// ====== api methods ======
166275

167276
/**
@@ -233,18 +342,7 @@ public static CustomPacketPayloadWrapper fromUnknownPayload(Object payload) {
233342
buf.resetReaderIndex();
234343
return data;
235344
})
236-
.orElseGet(() -> {
237-
if (SERIALIZE_PAYLOAD_METHOD == null) {
238-
throw new UnsupportedOperationException(
239-
"Cannot extract raw custom payload bytes from " + payload.getClass().getName()
240-
+ ": payload has no byte array or ByteBuf field and this Minecraft version "
241-
+ "does not expose CustomPacketPayload serialization");
242-
}
243-
ByteBuf buffer = Unpooled.buffer();
244-
Object serializer = MinecraftReflection.getPacketDataSerializer(buffer);
245-
SERIALIZE_PAYLOAD_METHOD.invoke(payload, serializer);
246-
return StreamSerializer.getDefault().getBytesAndRelease(buffer);
247-
});
345+
.orElseGet(() -> serializePayload(payload));
248346
}
249347

250348
return new CustomPacketPayloadWrapper(messagePayload, id);
@@ -255,8 +353,8 @@ private static MinecraftKey getPayloadId(Object payload) {
255353
if (GET_ID_PAYLOAD_METHOD != null) {
256354
messageId = GET_ID_PAYLOAD_METHOD.invoke(payload);
257355
} else {
258-
Object payloadType = GET_TYPE_PAYLOAD_METHOD.invoke(payload);
259-
messageId = GET_ID_PAYLOAD_TYPE_METHOD.invoke(payloadType);
356+
Object idHolder = GET_ID_HOLDER_PAYLOAD_METHOD.invoke(payload);
357+
messageId = GET_ID_PAYLOAD_HOLDER_METHOD.invoke(idHolder);
260358
}
261359
return MinecraftKey.getConverter().getSpecific(messageId);
262360
}

0 commit comments

Comments
 (0)