Skip to content

Configuration API #12301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build-data/paper.at
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public net.minecraft.server.level.ServerPlayer wardenSpawnTracker
public net.minecraft.server.level.ServerPlayer$RespawnPosAngle
public net.minecraft.server.level.ServerPlayerGameMode level
public net.minecraft.server.level.TicketType register(Ljava/lang/String;JZLnet/minecraft/server/level/TicketType$TicketUse;)Lnet/minecraft/server/level/TicketType;
public net.minecraft.server.network.ServerConfigurationPacketListenerImpl clientInformation
public net.minecraft.server.network.ServerConfigurationPacketListenerImpl currentTask
public net.minecraft.server.network.ServerConfigurationPacketListenerImpl finishCurrentTask(Lnet/minecraft/server/network/ConfigurationTask$Type;)V
public net.minecraft.server.network.ServerGamePacketListenerImpl isChatMessageIllegal(Ljava/lang/String;)Z
public net.minecraft.server.network.ServerLoginPacketListenerImpl authenticatedProfile
public net.minecraft.server.network.ServerLoginPacketListenerImpl connection
Expand Down
33 changes: 33 additions & 0 deletions notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
- Cookie handling will stall the connection when needed to prevent the connection from finishing before the cookie has been received.
- A player can still get timed out while waiting for a cookie.

Previously, the cookie handling for login occurred after the PlayerLoginEvent only. This meant that cookies could not be sent before the player was created.
New Use Cases:
- Instantly terminate the connection if a cookie is not present.

Behavior changes:
- PlayerLinksSendEvent now takes a configuration connection
- PlayerLoginEvent no longer blocks cookies
- PlayerLoginEvent now triggers twice due to reasons
- Disconnecting a duplicate player no longer causes a forced save. Check to make sure that still occurs

TODO:
- Figure out connection hierarchy
- Figure out naming

New Events:
PlayerServerFullCheckEvent - allows you to make players bypass the server fullness check without using PlayerLoginEvent

Protocol Lifecycles:
LOGIN:
entering login: AsyncPlayerPreLoginEvent
validating login: PlayerConnectionValidateLoginEvent (basically PlayerLoginEvent)
exiting login: PlayerFinishConnectionPhaseEvent (allows you to disconnect to prevent a ClientboundLoginFinishedPacket from being sent)
CONFIGURATION:
entering config: PlayerConnectionInitialConfigureEvent / PlayerConnectionReconfigurateEvent (split for convenience)
configuring player: AsyncPlayerConnectionConfigureEvent (allows for async processing, can run for as long as needed)
validating login: PlayerConnectionValidateLoginEvent (basically PlayerLoginEvent)
exiting config: PlayerFinishConnectionPhaseEvent (allows you to disconnect to prevent a player being placed/created)



Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.papermc.paper.connection;

import java.util.Map;

import com.destroystokyo.paper.ClientOption;
import org.bukkit.ServerLinks;

/**
* Represents a connection that has properties shared between the GAME and CONFIG stage.
*/
public interface PlayerCommonConnection extends WritablePlayerCookieConnection, ReadablePlayerCookieConnection {

/**
* Sends data to appear in this connection's report logs.
* This is useful for debugging server state that may be causing
* player disconnects.
* <p>
* These are formatted as key - value, where keys are limited to a length of 128 characters,
* values are limited to 4096, and 32 maximum entries can be sent.
*
* @param details report details
*/
void sendReportDetails(Map<String, String> details);

/**
* Sends the given server links to this connection.
*
* @param links links to send
*/
void sendLinks(ServerLinks links);

/**
* Transfers this connection to another server.
*
* @param host host
* @param port port
*/
void transfer(String host, int port);

/**
* @param type client option
* @return the client option value of the player
*/
<T> T getClientOption(ClientOption<T> type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.papermc.paper.connection;

import com.destroystokyo.paper.ClientOption;
import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.audience.Audience;

public interface PlayerConfigurationConnection extends PlayerCommonConnection {

/**
* Returns the audience representing the player in configuration mode.
* This can be used to interact with the Adventure API during the configuration stage.
* This is guaranteed to be an instance of {@link PlayerConfigurationConnection}
*
* @return the configuring player audience
*/
Audience getAudience();

/**
* Gets the profile for this connection.
*
* @return profile
*/
PlayerProfile getProfile();

/**
* Clears the players chat history and their local chat.
*/
void clearChat();

/**
* Completes the configuration for this player, which will cause this player to reenter the game.
* <p>
* Note, this should be only be called if you are reconfiguring the player.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, similar to #enterConfiguration poses the interesting issue of "ah, you just requested what is basically a destruction of this connection state, what happens to this instance".

Calling #clearChat after this is illegal.
Getters might be fine. But we should look into a proper way to signal, "you destroyed this connection instance".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree here. But, I am not sure as the current standard has been to silently fail in many places.
image
So we could very much add some validation, but it all ends up nooping.

*/
void completeReconfiguration();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.papermc.paper.connection;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import net.kyori.adventure.text.Component;
import org.jspecify.annotations.Nullable;

public interface PlayerConnection {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really good if the hierarchy of types here was adjusted so this class could be sealed and permit the two API interfaces. Would make switching on it nicer, especially in events where you only have the PlayerConnection, and to know what type it is, you do a switch pattern match. It's not a super simple change, because of the impl CommonCookeConnection abstract class, but things could def be moved around.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After playing around with this a bit, I am not sure about it really. This does require alot of moving around and possible duplication that I am not fully sure is worth it.

Additionally, its a bit of an odd situation, as I am not sure in what cases you'd be casting like this in the first place.


/**
* Disconnects the player connection.
* <p>
* Note that calling this during connection related events may caused undefined behavior.
*
* @param component disconnect reason
*/
void disconnect(Component component);

/**
* Gets if this connection originated from a transferred connection.
* <p>
* Do note that this is sent and stored on the client.
*
* @return is transferred
*/
boolean isTransferred();

/**
* Gets the raw remote address of the connection. This may be a proxy address
* or a Unix domain socket address, depending on how the channel was established.
*
* @return the remote {@link SocketAddress} of the channel
*/
SocketAddress getAddress();

/**
* Gets the real client address of the player. If the connection is behind a proxy,
* this will be the actual player’s IP address extracted from the proxy handshake.
*
* @return the client {@link InetSocketAddress}
*/
InetSocketAddress getClientAddress();

/**
* Returns the virtual host the client is connected to.
*
* <p>The virtual host refers to the hostname/port the client used to
* connect to the server.</p>
*
* @return The client's virtual host, or {@code null} if unknown
*/
@Nullable InetSocketAddress getVirtualHost();

/**
* Gets the socket address of this player's proxy
*
* @return the player's proxy address, null if the server doesn't have Proxy Protocol enabled, or the player didn't connect to an HAProxy instance
*/
@Nullable InetSocketAddress getHAProxyAddress();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.papermc.paper.connection;

import org.bukkit.entity.Player;

public interface PlayerGameConnection extends PlayerCommonConnection {

/**
* Bumps the player to the configuration stage.
* <p>
* This will, by default, cause the player to stay until their connection is released by
* {@link PlayerConfigurationConnection#completeReconfiguration()}
*/
void reenterConfiguration();

/**
* Gets the player that is associated with this game connection.
*
* @return player
*/
Player getPlayer();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.papermc.paper.connection;

import com.destroystokyo.paper.profile.PlayerProfile;
import org.jspecify.annotations.Nullable;

public interface PlayerLoginConnection extends ReadablePlayerCookieConnection {

/**
* Gets the authenticated profile for this connection.
* This may return null depending on what stage this connection is at.
*
* @return authenticated profile, or null if not present
*/
@Nullable PlayerProfile getAuthenticatedProfile();

/**
* Gets the player profile that this connection is requesting to authenticate as.
*
* @return the unsafe unauthenticated profile, or null if not sent
*/
@Nullable
PlayerProfile getUnsafeProfile();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.papermc.paper.connection;

import java.util.concurrent.CompletableFuture;
import org.bukkit.NamespacedKey;

public interface ReadablePlayerCookieConnection extends PlayerConnection {

/**
* Retrieves a cookie from this connection.
*
* @param key the key identifying the cookie
* @return a {@link CompletableFuture} that will be completed when the
* Cookie response is received or otherwise available. If the cookie is not
* set in the client, the {@link CompletableFuture} will complete with a
* null value.
*/
CompletableFuture<byte[]> retrieveCookie(NamespacedKey key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.papermc.paper.connection;

import org.bukkit.NamespacedKey;

public interface WritablePlayerCookieConnection extends PlayerConnection {

/**
* Stores a cookie in this player's client.
*
* @param key the key identifying the cookie
* @param value the data to store in the cookie
* @throws IllegalStateException if a cookie cannot be stored at this time
*/
void storeCookie(NamespacedKey key, byte[] value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* This package contains events related to player connections, such as joining and leaving the server.
*/
@ApiStatus.Experimental
@NullMarked
package io.papermc.paper.connection;

import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.papermc.paper.event.connection;

import io.papermc.paper.connection.PlayerConnection;
import net.kyori.adventure.text.Component;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;

/**
* Validates whether a player connection is able to log in.
* <p>
* Called when is attempting to log in for the first time, or is finishing up
* being configured.
*/
public class PlayerConnectionValidateLoginEvent extends Event {

private static final HandlerList HANDLER_LIST = new HandlerList();

private final PlayerConnection connection;
private @Nullable Component kickMessage;

@ApiStatus.Internal
public PlayerConnectionValidateLoginEvent(final PlayerConnection connection, final @Nullable Component kickMessage) {
super(false);
this.connection = connection;
this.kickMessage = kickMessage;
}

/**
* Gets the connection of the player in this event.
* Note, the type of this connection is not guaranteed to be stable across versions.
* Additionally, disconnecting the player through this connection / using any methods that may send packets
* is not supported.
*
* @return connection
*/
public PlayerConnection getConnection() {
return this.connection;
}

/**
* Allows the player to log in.
* This skips any login validation checks.
*/
public void allow() {
this.kickMessage = null;
}

/**
* Disallows the player from logging in, with the given reason
*
* @param message Kick message to display to the user
*/
public void kickMessage(final Component message) {
this.kickMessage = message;
}

/**
* Gets the reason for why a player is not allowed to join the server.
* This will be null in the case that the player is allowed to log in.
*
* @return disallow reason
*/
public @Nullable Component getKickMessage() {
return this.kickMessage;
}

/**
* Gets if the player is allowed to enter the next stage.
*
* @return if allowed
*/
public boolean isAllowed() {
return this.kickMessage == null;
}

@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}

public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}
Loading
Loading