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
|
||||
and pull requests:
|
||||
|
||||
* [Checker Framework](https://checkerframework.org/): an enhancement to Java's type
|
||||
system that is designed to help catch bugs. Velocity runs the _Nullness Checker_
|
||||
and the _Optional Checker_. The build will fail if Checker Framework notices an
|
||||
issue.
|
||||
* [SpotBugs](https://spotbugs.github.io/): ensures that common errors do not
|
||||
get into the codebase. The build will fail if SpotBugs finds an issue.
|
||||
* [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
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
|
||||
|
||||
// Mark sources as Java 8 source compatible
|
||||
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 {
|
||||
|
@ -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 {
|
||||
|
||||
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 name;
|
||||
|
@ -227,6 +227,11 @@ public final class ServerPing {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clearFavicon() {
|
||||
this.favicon = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -35,6 +35,11 @@ class MinecraftChannelIdentifierTest {
|
||||
assertEquals(expected, MinecraftChannelIdentifier.from("velocity:test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAllowsSlashes() {
|
||||
create("velocity", "test/test2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromIdentifierThrowsOnBadValues() {
|
||||
assertAll(
|
||||
|
@ -20,11 +20,11 @@ allprojects {
|
||||
|
||||
ext {
|
||||
// dependency versions
|
||||
adventureVersion = '4.1.1'
|
||||
adventureVersion = '4.5.0'
|
||||
junitVersion = '5.7.0'
|
||||
slf4jVersion = '1.7.30'
|
||||
log4jVersion = '2.13.3'
|
||||
nettyVersion = '4.1.56.Final'
|
||||
nettyVersion = '4.1.59.Final'
|
||||
guavaVersion = '30.0-jre'
|
||||
checkerFrameworkVersion = '3.6.1'
|
||||
configurateVersion = '4.0.0-SNAPSHOT'
|
||||
|
@ -55,13 +55,13 @@ dependencies {
|
||||
implementation "io.netty:netty-handler:${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-aarch64"
|
||||
implementation "io.netty:netty-resolver-dns:${nettyVersion}"
|
||||
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch_64"
|
||||
|
||||
implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||
implementation "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
||||
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${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.minecrell:terminalconsoleappender:1.2.0'
|
||||
@ -76,7 +76,7 @@ dependencies {
|
||||
implementation 'com.spotify:completable-futures:0.3.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 '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/*Object2IntArray*'
|
||||
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/*Object2Long*'
|
||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2Object*'
|
||||
@ -127,12 +126,10 @@ shadowJar {
|
||||
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
|
||||
exclude 'it/unimi/dsi/fastutil/shorts/**'
|
||||
exclude 'org/checkerframework/checker/**'
|
||||
|
||||
relocate 'org.bstats', 'com.velocitypowered.proxy.bstats'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives shadowJar
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
@ -1,82 +1,67 @@
|
||||
package com.velocitypowered.proxy;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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.Pattern;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.asynchttpclient.ListenableFuture;
|
||||
import org.asynchttpclient.Response;
|
||||
import org.bstats.MetricsBase;
|
||||
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 {
|
||||
|
||||
// The version of this bStats class
|
||||
private static final int B_STATS_METRICS_REVISION = 2;
|
||||
private MetricsBase metricsBase;
|
||||
|
||||
// The url to which the data is sent
|
||||
private static final String URL = "https://bstats.org/submitData/server-implementation";
|
||||
private Metrics(Logger logger, int serviceId, boolean defaultEnabled) {
|
||||
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
|
||||
private static final Logger logger = LogManager.getLogger(Metrics.class);
|
||||
metricsBase = new MetricsBase(
|
||||
"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?
|
||||
private boolean logFailedRequests = false;
|
||||
|
||||
// The name of the server software
|
||||
private final String name;
|
||||
|
||||
// The plugin ID for the server software as assigned by bStats.
|
||||
private final int pluginId;
|
||||
|
||||
// The uuid of the server
|
||||
private final String serverUuid;
|
||||
|
||||
// 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();
|
||||
if (!config.didExistBefore()) {
|
||||
// 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"
|
||||
+ " and send them to bStats (https://bStats.org).");
|
||||
logger.info("bStats collects some basic information for plugin"
|
||||
+ " authors, like how many people use");
|
||||
logger.info("their plugin and their total player count."
|
||||
+ " It's recommended to keep bStats enabled, but");
|
||||
logger.info("if you're not comfortable with this, you can opt-out"
|
||||
+ " by editing the config.txt file in");
|
||||
logger.info("the '/plugins/bStats/' folder and setting enabled to false.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,544 +70,69 @@ public class Metrics {
|
||||
* @param chart The chart to add.
|
||||
*/
|
||||
public void addCustomChart(CustomChart chart) {
|
||||
if (chart == null) {
|
||||
throw new IllegalArgumentException("Chart cannot be null!");
|
||||
}
|
||||
charts.add(chart);
|
||||
metricsBase.addCustomChart(chart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Scheduler which submits our data every 30 minutes.
|
||||
*/
|
||||
private void startSubmitting() {
|
||||
final Timer timer = new Timer(true);
|
||||
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;
|
||||
}
|
||||
|
||||
private void appendPlatformData(JsonObjectBuilder builder) {
|
||||
builder.appendField("osName", System.getProperty("os.name"));
|
||||
builder.appendField("osArch", System.getProperty("os.arch"));
|
||||
builder.appendField("osVersion", System.getProperty("os.version"));
|
||||
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
|
||||
static class VelocityMetrics {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(Metrics.class);
|
||||
|
||||
static void startMetrics(VelocityServer server, VelocityConfiguration.Metrics metricsConfig) {
|
||||
if (!metricsConfig.isFromConfig()) {
|
||||
// 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.");
|
||||
}
|
||||
Metrics metrics = new Metrics(logger, 4752, metricsConfig.isEnabled());
|
||||
|
||||
// Load the data
|
||||
String serverUuid = metricsConfig.getId();
|
||||
boolean logFailedRequests = metricsConfig.isLogFailure();
|
||||
// Only start Metrics, if it's enabled in the config
|
||||
if (metricsConfig.isEnabled()) {
|
||||
Metrics metrics = new Metrics("Velocity", 4752, serverUuid, logFailedRequests, server);
|
||||
metrics.addCustomChart(
|
||||
new SingleLineChart("players", server::getPlayerCount)
|
||||
);
|
||||
metrics.addCustomChart(
|
||||
new SingleLineChart("managed_servers", () -> server.getAllServers().size())
|
||||
);
|
||||
metrics.addCustomChart(
|
||||
new SimplePie("online_mode",
|
||||
() -> server.getConfiguration().isOnlineMode() ? "online" : "offline")
|
||||
);
|
||||
metrics.addCustomChart(new SimplePie("velocity_version",
|
||||
() -> server.getVersion().getVersion()));
|
||||
|
||||
metrics.addCustomChart(
|
||||
new Metrics.SingleLineChart("players", server::getPlayerCount)
|
||||
);
|
||||
metrics.addCustomChart(
|
||||
new Metrics.SingleLineChart("managed_servers", () -> server.getAllServers().size())
|
||||
);
|
||||
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 DrilldownPie("java_version", () -> {
|
||||
Map<String, Map<String, Integer>> map = new HashMap<>();
|
||||
String javaVersion = System.getProperty("java.version");
|
||||
Map<String, Integer> entry = new HashMap<>();
|
||||
entry.put(javaVersion, 1);
|
||||
|
||||
metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
|
||||
Map<String, Map<String, Integer>> map = new HashMap<>();
|
||||
String javaVersion = System.getProperty("java.version");
|
||||
Map<String, Integer> entry = new HashMap<>();
|
||||
entry.put(javaVersion, 1);
|
||||
// http://openjdk.java.net/jeps/223
|
||||
// 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;
|
||||
|
||||
// http://openjdk.java.net/jeps/223
|
||||
// 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('.');
|
||||
|
||||
int indexOf = javaVersion.lastIndexOf('.');
|
||||
|
||||
if (majorVersion.equals("1")) {
|
||||
release = "Java " + javaVersion.substring(0, indexOf);
|
||||
} else {
|
||||
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
|
||||
// would it valid strings for the major may potentially include values such as -ea to
|
||||
// denote a pre release
|
||||
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
|
||||
if (versionMatcher.find()) {
|
||||
majorVersion = versionMatcher.group(0);
|
||||
}
|
||||
release = "Java " + majorVersion;
|
||||
if (majorVersion.equals("1")) {
|
||||
release = "Java " + javaVersion.substring(0, indexOf);
|
||||
} else {
|
||||
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
|
||||
// would it valid strings for the major may potentially include values such as -ea to
|
||||
// denote a pre release
|
||||
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
|
||||
if (versionMatcher.find()) {
|
||||
majorVersion = versionMatcher.group(0);
|
||||
}
|
||||
map.put(release, entry);
|
||||
|
||||
return map;
|
||||
}));
|
||||
}
|
||||
release = "Java " + majorVersion;
|
||||
}
|
||||
map.put(release, entry);
|
||||
|
||||
return map;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -8,9 +8,12 @@ import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class Velocity {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(Velocity.class);
|
||||
private static final Logger logger;
|
||||
|
||||
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
|
||||
// memory for Minecraft, which imposes a maximum packet size of 2MiB! We'll use 4MiB as a more
|
||||
// sane default.
|
||||
|
@ -59,6 +59,9 @@ public interface CommandNodeFactory<T extends Command> {
|
||||
(context, builder) -> {
|
||||
I invocation = createInvocation(context);
|
||||
|
||||
if (!command.hasPermission(invocation)) {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
return command.suggestAsync(invocation).thenApply(values -> {
|
||||
for (String value : values) {
|
||||
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.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -188,6 +189,7 @@ public class VelocityCommand implements SimpleCommand {
|
||||
|
||||
private static class Info implements SubCommand {
|
||||
|
||||
private static final TextColor VELOCITY_COLOR = TextColor.fromHexString("#09add3");
|
||||
private final ProxyServer server;
|
||||
|
||||
private Info(ProxyServer server) {
|
||||
@ -205,11 +207,11 @@ public class VelocityCommand implements SimpleCommand {
|
||||
|
||||
TextComponent velocity = Component.text().content(version.getName() + " ")
|
||||
.decoration(TextDecoration.BOLD, true)
|
||||
.color(NamedTextColor.DARK_AQUA)
|
||||
.color(VELOCITY_COLOR)
|
||||
.append(Component.text(version.getVersion()).decoration(TextDecoration.BOLD, false))
|
||||
.build();
|
||||
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.");
|
||||
source.sendMessage(Identity.nil(), velocity);
|
||||
source.sendMessage(Identity.nil(), copyright);
|
||||
|
@ -30,7 +30,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
@ -426,11 +425,6 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
}
|
||||
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) {
|
||||
config.save();
|
||||
}
|
||||
@ -448,7 +442,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
PingPassthroughMode.DISABLED);
|
||||
|
||||
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);
|
||||
Boolean onlineMode = config.getOrElse("online-mode", 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.connectionTimeout = config.getIntOrElse("connection-timeout", 5000);
|
||||
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.bungeePluginMessageChannel = config.getOrElse("bungee-plugin-message-channel", true);
|
||||
this.showPingRequests = config.getOrElse("show-ping-requests", false);
|
||||
@ -769,39 +767,16 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
|
||||
public static class Metrics {
|
||||
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) {
|
||||
if (toml != null) {
|
||||
this.enabled = toml.getOrElse("enabled", false);
|
||||
this.id = toml.getOrElse("id", UUID.randomUUID().toString());
|
||||
this.logFailure = toml.getOrElse("log-failure", false);
|
||||
this.fromConfig = true;
|
||||
this.enabled = toml.getOrElse("enabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isLogFailure() {
|
||||
return logFailure;
|
||||
}
|
||||
|
||||
public boolean isFromConfig() {
|
||||
return fromConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Messages {
|
||||
|
@ -196,6 +196,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
public void write(Object msg) {
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(msg, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,6 +208,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
public void delayedWrite(Object msg) {
|
||||
if (channel.isActive()) {
|
||||
channel.write(msg, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,16 +383,25 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
if (threshold == -1) {
|
||||
channel.pipeline().remove(COMPRESSION_DECODER);
|
||||
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.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
import java.util.Collection;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -38,6 +39,8 @@ import org.apache.logging.log4j.Logger;
|
||||
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
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 VelocityServerConnection serverConn;
|
||||
private final ClientPlaySessionHandler playerSessionHandler;
|
||||
@ -64,10 +67,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public void activated() {
|
||||
serverConn.getServer().addPlayer(serverConn.getPlayer());
|
||||
MinecraftConnection serverMc = serverConn.ensureConnected();
|
||||
serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(),
|
||||
ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())), ServerboundPluginMessagePacket.FACTORY
|
||||
));
|
||||
|
||||
if (server.getConfiguration().isBungeePluginChannelEnabled()) {
|
||||
MinecraftConnection serverMc = serverConn.ensureConnected();
|
||||
serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(),
|
||||
ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion()))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
proxy.getPlayer(in.readUTF())
|
||||
.flatMap(Player::getCurrentServer)
|
||||
.ifPresent(server -> sendServerResponse(player, prepareForwardMessage(in)));
|
||||
Optional<Player> player = proxy.getPlayer(in.readUTF());
|
||||
if (player.isPresent()) {
|
||||
ByteBuf toForward = in.unwrap().copy();
|
||||
sendServerResponse((ConnectedPlayer) player.get(), toForward);
|
||||
}
|
||||
}
|
||||
|
||||
private void processForwardToServer(ByteBufDataInput in) {
|
||||
String target = in.readUTF();
|
||||
ByteBuf toForward = prepareForwardMessage(in);
|
||||
ByteBuf toForward = in.unwrap().copy();
|
||||
if (target.equals("ALL")) {
|
||||
ByteBuf unreleasableForward = Unpooled.unreleasableBuffer(toForward);
|
||||
try {
|
||||
for (RegisteredServer rs : proxy.getAllServers()) {
|
||||
((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL, unreleasableForward);
|
||||
((VelocityRegisteredServer) rs).sendPluginMessage(LEGACY_CHANNEL,
|
||||
toForward.retainedSlice());
|
||||
}
|
||||
} finally {
|
||||
toForward.release();
|
||||
}
|
||||
} else {
|
||||
proxy.getServer(target).ifPresent(rs -> ((VelocityRegisteredServer) rs)
|
||||
.sendPluginMessage(LEGACY_CHANNEL, toForward));
|
||||
Optional<RegisteredServer> server = proxy.getServer(target);
|
||||
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) {
|
||||
return version.gte(ProtocolVersion.MINECRAFT_1_13) ? MODERN_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!
|
||||
private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) {
|
||||
MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected();
|
||||
String chan = getBungeeCordChannel(serverConnection.getProtocolVersion());
|
||||
|
||||
ServerboundPluginMessagePacket msg = null;
|
||||
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();
|
||||
}
|
||||
}
|
||||
ServerboundPluginMessagePacket msg = new ServerboundPluginMessagePacket(chan, buf);
|
||||
serverConnection.write(msg);
|
||||
}
|
||||
|
||||
boolean process(AbstractPluginMessagePacket<?> message) {
|
||||
|
@ -32,7 +32,9 @@ import io.netty.channel.ChannelFutureListener;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.UnaryOperator;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
@ -47,8 +49,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
private boolean hasCompletedJoin = false;
|
||||
private boolean gracefulDisconnect = false;
|
||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||
private long lastPingId;
|
||||
private long lastPingSent;
|
||||
private final Map<Long, Long> pendingPings = new HashMap<>();
|
||||
private @MonotonicNonNull DimensionRegistry activeDimensionRegistry;
|
||||
|
||||
/**
|
||||
@ -112,7 +113,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
return getHandshakeRemoteAddress();
|
||||
}
|
||||
StringBuilder data = new StringBuilder()
|
||||
.append(getHandshakeRemoteAddress())
|
||||
.append(registeredServer.getServerInfo().getAddress().getHostString())
|
||||
.append('\0')
|
||||
.append(((InetSocketAddress) proxyPlayer.getRemoteAddress()).getHostString())
|
||||
.append('\0')
|
||||
@ -259,21 +260,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
return gracefulDisconnect;
|
||||
}
|
||||
|
||||
public long getLastPingId() {
|
||||
return lastPingId;
|
||||
}
|
||||
|
||||
public long getLastPingSent() {
|
||||
return lastPingSent;
|
||||
}
|
||||
|
||||
void setLastPingId(long lastPingId) {
|
||||
this.lastPingId = lastPingId;
|
||||
this.lastPingSent = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void resetLastPingId() {
|
||||
this.lastPingId = -1;
|
||||
public Map<Long, Long> getPendingPings() {
|
||||
return pendingPings;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.proxy.network.PluginMessageUtil.constructChannelsPacket;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult;
|
||||
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.PlayerResourcePackStatusEvent;
|
||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
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.connection.ConnectionTypes;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
@ -102,12 +106,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public boolean handle(ServerboundKeepAlivePacket packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection != null && packet.getRandomId() == serverConnection.getLastPingId()) {
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null) {
|
||||
player.setPing(System.currentTimeMillis() - serverConnection.getLastPingSent());
|
||||
smc.write(packet);
|
||||
serverConnection.resetLastPingId();
|
||||
if (serverConnection != null) {
|
||||
Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
||||
if (sentTime != null) {
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null) {
|
||||
player.setPing(System.currentTimeMillis() - sentTime);
|
||||
smc.write(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@ -191,7 +197,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
logger.warn("A plugin message was received while the backend server was not "
|
||||
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
||||
} 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());
|
||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||
@ -303,7 +320,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
boolean writable = player.getConnection().getChannel().isWritable();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,7 @@ import java.net.SocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@ -103,6 +104,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
private @Nullable VelocityServerConnection connectionInFlight;
|
||||
private @Nullable PlayerSettings settings;
|
||||
private @Nullable ModInfo modInfo;
|
||||
private Component playerListHeader = Component.empty();
|
||||
private Component playerListFooter = Component.empty();
|
||||
private final VelocityTabList tabList;
|
||||
private final VelocityServer server;
|
||||
private ClientConnectionPhase connectionPhase;
|
||||
@ -113,11 +116,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
||||
@Nullable InetSocketAddress virtualHost, boolean onlineMode) {
|
||||
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.connection = connection;
|
||||
this.virtualHost = virtualHost;
|
||||
@ -125,6 +123,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
||||
this.onlineMode = onlineMode;
|
||||
|
||||
if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||
this.tabList = new VelocityTabList(this);
|
||||
} else {
|
||||
this.tabList = new VelocityTabListLegacy(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -265,46 +269,85 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showTitle(net.kyori.adventure.title.@NonNull Title title) {
|
||||
GsonComponentSerializer serializer = ProtocolUtils.getJsonChatSerializer(this
|
||||
.getProtocolVersion());
|
||||
public Component getPlayerListHeader() {
|
||||
return this.playerListHeader;
|
||||
}
|
||||
|
||||
connection.delayedWrite(new ClientboundTitlePacket(
|
||||
ClientboundTitlePacket.SET_TITLE,
|
||||
serializer.serialize(title.title())
|
||||
));
|
||||
@Override
|
||||
public Component getPlayerListFooter() {
|
||||
return this.playerListFooter;
|
||||
}
|
||||
|
||||
connection.delayedWrite(new ClientboundTitlePacket(
|
||||
ClientboundTitlePacket.SET_SUBTITLE,
|
||||
serializer.serialize(title.subtitle())
|
||||
));
|
||||
@Override
|
||||
public void sendPlayerListHeader(@NonNull final Component header) {
|
||||
this.sendPlayerListHeaderAndFooter(header, this.playerListFooter);
|
||||
}
|
||||
|
||||
net.kyori.adventure.title.Title.Times times = title.times();
|
||||
if (times != null) {
|
||||
connection.delayedWrite(ClientboundTitlePacket.times(this.getProtocolVersion(), times));
|
||||
@Override
|
||||
public void sendPlayerListFooter(@NonNull final Component footer) {
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
@ -317,6 +360,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
this.profile = profile.withProperties(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearHeaderAndFooter() {
|
||||
tabList.clearHeaderAndFooter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VelocityTabList getTabList() {
|
||||
return tabList;
|
||||
@ -412,7 +460,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
logger.error("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
||||
plainTextReason);
|
||||
handleConnectionException(server, disconnectReason, Component.text()
|
||||
.append(messages.getKickPrefix(server.getServerInfo().getName()))
|
||||
.append(messages.getKickPrefix(server.getServerInfo().getName())
|
||||
.colorIfAbsent(NamedTextColor.RED))
|
||||
.color(NamedTextColor.RED)
|
||||
.append(disconnectReason)
|
||||
.build(), safe);
|
||||
@ -420,8 +469,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
logger.error("{}: disconnected while connecting to {}: {}", this,
|
||||
server.getServerInfo().getName(), plainTextReason);
|
||||
handleConnectionException(server, disconnectReason, Component.text()
|
||||
.append(messages.getDisconnectPrefix(server.getServerInfo().getName()))
|
||||
.color(NamedTextColor.RED)
|
||||
.append(messages.getDisconnectPrefix(server.getServerInfo().getName())
|
||||
.colorIfAbsent(NamedTextColor.RED))
|
||||
.append(disconnectReason)
|
||||
.build(), safe);
|
||||
}
|
||||
@ -678,7 +727,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
public void sendResourcePack(String 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
|
||||
@ -687,7 +738,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
Preconditions.checkNotNull(hash, "hash");
|
||||
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.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@ -90,7 +91,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
try {
|
||||
KeyPair serverKeyPair = server.getServerKeyPair();
|
||||
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.");
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@ -227,11 +229,12 @@ public enum ProtocolUtils {
|
||||
/**
|
||||
* Reads a {@link net.kyori.adventure.nbt.CompoundBinaryTag} from the {@code buf}.
|
||||
* @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
|
||||
*/
|
||||
public static CompoundBinaryTag readCompoundTag(ByteBuf buf) {
|
||||
public static CompoundBinaryTag readCompoundTag(ByteBuf buf, BinaryTagIO.Reader reader) {
|
||||
try {
|
||||
return BinaryTagIO.readDataInput(new ByteBufInputStream(buf));
|
||||
return reader.read((DataInput) new ByteBufInputStream(buf));
|
||||
} catch (IOException thrown) {
|
||||
throw new DecoderException(
|
||||
"Unable to parse NBT CompoundTag, full error: " + thrown.getMessage());
|
||||
@ -245,7 +248,7 @@ public enum ProtocolUtils {
|
||||
*/
|
||||
public static void writeCompoundTag(ByteBuf buf, CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
BinaryTagIO.writeDataOutput(compoundTag, new ByteBufOutputStream(buf));
|
||||
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(buf));
|
||||
} catch (IOException e) {
|
||||
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.serialization.brigadier.ArgumentPropertyRegistry;
|
||||
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.Object2IntMap;
|
||||
import java.util.ArrayDeque;
|
||||
@ -99,7 +100,8 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
||||
public void encode(ByteBuf buf, PacketDirection direction, ProtocolVersion protocolVersion) {
|
||||
// Assign all the children an index.
|
||||
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()) {
|
||||
CommandNode<CommandSource> child = childrenQueue.poll();
|
||||
if (!idMappings.containsKey(child)) {
|
||||
@ -207,6 +209,7 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
||||
private final int redirectTo;
|
||||
private final @Nullable ArgumentBuilder<CommandSource, ?> args;
|
||||
private @MonotonicNonNull CommandNode<CommandSource> built;
|
||||
private boolean validated;
|
||||
|
||||
private WireNode(int idx, byte flags, int[] children, int redirectTo,
|
||||
@Nullable ArgumentBuilder<CommandSource, ?> args) {
|
||||
@ -215,18 +218,34 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
||||
this.children = children;
|
||||
this.redirectTo = redirectTo;
|
||||
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) {
|
||||
if (this.built == null) {
|
||||
// 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 >= wireNodes.length) {
|
||||
throw new IllegalStateException("Node points to non-existent index " + redirectTo);
|
||||
}
|
||||
}
|
||||
if (!this.validated) {
|
||||
this.validate(wireNodes);
|
||||
}
|
||||
|
||||
if (this.built == null) {
|
||||
int type = flags & FLAG_NODE_TYPE;
|
||||
if (type == NODE_TYPE_ROOT) {
|
||||
this.built = new RootCommandNode<>();
|
||||
@ -237,10 +256,6 @@ public class ClientboundAvailableCommandsPacket implements Packet {
|
||||
|
||||
// Add any redirects
|
||||
if (redirectTo != -1) {
|
||||
if (redirectTo >= wireNodes.length) {
|
||||
throw new IllegalStateException("Node points to non-existent index " + redirectTo);
|
||||
}
|
||||
|
||||
WireNode redirect = wireNodes[redirectTo];
|
||||
if (redirect.built != null) {
|
||||
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.PacketReader;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||
@ -20,6 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
public class ClientboundJoinGamePacket implements Packet {
|
||||
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 short gamemode;
|
||||
private int dimension;
|
||||
@ -37,14 +39,136 @@ public class ClientboundJoinGamePacket implements Packet {
|
||||
private short previousGamemode; // 1.16+
|
||||
private CompoundBinaryTag biomeRegistry; // 1.16.2+
|
||||
|
||||
public void withDimension(int dimension) {
|
||||
this.dimension = dimension;
|
||||
public int getEntityId() {
|
||||
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) {
|
||||
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
|
||||
public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) {
|
||||
this.entityId = buf.readInt();
|
||||
@ -61,7 +185,7 @@ public class ClientboundJoinGamePacket implements Packet {
|
||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16)) {
|
||||
this.previousGamemode = buf.readByte();
|
||||
ImmutableSet<String> levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf));
|
||||
CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf);
|
||||
CompoundBinaryTag registryContainer = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER);
|
||||
ListBinaryTag dimensionRegistryContainer = null;
|
||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
||||
dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type")
|
||||
@ -74,8 +198,8 @@ public class ClientboundJoinGamePacket implements Packet {
|
||||
ImmutableSet<DimensionData> readData =
|
||||
DimensionRegistry.fromGameData(dimensionRegistryContainer, version);
|
||||
this.dimensionRegistry = new DimensionRegistry(readData, levelNames);
|
||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
||||
CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf);
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER);
|
||||
dimensionIdentifier = ProtocolUtils.readString(buf);
|
||||
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version)
|
||||
.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
|
||||
public boolean handle(PacketHandler handler) {
|
||||
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.PacketReader;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
|
||||
public class ClientboundRespawnPacket implements Packet {
|
||||
@ -102,9 +103,9 @@ public class ClientboundRespawnPacket implements Packet {
|
||||
public void decode(ByteBuf buf, PacketDirection direction, ProtocolVersion version) {
|
||||
String dimensionIdentifier = null;
|
||||
String levelName = null;
|
||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16)) {
|
||||
if (version.gte(ProtocolVersion.MINECRAFT_1_16_2)) {
|
||||
CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf);
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader());
|
||||
dimensionIdentifier = ProtocolUtils.readString(buf);
|
||||
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version)
|
||||
.annotateWith(dimensionIdentifier, null);
|
||||
|
@ -20,7 +20,7 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
Boolean.getBoolean("velocity.increased-compression-cap")
|
||||
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
|
||||
|
||||
private final int threshold;
|
||||
private int threshold;
|
||||
private final 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 {
|
||||
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> {
|
||||
|
||||
private final int threshold;
|
||||
private int threshold;
|
||||
private final VelocityCompressor compressor;
|
||||
|
||||
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
|
||||
@ -20,7 +20,7 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
|
||||
int uncompressed = msg.readableBytes();
|
||||
if (uncompressed <= threshold) {
|
||||
if (uncompressed < threshold) {
|
||||
// Under the threshold, there is nothing to do.
|
||||
ProtocolUtils.writeVarInt(out, 0);
|
||||
out.writeBytes(msg);
|
||||
@ -54,4 +54,8 @@ public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||
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 static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging");
|
||||
private static final QuietDecoderException DECODE_FAILED =
|
||||
new QuietDecoderException("A packet did not decode successfully (invalid data). If you are a "
|
||||
private static final QuietRuntimeException DECODE_FAILED =
|
||||
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.");
|
||||
|
||||
private final PacketDirection direction;
|
||||
@ -64,8 +64,16 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
||||
ctx.fireChannelRead(buf);
|
||||
} else {
|
||||
try {
|
||||
doLengthSanityChecks(buf, packet);
|
||||
|
||||
try {
|
||||
packet.decode(buf, direction, registry.version);
|
||||
} catch (Exception e) {
|
||||
throw handleDecodeFailure(e, packet, packetId);
|
||||
}
|
||||
|
||||
if (buf.isReadable()) {
|
||||
throw handleNotReadEnough(packet, packetId);
|
||||
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
|
||||
}
|
||||
ctx.fireChannelRead(packet);
|
||||
} 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) {
|
||||
return new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " "
|
||||
+ getExtraConnectionDetail(packetId));
|
||||
return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too "
|
||||
+ "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 {
|
||||
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.Multimaps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.Scheduler;
|
||||
@ -183,8 +184,18 @@ public class VelocityScheduler implements Scheduler {
|
||||
currentTaskThread = Thread.currentThread();
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin, e);
|
||||
} catch (Throwable 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 {
|
||||
if (repeat == 0) {
|
||||
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 data the data
|
||||
* @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) {
|
||||
for (ConnectedPlayer player : players.values()) {
|
||||
VelocityServerConnection connection = player.getConnectedServer();
|
||||
if (connection != null && connection.getServerInfo().equals(serverInfo)) {
|
||||
if (connection != null && connection.getServer() == this) {
|
||||
return connection.sendPluginMessage(identifier, data);
|
||||
}
|
||||
}
|
||||
|
||||
data.release();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -21,16 +21,17 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class VelocityTabList implements TabList {
|
||||
|
||||
protected final ConnectedPlayer player;
|
||||
protected final MinecraftConnection connection;
|
||||
protected final Map<UUID, VelocityTabListEntry> entries = new ConcurrentHashMap<>();
|
||||
|
||||
public VelocityTabList(MinecraftConnection connection) {
|
||||
this.connection = connection;
|
||||
public VelocityTabList(final ConnectedPlayer player) {
|
||||
this.player = player;
|
||||
this.connection = player.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeaderAndFooter(net.kyori.adventure.text.Component header,
|
||||
net.kyori.adventure.text.Component footer) {
|
||||
public void setHeaderAndFooter(Component header, Component footer) {
|
||||
Preconditions.checkNotNull(header, "header");
|
||||
Preconditions.checkNotNull(footer, "footer");
|
||||
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
|
||||
# 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
|
||||
# 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.
|
||||
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 = ""
|
||||
|
||||
# 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"
|
||||
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 = [
|
||||
"lobby"
|
||||
]
|
||||
@ -105,8 +105,9 @@ connection-timeout = 5000
|
||||
# Specify a read timeout for connections here. The default is 30 seconds.
|
||||
read-timeout = 30000
|
||||
|
||||
# Enables compatibility with HAProxy.
|
||||
proxy-protocol = false
|
||||
# Enables compatibility with HAProxy's PROXY protocol. If you don't know what this is for, then
|
||||
# don't enable it.
|
||||
haproxy-protocol = false
|
||||
|
||||
# Enables TCP fast open support on the proxy. Requires the proxy to run on Linux.
|
||||
tcp-fast-open = false
|
||||
@ -142,19 +143,6 @@ map = "Velocity"
|
||||
# Whether plugins should be shown in query response by default or not
|
||||
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.
|
||||
[messages]
|
||||
# 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."
|
||||
already-connected = "&cYou are already connected to this proxy!"
|
||||
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.VelocityEventManager;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -423,6 +424,54 @@ public class CommandManagerTests {
|
||||
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 {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren