Delay force action until PlayerJoinEvent is fired

Fixes Paper async chunk loading -> delayed PlayerJoinEvent
(Related #331)
This commit is contained in:
games647
2020-05-09 18:49:43 +02:00
parent b6a95bb153
commit f40e787b55
14 changed files with 241 additions and 127 deletions

View File

@ -0,0 +1,160 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.bukkit.listener.BungeeListener;
import com.github.games647.fastlogin.core.message.ChannelMessage;
import com.github.games647.fastlogin.core.message.LoginActionMessage;
import com.github.games647.fastlogin.core.message.NamespaceKey;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageRecipient;
import static com.github.games647.fastlogin.core.message.ChangePremiumMessage.CHANGE_CHANNEL;
import static com.github.games647.fastlogin.core.message.SuccessMessage.SUCCESS_CHANNEL;
import static java.util.stream.Collectors.toSet;
public class BungeeManager {
private static final String FILE_NAME = "proxy-whitelist.txt";
//null if whitelist is empty so bungeecord support is disabled
private Set<UUID> proxyIds;
private final FastLoginBukkit plugin;
private boolean enabled;
private final Set<UUID> firedJoinEvents = new HashSet<>();
public BungeeManager(FastLoginBukkit plugin) {
this.plugin = plugin;
}
public void cleanup() {
//remove old blacklists
Bukkit.getOnlinePlayers().forEach(player -> player.removeMetadata(plugin.getName(), plugin));
}
public void sendPluginMessage(PluginMessageRecipient player, ChannelMessage message) {
if (player != null) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
message.writeTo(dataOutput);
NamespaceKey channel = new NamespaceKey(plugin.getName(), message.getChannelName());
player.sendPluginMessage(plugin, channel.getCombinedName(), dataOutput.toByteArray());
}
}
public boolean isEnabled() {
return enabled;
}
public void initialize() {
try {
enabled = detectBungeeCord();
} catch (Exception ex) {
plugin.getLog().warn("Cannot check bungeecord support. Fallback to non-bungee mode", ex);
}
if (enabled) {
proxyIds = loadBungeeCordIds();
registerPluginChannels();
}
}
private boolean detectBungeeCord() throws Exception {
try {
enabled = Class.forName("org.spigotmc.SpigotConfig").getDeclaredField("bungee").getBoolean(null);
return enabled;
} catch (ClassNotFoundException notFoundEx) {
//ignore server has no bungee support
return false;
} catch (Exception ex) {
throw ex;
}
}
private void registerPluginChannels() {
Server server = Bukkit.getServer();
// check for incoming messages from the bungeecord version of this plugin
String groupId = plugin.getName();
String forceChannel = NamespaceKey.getCombined(groupId, LoginActionMessage.FORCE_CHANNEL);
server.getMessenger().registerIncomingPluginChannel(plugin, forceChannel, new BungeeListener(plugin));
// outgoing
String successChannel = new NamespaceKey(groupId, SUCCESS_CHANNEL).getCombinedName();
String changeChannel = new NamespaceKey(groupId, CHANGE_CHANNEL).getCombinedName();
server.getMessenger().registerOutgoingPluginChannel(plugin, successChannel);
server.getMessenger().registerOutgoingPluginChannel(plugin, changeChannel);
}
private Set<UUID> loadBungeeCordIds() {
Path whitelistFile = plugin.getPluginFolder().resolve(FILE_NAME);
try {
if (Files.notExists(whitelistFile)) {
Files.createFile(whitelistFile);
}
try (Stream<String> lines = Files.lines(whitelistFile)) {
return lines.map(String::trim)
.map(UUID::fromString)
.collect(toSet());
}
} catch (IOException ex) {
plugin.getLog().error("Failed to read proxy whitelist", ex);
} catch (Exception ex) {
plugin.getLog().error("Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
}
return Collections.emptySet();
}
public boolean isProxyAllowed(UUID proxyId) {
return proxyIds != null && proxyIds.contains(proxyId);
}
/**
* Mark the event to be fired including the task delay.
*
* @param player
*/
public synchronized void markJoinEventFired(Player player) {
firedJoinEvents.add(player.getUniqueId());
}
/**
* Check if the event fired including with the task delay. This necessary to restore the order of processing the
* BungeeCord messages after the PlayerJoinEvent fires including the delay.
*
* If the join event fired, the delay exceeded, but it ran earlier and couldn't find the recently started login
* session. If not fired, we can start a new force login task. This will still match the requirement that we wait
* a certain time after the player join event fired.
*
* @param player
* @return event fired including delay
*/
public synchronized boolean didJoinEventFired(Player player) {
return firedJoinEvents.contains(player.getUniqueId());
}
/**
* Player quit clean up
*
* @param player
*/
public synchronized void cleanup(Player player) {
firedJoinEvents.remove(player.getUniqueId());
}
}

View File

@ -2,7 +2,6 @@ package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.bukkit.command.CrackedCommand;
import com.github.games647.fastlogin.bukkit.command.PremiumCommand;
import com.github.games647.fastlogin.bukkit.listener.BungeeListener;
import com.github.games647.fastlogin.bukkit.listener.ConnectionListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.ProtocolLibListener;
import com.github.games647.fastlogin.bukkit.listener.protocollib.SkinApplyListener;
@ -10,13 +9,8 @@ import com.github.games647.fastlogin.bukkit.listener.protocolsupport.ProtocolSup
import com.github.games647.fastlogin.bukkit.task.DelayedAuthHook;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.PremiumStatus;
import com.github.games647.fastlogin.core.message.ChannelMessage;
import com.github.games647.fastlogin.core.message.LoginActionMessage;
import com.github.games647.fastlogin.core.message.NamespaceKey;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.net.InetSocketAddress;
import java.nio.file.Path;
@ -29,12 +23,8 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.messaging.PluginMessageRecipient;
import org.slf4j.Logger;
import static com.github.games647.fastlogin.core.message.ChangePremiumMessage.CHANGE_CHANNEL;
import static com.github.games647.fastlogin.core.message.SuccessMessage.SUCCESS_CHANNEL;
/**
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
*/
@ -46,7 +36,7 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
private final Logger logger;
private boolean serverStarted;
private boolean bungeeCord;
private BungeeManager bungeeManager;
private final BukkitScheduler scheduler;
private FastLoginCore<Player, CommandSender, FastLoginBukkit> core;
@ -60,34 +50,19 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
core = new FastLoginCore<>(this);
core.load();
try {
bungeeCord = Class.forName("org.spigotmc.SpigotConfig").getDeclaredField("bungee").getBoolean(null);
} catch (ClassNotFoundException notFoundEx) {
//ignore server has no bungee support
} catch (Exception ex) {
logger.warn("Cannot check bungeecord support. You use a non-Spigot build", ex);
}
if (getServer().getOnlineMode()) {
//we need to require offline to prevent a loginSession request for a offline player
logger.error("Server have to be in offline mode");
logger.error("Server has to be in offline mode");
setEnabled(false);
return;
}
bungeeManager = new BungeeManager(this);
bungeeManager.initialize();
PluginManager pluginManager = getServer().getPluginManager();
if (bungeeCord) {
if (bungeeManager.isEnabled()) {
setServerStarted();
// check for incoming messages from the bungeecord version of this plugin
String forceChannel = NamespaceKey.getCombined(getName(), LoginActionMessage.FORCE_CHANNEL);
getServer().getMessenger().registerIncomingPluginChannel(this, forceChannel, new BungeeListener(this));
// outgoing
String successChannel = new NamespaceKey(getName(), SUCCESS_CHANNEL).getCombinedName();
String changeChannel = new NamespaceKey(getName(), CHANGE_CHANNEL).getCombinedName();
getServer().getMessenger().registerOutgoingPluginChannel(this, successChannel);
getServer().getMessenger().registerOutgoingPluginChannel(this, changeChannel);
} else {
if (!core.setupDatabase()) {
setEnabled(false);
@ -128,8 +103,7 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
core.close();
}
//remove old blacklists
getServer().getOnlinePlayers().forEach(player -> player.removeMetadata(getName(), this));
bungeeManager.cleanup();
}
public FastLoginCore<Player, CommandSender, FastLoginBukkit> getCore() {
@ -165,10 +139,6 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
return premiumPlayers;
}
public boolean isBungeeEnabled() {
return bungeeCord;
}
/**
* Fetches the premium status of an online player.
*
@ -198,14 +168,8 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
}
}
public void sendPluginMessage(PluginMessageRecipient player, ChannelMessage message) {
if (player != null) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
message.writeTo(dataOutput);
NamespaceKey channel = new NamespaceKey(getName(), message.getChannelName());
player.sendPluginMessage(this, channel.getCombinedName(), dataOutput.toByteArray());
}
public BungeeManager getBungeeManager() {
return bungeeManager;
}
@Override

View File

@ -32,7 +32,7 @@ public class CrackedCommand extends ToggleCommand {
return;
}
if (plugin.isBungeeEnabled()) {
if (plugin.getBungeeManager().isEnabled()) {
sendBungeeActivateMessage(sender, sender.getName(), false);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {

View File

@ -31,7 +31,7 @@ public abstract class ToggleCommand implements CommandExecutor {
}
protected boolean forwardBungeeCommand(CommandSender sender, String target, boolean activate) {
if (plugin.isBungeeEnabled()) {
if (plugin.getBungeeManager().isEnabled()) {
sendBungeeActivateMessage(sender, target, activate);
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
return true;
@ -53,7 +53,7 @@ public abstract class ToggleCommand implements CommandExecutor {
protected void sendBungeeActivateMessage(CommandSender invoker, String target, boolean activate) {
if (invoker instanceof PluginMessageRecipient) {
ChannelMessage message = new ChangePremiumMessage(target, activate, true);
plugin.sendPluginMessage((PluginMessageRecipient) invoker, message);
plugin.getBungeeManager().sendPluginMessage((PluginMessageRecipient) invoker, message);
} else {
Optional<? extends Player> optPlayer = Bukkit.getServer().getOnlinePlayers().stream().findFirst();
if (!optPlayer.isPresent()) {
@ -63,7 +63,7 @@ public abstract class ToggleCommand implements CommandExecutor {
Player sender = optPlayer.get();
ChannelMessage message = new ChangePremiumMessage(target, activate, false);
plugin.sendPluginMessage(sender, message);
plugin.getBungeeManager().sendPluginMessage(sender, message);
}
}
}

View File

@ -10,21 +10,13 @@ import com.github.games647.fastlogin.core.message.LoginActionMessage.Type;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import static java.util.stream.Collectors.toSet;
/**
* Responsible for receiving messages from a BungeeCord instance.
*
@ -33,15 +25,10 @@ import static java.util.stream.Collectors.toSet;
*/
public class BungeeListener implements PluginMessageListener {
private static final String FILE_NAME = "proxy-whitelist.txt";
private final FastLoginBukkit plugin;
//null if whitelist is empty so bungeecord support is disabled
private final Set<UUID> proxyIds;
public BungeeListener(FastLoginBukkit plugin) {
this.plugin = plugin;
this.proxyIds = loadBungeeCordIds();
}
@Override
@ -53,20 +40,23 @@ public class BungeeListener implements PluginMessageListener {
plugin.getLog().debug("Received plugin message {}", loginMessage);
//check if the player is still online or disconnected
Player checkedPlayer = Bukkit.getPlayerExact(loginMessage.getPlayerName());
if (checkedPlayer == null) {
Player targetPlayer = player;
if (!loginMessage.getPlayerName().equals(player.getName())) {
targetPlayer = Bukkit.getPlayerExact(loginMessage.getPlayerName());;
}
if (targetPlayer == null) {
plugin.getLog().warn("Force action player {} not found", loginMessage.getPlayerName());
return;
}
//fail if target player is blacklisted because already authenticated or wrong bungeecord id
if (checkedPlayer.hasMetadata(plugin.getName())) {
plugin.getLog().warn("Received message {} from a blacklisted player {}", loginMessage, checkedPlayer);
// fail if target player is blacklisted because already authenticated or wrong bungeecord id
if (targetPlayer.hasMetadata(plugin.getName())) {
plugin.getLog().warn("Received message {} from a blacklisted player {}", loginMessage, targetPlayer);
} else {
//fail if BungeeCord support is disabled (id = null)
UUID sourceId = loginMessage.getProxyId();
if (proxyIds.contains(sourceId)) {
readMessage(checkedPlayer, loginMessage);
if (plugin.getBungeeManager().isProxyAllowed(sourceId)) {
readMessage(targetPlayer, loginMessage);
} else {
plugin.getLog().warn("Received proxy id: {} that doesn't exist in the proxy whitelist file", sourceId);
}
@ -79,50 +69,43 @@ public class BungeeListener implements PluginMessageListener {
InetSocketAddress address = player.getAddress();
if (type == Type.LOGIN) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, true);
playerSession.setVerified(true);
plugin.putSession(address, playerSession);
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, new ForceLoginTask(plugin.getCore(), player), 10L);
onLoginMessage(player, playerName, address);
} else if (type == Type.REGISTER) {
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
AuthPlugin<Player> authPlugin = plugin.getCore().getAuthPluginHook();
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin == null || !authPlugin.isRegistered(playerName)) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, false);
playerSession.setVerified(true);
plugin.putSession(address, playerSession);
new ForceLoginTask(plugin.getCore(), player).run();
}
} catch (Exception ex) {
plugin.getLog().error("Failed to query isRegistered for player: {}", player, ex);
}
}, 10L);
onRegisterMessage(player, playerName, address);
} else if (type == Type.CRACKED) {
//we don't start a force login task here so update it manually
plugin.getPremiumPlayers().put(player.getUniqueId(), PremiumStatus.CRACKED);
}
}
public Set<UUID> loadBungeeCordIds() {
Path whitelistFile = plugin.getPluginFolder().resolve(FILE_NAME);
try {
if (Files.notExists(whitelistFile)) {
Files.createFile(whitelistFile);
}
private void onLoginMessage(Player player, String playerName, InetSocketAddress address) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, true);
startLoginTaskIfReady(player, playerSession);
}
try (Stream<String> lines = Files.lines(whitelistFile)) {
return lines.map(String::trim)
.map(UUID::fromString)
.collect(toSet());
private void onRegisterMessage(Player player, String playerName, InetSocketAddress address) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
AuthPlugin<Player> authPlugin = plugin.getCore().getAuthPluginHook();
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin == null || !authPlugin.isRegistered(playerName)) {
BukkitLoginSession playerSession = new BukkitLoginSession(playerName, false);
startLoginTaskIfReady(player, playerSession);
}
} catch (Exception ex) {
plugin.getLog().error("Failed to query isRegistered for player: {}", player, ex);
}
} catch (IOException ex) {
plugin.getLog().error("Failed to create file for Proxy whitelist", ex);
} catch (Exception ex) {
plugin.getLog().error("Failed to retrieve proxy Id. Disabling BungeeCord support", ex);
});
}
private void startLoginTaskIfReady(Player player, BukkitLoginSession session) {
session.setVerified(true);
plugin.putSession(player.getAddress(), session);
// only start a new login task if the join event fired earlier. This event then didn
if (plugin.getBungeeManager().didJoinEventFired(player)) {
Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player, session);
Bukkit.getScheduler().runTaskAsynchronously(plugin, forceLoginTask);
}
return Collections.emptySet();
}
}

View File

@ -1,5 +1,6 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.task.ForceLoginTask;
@ -29,6 +30,7 @@ public class ConnectionListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLogin(PlayerLoginEvent loginEvent) {
removeBlacklistStatus(loginEvent.getPlayer());
if (loginEvent.getResult() == Result.ALLOWED && !plugin.isServerFullyStarted()) {
loginEvent.disallow(Result.KICK_OTHER, plugin.getCore().getMessage("not-started"));
}
@ -38,21 +40,30 @@ public class ConnectionListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
Player player = joinEvent.getPlayer();
removeBlacklistStatus(player);
if (!plugin.isBungeeEnabled()) {
//Wait before auth plugin and we received a message from BungeeCord initializes the player
Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player);
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, forceLoginTask, DELAY_LOGIN);
}
Bukkit.getScheduler().runTaskLater(plugin, () -> {
// session exists so the player is ready for force login
// cases: Paper (firing BungeeCord message before PlayerJoinEvent) or not running BungeeCord and already
// having the login session from the login process
BukkitLoginSession session = plugin.getSession(player.getAddress());
if (session != null) {
Runnable forceLoginTask = new ForceLoginTask(plugin.getCore(), player, session);
Bukkit.getScheduler().runTaskAsynchronously(plugin, forceLoginTask);
}
plugin.getBungeeManager().markJoinEventFired(player);
// delay the login process to let auth plugins initialize the player
// Magic number however as there is no direct event from those plugins
}, DELAY_LOGIN);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent quitEvent) {
Player player = quitEvent.getPlayer();
removeBlacklistStatus(player);
removeBlacklistStatus(player);
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
plugin.getPremiumPlayers().remove(player.getUniqueId());
plugin.getBungeeManager().cleanup(player);
}
private void removeBlacklistStatus(Player player) {

View File

@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bukkit;
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;

View File

@ -5,7 +5,6 @@ import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.security.KeyPair;

View File

@ -5,7 +5,6 @@ import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.core.shared.LoginSource;
import java.lang.reflect.InvocationTargetException;
@ -19,7 +18,7 @@ import org.bukkit.entity.Player;
import static com.comphenix.protocol.PacketType.Login.Server.DISCONNECT;
import static com.comphenix.protocol.PacketType.Login.Server.ENCRYPTION_BEGIN;
public class ProtocolLibLoginSource implements LoginSource {
class ProtocolLibLoginSource implements LoginSource {
private final PacketEvent packetEvent;
private final Player player;

View File

@ -12,7 +12,6 @@ import com.github.games647.craftapi.model.auth.Verification;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.io.IOException;

View File

@ -28,7 +28,7 @@ public class DelayedAuthHook implements Runnable {
@Override
public void run() {
boolean hookFound = isHookFound();
if (plugin.isBungeeEnabled()) {
if (plugin.getBungeeManager().isEnabled()) {
plugin.getLog().info("BungeeCord setting detected. No auth plugin is required");
} else if (!hookFound) {
plugin.getLog().warn("No auth plugin were found by this plugin "

View File

@ -20,8 +20,9 @@ import org.bukkit.metadata.FixedMetadataValue;
public class ForceLoginTask extends ForceLoginManagement<Player, CommandSender, BukkitLoginSession, FastLoginBukkit> {
public ForceLoginTask(FastLoginCore<Player, CommandSender, FastLoginBukkit> core, Player player) {
super(core, player, core.getPlugin().getSession(player.getAddress()));
public ForceLoginTask(FastLoginCore<Player, CommandSender, FastLoginBukkit> core, Player player,
BukkitLoginSession session) {
super(core, player, session);
}
@Override
@ -49,8 +50,8 @@ public class ForceLoginTask extends ForceLoginManagement<Player, CommandSender,
@Override
public void onForceActionSuccess(LoginSession session) {
if (core.getPlugin().isBungeeEnabled()) {
core.getPlugin().sendPluginMessage(player, new SuccessMessage());
if (core.getPlugin().getBungeeManager().isEnabled()) {
core.getPlugin().getBungeeManager().sendPluginMessage(player, new SuccessMessage());
}
}

View File

@ -1,4 +1,4 @@
package com.github.games647.fastlogin.bukkit;
package com.github.games647.fastlogin.bukkit.listener.protocollib;
import java.security.SecureRandom;

View File

@ -9,9 +9,7 @@ import com.github.games647.fastlogin.core.shared.LoginSession;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
@ -106,7 +104,7 @@ public class ConnectListener implements Listener {
// In this case it means that the force command (plugin message) is already received and processed while
// player is still in the login phase and reported to be offline.
Runnable loginTask = new ForceLoginTask(plugin.getCore(), player, server);
ProxyServer.getInstance().getScheduler().schedule(plugin, loginTask, 500, TimeUnit.MILLISECONDS);
plugin.getScheduler().runAsync(loginTask);
}
@EventHandler