diff --git a/demo/src/main/java/net/minestom/demo/Main.java b/demo/src/main/java/net/minestom/demo/Main.java index 2e89c5b2c..66d014c5b 100644 --- a/demo/src/main/java/net/minestom/demo/Main.java +++ b/demo/src/main/java/net/minestom/demo/Main.java @@ -63,6 +63,7 @@ public class Main { commandManager.register(new DisplayCommand()); commandManager.register(new NotificationCommand()); commandManager.register(new TestCommand2()); + commandManager.register(new ConfigCommand()); commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED))); @@ -108,10 +109,10 @@ public class Main { OptifineSupport.enable(); - VelocityProxy.enable("abc"); +// VelocityProxy.enable("abc"); //BungeeCordProxy.enable(); - //MojangAuth.init();c + //MojangAuth.init(); // useful for testing - we don't need to worry about event calls so just set this to a long time OpenToLAN.open(new OpenToLANConfig().eventCallDelay(Duration.of(1, TimeUnit.DAY))); diff --git a/demo/src/main/java/net/minestom/demo/commands/ConfigCommand.java b/demo/src/main/java/net/minestom/demo/commands/ConfigCommand.java new file mode 100644 index 000000000..4be2adece --- /dev/null +++ b/demo/src/main/java/net/minestom/demo/commands/ConfigCommand.java @@ -0,0 +1,15 @@ +package net.minestom.demo.commands; + +import net.minestom.server.command.builder.Command; +import net.minestom.server.entity.Player; + +public class ConfigCommand extends Command { + public ConfigCommand() { + super("config"); + + setDefaultExecutor((sender, context) -> { + if (!(sender instanceof Player player)) return; + player.startConfigurationPhase2(); + }); + } +} diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 19591c428..cc51aadc8 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -1524,8 +1524,11 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev * WARNING: this does not trigger {@link EntityDeathEvent}. */ public void remove() { - if (isRemoved()) return; + remove(true); + } + protected void remove(boolean permanent) { + if (isRemoved()) return; EventDispatcher.call(new EntityDespawnEvent(this)); try { despawn(); @@ -1541,10 +1544,21 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev MinecraftServer.process().dispatcher().removeElement(this); this.removed = true; - Entity.ENTITY_BY_ID.remove(id); - Entity.ENTITY_BY_UUID.remove(uuid); + if (permanent) { + Entity.ENTITY_BY_ID.remove(id); + Entity.ENTITY_BY_UUID.remove(uuid); + } else { + // Reset some other state + this.position = Pos.ZERO; + this.previousPosition = Pos.ZERO; + this.lastSyncedPosition = Pos.ZERO; + } Instance currentInstance = this.instance; - if (currentInstance != null) removeFromInstance(currentInstance); + if (currentInstance != null) { + removeFromInstance(currentInstance); + this.instance = null; + } + } /** diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 6bce3dc48..2826f903c 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -59,6 +59,7 @@ import net.minestom.server.network.PlayerProvider; import net.minestom.server.network.packet.client.ClientPacket; 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; @@ -250,19 +251,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, metadata.setNotifyAboutChanges(false); } - public void UNSAFE_enterConfiguration(boolean isFirstConfig) { - AsyncUtils.runAsync(() -> { - var event = new AsyncPlayerConfigurationEvent(this, isFirstConfig); - EventDispatcher.call(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()); - }); - } - /** * Used when the player is created. * Init the player and spawn him. @@ -272,9 +260,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable, * * @param spawnInstance the player spawn instance (defined in {@link AsyncPlayerConfigurationEvent}) */ + @ApiStatus.Internal public CompletableFuture UNSAFE_init(@NotNull Instance spawnInstance) { this.dimensionType = spawnInstance.getDimensionType(); + System.out.println("INIT PLAYER"); + final JoinGamePacket joinGamePacket = new JoinGamePacket( getEntityId(), false, List.of(), 0, MinecraftServer.getChunkViewDistance(), MinecraftServer.getChunkViewDistance(), @@ -282,8 +273,6 @@ public class Player extends LivingEntity implements CommandSender, Localizable, 0, gameMode, null, false, levelFlat, deathLocation, portalCooldown); sendPacket(joinGamePacket); - // Server brand name - sendPacket(PluginMessagePacket.getBrandPacket()); // Difficulty sendPacket(new ServerDifficultyPacket(MinecraftServer.getDifficulty(), true)); @@ -357,6 +346,71 @@ public class Player extends LivingEntity implements CommandSender, Localizable, return setInstance(spawnInstance); } + /** + * Moves the player immediately to the configuration state. The player is automatically moved + * to configuration upon finishing login, this method can be used to move them back to configuration + * after entering the play state. + * + *

This will result in them being removed from the current instance, player list, etc.

+ */ + public void startConfigurationPhase() { + 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); + 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"); + }); + } + + 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); + 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"); +// }); + } + /** * Used to initialize the player connection */ @@ -539,11 +593,16 @@ public class Player extends LivingEntity implements CommandSender, Localizable, } @Override - public void remove() { + public void remove(boolean permanent) { if (isRemoved()) return; - EventDispatcher.call(new PlayerDisconnectEvent(this)); - super.remove(); - this.packets.clear(); + + if (permanent) { + this.packets.clear(); + EventDispatcher.call(new PlayerDisconnectEvent(this)); + } + + super.remove(permanent); + final Inventory currentInventory = getOpenInventory(); if (currentInventory != null) currentInventory.removeViewer(this); MinecraftServer.getBossBarManager().removeAllBossBars(this); @@ -566,7 +625,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, // 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. - if (playerConnection.isOnline()) kick(REMOVE_MESSAGE); + if (permanent && playerConnection.isOnline()) kick(REMOVE_MESSAGE); } @Override diff --git a/src/main/java/net/minestom/server/listener/PlayConfigListener.java b/src/main/java/net/minestom/server/listener/PlayConfigListener.java new file mode 100644 index 000000000..f18688e85 --- /dev/null +++ b/src/main/java/net/minestom/server/listener/PlayConfigListener.java @@ -0,0 +1,12 @@ +package net.minestom.server.listener; + +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.client.play.ClientConfigurationAckPacket; +import org.jetbrains.annotations.NotNull; + +public class PlayConfigListener { + + public static void configAckListener(@NotNull ClientConfigurationAckPacket packet, @NotNull Player player) { + player.startConfigurationPhase(); + } +} diff --git a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java index b7b80c472..97bd8024b 100644 --- a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java +++ b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java @@ -65,6 +65,7 @@ public final class PacketListenerManager { setPlayListener(ClientChatMessagePacket.class, ChatMessageListener::chatMessageListener); setPlayListener(ClientClickWindowPacket.class, WindowListener::clickWindowListener); setPlayListener(ClientCloseWindowPacket.class, WindowListener::closeWindowListener); + setPlayListener(ClientConfigurationAckPacket.class, PlayConfigListener::configAckListener); setPlayListener(ClientPongPacket.class, WindowListener::pong); setPlayListener(ClientEntityActionPacket.class, EntityActionListener::listener); setPlayListener(ClientHeldItemChangePacket.class, PlayerHeldListener::heldListener); diff --git a/src/main/java/net/minestom/server/listener/preplay/LoginListener.java b/src/main/java/net/minestom/server/listener/preplay/LoginListener.java index 6e282798b..3f407f117 100644 --- a/src/main/java/net/minestom/server/listener/preplay/LoginListener.java +++ b/src/main/java/net/minestom/server/listener/preplay/LoginListener.java @@ -8,24 +8,19 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.entity.damage.DamageType; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.extras.MojangAuth; import net.minestom.server.extras.bungee.BungeeCordProxy; import net.minestom.server.extras.mojangAuth.MojangCrypt; import net.minestom.server.extras.velocity.VelocityProxy; -import net.minestom.server.instance.Instance; import net.minestom.server.message.Messenger; import net.minestom.server.network.ConnectionManager; -import net.minestom.server.network.ConnectionState; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.client.login.ClientEncryptionResponsePacket; import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket; import net.minestom.server.network.packet.client.login.ClientLoginPluginResponsePacket; import net.minestom.server.network.packet.client.login.ClientLoginStartPacket; -import net.minestom.server.network.packet.server.common.ResourcePackSendPacket; +import net.minestom.server.network.packet.server.common.PluginMessagePacket; import net.minestom.server.network.packet.server.common.TagsPacket; -import net.minestom.server.network.packet.server.configuration.FinishConfigurationPacket; import net.minestom.server.network.packet.server.configuration.RegistryDataPacket; import net.minestom.server.network.packet.server.login.EncryptionRequestPacket; import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; @@ -33,12 +28,9 @@ import net.minestom.server.network.packet.server.login.LoginPluginRequestPacket; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; -import net.minestom.server.registry.Registry; import net.minestom.server.utils.async.AsyncUtils; -import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import org.jglrxavpok.hephaistos.nbt.NBT; -import org.jglrxavpok.hephaistos.nbt.NBTType; import javax.crypto.SecretKey; import java.math.BigInteger; @@ -49,7 +41,6 @@ import java.net.http.HttpResponse; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ThreadLocalRandom; import static net.minestom.server.network.NetworkBuffer.STRING; @@ -241,8 +232,11 @@ public final class LoginListener { // Tags connection.sendPacket(TagsPacket.DEFAULT_TAGS); + // Server Brand + connection.sendPacket(PluginMessagePacket.getBrandPacket()); + // Enter configuration phase (for the first time) - player.UNSAFE_enterConfiguration(true); + player.startConfigurationPhase(); } } diff --git a/src/main/java/net/minestom/server/network/ConnectionManager.java b/src/main/java/net/minestom/server/network/ConnectionManager.java index 4901e0fcd..62a933974 100644 --- a/src/main/java/net/minestom/server/network/ConnectionManager.java +++ b/src/main/java/net/minestom/server/network/ConnectionManager.java @@ -8,14 +8,13 @@ import net.minestom.server.event.EventDispatcher; 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.login.LoginSuccessPacket; import net.minestom.server.network.packet.server.common.KeepAlivePacket; +import net.minestom.server.network.packet.server.login.LoginSuccessPacket; 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; @@ -36,11 +35,14 @@ 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 record PendingPlayer(@NotNull Player player, @NotNull Instance instance) { + } private final MessagePassingQueue waitingPlayers = new MpscUnboundedArrayQueue<>(64); private final Set players = new CopyOnWriteArraySet<>(); private final Set unmodifiablePlayers = Collections.unmodifiableSet(players); + + // All players once their Player object has been instantiated. private final Map connectionPlayerMap = new ConcurrentHashMap<>(); // The uuid provider once a player login @@ -48,16 +50,6 @@ public final class ConnectionManager { // The player provider to have your own Player implementation private volatile PlayerProvider playerProvider = Player::new; - /** - * 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. * @@ -68,25 +60,28 @@ public final class ConnectionManager { } /** - * Finds the closest player matching a given username. + * Gets online players filtered by state. * - * @param username the player username (can be partial) - * @return the closest match, null if no players are online + *

States before login are not considered online, and will result in nothing being returned.

+ * + * @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 @Nullable Player findPlayer(@NotNull String username) { - Player exact = getPlayer(username); - if (exact != null) return exact; - final String username1 = username.toLowerCase(Locale.ROOT); + public @NotNull Collection<@NotNull Player> getPlayers(@NotNull ConnectionState... state) { +// connectionPlayerMap.values().stream() +// .filter() + return List.of(); + } - 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 {@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); } /** @@ -121,6 +116,28 @@ public final class ConnectionManager { return null; } + /** + * 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); + } + /** * Changes how {@link UUID} are attributed to players. *

@@ -168,6 +185,23 @@ public final class ConnectionManager { return playerProvider; } + //todo REDONE TRANSITIONS + + @ApiStatus.Internal + public void transitionLoginToConfig(@NotNull Player player) { + + } + + @ApiStatus.Internal + public void transitionConfigToPlay(@NotNull Player player) { + + } + + @ApiStatus.Internal + public void transitionPlayToConfig(@NotNull Player player) { + + } + /** * Adds a {@link Player} to the players list. *

@@ -198,7 +232,7 @@ public final class ConnectionManager { @ApiStatus.Internal public @NotNull Player startConfigurationState(@NotNull PlayerConnection connection, - @NotNull UUID uuid, @NotNull String username) { + @NotNull UUID uuid, @NotNull String username) { final Player player = playerProvider.createPlayer(uuid, username, connection); startConfigurationState(player); return player; @@ -255,6 +289,8 @@ public final class ConnectionManager { public void updateWaitingPlayers() { this.waitingPlayers.drain(pp -> { + System.out.println("DRAIN PP " + pp); + // Spawn the player at Player#getRespawnPoint CompletableFuture spawnFuture = pp.player.UNSAFE_init(pp.instance); diff --git a/src/main/java/net/minestom/server/network/packet/client/ClientPacketsHandler.java b/src/main/java/net/minestom/server/network/packet/client/ClientPacketsHandler.java index f80554afd..4ce49a3d3 100644 --- a/src/main/java/net/minestom/server/network/packet/client/ClientPacketsHandler.java +++ b/src/main/java/net/minestom/server/network/packet/client/ClientPacketsHandler.java @@ -100,7 +100,7 @@ public sealed class ClientPacketsHandler permits ClientPacketsHandler.Status, Cl register(nextId(), ClientStatusPacket::new); register(nextId(), ClientSettingsPacket::new); register(nextId(), ClientTabCompletePacket::new); - nextId(); // configuration acknowledged + register(nextId(), ClientConfigurationAckPacket::new); register(nextId(), ClientClickWindowButtonPacket::new); register(nextId(), ClientClickWindowPacket::new); register(nextId(), ClientCloseWindowPacket::new); diff --git a/src/main/java/net/minestom/server/network/packet/client/play/ClientConfigurationAckPacket.java b/src/main/java/net/minestom/server/network/packet/client/play/ClientConfigurationAckPacket.java new file mode 100644 index 000000000..370b2513a --- /dev/null +++ b/src/main/java/net/minestom/server/network/packet/client/play/ClientConfigurationAckPacket.java @@ -0,0 +1,22 @@ +package net.minestom.server.network.packet.client.play; + +import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.network.packet.client.ClientPacket; +import org.jetbrains.annotations.NotNull; + +public record ClientConfigurationAckPacket() implements ClientPacket { + + public ClientConfigurationAckPacket(@NotNull NetworkBuffer buffer) { + this(); + } + + @Override + public void write(@NotNull NetworkBuffer writer) { + } + + @Override + public @NotNull ConnectionState nextState() { + return ConnectionState.CONFIGURATION; + } +} diff --git a/src/main/java/net/minestom/server/network/packet/server/play/StartConfigurationPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/StartConfigurationPacket.java new file mode 100644 index 000000000..72b799a34 --- /dev/null +++ b/src/main/java/net/minestom/server/network/packet/server/play/StartConfigurationPacket.java @@ -0,0 +1,25 @@ +package net.minestom.server.network.packet.server.play; + +import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.NetworkBuffer; +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.network.packet.server.ServerPacketIdentifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record StartConfigurationPacket() implements ServerPacket { + + @Override + public void write(@NotNull NetworkBuffer writer) { + } + + @Override + public int getId(@NotNull ConnectionState state) { + return ServerPacketIdentifier.START_CONFIGURATION_PACKET; + } + + @Override + public @NotNull ConnectionState nextState() { + return ConnectionState.CONFIGURATION; + } +}