diff --git a/src/main/java/com/comphenix/protocol/events/PacketEvent.java b/src/main/java/com/comphenix/protocol/events/PacketEvent.java index df3f3c20..6c602252 100644 --- a/src/main/java/com/comphenix/protocol/events/PacketEvent.java +++ b/src/main/java/com/comphenix/protocol/events/PacketEvent.java @@ -527,7 +527,7 @@ public class PacketEvent extends EventObject implements Cancellable { // Write the name of the player (or NULL if it's not set) Player player = getPlayer(); - output.writeObject(player != null ? new SerializedOfflinePlayer(player) : null); + output.writeObject(player != null ? SerializedOfflinePlayer.init(player) : null); } private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { diff --git a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java index 57713e19..3ecc54a5 100644 --- a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java +++ b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java @@ -51,8 +51,12 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -61,7 +65,7 @@ import java.util.concurrent.ConcurrentHashMap; * * @author Kristian */ -class SerializedOfflinePlayer implements OfflinePlayer, Serializable { +abstract class SerializedOfflinePlayer implements OfflinePlayer, Serializable { /** * Generated by Eclipse. @@ -84,11 +88,33 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable { private long lastSeen; private static final Constructor proxyPlayerConstructor = setupProxyPlayerConstructor(); + private static final Constructor CLASS_CONSTRUCTOR = setupClassConstructor(); + + /** + * Initialize a serializable offline player object from another offline player. + *

+ * All other methods cause an exception. + * + * @param player - another offline player. + * @return A serializable offline player object. + */ + public static SerializedOfflinePlayer init(OfflinePlayer player) { + try { + CLASS_CONSTRUCTOR.setAccessible(true); + return CLASS_CONSTRUCTOR.newInstance(player); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access reflection.", e); + } catch (InstantiationException e) { + throw new RuntimeException("Cannot instantiate object.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error in invocation.", e); + } + } /** * Constructor used by serialization. */ - public SerializedOfflinePlayer() { + protected SerializedOfflinePlayer() { // Do nothing } @@ -97,7 +123,7 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable { * * @param offline - another player. */ - public SerializedOfflinePlayer(OfflinePlayer offline) { + protected SerializedOfflinePlayer(OfflinePlayer offline) { this.name = offline.getName(); this.uuid = offline.getUniqueId(); this.firstPlayed = offline.getFirstPlayed(); @@ -342,6 +368,47 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable { } } + private static Constructor setupClassConstructor() { + final Method[] existingMethods = SerializedOfflinePlayer.class.getDeclaredMethods(); + final Set existingMethodNames = new HashSet<>(); + + for (int idx = 0; idx < existingMethods.length; idx++) { + existingMethodNames.add(existingMethods[idx].getName()); + } + + final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods(); + final List methodNamesToAdd = new ArrayList<>(); + + for (int idx = 0; idx < offlinePlayerMethods.length; idx++) { + final String name = offlinePlayerMethods[idx].getName(); + + if (!existingMethodNames.contains(name)) { + methodNamesToAdd.add(name); + } + } + + final ElementMatcher.Junction missingMethods = + ElementMatchers.namedOneOf(methodNamesToAdd.toArray(new String[methodNamesToAdd.size()])); + + final InvocationHandlerAdapter throwException = InvocationHandlerAdapter.of((obj, method, args) -> { + throw new UnsupportedOperationException( + "The method " + method.getName() + " is not supported."); + }); + + try { + return ByteBuddyFactory.getInstance() + .createSubclass(SerializedOfflinePlayer.class) + .method(missingMethods) + .intercept(throwException) + .make() + .load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .getLoaded() + .getConstructor(OfflinePlayer.class); + } catch (NoSuchMethodException ex) { + throw new RuntimeException("Failed to find SerializedOfflinePlayer constructor!", ex); + } + } + private static Constructor setupProxyPlayerConstructor() { final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods(); final String[] methodNames = new String[offlinePlayerMethods.length]; diff --git a/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java b/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java index 4bd8349c..1d8ba4f7 100644 --- a/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java +++ b/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java @@ -39,7 +39,7 @@ public class SerializedOfflinePlayerTest { when(offlinePlayer.hasPlayedBefore()).thenReturn(playedBefore); when(offlinePlayer.isWhitelisted()).thenReturn(whitelisted); - serializedOfflinePlayer = new SerializedOfflinePlayer(offlinePlayer); + serializedOfflinePlayer = SerializedOfflinePlayer.init(offlinePlayer); } @Test