Merge branch 'dev/3.0.0' into netease/dev
Some checks failed
Java CI with Gradle / build (push) Failing after 6m42s

# Conflicts:
#	api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java
#	proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
This commit is contained in:
2025-07-24 09:19:06 +08:00
88 changed files with 1592 additions and 416 deletions

View File

@@ -61,6 +61,7 @@ tasks {
o.encoding = "UTF-8"
o.source = "17"
o.use()
o.links(
"https://www.slf4j.org/apidocs/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",

View File

@@ -7,8 +7,10 @@
package com.velocitypowered.api.command;
import com.mojang.brigadier.suggestion.Suggestions;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -116,6 +118,27 @@ public interface CommandManager {
*/
CompletableFuture<Boolean> executeImmediatelyAsync(CommandSource source, String cmdLine);
/**
* Asynchronously collects suggestions to fill in the given command {@code cmdLine}.
* Returns only the raw completion suggestions without tooltips.
*
* @param source the source to execute the command for
* @param cmdLine the partially completed command
* @return a {@link CompletableFuture} eventually completed with a {@link List}, possibly empty
*/
CompletableFuture<List<String>> offerSuggestions(CommandSource source, String cmdLine);
/**
* Asynchronously collects suggestions to fill in the given command {@code cmdLine}.
* Returns the brigadier {@link Suggestions} with tooltips for each result.
*
* @param source the source to execute the command for
* @param cmdLine the partially completed command
* @return a {@link CompletableFuture} eventually completed with {@link Suggestions}, possibly
* empty
*/
CompletableFuture<Suggestions> offerBrigadierSuggestions(CommandSource source, String cmdLine);
/**
* Returns an immutable collection of the case-insensitive aliases registered
* on this manager.

View File

@@ -27,7 +27,7 @@ public interface CommandSource extends Audience, PermissionSubject {
* for more information on the format.
**/
default void sendRichMessage(final @NotNull String message) {
this.sendMessage(MiniMessage.miniMessage().deserialize(message));
this.sendMessage(MiniMessage.miniMessage().deserialize(message, this));
}
/**
@@ -43,7 +43,7 @@ public interface CommandSource extends Audience, PermissionSubject {
final @NotNull String message,
final @NotNull TagResolver @NotNull... resolvers
) {
this.sendMessage(MiniMessage.miniMessage().deserialize(message, resolvers));
this.sendMessage(MiniMessage.miniMessage().deserialize(message, this, resolvers));
}
/**

View File

@@ -9,7 +9,6 @@ package com.velocitypowered.api.event.command;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.Beta;
import com.mojang.brigadier.tree.RootCommandNode;
import com.velocitypowered.api.event.annotation.AwaitingEvent;
import com.velocitypowered.api.proxy.Player;
@@ -21,7 +20,6 @@ import com.velocitypowered.api.proxy.Player;
* client.
*/
@AwaitingEvent
@Beta
public class PlayerAvailableCommandsEvent {
private final Player player;

View File

@@ -7,7 +7,6 @@
package com.velocitypowered.api.event.player;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -18,7 +17,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* available in {@link Player#getCurrentServer()}. Velocity will not wait on this event to finish
* firing.
*/
@Beta
public class ServerPostConnectEvent {
private final Player player;
private final RegisteredServer previousServer;

View File

@@ -7,7 +7,6 @@
package com.velocitypowered.api.event.proxy.server;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
@@ -23,7 +22,6 @@ import org.jetbrains.annotations.NotNull;
* @param registeredServer A {@link RegisteredServer} that has been registered.
* @since 3.3.0
*/
@Beta
public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) {
public ServerRegisteredEvent {
Preconditions.checkNotNull(registeredServer, "registeredServer");

View File

@@ -7,7 +7,6 @@
package com.velocitypowered.api.event.proxy.server;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
@@ -23,7 +22,6 @@ import org.jetbrains.annotations.NotNull;
* @param unregisteredServer A {@link RegisteredServer} that has been unregistered.
* @since 3.3.0
*/
@Beta
public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) {
public ServerUnregisteredEvent {
Preconditions.checkNotNull(unregisteredServer, "unregisteredServer");

View File

@@ -89,7 +89,10 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"),
MINECRAFT_1_21(767, "1.21", "1.21.1"),
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
MINECRAFT_1_21_4(769, "1.21.4");
MINECRAFT_1_21_4(769, "1.21.4"),
MINECRAFT_1_21_5(770, "1.21.5"),
MINECRAFT_1_21_6(771, "1.21.6"),
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8");
private static final int SNAPSHOT_BIT = 30;

View File

@@ -41,6 +41,13 @@ public interface ProxyServer extends Audience {
*/
void shutdown();
/**
* Returns whether the proxy is currently shutting down.
*
* @return {@code true} if the proxy is shutting down, {@code false} otherwise
*/
boolean isShuttingDown();
/**
* Closes all listening endpoints for this server.
* This includes the main minecraft listener and query channel.

View File

@@ -149,6 +149,61 @@ public interface ProxyConfig {
*/
int getReadTimeout();
/**
* Get the rate limit for how fast a player can execute commands.
*
* @return the command rate limit (in milliseconds)
*/
int getCommandRatelimit();
/**
* Get whether we should forward commands to the backend if the player is rate limited.
*
* @return whether to forward commands if rate limited
*/
boolean isForwardCommandsIfRateLimited();
/**
* Get the kick limit for commands that are rate limited.
* If this limit is 0 or less, the player will be not be kicked.
*
* @return the rate limited command rate limit
*/
int getKickAfterRateLimitedCommands();
/**
* Get whether the proxy should kick players who are command rate limited.
*
* @return whether to kick players who are rate limited
*/
default boolean isKickOnCommandRateLimit() {
return getKickAfterRateLimitedCommands() > 0;
}
/**
* Get the rate limit for how fast a player can tab complete.
*
* @return the tab complete rate limit (in milliseconds)
*/
int getTabCompleteRatelimit();
/**
* Get the kick limit for tab completes that are rate limited.
* If this limit is 0 or less, the player will be not be kicked.
*
* @return the rate limited command rate limit
*/
int getKickAfterRateLimitedTabCompletes();
/**
* Get whether the proxy should kick players who are tab complete rate limited.
*
* @return whether to kick players who are rate limited
*/
default boolean isKickOnTabCompleteRateLimit() {
return getKickAfterRateLimitedTabCompletes() > 0;
}
String getNeteaseAuthUrl();
String getNeteaseGameId();

View File

@@ -11,7 +11,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Strings;
import java.util.Objects;
import java.util.regex.Pattern;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -21,8 +20,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9/\\-_]*");
private final String namespace;
private final String name;
@@ -39,7 +36,7 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
* @return a new channel identifier
*/
public static MinecraftChannelIdentifier forDefaultNamespace(String name) {
return new MinecraftChannelIdentifier("minecraft", name);
return new MinecraftChannelIdentifier(Key.MINECRAFT_NAMESPACE, name);
}
/**
@@ -52,14 +49,10 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
public static MinecraftChannelIdentifier create(String namespace, String name) {
checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty");
checkArgument(name != null, "namespace is null or empty");
checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(),
"namespace is not valid, must match: %s got %s",
VALID_IDENTIFIER_REGEX.toString(),
namespace);
checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(),
"name is not valid, must match: %s got %s",
VALID_IDENTIFIER_REGEX.toString(),
name);
checkArgument(Key.parseableNamespace(namespace),
"namespace is not valid, must match: [a-z0-9_.-] got %s", namespace);
checkArgument(Key.parseableValue(name),
"name is not valid, must match: [a-z0-9/._-] got %s", name);
return new MinecraftChannelIdentifier(namespace, name);
}
@@ -72,10 +65,9 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
public static MinecraftChannelIdentifier from(String identifier) {
int colonPos = identifier.indexOf(':');
if (colonPos == -1) {
throw new IllegalArgumentException("Identifier does not contain a colon.");
}
if (colonPos + 1 == identifier.length()) {
throw new IllegalArgumentException("Identifier is empty.");
return create(Key.MINECRAFT_NAMESPACE, identifier);
} else if (colonPos == 0) {
return create(Key.MINECRAFT_NAMESPACE, identifier.substring(1));
}
String namespace = identifier.substring(0, colonPos);
String name = identifier.substring(colonPos + 1);

View File

@@ -40,6 +40,7 @@ public interface TabList {
* Adds a {@link TabListEntry} to the {@link Player}'s tab list.
*
* @param entry to add to the tab list
* @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists
*/
void addEntry(TabListEntry entry);
@@ -47,6 +48,7 @@ public interface TabList {
* Adds a {@link Iterable} of {@link TabListEntry}'s to the {@link Player}'s tab list.
*
* @param entries to add to the tab list
* @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists
*/
default void addEntries(Iterable<TabListEntry> entries) {
for (TabListEntry entry : entries) {
@@ -58,6 +60,7 @@ public interface TabList {
* Adds an array of {@link TabListEntry}'s to the {@link Player}'s tab list.
*
* @param entries to add to the tab list
* @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists
*/
default void addEntries(TabListEntry... entries) {
for (TabListEntry entry : entries) {
@@ -187,6 +190,26 @@ public interface TabList {
* @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
*/
@Deprecated
default TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder) {
return buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, true);
}
/**
* Represents an entry in a {@link Player}'s tab list.
*
* @param profile the profile
* @param displayName the display name
* @param latency the latency
* @param gameMode the game mode
* @param chatSession the chat session
* @param listed the visible status of entry
* @param listOrder the order/priority of entry in the tab list
* @param showHat the visibility of this entry's hat layer
* @return the entry
* @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
*/
@Deprecated
TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder);
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat);
}

View File

@@ -160,6 +160,27 @@ public interface TabListEntry extends KeyIdentifiable {
return this;
}
/**
* Returns whether this entry's hat layer is shown in the tab list.
*
* @return whether to show this entry's hat layer
* @sinceMinecraft 1.21.4
*/
default boolean isShowHat() {
return true;
}
/**
* Sets whether to show this entry's hat layer in the tab list.
*
* @param showHat whether to show this entry's hat layer
* @return {@code this}, for chaining
* @sinceMinecraft 1.21.4
*/
default TabListEntry setShowHat(boolean showHat) {
return this;
}
/**
* Returns a {@link Builder} to create a {@link TabListEntry}.
*
@@ -183,6 +204,7 @@ public interface TabListEntry extends KeyIdentifiable {
private int gameMode = 0;
private boolean listed = true;
private int listOrder = 0;
private boolean showHat;
private @Nullable ChatSession chatSession;
@@ -268,7 +290,7 @@ public interface TabListEntry extends KeyIdentifiable {
* Sets whether this entry should be visible.
*
* @param listed to set
* @return ${code this}, for chaining
* @return {@code this}, for chaining
* @see TabListEntry#isListed()
*/
public Builder listed(boolean listed) {
@@ -280,7 +302,7 @@ public interface TabListEntry extends KeyIdentifiable {
* Sets the order/priority of this entry in the tab list.
*
* @param order to set
* @return ${code this}, for chaining
* @return {@code this}, for chaining
* @sinceMinecraft 1.21.2
* @see TabListEntry#getListOrder()
*/
@@ -289,6 +311,18 @@ public interface TabListEntry extends KeyIdentifiable {
return this;
}
/**
* Sets whether this entry's hat layer should be shown in the tab list.
*
* @param showHat to set
* @return {@code this}, for chaining
* @see TabListEntry#isShowHat()
*/
public Builder showHat(boolean showHat) {
this.showHat = showHat;
return this;
}
/**
* Constructs the {@link TabListEntry} specified by {@code this} {@link Builder}.
*
@@ -301,7 +335,7 @@ public interface TabListEntry extends KeyIdentifiable {
if (profile == null) {
throw new IllegalStateException("The GameProfile must be set when building a TabListEntry");
}
return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder);
return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, showHat);
}
}
}

View File

@@ -15,6 +15,7 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.builder.AbstractBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Contains the parameters used to ping a {@link RegisteredServer}.
@@ -30,10 +31,12 @@ public final class PingOptions {
public static final PingOptions DEFAULT = PingOptions.builder().build();
private final ProtocolVersion protocolVersion;
private final long timeout;
private final String virtualHost;
private PingOptions(final Builder builder) {
this.protocolVersion = builder.protocolVersion;
this.timeout = builder.timeout;
this.virtualHost = builder.virtualHost;
}
/**
@@ -54,6 +57,16 @@ public final class PingOptions {
return this.timeout;
}
/**
* The virtual host to pass to the server for the ping.
*
* @return the virtual hostname to pass to the server for the ping
* @since 3.4.0
*/
public @Nullable String getVirtualHost() {
return this.virtualHost;
}
/**
* Create a new builder to assign values to a new PingOptions.
*
@@ -68,10 +81,9 @@ public final class PingOptions {
if (o == null) {
return false;
}
if (!(o instanceof PingOptions)) {
if (!(o instanceof final PingOptions other)) {
return false;
}
final PingOptions other = (PingOptions) o;
return Objects.equals(this.protocolVersion, other.protocolVersion)
&& Objects.equals(this.timeout, other.timeout);
}
@@ -97,6 +109,7 @@ public final class PingOptions {
public static final class Builder implements AbstractBuilder<PingOptions> {
private ProtocolVersion protocolVersion = ProtocolVersion.UNKNOWN;
private long timeout = 0;
private String virtualHost = null;
private Builder() {
}
@@ -146,6 +159,18 @@ public final class PingOptions {
return this;
}
/**
* Sets the virtual host to pass to the server for the ping.
*
* @param virtualHost the virtual hostname to pass to the server for the ping
* @return this builder
* @since 3.4.0
*/
public Builder virtualHost(final @Nullable String virtualHost) {
this.virtualHost = virtualHost;
return this;
}
/**
* Create a new {@link PingOptions} with the values of this Builder.
*

View File

@@ -14,6 +14,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -159,31 +160,79 @@ public final class ServerPing {
}
/**
* Uses the modified {@code version} info in the response.
*
* @param version version info to set
* @return this builder, for chaining
*/
public Builder version(Version version) {
this.version = Preconditions.checkNotNull(version, "version");
return this;
}
/**
* Uses the modified {@code onlinePlayers} number in the response.
*
* @param onlinePlayers number for online players to set
* @return this builder, for chaining
*/
public Builder onlinePlayers(int onlinePlayers) {
this.onlinePlayers = onlinePlayers;
return this;
}
/**
* Uses the modified {@code maximumPlayers} number in the response.
* <b>This will not modify the actual maximum players that can join the server.</b>
*
* @param maximumPlayers number for maximum players to set
* @return this builder, for chaining
*/
public Builder maximumPlayers(int maximumPlayers) {
this.maximumPlayers = maximumPlayers;
return this;
}
/**
* Uses the modified {@code players} array in the response.
*
* @param players array of SamplePlayers to add
* @return this builder, for chaining
*/
public Builder samplePlayers(SamplePlayer... players) {
this.samplePlayers.addAll(Arrays.asList(players));
return this;
}
/**
* Uses the modified {@code players} collection in the response.
*
* @param players collection of SamplePlayers to add
* @return this builder, for chaining
*/
public Builder samplePlayers(Collection<SamplePlayer> players) {
this.samplePlayers.addAll(players);
return this;
}
/**
* Uses the modified {@code modType} in the response.
*
* @param modType the mod type to set
* @return this builder, for chaining
*/
public Builder modType(String modType) {
this.modType = Preconditions.checkNotNull(modType, "modType");
return this;
}
/**
* Uses the modified {@code mods} array in the response.
*
* @param mods array of mods to use
* @return this builder, for chaining
*/
public Builder mods(ModInfo.Mod... mods) {
this.mods.addAll(Arrays.asList(mods));
return this;
@@ -193,7 +242,7 @@ public final class ServerPing {
* Uses the modified {@code mods} list in the response.
*
* @param mods the mods list to use
* @return this build, for chaining
* @return this builder, for chaining
*/
public Builder mods(ModInfo mods) {
Preconditions.checkNotNull(mods, "mods");
@@ -203,36 +252,74 @@ public final class ServerPing {
return this;
}
/**
* Clears the current list of mods to use in the response.
*
* @return this builder, for chaining
*/
public Builder clearMods() {
this.mods.clear();
return this;
}
/**
* Clears the current list of PlayerSamples to use in the response.
*
* @return this builder, for chaining
*/
public Builder clearSamplePlayers() {
this.samplePlayers.clear();
return this;
}
/**
* Defines the server as mod incompatible in the response.
*
* @return this builder, for chaining
*/
public Builder notModCompatible() {
this.nullOutModinfo = true;
return this;
}
/**
* Enables nulling Players in the response.
* This will display the player count as {@code ???}.
*
* @return this builder, for chaining
*/
public Builder nullPlayers() {
this.nullOutPlayers = true;
return this;
}
/**
* Uses the {@code description} Component in the response.
*
* @param description Component to use as the description.
* @return this builder, for chaining
*/
public Builder description(net.kyori.adventure.text.Component description) {
this.description = Preconditions.checkNotNull(description, "description");
return this;
}
/**
* Uses the {@code favicon} in the response.
*
* @param favicon Favicon instance to use.
* @return this builder, for chaining
*/
public Builder favicon(Favicon favicon) {
this.favicon = Preconditions.checkNotNull(favicon, "favicon");
return this;
}
/**
* Clears the current favicon used in the response.
*
* @return this builder, for chaining
*/
public Builder clearFavicon() {
this.favicon = null;
return this;
@@ -429,6 +516,10 @@ public final class ServerPing {
*/
public static final class SamplePlayer {
public static final SamplePlayer ANONYMOUS = new SamplePlayer(
"Anonymous Player",
new UUID(0L, 0L)
);
private final String name;
private final UUID id;

View File

@@ -76,9 +76,17 @@ public final class ModInfo {
private final String id;
private final String version;
/**
* Creates a new mod info.
*
* @param id the mod identifier
* @param version the mod version
*/
public Mod(String id, String version) {
this.id = Preconditions.checkNotNull(id, "id");
this.version = Preconditions.checkNotNull(version, "version");
Preconditions.checkArgument(id.length() < 128, "mod id is too long");
Preconditions.checkArgument(version.length() < 128, "mod version is too long");
}
public String getId() {

View File

@@ -47,17 +47,25 @@ class MinecraftChannelIdentifierTest {
create("velocity", "test/test2");
}
@Test
void fromIdentifierDefaultNamespace() {
assertEquals("minecraft", from("test").getNamespace());
assertEquals("minecraft", from(":test").getNamespace());
}
@Test
void fromIdentifierAllowsEmptyName() {
from("minecraft:");
from(":");
from("");
}
@Test
void fromIdentifierThrowsOnBadValues() {
assertAll(
() -> assertThrows(IllegalArgumentException.class, () -> from("")),
() -> assertThrows(IllegalArgumentException.class, () -> from(":")),
() -> assertThrows(IllegalArgumentException.class, () -> from(":a")),
() -> assertThrows(IllegalArgumentException.class, () -> from("a:")),
() -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")),
() -> assertThrows(IllegalArgumentException.class, () -> from("he/llo:wor/ld")),
() -> assertThrows(IllegalArgumentException.class, () -> from("hello::"))
);
}
}
}