Folia support

This commit is contained in:
mani123 2023-04-17 20:43:18 +02:00
parent ac6f911f15
commit ff0f1be20e
6 changed files with 339 additions and 279 deletions

View File

@ -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 {

View File

@ -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;
}

View File

@ -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<String, Object> 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<String, Object> 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.
* <p>
* 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<? extends Player> 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<Method, MethodAccessor> accessorCache = new ConcurrentHashMap<>();
final ElementMatcher.Junction<ByteCodeElement> 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.
* <p>
* 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<? extends Player> 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<Method, MethodAccessor> accessorCache = new ConcurrentHashMap<>();
final ElementMatcher.Junction<ByteCodeElement> 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 {
}
}

View File

@ -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);
}
}
}
}

View File

@ -8,6 +8,8 @@ load: STARTUP
database: false
api-version: "1.13"
folia-supported: true
commands:
protocol:
description: Performs administrative tasks regarding ProtocolLib.

View File

@ -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;