1818import java .lang .reflect .Method ;
1919import java .lang .reflect .Modifier ;
2020import java .util .Objects ;
21+ import java .util .Optional ;
22+ import java .util .concurrent .ConcurrentHashMap ;
2123
2224import net .bytebuddy .ByteBuddy ;
2325import net .bytebuddy .dynamic .loading .ClassLoadingStrategy ;
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