Compare commits
219
Commits
updatev2
...
a52f338383
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a52f338383 | ||
|
|
7c79516567 | ||
|
|
f4eb5c7536 | ||
|
|
664826fec3 | ||
|
|
fb0f21f425 | ||
|
|
5bb1992014 | ||
|
|
f975230f14 | ||
|
|
a6f9de9581 | ||
|
|
5000227d11 | ||
|
|
c10b492554 | ||
|
|
dff94da2b3 | ||
|
|
1b48f40534 | ||
|
|
bc55300123 | ||
|
|
15b6ddd1bf | ||
|
|
21bbf35d1d | ||
|
|
e84b48a716 | ||
|
|
940f395e3e | ||
|
|
ddea763c8b | ||
|
|
a3b6177059 | ||
|
|
3ef5b2ab99 | ||
|
|
7a8bf8a5e0 | ||
|
|
05de745780 | ||
|
|
75ce1a9bf7 | ||
|
|
db0a17eea6 | ||
|
|
133f0e36b1 | ||
|
|
b2ba2e66bf | ||
|
|
a078053e26 | ||
|
|
7a39ebb058 | ||
|
|
57e9cd7f24 | ||
|
|
4772ca3022 | ||
|
|
b9463125d1 | ||
|
|
f918d0d649 | ||
|
|
e6fbcc9196 | ||
|
|
14a69904f9 | ||
|
|
71c50a75eb | ||
|
|
00759e5279 | ||
|
|
06ade4775e | ||
|
|
2676520c6a | ||
|
|
e11584ba35 | ||
|
|
a08972749b | ||
|
|
c6e9ca989e | ||
|
|
d30f1d9a74 | ||
|
|
b45716deff | ||
|
|
e653647962 | ||
|
|
5aab0d1427 | ||
|
|
1cd8d51d02 | ||
|
|
9eb338bd1c | ||
|
|
60380211f8 | ||
|
|
3b239daf4c | ||
|
|
90f89053a7 | ||
|
|
67e16a308e
|
||
|
|
4bd30bac33 | ||
|
|
28c9f5a356 | ||
|
|
da7427fb51 | ||
|
|
b8d1f16b07 | ||
|
|
729a050662 | ||
|
|
c690b4abfc | ||
|
|
5cc6b0b7e5 | ||
|
|
757711a5bc | ||
|
|
95694cba28 | ||
|
|
7cb01b85f0 | ||
|
|
0942e162c0 | ||
|
|
c1cd71a4bc | ||
|
|
843a47e2a3 | ||
|
|
4498f1e03d | ||
|
|
1abb3ba56a | ||
|
|
a5680fc223 | ||
|
|
ffca982a9d | ||
|
|
2b5d964ab0 | ||
|
|
f11cc86e69 | ||
|
|
06eb052ab5 | ||
|
|
97b386d86f | ||
|
|
81a5817a82 | ||
|
|
1edab1411d | ||
|
|
a7581821fb | ||
|
|
d7ad0522e9 | ||
|
|
0cbe10e951 | ||
|
|
bcf1bba3a1 | ||
|
|
3b892899db | ||
|
|
1b64a70a00 | ||
|
|
3b142f3099 | ||
|
|
25fbd833cd | ||
|
|
b72cf26802 | ||
|
|
7d68208e11 | ||
|
|
38ff21afd7 | ||
|
|
4b8ceb5264 | ||
|
|
2d1cade7bd | ||
|
|
4067c44cd1 | ||
|
|
45bb5de601 | ||
|
|
ebb9eb046d | ||
|
|
afc84b88ac | ||
|
|
e6caa87865 | ||
|
|
cdeabe91d0 | ||
|
|
9c0c9b0218 | ||
|
|
ad8de4361c | ||
|
|
f712997dd7 | ||
|
|
a6d97e28ad | ||
|
|
339a4c1887 | ||
|
|
1a41b77ccb | ||
|
|
e834af9cf1 | ||
|
|
affc1d6e08 | ||
|
|
6ce432e4ae | ||
|
|
b1a1b8bda3 | ||
|
|
6f01587318 | ||
|
|
308ce6d992 | ||
|
|
9890c429c6 | ||
|
|
0219993c8a | ||
|
|
f6d48c90f9 | ||
|
|
ab99bde9d6 | ||
|
|
2cf181df0c | ||
|
|
d11511c184 | ||
|
|
99bd030996 | ||
|
|
5017f8c9f2 | ||
|
|
e8b64aa6c0 | ||
|
|
470cd7a9d2 | ||
|
|
0783b1d4e4 | ||
|
|
747cc8984f | ||
|
|
e0db25664f | ||
|
|
6aff78728c | ||
|
|
c2fd3c07ac | ||
|
|
2535751cd9 | ||
|
|
7e01491e2f | ||
|
|
14160e1988 | ||
|
|
9bfe19f795 | ||
|
|
7d0c002f89 | ||
|
|
5320aae5d9 | ||
|
|
fe8dc5eaa2 | ||
|
|
eb2bea79ba | ||
|
|
75ecb64159 | ||
|
|
d69431a08e | ||
|
|
7e4f37b5f5 | ||
|
|
1df79a403b | ||
|
|
6b1ea78ff7 | ||
|
|
3022793418 | ||
|
|
21671daebe | ||
|
|
372a3b28bd | ||
|
|
a03bd884aa | ||
|
|
4bc3f00424 | ||
|
|
8f65a81420 | ||
|
|
6cc1be7746 | ||
|
|
a046f70075 | ||
|
|
75d68115ef | ||
|
|
babb22bf88
|
||
|
|
468127996c
|
||
|
|
b6b6b20fe9 | ||
|
|
f75b512837 | ||
|
|
7412aca81c | ||
|
|
02cf349075 | ||
|
|
67b988e6d2 | ||
|
|
d2c13c2a4c | ||
|
|
498a38cf74 | ||
|
|
13a1c93ea6 | ||
|
|
38a0a7ed27 | ||
|
|
70c3eabdb1 | ||
|
|
4cd3b68697 | ||
|
|
1140fc65ba | ||
|
|
5753548b44 | ||
|
|
806b386cdb | ||
|
|
d266059abe | ||
|
|
b1dd26fbc4 | ||
|
|
c8c27af7c3 | ||
|
|
ba01492790 | ||
|
|
94368d5021 | ||
|
|
ec793a9fdb | ||
|
|
37f622f226 | ||
|
|
87f74eaeda | ||
|
|
8406979e71 | ||
|
|
6e80f57739 | ||
|
|
1532fb360b | ||
|
|
180af8c844 | ||
|
|
311e2bc18d | ||
|
|
bfd15e1a81 | ||
|
|
d2d333a958 | ||
|
|
60a22ff330 | ||
|
|
946e5c47d4 | ||
|
|
5d450ab3c7 | ||
|
|
a509a878e9 | ||
|
|
49e2988e37 | ||
|
|
db8d16fd6e | ||
|
|
d47848cb93 | ||
|
|
873fca763d | ||
|
|
e99407132f | ||
|
|
67d63faeca | ||
|
|
871b053561 | ||
|
|
81deb1fff8 | ||
|
|
59560ebad1 | ||
|
|
67a6600c05 | ||
|
|
f3e30558e4 | ||
|
|
e46ab6ad7d | ||
|
|
b6fd48f282 | ||
|
|
c2edc26d8e | ||
|
|
76417b13d4 | ||
|
|
91a61643bd | ||
|
|
b6e05cb0b9 | ||
|
|
1507b91463 | ||
|
|
b06af3718c | ||
|
|
a20a896582 | ||
|
|
e1a3421212 | ||
|
|
19e51a2b12 | ||
|
|
b89a5c5ce9 | ||
|
|
65d3277319 | ||
|
|
a22bfa10f9 | ||
|
|
d9d1319a3a | ||
|
|
15ecbf4345 | ||
|
|
5e3bbcd427 | ||
|
|
a6c79db07b | ||
|
|
6e33bc6c17 | ||
|
|
01208bb359 | ||
|
|
fa88aaae52 | ||
|
|
2da400a267 | ||
|
|
8103135dfb | ||
|
|
cfabff7288 | ||
|
|
2f5a27a708 | ||
|
|
fdfe8bcc4b | ||
|
|
a19fd8db74 | ||
|
|
e63d71423d | ||
|
|
a7afe35fab | ||
|
|
56d6339313 | ||
|
|
2475572573 |
@@ -6,18 +6,28 @@ on: [push, pull_request]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: namespace-profile-linux-default
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: namespacelabs/nscloud-checkout-action@v9
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Gradle
|
- name: Set up cache
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: namespacelabs/nscloud-cache-action@v1
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
cache: gradle
|
||||||
distribution: 'temurin'
|
- name: Setup gradle build cache
|
||||||
|
run: |
|
||||||
|
nsc cache gradle setup --init-gradle /tmp/init.gradle
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
with:
|
||||||
|
cache-disabled: true # namespace handles this
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v6
|
||||||
|
with:
|
||||||
|
java-version: 21
|
||||||
|
distribution: 'zulu'
|
||||||
|
cache: "" # namespace handles this
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build
|
run: ./gradlew --init-script=/tmp/init.gradle build
|
||||||
|
|||||||
@@ -34,3 +34,11 @@ and you can configure it from there.
|
|||||||
|
|
||||||
Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity)
|
Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity)
|
||||||
page.
|
page.
|
||||||
|
|
||||||
|
# Localisation
|
||||||
|
|
||||||
|
Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity).
|
||||||
|
If you want to translate a language not available on Crowdin,
|
||||||
|
you might want to ask in the [Discord](https://discord.gg/papermc) about it.
|
||||||
|
|
||||||
|
[](https://namespace.so/github-actions/?utm_source=oss&utm_campaign=papermc)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ java {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnlyApi(libs.jspecify)
|
api(libs.jspecify)
|
||||||
|
|
||||||
api(libs.gson)
|
api(libs.gson)
|
||||||
api(libs.guava)
|
api(libs.guava)
|
||||||
@@ -59,18 +59,16 @@ tasks {
|
|||||||
|
|
||||||
val o = options as StandardJavadocDocletOptions
|
val o = options as StandardJavadocDocletOptions
|
||||||
o.encoding = "UTF-8"
|
o.encoding = "UTF-8"
|
||||||
o.source = "17"
|
o.source = "25"
|
||||||
|
|
||||||
o.use()
|
o.use()
|
||||||
o.links(
|
o.links(
|
||||||
"https://www.slf4j.org/apidocs/",
|
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
|
||||||
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
|
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
|
||||||
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
||||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
"https://docs.oracle.com/en/java/javase/25/docs/api/",
|
||||||
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
|
"https://jd.papermc.io/adventure/${libs.adventure.bom.get().version}/",
|
||||||
"https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
|
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
|
||||||
"https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
|
|
||||||
"https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
o.tags(
|
o.tags(
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import com.velocitypowered.api.plugin.Plugin;
|
|||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.processing.AbstractProcessor;
|
import javax.annotation.processing.AbstractProcessor;
|
||||||
import javax.annotation.processing.ProcessingEnvironment;
|
import javax.annotation.processing.ProcessingEnvironment;
|
||||||
@@ -68,8 +67,8 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
|
|||||||
|
|
||||||
Name qualifiedName = ((TypeElement) element).getQualifiedName();
|
Name qualifiedName = ((TypeElement) element).getQualifiedName();
|
||||||
|
|
||||||
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
|
if (pluginClassFound != null) {
|
||||||
if (!warnedAboutMultiplePlugins) {
|
if (!pluginClassFound.equals(qualifiedName.toString()) && !warnedAboutMultiplePlugins) {
|
||||||
environment.getMessager()
|
environment.getMessager()
|
||||||
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
|
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
|
||||||
+ "multiple plugins. We are using " + pluginClassFound
|
+ "multiple plugins. We are using " + pluginClassFound
|
||||||
@@ -98,6 +97,16 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (String provided : plugin.provides()) {
|
||||||
|
if (!SerializedPluginDescription.ID_PATTERN.matcher(provided).matches()) {
|
||||||
|
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
|
||||||
|
"Invalid provided ID '" + provided + "' for plugin " + qualifiedName
|
||||||
|
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
|
||||||
|
+ "can contain dashes or underscores.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// All good, generate the velocity-plugin.json.
|
// All good, generate the velocity-plugin.json.
|
||||||
SerializedPluginDescription description = SerializedPluginDescription
|
SerializedPluginDescription description = SerializedPluginDescription
|
||||||
.from(plugin, qualifiedName.toString());
|
.from(plugin, qualifiedName.toString());
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
*/
|
*/
|
||||||
public final class SerializedPluginDescription {
|
public final class SerializedPluginDescription {
|
||||||
|
|
||||||
public static final Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");
|
public static final String ID_PATTERN_STRING = "[a-z][a-z0-9-_]{0,63}";
|
||||||
|
public static final Pattern ID_PATTERN = Pattern.compile(ID_PATTERN_STRING);
|
||||||
|
|
||||||
// @Nullable is used here to make GSON skip these in the serialized file
|
// @Nullable is used here to make GSON skip these in the serialized file
|
||||||
private final String id;
|
private final String id;
|
||||||
@@ -34,11 +35,12 @@ public final class SerializedPluginDescription {
|
|||||||
private final @Nullable String url;
|
private final @Nullable String url;
|
||||||
private final @Nullable List<String> authors;
|
private final @Nullable List<String> authors;
|
||||||
private final @Nullable List<Dependency> dependencies;
|
private final @Nullable List<Dependency> dependencies;
|
||||||
|
private final @Nullable List<String> provides;
|
||||||
private final String main;
|
private final String main;
|
||||||
|
|
||||||
private SerializedPluginDescription(String id, String name, String version, String description,
|
private SerializedPluginDescription(String id, String name, String version, String description,
|
||||||
String url,
|
String url,
|
||||||
List<String> authors, List<Dependency> dependencies, String main) {
|
List<String> authors, List<Dependency> dependencies, List<String> provides, String main) {
|
||||||
Preconditions.checkNotNull(id, "id");
|
Preconditions.checkNotNull(id, "id");
|
||||||
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
|
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -49,6 +51,7 @@ public final class SerializedPluginDescription {
|
|||||||
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
|
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
|
||||||
this.dependencies =
|
this.dependencies =
|
||||||
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
|
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
|
||||||
|
this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides;
|
||||||
this.main = Preconditions.checkNotNull(main, "main");
|
this.main = Preconditions.checkNotNull(main, "main");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +63,9 @@ public final class SerializedPluginDescription {
|
|||||||
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
|
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
|
||||||
plugin.description(), plugin.url(),
|
plugin.description(), plugin.url(),
|
||||||
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
|
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
|
||||||
.collect(Collectors.toList()), dependencies, qualifiedName);
|
.collect(Collectors.toList()), dependencies,
|
||||||
|
Arrays.stream(plugin.provides()).filter(provided -> !provided.isEmpty())
|
||||||
|
.collect(Collectors.toList()), qualifiedName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
@@ -91,6 +96,10 @@ public final class SerializedPluginDescription {
|
|||||||
return dependencies == null ? ImmutableList.of() : dependencies;
|
return dependencies == null ? ImmutableList.of() : dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getProvides() {
|
||||||
|
return provides == null ? ImmutableList.of() : provides;
|
||||||
|
}
|
||||||
|
|
||||||
public String getMain() {
|
public String getMain() {
|
||||||
return main;
|
return main;
|
||||||
}
|
}
|
||||||
@@ -111,12 +120,13 @@ public final class SerializedPluginDescription {
|
|||||||
&& Objects.equals(url, that.url)
|
&& Objects.equals(url, that.url)
|
||||||
&& Objects.equals(authors, that.authors)
|
&& Objects.equals(authors, that.authors)
|
||||||
&& Objects.equals(dependencies, that.dependencies)
|
&& Objects.equals(dependencies, that.dependencies)
|
||||||
|
&& Objects.equals(provides, that.provides)
|
||||||
&& Objects.equals(main, that.main);
|
&& Objects.equals(main, that.main);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(id, name, version, description, url, authors, dependencies);
|
return Objects.hash(id, name, version, description, url, authors, dependencies, provides);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -129,6 +139,7 @@ public final class SerializedPluginDescription {
|
|||||||
+ ", url='" + url + '\''
|
+ ", url='" + url + '\''
|
||||||
+ ", authors=" + authors
|
+ ", authors=" + authors
|
||||||
+ ", dependencies=" + dependencies
|
+ ", dependencies=" + dependencies
|
||||||
|
+ ", provides=" + provides
|
||||||
+ ", main='" + main + '\''
|
+ ", main='" + main + '\''
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public interface CommandSource extends Audience, PermissionSubject {
|
|||||||
* Sends a message with the MiniMessage format to this source.
|
* Sends a message with the MiniMessage format to this source.
|
||||||
*
|
*
|
||||||
* @param message MiniMessage content
|
* @param message MiniMessage content
|
||||||
* @see <a href="https://docs.advntr.dev/minimessage/format.html">MiniMessage docs</a>
|
* @see <a href="https://docs.papermc.io/adventure/minimessage/format/">MiniMessage docs</a>
|
||||||
* for more information on the format.
|
* for more information on the format.
|
||||||
**/
|
**/
|
||||||
default void sendRichMessage(final @NotNull String message) {
|
default void sendRichMessage(final @NotNull String message) {
|
||||||
@@ -35,10 +35,10 @@ public interface CommandSource extends Audience, PermissionSubject {
|
|||||||
*
|
*
|
||||||
* @param message MiniMessage content
|
* @param message MiniMessage content
|
||||||
* @param resolvers resolvers to use
|
* @param resolvers resolvers to use
|
||||||
* @see <a href="https://docs.advntr.dev/minimessage/">MiniMessage docs</a>
|
* @see <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a>
|
||||||
* and <a href="https://docs.advntr.dev/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
|
* and <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
|
||||||
* for more information on the format.
|
* for more information on the format.
|
||||||
**/
|
*/
|
||||||
default void sendRichMessage(
|
default void sendRichMessage(
|
||||||
final @NotNull String message,
|
final @NotNull String message,
|
||||||
final @NotNull TagResolver @NotNull... resolvers
|
final @NotNull TagResolver @NotNull... resolvers
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ public interface EventManager {
|
|||||||
*
|
*
|
||||||
* @param plugin the plugin to associate with the handler
|
* @param plugin the plugin to associate with the handler
|
||||||
* @param eventClass the class for the event handler to register
|
* @param eventClass the class for the event handler to register
|
||||||
* @param postOrder the relative order in which events should be posted to the handler
|
* @param postOrder the relative order in which events should be posted to the handler. The higher
|
||||||
|
* the priority, the earlier the event handler will be called
|
||||||
* @param handler the handler to register
|
* @param handler the handler to register
|
||||||
* @param <E> the event type to handle
|
* @param <E> the event type to handle
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides events for handling command execution.
|
||||||
|
*/
|
||||||
|
package com.velocitypowered.api.event.command;
|
||||||
@@ -11,6 +11,7 @@ import com.google.common.base.Preconditions;
|
|||||||
import com.velocitypowered.api.event.ResultedEvent;
|
import com.velocitypowered.api.event.ResultedEvent;
|
||||||
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This event is fired once the player has been authenticated, but before they connect to a server.
|
* This event is fired once the player has been authenticated, but before they connect to a server.
|
||||||
@@ -22,10 +23,24 @@ import com.velocitypowered.api.proxy.Player;
|
|||||||
public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
|
public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
|
||||||
|
|
||||||
private final Player player;
|
private final Player player;
|
||||||
|
private final String serverIdHash;
|
||||||
private ComponentResult result;
|
private ComponentResult result;
|
||||||
|
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
public LoginEvent(Player player) {
|
public LoginEvent(Player player) {
|
||||||
|
this(player, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link LoginEvent}.
|
||||||
|
*
|
||||||
|
* @param player the player who has completed authentication
|
||||||
|
* @param serverIdHash the server ID hash sent to Mojang for authentication,
|
||||||
|
* or {@code null} if the connection is in offline-mode
|
||||||
|
*/
|
||||||
|
public LoginEvent(Player player, @Nullable String serverIdHash) {
|
||||||
this.player = Preconditions.checkNotNull(player, "player");
|
this.player = Preconditions.checkNotNull(player, "player");
|
||||||
|
this.serverIdHash = serverIdHash;
|
||||||
this.result = ComponentResult.allowed();
|
this.result = ComponentResult.allowed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +48,16 @@ public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentRe
|
|||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the server ID hash that was sent to Mojang to authenticate the player.
|
||||||
|
* If the connection was in offline-mode, this returns {@code null}.
|
||||||
|
*
|
||||||
|
* @return the server ID hash that was sent to Mojang to authenticate the player
|
||||||
|
*/
|
||||||
|
public @Nullable String getServerIdHash() {
|
||||||
|
return serverIdHash;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ComponentResult getResult() {
|
public ComponentResult getResult() {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public final class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLogin
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the UUID of the connecting player.
|
* Returns the UUID of the connecting player.
|
||||||
|
*
|
||||||
* <p>This value is {@code null} on 1.19.2 and lower,
|
* <p>This value is {@code null} on 1.19.2 and lower,
|
||||||
* up to 1.20.1 it is optional and from 1.20.2 it will always be available.</p>
|
* up to 1.20.1 it is optional and from 1.20.2 it will always be available.</p>
|
||||||
*
|
*
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
* unregister channel. Velocity will not wait on this event to finish firing.
|
||||||
|
*/
|
||||||
|
public final class PlayerChannelUnregisterEvent {
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final List<ChannelIdentifier> channels;
|
||||||
|
|
||||||
|
public PlayerChannelUnregisterEvent(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 "PlayerChannelUnregisterEvent{"
|
||||||
|
+ "player=" + player
|
||||||
|
+ ", channels=" + channels
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018-2026 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a player is marked as loaded by the client.
|
||||||
|
*
|
||||||
|
* <p>This event is fired once per {@link com.velocitypowered.api.proxy.ServerConnection}
|
||||||
|
* when the player explicitly notifies the server after loading the world (closing the downloading terrain screen)
|
||||||
|
*
|
||||||
|
* @implNote Unlike Paper this event will <u>not</u> fire due to a timeout nor respawning.
|
||||||
|
* Though plugins can implement a timeout by scheduling a task in {@link ServerPostConnectEvent}
|
||||||
|
* and checking {@link com.velocitypowered.api.proxy.ServerConnection#isClientLoaded()}.
|
||||||
|
* @sinceMinecraft 1.21.4
|
||||||
|
* @since 4.1.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public final class PlayerClientLoadedWorldEvent {
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
|
||||||
|
public PlayerClientLoadedWorldEvent(Player player) {
|
||||||
|
this.player = Preconditions.checkNotNull(player, "player");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getPlayer() {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PlayerClientLoadedWorldEvent{"
|
||||||
|
+ "player=" + player
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,7 +143,7 @@ public final class ServerPreConnectEvent implements
|
|||||||
* is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status
|
* is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status
|
||||||
* {@link Status#CONNECTION_CANCELLED}.
|
* {@link Status#CONNECTION_CANCELLED}.
|
||||||
*
|
*
|
||||||
* @return a result to deny conneections
|
* @return a result to deny connections
|
||||||
*/
|
*/
|
||||||
public static ServerResult denied() {
|
public static ServerResult denied() {
|
||||||
return DENIED;
|
return DENIED;
|
||||||
|
|||||||
+1
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This event is executed when a player entered the configuration state and can be configured by Velocity.
|
* This event is executed when a player entered the configuration state and can be configured by Velocity.
|
||||||
|
*
|
||||||
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
|
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
|
||||||
*
|
*
|
||||||
* @param player The player who can be configured.
|
* @param player The player who can be configured.
|
||||||
|
|||||||
+1
@@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
/**
|
/**
|
||||||
* This event is executed when a player is about to enter the configuration state.
|
* This event is executed when a player is about to enter the configuration state.
|
||||||
* It is <b>not</b> called for the initial configuration of a player after login.
|
* It is <b>not</b> called for the initial configuration of a player after login.
|
||||||
|
*
|
||||||
* <p>Velocity will wait for this event before asking the client to enter configuration state.
|
* <p>Velocity will wait for this event before asking the client to enter configuration state.
|
||||||
* However due to backend server being unable to keep the connection alive during state changes,
|
* However due to backend server being unable to keep the connection alive during state changes,
|
||||||
* Velocity will only wait for a maximum of 5 seconds.</p>
|
* Velocity will only wait for a maximum of 5 seconds.</p>
|
||||||
|
|||||||
+1
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This event is executed when a player has entered the configuration state.
|
* This event is executed when a player has entered the configuration state.
|
||||||
|
*
|
||||||
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
|
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
|
||||||
* the {@linkplain Player#getProtocolState()} method is guaranteed
|
* the {@linkplain Player#getProtocolState()} method is guaranteed
|
||||||
* to return {@link ProtocolState#CONFIGURATION}.</p>
|
* to return {@link ProtocolState#CONFIGURATION}.</p>
|
||||||
|
|||||||
+1
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This event is executed when a player is about to finish the configuration state.
|
* This event is executed when a player is about to finish the configuration state.
|
||||||
|
*
|
||||||
* <p>Velocity will wait for this event before asking the client to finish the configuration state.
|
* <p>Velocity will wait for this event before asking the client to finish the configuration state.
|
||||||
* However due to backend server being unable to keep the connection alive during state changes,
|
* However due to backend server being unable to keep the connection alive during state changes,
|
||||||
* Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
|
* Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
|
||||||
|
|||||||
+1
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This event is executed when a player has finished the configuration state.
|
* This event is executed when a player has finished the configuration state.
|
||||||
|
*
|
||||||
* <p>From this moment on, the {@link Player#getProtocolState()} method
|
* <p>From this moment on, the {@link Player#getProtocolState()} method
|
||||||
* will return {@link ProtocolState#PLAY}.</p>
|
* will return {@link ProtocolState#PLAY}.</p>
|
||||||
*
|
*
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides events for handling the player configuration phase.
|
||||||
|
*/
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018-2025 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.proxy;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is fired by the proxy after it has stopped accepting new connections,
|
||||||
|
* but before players are disconnected.
|
||||||
|
* This is the last point at which you can interact with currently connected players,
|
||||||
|
* for example to transfer them to another proxy or perform other cleanup tasks.
|
||||||
|
*
|
||||||
|
* @implNote Velocity will wait for all event listeners to complete before disconnecting players,
|
||||||
|
* but note that the event will time out after the configured value of the
|
||||||
|
* <code>velocity.pre-shutdown-timeout</code> system property, default 10 seconds,
|
||||||
|
* in seconds to prevent shutdown from hanging indefinitely
|
||||||
|
* @since 3.4.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
@AwaitingEvent
|
||||||
|
public final class ProxyPreShutdownEvent {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ProxyPreShutdownEvent";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides events for handling registration of servers on the proxy.
|
||||||
|
*/
|
||||||
|
package com.velocitypowered.api.event.proxy.server;
|
||||||
@@ -16,6 +16,7 @@ package com.velocitypowered.api.network;
|
|||||||
public enum ProtocolState {
|
public enum ProtocolState {
|
||||||
/**
|
/**
|
||||||
* Initial connection State.
|
* Initial connection State.
|
||||||
|
*
|
||||||
* <p>This status can be caused by a {@link HandshakeIntent#STATUS},
|
* <p>This status can be caused by a {@link HandshakeIntent#STATUS},
|
||||||
* {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p>
|
* {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p>
|
||||||
* If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN},
|
* If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN},
|
||||||
@@ -24,6 +25,7 @@ public enum ProtocolState {
|
|||||||
HANDSHAKE,
|
HANDSHAKE,
|
||||||
/**
|
/**
|
||||||
* Ping State of a connection.
|
* Ping State of a connection.
|
||||||
|
*
|
||||||
* <p>Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state
|
* <p>Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state
|
||||||
* and be disconnected after it requests the ping from the server
|
* and be disconnected after it requests the ping from the server
|
||||||
* and the server responds with the respective ping.</p>
|
* and the server responds with the respective ping.</p>
|
||||||
@@ -31,11 +33,13 @@ public enum ProtocolState {
|
|||||||
STATUS,
|
STATUS,
|
||||||
/**
|
/**
|
||||||
* Authentication State of a connection.
|
* Authentication State of a connection.
|
||||||
|
*
|
||||||
* <p>At this moment the player is authenticating with the authentication servers.</p>
|
* <p>At this moment the player is authenticating with the authentication servers.</p>
|
||||||
*/
|
*/
|
||||||
LOGIN,
|
LOGIN,
|
||||||
/**
|
/**
|
||||||
* Configuration State of a connection.
|
* Configuration State of a connection.
|
||||||
|
*
|
||||||
* <p>At this point the player allows the server to send information
|
* <p>At this point the player allows the server to send information
|
||||||
* such as resource packs and plugin messages, at the same time the player
|
* such as resource packs and plugin messages, at the same time the player
|
||||||
* will send his client brand and the respective plugin messages
|
* will send his client brand and the respective plugin messages
|
||||||
@@ -46,6 +50,7 @@ public enum ProtocolState {
|
|||||||
CONFIGURATION,
|
CONFIGURATION,
|
||||||
/**
|
/**
|
||||||
* Game State of a connection.
|
* Game State of a connection.
|
||||||
|
*
|
||||||
* <p>In this state is where the whole game runs, the server is able to change
|
* <p>In this state is where the whole game runs, the server is able to change
|
||||||
* the player's state to {@link #CONFIGURATION} as needed in versions 1.20.2 and higher.</p>
|
* the player's state to {@link #CONFIGURATION} as needed in versions 1.20.2 and higher.</p>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -91,7 +91,13 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
|
|||||||
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
|
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
|
||||||
MINECRAFT_1_21_4(769, "1.21.4"),
|
MINECRAFT_1_21_4(769, "1.21.4"),
|
||||||
MINECRAFT_1_21_5(770, "1.21.5"),
|
MINECRAFT_1_21_5(770, "1.21.5"),
|
||||||
MINECRAFT_1_21_6(771, "1.21.6");
|
MINECRAFT_1_21_6(771, "1.21.6"),
|
||||||
|
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"),
|
||||||
|
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
|
||||||
|
MINECRAFT_1_21_11(774, "1.21.11"),
|
||||||
|
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"),
|
||||||
|
MINECRAFT_26_2(776, "26.2"),
|
||||||
|
MINECRAFT_26_3(777, "26.3");
|
||||||
|
|
||||||
private static final int SNAPSHOT_BIT = 30;
|
private static final int SNAPSHOT_BIT = 30;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
* Represents three different states of a setting.
|
* Represents three different states of a setting.
|
||||||
*
|
*
|
||||||
* <p>Possible values:</p>
|
* <p>Possible values:</p>
|
||||||
* <p></p>
|
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link #TRUE} - a positive setting</li>
|
* <li>{@link #TRUE} - a positive setting</li>
|
||||||
* <li>{@link #FALSE} - a negative (negated) setting</li>
|
* <li>{@link #FALSE} - a negative (negated) setting</li>
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
package com.velocitypowered.api.plugin;
|
package com.velocitypowered.api.plugin;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
import org.intellij.lang.annotations.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the {@link Plugin} depends on another plugin in order to enable.
|
* Indicates that the {@link Plugin} depends on another plugin in order to enable.
|
||||||
@@ -24,6 +26,7 @@ public @interface Dependency {
|
|||||||
* @return The dependency plugin ID
|
* @return The dependency plugin ID
|
||||||
* @see Plugin#id()
|
* @see Plugin#id()
|
||||||
*/
|
*/
|
||||||
|
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
|
||||||
String id();
|
String id();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
|
|
||||||
package com.velocitypowered.api.plugin;
|
package com.velocitypowered.api.plugin;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
import org.intellij.lang.annotations.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotation used to describe a Velocity plugin.
|
* Annotation used to describe a Velocity plugin.
|
||||||
@@ -26,6 +28,7 @@ public @interface Plugin {
|
|||||||
*
|
*
|
||||||
* @return the ID for this plugin
|
* @return the ID for this plugin
|
||||||
*/
|
*/
|
||||||
|
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
|
||||||
String id();
|
String id();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,4 +72,12 @@ public @interface Plugin {
|
|||||||
* @return the plugin dependencies
|
* @return the plugin dependencies
|
||||||
*/
|
*/
|
||||||
Dependency[] dependencies() default {};
|
Dependency[] dependencies() default {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plugin IDs this plugin "provides" for. Each ID must match
|
||||||
|
* {@link SerializedPluginDescription#ID_PATTERN_STRING}.
|
||||||
|
*
|
||||||
|
* @return the provided IDs
|
||||||
|
*/
|
||||||
|
String[] provides() default {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ public interface PluginDescription {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link Collection} of the provided IDs of the {@link Plugin} within this container.
|
||||||
|
*
|
||||||
|
* @return the provided plugins IDs, can be empty
|
||||||
|
* @see Plugin#provides()
|
||||||
|
*/
|
||||||
|
default Collection<String> getProvidedIds() {
|
||||||
|
return ImmutableSet.of();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the source the plugin was loaded from.
|
* Returns the source the plugin was loaded from.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import java.util.Locale;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
|
import net.kyori.adventure.dialog.DialogLike;
|
||||||
import net.kyori.adventure.identity.Identified;
|
import net.kyori.adventure.identity.Identified;
|
||||||
import net.kyori.adventure.inventory.Book;
|
import net.kyori.adventure.inventory.Book;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
@@ -38,6 +39,7 @@ import net.kyori.adventure.sound.SoundStop;
|
|||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.event.HoverEvent;
|
import net.kyori.adventure.text.event.HoverEvent;
|
||||||
import net.kyori.adventure.text.event.HoverEventSource;
|
import net.kyori.adventure.text.event.HoverEventSource;
|
||||||
|
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -48,7 +50,8 @@ public interface Player extends
|
|||||||
/* Fundamental Velocity interfaces */
|
/* Fundamental Velocity interfaces */
|
||||||
CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink,
|
CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink,
|
||||||
/* Adventure-specific interfaces */
|
/* Adventure-specific interfaces */
|
||||||
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable {
|
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable, Sound.Emitter,
|
||||||
|
PlayerHeadObjectContents.SkinSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the player's current username.
|
* Returns the player's current username.
|
||||||
@@ -194,7 +197,7 @@ public interface Player extends
|
|||||||
*
|
*
|
||||||
* @param reason component with the reason
|
* @param reason component with the reason
|
||||||
*/
|
*/
|
||||||
void disconnect(Component reason);
|
void disconnect(@NotNull Component reason);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends chat input onto the players current server as if they typed it into the client chat box.
|
* Sends chat input onto the players current server as if they typed it into the client chat box.
|
||||||
@@ -312,6 +315,7 @@ public interface Player extends
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
* <p><strong>Note that this method does not send a plugin message to the server the player
|
* <p><strong>Note that this method does not send a plugin message to the server the player
|
||||||
* is connected to.</strong> You should only use this method if you are trying to communicate
|
* is connected to.</strong> You should only use this method if you are trying to communicate
|
||||||
* with a mod that is installed on the player's client.</p>
|
* with a mod that is installed on the player's client.</p>
|
||||||
@@ -335,6 +339,15 @@ public interface Player extends
|
|||||||
Component.text(getUsername()))));
|
Component.text(getUsername()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage") // permitted implementation
|
||||||
|
@Override
|
||||||
|
default void applySkinToPlayerHeadContents(
|
||||||
|
final PlayerHeadObjectContents.@NotNull Builder builder) {
|
||||||
|
builder.skin(this.getGameProfile());
|
||||||
|
if (this.hasSentPlayerSettings()) {
|
||||||
|
builder.hat(this.getPlayerSettings().getSkinParts().hasHat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the player's client brand.
|
* Gets the player's client brand.
|
||||||
@@ -383,8 +396,12 @@ public interface Player extends
|
|||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* <b>This method is not currently implemented in Velocity
|
*
|
||||||
|
* @apiNote <b>This method is not currently implemented in Velocity
|
||||||
* and will not perform any actions.</b>
|
* and will not perform any actions.</b>
|
||||||
|
* @see #playSound(Sound, Sound.Emitter)
|
||||||
|
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
|
||||||
|
* Unsupported Adventure Operations</a>
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default void playSound(@NotNull Sound sound) {
|
default void playSound(@NotNull Sound sound) {
|
||||||
@@ -393,8 +410,11 @@ public interface Player extends
|
|||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* <b>This method is not currently implemented in Velocity
|
* @apiNote <b>This method is not currently implemented in Velocity
|
||||||
* and will not perform any actions.</b>
|
* and will not perform any actions.</b>
|
||||||
|
* @see #playSound(Sound, Sound.Emitter)
|
||||||
|
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
|
||||||
|
* Unsupported Adventure Operations</a>
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default void playSound(@NotNull Sound sound, double x, double y, double z) {
|
default void playSound(@NotNull Sound sound, double x, double y, double z) {
|
||||||
@@ -403,18 +423,28 @@ public interface Player extends
|
|||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* <b>This method is not currently implemented in Velocity
|
* <p><b>Note</b>: Due to <a href="https://bugs.mojang.com/browse/MC/issues/MC-146721">MC-146721</a>, stereo sounds are always played globally in 1.14+.
|
||||||
* and will not perform any actions.</b>
|
*
|
||||||
|
* <p><b>Note</b>: Due to <a href="https://bugs.mojang.com/browse/MC/issues/MC-138832">MC-138832</a>, the volume and pitch are ignored when using this method in 1.14 to 1.16.5.
|
||||||
|
*
|
||||||
|
* @param sound the sound to play
|
||||||
|
* @param emitter the emitter of the sound; may be another player of this player's server
|
||||||
|
* @since 3.4.0
|
||||||
|
* @sinceMinecraft 1.19.3
|
||||||
|
* @apiNote This method is currently only implemented for players on 1.19.3+
|
||||||
|
* and requires a present {@link #getCurrentServer} for the emitting player as well as this player.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default void playSound(@NotNull Sound sound, Sound.Emitter emitter) {
|
default void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* <b>This method is not currently implemented in Velocity
|
* @param stop the sound and/or a sound source, to stop
|
||||||
* and will not perform any actions.</b>
|
* @since 3.4.0
|
||||||
|
* @sinceMinecraft 1.19.3
|
||||||
|
* @apiNote This method is currently only implemented for players on 1.19.3+.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default void stopSound(@NotNull SoundStop stop) {
|
default void stopSound(@NotNull SoundStop stop) {
|
||||||
@@ -425,11 +455,40 @@ public interface Player extends
|
|||||||
*
|
*
|
||||||
* <b>This method is not currently implemented in Velocity
|
* <b>This method is not currently implemented in Velocity
|
||||||
* and will not perform any actions.</b>
|
* and will not perform any actions.</b>
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
|
||||||
|
* Unsupported Adventure Operations</a>
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default void openBook(@NotNull Book book) {
|
default void openBook(@NotNull Book book) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <b>This method is not currently implemented in Velocity
|
||||||
|
* and will not perform any actions.</b>
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
|
||||||
|
* Unsupported Adventure Operations</a>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
default void showDialog(@NotNull DialogLike dialog) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <b>This method is not currently implemented in Velocity
|
||||||
|
* and will not perform any actions.</b>
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
|
||||||
|
* Unsupported Adventure Operations</a>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
default void closeDialog() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transfers a Player to a host.
|
* Transfers a Player to a host.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package com.velocitypowered.api.proxy;
|
package com.velocitypowered.api.proxy;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
@@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
|
|||||||
*/
|
*/
|
||||||
ServerInfo getServerInfo();
|
ServerInfo getServerInfo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the client notified this connection of having loaded the world.
|
||||||
|
*
|
||||||
|
* @return true if the client has loaded the world
|
||||||
|
* @implNote This is purely client-dependent; see {@link com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent}.
|
||||||
|
* @sinceMinecraft 1.21.4
|
||||||
|
* @since 4.1.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
boolean isClientLoaded();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the player that this connection is associated with.
|
* Returns the player that this connection is associated with.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public interface KeySigned {
|
|||||||
* Validates the signature, expiry temporal and key against the
|
* Validates the signature, expiry temporal and key against the
|
||||||
* signer public key. Note: This will **not** check for
|
* signer public key. Note: This will **not** check for
|
||||||
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
|
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
|
||||||
|
*
|
||||||
* <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p>
|
* <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p>
|
||||||
* Addendum: Does not work for 1.19.1 until the user has authenticated.
|
* Addendum: Does not work for 1.19.1 until the user has authenticated.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -237,8 +237,11 @@ public interface TabListEntry extends KeyIdentifiable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link IdentifiedKey} of the {@link TabListEntry}.
|
* Sets the {@link IdentifiedKey} of the {@link TabListEntry}.
|
||||||
|
*
|
||||||
* <p>This only works for players currently <b>not</b> connected to this proxy.</p>
|
* <p>This only works for players currently <b>not</b> connected to this proxy.</p>
|
||||||
|
*
|
||||||
* <p>For any player currently connected to this proxy this will be filled automatically.</p>
|
* <p>For any player currently connected to this proxy this will be filled automatically.</p>
|
||||||
|
*
|
||||||
* <p>Will ignore mismatching key revisions data.</p>
|
* <p>Will ignore mismatching key revisions data.</p>
|
||||||
*
|
*
|
||||||
* @param chatSession session to set
|
* @param chatSession session to set
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a 1.7 and above server list ping response. This class is immutable.
|
* Represents a 1.7 and above server list ping response. This class is immutable.
|
||||||
@@ -28,9 +30,10 @@ public final class ServerPing {
|
|||||||
|
|
||||||
private final Version version;
|
private final Version version;
|
||||||
private final @Nullable Players players;
|
private final @Nullable Players players;
|
||||||
private final net.kyori.adventure.text.Component description;
|
private final @Nullable Component description;
|
||||||
private final @Nullable Favicon favicon;
|
private final @Nullable Favicon favicon;
|
||||||
private final @Nullable ModInfo modinfo;
|
private final @Nullable ModInfo modinfo;
|
||||||
|
private final boolean preventsChatReports = true;
|
||||||
|
|
||||||
public ServerPing(Version version, @Nullable Players players,
|
public ServerPing(Version version, @Nullable Players players,
|
||||||
net.kyori.adventure.text.Component description, @Nullable Favicon favicon) {
|
net.kyori.adventure.text.Component description, @Nullable Favicon favicon) {
|
||||||
@@ -47,7 +50,7 @@ public final class ServerPing {
|
|||||||
* @param modinfo the mods this server runs
|
* @param modinfo the mods this server runs
|
||||||
*/
|
*/
|
||||||
public ServerPing(Version version, @Nullable Players players,
|
public ServerPing(Version version, @Nullable Players players,
|
||||||
net.kyori.adventure.text.Component description, @Nullable Favicon favicon,
|
Component description, @Nullable Favicon favicon,
|
||||||
@Nullable ModInfo modinfo) {
|
@Nullable ModInfo modinfo) {
|
||||||
this.version = Preconditions.checkNotNull(version, "version");
|
this.version = Preconditions.checkNotNull(version, "version");
|
||||||
this.players = players;
|
this.players = players;
|
||||||
@@ -64,7 +67,8 @@ public final class ServerPing {
|
|||||||
return Optional.ofNullable(players);
|
return Optional.ofNullable(players);
|
||||||
}
|
}
|
||||||
|
|
||||||
public net.kyori.adventure.text.Component getDescriptionComponent() {
|
@Nullable
|
||||||
|
public Component getDescriptionComponent() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ public final class ServerPing {
|
|||||||
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
|
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
|
||||||
private String modType = "FML";
|
private String modType = "FML";
|
||||||
private final List<ModInfo.Mod> mods = new ArrayList<>();
|
private final List<ModInfo.Mod> mods = new ArrayList<>();
|
||||||
private net.kyori.adventure.text.Component description;
|
private Component description;
|
||||||
private @Nullable Favicon favicon;
|
private @Nullable Favicon favicon;
|
||||||
private boolean nullOutPlayers;
|
private boolean nullOutPlayers;
|
||||||
private boolean nullOutModinfo;
|
private boolean nullOutModinfo;
|
||||||
@@ -299,7 +303,7 @@ public final class ServerPing {
|
|||||||
* @param description Component to use as the description.
|
* @param description Component to use as the description.
|
||||||
* @return this builder, for chaining
|
* @return this builder, for chaining
|
||||||
*/
|
*/
|
||||||
public Builder description(net.kyori.adventure.text.Component description) {
|
public Builder description(Component description) {
|
||||||
this.description = Preconditions.checkNotNull(description, "description");
|
this.description = Preconditions.checkNotNull(description, "description");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -359,7 +363,7 @@ public final class ServerPing {
|
|||||||
return samplePlayers;
|
return samplePlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<net.kyori.adventure.text.Component> getDescriptionComponent() {
|
public Optional<Component> getDescriptionComponent() {
|
||||||
return Optional.ofNullable(description);
|
return Optional.ofNullable(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ import com.google.common.base.Preconditions;
|
|||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Mojang game profile. This class is immutable.
|
* Represents a Mojang game profile. This class is immutable.
|
||||||
*/
|
*/
|
||||||
public final class GameProfile {
|
public final class GameProfile implements PlayerHeadObjectContents.SkinSource {
|
||||||
|
|
||||||
private final UUID id;
|
private final UUID id;
|
||||||
private final String undashedId;
|
private final String undashedId;
|
||||||
@@ -169,6 +172,23 @@ public final class GameProfile {
|
|||||||
ImmutableList.of());
|
ImmutableList.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage") // permitted implementation
|
||||||
|
@Override
|
||||||
|
public void applySkinToPlayerHeadContents(
|
||||||
|
final PlayerHeadObjectContents.@NotNull Builder builder) {
|
||||||
|
if (this.properties.isEmpty()) {
|
||||||
|
builder.id(this.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.id(this.id)
|
||||||
|
.name(this.name)
|
||||||
|
.profileProperties(this.properties.stream()
|
||||||
|
.map(property -> PlayerHeadObjectContents.property(property.getName(),
|
||||||
|
property.getValue(), property.getSignature()))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "GameProfile{"
|
return "GameProfile{"
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar
|
|||||||
import org.gradle.kotlin.dsl.withType
|
import org.gradle.kotlin.dsl.withType
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
// This interface is needed as a workaround to get an instance of ExecOperations
|
||||||
|
interface Injected {
|
||||||
|
@get:Inject
|
||||||
|
val execOps: ExecOperations
|
||||||
|
}
|
||||||
|
|
||||||
val currentShortRevision = ByteArrayOutputStream().use {
|
val currentShortRevision = ByteArrayOutputStream().use {
|
||||||
exec {
|
val execOps = objects.newInstance<Injected>().execOps
|
||||||
|
execOps.exec {
|
||||||
executable = "git"
|
executable = "git"
|
||||||
args = listOf("rev-parse", "HEAD")
|
args = listOf("rev-parse", "HEAD")
|
||||||
standardOutput = it
|
standardOutput = it
|
||||||
@@ -25,5 +32,6 @@ tasks.withType<Jar> {
|
|||||||
archiveVersion.get()
|
archiveVersion.get()
|
||||||
}
|
}
|
||||||
attributes["Implementation-Version"] = velocityHumanVersion
|
attributes["Implementation-Version"] = velocityHumanVersion
|
||||||
|
attributes["Enable-Native-Access"] = "ALL-UNNAMED"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ extensions.configure<PublishingExtension> {
|
|||||||
maven {
|
maven {
|
||||||
credentials(PasswordCredentials::class.java)
|
credentials(PasswordCredentials::class.java)
|
||||||
|
|
||||||
name = "paper"
|
name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined
|
||||||
val base = "https://repo.papermc.io/repository/maven"
|
val base = "https://artifactory.papermc.io/artifactory"
|
||||||
val releasesRepoUrl = "$base-releases/"
|
val releasesRepoUrl = "$base/releases/"
|
||||||
val snapshotsRepoUrl = "$base-snapshots/"
|
val snapshotsRepoUrl = "$base/snapshots/"
|
||||||
setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
|
setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -12,7 +12,7 @@ subprojects {
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(17))
|
languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@ subprojects {
|
|||||||
testImplementation(rootProject.libs.junit)
|
testImplementation(rootProject.libs.junit)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
testing.suites.named<JvmTestSuite>("test") {
|
||||||
test {
|
useJUnitJupiter()
|
||||||
useJUnitPlatform()
|
targets.all {
|
||||||
reports {
|
testTask.configure {
|
||||||
junitXml.required.set(true)
|
reports.junitXml.required = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
group=com.velocitypowered
|
group=com.velocitypowered
|
||||||
version=3.4.0-SNAPSHOT
|
version=4.2.1-SNAPSHOT
|
||||||
|
|||||||
+28
-29
@@ -1,27 +1,26 @@
|
|||||||
[versions]
|
[versions]
|
||||||
configurate3 = "3.7.3"
|
configurate3 = "3.7.3"
|
||||||
configurate4 = "4.1.2"
|
configurate4 = "4.2.0"
|
||||||
flare = "2.0.1"
|
flare = "2.0.1"
|
||||||
log4j = "2.24.3"
|
log4j = "2.26.1"
|
||||||
netty = "4.2.1.Final"
|
netty = "4.2.18.Final"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
indra-publishing = "net.kyori.indra.publishing:2.0.6"
|
fill = "io.papermc.fill.gradle:1.0.12"
|
||||||
shadow = "io.github.goooler.shadow:8.1.5"
|
shadow = "com.gradleup.shadow:9.6.1"
|
||||||
spotless = "com.diffplug.spotless:6.25.0"
|
spotless = "com.diffplug.spotless:8.10.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
adventure-bom = "net.kyori:adventure-bom:4.21.0"
|
adventure-bom = "net.kyori:adventure-bom:5.2.0"
|
||||||
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.21.0"
|
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0"
|
||||||
adventure-facet = "net.kyori:adventure-platform-facet:4.3.4"
|
asm = "org.ow2.asm:asm:9.10.1"
|
||||||
asm = "org.ow2.asm:asm:9.8"
|
auto-service = "com.google.auto.service:auto-service:1.1.1"
|
||||||
auto-service = "com.google.auto.service:auto-service:1.0.1"
|
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
|
||||||
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
|
|
||||||
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
|
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
|
||||||
bstats = "org.bstats:bstats-base:3.0.3"
|
bstats = "org.bstats:bstats-base:3.2.1"
|
||||||
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
|
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
|
||||||
checker-qual = "org.checkerframework:checker-qual:3.42.0"
|
checker-qual = "org.checkerframework:checker-qual:4.2.3"
|
||||||
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
|
checkstyle = "com.puppycrawl.tools:checkstyle:14.1.0"
|
||||||
completablefutures = "com.spotify:completable-futures:0.3.6"
|
completablefutures = "com.spotify:completable-futures:0.3.6"
|
||||||
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
|
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
|
||||||
configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" }
|
configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" }
|
||||||
@@ -30,24 +29,24 @@ configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.r
|
|||||||
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
|
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
|
||||||
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
|
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
|
||||||
disruptor = "com.lmax:disruptor:4.0.0"
|
disruptor = "com.lmax:disruptor:4.0.0"
|
||||||
fastutil = "it.unimi.dsi:fastutil:8.5.15"
|
fastutil = "it.unimi.dsi:fastutil:8.5.19"
|
||||||
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
|
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
|
||||||
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
|
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
|
||||||
jline = "org.jline:jline-terminal-jansi:3.30.2"
|
jline = "org.jline:jline-terminal-jni:4.3.1"
|
||||||
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
|
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
|
||||||
junit = "org.junit.jupiter:junit-jupiter:5.10.2"
|
junit = "org.junit.jupiter:junit-jupiter:6.1.3"
|
||||||
jspecify = "org.jspecify:jspecify:0.3.0"
|
jspecify = "org.jspecify:jspecify:1.0.1"
|
||||||
kyori-ansi = "net.kyori:ansi:1.1.1"
|
kyori-ansi = "net.kyori:ansi:1.1.1"
|
||||||
guava = "com.google.guava:guava:25.1-jre"
|
guava = "com.google.guava:guava:33.7.1-jre"
|
||||||
gson = "com.google.code.gson:gson:2.10.1"
|
gson = "com.google.code.gson:gson:2.14.0"
|
||||||
guice = "com.google.inject:guice:6.0.0"
|
guice = "com.google.inject:guice:7.0.0"
|
||||||
lmbda = "org.lanternpowered:lmbda:2.0.0"
|
lmbda = "org.lanternpowered:lmbda:3.0.0"
|
||||||
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
|
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
|
||||||
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
|
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
|
||||||
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
|
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
|
||||||
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
|
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
|
||||||
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
|
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
|
||||||
mockito = "org.mockito:mockito-core:5.10.0"
|
mockito = "org.mockito:mockito-core:5.23.0"
|
||||||
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
|
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
|
||||||
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
|
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
|
||||||
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
||||||
@@ -55,10 +54,10 @@ netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
|
|||||||
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
|
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
|
||||||
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
|
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
|
||||||
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
|
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
|
||||||
nightconfig = "com.electronwill.night-config:toml:3.6.7"
|
nightconfig = "com.electronwill.night-config:toml:3.9.0"
|
||||||
slf4j = "org.slf4j:slf4j-api:2.0.17"
|
slf4j = "org.slf4j:slf4j-api:2.0.19"
|
||||||
snakeyaml = "org.yaml:snakeyaml:1.33"
|
snakeyaml = "org.yaml:snakeyaml:2.7"
|
||||||
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
|
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.4"
|
||||||
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
|
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -115,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -173,7 +171,6 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -203,18 +200,17 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
Vendored
+2
-3
@@ -36,7 +36,7 @@ set APP_HOME=%DIRNAME%
|
|||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
@@ -70,11 +70,10 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -5,29 +5,44 @@ set -e
|
|||||||
# make sure we're in the correct directory - the top-level `native` directory
|
# make sure we're in the correct directory - the top-level `native` directory
|
||||||
cd "$(dirname "$0")/.." || exit 1
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
# Use docker by default, falling back to podman. Set CONTAINER_ENGINE to pick one explicitly.
|
||||||
|
if [ -z "$CONTAINER_ENGINE" ]; then
|
||||||
|
if command -v docker > /dev/null 2>&1; then
|
||||||
|
CONTAINER_ENGINE=docker
|
||||||
|
elif command -v podman > /dev/null 2>&1; then
|
||||||
|
CONTAINER_ENGINE=podman
|
||||||
|
else
|
||||||
|
echo "Neither docker nor podman was found on PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using container engine: $CONTAINER_ENGINE"
|
||||||
|
|
||||||
ARCHS=(x86_64 aarch64)
|
ARCHS=(x86_64 aarch64)
|
||||||
BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine)
|
BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine)
|
||||||
COMPRESSION_VARIANTS=(ubuntu-focal alpine)
|
COMPRESSION_VARIANTS=(ubuntu-focal alpine)
|
||||||
|
|
||||||
|
# Build one image per (variant, arch). Passing several --platform flags to a single tagged build
|
||||||
|
# does not produce a multi-arch tag - only the last architecture keeps the tag, and the run below
|
||||||
|
# then sees a platform mismatch, treats the image as missing and tries to pull it from a registry.
|
||||||
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
|
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
|
||||||
docker_platforms=""
|
|
||||||
for arch in "${ARCHS[@]}"; do
|
for arch in "${ARCHS[@]}"; do
|
||||||
docker_platforms="$docker_platforms --platform linux/${arch}"
|
echo "Building base build image for $variant on $arch..."
|
||||||
|
$CONTAINER_ENGINE build -t velocity-native-build:$variant-$arch --platform linux/${arch} \
|
||||||
|
-f build-support/$variant.Dockerfile .
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Building base build image for $variant..."
|
|
||||||
docker build -t velocity-native-build:$variant $docker_platforms -f build-support/$variant.Dockerfile .
|
|
||||||
done
|
done
|
||||||
|
|
||||||
for arch in "${ARCHS[@]}"; do
|
for arch in "${ARCHS[@]}"; do
|
||||||
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
|
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
|
||||||
echo "Building native crypto for $arch on $variant..."
|
echo "Building native crypto for $arch on $variant..."
|
||||||
|
|
||||||
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh"
|
$CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh"
|
||||||
done
|
done
|
||||||
|
|
||||||
for variant in "${COMPRESSION_VARIANTS[@]}"; do
|
for variant in "${COMPRESSION_VARIANTS[@]}"; do
|
||||||
echo "Building native compression for $arch on $variant..."
|
echo "Building native compression for $arch on $variant..."
|
||||||
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh"
|
$CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -34,8 +34,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e
|
|||||||
jlong sourceAddress,
|
jlong sourceAddress,
|
||||||
jint sourceLength,
|
jint sourceLength,
|
||||||
jlong destinationAddress,
|
jlong destinationAddress,
|
||||||
jint destinationLength,
|
jint destinationLength)
|
||||||
jlong maximumSize)
|
|
||||||
{
|
{
|
||||||
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
|
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
|
||||||
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
|
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
|
||||||
|
|||||||
+26
-5
@@ -56,24 +56,45 @@ public class JavaVelocityCompressor implements VelocityCompressor {
|
|||||||
final int origIdx = source.readerIndex();
|
final int origIdx = source.readerIndex();
|
||||||
inflater.setInput(source.nioBuffer());
|
inflater.setInput(source.nioBuffer());
|
||||||
|
|
||||||
|
int totalProduced = 0;
|
||||||
try {
|
try {
|
||||||
final int readable = source.readableBytes();
|
final int readable = source.readableBytes();
|
||||||
while (!inflater.finished() && inflater.getBytesRead() < readable) {
|
while (!inflater.finished() && inflater.getBytesRead() < readable) {
|
||||||
|
if (totalProduced >= uncompressedSize) {
|
||||||
|
throw new DataFormatException("Decompressed data exceeds the claimed uncompressed size "
|
||||||
|
+ "of " + uncompressedSize + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int remaining = uncompressedSize - totalProduced;
|
||||||
if (!destination.isWritable()) {
|
if (!destination.isWritable()) {
|
||||||
destination.ensureWritable(ZLIB_BUFFER_SIZE);
|
destination.ensureWritable(Math.min(ZLIB_BUFFER_SIZE, remaining));
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(),
|
ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(),
|
||||||
destination.writableBytes());
|
destination.writableBytes());
|
||||||
|
|
||||||
|
// Never let a single inflate step write past the claimed size
|
||||||
|
if (destNioBuf.remaining() > remaining) {
|
||||||
|
destNioBuf.limit(destNioBuf.position() + remaining);
|
||||||
|
}
|
||||||
|
|
||||||
int produced = inflater.inflate(destNioBuf);
|
int produced = inflater.inflate(destNioBuf);
|
||||||
|
if (produced == 0 && !inflater.finished()) {
|
||||||
|
// Output space was available yet the inflater made no progress: the stream is truncated
|
||||||
|
// or corrupt (this also covers a peer that over-reported the uncompressed size).
|
||||||
|
throw new DataFormatException("Received a truncated or malformed deflate stream, "
|
||||||
|
+ "expected " + uncompressedSize + " bytes");
|
||||||
|
}
|
||||||
|
totalProduced += produced;
|
||||||
destination.writerIndex(destination.writerIndex() + produced);
|
destination.writerIndex(destination.writerIndex() + produced);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inflater.finished()) {
|
if (!inflater.finished()) {
|
||||||
throw new DataFormatException("Received a deflate stream that was too large, wanted "
|
throw new DataFormatException("Received a truncated or malformed deflate stream, expected "
|
||||||
+ uncompressedSize);
|
+ uncompressedSize + " bytes");
|
||||||
}
|
}
|
||||||
source.readerIndex(origIdx + inflater.getTotalIn());
|
|
||||||
|
source.readerIndex(origIdx + (int) inflater.getBytesRead());
|
||||||
} finally {
|
} finally {
|
||||||
inflater.reset();
|
inflater.reset();
|
||||||
}
|
}
|
||||||
@@ -102,7 +123,7 @@ public class JavaVelocityCompressor implements VelocityCompressor {
|
|||||||
destination.writerIndex(destination.writerIndex() + produced);
|
destination.writerIndex(destination.writerIndex() + produced);
|
||||||
}
|
}
|
||||||
|
|
||||||
source.readerIndex(origIdx + deflater.getTotalIn());
|
source.readerIndex(origIdx + (int) deflater.getBytesRead());
|
||||||
deflater.reset();
|
deflater.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+78
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package com.velocitypowered.natives.compression;
|
package com.velocitypowered.natives.compression;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
@@ -77,6 +78,83 @@ class VelocityCompressorTest {
|
|||||||
check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32));
|
check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int BOMB_ACTUAL_SIZE = 1 << 20;
|
||||||
|
private static final int BOMB_LYING_CLAIM = 1024;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void javaRejectsUnderReportedUncompressedSize() throws DataFormatException {
|
||||||
|
VelocityCompressor compressor = JavaVelocityCompressor.FACTORY
|
||||||
|
.create(Deflater.DEFAULT_COMPRESSION);
|
||||||
|
DataFormatException ex = assertRejectsDecompressionBomb(compressor);
|
||||||
|
// The Java compressor's size guard names the claimed size in its message, so operators can
|
||||||
|
// tell an over-size rejection apart from a genuinely corrupt stream.
|
||||||
|
assertTrue(ex.getMessage().contains(String.valueOf(BOMB_LYING_CLAIM)),
|
||||||
|
"rejection must originate from the uncompressed-size guard, got: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@EnabledOnOs({LINUX})
|
||||||
|
void nativeRejectsUnderReportedUncompressedSize() throws DataFormatException {
|
||||||
|
VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION);
|
||||||
|
if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) {
|
||||||
|
compressor.close();
|
||||||
|
fail("Loaded regular compressor");
|
||||||
|
}
|
||||||
|
// libdeflate rejects with its own native-origin message ("uncompressed size is inaccurate"),
|
||||||
|
// so we only assert the behavioural guarantee here, not the message text.
|
||||||
|
assertRejectsDecompressionBomb(compressor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that a compressor refuses a decompression bomb: a small, valid deflate stream whose
|
||||||
|
* claimed uncompressed size is far smaller than what it actually inflates to. Verifies the same
|
||||||
|
* stream round-trips when the claimed size is honest (proving the rejection is caused by the
|
||||||
|
* under-reported size, not corrupt input) and that no output is written past the claimed size.
|
||||||
|
* Closes the compressor before returning the exception thrown by the rejected inflate.
|
||||||
|
*/
|
||||||
|
private DataFormatException assertRejectsDecompressionBomb(VelocityCompressor compressor)
|
||||||
|
throws DataFormatException {
|
||||||
|
// Direct buffers so this works for the native compressor, which requires them.
|
||||||
|
ByteBuf source = Unpooled.directBuffer(BOMB_ACTUAL_SIZE);
|
||||||
|
ByteBuf compressed = Unpooled.directBuffer();
|
||||||
|
try {
|
||||||
|
source.writeZero(BOMB_ACTUAL_SIZE);
|
||||||
|
compressor.deflate(source, compressed);
|
||||||
|
final int compressedSize = compressed.readableBytes();
|
||||||
|
assertTrue(compressedSize < BOMB_ACTUAL_SIZE / 100,
|
||||||
|
"sanity: payload really is a decompression bomb (" + compressedSize + " -> "
|
||||||
|
+ BOMB_ACTUAL_SIZE + ")");
|
||||||
|
|
||||||
|
// Positive control: the compressed stream is perfectly valid and round-trips when the peer
|
||||||
|
// tells the truth about its uncompressed size.
|
||||||
|
ByteBuf honest = Unpooled.directBuffer();
|
||||||
|
try {
|
||||||
|
compressor.inflate(compressed.duplicate(), honest, BOMB_ACTUAL_SIZE);
|
||||||
|
assertEquals(BOMB_ACTUAL_SIZE, honest.readableBytes(),
|
||||||
|
"valid stream must fully decompress when the claimed size is honest");
|
||||||
|
} finally {
|
||||||
|
honest.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack: same valid stream, but a tiny claimed size. inflate must refuse rather than grow
|
||||||
|
// the destination without bound.
|
||||||
|
ByteBuf decompressed = Unpooled.directBuffer();
|
||||||
|
try {
|
||||||
|
DataFormatException ex = assertThrows(DataFormatException.class,
|
||||||
|
() -> compressor.inflate(compressed.duplicate(), decompressed, BOMB_LYING_CLAIM));
|
||||||
|
assertTrue(decompressed.writerIndex() <= BOMB_LYING_CLAIM,
|
||||||
|
"inflate must not write past the claimed uncompressed size");
|
||||||
|
return ex;
|
||||||
|
} finally {
|
||||||
|
decompressed.release();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
source.release();
|
||||||
|
compressed.release();
|
||||||
|
compressor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier)
|
private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier)
|
||||||
throws DataFormatException {
|
throws DataFormatException {
|
||||||
ByteBuf source = bufSupplier.get();
|
ByteBuf source = bufSupplier.get();
|
||||||
|
|||||||
+39
-62
@@ -1,14 +1,16 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
|
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
|
||||||
|
import io.papermc.fill.model.BuildChannel
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
application
|
application
|
||||||
id("velocity-init-manifest")
|
id("velocity-init-manifest")
|
||||||
alias(libs.plugins.shadow)
|
alias(libs.plugins.shadow)
|
||||||
|
alias(libs.plugins.fill)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("com.velocitypowered.proxy.Velocity")
|
mainClass.set("com.velocitypowered.proxy.Velocity")
|
||||||
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true");
|
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -25,67 +27,12 @@ tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
|
filesMatching("META-INF/org/apache/logging/log4j/core/config/plugins/**") {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
transform(Log4j2PluginsCacheFileTransformer::class.java)
|
transform(Log4j2PluginsCacheFileTransformer::class.java)
|
||||||
|
|
||||||
// Exclude all the collection types we don"t intend to use
|
|
||||||
exclude("it/unimi/dsi/fastutil/booleans/**")
|
|
||||||
exclude("it/unimi/dsi/fastutil/bytes/**")
|
|
||||||
exclude("it/unimi/dsi/fastutil/chars/**")
|
|
||||||
exclude("it/unimi/dsi/fastutil/doubles/**")
|
|
||||||
exclude("it/unimi/dsi/fastutil/floats/**")
|
|
||||||
exclude("it/unimi/dsi/fastutil/longs/**")
|
|
||||||
exclude("it/unimi/dsi/fastutil/shorts/**")
|
|
||||||
|
|
||||||
// Exclude the fastutil IO utilities - we don"t use them.
|
|
||||||
exclude("it/unimi/dsi/fastutil/io/**")
|
|
||||||
|
|
||||||
// Exclude most of the int types - Object2IntMap have a values() method that returns an
|
|
||||||
// IntCollection, and we need Int2ObjectMap
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Boolean*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Byte*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Char*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Double*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Float*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Int*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Long*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntList*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntHeap*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntOpen*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntRB*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/IntSorted*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*Priority*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/ints/*BigList*")
|
|
||||||
|
|
||||||
// Try to exclude everything BUT Object2Int{LinkedOpen,Open,CustomOpen}HashMap
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*ObjectArray*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*ObjectAVL*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object*Big*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Boolean*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Byte*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Char*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Double*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Float*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2IntArray*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2IntAVL*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2IntRB*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Long*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Object*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Reference*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Object2Short*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*ObjectRB*")
|
|
||||||
exclude("it/unimi/dsi/fastutil/objects/*Reference*")
|
|
||||||
|
|
||||||
// Exclude Checker Framework annotations
|
// Exclude Checker Framework annotations
|
||||||
exclude("org/checkerframework/checker/**")
|
exclude("org/checkerframework/checker/**")
|
||||||
|
|
||||||
@@ -106,12 +53,42 @@ tasks {
|
|||||||
workingDir = file("run").also(File::mkdirs)
|
workingDir = file("run").also(File::mkdirs)
|
||||||
standardInput = System.`in` // Doesn't work?
|
standardInput = System.`in` // Doesn't work?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withType<JavaCompile>().configureEach {
|
||||||
|
options.compilerArgs.addAll(
|
||||||
|
listOf(
|
||||||
|
"-Alog4j.graalvm.groupId=${project.group}",
|
||||||
|
"-Alog4j.graalvm.artifactId=${project.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val projectVersion = version as String
|
||||||
|
fill {
|
||||||
|
project("velocity")
|
||||||
|
|
||||||
|
build {
|
||||||
|
channel = BuildChannel.STABLE
|
||||||
|
versionFamily("4.0.0")
|
||||||
|
version(projectVersion)
|
||||||
|
|
||||||
|
if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) {
|
||||||
|
throw IllegalArgumentException("Version family does not match project version")
|
||||||
|
}
|
||||||
|
|
||||||
|
downloads {
|
||||||
|
register("server:default") {
|
||||||
|
file = tasks.shadowJar.flatMap { it.archiveFile }
|
||||||
|
nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":velocity-api"))
|
implementation(project(":velocity-api"))
|
||||||
implementation(project(":velocity-native"))
|
implementation(project(":velocity-native"))
|
||||||
implementation(project(":velocity-proxy-log4j2-plugin"))
|
|
||||||
|
|
||||||
implementation(libs.bundles.log4j)
|
implementation(libs.bundles.log4j)
|
||||||
implementation(libs.kyori.ansi)
|
implementation(libs.kyori.ansi)
|
||||||
@@ -136,7 +113,6 @@ dependencies {
|
|||||||
implementation(libs.fastutil)
|
implementation(libs.fastutil)
|
||||||
implementation(platform(libs.adventure.bom))
|
implementation(platform(libs.adventure.bom))
|
||||||
implementation(libs.adventure.text.serializer.json.legacy.impl)
|
implementation(libs.adventure.text.serializer.json.legacy.impl)
|
||||||
implementation(libs.adventure.facet)
|
|
||||||
implementation(libs.completablefutures)
|
implementation(libs.completablefutures)
|
||||||
implementation(libs.nightconfig)
|
implementation(libs.nightconfig)
|
||||||
implementation(libs.bstats)
|
implementation(libs.bstats)
|
||||||
@@ -148,4 +124,5 @@ dependencies {
|
|||||||
testImplementation(libs.mockito)
|
testImplementation(libs.mockito)
|
||||||
|
|
||||||
annotationProcessor(libs.auto.service)
|
annotationProcessor(libs.auto.service)
|
||||||
|
annotationProcessor(libs.log4j.core)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
implementation(libs.bundles.log4j)
|
|
||||||
annotationProcessor(libs.log4j.core)
|
|
||||||
}
|
|
||||||
@@ -21,10 +21,8 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.stream.Collectors;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bstats.MetricsBase;
|
import org.bstats.MetricsBase;
|
||||||
@@ -120,38 +118,28 @@ public class Metrics {
|
|||||||
() -> server.getVersion().getVersion()));
|
() -> server.getVersion().getVersion()));
|
||||||
|
|
||||||
metrics.addCustomChart(new DrilldownPie("java_version", () -> {
|
metrics.addCustomChart(new DrilldownPie("java_version", () -> {
|
||||||
Map<String, Map<String, Integer>> map = new HashMap<>();
|
Runtime.Version version = Runtime.version();
|
||||||
String javaVersion = System.getProperty("java.version");
|
|
||||||
Map<String, Integer> entry = new HashMap<>();
|
|
||||||
entry.put(javaVersion, 1);
|
|
||||||
|
|
||||||
// http://openjdk.java.net/jeps/223
|
return Map.of(
|
||||||
// Java decided to change their versioning scheme and in doing so modified the
|
"Java " + version.feature(),
|
||||||
// java.version system property to return $major[.$minor][.$security][-ea], as opposed to
|
Map.of(javaVersion(version), 1));
|
||||||
// 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1",
|
|
||||||
// otherwise, 9+
|
|
||||||
String majorVersion = javaVersion.split("\\.")[0];
|
|
||||||
String release;
|
|
||||||
|
|
||||||
int indexOf = javaVersion.lastIndexOf('.');
|
|
||||||
|
|
||||||
if (majorVersion.equals("1")) {
|
|
||||||
release = "Java " + javaVersion.substring(0, indexOf);
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
map.put(release, entry);
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recreates the exact {@code java.version} system property value from a {@link Runtime.Version}.
|
||||||
|
*
|
||||||
|
* <p>Per <a href="https://openjdk.org/jeps/223">JEP 223</a>, {@code java.version} is
|
||||||
|
* {@code $VNUM(-$PRE)?}; the build and optional segments only appear in {@code java.runtime.version}.
|
||||||
|
*
|
||||||
|
* @param v the runtime version
|
||||||
|
* @return the value {@code java.version} would hold on this JVM
|
||||||
|
*/
|
||||||
|
private static String javaVersion(Runtime.Version v) {
|
||||||
|
return v.version().stream()
|
||||||
|
.map(Object::toString)
|
||||||
|
.collect(Collectors.joining("."))
|
||||||
|
+ v.pre().map(p -> "-" + p).orElse("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -47,11 +47,6 @@ public class Velocity {
|
|||||||
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir"));
|
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator
|
|
||||||
if (System.getProperty("io.netty.allocator.type") == null) {
|
|
||||||
System.setProperty("io.netty.allocator.type", "pooled");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable the resource leak detector by default as it reduces performance. Allow the user to
|
// Disable the resource leak detector by default as it reduces performance. Allow the user to
|
||||||
// override this if desired.
|
// override this if desired.
|
||||||
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {
|
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.google.gson.Gson;
|
|||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.velocitypowered.api.command.BrigadierCommand;
|
import com.velocitypowered.api.command.BrigadierCommand;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||||
|
import com.velocitypowered.api.event.proxy.ProxyPreShutdownEvent;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
|
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
@@ -81,7 +82,6 @@ import java.net.http.HttpClient;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.text.MessageFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -104,8 +104,8 @@ import net.kyori.adventure.audience.Audience;
|
|||||||
import net.kyori.adventure.audience.ForwardingAudience;
|
import net.kyori.adventure.audience.ForwardingAudience;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.translation.MiniMessageTranslationStore;
|
||||||
import net.kyori.adventure.translation.GlobalTranslator;
|
import net.kyori.adventure.translation.GlobalTranslator;
|
||||||
import net.kyori.adventure.translation.TranslationStore;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bstats.MetricsBase;
|
import org.bstats.MetricsBase;
|
||||||
@@ -119,7 +119,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
*/
|
*/
|
||||||
public class VelocityServer implements ProxyServer, ForwardingAudience {
|
public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||||
|
|
||||||
public static final String VELOCITY_URL = "https://velocitypowered.com";
|
public static final String VELOCITY_URL = "https://papermc.io/software/velocity";
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
|
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
|
||||||
public static final Gson GENERAL_GSON = new GsonBuilder()
|
public static final Gson GENERAL_GSON = new GsonBuilder()
|
||||||
@@ -150,6 +150,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
)
|
)
|
||||||
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
|
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
|
||||||
.create();
|
.create();
|
||||||
|
private static final int PRE_SHUTDOWN_TIMEOUT =
|
||||||
|
Integer.getInteger("velocity.pre-shutdown-timeout", 10);
|
||||||
|
|
||||||
private final ConnectionManager cm;
|
private final ConnectionManager cm;
|
||||||
private final ProxyOptions options;
|
private final ProxyOptions options;
|
||||||
@@ -163,6 +165,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
|
|
||||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||||
|
private final Object sessionIdLock = new Object();
|
||||||
|
private volatile @Nullable UUID sessionId;
|
||||||
private final VelocityConsole console;
|
private final VelocityConsole console;
|
||||||
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
|
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
|
||||||
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
|
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
|
||||||
@@ -216,7 +220,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
ProxyVersion version = getVersion();
|
ProxyVersion version = getVersion();
|
||||||
PluginDescription description = new VelocityPluginDescription(
|
PluginDescription description = new VelocityPluginDescription(
|
||||||
"velocity", version.getName(), version.getVersion(), "The Velocity proxy",
|
"velocity", version.getName(), version.getVersion(), "The Velocity proxy",
|
||||||
VELOCITY_URL, ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
|
version.getName().equals("Velocity") ? VELOCITY_URL : null,
|
||||||
|
ImmutableList.of(version.getVendor()), Collections.emptyList(),
|
||||||
|
Collections.emptyList(), null);
|
||||||
VelocityPluginContainer container = new VelocityPluginContainer(description);
|
VelocityPluginContainer container = new VelocityPluginContainer(description);
|
||||||
container.setInstance(VelocityVirtualPlugin.INSTANCE);
|
container.setInstance(VelocityVirtualPlugin.INSTANCE);
|
||||||
return container;
|
return container;
|
||||||
@@ -238,8 +244,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
console.setupStreams();
|
console.setupStreams();
|
||||||
pluginManager.registerPlugin(this.createVirtualPlugin());
|
pluginManager.registerPlugin(this.createVirtualPlugin());
|
||||||
|
|
||||||
registerTranslations();
|
|
||||||
|
|
||||||
// Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds
|
// Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds
|
||||||
// dangerous, right? We're well within the realm of factoring such a key...
|
// dangerous, right? We're well within the realm of factoring such a key...
|
||||||
//
|
//
|
||||||
@@ -288,6 +292,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
|
|
||||||
this.doStartupConfigLoad();
|
this.doStartupConfigLoad();
|
||||||
|
|
||||||
|
registerTranslations();
|
||||||
|
|
||||||
for (ServerInfo cliServer : options.getServers()) {
|
for (ServerInfo cliServer : options.getServers()) {
|
||||||
servers.register(cliServer);
|
servers.register(cliServer);
|
||||||
}
|
}
|
||||||
@@ -338,8 +344,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void registerTranslations() {
|
private void registerTranslations() {
|
||||||
final TranslationStore.StringBased<MessageFormat> translationRegistry =
|
final MiniMessageTranslationStore translationRegistry =
|
||||||
TranslationStore.messageFormat(Key.key("velocity", "translations"));
|
MiniMessageTranslationStore.create(Key.key("velocity", "translations"));
|
||||||
translationRegistry.defaultLocale(Locale.US);
|
translationRegistry.defaultLocale(Locale.US);
|
||||||
try {
|
try {
|
||||||
ResourceUtils.visitResources(VelocityServer.class, path -> {
|
ResourceUtils.visitResources(VelocityServer.class, path -> {
|
||||||
@@ -482,23 +488,46 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-register servers. If a server is being replaced, make sure to note what players need to
|
// Re-register servers. If a server is being replaced or removed, make sure to note what
|
||||||
// move back to a fallback server.
|
// players need to move back to a fallback server.
|
||||||
Collection<ConnectedPlayer> evacuate = new ArrayList<>();
|
Collection<ConnectedPlayer> evacuate = new ArrayList<>();
|
||||||
for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) {
|
for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
|
||||||
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
|
if (newConfiguration.getServers().keySet().stream()
|
||||||
Optional<RegisteredServer> rs = servers.getServer(entry.getKey());
|
.anyMatch(name -> name.equalsIgnoreCase(entry.getKey()))) {
|
||||||
if (rs.isEmpty()) {
|
continue;
|
||||||
servers.register(newInfo);
|
}
|
||||||
} else if (!rs.get().getServerInfo().equals(newInfo)) {
|
|
||||||
for (Player player : rs.get().getPlayersConnected()) {
|
RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
|
||||||
|
if (rs == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Player player : rs.getPlayersConnected()) {
|
||||||
if (!(player instanceof ConnectedPlayer)) {
|
if (!(player instanceof ConnectedPlayer)) {
|
||||||
throw new IllegalStateException("ConnectedPlayer not found for player " + player
|
throw new IllegalStateException("Expected ConnectedPlayer for player " + player
|
||||||
+ " in server " + rs.get().getServerInfo().getName());
|
+ " on server " + rs.getServerInfo().getName() + ", got "
|
||||||
|
+ player.getClass().getName());
|
||||||
}
|
}
|
||||||
evacuate.add((ConnectedPlayer) player);
|
evacuate.add((ConnectedPlayer) player);
|
||||||
}
|
}
|
||||||
servers.unregister(rs.get().getServerInfo());
|
servers.unregister(rs.getServerInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) {
|
||||||
|
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
|
||||||
|
RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
|
||||||
|
if (rs == null) {
|
||||||
|
servers.register(newInfo);
|
||||||
|
} else if (!rs.getServerInfo().equals(newInfo)) {
|
||||||
|
for (Player player : rs.getPlayersConnected()) {
|
||||||
|
if (!(player instanceof ConnectedPlayer)) {
|
||||||
|
throw new IllegalStateException("Expected ConnectedPlayer for player " + player
|
||||||
|
+ " on server " + rs.getServerInfo().getName() + ", got "
|
||||||
|
+ player.getClass().getName());
|
||||||
|
}
|
||||||
|
evacuate.add((ConnectedPlayer) player);
|
||||||
|
}
|
||||||
|
servers.unregister(rs.getServerInfo());
|
||||||
servers.register(newInfo);
|
servers.register(newInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -578,6 +607,20 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
// done first to refuse new connections
|
// done first to refuse new connections
|
||||||
cm.shutdown();
|
cm.shutdown();
|
||||||
|
|
||||||
|
try {
|
||||||
|
eventManager.fire(new ProxyPreShutdownEvent())
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get(PRE_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
|
||||||
|
} catch (TimeoutException ignored) {
|
||||||
|
logger.warn("Your plugins took over {} seconds during pre shutdown.",
|
||||||
|
PRE_SHUTDOWN_TIMEOUT);
|
||||||
|
} catch (ExecutionException ee) {
|
||||||
|
logger.error("Exception in ProxyPreShutdownEvent handler; continuing shutdown.", ee);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.warn("Interrupted while waiting for ProxyPreShutdownEvent; continuing shutdown.");
|
||||||
|
}
|
||||||
|
|
||||||
ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
|
ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
|
||||||
for (ConnectedPlayer player : players) {
|
for (ConnectedPlayer player : players) {
|
||||||
player.disconnect(reason);
|
player.disconnect(reason);
|
||||||
@@ -633,7 +676,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls {@link #shutdown(boolean, Component)} with the default reason "Proxy shutting down."
|
* Calls {@link #shutdown(boolean, Component)} with the default reason "Proxy shutting down".
|
||||||
*
|
*
|
||||||
* @param explicitExit whether the user explicitly shut down the proxy
|
* @param explicitExit whether the user explicitly shut down the proxy
|
||||||
*/
|
*/
|
||||||
@@ -726,6 +769,36 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
||||||
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
||||||
connection.disconnected();
|
connection.disconnected();
|
||||||
|
|
||||||
|
if (this.sessionId != null && connectionsByUuid.isEmpty()) {
|
||||||
|
synchronized (this.sessionIdLock) {
|
||||||
|
if (connectionsByUuid.isEmpty()) {
|
||||||
|
this.sessionId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the metrics session ID for this proxy, generating one if none is currently active. The
|
||||||
|
* ID is shared by every player connected during a populated period and is regenerated once the
|
||||||
|
* proxy empties.
|
||||||
|
*
|
||||||
|
* @return the current session ID
|
||||||
|
*/
|
||||||
|
public UUID getSessionId() {
|
||||||
|
UUID uuid = this.sessionId;
|
||||||
|
if (uuid != null) {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
synchronized (this.sessionIdLock) {
|
||||||
|
uuid = this.sessionId;
|
||||||
|
if (uuid == null) {
|
||||||
|
uuid = UUID.randomUUID();
|
||||||
|
this.sessionId = uuid;
|
||||||
|
}
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+15
-7
@@ -53,15 +53,23 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
|||||||
viewer.getProtocolVersion(),
|
viewer.getProtocolVersion(),
|
||||||
viewer.translateMessage(this.bar.name())
|
viewer.translateMessage(this.bar.name())
|
||||||
);
|
);
|
||||||
viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
|
viewer.getBossBarManager().writeUpdate(this, BossBarPacket.createAddPacket(this.id, this.bar, name));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void createDirect(final ConnectedPlayer viewer) {
|
||||||
|
final ComponentHolder name = new ComponentHolder(
|
||||||
|
viewer.getProtocolVersion(),
|
||||||
|
viewer.translateMessage(this.bar.name())
|
||||||
|
);
|
||||||
|
viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean viewerRemove(final ConnectedPlayer viewer) {
|
public boolean viewerRemove(final ConnectedPlayer viewer) {
|
||||||
if (this.viewers.remove(viewer)) {
|
if (this.viewers.remove(viewer)) {
|
||||||
viewer.getConnection().write(BossBarPacket.createRemovePacket(this.id, this.bar));
|
viewer.getBossBarManager().remove(this, BossBarPacket.createRemovePacket(this.id, this.bar));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -84,7 +92,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
|||||||
this.bar,
|
this.bar,
|
||||||
new ComponentHolder(viewer.getProtocolVersion(), translated)
|
new ComponentHolder(viewer.getProtocolVersion(), translated)
|
||||||
);
|
);
|
||||||
viewer.getConnection().write(packet);
|
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +104,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
|||||||
) {
|
) {
|
||||||
final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar);
|
final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar);
|
||||||
for (final ConnectedPlayer viewer : this.viewers) {
|
for (final ConnectedPlayer viewer : this.viewers) {
|
||||||
viewer.getConnection().write(packet);
|
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +116,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
|||||||
) {
|
) {
|
||||||
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
|
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
|
||||||
for (final ConnectedPlayer viewer : this.viewers) {
|
for (final ConnectedPlayer viewer : this.viewers) {
|
||||||
viewer.getConnection().write(packet);
|
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +128,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
|||||||
) {
|
) {
|
||||||
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
|
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
|
||||||
for (final ConnectedPlayer viewer : this.viewers) {
|
for (final ConnectedPlayer viewer : this.viewers) {
|
||||||
viewer.getConnection().write(packet);
|
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +140,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
|||||||
) {
|
) {
|
||||||
final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar);
|
final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar);
|
||||||
for (final ConnectedPlayer viewer : this.viewers) {
|
for (final ConnectedPlayer viewer : this.viewers) {
|
||||||
viewer.getConnection().write(packet);
|
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ final class SuggestionsProvider<S> {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return potentials.get(0);
|
return potentials.getFirst();
|
||||||
}
|
}
|
||||||
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
|
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import com.velocitypowered.api.command.CommandManager;
|
|||||||
import com.velocitypowered.api.command.CommandMeta;
|
import com.velocitypowered.api.command.CommandMeta;
|
||||||
import com.velocitypowered.api.command.CommandResult;
|
import com.velocitypowered.api.command.CommandResult;
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
import com.velocitypowered.api.command.VelocityBrigadierMessage;
|
|
||||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||||
import com.velocitypowered.api.event.command.PostCommandInvocationEvent;
|
import com.velocitypowered.api.event.command.PostCommandInvocationEvent;
|
||||||
import com.velocitypowered.api.plugin.PluginManager;
|
import com.velocitypowered.api.plugin.PluginManager;
|
||||||
@@ -59,6 +58,7 @@ import java.util.concurrent.locks.ReadWriteLock;
|
|||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.ComponentLike;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import org.checkerframework.checker.lock.qual.GuardedBy;
|
import org.checkerframework.checker.lock.qual.GuardedBy;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
@@ -140,7 +140,7 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
command + " implements multiple registrable Command subinterfaces: "
|
command + " implements multiple registrable Command subinterfaces: "
|
||||||
+ implementedInterfaces);
|
+ implementedInterfaces);
|
||||||
} else {
|
} else {
|
||||||
this.internalRegister(commandRegistrars.get(0), command, meta);
|
this.internalRegister(commandRegistrars.getFirst(), command, meta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,8 +242,8 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
|
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
|
||||||
if (isSyntaxError) {
|
if (isSyntaxError) {
|
||||||
final Message message = e.getRawMessage();
|
final Message message = e.getRawMessage();
|
||||||
if (message instanceof VelocityBrigadierMessage velocityMessage) {
|
if (message instanceof ComponentLike componentLike) {
|
||||||
source.sendMessage(velocityMessage.asComponent().applyFallbackStyle(NamedTextColor.RED));
|
source.sendMessage(componentLike.asComponent().applyFallbackStyle(NamedTextColor.RED));
|
||||||
} else {
|
} else {
|
||||||
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
|
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ public final class VelocityCommands {
|
|||||||
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
|
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delegate instanceof LiteralCommandNode<CommandSource> lcn) {
|
return switch (delegate) {
|
||||||
|
case LiteralCommandNode<CommandSource> lcn -> {
|
||||||
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
|
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
|
||||||
literalBuilder.executes(maybeCommand);
|
literalBuilder.executes(maybeCommand);
|
||||||
// we also need to wrap any children
|
// we also need to wrap any children
|
||||||
@@ -80,12 +81,12 @@ public final class VelocityCommands {
|
|||||||
if (delegate.getRedirect() != null) {
|
if (delegate.getRedirect() != null) {
|
||||||
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
||||||
}
|
}
|
||||||
return literalBuilder.build();
|
yield literalBuilder.build();
|
||||||
} else if (delegate instanceof VelocityArgumentCommandNode<CommandSource, ?> vacn) {
|
}
|
||||||
return vacn.withCommand(maybeCommand)
|
case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand)
|
||||||
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
|
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
|
||||||
} else if (delegate instanceof ArgumentCommandNode) {
|
case ArgumentCommandNode<CommandSource, ?> node -> {
|
||||||
var argBuilder = delegate.createBuilder().executes(maybeCommand);
|
var argBuilder = node.createBuilder().executes(maybeCommand);
|
||||||
// we also need to wrap any children
|
// we also need to wrap any children
|
||||||
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
|
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
|
||||||
argBuilder.then(wrap(child, registrant));
|
argBuilder.then(wrap(child, registrant));
|
||||||
@@ -93,10 +94,10 @@ public final class VelocityCommands {
|
|||||||
if (delegate.getRedirect() != null) {
|
if (delegate.getRedirect() != null) {
|
||||||
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
||||||
}
|
}
|
||||||
return argBuilder.build();
|
yield argBuilder.build();
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
|
|
||||||
}
|
}
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalization
|
// Normalization
|
||||||
@@ -133,7 +134,7 @@ public final class VelocityCommands {
|
|||||||
if (nodes.isEmpty()) {
|
if (nodes.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Cannot read alias from empty node list");
|
throw new IllegalArgumentException("Cannot read alias from empty node list");
|
||||||
}
|
}
|
||||||
return nodes.get(0).getNode().getName();
|
return nodes.getFirst().getNode().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String ARGS_NODE_NAME = "arguments";
|
public static final String ARGS_NODE_NAME = "arguments";
|
||||||
|
|||||||
+2
-4
@@ -118,14 +118,12 @@ public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, St
|
|||||||
if (this == o) {
|
if (this == o) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!(o instanceof VelocityArgumentCommandNode)) {
|
if (!(o instanceof VelocityArgumentCommandNode that)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!super.equals(o)) {
|
if (!super.equals(that)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final VelocityArgumentCommandNode<?, ?> that = (VelocityArgumentCommandNode<?, ?>) o;
|
|
||||||
return this.type.equals(that.type);
|
return this.type.equals(that.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import net.kyori.adventure.text.Component;
|
|||||||
import net.kyori.adventure.text.TextComponent;
|
import net.kyori.adventure.text.TextComponent;
|
||||||
import net.kyori.adventure.text.TranslatableComponent;
|
import net.kyori.adventure.text.TranslatableComponent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the Velocity default {@code /glist} command.
|
* Implements the Velocity default {@code /glist} command.
|
||||||
@@ -111,7 +112,7 @@ public class GlistCommand {
|
|||||||
if (registeredServer.isEmpty()) {
|
if (registeredServer.isEmpty()) {
|
||||||
source.sendMessage(
|
source.sendMessage(
|
||||||
CommandMessages.SERVER_DOES_NOT_EXIST
|
CommandMessages.SERVER_DOES_NOT_EXIST
|
||||||
.arguments(Component.text(serverName)));
|
.arguments(Argument.string("server", serverName)));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
sendServerPlayers(source, registeredServer.get(), false);
|
sendServerPlayers(source, registeredServer.get(), false);
|
||||||
@@ -126,7 +127,8 @@ public class GlistCommand {
|
|||||||
? "velocity.command.glist-player-singular"
|
? "velocity.command.glist-player-singular"
|
||||||
: "velocity.command.glist-player-plural"
|
: "velocity.command.glist-player-plural"
|
||||||
).color(NamedTextColor.YELLOW)
|
).color(NamedTextColor.YELLOW)
|
||||||
.arguments(Component.text(Integer.toString(online), NamedTextColor.GREEN));
|
.arguments(Argument.component(
|
||||||
|
"players", Component.text(Integer.toString(online), NamedTextColor.GREEN)));
|
||||||
target.sendMessage(msg.build());
|
target.sendMessage(msg.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the Velocity default {@code /send} command.
|
* Implements the Velocity default {@code /send} command.
|
||||||
@@ -121,7 +122,7 @@ public class SendCommand {
|
|||||||
|
|
||||||
if (maybeServer.isEmpty()) {
|
if (maybeServer.isEmpty()) {
|
||||||
context.getSource().sendMessage(
|
context.getSource().sendMessage(
|
||||||
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName))
|
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Argument.string("server", serverName))
|
||||||
);
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ public class SendCommand {
|
|||||||
&& !Objects.equals(player, "all")
|
&& !Objects.equals(player, "all")
|
||||||
&& !Objects.equals(player, "current")) {
|
&& !Objects.equals(player, "current")) {
|
||||||
context.getSource().sendMessage(
|
context.getSource().sendMessage(
|
||||||
CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player))
|
CommandMessages.PLAYER_NOT_FOUND.arguments(Argument.string("player", player))
|
||||||
);
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import net.kyori.adventure.text.TextComponent;
|
|||||||
import net.kyori.adventure.text.TranslatableComponent;
|
import net.kyori.adventure.text.TranslatableComponent;
|
||||||
import net.kyori.adventure.text.event.ClickEvent;
|
import net.kyori.adventure.text.event.ClickEvent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements Velocity's {@code /server} command.
|
* Implements Velocity's {@code /server} command.
|
||||||
@@ -76,7 +77,7 @@ public final class ServerCommand {
|
|||||||
final Optional<RegisteredServer> toConnect = server.getServer(serverName);
|
final Optional<RegisteredServer> toConnect = server.getServer(serverName);
|
||||||
if (toConnect.isEmpty()) {
|
if (toConnect.isEmpty()) {
|
||||||
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST
|
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST
|
||||||
.arguments(Component.text(serverName)));
|
.arguments(Argument.string("server", serverName)));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ public final class ServerCommand {
|
|||||||
} else {
|
} else {
|
||||||
playersTextComponent.key("velocity.command.server-tooltip-players-online");
|
playersTextComponent.key("velocity.command.server-tooltip-players-online");
|
||||||
}
|
}
|
||||||
playersTextComponent.arguments(Component.text(connectedPlayers));
|
playersTextComponent.arguments(Argument.component("players", Component.text(connectedPlayers)));
|
||||||
if (serverInfo.getName().equals(currentPlayerServer)) {
|
if (serverInfo.getName().equals(currentPlayerServer)) {
|
||||||
serverTextComponent.color(NamedTextColor.GREEN)
|
serverTextComponent.color(NamedTextColor.GREEN)
|
||||||
.hoverEvent(
|
.hoverEvent(
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import net.kyori.adventure.text.event.HoverEvent;
|
|||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.format.TextColor;
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
import net.kyori.adventure.text.format.TextDecoration;
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
@@ -162,12 +163,15 @@ public final class VelocityCommand {
|
|||||||
.append(Component.text()
|
.append(Component.text()
|
||||||
.content(version.getVersion())
|
.content(version.getVersion())
|
||||||
.decoration(TextDecoration.BOLD, false))
|
.decoration(TextDecoration.BOLD, false))
|
||||||
|
.hoverEvent(Component.translatable("velocity.command.version-offer-copy-version"))
|
||||||
|
.clickEvent(ClickEvent.copyToClipboard(version.getName() + " "
|
||||||
|
+ version.getVersion()))
|
||||||
.build();
|
.build();
|
||||||
final Component copyright = Component
|
final Component copyright = Component
|
||||||
.translatable("velocity.command.version-copyright",
|
.translatable("velocity.command.version-copyright",
|
||||||
Component.text(version.getVendor()),
|
Argument.string("vendor", version.getVendor()),
|
||||||
Component.text(version.getName()),
|
Argument.string("name", version.getName()),
|
||||||
Component.text(LocalDate.now().getYear()));
|
Argument.component("year", Component.text(LocalDate.now().getYear())));
|
||||||
source.sendMessage(velocity);
|
source.sendMessage(velocity);
|
||||||
source.sendMessage(copyright);
|
source.sendMessage(copyright);
|
||||||
|
|
||||||
@@ -176,8 +180,7 @@ public final class VelocityCommand {
|
|||||||
.append(Component.text()
|
.append(Component.text()
|
||||||
.content("PaperMC")
|
.content("PaperMC")
|
||||||
.color(NamedTextColor.GREEN)
|
.color(NamedTextColor.GREEN)
|
||||||
.clickEvent(
|
.clickEvent(ClickEvent.openUrl(VelocityServer.VELOCITY_URL))
|
||||||
ClickEvent.openUrl("https://papermc.io/software/velocity"))
|
|
||||||
.build())
|
.build())
|
||||||
.append(Component.text(" - "))
|
.append(Component.text(" - "))
|
||||||
.append(Component.text()
|
.append(Component.text()
|
||||||
@@ -221,7 +224,7 @@ public final class VelocityCommand {
|
|||||||
final TranslatableComponent output = Component.translatable()
|
final TranslatableComponent output = Component.translatable()
|
||||||
.key("velocity.command.plugins-list")
|
.key("velocity.command.plugins-list")
|
||||||
.color(NamedTextColor.YELLOW)
|
.color(NamedTextColor.YELLOW)
|
||||||
.arguments(listBuilder.build())
|
.arguments(Argument.component("plugins", listBuilder.build()))
|
||||||
.build();
|
.build();
|
||||||
source.sendMessage(output);
|
source.sendMessage(output);
|
||||||
return Command.SINGLE_SUCCESS;
|
return Command.SINGLE_SUCCESS;
|
||||||
@@ -237,17 +240,17 @@ public final class VelocityCommand {
|
|||||||
hoverText.append(Component.newline());
|
hoverText.append(Component.newline());
|
||||||
hoverText.append(Component.translatable(
|
hoverText.append(Component.translatable(
|
||||||
"velocity.command.plugin-tooltip-website",
|
"velocity.command.plugin-tooltip-website",
|
||||||
Component.text(url)));
|
Argument.component("url", Component.text(url))));
|
||||||
});
|
});
|
||||||
if (!description.getAuthors().isEmpty()) {
|
if (!description.getAuthors().isEmpty()) {
|
||||||
hoverText.append(Component.newline());
|
hoverText.append(Component.newline());
|
||||||
if (description.getAuthors().size() == 1) {
|
if (description.getAuthors().size() == 1) {
|
||||||
hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author",
|
hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author",
|
||||||
Component.text(description.getAuthors().get(0))));
|
Component.text(description.getAuthors().getFirst())));
|
||||||
} else {
|
} else {
|
||||||
hoverText.append(
|
hoverText.append(
|
||||||
Component.translatable("velocity.command.plugin-tooltip-author",
|
Component.translatable("velocity.command.plugin-tooltip-author",
|
||||||
Component.text(String.join(", ", description.getAuthors()))
|
Argument.string("authors", String.join(", ", description.getAuthors()))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -103,13 +103,17 @@ abstract class InvocableCommandRegistrar<T extends InvocableCommand<I>,
|
|||||||
.requiresWithContext((context, reader) -> requirement.test(context))
|
.requiresWithContext((context, reader) -> requirement.test(context))
|
||||||
.executes(callback)
|
.executes(callback)
|
||||||
.suggests((context, builder) -> {
|
.suggests((context, builder) -> {
|
||||||
|
// Offset the suggestion to the last space seperated word
|
||||||
|
int lastSpace = builder.getRemaining().lastIndexOf(' ') + 1;
|
||||||
|
final var offsetBuilder = builder.createOffset(builder.getStart() + lastSpace);
|
||||||
|
|
||||||
final I invocation = invocationFactory.create(context);
|
final I invocation = invocationFactory.create(context);
|
||||||
return command.suggestAsync(invocation).thenApply(suggestions -> {
|
return command.suggestAsync(invocation).thenApply(suggestions -> {
|
||||||
for (String value : suggestions) {
|
for (String value : suggestions) {
|
||||||
Preconditions.checkNotNull(value, "suggestion");
|
Preconditions.checkNotNull(value, "suggestion");
|
||||||
builder.suggest(value);
|
offsetBuilder.suggest(value);
|
||||||
}
|
}
|
||||||
return builder.build();
|
return offsetBuilder.build();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -16,13 +16,49 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.config;
|
package com.velocitypowered.proxy.config;
|
||||||
|
import com.electronwill.nightconfig.core.Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported passthrough modes for ping passthrough.
|
* Object to contain all the things that can be toggled for ping passthrough.
|
||||||
|
*
|
||||||
|
* @param version Whether the version should be passed through.
|
||||||
|
* @param players Whether the player count should be passed through.
|
||||||
|
* @param description Whether the description should be passed through.
|
||||||
|
* @param favicon Whether the favicon should be passed through.
|
||||||
|
* @param modinfo Whether the modinfo should be passed through.
|
||||||
*/
|
*/
|
||||||
public enum PingPassthroughMode {
|
public record PingPassthroughMode(boolean version, boolean players,
|
||||||
DISABLED,
|
boolean description, boolean favicon, boolean modinfo) {
|
||||||
MODS,
|
public static final PingPassthroughMode DEFAULT = new PingPassthroughMode();
|
||||||
DESCRIPTION,
|
|
||||||
ALL
|
/**
|
||||||
|
* Creates a default PingPassthroughMode.
|
||||||
|
*/
|
||||||
|
private PingPassthroughMode() {
|
||||||
|
this(false, false, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a PingPassthroughMode from a config section, or the default if the section is null.
|
||||||
|
* Based on the code for PacketLimiterConfig.
|
||||||
|
*
|
||||||
|
* @param config The configuration object to parse.
|
||||||
|
* @return The PingPassthroughMode, or the default if {@code config} is null.
|
||||||
|
*/
|
||||||
|
public static PingPassthroughMode fromConfig(Config config) {
|
||||||
|
if (config == null) {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
return new PingPassthroughMode(
|
||||||
|
config.getOrElse("version", DEFAULT.version()),
|
||||||
|
config.getOrElse("players", DEFAULT.players()),
|
||||||
|
config.getOrElse("description", DEFAULT.description()),
|
||||||
|
config.getOrElse("favicon", DEFAULT.favicon()),
|
||||||
|
config.getOrElse("modinfo", DEFAULT.modinfo()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean enabled() {
|
||||||
|
return this.version || this.players || this.description || this.favicon
|
||||||
|
|| this.modinfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ import com.velocitypowered.api.util.Favicon;
|
|||||||
import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
|
import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.ForwardingMigration;
|
import com.velocitypowered.proxy.config.migration.ForwardingMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
|
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
|
||||||
|
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.MotdMigration;
|
import com.velocitypowered.proxy.config.migration.MotdMigration;
|
||||||
|
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
|
||||||
|
import com.velocitypowered.proxy.config.migration.PingPassthroughMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
|
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
|
||||||
import com.velocitypowered.proxy.util.AddressUtil;
|
import com.velocitypowered.proxy.util.AddressUtil;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
@@ -77,7 +80,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
@Expose
|
@Expose
|
||||||
private boolean onlineModeKickExistingPlayers = false;
|
private boolean onlineModeKickExistingPlayers = false;
|
||||||
@Expose
|
@Expose
|
||||||
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
|
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DEFAULT;
|
||||||
@Expose
|
@Expose
|
||||||
private boolean samplePlayersInPing = false;
|
private boolean samplePlayersInPing = false;
|
||||||
private final Servers servers;
|
private final Servers servers;
|
||||||
@@ -93,6 +96,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
private @Nullable Favicon favicon;
|
private @Nullable Favicon favicon;
|
||||||
@Expose
|
@Expose
|
||||||
private boolean forceKeyAuthentication = true; // Added in 1.19
|
private boolean forceKeyAuthentication = true; // Added in 1.19
|
||||||
|
@Expose
|
||||||
|
private PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.DEFAULT;
|
||||||
|
|
||||||
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
|
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
|
||||||
Query query, Metrics metrics) {
|
Query query, Metrics metrics) {
|
||||||
@@ -109,7 +114,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
||||||
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
|
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
|
||||||
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
|
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
|
||||||
boolean forceKeyAuthentication) {
|
boolean forceKeyAuthentication, PacketLimiterConfig packetLimiterConfig) {
|
||||||
this.bind = bind;
|
this.bind = bind;
|
||||||
this.motd = motd;
|
this.motd = motd;
|
||||||
this.showMaxPlayers = showMaxPlayers;
|
this.showMaxPlayers = showMaxPlayers;
|
||||||
@@ -128,6 +133,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
this.query = query;
|
this.query = query;
|
||||||
this.metrics = metrics;
|
this.metrics = metrics;
|
||||||
this.forceKeyAuthentication = forceKeyAuthentication;
|
this.forceKeyAuthentication = forceKeyAuthentication;
|
||||||
|
this.packetLimiterConfig = packetLimiterConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,19 +162,16 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (playerInfoForwardingMode) {
|
switch (playerInfoForwardingMode) {
|
||||||
case NONE:
|
case NONE -> logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
|
||||||
logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
|
|
||||||
+ "from the proxy and will have offline-mode UUIDs.");
|
+ "from the proxy and will have offline-mode UUIDs.");
|
||||||
break;
|
case MODERN, BUNGEEGUARD -> {
|
||||||
case MODERN:
|
|
||||||
case BUNGEEGUARD:
|
|
||||||
if (forwardingSecret == null || forwardingSecret.length == 0) {
|
if (forwardingSecret == null || forwardingSecret.length == 0) {
|
||||||
logger.error("You don't have a forwarding secret set. This is required for security.");
|
logger.error("You don't have a forwarding secret set. This is required for security.");
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
default -> {
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (servers.getServers().isEmpty()) {
|
if (servers.getServers().isEmpty()) {
|
||||||
@@ -449,6 +452,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return advanced.isEnableReusePort();
|
return advanced.isEnableReusePort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PacketLimiterConfig getPacketLimiterConfig() {
|
||||||
|
return packetLimiterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
@@ -466,6 +473,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
.add("favicon", favicon)
|
.add("favicon", favicon)
|
||||||
.add("enablePlayerAddressLogging", enablePlayerAddressLogging)
|
.add("enablePlayerAddressLogging", enablePlayerAddressLogging)
|
||||||
.add("forceKeyAuthentication", forceKeyAuthentication)
|
.add("forceKeyAuthentication", forceKeyAuthentication)
|
||||||
|
.add("packetLimiterConfig", packetLimiterConfig)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +512,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
new ForwardingMigration(),
|
new ForwardingMigration(),
|
||||||
new KeyAuthenticationMigration(),
|
new KeyAuthenticationMigration(),
|
||||||
new MotdMigration(),
|
new MotdMigration(),
|
||||||
new TransferIntegrationMigration()
|
new MiniMessageTranslationsMigration(),
|
||||||
|
new TransferIntegrationMigration(),
|
||||||
|
new PacketLimiterMigration(),
|
||||||
|
new PingPassthroughMigration(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (final ConfigurationMigration migration : migrations) {
|
for (final ConfigurationMigration migration : migrations) {
|
||||||
@@ -515,7 +526,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
|
|
||||||
String forwardingSecretString = System.getenv().getOrDefault(
|
String forwardingSecretString = System.getenv().getOrDefault(
|
||||||
"VELOCITY_FORWARDING_SECRET", "");
|
"VELOCITY_FORWARDING_SECRET", "");
|
||||||
if (forwardingSecretString.isEmpty()) {
|
if (forwardingSecretString.isBlank()) {
|
||||||
final String forwardSecretFile = config.get("forwarding-secret-file");
|
final String forwardSecretFile = config.get("forwarding-secret-file");
|
||||||
final Path secretPath = forwardSecretFile == null
|
final Path secretPath = forwardSecretFile == null
|
||||||
? defaultForwardingSecretPath
|
? defaultForwardingSecretPath
|
||||||
@@ -528,7 +539,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
"The file " + forwardSecretFile + " is not a valid file or it is a directory.");
|
"The file " + forwardSecretFile + " is not a valid file or it is a directory.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("The forwarding-secret-file does not exist.");
|
Files.createFile(secretPath);
|
||||||
|
Files.writeString(secretPath, forwardingSecretString = generateRandomString(12),
|
||||||
|
StandardCharsets.UTF_8);
|
||||||
|
logger.info("The forwarding-secret-file does not exist. A new file has been created at {}",
|
||||||
|
forwardSecretFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
|
final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
|
||||||
@@ -542,9 +557,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
final CommentedConfig metricsConfig = config.get("metrics");
|
final CommentedConfig metricsConfig = config.get("metrics");
|
||||||
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
|
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
|
||||||
"player-info-forwarding-mode", PlayerInfoForwarding.NONE);
|
"player-info-forwarding-mode", PlayerInfoForwarding.NONE);
|
||||||
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
|
final PingPassthroughMode pingPassthrough = PingPassthroughMode.fromConfig(config.get("ping-passthrough"));
|
||||||
PingPassthroughMode.DISABLED);
|
|
||||||
|
|
||||||
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
|
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
|
||||||
|
|
||||||
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
|
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
|
||||||
@@ -557,6 +570,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
final boolean kickExisting = config.getOrElse("kick-existing-players", false);
|
final boolean kickExisting = config.getOrElse("kick-existing-players", false);
|
||||||
final boolean enablePlayerAddressLogging = config.getOrElse(
|
final boolean enablePlayerAddressLogging = config.getOrElse(
|
||||||
"enable-player-address-logging", true);
|
"enable-player-address-logging", true);
|
||||||
|
final PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.fromConfig(config.get("packet-limiter"));
|
||||||
|
|
||||||
// Throw an exception if the forwarding-secret file is empty and the proxy is using a
|
// Throw an exception if the forwarding-secret file is empty and the proxy is using a
|
||||||
// forwarding mode that requires it.
|
// forwarding mode that requires it.
|
||||||
@@ -576,7 +590,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
forwardingMode,
|
forwardingMode,
|
||||||
forwardingSecret,
|
forwardingSecret,
|
||||||
kickExisting,
|
kickExisting,
|
||||||
pingPassthroughMode,
|
pingPassthrough,
|
||||||
samplePlayersInPing,
|
samplePlayersInPing,
|
||||||
enablePlayerAddressLogging,
|
enablePlayerAddressLogging,
|
||||||
new Servers(serversConfig),
|
new Servers(serversConfig),
|
||||||
@@ -584,7 +598,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
new Advanced(advancedConfig),
|
new Advanced(advancedConfig),
|
||||||
new Query(queryConfig),
|
new Query(queryConfig),
|
||||||
new Metrics(metricsConfig),
|
new Metrics(metricsConfig),
|
||||||
forceKeyAuthentication
|
forceKeyAuthentication,
|
||||||
|
packetLimiterConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -987,4 +1002,35 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for packet limiting.
|
||||||
|
*
|
||||||
|
* @param interval the interval in seconds to measure packets over
|
||||||
|
* @param pps the maximum number of packets per second allowed
|
||||||
|
* @param bytes the maximum number of bytes per second allowed
|
||||||
|
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
|
||||||
|
*/
|
||||||
|
public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
|
||||||
|
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
|
||||||
|
*
|
||||||
|
* @param config the configuration object to parse
|
||||||
|
* @return the packet limiter config, or the default if {@code config} is null
|
||||||
|
*/
|
||||||
|
public static PacketLimiterConfig fromConfig(CommentedConfig config) {
|
||||||
|
if (config != null) {
|
||||||
|
return new PacketLimiterConfig(
|
||||||
|
config.getIntOrElse("interval", DEFAULT.interval()),
|
||||||
|
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
|
||||||
|
config.getIntOrElse("bytes-per-second", DEFAULT.bytes()),
|
||||||
|
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -28,7 +28,10 @@ public sealed interface ConfigurationMigration
|
|||||||
permits ForwardingMigration,
|
permits ForwardingMigration,
|
||||||
KeyAuthenticationMigration,
|
KeyAuthenticationMigration,
|
||||||
MotdMigration,
|
MotdMigration,
|
||||||
TransferIntegrationMigration {
|
MiniMessageTranslationsMigration,
|
||||||
|
TransferIntegrationMigration,
|
||||||
|
PacketLimiterMigration,
|
||||||
|
PingPassthroughMigration {
|
||||||
boolean shouldMigrate(CommentedFileConfig config);
|
boolean shouldMigrate(CommentedFileConfig config);
|
||||||
|
|
||||||
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
|
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.config.migration;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from old to modern language argument format with MiniMessage.
|
||||||
|
* Also migrates possible use of legacy colors to MiniMessage format.
|
||||||
|
*/
|
||||||
|
public final class MiniMessageTranslationsMigration implements ConfigurationMigration {
|
||||||
|
@Override
|
||||||
|
public boolean shouldMigrate(final CommentedFileConfig config) {
|
||||||
|
// Checking whether translations should be migrated would be just as costly as attempting to migrate them directly.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(final CommentedFileConfig config, final Logger logger) throws IOException {
|
||||||
|
final Path langFolder = Path.of("lang");
|
||||||
|
if (Files.notExists(langFolder)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Pattern oldPlaceholderPattern = Pattern.compile("\\{(\\d+)}");
|
||||||
|
try (final DirectoryStream<Path> stream
|
||||||
|
= Files.newDirectoryStream(langFolder, Files::isRegularFile)) {
|
||||||
|
for (final Path path : stream) {
|
||||||
|
String content = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
|
if (content.indexOf('{') == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Migrate old arguments
|
||||||
|
content = oldPlaceholderPattern.matcher(content).replaceAll("<arg:$1>");
|
||||||
|
// Some setups use legacy color codes, this format is migrated to MiniMessage
|
||||||
|
content = MiniMessage.miniMessage().serialize(
|
||||||
|
LegacyComponentSerializer.legacySection().deserialize(content));
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.config.migration;
|
||||||
|
|
||||||
|
import static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration migration for the new [packet-limiter] section.
|
||||||
|
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
|
||||||
|
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
|
||||||
|
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
|
||||||
|
*/
|
||||||
|
public final class PacketLimiterMigration implements ConfigurationMigration {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldMigrate(CommentedFileConfig config) {
|
||||||
|
return configVersion(config) < 2.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(CommentedFileConfig config, Logger logger) {
|
||||||
|
config.set("packet-limiter.interval", DEFAULT.interval());
|
||||||
|
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
|
||||||
|
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
|
||||||
|
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.interval", """
|
||||||
|
Size of the moving time window in seconds used to calculate average rates.
|
||||||
|
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.packets-per-second", """
|
||||||
|
Maximum average number of packets per second a client may send. -1 disables this check.""");
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.bytes-per-second", """
|
||||||
|
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.decompressed-bytes-per-second", """
|
||||||
|
Maximum average number of decompressed bytes per second a client may send.
|
||||||
|
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
|
||||||
|
-1 disables this check.""");
|
||||||
|
|
||||||
|
config.set("config-version", "2.8");
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2026 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.config.migration;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate the old ping passthrough entry to separate config entries.
|
||||||
|
*/
|
||||||
|
public final class PingPassthroughMigration implements ConfigurationMigration {
|
||||||
|
@Override
|
||||||
|
public boolean shouldMigrate(final CommentedFileConfig config) {
|
||||||
|
return configVersion(config) < 2.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(final CommentedFileConfig config, final Logger logger) {
|
||||||
|
// Get legacy ping passthrough value
|
||||||
|
final String legacyMode = config.getOrElse("ping-passthrough", "DISABLED")
|
||||||
|
.toUpperCase(Locale.ROOT);
|
||||||
|
boolean version = false;
|
||||||
|
boolean players = false;
|
||||||
|
boolean description = false;
|
||||||
|
boolean favicon = false;
|
||||||
|
boolean modinfo = false;
|
||||||
|
|
||||||
|
switch (legacyMode) {
|
||||||
|
case "ALL":
|
||||||
|
version = true;
|
||||||
|
players = true;
|
||||||
|
description = true;
|
||||||
|
favicon = true;
|
||||||
|
modinfo = true;
|
||||||
|
break;
|
||||||
|
case "DESCRIPTION":
|
||||||
|
description = true;
|
||||||
|
modinfo = true;
|
||||||
|
break;
|
||||||
|
case "MODS":
|
||||||
|
modinfo = true;
|
||||||
|
break;
|
||||||
|
default: // DISABLED
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.removeComment("ping-passthrough");
|
||||||
|
config.remove("ping-passthrough");
|
||||||
|
|
||||||
|
config.setComment("announce-forge",
|
||||||
|
" Announce whether or not your server supports Forge. If you run a modded server, we\n"
|
||||||
|
+ " suggest turning this on.\n"
|
||||||
|
+ "\n"
|
||||||
|
+ " If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true\n"
|
||||||
|
+ " instead for a nicer display in the server list.");
|
||||||
|
|
||||||
|
config.set("ping-passthrough.version", version);
|
||||||
|
config.setComment(
|
||||||
|
"ping-passthrough.version",
|
||||||
|
" Should Velocity pass the version number from the backend server when responding to server list ping requests?"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set("ping-passthrough.players", players);
|
||||||
|
config.setComment(
|
||||||
|
"ping-passthrough.players",
|
||||||
|
" Should Velocity pass the player count from the backend server when responding to server list ping requests?"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set("ping-passthrough.description", description);
|
||||||
|
config.setComment(
|
||||||
|
"ping-passthrough.description",
|
||||||
|
" Should Velocity pass the description from the backend server when responding to server list ping requests?"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set("ping-passthrough.favicon", favicon);
|
||||||
|
config.setComment(
|
||||||
|
"ping-passthrough.favicon",
|
||||||
|
" Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set("ping-passthrough.modinfo", modinfo);
|
||||||
|
config.setComment(
|
||||||
|
"ping-passthrough.modinfo",
|
||||||
|
" Should Velocity pass the mod list from the backend server when responding to server list ping requests?"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.set("config-version", "2.9");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,11 +33,14 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
|
|||||||
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
|
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
|
||||||
import com.velocitypowered.natives.util.Natives;
|
import com.velocitypowered.natives.util.Natives;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
|
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
|
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
import com.velocitypowered.proxy.network.Connections;
|
||||||
|
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
|
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
|
||||||
@@ -66,7 +69,7 @@ import io.netty.util.ReferenceCountUtil;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.util.HashMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -108,7 +111,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
this.server = server;
|
this.server = server;
|
||||||
this.state = StateRegistry.HANDSHAKE;
|
this.state = StateRegistry.HANDSHAKE;
|
||||||
|
|
||||||
this.sessionHandlers = new HashMap<>();
|
this.sessionHandlers = new EnumMap<>(StateRegistry.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -153,13 +156,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
if (msg instanceof MinecraftPacket pkt) {
|
if (msg instanceof MinecraftPacket pkt) {
|
||||||
if (!pkt.handle(activeSessionHandler)) {
|
if (!pkt.handle(activeSessionHandler)) {
|
||||||
activeSessionHandler.handleGeneric((MinecraftPacket) msg);
|
activeSessionHandler.handleGeneric(pkt);
|
||||||
}
|
}
|
||||||
} else if (msg instanceof HAProxyMessage proxyMessage) {
|
} else if (msg instanceof HAProxyMessage proxyMessage) {
|
||||||
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
|
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
|
||||||
proxyMessage.sourcePort());
|
proxyMessage.sourcePort());
|
||||||
} else if (msg instanceof ByteBuf) {
|
} else if (msg instanceof ByteBuf buf) {
|
||||||
activeSessionHandler.handleUnknown((ByteBuf) msg);
|
activeSessionHandler.handleUnknown(buf);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
ReferenceCountUtil.release(msg);
|
ReferenceCountUtil.release(msg);
|
||||||
@@ -368,6 +371,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
public void setState(StateRegistry state) {
|
public void setState(StateRegistry state) {
|
||||||
ensureInEventLoop();
|
ensureInEventLoop();
|
||||||
|
|
||||||
|
final StateRegistry previousState = this.state;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
|
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
|
||||||
.get(MinecraftVarintFrameDecoder.class);
|
.get(MinecraftVarintFrameDecoder.class);
|
||||||
@@ -388,7 +392,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
if (state == StateRegistry.CONFIG) {
|
if (state == StateRegistry.CONFIG) {
|
||||||
// Activate the play packet queue
|
// Activate the play packet queue
|
||||||
|
if (previousState == StateRegistry.PLAY
|
||||||
|
&& this.pendingConfigurationSwitch
|
||||||
|
&& this.association instanceof ConnectedPlayer) {
|
||||||
|
addPlayPacketQueueOutboundHandler();
|
||||||
|
} else {
|
||||||
addPlayPacketQueueHandler();
|
addPlayPacketQueueHandler();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remove the queue
|
// Remove the queue
|
||||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
|
||||||
@@ -404,13 +414,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* Adds the play packet queue handler.
|
* Adds the play packet queue handler.
|
||||||
*/
|
*/
|
||||||
public void addPlayPacketQueueHandler() {
|
public void addPlayPacketQueueHandler() {
|
||||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
|
addPlayPacketQueueOutboundHandler();
|
||||||
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
|
|
||||||
new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
|
|
||||||
}
|
|
||||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
|
||||||
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
|
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
|
||||||
new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection()));
|
new PlayPacketQueueInboundHandler(this.protocolVersion,
|
||||||
|
channel.pipeline().get(MinecraftDecoder.class).getDirection()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds only the outbound play packet queue handler.
|
||||||
|
*/
|
||||||
|
public void addPlayPacketQueueOutboundHandler() {
|
||||||
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
|
||||||
|
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
|
||||||
|
new PlayPacketQueueOutboundHandler(this.protocolVersion,
|
||||||
|
channel.pipeline().get(MinecraftEncoder.class).getDirection()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,14 +564,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
} else {
|
} else {
|
||||||
int level = server.getConfiguration().getCompressionLevel();
|
int level = server.getConfiguration().getCompressionLevel();
|
||||||
VelocityCompressor compressor = Natives.compress.get().create(level);
|
VelocityCompressor compressor = Natives.compress.get().create(level);
|
||||||
|
final MinecraftDecoder minecraftDecoder = (MinecraftDecoder) channel.pipeline().get(MINECRAFT_DECODER);
|
||||||
|
|
||||||
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
|
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
|
||||||
decoder = new MinecraftCompressDecoder(threshold, compressor);
|
decoder = new MinecraftCompressDecoder(threshold, compressor, minecraftDecoder.getDirection());
|
||||||
|
|
||||||
channel.pipeline().remove(FRAME_ENCODER);
|
channel.pipeline().remove(FRAME_ENCODER);
|
||||||
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
||||||
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
||||||
|
|
||||||
|
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
|
||||||
|
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
|
||||||
|
&& packetLimiterConfig.interval() > 0
|
||||||
|
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
|
||||||
|
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
|
||||||
|
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
|
||||||
|
}
|
||||||
|
|
||||||
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
|
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.DialogClearPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
|
||||||
@@ -48,6 +53,8 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
|
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||||
@@ -67,6 +74,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerComma
|
|||||||
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
|
||||||
@@ -193,6 +202,10 @@ public interface MinecraftSessionHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean handle(ServerboundPlayerLoadedPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
default boolean handle(ServerLoginPacket packet) {
|
default boolean handle(ServerLoginPacket packet) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -364,4 +377,36 @@ public interface MinecraftSessionHandler {
|
|||||||
default boolean handle(ClientboundServerLinksPacket packet) {
|
default boolean handle(ClientboundServerLinksPacket packet) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean handle(DialogClearPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(DialogShowPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(ServerboundCustomClickActionPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(CodeOfConductPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(CodeOfConductAcceptPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(ClientboundSoundEntityPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(ClientboundStopSoundPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean handle(ClientboundPostEffectsPacket packet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-11
@@ -34,6 +34,8 @@ import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
|
|||||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.command.CommandGraphInjector;
|
import com.velocitypowered.proxy.command.CommandGraphInjector;
|
||||||
@@ -55,6 +57,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
|
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||||
@@ -68,6 +71,7 @@ import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
@@ -91,6 +95,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
Boolean.getBoolean("velocity.log-server-backpressure");
|
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||||
private static final int MAXIMUM_PACKETS_TO_FLUSH =
|
private static final int MAXIMUM_PACKETS_TO_FLUSH =
|
||||||
Integer.getInteger("velocity.max-packets-per-flush", 8192);
|
Integer.getInteger("velocity.max-packets-per-flush", 8192);
|
||||||
|
private static final int LARGE_PACKET_THRESHOLD = 1024 * 128;
|
||||||
|
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private final VelocityServerConnection serverConn;
|
private final VelocityServerConnection serverConn;
|
||||||
@@ -162,6 +167,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return false; // forwards on
|
return false; // forwards on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(JoinGamePacket packet) {
|
||||||
|
// We receive this if the connection is reconfigured
|
||||||
|
packet.setOnlineMode(serverConn.getPlayer().isOnlineMode());
|
||||||
|
return false; // forward
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(ClientSettingsPacket packet) {
|
public boolean handle(ClientSettingsPacket packet) {
|
||||||
serverConn.ensureConnected().write(packet);
|
serverConn.ensureConnected().write(packet);
|
||||||
@@ -177,11 +189,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(BossBarPacket packet) {
|
public boolean handle(BossBarPacket packet) {
|
||||||
|
if (serverConn.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||||
if (packet.getAction() == BossBarPacket.ADD) {
|
if (packet.getAction() == BossBarPacket.ADD) {
|
||||||
playerSessionHandler.getServerBossBars().add(packet.getUuid());
|
playerSessionHandler.getServerBossBars().add(packet.getUuid());
|
||||||
} else if (packet.getAction() == BossBarPacket.REMOVE) {
|
} else if (packet.getAction() == BossBarPacket.REMOVE) {
|
||||||
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
|
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false; // forward
|
return false; // forward
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +304,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register and unregister packets are simply forwarded to the server as-is.
|
// Register and unregister packets are simply forwarded to the client as-is.
|
||||||
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
|
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -308,13 +322,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
|
|
||||||
if (id == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy);
|
String channel = packet.getChannel();
|
||||||
|
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
|
||||||
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
|
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
|
||||||
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
|
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
|
||||||
PluginMessagePacket copied = new PluginMessagePacket(
|
PluginMessagePacket copied = new PluginMessagePacket(
|
||||||
@@ -359,8 +369,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
// Inject commands from the proxy.
|
// Inject commands from the proxy.
|
||||||
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
|
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
|
||||||
injector.inject(rootNode, serverConn.getPlayer());
|
injector.inject(rootNode, serverConn.getPlayer());
|
||||||
|
|
||||||
|
// In 1.21.6 a confirmation prompt was added when executing a command via `run_command` click
|
||||||
|
// action if the command is unknown. To prevent this prompt we have to send the command.
|
||||||
|
if (this.playerConnection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6)) {
|
||||||
rootNode.removeChildByName("velocity:callback");
|
rootNode.removeChildByName("velocity:callback");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
server.getEventManager().fire(
|
server.getEventManager().fire(
|
||||||
new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode))
|
new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode))
|
||||||
@@ -445,11 +460,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleGeneric(MinecraftPacket packet) {
|
public void handleGeneric(MinecraftPacket packet) {
|
||||||
if (packet instanceof PluginMessagePacket) {
|
if (packet instanceof PluginMessagePacket pluginMessage) {
|
||||||
((PluginMessagePacket) packet).retain();
|
pluginMessage.retain();
|
||||||
}
|
}
|
||||||
|
boolean huge = packet instanceof DeferredByteBufHolder def && def.content().readableBytes() > LARGE_PACKET_THRESHOLD;
|
||||||
playerConnection.delayedWrite(packet);
|
playerConnection.delayedWrite(packet);
|
||||||
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||||
playerConnection.flush();
|
playerConnection.flush();
|
||||||
packetsFlushed = 0;
|
packetsFlushed = 0;
|
||||||
}
|
}
|
||||||
@@ -457,8 +473,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleUnknown(ByteBuf buf) {
|
public void handleUnknown(ByteBuf buf) {
|
||||||
|
boolean huge = buf.readableBytes() > LARGE_PACKET_THRESHOLD;
|
||||||
playerConnection.delayedWrite(buf.retain());
|
playerConnection.delayedWrite(buf.retain());
|
||||||
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||||
playerConnection.flush();
|
playerConnection.flush();
|
||||||
packetsFlushed = 0;
|
packetsFlushed = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-58
@@ -344,66 +344,30 @@ public class BungeeCordMessageResponder {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBufDataInput in = new ByteBufDataInput(message.content());
|
final ByteBufDataInput in = new ByteBufDataInput(message.content());
|
||||||
String subChannel = in.readUTF();
|
final String subChannel = in.readUTF();
|
||||||
switch (subChannel) {
|
switch (subChannel) {
|
||||||
case "GetPlayerServer":
|
case "GetPlayerServer" -> this.processGetPlayerServer(in);
|
||||||
this.processGetPlayerServer(in);
|
case "ForwardToPlayer" -> this.processForwardToPlayer(in);
|
||||||
break;
|
case "Forward" -> this.processForwardToServer(in);
|
||||||
case "ForwardToPlayer":
|
case "Connect" -> this.processConnect(in);
|
||||||
this.processForwardToPlayer(in);
|
case "ConnectOther" -> this.processConnectOther(in);
|
||||||
break;
|
case "IP" -> this.processIp(in);
|
||||||
case "Forward":
|
case "PlayerCount" -> this.processPlayerCount(in);
|
||||||
this.processForwardToServer(in);
|
case "PlayerList" -> this.processPlayerList(in);
|
||||||
break;
|
case "GetServers" -> this.processGetServers();
|
||||||
case "Connect":
|
case "Message" -> this.processMessage(in);
|
||||||
this.processConnect(in);
|
case "MessageRaw" -> this.processMessageRaw(in);
|
||||||
break;
|
case "GetServer" -> this.processGetServer();
|
||||||
case "ConnectOther":
|
case "UUID" -> this.processUuid();
|
||||||
this.processConnectOther(in);
|
case "UUIDOther" -> this.processUuidOther(in);
|
||||||
break;
|
case "IPOther" -> this.processIpOther(in);
|
||||||
case "IP":
|
case "ServerIP" -> this.processServerIp(in);
|
||||||
this.processIp(in);
|
case "KickPlayer" -> this.processKick(in);
|
||||||
break;
|
case "KickPlayerRaw" -> this.processKickRaw(in);
|
||||||
case "PlayerCount":
|
default -> {
|
||||||
this.processPlayerCount(in);
|
|
||||||
break;
|
|
||||||
case "PlayerList":
|
|
||||||
this.processPlayerList(in);
|
|
||||||
break;
|
|
||||||
case "GetServers":
|
|
||||||
this.processGetServers();
|
|
||||||
break;
|
|
||||||
case "Message":
|
|
||||||
this.processMessage(in);
|
|
||||||
break;
|
|
||||||
case "MessageRaw":
|
|
||||||
this.processMessageRaw(in);
|
|
||||||
break;
|
|
||||||
case "GetServer":
|
|
||||||
this.processGetServer();
|
|
||||||
break;
|
|
||||||
case "UUID":
|
|
||||||
this.processUuid();
|
|
||||||
break;
|
|
||||||
case "UUIDOther":
|
|
||||||
this.processUuidOther(in);
|
|
||||||
break;
|
|
||||||
case "IPOther":
|
|
||||||
this.processIpOther(in);
|
|
||||||
break;
|
|
||||||
case "ServerIP":
|
|
||||||
this.processServerIp(in);
|
|
||||||
break;
|
|
||||||
case "KickPlayer":
|
|
||||||
this.processKick(in);
|
|
||||||
break;
|
|
||||||
case "KickPlayerRaw":
|
|
||||||
this.processKickRaw(in);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Do nothing, unknown command
|
// Do nothing, unknown command
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+36
-4
@@ -52,6 +52,7 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
||||||
@@ -59,7 +60,7 @@ import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import java.io.IOException;
|
import io.netty.channel.Channel;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
@@ -71,6 +72,9 @@ import org.apache.logging.log4j.Logger;
|
|||||||
* 1.20.2+ switching. Yes, some of this is exceptionally stupid.
|
* 1.20.2+ switching. Yes, some of this is exceptionally stupid.
|
||||||
*/
|
*/
|
||||||
public class ConfigSessionHandler implements MinecraftSessionHandler {
|
public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||||
|
private static final boolean BACKPRESSURE_LOG =
|
||||||
|
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class);
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private final VelocityServerConnection serverConn;
|
private final VelocityServerConnection serverConn;
|
||||||
@@ -256,7 +260,13 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public boolean handle(DisconnectPacket packet) {
|
public boolean handle(DisconnectPacket packet) {
|
||||||
serverConn.disconnect();
|
serverConn.disconnect();
|
||||||
|
// If the player receives a DisconnectPacket without a connection to a server in progress,
|
||||||
|
// it means that the backend server has kicked the player during reconfiguration
|
||||||
|
if (serverConn.getPlayer().getConnectionInFlight() != null) {
|
||||||
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
|
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
|
||||||
|
} else {
|
||||||
|
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), packet, true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
|
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
|
||||||
serverConn.getPlayer().getProtocolVersion()));
|
serverConn.getPlayer().getProtocolVersion()));
|
||||||
} else {
|
} else {
|
||||||
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
|
||||||
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
|
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||||
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
@@ -277,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
// Handling this stuff async means that we should probably pause
|
// Handling this stuff async means that we should probably pause
|
||||||
// the connection while we toss this off into another pool
|
// the connection while we toss this off into another pool
|
||||||
|
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
||||||
this.serverConn.getConnection().setAutoReading(false);
|
this.serverConn.getConnection().setAutoReading(false);
|
||||||
this.server.getEventManager()
|
this.server.getEventManager()
|
||||||
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
|
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
|
||||||
@@ -358,10 +368,16 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(CodeOfConductPacket packet) {
|
||||||
|
this.serverConn.getPlayer().getConnection().write(packet.retain());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disconnected() {
|
public void disconnected() {
|
||||||
resultFuture.completeExceptionally(
|
resultFuture.complete(ConnectionRequestResults.forDisconnect(
|
||||||
new IOException("Unexpectedly disconnected from remote server"));
|
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -369,6 +385,22 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
serverConn.getPlayer().getConnection().write(packet);
|
serverConn.getPlayer().getConnection().write(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writabilityChanged() {
|
||||||
|
Channel serverChan = serverConn.ensureConnected().getChannel();
|
||||||
|
boolean writable = serverChan.isWritable();
|
||||||
|
|
||||||
|
if (BACKPRESSURE_LOG) {
|
||||||
|
if (writable) {
|
||||||
|
logger.info("{} is writable, will auto-read player connection data", this.serverConn);
|
||||||
|
} else {
|
||||||
|
logger.info("{} is not writable, not auto-reading player connection data", this.serverConn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConn.getPlayer().getConnection().setAutoReading(writable);
|
||||||
|
}
|
||||||
|
|
||||||
private void switchFailure(Throwable cause) {
|
private void switchFailure(Throwable cause) {
|
||||||
logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(),
|
logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(),
|
||||||
serverConn.getPlayer().getUsername(), cause);
|
serverConn.getPlayer().getUsername(), cause);
|
||||||
|
|||||||
+1
-1
@@ -165,7 +165,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
|
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
|
||||||
smc.setAutoReading(false);
|
smc.setAutoReading(false);
|
||||||
clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
|
clientPlaySessionHandler.doSwitch().thenRunAsync(() -> smc.setAutoReading(true), smc.eventLoop());
|
||||||
} else {
|
} else {
|
||||||
// Initial login - the player is already in configuration state.
|
// Initial login - the player is already in configuration state.
|
||||||
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
|
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
|
||||||
|
|||||||
+8
-7
@@ -38,7 +38,6 @@ import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -90,7 +89,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(JoinGamePacket packet) {
|
public boolean handle(JoinGamePacket packet) {
|
||||||
MinecraftConnection smc = serverConn.ensureConnected();
|
final MinecraftConnection smc = serverConn.ensureConnected();
|
||||||
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
|
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
|
||||||
final ConnectedPlayer player = serverConn.getPlayer();
|
final ConnectedPlayer player = serverConn.getPlayer();
|
||||||
final VelocityServerConnection existingConnection = player.getConnectedServer();
|
final VelocityServerConnection existingConnection = player.getConnectedServer();
|
||||||
@@ -107,6 +106,9 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
// Reset Tablist header and footer to prevent desync
|
// Reset Tablist header and footer to prevent desync
|
||||||
player.clearPlayerListHeaderAndFooter();
|
player.clearPlayerListHeaderAndFooter();
|
||||||
|
|
||||||
|
// Override online mode
|
||||||
|
packet.setOnlineMode(player.isOnlineMode());
|
||||||
|
|
||||||
// The goods are in hand! We got JoinGame. Let's transition completely to the new state.
|
// The goods are in hand! We got JoinGame. Let's transition completely to the new state.
|
||||||
smc.setAutoReading(false);
|
smc.setAutoReading(false);
|
||||||
server.getEventManager()
|
server.getEventManager()
|
||||||
@@ -122,9 +124,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
// Change the client to use the ClientPlaySessionHandler if required.
|
// Change the client to use the ClientPlaySessionHandler if required.
|
||||||
ClientPlaySessionHandler playHandler;
|
ClientPlaySessionHandler playHandler;
|
||||||
if (player.getConnection()
|
if (player.getConnection()
|
||||||
.getActiveSessionHandler() instanceof ClientPlaySessionHandler) {
|
.getActiveSessionHandler() instanceof ClientPlaySessionHandler sessionHandler) {
|
||||||
playHandler =
|
playHandler = sessionHandler;
|
||||||
(ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler();
|
|
||||||
} else {
|
} else {
|
||||||
playHandler = new ClientPlaySessionHandler(server, player);
|
playHandler = new ClientPlaySessionHandler(server, player);
|
||||||
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler);
|
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler);
|
||||||
@@ -214,7 +215,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disconnected() {
|
public void disconnected() {
|
||||||
resultFuture
|
resultFuture.complete(ConnectionRequestResults.forDisconnect(
|
||||||
.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
|
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -53,6 +53,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -67,9 +68,11 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private @Nullable MinecraftConnection connection;
|
private @Nullable MinecraftConnection connection;
|
||||||
private boolean hasCompletedJoin = false;
|
private boolean hasCompletedJoin = false;
|
||||||
|
private boolean clientLoaded = false; // 1.21.4+
|
||||||
private boolean gracefulDisconnect = false;
|
private boolean gracefulDisconnect = false;
|
||||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||||
private final Map<Long, Long> pendingPings = new HashMap<>();
|
private final Map<Long, Long> pendingPings = new HashMap<>();
|
||||||
|
private @MonotonicNonNull Integer entityId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new server connection.
|
* Initializes a new server connection.
|
||||||
@@ -178,9 +181,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
handshake.setServerAddress(createBungeeGuardForwardingAddress(secret));
|
handshake.setServerAddress(createBungeeGuardForwardingAddress(secret));
|
||||||
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||||
handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN);
|
handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN);
|
||||||
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType) {
|
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType forgeConnection) {
|
||||||
handshake.setServerAddress(playerVhost + ((ModernForgeConnectionType) proxyPlayer
|
handshake.setServerAddress(playerVhost + forgeConnection.getModernToken());
|
||||||
.getConnection().getType()).getModernToken());
|
|
||||||
} else {
|
} else {
|
||||||
handshake.setServerAddress(playerVhost);
|
handshake.setServerAddress(playerVhost);
|
||||||
}
|
}
|
||||||
@@ -316,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setClientLoaded(boolean clientLoaded) {
|
||||||
|
this.clientLoaded = clientLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClientLoaded() {
|
||||||
|
return clientLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
boolean isGracefulDisconnect() {
|
boolean isGracefulDisconnect() {
|
||||||
return gracefulDisconnect;
|
return gracefulDisconnect;
|
||||||
}
|
}
|
||||||
@@ -324,6 +335,14 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
return pendingPings;
|
return pendingPings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getEntityId() {
|
||||||
|
return entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEntityId(Integer entityId) {
|
||||||
|
this.entityId = entityId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that this server connection remains "active": the connection is established and not
|
* Ensures that this server connection remains "active": the connection is established and not
|
||||||
* closed, the player is still connected to the server, and the player still remains online.
|
* closed, the player is still connected to the server, and the player still remains online.
|
||||||
|
|||||||
+7
-2
@@ -69,14 +69,16 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
|
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
|
||||||
private final boolean onlineMode;
|
private final boolean onlineMode;
|
||||||
private State loginState = State.START; // 1.20.2+
|
private State loginState = State.START; // 1.20.2+
|
||||||
|
private final String serverIdHash;
|
||||||
|
|
||||||
AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound,
|
AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound,
|
||||||
GameProfile profile, boolean onlineMode) {
|
GameProfile profile, boolean onlineMode, String serverIdHash) {
|
||||||
this.server = Preconditions.checkNotNull(server, "server");
|
this.server = Preconditions.checkNotNull(server, "server");
|
||||||
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
|
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
|
||||||
this.profile = Preconditions.checkNotNull(profile, "profile");
|
this.profile = Preconditions.checkNotNull(profile, "profile");
|
||||||
this.onlineMode = onlineMode;
|
this.onlineMode = onlineMode;
|
||||||
this.mcConnection = inbound.delegatedConnection();
|
this.mcConnection = inbound.delegatedConnection();
|
||||||
|
this.serverIdHash = serverIdHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -213,7 +215,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) {
|
private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) {
|
||||||
mcConnection.setAssociation(player);
|
mcConnection.setAssociation(player);
|
||||||
|
|
||||||
server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> {
|
server.getEventManager().fire(new LoginEvent(player, serverIdHash)).thenAcceptAsync(event -> {
|
||||||
if (mcConnection.isClosed()) {
|
if (mcConnection.isClosed()) {
|
||||||
// The player was disconnected
|
// The player was disconnected
|
||||||
server.getEventManager().fireAndForget(new DisconnectEvent(player,
|
server.getEventManager().fireAndForget(new DisconnectEvent(player,
|
||||||
@@ -234,6 +236,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
success.setUsername(player.getUsername());
|
success.setUsername(player.getUsername());
|
||||||
success.setProperties(player.getGameProfileProperties());
|
success.setProperties(player.getGameProfileProperties());
|
||||||
success.setUuid(player.getUniqueId());
|
success.setUuid(player.getUniqueId());
|
||||||
|
if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
|
||||||
|
success.setSessionId(server.getSessionId());
|
||||||
|
}
|
||||||
mcConnection.write(success);
|
mcConnection.write(success);
|
||||||
|
|
||||||
loginState = State.SUCCESS_SENT;
|
loginState = State.SUCCESS_SENT;
|
||||||
|
|||||||
+60
-3
@@ -33,6 +33,7 @@ import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResp
|
|||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
|
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
||||||
@@ -40,10 +41,13 @@ import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
|
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
|
||||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.ByteBufHolder;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -58,6 +62,8 @@ import org.apache.logging.log4j.Logger;
|
|||||||
* Handles the client config stage.
|
* Handles the client config stage.
|
||||||
*/
|
*/
|
||||||
public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||||
|
private static final boolean BACKPRESSURE_LOG =
|
||||||
|
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class);
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
@@ -130,7 +136,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
|
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
|
||||||
return true;
|
return true;
|
||||||
} else if (serverConn != null) {
|
} else if (serverConn != null) {
|
||||||
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
|
||||||
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
|
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||||
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
@@ -140,6 +145,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
// Handling this stuff async means that we should probably pause
|
// Handling this stuff async means that we should probably pause
|
||||||
// the connection while we toss this off into another pool
|
// the connection while we toss this off into another pool
|
||||||
|
byte[] bytes = ByteBufUtil.getBytes(packet.content());
|
||||||
serverConn.getPlayer().getConnection().setAutoReading(false);
|
serverConn.getPlayer().getConnection().setAutoReading(false);
|
||||||
this.server.getEventManager()
|
this.server.getEventManager()
|
||||||
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
|
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
|
||||||
@@ -205,6 +211,27 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(ServerboundCustomClickActionPacket packet) {
|
||||||
|
VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer();
|
||||||
|
if (serverConnection != null) {
|
||||||
|
serverConnection.ensureConnected().write(packet.retain());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(CodeOfConductAcceptPacket packet) {
|
||||||
|
if (this.player.getConnectionInFlight() != null) {
|
||||||
|
this.player.getConnectionInFlight().ensureConnected().write(packet);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleGeneric(MinecraftPacket packet) {
|
public void handleGeneric(MinecraftPacket packet) {
|
||||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||||
@@ -215,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
MinecraftConnection smc = serverConnection.getConnection();
|
MinecraftConnection smc = serverConnection.getConnection();
|
||||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||||
if (packet instanceof PluginMessagePacket) {
|
if (packet instanceof ByteBufHolder bufHolder) {
|
||||||
((PluginMessagePacket) packet).retain();
|
bufHolder.retain();
|
||||||
}
|
}
|
||||||
smc.write(packet);
|
smc.write(packet);
|
||||||
}
|
}
|
||||||
@@ -244,6 +271,36 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void exception(Throwable throwable) {
|
public void exception(Throwable throwable) {
|
||||||
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
|
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
|
||||||
|
if (MinecraftDecoder.DEBUG) {
|
||||||
|
logger.info("Exception while handling packet for {}", player, throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writabilityChanged() {
|
||||||
|
final boolean writable = player.getConnection().getChannel().isWritable();
|
||||||
|
|
||||||
|
if (BACKPRESSURE_LOG) {
|
||||||
|
if (writable) {
|
||||||
|
logger.info("{} is writable, will auto-read backend connection data", player);
|
||||||
|
} else {
|
||||||
|
logger.info("{} is not writable, not auto-reading backend connection data", player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!writable) {
|
||||||
|
// Flush pending packets to free up memory. Schedule on a future event loop invocation
|
||||||
|
// to avoid disabling auto-read while the flush resolves backpressure.
|
||||||
|
player.getConnection().eventLoop().execute(() -> player.getConnection().flush());
|
||||||
|
}
|
||||||
|
|
||||||
|
final VelocityServerConnection serverConn = player.getConnectionInFlightOrConnectedServer();
|
||||||
|
if (serverConn != null) {
|
||||||
|
final MinecraftConnection smc = serverConn.getConnection();
|
||||||
|
if (smc != null) {
|
||||||
|
smc.setAutoReading(writable);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+137
-26
@@ -21,15 +21,18 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.mojang.brigadier.suggestion.Suggestion;
|
import com.mojang.brigadier.suggestion.Suggestion;
|
||||||
import com.velocitypowered.api.command.VelocityBrigadierMessage;
|
|
||||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||||
import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
||||||
|
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
||||||
|
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
|
||||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||||
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
@@ -41,6 +44,7 @@ import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
|
|||||||
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
|
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
|
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
|
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
||||||
@@ -49,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
|
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
||||||
@@ -85,8 +90,11 @@ import java.util.Queue;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.ComponentLike;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -97,6 +105,16 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
* center that joins backend servers with players.
|
* center that joins backend servers with players.
|
||||||
*/
|
*/
|
||||||
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||||
|
private static final boolean BACKPRESSURE_LOG =
|
||||||
|
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||||
|
|
||||||
|
// Caps the per-connection queue used while the FML/login phases are not yet "complete". Without
|
||||||
|
// these caps, a client that never completes its handshake phase can spam plugin messages (each up
|
||||||
|
// to ~32 KiB serverbound) and grow the queue without bound.
|
||||||
|
private static final long MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES =
|
||||||
|
Long.getLong("velocity.max-queued-login-plugin-message-bytes", 4L * 1024 * 1024);
|
||||||
|
private static final int MAX_QUEUED_LOGIN_PLUGIN_MESSAGES =
|
||||||
|
Integer.getInteger("velocity.max-queued-login-plugin-messages", 1024);
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
|
||||||
|
|
||||||
@@ -104,6 +122,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
private boolean spawned = false;
|
private boolean spawned = false;
|
||||||
private final List<UUID> serverBossBars = new ArrayList<>();
|
private final List<UUID> serverBossBars = new ArrayList<>();
|
||||||
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
|
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
|
||||||
|
private final AtomicLong loginPluginMessagesBytes = new AtomicLong();
|
||||||
|
private final AtomicInteger loginPluginMessagesCount = new AtomicInteger();
|
||||||
|
private volatile boolean loginPluginMessagesOverflowed;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private @Nullable TabCompleteRequestPacket outstandingTabComplete;
|
private @Nullable TabCompleteRequestPacket outstandingTabComplete;
|
||||||
private final ChatHandler<? extends MinecraftPacket> chatHandler;
|
private final ChatHandler<? extends MinecraftPacket> chatHandler;
|
||||||
@@ -172,9 +193,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void deactivated() {
|
public void deactivated() {
|
||||||
player.discardChatQueue();
|
player.discardChatQueue();
|
||||||
for (PluginMessagePacket message : loginPluginMessages) {
|
PluginMessagePacket message;
|
||||||
|
while ((message = loginPluginMessages.poll()) != null) {
|
||||||
ReferenceCountUtil.release(message);
|
ReferenceCountUtil.release(message);
|
||||||
}
|
}
|
||||||
|
loginPluginMessagesBytes.set(0);
|
||||||
|
loginPluginMessagesCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a retained plugin message to the queue used while the FML/login phases are still in
|
||||||
|
* progress, enforcing the per-connection byte and count caps. Returns {@code true} if queued,
|
||||||
|
* {@code false} if the packet was released (and the player disconnected on overflow).
|
||||||
|
*/
|
||||||
|
private boolean enqueueLoginPluginMessage(PluginMessagePacket packet) {
|
||||||
|
if (loginPluginMessagesOverflowed) {
|
||||||
|
ReferenceCountUtil.release(packet);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int packetSize = packet.content().readableBytes();
|
||||||
|
long newBytes = loginPluginMessagesBytes.addAndGet(packetSize);
|
||||||
|
int newCount = loginPluginMessagesCount.incrementAndGet();
|
||||||
|
if (newBytes > MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES
|
||||||
|
|| newCount > MAX_QUEUED_LOGIN_PLUGIN_MESSAGES) {
|
||||||
|
loginPluginMessagesOverflowed = true;
|
||||||
|
ReferenceCountUtil.release(packet);
|
||||||
|
logger.warn("Disconnecting {}: pre-join plugin-message queue exceeded its limits "
|
||||||
|
+ "({} messages, {} bytes).", player, newCount, newBytes);
|
||||||
|
player.disconnect(Component.translatable("velocity.error.plugin-message-overflow"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loginPluginMessages.add(packet);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -195,6 +245,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true; // will forward onto the server
|
return true; // will forward onto the server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(ServerboundPlayerLoadedPacket packet) {
|
||||||
|
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||||
|
if (serverConnection == null) {
|
||||||
|
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!serverConnection.isClientLoaded()) {
|
||||||
|
serverConnection.setClientLoaded(true);
|
||||||
|
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(SessionPlayerCommandPacket packet) {
|
public boolean handle(SessionPlayerCommandPacket packet) {
|
||||||
if (player.getCurrentServer().isEmpty()) {
|
if (player.getCurrentServer().isEmpty()) {
|
||||||
@@ -318,8 +382,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
|
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
|
||||||
backendConn.write(packet.retain());
|
backendConn.write(packet.retain());
|
||||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||||
player.getClientsideChannels()
|
List<ChannelIdentifier> channels =
|
||||||
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()));
|
PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion());
|
||||||
|
player.getClientsideChannels().removeAll(channels);
|
||||||
|
server.getEventManager()
|
||||||
|
.fireAndForget(
|
||||||
|
new PlayerChannelUnregisterEvent(player, ImmutableList.copyOf(channels)));
|
||||||
backendConn.write(packet.retain());
|
backendConn.write(packet.retain());
|
||||||
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
||||||
String brand = PluginMessageUtil.readBrandMessage(packet.content());
|
String brand = PluginMessageUtil.readBrandMessage(packet.content());
|
||||||
@@ -351,7 +419,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
//
|
//
|
||||||
// We also need to make sure to retain these packets, so they can be flushed
|
// We also need to make sure to retain these packets, so they can be flushed
|
||||||
// appropriately.
|
// appropriately.
|
||||||
loginPluginMessages.add(packet.retain());
|
enqueueLoginPluginMessage(packet.retain());
|
||||||
} else {
|
} else {
|
||||||
// The connection is ready, send the packet now.
|
// The connection is ready, send the packet now.
|
||||||
backendConn.write(packet.retain());
|
backendConn.write(packet.retain());
|
||||||
@@ -366,7 +434,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||||
.consideredComplete()) {
|
.consideredComplete()) {
|
||||||
// We're still processing the connection (see above), enqueue the packet for now.
|
// We're still processing the connection (see above), enqueue the packet for now.
|
||||||
loginPluginMessages.add(message.retain());
|
enqueueLoginPluginMessage(message.retain());
|
||||||
} else {
|
} else {
|
||||||
backendConn.write(message);
|
backendConn.write(message);
|
||||||
}
|
}
|
||||||
@@ -463,7 +531,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MinecraftConnection smc = serverConnection.getConnection();
|
MinecraftConnection smc = serverConnection.getConnection();
|
||||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
final boolean stateAllowsForward = smc != null
|
||||||
|
&& !smc.isClosed()
|
||||||
|
&& serverConnection.getPhase().consideredComplete()
|
||||||
|
&& smc.getState() == StateRegistry.PLAY;
|
||||||
|
if (stateAllowsForward) {
|
||||||
if (packet instanceof PluginMessagePacket) {
|
if (packet instanceof PluginMessagePacket) {
|
||||||
((PluginMessagePacket) packet).retain();
|
((PluginMessagePacket) packet).retain();
|
||||||
}
|
}
|
||||||
@@ -480,7 +552,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MinecraftConnection smc = serverConnection.getConnection();
|
MinecraftConnection smc = serverConnection.getConnection();
|
||||||
if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) {
|
final boolean stateAllowsForward = smc != null
|
||||||
|
&& !smc.isClosed()
|
||||||
|
&& serverConnection.getPhase().consideredComplete()
|
||||||
|
&& smc.getState() == StateRegistry.PLAY;
|
||||||
|
if (stateAllowsForward) {
|
||||||
smc.write(buf.retain());
|
smc.write(buf.retain());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,14 +568,24 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exception(Throwable throwable) {
|
public void exception(Throwable throwable) {
|
||||||
player.disconnect(
|
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
|
||||||
Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
|
if (MinecraftDecoder.DEBUG) {
|
||||||
|
logger.info("Exception while handling packet for {}", player, throwable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writabilityChanged() {
|
public void writabilityChanged() {
|
||||||
boolean writable = player.getConnection().getChannel().isWritable();
|
boolean writable = player.getConnection().getChannel().isWritable();
|
||||||
|
|
||||||
|
if (BACKPRESSURE_LOG) {
|
||||||
|
if (writable) {
|
||||||
|
logger.info("{} is writable, will auto-read backend connection data", player);
|
||||||
|
} else {
|
||||||
|
logger.info("{} is not writable, not auto-reading backend connection data", player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!writable) {
|
if (!writable) {
|
||||||
// We might have packets queued from the server, so flush them now to free up memory. Make
|
// We might have packets queued from the server, so flush them now to free up memory. Make
|
||||||
// sure to do it on a future invocation of the event loop, otherwise while the issue will
|
// sure to do it on a future invocation of the event loop, otherwise while the issue will
|
||||||
@@ -535,9 +621,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
// Config state clears everything in the client. No need to clear later.
|
// Config state clears everything in the client. No need to clear later.
|
||||||
spawned = false;
|
spawned = false;
|
||||||
serverBossBars.clear();
|
|
||||||
player.clearPlayerListHeaderAndFooterSilent();
|
player.clearPlayerListHeaderAndFooterSilent();
|
||||||
player.getTabList().clearAllSilent();
|
player.getTabList().clearAllSilent();
|
||||||
|
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||||
|
player.getBossBarManager().dropPackets();
|
||||||
|
} else {
|
||||||
|
serverBossBars.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
player.switchToConfigState();
|
player.switchToConfigState();
|
||||||
@@ -575,8 +665,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
|
destination.setEntityId(joinGame.getEntityId()); // used for sound api
|
||||||
// track them.
|
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||||
|
player.getBossBarManager().sendBossBars();
|
||||||
|
} else {
|
||||||
|
// Remove previous boss bars. These don't get cleared when sending JoinGame (up until 1.20.2),
|
||||||
|
// thus the need to track them.
|
||||||
for (UUID serverBossBar : serverBossBars) {
|
for (UUID serverBossBar : serverBossBars) {
|
||||||
BossBarPacket deletePacket = new BossBarPacket();
|
BossBarPacket deletePacket = new BossBarPacket();
|
||||||
deletePacket.setUuid(serverBossBar);
|
deletePacket.setUuid(serverBossBar);
|
||||||
@@ -584,6 +678,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
player.getConnection().delayedWrite(deletePacket);
|
player.getConnection().delayedWrite(deletePacket);
|
||||||
}
|
}
|
||||||
serverBossBars.clear();
|
serverBossBars.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Tell the server about the proxy's plugin message channels.
|
// Tell the server about the proxy's plugin message channels.
|
||||||
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
||||||
@@ -602,6 +697,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
while ((pm = loginPluginMessages.poll()) != null) {
|
while ((pm = loginPluginMessages.poll()) != null) {
|
||||||
serverMc.delayedWrite(pm);
|
serverMc.delayedWrite(pm);
|
||||||
}
|
}
|
||||||
|
loginPluginMessagesBytes.set(0);
|
||||||
|
loginPluginMessagesCount.set(0);
|
||||||
|
|
||||||
// Clear any title from the previous server.
|
// Clear any title from the previous server.
|
||||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
||||||
@@ -694,23 +791,35 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int startPos = -1;
|
||||||
|
for (var suggestion : suggestions.getList()) {
|
||||||
|
if (startPos == -1 || startPos > suggestion.getRange().getStart()) {
|
||||||
|
startPos = suggestion.getRange().getStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPos > 0) {
|
||||||
List<Offer> offers = new ArrayList<>();
|
List<Offer> offers = new ArrayList<>();
|
||||||
for (Suggestion suggestion : suggestions.getList()) {
|
for (Suggestion suggestion : suggestions.getList()) {
|
||||||
String offer = suggestion.getText();
|
String offer;
|
||||||
|
if (suggestion.getRange().getStart() == startPos) {
|
||||||
|
offer = suggestion.getText();
|
||||||
|
} else {
|
||||||
|
offer = command.substring(startPos, suggestion.getRange().getStart()) + suggestion.getText();
|
||||||
|
}
|
||||||
ComponentHolder tooltip = null;
|
ComponentHolder tooltip = null;
|
||||||
if (suggestion.getTooltip() != null
|
if (suggestion.getTooltip() instanceof ComponentLike componentLike) {
|
||||||
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
|
tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent());
|
||||||
tooltip = new ComponentHolder(player.getProtocolVersion(),
|
} else if (suggestion.getTooltip() != null) {
|
||||||
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
|
tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString()));
|
||||||
}
|
}
|
||||||
offers.add(new Offer(offer, tooltip));
|
offers.add(new Offer(offer, tooltip));
|
||||||
}
|
}
|
||||||
int startPos = packet.getCommand().lastIndexOf(' ') + 1;
|
|
||||||
if (startPos > 0) {
|
|
||||||
TabCompleteResponsePacket resp = new TabCompleteResponsePacket();
|
TabCompleteResponsePacket resp = new TabCompleteResponsePacket();
|
||||||
resp.setTransactionId(packet.getTransactionId());
|
resp.setTransactionId(packet.getTransactionId());
|
||||||
resp.setStart(startPos);
|
resp.setStart(startPos + 1);
|
||||||
resp.setLength(packet.getCommand().length() - startPos);
|
resp.setLength(packet.getCommand().length() - startPos - 1);
|
||||||
resp.getOffers().addAll(offers);
|
resp.getOffers().addAll(offers);
|
||||||
player.getConnection().write(resp);
|
player.getConnection().write(resp);
|
||||||
}
|
}
|
||||||
@@ -765,10 +874,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
offer = offer.substring(command.length());
|
offer = offer.substring(command.length());
|
||||||
}
|
}
|
||||||
ComponentHolder tooltip = null;
|
ComponentHolder tooltip = null;
|
||||||
if (suggestion.getTooltip() != null
|
if (suggestion.getTooltip() instanceof ComponentLike componentLike) {
|
||||||
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
|
tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent());
|
||||||
tooltip = new ComponentHolder(player.getProtocolVersion(),
|
} else if (suggestion.getTooltip() != null) {
|
||||||
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
|
tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString()));
|
||||||
}
|
}
|
||||||
response.getOffers().add(new Offer(offer, tooltip));
|
response.getOffers().add(new Offer(offer, tooltip));
|
||||||
}
|
}
|
||||||
@@ -822,6 +931,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
while ((pm = loginPluginMessages.poll()) != null) {
|
while ((pm = loginPluginMessages.poll()) != null) {
|
||||||
connection.write(pm);
|
connection.write(pm);
|
||||||
}
|
}
|
||||||
|
loginPluginMessagesBytes.set(0);
|
||||||
|
loginPluginMessagesCount.set(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-67
@@ -62,6 +62,7 @@ import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
|
|||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||||
|
import com.velocitypowered.proxy.connection.player.bossbar.BossBarManager;
|
||||||
import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
|
import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
|
||||||
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
|
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
|
||||||
@@ -73,6 +74,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
|
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
|
||||||
@@ -81,7 +84,6 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
|
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
|
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
|
||||||
@@ -108,6 +110,7 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -115,23 +118,24 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import net.kyori.adventure.audience.MessageType;
|
|
||||||
import net.kyori.adventure.bossbar.BossBar;
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
import net.kyori.adventure.identity.Identity;
|
import net.kyori.adventure.identity.Identity;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import net.kyori.adventure.permission.PermissionChecker;
|
import net.kyori.adventure.permission.PermissionChecker;
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers;
|
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers.Type;
|
|
||||||
import net.kyori.adventure.pointer.Pointers;
|
import net.kyori.adventure.pointer.Pointers;
|
||||||
import net.kyori.adventure.pointer.PointersSupplier;
|
import net.kyori.adventure.pointer.PointersSupplier;
|
||||||
import net.kyori.adventure.resource.ResourcePackInfoLike;
|
import net.kyori.adventure.resource.ResourcePackInfoLike;
|
||||||
import net.kyori.adventure.resource.ResourcePackRequest;
|
import net.kyori.adventure.resource.ResourcePackRequest;
|
||||||
import net.kyori.adventure.resource.ResourcePackRequestLike;
|
import net.kyori.adventure.resource.ResourcePackRequestLike;
|
||||||
|
import net.kyori.adventure.sound.Sound;
|
||||||
|
import net.kyori.adventure.sound.SoundStop;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
||||||
|
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
|
import net.kyori.adventure.title.Title;
|
||||||
import net.kyori.adventure.title.Title.Times;
|
import net.kyori.adventure.title.Title.Times;
|
||||||
import net.kyori.adventure.title.TitlePart;
|
import net.kyori.adventure.title.TitlePart;
|
||||||
import net.kyori.adventure.translation.GlobalTranslator;
|
import net.kyori.adventure.translation.GlobalTranslator;
|
||||||
@@ -160,7 +164,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
|
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
|
||||||
.resolving(Identity.LOCALE, Player::getEffectiveLocale)
|
.resolving(Identity.LOCALE, Player::getEffectiveLocale)
|
||||||
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
|
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
|
||||||
.resolving(FacetPointers.TYPE, player -> Type.PLAYER)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,6 +200,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
private @Nullable ClientSettingsPacket clientSettingsPacket;
|
private @Nullable ClientSettingsPacket clientSettingsPacket;
|
||||||
private volatile ChatQueue chatQueue;
|
private volatile ChatQueue chatQueue;
|
||||||
private final ChatBuilderFactory chatBuilderFactory;
|
private final ChatBuilderFactory chatBuilderFactory;
|
||||||
|
private final BossBarManager bossBarManager;
|
||||||
|
|
||||||
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
||||||
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
|
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
|
||||||
@@ -223,6 +227,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
this.chatQueue = new ChatQueue(this);
|
this.chatQueue = new ChatQueue(this);
|
||||||
this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion());
|
this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion());
|
||||||
this.resourcePackHandler = ResourcePackHandler.create(this, server);
|
this.resourcePackHandler = ResourcePackHandler.create(this, server);
|
||||||
|
this.bossBarManager = new BossBarManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -416,29 +421,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message) {
|
public void sendMessage(final @NonNull Component message) {
|
||||||
|
Preconditions.checkNotNull(message, "message");
|
||||||
final Component translated = translateMessage(message);
|
final Component translated = translateMessage(message);
|
||||||
|
|
||||||
connection.write(getChatBuilderFactory().builder()
|
connection.write(getChatBuilderFactory().builder()
|
||||||
.component(translated).forIdentity(identity).toClient());
|
.component(translated).toClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message,
|
public void sendActionBar(@NonNull Component message) {
|
||||||
@NonNull MessageType type) {
|
|
||||||
Preconditions.checkNotNull(message, "message");
|
|
||||||
Preconditions.checkNotNull(type, "type");
|
|
||||||
|
|
||||||
Component translated = translateMessage(message);
|
|
||||||
|
|
||||||
connection.write(getChatBuilderFactory().builder()
|
|
||||||
.component(translated).forIdentity(identity)
|
|
||||||
.setType(type == MessageType.CHAT ? ChatType.CHAT : ChatType.SYSTEM)
|
|
||||||
.toClient());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendActionBar(net.kyori.adventure.text.@NonNull Component message) {
|
|
||||||
Component translated = translateMessage(message);
|
Component translated = translateMessage(message);
|
||||||
|
|
||||||
ProtocolVersion playerVersion = getProtocolVersion();
|
ProtocolVersion playerVersion = getProtocolVersion();
|
||||||
@@ -495,11 +487,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showTitle(net.kyori.adventure.title.@NonNull Title title) {
|
public void showTitle(@NonNull Title title) {
|
||||||
if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
||||||
GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket(
|
GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket(
|
||||||
GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion());
|
GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion());
|
||||||
net.kyori.adventure.title.Title.Times times = title.times();
|
Title.Times times = title.times();
|
||||||
if (times != null) {
|
if (times != null) {
|
||||||
timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn()));
|
timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn()));
|
||||||
timesPkt.setStay((int) DurationUtils.toTicks(times.stay()));
|
timesPkt.setStay((int) DurationUtils.toTicks(times.stay()));
|
||||||
@@ -637,7 +629,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disconnect(Component reason) {
|
public void disconnect(@NotNull Component reason) {
|
||||||
|
Objects.requireNonNull(reason, "reason");
|
||||||
if (connection.eventLoop().inEventLoop()) {
|
if (connection.eventLoop().inEventLoop()) {
|
||||||
disconnect0(reason, false);
|
disconnect0(reason, false);
|
||||||
} else {
|
} else {
|
||||||
@@ -706,12 +699,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
Component friendlyError;
|
Component friendlyError;
|
||||||
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
|
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
|
||||||
friendlyError = Component.translatable("velocity.error.connected-server-error",
|
friendlyError = Component.translatable("velocity.error.connected-server-error",
|
||||||
Component.text(server.getServerInfo().getName()));
|
Argument.string("server", server.getServerInfo().getName()));
|
||||||
} else {
|
} else {
|
||||||
logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(),
|
logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(),
|
||||||
wrapped);
|
wrapped);
|
||||||
friendlyError = Component.translatable("velocity.error.connecting-server-error",
|
friendlyError = Component.translatable("velocity.error.connecting-server-error",
|
||||||
Component.text(server.getServerInfo().getName()));
|
Argument.string("server", server.getServerInfo().getName()));
|
||||||
}
|
}
|
||||||
handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
|
handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
|
||||||
}
|
}
|
||||||
@@ -733,18 +726,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
Component disconnectReason = disconnect.getReason().getComponent();
|
Component disconnectReason = disconnect.getReason().getComponent();
|
||||||
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
|
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
|
||||||
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
|
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
|
||||||
|
if (this.server.getConfiguration().isLogPlayerConnections()) {
|
||||||
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
||||||
plainTextReason);
|
plainTextReason);
|
||||||
|
}
|
||||||
handleConnectionException(server, disconnectReason,
|
handleConnectionException(server, disconnectReason,
|
||||||
Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED,
|
Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED,
|
||||||
Component.text(server.getServerInfo().getName()),
|
Argument.string("server", server.getServerInfo().getName()),
|
||||||
disconnectReason), safe);
|
disconnectReason), safe);
|
||||||
} else {
|
} else {
|
||||||
|
if (this.server.getConfiguration().isLogPlayerConnections()) {
|
||||||
logger.error("{}: disconnected while connecting to {}: {}", this,
|
logger.error("{}: disconnected while connecting to {}: {}", this,
|
||||||
server.getServerInfo().getName(), plainTextReason);
|
server.getServerInfo().getName(), plainTextReason);
|
||||||
|
}
|
||||||
handleConnectionException(server, disconnectReason,
|
handleConnectionException(server, disconnectReason,
|
||||||
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
|
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
|
||||||
Component.text(server.getServerInfo().getName()),
|
Argument.string("server", server.getServerInfo().getName()),
|
||||||
disconnectReason), safe);
|
disconnectReason), safe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,41 +797,34 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getResult() instanceof final DisconnectPlayer res) {
|
switch (event.getResult()) {
|
||||||
disconnect(res.getReasonComponent());
|
case DisconnectPlayer res -> disconnect(res.getReasonComponent());
|
||||||
} else if (event.getResult() instanceof final RedirectPlayer res) {
|
case RedirectPlayer res -> createConnectionRequest(res.getServer(), previousConnection).connect()
|
||||||
createConnectionRequest(res.getServer(), previousConnection).connect()
|
|
||||||
.whenCompleteAsync((status, throwable) -> {
|
.whenCompleteAsync((status, throwable) -> {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
handleConnectionException(
|
handleConnectionException(res.getServer(), throwable, true);
|
||||||
status != null ? status.getAttemptedConnection() : res.getServer(), throwable,
|
|
||||||
true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status.getStatus()) {
|
switch (status.getStatus()) {
|
||||||
// Impossible/nonsensical cases
|
// Impossible/nonsensical cases
|
||||||
case ALREADY_CONNECTED:
|
case ALREADY_CONNECTED -> logger.error("{}: already connected to {}", this,
|
||||||
logger.error("{}: already connected to {}", this,
|
|
||||||
status.getAttemptedConnection().getServerInfo().getName());
|
status.getAttemptedConnection().getServerInfo().getName());
|
||||||
break;
|
case CONNECTION_IN_PROGRESS, CONNECTION_CANCELLED -> {
|
||||||
case CONNECTION_IN_PROGRESS:
|
|
||||||
// Fatal case
|
|
||||||
case CONNECTION_CANCELLED:
|
|
||||||
Component fallbackMsg = res.getMessageComponent();
|
Component fallbackMsg = res.getMessageComponent();
|
||||||
if (fallbackMsg == null) {
|
if (fallbackMsg == null) {
|
||||||
fallbackMsg = friendlyReason;
|
fallbackMsg = friendlyReason;
|
||||||
}
|
}
|
||||||
disconnect(status.getReasonComponent().orElse(fallbackMsg));
|
disconnect(status.getReasonComponent().orElse(fallbackMsg));
|
||||||
break;
|
}
|
||||||
case SERVER_DISCONNECTED:
|
case SERVER_DISCONNECTED -> {
|
||||||
Component reason = status.getReasonComponent()
|
Component reason = status.getReasonComponent()
|
||||||
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
||||||
handleConnectionException(res.getServer(),
|
handleConnectionException(res.getServer(),
|
||||||
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
|
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
|
||||||
((Impl) status).isSafe());
|
((Impl) status).isSafe());
|
||||||
break;
|
}
|
||||||
case SUCCESS:
|
case SUCCESS -> {
|
||||||
Component requestedMessage = res.getMessageComponent();
|
Component requestedMessage = res.getMessageComponent();
|
||||||
if (requestedMessage == null) {
|
if (requestedMessage == null) {
|
||||||
requestedMessage = friendlyReason;
|
requestedMessage = friendlyReason;
|
||||||
@@ -842,21 +832,21 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
if (requestedMessage != Component.empty()) {
|
if (requestedMessage != Component.empty()) {
|
||||||
sendMessage(requestedMessage);
|
sendMessage(requestedMessage);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
default -> {
|
||||||
// The only remaining value is successful (no need to do anything!)
|
// The only remaining value is successful (no need to do anything!)
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
} else if (event.getResult() instanceof final Notify res) {
|
case Notify res -> {
|
||||||
if (event.kickedDuringServerConnect() && previousConnection != null) {
|
if (event.kickedDuringServerConnect() && previousConnection != null) {
|
||||||
sendMessage(res.getMessageComponent());
|
sendMessage(res.getMessageComponent());
|
||||||
} else {
|
} else {
|
||||||
disconnect(res.getMessageComponent());
|
disconnect(res.getMessageComponent());
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// In case someone gets creative, assume we want to disconnect the player.
|
// In case someone gets creative, assume we want to disconnect the player.
|
||||||
disconnect(friendlyReason);
|
default -> disconnect(friendlyReason);
|
||||||
}
|
}
|
||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
}
|
}
|
||||||
@@ -1038,6 +1028,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
this.clientBrand = clientBrand;
|
this.clientBrand = clientBrand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
|
||||||
|
Preconditions.checkNotNull(sound, "sound");
|
||||||
|
Preconditions.checkNotNull(emitter, "emitter");
|
||||||
|
VelocityServerConnection soundTargetServerConn = getConnectedServer();
|
||||||
|
if (getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)
|
||||||
|
|| connection.getState() != StateRegistry.PLAY
|
||||||
|
|| soundTargetServerConn == null
|
||||||
|
|| (sound.source() == Sound.Source.UI
|
||||||
|
&& getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_5))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VelocityServerConnection soundEmitterServerConn;
|
||||||
|
if (emitter == Sound.Emitter.self()) {
|
||||||
|
soundEmitterServerConn = soundTargetServerConn;
|
||||||
|
} else if (emitter instanceof ConnectedPlayer player) {
|
||||||
|
if ((soundEmitterServerConn = player.getConnectedServer()) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!soundEmitterServerConn.getServer().equals(soundTargetServerConn.getServer())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.write(new ClientboundSoundEntityPacket(sound, null, soundEmitterServerConn.getEntityId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopSound(@NotNull SoundStop stop) {
|
||||||
|
Preconditions.checkNotNull(stop, "stop");
|
||||||
|
if (getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)
|
||||||
|
|| connection.getState() != StateRegistry.PLAY
|
||||||
|
|| (stop.source() == Sound.Source.UI
|
||||||
|
&& getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_5))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.write(new ClientboundStopSoundPacket(stop));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void transferToHost(final InetSocketAddress address) {
|
public void transferToHost(final InetSocketAddress address) {
|
||||||
Preconditions.checkNotNull(address);
|
Preconditions.checkNotNull(address);
|
||||||
@@ -1300,11 +1334,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
||||||
if (sentTime != null) {
|
if (sentTime != null) {
|
||||||
final MinecraftConnection smc = serverConnection.getConnection();
|
final MinecraftConnection smc = serverConnection.getConnection();
|
||||||
if (smc != null) {
|
final StateRegistry clientState = connection.getState();
|
||||||
|
final boolean stateAllowsForward = smc != null
|
||||||
|
&& !smc.isClosed()
|
||||||
|
&& clientState == smc.getState()
|
||||||
|
&& (clientState == StateRegistry.CONFIG || clientState == StateRegistry.PLAY);
|
||||||
|
if (stateAllowsForward) {
|
||||||
setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
|
setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
|
||||||
smc.write(packet);
|
smc.write(packet);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
// We removed this, and so this is ours
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1314,7 +1354,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
* Switches the connection to the client into config state.
|
* Switches the connection to the client into config state.
|
||||||
*/
|
*/
|
||||||
public void switchToConfigState() {
|
public void switchToConfigState() {
|
||||||
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
|
final VelocityServerConnection targetServer = getConnectionInFlightOrConnectedServer();
|
||||||
|
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, targetServer))
|
||||||
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
|
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
|
||||||
// if the connection was closed earlier, there is a risk that the player is no longer connected
|
// if the connection was closed earlier, there is a risk that the player is no longer connected
|
||||||
if (!connection.getChannel().isActive()) {
|
if (!connection.getChannel().isActive()) {
|
||||||
@@ -1329,7 +1370,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
connection.pendingConfigurationSwitch = true;
|
connection.pendingConfigurationSwitch = true;
|
||||||
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
||||||
// Make sure we don't send any play packets to the player after update start
|
// Make sure we don't send any play packets to the player after update start
|
||||||
connection.addPlayPacketQueueHandler();
|
connection.addPlayPacketQueueOutboundHandler();
|
||||||
}, connection.eventLoop()).exceptionally((ex) -> {
|
}, connection.eventLoop()).exceptionally((ex) -> {
|
||||||
logger.error("Error switching player connection to config state", ex);
|
logger.error("Error switching player connection to config state", ex);
|
||||||
return null;
|
return null;
|
||||||
@@ -1379,6 +1420,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
return handshakeIntent;
|
return handshakeIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BossBarManager getBossBarManager() {
|
||||||
|
return bossBarManager;
|
||||||
|
}
|
||||||
|
|
||||||
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
||||||
|
|
||||||
private final RegisteredServer toConnect;
|
private final RegisteredServer toConnect;
|
||||||
@@ -1438,7 +1483,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
VelocityServerConnection con =
|
VelocityServerConnection con =
|
||||||
new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server);
|
new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server);
|
||||||
connectionInFlight = con;
|
connectionInFlight = con;
|
||||||
return con.connect().whenCompleteAsync((result, exception) -> this.resetIfInFlightIs(con),
|
|
||||||
|
return con.connect().whenCompleteAsync((result, exception) -> {
|
||||||
|
if (result != null && !result.isSuccessful() && !result.isSafe()) {
|
||||||
|
handleConnectionException(result.getAttemptedConnection(),
|
||||||
|
// The only way for the reason to be null is if the result is safe
|
||||||
|
DisconnectPacket.create(result.getReasonComponent().orElseThrow(),
|
||||||
|
getProtocolVersion(), connection.getState()), false);
|
||||||
|
}
|
||||||
|
this.resetIfInFlightIs(con);
|
||||||
|
},
|
||||||
connection.eventLoop());
|
connection.eventLoop());
|
||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
});
|
});
|
||||||
@@ -1452,22 +1506,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Result> connect() {
|
public CompletableFuture<Result> connect() {
|
||||||
return this.internalConnect().whenCompleteAsync((status, throwable) -> {
|
return this.internalConnect().thenApply(x -> x);
|
||||||
if (status != null && !status.isSuccessful()) {
|
|
||||||
if (!status.isSafe()) {
|
|
||||||
handleConnectionException(status.getAttemptedConnection(), throwable, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, connection.eventLoop()).thenApply(x -> x);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Boolean> connectWithIndication() {
|
public CompletableFuture<Boolean> connectWithIndication() {
|
||||||
return internalConnect().whenCompleteAsync((status, throwable) -> {
|
return internalConnect().whenCompleteAsync((status, throwable) -> {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
// TODO: The exception handling from this is not very good. Find a better way.
|
handleConnectionException(toConnect, throwable, true);
|
||||||
handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect,
|
|
||||||
throwable, true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -127,10 +127,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
|||||||
if (!handshake.getProtocolVersion().isSupported()) {
|
if (!handshake.getProtocolVersion().isSupported()) {
|
||||||
// Bump connection into correct protocol state so that we can send the disconnect packet.
|
// Bump connection into correct protocol state so that we can send the disconnect packet.
|
||||||
connection.setState(StateRegistry.LOGIN);
|
connection.setState(StateRegistry.LOGIN);
|
||||||
ic.disconnectQuietly(Component.translatable()
|
ic.disconnectQuietly(Component.translatable(
|
||||||
.key("multiplayer.disconnect.outdated_client")
|
"multiplayer.disconnect.outdated_client",
|
||||||
.arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING))
|
Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)
|
||||||
.build());
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -65,8 +65,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
|
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
|
||||||
copy);
|
|
||||||
server.getEventManager().fire(event)
|
server.getEventManager().fire(event)
|
||||||
.thenAcceptAsync(pme -> {
|
.thenAcceptAsync(pme -> {
|
||||||
if (pme.getResult().isAllowed() && serverConn.isActive()) {
|
if (pme.getResult().isAllowed() && serverConn.isActive()) {
|
||||||
|
|||||||
+13
-27
@@ -50,9 +50,9 @@ import java.net.http.HttpResponse;
|
|||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -65,6 +65,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class);
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
private static final String MOJANG_HASJOINED_URL =
|
private static final String MOJANG_HASJOINED_URL =
|
||||||
System.getProperty("mojang.sessionserver",
|
System.getProperty("mojang.sessionserver",
|
||||||
"https://sessionserver.mojang.com/session/minecraft/hasJoined")
|
"https://sessionserver.mojang.com/session/minecraft/hasJoined")
|
||||||
@@ -152,7 +153,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
} else {
|
} else {
|
||||||
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
|
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
|
||||||
new AuthSessionHandler(server, inbound,
|
new AuthSessionHandler(server, inbound,
|
||||||
GameProfile.forOfflinePlayer(login.getUsername()), false));
|
GameProfile.forOfflinePlayer(login.getUsername()), false, null));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -199,8 +200,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
|
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
|
||||||
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
|
|
||||||
|
|
||||||
|
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
||||||
|
// is enabled.
|
||||||
|
mcConnection.enableEncryption(decryptedSharedSecret);
|
||||||
|
|
||||||
|
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
|
||||||
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
|
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
|
||||||
String url = String.format(MOJANG_HASJOINED_URL,
|
String url = String.format(MOJANG_HASJOINED_URL,
|
||||||
urlFormParameterEscaper().escape(login.getUsername()), serverId);
|
urlFormParameterEscaper().escape(login.getUsername()), serverId);
|
||||||
@@ -214,6 +219,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
server.getVersion().getName() + "/" + server.getVersion().getVersion())
|
server.getVersion().getName() + "/" + server.getVersion().getVersion())
|
||||||
.uri(URI.create(url))
|
.uri(URI.create(url))
|
||||||
.build();
|
.build();
|
||||||
|
//noinspection resource
|
||||||
final HttpClient httpClient = server.createHttpClient();
|
final HttpClient httpClient = server.createHttpClient();
|
||||||
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
|
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
|
||||||
.whenCompleteAsync((response, throwable) -> {
|
.whenCompleteAsync((response, throwable) -> {
|
||||||
@@ -228,18 +234,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
|
||||||
// is enabled.
|
|
||||||
try {
|
|
||||||
mcConnection.enableEncryption(decryptedSharedSecret);
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
logger.error("Unable to enable encryption for connection", e);
|
|
||||||
// At this point, the connection is encrypted, but something's wrong on our side and
|
|
||||||
// we can't do anything about it.
|
|
||||||
mcConnection.close(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
final GameProfile profile = GENERAL_GSON.fromJson(response.body(),
|
final GameProfile profile = GENERAL_GSON.fromJson(response.body(),
|
||||||
GameProfile.class);
|
GameProfile.class);
|
||||||
@@ -254,7 +248,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
// All went well, initialize the session.
|
// All went well, initialize the session.
|
||||||
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
|
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
|
||||||
new AuthSessionHandler(server, inbound, profile, true));
|
new AuthSessionHandler(server, inbound, profile, true, serverId));
|
||||||
} else if (response.statusCode() == 204) {
|
} else if (response.statusCode() == 204) {
|
||||||
// Apparently an offline-mode user logged onto this online-mode proxy.
|
// Apparently an offline-mode user logged onto this online-mode proxy.
|
||||||
inbound.disconnect(
|
inbound.disconnect(
|
||||||
@@ -267,16 +261,8 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
|
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
|
||||||
}
|
}
|
||||||
}, mcConnection.eventLoop())
|
}, mcConnection.eventLoop())
|
||||||
.thenRun(() -> {
|
.whenComplete((ignored, throwable) -> {
|
||||||
if (httpClient instanceof final AutoCloseable closeable) {
|
httpClient.close();
|
||||||
try {
|
|
||||||
closeable.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// In Java 21, the HttpClient does not throw any Exception
|
|
||||||
// when trying to clean its resources, so this should not happen
|
|
||||||
logger.error("An unknown error occurred while trying to close an HttpClient", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
logger.error("Unable to enable encryption", e);
|
logger.error("Unable to enable encryption", e);
|
||||||
@@ -287,7 +273,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
private EncryptionRequestPacket generateEncryptionRequest() {
|
private EncryptionRequestPacket generateEncryptionRequest() {
|
||||||
byte[] verify = new byte[4];
|
byte[] verify = new byte[4];
|
||||||
ThreadLocalRandom.current().nextBytes(verify);
|
SECURE_RANDOM.nextBytes(verify);
|
||||||
|
|
||||||
EncryptionRequestPacket request = new EncryptionRequestPacket();
|
EncryptionRequestPacket request = new EncryptionRequestPacket();
|
||||||
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
|
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
|
||||||
|
|||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2023 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.connection.player.bossbar;
|
||||||
|
|
||||||
|
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
|
||||||
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles dropping and resending boss bar packets on versions 1.20.2 and newer because the client now
|
||||||
|
* deletes all boss bars during the login phase, and sending update packets would cause the client to be disconnected.
|
||||||
|
*/
|
||||||
|
public final class BossBarManager {
|
||||||
|
|
||||||
|
private final ConnectedPlayer player;
|
||||||
|
private final Set<VelocityBossBarImplementation> bossBars = new HashSet<>();
|
||||||
|
|
||||||
|
private boolean dropPackets = false;
|
||||||
|
|
||||||
|
public BossBarManager(ConnectedPlayer player) {
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the specified boss bar to be re-sent when a player changes server, and sends the update packet
|
||||||
|
* if the client is able to receive it and not be disconnected.
|
||||||
|
*/
|
||||||
|
public synchronized void writeUpdate(VelocityBossBarImplementation bar, BossBarPacket packet) {
|
||||||
|
this.bossBars.add(bar);
|
||||||
|
if (!this.dropPackets) {
|
||||||
|
this.player.getConnection().write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the specified boss bar from the player to ensure it is not re-sent.
|
||||||
|
*/
|
||||||
|
public synchronized void remove(VelocityBossBarImplementation bar, BossBarPacket packet) {
|
||||||
|
this.bossBars.remove(bar);
|
||||||
|
if (!this.dropPackets) {
|
||||||
|
this.player.getConnection().write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-creates the boss bars the player can see with any updates that may have occurred in the meantime,
|
||||||
|
* and allows update packets for those boss bars to be sent.
|
||||||
|
*/
|
||||||
|
public synchronized void sendBossBars() {
|
||||||
|
for (VelocityBossBarImplementation bossBar : bossBars) {
|
||||||
|
bossBar.createDirect(player);
|
||||||
|
}
|
||||||
|
this.dropPackets = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the player from receiving boss bar update packets while logging in to a new server.
|
||||||
|
*/
|
||||||
|
public synchronized void dropPackets() {
|
||||||
|
this.dropPackets = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modern (Minecraft 1.20.3+) ResourcePackHandler
|
* Modern (Minecraft 1.20.3+) ResourcePackHandler.
|
||||||
*/
|
*/
|
||||||
public final class ModernResourcePackHandler extends ResourcePackHandler {
|
public final class ModernResourcePackHandler extends ResourcePackHandler {
|
||||||
private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks =
|
private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks =
|
||||||
@@ -94,7 +94,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
|
|||||||
this.outstandingResourcePacks.get(info.getId());
|
this.outstandingResourcePacks.get(info.getId());
|
||||||
outstandingResourcePacks.add(info);
|
outstandingResourcePacks.add(info);
|
||||||
if (outstandingResourcePacks.size() == 1) {
|
if (outstandingResourcePacks.size() == 1) {
|
||||||
tickResourcePackQueue(outstandingResourcePacks.get(0).getId());
|
tickResourcePackQueue(outstandingResourcePacks.getFirst().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
|
|||||||
final List<ResourcePackInfo> outstandingResourcePacks =
|
final List<ResourcePackInfo> outstandingResourcePacks =
|
||||||
this.outstandingResourcePacks.get(uuid);
|
this.outstandingResourcePacks.get(uuid);
|
||||||
if (!outstandingResourcePacks.isEmpty()) {
|
if (!outstandingResourcePacks.isEmpty()) {
|
||||||
sendResourcePackRequestPacket(outstandingResourcePacks.get(0));
|
sendResourcePackRequestPacket(outstandingResourcePacks.getFirst());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
|
|||||||
this.outstandingResourcePacks.get(uuid);
|
this.outstandingResourcePacks.get(uuid);
|
||||||
final boolean peek = bundle.status().isIntermediate();
|
final boolean peek = bundle.status().isIntermediate();
|
||||||
final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null :
|
final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null :
|
||||||
peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0);
|
peek ? outstandingResourcePacks.getFirst() : outstandingResourcePacks.removeFirst();
|
||||||
|
|
||||||
server.getEventManager()
|
server.getEventManager()
|
||||||
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
|
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
|
||||||
|
|||||||
+2
-1
@@ -111,13 +111,14 @@ public abstract sealed class ResourcePackHandler
|
|||||||
}
|
}
|
||||||
request.setRequired(queued.getShouldForce());
|
request.setRequired(queued.getShouldForce());
|
||||||
request.setPrompt(queued.getPrompt() == null ? null :
|
request.setPrompt(queued.getPrompt() == null ? null :
|
||||||
new ComponentHolder(player.getProtocolVersion(), queued.getPrompt()));
|
new ComponentHolder(player.getProtocolVersion(), player.translateMessage(queued.getPrompt())));
|
||||||
|
|
||||||
player.getConnection().write(request);
|
player.getConnection().write(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes a client response to a sent resource-pack.
|
* Processes a client response to a sent resource-pack.
|
||||||
|
*
|
||||||
* <p>Cases in which no action will be taken:</p>
|
* <p>Cases in which no action will be taken:</p>
|
||||||
* <ul>
|
* <ul>
|
||||||
*
|
*
|
||||||
|
|||||||
+51
-39
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
|||||||
import com.velocitypowered.api.proxy.server.PingOptions;
|
import com.velocitypowered.api.proxy.server.PingOptions;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||||
|
import com.velocitypowered.api.util.Favicon;
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
import com.velocitypowered.api.util.ModInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.config.PingPassthroughMode;
|
import com.velocitypowered.proxy.config.PingPassthroughMode;
|
||||||
@@ -36,6 +37,7 @@ import java.util.Locale;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common utilities for handling server list ping results.
|
* Common utilities for handling server list ping results.
|
||||||
@@ -65,7 +67,7 @@ public class ServerListPingHandler {
|
|||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Collections.shuffle(unshuffledPlayers);
|
Collections.shuffle(unshuffledPlayers);
|
||||||
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount()));
|
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, unshuffledPlayers.size()));
|
||||||
} else {
|
} else {
|
||||||
samplePlayers = ImmutableList.of();
|
samplePlayers = ImmutableList.of();
|
||||||
}
|
}
|
||||||
@@ -99,58 +101,68 @@ public class ServerListPingHandler {
|
|||||||
|
|
||||||
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
|
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
|
||||||
(ex) -> fallback);
|
(ex) -> fallback);
|
||||||
switch (mode) {
|
|
||||||
case ALL:
|
// Return early if ping passthrough is not enabled
|
||||||
|
if (!mode.enabled()) {
|
||||||
|
return CompletableFuture.completedFuture(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
return pingResponses.thenApply(responses -> {
|
return pingResponses.thenApply(responses -> {
|
||||||
// Find the first non-fallback
|
// Find the first non-fallback
|
||||||
for (ServerPing response : responses) {
|
for (ServerPing response : responses) {
|
||||||
if (response == fallback) {
|
if (response == fallback) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return response;
|
|
||||||
}
|
ServerPing.Version version;
|
||||||
return fallback;
|
if (mode.version()) {
|
||||||
});
|
version = response.getVersion();
|
||||||
case MODS:
|
} else {
|
||||||
return pingResponses.thenApply(responses -> {
|
version = fallback.getVersion();
|
||||||
// Find the first non-fallback that contains a mod list
|
|
||||||
for (ServerPing response : responses) {
|
|
||||||
if (response == fallback) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Optional<ModInfo> modInfo = response.getModinfo();
|
|
||||||
if (modInfo.isPresent()) {
|
|
||||||
return fallback.asBuilder().mods(modInfo.get()).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
});
|
|
||||||
case DESCRIPTION:
|
|
||||||
return pingResponses.thenApply(responses -> {
|
|
||||||
// Find the first non-fallback. If it includes a modlist, add it too.
|
|
||||||
for (ServerPing response : responses) {
|
|
||||||
if (response == fallback) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.getDescriptionComponent() == null) {
|
ServerPing.Players players;
|
||||||
continue;
|
if (mode.players()) {
|
||||||
|
players = response.getPlayers().orElse(null);
|
||||||
|
} else {
|
||||||
|
players = fallback.getPlayers().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Component description;
|
||||||
|
if (mode.description()) {
|
||||||
|
if (response.getDescriptionComponent() != null) {
|
||||||
|
description = response.getDescriptionComponent();
|
||||||
|
} else {
|
||||||
|
description = Component.empty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
description = fallback.getDescriptionComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
Favicon favicon;
|
||||||
|
if (mode.favicon()) {
|
||||||
|
favicon = response.getFavicon().orElse(null);
|
||||||
|
} else {
|
||||||
|
favicon = fallback.getFavicon().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModInfo modinfo;
|
||||||
|
if (mode.modinfo()) {
|
||||||
|
modinfo = response.getModinfo().orElse(null);
|
||||||
|
} else {
|
||||||
|
modinfo = fallback.getModinfo().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ServerPing(
|
return new ServerPing(
|
||||||
fallback.getVersion(),
|
version,
|
||||||
fallback.getPlayers().orElse(null),
|
players,
|
||||||
response.getDescriptionComponent(),
|
description,
|
||||||
fallback.getFavicon().orElse(null),
|
favicon,
|
||||||
response.getModinfo().orElse(null)
|
modinfo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
});
|
});
|
||||||
// Not possible, but covered for completeness.
|
|
||||||
default:
|
|
||||||
return CompletableFuture.completedFuture(fallback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,7 +177,7 @@ public class ServerListPingHandler {
|
|||||||
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
||||||
PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
|
PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
|
||||||
|
|
||||||
if (passthroughMode == PingPassthroughMode.DISABLED) {
|
if (!passthroughMode.enabled()) {
|
||||||
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
|
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
|
||||||
} else {
|
} else {
|
||||||
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
|
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ import com.velocitypowered.api.permission.Tristate;
|
|||||||
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import net.kyori.adventure.audience.MessageType;
|
|
||||||
import net.kyori.adventure.identity.Identity;
|
import net.kyori.adventure.identity.Identity;
|
||||||
import net.kyori.adventure.permission.PermissionChecker;
|
import net.kyori.adventure.permission.PermissionChecker;
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers;
|
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers.Type;
|
|
||||||
import net.kyori.adventure.pointer.Pointers;
|
import net.kyori.adventure.pointer.Pointers;
|
||||||
|
import net.kyori.adventure.pointer.PointersSupplier;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
||||||
@@ -59,11 +58,10 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
|
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private PermissionFunction permissionFunction = ALWAYS_TRUE;
|
private PermissionFunction permissionFunction = ALWAYS_TRUE;
|
||||||
private final @NotNull Pointers pointers = ConsoleCommandSource.super.pointers().toBuilder()
|
private static final @NotNull PointersSupplier<VelocityConsole> POINTERS = PointersSupplier.<VelocityConsole>builder()
|
||||||
.withDynamic(PermissionChecker.POINTER, this::getPermissionChecker)
|
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
|
||||||
.withDynamic(Identity.LOCALE, () -> ClosestLocaleMatcher.INSTANCE
|
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
|
||||||
.lookupClosest(Locale.getDefault()))
|
.lookupClosest(Locale.getDefault()))
|
||||||
.withStatic(FacetPointers.TYPE, Type.CONSOLE)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public VelocityConsole(VelocityServer server) {
|
public VelocityConsole(VelocityServer server) {
|
||||||
@@ -71,8 +69,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message,
|
public void sendMessage(@NonNull Component message) {
|
||||||
@NonNull MessageType messageType) {
|
|
||||||
componentLogger.info(message);
|
componentLogger.info(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +107,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
protected LineReader buildReader(LineReaderBuilder builder) {
|
protected LineReader buildReader(LineReaderBuilder builder) {
|
||||||
return super.buildReader(builder
|
return super.buildReader(builder
|
||||||
.appName("Velocity")
|
.appName("Velocity")
|
||||||
|
.variable(LineReader.HISTORY_FILE, Path.of(".console_history"))
|
||||||
.completer((reader, parsedLine, list) -> {
|
.completer((reader, parsedLine, list) -> {
|
||||||
try {
|
try {
|
||||||
List<String> offers = this.server.getCommandManager()
|
List<String> offers = this.server.getCommandManager()
|
||||||
@@ -136,6 +134,10 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
if (!this.server.getCommandManager().executeAsync(this, command).join()) {
|
if (!this.server.getCommandManager().executeAsync(this, command).join()) {
|
||||||
sendMessage(Component.translatable("velocity.command.command-does-not-exist",
|
sendMessage(Component.translatable("velocity.command.command-does-not-exist",
|
||||||
NamedTextColor.RED));
|
NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (server.getConfiguration().isLogCommandExecutions()) {
|
||||||
|
logger.info("CONSOLE -> executed command /{}", command);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("An error occurred while running this command.", e);
|
logger.error("An error occurred while running this command.", e);
|
||||||
@@ -149,6 +151,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Pointers pointers() {
|
public @NotNull Pointers pointers() {
|
||||||
return pointers;
|
return POINTERS.view(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ final class CustomHandlerAdapter<F> {
|
|||||||
UntargetedEventHandler buildUntargetedHandler(final Method method)
|
UntargetedEventHandler buildUntargetedHandler(final Method method)
|
||||||
throws IllegalAccessException {
|
throws IllegalAccessException {
|
||||||
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
|
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
|
||||||
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn(
|
final LambdaType<F> lambdaType = functionType.defineClassesWith(methodHandlesLookup);
|
||||||
method.getDeclaringClass(), methodHandlesLookup);
|
|
||||||
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
|
|
||||||
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
|
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
|
||||||
final BiFunction<Object, Object, EventTask> handlerFunction =
|
final BiFunction<Object, Object, EventTask> handlerFunction =
|
||||||
handlerBuilder.apply(invokeFunction);
|
handlerBuilder.apply(invokeFunction);
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager {
|
|||||||
} else {
|
} else {
|
||||||
type = untargetedVoidHandlerType;
|
type = untargetedVoidHandlerType;
|
||||||
}
|
}
|
||||||
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle);
|
return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class MethodHandlerInfo {
|
static final class MethodHandlerInfo {
|
||||||
@@ -545,6 +545,36 @@ public class VelocityEventManager implements EventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private <E> void fire(final @Nullable CompletableFuture<E> future, final E event,
|
||||||
|
final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
|
||||||
|
for (int i = offset; i < registrations.length; i++) {
|
||||||
|
final HandlerRegistration registration = registrations[i];
|
||||||
|
try {
|
||||||
|
final EventTask eventTask = registration.handler.executeAsync(event);
|
||||||
|
if (eventTask == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final ContinuationTask<E> continuationTask = new ContinuationTask<>(eventTask,
|
||||||
|
registrations, future, event, i, currentlyAsync);
|
||||||
|
if (currentlyAsync || !eventTask.requiresAsync()) {
|
||||||
|
if (continuationTask.execute()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registration.plugin.getExecutorService().execute(continuationTask);
|
||||||
|
}
|
||||||
|
// fire will continue in another thread once the async task is
|
||||||
|
// executed and the continuation is resumed
|
||||||
|
return;
|
||||||
|
} catch (final Throwable t) {
|
||||||
|
logHandlerException(registration, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (future != null) {
|
||||||
|
future.complete(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final int TASK_STATE_DEFAULT = 0;
|
private static final int TASK_STATE_DEFAULT = 0;
|
||||||
private static final int TASK_STATE_EXECUTING = 1;
|
private static final int TASK_STATE_EXECUTING = 1;
|
||||||
private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2;
|
private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2;
|
||||||
@@ -669,36 +699,6 @@ public class VelocityEventManager implements EventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <E> void fire(final @Nullable CompletableFuture<E> future, final E event,
|
|
||||||
final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
|
|
||||||
for (int i = offset; i < registrations.length; i++) {
|
|
||||||
final HandlerRegistration registration = registrations[i];
|
|
||||||
try {
|
|
||||||
final EventTask eventTask = registration.handler.executeAsync(event);
|
|
||||||
if (eventTask == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final ContinuationTask<E> continuationTask = new ContinuationTask<>(eventTask,
|
|
||||||
registrations, future, event, i, currentlyAsync);
|
|
||||||
if (currentlyAsync || !eventTask.requiresAsync()) {
|
|
||||||
if (continuationTask.execute()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
registration.plugin.getExecutorService().execute(continuationTask);
|
|
||||||
}
|
|
||||||
// fire will continue in another thread once the async task is
|
|
||||||
// executed and the continuation is resumed
|
|
||||||
return;
|
|
||||||
} catch (final Throwable t) {
|
|
||||||
logHandlerException(registration, t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (future != null) {
|
|
||||||
future.complete(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void logHandlerException(
|
private static void logHandlerException(
|
||||||
final HandlerRegistration registration, final Throwable t) {
|
final HandlerRegistration registration, final Throwable t) {
|
||||||
final PluginDescription pluginDescription = registration.plugin.getDescription();
|
final PluginDescription pluginDescription = registration.plugin.getDescription();
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
|
|||||||
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
||||||
|
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
|
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
|
||||||
@@ -72,6 +74,17 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
|
|||||||
new HandshakeSessionHandler(connection, this.server));
|
new HandshakeSessionHandler(connection, this.server));
|
||||||
ch.pipeline().addLast(Connections.HANDLER, connection);
|
ch.pipeline().addLast(Connections.HANDLER, connection);
|
||||||
|
|
||||||
|
VelocityConfiguration.PacketLimiterConfig packetLimiterConfig =
|
||||||
|
server.getConfiguration().getPacketLimiterConfig();
|
||||||
|
int configuredInterval = packetLimiterConfig.interval();
|
||||||
|
int configuredPacketsPerSecond = packetLimiterConfig.pps();
|
||||||
|
int configuredBytes = packetLimiterConfig.bytes();
|
||||||
|
|
||||||
|
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
|
||||||
|
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
|
||||||
|
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
|
||||||
|
);
|
||||||
|
}
|
||||||
if (this.server.getConfiguration().isProxyProtocol()) {
|
if (this.server.getConfiguration().isProxyProtocol()) {
|
||||||
ch.pipeline().addFirst(new HAProxyMessageDecoder());
|
ch.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ public enum TransportType {
|
|||||||
return NIO;
|
return NIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) {
|
if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) {
|
||||||
return IO_URING;
|
return IO_URING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.network.limiter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PacketLimiter enforces a limit on the number of bytes processed over a time window.
|
||||||
|
* Implementations should be thread-safe.
|
||||||
|
*/
|
||||||
|
public interface PacketLimiter {
|
||||||
|
/**
|
||||||
|
* Attempts to record the specified number of bytes within the current window.
|
||||||
|
*
|
||||||
|
* @param bytes the number of bytes to record
|
||||||
|
* @return true if the bytes are allowed and recorded; false if the limit would be exceeded
|
||||||
|
*/
|
||||||
|
boolean account(int bytes);
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.network.limiter;
|
||||||
|
|
||||||
|
import com.velocitypowered.proxy.util.IntervalledCounter;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A moving-window limiter over a configurable number of seconds.
|
||||||
|
* It enforces both packets-per-second and average bytes-per-second limits.
|
||||||
|
* The effective cap over the full window equals limitPerSecond * windowSeconds.
|
||||||
|
*/
|
||||||
|
public final class SimpleBytesPerSecondLimiter implements PacketLimiter {
|
||||||
|
@Nullable
|
||||||
|
private final IntervalledCounter bytesCounter;
|
||||||
|
@Nullable
|
||||||
|
private final IntervalledCounter packetsCounter;
|
||||||
|
private final int packetsPerSecond;
|
||||||
|
private final int bytesPerSecond;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SimpleBytesPerSecondLimiter.
|
||||||
|
*
|
||||||
|
* @param packetsPerSecond maximum average packets per second allowed (> 0)
|
||||||
|
* @param bytesPerSecond maximum average bytes per second allowed (> 0)
|
||||||
|
* @param windowSeconds number of seconds in the moving window (> 0)
|
||||||
|
*/
|
||||||
|
public SimpleBytesPerSecondLimiter(int packetsPerSecond, int bytesPerSecond, int windowSeconds) {
|
||||||
|
this.packetsPerSecond = packetsPerSecond;
|
||||||
|
if (windowSeconds <= 0) {
|
||||||
|
throw new IllegalArgumentException("windowSeconds must be > 0");
|
||||||
|
}
|
||||||
|
this.bytesPerSecond = bytesPerSecond;
|
||||||
|
this.packetsCounter = packetsPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
|
||||||
|
this.bytesCounter = bytesPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the given payload length as one packet and returns whether it is allowed.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("RedundantIfStatement")
|
||||||
|
@Override
|
||||||
|
public boolean account(int bytes) {
|
||||||
|
long currTime = System.nanoTime();
|
||||||
|
if (packetsCounter != null) {
|
||||||
|
packetsCounter.updateAndAdd(1, currTime);
|
||||||
|
if (packetsCounter.getRate() > packetsPerSecond) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesCounter != null) {
|
||||||
|
bytesCounter.updateAndAdd(bytes, currTime);
|
||||||
|
if (bytesCounter.getRate() > bytesPerSecond) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-6
@@ -32,17 +32,20 @@ import java.net.InetAddress;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
|
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
|
||||||
* in a separate thread, avoiding blocking the Netty threads for an extended period of time
|
* on a small bounded pool of separate threads, avoiding blocking the Netty threads for an
|
||||||
* and without the downsides of Netty's native DNS resolver.
|
* extended period of time and without the downsides of Netty's native DNS resolver.
|
||||||
*/
|
*/
|
||||||
public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
||||||
|
|
||||||
|
private static final int MAX_RESOLVE_THREADS = 8;
|
||||||
|
|
||||||
private final ExecutorService resolveExecutor;
|
private final ExecutorService resolveExecutor;
|
||||||
private final InetNameResolver delegate;
|
private final InetNameResolver delegate;
|
||||||
private final Cache<String, List<InetAddress>> cache;
|
private final Cache<String, List<InetAddress>> cache;
|
||||||
@@ -56,11 +59,15 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
|||||||
*/
|
*/
|
||||||
public SeparatePoolInetNameResolver(EventExecutor executor) {
|
public SeparatePoolInetNameResolver(EventExecutor executor) {
|
||||||
super(executor);
|
super(executor);
|
||||||
this.resolveExecutor = Executors.newSingleThreadExecutor(
|
ThreadPoolExecutor resolveExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_RESOLVE_THREADS, MAX_RESOLVE_THREADS,
|
||||||
|
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
|
||||||
new ThreadFactoryBuilder()
|
new ThreadFactoryBuilder()
|
||||||
.setNameFormat("Velocity DNS Resolver")
|
.setNameFormat("Velocity DNS Resolver #%d")
|
||||||
.setDaemon(true)
|
.setDaemon(true)
|
||||||
.build());
|
.build());
|
||||||
|
resolveExecutor.allowCoreThreadTimeOut(true);
|
||||||
|
this.resolveExecutor = resolveExecutor;
|
||||||
this.delegate = new DefaultNameResolver(executor);
|
this.delegate = new DefaultNameResolver(executor);
|
||||||
this.cache = Caffeine.newBuilder()
|
this.cache = Caffeine.newBuilder()
|
||||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||||
@@ -71,7 +78,7 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
|||||||
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
|
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
|
||||||
List<InetAddress> addresses = cache.getIfPresent(inetHost);
|
List<InetAddress> addresses = cache.getIfPresent(inetHost);
|
||||||
if (addresses != null) {
|
if (addresses != null) {
|
||||||
promise.trySuccess(addresses.get(0));
|
promise.trySuccess(addresses.getFirst());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
|
|
||||||
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
|
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
|
||||||
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
||||||
|
private final Set<PluginContainer> plugins = new LinkedHashSet<>();
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public VelocityPluginManager(VelocityServer server) {
|
public VelocityPluginManager(VelocityServer server) {
|
||||||
@@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
* @param plugin the plugin to register
|
* @param plugin the plugin to register
|
||||||
*/
|
*/
|
||||||
public void registerPlugin(PluginContainer plugin) {
|
public void registerPlugin(PluginContainer plugin) {
|
||||||
|
plugins.add(plugin);
|
||||||
pluginsById.put(plugin.getDescription().getId(), plugin);
|
pluginsById.put(plugin.getDescription().getId(), plugin);
|
||||||
|
plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin));
|
||||||
Optional<?> instance = plugin.getInstance();
|
Optional<?> instance = plugin.getInstance();
|
||||||
instance.ifPresent(o -> pluginInstances.put(o, plugin));
|
instance.ifPresent(o -> pluginInstances.put(o, plugin));
|
||||||
}
|
}
|
||||||
@@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
try {
|
try {
|
||||||
PluginDescription candidate = loader.loadCandidate(path);
|
PluginDescription candidate = loader.loadCandidate(path);
|
||||||
|
|
||||||
// If we found a duplicate candidate (with the same ID), don't load it.
|
// A plugin claims its own ID plus every ID it provides. If any of those are already
|
||||||
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent(
|
// claimed by another candidate, don't load this one.
|
||||||
candidate.getId(), candidate);
|
List<String> claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1);
|
||||||
|
claimedIds.add(candidate.getId());
|
||||||
|
claimedIds.addAll(candidate.getProvidedIds());
|
||||||
|
|
||||||
if (maybeExistingCandidate != null) {
|
PluginDescription conflict = null;
|
||||||
logger.error("Refusing to load plugin at path {} since we already "
|
String conflictingId = null;
|
||||||
+ "loaded a plugin with the same ID {} from {}",
|
for (String id : claimedIds) {
|
||||||
|
PluginDescription existing = foundCandidates.get(id);
|
||||||
|
if (existing != null) {
|
||||||
|
conflict = existing;
|
||||||
|
conflictingId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conflict != null) {
|
||||||
|
logger.error("Refusing to load plugin at path {} since ID {} was already "
|
||||||
|
+ "claimed by a plugin loaded from {}",
|
||||||
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
|
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
|
||||||
candidate.getId(),
|
conflictingId,
|
||||||
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
conflict.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String id : claimedIds) {
|
||||||
|
foundCandidates.put(id, candidate);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.error("Unable to load plugin {}", path, e);
|
logger.error("Unable to load plugin {}", path, e);
|
||||||
@@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// foundCandidates indexes each candidate under its ID and any provided IDs, so dedupe before
|
||||||
|
// sorting to avoid loading a plugin more than once.
|
||||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
|
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
|
||||||
new ArrayList<>(foundCandidates.values()));
|
new ArrayList<>(new LinkedHashSet<>(foundCandidates.values())));
|
||||||
|
|
||||||
Map<String, PluginDescription> loadedCandidates = new HashMap<>();
|
Map<String, PluginDescription> loadedCandidates = new HashMap<>();
|
||||||
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
||||||
@@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
||||||
pluginContainers.put(container, loader.createModule(container));
|
pluginContainers.put(container, loader.createModule(container));
|
||||||
loadedCandidates.put(realPlugin.getId(), realPlugin);
|
loadedCandidates.put(realPlugin.getId(), realPlugin);
|
||||||
|
realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.error("Can't create module for plugin {}", candidate.getId(), e);
|
logger.error("Can't create module for plugin {}", candidate.getId(), e);
|
||||||
}
|
}
|
||||||
@@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<PluginContainer> getPlugins() {
|
public Collection<PluginContainer> getPlugins() {
|
||||||
return Collections.unmodifiableCollection(pluginsById.values());
|
return Collections.unmodifiableCollection(plugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+13
-1
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.velocitypowered.api.plugin.PluginDescription;
|
import com.velocitypowered.api.plugin.PluginDescription;
|
||||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||||
@@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription {
|
|||||||
private final @Nullable String url;
|
private final @Nullable String url;
|
||||||
private final List<String> authors;
|
private final List<String> authors;
|
||||||
private final Map<String, PluginDependency> dependencies;
|
private final Map<String, PluginDependency> dependencies;
|
||||||
|
private final Collection<String> providedIds;
|
||||||
private final Path source;
|
private final Path source;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription {
|
|||||||
* @param url the website for the plugin
|
* @param url the website for the plugin
|
||||||
* @param authors the authors of this plugin
|
* @param authors the authors of this plugin
|
||||||
* @param dependencies the dependencies for this plugin
|
* @param dependencies the dependencies for this plugin
|
||||||
|
* @param providedIds the IDs this plugin provides for
|
||||||
* @param source the original source for the plugin
|
* @param source the original source for the plugin
|
||||||
*/
|
*/
|
||||||
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
|
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
|
||||||
@Nullable String description, @Nullable String url,
|
@Nullable String description, @Nullable String url,
|
||||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) {
|
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||||
|
@Nullable Collection<String> providedIds, Path source) {
|
||||||
this.id = checkNotNull(id, "id");
|
this.id = checkNotNull(id, "id");
|
||||||
this.name = Strings.emptyToNull(name);
|
this.name = Strings.emptyToNull(name);
|
||||||
this.version = Strings.emptyToNull(version);
|
this.version = Strings.emptyToNull(version);
|
||||||
@@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription {
|
|||||||
this.url = Strings.emptyToNull(url);
|
this.url = Strings.emptyToNull(url);
|
||||||
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
|
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
|
||||||
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
|
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
|
||||||
|
this.providedIds =
|
||||||
|
providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds);
|
||||||
this.source = source;
|
this.source = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription {
|
|||||||
return Optional.ofNullable(dependencies.get(id));
|
return Optional.ofNullable(dependencies.get(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getProvidedIds() {
|
||||||
|
return providedIds;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Path> getSource() {
|
public Optional<Path> getSource() {
|
||||||
return Optional.ofNullable(source);
|
return Optional.ofNullable(source);
|
||||||
@@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription {
|
|||||||
+ ", url='" + url + '\''
|
+ ", url='" + url + '\''
|
||||||
+ ", authors=" + authors
|
+ ", authors=" + authors
|
||||||
+ ", dependencies=" + dependencies
|
+ ", dependencies=" + dependencies
|
||||||
|
+ ", providedIds=" + providedIds
|
||||||
+ ", source=" + source
|
+ ", source=" + source
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user