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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
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)



2 changes: 2 additions & 0 deletions paper-api-generator.settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Uncomment to enable the 'paper-api-generator' project
// include(":paper-api-generator")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.papermc.paper.connection;

import org.bukkit.NamespacedKey;
import org.jspecify.annotations.NullMarked;
import java.util.concurrent.CompletableFuture;

@NullMarked
public interface CookieConnection 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);

/**
* 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,58 @@
package io.papermc.paper.connection;

import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike;
import org.bukkit.ServerLinks;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NullMarked;

import java.util.Map;
import java.util.UUID;

/**
* Represents a connection that has properties shared between the GAME and CONFIG stage.
*/
@NullMarked
public interface PlayerCommonConnection extends CookieConnection {

void sendResourcePacks(final @NotNull ResourcePackRequest request);

void removeResourcePacks(final @NotNull UUID id, final @NotNull UUID... others);

void clearResourcePacks();

/**
* 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
*/
void sendLinks(ServerLinks links);

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

/**
* Gets the brand of this player.
* It is generally expected that the brand will be available.
* @return brand, or null if not present
*/
@Nullable
String getBrand();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.papermc.paper.connection;

import com.destroystokyo.paper.profile.PlayerProfile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
import java.net.InetAddress;
import java.net.InetSocketAddress;

@NullMarked
public interface PlayerConfigurationConnection extends PlayerCommonConnection {

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

/**
* Gets the player IP address.
*
* @return The IP address
*/
InetAddress getAddress();

/**
* Gets the raw address of the player logging in
* @return The address
*/
InetAddress getRawAddress();

/**
* Gets the hostname that the player used to connect to the server, or
* blank if unknown
*
* @return The hostname
*/
String getHostname();

/**
* 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.
*/
void completeConfiguration();

/**
* @param type client option
* @return the client option value of the player
*/
<T> T getClientOption(com.destroystokyo.paper.ClientOption<T> type);

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

import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;

@NullMarked
public interface PlayerConnection {

/**
* 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.
* @return is transferred
*/
boolean isTransferred();

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

import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;

@NullMarked
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#completeConfiguration()}
*/
void enterConfiguration();

/**
* Gets the player that is currently associated with this game connection.
*
* @return player
*/
Player getPlayer();

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

import com.destroystokyo.paper.profile.PlayerProfile;
import org.bukkit.NamespacedKey;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NullMarked;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;

@NullMarked
public interface PlayerLoginConnection extends CookieConnection {

/**
* 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 currently 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 currently sent
*/
@Nullable
PlayerProfile getUnsafeProfile();

/**
* Gets the player IP address.
*
* @return The IP address
*/
@NotNull
InetAddress getAddress();

/**
* Gets the raw address of the player logging in
* @return The address
*/
@NotNull
InetAddress getRawAddress();

/**
* Gets the hostname that the player used to connect to the server, or
* blank if unknown
*
* @return The hostname
*/
@NotNull
String getHostname();

// TODO: Should we have a read-only interface?
@Deprecated
@Override
void storeCookie(NamespacedKey key, byte[] value) throws UnsupportedOperationException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.papermc.paper.event.connection.common;

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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NullMarked;

/**
* Validates whether a player connection would be able to login at this event.
* <p>
* Currently, this occurs when the player connection is attempting to log in for the first time, or is finishing up
* being configured.
*/
@NullMarked
public class PlayerConnectionValidateLoginEvent extends Event {
private static final HandlerList handlers = new HandlerList();

private final PlayerConnection connection;

@Nullable
private Component disallowedReason;

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

public PlayerConnection getConnection() {
return connection;
}

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

/**
* Disallows the player from logging in, with the given reason
*
* @param message Kick message to display to the user
*/
public void disallow(@NotNull final net.kyori.adventure.text.Component message) {
this.disallowedReason = message;
}

/**
* Gets the reason for why a player is not allowed to join the server.
* This may be null in the case that the player is allowed to login.
* @return disallow reason
*/
public @Nullable Component getDisallowedReason() {
return disallowedReason;
}

@NotNull
@Override
public HandlerList getHandlers() {
return handlers;
}

@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
}
Loading
Loading