Merge remote-tracking branch 'upstream/dev/4.0.0'
# Conflicts: # proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
This commit is contained in:
+5
-60
@@ -33,65 +33,6 @@ tasks {
|
||||
|
||||
transform(Log4j2PluginsCacheFileTransformer::class.java)
|
||||
|
||||
// Exclude all the collection types we don"t intend to use
|
||||
exclude("it/unimi/dsi/fastutil/booleans/**")
|
||||
exclude("it/unimi/dsi/fastutil/bytes/**")
|
||||
exclude("it/unimi/dsi/fastutil/chars/**")
|
||||
exclude("it/unimi/dsi/fastutil/doubles/**")
|
||||
exclude("it/unimi/dsi/fastutil/floats/**")
|
||||
exclude("it/unimi/dsi/fastutil/longs/**")
|
||||
exclude("it/unimi/dsi/fastutil/shorts/**")
|
||||
|
||||
// Exclude the fastutil IO utilities - we don"t use them.
|
||||
exclude("it/unimi/dsi/fastutil/io/**")
|
||||
|
||||
// Exclude most of the int types - Object2IntMap have a values() method that returns an
|
||||
// IntCollection, and we need Int2ObjectMap
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Boolean*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Byte*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Char*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Double*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Float*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Int*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Long*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntList*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntHeap*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntOpen*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntRB*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/IntSorted*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*Priority*")
|
||||
exclude("it/unimi/dsi/fastutil/ints/*BigList*")
|
||||
|
||||
// Try to exclude everything BUT Object2Int{LinkedOpen,Open,CustomOpen}HashMap
|
||||
exclude("it/unimi/dsi/fastutil/objects/*ObjectArray*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*ObjectAVL*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object*Big*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Boolean*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Byte*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Char*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Double*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Float*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2IntArray*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2IntAVL*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2IntRB*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Long*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Object*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Reference*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Short*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*ObjectRB*")
|
||||
exclude("it/unimi/dsi/fastutil/objects/*Reference*")
|
||||
|
||||
// Exclude Checker Framework annotations
|
||||
exclude("org/checkerframework/checker/**")
|
||||
|
||||
@@ -129,9 +70,13 @@ fill {
|
||||
|
||||
build {
|
||||
channel = BuildChannel.STABLE
|
||||
versionFamily("3.0.0")
|
||||
versionFamily("4.0.0")
|
||||
version(projectVersion)
|
||||
|
||||
if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) {
|
||||
throw IllegalArgumentException("Version family does not match project version")
|
||||
}
|
||||
|
||||
downloads {
|
||||
register("server:default") {
|
||||
file = tasks.shadowJar.flatMap { it.archiveFile }
|
||||
|
||||
@@ -221,7 +221,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
PluginDescription description = new VelocityPluginDescription(
|
||||
"velocity", version.getName(), version.getVersion(), "The Velocity proxy",
|
||||
version.getName().equals("Velocity") ? VELOCITY_URL : null,
|
||||
ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
|
||||
ImmutableList.of(version.getVendor()), Collections.emptyList(),
|
||||
Collections.emptyList(), null);
|
||||
VelocityPluginContainer container = new VelocityPluginContainer(description);
|
||||
container.setInstance(VelocityVirtualPlugin.INSTANCE);
|
||||
return container;
|
||||
|
||||
@@ -161,14 +161,17 @@ public final class VelocityCommand {
|
||||
.decoration(TextDecoration.BOLD, true)
|
||||
.color(VELOCITY_COLOR)
|
||||
.append(Component.text()
|
||||
.content(version.getVersion())
|
||||
.decoration(TextDecoration.BOLD, false))
|
||||
.content(version.getVersion())
|
||||
.decoration(TextDecoration.BOLD, false))
|
||||
.hoverEvent(Component.translatable("velocity.command.version-offer-copy-version"))
|
||||
.clickEvent(ClickEvent.copyToClipboard(version.getName() + " "
|
||||
+ version.getVersion()))
|
||||
.build();
|
||||
final Component copyright = Component
|
||||
.translatable("velocity.command.version-copyright",
|
||||
Argument.string("vendor", version.getVendor()),
|
||||
Argument.string("name", version.getName()),
|
||||
Argument.component("year", Component.text(LocalDate.now().getYear())));
|
||||
Argument.string("name", version.getName()),
|
||||
Argument.component("year", Component.text(LocalDate.now().getYear())));
|
||||
source.sendMessage(velocity);
|
||||
source.sendMessage(copyright);
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||
@@ -200,6 +201,10 @@ public interface MinecraftSessionHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(ServerboundPlayerLoadedPacket packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(ServerLoginPacket packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+13
@@ -296,6 +296,19 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register and unregister packets are simply forwarded to the client as-is.
|
||||
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PluginMessageUtil.isMcBrand(packet)) {
|
||||
PluginMessagePacket rewritten = PluginMessageUtil
|
||||
.rewriteMinecraftBrand(packet,
|
||||
server.getVersion(), playerConnection.getProtocolVersion());
|
||||
playerConnection.write(rewritten);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
|
||||
// Handled.
|
||||
return true;
|
||||
|
||||
+1
-1
@@ -277,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
|
||||
serverConn.getPlayer().getProtocolVersion()));
|
||||
} else {
|
||||
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
||||
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||
|
||||
if (id == null) {
|
||||
@@ -287,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
// Handling this stuff async means that we should probably pause
|
||||
// the connection while we toss this off into another pool
|
||||
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
||||
this.serverConn.getConnection().setAutoReading(false);
|
||||
this.server.getEventManager()
|
||||
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
|
||||
|
||||
+10
@@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
private final VelocityServer server;
|
||||
private @Nullable MinecraftConnection connection;
|
||||
private boolean hasCompletedJoin = false;
|
||||
private boolean clientLoaded = false; // 1.21.4+
|
||||
private boolean gracefulDisconnect = false;
|
||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||
private final Map<Long, Long> pendingPings = new HashMap<>();
|
||||
@@ -317,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
}
|
||||
}
|
||||
|
||||
public void setClientLoaded(boolean clientLoaded) {
|
||||
this.clientLoaded = clientLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClientLoaded() {
|
||||
return clientLoaded;
|
||||
}
|
||||
|
||||
boolean isGracefulDisconnect() {
|
||||
return gracefulDisconnect;
|
||||
}
|
||||
|
||||
+7
-5
@@ -47,6 +47,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufHolder;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -135,7 +136,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
|
||||
return true;
|
||||
} else if (serverConn != null) {
|
||||
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
||||
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||
|
||||
if (id == null) {
|
||||
@@ -145,6 +145,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
// Handling this stuff async means that we should probably pause
|
||||
// the connection while we toss this off into another pool
|
||||
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
||||
serverConn.getPlayer().getConnection().setAutoReading(false);
|
||||
this.server.getEventManager()
|
||||
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
|
||||
@@ -212,8 +213,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public boolean handle(ServerboundCustomClickActionPacket packet) {
|
||||
if (player.getConnectionInFlight() != null) {
|
||||
player.getConnectionInFlight().ensureConnected().write(packet.retain());
|
||||
VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer();
|
||||
if (serverConnection != null) {
|
||||
serverConnection.ensureConnected().write(packet.retain());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -240,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
if (packet instanceof PluginMessagePacket) {
|
||||
((PluginMessagePacket) packet).retain();
|
||||
if (packet instanceof ByteBufHolder bufHolder) {
|
||||
bufHolder.retain();
|
||||
}
|
||||
smc.write(packet);
|
||||
}
|
||||
|
||||
+16
@@ -26,6 +26,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
|
||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
@@ -52,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
||||
@@ -243,6 +245,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
return true; // will forward onto the server
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(ServerboundPlayerLoadedPacket packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
|
||||
return true;
|
||||
}
|
||||
if (!serverConnection.isClientLoaded()) {
|
||||
serverConnection.setClientLoaded(true);
|
||||
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(SessionPlayerCommandPacket packet) {
|
||||
if (player.getCurrentServer().isEmpty()) {
|
||||
|
||||
@@ -60,9 +60,7 @@ final class CustomHandlerAdapter<F> {
|
||||
UntargetedEventHandler buildUntargetedHandler(final Method method)
|
||||
throws IllegalAccessException {
|
||||
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
|
||||
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn(
|
||||
method.getDeclaringClass(), methodHandlesLookup);
|
||||
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
|
||||
final LambdaType<F> lambdaType = functionType.defineClassesWith(methodHandlesLookup);
|
||||
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
|
||||
final BiFunction<Object, Object, EventTask> handlerFunction =
|
||||
handlerBuilder.apply(invokeFunction);
|
||||
|
||||
@@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager {
|
||||
} else {
|
||||
type = untargetedVoidHandlerType;
|
||||
}
|
||||
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle);
|
||||
return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle);
|
||||
}
|
||||
|
||||
static final class MethodHandlerInfo {
|
||||
|
||||
@@ -46,10 +46,12 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager {
|
||||
|
||||
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
|
||||
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
||||
private final Set<PluginContainer> plugins = new LinkedHashSet<>();
|
||||
private final VelocityServer server;
|
||||
|
||||
public VelocityPluginManager(VelocityServer server) {
|
||||
@@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager {
|
||||
* @param plugin the plugin to register
|
||||
*/
|
||||
public void registerPlugin(PluginContainer plugin) {
|
||||
plugins.add(plugin);
|
||||
pluginsById.put(plugin.getDescription().getId(), plugin);
|
||||
plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin));
|
||||
Optional<?> instance = plugin.getInstance();
|
||||
instance.ifPresent(o -> pluginInstances.put(o, plugin));
|
||||
}
|
||||
@@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager {
|
||||
try {
|
||||
PluginDescription candidate = loader.loadCandidate(path);
|
||||
|
||||
// If we found a duplicate candidate (with the same ID), don't load it.
|
||||
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent(
|
||||
candidate.getId(), candidate);
|
||||
// A plugin claims its own ID plus every ID it provides. If any of those are already
|
||||
// claimed by another candidate, don't load this one.
|
||||
List<String> claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1);
|
||||
claimedIds.add(candidate.getId());
|
||||
claimedIds.addAll(candidate.getProvidedIds());
|
||||
|
||||
if (maybeExistingCandidate != null) {
|
||||
logger.error("Refusing to load plugin at path {} since we already "
|
||||
+ "loaded a plugin with the same ID {} from {}",
|
||||
PluginDescription conflict = null;
|
||||
String conflictingId = null;
|
||||
for (String id : claimedIds) {
|
||||
PluginDescription existing = foundCandidates.get(id);
|
||||
if (existing != null) {
|
||||
conflict = existing;
|
||||
conflictingId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (conflict != null) {
|
||||
logger.error("Refusing to load plugin at path {} since ID {} was already "
|
||||
+ "claimed by a plugin loaded from {}",
|
||||
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
|
||||
candidate.getId(),
|
||||
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
||||
conflictingId,
|
||||
conflict.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String id : claimedIds) {
|
||||
foundCandidates.put(id, candidate);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.error("Unable to load plugin {}", path, e);
|
||||
@@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// foundCandidates indexes each candidate under its ID and any provided IDs, so dedupe before
|
||||
// sorting to avoid loading a plugin more than once.
|
||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
|
||||
new ArrayList<>(foundCandidates.values()));
|
||||
new ArrayList<>(new LinkedHashSet<>(foundCandidates.values())));
|
||||
|
||||
Map<String, PluginDescription> loadedCandidates = new HashMap<>();
|
||||
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
||||
@@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager {
|
||||
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
||||
pluginContainers.put(container, loader.createModule(container));
|
||||
loadedCandidates.put(realPlugin.getId(), realPlugin);
|
||||
realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin));
|
||||
} catch (Throwable e) {
|
||||
logger.error("Can't create module for plugin {}", candidate.getId(), e);
|
||||
}
|
||||
@@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager {
|
||||
|
||||
@Override
|
||||
public Collection<PluginContainer> getPlugins() {
|
||||
return Collections.unmodifiableCollection(pluginsById.values());
|
||||
return Collections.unmodifiableCollection(plugins);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+13
-1
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
@@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
private final @Nullable String url;
|
||||
private final List<String> authors;
|
||||
private final Map<String, PluginDependency> dependencies;
|
||||
private final Collection<String> providedIds;
|
||||
private final Path source;
|
||||
|
||||
/**
|
||||
@@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
* @param url the website for the plugin
|
||||
* @param authors the authors of this plugin
|
||||
* @param dependencies the dependencies for this plugin
|
||||
* @param providedIds the IDs this plugin provides for
|
||||
* @param source the original source for the plugin
|
||||
*/
|
||||
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) {
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Collection<String> providedIds, Path source) {
|
||||
this.id = checkNotNull(id, "id");
|
||||
this.name = Strings.emptyToNull(name);
|
||||
this.version = Strings.emptyToNull(version);
|
||||
@@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
this.url = Strings.emptyToNull(url);
|
||||
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
|
||||
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
|
||||
this.providedIds =
|
||||
providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds);
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
return Optional.ofNullable(dependencies.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getProvidedIds() {
|
||||
return providedIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getSource() {
|
||||
return Optional.ofNullable(source);
|
||||
@@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
+ ", url='" + url + '\''
|
||||
+ ", authors=" + authors
|
||||
+ ", dependencies=" + dependencies
|
||||
+ ", providedIds=" + providedIds
|
||||
+ ", source=" + source
|
||||
+ '}';
|
||||
}
|
||||
|
||||
+10
@@ -78,6 +78,14 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
}
|
||||
}
|
||||
|
||||
for (String providedId : pd.getProvides()) {
|
||||
if (!SerializedPluginDescription.ID_PATTERN.matcher(providedId).matches()) {
|
||||
throw new InvalidPluginException(
|
||||
"Provided ID '" + providedId + "' for plugin '" + pd.getId() + "' is invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return createCandidateDescription(pd, source);
|
||||
}
|
||||
|
||||
@@ -181,6 +189,7 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
description.getUrl(),
|
||||
description.getAuthors(),
|
||||
dependencies,
|
||||
description.getProvides(),
|
||||
source,
|
||||
description.getMain()
|
||||
);
|
||||
@@ -197,6 +206,7 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
description.getUrl().orElse(null),
|
||||
description.getAuthors(),
|
||||
description.getDependencies(),
|
||||
description.getProvidedIds(),
|
||||
description.getSource().orElse(null),
|
||||
mainClass
|
||||
);
|
||||
|
||||
+3
-3
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescription extends VelocityPluginDescription {
|
||||
|
||||
JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source,
|
||||
Class<?> mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, source);
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Collection<String> providedIds, Path source, Class<?> mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, providedIds, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescriptionCandidate extends VelocityPluginDescription {
|
||||
|
||||
JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source,
|
||||
String mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, source);
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Collection<String> providedIds, Path source, String mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, providedIds, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -17,7 +17,6 @@
|
||||
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.graph.Graph;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
@@ -60,8 +59,19 @@ public class PluginDependencyUtils {
|
||||
.allowsSelfLoops(false)
|
||||
.expectedNodeCount(sortedCandidates.size())
|
||||
.build();
|
||||
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(sortedCandidates,
|
||||
PluginDescription::getId);
|
||||
|
||||
// Index candidates by their own ID and any IDs they provide, so a dependency can be satisfied
|
||||
// by a plugin that provides that ID. Real IDs take precedence over provided IDs, and the first
|
||||
// provider of a given ID wins; upstream loading rejects such conflicts before we get here.
|
||||
Map<String, PluginDescription> candidateMap = new HashMap<>();
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
candidateMap.putIfAbsent(description.getId(), description);
|
||||
}
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
for (String provided : description.getProvidedIds()) {
|
||||
candidateMap.putIfAbsent(provided, description);
|
||||
}
|
||||
}
|
||||
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
graph.addNode(description);
|
||||
@@ -69,7 +79,8 @@ public class PluginDependencyUtils {
|
||||
for (PluginDependency dependency : description.getDependencies()) {
|
||||
PluginDescription in = candidateMap.get(dependency.getId());
|
||||
|
||||
if (in != null) {
|
||||
// Guard against self-loops: a plugin may name an ID it itself provides.
|
||||
if (in != null && !in.equals(description)) {
|
||||
graph.putEdge(description, in);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||
@@ -345,6 +346,12 @@ public enum StateRegistry {
|
||||
map(0x13, MINECRAFT_1_21_2, false),
|
||||
map(0x14, MINECRAFT_1_21_6, false),
|
||||
map(0x15, MINECRAFT_26_1, false));
|
||||
serverbound.register(
|
||||
ServerboundPlayerLoadedPacket.class,
|
||||
() -> ServerboundPlayerLoadedPacket.INSTANCE,
|
||||
map(0x2A, MINECRAFT_1_21_4, false),
|
||||
map(0x2B, MINECRAFT_1_21_6, false),
|
||||
map(0x2C, MINECRAFT_26_1, false));
|
||||
serverbound.register(
|
||||
PluginMessagePacket.class,
|
||||
PluginMessagePacket::new,
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
public class ServerboundPlayerLoadedPacket implements MinecraftPacket {
|
||||
|
||||
public static final ServerboundPlayerLoadedPacket INSTANCE = new ServerboundPlayerLoadedPacket();
|
||||
|
||||
private ServerboundPlayerLoadedPacket() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ velocity.command.glist-view-all=To view all players on servers, use /glist all.
|
||||
velocity.command.reload-success=Velocity configuration successfully reloaded.
|
||||
velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details.
|
||||
velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3.
|
||||
velocity.command.version-offer-copy-version=Click to copy version to clipboard
|
||||
velocity.command.no-plugins=There are no plugins currently installed.
|
||||
velocity.command.plugins-list=Plugins: <arg:0>
|
||||
velocity.command.plugin-tooltip-website=Website: <arg:0>
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2026 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
|
||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ClientConfigSessionHandlerTest {
|
||||
|
||||
private VelocityServer server;
|
||||
private ConnectedPlayer player;
|
||||
private ClientConfigSessionHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
server = mock(VelocityServer.class);
|
||||
player = mock(ConnectedPlayer.class);
|
||||
handler = new ClientConfigSessionHandler(server, player);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// nothing to clean up; each test manages its own ByteBufs
|
||||
}
|
||||
|
||||
private ServerboundCustomClickActionPacket makePacket() {
|
||||
ByteBuf frame = Unpooled.buffer().writeByte(0);
|
||||
ServerboundCustomClickActionPacket pkt = new ServerboundCustomClickActionPacket();
|
||||
pkt.replace(frame.readRetainedSlice(frame.readableBytes()));
|
||||
return pkt;
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleForwardsToInFlightServer() {
|
||||
VelocityServerConnection inFlight = mock(VelocityServerConnection.class);
|
||||
MinecraftConnection backend = mock(MinecraftConnection.class);
|
||||
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(inFlight);
|
||||
when(inFlight.ensureConnected()).thenReturn(backend);
|
||||
|
||||
ServerboundCustomClickActionPacket pkt = makePacket();
|
||||
assertTrue(handler.handle(pkt));
|
||||
verify(backend).write(pkt);
|
||||
ReferenceCountUtil.release(pkt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleForwardsToConnectedServerWhenInFlightIsNull() {
|
||||
VelocityServerConnection connected = mock(VelocityServerConnection.class);
|
||||
MinecraftConnection backend = mock(MinecraftConnection.class);
|
||||
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(connected);
|
||||
when(connected.ensureConnected()).thenReturn(backend);
|
||||
|
||||
ServerboundCustomClickActionPacket pkt = makePacket();
|
||||
assertTrue(handler.handle(pkt));
|
||||
verify(backend).write(pkt);
|
||||
ReferenceCountUtil.release(pkt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleReturnsFalseWhenNoServer() {
|
||||
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(null);
|
||||
|
||||
ServerboundCustomClickActionPacket pkt = makePacket();
|
||||
assertFalse(handler.handle(pkt));
|
||||
ReferenceCountUtil.release(pkt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleGenericRetainsAndForwards() {
|
||||
VelocityServerConnection connected = mock(VelocityServerConnection.class);
|
||||
MinecraftConnection backend = mock(MinecraftConnection.class);
|
||||
BackendConnectionPhase phase = mock(BackendConnectionPhase.class);
|
||||
when(player.getConnectedServer()).thenReturn(connected);
|
||||
when(connected.getConnection()).thenReturn(backend);
|
||||
when(connected.getPhase()).thenReturn(phase);
|
||||
when(phase.consideredComplete()).thenReturn(true);
|
||||
|
||||
ServerboundCustomClickActionPacket pkt = makePacket();
|
||||
int refBefore = pkt.refCnt();
|
||||
|
||||
handler.handleGeneric(pkt);
|
||||
|
||||
// retain() was called (+1) before write
|
||||
assertEquals(refBefore + 1, pkt.refCnt());
|
||||
verify(backend).write(pkt);
|
||||
ReferenceCountUtil.release(pkt);
|
||||
}
|
||||
}
|
||||
+31
-1
@@ -44,6 +44,15 @@ class PluginDependencyUtilsTest {
|
||||
private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval",
|
||||
new PluginDependency("circle", "", false));
|
||||
|
||||
// "provider" is loaded from a real ID but provides the virtual ID "some-api"; "consumer" and
|
||||
// "zdependent" depend on it only through that provided ID / a chain that reaches it.
|
||||
private static final PluginDescription PROVIDES_API = providingDescription("provider",
|
||||
ImmutableList.of("some-api"));
|
||||
private static final PluginDescription DEPENDS_ON_PROVIDED = providingDescription("consumer",
|
||||
ImmutableList.of(), new PluginDependency("some-api", null, false));
|
||||
private static final PluginDescription DEPENDS_ON_CONSUMER = testDescription("zdependent",
|
||||
new PluginDependency("consumer", null, false));
|
||||
|
||||
@Test
|
||||
void sortCandidatesTrivial() throws Exception {
|
||||
List<PluginDescription> descriptionList = new ArrayList<>();
|
||||
@@ -96,10 +105,31 @@ class PluginDependencyUtilsTest {
|
||||
assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesResolvesProvidedDependency() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_PROVIDED, PROVIDES_API);
|
||||
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesResolvesTransitiveProvidedDependency() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_CONSUMER, DEPENDS_ON_PROVIDED,
|
||||
PROVIDES_API);
|
||||
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED,
|
||||
DEPENDS_ON_CONSUMER);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
private static PluginDescription testDescription(String id, PluginDependency... dependencies) {
|
||||
return providingDescription(id, ImmutableList.of(), dependencies);
|
||||
}
|
||||
|
||||
private static PluginDescription providingDescription(String id, List<String> provides,
|
||||
PluginDependency... dependencies) {
|
||||
return new VelocityPluginDescription(
|
||||
id, "tuxed", "0.1", null, null, ImmutableList.of(),
|
||||
ImmutableList.copyOf(dependencies), null
|
||||
ImmutableList.copyOf(dependencies), provides, null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user