feat: start reentry of config phase

This commit is contained in:
mworzala 2023-11-25 21:43:28 +02:00 committed by Matt Worzala
parent f73d8faf9e
commit e702c09f06
11 changed files with 247 additions and 68 deletions

View File

@ -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)));

View File

@ -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();
});
}
}

View File

@ -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;
}
}
/**

View File

@ -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<Void> 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.
*
* <p>This will result in them being removed from the current instance, player list, etc.</p>
*/
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

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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<PendingPlayer> waitingPlayers = new MpscUnboundedArrayQueue<>(64);
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<>();
// 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
* <p>States before login are not considered online, and will result in nothing being returned.</p>
*
* @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<Player, Double> 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<Player, Double> 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.
* <p>
@ -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.
* <p>
@ -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<Void> spawnFuture = pp.player.UNSAFE_init(pp.instance);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}
}