Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-17 05:20:14 +01:00
Merge branch 'dev/1.1.0' into dev/2.0.0
# Conflicts: # api/build.gradle # api/src/main/java/com/velocitypowered/api/proxy/connection/Player.java # api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java # build.gradle # proxy/build.gradle # proxy/src/main/java/com/velocitypowered/proxy/Velocity.java # proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java # proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java # proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java # proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java # proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java # proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundAvailableCommandsPacket.java # proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundJoinGamePacket.java # proxy/src/main/java/com/velocitypowered/proxy/network/packet/clientbound/ClientboundRespawnPacket.java # proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/MinecraftPacket.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/EncryptionResponse.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HeaderAndFooter.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessage.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusPing.java # proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusRequest.java # proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java # proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java # proxy/src/test/java/com/velocitypowered/proxy/command/CommandManagerTests.java
Dieser Commit ist enthalten in:
Commit
699147c916
32
.github/workflows/gradle.yml
vendored
Normale Datei
32
.github/workflows/gradle.yml
vendored
Normale Datei
@ -0,0 +1,32 @@
|
|||||||
|
# This workflow will build a Java project with Gradle
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||||
|
|
||||||
|
name: Java CI with Gradle
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-8:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
||||||
|
build-11:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
@ -28,9 +28,7 @@ sure that you are properly adhering to the code style.
|
|||||||
To reduce bugs and ensure code quality, we run the following tools on all commits
|
To reduce bugs and ensure code quality, we run the following tools on all commits
|
||||||
and pull requests:
|
and pull requests:
|
||||||
|
|
||||||
* [Checker Framework](https://checkerframework.org/): an enhancement to Java's type
|
* [SpotBugs](https://spotbugs.github.io/): ensures that common errors do not
|
||||||
system that is designed to help catch bugs. Velocity runs the _Nullness Checker_
|
get into the codebase. The build will fail if SpotBugs finds an issue.
|
||||||
and the _Optional Checker_. The build will fail if Checker Framework notices an
|
|
||||||
issue.
|
|
||||||
* [Checkstyle](http://checkstyle.sourceforge.net/): ensures that your code is
|
* [Checkstyle](http://checkstyle.sourceforge.net/): ensures that your code is
|
||||||
correctly formatted. The build will fail if Checkstyle detects a problem.
|
correctly formatted. The build will fail if Checkstyle detects a problem.
|
||||||
|
@ -81,9 +81,18 @@ javadoc {
|
|||||||
|
|
||||||
// Disable the crazy super-strict doclint tool in Java 8
|
// Disable the crazy super-strict doclint tool in Java 8
|
||||||
options.addStringOption('Xdoclint:none', '-quiet')
|
options.addStringOption('Xdoclint:none', '-quiet')
|
||||||
|
|
||||||
// Mark sources as Java 8 source compatible
|
// Mark sources as Java 8 source compatible
|
||||||
options.source = '8'
|
options.source = '8'
|
||||||
|
|
||||||
|
// Remove 'undefined' from seach paths when generating javadoc for a non-modular project (JDK-8215291)
|
||||||
|
if (JavaVersion.current() >= JavaVersion.VERSION_1_9 && JavaVersion.current() < JavaVersion.VERSION_12) {
|
||||||
|
options.addBooleanOption('-no-module-directories', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.velocitypowered.api.event.player;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is fired when a client ({@link Player}) sends a plugin message through the
|
||||||
|
* register channel.
|
||||||
|
*/
|
||||||
|
public final class PlayerChannelRegisterEvent {
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final List<ChannelIdentifier> channels;
|
||||||
|
|
||||||
|
public PlayerChannelRegisterEvent(Player player, List<ChannelIdentifier> channels) {
|
||||||
|
this.player = Preconditions.checkNotNull(player, "player");
|
||||||
|
this.channels = Preconditions.checkNotNull(channels, "channels");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getPlayer() {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChannelIdentifier> getChannels() {
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PlayerChannelRegisterEvent{"
|
||||||
|
+ "player=" + player
|
||||||
|
+ ", channels=" + channels
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
*/
|
*/
|
||||||
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
||||||
|
|
||||||
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]*");
|
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9/\\-_]*");
|
||||||
|
|
||||||
private final String namespace;
|
private final String namespace;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -227,6 +227,11 @@ public final class ServerPing {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder clearFavicon() {
|
||||||
|
this.favicon = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses the information from this builder to create a new {@link ServerPing} instance. The
|
* Uses the information from this builder to create a new {@link ServerPing} instance. The
|
||||||
* builder can be re-used after this event has been called.
|
* builder can be re-used after this event has been called.
|
||||||
|
@ -35,6 +35,11 @@ class MinecraftChannelIdentifierTest {
|
|||||||
assertEquals(expected, MinecraftChannelIdentifier.from("velocity:test"));
|
assertEquals(expected, MinecraftChannelIdentifier.from("velocity:test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createAllowsSlashes() {
|
||||||
|
create("velocity", "test/test2");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void fromIdentifierThrowsOnBadValues() {
|
void fromIdentifierThrowsOnBadValues() {
|
||||||
assertAll(
|
assertAll(
|
||||||
|
@ -20,11 +20,11 @@ allprojects {
|
|||||||
|
|
||||||
ext {
|
ext {
|
||||||
// dependency versions
|
// dependency versions
|
||||||
adventureVersion = '4.1.1'
|
adventureVersion = '4.5.0'
|
||||||
junitVersion = '5.7.0'
|
junitVersion = '5.7.0'
|
||||||
slf4jVersion = '1.7.30'
|
slf4jVersion = '1.7.30'
|
||||||
log4jVersion = '2.13.3'
|
log4jVersion = '2.13.3'
|
||||||
nettyVersion = '4.1.56.Final'
|
nettyVersion = '4.1.59.Final'
|
||||||
guavaVersion = '30.0-jre'
|
guavaVersion = '30.0-jre'
|
||||||
checkerFrameworkVersion = '3.6.1'
|
checkerFrameworkVersion = '3.6.1'
|
||||||
configurateVersion = '4.0.0-SNAPSHOT'
|
configurateVersion = '4.0.0-SNAPSHOT'
|
||||||
|
@ -55,13 +55,13 @@ dependencies {
|
|||||||
implementation "io.netty:netty-handler:${nettyVersion}"
|
implementation "io.netty:netty-handler:${nettyVersion}"
|
||||||
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}"
|
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}"
|
||||||
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
||||||
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch64"
|
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch_64"
|
||||||
implementation "io.netty:netty-resolver-dns:${nettyVersion}"
|
|
||||||
|
|
||||||
implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||||
implementation "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
implementation "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
||||||
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
|
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
|
||||||
implementation "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
implementation "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
||||||
|
implementation "org.apache.logging.log4j:log4j-jul:${log4jVersion}"
|
||||||
|
|
||||||
implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
||||||
implementation 'net.minecrell:terminalconsoleappender:1.2.0'
|
implementation 'net.minecrell:terminalconsoleappender:1.2.0'
|
||||||
@ -76,7 +76,7 @@ dependencies {
|
|||||||
implementation 'com.spotify:completable-futures:0.3.3'
|
implementation 'com.spotify:completable-futures:0.3.3'
|
||||||
|
|
||||||
implementation 'com.electronwill.night-config:toml:3.6.3'
|
implementation 'com.electronwill.night-config:toml:3.6.3'
|
||||||
|
implementation 'org.bstats:bstats-base:2.2.0'
|
||||||
implementation 'org.lanternpowered:lmbda:2.0.0-SNAPSHOT'
|
implementation 'org.lanternpowered:lmbda:2.0.0-SNAPSHOT'
|
||||||
|
|
||||||
implementation 'com.github.ben-manes.caffeine:caffeine:2.8.8'
|
implementation 'com.github.ben-manes.caffeine:caffeine:2.8.8'
|
||||||
@ -117,7 +117,6 @@ shadowJar {
|
|||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2Float*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Object2Float*'
|
||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntArray*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntArray*'
|
||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntAVL*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntAVL*'
|
||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object*OpenCustom*'
|
|
||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntRB*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntRB*'
|
||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2Long*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Object2Long*'
|
||||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2Object*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Object2Object*'
|
||||||
@ -127,12 +126,10 @@ shadowJar {
|
|||||||
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
|
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
|
||||||
exclude 'it/unimi/dsi/fastutil/shorts/**'
|
exclude 'it/unimi/dsi/fastutil/shorts/**'
|
||||||
exclude 'org/checkerframework/checker/**'
|
exclude 'org/checkerframework/checker/**'
|
||||||
|
|
||||||
|
relocate 'org.bstats', 'com.velocitypowered.proxy.bstats'
|
||||||
}
|
}
|
||||||
|
|
||||||
artifacts {
|
artifacts {
|
||||||
archives shadowJar
|
archives shadowJar
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
@ -1,82 +1,67 @@
|
|||||||
package com.velocitypowered.proxy;
|
package com.velocitypowered.proxy;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.File;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.nio.file.Paths;
|
||||||
import java.io.Writer;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.GZIPOutputStream;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.asynchttpclient.ListenableFuture;
|
import org.bstats.MetricsBase;
|
||||||
import org.asynchttpclient.Response;
|
import org.bstats.charts.CustomChart;
|
||||||
|
import org.bstats.charts.DrilldownPie;
|
||||||
|
import org.bstats.charts.SimplePie;
|
||||||
|
import org.bstats.charts.SingleLineChart;
|
||||||
|
import org.bstats.config.MetricsConfig;
|
||||||
|
import org.bstats.json.JsonObjectBuilder;
|
||||||
|
|
||||||
/**
|
|
||||||
* bStats collects some data for plugin authors.
|
|
||||||
* <p/>
|
|
||||||
* Check out https://bStats.org/ to learn more about bStats!
|
|
||||||
*/
|
|
||||||
public class Metrics {
|
public class Metrics {
|
||||||
|
|
||||||
// The version of this bStats class
|
private MetricsBase metricsBase;
|
||||||
private static final int B_STATS_METRICS_REVISION = 2;
|
|
||||||
|
|
||||||
// The url to which the data is sent
|
private Metrics(Logger logger, int serviceId, boolean defaultEnabled) {
|
||||||
private static final String URL = "https://bstats.org/submitData/server-implementation";
|
File configFile = Paths.get("plugins").resolve("bStats").resolve("config.txt").toFile();
|
||||||
|
MetricsConfig config;
|
||||||
|
try {
|
||||||
|
config = new MetricsConfig(configFile, defaultEnabled);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Failed to create bStats config", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The logger for the failed requests
|
metricsBase = new MetricsBase(
|
||||||
private static final Logger logger = LogManager.getLogger(Metrics.class);
|
"server-implementation",
|
||||||
|
config.getServerUUID(),
|
||||||
|
serviceId,
|
||||||
|
config.isEnabled(),
|
||||||
|
this::appendPlatformData,
|
||||||
|
jsonObjectBuilder -> { /* NOP */ },
|
||||||
|
null,
|
||||||
|
() -> true,
|
||||||
|
logger::warn,
|
||||||
|
logger::info,
|
||||||
|
config.isLogErrorsEnabled(),
|
||||||
|
config.isLogSentDataEnabled(),
|
||||||
|
config.isLogResponseStatusTextEnabled()
|
||||||
|
);
|
||||||
|
|
||||||
// Should failed requests be logged?
|
if (!config.didExistBefore()) {
|
||||||
private boolean logFailedRequests = false;
|
// Send an info message when the bStats config file gets created for the first time
|
||||||
|
logger.info("Velocity and some of its plugins collect metrics"
|
||||||
// The name of the server software
|
+ " and send them to bStats (https://bStats.org).");
|
||||||
private final String name;
|
logger.info("bStats collects some basic information for plugin"
|
||||||
|
+ " authors, like how many people use");
|
||||||
// The plugin ID for the server software as assigned by bStats.
|
logger.info("their plugin and their total player count."
|
||||||
private final int pluginId;
|
+ " It's recommended to keep bStats enabled, but");
|
||||||
|
logger.info("if you're not comfortable with this, you can opt-out"
|
||||||
// The uuid of the server
|
+ " by editing the config.txt file in");
|
||||||
private final String serverUuid;
|
logger.info("the '/plugins/bStats/' folder and setting enabled to false.");
|
||||||
|
}
|
||||||
// A list with all custom charts
|
|
||||||
private final List<CustomChart> charts = new ArrayList<>();
|
|
||||||
|
|
||||||
private final VelocityServer server;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
* @param name The name of the server software.
|
|
||||||
* @param pluginId The plugin ID for the server software as assigned by bStats.
|
|
||||||
* @param serverUuid The uuid of the server.
|
|
||||||
* @param logFailedRequests Whether failed requests should be logged or not.
|
|
||||||
* @param server The Velocity server instance.
|
|
||||||
*/
|
|
||||||
private Metrics(String name, int pluginId, String serverUuid, boolean logFailedRequests,
|
|
||||||
VelocityServer server) {
|
|
||||||
this.name = name;
|
|
||||||
this.pluginId = pluginId;
|
|
||||||
this.serverUuid = serverUuid;
|
|
||||||
this.logFailedRequests = logFailedRequests;
|
|
||||||
this.server = server;
|
|
||||||
|
|
||||||
// Start submitting the data
|
|
||||||
startSubmitting();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,544 +70,69 @@ public class Metrics {
|
|||||||
* @param chart The chart to add.
|
* @param chart The chart to add.
|
||||||
*/
|
*/
|
||||||
public void addCustomChart(CustomChart chart) {
|
public void addCustomChart(CustomChart chart) {
|
||||||
if (chart == null) {
|
metricsBase.addCustomChart(chart);
|
||||||
throw new IllegalArgumentException("Chart cannot be null!");
|
|
||||||
}
|
|
||||||
charts.add(chart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void appendPlatformData(JsonObjectBuilder builder) {
|
||||||
* Starts the Scheduler which submits our data every 30 minutes.
|
builder.appendField("osName", System.getProperty("os.name"));
|
||||||
*/
|
builder.appendField("osArch", System.getProperty("os.arch"));
|
||||||
private void startSubmitting() {
|
builder.appendField("osVersion", System.getProperty("os.version"));
|
||||||
final Timer timer = new Timer(true);
|
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
|
||||||
timer.scheduleAtFixedRate(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
submitData();
|
|
||||||
}
|
|
||||||
}, 1000, 1000 * 60 * 30);
|
|
||||||
// Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough
|
|
||||||
// time to start.
|
|
||||||
//
|
|
||||||
// WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted!
|
|
||||||
// WARNING: Just don't do it!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the plugin specific data.
|
|
||||||
*
|
|
||||||
* @return The plugin specific data.
|
|
||||||
*/
|
|
||||||
private JsonObject getPluginData() {
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
|
|
||||||
data.addProperty("pluginName", name); // Append the name of the server software
|
|
||||||
data.addProperty("id", pluginId);
|
|
||||||
data.addProperty("metricsRevision", B_STATS_METRICS_REVISION);
|
|
||||||
JsonArray customCharts = new JsonArray();
|
|
||||||
for (CustomChart customChart : charts) {
|
|
||||||
// Add the data of the custom charts
|
|
||||||
JsonObject chart = customChart.getRequestJsonObject();
|
|
||||||
if (chart == null) { // If the chart is null, we skip it
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
customCharts.add(chart);
|
|
||||||
}
|
|
||||||
data.add("customCharts", customCharts);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the server specific data.
|
|
||||||
*
|
|
||||||
* @return The server specific data.
|
|
||||||
*/
|
|
||||||
private JsonObject getServerData() {
|
|
||||||
// OS specific data
|
|
||||||
String osName = System.getProperty("os.name");
|
|
||||||
String osArch = System.getProperty("os.arch");
|
|
||||||
String osVersion = System.getProperty("os.version");
|
|
||||||
int coreCount = Runtime.getRuntime().availableProcessors();
|
|
||||||
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
|
|
||||||
data.addProperty("serverUUID", serverUuid);
|
|
||||||
|
|
||||||
data.addProperty("osName", osName);
|
|
||||||
data.addProperty("osArch", osArch);
|
|
||||||
data.addProperty("osVersion", osVersion);
|
|
||||||
data.addProperty("coreCount", coreCount);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects the data and sends it afterwards.
|
|
||||||
*/
|
|
||||||
private void submitData() {
|
|
||||||
final JsonObject data = getServerData();
|
|
||||||
|
|
||||||
JsonArray pluginData = new JsonArray();
|
|
||||||
pluginData.add(getPluginData());
|
|
||||||
data.add("plugins", pluginData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// We are still in the Thread of the timer, so nothing get blocked :)
|
|
||||||
sendData(data);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Something went wrong! :(
|
|
||||||
if (logFailedRequests) {
|
|
||||||
logger.warn("Could not submit stats of {}", name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the data to the bStats server.
|
|
||||||
*
|
|
||||||
* @param data The data to send.
|
|
||||||
* @throws Exception If the request failed.
|
|
||||||
*/
|
|
||||||
private void sendData(JsonObject data) throws Exception {
|
|
||||||
if (data == null) {
|
|
||||||
throw new IllegalArgumentException("Data cannot be null!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compress the data to save bandwidth
|
|
||||||
ListenableFuture<Response> future = server.getAsyncHttpClient()
|
|
||||||
.preparePost(URL)
|
|
||||||
.addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip")
|
|
||||||
.addHeader(HttpHeaderNames.ACCEPT, "application/json")
|
|
||||||
.addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
|
||||||
.setBody(createResponseBody(data))
|
|
||||||
.execute();
|
|
||||||
future.addListener(() -> {
|
|
||||||
if (logFailedRequests) {
|
|
||||||
try {
|
|
||||||
Response r = future.get();
|
|
||||||
if (r.getStatusCode() != 429) {
|
|
||||||
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
|
||||||
r.getStatusCode());
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
logger.error("Unable to send metrics to bStats", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] createResponseBody(JsonObject object) throws IOException {
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
||||||
try (Writer writer =
|
|
||||||
new BufferedWriter(
|
|
||||||
new OutputStreamWriter(
|
|
||||||
new GZIPOutputStream(os), StandardCharsets.UTF_8
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
VelocityServer.GENERAL_GSON.toJson(object, writer);
|
|
||||||
}
|
|
||||||
return os.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom chart.
|
|
||||||
*/
|
|
||||||
public abstract static class CustomChart {
|
|
||||||
|
|
||||||
// The id of the chart
|
|
||||||
final String chartId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
*/
|
|
||||||
CustomChart(String chartId) {
|
|
||||||
if (chartId == null || chartId.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("ChartId cannot be null or empty!");
|
|
||||||
}
|
|
||||||
this.chartId = chartId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonObject getRequestJsonObject() {
|
|
||||||
JsonObject chart = new JsonObject();
|
|
||||||
chart.addProperty("chartId", chartId);
|
|
||||||
try {
|
|
||||||
JsonObject data = getChartData();
|
|
||||||
if (data == null) {
|
|
||||||
// If the data is null we don't send the chart.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
chart.add("data", data);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return chart;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract JsonObject getChartData() throws Exception;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom simple pie.
|
|
||||||
*/
|
|
||||||
public static class SimplePie extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<String> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public SimplePie(String chartId, Callable<String> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JsonObject getChartData() throws Exception {
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
String value = callable.call();
|
|
||||||
if (value == null || value.isEmpty()) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
data.addProperty("value", value);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom advanced pie.
|
|
||||||
*/
|
|
||||||
public static class AdvancedPie extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<Map<String, Integer>> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JsonObject getChartData() throws Exception {
|
|
||||||
Map<String, Integer> map = callable.call();
|
|
||||||
if (map == null || map.isEmpty()) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
JsonObject values = new JsonObject();
|
|
||||||
boolean allSkipped = true;
|
|
||||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
|
||||||
if (entry.getValue() == 0) {
|
|
||||||
continue; // Skip this invalid
|
|
||||||
}
|
|
||||||
allSkipped = false;
|
|
||||||
values.addProperty(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
if (allSkipped) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
data.add("values", values);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom drilldown pie.
|
|
||||||
*/
|
|
||||||
public static class DrilldownPie extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<Map<String, Map<String, Integer>>> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JsonObject getChartData() throws Exception {
|
|
||||||
Map<String, Map<String, Integer>> map = callable.call();
|
|
||||||
if (map == null || map.isEmpty()) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
boolean reallyAllSkipped = true;
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
JsonObject values = new JsonObject();
|
|
||||||
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
|
|
||||||
JsonObject value = new JsonObject();
|
|
||||||
boolean allSkipped = true;
|
|
||||||
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
|
|
||||||
value.addProperty(valueEntry.getKey(), valueEntry.getValue());
|
|
||||||
allSkipped = false;
|
|
||||||
}
|
|
||||||
if (!allSkipped) {
|
|
||||||
reallyAllSkipped = false;
|
|
||||||
values.add(entryValues.getKey(), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (reallyAllSkipped) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
data.add("values", values);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom single line chart.
|
|
||||||
*/
|
|
||||||
public static class SingleLineChart extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<Integer> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JsonObject getChartData() throws Exception {
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
int value = callable.call();
|
|
||||||
if (value == 0) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
data.addProperty("value", value);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom multi line chart.
|
|
||||||
*/
|
|
||||||
public static class MultiLineChart extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<Map<String, Integer>> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JsonObject getChartData() throws Exception {
|
|
||||||
Map<String, Integer> map = callable.call();
|
|
||||||
if (map == null || map.isEmpty()) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
JsonObject values = new JsonObject();
|
|
||||||
boolean allSkipped = true;
|
|
||||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
|
||||||
if (entry.getValue() == 0) {
|
|
||||||
continue; // Skip this invalid
|
|
||||||
}
|
|
||||||
allSkipped = false;
|
|
||||||
values.addProperty(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
if (allSkipped) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
data.add("values", values);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom simple bar chart.
|
|
||||||
*/
|
|
||||||
public static class SimpleBarChart extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<Map<String, Integer>> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JsonObject getChartData() throws Exception {
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
JsonObject values = new JsonObject();
|
|
||||||
Map<String, Integer> map = callable.call();
|
|
||||||
if (map == null || map.isEmpty()) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
|
||||||
JsonArray categoryValues = new JsonArray();
|
|
||||||
categoryValues.add(entry.getValue());
|
|
||||||
values.add(entry.getKey(), categoryValues);
|
|
||||||
}
|
|
||||||
data.add("values", values);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a custom advanced bar chart.
|
|
||||||
*/
|
|
||||||
public static class AdvancedBarChart extends CustomChart {
|
|
||||||
|
|
||||||
private final Callable<Map<String, int[]>> callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor.
|
|
||||||
*
|
|
||||||
* @param chartId The id of the chart.
|
|
||||||
* @param callable The callable which is used to request the chart data.
|
|
||||||
*/
|
|
||||||
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
|
||||||
super(chartId);
|
|
||||||
this.callable = callable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected JsonObject getChartData() throws Exception {
|
|
||||||
JsonObject values = new JsonObject();
|
|
||||||
Map<String, int[]> map = callable.call();
|
|
||||||
if (map == null || map.isEmpty()) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
boolean allSkipped = true;
|
|
||||||
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
|
||||||
if (entry.getValue().length == 0) {
|
|
||||||
continue; // Skip this invalid
|
|
||||||
}
|
|
||||||
allSkipped = false;
|
|
||||||
JsonArray categoryValues = new JsonArray();
|
|
||||||
for (int categoryValue : entry.getValue()) {
|
|
||||||
categoryValues.add(categoryValue);
|
|
||||||
}
|
|
||||||
values.add(entry.getKey(), categoryValues);
|
|
||||||
}
|
|
||||||
if (allSkipped) {
|
|
||||||
// Null = skip the chart
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
JsonObject data = new JsonObject();
|
|
||||||
data.add("values", values);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class VelocityMetrics {
|
static class VelocityMetrics {
|
||||||
|
|
||||||
|
private static final Logger logger = LogManager.getLogger(Metrics.class);
|
||||||
|
|
||||||
static void startMetrics(VelocityServer server, VelocityConfiguration.Metrics metricsConfig) {
|
static void startMetrics(VelocityServer server, VelocityConfiguration.Metrics metricsConfig) {
|
||||||
if (!metricsConfig.isFromConfig()) {
|
Metrics metrics = new Metrics(logger, 4752, metricsConfig.isEnabled());
|
||||||
// Log an informational message.
|
|
||||||
logger.info("Velocity collects metrics and sends them to bStats (https://bstats.org).");
|
|
||||||
logger.info("bStats collects some basic information like how many people use Velocity and");
|
|
||||||
logger.info("their player count. This has no impact on performance and this data does not");
|
|
||||||
logger.info("identify your server in any way. However, you may opt-out by editing your");
|
|
||||||
logger.info("velocity.toml and setting enabled = false in the [metrics] section.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the data
|
metrics.addCustomChart(
|
||||||
String serverUuid = metricsConfig.getId();
|
new SingleLineChart("players", server::getPlayerCount)
|
||||||
boolean logFailedRequests = metricsConfig.isLogFailure();
|
);
|
||||||
// Only start Metrics, if it's enabled in the config
|
metrics.addCustomChart(
|
||||||
if (metricsConfig.isEnabled()) {
|
new SingleLineChart("managed_servers", () -> server.getAllServers().size())
|
||||||
Metrics metrics = new Metrics("Velocity", 4752, serverUuid, logFailedRequests, server);
|
);
|
||||||
|
metrics.addCustomChart(
|
||||||
|
new SimplePie("online_mode",
|
||||||
|
() -> server.getConfiguration().isOnlineMode() ? "online" : "offline")
|
||||||
|
);
|
||||||
|
metrics.addCustomChart(new SimplePie("velocity_version",
|
||||||
|
() -> server.getVersion().getVersion()));
|
||||||
|
|
||||||
metrics.addCustomChart(
|
metrics.addCustomChart(new DrilldownPie("java_version", () -> {
|
||||||
new Metrics.SingleLineChart("players", server::getPlayerCount)
|
Map<String, Map<String, Integer>> map = new HashMap<>();
|
||||||
);
|
String javaVersion = System.getProperty("java.version");
|
||||||
metrics.addCustomChart(
|
Map<String, Integer> entry = new HashMap<>();
|
||||||
new Metrics.SingleLineChart("managed_servers", () -> server.getAllServers().size())
|
entry.put(javaVersion, 1);
|
||||||
);
|
|
||||||
metrics.addCustomChart(
|
|
||||||
new Metrics.SimplePie("online_mode",
|
|
||||||
() -> server.getConfiguration().isOnlineMode() ? "online" : "offline")
|
|
||||||
);
|
|
||||||
metrics.addCustomChart(new Metrics.SimplePie("velocity_version",
|
|
||||||
() -> server.getVersion().getVersion()));
|
|
||||||
|
|
||||||
metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
|
// http://openjdk.java.net/jeps/223
|
||||||
Map<String, Map<String, Integer>> map = new HashMap<>();
|
// Java decided to change their versioning scheme and in doing so modified the
|
||||||
String javaVersion = System.getProperty("java.version");
|
// java.version system property to return $major[.$minor][.$security][-ea], as opposed to
|
||||||
Map<String, Integer> entry = new HashMap<>();
|
// 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1",
|
||||||
entry.put(javaVersion, 1);
|
// otherwise, 9+
|
||||||
|
String majorVersion = javaVersion.split("\\.")[0];
|
||||||
|
String release;
|
||||||
|
|
||||||
// http://openjdk.java.net/jeps/223
|
int indexOf = javaVersion.lastIndexOf('.');
|
||||||
// Java decided to change their versioning scheme and in doing so modified the
|
|
||||||
// java.version system property to return $major[.$minor][.$security][-ea], as opposed to
|
|
||||||
// 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1",
|
|
||||||
// otherwise, 9+
|
|
||||||
String majorVersion = javaVersion.split("\\.")[0];
|
|
||||||
String release;
|
|
||||||
|
|
||||||
int indexOf = javaVersion.lastIndexOf('.');
|
if (majorVersion.equals("1")) {
|
||||||
|
release = "Java " + javaVersion.substring(0, indexOf);
|
||||||
if (majorVersion.equals("1")) {
|
} else {
|
||||||
release = "Java " + javaVersion.substring(0, indexOf);
|
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
|
||||||
} else {
|
// would it valid strings for the major may potentially include values such as -ea to
|
||||||
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
|
// denote a pre release
|
||||||
// would it valid strings for the major may potentially include values such as -ea to
|
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
|
||||||
// denote a pre release
|
if (versionMatcher.find()) {
|
||||||
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
|
majorVersion = versionMatcher.group(0);
|
||||||
if (versionMatcher.find()) {
|
|
||||||
majorVersion = versionMatcher.group(0);
|
|
||||||
}
|
|
||||||
release = "Java " + majorVersion;
|
|
||||||
}
|
}
|
||||||
map.put(release, entry);
|
release = "Java " + majorVersion;
|
||||||
|
}
|
||||||
return map;
|
map.put(release, entry);
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
@ -8,9 +8,12 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
public class Velocity {
|
public class Velocity {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(Velocity.class);
|
private static final Logger logger;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||||
|
logger = LogManager.getLogger(Velocity.class);
|
||||||
|
|
||||||
// By default, Netty allocates 16MiB arenas for the PooledByteBufAllocator. This is too much
|
// By default, Netty allocates 16MiB arenas for the PooledByteBufAllocator. This is too much
|
||||||
// memory for Minecraft, which imposes a maximum packet size of 2MiB! We'll use 4MiB as a more
|
// memory for Minecraft, which imposes a maximum packet size of 2MiB! We'll use 4MiB as a more
|
||||||
// sane default.
|
// sane default.
|
||||||
|
@ -59,6 +59,9 @@ public interface CommandNodeFactory<T extends Command> {
|
|||||||
(context, builder) -> {
|
(context, builder) -> {
|
||||||
I invocation = createInvocation(context);
|
I invocation = createInvocation(context);
|
||||||
|
|
||||||
|
if (!command.hasPermission(invocation)) {
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
return command.suggestAsync(invocation).thenApply(values -> {
|
return command.suggestAsync(invocation).thenApply(values -> {
|
||||||
for (String value : values) {
|
for (String value : values) {
|
||||||
builder.suggest(value);
|
builder.suggest(value);
|
||||||
|
@ -38,6 +38,7 @@ import net.kyori.adventure.text.TextComponent;
|
|||||||
import net.kyori.adventure.text.event.ClickEvent;
|
import net.kyori.adventure.text.event.ClickEvent;
|
||||||
import net.kyori.adventure.text.event.HoverEvent;
|
import net.kyori.adventure.text.event.HoverEvent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
import net.kyori.adventure.text.format.TextDecoration;
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -188,6 +189,7 @@ public class VelocityCommand implements SimpleCommand {
|
|||||||
|
|
||||||
private static class Info implements SubCommand {
|
private static class Info implements SubCommand {
|
||||||
|
|
||||||
|
private static final TextColor VELOCITY_COLOR = TextColor.fromHexString("#09add3");
|
||||||
private final ProxyServer server;
|
private final ProxyServer server;
|
||||||
|
|
||||||
private Info(ProxyServer server) {
|
private Info(ProxyServer server) {
|
||||||
@ -205,11 +207,11 @@ public class VelocityCommand implements SimpleCommand {
|
|||||||
|
|
||||||
TextComponent velocity = Component.text().content(version.getName() + " ")
|
TextComponent velocity = Component.text().content(version.getName() + " ")
|
||||||
.decoration(TextDecoration.BOLD, true)
|
.decoration(TextDecoration.BOLD, true)
|
||||||
.color(NamedTextColor.DARK_AQUA)
|
.color(VELOCITY_COLOR)
|
||||||
.append(Component.text(version.getVersion()).decoration(TextDecoration.BOLD, false))
|
.append(Component.text(version.getVersion()).decoration(TextDecoration.BOLD, false))
|
||||||
.build();
|
.build();
|
||||||
TextComponent copyright = Component
|
TextComponent copyright = Component
|
||||||
.text("Copyright 2018-2020 " + version.getVendor() + ". " + version.getName()
|
.text("Copyright 2018-2021 " + version.getVendor() + ". " + version.getName()
|
||||||
+ " is freely licensed under the terms of the MIT License.");
|
+ " is freely licensed under the terms of the MIT License.");
|
||||||
source.sendMessage(Identity.nil(), velocity);
|
source.sendMessage(Identity.nil(), velocity);
|
||||||
source.sendMessage(Identity.nil(), copyright);
|
source.sendMessage(Identity.nil(), copyright);
|
||||||
|
@ -30,7 +30,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||||
@ -426,11 +425,6 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
}
|
}
|
||||||
forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
|
forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (!config.contains("metrics.id") || config.<String>get("metrics.id").isEmpty()) {
|
|
||||||
config.set("metrics.id", UUID.randomUUID().toString());
|
|
||||||
mustResave = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mustResave) {
|
if (mustResave) {
|
||||||
config.save();
|
config.save();
|
||||||
}
|
}
|
||||||
@ -448,7 +442,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
PingPassthroughMode.DISABLED);
|
PingPassthroughMode.DISABLED);
|
||||||
|
|
||||||
String bind = config.getOrElse("bind", "0.0.0.0:25577");
|
String bind = config.getOrElse("bind", "0.0.0.0:25577");
|
||||||
String motd = config.getOrElse("motd", "&3A Velocity Server");
|
String motd = config.getOrElse("motd", "	add3A Velocity Server");
|
||||||
int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
||||||
Boolean onlineMode = config.getOrElse("online-mode", true);
|
Boolean onlineMode = config.getOrElse("online-mode", true);
|
||||||
Boolean announceForge = config.getOrElse("announce-forge", true);
|
Boolean announceForge = config.getOrElse("announce-forge", true);
|
||||||
@ -636,7 +630,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
this.loginRatelimit = config.getIntOrElse("login-ratelimit", 3000);
|
this.loginRatelimit = config.getIntOrElse("login-ratelimit", 3000);
|
||||||
this.connectionTimeout = config.getIntOrElse("connection-timeout", 5000);
|
this.connectionTimeout = config.getIntOrElse("connection-timeout", 5000);
|
||||||
this.readTimeout = config.getIntOrElse("read-timeout", 30000);
|
this.readTimeout = config.getIntOrElse("read-timeout", 30000);
|
||||||
this.proxyProtocol = config.getOrElse("proxy-protocol", false);
|
if (config.contains("haproxy-protocol")) {
|
||||||
|
this.proxyProtocol = config.getOrElse("haproxy-protocol", false);
|
||||||
|
} else {
|
||||||
|
this.proxyProtocol = config.getOrElse("proxy-protocol", false);
|
||||||
|
}
|
||||||
this.tcpFastOpen = config.getOrElse("tcp-fast-open", false);
|
this.tcpFastOpen = config.getOrElse("tcp-fast-open", false);
|
||||||
this.bungeePluginMessageChannel = config.getOrElse("bungee-plugin-message-channel", true);
|
this.bungeePluginMessageChannel = config.getOrElse("bungee-plugin-message-channel", true);
|
||||||
this.showPingRequests = config.getOrElse("show-ping-requests", false);
|
this.showPingRequests = config.getOrElse("show-ping-requests", false);
|
||||||
@ -769,39 +767,16 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
|
|
||||||
public static class Metrics {
|
public static class Metrics {
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
private String id = UUID.randomUUID().toString();
|
|
||||||
private boolean logFailure = false;
|
|
||||||
|
|
||||||
private boolean fromConfig;
|
|
||||||
|
|
||||||
private Metrics() {
|
|
||||||
this.fromConfig = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Metrics(CommentedConfig toml) {
|
private Metrics(CommentedConfig toml) {
|
||||||
if (toml != null) {
|
if (toml != null) {
|
||||||
this.enabled = toml.getOrElse("enabled", false);
|
this.enabled = toml.getOrElse("enabled", true);
|
||||||
this.id = toml.getOrElse("id", UUID.randomUUID().toString());
|
|
||||||
this.logFailure = toml.getOrElse("log-failure", false);
|
|
||||||
this.fromConfig = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLogFailure() {
|
|
||||||
return logFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFromConfig() {
|
|
||||||
return fromConfig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Messages {
|
public static class Messages {
|
||||||
|
@ -196,6 +196,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
public void write(Object msg) {
|
public void write(Object msg) {
|
||||||
if (channel.isActive()) {
|
if (channel.isActive()) {
|
||||||
channel.writeAndFlush(msg, channel.voidPromise());
|
channel.writeAndFlush(msg, channel.voidPromise());
|
||||||
|
} else {
|
||||||
|
ReferenceCountUtil.release(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,6 +208,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
public void delayedWrite(Object msg) {
|
public void delayedWrite(Object msg) {
|
||||||
if (channel.isActive()) {
|
if (channel.isActive()) {
|
||||||
channel.write(msg, channel.voidPromise());
|
channel.write(msg, channel.voidPromise());
|
||||||
|
} else {
|
||||||
|
ReferenceCountUtil.release(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,16 +383,25 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
if (threshold == -1) {
|
if (threshold == -1) {
|
||||||
channel.pipeline().remove(COMPRESSION_DECODER);
|
channel.pipeline().remove(COMPRESSION_DECODER);
|
||||||
channel.pipeline().remove(COMPRESSION_ENCODER);
|
channel.pipeline().remove(COMPRESSION_ENCODER);
|
||||||
return;
|
} else {
|
||||||
|
MinecraftCompressDecoder decoder = (MinecraftCompressDecoder) channel.pipeline()
|
||||||
|
.get(COMPRESSION_DECODER);
|
||||||
|
MinecraftCompressEncoder encoder = (MinecraftCompressEncoder) channel.pipeline()
|
||||||
|
.get(COMPRESSION_ENCODER);
|
||||||
|
if (decoder != null && encoder != null) {
|
||||||
|
decoder.setThreshold(threshold);
|
||||||
|
encoder.setThreshold(threshold);
|
||||||
|
} else {
|
||||||
|
int level = server.getConfiguration().getCompressionLevel();
|
||||||
|
VelocityCompressor compressor = Natives.compress.get().create(level);
|
||||||
|
|
||||||
|
encoder = new MinecraftCompressEncoder(threshold, compressor);
|
||||||
|
decoder = new MinecraftCompressDecoder(threshold, compressor);
|
||||||
|
|
||||||
|
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
||||||
|
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int level = server.getConfiguration().getCompressionLevel();
|
|
||||||
VelocityCompressor compressor = Natives.compress.get().create(level);
|
|
||||||
MinecraftCompressEncoder encoder = new MinecraftCompressEncoder(threshold, compressor);
|
|
||||||
MinecraftCompressDecoder decoder = new MinecraftCompressDecoder(threshold, compressor);
|
|
||||||
|
|
||||||
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
|
||||||
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,6 +30,7 @@ import com.velocitypowered.proxy.network.packet.serverbound.ServerboundPluginMes
|
|||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
import io.netty.handler.timeout.ReadTimeoutException;
|
import io.netty.handler.timeout.ReadTimeoutException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -38,6 +39,8 @@ import org.apache.logging.log4j.Logger;
|
|||||||
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class);
|
||||||
|
private static final boolean BACKPRESSURE_LOG = Boolean
|
||||||
|
.getBoolean("velocity.log-server-backpressure");
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private final VelocityServerConnection serverConn;
|
private final VelocityServerConnection serverConn;
|
||||||
private final ClientPlaySessionHandler playerSessionHandler;
|
private final ClientPlaySessionHandler playerSessionHandler;
|
||||||
@ -64,10 +67,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void activated() {
|
public void activated() {
|
||||||
serverConn.getServer().addPlayer(serverConn.getPlayer());
|
serverConn.getServer().addPlayer(serverConn.getPlayer());
|
||||||
MinecraftConnection serverMc = serverConn.ensureConnected();
|
|
||||||
serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(),
|
if (server.getConfiguration().isBungeePluginChannelEnabled()) {
|
||||||
ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())), ServerboundPluginMessagePacket.FACTORY
|
MinecraftConnection serverMc = serverConn.ensureConnected();
|
||||||
));
|
serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(),
|
||||||
|
ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion()))
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -286,4 +292,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writabilityChanged() {
|
||||||
|
Channel serverChan = serverConn.ensureConnected().getChannel();
|
||||||
|
boolean writable = serverChan.isWritable();
|
||||||
|
|
||||||
|
if (BACKPRESSURE_LOG) {
|
||||||
|
if (writable) {
|
||||||
|
logger.info("{} is not writable, not auto-reading player connection data", this.serverConn);
|
||||||
|
} else {
|
||||||
|
logger.info("{} is writable, will auto-read player connection data", this.serverConn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerConnection.setAutoReading(writable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,75 +262,52 @@ public class BungeeCordMessageResponder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ByteBuf prepareForwardMessage(ByteBufDataInput in) {
|
|
||||||
String channel = in.readUTF();
|
|
||||||
short messageLength = in.readShort();
|
|
||||||
|
|
||||||
ByteBuf buf = Unpooled.buffer();
|
|
||||||
ByteBufDataOutput forwarded = new ByteBufDataOutput(buf);
|
|
||||||
forwarded.writeUTF(channel);
|
|
||||||
forwarded.writeShort(messageLength);
|
|
||||||
buf.writeBytes(in.unwrap().readSlice(messageLength));
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processForwardToPlayer(ByteBufDataInput in) {
|
private void processForwardToPlayer(ByteBufDataInput in) {
|
||||||
proxy.getPlayer(in.readUTF())
|
Optional<Player> player = proxy.getPlayer(in.readUTF());
|
||||||
.flatMap(Player::getCurrentServer)
|
if (player.isPresent()) {
|
||||||
.ifPresent(server -> sendServerResponse(player, prepareForwardMessage(in)));
|
ByteBuf toForward = in.unwrap().copy();
|
||||||
|
sendServerResponse((ConnectedPlayer) player.get(), toForward);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processForwardToServer(ByteBufDataInput in) {
|
private void processForwardToServer(ByteBufDataInput in) {
|
||||||
String target = in.readUTF();
|
String target = in.readUTF();
|
||||||
ByteBuf toForward = prepareForwardMessage(in);
|
ByteBuf toForward = in.unwrap().copy();
|
||||||
if (target.equals("ALL")) {
|
if (target.equals("ALL")) {
|
||||||
ByteBuf unreleasableForward = Unpooled.unreleasableBuffer(toForward);
|
|
||||||
try {
|
try {
|
||||||
for (RegisteredServer rs : proxy.getAllServers()) {
|
for (RegisteredServer rs : proxy.getAllServers()) {
|
||||||
((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL, unreleasableForward);
|
((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL,
|
||||||
|
toForward.retainedSlice());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
toForward.release();
|
toForward.release();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
proxy.getServer(target).ifPresent(rs -> ((VelocityRegisteredServer) rs)
|
Optional<RegisteredServer> server = proxy.getServer(target);
|
||||||
.sendPluginMessage(LEGACY_CHANNEL, toForward));
|
if (server.isPresent()) {
|
||||||
|
((VelocityRegisteredServer) server.get()).sendPluginMessage(LEGACY_CHANNEL, toForward);
|
||||||
|
} else {
|
||||||
|
toForward.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this method will always release the buffer!
|
|
||||||
private void sendResponseOnConnection(ByteBuf buf) {
|
|
||||||
sendServerResponse(this.player, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getBungeeCordChannel(ProtocolVersion version) {
|
static String getBungeeCordChannel(ProtocolVersion version) {
|
||||||
return version.gte(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId()
|
return version.gte(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId()
|
||||||
: LEGACY_CHANNEL.getId();
|
: LEGACY_CHANNEL.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: this method will always release the buffer!
|
||||||
|
private void sendResponseOnConnection(ByteBuf buf) {
|
||||||
|
sendServerResponse(this.player, buf);
|
||||||
|
}
|
||||||
|
|
||||||
// Note: this method will always release the buffer!
|
// Note: this method will always release the buffer!
|
||||||
private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) {
|
private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) {
|
||||||
MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected();
|
MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected();
|
||||||
String chan = getBungeeCordChannel(serverConnection.getProtocolVersion());
|
String chan = getBungeeCordChannel(serverConnection.getProtocolVersion());
|
||||||
|
ServerboundPluginMessagePacket msg = new ServerboundPluginMessagePacket(chan, buf);
|
||||||
ServerboundPluginMessagePacket msg = null;
|
serverConnection.write(msg);
|
||||||
boolean released = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
VelocityServerConnection vsc = player.getConnectedServer();
|
|
||||||
if (vsc == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MinecraftConnection serverConn = vsc.ensureConnected();
|
|
||||||
msg = new ServerboundPluginMessagePacket(chan, buf);
|
|
||||||
serverConn.write(msg);
|
|
||||||
released = true;
|
|
||||||
} finally {
|
|
||||||
if (!released && msg != null) {
|
|
||||||
msg.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean process(AbstractPluginMessagePacket<?> message) {
|
boolean process(AbstractPluginMessagePacket<?> message) {
|
||||||
|
@ -32,7 +32,9 @@ import io.netty.channel.ChannelFutureListener;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
@ -47,8 +49,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
private boolean hasCompletedJoin = false;
|
private boolean hasCompletedJoin = false;
|
||||||
private boolean gracefulDisconnect = false;
|
private boolean gracefulDisconnect = false;
|
||||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||||
private long lastPingId;
|
private final Map<Long, Long> pendingPings = new HashMap<>();
|
||||||
private long lastPingSent;
|
|
||||||
private @MonotonicNonNull DimensionRegistry activeDimensionRegistry;
|
private @MonotonicNonNull DimensionRegistry activeDimensionRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,7 +113,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
return getHandshakeRemoteAddress();
|
return getHandshakeRemoteAddress();
|
||||||
}
|
}
|
||||||
StringBuilder data = new StringBuilder()
|
StringBuilder data = new StringBuilder()
|
||||||
.append(getHandshakeRemoteAddress())
|
.append(registeredServer.getServerInfo().getAddress().getHostString())
|
||||||
.append('\0')
|
.append('\0')
|
||||||
.append(((InetSocketAddress) proxyPlayer.getRemoteAddress()).getHostString())
|
.append(((InetSocketAddress) proxyPlayer.getRemoteAddress()).getHostString())
|
||||||
.append('\0')
|
.append('\0')
|
||||||
@ -259,21 +260,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
return gracefulDisconnect;
|
return gracefulDisconnect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getLastPingId() {
|
public Map<Long, Long> getPendingPings() {
|
||||||
return lastPingId;
|
return pendingPings;
|
||||||
}
|
|
||||||
|
|
||||||
public long getLastPingSent() {
|
|
||||||
return lastPingSent;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setLastPingId(long lastPingId) {
|
|
||||||
this.lastPingId = lastPingId;
|
|
||||||
this.lastPingSent = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetLastPingId() {
|
|
||||||
this.lastPingId = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,13 +5,17 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16;
|
|||||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
|
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
|
||||||
import static com.velocitypowered.proxy.network.PluginMessageUtil.constructChannelsPacket;
|
import static com.velocitypowered.proxy.network.PluginMessageUtil.constructChannelsPacket;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult;
|
import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult;
|
||||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||||
|
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
@ -102,12 +106,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public boolean handle(ServerboundKeepAlivePacket packet) {
|
public boolean handle(ServerboundKeepAlivePacket packet) {
|
||||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||||
if (serverConnection != null && packet.getRandomId() == serverConnection.getLastPingId()) {
|
if (serverConnection != null) {
|
||||||
MinecraftConnection smc = serverConnection.getConnection();
|
Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
||||||
if (smc != null) {
|
if (sentTime != null) {
|
||||||
player.setPing(System.currentTimeMillis() - serverConnection.getLastPingSent());
|
MinecraftConnection smc = serverConnection.getConnection();
|
||||||
smc.write(packet);
|
if (smc != null) {
|
||||||
serverConnection.resetLastPingId();
|
player.setPing(System.currentTimeMillis() - sentTime);
|
||||||
|
smc.write(packet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -191,7 +197,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
logger.warn("A plugin message was received while the backend server was not "
|
logger.warn("A plugin message was received while the backend server was not "
|
||||||
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
||||||
} else if (PluginMessageUtil.isRegister(packet)) {
|
} else if (PluginMessageUtil.isRegister(packet)) {
|
||||||
player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet));
|
List<String> channels = PluginMessageUtil.getChannels(packet);
|
||||||
|
player.getKnownChannels().addAll(channels);
|
||||||
|
List<ChannelIdentifier> channelIdentifiers = new ArrayList<>();
|
||||||
|
for (String channel : channels) {
|
||||||
|
try {
|
||||||
|
channelIdentifiers.add(MinecraftChannelIdentifier.from(channel));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
channelIdentifiers.add(new LegacyChannelIdentifier(channel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.getEventManager().fireAndForget(new PlayerChannelRegisterEvent(player,
|
||||||
|
ImmutableList.copyOf(channelIdentifiers)));
|
||||||
backendConn.write(packet.retain());
|
backendConn.write(packet.retain());
|
||||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||||
@ -303,7 +320,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
boolean writable = player.getConnection().getChannel().isWritable();
|
boolean writable = player.getConnection().getChannel().isWritable();
|
||||||
|
|
||||||
if (!writable) {
|
if (!writable) {
|
||||||
// We might have packets queued for the server, so flush them now to free up memory.
|
// We might have packets queued from the server, so flush them now to free up memory.
|
||||||
player.getConnection().flush();
|
player.getConnection().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ import java.net.SocketAddress;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@ -103,6 +104,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
private @Nullable VelocityServerConnection connectionInFlight;
|
private @Nullable VelocityServerConnection connectionInFlight;
|
||||||
private @Nullable PlayerSettings settings;
|
private @Nullable PlayerSettings settings;
|
||||||
private @Nullable ModInfo modInfo;
|
private @Nullable ModInfo modInfo;
|
||||||
|
private Component playerListHeader = Component.empty();
|
||||||
|
private Component playerListFooter = Component.empty();
|
||||||
private final VelocityTabList tabList;
|
private final VelocityTabList tabList;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private ClientConnectionPhase connectionPhase;
|
private ClientConnectionPhase connectionPhase;
|
||||||
@ -113,11 +116,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
||||||
@Nullable InetSocketAddress virtualHost, boolean onlineMode) {
|
@Nullable InetSocketAddress virtualHost, boolean onlineMode) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
if (connection.getProtocolVersion().gte(ProtocolVersion.MINECRAFT_1_8)) {
|
|
||||||
this.tabList = new VelocityTabList(connection);
|
|
||||||
} else {
|
|
||||||
this.tabList = new VelocityTabListLegacy(connection);
|
|
||||||
}
|
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.virtualHost = virtualHost;
|
this.virtualHost = virtualHost;
|
||||||
@ -125,6 +123,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
this.connectionPhase = connection.getType().getInitialClientPhase();
|
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||||
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
||||||
this.onlineMode = onlineMode;
|
this.onlineMode = onlineMode;
|
||||||
|
|
||||||
|
if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
this.tabList = new VelocityTabList(this);
|
||||||
|
} else {
|
||||||
|
this.tabList = new VelocityTabListLegacy(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -265,46 +269,85 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showTitle(net.kyori.adventure.title.@NonNull Title title) {
|
public Component getPlayerListHeader() {
|
||||||
GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(this
|
return this.playerListHeader;
|
||||||
.getProtocolVersion());
|
}
|
||||||
|
|
||||||
connection.delayedWrite(new ClientboundTitlePacket(
|
@Override
|
||||||
ClientboundTitlePacket.SET_TITLE,
|
public Component getPlayerListFooter() {
|
||||||
serializer.serialize(title.title())
|
return this.playerListFooter;
|
||||||
));
|
}
|
||||||
|
|
||||||
connection.delayedWrite(new ClientboundTitlePacket(
|
@Override
|
||||||
ClientboundTitlePacket.SET_SUBTITLE,
|
public void sendPlayerListHeader(@NonNull final Component header) {
|
||||||
serializer.serialize(title.subtitle())
|
this.sendPlayerListHeaderAndFooter(header, this.playerListFooter);
|
||||||
));
|
}
|
||||||
|
|
||||||
net.kyori.adventure.title.Title.Times times = title.times();
|
@Override
|
||||||
if (times != null) {
|
public void sendPlayerListFooter(@NonNull final Component footer) {
|
||||||
connection.delayedWrite(ClientboundTitlePacket.times(this.getProtocolVersion(), times));
|
this.sendPlayerListHeaderAndFooter(this.playerListHeader, footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendPlayerListHeaderAndFooter(final Component header, final Component footer) {
|
||||||
|
this.playerListHeader = Objects.requireNonNull(header, "header");
|
||||||
|
this.playerListFooter = Objects.requireNonNull(footer, "footer");
|
||||||
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
this.connection.write(HeaderAndFooter.create(header, footer, this.getProtocolVersion()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connection.flush();
|
@Override
|
||||||
|
public void showTitle(net.kyori.adventure.title.@NonNull Title title) {
|
||||||
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(this
|
||||||
|
.getProtocolVersion());
|
||||||
|
|
||||||
|
connection.delayedWrite(new ClientboundTitlePacket(
|
||||||
|
ClientboundTitlePacket.SET_TITLE,
|
||||||
|
serializer.serialize(title.title())
|
||||||
|
));
|
||||||
|
|
||||||
|
connection.delayedWrite(new ClientboundTitlePacket(
|
||||||
|
ClientboundTitlePacket.SET_SUBTITLE,
|
||||||
|
serializer.serialize(title.subtitle())
|
||||||
|
));
|
||||||
|
|
||||||
|
net.kyori.adventure.title.Title.Times times = title.times();
|
||||||
|
if (times != null) {
|
||||||
|
connection.delayedWrite(ClientboundTitlePacket.times(this.getProtocolVersion(), times));
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearTitle() {
|
public void clearTitle() {
|
||||||
connection.write(ClientboundTitlePacket.hide(this.getProtocolVersion()));
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
connection.write(ClientboundTitlePacket.hide(this.getProtocolVersion()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resetTitle() {
|
public void resetTitle() {
|
||||||
connection.write(ClientboundTitlePacket.reset(this.getProtocolVersion()));
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
connection.write(ClientboundTitlePacket.reset(this.getProtocolVersion()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hideBossBar(@NonNull BossBar bar) {
|
public void hideBossBar(@NonNull BossBar bar) {
|
||||||
this.server.getBossBarManager().removeBossBar(this, bar);
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) {
|
||||||
|
this.server.getBossBarManager().removeBossBar(this, bar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showBossBar(@NonNull BossBar bar) {
|
public void showBossBar(@NonNull BossBar bar) {
|
||||||
this.server.getBossBarManager().addBossBar(this, bar);
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) {
|
||||||
|
this.server.getBossBarManager().addBossBar(this, bar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -317,6 +360,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
this.profile = profile.withProperties(properties);
|
this.profile = profile.withProperties(properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearHeaderAndFooter() {
|
||||||
|
tabList.clearHeaderAndFooter();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VelocityTabList getTabList() {
|
public VelocityTabList getTabList() {
|
||||||
return tabList;
|
return tabList;
|
||||||
@ -412,7 +460,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
logger.error("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
logger.error("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
||||||
plainTextReason);
|
plainTextReason);
|
||||||
handleConnectionException(server, disconnectReason, Component.text()
|
handleConnectionException(server, disconnectReason, Component.text()
|
||||||
.append(messages.getKickPrefix(server.getServerInfo().getName()))
|
.append(messages.getKickPrefix(server.getServerInfo().getName())
|
||||||
|
.colorIfAbsent(NamedTextColor.RED))
|
||||||
.color(NamedTextColor.RED)
|
.color(NamedTextColor.RED)
|
||||||
.append(disconnectReason)
|
.append(disconnectReason)
|
||||||
.build(), safe);
|
.build(), safe);
|
||||||
@ -420,8 +469,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
logger.error("{}: disconnected while connecting to {}: {}", this,
|
logger.error("{}: disconnected while connecting to {}: {}", this,
|
||||||
server.getServerInfo().getName(), plainTextReason);
|
server.getServerInfo().getName(), plainTextReason);
|
||||||
handleConnectionException(server, disconnectReason, Component.text()
|
handleConnectionException(server, disconnectReason, Component.text()
|
||||||
.append(messages.getDisconnectPrefix(server.getServerInfo().getName()))
|
.append(messages.getDisconnectPrefix(server.getServerInfo().getName())
|
||||||
.color(NamedTextColor.RED)
|
.colorIfAbsent(NamedTextColor.RED))
|
||||||
.append(disconnectReason)
|
.append(disconnectReason)
|
||||||
.build(), safe);
|
.build(), safe);
|
||||||
}
|
}
|
||||||
@ -678,7 +727,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
public void sendResourcePack(String url) {
|
public void sendResourcePack(String url) {
|
||||||
Preconditions.checkNotNull(url, "url");
|
Preconditions.checkNotNull(url, "url");
|
||||||
|
|
||||||
connection.write(new ClientboundResourcePackRequestPacket(url, ""));
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
connection.write(new ClientboundResourcePackRequestPacket(url, ""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -687,7 +738,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
Preconditions.checkNotNull(hash, "hash");
|
Preconditions.checkNotNull(hash, "hash");
|
||||||
Preconditions.checkArgument(hash.length == 20, "Hash length is not 20");
|
Preconditions.checkArgument(hash.length == 20, "Hash length is not 20");
|
||||||
|
|
||||||
connection.write(new ClientboundResourcePackRequestPacket(url, ByteBufUtil.hexDump(hash)));
|
if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
connection.write(new ClientboundResourcePackRequestPacket(url, ByteBufUtil.hexDump(hash)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,6 +36,7 @@ import io.netty.buffer.ByteBuf;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -90,7 +91,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
try {
|
try {
|
||||||
KeyPair serverKeyPair = server.getServerKeyPair();
|
KeyPair serverKeyPair = server.getServerKeyPair();
|
||||||
byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken());
|
byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken());
|
||||||
if (!Arrays.equals(verify, decryptedVerifyToken)) {
|
if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) {
|
||||||
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
|
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ import io.netty.handler.codec.CorruptedFrameException;
|
|||||||
import io.netty.handler.codec.DecoderException;
|
import io.netty.handler.codec.DecoderException;
|
||||||
import io.netty.handler.codec.EncoderException;
|
import io.netty.handler.codec.EncoderException;
|
||||||
|
|
||||||
|
import java.io.DataInput;
|
||||||
|
import java.io.DataOutput;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -227,11 +229,12 @@ public enum ProtocolUtils {
|
|||||||
/**
|
/**
|
||||||
* Reads a {@link net.kyori.adventure.nbt.CompoundBinaryTag} from the {@code buf}.
|
* Reads a {@link net.kyori.adventure.nbt.CompoundBinaryTag} from the {@code buf}.
|
||||||
* @param buf the buffer to read from
|
* @param buf the buffer to read from
|
||||||
|
* @param reader the NBT reader to use
|
||||||
* @return {@link net.kyori.adventure.nbt.CompoundBinaryTag} the CompoundTag from the buffer
|
* @return {@link net.kyori.adventure.nbt.CompoundBinaryTag} the CompoundTag from the buffer
|
||||||
*/
|
*/
|
||||||
public static CompoundBinaryTag readCompoundTag(ByteBuf buf) {
|
public static CompoundBinaryTag readCompoundTag(ByteBuf buf, BinaryTagIO.Reader reader) {
|
||||||
try {
|
try {
|
||||||
return BinaryTagIO.readDataInput(new ByteBufInputStream(buf));
|
return reader.read((DataInput) new ByteBufInputStream(buf));
|
||||||
} catch (IOException thrown) {
|
} catch (IOException thrown) {
|
||||||
throw new DecoderException(
|
throw new DecoderException(
|
||||||
"Unable to parse NBT CompoundTag, full error: " + thrown.getMessage());
|
"Unable to parse NBT CompoundTag, full error: " + thrown.getMessage());
|
||||||
@ -245,7 +248,7 @@ public enum ProtocolUtils {
|
|||||||
*/
|
*/
|
||||||
public static void writeCompoundTag(ByteBuf buf, CompoundBinaryTag compoundTag) {
|
public static void writeCompoundTag(ByteBuf buf, CompoundBinaryTag compoundTag) {
|
||||||
try {
|
try {
|
||||||
BinaryTagIO.writeDataOutput(compoundTag, new ByteBufOutputStream(buf));
|
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(buf));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new EncoderException("Unable to encode NBT CompoundTag");
|
throw new EncoderException("Unable to encode NBT CompoundTag");
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import com.velocitypowered.proxy.network.packet.PacketHandler;
|
|||||||
import com.velocitypowered.proxy.network.packet.PacketReader;
|
import com.velocitypowered.proxy.network.packet.PacketReader;
|
||||||
import com.velocitypowered.proxy.network.serialization.brigadier.ArgumentPropertyRegistry;
|
import com.velocitypowered.proxy.network.serialization.brigadier.ArgumentPropertyRegistry;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
@ -99,7 +100,8 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
|||||||
public void encode(ByteBuf buf, PacketDirection direction, ProtocolVersion protocolVersion) {
|
public void encode(ByteBuf buf, PacketDirection direction, ProtocolVersion protocolVersion) {
|
||||||
// Assign all the children an index.
|
// Assign all the children an index.
|
||||||
Deque<CommandNode<CommandSource>> childrenQueue = new ArrayDeque<>(ImmutableList.of(rootNode));
|
Deque<CommandNode<CommandSource>> childrenQueue = new ArrayDeque<>(ImmutableList.of(rootNode));
|
||||||
Object2IntMap<CommandNode<CommandSource>> idMappings = new Object2IntLinkedOpenHashMap<>();
|
Object2IntMap<CommandNode<CommandSource>> idMappings = new Object2IntLinkedOpenCustomHashMap<>(
|
||||||
|
IdentityHashStrategy.instance());
|
||||||
while (!childrenQueue.isEmpty()) {
|
while (!childrenQueue.isEmpty()) {
|
||||||
CommandNode<CommandSource> child = childrenQueue.poll();
|
CommandNode<CommandSource> child = childrenQueue.poll();
|
||||||
if (!idMappings.containsKey(child)) {
|
if (!idMappings.containsKey(child)) {
|
||||||
@ -207,6 +209,7 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
|||||||
private final int redirectTo;
|
private final int redirectTo;
|
||||||
private final @Nullable ArgumentBuilder<CommandSource, ?> args;
|
private final @Nullable ArgumentBuilder<CommandSource, ?> args;
|
||||||
private @MonotonicNonNull CommandNode<CommandSource> built;
|
private @MonotonicNonNull CommandNode<CommandSource> built;
|
||||||
|
private boolean validated;
|
||||||
|
|
||||||
private WireNode(int idx, byte flags, int[] children, int redirectTo,
|
private WireNode(int idx, byte flags, int[] children, int redirectTo,
|
||||||
@Nullable ArgumentBuilder<CommandSource, ?> args) {
|
@Nullable ArgumentBuilder<CommandSource, ?> args) {
|
||||||
@ -215,18 +218,34 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
|||||||
this.children = children;
|
this.children = children;
|
||||||
this.redirectTo = redirectTo;
|
this.redirectTo = redirectTo;
|
||||||
this.args = args;
|
this.args = args;
|
||||||
|
this.validated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate(WireNode[] wireNodes) {
|
||||||
|
// Ensure all children exist. Note that we delay checking if the node has been built yet;
|
||||||
|
// that needs to come after this node is built.
|
||||||
|
for (int child : children) {
|
||||||
|
if (child < 0 || child >= wireNodes.length) {
|
||||||
|
throw new IllegalStateException("Node points to non-existent index " + child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectTo != -1) {
|
||||||
|
if (redirectTo < 0 || redirectTo >= wireNodes.length) {
|
||||||
|
throw new IllegalStateException("Redirect node points to non-existent index "
|
||||||
|
+ redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean toNode(WireNode[] wireNodes) {
|
boolean toNode(WireNode[] wireNodes) {
|
||||||
if (this.built == null) {
|
if (!this.validated) {
|
||||||
// Ensure all children exist. Note that we delay checking if the node has been built yet;
|
this.validate(wireNodes);
|
||||||
// that needs to come after this node is built.
|
}
|
||||||
for (int child : children) {
|
|
||||||
if (child >= wireNodes.length) {
|
|
||||||
throw new IllegalStateException("Node points to non-existent index " + redirectTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (this.built == null) {
|
||||||
int type = flags & FLAG_NODE_TYPE;
|
int type = flags & FLAG_NODE_TYPE;
|
||||||
if (type == NODE_TYPE_ROOT) {
|
if (type == NODE_TYPE_ROOT) {
|
||||||
this.built = new RootCommandNode<>();
|
this.built = new RootCommandNode<>();
|
||||||
@ -237,10 +256,6 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
|||||||
|
|
||||||
// Add any redirects
|
// Add any redirects
|
||||||
if (redirectTo != -1) {
|
if (redirectTo != -1) {
|
||||||
if (redirectTo >= wireNodes.length) {
|
|
||||||
throw new IllegalStateException("Node points to non-existent index " + redirectTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
WireNode redirect = wireNodes[redirectTo];
|
WireNode redirect = wireNodes[redirectTo];
|
||||||
if (redirect.built != null) {
|
if (redirect.built != null) {
|
||||||
args.redirect(redirect.built);
|
args.redirect(redirect.built);
|
||||||
|
@ -12,6 +12,7 @@ import com.velocitypowered.proxy.network.packet.PacketDirection;
|
|||||||
import com.velocitypowered.proxy.network.packet.PacketHandler;
|
import com.velocitypowered.proxy.network.packet.PacketHandler;
|
||||||
import com.velocitypowered.proxy.network.packet.PacketReader;
|
import com.velocitypowered.proxy.network.packet.PacketReader;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||||
@ -20,6 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
public class ClientboundJoinGamePacket implements Packet {
|
public class ClientboundJoinGamePacket implements Packet {
|
||||||
public static final PacketReader<ClientboundJoinGamePacket> DECODER = PacketReader.method(ClientboundJoinGamePacket::new);
|
public static final PacketReader<ClientboundJoinGamePacket> DECODER = PacketReader.method(ClientboundJoinGamePacket::new);
|
||||||
|
|
||||||
|
private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(2 * 1024 * 1024);
|
||||||
private int entityId;
|
private int entityId;
|
||||||
private short gamemode;
|
private short gamemode;
|
||||||
private int dimension;
|
private int dimension;
|
||||||
@ -37,14 +39,136 @@ public class ClientboundJoinGamePacket implements Packet {
|
|||||||
private short previousGamemode; // 1.16+
|
private short previousGamemode; // 1.16+
|
||||||
private CompoundBinaryTag biomeRegistry; // 1.16.2+
|
private CompoundBinaryTag biomeRegistry; // 1.16.2+
|
||||||
|
|
||||||
public void withDimension(int dimension) {
|
public int getEntityId() {
|
||||||
this.dimension = dimension;
|
return entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEntityId(int entityId) {
|
||||||
|
this.entityId = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getGamemode() {
|
||||||
|
return gamemode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGamemode(short gamemode) {
|
||||||
|
this.gamemode = gamemode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDimension() {
|
||||||
|
return dimension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDimension(int dimension) {
|
public void setDimension(int dimension) {
|
||||||
this.dimension = dimension;
|
this.dimension = dimension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getPartialHashedSeed() {
|
||||||
|
return partialHashedSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getDifficulty() {
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDifficulty(short difficulty) {
|
||||||
|
this.difficulty = difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxPlayers() {
|
||||||
|
return maxPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxPlayers(int maxPlayers) {
|
||||||
|
this.maxPlayers = maxPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getLevelType() {
|
||||||
|
return levelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLevelType(String levelType) {
|
||||||
|
this.levelType = levelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getViewDistance() {
|
||||||
|
return viewDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViewDistance(int viewDistance) {
|
||||||
|
this.viewDistance = viewDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReducedDebugInfo() {
|
||||||
|
return reducedDebugInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReducedDebugInfo(boolean reducedDebugInfo) {
|
||||||
|
this.reducedDebugInfo = reducedDebugInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DimensionInfo getDimensionInfo() {
|
||||||
|
return dimensionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDimensionInfo(DimensionInfo dimensionInfo) {
|
||||||
|
this.dimensionInfo = dimensionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DimensionRegistry getDimensionRegistry() {
|
||||||
|
return dimensionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDimensionRegistry(DimensionRegistry dimensionRegistry) {
|
||||||
|
this.dimensionRegistry = dimensionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getPreviousGamemode() {
|
||||||
|
return previousGamemode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPreviousGamemode(short previousGamemode) {
|
||||||
|
this.previousGamemode = previousGamemode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getIsHardcore() {
|
||||||
|
return isHardcore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsHardcore(boolean isHardcore) {
|
||||||
|
this.isHardcore = isHardcore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompoundBinaryTag getBiomeRegistry() {
|
||||||
|
return biomeRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBiomeRegistry(CompoundBinaryTag biomeRegistry) {
|
||||||
|
this.biomeRegistry = biomeRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DimensionData getCurrentDimensionData() {
|
||||||
|
return currentDimensionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "JoinGame{"
|
||||||
|
+ "entityId=" + entityId
|
||||||
|
+ ", gamemode=" + gamemode
|
||||||
|
+ ", dimension=" + dimension
|
||||||
|
+ ", partialHashedSeed=" + partialHashedSeed
|
||||||
|
+ ", difficulty=" + difficulty
|
||||||
|
+ ", maxPlayers=" + maxPlayers
|
||||||
|
+ ", levelType='" + levelType + '\''
|
||||||
|
+ ", viewDistance=" + viewDistance
|
||||||
|
+ ", reducedDebugInfo=" + reducedDebugInfo
|
||||||
|
+ ", dimensionRegistry='" + dimensionRegistry + '\''
|
||||||
|
+ ", dimensionInfo='" + dimensionInfo + '\''
|
||||||
|
+ ", previousGamemode=" + previousGamemode
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) {
|
public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) {
|
||||||
this.entityId = buf.readInt();
|
this.entityId = buf.readInt();
|
||||||
@ -61,7 +185,7 @@ public class ClientboundJoinGamePacket implements Packet {
|
|||||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16)) {
|
if (version.gte(ProtocolVersion.MINECRAFT_1_16)) {
|
||||||
this.previousGamemode = buf.readByte();
|
this.previousGamemode = buf.readByte();
|
||||||
ImmutableSet<String> levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf));
|
ImmutableSet<String> levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf));
|
||||||
CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf);
|
CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER);
|
||||||
ListBinaryTag dimensionRegistryContainer = null;
|
ListBinaryTag dimensionRegistryContainer = null;
|
||||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
||||||
dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type")
|
dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type")
|
||||||
@ -74,8 +198,8 @@ public class ClientboundJoinGamePacket implements Packet {
|
|||||||
ImmutableSet<DimensionData> readData =
|
ImmutableSet<DimensionData> readData =
|
||||||
DimensionRegistry.fromGameData(dimensionRegistryContainer, version);
|
DimensionRegistry.fromGameData(dimensionRegistryContainer, version);
|
||||||
this.dimensionRegistry = new DimensionRegistry(readData, levelNames);
|
this.dimensionRegistry = new DimensionRegistry(readData, levelNames);
|
||||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||||
CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf);
|
CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER);
|
||||||
dimensionIdentifier = ProtocolUtils.readString(buf);
|
dimensionIdentifier = ProtocolUtils.readString(buf);
|
||||||
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version)
|
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version)
|
||||||
.annotateWith(dimensionIdentifier, null);
|
.annotateWith(dimensionIdentifier, null);
|
||||||
@ -186,66 +310,6 @@ public class ClientboundJoinGamePacket implements Packet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getEntityId() {
|
|
||||||
return entityId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getGamemode() {
|
|
||||||
return gamemode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDimension() {
|
|
||||||
return dimension;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getPartialHashedSeed() {
|
|
||||||
return partialHashedSeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getDifficulty() {
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxPlayers() {
|
|
||||||
return maxPlayers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable String getLevelType() {
|
|
||||||
return levelType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getViewDistance() {
|
|
||||||
return viewDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isReducedDebugInfo() {
|
|
||||||
return reducedDebugInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DimensionInfo getDimensionInfo() {
|
|
||||||
return dimensionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DimensionRegistry getDimensionRegistry() {
|
|
||||||
return dimensionRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public short getPreviousGamemode() {
|
|
||||||
return previousGamemode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getIsHardcore() {
|
|
||||||
return isHardcore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompoundBinaryTag getBiomeRegistry() {
|
|
||||||
return biomeRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DimensionData getCurrentDimensionData() {
|
|
||||||
return currentDimensionData;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(PacketHandler handler) {
|
public boolean handle(PacketHandler handler) {
|
||||||
return handler.handle(this);
|
return handler.handle(this);
|
||||||
|
@ -10,6 +10,7 @@ import com.velocitypowered.proxy.network.packet.PacketDirection;
|
|||||||
import com.velocitypowered.proxy.network.packet.PacketHandler;
|
import com.velocitypowered.proxy.network.packet.PacketHandler;
|
||||||
import com.velocitypowered.proxy.network.packet.PacketReader;
|
import com.velocitypowered.proxy.network.packet.PacketReader;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||||
|
|
||||||
public class ClientboundRespawnPacket implements Packet {
|
public class ClientboundRespawnPacket implements Packet {
|
||||||
@ -102,9 +103,9 @@ public class ClientboundRespawnPacket implements Packet {
|
|||||||
public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) {
|
public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) {
|
||||||
String dimensionIdentifier = null;
|
String dimensionIdentifier = null;
|
||||||
String levelName = null;
|
String levelName = null;
|
||||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16)) {
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||||
CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf);
|
CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader());
|
||||||
dimensionIdentifier = ProtocolUtils.readString(buf);
|
dimensionIdentifier = ProtocolUtils.readString(buf);
|
||||||
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version)
|
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version)
|
||||||
.annotateWith(dimensionIdentifier, null);
|
.annotateWith(dimensionIdentifier, null);
|
||||||
|
@ -20,7 +20,7 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
Boolean.getBoolean("velocity.increased-compression-cap")
|
Boolean.getBoolean("velocity.increased-compression-cap")
|
||||||
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
|
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
|
||||||
|
|
||||||
private final int threshold;
|
private int threshold;
|
||||||
private final VelocityCompressor compressor;
|
private final VelocityCompressor compressor;
|
||||||
|
|
||||||
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
|
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
|
||||||
@ -60,4 +60,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||||
compressor.close();
|
compressor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setThreshold(int threshold) {
|
||||||
|
this.threshold = threshold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import io.netty.handler.codec.MessageToByteEncoder;
|
|||||||
|
|
||||||
public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||||
|
|
||||||
private final int threshold;
|
private int threshold;
|
||||||
private final VelocityCompressor compressor;
|
private final VelocityCompressor compressor;
|
||||||
|
|
||||||
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
|
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
|
||||||
@ -20,7 +20,7 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
|||||||
@Override
|
@Override
|
||||||
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
|
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
|
||||||
int uncompressed = msg.readableBytes();
|
int uncompressed = msg.readableBytes();
|
||||||
if (uncompressed <= threshold) {
|
if (uncompressed < threshold) {
|
||||||
// Under the threshold, there is nothing to do.
|
// Under the threshold, there is nothing to do.
|
||||||
ProtocolUtils.writeVarInt(out, 0);
|
ProtocolUtils.writeVarInt(out, 0);
|
||||||
out.writeBytes(msg);
|
out.writeBytes(msg);
|
||||||
@ -54,4 +54,8 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
|||||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||||
compressor.close();
|
compressor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setThreshold(int threshold) {
|
||||||
|
this.threshold = threshold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ import io.netty.handler.codec.CorruptedFrameException;
|
|||||||
public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
public static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging");
|
public static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging");
|
||||||
private static final QuietDecoderException DECODE_FAILED =
|
private static final QuietRuntimeException DECODE_FAILED =
|
||||||
new QuietDecoderException("A packet did not decode successfully (invalid data). If you are a "
|
new QuietRuntimeException("A packet did not decode successfully (invalid data). If you are a "
|
||||||
+ "developer, launch Velocity with -Dvelocity.packet-decode-logging=true to see more.");
|
+ "developer, launch Velocity with -Dvelocity.packet-decode-logging=true to see more.");
|
||||||
|
|
||||||
private final PacketDirection direction;
|
private final PacketDirection direction;
|
||||||
@ -64,8 +64,16 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
|||||||
ctx.fireChannelRead(buf);
|
ctx.fireChannelRead(buf);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
doLengthSanityChecks(buf, packet);
|
||||||
|
|
||||||
|
try {
|
||||||
|
packet.decode(buf, direction, registry.version);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw handleDecodeFailure(e, packet, packetId);
|
||||||
|
}
|
||||||
|
|
||||||
if (buf.isReadable()) {
|
if (buf.isReadable()) {
|
||||||
throw handleNotReadEnough(packet, packetId);
|
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
|
||||||
}
|
}
|
||||||
ctx.fireChannelRead(packet);
|
ctx.fireChannelRead(packet);
|
||||||
} finally {
|
} finally {
|
||||||
@ -74,10 +82,30 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Exception handleNotReadEnough(Packet packet, int packetId) {
|
private void doLengthSanityChecks(ByteBuf buf, Packet packet) throws Exception {
|
||||||
|
int expectedMinLen = packet.expectedMinLength(buf, direction, registry.version);
|
||||||
|
int expectedMaxLen = packet.expectedMaxLength(buf, direction, registry.version);
|
||||||
|
if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) {
|
||||||
|
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
|
||||||
|
}
|
||||||
|
if (buf.readableBytes() < expectedMinLen) {
|
||||||
|
throw handleUnderflow(packet, expectedMaxLen, buf.readableBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Exception handleOverflow(Packet packet, int expected, int actual) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
return new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " "
|
return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too "
|
||||||
+ getExtraConnectionDetail(packetId));
|
+ "big (expected " + expected + " bytes, got " + actual + " bytes)");
|
||||||
|
} else {
|
||||||
|
return DECODE_FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Exception handleUnderflow(Packet packet, int expected, int actual) {
|
||||||
|
if (DEBUG) {
|
||||||
|
return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too "
|
||||||
|
+ "small (expected " + expected + " bytes, got " + actual + " bytes)");
|
||||||
} else {
|
} else {
|
||||||
return DECODE_FAILED;
|
return DECODE_FAILED;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.velocitypowered.proxy.protocol;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
|
||||||
|
public interface MinecraftPacket {
|
||||||
|
|
||||||
|
void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion);
|
||||||
|
|
||||||
|
void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion);
|
||||||
|
|
||||||
|
boolean handle(MinecraftSessionHandler handler);
|
||||||
|
|
||||||
|
default int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||||
|
ProtocolVersion version) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
default int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||||
|
ProtocolVersion version) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
package com.velocitypowered.proxy.protocol.packet;
|
||||||
|
|
||||||
|
import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.transformLegacyToModernChannel;
|
||||||
|
|
||||||
|
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 com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
|
public class PluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
|
||||||
|
|
||||||
|
private @Nullable String channel;
|
||||||
|
|
||||||
|
public PluginMessage() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginMessage(String channel,
|
||||||
|
@MonotonicNonNull ByteBuf backing) {
|
||||||
|
super(backing);
|
||||||
|
this.channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChannel() {
|
||||||
|
if (channel == null) {
|
||||||
|
throw new IllegalStateException("Channel is not specified.");
|
||||||
|
}
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannel(String channel) {
|
||||||
|
this.channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PluginMessage{"
|
||||||
|
+ "channel='" + channel + '\''
|
||||||
|
+ ", data=" + super.toString()
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||||
|
this.channel = ProtocolUtils.readString(buf);
|
||||||
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) {
|
||||||
|
this.channel = transformLegacyToModernChannel(this.channel);
|
||||||
|
}
|
||||||
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
this.replace(buf.readRetainedSlice(buf.readableBytes()));
|
||||||
|
} else {
|
||||||
|
this.replace(ProtocolUtils.readRetainedByteBufSlice17(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||||
|
if (channel == null) {
|
||||||
|
throw new IllegalStateException("Channel is not specified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refCnt() == 0) {
|
||||||
|
throw new IllegalStateException("Plugin message contents for " + this.channel
|
||||||
|
+ " freed too many times.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) {
|
||||||
|
ProtocolUtils.writeString(buf, transformLegacyToModernChannel(this.channel));
|
||||||
|
} else {
|
||||||
|
ProtocolUtils.writeString(buf, this.channel);
|
||||||
|
}
|
||||||
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
|
buf.writeBytes(content());
|
||||||
|
} else {
|
||||||
|
ProtocolUtils.writeByteBuf17(content(), buf, true); // True for Forge support
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(MinecraftSessionHandler handler) {
|
||||||
|
return handler.handle(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage copy() {
|
||||||
|
return (PluginMessage) super.copy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage duplicate() {
|
||||||
|
return (PluginMessage) super.duplicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage retainedDuplicate() {
|
||||||
|
return (PluginMessage) super.retainedDuplicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage replace(ByteBuf content) {
|
||||||
|
return (PluginMessage) super.replace(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage retain() {
|
||||||
|
return (PluginMessage) super.retain();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage retain(int increment) {
|
||||||
|
return (PluginMessage) super.retain(increment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage touch() {
|
||||||
|
return (PluginMessage) super.touch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMessage touch(Object hint) {
|
||||||
|
return (PluginMessage) super.touch(hint);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.common.collect.Multimaps;
|
import com.google.common.collect.Multimaps;
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import com.velocitypowered.api.plugin.PluginContainer;
|
||||||
import com.velocitypowered.api.plugin.PluginManager;
|
import com.velocitypowered.api.plugin.PluginManager;
|
||||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||||
import com.velocitypowered.api.scheduler.Scheduler;
|
import com.velocitypowered.api.scheduler.Scheduler;
|
||||||
@ -183,8 +184,18 @@ public class VelocityScheduler implements Scheduler {
|
|||||||
currentTaskThread = Thread.currentThread();
|
currentTaskThread = Thread.currentThread();
|
||||||
try {
|
try {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
} catch (Exception e) {
|
} catch (Throwable e) {
|
||||||
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin, e);
|
//noinspection ConstantConditions
|
||||||
|
if (e instanceof InterruptedException) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} else {
|
||||||
|
String friendlyPluginName = pluginManager.fromInstance(plugin)
|
||||||
|
.map(container -> container.getDescription().getName()
|
||||||
|
.orElse(container.getDescription().getId()))
|
||||||
|
.orElse("UNKNOWN");
|
||||||
|
Log.logger.error("Exception in task {} by plugin {}", runnable, friendlyPluginName,
|
||||||
|
e);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (repeat == 0) {
|
if (repeat == 0) {
|
||||||
onFinish();
|
onFinish();
|
||||||
|
@ -125,7 +125,9 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a plugin message to the server through this connection.
|
* Sends a plugin message to the server through this connection. The message will be released
|
||||||
|
* afterwards.
|
||||||
|
*
|
||||||
* @param identifier the channel ID to use
|
* @param identifier the channel ID to use
|
||||||
* @param data the data
|
* @param data the data
|
||||||
* @return whether or not the message was sent
|
* @return whether or not the message was sent
|
||||||
@ -133,11 +135,12 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud
|
|||||||
public boolean sendPluginMessage(ChannelIdentifier identifier, ByteBuf data) {
|
public boolean sendPluginMessage(ChannelIdentifier identifier, ByteBuf data) {
|
||||||
for (ConnectedPlayer player : players.values()) {
|
for (ConnectedPlayer player : players.values()) {
|
||||||
VelocityServerConnection connection = player.getConnectedServer();
|
VelocityServerConnection connection = player.getConnectedServer();
|
||||||
if (connection != null && connection.getServerInfo().equals(serverInfo)) {
|
if (connection != null && connection.getServer() == this) {
|
||||||
return connection.sendPluginMessage(identifier, data);
|
return connection.sendPluginMessage(identifier, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,16 +21,17 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
|
|
||||||
public class VelocityTabList implements TabList {
|
public class VelocityTabList implements TabList {
|
||||||
|
|
||||||
|
protected final ConnectedPlayer player;
|
||||||
protected final MinecraftConnection connection;
|
protected final MinecraftConnection connection;
|
||||||
protected final Map<UUID, VelocityTabListEntry> entries = new ConcurrentHashMap<>();
|
protected final Map<UUID, VelocityTabListEntry> entries = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public VelocityTabList(MinecraftConnection connection) {
|
public VelocityTabList(final ConnectedPlayer player) {
|
||||||
this.connection = connection;
|
this.player = player;
|
||||||
|
this.connection = player.getConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setHeaderAndFooter(net.kyori.adventure.text.Component header,
|
public void setHeaderAndFooter(Component header, Component footer) {
|
||||||
net.kyori.adventure.text.Component footer) {
|
|
||||||
Preconditions.checkNotNull(header, "header");
|
Preconditions.checkNotNull(header, "header");
|
||||||
Preconditions.checkNotNull(footer, "footer");
|
Preconditions.checkNotNull(footer, "footer");
|
||||||
GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(
|
GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.velocitypowered.proxy.util.collect;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.Hash.Strategy;
|
||||||
|
|
||||||
|
public final class IdentityHashStrategy<T> implements Strategy<T> {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static final IdentityHashStrategy INSTANCE = new IdentityHashStrategy();
|
||||||
|
|
||||||
|
public static <T> Strategy<T> instance() {
|
||||||
|
//noinspection unchecked
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode(T o) {
|
||||||
|
return System.identityHashCode(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(T a, T b) {
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ bind = "0.0.0.0:25577"
|
|||||||
|
|
||||||
# What should be the MOTD? This gets displayed when the player adds your server to
|
# What should be the MOTD? This gets displayed when the player adds your server to
|
||||||
# their server list. Legacy color codes and JSON are accepted.
|
# their server list. Legacy color codes and JSON are accepted.
|
||||||
motd = "&3A Velocity Server"
|
motd = "	add3A Velocity Server"
|
||||||
|
|
||||||
# What should we display for the maximum number of players? (Velocity does not support a cap
|
# What should we display for the maximum number of players? (Velocity does not support a cap
|
||||||
# on the number of players online.)
|
# on the number of players online.)
|
||||||
@ -33,7 +33,7 @@ prevent-client-proxy-connections = false
|
|||||||
# Velocity's native forwarding. Only applicable for Minecraft 1.13 or higher.
|
# Velocity's native forwarding. Only applicable for Minecraft 1.13 or higher.
|
||||||
player-info-forwarding-mode = "NONE"
|
player-info-forwarding-mode = "NONE"
|
||||||
|
|
||||||
# If you are using modern or BungeeGuard IP forwarding, configure an unique secret here.
|
# If you are using modern or BungeeGuard IP forwarding, configure a unique secret here.
|
||||||
forwarding-secret = ""
|
forwarding-secret = ""
|
||||||
|
|
||||||
# Announce whether or not your server supports Forge. If you run a modded server, we
|
# Announce whether or not your server supports Forge. If you run a modded server, we
|
||||||
@ -69,7 +69,7 @@ lobby = "127.0.0.1:30066"
|
|||||||
factions = "127.0.0.1:30067"
|
factions = "127.0.0.1:30067"
|
||||||
minigames = "127.0.0.1:30068"
|
minigames = "127.0.0.1:30068"
|
||||||
|
|
||||||
# In what order we should try servers when a player logs in or is kicked from aserver.
|
# In what order we should try servers when a player logs in or is kicked from a server.
|
||||||
try = [
|
try = [
|
||||||
"lobby"
|
"lobby"
|
||||||
]
|
]
|
||||||
@ -105,8 +105,9 @@ connection-timeout = 5000
|
|||||||
# Specify a read timeout for connections here. The default is 30 seconds.
|
# Specify a read timeout for connections here. The default is 30 seconds.
|
||||||
read-timeout = 30000
|
read-timeout = 30000
|
||||||
|
|
||||||
# Enables compatibility with HAProxy.
|
# Enables compatibility with HAProxy's PROXY protocol. If you don't know what this is for, then
|
||||||
proxy-protocol = false
|
# don't enable it.
|
||||||
|
haproxy-protocol = false
|
||||||
|
|
||||||
# Enables TCP fast open support on the proxy. Requires the proxy to run on Linux.
|
# Enables TCP fast open support on the proxy. Requires the proxy to run on Linux.
|
||||||
tcp-fast-open = false
|
tcp-fast-open = false
|
||||||
@ -142,19 +143,6 @@ map = "Velocity"
|
|||||||
# Whether plugins should be shown in query response by default or not
|
# Whether plugins should be shown in query response by default or not
|
||||||
show-plugins = false
|
show-plugins = false
|
||||||
|
|
||||||
[metrics]
|
|
||||||
# Whether metrics will be reported to bStats (https://bstats.org).
|
|
||||||
# bStats collects some basic information, like how many people use Velocity and their
|
|
||||||
# player count. We recommend keeping bStats enabled, but if you're not comfortable with
|
|
||||||
# this, you can turn this setting off. There is no performance penalty associated with
|
|
||||||
# having metrics enabled, and data sent to bStats can't identify your server.
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
# A unique, anonymous ID to identify this proxy with.
|
|
||||||
id = ""
|
|
||||||
|
|
||||||
log-failure = false
|
|
||||||
|
|
||||||
# Legacy color codes and JSON are accepted in all messages.
|
# Legacy color codes and JSON are accepted in all messages.
|
||||||
[messages]
|
[messages]
|
||||||
# Prefix when the player gets kicked from a server.
|
# Prefix when the player gets kicked from a server.
|
||||||
@ -169,4 +157,4 @@ online-mode-only = "&cThis server only accepts connections from online-mode clie
|
|||||||
no-available-servers = "&cThere are no available servers."
|
no-available-servers = "&cThere are no available servers."
|
||||||
already-connected = "&cYou are already connected to this proxy!"
|
already-connected = "&cYou are already connected to this proxy!"
|
||||||
moved-to-new-server-prefix = "&cThe server you were on kicked you: "
|
moved-to-new-server-prefix = "&cThe server you were on kicked you: "
|
||||||
generic-connection-error = "&cAn internal error occurred in your connection."
|
generic-connection-error = "&cAn internal error occurred in your connection."
|
||||||
|
@ -24,6 +24,7 @@ import com.velocitypowered.api.command.SimpleCommand;
|
|||||||
import com.velocitypowered.proxy.event.MockEventManager;
|
import com.velocitypowered.proxy.event.MockEventManager;
|
||||||
import com.velocitypowered.proxy.event.VelocityEventManager;
|
import com.velocitypowered.proxy.event.VelocityEventManager;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -423,6 +424,54 @@ public class CommandManagerTests {
|
|||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo2 baz ").join());
|
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo2 baz ").join());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestionPermissions() throws ExecutionException, InterruptedException {
|
||||||
|
VelocityCommandManager manager = createManager();
|
||||||
|
RawCommand rawCommand = new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail("The Command should not be executed while testing suggestions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Invocation invocation) {
|
||||||
|
return invocation.arguments().length() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("suggestion");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.register(rawCommand, "foo");
|
||||||
|
|
||||||
|
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo").get().isEmpty());
|
||||||
|
assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo bar").get().isEmpty());
|
||||||
|
|
||||||
|
Command oldCommand = new Command() {
|
||||||
|
@Override
|
||||||
|
public void execute(CommandSource source, String @NonNull [] args) {
|
||||||
|
fail("The Command should not be executed while testing suggestions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(CommandSource source, String @NonNull [] args) {
|
||||||
|
return args.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
|
||||||
|
return ImmutableList.of("suggestion");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.register(oldCommand, "bar");
|
||||||
|
|
||||||
|
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar").get().isEmpty());
|
||||||
|
assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar foo").get().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
static class NoopSimpleCommand implements SimpleCommand {
|
static class NoopSimpleCommand implements SimpleCommand {
|
||||||
@Override
|
@Override
|
||||||
public void execute(final Invocation invocation) {
|
public void execute(final Invocation invocation) {
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren