package net.minestom.server.network; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.player.AsyncPlayerPreLoginEvent; import net.minestom.server.event.player.PlayerLoginEvent; import net.minestom.server.instance.Instance; import net.minestom.server.network.packet.server.login.LoginSuccessPacket; import net.minestom.server.network.packet.server.play.DisconnectPacket; import net.minestom.server.network.packet.server.play.KeepAlivePacket; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; import net.minestom.server.utils.StringUtils; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.debug.DebugUtils; import net.minestom.server.utils.validate.Check; import org.jctools.queues.MessagePassingQueue; import org.jctools.queues.MpscUnboundedArrayQueue; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.Function; /** * Manages the connected clients. */ public final class ConnectionManager { private static final long KEEP_ALIVE_DELAY = 10_000; private static final long KEEP_ALIVE_KICK = 30_000; private static final Component TIMEOUT_TEXT = Component.text("Timeout", NamedTextColor.RED); private final MessagePassingQueue waitingPlayers = new MpscUnboundedArrayQueue<>(64); private final Set players = new CopyOnWriteArraySet<>(); private final Set unmodifiablePlayers = Collections.unmodifiableSet(players); private final Map connectionPlayerMap = new ConcurrentHashMap<>(); // The uuid provider once a player login private volatile UuidProvider uuidProvider = (playerConnection, username) -> UUID.randomUUID(); // The player provider to have your own Player implementation private volatile PlayerProvider playerProvider = Player::new; private Component shutdownText = Component.text("The server is shutting down.", NamedTextColor.RED); /** * Gets the {@link Player} linked to a {@link PlayerConnection}. * * @param connection the player connection * @return the player linked to the connection */ public Player getPlayer(@NotNull PlayerConnection connection) { return connectionPlayerMap.get(connection); } /** * Gets all online players. * * @return an unmodifiable collection containing all the online players */ public @NotNull Collection<@NotNull Player> getOnlinePlayers() { return unmodifiablePlayers; } /** * Finds the closest player matching a given username. * * @param username the player username (can be partial) * @return the closest match, null if no players are online */ public @Nullable Player findPlayer(@NotNull String username) { Player exact = getPlayer(username); if (exact != null) return exact; final String username1 = username.toLowerCase(Locale.ROOT); Function distanceFunction = player -> { final String username2 = player.getUsername().toLowerCase(Locale.ROOT); return StringUtils.jaroWinklerScore(username1, username2); }; return getOnlinePlayers() .stream() .min(Comparator.comparingDouble(distanceFunction::apply)) .filter(player -> distanceFunction.apply(player) > 0) .orElse(null); } /** * Gets the first player which validate {@link String#equalsIgnoreCase(String)}. *

* This can cause issue if two or more players have the same username. * * @param username the player username (ignoreCase) * @return the first player who validate the username condition, null if none was found */ public @Nullable Player getPlayer(@NotNull String username) { for (Player player : getOnlinePlayers()) { if (player.getUsername().equalsIgnoreCase(username)) return player; } return null; } /** * Gets the first player which validate {@link UUID#equals(Object)}. *

* This can cause issue if two or more players have the same UUID. * * @param uuid the player UUID * @return the first player who validate the UUID condition, null if none was found */ public @Nullable Player getPlayer(@NotNull UUID uuid) { for (Player player : getOnlinePlayers()) { if (player.getUuid().equals(uuid)) return player; } return null; } /** * Changes how {@link UUID} are attributed to players. *

* Shouldn't be override if already defined. *

* Be aware that it is possible for an UUID provider to be ignored, for example in the case of a proxy (eg: velocity). * * @param uuidProvider the new player connection uuid provider, * setting it to null would apply a random UUID for each player connection * @see #getPlayerConnectionUuid(PlayerConnection, String) */ public void setUuidProvider(@Nullable UuidProvider uuidProvider) { this.uuidProvider = uuidProvider != null ? uuidProvider : (playerConnection, username) -> UUID.randomUUID(); } /** * Computes the UUID of the specified connection. * Used in {@link net.minestom.server.network.packet.client.login.LoginStartPacket} in order * to give the player the right {@link UUID}. * * @param playerConnection the player connection * @param username the username given by the connection * @return the uuid based on {@code playerConnection} * return a random UUID if no UUID provider is defined see {@link #setUuidProvider(UuidProvider)} */ public @NotNull UUID getPlayerConnectionUuid(@NotNull PlayerConnection playerConnection, @NotNull String username) { return uuidProvider.provide(playerConnection, username); } /** * Changes the {@link Player} provider, to change which object to link to him. * * @param playerProvider the new {@link PlayerProvider}, can be set to null to apply the default provider */ public void setPlayerProvider(@Nullable PlayerProvider playerProvider) { this.playerProvider = playerProvider != null ? playerProvider : Player::new; } /** * Retrieves the current {@link PlayerProvider}, can be the default one if none is defined. * * @return the current {@link PlayerProvider} */ public @NotNull PlayerProvider getPlayerProvider() { return playerProvider; } /** * Gets the kick reason when the server is shutdown using {@link MinecraftServer#stopCleanly()}. * * @return the kick reason in case on a shutdown */ public @NotNull Component getShutdownText() { return shutdownText; } /** * Changes the kick reason in case of a shutdown. * * @param shutdownText the new shutdown kick reason * @see #getShutdownText() */ public void setShutdownText(@NotNull Component shutdownText) { this.shutdownText = shutdownText; } public synchronized void registerPlayer(@NotNull Player player) { this.players.add(player); this.connectionPlayerMap.put(player.getPlayerConnection(), player); } /** * Removes a {@link Player} from the players list. *

* Used during disconnection, you shouldn't have to do it manually. * * @param connection the player connection * @see PlayerConnection#disconnect() to properly disconnect a player */ public synchronized void removePlayer(@NotNull PlayerConnection connection) { final Player player = this.connectionPlayerMap.remove(connection); if (player == null) return; this.players.remove(player); } /** * Calls the player initialization callbacks and the event {@link AsyncPlayerPreLoginEvent}. *

* Sends a {@link LoginSuccessPacket} if successful (not kicked) * and change the connection state to {@link ConnectionState#PLAY}. * * @param player the player * @param register true to register the newly created player in {@link ConnectionManager} lists */ public CompletableFuture startPlayState(@NotNull Player player, boolean register) { return AsyncUtils.runAsync(() -> { final PlayerConnection playerConnection = player.getPlayerConnection(); // Call pre login event AsyncPlayerPreLoginEvent asyncPlayerPreLoginEvent = new AsyncPlayerPreLoginEvent(player); EventDispatcher.call(asyncPlayerPreLoginEvent); // Close the player channel if he has been disconnected (kick) if (!player.isOnline()) { playerConnection.flush(); //playerConnection.disconnect(); return; } // Change UUID/Username based on the event { final String eventUsername = asyncPlayerPreLoginEvent.getUsername(); final UUID eventUuid = asyncPlayerPreLoginEvent.getPlayerUuid(); if (!player.getUsername().equals(eventUsername)) { player.setUsernameField(eventUsername); } if (!player.getUuid().equals(eventUuid)) { player.setUuid(eventUuid); } } // Send login success packet LoginSuccessPacket loginSuccessPacket = new LoginSuccessPacket(player.getUuid(), player.getUsername()); if (playerConnection instanceof PlayerSocketConnection socketConnection) { socketConnection.writeAndFlush(loginSuccessPacket); } else { playerConnection.sendPacket(loginSuccessPacket); } playerConnection.setConnectionState(ConnectionState.PLAY); if (register) registerPlayer(player); this.waitingPlayers.relaxedOffer(player); }); } /** * Creates a {@link Player} using the defined {@link PlayerProvider} * and execute {@link #startPlayState(Player, boolean)}. * * @return the newly created player object * @see #startPlayState(Player, boolean) */ public @NotNull Player startPlayState(@NotNull PlayerConnection connection, @NotNull UUID uuid, @NotNull String username, boolean register) { final Player player = playerProvider.createPlayer(uuid, username, connection); startPlayState(player, register); return player; } /** * Shutdowns the connection manager by kicking all the currently connected players. */ public synchronized void shutdown() { DisconnectPacket disconnectPacket = new DisconnectPacket(shutdownText); for (Player player : getOnlinePlayers()) { final PlayerConnection playerConnection = player.getPlayerConnection(); playerConnection.sendPacket(disconnectPacket); playerConnection.flush(); player.remove(); playerConnection.disconnect(); } this.players.clear(); this.connectionPlayerMap.clear(); } /** * Connects waiting players. */ public void updateWaitingPlayers() { this.waitingPlayers.drain(waitingPlayer -> { PlayerLoginEvent loginEvent = new PlayerLoginEvent(waitingPlayer); EventDispatcher.call(loginEvent); final Instance spawningInstance = loginEvent.getSpawningInstance(); Check.notNull(spawningInstance, "You need to specify a spawning instance in the PlayerLoginEvent"); // Spawn the player at Player#getRespawnPoint if (DebugUtils.INSIDE_TEST) { // Required to get the exact moment the player spawns waitingPlayer.UNSAFE_init(spawningInstance).join(); } else { waitingPlayer.UNSAFE_init(spawningInstance); } }); } /** * Updates keep alive by checking the last keep alive packet and send a new one if needed. * * @param tickStart the time of the update in milliseconds, forwarded to the packet */ public void handleKeepAlive(long tickStart) { final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(tickStart); for (Player player : getOnlinePlayers()) { final long lastKeepAlive = tickStart - player.getLastKeepAlive(); if (lastKeepAlive > KEEP_ALIVE_DELAY && player.didAnswerKeepAlive()) { final PlayerConnection playerConnection = player.getPlayerConnection(); player.refreshKeepAlive(tickStart); playerConnection.sendPacket(keepAlivePacket); } else if (lastKeepAlive >= KEEP_ALIVE_KICK) { player.kick(TIMEOUT_TEXT); } } } }