feat: cleanup, remove ConnectionManager#getOnlinePlayers

This commit is contained in:
mworzala 2023-11-28 18:48:00 +02:00 committed by Matt Worzala
parent 50b868229a
commit 8a75e9e986
24 changed files with 206 additions and 222 deletions

View File

@ -177,8 +177,7 @@ public class PlayerInit {
BenchmarkManager benchmarkManager = MinecraftServer.getBenchmarkManager();
MinecraftServer.getSchedulerManager().buildTask(() -> {
Collection<Player> players = MinecraftServer.getConnectionManager().getOnlinePlayers();
if (players.isEmpty())
if (MinecraftServer.getConnectionManager().getOnlinePlayerCount() != 0)
return;
long ramUsage = benchmarkManager.getUsedMemory();

View File

@ -9,7 +9,7 @@ public class ConfigCommand extends Command {
setDefaultExecutor((sender, context) -> {
if (!(sender instanceof Player player)) return;
player.startConfigurationPhase2();
player.startConfigurationPhase();
});
}
}

View File

@ -6,8 +6,10 @@ import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionState;
import java.util.Collection;
import java.util.List;
public class PlayersCommand extends Command {
@ -17,20 +19,20 @@ public class PlayersCommand extends Command {
}
private void usage(CommandSender sender, CommandContext context) {
final Collection<Player> players = MinecraftServer.getConnectionManager().getOnlinePlayers();
final var players = List.copyOf(MinecraftServer.getConnectionManager().getPlayers());
final int playerCount = players.size();
sender.sendMessage(Component.text("Total players: " + playerCount));
final int limit = 15;
if (playerCount <= limit) {
for (final Player player : players) {
sender.sendMessage(Component.text(player.getUsername()));
}
} else {
for (final Player player : players.stream().limit(limit).toList()) {
sender.sendMessage(Component.text(player.getUsername()));
}
sender.sendMessage(Component.text("..."));
for (int i = 0; i < Math.min(limit, playerCount); i++) {
final var player = players.get(i);
var msg = Component.text(player.getUsername());
if (player.getPlayerConnection().getServerState() == ConnectionState.CONFIGURATION)
msg = msg.append(Component.text(" (config)"));
sender.sendMessage(msg);
}
if (playerCount > limit) sender.sendMessage(Component.text("..."));
}
}

View File

@ -103,7 +103,7 @@ public final class MinecraftServer {
*/
public static void setBrandName(@NotNull String brandName) {
MinecraftServer.brandName = brandName;
PacketUtils.broadcastPacket(PluginMessagePacket.getBrandPacket());
PacketUtils.broadcastPlayPacket(PluginMessagePacket.getBrandPacket());
}
/**
@ -123,7 +123,7 @@ public final class MinecraftServer {
*/
public static void setDifficulty(@NotNull Difficulty difficulty) {
MinecraftServer.difficulty = difficulty;
PacketUtils.broadcastPacket(new ServerDifficultyPacket(difficulty, true));
PacketUtils.broadcastPlayPacket(new ServerDifficultyPacket(difficulty, true));
}
@ApiStatus.Experimental

View File

@ -5,6 +5,7 @@ import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.Keyed;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionState;
import org.jetbrains.annotations.NotNull;
import java.util.function.Predicate;
@ -41,7 +42,7 @@ public class Audiences {
* @return all audience members
*/
public static @NotNull Audience all() {
return Audience.audience(audience.server, audience.customs());
return Audience.audience(audience.server(), audience.customs());
}
/**
@ -50,7 +51,7 @@ public class Audiences {
* @return all players
*/
public static @NotNull Audience players() {
return audience.players;
return audience.players();
}
/**
@ -60,7 +61,8 @@ public class Audiences {
* @return all players matching the predicate
*/
public static @NotNull Audience players(@NotNull Predicate<Player> filter) {
return PacketGroupingAudience.of(MinecraftServer.getConnectionManager().getOnlinePlayers().stream().filter(filter).toList());
return PacketGroupingAudience.of(MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY)
.stream().filter(filter).toList());
}
/**
@ -78,7 +80,7 @@ public class Audiences {
* @return the audience of all players and the console
*/
public static @NotNull Audience server() {
return audience.server;
return audience.server();
}
/**

View File

@ -5,6 +5,7 @@ import net.kyori.adventure.key.Key;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.ConsoleSender;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionState;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@ -35,12 +36,12 @@ class IterableAudienceProvider implements AudienceProvider<Iterable<? extends Au
@Override
public @NotNull Iterable<? extends Audience> players() {
return MinecraftServer.getConnectionManager().getOnlinePlayers();
return MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY);
}
@Override
public @NotNull Iterable<? extends Audience> players(@NotNull Predicate<Player> filter) {
return MinecraftServer.getConnectionManager().getOnlinePlayers().stream().filter(filter).toList();
return MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY).stream().filter(filter).toList();
}
@Override

View File

@ -4,6 +4,7 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.key.Key;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionState;
import org.jetbrains.annotations.NotNull;
import java.util.function.Predicate;
@ -15,8 +16,6 @@ import java.util.function.Predicate;
class SingleAudienceProvider implements AudienceProvider<Audience> {
protected final IterableAudienceProvider collection = new IterableAudienceProvider();
protected final Audience players = PacketGroupingAudience.of(MinecraftServer.getConnectionManager().getOnlinePlayers());
protected final Audience server = Audience.audience(this.players, MinecraftServer.getCommandManager().getConsoleSender());
protected SingleAudienceProvider() {
}
@ -32,17 +31,18 @@ class SingleAudienceProvider implements AudienceProvider<Audience> {
@Override
public @NotNull Audience all() {
return Audience.audience(this.server, this.customs());
return Audience.audience(this.server(), this.customs());
}
@Override
public @NotNull Audience players() {
return this.players;
return PacketGroupingAudience.of(MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY));
}
@Override
public @NotNull Audience players(@NotNull Predicate<Player> filter) {
return PacketGroupingAudience.of(MinecraftServer.getConnectionManager().getOnlinePlayers().stream().filter(filter).toList());
return PacketGroupingAudience.of(MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY)
.stream().filter(filter).toList());
}
@Override
@ -52,7 +52,7 @@ class SingleAudienceProvider implements AudienceProvider<Audience> {
@Override
public @NotNull Audience server() {
return this.server;
return Audience.audience(players(), MinecraftServer.getCommandManager().getConsoleSender());
}
@Override

View File

@ -57,12 +57,9 @@ import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.PlayerProvider;
import net.minestom.server.network.packet.client.ClientPacket;
import net.minestom.server.network.packet.server.CachedPacket;
import net.minestom.server.network.packet.server.SendablePacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.network.packet.server.common.*;
import net.minestom.server.network.packet.server.configuration.FinishConfigurationPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.packet.server.play.*;
import net.minestom.server.network.packet.server.play.data.DeathLocation;
@ -134,6 +131,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
private Component displayName;
private PlayerSkin skin;
private Instance pendingInstance = null;
private DimensionType dimensionType;
private GameMode gameMode;
private DeathLocation deathLocation;
@ -254,17 +252,25 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
metadata.setNotifyAboutChanges(false);
}
@ApiStatus.Internal
public void setPendingInstance(@NotNull Instance pendingInstance) {
// I(mattw) am not a big fan of this function, but somehow we need to store
// the instance and i didn't like a record in ConnectionManager either.
this.pendingInstance = pendingInstance;
}
/**
* Used when the player is created.
* Init the player and spawn him.
* <p>
* WARNING: executed in the main update thread
* UNSAFE: Only meant to be used when a socket player connects through the server.
*
* @param spawnInstance the player spawn instance (defined in {@link AsyncPlayerConfigurationEvent})
*/
@ApiStatus.Internal
public CompletableFuture<Void> UNSAFE_init(@NotNull Instance spawnInstance) {
public CompletableFuture<Void> UNSAFE_init() {
final Instance spawnInstance = this.pendingInstance;
this.pendingInstance = null;
this.removed = false;
this.dimensionType = spawnInstance.getDimensionType();
@ -297,9 +303,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
EventDispatcher.call(skinInitEvent);
this.skin = skinInitEvent.getSkin();
// FIXME: when using Geyser, this line remove the skin of the client
PacketUtils.broadcastPacket(getAddPlayerToList());
PacketUtils.broadcastPlayPacket(getAddPlayerToList());
for (var player : MinecraftServer.getConnectionManager().getOnlinePlayers()) {
var connectionManager = MinecraftServer.getConnectionManager();
for (var player : connectionManager.getPlayers(ConnectionState.PLAY)) {
if (player != this) {
sendPacket(player.getAddPlayerToList());
if (player.displayName != null) {
@ -356,63 +363,15 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* <p>This will result in them being removed from the current instance, player list, etc.</p>
*/
public void startConfigurationPhase() {
boolean isFirstConfig = true;
Check.stateCondition(playerConnection.getClientState() != ConnectionState.PLAY,
"Player must be in the play state for reconfiguration.");
// If the player is currently in the play state, we need to put them back in configuration.
if (playerConnection.getClientState() == ConnectionState.PLAY) {
remove(false);
sendPacket(new StartConfigurationPacket());
} else {
// Sanity check that they are already in configuration.
// On first join, they will already be in the config state when this is called.
assert playerConnection.getClientState() == ConnectionState.CONFIGURATION;
}
// Remove the player, then send them back to configuration
remove(false);
// Call the event and spawn the player.
AsyncUtils.runAsync(() -> {
System.out.println("CALL EVENT");
var event = new AsyncPlayerConfigurationEvent(this, isFirstConfig);
EventDispatcher.call(event);
var connectionManager = MinecraftServer.getConnectionManager();
connectionManager.transitionPlayToConfig(this);
System.out.println("FINISHED EVENT");
final Instance spawningInstance = event.getSpawningInstance();
Check.notNull(spawningInstance, "You need to specify a spawning instance in the AsyncPlayerConfigurationEvent");
MinecraftServer.getConnectionManager().startPlayState(this, event.getSpawningInstance());
sendPacket(new FinishConfigurationPacket());
System.out.println("SENT CHANGE PACKET");
});
}
public void startConfigurationPhase2() {
boolean isFirstConfig = true;
// If the player is currently in the play state, we need to put them back in configuration.
if (playerConnection.getClientState() == ConnectionState.PLAY) {
remove(false);
System.out.println("SEND REENTER");
MinecraftServer.getConnectionManager().transitionPlayToConfig(this);
sendPacket(new StartConfigurationPacket());
} else {
// Sanity check that they are already in configuration.
// On first join, they will already be in the config state when this is called.
assert playerConnection.getClientState() == ConnectionState.CONFIGURATION;
}
// Call the event and spawn the player.
// AsyncUtils.runAsync(() -> {
// System.out.println("CALL EVENT");
// var event = new AsyncPlayerConfigurationEvent(this, isFirstConfig);
// EventDispatcher.call(event);
//
// System.out.println("FINISHED EVENT");
// final Instance spawningInstance = event.getSpawningInstance();
// Check.notNull(spawningInstance, "You need to specify a spawning instance in the AsyncPlayerConfigurationEvent");
//
// MinecraftServer.getConnectionManager().startPlayState(this, event.getSpawningInstance());
// sendPacket(new FinishConfigurationPacket());
// System.out.println("SENT CHANGE PACKET");
// });
}
/**
@ -427,6 +386,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
public void update(long time) {
// Process received packets
interpretPacketQueue();
// It is possible to be removed during packet processing, if thats the case exit immediately.
if (isRemoved()) return;
super.update(time); // Super update (item pickup/fire management)
@ -626,7 +586,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Clear all viewable chunks
ChunkUtils.forChunksInRange(chunkX, chunkZ, MinecraftServer.getChunkViewDistance(), chunkRemover);
// Remove from the tab-list
PacketUtils.broadcastPacket(getRemovePlayerToList());
PacketUtils.broadcastPlayPacket(getRemovePlayerToList());
// Prevent the player from being stuck in loading screen, or just unable to interact with the server
// This should be considered as a bug, since the player will ultimately time out anyway.
@ -1126,7 +1086,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
*/
public void setDisplayName(@Nullable Component displayName) {
this.displayName = displayName;
PacketUtils.broadcastPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, infoEntry()));
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, infoEntry()));
}
/**
@ -1168,11 +1128,11 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
{
// Remove player
PacketUtils.broadcastPacket(removePlayerPacket);
PacketUtils.broadcastPlayPacket(removePlayerPacket);
sendPacketToViewers(destroyEntitiesPacket);
// Show player again
PacketUtils.broadcastPacket(addPlayerPacket);
PacketUtils.broadcastPlayPacket(addPlayerPacket);
getViewers().forEach(player -> showPlayer(player.getPlayerConnection()));
}
@ -1507,7 +1467,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
// Condition to prevent sending the packets before spawning the player
if (isActive()) {
sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.CHANGE_GAMEMODE, gameMode.id()));
PacketUtils.broadcastPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, infoEntry()));
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, infoEntry()));
}
// The client updates their abilities based on the GameMode as follows
@ -2004,7 +1964,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
public void refreshLatency(int latency) {
this.latency = latency;
if (getPlayerConnection().getServerState() == ConnectionState.PLAY) {
PacketUtils.broadcastPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LATENCY, infoEntry()));
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LATENCY, infoEntry()));
}
}

View File

@ -67,7 +67,8 @@ public class FakePlayer extends Player implements NavigableEntity {
}).build();
MinecraftServer.getGlobalEventHandler().addListener(spawnListener);
}
CONNECTION_MANAGER.startConfigurationState(this);
CONNECTION_MANAGER.transitionLoginToConfig(this);
}
/**

View File

@ -21,7 +21,7 @@ public class BasicQueryResponse implements Writeable {
this.motd = "A Minestom Server";
this.gametype = "SMP";
this.map = "world";
this.numPlayers = String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size());
this.numPlayers = String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayerCount());
this.maxPlayers = String.valueOf(Integer.parseInt(this.numPlayers) + 1);
}

View File

@ -1,8 +1,10 @@
package net.minestom.server.extras.query.response;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extras.query.Query;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.binary.Writeable;
import org.jetbrains.annotations.NotNull;
@ -13,7 +15,7 @@ import java.util.*;
* A full query response containing a dynamic set of responses.
*/
public class FullQueryResponse implements Writeable {
private static final PlainComponentSerializer PLAIN = PlainComponentSerializer.plain();
private static final PlainTextComponentSerializer PLAIN = PlainTextComponentSerializer.plainText();
private static final byte[] PADDING_10 = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
PADDING_11 = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
@ -31,7 +33,7 @@ public class FullQueryResponse implements Writeable {
this.kv.put(key.getKey(), key.getValue());
}
this.players = MinecraftServer.getConnectionManager().getOnlinePlayers()
this.players = MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY)
.stream()
.map(player -> PLAIN.serialize(player.getName()))
.toList();

View File

@ -1,6 +1,7 @@
package net.minestom.server.extras.query.response;
import net.minestom.server.MinecraftServer;
import net.minestom.server.network.ConnectionState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -18,8 +19,8 @@ public enum QueryKey {
VERSION(() -> MinecraftServer.VERSION_NAME),
PLUGINS(FullQueryResponse::generatePluginsValue),
MAP(() -> "world"),
NUM_PLAYERS("numplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size())),
MAX_PLAYERS("maxplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size() + 1)),
NUM_PLAYERS("numplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayerCount())),
MAX_PLAYERS("maxplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayerCount() + 1)),
HOST_PORT("hostport", () -> String.valueOf(MinecraftServer.getServer().getPort())),
HOST_IP("hostip", () -> Objects.requireNonNullElse(MinecraftServer.getServer().getAddress(), "localhost"));

View File

@ -10,6 +10,7 @@ import net.minestom.server.event.player.PlayerChatEvent;
import net.minestom.server.message.ChatPosition;
import net.minestom.server.message.Messenger;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.packet.client.play.ClientChatMessagePacket;
import net.minestom.server.network.packet.client.play.ClientCommandChatPacket;
import org.jetbrains.annotations.NotNull;
@ -38,7 +39,7 @@ public class ChatMessageListener {
return;
}
final Collection<Player> players = CONNECTION_MANAGER.getOnlinePlayers();
final Collection<Player> players = CONNECTION_MANAGER.getPlayers(ConnectionState.PLAY);
PlayerChatEvent playerChatEvent = new PlayerChatEvent(player, players, () -> buildDefaultChatMessage(player, message), message);
// Call the event

View File

@ -1,23 +1,17 @@
package net.minestom.server.listener;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.packet.client.play.ClientConfigurationAckPacket;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ForkJoinPool;
public class PlayConfigListener {
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
public static void configAckListener(@NotNull ClientConfigurationAckPacket packet, @NotNull Player player) {
// player.startConfigurationPhase();
System.out.println("PLAYER HAS REENTERED CONFIGURATION PHASE!!!!!" + player.getUsername());
ForkJoinPool.commonPool().execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
player.startConfigurationPhase();
});
CONNECTION_MANAGER.transitionConfigToPlay(player, false);
}
}

View File

@ -9,7 +9,6 @@ public final class KeepAliveListener {
private static final Component KICK_MESSAGE = Component.text("Bad Keep Alive packet", NamedTextColor.RED);
public static void listener(ClientKeepAlivePacket packet, Player player) {
System.out.println("KEEP ALIVE LISTENER");
final long packetId = packet.id();
if (packetId != player.getLastKeepAlive()) {
player.kick(KICK_MESSAGE);

View File

@ -89,7 +89,7 @@ public final class LoginListener {
final UUID playerUuid = bungee && isSocketConnection ?
((PlayerSocketConnection) connection).gameProfile().uuid() :
CONNECTION_MANAGER.getPlayerConnectionUuid(connection, packet.username());
CONNECTION_MANAGER.startConfigurationState(connection, playerUuid, packet.username());
CONNECTION_MANAGER.createPlayer(connection, playerUuid, packet.username());
}
}
@ -155,7 +155,7 @@ public final class LoginListener {
final String profileName = gameProfile.get("name").getAsString();
MinecraftServer.LOGGER.info("UUID of player {} is {}", loginUsername, profileUUID);
CONNECTION_MANAGER.startConfigurationState(connection, profileUUID, profileName);
CONNECTION_MANAGER.createPlayer(connection, profileUUID, profileName);
List<GameProfile.Property> propertyList = new ArrayList<>();
for (JsonElement element : gameProfile.get("properties").getAsJsonArray()) {
JsonObject object = element.getAsJsonObject();
@ -208,7 +208,7 @@ public final class LoginListener {
if (success) {
socketConnection.setRemoteAddress(socketAddress);
socketConnection.UNSAFE_setProfile(gameProfile);
CONNECTION_MANAGER.startConfigurationState(connection, gameProfile.uuid(), gameProfile.name());
CONNECTION_MANAGER.createPlayer(connection, gameProfile.uuid(), gameProfile.name());
} else {
LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE);
socketConnection.sendPacket(disconnectPacket);
@ -219,7 +219,6 @@ public final class LoginListener {
public static void loginAckListener(@NotNull ClientLoginAcknowledgedPacket ignored, @NotNull PlayerConnection connection) {
final Player player = Objects.requireNonNull(connection.getPlayer());
CONNECTION_MANAGER.registerPlayer(player);
// Registry data
var registry = new HashMap<String, NBT>();
@ -236,7 +235,7 @@ public final class LoginListener {
connection.sendPacket(PluginMessagePacket.getBrandPacket());
// Enter configuration phase (for the first time)
player.startConfigurationPhase();
CONNECTION_MANAGER.transitionConfigToPlay(player, true);
}
}

View File

@ -5,16 +5,20 @@ 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.AsyncPlayerConfigurationEvent;
import net.minestom.server.event.player.AsyncPlayerPreLoginEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.client.login.ClientLoginStartPacket;
import net.minestom.server.network.packet.server.common.KeepAlivePacket;
import net.minestom.server.network.packet.server.configuration.FinishConfigurationPacket;
import net.minestom.server.network.packet.server.login.LoginSuccessPacket;
import net.minestom.server.network.packet.server.play.StartConfigurationPacket;
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.ApiStatus;
@ -35,16 +39,16 @@ public final class ConnectionManager {
private static final long KEEP_ALIVE_KICK = 30_000;
private static final Component TIMEOUT_TEXT = Component.text("Timeout", NamedTextColor.RED);
private record PendingPlayer(@NotNull Player player, @NotNull Instance instance) {
}
private final MessagePassingQueue<PendingPlayer> waitingPlayers = new MpscUnboundedArrayQueue<>(64);
private final Set<Player> configurationPlayers = new CopyOnWriteArraySet<>();
private final Set<Player> players = new CopyOnWriteArraySet<>();
private final Set<Player> unmodifiablePlayers = Collections.unmodifiableSet(players);
// All players once their Player object has been instantiated.
private final Map<PlayerConnection, Player> connectionPlayerMap = new ConcurrentHashMap<>();
// Players waiting to be spawned (post configuration state)
private final MessagePassingQueue<Player> waitingPlayers = new MpscUnboundedArrayQueue<>(64);
// Players in configuration state
private final Set<Player> configurationPlayers = new CopyOnWriteArraySet<>();
// Players in play state
private final Set<Player> playPlayers = new CopyOnWriteArraySet<>();
// The uuid provider once a player login
private volatile UuidProvider uuidProvider = (playerConnection, username) -> UUID.randomUUID();
@ -52,27 +56,44 @@ public final class ConnectionManager {
private volatile PlayerProvider playerProvider = Player::new;
/**
* Gets all online players.
* Gets the number of "online" players, eg for the query response.
*
* @return an unmodifiable collection containing all the online players
* @apiNote Only includes players in the play state, not players in configuration.
*/
public @NotNull Collection<@NotNull Player> getOnlinePlayers() {
return unmodifiablePlayers;
public int getOnlinePlayerCount() {
return playPlayers.size();
}
/**
* Gets online players filtered by state.
* Gets players filtered by state.
*
* <p>States before login are not considered online, and will result in nothing being returned.</p>
* @param states The state(s) to return players, if empty all players (play and config) are returned.
* <b>Only</b> {@link ConnectionState#CONFIGURATION} and {@link ConnectionState#PLAY} are valid.
*
* @return An unmodifiable collection containing the filtered players.
* @apiNote The returned collection has no defined update behavior relative to the state of the server,
* so it should be refetched whenever used, rather than kept and reused.
*/
public @NotNull Collection<@NotNull Player> getPlayers(@NotNull ConnectionState... state) {
// connectionPlayerMap.values().stream()
// .filter()
return List.of();
public @NotNull Collection<@NotNull Player> getPlayers(@NotNull ConnectionState... states) {
boolean play = false, config = false;
for (var state : states) {
switch (state) {
case PLAY -> play = true;
case CONFIGURATION -> config = true;
default -> throw new IllegalArgumentException("Cannot fetch players in " + state + "!");
}
}
if (config && !play) { // Only play
return Collections.unmodifiableCollection(configurationPlayers);
} else if (!config && play) { // Only configuration
return Collections.unmodifiableCollection(playPlayers);
} else { // Both or neither was specified
final var players = new ArrayList<Player>(playPlayers.size() + configurationPlayers.size());
players.addAll(configurationPlayers);
players.addAll(playPlayers);
return Collections.unmodifiableCollection(players);
}
}
/**
@ -90,11 +111,13 @@ public final class ConnectionManager {
* <p>
* This can cause issue if two or more players have the same username.
*
* @param username the player username (ignoreCase)
* @param username the player username (case-insensitive)
* @param states The state(s) to return players, if empty all players (play and config) are returned.
* <b>Only</b> {@link ConnectionState#CONFIGURATION} and {@link ConnectionState#PLAY} are valid.
* @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()) {
public @Nullable Player getPlayer(@NotNull String username, @NotNull ConnectionState... states) {
for (Player player : getPlayers(states)) {
if (player.getUsername().equalsIgnoreCase(username))
return player;
}
@ -107,10 +130,12 @@ public final class ConnectionManager {
* This can cause issue if two or more players have the same UUID.
*
* @param uuid the player UUID
* @param states The state(s) to return players, if empty all players (play and config) are returned.
* <b>Only</b> {@link ConnectionState#CONFIGURATION} and {@link ConnectionState#PLAY} are valid.
* @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()) {
public @Nullable Player getPlayer(@NotNull UUID uuid, @NotNull ConnectionState... states) {
for (Player player : getPlayers(states)) {
if (player.getUuid().equals(uuid))
return player;
}
@ -121,9 +146,11 @@ public final class ConnectionManager {
* Finds the closest player matching a given username.
*
* @param username the player username (can be partial)
* @param states The state(s) to return players, if empty all players (play and config) are returned.
* <b>Only</b> {@link ConnectionState#CONFIGURATION} and {@link ConnectionState#PLAY} are valid.
* @return the closest match, null if no players are online
*/
public @Nullable Player findPlayer(@NotNull String username) {
public @Nullable Player findPlayer(@NotNull String username, @NotNull ConnectionState... states) {
Player exact = getPlayer(username);
if (exact != null) return exact;
final String username1 = username.toLowerCase(Locale.ROOT);
@ -132,8 +159,7 @@ public final class ConnectionManager {
final String username2 = player.getUsername().toLowerCase(Locale.ROOT);
return StringUtils.jaroWinklerScore(username1, username2);
};
return getOnlinePlayers()
.stream()
return getPlayers(states).stream()
.min(Comparator.comparingDouble(distanceFunction::apply))
.filter(player -> distanceFunction.apply(player) > 0)
.orElse(null);
@ -186,73 +212,35 @@ public final class ConnectionManager {
return playerProvider;
}
//todo REDONE TRANSITIONS
@ApiStatus.Internal
public void transitionLoginToConfig(@NotNull Player player) {
configurationPlayers.add(player);
}
@ApiStatus.Internal
public void transitionConfigToPlay(@NotNull Player player) {
}
@ApiStatus.Internal
public void transitionPlayToConfig(@NotNull Player player) {
configurationPlayers.add(player);
}
/**
* Adds a {@link Player} to the players list.
* <p>
* Used during connection, you shouldn't have to do it manually.
*
* @param player the player
* Creates a player object and begins the transition from the login state to the config state.
*/
@ApiStatus.Internal
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.
* <p>
* Used during disconnection, you shouldn't have to do it manually.
*
* @param connection the player connection
* @see PlayerConnection#disconnect() to properly disconnect a player
*/
@ApiStatus.Internal
public synchronized void removePlayer(@NotNull PlayerConnection connection) {
final Player player = this.connectionPlayerMap.remove(connection);
if (player == null) return;
this.players.remove(player);
}
@ApiStatus.Internal
public @NotNull Player startConfigurationState(@NotNull PlayerConnection connection,
@NotNull UUID uuid, @NotNull String username) {
public @NotNull Player createPlayer(@NotNull PlayerConnection connection, @NotNull UUID uuid, @NotNull String username) {
final Player player = playerProvider.createPlayer(uuid, username, connection);
startConfigurationState(player);
this.connectionPlayerMap.put(connection, player);
transitionLoginToConfig(player);
return player;
}
@ApiStatus.Internal
public CompletableFuture<Void> startConfigurationState(@NotNull Player player) {
return AsyncUtils.runAsync(() -> {
public void transitionLoginToConfig(@NotNull Player player) {
AsyncUtils.runAsync(() -> {
final PlayerConnection playerConnection = player.getPlayerConnection();
// Compression
if (playerConnection instanceof PlayerSocketConnection socketConnection) {
final int threshold = MinecraftServer.getCompressionThreshold();
if (threshold > 0) socketConnection.startCompression();
}
// Call pre login event
AsyncPlayerPreLoginEvent asyncPlayerPreLoginEvent = new AsyncPlayerPreLoginEvent(player);
EventDispatcher.call(asyncPlayerPreLoginEvent);
if (!player.isOnline())
return; // Player has been kicked
// Change UUID/Username based on the event
{
final String eventUsername = asyncPlayerPreLoginEvent.getUsername();
@ -273,42 +261,74 @@ public final class ConnectionManager {
}
@ApiStatus.Internal
public void startPlayState(@NotNull Player player, @NotNull Instance instance) {
this.waitingPlayers.relaxedOffer(new PendingPlayer(player, instance));
public void transitionPlayToConfig(@NotNull Player player) {
player.sendPacket(new StartConfigurationPacket());
configurationPlayers.add(player);
}
@ApiStatus.Internal
public void transitionConfigToPlay(@NotNull Player player, boolean isFirstConfig) {
// Call the event and spawn the player.
AsyncUtils.runAsync(() -> {
var event = new AsyncPlayerConfigurationEvent(player, isFirstConfig);
EventDispatcher.call(event);
final Instance spawningInstance = event.getSpawningInstance();
Check.notNull(spawningInstance, "You need to specify a spawning instance in the AsyncPlayerConfigurationEvent");
player.setPendingInstance(spawningInstance);
this.waitingPlayers.relaxedOffer(player);
player.sendPacket(new FinishConfigurationPacket());
});
}
/**
* Removes a {@link Player} from the players list.
* <p>
* Used during disconnection, you shouldn't have to do it manually.
*
* @param connection the player connection
* @see PlayerConnection#disconnect() to properly disconnect a player
*/
@ApiStatus.Internal
public synchronized void removePlayer(@NotNull PlayerConnection connection) {
final Player player = this.connectionPlayerMap.remove(connection);
if (player == null) return;
this.configurationPlayers.remove(player);
this.playPlayers.remove(player);
}
/**
* Shutdowns the connection manager by kicking all the currently connected players.
*/
public synchronized void shutdown() {
this.players.clear();
this.configurationPlayers.clear();
this.playPlayers.clear();
this.connectionPlayerMap.clear();
}
public void tick(long tickStart) {
// Let waiting players into their instances
updateWaitingPlayers();
handleKeepAlive(tickStart);
// Send keep alive packets
handleKeepAlive(configurationPlayers, tickStart);
handleKeepAlive(playPlayers, tickStart);
// Interpret packets for configuration players
for (var player : configurationPlayers) {
// System.out.println("CONFIG INTERPRET: " + player.getUsername());
//todo we are required to do this because when we wait for the config ack packet we are not in an instance so not ticking.
// but then once we switch to config the packets must be eval-ed immediately because we are in an instance. What if we change
// the protocol state swap to happen immediately when the packet is read, not when its processed?
player.interpretPacketQueue();
}
configurationPlayers.forEach(Player::interpretPacketQueue);
}
/**
* Connects waiting players.
*/
private void updateWaitingPlayers() {
this.waitingPlayers.drain(pp -> {
configurationPlayers.remove(pp.player);
this.waitingPlayers.drain(player -> {
configurationPlayers.remove(player);
playPlayers.add(player);
// Spawn the player at Player#getRespawnPoint
CompletableFuture<Void> spawnFuture = pp.player.UNSAFE_init(pp.instance);
CompletableFuture<Void> spawnFuture = player.UNSAFE_init();
// Required to get the exact moment the player spawns
if (DebugUtils.INSIDE_TEST) spawnFuture.join();
@ -320,10 +340,9 @@ public final class ConnectionManager {
*
* @param tickStart the time of the update in milliseconds, forwarded to the packet
*/
private void handleKeepAlive(long tickStart) {
//todo need to send keep alives for config players also!!
private void handleKeepAlive(@NotNull Collection<Player> playerGroup, long tickStart) {
final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(tickStart);
for (Player player : getOnlinePlayers()) {
for (Player player : playerGroup) {
final long lastKeepAlive = tickStart - player.getLastKeepAlive();
if (lastKeepAlive > KEEP_ALIVE_DELAY && player.didAnswerKeepAlive()) {
player.refreshKeepAlive(tickStart);

View File

@ -53,10 +53,8 @@ public class PacketProcessor {
}
public ClientPacket process(@NotNull PlayerConnection connection, int packetId, ByteBuffer body) {
// System.out.println("READ: " + connection.getClientState() + " " + packetId);
final ClientPacket packet = create(connection.getClientState(), packetId, body);
final ConnectionState state = connection.getClientState();
// System.out.println("READ2: " + state + " " + packet.getClass().getSimpleName());
// If the packet intends to switch state, do it now.
// Since packets are processed next tick for players, we have to switch immediately.
@ -67,7 +65,9 @@ public class PacketProcessor {
}
switch (state) {
// Process all pre-config packets immediately
case HANDSHAKE, STATUS, LOGIN -> packetListenerManager.processClientPacket(state, packet, connection);
// Process config and play packets on the next tick
case CONFIGURATION, PLAY -> {
final Player player = connection.getPlayer();
assert player != null;

View File

@ -7,6 +7,8 @@ import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.utils.identity.NamedAndIdentified;
import org.jetbrains.annotations.NotNull;
@ -37,7 +39,7 @@ public class ResponseData {
this.entries = new ArrayList<>();
this.version = MinecraftServer.VERSION_NAME;
this.protocol = MinecraftServer.PROTOCOL_VERSION;
this.online = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
this.online = MinecraftServer.getConnectionManager().getOnlinePlayerCount();
this.maxPlayer = this.online + 1;
this.description = DEFAULT_DESCRIPTION;
this.favicon = "";

View File

@ -128,7 +128,7 @@ public class Team implements PacketGroupingAudience {
final TeamsPacket addPlayerPacket = new TeamsPacket(teamName,
new TeamsPacket.AddEntitiesToTeamAction(toAdd));
// Sends to all online players the add player packet
PacketUtils.broadcastPacket(addPlayerPacket);
PacketUtils.broadcastPlayPacket(addPlayerPacket);
// invalidate player members
this.isPlayerMembersUpToDate = false;
@ -159,7 +159,7 @@ public class Team implements PacketGroupingAudience {
final TeamsPacket removePlayerPacket = new TeamsPacket(teamName,
new TeamsPacket.RemoveEntitiesToTeamAction(toRemove));
// Sends to all online player the remove player packet
PacketUtils.broadcastPacket(removePlayerPacket);
PacketUtils.broadcastPlayPacket(removePlayerPacket);
// Removes the member from the team
this.members.removeAll(toRemove);
@ -463,7 +463,7 @@ public class Team implements PacketGroupingAudience {
public void sendUpdatePacket() {
final var info = new TeamsPacket.UpdateTeamAction(teamDisplayName, friendlyFlags,
nameTagVisibility, collisionRule, teamColor, prefix, suffix);
PacketUtils.broadcastPacket(new TeamsPacket(teamName, info));
PacketUtils.broadcastPlayPacket(new TeamsPacket(teamName, info));
}
@Override

View File

@ -37,7 +37,7 @@ public final class TeamManager {
*/
protected void registerNewTeam(@NotNull Team team) {
this.teams.add(team);
PacketUtils.broadcastPacket(team.createTeamsCreationPacket());
PacketUtils.broadcastPlayPacket(team.createTeamsCreationPacket());
}
/**
@ -60,7 +60,7 @@ public final class TeamManager {
*/
public boolean deleteTeam(@NotNull Team team) {
// Sends to all online players a team destroy packet
PacketUtils.broadcastPacket(team.createTeamDestructionPacket());
PacketUtils.broadcastPlayPacket(team.createTeamDestructionPacket());
return this.teams.remove(team);
}

View File

@ -113,7 +113,7 @@ public final class PacketUtils {
* Checks if the {@link ServerPacket} is suitable to be wrapped into a {@link CachedPacket}.
* Note: {@link ComponentHoldingServerPacket}s are not translated inside a {@link CachedPacket}.
*
* @see CachedPacket#body()
* @see CachedPacket#body(ConnectionState)
* @see PlayerSocketConnection#writePacketSync(SendablePacket, boolean)
*/
static boolean shouldUseCachePacket(final @NotNull ServerPacket packet) {
@ -153,8 +153,8 @@ public final class PacketUtils {
sendGroupedPacket(players, packet, player -> true);
}
public static void broadcastPacket(@NotNull ServerPacket packet) {
sendGroupedPacket(MinecraftServer.getConnectionManager().getOnlinePlayers(), packet);
public static void broadcastPlayPacket(@NotNull ServerPacket packet) {
sendGroupedPacket(MinecraftServer.getConnectionManager().getPlayers(ConnectionState.PLAY), packet);
}
@ApiStatus.Experimental

View File

@ -11,6 +11,8 @@ import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.math.IntRange;
import net.minestom.server.utils.validate.Check;
@ -27,6 +29,7 @@ import java.util.concurrent.ThreadLocalRandom;
* It is based on the target selectors used in commands.
*/
public class EntityFinder {
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private TargetSelector targetSelector;
@ -307,8 +310,7 @@ public class EntityFinder {
private static @NotNull List<@NotNull Entity> findTarget(@Nullable Instance instance,
@NotNull TargetSelector targetSelector,
@NotNull Point startPosition, @Nullable Entity self) {
final var players = instance != null ?
instance.getPlayers() : MinecraftServer.getConnectionManager().getOnlinePlayers();
final var players = instance != null ? instance.getPlayers() : CONNECTION_MANAGER.getPlayers(ConnectionState.PLAY);
if (targetSelector == TargetSelector.NEAREST_PLAYER) {
return players.stream()
.min(Comparator.comparingDouble(p -> p.getPosition().distanceSquared(startPosition)))

View File

@ -40,7 +40,7 @@ final class TestConnectionImpl implements TestConnection {
event.getPlayer().setRespawnPoint(pos);
});
return process.connection().startConfigurationState(player, true)
return process.connection().createPlayer(player, true)
.thenApply(unused -> {
process.connection().updateWaitingPlayers();
return player;