diff --git a/build.gradle b/build.gradle index 52d611a0..8915b7fc 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,10 @@ repositories { url 'https://hub.spigotmc.org/nexus/content/groups/public/' } + maven { + url 'https://repo.papermc.io/repository/maven-public/' + } + maven { url 'https://libraries.minecraft.net/' metadataSources { @@ -34,6 +38,7 @@ repositories { dependencies { implementation 'net.bytebuddy:byte-buddy:1.14.3' + compileOnly 'dev.folia:folia-api:1.19.4-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot:1.19.4-R0.1-SNAPSHOT' compileOnly 'io.netty:netty-all:4.0.23.Final' @@ -50,11 +55,11 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_17 - withJavadocJar() - withSourcesJar() + withJavadocJar() + withSourcesJar() } shadowJar { diff --git a/src/main/java/com/comphenix/protocol/ProtocolLib.java b/src/main/java/com/comphenix/protocol/ProtocolLib.java index 6a1f5d39..f9d604b9 100644 --- a/src/main/java/com/comphenix/protocol/ProtocolLib.java +++ b/src/main/java/com/comphenix/protocol/ProtocolLib.java @@ -490,20 +490,14 @@ public class ProtocolLib extends JavaPlugin { } // Attempt to create task - this.packetTask = server.getScheduler().scheduleSyncRepeatingTask(this, () -> { - AsyncFilterManager manager = (AsyncFilterManager) protocolManager.getAsynchronousManager(); - // We KNOW we're on the main thread at the moment - manager.sendProcessedPackets(ProtocolLib.this.tickCounter++, true); - - // House keeping - ProtocolLib.this.updateConfiguration(); - - // Check for updates too - if (!ProtocolLibrary.updatesDisabled() && (ProtocolLib.this.tickCounter % 20) == 0) { - ProtocolLib.this.checkUpdates(); - } - }, ASYNC_MANAGER_DELAY, ASYNC_MANAGER_DELAY); + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + this.packetTask = 1; + server.getGlobalRegionScheduler().runAtFixedRate(this, task -> packetTaskRegistrator(), ASYNC_MANAGER_DELAY, ASYNC_MANAGER_DELAY); + } catch (ClassNotFoundException e) { + this.packetTask = server.getScheduler().scheduleSyncRepeatingTask(this, this::packetTaskRegistrator, ASYNC_MANAGER_DELAY, ASYNC_MANAGER_DELAY); + } } catch (OutOfMemoryError e) { throw e; } catch (Throwable e) { @@ -512,6 +506,20 @@ public class ProtocolLib extends JavaPlugin { } } } + + private void packetTaskRegistrator() { + AsyncFilterManager manager = (AsyncFilterManager) protocolManager.getAsynchronousManager(); + // We KNOW we're on the main thread at the moment + manager.sendProcessedPackets(ProtocolLib.this.tickCounter++, true); + + // House keeping + ProtocolLib.this.updateConfiguration(); + + // Check for updates too + if (!ProtocolLibrary.updatesDisabled() && (ProtocolLib.this.tickCounter % 20) == 0) { + ProtocolLib.this.checkUpdates(); + } + } private void updateConfiguration() { if (config != null && config.getModificationCount() != this.configExpectedMod) { @@ -567,7 +575,12 @@ public class ProtocolLib extends JavaPlugin { // Clean up if (this.packetTask >= 0) { - this.getServer().getScheduler().cancelTask(this.packetTask); + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + this.getServer().getGlobalRegionScheduler().cancelTasks(this); + } catch (ClassNotFoundException e) { + this.getServer().getScheduler().cancelTask(this.packetTask); + } this.packetTask = -1; } diff --git a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java index e73e3935..4c09d490 100644 --- a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java +++ b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java @@ -2,16 +2,16 @@ * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. * Copyright (C) 2012 Kristian S. Stangeland * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ @@ -32,10 +32,10 @@ import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.MethodAccessor; import com.comphenix.protocol.utility.ByteBuddyFactory; +import com.destroystokyo.paper.profile.PlayerProfile; import org.bukkit.*; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import org.bukkit.profile.PlayerProfile; import net.bytebuddy.description.ByteCodeElement; import net.bytebuddy.description.modifier.Visibility; @@ -51,310 +51,343 @@ import net.bytebuddy.implementation.bind.annotation.FieldValue; import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; +import org.jetbrains.annotations.NotNull; /** * Represents a player object that can be serialized by Java. - * + * * @author Kristian */ class SerializedOfflinePlayer implements OfflinePlayer, Serializable { - /** - * Generated by Eclipse. - */ - private static final long serialVersionUID = -2728976288470282810L; + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = -2728976288470282810L; - private transient Location bedSpawnLocation; - - // Relevant data about an offline player - private String name; - private UUID uuid; - private long firstPlayed; - private long lastPlayed; - private boolean operator; - private boolean banned; - private boolean playedBefore; - private boolean online; - private boolean whitelisted; + private transient Location bedSpawnLocation; - private static final Constructor proxyPlayerConstructor = setupProxyPlayerConstructor(); + // Relevant data about an offline player + private String name; + private UUID uuid; + private long firstPlayed; + private long lastPlayed; + private boolean operator; + private boolean banned; + private boolean playedBefore; + private boolean online; + private boolean whitelisted; + long lastLogin; + long lastSeen; - /** - * Constructor used by serialization. - */ - public SerializedOfflinePlayer() { - // Do nothing - } - - /** - * Initialize this serializable offline player from another player. - * @param offline - another player. - */ - public SerializedOfflinePlayer(OfflinePlayer offline) { - this.name = offline.getName(); - this.uuid = offline.getUniqueId(); - this.firstPlayed = offline.getFirstPlayed(); - this.lastPlayed = offline.getLastPlayed(); - this.operator = offline.isOp(); - this.banned = offline.isBanned(); - this.playedBefore = offline.hasPlayedBefore(); - this.online = offline.isOnline(); - this.whitelisted = offline.isWhitelisted(); - } - - @Override - public boolean isOp() { - return operator; - } + private static final Constructor proxyPlayerConstructor = setupProxyPlayerConstructor(); - @Override - public void setOp(boolean operator) { - this.operator = operator; - } + /** + * Constructor used by serialization. + */ + public SerializedOfflinePlayer() { + // Do nothing + } - @Override - public Map serialize() { - throw new UnsupportedOperationException(); - } + /** + * Initialize this serializable offline player from another player. + * + * @param offline - another player. + */ + public SerializedOfflinePlayer(OfflinePlayer offline) { + this.name = offline.getName(); + this.uuid = offline.getUniqueId(); + this.firstPlayed = offline.getFirstPlayed(); + this.lastPlayed = offline.getLastPlayed(); + this.operator = offline.isOp(); + this.banned = offline.isBanned(); + this.playedBefore = offline.hasPlayedBefore(); + this.online = offline.isOnline(); + this.whitelisted = offline.isWhitelisted(); + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + this.lastSeen = offline.getLastSeen(); + this.lastLogin = offline.getLastLogin(); + } catch (ClassNotFoundException ignored) {} + } - @Override - public Location getBedSpawnLocation() { - return bedSpawnLocation; - } + @Override + public boolean isOp() { + return operator; + } - // TODO do we need to implement this? - - public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { } + @Override + public void setOp(boolean operator) { + this.operator = operator; + } - public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { } + @Override + public Map serialize() { + throw new UnsupportedOperationException(); + } - public void incrementStatistic(Statistic statistic, int i) throws IllegalArgumentException { } + @Override + public Location getBedSpawnLocation() { + return bedSpawnLocation; + } - public void decrementStatistic(Statistic statistic, int i) throws IllegalArgumentException { } + @Override + public long getLastLogin() { + return lastLogin; + } - public void setStatistic(Statistic statistic, int i) throws IllegalArgumentException { } + @Override + public long getLastSeen() { + return lastSeen; + } - public int getStatistic(Statistic statistic) throws IllegalArgumentException { - return 0; - } + // TODO do we need to implement this? - public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { } + public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { + } - public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { } + public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { + } - public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { - return 0; - } + public void incrementStatistic(Statistic statistic, int i) throws IllegalArgumentException { + } - public void incrementStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { } + public void decrementStatistic(Statistic statistic, int i) throws IllegalArgumentException { + } - public void decrementStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { } + public void setStatistic(Statistic statistic, int i) throws IllegalArgumentException { + } - public void setStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { } + public int getStatistic(Statistic statistic) throws IllegalArgumentException { + return 0; + } - public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { } + public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + } - public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { } + public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + } - public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { - return 0; - } + public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + return 0; + } - public void incrementStatistic(Statistic statistic, EntityType entityType, int i) throws IllegalArgumentException { } + public void incrementStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { + } - public void decrementStatistic(Statistic statistic, EntityType entityType, int i) { } + public void decrementStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { + } - public void setStatistic(Statistic statistic, EntityType entityType, int i) { } + public void setStatistic(Statistic statistic, Material material, int i) throws IllegalArgumentException { + } - @Override - public Location getLastDeathLocation() { - return null; - } + public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + } - @Override - public long getFirstPlayed() { - return firstPlayed; - } + public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + } - @Override - public long getLastPlayed() { - return lastPlayed; - } + public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + return 0; + } - @Override - public UUID getUniqueId() { - return uuid; - } + public void incrementStatistic(Statistic statistic, EntityType entityType, int i) throws IllegalArgumentException { + } - @Override - public PlayerProfile getPlayerProfile() { - return null; - } + public void decrementStatistic(Statistic statistic, EntityType entityType, int i) { + } - @Override - public String getName() { - return name; - } - - @Override - public boolean hasPlayedBefore() { - return playedBefore; - } + public void setStatistic(Statistic statistic, EntityType entityType, int i) { + } - @Override - public boolean isBanned() { - return banned; - } + @Override + public Location getLastDeathLocation() { + return null; + } - public void setBanned(boolean banned) { - this.banned = banned; - } - - @Override - public boolean isOnline() { - return online; - } + @Override + public long getFirstPlayed() { + return firstPlayed; + } - @Override - public boolean isWhitelisted() { - return whitelisted; - } + @Override + public long getLastPlayed() { + return lastPlayed; + } - @Override - public void setWhitelisted(boolean whitelisted) { - this.whitelisted = whitelisted; - } + @Override + public UUID getUniqueId() { + return uuid; + } - private void writeObject(ObjectOutputStream output) throws IOException { - output.defaultWriteObject(); - - // Serialize the bed spawn location - output.writeUTF(bedSpawnLocation.getWorld().getName()); - output.writeDouble(bedSpawnLocation.getX()); - output.writeDouble(bedSpawnLocation.getY()); - output.writeDouble(bedSpawnLocation.getZ()); - } - - private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { - input.defaultReadObject(); + @Override + public @NotNull PlayerProfile getPlayerProfile() { + return null; + } - // Well, this is a problem - bedSpawnLocation = new Location( - getWorld(input.readUTF()), - input.readDouble(), - input.readDouble(), - input.readDouble() - ); - } - - private World getWorld(String name) { - try { - // Try to get the world at least - return Bukkit.getServer().getWorld(name); - } catch (Exception e) { - // Screw it - return null; - } - } - - @Override - public Player getPlayer() { - try { - // Try to get the real player underneath - return Bukkit.getServer().getPlayerExact(name); - } catch (Exception e) { - return getProxyPlayer(); - } - } - - /** - * Retrieve a player object that implements OfflinePlayer by referring to this object. - *

- * All other methods cause an exception. - * @return Proxy object. - */ - public Player getProxyPlayer() { - try { - return (Player) proxyPlayerConstructor.newInstance(this); - } 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); - } - } + @Override + public String getName() { + return name; + } - private static Constructor setupProxyPlayerConstructor() - { - final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods(); - final String[] methodNames = new String[offlinePlayerMethods.length]; - for (int idx = 0; idx < offlinePlayerMethods.length; ++idx) - methodNames[idx] = offlinePlayerMethods[idx].getName(); + @Override + public boolean hasPlayedBefore() { + return playedBefore; + } - final Map accessorCache = new ConcurrentHashMap<>(); - final ElementMatcher.Junction forwardedMethods = ElementMatchers.namedOneOf(methodNames); + @Override + public boolean isBanned() { + return banned; + } - try { - final MethodDelegation forwarding = MethodDelegation.withDefaultConfiguration().to(new Object() { - @RuntimeType - public Object intercept( - @Origin Method calledMethod, - @AllArguments Object[] args, - @FieldValue("offlinePlayer") OfflinePlayer proxy - ) { - MethodAccessor accessor = accessorCache.computeIfAbsent(calledMethod, method -> { - // special case - some methods (like getName) are defined in OfflinePlayer as well - // as the online Player class. This causes cast exceptions if we try to invoke the method on - // the online player with our proxy. Prevent that - if (OfflinePlayer.class.isAssignableFrom(method.getDeclaringClass())) { - return Accessors.getMethodAccessor( - OfflinePlayer.class, - method.getName(), - method.getParameterTypes()); - } else { - return Accessors.getMethodAccessor(method); - } - }); - return accessor.invoke(proxy, args); - } - }); + public void setBanned(boolean banned) { + this.banned = banned; + } - final InvocationHandlerAdapter throwException = InvocationHandlerAdapter.of((obj, method, args) -> { - throw new UnsupportedOperationException( - "The method " + method.getName() + " is not supported for offline players."); - }); + @Override + public boolean isOnline() { + return online; + } - return ByteBuddyFactory.getInstance() - .createSubclass(PlayerUnion.class, ConstructorStrategy.Default.NO_CONSTRUCTORS) - .name(SerializedOfflinePlayer.class.getPackage().getName() + ".PlayerInvocationHandler") + @Override + public boolean isWhitelisted() { + return whitelisted; + } - .defineField("offlinePlayer", OfflinePlayer.class, Visibility.PRIVATE) - .defineConstructor(Visibility.PUBLIC) - .withParameters(OfflinePlayer.class) - .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()) - .andThen(FieldAccessor.ofField("offlinePlayer").setsArgumentAt(0))) + @Override + public void setWhitelisted(boolean whitelisted) { + this.whitelisted = whitelisted; + } - .method(forwardedMethods) - .intercept(forwarding) + private void writeObject(ObjectOutputStream output) throws IOException { + output.defaultWriteObject(); - .method(ElementMatchers.not(forwardedMethods)) - .intercept(throwException) + // Serialize the bed spawn location + output.writeUTF(bedSpawnLocation.getWorld().getName()); + output.writeDouble(bedSpawnLocation.getX()); + output.writeDouble(bedSpawnLocation.getY()); + output.writeDouble(bedSpawnLocation.getZ()); + } - .make() - .load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION) - .getLoaded() - .getDeclaredConstructor(OfflinePlayer.class); - } catch (NoSuchMethodException e) { - throw new RuntimeException("Failed to find Player constructor!", e); - } - } + private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { + input.defaultReadObject(); - /** - * This interface extends both OfflinePlayer and Player (in that order) so that the class generated by ByteBuddy - * looks at OfflinePlayer's methods first while still being a Player. - */ - private interface PlayerUnion extends OfflinePlayer, Player - { - } + // Well, this is a problem + bedSpawnLocation = new Location( + getWorld(input.readUTF()), + input.readDouble(), + input.readDouble(), + input.readDouble() + ); + } + + private World getWorld(String name) { + try { + // Try to get the world at least + return Bukkit.getServer().getWorld(name); + } catch (Exception e) { + // Screw it + return null; + } + } + + @Override + public Player getPlayer() { + try { + // Try to get the real player underneath + return Bukkit.getServer().getPlayerExact(name); + } catch (Exception e) { + return getProxyPlayer(); + } + } + + /** + * Retrieve a player object that implements OfflinePlayer by referring to this object. + *

+ * All other methods cause an exception. + * + * @return Proxy object. + */ + public Player getProxyPlayer() { + try { + return (Player) proxyPlayerConstructor.newInstance(this); + } 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); + } + } + + private static Constructor setupProxyPlayerConstructor() { + final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods(); + final String[] methodNames = new String[offlinePlayerMethods.length]; + for (int idx = 0; idx < offlinePlayerMethods.length; ++idx) + methodNames[idx] = offlinePlayerMethods[idx].getName(); + + final Map accessorCache = new ConcurrentHashMap<>(); + final ElementMatcher.Junction forwardedMethods = ElementMatchers.namedOneOf(methodNames); + + try { + final MethodDelegation forwarding = MethodDelegation.withDefaultConfiguration().to(new Object() { + @RuntimeType + public Object intercept( + @Origin Method calledMethod, + @AllArguments Object[] args, + @FieldValue("offlinePlayer") OfflinePlayer proxy + ) { + MethodAccessor accessor = accessorCache.computeIfAbsent(calledMethod, method -> { + // special case - some methods (like getName) are defined in OfflinePlayer as well + // as the online Player class. This causes cast exceptions if we try to invoke the method on + // the online player with our proxy. Prevent that + if (OfflinePlayer.class.isAssignableFrom(method.getDeclaringClass())) { + return Accessors.getMethodAccessor( + OfflinePlayer.class, + method.getName(), + method.getParameterTypes()); + } else { + return Accessors.getMethodAccessor(method); + } + }); + return accessor.invoke(proxy, args); + } + }); + + final InvocationHandlerAdapter throwException = InvocationHandlerAdapter.of((obj, method, args) -> { + throw new UnsupportedOperationException( + "The method " + method.getName() + " is not supported for offline players."); + }); + + return ByteBuddyFactory.getInstance() + .createSubclass(PlayerUnion.class, ConstructorStrategy.Default.NO_CONSTRUCTORS) + .name(SerializedOfflinePlayer.class.getPackage().getName() + ".PlayerInvocationHandler") + + .defineField("offlinePlayer", OfflinePlayer.class, Visibility.PRIVATE) + .defineConstructor(Visibility.PUBLIC) + .withParameters(OfflinePlayer.class) + .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()) + .andThen(FieldAccessor.ofField("offlinePlayer").setsArgumentAt(0))) + + .method(forwardedMethods) + .intercept(forwarding) + + .method(ElementMatchers.not(forwardedMethods)) + .intercept(throwException) + + .make() + .load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION) + .getLoaded() + .getDeclaredConstructor(OfflinePlayer.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Failed to find Player constructor!", e); + } + } + + /** + * This interface extends both OfflinePlayer and Player (in that order) so that the class generated by ByteBuddy + * looks at OfflinePlayer's methods first while still being a Player. + */ + private interface PlayerUnion extends OfflinePlayer, Player { + } } diff --git a/src/main/java/com/comphenix/protocol/updater/SpigotUpdater.java b/src/main/java/com/comphenix/protocol/updater/SpigotUpdater.java index 11b7cb81..e27bc6ac 100644 --- a/src/main/java/com/comphenix/protocol/updater/SpigotUpdater.java +++ b/src/main/java/com/comphenix/protocol/updater/SpigotUpdater.java @@ -80,7 +80,12 @@ public final class SpigotUpdater extends Updater { } finally { // Invoke the listeners on the main thread for (Runnable listener : listeners) { - plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, listener); + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + plugin.getServer().getGlobalRegionScheduler().execute(plugin, listener); + } catch (ClassNotFoundException e) { + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, listener); + } } } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3db7f73c..f499d374 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -8,6 +8,8 @@ load: STARTUP database: false api-version: "1.13" +folia-supported: true + commands: protocol: description: Performs administrative tasks regarding ProtocolLib. diff --git a/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java b/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java index ff75e102..02c753ed 100644 --- a/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java +++ b/src/test/java/com/comphenix/protocol/events/SerializedOfflinePlayerTest.java @@ -17,6 +17,8 @@ public class SerializedOfflinePlayerTest { private static final String name = "playerName"; private static final long firstPlayed = 1000L; + private static final long lastLogin = 1000L; + private static final long lastSeen = 1000L; private static final long lastPlayed = firstPlayed + 100L; private static final boolean isOp = false; private static final boolean playedBefore = true;