geforkt von Mirrors/Velocity
Merge branch 'master' into native-crypto
# Conflicts: # native/src/main/java/com/velocitypowered/natives/util/Natives.java
Dieser Commit ist enthalten in:
Commit
a37a0d6665
17
.gitignore
vendored
17
.gitignore
vendored
@ -11,6 +11,7 @@
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/**/compiler.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
@ -85,6 +86,15 @@ modules.xml
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Eclipse #
|
||||
**/.classpath
|
||||
**/.project
|
||||
**/.settings/
|
||||
**/bin/
|
||||
|
||||
# NetBeans Gradle#
|
||||
.nb-gradle/
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
@ -102,7 +112,7 @@ hs_err_pid*
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
/build/
|
||||
build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
@ -121,4 +131,7 @@ gradle-app.setting
|
||||
|
||||
# Other trash
|
||||
logs/
|
||||
velocity.toml
|
||||
/velocity.toml
|
||||
server-icon.png
|
||||
/bin/
|
||||
run/
|
||||
|
14
Jenkinsfile
vendored
14
Jenkinsfile
vendored
@ -1,8 +1,8 @@
|
||||
pipeline {
|
||||
agent {
|
||||
docker {
|
||||
image 'openjdk:8-jdk-slim'
|
||||
args '-v gradle-cache:/root/.gradle:rw'
|
||||
image 'velocitypowered/openjdk8-plus-git:slim'
|
||||
args '-v gradle-cache:/root/.gradle:rw -v maven-repo:/maven-repo:rw -v javadoc:/javadoc'
|
||||
}
|
||||
|
||||
}
|
||||
@ -18,5 +18,15 @@ pipeline {
|
||||
sh './gradlew test'
|
||||
}
|
||||
}
|
||||
stage('Deploy Artifacts') {
|
||||
steps {
|
||||
sh 'export MAVEN_DEPLOYMENT=true; ./gradlew publish'
|
||||
}
|
||||
}
|
||||
stage('Deploy Javadoc') {
|
||||
steps {
|
||||
sh 'rsync -av --delete ./api/build/docs/javadoc/ /javadoc'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
README.md
18
README.md
@ -1,6 +1,7 @@
|
||||
# Velocity
|
||||
|
||||
[![Build Status](https://img.shields.io/jenkins/s/https/ci.velocitypowered.com/job/velocity/job/master.svg)](https://ci.velocitypowered.com/job/velocity/job/master/)
|
||||
[![Join our Discord](https://img.shields.io/discord/472484458856185878.svg?logo=discord&label=)](https://discord.gg/8cB9Bgf)
|
||||
|
||||
Velocity is a next-generation Minecraft: Java Edition proxy suite. It is
|
||||
designed specifically with mass-scale Minecraft in mind.
|
||||
@ -22,10 +23,17 @@ wrapper script (`./gradlew`) as our CI builds using it.
|
||||
|
||||
It is sufficient to run `./gradlew build` to run the full build cycle.
|
||||
|
||||
## Running
|
||||
|
||||
Once you've built Velocity, you can copy and run the `-all` JAR from
|
||||
`proxy/build/libs`. Velocity will generate a default configuration file
|
||||
and you can configure it from there.
|
||||
|
||||
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
||||
page.
|
||||
|
||||
## Status
|
||||
|
||||
Velocity is far from finished, but most of the essential pieces are in place:
|
||||
you can switch between two servers running Minecraft 1.9-1.13. More versions
|
||||
and functionality is planned.
|
||||
|
||||
You should join us on **irc.spi.gt** `#velocity` or send us a pull request.
|
||||
Velocity is far from finished, but most of the essential pieces you would
|
||||
expect are in place. Velocity supports Minecraft 1.8-1.13. More functionality
|
||||
is planned.
|
||||
|
@ -1,13 +1,24 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'com.github.johnrengelman.shadow' version '2.0.4'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
ap {
|
||||
compileClasspath += main.compileClasspath + main.output
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.google.code.gson:gson:2.8.5'
|
||||
compile "com.google.guava:guava:${guavaVersion}"
|
||||
compile 'net.kyori:text:1.12-1.6.0-SNAPSHOT'
|
||||
compile 'net.kyori:text:1.12-1.6.4'
|
||||
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||
compile "org.slf4j:slf4j-api:${slf4jVersion}"
|
||||
compile 'com.google.inject:guice:4.2.0'
|
||||
compile 'org.checkerframework:checker-qual:2.5.4'
|
||||
|
||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
}
|
||||
@ -20,10 +31,55 @@ task javadocJar(type: Jar) {
|
||||
task sourcesJar(type: Jar) {
|
||||
classifier 'sources'
|
||||
from sourceSets.main.allSource
|
||||
from sourceSets.ap.output
|
||||
}
|
||||
|
||||
jar {
|
||||
from sourceSets.ap.output
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
from sourceSets.ap.output
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives javadocJar
|
||||
archives shadowJar
|
||||
archives sourcesJar
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.encoding = 'UTF-8'
|
||||
options.charSet = 'UTF-8'
|
||||
options.links(
|
||||
'http://www.slf4j.org/apidocs/',
|
||||
'https://google.github.io/guava/releases/25.1-jre/api/docs/',
|
||||
'https://google.github.io/guice/api-docs/4.2/javadoc/',
|
||||
'https://jd.kyori.net/text/1.12-1.6.4/',
|
||||
'https://docs.oracle.com/javase/8/docs/api/'
|
||||
)
|
||||
|
||||
// Disable the crazy super-strict doclint tool in Java 8
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = 'myRepo'
|
||||
def base = project.ext.getCurrentBranchName() == "master" ? 'file:///maven-repo' : File.createTempDir().toURI().toURL().toString()
|
||||
def releasesRepoUrl = "$base/releases"
|
||||
def snapshotsRepoUrl = "$base/snapshots"
|
||||
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.velocitypowered.api.plugin.ap;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.Name;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.FileObject;
|
||||
import javax.tools.StandardLocation;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.*;
|
||||
|
||||
@SupportedAnnotationTypes({"com.velocitypowered.api.plugin.Plugin"})
|
||||
public class PluginAnnotationProcessor extends AbstractProcessor {
|
||||
private ProcessingEnvironment environment;
|
||||
private String pluginClassFound;
|
||||
private boolean warnedAboutMultiplePlugins;
|
||||
|
||||
@Override
|
||||
public synchronized void init(ProcessingEnvironment processingEnv) {
|
||||
this.environment = processingEnv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.latestSupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
if (roundEnv.processingOver()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Element element : roundEnv.getElementsAnnotatedWith(Plugin.class)) {
|
||||
if (element.getKind() != ElementKind.CLASS) {
|
||||
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated with "
|
||||
+ Plugin.class.getCanonicalName());
|
||||
return false;
|
||||
}
|
||||
|
||||
Name qualifiedName = ((TypeElement) element).getQualifiedName();
|
||||
|
||||
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
|
||||
if (!warnedAboutMultiplePlugins) {
|
||||
environment.getMessager().printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support " +
|
||||
"multiple plugins. We are using " + pluginClassFound + " for your plugin's main class.");
|
||||
warnedAboutMultiplePlugins = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Plugin plugin = element.getAnnotation(Plugin.class);
|
||||
if (!PluginDescription.ID_PATTERN.matcher(plugin.id()).matches()) {
|
||||
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Invalid ID for plugin "
|
||||
+ qualifiedName + ". IDs must start alphabetically, have alphanumeric characters, and can " +
|
||||
"contain dashes or underscores.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// All good, generate the velocity-plugin.json.
|
||||
SerializedPluginDescription description = SerializedPluginDescription.from(plugin, qualifiedName.toString());
|
||||
try {
|
||||
FileObject object = environment.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "velocity-plugin.json");
|
||||
try (Writer writer = new BufferedWriter(object.openWriter())) {
|
||||
new Gson().toJson(description, writer);
|
||||
}
|
||||
pluginClassFound = qualifiedName.toString();
|
||||
} catch (IOException e) {
|
||||
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to generate plugin file");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package com.velocitypowered.api.plugin.ap;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SerializedPluginDescription {
|
||||
// @Nullable is used here to make GSON skip these in the serialized file
|
||||
private final String id;
|
||||
private final @Nullable String name;
|
||||
private final @Nullable String version;
|
||||
private final @Nullable String description;
|
||||
private final @Nullable String url;
|
||||
private final @Nullable List<String> authors;
|
||||
private final @Nullable List<Dependency> dependencies;
|
||||
private final String main;
|
||||
|
||||
public SerializedPluginDescription(String id, String name, String version, String description, String url,
|
||||
List<String> authors, List<Dependency> dependencies, String main) {
|
||||
this.id = Preconditions.checkNotNull(id, "id");
|
||||
this.name = Strings.emptyToNull(name);
|
||||
this.version = Strings.emptyToNull(version);
|
||||
this.description = Strings.emptyToNull(description);
|
||||
this.url = Strings.emptyToNull(url);
|
||||
this.authors = authors == null || authors.isEmpty() ? null : authors;
|
||||
this.dependencies = dependencies == null || dependencies.isEmpty() ? null : dependencies;
|
||||
this.main = Preconditions.checkNotNull(main, "main");
|
||||
}
|
||||
|
||||
public static SerializedPluginDescription from(Plugin plugin, String qualifiedName) {
|
||||
List<Dependency> dependencies = new ArrayList<>();
|
||||
for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) {
|
||||
dependencies.add(new Dependency(dependency.id(), dependency.optional()));
|
||||
}
|
||||
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), plugin.description(), plugin.url(),
|
||||
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()).collect(Collectors.toList()), dependencies, qualifiedName);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @Nullable String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public @Nullable String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public @Nullable String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public @Nullable String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public @Nullable List<String> getAuthors() {
|
||||
return authors;
|
||||
}
|
||||
|
||||
public @Nullable List<Dependency> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
public String getMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SerializedPluginDescription that = (SerializedPluginDescription) o;
|
||||
return Objects.equals(id, that.id) &&
|
||||
Objects.equals(name, that.name) &&
|
||||
Objects.equals(version, that.version) &&
|
||||
Objects.equals(description, that.description) &&
|
||||
Objects.equals(url, that.url) &&
|
||||
Objects.equals(authors, that.authors) &&
|
||||
Objects.equals(dependencies, that.dependencies) &&
|
||||
Objects.equals(main, that.main);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, name, version, description, url, authors, dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SerializedPluginDescription{" +
|
||||
"id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", url='" + url + '\'' +
|
||||
", authors=" + authors +
|
||||
", dependencies=" + dependencies +
|
||||
", main='" + main + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class Dependency {
|
||||
private final String id;
|
||||
private final boolean optional;
|
||||
|
||||
public Dependency(String id, boolean optional) {
|
||||
this.id = id;
|
||||
this.optional = optional;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isOptional() {
|
||||
return optional;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Dependency that = (Dependency) o;
|
||||
return optional == that.optional &&
|
||||
Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, optional);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Dependency{" +
|
||||
"id='" + id + '\'' +
|
||||
", optional=" + optional +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor
|
29
api/src/main/java/com/velocitypowered/api/command/Command.java
Normale Datei
29
api/src/main/java/com/velocitypowered/api/command/Command.java
Normale Datei
@ -0,0 +1,29 @@
|
||||
package com.velocitypowered.api.command;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a command that can be executed by a {@link CommandSource}, such as a {@link com.velocitypowered.api.proxy.Player}
|
||||
* or the console.
|
||||
*/
|
||||
public interface Command {
|
||||
/**
|
||||
* Executes the command for the specified {@link CommandSource}.
|
||||
* @param source the source of this command
|
||||
* @param args the arguments for this command
|
||||
*/
|
||||
void execute(@NonNull CommandSource source, @NonNull String[] args);
|
||||
|
||||
/**
|
||||
* Provides tab complete suggestions for a command for a specified {@link CommandSource}.
|
||||
* @param source the source to run the command for
|
||||
* @param currentArgs the current, partial arguments for this command
|
||||
* @return tab complete suggestions
|
||||
*/
|
||||
default List<String> suggest(@NonNull CommandSource source, @NonNull String[] currentArgs) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.velocitypowered.api.command;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Represents an interface to register a command executor with the proxy.
|
||||
*/
|
||||
public interface CommandManager {
|
||||
/**
|
||||
* Registers the specified command with the manager with the specified aliases.
|
||||
* @param command the command to register
|
||||
* @param aliases the alias to use
|
||||
*/
|
||||
void register(@NonNull Command command, String... aliases);
|
||||
|
||||
/**
|
||||
* Unregisters a command.
|
||||
* @param alias the command alias to unregister
|
||||
*/
|
||||
void unregister(@NonNull String alias);
|
||||
|
||||
/**
|
||||
* Attempts to execute a command from the specified {@code cmdLine}.
|
||||
* @param source the command's source
|
||||
* @param cmdLine the command to run
|
||||
* @return true if the command was found and executed, false if it was not
|
||||
*/
|
||||
boolean execute(@NonNull CommandSource source, @NonNull String cmdLine);
|
||||
}
|
16
api/src/main/java/com/velocitypowered/api/command/CommandSource.java
Normale Datei
16
api/src/main/java/com/velocitypowered/api/command/CommandSource.java
Normale Datei
@ -0,0 +1,16 @@
|
||||
package com.velocitypowered.api.command;
|
||||
|
||||
import com.velocitypowered.api.permission.PermissionSubject;
|
||||
import net.kyori.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Represents something that can be used to run a {@link Command}.
|
||||
*/
|
||||
public interface CommandSource extends PermissionSubject {
|
||||
/**
|
||||
* Sends the specified {@code component} to the invoker.
|
||||
* @param component the text component to send
|
||||
*/
|
||||
void sendMessage(@NonNull Component component);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides a simple command framework.
|
||||
*/
|
||||
package com.velocitypowered.api.command;
|
12
api/src/main/java/com/velocitypowered/api/event/EventHandler.java
Normale Datei
12
api/src/main/java/com/velocitypowered/api/event/EventHandler.java
Normale Datei
@ -0,0 +1,12 @@
|
||||
package com.velocitypowered.api.event;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Represents an interface to perform direct dispatch of an event. This makes integration easier to achieve with platforms
|
||||
* such as RxJava.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EventHandler<E> {
|
||||
void execute(@NonNull E event);
|
||||
}
|
75
api/src/main/java/com/velocitypowered/api/event/EventManager.java
Normale Datei
75
api/src/main/java/com/velocitypowered/api/event/EventManager.java
Normale Datei
@ -0,0 +1,75 @@
|
||||
package com.velocitypowered.api.event;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Allows plugins to register and deregister listeners for event handlers.
|
||||
*/
|
||||
public interface EventManager {
|
||||
/**
|
||||
* Requests that the specified {@code listener} listen for events and associate it with the {@code plugin}.
|
||||
* @param plugin the plugin to associate with the listener
|
||||
* @param listener the listener to register
|
||||
*/
|
||||
void register(@NonNull Object plugin, @NonNull Object listener);
|
||||
|
||||
/**
|
||||
* Requests that the specified {@code handler} listen for events and associate it with the {@code plugin}.
|
||||
* @param plugin the plugin to associate with the handler
|
||||
* @param eventClass the class for the event handler to register
|
||||
* @param handler the handler to register
|
||||
* @param <E> the event type to handle
|
||||
*/
|
||||
default <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull EventHandler<E> handler) {
|
||||
register(plugin, eventClass, PostOrder.NORMAL, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that the specified {@code handler} listen for events and associate it with the {@code plugin}.
|
||||
* @param plugin the plugin to associate with the handler
|
||||
* @param eventClass the class for the event handler to register
|
||||
* @param postOrder the order in which events should be posted to the handler
|
||||
* @param handler the handler to register
|
||||
* @param <E> the event type to handle
|
||||
*/
|
||||
<E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler);
|
||||
|
||||
/**
|
||||
* Fires the specified event to the event bus asynchronously. This allows Velocity to continue servicing connections
|
||||
* while a plugin handles a potentially long-running operation such as a database query.
|
||||
* @param event the event to fire
|
||||
* @return a {@link CompletableFuture} representing the posted event
|
||||
*/
|
||||
@NonNull <E> CompletableFuture<E> fire(@NonNull E event);
|
||||
|
||||
/**
|
||||
* Posts the specified event to the event bus and discards the result.
|
||||
* @param event the event to fire
|
||||
*/
|
||||
default void fireAndForget(@NonNull Object event) {
|
||||
fire(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all listeners for the specified {@code plugin}.
|
||||
* @param plugin the plugin to deregister listeners for
|
||||
*/
|
||||
void unregisterListeners(@NonNull Object plugin);
|
||||
|
||||
/**
|
||||
* Unregisters a specific listener for a specific plugin.
|
||||
* @param plugin the plugin associated with the listener
|
||||
* @param listener the listener to deregister
|
||||
*/
|
||||
void unregisterListener(@NonNull Object plugin, @NonNull Object listener);
|
||||
|
||||
/**
|
||||
* Unregisters a specific event handler for a specific plugin.
|
||||
* @param plugin the plugin to associate with the handler
|
||||
* @param handler the handler to register
|
||||
* @param <E> the event type to handle
|
||||
*/
|
||||
<E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler);
|
||||
}
|
11
api/src/main/java/com/velocitypowered/api/event/PostOrder.java
Normale Datei
11
api/src/main/java/com/velocitypowered/api/event/PostOrder.java
Normale Datei
@ -0,0 +1,11 @@
|
||||
package com.velocitypowered.api.event;
|
||||
|
||||
/**
|
||||
* Represents the order an event will be posted to a listener method, relative
|
||||
* to other listeners.
|
||||
*/
|
||||
public enum PostOrder {
|
||||
|
||||
FIRST, EARLY, NORMAL, LATE, LAST;
|
||||
|
||||
}
|
114
api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java
Normale Datei
114
api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java
Normale Datei
@ -0,0 +1,114 @@
|
||||
package com.velocitypowered.api.event;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Indicates an event that has a result attached to it.
|
||||
*/
|
||||
public interface ResultedEvent<R extends ResultedEvent.Result> {
|
||||
/**
|
||||
* Returns the result associated with this event.
|
||||
* @return the result of this event
|
||||
*/
|
||||
R getResult();
|
||||
|
||||
/**
|
||||
* Sets the result of this event. The result must be non-null.
|
||||
* @param result the new result
|
||||
*/
|
||||
void setResult(@NonNull R result);
|
||||
|
||||
/**
|
||||
* Represents a result for an event.
|
||||
*/
|
||||
interface Result {
|
||||
/**
|
||||
* Returns whether or not the event is allowed to proceed. Plugins may choose to skip denied events, and the
|
||||
* proxy will respect the result of this method.
|
||||
* @return whether or not the event is allowed to proceed
|
||||
*/
|
||||
boolean isAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic "allowed/denied" result.
|
||||
*/
|
||||
class GenericResult implements Result {
|
||||
private static final GenericResult ALLOWED = new GenericResult(true);
|
||||
private static final GenericResult DENIED = new GenericResult(true);
|
||||
|
||||
private final boolean allowed;
|
||||
|
||||
private GenericResult(boolean b) {
|
||||
this.allowed = b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowed() {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return allowed ? "allowed" : "denied";
|
||||
}
|
||||
|
||||
public static GenericResult allowed() {
|
||||
return ALLOWED;
|
||||
}
|
||||
|
||||
public static GenericResult denied() {
|
||||
return DENIED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an "allowed/denied" result with a reason allowed for denial.
|
||||
*/
|
||||
class ComponentResult implements Result {
|
||||
private static final ComponentResult ALLOWED = new ComponentResult(true, null);
|
||||
|
||||
private final boolean allowed;
|
||||
private final @Nullable Component reason;
|
||||
|
||||
protected ComponentResult(boolean allowed, @Nullable Component reason) {
|
||||
this.allowed = allowed;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowed() {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
public Optional<Component> getReason() {
|
||||
return Optional.ofNullable(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (allowed) {
|
||||
return "allowed";
|
||||
}
|
||||
if (reason != null) {
|
||||
return "denied: " + ComponentSerializers.PLAIN.serialize(reason);
|
||||
}
|
||||
return "denied";
|
||||
}
|
||||
|
||||
public static ComponentResult allowed() {
|
||||
return ALLOWED;
|
||||
}
|
||||
|
||||
public static ComponentResult denied(@NonNull Component reason) {
|
||||
Preconditions.checkNotNull(reason, "reason");
|
||||
return new ComponentResult(false, reason);
|
||||
}
|
||||
}
|
||||
}
|
22
api/src/main/java/com/velocitypowered/api/event/Subscribe.java
Normale Datei
22
api/src/main/java/com/velocitypowered/api/event/Subscribe.java
Normale Datei
@ -0,0 +1,22 @@
|
||||
package com.velocitypowered.api.event;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* An annotation that indicates that this method can be used to listen for an event from the proxy.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Subscribe {
|
||||
|
||||
/**
|
||||
* The order events will be posted to this listener.
|
||||
*
|
||||
* @return the order
|
||||
*/
|
||||
PostOrder order() default PostOrder.NORMAL;
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.velocitypowered.api.event.connection;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* This event is fired when a handshake is established between a client and Velocity.
|
||||
*/
|
||||
public class ConnectionHandshakeEvent {
|
||||
private final @NonNull InboundConnection connection;
|
||||
|
||||
public ConnectionHandshakeEvent(@NonNull InboundConnection connection) {
|
||||
this.connection = Preconditions.checkNotNull(connection, "connection");
|
||||
}
|
||||
|
||||
public InboundConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ConnectionHandshakeEvent{" +
|
||||
"connection=" + connection +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.velocitypowered.api.event.connection;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* This event is fired when a player disconnects from the proxy. Operations on the provided player, aside from basic
|
||||
* data retrieval operations, may behave in undefined ways.
|
||||
*/
|
||||
public class DisconnectEvent {
|
||||
private @NonNull final Player player;
|
||||
|
||||
public DisconnectEvent(@NonNull Player player) {
|
||||
this.player = Preconditions.checkNotNull(player, "player");
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DisconnectEvent{" +
|
||||
"player=" + player +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package com.velocitypowered.api.event.connection;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.event.ResultedEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* This event is fired once the player has been authenticated but before they connect to a server on the proxy.
|
||||
*/
|
||||
public class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
|
||||
private final Player player;
|
||||
private ComponentResult result;
|
||||
|
||||
public LoginEvent(@NonNull Player player) {
|
||||
this.player = Preconditions.checkNotNull(player, "player");
|
||||
this.result = ComponentResult.allowed();
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(@NonNull ComponentResult result) {
|
||||
this.result = Preconditions.checkNotNull(result, "result");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoginEvent{" +
|
||||
"player=" + player +
|
||||
", result=" + result +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
package com.velocitypowered.api.event.connection;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.event.ResultedEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
|
||||
import net.kyori.text.Component;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* This event is fired when a player has initiated a connection with the proxy but before the proxy authenticates the
|
||||
* player with Mojang or before the player's proxy connection is fully established (for offline mode).
|
||||
*/
|
||||
public class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLoginComponentResult> {
|
||||
private final InboundConnection connection;
|
||||
private final String username;
|
||||
private PreLoginComponentResult result;
|
||||
|
||||
public PreLoginEvent(InboundConnection connection, String username) {
|
||||
this.connection = Preconditions.checkNotNull(connection, "connection");
|
||||
this.username = Preconditions.checkNotNull(username, "username");
|
||||
this.result = PreLoginComponentResult.allowed();
|
||||
}
|
||||
|
||||
public InboundConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreLoginComponentResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(@NonNull PreLoginComponentResult result) {
|
||||
this.result = Preconditions.checkNotNull(result, "result");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PreLoginEvent{" +
|
||||
"connection=" + connection +
|
||||
", username='" + username + '\'' +
|
||||
", result=" + result +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an "allowed/allowed with online mode/denied" result with a reason allowed for denial.
|
||||
*/
|
||||
public static class PreLoginComponentResult extends ResultedEvent.ComponentResult {
|
||||
private static final PreLoginComponentResult ALLOWED = new PreLoginComponentResult((Component) null);
|
||||
private static final PreLoginComponentResult FORCE_ONLINEMODE = new PreLoginComponentResult(true);
|
||||
|
||||
private final boolean onlineMode;
|
||||
|
||||
/**
|
||||
* Allows online mode to be enabled for the player connection, if Velocity is running in offline mode.
|
||||
* @param allowedOnlineMode if true, online mode will be used for the connection
|
||||
*/
|
||||
private PreLoginComponentResult(boolean allowedOnlineMode) {
|
||||
super(true, null);
|
||||
this.onlineMode = allowedOnlineMode;
|
||||
}
|
||||
|
||||
private PreLoginComponentResult(@Nullable Component reason) {
|
||||
super(reason == null, reason);
|
||||
// Don't care about this
|
||||
this.onlineMode = false;
|
||||
}
|
||||
|
||||
public boolean isOnlineModeAllowed() {
|
||||
return this.onlineMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (isOnlineModeAllowed()) {
|
||||
return "allowed with online mode";
|
||||
}
|
||||
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a result indicating the connection will be allowed through the proxy.
|
||||
* @return the allowed result
|
||||
*/
|
||||
public static PreLoginComponentResult allowed() {
|
||||
return ALLOWED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a result indicating the connection will be allowed through the proxy, but the connection will be
|
||||
* forced to use online mode provided that the proxy is in offline mode. This acts similarly to {@link #allowed()}
|
||||
* on an online-mode proxy.
|
||||
* @return the result
|
||||
*/
|
||||
public static PreLoginComponentResult forceOnlineMode() {
|
||||
return FORCE_ONLINEMODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies the login with the specified reason.
|
||||
* @param reason the reason for disallowing the connection
|
||||
* @return a new result
|
||||
*/
|
||||
public static PreLoginComponentResult denied(@NonNull Component reason) {
|
||||
Preconditions.checkNotNull(reason, "reason");
|
||||
return new PreLoginComponentResult(reason);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides events for handling incoming connections to the proxy and loigns.
|
||||
*/
|
||||
package com.velocitypowered.api.event.connection;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides core support for handling events with Velocity. Subpackages include event classes.
|
||||
*/
|
||||
package com.velocitypowered.api.event;
|
@ -0,0 +1,64 @@
|
||||
package com.velocitypowered.api.event.permission;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.permission.PermissionFunction;
|
||||
import com.velocitypowered.api.permission.PermissionProvider;
|
||||
import com.velocitypowered.api.permission.PermissionSubject;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* Called when a {@link PermissionSubject}'s permissions are being setup.
|
||||
*
|
||||
* <p>This event is only called once per subject, on initialisation.</p>
|
||||
*/
|
||||
public class PermissionsSetupEvent {
|
||||
private final PermissionSubject subject;
|
||||
private final PermissionProvider defaultProvider;
|
||||
private PermissionProvider provider;
|
||||
|
||||
public PermissionsSetupEvent(PermissionSubject subject, PermissionProvider provider) {
|
||||
this.subject = Preconditions.checkNotNull(subject, "subject");
|
||||
this.provider = this.defaultProvider = Preconditions.checkNotNull(provider, "provider");
|
||||
}
|
||||
|
||||
public @NonNull PermissionSubject getSubject() {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the provider function to obtain a {@link PermissionFunction} for
|
||||
* the subject.
|
||||
*
|
||||
* @param subject the subject
|
||||
* @return the obtained permission function
|
||||
*/
|
||||
public @NonNull PermissionFunction createFunction(PermissionSubject subject) {
|
||||
return this.provider.createFunction(subject);
|
||||
}
|
||||
|
||||
public @NonNull PermissionProvider getProvider() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PermissionFunction} that should be used for the subject.
|
||||
*
|
||||
* <p>Specifying <code>null</code> will reset the provider to the default
|
||||
* instance given when the event was posted.</p>
|
||||
*
|
||||
* @param provider the provider
|
||||
*/
|
||||
public void setProvider(@Nullable PermissionProvider provider) {
|
||||
this.provider = provider == null ? this.defaultProvider : provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PermissionsSetupEvent{" +
|
||||
"subject=" + subject +
|
||||
", defaultProvider=" + defaultProvider +
|
||||
", provider=" + provider +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides events to handle setting up permissions for permission subjects.
|
||||
*/
|
||||
package com.velocitypowered.api.event.permission;
|
@ -0,0 +1,71 @@
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
|
||||
/**
|
||||
* This event is fired after the {@link com.velocitypowered.api.event.connection.PreLoginEvent} in order to set up the
|
||||
* game profile for the user. This can be used to configure a custom profile for a user, i.e. skin replacement.
|
||||
*/
|
||||
public class GameProfileRequestEvent {
|
||||
private final String username;
|
||||
private final InboundConnection connection;
|
||||
private final GameProfile originalProfile;
|
||||
private final boolean onlineMode;
|
||||
private GameProfile gameProfile;
|
||||
|
||||
public GameProfileRequestEvent(InboundConnection connection, GameProfile originalProfile, boolean onlineMode) {
|
||||
this.connection = Preconditions.checkNotNull(connection, "connection");
|
||||
this.originalProfile = Preconditions.checkNotNull(originalProfile, "originalProfile");
|
||||
this.username = originalProfile.getName();
|
||||
this.onlineMode = onlineMode;
|
||||
}
|
||||
|
||||
public InboundConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public GameProfile getOriginalProfile() {
|
||||
return originalProfile;
|
||||
}
|
||||
|
||||
public boolean isOnlineMode() {
|
||||
return onlineMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the game profile that will be used to initialize the connection with. Should no profile be currently
|
||||
* specified, the one generated by the proxy (for offline mode) or retrieved from the Mojang session servers (for
|
||||
* online mode) will be returned instead.
|
||||
* @return the user's {@link GameProfile}
|
||||
*/
|
||||
public GameProfile getGameProfile() {
|
||||
return gameProfile == null ? originalProfile : gameProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the game profile to use for this connection. It is invalid to use this method on an online-mode connection.
|
||||
* @param gameProfile the profile to use for the connection, {@code null} uses the original profile
|
||||
*/
|
||||
public void setGameProfile(@Nullable GameProfile gameProfile) {
|
||||
Preconditions.checkState(!onlineMode, "Connection is in online mode, profiles can not be faked");
|
||||
this.gameProfile = gameProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GameProfileRequestEvent{"+
|
||||
"username=" + username +
|
||||
", gameProfile=" + gameProfile +
|
||||
"}";
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
|
||||
/**
|
||||
* This event is fired once the player has successfully connected to the target server and the connection to the previous
|
||||
* server has been de-established.
|
||||
*/
|
||||
public class ServerConnectedEvent {
|
||||
private final Player player;
|
||||
private final ServerInfo server;
|
||||
|
||||
public ServerConnectedEvent(Player player, ServerInfo server) {
|
||||
this.player = Preconditions.checkNotNull(player, "player");
|
||||
this.server = Preconditions.checkNotNull(server, "server");
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public ServerInfo getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerConnectedEvent{" +
|
||||
"player=" + player +
|
||||
", server=" + server +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.event.ResultedEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This event is fired before the player connects to a server.
|
||||
*/
|
||||
public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEvent.ServerResult> {
|
||||
private final Player player;
|
||||
private ServerResult result;
|
||||
|
||||
public ServerPreConnectEvent(Player player, ServerResult result) {
|
||||
this.player = Preconditions.checkNotNull(player, "player");
|
||||
this.result = Preconditions.checkNotNull(result, "result");
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(@NonNull ServerResult result) {
|
||||
this.result = Preconditions.checkNotNull(result, "result");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerPreConnectEvent{" +
|
||||
"player=" + player +
|
||||
", result=" + result +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the result of the {@link ServerPreConnectEvent}.
|
||||
*/
|
||||
public static class ServerResult implements ResultedEvent.Result {
|
||||
private static final ServerResult DENIED = new ServerResult(false, null);
|
||||
|
||||
private final boolean allowed;
|
||||
private final ServerInfo info;
|
||||
|
||||
private ServerResult(boolean allowed, @Nullable ServerInfo info) {
|
||||
this.allowed = allowed;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowed() {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
public Optional<ServerInfo> getInfo() {
|
||||
return Optional.ofNullable(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!allowed) {
|
||||
return "denied";
|
||||
}
|
||||
return "allowed: connect to " + info.getName();
|
||||
}
|
||||
|
||||
public static ServerResult denied() {
|
||||
return DENIED;
|
||||
}
|
||||
|
||||
public static ServerResult allowed(ServerInfo server) {
|
||||
Preconditions.checkNotNull(server, "server");
|
||||
return new ServerResult(true, server);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides events for handling actions performed by players.
|
||||
*/
|
||||
package com.velocitypowered.api.event.player;
|
@ -0,0 +1,11 @@
|
||||
package com.velocitypowered.api.event.proxy;
|
||||
|
||||
/**
|
||||
* This event is fired by the proxy after plugins have been loaded but before the proxy starts accepting connections.
|
||||
*/
|
||||
public class ProxyInitializeEvent {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProxyInitializeEvent";
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.velocitypowered.api.event.proxy;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||
|
||||
/**
|
||||
* This event is fired when a server list ping request is sent by a remote client.
|
||||
*/
|
||||
public class ProxyPingEvent {
|
||||
private final InboundConnection connection;
|
||||
private ServerPing ping;
|
||||
|
||||
public ProxyPingEvent(InboundConnection connection, ServerPing ping) {
|
||||
this.connection = Preconditions.checkNotNull(connection, "connection");
|
||||
this.ping = Preconditions.checkNotNull(ping, "ping");
|
||||
}
|
||||
|
||||
public InboundConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public ServerPing getPing() {
|
||||
return ping;
|
||||
}
|
||||
|
||||
public void setPing(ServerPing ping) {
|
||||
this.ping = Preconditions.checkNotNull(ping, "ping");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProxyPingEvent{" +
|
||||
"connection=" + connection +
|
||||
", ping=" + ping +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.velocitypowered.api.event.proxy;
|
||||
|
||||
/**
|
||||
* This event is fired by the proxy after the proxy has stopped accepting connections but before the proxy process
|
||||
* exits.
|
||||
*/
|
||||
public class ProxyShutdownEvent {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProxyShutdownEvent";
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides events for handling the lifecycle of the proxy.
|
||||
*/
|
||||
package com.velocitypowered.api.event.proxy;
|
@ -1,5 +0,0 @@
|
||||
package com.velocitypowered.api;
|
||||
|
||||
/**
|
||||
* Welcome to the Velocity API documentation.
|
||||
*/
|
@ -0,0 +1,33 @@
|
||||
package com.velocitypowered.api.permission;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Function that calculates the permission settings for a given
|
||||
* {@link PermissionSubject}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PermissionFunction {
|
||||
/**
|
||||
* A permission function that always returns {@link Tristate#TRUE}.
|
||||
*/
|
||||
PermissionFunction ALWAYS_TRUE = p -> Tristate.TRUE;
|
||||
|
||||
/**
|
||||
* A permission function that always returns {@link Tristate#FALSE}.
|
||||
*/
|
||||
PermissionFunction ALWAYS_FALSE = p -> Tristate.FALSE;
|
||||
|
||||
/**
|
||||
* A permission function that always returns {@link Tristate#UNDEFINED}.
|
||||
*/
|
||||
PermissionFunction ALWAYS_UNDEFINED = p -> Tristate.UNDEFINED;
|
||||
|
||||
/**
|
||||
* Gets the subjects setting for a particular permission.
|
||||
*
|
||||
* @param permission the permission
|
||||
* @return the value the permission is set to
|
||||
*/
|
||||
@NonNull Tristate getPermissionSetting(@NonNull String permission);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.velocitypowered.api.permission;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Provides {@link PermissionFunction}s for {@link PermissionSubject}s.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PermissionProvider {
|
||||
/**
|
||||
* Creates a {@link PermissionFunction} for the subject.
|
||||
*
|
||||
* @param subject the subject
|
||||
* @return the function
|
||||
*/
|
||||
@NonNull PermissionFunction createFunction(@NonNull PermissionSubject subject);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.velocitypowered.api.permission;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* Represents a object that has a set of queryable permissions.
|
||||
*/
|
||||
public interface PermissionSubject {
|
||||
/**
|
||||
* Determines whether or not the subject has a particular permission.
|
||||
*
|
||||
* @param permission the permission to check for
|
||||
* @return whether or not the subject has the permission
|
||||
*/
|
||||
boolean hasPermission(@NonNull String permission);
|
||||
}
|
74
api/src/main/java/com/velocitypowered/api/permission/Tristate.java
Normale Datei
74
api/src/main/java/com/velocitypowered/api/permission/Tristate.java
Normale Datei
@ -0,0 +1,74 @@
|
||||
package com.velocitypowered.api.permission;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* Represents three different states of a setting.
|
||||
*
|
||||
* <p>Possible values:</p>
|
||||
* <p></p>
|
||||
* <ul>
|
||||
* <li>{@link #TRUE} - a positive setting</li>
|
||||
* <li>{@link #FALSE} - a negative (negated) setting</li>
|
||||
* <li>{@link #UNDEFINED} - a non-existent setting</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum Tristate {
|
||||
|
||||
/**
|
||||
* A value indicating a positive setting
|
||||
*/
|
||||
TRUE(true),
|
||||
|
||||
/**
|
||||
* A value indicating a negative (negated) setting
|
||||
*/
|
||||
FALSE(false),
|
||||
|
||||
/**
|
||||
* A value indicating a non-existent setting
|
||||
*/
|
||||
UNDEFINED(false);
|
||||
|
||||
/**
|
||||
* Returns a {@link Tristate} from a boolean
|
||||
*
|
||||
* @param val the boolean value
|
||||
* @return {@link #TRUE} or {@link #FALSE}, if the value is <code>true</code> or <code>false</code>, respectively.
|
||||
*/
|
||||
public static @NonNull Tristate fromBoolean(boolean val) {
|
||||
return val ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Tristate} from a nullable boolean.
|
||||
*
|
||||
* <p>Unlike {@link #fromBoolean(boolean)}, this method returns {@link #UNDEFINED}
|
||||
* if the value is null.</p>
|
||||
*
|
||||
* @param val the boolean value
|
||||
* @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value
|
||||
* is <code>null</code>, <code>true</code> or <code>false</code>, respectively.
|
||||
*/
|
||||
public static @NonNull Tristate fromNullableBoolean(@Nullable Boolean val) {
|
||||
return val == null ? UNDEFINED : val ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
private final boolean booleanValue;
|
||||
|
||||
Tristate(boolean booleanValue) {
|
||||
this.booleanValue = booleanValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the Tristate as a boolean.
|
||||
*
|
||||
* <p>A value of {@link #UNDEFINED} converts to false.</p>
|
||||
*
|
||||
* @return a boolean representation of the Tristate.
|
||||
*/
|
||||
public boolean asBoolean() {
|
||||
return this.booleanValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides the basic building blocks for a custom permission system.
|
||||
*/
|
||||
package com.velocitypowered.api.permission;
|
30
api/src/main/java/com/velocitypowered/api/plugin/Dependency.java
Normale Datei
30
api/src/main/java/com/velocitypowered/api/plugin/Dependency.java
Normale Datei
@ -0,0 +1,30 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Represents a dependency for a {@link Plugin}
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({})
|
||||
public @interface Dependency {
|
||||
/**
|
||||
* The plugin ID of the dependency.
|
||||
*
|
||||
* @return The dependency plugin ID
|
||||
* @see Plugin#id()
|
||||
*/
|
||||
String id();
|
||||
|
||||
// TODO Add required version field
|
||||
|
||||
/**
|
||||
* If this dependency is optional for the plugin to work. By default
|
||||
* this is {@code false}.
|
||||
*
|
||||
* @return true if the dependency is optional for the plugin to work
|
||||
*/
|
||||
boolean optional() default false;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
public class InvalidPluginException extends Exception {
|
||||
public InvalidPluginException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public InvalidPluginException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidPluginException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidPluginException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
66
api/src/main/java/com/velocitypowered/api/plugin/Plugin.java
Normale Datei
66
api/src/main/java/com/velocitypowered/api/plugin/Plugin.java
Normale Datei
@ -0,0 +1,66 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation used to describe a Velocity plugin.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Plugin {
|
||||
/**
|
||||
* The ID of the plugin. This ID should be unique as to
|
||||
* not conflict with other plugins.
|
||||
*
|
||||
* The plugin ID must match the {@link PluginDescription#ID_PATTERN}.
|
||||
*
|
||||
* @return the ID for this plugin
|
||||
*/
|
||||
String id();
|
||||
|
||||
/**
|
||||
* The human readable name of the plugin as to be used in descriptions and
|
||||
* similar things.
|
||||
*
|
||||
* @return The plugin name, or an empty string if unknown
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* The version of the plugin.
|
||||
*
|
||||
* @return the version of the plugin, or an empty string if unknown
|
||||
*/
|
||||
String version() default "";
|
||||
|
||||
/**
|
||||
* The description of the plugin, explaining what it can be used for.
|
||||
*
|
||||
* @return The plugin description, or an empty string if unknown
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
/**
|
||||
* The URL or website of the plugin.
|
||||
*
|
||||
* @return The plugin url, or an empty string if unknown
|
||||
*/
|
||||
String url() default "";
|
||||
|
||||
/**
|
||||
* The author of the plugin.
|
||||
*
|
||||
* @return the plugin's author, or empty if unknown
|
||||
*/
|
||||
String[] authors() default "";
|
||||
|
||||
/**
|
||||
* The dependencies required to load before this plugin.
|
||||
*
|
||||
* @return the plugin dependencies
|
||||
*/
|
||||
Dependency[] dependencies() default {};
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A wrapper around a plugin loaded by the proxy.
|
||||
*/
|
||||
public interface PluginContainer {
|
||||
/**
|
||||
* Returns the plugin's description.
|
||||
*
|
||||
* @return the plugin's description
|
||||
*/
|
||||
@NonNull PluginDescription getDescription();
|
||||
|
||||
/**
|
||||
* Returns the created plugin if it is available.
|
||||
*
|
||||
* @return the instance if available
|
||||
*/
|
||||
default Optional<?> getInstance() {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
106
api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java
Normale Datei
106
api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java
Normale Datei
@ -0,0 +1,106 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Represents metadata for a specific version of a plugin.
|
||||
*/
|
||||
public interface PluginDescription {
|
||||
/**
|
||||
* The pattern plugin IDs must match. Plugin IDs may only contain
|
||||
* alphanumeric characters, dashes or underscores, must start with
|
||||
* an alphabetic character and cannot be longer than 64 characters.
|
||||
*/
|
||||
Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");
|
||||
|
||||
/**
|
||||
* Gets the qualified ID of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return the plugin ID
|
||||
* @see Plugin#id()
|
||||
*/
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Gets the name of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return an {@link Optional} with the plugin name, may be empty
|
||||
* @see Plugin#name()
|
||||
*/
|
||||
default Optional<String> getName() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return an {@link Optional} with the plugin version, may be empty
|
||||
* @see Plugin#version()
|
||||
*/
|
||||
default Optional<String> getVersion() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the description of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return an {@link Optional} with the plugin description, may be empty
|
||||
* @see Plugin#description()
|
||||
*/
|
||||
default Optional<String> getDescription() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the url or website of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return an {@link Optional} with the plugin url, may be empty
|
||||
* @see Plugin#url()
|
||||
*/
|
||||
default Optional<String> getUrl() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authors of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return the plugin authors, may be empty
|
||||
* @see Plugin#authors()
|
||||
*/
|
||||
default List<String> getAuthors() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link Collection} of all dependencies of the {@link Plugin} within
|
||||
* this container.
|
||||
*
|
||||
* @return the plugin dependencies, can be empty
|
||||
* @see Plugin#dependencies()
|
||||
*/
|
||||
default Collection<PluginDependency> getDependencies() {
|
||||
return ImmutableSet.of();
|
||||
}
|
||||
|
||||
default Optional<PluginDependency> getDependency(String id) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source the plugin was loaded from.
|
||||
*
|
||||
* @return the source the plugin was loaded from or {@link Optional#empty()}
|
||||
* if unknown
|
||||
*/
|
||||
default Optional<Path> getSource() {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
55
api/src/main/java/com/velocitypowered/api/plugin/PluginManager.java
Normale Datei
55
api/src/main/java/com/velocitypowered/api/plugin/PluginManager.java
Normale Datei
@ -0,0 +1,55 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* The class that manages plugins. This manager can retrieve
|
||||
* {@link PluginContainer}s from {@link Plugin} instances, getting
|
||||
* {@link Logger}s, etc.
|
||||
*/
|
||||
public interface PluginManager {
|
||||
/**
|
||||
* Gets the plugin container from an instance.
|
||||
*
|
||||
* @param instance the instance
|
||||
* @return the container
|
||||
*/
|
||||
@NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance);
|
||||
|
||||
/**
|
||||
* Retrieves a {@link PluginContainer} based on its ID.
|
||||
*
|
||||
* @param id the plugin ID
|
||||
* @return the plugin, if available
|
||||
*/
|
||||
@NonNull Optional<PluginContainer> getPlugin(@NonNull String id);
|
||||
|
||||
/**
|
||||
* Gets a {@link Collection} of all {@link PluginContainer}s.
|
||||
*
|
||||
* @return the plugins
|
||||
*/
|
||||
@NonNull Collection<PluginContainer> getPlugins();
|
||||
|
||||
/**
|
||||
* Checks if a plugin is loaded based on its ID.
|
||||
*
|
||||
* @param id the id of the {@link Plugin}
|
||||
* @return {@code true} if loaded
|
||||
*/
|
||||
boolean isLoaded(@NonNull String id);
|
||||
|
||||
/**
|
||||
* Adds the specified {@code path} to the plugin classpath.
|
||||
*
|
||||
* @param plugin the plugin
|
||||
* @param path the path to the JAR you want to inject into the classpath
|
||||
* @throws UnsupportedOperationException if the operation is not applicable to this plugin
|
||||
*/
|
||||
void addToClasspath(@NonNull Object plugin, @NonNull Path path);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.velocitypowered.api.plugin.annotation;
|
||||
|
||||
import com.google.inject.BindingAnnotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* This annotation requests that Velocity inject a {@link java.nio.file.Path} instance with a plugin-specific data
|
||||
* directory.
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@BindingAnnotation
|
||||
public @interface DataDirectory {
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides annotations to handle injecting dependencies for plugins.
|
||||
*/
|
||||
package com.velocitypowered.api.plugin.annotation;
|
@ -0,0 +1,79 @@
|
||||
package com.velocitypowered.api.plugin.meta;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
|
||||
/**
|
||||
* Represents a dependency on another plugin.
|
||||
*/
|
||||
public final class PluginDependency {
|
||||
private final String id;
|
||||
@Nullable private final String version;
|
||||
|
||||
private final boolean optional;
|
||||
|
||||
public PluginDependency(String id, @Nullable String version, boolean optional) {
|
||||
this.id = checkNotNull(id, "id");
|
||||
checkArgument(!id.isEmpty(), "id cannot be empty");
|
||||
this.version = emptyToNull(version);
|
||||
this.optional = optional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin ID of this {@link PluginDependency}
|
||||
*
|
||||
* @return the plugin ID
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version this {@link PluginDependency} should match.
|
||||
*
|
||||
* @return an {@link Optional} with the plugin version, may be empty
|
||||
*/
|
||||
public Optional<String> getVersion() {
|
||||
return Optional.ofNullable(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the dependency is optional for the plugin to work
|
||||
* correctly.
|
||||
*
|
||||
* @return true if dependency is optional
|
||||
*/
|
||||
public boolean isOptional() {
|
||||
return optional;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PluginDependency that = (PluginDependency) o;
|
||||
return optional == that.optional &&
|
||||
Objects.equals(id, that.id) &&
|
||||
Objects.equals(version, that.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, version, optional);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PluginDependency{" +
|
||||
"id='" + id + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", optional=" + optional +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides metadata for plugins.
|
||||
*/
|
||||
package com.velocitypowered.api.plugin.meta;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides the Velocity plugin API.
|
||||
*/
|
||||
package com.velocitypowered.api.plugin;
|
@ -1,28 +1,29 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import net.kyori.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Represents a connection request. A connection request is created using {@link Player#createConnectionRequest(ServerInfo)}
|
||||
* and is used to allow a plugin to compose and request a connection to another Minecraft server using a fluent API.
|
||||
* Provides a fluent interface to compose and send a connection request to another server behind the proxy. A connection
|
||||
* request is created using {@link Player#createConnectionRequest(ServerInfo)}.
|
||||
*/
|
||||
public interface ConnectionRequestBuilder {
|
||||
/**
|
||||
* Returns the server that this connection request represents.
|
||||
* @return the server this request will connect to
|
||||
*/
|
||||
ServerInfo getServer();
|
||||
@NonNull ServerInfo getServer();
|
||||
|
||||
/**
|
||||
* Initiates the connection to the remote server and emits a result on the {@link CompletableFuture} after the user
|
||||
* has logged on. No messages will be communicated to the client: the user is responsible for all error handling.
|
||||
* @return a {@link CompletableFuture} representing the status of this connection
|
||||
*/
|
||||
CompletableFuture<Result> connect();
|
||||
@NonNull CompletableFuture<Result> connect();
|
||||
|
||||
/**
|
||||
* Initiates the connection to the remote server without waiting for a result. Velocity will use generic error
|
||||
@ -49,7 +50,7 @@ public interface ConnectionRequestBuilder {
|
||||
Status getStatus();
|
||||
|
||||
/**
|
||||
* Returns a reason for the failure to connect to the server. None may be provided.
|
||||
* Returns an (optional) textual reason for the failure to connect to the server.
|
||||
* @return the reason why the user could not connect to the server
|
||||
*/
|
||||
Optional<Component> getReason();
|
||||
|
@ -0,0 +1,33 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents an incoming connection to the proxy.
|
||||
*/
|
||||
public interface InboundConnection {
|
||||
/**
|
||||
* Returns the player's IP address.
|
||||
* @return the player's IP
|
||||
*/
|
||||
InetSocketAddress getRemoteAddress();
|
||||
|
||||
/**
|
||||
* Returns the hostname that the user entered into the client, if applicable.
|
||||
* @return the hostname from the client
|
||||
*/
|
||||
Optional<InetSocketAddress> getVirtualHost();
|
||||
|
||||
/**
|
||||
* Determine whether or not the player remains online.
|
||||
* @return whether or not the player active
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
/**
|
||||
* Returns the current protocol version this connection uses.
|
||||
* @return the protocol version the connection uses
|
||||
*/
|
||||
int getProtocolVersion();
|
||||
}
|
@ -1,18 +1,20 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.api.util.MessagePosition;
|
||||
import net.kyori.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a player who is connected to the proxy.
|
||||
*/
|
||||
public interface Player {
|
||||
public interface Player extends CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink {
|
||||
/**
|
||||
* Returns the player's current username.
|
||||
* @return the username
|
||||
@ -29,25 +31,13 @@ public interface Player {
|
||||
* Returns the server that the player is currently connected to.
|
||||
* @return an {@link Optional} the server that the player is connected to, which may be empty
|
||||
*/
|
||||
Optional<ServerInfo> getCurrentServer();
|
||||
|
||||
/**
|
||||
* Returns the player's IP address.
|
||||
* @return the player's IP
|
||||
*/
|
||||
InetSocketAddress getRemoteAddress();
|
||||
|
||||
/**
|
||||
* Determine whether or not the player remains online.
|
||||
* @return whether or not the player active
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
Optional<ServerConnection> getCurrentServer();
|
||||
|
||||
/**
|
||||
* Sends a chat message to the player's client.
|
||||
* @param component the chat message to send
|
||||
*/
|
||||
default void sendMessage(@Nonnull Component component) {
|
||||
default void sendMessage(@NonNull Component component) {
|
||||
sendMessage(component, MessagePosition.CHAT);
|
||||
}
|
||||
|
||||
@ -56,12 +46,31 @@ public interface Player {
|
||||
* @param component the chat message to send
|
||||
* @param position the position for the message
|
||||
*/
|
||||
void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position);
|
||||
void sendMessage(@NonNull Component component, @NonNull MessagePosition position);
|
||||
|
||||
/**
|
||||
* Creates a new connection request so that the player can connect to another server.
|
||||
* @param info the server to connect to
|
||||
* @return a new connection request
|
||||
*/
|
||||
ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info);
|
||||
ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info);
|
||||
|
||||
/**
|
||||
* Sets the tab list header and footer for the player.
|
||||
* @param header the header component
|
||||
* @param footer the footer component
|
||||
*/
|
||||
void setHeaderAndFooter(Component header, Component footer);
|
||||
|
||||
/**
|
||||
* Clears the tab list header and footer for the player.
|
||||
*/
|
||||
void clearHeaderAndFooter();
|
||||
|
||||
/**
|
||||
* Disconnects the player with the specified reason. Once this method is called, further calls to other {@link Player}
|
||||
* methods will become undefined.
|
||||
* @param reason component with the reason
|
||||
*/
|
||||
void disconnect(Component reason);
|
||||
}
|
||||
|
110
api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java
Normale Datei
110
api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java
Normale Datei
@ -0,0 +1,110 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelRegistrar;
|
||||
import com.velocitypowered.api.scheduler.Scheduler;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Provides an interface to a Minecraft server proxy.
|
||||
*/
|
||||
public interface ProxyServer {
|
||||
/**
|
||||
* Retrieves the player currently connected to this proxy by their Minecraft username. The search is case-insensitive.
|
||||
* @param username the username to search for
|
||||
* @return an {@link Optional} with the player, which may be empty
|
||||
*/
|
||||
Optional<Player> getPlayer(String username);
|
||||
|
||||
/**
|
||||
* Retrieves the player currently connected to this proxy by their Minecraft UUID.
|
||||
* @param uuid the UUID
|
||||
* @return an {@link Optional} with the player, which may be empty
|
||||
*/
|
||||
Optional<Player> getPlayer(UUID uuid);
|
||||
|
||||
/**
|
||||
* Retrieves all players currently connected to this proxy. This call may or may not be a snapshot of all players
|
||||
* online.
|
||||
* @return the players online on this proxy
|
||||
*/
|
||||
Collection<Player> getAllPlayers();
|
||||
|
||||
/**
|
||||
* Returns the number of players currently connected to this proxy.
|
||||
* @return the players on this proxy
|
||||
*/
|
||||
int getPlayerCount();
|
||||
|
||||
/**
|
||||
* Retrieves a registered {@link ServerInfo} instance by its name. The search is case-insensitive.
|
||||
* @param name the name of the server
|
||||
* @return the registered server, which may be empty
|
||||
*/
|
||||
Optional<ServerInfo> getServerInfo(String name);
|
||||
|
||||
/**
|
||||
* Retrieves all {@link ServerInfo}s registered with this proxy.
|
||||
* @return the servers registered with this proxy
|
||||
*/
|
||||
Collection<ServerInfo> getAllServers();
|
||||
|
||||
/**
|
||||
* Registers a server with this proxy. A server with this name should not already exist.
|
||||
* @param server the server to register
|
||||
*/
|
||||
void registerServer(ServerInfo server);
|
||||
|
||||
/**
|
||||
* Unregisters this server from the proxy.
|
||||
* @param server the server to unregister
|
||||
*/
|
||||
void unregisterServer(ServerInfo server);
|
||||
|
||||
/**
|
||||
* Returns an instance of {@link CommandSource} that can be used to determine if the command is being invoked by
|
||||
* the console or a console-like executor. Plugins that execute commands are strongly urged to implement their own
|
||||
* {@link CommandSource} instead of using the console invoker.
|
||||
* @return the console command invoker
|
||||
*/
|
||||
CommandSource getConsoleCommandSource();
|
||||
|
||||
/**
|
||||
* Gets the {@link PluginManager} instance.
|
||||
*
|
||||
* @return the plugin manager instance
|
||||
*/
|
||||
PluginManager getPluginManager();
|
||||
|
||||
/**
|
||||
* Gets the {@link EventManager} instance.
|
||||
*
|
||||
* @return the event manager instance
|
||||
*/
|
||||
EventManager getEventManager();
|
||||
|
||||
/**
|
||||
* Gets the {@link CommandManager} instance.
|
||||
* @return the command manager
|
||||
*/
|
||||
CommandManager getCommandManager();
|
||||
|
||||
/**
|
||||
* Gets the {@link Scheduler} instance.
|
||||
* @return the scheduler instance
|
||||
*/
|
||||
Scheduler getScheduler();
|
||||
|
||||
/**
|
||||
* Gets the {@link ChannelRegistrar} instance.
|
||||
* @return the channel registrar
|
||||
*/
|
||||
ChannelRegistrar getChannelRegistrar();
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
|
||||
/**
|
||||
* Represents a connection to a backend server from the proxy for a client.
|
||||
*/
|
||||
public interface ServerConnection extends ChannelMessageSource, ChannelMessageSink {
|
||||
/**
|
||||
* Returns the server that this connection is connected to.
|
||||
* @return the server this connection is connected to
|
||||
*/
|
||||
ServerInfo getServerInfo();
|
||||
|
||||
/**
|
||||
* Returns the player that this connection is associated with.
|
||||
* @return the player for this connection
|
||||
*/
|
||||
Player getPlayer();
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
/**
|
||||
* Represents a kind of channel identifier.
|
||||
*/
|
||||
public interface ChannelIdentifier {
|
||||
/**
|
||||
* Returns the textual representation of this identifier.
|
||||
* @return the textual representation of the identifier
|
||||
*/
|
||||
String getId();
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
/**
|
||||
* Represents something that can send plugin messages.
|
||||
*/
|
||||
public interface ChannelMessageSink {
|
||||
/**
|
||||
* Sends a plugin message to this target.
|
||||
* @param identifier the channel identifier to send the message on
|
||||
* @param data the data to send
|
||||
*/
|
||||
void sendPluginMessage(ChannelIdentifier identifier, byte[] data);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
/**
|
||||
* A marker interface that indicates a source of plugin messages.
|
||||
*/
|
||||
public interface ChannelMessageSource {
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
/**
|
||||
* Represents an interface to register and unregister {@link MessageHandler} instances for handling plugin messages from
|
||||
* the client or the server.
|
||||
*/
|
||||
public interface ChannelRegistrar {
|
||||
/**
|
||||
* Registers the specified message handler to listen for plugin messages on the specified channels.
|
||||
* @param handler the handler to register
|
||||
* @param identifiers the channel identifiers to register
|
||||
*/
|
||||
void register(MessageHandler handler, ChannelIdentifier... identifiers);
|
||||
|
||||
/**
|
||||
* Unregisters the handler for the specified channel.
|
||||
* @param identifiers the identifiers to unregister
|
||||
*/
|
||||
void unregister(ChannelIdentifier... identifiers);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
/**
|
||||
* Represents from "which side" of the proxy the plugin message came from.
|
||||
*/
|
||||
public enum ChannelSide {
|
||||
/**
|
||||
* The plugin message came from a server that a client was connected to.
|
||||
*/
|
||||
FROM_SERVER,
|
||||
/**
|
||||
* The plugin message came from the client.
|
||||
*/
|
||||
FROM_CLIENT
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Reperesents a legacy channel identifier (for Minecraft 1.12 and below). For modern 1.13 plugin messages, please see
|
||||
* {@link MinecraftChannelIdentifier}. This class is immutable and safe for multi-threaded use.
|
||||
*/
|
||||
public final class LegacyChannelIdentifier implements ChannelIdentifier {
|
||||
private final String name;
|
||||
|
||||
public LegacyChannelIdentifier(String name) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "provided name is empty");
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LegacyChannelIdentifier{" +
|
||||
"name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LegacyChannelIdentifier that = (LegacyChannelIdentifier) o;
|
||||
return Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
/**
|
||||
* Represents a handler for handling plugin messages.
|
||||
*/
|
||||
public interface MessageHandler {
|
||||
/**
|
||||
* Handles an incoming plugin message.
|
||||
* @param source the source of the plugin message
|
||||
* @param side from where the plugin message originated
|
||||
* @param identifier the channel on which the message was sent
|
||||
* @param data the data inside the plugin message
|
||||
* @return a {@link ForwardStatus} indicating whether or not to forward this plugin message on
|
||||
*/
|
||||
ForwardStatus handle(ChannelMessageSource source, ChannelSide side, ChannelIdentifier identifier, byte[] data);
|
||||
|
||||
enum ForwardStatus {
|
||||
/**
|
||||
* Forwards this plugin message on to the client or server, depending on the {@link ChannelSide} it originated
|
||||
* from.
|
||||
*/
|
||||
FORWARD,
|
||||
/**
|
||||
* Discard the plugin message and do not forward it on.
|
||||
*/
|
||||
HANDLED
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.velocitypowered.api.proxy.messages;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Represents a Minecraft 1.13+ channel identifier. This class is immutable and safe for multi-threaded use.
|
||||
*/
|
||||
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
||||
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]+");
|
||||
|
||||
private final String namespace;
|
||||
private final String name;
|
||||
|
||||
private MinecraftChannelIdentifier(String namespace, String name) {
|
||||
this.namespace = namespace;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identifier in the default namespace ({@code minecraft}). Plugins are strongly encouraged to provide
|
||||
* their own namespace.
|
||||
* @param name the name in the default namespace to use
|
||||
* @return a new channel identifier
|
||||
*/
|
||||
public static MinecraftChannelIdentifier forDefaultNamespace(String name) {
|
||||
return new MinecraftChannelIdentifier("minecraft", name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identifier in the specified namespace.
|
||||
* @param namespace the namespace to use
|
||||
* @param name the channel name inside the specified namespace
|
||||
* @return a new channel identifier
|
||||
*/
|
||||
public static MinecraftChannelIdentifier create(String namespace, String name) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "namespace is null or empty");
|
||||
Preconditions.checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(), "namespace is not valid");
|
||||
Preconditions.checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(), "name is not valid");
|
||||
return new MinecraftChannelIdentifier(namespace, name);
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MinecraftChannelIdentifier{" +
|
||||
"namespace='" + namespace + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
MinecraftChannelIdentifier that = (MinecraftChannelIdentifier) o;
|
||||
return Objects.equals(namespace, that.namespace) &&
|
||||
Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(namespace, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return namespace + ":" + name;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides an interface to receive, handle, and send plugin messages on the proxy from clients and servers.
|
||||
*/
|
||||
package com.velocitypowered.api.proxy.messages;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides an interface to interact with the proxy at a low level.
|
||||
*/
|
||||
package com.velocitypowered.api.proxy;
|
@ -1,6 +1,7 @@
|
||||
package com.velocitypowered.api.server;
|
||||
package com.velocitypowered.api.proxy.server;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Objects;
|
||||
@ -9,24 +10,24 @@ import java.util.Objects;
|
||||
* ServerInfo represents a server that a player can connect to. This object is immutable and safe for concurrent access.
|
||||
*/
|
||||
public final class ServerInfo {
|
||||
private final String name;
|
||||
private final InetSocketAddress address;
|
||||
private final @NonNull String name;
|
||||
private final @NonNull InetSocketAddress address;
|
||||
|
||||
/**
|
||||
* Creates a new ServerInfo object.
|
||||
* @param name the name for the server
|
||||
* @param address the address of the server to connect to
|
||||
*/
|
||||
public ServerInfo(String name, InetSocketAddress address) {
|
||||
public ServerInfo(@NonNull String name, @NonNull InetSocketAddress address) {
|
||||
this.name = Preconditions.checkNotNull(name, "name");
|
||||
this.address = Preconditions.checkNotNull(address, "address");
|
||||
}
|
||||
|
||||
public final String getName() {
|
||||
public final @NonNull String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public final InetSocketAddress getAddress() {
|
||||
public final @NonNull InetSocketAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
244
api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java
Normale Datei
244
api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java
Normale Datei
@ -0,0 +1,244 @@
|
||||
package com.velocitypowered.api.proxy.server;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.util.Favicon;
|
||||
import net.kyori.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Represents a 1.7 and above server list ping response. This class is immutable.
|
||||
*/
|
||||
public class ServerPing {
|
||||
private final Version version;
|
||||
private final Players players;
|
||||
private final Component description;
|
||||
private final @Nullable Favicon favicon;
|
||||
|
||||
public ServerPing(@NonNull Version version, @NonNull Players players, @NonNull Component description, @Nullable Favicon favicon) {
|
||||
this.version = Preconditions.checkNotNull(version, "version");
|
||||
this.players = Preconditions.checkNotNull(players, "players");
|
||||
this.description = Preconditions.checkNotNull(description, "description");
|
||||
this.favicon = favicon;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Players getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
||||
public Component getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Optional<Favicon> getFavicon() {
|
||||
return Optional.ofNullable(favicon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerPing{" +
|
||||
"version=" + version +
|
||||
", players=" + players +
|
||||
", description=" + description +
|
||||
", favicon='" + favicon + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public Builder asBuilder() {
|
||||
Builder builder = new Builder();
|
||||
builder.version = version;
|
||||
builder.onlinePlayers = players.online;
|
||||
builder.maximumPlayers = players.max;
|
||||
builder.samplePlayers.addAll(players.sample);
|
||||
builder.description = description;
|
||||
builder.favicon = favicon;
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link ServerPing} objects.
|
||||
*/
|
||||
public static class Builder {
|
||||
private Version version;
|
||||
private int onlinePlayers;
|
||||
private int maximumPlayers;
|
||||
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
|
||||
private Component description;
|
||||
private Favicon favicon;
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
public Builder version(Version version) {
|
||||
this.version = Preconditions.checkNotNull(version, "version");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onlinePlayers(int onlinePlayers) {
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximumPlayers(int maximumPlayers) {
|
||||
this.maximumPlayers = maximumPlayers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder samplePlayers(SamplePlayer... players) {
|
||||
this.samplePlayers.addAll(Arrays.asList(players));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clearSamplePlayers() {
|
||||
this.samplePlayers.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder description(Component description) {
|
||||
this.description = Preconditions.checkNotNull(description, "description");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder favicon(Favicon favicon) {
|
||||
this.favicon = Preconditions.checkNotNull(favicon, "favicon");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ServerPing build() {
|
||||
return new ServerPing(version, new Players(onlinePlayers, maximumPlayers, samplePlayers), description, favicon);
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public int getOnlinePlayers() {
|
||||
return onlinePlayers;
|
||||
}
|
||||
|
||||
public int getMaximumPlayers() {
|
||||
return maximumPlayers;
|
||||
}
|
||||
|
||||
public List<SamplePlayer> getSamplePlayers() {
|
||||
return samplePlayers;
|
||||
}
|
||||
|
||||
public Component getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Favicon getFavicon() {
|
||||
return favicon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Builder{" +
|
||||
"version=" + version +
|
||||
", onlinePlayers=" + onlinePlayers +
|
||||
", maximumPlayers=" + maximumPlayers +
|
||||
", samplePlayers=" + samplePlayers +
|
||||
", description=" + description +
|
||||
", favicon=" + favicon +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class Version {
|
||||
private final int protocol;
|
||||
private final String name;
|
||||
|
||||
public Version(int protocol, String name) {
|
||||
this.protocol = protocol;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Version{" +
|
||||
"protocol=" + protocol +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class Players {
|
||||
private final int online;
|
||||
private final int max;
|
||||
private final List<SamplePlayer> sample;
|
||||
|
||||
public Players(int online, int max, List<SamplePlayer> sample) {
|
||||
this.online = online;
|
||||
this.max = max;
|
||||
this.sample = ImmutableList.copyOf(sample);
|
||||
}
|
||||
|
||||
public int getOnline() {
|
||||
return online;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
public List<SamplePlayer> getSample() {
|
||||
return sample;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Players{" +
|
||||
"online=" + online +
|
||||
", max=" + max +
|
||||
", sample=" + sample +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class SamplePlayer {
|
||||
private final String name;
|
||||
private final UUID id;
|
||||
|
||||
public SamplePlayer(String name, UUID id) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SamplePlayer{" +
|
||||
"name='" + name + '\'' +
|
||||
", id=" + id +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides utilities to handle server information.
|
||||
*/
|
||||
package com.velocitypowered.api.proxy.server;
|
@ -0,0 +1,24 @@
|
||||
package com.velocitypowered.api.scheduler;
|
||||
|
||||
/**
|
||||
* Represents a task that is scheduled to run on the proxy.
|
||||
*/
|
||||
public interface ScheduledTask {
|
||||
/**
|
||||
* Returns the plugin that scheduled this task.
|
||||
* @return the plugin that scheduled this task
|
||||
*/
|
||||
Object plugin();
|
||||
|
||||
/**
|
||||
* Returns the current status of this task.
|
||||
* @return the current status of this task
|
||||
*/
|
||||
TaskStatus status();
|
||||
|
||||
/**
|
||||
* Cancels this task. If the task is already running, the thread in which it is running will be interrupted.
|
||||
* If the task is not currently running, Velocity will terminate it safely.
|
||||
*/
|
||||
void cancel();
|
||||
}
|
55
api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java
Normale Datei
55
api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java
Normale Datei
@ -0,0 +1,55 @@
|
||||
package com.velocitypowered.api.scheduler;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Represents a scheduler to execute tasks on the proxy.
|
||||
*/
|
||||
public interface Scheduler {
|
||||
/**
|
||||
* Initializes a new {@link TaskBuilder} for creating a task on the proxy.
|
||||
* @param plugin the plugin to request the task for
|
||||
* @param runnable the task to run when scheduled
|
||||
* @return the task builder
|
||||
*/
|
||||
TaskBuilder buildTask(Object plugin, Runnable runnable);
|
||||
|
||||
/**
|
||||
* Represents a fluent interface to schedule tasks on the proxy.
|
||||
*/
|
||||
interface TaskBuilder {
|
||||
/**
|
||||
* Specifies that the task should delay its execution by the specified amount of time.
|
||||
* @param time the time to delay by
|
||||
* @param unit the unit of time for {@code time}
|
||||
* @return this builder, for chaining
|
||||
*/
|
||||
TaskBuilder delay(int time, TimeUnit unit);
|
||||
|
||||
/**
|
||||
* Specifies that the task should continue running after waiting for the specified amount, until it is cancelled.
|
||||
* @param time the time to delay by
|
||||
* @param unit the unit of time for {@code time}
|
||||
* @return this builder, for chaining
|
||||
*/
|
||||
TaskBuilder repeat(int time, TimeUnit unit);
|
||||
|
||||
/**
|
||||
* Clears the delay on this task.
|
||||
* @return this builder, for chaining
|
||||
*/
|
||||
TaskBuilder clearDelay();
|
||||
|
||||
/**
|
||||
* Clears the repeat interval on this task.
|
||||
* @return this builder, for chaining
|
||||
*/
|
||||
TaskBuilder clearRepeat();
|
||||
|
||||
/**
|
||||
* Schedules this task for execution.
|
||||
* @return the scheduled task
|
||||
*/
|
||||
ScheduledTask schedule();
|
||||
}
|
||||
}
|
16
api/src/main/java/com/velocitypowered/api/scheduler/TaskStatus.java
Normale Datei
16
api/src/main/java/com/velocitypowered/api/scheduler/TaskStatus.java
Normale Datei
@ -0,0 +1,16 @@
|
||||
package com.velocitypowered.api.scheduler;
|
||||
|
||||
public enum TaskStatus {
|
||||
/**
|
||||
* The task is scheduled and is currently running.
|
||||
*/
|
||||
SCHEDULED,
|
||||
/**
|
||||
* The task was cancelled with {@link ScheduledTask#cancel()}.
|
||||
*/
|
||||
CANCELLED,
|
||||
/**
|
||||
* The task has run to completion. This is applicable only for tasks without a repeat.
|
||||
*/
|
||||
FINISHED
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides utilities for scheduling tasks with a fluent builder.
|
||||
*/
|
||||
package com.velocitypowered.api.scheduler;
|
89
api/src/main/java/com/velocitypowered/api/util/Favicon.java
Normale Datei
89
api/src/main/java/com/velocitypowered/api/util/Favicon.java
Normale Datei
@ -0,0 +1,89 @@
|
||||
package com.velocitypowered.api.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a Minecraft server favicon. A Minecraft server favicon is a 64x64 image that can be displayed to a remote
|
||||
* client that sends a Server List Ping packet, and is automatically displayed in the Minecraft client.
|
||||
*/
|
||||
public final class Favicon {
|
||||
private final String base64Url;
|
||||
|
||||
/**
|
||||
* Directly create a favicon using its Base64 URL directly. You are generally better served by the create() series
|
||||
* of functions.
|
||||
* @param base64Url the url for use with this favicon
|
||||
*/
|
||||
public Favicon(@NonNull String base64Url) {
|
||||
this.base64Url = Preconditions.checkNotNull(base64Url, "base64Url");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Base64-encoded URI for this image.
|
||||
* @return a URL representing this favicon
|
||||
*/
|
||||
public String getBase64Url() {
|
||||
return base64Url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Favicon favicon = (Favicon) o;
|
||||
return Objects.equals(base64Url, favicon.base64Url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(base64Url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Favicon{" +
|
||||
"base64Url='" + base64Url + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code Favicon} from the specified {@code image}.
|
||||
* @param image the image to use for the favicon
|
||||
* @return the created {@link Favicon} instance
|
||||
*/
|
||||
public static Favicon create(@NonNull BufferedImage image) {
|
||||
Preconditions.checkNotNull(image, "image");
|
||||
Preconditions.checkArgument(image.getWidth() == 64 && image.getHeight() == 64, "Image does not have" +
|
||||
" 64x64 dimensions (found %sx%s)", image.getWidth(), image.getHeight());
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
ImageIO.write(image, "PNG", os);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return new Favicon("data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code Favicon} by reading the image from the specified {@code path}.
|
||||
* @param path the path to the image to create a favicon for
|
||||
* @return the created {@link Favicon} instance
|
||||
* @throws IOException if the file could not be read from the path
|
||||
*/
|
||||
public static Favicon create(@NonNull Path path) throws IOException {
|
||||
try (InputStream stream = Files.newInputStream(path)) {
|
||||
return create(ImageIO.read(stream));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
package com.velocitypowered.proxy.data;
|
||||
package com.velocitypowered.api.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.proxy.util.UuidUtils;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GameProfile {
|
||||
/**
|
||||
* Represents a Mojang game profile. This class is immutable.
|
||||
*/
|
||||
public final class GameProfile {
|
||||
private final String id;
|
||||
private final String name;
|
||||
private final List<Property> properties;
|
||||
|
||||
public GameProfile(String id, String name, List<Property> properties) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
public GameProfile(@NonNull String id, @NonNull String name, @NonNull List<Property> properties) {
|
||||
this.id = Preconditions.checkNotNull(id, "id");
|
||||
this.name = Preconditions.checkNotNull(name, "name");
|
||||
this.properties = ImmutableList.copyOf(properties);
|
||||
}
|
||||
|
||||
@ -31,10 +34,15 @@ public class GameProfile {
|
||||
}
|
||||
|
||||
public List<Property> getProperties() {
|
||||
return ImmutableList.copyOf(properties);
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static GameProfile forOfflinePlayer(String username) {
|
||||
/**
|
||||
* Creates a game profile suitable for use in offline-mode.
|
||||
* @param username the username to use
|
||||
* @return the new offline-mode game profile
|
||||
*/
|
||||
public static GameProfile forOfflinePlayer(@NonNull String username) {
|
||||
Preconditions.checkNotNull(username, "username");
|
||||
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
|
||||
return new GameProfile(id, username, ImmutableList.of());
|
||||
@ -49,15 +57,15 @@ public class GameProfile {
|
||||
'}';
|
||||
}
|
||||
|
||||
public class Property {
|
||||
public final class Property {
|
||||
private final String name;
|
||||
private final String value;
|
||||
private final String signature;
|
||||
|
||||
public Property(String name, String value, String signature) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.signature = signature;
|
||||
public Property(@NonNull String name, @NonNull String value, @NonNull String signature) {
|
||||
this.name = Preconditions.checkNotNull(name, "name");
|
||||
this.value = Preconditions.checkNotNull(value, "value");
|
||||
this.signature = Preconditions.checkNotNull(signature, "signature");
|
||||
}
|
||||
|
||||
public String getName() {
|
@ -1,6 +1,7 @@
|
||||
package com.velocitypowered.api.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -25,7 +26,7 @@ public class LegacyChatColorUtils {
|
||||
* @param text the text to translate
|
||||
* @return the translated text
|
||||
*/
|
||||
public static String translate(char originalChar, String text) {
|
||||
public static String translate(char originalChar, @NonNull String text) {
|
||||
Preconditions.checkNotNull(text, "text");
|
||||
char[] textChars = text.toCharArray();
|
||||
int foundSectionIdx = -1;
|
||||
@ -58,7 +59,7 @@ public class LegacyChatColorUtils {
|
||||
* @param text the text to remove color codes from
|
||||
* @return a new String without Minecraft color codes
|
||||
*/
|
||||
public static String removeFormatting(String text) {
|
||||
public static String removeFormatting(@NonNull String text) {
|
||||
Preconditions.checkNotNull(text, "text");
|
||||
return CHAT_COLOR_MATCHER.matcher(text).replaceAll("");
|
||||
}
|
||||
|
50
api/src/main/java/com/velocitypowered/api/util/UuidUtils.java
Normale Datei
50
api/src/main/java/com/velocitypowered/api/util/UuidUtils.java
Normale Datei
@ -0,0 +1,50 @@
|
||||
package com.velocitypowered.api.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Provides a small, useful selection of utilities for working with Minecraft UUIDs.
|
||||
*/
|
||||
public class UuidUtils {
|
||||
private UuidUtils() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from an undashed Mojang-style UUID into a Java {@link UUID} object.
|
||||
* @param string the string to convert
|
||||
* @return the UUID object
|
||||
*/
|
||||
public static @NonNull UUID fromUndashed(final @NonNull String string) {
|
||||
Objects.requireNonNull(string, "string");
|
||||
Preconditions.checkArgument(string.length() == 32, "Length is incorrect");
|
||||
return new UUID(
|
||||
Long.parseUnsignedLong(string.substring(0, 16), 16),
|
||||
Long.parseUnsignedLong(string.substring(16), 16)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from a Java {@link UUID} object into an undashed Mojang-style UUID.
|
||||
* @param uuid the UUID to convert
|
||||
* @return the undashed UUID
|
||||
*/
|
||||
public static @NonNull String toUndashed(final @NonNull UUID uuid) {
|
||||
Preconditions.checkNotNull(uuid, "uuid");
|
||||
return Long.toUnsignedString(uuid.getMostSignificantBits(), 16) + Long.toUnsignedString(uuid.getLeastSignificantBits(), 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a UUID for use for offline mode.
|
||||
* @param username the username to use
|
||||
* @return the offline mode UUID
|
||||
*/
|
||||
public static @NonNull UUID generateOfflinePlayerUuid(@NonNull String username) {
|
||||
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
4
api/src/main/java/com/velocitypowered/api/util/package-info.java
Normale Datei
4
api/src/main/java/com/velocitypowered/api/util/package-info.java
Normale Datei
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides a selection of miscellaneous utilities for use by plugins and the proxy.
|
||||
*/
|
||||
package com.velocitypowered.api.util;
|
14
build.gradle
14
build.gradle
@ -12,9 +12,21 @@ allprojects {
|
||||
ext {
|
||||
// dependency versions
|
||||
junitVersion = '5.3.0-M1'
|
||||
slf4jVersion = '1.7.25'
|
||||
log4jVersion = '2.11.0'
|
||||
nettyVersion = '4.1.28.Final'
|
||||
guavaVersion = '25.1-jre'
|
||||
|
||||
getCurrentBranchName = {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
exec {
|
||||
executable = "git"
|
||||
args = ["rev-parse", "--abbrev-ref", "HEAD"]
|
||||
standardOutput = os
|
||||
}
|
||||
return os.toString().trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@ -30,4 +42,4 @@ allprojects {
|
||||
junitXml.enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,12 +41,12 @@ public class Natives {
|
||||
public static final NativeCodeLoader<VelocityCompressorFactory> compressor = new NativeCodeLoader<>(
|
||||
ImmutableList.of(
|
||||
new NativeCodeLoader.Variant<>(NativeCodeLoader.MACOS,
|
||||
copyAndLoadNative("/macosx/velocity-compress.dylib"), "native compression (macOS)",
|
||||
copyAndLoadNative("/macosx/velocity-compress.dylib"), "native (macOS)",
|
||||
NativeVelocityCompressor.FACTORY),
|
||||
new NativeCodeLoader.Variant<>(NativeCodeLoader.LINUX,
|
||||
copyAndLoadNative("/linux_x64/velocity-compress.so"), "native compression (Linux amd64)",
|
||||
copyAndLoadNative("/linux_x64/velocity-compress.so"), "native (Linux amd64)",
|
||||
NativeVelocityCompressor.FACTORY),
|
||||
new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> {}, "Java compression", JavaVelocityCompressor.FACTORY)
|
||||
new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> {}, "Java", JavaVelocityCompressor.FACTORY)
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'com.github.johnrengelman.shadow' version '2.0.4'
|
||||
id 'de.sebastianboegl.shadow.transformer.log4j' version '2.1.1'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@ -21,17 +22,70 @@ jar {
|
||||
dependencies {
|
||||
compile project(':velocity-api')
|
||||
compile project(':velocity-native')
|
||||
|
||||
compile "io.netty:netty-codec:${nettyVersion}"
|
||||
compile "io.netty:netty-codec-http:${nettyVersion}"
|
||||
compile "io.netty:netty-handler:${nettyVersion}"
|
||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
|
||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
||||
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
|
||||
|
||||
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||
compile "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
|
||||
|
||||
compile 'net.minecrell:terminalconsoleappender:1.1.1'
|
||||
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
|
||||
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
|
||||
|
||||
compile 'it.unimi.dsi:fastutil:8.2.1'
|
||||
compile 'net.kyori:event-method-asm:3.0.0'
|
||||
|
||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
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/ints/*Int2*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntAVL*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntArray*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntBi*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntList*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntOpen*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntRB*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntSet*'
|
||||
exclude 'it/unimi/dsi/fastutil/ints/IntSorted*'
|
||||
exclude 'it/unimi/dsi/fastutil/io/**'
|
||||
exclude 'it/unimi/dsi/fastutil/longs/**'
|
||||
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/*Object2IntLinked*'
|
||||
exclude 'it/unimi/dsi/fastutil/objects/*Object*OpenCustom*'
|
||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntRB*'
|
||||
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntSorted*'
|
||||
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/*ObjectSorted*'
|
||||
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
|
||||
exclude 'it/unimi/dsi/fastutil/shorts/**'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives shadowJar
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,33 @@
|
||||
package com.velocitypowered.proxy;
|
||||
|
||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class Velocity {
|
||||
public static void main(String... args) throws InterruptedException {
|
||||
private static final Logger logger = LogManager.getLogger(Velocity.class);
|
||||
|
||||
private static long startTime;
|
||||
|
||||
static {
|
||||
// We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient.
|
||||
// Force AWT to work with its head chopped off.
|
||||
System.setProperty("java.awt.headless", "true");
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
startTime = System.currentTimeMillis();
|
||||
logger.info("Booting up Velocity...");
|
||||
|
||||
final VelocityServer server = VelocityServer.getServer();
|
||||
server.start();
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown, "Shutdown thread"));
|
||||
|
||||
Thread.currentThread().join();
|
||||
double bootTime = (System.currentTimeMillis() - startTime) / 1000d;
|
||||
logger.info("Done ({}s)!", new DecimalFormat("#.##").format(bootTime));
|
||||
new VelocityConsole(server).start();
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,40 @@
|
||||
package com.velocitypowered.proxy;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import com.velocitypowered.network.ConnectionManager;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.api.util.Favicon;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.command.ServerCommand;
|
||||
import com.velocitypowered.proxy.command.ShutdownCommand;
|
||||
import com.velocitypowered.proxy.command.VelocityCommand;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.command.VelocityCommandManager;
|
||||
import com.velocitypowered.proxy.messages.VelocityChannelRegistrar;
|
||||
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
||||
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
|
||||
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
||||
import com.velocitypowered.proxy.scheduler.Sleeper;
|
||||
import com.velocitypowered.proxy.scheduler.VelocityScheduler;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.util.Ratelimiter;
|
||||
import com.velocitypowered.proxy.util.ServerMap;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
import net.kyori.text.serializer.GsonComponentSerializer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -22,13 +45,17 @@ import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class VelocityServer {
|
||||
public class VelocityServer implements ProxyServer {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
|
||||
private static final VelocityServer INSTANCE = new VelocityServer();
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
|
||||
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
|
||||
.create();
|
||||
|
||||
private final ConnectionManager cm = new ConnectionManager();
|
||||
@ -36,8 +63,33 @@ public class VelocityServer {
|
||||
private NettyHttpClient httpClient;
|
||||
private KeyPair serverKeyPair;
|
||||
private final ServerMap servers = new ServerMap();
|
||||
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
||||
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
|
||||
private boolean shutdown = false;
|
||||
private final VelocityPluginManager pluginManager = new VelocityPluginManager(this);
|
||||
|
||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||
private final CommandSource consoleCommandSource = new CommandSource() {
|
||||
@Override
|
||||
public void sendMessage(Component component) {
|
||||
logger.info(ComponentSerializers.LEGACY.serialize(component));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(String permission) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
private Ratelimiter ipAttemptLimiter;
|
||||
private VelocityEventManager eventManager;
|
||||
private VelocityScheduler scheduler;
|
||||
private VelocityChannelRegistrar channelRegistrar;
|
||||
|
||||
private VelocityServer() {
|
||||
commandManager.register(new VelocityCommand(), "velocity");
|
||||
commandManager.register(new ServerCommand(), "server");
|
||||
commandManager.register(new ShutdownCommand(), "shutdown");
|
||||
}
|
||||
|
||||
public static VelocityServer getServer() {
|
||||
@ -52,12 +104,12 @@ public class VelocityServer {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
logger.info("Using {}", Natives.compressor.getLoadedVariant());
|
||||
logger.info("Using {}", Natives.cipher.getLoadedVariant());
|
||||
@Override
|
||||
public VelocityCommandManager getCommandManager() {
|
||||
return commandManager;
|
||||
}
|
||||
|
||||
// Create a key pair
|
||||
logger.info("Booting up Velocity...");
|
||||
public void start() {
|
||||
try {
|
||||
Path configPath = Paths.get("velocity.toml");
|
||||
try {
|
||||
@ -82,10 +134,55 @@ public class VelocityServer {
|
||||
}
|
||||
|
||||
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
|
||||
|
||||
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
|
||||
httpClient = new NettyHttpClient(this);
|
||||
eventManager = new VelocityEventManager(pluginManager);
|
||||
scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM);
|
||||
channelRegistrar = new VelocityChannelRegistrar();
|
||||
loadPlugins();
|
||||
|
||||
try {
|
||||
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
||||
// to fully initialize before we accept any connections to the server.
|
||||
eventManager.fire(new ProxyInitializeEvent()).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
// Ignore, we don't care. InterruptedException is unlikely to happen (and if it does, you've got bigger
|
||||
// issues) and there is almost no chance ExecutionException will be thrown.
|
||||
}
|
||||
|
||||
this.cm.bind(configuration.getBind());
|
||||
|
||||
if (configuration.isQueryEnabled()) {
|
||||
this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPlugins() {
|
||||
logger.info("Loading plugins...");
|
||||
|
||||
try {
|
||||
Path pluginPath = Paths.get("plugins");
|
||||
|
||||
if (Files.notExists(pluginPath)) {
|
||||
Files.createDirectory(pluginPath);
|
||||
} else {
|
||||
if (!Files.isDirectory(pluginPath)) {
|
||||
logger.warn("Plugin location {} is not a directory, continuing without loading plugins", pluginPath);
|
||||
return;
|
||||
}
|
||||
|
||||
pluginManager.loadPlugins(pluginPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Couldn't load plugins", e);
|
||||
}
|
||||
|
||||
// Register the plugin main classes so that we may proceed with firing the proxy initialize event
|
||||
pluginManager.getPlugins().forEach(container -> {
|
||||
container.getInstance().ifPresent(plugin -> eventManager.register(plugin, plugin));
|
||||
});
|
||||
|
||||
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
||||
}
|
||||
|
||||
public ServerMap getServers() {
|
||||
@ -96,11 +193,122 @@ public class VelocityServer {
|
||||
return this.cm.createWorker();
|
||||
}
|
||||
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (!shutdownInProgress.compareAndSet(false, true)) return;
|
||||
logger.info("Shutting down the proxy...");
|
||||
|
||||
for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) {
|
||||
player.close(TextComponent.of("Proxy shutting down."));
|
||||
}
|
||||
|
||||
this.cm.shutdown();
|
||||
|
||||
eventManager.fire(new ProxyShutdownEvent());
|
||||
try {
|
||||
if (!eventManager.shutdown() || !scheduler.shutdown()) {
|
||||
logger.error("Your plugins took over 10 seconds to shut down.");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Not much we can do about this...
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
public NettyHttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public Ratelimiter getIpAttemptLimiter() {
|
||||
return ipAttemptLimiter;
|
||||
}
|
||||
|
||||
public boolean registerConnection(ConnectedPlayer connection) {
|
||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||
return false;
|
||||
}
|
||||
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
|
||||
connectionsByName.remove(lowerName, connection);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unregisterConnection(ConnectedPlayer connection) {
|
||||
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
||||
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Player> getPlayer(String username) {
|
||||
Preconditions.checkNotNull(username, "username");
|
||||
return Optional.ofNullable(connectionsByName.get(username.toLowerCase(Locale.US)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Player> getPlayer(UUID uuid) {
|
||||
Preconditions.checkNotNull(uuid, "uuid");
|
||||
return Optional.ofNullable(connectionsByUuid.get(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Player> getAllPlayers() {
|
||||
return ImmutableList.copyOf(connectionsByUuid.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlayerCount() {
|
||||
return connectionsByUuid.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ServerInfo> getServerInfo(String name) {
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
return servers.getServer(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ServerInfo> getAllServers() {
|
||||
return servers.getAllServers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerServer(ServerInfo server) {
|
||||
servers.register(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterServer(ServerInfo server) {
|
||||
servers.unregister(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSource getConsoleCommandSource() {
|
||||
return consoleCommandSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginManager getPluginManager() {
|
||||
return pluginManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VelocityScheduler getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VelocityChannelRegistrar getChannelRegistrar() {
|
||||
return channelRegistrar;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ServerCommand implements Command {
|
||||
@Override
|
||||
public void execute(CommandSource source, String[] args) {
|
||||
if (!(source instanceof Player)) {
|
||||
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = (Player) source;
|
||||
if (args.length == 1) {
|
||||
// Trying to connect to a server.
|
||||
String serverName = args[0];
|
||||
Optional<ServerInfo> server = VelocityServer.getServer().getServerInfo(serverName);
|
||||
if (!server.isPresent()) {
|
||||
player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
player.createConnectionRequest(server.get()).fireAndForget();
|
||||
} else {
|
||||
String serverList = VelocityServer.getServer().getAllServers().stream()
|
||||
.map(ServerInfo::getName)
|
||||
.collect(Collectors.joining(", "));
|
||||
player.sendMessage(TextComponent.of("Available servers: " + serverList, TextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(CommandSource source, String[] currentArgs) {
|
||||
if (currentArgs.length == 0) {
|
||||
return VelocityServer.getServer().getAllServers().stream()
|
||||
.map(ServerInfo::getName)
|
||||
.collect(Collectors.toList());
|
||||
} else if (currentArgs.length == 1) {
|
||||
return VelocityServer.getServer().getAllServers().stream()
|
||||
.map(ServerInfo::getName)
|
||||
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
|
||||
public class ShutdownCommand implements Command {
|
||||
@Override
|
||||
public void execute(CommandSource source, String[] args) {
|
||||
if (source != VelocityServer.getServer().getConsoleCommandSource()) {
|
||||
source.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
VelocityServer.getServer().shutdown();
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.event.ClickEvent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
|
||||
public class VelocityCommand implements Command {
|
||||
@Override
|
||||
public void execute(CommandSource source, String[] args) {
|
||||
String implVersion = VelocityServer.class.getPackage().getImplementationVersion();
|
||||
TextComponent thisIsVelocity = TextComponent.builder()
|
||||
.content("This is ")
|
||||
.append(TextComponent.of("Velocity " + implVersion, TextColor.DARK_AQUA))
|
||||
.append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.").resetStyle())
|
||||
.build();
|
||||
TextComponent velocityInfo = TextComponent.builder()
|
||||
.content("Copyright 2018 Velocity Contributors. Velocity is freely licensed under the terms of the " +
|
||||
"MIT License.")
|
||||
.build();
|
||||
TextComponent velocityWebsite = TextComponent.builder()
|
||||
.content("Visit the ")
|
||||
.append(TextComponent.builder("Velocity website")
|
||||
.color(TextColor.GREEN)
|
||||
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.velocitypowered.com"))
|
||||
.build())
|
||||
.append(TextComponent.of(" or the ").resetStyle())
|
||||
.append(TextComponent.builder("Velocity GitHub")
|
||||
.color(TextColor.GREEN)
|
||||
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity"))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
source.sendMessage(thisIsVelocity);
|
||||
source.sendMessage(velocityInfo);
|
||||
source.sendMessage(velocityWebsite);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class VelocityCommandManager implements CommandManager {
|
||||
private final Map<String, Command> commands = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void register(final Command command, final String... aliases) {
|
||||
Preconditions.checkNotNull(aliases, "aliases");
|
||||
Preconditions.checkNotNull(command, "executor");
|
||||
for (int i = 0, length = aliases.length; i < length; i++) {
|
||||
final String alias = aliases[i];
|
||||
Preconditions.checkNotNull(aliases, "alias at index %s", i);
|
||||
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(final String alias) {
|
||||
Preconditions.checkNotNull(alias, "name");
|
||||
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(CommandSource source, String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "invoker");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String alias = split[0];
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
if (command == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
command.execute(source, actualArgs);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<List<String>> offerSuggestions(CommandSource source, String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String command = split[0];
|
||||
if (split.length == 1) {
|
||||
return Optional.of(commands.keySet().stream()
|
||||
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
Command executor = commands.get(command);
|
||||
if (executor == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(executor.suggest(source, actualArgs));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + source, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.velocitypowered.proxy.config;
|
||||
|
||||
public enum IPForwardingMode {
|
||||
public enum PlayerInfoForwarding {
|
||||
NONE,
|
||||
LEGACY,
|
||||
MODERN
|
@ -2,8 +2,10 @@ package com.velocitypowered.proxy.config;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.moandjiezana.toml.Toml;
|
||||
import com.velocitypowered.api.util.Favicon;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
import com.velocitypowered.api.util.LegacyChatColorUtils;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -15,6 +17,7 @@ import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -26,27 +29,39 @@ public class VelocityConfiguration {
|
||||
private final String motd;
|
||||
private final int showMaxPlayers;
|
||||
private final boolean onlineMode;
|
||||
private final IPForwardingMode ipForwardingMode;
|
||||
private final PlayerInfoForwarding playerInfoForwardingMode;
|
||||
private final Map<String, String> servers;
|
||||
private final List<String> attemptConnectionOrder;
|
||||
private final int compressionThreshold;
|
||||
private final int compressionLevel;
|
||||
private final int loginRatelimit;
|
||||
|
||||
private final boolean queryEnabled;
|
||||
private final int queryPort;
|
||||
|
||||
private Component motdAsComponent;
|
||||
private Favicon favicon;
|
||||
|
||||
private final byte[] forwardingSecret;
|
||||
|
||||
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
||||
IPForwardingMode ipForwardingMode, Map<String, String> servers,
|
||||
PlayerInfoForwarding playerInfoForwardingMode, Map<String, String> servers,
|
||||
List<String> attemptConnectionOrder, int compressionThreshold,
|
||||
int compressionLevel) {
|
||||
int compressionLevel, int loginRatelimit, boolean queryEnabled,
|
||||
int queryPort, byte[] forwardingSecret) {
|
||||
this.bind = bind;
|
||||
this.motd = motd;
|
||||
this.showMaxPlayers = showMaxPlayers;
|
||||
this.onlineMode = onlineMode;
|
||||
this.ipForwardingMode = ipForwardingMode;
|
||||
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
||||
this.servers = servers;
|
||||
this.attemptConnectionOrder = attemptConnectionOrder;
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.loginRatelimit = loginRatelimit;
|
||||
this.queryEnabled = queryEnabled;
|
||||
this.queryPort = queryPort;
|
||||
this.forwardingSecret = forwardingSecret;
|
||||
}
|
||||
|
||||
public boolean validate() {
|
||||
@ -68,9 +83,15 @@ public class VelocityConfiguration {
|
||||
logger.info("Proxy is running in offline mode!");
|
||||
}
|
||||
|
||||
switch (ipForwardingMode) {
|
||||
switch (playerInfoForwardingMode) {
|
||||
case NONE:
|
||||
logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
|
||||
logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
|
||||
break;
|
||||
case MODERN:
|
||||
if (forwardingSecret.length == 0) {
|
||||
logger.error("You don't have a forwarding secret set.");
|
||||
valid = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@ -109,23 +130,51 @@ public class VelocityConfiguration {
|
||||
|
||||
if (compressionLevel < -1 || compressionLevel > 9) {
|
||||
logger.error("Invalid compression level {}", compressionLevel);
|
||||
valid = false;
|
||||
} else if (compressionLevel == 0) {
|
||||
logger.warn("ALL packets going through the proxy are going to be uncompressed. This will increase bandwidth usage.");
|
||||
}
|
||||
|
||||
if (compressionThreshold < -1) {
|
||||
logger.error("Invalid compression threshold {}", compressionLevel);
|
||||
valid = false;
|
||||
} else if (compressionThreshold == 0) {
|
||||
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
|
||||
}
|
||||
|
||||
if (loginRatelimit < 0) {
|
||||
logger.error("Invalid login ratelimit {}", loginRatelimit);
|
||||
valid = false;
|
||||
}
|
||||
|
||||
loadFavicon();
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
private void loadFavicon() {
|
||||
Path faviconPath = Paths.get("server-icon.png");
|
||||
if (Files.exists(faviconPath)) {
|
||||
try {
|
||||
this.favicon = Favicon.create(faviconPath);
|
||||
} catch (Exception e) {
|
||||
logger.info("Unable to load your server-icon.png, continuing without it.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InetSocketAddress getBind() {
|
||||
return AddressUtil.parseAddress(bind);
|
||||
}
|
||||
|
||||
public boolean isQueryEnabled() {
|
||||
return queryEnabled;
|
||||
}
|
||||
|
||||
public int getQueryPort() {
|
||||
return queryPort;
|
||||
}
|
||||
|
||||
public String getMotd() {
|
||||
return motd;
|
||||
}
|
||||
@ -149,8 +198,8 @@ public class VelocityConfiguration {
|
||||
return onlineMode;
|
||||
}
|
||||
|
||||
public IPForwardingMode getIpForwardingMode() {
|
||||
return ipForwardingMode;
|
||||
public PlayerInfoForwarding getPlayerInfoForwardingMode() {
|
||||
return playerInfoForwardingMode;
|
||||
}
|
||||
|
||||
public Map<String, String> getServers() {
|
||||
@ -169,6 +218,18 @@ public class VelocityConfiguration {
|
||||
return compressionLevel;
|
||||
}
|
||||
|
||||
public int getLoginRatelimit() {
|
||||
return loginRatelimit;
|
||||
}
|
||||
|
||||
public Favicon getFavicon() {
|
||||
return favicon;
|
||||
}
|
||||
|
||||
public byte[] getForwardingSecret() {
|
||||
return forwardingSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VelocityConfiguration{" +
|
||||
@ -176,12 +237,17 @@ public class VelocityConfiguration {
|
||||
", motd='" + motd + '\'' +
|
||||
", showMaxPlayers=" + showMaxPlayers +
|
||||
", onlineMode=" + onlineMode +
|
||||
", ipForwardingMode=" + ipForwardingMode +
|
||||
", playerInfoForwardingMode=" + playerInfoForwardingMode +
|
||||
", servers=" + servers +
|
||||
", attemptConnectionOrder=" + attemptConnectionOrder +
|
||||
", compressionThreshold=" + compressionThreshold +
|
||||
", compressionLevel=" + compressionLevel +
|
||||
", loginRatelimit=" + loginRatelimit +
|
||||
", queryEnabled=" + queryEnabled +
|
||||
", queryPort=" + queryPort +
|
||||
", motdAsComponent=" + motdAsComponent +
|
||||
", favicon=" + favicon +
|
||||
", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret) +
|
||||
'}';
|
||||
}
|
||||
|
||||
@ -200,16 +266,23 @@ public class VelocityConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
byte[] forwardingSecret = toml.getString("player-info-forwarding-secret", "5up3r53cr3t")
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
return new VelocityConfiguration(
|
||||
toml.getString("bind"),
|
||||
toml.getString("motd"),
|
||||
toml.getLong("show-max-players").intValue(),
|
||||
toml.getBoolean("online-mode"),
|
||||
IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()),
|
||||
toml.getString("bind", "0.0.0.0:25577"),
|
||||
toml.getString("motd", "&3A Velocity Server"),
|
||||
toml.getLong("show-max-players", 500L).intValue(),
|
||||
toml.getBoolean("online-mode", true),
|
||||
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding", "MODERN").toUpperCase()),
|
||||
ImmutableMap.copyOf(servers),
|
||||
toml.getTable("servers").getList("try"),
|
||||
toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(),
|
||||
toml.getTable("advanced").getLong("compression-level", -1L).intValue());
|
||||
toml.getTable("advanced").getLong("compression-level", -1L).intValue(),
|
||||
toml.getTable("advanced").getLong("login-ratelimit", 3000L).intValue(),
|
||||
toml.getTable("query").getBoolean("enabled", false),
|
||||
toml.getTable("query").getLong("port", 25577L).intValue(),
|
||||
forwardingSecret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.protocol.PacketWrapper;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.natives.encryption.JavaVelocityCipher;
|
||||
import com.velocitypowered.natives.encryption.VelocityCipher;
|
||||
import com.velocitypowered.proxy.protocol.netty.*;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@ -23,14 +24,14 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import static com.velocitypowered.network.Connections.CIPHER_DECODER;
|
||||
import static com.velocitypowered.network.Connections.CIPHER_ENCODER;
|
||||
import static com.velocitypowered.network.Connections.COMPRESSION_DECODER;
|
||||
import static com.velocitypowered.network.Connections.COMPRESSION_ENCODER;
|
||||
import static com.velocitypowered.network.Connections.FRAME_DECODER;
|
||||
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
|
||||
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
|
||||
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.CIPHER_DECODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.CIPHER_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.COMPRESSION_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
|
||||
|
||||
/**
|
||||
* A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft
|
||||
@ -40,7 +41,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
|
||||
|
||||
private final Channel channel;
|
||||
private boolean closed;
|
||||
private StateRegistry state;
|
||||
private MinecraftSessionHandler sessionHandler;
|
||||
private int protocolVersion;
|
||||
@ -48,7 +48,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
|
||||
public MinecraftConnection(Channel channel) {
|
||||
this.channel = channel;
|
||||
this.closed = false;
|
||||
this.state = StateRegistry.HANDSHAKE;
|
||||
}
|
||||
|
||||
@ -72,24 +71,17 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
if (association != null) {
|
||||
logger.info("{} has disconnected", association);
|
||||
}
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof PacketWrapper) {
|
||||
PacketWrapper pw = (PacketWrapper) msg;
|
||||
if (msg instanceof MinecraftPacket) {
|
||||
sessionHandler.handle((MinecraftPacket) msg);
|
||||
} else if (msg instanceof ByteBuf) {
|
||||
try {
|
||||
if (sessionHandler != null) {
|
||||
if (pw.getPacket() == null) {
|
||||
sessionHandler.handleUnknown(pw.getBuffer());
|
||||
} else {
|
||||
sessionHandler.handle(pw.getPacket());
|
||||
}
|
||||
}
|
||||
sessionHandler.handleUnknown((ByteBuf) msg);
|
||||
} finally {
|
||||
ReferenceCountUtil.release(pw.getBuffer());
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -107,40 +99,38 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
logger.error("{} encountered an exception", ctx.channel().remoteAddress(), cause);
|
||||
}
|
||||
|
||||
closed = true;
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(Object msg) {
|
||||
ensureOpen();
|
||||
channel.writeAndFlush(msg, channel.voidPromise());
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(msg, channel.voidPromise());
|
||||
}
|
||||
}
|
||||
|
||||
public void delayedWrite(Object msg) {
|
||||
ensureOpen();
|
||||
channel.write(msg, channel.voidPromise());
|
||||
if (channel.isActive()) {
|
||||
channel.write(msg, channel.voidPromise());
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
ensureOpen();
|
||||
channel.flush();
|
||||
if (channel.isActive()) {
|
||||
channel.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void closeWith(Object msg) {
|
||||
ensureOpen();
|
||||
teardown();
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
ensureOpen();
|
||||
teardown();
|
||||
channel.close();
|
||||
}
|
||||
|
||||
public void teardown() {
|
||||
closed = true;
|
||||
if (channel.isActive()) {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
public Channel getChannel() {
|
||||
@ -148,7 +138,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return closed;
|
||||
return !channel.isActive();
|
||||
}
|
||||
|
||||
public StateRegistry getState() {
|
||||
@ -167,8 +157,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
|
||||
public void setProtocolVersion(int protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
|
||||
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
|
||||
if (protocolVersion != ProtocolConstants.LEGACY) {
|
||||
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
|
||||
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
|
||||
} else {
|
||||
// Legacy handshake handling
|
||||
this.channel.pipeline().remove(MINECRAFT_ENCODER);
|
||||
this.channel.pipeline().remove(MINECRAFT_DECODER);
|
||||
}
|
||||
}
|
||||
|
||||
public MinecraftSessionHandler getSessionHandler() {
|
||||
@ -184,10 +180,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
private void ensureOpen() {
|
||||
Preconditions.checkState(!closed, "Connection is closed.");
|
||||
Preconditions.checkState(!isClosed(), "Connection is closed.");
|
||||
}
|
||||
|
||||
public void setCompressionThreshold(int threshold) {
|
||||
ensureOpen();
|
||||
|
||||
if (threshold == -1) {
|
||||
channel.pipeline().remove(COMPRESSION_DECODER);
|
||||
channel.pipeline().remove(COMPRESSION_ENCODER);
|
||||
@ -204,6 +202,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
|
||||
ensureOpen();
|
||||
|
||||
SecretKey key = new SecretKeySpec(secret, "AES");
|
||||
|
||||
VelocityCipherFactory factory = Natives.cipher.get();
|
||||
|
@ -4,7 +4,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
public interface MinecraftSessionHandler {
|
||||
void handle(MinecraftPacket packet) throws Exception;
|
||||
void handle(MinecraftPacket packet);
|
||||
|
||||
default void handleUnknown(ByteBuf buf) {
|
||||
// No-op: we'll release the buffer later.
|
||||
|
@ -1,5 +1,9 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelSide;
|
||||
import com.velocitypowered.api.proxy.messages.MessageHandler;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
@ -7,31 +11,40 @@ import com.velocitypowered.proxy.protocol.packet.*;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
private final ServerConnection connection;
|
||||
private final VelocityServerConnection connection;
|
||||
|
||||
public BackendPlaySessionHandler(ServerConnection connection) {
|
||||
public BackendPlaySessionHandler(VelocityServerConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activated() {
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new ServerConnectedEvent(connection.getPlayer(),
|
||||
connection.getServerInfo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
if (!connection.getPlayer().isActive()) {
|
||||
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
|
||||
// errors.
|
||||
connection.getMinecraftConnection().close();
|
||||
return;
|
||||
}
|
||||
|
||||
ClientPlaySessionHandler playerHandler =
|
||||
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
|
||||
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
|
||||
if (packet instanceof KeepAlive) {
|
||||
// Forward onto the server
|
||||
connection.getMinecraftConnection().write(packet);
|
||||
// Forward onto the player
|
||||
playerHandler.setLastPing(((KeepAlive) packet).getRandomId());
|
||||
connection.getPlayer().getConnection().write(packet);
|
||||
} else if (packet instanceof Disconnect) {
|
||||
Disconnect original = (Disconnect) packet;
|
||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), original);
|
||||
connection.getPlayer().handleConnectionException(connection.getServerInfo(), original);
|
||||
} else if (packet instanceof JoinGame) {
|
||||
playerHandler.handleBackendJoinGame((JoinGame) packet);
|
||||
} else if (packet instanceof Respawn) {
|
||||
// Record the dimension switch, and then forward the packet on.
|
||||
playerHandler.setCurrentDimension(((Respawn) packet).getDimension());
|
||||
connection.getProxyPlayer().getConnection().write(packet);
|
||||
} else if (packet instanceof BossBar) {
|
||||
BossBar bossBar = (BossBar) packet;
|
||||
switch (bossBar.getAction()) {
|
||||
@ -42,7 +55,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
playerHandler.getServerBossBars().remove(bossBar.getUuid());
|
||||
break;
|
||||
}
|
||||
connection.getProxyPlayer().getConnection().write(packet);
|
||||
connection.getPlayer().getConnection().write(packet);
|
||||
} else if (packet instanceof PluginMessage) {
|
||||
PluginMessage pm = (PluginMessage) packet;
|
||||
if (!canForwardPluginMessage(pm)) {
|
||||
@ -50,45 +63,52 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
if (PluginMessageUtil.isMCBrand(pm)) {
|
||||
connection.getProxyPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(pm));
|
||||
connection.getPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(pm));
|
||||
return;
|
||||
}
|
||||
|
||||
connection.getProxyPlayer().getConnection().write(pm);
|
||||
MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage(
|
||||
connection, ChannelSide.FROM_SERVER, pm);
|
||||
if (status == MessageHandler.ForwardStatus.FORWARD) {
|
||||
connection.getPlayer().getConnection().write(pm);
|
||||
}
|
||||
} else {
|
||||
// Just forward the packet on. We don't have anything to handle at this time.
|
||||
if (packet instanceof ScoreboardTeam ||
|
||||
packet instanceof ScoreboardObjective ||
|
||||
packet instanceof ScoreboardSetScore ||
|
||||
packet instanceof ScoreboardDisplay) {
|
||||
playerHandler.handleServerScoreboardPacket(packet);
|
||||
}
|
||||
connection.getProxyPlayer().getConnection().write(packet);
|
||||
connection.getPlayer().getConnection().write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUnknown(ByteBuf buf) {
|
||||
if (!connection.getPlayer().isActive()) {
|
||||
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
|
||||
// errors.
|
||||
connection.getMinecraftConnection().close();
|
||||
return;
|
||||
}
|
||||
|
||||
ClientPlaySessionHandler playerHandler =
|
||||
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
|
||||
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
|
||||
ByteBuf remapped = playerHandler.getIdRemapper().remap(buf, ProtocolConstants.Direction.CLIENTBOUND);
|
||||
connection.getProxyPlayer().getConnection().write(remapped);
|
||||
connection.getPlayer().getConnection().write(remapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exception(Throwable throwable) {
|
||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
|
||||
connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable);
|
||||
}
|
||||
|
||||
private boolean canForwardPluginMessage(PluginMessage message) {
|
||||
ClientPlaySessionHandler playerHandler =
|
||||
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
|
||||
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
|
||||
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
|
||||
return message.getChannel().startsWith("MC|") ||
|
||||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
|
||||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
|
||||
VelocityServer.getServer().getChannelRegistrar().registered(message.getChannel());
|
||||
} else {
|
||||
return message.getChannel().startsWith("minecraft:") ||
|
||||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
|
||||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
|
||||
VelocityServer.getServer().getChannelRegistrar().registered(message.getChannel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,12 @@ package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.IPForwardingMode;
|
||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.VelocityConstants;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.data.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@ -14,49 +15,40 @@ import com.velocitypowered.proxy.protocol.packet.*;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import net.kyori.text.TextComponent;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
private final ServerConnection connection;
|
||||
private ScheduledFuture<?> forwardingCheckTask;
|
||||
private final VelocityServerConnection connection;
|
||||
private boolean informationForwarded;
|
||||
|
||||
public LoginSessionHandler(ServerConnection connection) {
|
||||
public LoginSessionHandler(VelocityServerConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activated() {
|
||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) {
|
||||
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
|
||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
|
||||
TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?"));
|
||||
}, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
if (packet instanceof EncryptionRequest) {
|
||||
throw new IllegalStateException("Backend server is online-mode!");
|
||||
} else if (packet instanceof LoginPluginMessage) {
|
||||
LoginPluginMessage message = (LoginPluginMessage) packet;
|
||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN &&
|
||||
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
|
||||
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
|
||||
message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
||||
LoginPluginResponse response = new LoginPluginResponse();
|
||||
response.setSuccess(true);
|
||||
response.setId(message.getId());
|
||||
response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(),
|
||||
connection.getProxyPlayer().getProfile()));
|
||||
response.setData(createForwardingData(configuration.getForwardingSecret(),
|
||||
connection.getPlayer().getRemoteAddress().getHostString(),
|
||||
connection.getPlayer().getProfile()));
|
||||
connection.getMinecraftConnection().write(response);
|
||||
cancelForwardingCheck();
|
||||
|
||||
ServerLogin login = new ServerLogin();
|
||||
login.setUsername(connection.getProxyPlayer().getUsername());
|
||||
connection.getMinecraftConnection().write(login);
|
||||
informationForwarded = true;
|
||||
} else {
|
||||
// Don't understand
|
||||
LoginPluginResponse response = new LoginPluginResponse();
|
||||
@ -67,78 +59,92 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
} else if (packet instanceof Disconnect) {
|
||||
Disconnect disconnect = (Disconnect) packet;
|
||||
connection.disconnect();
|
||||
|
||||
// Do we have an outstanding notification? If so, fulfill it.
|
||||
doNotify(ConnectionRequestResults.forDisconnect(disconnect));
|
||||
|
||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), disconnect);
|
||||
connection.disconnect();
|
||||
} else if (packet instanceof SetCompression) {
|
||||
SetCompression sc = (SetCompression) packet;
|
||||
connection.getMinecraftConnection().setCompressionThreshold(sc.getThreshold());
|
||||
} else if (packet instanceof ServerLoginSuccess) {
|
||||
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
|
||||
!informationForwarded) {
|
||||
doNotify(ConnectionRequestResults.forDisconnect(
|
||||
TextComponent.of("Your server did not send a forwarding request to the proxy. Is it set up correctly?")));
|
||||
connection.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// The player has been logged on to the backend server.
|
||||
connection.getMinecraftConnection().setState(StateRegistry.PLAY);
|
||||
ServerConnection existingConnection = connection.getProxyPlayer().getConnectedServer();
|
||||
VelocityServerConnection existingConnection = connection.getPlayer().getConnectedServer();
|
||||
if (existingConnection == null) {
|
||||
// Strap on the play session handler
|
||||
connection.getProxyPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getProxyPlayer()));
|
||||
connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getPlayer()));
|
||||
} else {
|
||||
// The previous server connection should become obsolete.
|
||||
existingConnection.disconnect();
|
||||
}
|
||||
|
||||
// Do we have an outstanding notification? If so, fulfill it.
|
||||
doNotify(ConnectionRequestResults.SUCCESSFUL);
|
||||
|
||||
connection.getMinecraftConnection().setSessionHandler(new BackendPlaySessionHandler(connection));
|
||||
connection.getProxyPlayer().setConnectedServer(connection);
|
||||
connection.getPlayer().setConnectedServer(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivated() {
|
||||
cancelForwardingCheck();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exception(Throwable throwable) {
|
||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
|
||||
CompletableFuture<ConnectionRequestBuilder.Result> future = connection.getMinecraftConnection().getChannel()
|
||||
.attr(VelocityServerConnection.CONNECTION_NOTIFIER).getAndSet(null);
|
||||
if (future != null) {
|
||||
future.completeExceptionally(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private void doNotify(ConnectionRequestBuilder.Result result) {
|
||||
ChannelPipeline pipeline = connection.getMinecraftConnection().getChannel().pipeline();
|
||||
ServerConnection.ConnectionNotifier n = pipeline.get(ServerConnection.ConnectionNotifier.class);
|
||||
if (n != null) {
|
||||
n.getResult().complete(result);
|
||||
pipeline.remove(ServerConnection.ConnectionNotifier.class);
|
||||
CompletableFuture<ConnectionRequestBuilder.Result> future = connection.getMinecraftConnection().getChannel()
|
||||
.attr(VelocityServerConnection.CONNECTION_NOTIFIER).getAndSet(null);
|
||||
if (future != null) {
|
||||
future.complete(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelForwardingCheck() {
|
||||
if (forwardingCheckTask != null) {
|
||||
forwardingCheckTask.cancel(false);
|
||||
forwardingCheckTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuf createForwardingData(String address, GameProfile profile) {
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
ProtocolUtils.writeString(buf, address);
|
||||
ProtocolUtils.writeUuid(buf, profile.idAsUuid());
|
||||
ProtocolUtils.writeString(buf, profile.getName());
|
||||
ProtocolUtils.writeVarInt(buf, profile.getProperties().size());
|
||||
for (GameProfile.Property property : profile.getProperties()) {
|
||||
ProtocolUtils.writeString(buf, property.getName());
|
||||
ProtocolUtils.writeString(buf, property.getValue());
|
||||
String signature = property.getSignature();
|
||||
if (signature != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writeString(buf, signature);
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
|
||||
ByteBuf dataToForward = Unpooled.buffer();
|
||||
ByteBuf finalData = Unpooled.buffer();
|
||||
try {
|
||||
ProtocolUtils.writeString(dataToForward, address);
|
||||
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
|
||||
ProtocolUtils.writeString(dataToForward, profile.getName());
|
||||
ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size());
|
||||
for (GameProfile.Property property : profile.getProperties()) {
|
||||
ProtocolUtils.writeString(dataToForward, property.getName());
|
||||
ProtocolUtils.writeString(dataToForward, property.getValue());
|
||||
String signature = property.getSignature();
|
||||
if (signature != null) {
|
||||
dataToForward.writeBoolean(true);
|
||||
ProtocolUtils.writeString(dataToForward, signature);
|
||||
} else {
|
||||
dataToForward.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
|
||||
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(key);
|
||||
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
|
||||
byte[] sig = mac.doFinal();
|
||||
finalData.writeBytes(sig);
|
||||
finalData.writeBytes(dataToForward);
|
||||
return finalData;
|
||||
} catch (InvalidKeyException e) {
|
||||
finalData.release();
|
||||
throw new RuntimeException("Unable to authenticate data", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
finalData.release();
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
dataToForward.release();
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,49 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.proxy.config.IPForwardingMode;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
|
||||
import com.velocitypowered.proxy.protocol.packet.Handshake;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.velocitypowered.network.Connections.FRAME_DECODER;
|
||||
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
|
||||
import static com.velocitypowered.network.Connections.HANDLER;
|
||||
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
|
||||
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
|
||||
import static com.velocitypowered.network.Connections.READ_TIMEOUT;
|
||||
import static com.velocitypowered.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
|
||||
import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.HANDLER;
|
||||
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
|
||||
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.SERVER_READ_TIMEOUT_SECONDS;
|
||||
|
||||
public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
static final String CONNECTION_NOTIFIER = "connection-notifier";
|
||||
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
|
||||
static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER =
|
||||
AttributeKey.newInstance("connection-notification-result");
|
||||
|
||||
private final ServerInfo serverInfo;
|
||||
private final ConnectedPlayer proxyPlayer;
|
||||
private final VelocityServer server;
|
||||
private MinecraftConnection minecraftConnection;
|
||||
|
||||
public ServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
|
||||
public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
|
||||
this.serverInfo = target;
|
||||
this.proxyPlayer = proxyPlayer;
|
||||
this.server = server;
|
||||
@ -55,12 +60,12 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
|
||||
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
|
||||
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
|
||||
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND))
|
||||
.addLast(CONNECTION_NOTIFIER, new ConnectionNotifier(result));
|
||||
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
|
||||
|
||||
ch.attr(CONNECTION_NOTIFIER).set(result);
|
||||
MinecraftConnection connection = new MinecraftConnection(ch);
|
||||
connection.setState(StateRegistry.HANDSHAKE);
|
||||
connection.setAssociation(ServerConnection.this);
|
||||
connection.setAssociation(VelocityServerConnection.this);
|
||||
ch.pipeline().addLast(HANDLER, connection);
|
||||
}
|
||||
})
|
||||
@ -72,7 +77,7 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
minecraftConnection = future.channel().pipeline().get(MinecraftConnection.class);
|
||||
|
||||
// Kick off the connection process
|
||||
minecraftConnection.setSessionHandler(new LoginSessionHandler(ServerConnection.this));
|
||||
minecraftConnection.setSessionHandler(new LoginSessionHandler(VelocityServerConnection.this));
|
||||
startHandshake();
|
||||
} else {
|
||||
result.completeExceptionally(future.cause());
|
||||
@ -93,11 +98,13 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
}
|
||||
|
||||
private void startHandshake() {
|
||||
PlayerInfoForwarding forwardingMode = VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode();
|
||||
|
||||
// Initiate a handshake.
|
||||
Handshake handshake = new Handshake();
|
||||
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
||||
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
|
||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) {
|
||||
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
||||
handshake.setServerAddress(createBungeeForwardingAddress());
|
||||
} else {
|
||||
handshake.setServerAddress(serverInfo.getAddress().getHostString());
|
||||
@ -109,17 +116,9 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
minecraftConnection.setProtocolVersion(protocolVersion);
|
||||
minecraftConnection.setState(StateRegistry.LOGIN);
|
||||
|
||||
// Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding.
|
||||
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
|
||||
VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) {
|
||||
ServerLogin login = new ServerLogin();
|
||||
login.setUsername(proxyPlayer.getUsername());
|
||||
minecraftConnection.write(login);
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectedPlayer getProxyPlayer() {
|
||||
return proxyPlayer;
|
||||
ServerLogin login = new ServerLogin();
|
||||
login.setUsername(proxyPlayer.getUsername());
|
||||
minecraftConnection.write(login);
|
||||
}
|
||||
|
||||
public MinecraftConnection getMinecraftConnection() {
|
||||
@ -130,6 +129,11 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectedPlayer getPlayer() {
|
||||
return proxyPlayer;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
minecraftConnection.close();
|
||||
minecraftConnection = null;
|
||||
@ -140,24 +144,13 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + serverInfo.getName();
|
||||
}
|
||||
|
||||
static class ConnectionNotifier extends ChannelInboundHandlerAdapter {
|
||||
private final CompletableFuture<ConnectionRequestBuilder.Result> result;
|
||||
|
||||
public ConnectionNotifier(CompletableFuture<ConnectionRequestBuilder.Result> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public CompletableFuture<ConnectionRequestBuilder.Result> getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void onComplete() {
|
||||
result.complete(ConnectionRequestResults.SUCCESSFUL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
result.completeExceptionally(cause);
|
||||
}
|
||||
@Override
|
||||
public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
|
||||
Preconditions.checkNotNull(identifier, "identifier");
|
||||
Preconditions.checkNotNull(data, "data");
|
||||
PluginMessage message = new PluginMessage();
|
||||
message.setChannel(identifier.getId());
|
||||
message.setData(data);
|
||||
minecraftConnection.write(message);
|
||||
}
|
||||
}
|
@ -1,47 +1,41 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelSide;
|
||||
import com.velocitypowered.api.proxy.messages.MessageHandler;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.backend.ServerConnection;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.data.scoreboard.Objective;
|
||||
import com.velocitypowered.proxy.data.scoreboard.Score;
|
||||
import com.velocitypowered.proxy.data.scoreboard.Scoreboard;
|
||||
import com.velocitypowered.proxy.data.scoreboard.Team;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.packet.*;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.remap.EntityIdRemapper;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import com.velocitypowered.proxy.util.ThrowableUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Handles communication with the connected Minecraft client. This is effectively the primary nerve center that
|
||||
* joins backend servers with players.
|
||||
*/
|
||||
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
|
||||
private static final int MAX_PLUGIN_CHANNELS = 128;
|
||||
|
||||
private final ConnectedPlayer player;
|
||||
private ScheduledFuture<?> pingTask;
|
||||
private long lastPing = -1;
|
||||
private boolean spawned = false;
|
||||
private final List<UUID> serverBossBars = new ArrayList<>();
|
||||
private final Set<String> clientPluginMsgChannels = new HashSet<>();
|
||||
private int currentDimension;
|
||||
private Scoreboard serverScoreboard = new Scoreboard();
|
||||
private EntityIdRemapper idRemapper;
|
||||
|
||||
public ClientPlaySessionHandler(ConnectedPlayer player) {
|
||||
@ -50,16 +44,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void activated() {
|
||||
EventLoop loop = player.getConnection().getChannel().eventLoop();
|
||||
pingTask = loop.scheduleAtFixedRate(this::ping, 5, 15, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void ping() {
|
||||
long randomId = ThreadLocalRandom.current().nextInt();
|
||||
lastPing = randomId;
|
||||
KeepAlive keepAlive = new KeepAlive();
|
||||
keepAlive.setRandomId(randomId);
|
||||
player.getConnection().write(keepAlive);
|
||||
PluginMessage message;
|
||||
if (player.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
|
||||
message = PluginMessageUtil.constructChannelsPacket("minecraft:register", VelocityServer.getServer().getChannelRegistrar().getModernChannelIds());
|
||||
} else {
|
||||
message = PluginMessageUtil.constructChannelsPacket("REGISTER", VelocityServer.getServer().getChannelRegistrar().getLegacyChannelIds());
|
||||
}
|
||||
player.getConnection().write(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,11 +58,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
if (packet instanceof KeepAlive) {
|
||||
KeepAlive keepAlive = (KeepAlive) packet;
|
||||
if (keepAlive.getRandomId() != lastPing) {
|
||||
throw new IllegalStateException("Client sent invalid keepAlive; expected " + lastPing + ", got " + keepAlive.getRandomId());
|
||||
// The last keep alive we got was probably from a different server. Let's ignore it, and hope the next
|
||||
// ping is alright.
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not forward the packet to the player's server, because we handle pings for all servers already.
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof ClientSettings) {
|
||||
@ -80,10 +70,49 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
if (packet instanceof Chat) {
|
||||
// Try to handle any commands on the proxy. If that fails, send it onto the client.
|
||||
Chat chat = (Chat) packet;
|
||||
if (chat.getMessage().equals("/connect")) {
|
||||
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566));
|
||||
player.createConnectionRequest(info).fireAndForget();
|
||||
String msg = ((Chat) packet).getMessage();
|
||||
if (msg.startsWith("/")) {
|
||||
try {
|
||||
if (!VelocityServer.getServer().getCommandManager().execute(player, msg.substring(1))) {
|
||||
player.getConnectedServer().getMinecraftConnection().write(chat);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.info("Exception occurred while running command for {}", player.getProfile().getName(), e);
|
||||
player.sendMessage(TextComponent.of("An error occurred while running this command.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
player.getConnectedServer().getMinecraftConnection().write(chat);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof TabCompleteRequest) {
|
||||
TabCompleteRequest req = (TabCompleteRequest) packet;
|
||||
int lastSpace = req.getCommand().indexOf(' ');
|
||||
if (!req.isAssumeCommand() && lastSpace != -1) {
|
||||
String command = req.getCommand().substring(1);
|
||||
try {
|
||||
Optional<List<String>> offers = VelocityServer.getServer().getCommandManager().offerSuggestions(player, command);
|
||||
if (offers.isPresent()) {
|
||||
TabCompleteResponse response = new TabCompleteResponse();
|
||||
response.setTransactionId(req.getTransactionId());
|
||||
response.setStart(lastSpace);
|
||||
response.setLength(req.getCommand().length() - lastSpace);
|
||||
|
||||
for (String s : offers.get()) {
|
||||
response.getOffers().add(new TabCompleteResponse.Offer(s, null));
|
||||
}
|
||||
|
||||
player.getConnection().write(response);
|
||||
} else {
|
||||
player.getConnectedServer().getMinecraftConnection().write(packet);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -106,11 +135,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public void disconnected() {
|
||||
player.teardown();
|
||||
|
||||
if (pingTask != null && !pingTask.isCancelled()) {
|
||||
pingTask.cancel(false);
|
||||
pingTask = null;
|
||||
}
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new DisconnectEvent(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -123,21 +148,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
public void handleBackendJoinGame(JoinGame joinGame) {
|
||||
lastPing = Long.MIN_VALUE; // reset last ping
|
||||
if (!spawned) {
|
||||
// nothing special to do here
|
||||
spawned = true;
|
||||
currentDimension = joinGame.getDimension();
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
idRemapper = EntityIdRemapper.getMapper(joinGame.getEntityId(), player.getConnection().getProtocolVersion());
|
||||
} else {
|
||||
// In order to handle switching to another server we will need send three packets:
|
||||
// Ah, this is the meat and potatoes of the whole venture!
|
||||
//
|
||||
// In order to handle switching to another server, you will need to send three packets:
|
||||
//
|
||||
// - The join game packet from the backend server
|
||||
// - A respawn packet with a different dimension
|
||||
// - Another respawn with the correct dimension
|
||||
//
|
||||
// We can't simply ignore the packet with the different dimension. If you try to be smart about it it doesn't
|
||||
// work.
|
||||
// The two respawns with different dimensions are required, otherwise the client gets confused.
|
||||
//
|
||||
// Most notably, by having the client accept the join game packet, we can work around the need to perform
|
||||
// entity ID rewrites, eliminating potential issues from rewriting packets and improving compatibility with
|
||||
@ -147,13 +173,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
|
||||
player.getConnection().delayedWrite(new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
|
||||
player.getConnection().delayedWrite(new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
|
||||
currentDimension = joinGame.getDimension();
|
||||
}
|
||||
|
||||
// Resend client settings packet to remote server if we have it, this preserves client settings across
|
||||
// transitions.
|
||||
if (player.getClientSettings() != null) {
|
||||
player.getConnectedServer().getMinecraftConnection().delayedWrite(player.getClientSettings());
|
||||
}
|
||||
|
||||
// Remove old boss bars.
|
||||
@ -165,15 +184,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
serverBossBars.clear();
|
||||
|
||||
// Remove scoreboard junk.
|
||||
clearServerScoreboard();
|
||||
|
||||
// Tell the server about this client's plugin messages. Velocity will forward them on to the client.
|
||||
if (!clientPluginMsgChannels.isEmpty()) {
|
||||
Collection<String> toRegister = new HashSet<>(clientPluginMsgChannels);
|
||||
if (player.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
|
||||
toRegister.addAll(VelocityServer.getServer().getChannelRegistrar().getModernChannelIds());
|
||||
} else {
|
||||
toRegister.addAll(VelocityServer.getServer().getChannelRegistrar().getLegacyChannelIds());
|
||||
}
|
||||
if (!toRegister.isEmpty()) {
|
||||
String channel = player.getConnection().getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13 ?
|
||||
"minecraft:register" : "REGISTER";
|
||||
player.getConnectedServer().getMinecraftConnection().delayedWrite(
|
||||
PluginMessageUtil.constructChannelsPacket(channel, clientPluginMsgChannels));
|
||||
player.getConnectedServer().getMinecraftConnection().delayedWrite(PluginMessageUtil.constructChannelsPacket(
|
||||
channel, toRegister));
|
||||
}
|
||||
|
||||
// Flush everything
|
||||
@ -181,10 +203,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
player.getConnectedServer().getMinecraftConnection().flush();
|
||||
}
|
||||
|
||||
public void setCurrentDimension(int currentDimension) {
|
||||
this.currentDimension = currentDimension;
|
||||
}
|
||||
|
||||
public List<UUID> getServerBossBars() {
|
||||
return serverBossBars;
|
||||
}
|
||||
@ -204,7 +222,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
if (actuallyRegistered.size() > 0) {
|
||||
logger.info("Rewritten register packet: {}", actuallyRegistered);
|
||||
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
|
||||
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
|
||||
}
|
||||
@ -222,88 +239,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're going to forward on the original packet.
|
||||
player.getConnectedServer().getMinecraftConnection().write(packet);
|
||||
}
|
||||
|
||||
public void handleServerScoreboardPacket(MinecraftPacket packet) {
|
||||
if (packet instanceof ScoreboardDisplay) {
|
||||
ScoreboardDisplay sd = (ScoreboardDisplay) packet;
|
||||
serverScoreboard.setPosition(sd.getPosition());
|
||||
serverScoreboard.setDisplayName(sd.getDisplayName());
|
||||
MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage(
|
||||
player, ChannelSide.FROM_CLIENT, packet);
|
||||
if (status == MessageHandler.ForwardStatus.FORWARD) {
|
||||
// We're going to forward on the original packet.
|
||||
player.getConnectedServer().getMinecraftConnection().write(packet);
|
||||
}
|
||||
|
||||
if (packet instanceof ScoreboardObjective) {
|
||||
ScoreboardObjective so = (ScoreboardObjective) packet;
|
||||
switch (so.getMode()) {
|
||||
case ScoreboardObjective.ADD:
|
||||
Objective o = new Objective(so.getId());
|
||||
o.setDisplayName(so.getDisplayName());
|
||||
o.setType(so.getType());
|
||||
serverScoreboard.getObjectives().put(so.getId(), o);
|
||||
break;
|
||||
case ScoreboardObjective.REMOVE:
|
||||
serverScoreboard.getObjectives().remove(so.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet instanceof ScoreboardSetScore) {
|
||||
ScoreboardSetScore sss = (ScoreboardSetScore) packet;
|
||||
Objective objective = serverScoreboard.getObjectives().get(sss.getObjective());
|
||||
if (objective == null) {
|
||||
return;
|
||||
}
|
||||
switch (sss.getAction()) {
|
||||
case ScoreboardSetScore.CHANGE:
|
||||
Score score = new Score(sss.getEntity(), sss.getValue());
|
||||
objective.getScores().put(sss.getEntity(), score);
|
||||
break;
|
||||
case ScoreboardSetScore.REMOVE:
|
||||
objective.getScores().remove(sss.getEntity());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet instanceof ScoreboardTeam) {
|
||||
ScoreboardTeam st = (ScoreboardTeam) packet;
|
||||
switch (st.getMode()) {
|
||||
case ScoreboardTeam.ADD:
|
||||
// TODO: Preserve other team information? We might not need to...
|
||||
Team team = new Team(st.getId());
|
||||
serverScoreboard.getTeams().put(st.getId(), team);
|
||||
break;
|
||||
case ScoreboardTeam.REMOVE:
|
||||
serverScoreboard.getTeams().remove(st.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearServerScoreboard() {
|
||||
for (Objective objective : serverScoreboard.getObjectives().values()) {
|
||||
for (Score score : objective.getScores().values()) {
|
||||
ScoreboardSetScore sss = new ScoreboardSetScore();
|
||||
sss.setObjective(objective.getId());
|
||||
sss.setAction(ScoreboardSetScore.REMOVE);
|
||||
sss.setEntity(score.getTarget());
|
||||
player.getConnection().delayedWrite(sss);
|
||||
}
|
||||
|
||||
ScoreboardObjective so = new ScoreboardObjective();
|
||||
so.setId(objective.getId());
|
||||
so.setMode(ScoreboardObjective.REMOVE);
|
||||
player.getConnection().delayedWrite(so);
|
||||
}
|
||||
|
||||
for (Team team : serverScoreboard.getTeams().values()) {
|
||||
ScoreboardTeam st = new ScoreboardTeam();
|
||||
st.setId(team.getId());
|
||||
st.setMode(ScoreboardTeam.REMOVE);
|
||||
player.getConnection().delayedWrite(st);
|
||||
}
|
||||
|
||||
serverScoreboard = new Scoreboard();
|
||||
}
|
||||
|
||||
public Set<String> getClientPluginMsgChannels() {
|
||||
@ -313,4 +254,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
public EntityIdRemapper getIdRemapper() {
|
||||
return idRemapper;
|
||||
}
|
||||
|
||||
public void setLastPing(long lastPing) {
|
||||
this.lastPing = lastPing;
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,29 @@ package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||
import com.velocitypowered.api.permission.PermissionFunction;
|
||||
import com.velocitypowered.api.permission.PermissionProvider;
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.util.MessagePosition;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.data.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.backend.ServerConnection;
|
||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import com.velocitypowered.proxy.util.ThrowableUtils;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
|
||||
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.TranslatableComponent;
|
||||
@ -25,8 +33,8 @@ import net.kyori.text.serializer.ComponentSerializers;
|
||||
import net.kyori.text.serializer.PlainComponentSerializer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -35,19 +43,23 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
|
||||
public static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
|
||||
|
||||
private final GameProfile profile;
|
||||
private final MinecraftConnection connection;
|
||||
private final InetSocketAddress virtualHost;
|
||||
private final GameProfile profile;
|
||||
private PermissionFunction permissionFunction = null;
|
||||
private int tryIndex = 0;
|
||||
private ServerConnection connectedServer;
|
||||
private VelocityServerConnection connectedServer;
|
||||
private ClientSettings clientSettings;
|
||||
private ServerConnection connectionInFlight;
|
||||
private VelocityServerConnection connectionInFlight;
|
||||
|
||||
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
|
||||
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection, InetSocketAddress virtualHost) {
|
||||
this.profile = profile;
|
||||
this.connection = connection;
|
||||
this.virtualHost = virtualHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -61,8 +73,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ServerInfo> getCurrentServer() {
|
||||
return connectedServer != null ? Optional.of(connectedServer.getServerInfo()) : Optional.empty();
|
||||
public Optional<ServerConnection> getCurrentServer() {
|
||||
return Optional.ofNullable(connectedServer);
|
||||
}
|
||||
|
||||
public GameProfile getProfile() {
|
||||
@ -78,13 +90,27 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.ofNullable(virtualHost);
|
||||
}
|
||||
|
||||
public void setPermissionFunction(PermissionFunction permissionFunction) {
|
||||
this.permissionFunction = permissionFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return connection.getChannel().isActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
|
||||
public int getProtocolVersion() {
|
||||
return connection.getProtocolVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NonNull Component component, @NonNull MessagePosition position) {
|
||||
Preconditions.checkNotNull(component, "component");
|
||||
Preconditions.checkNotNull(position, "position");
|
||||
|
||||
@ -107,11 +133,28 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
|
||||
public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) {
|
||||
return new ConnectionRequestBuilderImpl(info);
|
||||
}
|
||||
|
||||
public ServerConnection getConnectedServer() {
|
||||
@Override
|
||||
public void setHeaderAndFooter(@NonNull Component header, @NonNull Component footer) {
|
||||
Preconditions.checkNotNull(header, "header");
|
||||
Preconditions.checkNotNull(footer, "footer");
|
||||
connection.write(HeaderAndFooter.create(header, footer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearHeaderAndFooter() {
|
||||
connection.write(HeaderAndFooter.reset());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(Component reason) {
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
public VelocityServerConnection getConnectedServer() {
|
||||
return connectedServer;
|
||||
}
|
||||
|
||||
@ -127,7 +170,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
String error = ThrowableUtils.briefDescription(throwable);
|
||||
String userMessage;
|
||||
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
|
||||
logger.error("{}: exception occurred in connection to {}", this, info.getName(), throwable);
|
||||
userMessage = "Exception in server " + info.getName();
|
||||
} else {
|
||||
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
|
||||
@ -153,9 +195,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
|
||||
public void handleConnectionException(ServerInfo info, Component disconnectReason) {
|
||||
connectionInFlight = null;
|
||||
if (connectedServer == null || connectedServer.getServerInfo().equals(info)) {
|
||||
// The player isn't yet connected to a server or they are already connected to the server
|
||||
// they're disconnected from.
|
||||
if (connectedServer == null) {
|
||||
// The player isn't yet connected to a server.
|
||||
Optional<ServerInfo> nextServer = getNextServerToTry();
|
||||
if (nextServer.isPresent()) {
|
||||
createConnectionRequest(nextServer.get()).fireAndForget();
|
||||
} else {
|
||||
connection.closeWith(Disconnect.create(disconnectReason));
|
||||
}
|
||||
} else if (connectedServer.getServerInfo().equals(info)) {
|
||||
// Already connected to the server being disconnected from.
|
||||
// TODO: ServerKickEvent
|
||||
connection.closeWith(Disconnect.create(disconnectReason));
|
||||
} else {
|
||||
connection.write(Chat.create(disconnectReason));
|
||||
@ -187,11 +237,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
// Otherwise, initiate the connection.
|
||||
ServerConnection connection = new ServerConnection(request.getServer(), this, VelocityServer.getServer());
|
||||
return connection.connect();
|
||||
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
|
||||
return VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenCompose((newEvent) -> {
|
||||
if (!newEvent.getResult().isAllowed()) {
|
||||
return CompletableFuture.completedFuture(
|
||||
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED)
|
||||
);
|
||||
}
|
||||
|
||||
return new VelocityServerConnection(newEvent.getResult().getInfo().get(), this, VelocityServer.getServer()).connect();
|
||||
});
|
||||
}
|
||||
|
||||
public void setConnectedServer(ServerConnection serverConnection) {
|
||||
public void setConnectedServer(VelocityServerConnection serverConnection) {
|
||||
if (this.connectedServer != null && !serverConnection.getServerInfo().equals(connectedServer.getServerInfo())) {
|
||||
this.tryIndex = 0;
|
||||
}
|
||||
@ -209,6 +268,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
if (connectedServer != null) {
|
||||
connectedServer.disconnect();
|
||||
}
|
||||
VelocityServer.getServer().unregisterConnection(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -216,10 +276,25 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(String permission) {
|
||||
return permissionFunction.getPermissionSetting(permission).asBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
|
||||
Preconditions.checkNotNull(identifier, "identifier");
|
||||
Preconditions.checkNotNull(data, "data");
|
||||
PluginMessage message = new PluginMessage();
|
||||
message.setChannel(identifier.getId());
|
||||
message.setData(data);
|
||||
connection.write(message);
|
||||
}
|
||||
|
||||
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
||||
private final ServerInfo info;
|
||||
|
||||
public ConnectionRequestBuilderImpl(ServerInfo info) {
|
||||
ConnectionRequestBuilderImpl(ServerInfo info) {
|
||||
this.info = Preconditions.checkNotNull(info, "info");
|
||||
}
|
||||
|
||||
@ -236,7 +311,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
@Override
|
||||
public void fireAndForget() {
|
||||
connect()
|
||||
.whenComplete((status, throwable) -> {
|
||||
.whenCompleteAsync((status, throwable) -> {
|
||||
if (throwable != null) {
|
||||
handleConnectionException(info, throwable);
|
||||
return;
|
||||
@ -256,7 +331,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
handleConnectionException(info, Disconnect.create(status.getReason().orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR)));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, connection.getChannel().eventLoop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,28 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.Handshake;
|
||||
import com.velocitypowered.proxy.protocol.packet.*;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.TranslatableComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
private final MinecraftConnection connection;
|
||||
@ -19,16 +33,24 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
if (packet instanceof LegacyPing || packet instanceof LegacyHandshake) {
|
||||
connection.setProtocolVersion(ProtocolConstants.LEGACY);
|
||||
handleLegacy(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(packet instanceof Handshake)) {
|
||||
throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName());
|
||||
}
|
||||
|
||||
InitialInboundConnection ic = new InitialInboundConnection(connection, (Handshake) packet);
|
||||
|
||||
Handshake handshake = (Handshake) packet;
|
||||
switch (handshake.getNextStatus()) {
|
||||
case StateRegistry.STATUS_ID:
|
||||
connection.setState(StateRegistry.STATUS);
|
||||
connection.setProtocolVersion(handshake.getProtocolVersion());
|
||||
connection.setSessionHandler(new StatusSessionHandler(connection));
|
||||
connection.setSessionHandler(new StatusSessionHandler(connection, ic));
|
||||
break;
|
||||
case StateRegistry.LOGIN_ID:
|
||||
connection.setState(StateRegistry.LOGIN);
|
||||
@ -37,12 +59,70 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
|
||||
return;
|
||||
} else {
|
||||
connection.setSessionHandler(new LoginSessionHandler(connection));
|
||||
InetAddress address = ((InetSocketAddress) connection.getChannel().remoteAddress()).getAddress();
|
||||
if (!VelocityServer.getServer().getIpAttemptLimiter().attempt(address)) {
|
||||
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
|
||||
return;
|
||||
}
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
|
||||
connection.setSessionHandler(new LoginSessionHandler(connection, ic));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUnknown(ByteBuf buf) {
|
||||
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
|
||||
}
|
||||
|
||||
private void handleLegacy(MinecraftPacket packet) {
|
||||
if (packet instanceof LegacyPing) {
|
||||
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
|
||||
ServerPing ping = new ServerPing(
|
||||
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
|
||||
configuration.getMotdComponent(),
|
||||
null
|
||||
);
|
||||
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
// The disconnect packet is the same as the server response one.
|
||||
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(event.getPing())));
|
||||
}, connection.getChannel().eventLoop());
|
||||
} else if (packet instanceof LegacyHandshake) {
|
||||
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
|
||||
}
|
||||
}
|
||||
|
||||
private static class LegacyInboundConnection implements InboundConnection {
|
||||
private final MinecraftConnection connection;
|
||||
|
||||
private LegacyInboundConnection(MinecraftConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return !connection.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProtocolVersion() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.protocol.packet.Handshake;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
class InitialInboundConnection implements InboundConnection {
|
||||
private final MinecraftConnection connection;
|
||||
private final Handshake handshake;
|
||||
|
||||
InitialInboundConnection(MinecraftConnection connection, Handshake handshake) {
|
||||
this.connection = connection;
|
||||
this.handshake = handshake;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.of(InetSocketAddress.createUnresolved(handshake.getServerAddress(), handshake.getPort()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return connection.getChannel().isActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProtocolVersion() {
|
||||
return connection.getProtocolVersion();
|
||||
}
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.event.connection.LoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
|
||||
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
|
||||
import com.velocitypowered.api.event.player.GameProfileRequestEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.connection.VelocityConstants;
|
||||
import com.velocitypowered.proxy.data.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@ -10,8 +17,9 @@ import com.velocitypowered.proxy.protocol.packet.*;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
@ -19,6 +27,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
@ -30,82 +39,115 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
|
||||
private static final String MOJANG_SERVER_AUTH_URL =
|
||||
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
|
||||
|
||||
|
||||
private final MinecraftConnection inbound;
|
||||
private final InboundConnection apiInbound;
|
||||
private ServerLogin login;
|
||||
private byte[] verify;
|
||||
private int playerInfoId;
|
||||
|
||||
public LoginSessionHandler(MinecraftConnection inbound) {
|
||||
public LoginSessionHandler(MinecraftConnection inbound, InboundConnection apiInbound) {
|
||||
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
|
||||
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activated() {
|
||||
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
|
||||
LoginPluginMessage message = new LoginPluginMessage();
|
||||
playerInfoId = ThreadLocalRandom.current().nextInt();
|
||||
message.setId(playerInfoId);
|
||||
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
|
||||
message.setData(Unpooled.EMPTY_BUFFER);
|
||||
inbound.write(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) throws Exception {
|
||||
public void handle(MinecraftPacket packet) {
|
||||
if (packet instanceof LoginPluginResponse) {
|
||||
LoginPluginResponse lpr = (LoginPluginResponse) packet;
|
||||
if (lpr.getId() == playerInfoId && lpr.isSuccess()) {
|
||||
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
|
||||
inbound.closeWith(Disconnect.create(
|
||||
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
|
||||
));
|
||||
if (lpr.getId() == playerInfoId) {
|
||||
if (lpr.isSuccess()) {
|
||||
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
|
||||
inbound.closeWith(Disconnect.create(
|
||||
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
|
||||
));
|
||||
} else {
|
||||
// Proceed with the regular login process.
|
||||
beginPreLogin();
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ServerLogin) {
|
||||
this.login = (ServerLogin) packet;
|
||||
|
||||
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
|
||||
// Request encryption.
|
||||
EncryptionRequest request = generateRequest();
|
||||
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
|
||||
inbound.write(request);
|
||||
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
|
||||
LoginPluginMessage message = new LoginPluginMessage();
|
||||
playerInfoId = ThreadLocalRandom.current().nextInt();
|
||||
message.setId(playerInfoId);
|
||||
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
|
||||
message.setData(Unpooled.EMPTY_BUFFER);
|
||||
inbound.write(message);
|
||||
} else {
|
||||
// Offline-mode, don't try to request encryption.
|
||||
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
|
||||
beginPreLogin();
|
||||
}
|
||||
} else if (packet instanceof EncryptionResponse) {
|
||||
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
|
||||
EncryptionResponse response = (EncryptionResponse) packet;
|
||||
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
|
||||
if (!Arrays.equals(verify, decryptedVerifyToken)) {
|
||||
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
|
||||
try {
|
||||
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
|
||||
EncryptionResponse response = (EncryptionResponse) packet;
|
||||
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
|
||||
if (!Arrays.equals(verify, decryptedVerifyToken)) {
|
||||
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
|
||||
}
|
||||
|
||||
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
|
||||
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
|
||||
|
||||
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
|
||||
VelocityServer.getServer().getHttpClient()
|
||||
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
||||
.thenAcceptAsync(profileResponse -> {
|
||||
if (inbound.isClosed()) {
|
||||
// The player disconnected after we authenticated them.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
inbound.enableEncryption(decryptedSharedSecret);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
initializePlayer(VelocityServer.GSON.fromJson(profileResponse, GameProfile.class), true);
|
||||
}, inbound.getChannel().eventLoop())
|
||||
.exceptionally(exception -> {
|
||||
logger.error("Unable to enable encryption", exception);
|
||||
inbound.close();
|
||||
return null;
|
||||
});
|
||||
} catch (GeneralSecurityException e) {
|
||||
logger.error("Unable to enable encryption", e);
|
||||
inbound.close();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
|
||||
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
|
||||
|
||||
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
|
||||
VelocityServer.getServer().getHttpClient()
|
||||
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
||||
.thenAcceptAsync(profileResponse -> {
|
||||
try {
|
||||
inbound.enableEncryption(decryptedSharedSecret);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
|
||||
handleSuccessfulLogin(profile);
|
||||
}, inbound.getChannel().eventLoop())
|
||||
.exceptionally(exception -> {
|
||||
logger.error("Unable to enable encryption", exception);
|
||||
inbound.close();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void beginPreLogin() {
|
||||
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
if (inbound.isClosed()) {
|
||||
// The player was disconnected
|
||||
return;
|
||||
}
|
||||
PreLoginComponentResult result = event.getResult();
|
||||
if (!result.isAllowed()) {
|
||||
// The component is guaranteed to be provided if the connection was denied.
|
||||
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (VelocityServer.getServer().getConfiguration().isOnlineMode() || result.isOnlineModeAllowed()) {
|
||||
// Request encryption.
|
||||
EncryptionRequest request = generateRequest();
|
||||
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
|
||||
inbound.write(request);
|
||||
} else {
|
||||
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()), false);
|
||||
}
|
||||
}, inbound.getChannel().eventLoop());
|
||||
}
|
||||
|
||||
private EncryptionRequest generateRequest() {
|
||||
byte[] verify = new byte[4];
|
||||
ThreadLocalRandom.current().nextBytes(verify);
|
||||
@ -116,9 +158,40 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
return request;
|
||||
}
|
||||
|
||||
private void handleSuccessfulLogin(GameProfile profile) {
|
||||
// Initiate a regular connection and move over to it.
|
||||
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
|
||||
private void initializePlayer(GameProfile profile, boolean onlineMode) {
|
||||
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
|
||||
|
||||
VelocityServer.getServer().getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
|
||||
// Initiate a regular connection and move over to it.
|
||||
ConnectedPlayer player = new ConnectedPlayer(profileEvent.getGameProfile(), inbound,
|
||||
apiInbound.getVirtualHost().orElse(null));
|
||||
|
||||
return VelocityServer.getServer().getEventManager().fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
|
||||
.thenCompose(event -> {
|
||||
// wait for permissions to load, then set the players permission function
|
||||
player.setPermissionFunction(event.createFunction(player));
|
||||
// then call & wait for the login event
|
||||
return VelocityServer.getServer().getEventManager().fire(new LoginEvent(player));
|
||||
})
|
||||
// then complete the connection
|
||||
.thenAcceptAsync(event -> {
|
||||
if (inbound.isClosed()) {
|
||||
// The player was disconnected
|
||||
return;
|
||||
}
|
||||
if (!event.getResult().isAllowed()) {
|
||||
// The component is guaranteed to be provided if the connection was denied.
|
||||
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
|
||||
return;
|
||||
}
|
||||
|
||||
handleProxyLogin(player);
|
||||
}, inbound.getChannel().eventLoop());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void handleProxyLogin(ConnectedPlayer player) {
|
||||
Optional<ServerInfo> toTry = player.getNextServerToTry();
|
||||
if (!toTry.isPresent()) {
|
||||
player.close(TextComponent.of("No available servers", TextColor.RED));
|
||||
@ -132,14 +205,24 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
ServerLoginSuccess success = new ServerLoginSuccess();
|
||||
success.setUsername(profile.getName());
|
||||
success.setUuid(profile.idAsUuid());
|
||||
success.setUsername(player.getUsername());
|
||||
success.setUuid(player.getUniqueId());
|
||||
inbound.write(success);
|
||||
|
||||
logger.info("{} has connected", player);
|
||||
inbound.setAssociation(player);
|
||||
inbound.setState(StateRegistry.PLAY);
|
||||
|
||||
if (!VelocityServer.getServer().registerConnection(player)) {
|
||||
inbound.closeWith(Disconnect.create(TextComponent.of("You are already on this proxy!", TextColor.RED)));
|
||||
}
|
||||
|
||||
logger.info("{} has connected", player);
|
||||
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
|
||||
player.createConnectionRequest(toTry.get()).fireAndForget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUnknown(ByteBuf buf) {
|
||||
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,34 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusPing;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.data.ServerPing;
|
||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
|
||||
public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
private final MinecraftConnection connection;
|
||||
private final InboundConnection inboundWrapper;
|
||||
|
||||
public StatusSessionHandler(MinecraftConnection connection) {
|
||||
public StatusSessionHandler(MinecraftConnection connection, InboundConnection inboundWrapper) {
|
||||
this.connection = connection;
|
||||
this.inboundWrapper = inboundWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
Preconditions.checkArgument(packet instanceof StatusPing|| packet instanceof StatusRequest,
|
||||
Preconditions.checkArgument(packet instanceof StatusPing || packet instanceof StatusRequest,
|
||||
"Unrecognized packet type " + packet.getClass().getName());
|
||||
|
||||
if (packet instanceof StatusPing) {
|
||||
@ -34,15 +40,22 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
|
||||
|
||||
// Status request
|
||||
ServerPing ping = new ServerPing(
|
||||
new ServerPing.Version(connection.getProtocolVersion(), "Velocity 1.9-1.13"),
|
||||
new ServerPing.Players(0, configuration.getShowMaxPlayers()),
|
||||
int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection.getProtocolVersion() :
|
||||
ProtocolConstants.MAXIMUM_GENERIC_VERSION;
|
||||
ServerPing initialPing = new ServerPing(
|
||||
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
|
||||
configuration.getMotdComponent(),
|
||||
null
|
||||
configuration.getFavicon()
|
||||
);
|
||||
StatusResponse response = new StatusResponse();
|
||||
response.setStatus(VelocityServer.GSON.toJson(ping));
|
||||
connection.write(response);
|
||||
|
||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
StatusResponse response = new StatusResponse();
|
||||
response.setStatus(VelocityServer.GSON.toJson(event.getPing()));
|
||||
connection.write(response);
|
||||
}, connection.getChannel().eventLoop());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -3,6 +3,7 @@ package com.velocitypowered.proxy.connection.util;
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -42,4 +43,18 @@ public class ConnectionRequestResults {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ConnectionRequestBuilder.Result forDisconnect(TextComponent component) {
|
||||
return new ConnectionRequestBuilder.Result() {
|
||||
@Override
|
||||
public ConnectionRequestBuilder.Status getStatus() {
|
||||
return ConnectionRequestBuilder.Status.SERVER_DISCONNECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Component> getReason() {
|
||||
return Optional.of(component);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
package com.velocitypowered.proxy.console;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
import net.minecrell.terminalconsole.SimpleTerminalConsole;
|
||||
import org.jline.reader.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class VelocityConsole extends SimpleTerminalConsole {
|
||||
|
||||
private final VelocityServer server;
|
||||
|
||||
public VelocityConsole(VelocityServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LineReader buildReader(LineReaderBuilder builder) {
|
||||
return super.buildReader(builder
|
||||
.appName("Velocity")
|
||||
.completer((reader, parsedLine, list) -> {
|
||||
Optional<List<String>> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line());
|
||||
o.ifPresent(offers -> {
|
||||
for (String offer : offers) {
|
||||
list.add(new Candidate(offer));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isRunning() {
|
||||
return !this.server.isShutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void runCommand(String command) {
|
||||
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) {
|
||||
server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
}
|
Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden Mehr anzeigen
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren