1
0
mirror of https://github.com/GeyserMC/Floodgate.git synced 2025-12-19 06:49:24 +00:00

Update to support 1.21.9 (#616)

* Start work on 1.21.9

* Works on 1.21.9

* Add native Mojmap support for the spigot module (#591)

* Add native Mojmap support for the spigot module

* Don't remove spigot class name for CraftPlayer

* Also include CraftServer and CraftOfflinePlayer

* Restored compatibility with 1.16.5

* Import cleanup

* Fix /fwhitelist on 1.21.9

(cherry picked from commit 06bb54395f)

* Re-use existing name/uuid from old profile when applying new skin

---------

Co-authored-by: Eclipse <eclipse@eclipseisoffline.xyz>
Co-authored-by: Aurorawr <auroranova8756@gmail.com>
Co-authored-by: onebeastchris <github@onechris.mozmail.com>
This commit is contained in:
Camotoy
2025-10-03 14:55:57 -04:00
committed by GitHub
parent d5be877ed9
commit 0a8cba05f8
8 changed files with 251 additions and 34 deletions

View File

@@ -30,6 +30,7 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.Setter;
@@ -135,6 +136,39 @@ public final class ReflectionUtils {
return clazz;
}
public static Class<?> getClassOrFallback(String... classNames) {
for (String className : classNames) {
Class<?> clazz = getClassSilently(className);
if (clazz != null) {
if (Constants.DEBUG_MODE) {
System.out.println("Found class: " + clazz.getName() + " for choices: " +
Arrays.toString(classNames));
}
return clazz;
}
}
throw new IllegalStateException("Could not find class between these choices: " +
Arrays.toString(classNames));
}
@Nullable
public static Class<?> getClassOrFallbackSilently(String... classNames) {
for (String className : classNames) {
Class<?> clazz = getClassSilently(className);
if (clazz != null) {
return clazz;
}
}
return null;
}
@Nullable
@SuppressWarnings("unchecked")
public static <T> Class<T> getCastedClassOrFallback(String className, String fallbackClassName) {
return (Class<T>) getClassOrFallback(className, fallbackClassName);
}
@Nullable
public static <T> Constructor<T> getConstructor(Class<T> clazz, boolean declared, Class<?>... parameters) {
try {
@@ -151,6 +185,15 @@ public final class ReflectionUtils {
}
}
@Nullable
public static <T> T newInstanceOrThrow(Constructor<T> constructor, Object... parameters) {
try {
return constructor.newInstance(parameters);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
@Nullable
public static <T> T newInstance(Constructor<T> constructor, Object... parameters) {
try {
@@ -201,6 +244,28 @@ public final class ReflectionUtils {
return getField(clazz, fieldName, false);
}
/**
* Get a field from a class, it doesn't matter if the field is public or not. This method will
* first try to get a declared field and if that failed it'll try to get a public field.
*
* @param clazz the class to get the field from
* @param fieldName the name of the field
* @param fallbackFieldName the fallback incase fieldName doesn't exist
* @return the field if found from the name or fallback, otherwise null
*/
@Nullable
public static Field getField(Class<?> clazz, String fieldName, String fallbackFieldName) {
Field field = getField(clazz, fieldName, true);
if (field != null) {
return field;
}
field = getField(clazz, fieldName, false);
if (field != null) {
return field;
}
return getField(clazz, fallbackFieldName);
}
/**
* Get a field from a class without having to provide a field name.
*
@@ -409,6 +474,29 @@ public final class ReflectionUtils {
return getMethod(clazz, methodName, false, arguments);
}
/**
* Get a method from a class, it doesn't matter if the method is public or not. This method will
* first try to get a declared method and if that fails it'll try to get a public method.
*
* @param clazz the class to get the method from
* @param methodName the name of the method to find
* @param fallbackName the fallback instead methodName does not exist
* @param arguments the classes of the method arguments
* @return the requested method if it has been found, otherwise null
*/
@Nullable
public static Method getMethod(Class<?> clazz, String methodName, String fallbackName, Class<?>... arguments) {
Method method = getMethod(clazz, methodName, true, arguments);
if (method != null) {
return method;
}
method = getMethod(clazz, methodName, false, arguments);
if (method != null) {
return method;
}
return getMethod(clazz, fallbackName, arguments);
}
/**
* Get a method from a class, it doesn't matter if the method is public or not. This method will
* first try to get a declared method and if that fails it'll try to get a public method.

View File

@@ -2,4 +2,4 @@ org.gradle.configureondemand=true
org.gradle.caching=true
org.gradle.parallel=true
version=2.2.4-SNAPSHOT
version=2.2.5-SNAPSHOT

View File

@@ -35,6 +35,7 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.config.FloodgateConfig;
import org.geysermc.floodgate.player.FloodgateHandshakeHandler;
import org.geysermc.floodgate.util.SpigotVersionSpecificMethods;
public final class SpigotDataAddon implements InjectorAddon {
@Inject private FloodgateHandshakeHandler handshakeHandler;
@@ -54,12 +55,15 @@ public final class SpigotDataAddon implements InjectorAddon {
@Named("playerAttribute")
private AttributeKey<FloodgatePlayer> playerAttribute;
@Inject
private SpigotVersionSpecificMethods versionSpecificMethods;
@Override
public void onInject(Channel channel, boolean toServer) {
// we have to add the packet blocker in the data handler, otherwise ProtocolSupport breaks
channel.pipeline().addBefore(
packetHandlerName, "floodgate_data_handler",
new SpigotDataHandler(handshakeHandler, config, kickMessageAttribute)
new SpigotDataHandler(handshakeHandler, config, kickMessageAttribute, versionSpecificMethods)
);
}

View File

@@ -41,6 +41,7 @@ import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult;
import org.geysermc.floodgate.util.ClassNames;
import org.geysermc.floodgate.util.Constants;
import org.geysermc.floodgate.util.ProxyUtils;
import org.geysermc.floodgate.util.SpigotVersionSpecificMethods;
public final class SpigotDataHandler extends CommonDataHandler {
private static final Property DEFAULT_TEXTURE_PROPERTY = new Property(
@@ -49,6 +50,7 @@ public final class SpigotDataHandler extends CommonDataHandler {
Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE
);
private final SpigotVersionSpecificMethods versionSpecificMethods;
private Object networkManager;
private FloodgatePlayer player;
private boolean proxyData;
@@ -56,8 +58,10 @@ public final class SpigotDataHandler extends CommonDataHandler {
public SpigotDataHandler(
FloodgateHandshakeHandler handshakeHandler,
FloodgateConfig config,
AttributeKey<String> kickMessageAttribute) {
AttributeKey<String> kickMessageAttribute,
SpigotVersionSpecificMethods versionSpecificMethods) {
super(handshakeHandler, config, kickMessageAttribute, new PacketBlocker());
this.versionSpecificMethods = versionSpecificMethods;
}
@Override
@@ -175,16 +179,15 @@ public final class SpigotDataHandler extends CommonDataHandler {
}
}
GameProfile gameProfile = new GameProfile(
player.getCorrectUniqueId(), player.getCorrectUsername()
);
Property texturesProperty = null;
if (!player.isLinked()) {
// Otherwise game server will try to fetch the skin from Mojang.
// No need to worry that this overrides proxy data, because those won't reach this
// method / are already removed (in the case of username validation)
gameProfile.getProperties().put("textures", DEFAULT_TEXTURE_PROPERTY);
texturesProperty = DEFAULT_TEXTURE_PROPERTY;
}
GameProfile gameProfile = versionSpecificMethods.createGameProfile(
player.getCorrectUniqueId(), player.getCorrectUsername(), texturesProperty);
// we have to fake the offline player (login) cycle

View File

@@ -75,9 +75,7 @@ public final class SpigotSkinApplier implements SkinApplier {
// Need to be careful here - getProperties() returns an authlib PropertyMap, which extends
// MultiMap from Guava. Floodgate relocates Guava.
PropertyMap properties = profile.getProperties();
SkinData currentSkin = versionSpecificMethods.currentSkin(properties);
SkinData currentSkin = versionSpecificMethods.currentSkin(profile);
SkinApplyEvent event = new SkinApplyEventImpl(floodgatePlayer, currentSkin, skinData);
event.setCancelled(floodgatePlayer.isLinked());
@@ -88,7 +86,12 @@ public final class SpigotSkinApplier implements SkinApplier {
return;
}
replaceSkin(properties, event.newSkin());
if (ClassNames.GAME_PROFILE_FIELD != null) {
replaceSkin(player, profile, event.newSkin());
} else {
// We're on a version with mutable GameProfiles
replaceSkinOld(profile.getProperties(), event.newSkin());
}
versionSpecificMethods.maybeSchedule(() -> {
for (Player p : Bukkit.getOnlinePlayers()) {
@@ -99,7 +102,14 @@ public final class SpigotSkinApplier implements SkinApplier {
});
}
private void replaceSkin(PropertyMap properties, SkinData skinData) {
private void replaceSkin(Player player, GameProfile oldProfile, SkinData skinData) {
Property skinProperty = new Property("textures", skinData.value(), skinData.signature());
GameProfile profile = versionSpecificMethods.createGameProfile(oldProfile, skinProperty);
Object entityHuman = ReflectionUtils.invoke(player, ClassNames.GET_ENTITY_HUMAN_METHOD);
ReflectionUtils.setValue(entityHuman, ClassNames.GAME_PROFILE_FIELD, profile);
}
private void replaceSkinOld(PropertyMap properties, SkinData skinData) {
properties.removeAll("textures");
Property property = new Property("textures", skinData.value(), skinData.signature());
properties.put("textures", property);

View File

@@ -28,6 +28,7 @@ package org.geysermc.floodgate.util;
import static org.geysermc.floodgate.util.ReflectionUtils.castedStaticBooleanValue;
import static org.geysermc.floodgate.util.ReflectionUtils.getBooleanValue;
import static org.geysermc.floodgate.util.ReflectionUtils.getClassOrFallback;
import static org.geysermc.floodgate.util.ReflectionUtils.getClassOrFallbackSilently;
import static org.geysermc.floodgate.util.ReflectionUtils.getClassSilently;
import static org.geysermc.floodgate.util.ReflectionUtils.getConstructor;
import static org.geysermc.floodgate.util.ReflectionUtils.getField;
@@ -59,7 +60,9 @@ public class ClassNames {
public static final Class<?> LOGIN_LISTENER;
@Nullable public static final Class<?> CLIENT_INTENT;
public static final Constructor<OfflinePlayer> CRAFT_OFFLINE_PLAYER_CONSTRUCTOR;
@Nullable public static final Constructor<OfflinePlayer> CRAFT_OFFLINE_PLAYER_CONSTRUCTOR;
@Nullable public static final Constructor<OfflinePlayer> CRAFT_NEW_OFFLINE_PLAYER_CONSTRUCTOR;
@Nullable public static final Constructor<?> NAME_AND_ID_CONSTRUCTOR;
@Nullable public static final Constructor<?> LOGIN_HANDLER_CONSTRUCTOR;
@Nullable public static final Constructor<?> HANDSHAKE_PACKET_CONSTRUCTOR;
@@ -76,6 +79,10 @@ public class ClassNames {
@Nullable public static final BooleanSupplier PAPER_VELOCITY_SUPPORT;
public static final Method GET_PROFILE_METHOD;
@Nullable public static final Method GET_ENTITY_HUMAN_METHOD;
@Nullable public static final Field GAME_PROFILE_FIELD;
public static final Method LOGIN_DISCONNECT;
public static final Method NETWORK_EXCEPTION_CAUGHT;
@Nullable public static final Method INIT_UUID;
@@ -110,11 +117,25 @@ public class ClassNames {
// SpigotSkinApplier
Class<?> craftPlayerClass = ReflectionUtils.getClass(
"org.bukkit.craftbukkit." + version + "entity.CraftPlayer");
Class<?> craftPlayerClass = getClassOrFallback(
"org.bukkit.craftbukkit.entity.CraftPlayer",
"org.bukkit.craftbukkit." + version + "entity.CraftPlayer"
);
GET_PROFILE_METHOD = getMethod(craftPlayerClass, "getProfile");
checkNotNull(GET_PROFILE_METHOD, "Get profile method");
GET_ENTITY_HUMAN_METHOD = getMethod(craftPlayerClass, "getHandle");
Class<?> entityHumanClass = getClassOrFallbackSilently(
"net.minecraft.world.entity.player.EntityHuman",
"net.minecraft.world.entity.player.Player"
);
if (entityHumanClass != null) {
// Spigot obfuscates field name
GAME_PROFILE_FIELD = getFieldOfType(entityHumanClass, GameProfile.class);
} else {
GAME_PROFILE_FIELD = null;
}
// SpigotInjector
MINECRAFT_SERVER = getClassOrFallback(
"net.minecraft.server.MinecraftServer",
@@ -122,21 +143,39 @@ public class ClassNames {
);
SERVER_CONNECTION = getClassOrFallback(
"net.minecraft.server.network.ServerConnectionListener",
"net.minecraft.server.network.ServerConnection",
nmsPackage + "ServerConnection"
);
// WhitelistUtils
Class<?> craftServerClass = ReflectionUtils.getClass(
"org.bukkit.craftbukkit." + version + "CraftServer");
Class<OfflinePlayer> craftOfflinePlayerClass = ReflectionUtils.getCastedClass(
"org.bukkit.craftbukkit." + version + "CraftOfflinePlayer");
Class<?> craftServerClass = getClassOrFallback(
"org.bukkit.craftbukkit.CraftServer",
"org.bukkit.craftbukkit." + version + "CraftServer"
);
Class<OfflinePlayer> craftOfflinePlayerClass = ReflectionUtils.getCastedClassOrFallback(
"org.bukkit.craftbukkit.CraftOfflinePlayer",
"org.bukkit.craftbukkit." + version + "CraftOfflinePlayer"
);
CRAFT_OFFLINE_PLAYER_CONSTRUCTOR = ReflectionUtils.getConstructor(
craftOfflinePlayerClass, true, craftServerClass, GameProfile.class);
if (CRAFT_OFFLINE_PLAYER_CONSTRUCTOR == null) { // Changed in 1.21.9
Class<?> nameAndIdClass = getClassSilently("net.minecraft.server.players.NameAndId");
CRAFT_NEW_OFFLINE_PLAYER_CONSTRUCTOR = ReflectionUtils.getConstructor(
craftOfflinePlayerClass, true, craftServerClass, nameAndIdClass);
NAME_AND_ID_CONSTRUCTOR = ReflectionUtils.getConstructor(nameAndIdClass, true, GameProfile.class);
} else {
CRAFT_NEW_OFFLINE_PLAYER_CONSTRUCTOR = null;
NAME_AND_ID_CONSTRUCTOR = null;
}
// SpigotDataHandler
Class<?> networkManager = getClassOrFallback(
"net.minecraft.network.Connection",
"net.minecraft.network.NetworkManager",
nmsPackage + "NetworkManager"
);
@@ -144,6 +183,7 @@ public class ClassNames {
SOCKET_ADDRESS = getFieldOfType(networkManager, SocketAddress.class, false);
HANDSHAKE_PACKET = getClassOrFallback(
"net.minecraft.network.protocol.handshake.ClientIntentionPacket",
"net.minecraft.network.protocol.handshake.PacketHandshakingInSetProtocol",
nmsPackage + "PacketHandshakingInSetProtocol"
);
@@ -152,11 +192,13 @@ public class ClassNames {
checkNotNull(HANDSHAKE_HOST, "Handshake host");
LOGIN_START_PACKET = getClassOrFallback(
"net.minecraft.network.protocol.login.ServerboundHelloPacket",
"net.minecraft.network.protocol.login.PacketLoginInStart",
nmsPackage + "PacketLoginInStart"
);
LOGIN_LISTENER = getClassOrFallback(
"net.minecraft.server.network.ServerLoginPacketListenerImpl",
"net.minecraft.server.network.LoginListener",
nmsPackage + "LoginListener"
);
@@ -197,7 +239,7 @@ public class ClassNames {
// We get the field by name on 1.20.2+ as there are now multiple fields of this type in network manager
// PacketListener packetListener of NetworkManager
PACKET_LISTENER = getField(networkManager, "q");
PACKET_LISTENER = getField(networkManager, "packetListener", "q");
makeAccessible(PACKET_LISTENER);
}
checkNotNull(PACKET_LISTENER, "Packet listener");
@@ -205,7 +247,7 @@ public class ClassNames {
if (IS_POST_LOGIN_HANDLER) {
makeAccessible(CALL_PLAYER_PRE_LOGIN_EVENTS);
START_CLIENT_VERIFICATION = getMethod(LOGIN_LISTENER, "b", GameProfile.class);
START_CLIENT_VERIFICATION = getMethod(LOGIN_LISTENER, "startClientVerification", "b", GameProfile.class);
checkNotNull(START_CLIENT_VERIFICATION, "startClientVerification");
makeAccessible(START_CLIENT_VERIFICATION);
@@ -301,15 +343,15 @@ public class ClassNames {
String.class, int.class, CLIENT_INTENT);
checkNotNull(HANDSHAKE_PACKET_CONSTRUCTOR, "Handshake packet constructor");
Field a = getField(HANDSHAKE_PACKET, "a");
Field a = getField(HANDSHAKE_PACKET, "STREAM_CODEC", "a");
checkNotNull(a, "Handshake \"a\" field (protocol version, or stream codec)");
if (a.getType().isPrimitive()) { // 1.20.2 - 1.20.4: a is the protocol version (int)
HANDSHAKE_PROTOCOL = a;
HANDSHAKE_PORT = getField(HANDSHAKE_PACKET, "c");
} else { // 1.20.5: a is the stream_codec thing, so everything is shifted
HANDSHAKE_PROTOCOL = getField(HANDSHAKE_PACKET, "b");
HANDSHAKE_PORT = getField(HANDSHAKE_PACKET, "d");
HANDSHAKE_PROTOCOL = getField(HANDSHAKE_PACKET, "protocolVersion", "b");
HANDSHAKE_PORT = getField(HANDSHAKE_PACKET, "port", "d");
}
checkNotNull(HANDSHAKE_PROTOCOL, "Handshake protocol");

View File

@@ -25,9 +25,15 @@
package org.geysermc.floodgate.util;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
@@ -42,6 +48,12 @@ public final class SpigotVersionSpecificMethods {
private static final Method NEW_PROPERTY_VALUE;
private static final Method NEW_PROPERTY_SIGNATURE;
private static final Method NEW_GAME_PROFILE_PROPERTIES;
private static final Constructor<GameProfile> RECORD_GAME_PROFILE_CONSTRUCTOR;
private static final Constructor<PropertyMap> IMMUTABLE_PROPERTY_MAP_CONSTRUCTOR;
private static final Method MULTIMAP_FROM_MAP;
private static final Field PROFILE_NAME_FIELD;
private static final Field PROFILE_UUID_FIELD;
static {
GET_SPIGOT = ReflectionUtils.getMethod(Player.class, "spigot");
@@ -54,6 +66,18 @@ public final class SpigotVersionSpecificMethods {
NEW_PROPERTY_VALUE = ReflectionUtils.getMethod(Property.class, "value");
NEW_PROPERTY_SIGNATURE = ReflectionUtils.getMethod(Property.class, "signature");
NEW_GAME_PROFILE_PROPERTIES = ReflectionUtils.getMethod(
GameProfile.class, "properties");
RECORD_GAME_PROFILE_CONSTRUCTOR = ReflectionUtils.getConstructor(
GameProfile.class, true, UUID.class, String.class, PropertyMap.class);
IMMUTABLE_PROPERTY_MAP_CONSTRUCTOR = (Constructor<PropertyMap>)
PropertyMap.class.getConstructors()[0];
PROFILE_NAME_FIELD = ReflectionUtils.getField(GameProfile.class, "name");
PROFILE_UUID_FIELD = ReflectionUtils.getField(GameProfile.class, "id");
// Avoid relocation for this class.
Class<?> multimaps = ReflectionUtils.getClass(String.join(".", "com",
"google", "common", "collect", "Multimaps"));
MULTIMAP_FROM_MAP = ReflectionUtils.getMethod(multimaps, "forMap", Map.class);
}
private final SpigotPlugin plugin;
@@ -62,6 +86,31 @@ public final class SpigotVersionSpecificMethods {
this.plugin = plugin;
}
public GameProfile createGameProfile(GameProfile oldProfile, Property textureProperty) {
String name = (String) ReflectionUtils.getValue(oldProfile, PROFILE_NAME_FIELD);
UUID uuid = (UUID) ReflectionUtils.getValue(oldProfile, PROFILE_UUID_FIELD);
return createGameProfile(uuid, name, textureProperty);
}
public GameProfile createGameProfile(UUID uuid, String name, Property texturesProperty) {
if (RECORD_GAME_PROFILE_CONSTRUCTOR != null && IMMUTABLE_PROPERTY_MAP_CONSTRUCTOR != null) {
if (texturesProperty != null) {
Map<String, Property> properties = new HashMap<>();
properties.put("textures", texturesProperty);
Object multimap = ReflectionUtils.invoke(null, MULTIMAP_FROM_MAP, properties);
return ReflectionUtils.newInstanceOrThrow(RECORD_GAME_PROFILE_CONSTRUCTOR, uuid,
name,
ReflectionUtils.newInstanceOrThrow(IMMUTABLE_PROPERTY_MAP_CONSTRUCTOR,
multimap));
}
}
GameProfile profile = new GameProfile(uuid, name);
if (texturesProperty != null) {
profile.getProperties().put("textures", texturesProperty);
}
return profile;
}
public String getLocale(Player player) {
if (OLD_GET_LOCALE == null) {
return player.getLocale();
@@ -80,7 +129,14 @@ public final class SpigotVersionSpecificMethods {
hideAndShowPlayer0(on, target);
}
public SkinApplyEvent.SkinData currentSkin(PropertyMap properties) {
public SkinApplyEvent.SkinData currentSkin(GameProfile profile) {
PropertyMap properties;
if (NEW_GAME_PROFILE_PROPERTIES != null) {
properties = ReflectionUtils.castedInvoke(profile, NEW_GAME_PROFILE_PROPERTIES);
} else {
properties = profile.getProperties();
}
for (Property property : properties.get("textures")) {
String value;
String signature;

View File

@@ -29,6 +29,7 @@ import com.mojang.authlib.GameProfile;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
@SuppressWarnings("ConstantConditions")
public final class WhitelistUtils {
@@ -44,10 +45,7 @@ public final class WhitelistUtils {
public static boolean addPlayer(UUID uuid, String username, SpigotVersionSpecificMethods versionSpecificMethods) {
GameProfile profile = new GameProfile(uuid, username);
OfflinePlayer player = ReflectionUtils.newInstance(
ClassNames.CRAFT_OFFLINE_PLAYER_CONSTRUCTOR,
Bukkit.getServer(), profile
);
OfflinePlayer player = getOfflinePlayer(profile);
if (player.isWhitelisted()) {
return false;
}
@@ -67,10 +65,7 @@ public final class WhitelistUtils {
public static boolean removePlayer(UUID uuid, String username, SpigotVersionSpecificMethods versionSpecificMethods) {
GameProfile profile = new GameProfile(uuid, username);
OfflinePlayer player = ReflectionUtils.newInstance(
ClassNames.CRAFT_OFFLINE_PLAYER_CONSTRUCTOR,
Bukkit.getServer(), profile
);
OfflinePlayer player = getOfflinePlayer(profile);
if (!player.isWhitelisted()) {
return false;
}
@@ -81,4 +76,23 @@ public final class WhitelistUtils {
static void setWhitelist(OfflinePlayer player, boolean whitelist, SpigotVersionSpecificMethods versionSpecificMethods) {
versionSpecificMethods.maybeSchedule(() -> player.setWhitelisted(whitelist), true); // Whitelisting is on the global thread
}
static OfflinePlayer getOfflinePlayer(GameProfile profile) {
if (ClassNames.CRAFT_NEW_OFFLINE_PLAYER_CONSTRUCTOR != null) {
Object nameAndId = ReflectionUtils.newInstance(
ClassNames.NAME_AND_ID_CONSTRUCTOR,
profile
);
return ReflectionUtils.newInstance(
ClassNames.CRAFT_NEW_OFFLINE_PLAYER_CONSTRUCTOR,
Bukkit.getServer(), nameAndId
);
} else {
return ReflectionUtils.newInstance(
ClassNames.CRAFT_OFFLINE_PLAYER_CONSTRUCTOR,
Bukkit.getServer(), profile
);
}
}
}