geforkt von Mirrors/Velocity
Plugin API (#34)
The Velocity API has had a lot of community input (special thanks to @hugmanrique who started the work, @lucko who contributed permissions support, and @Minecrell for providing initial feedback and an initial version of ServerListPlus). While the API is far from complete, there is enough available for people to start doing useful stuff with Velocity.
Dieser Commit ist enthalten in:
Ursprung
8e836a5066
Commit
a028467e66
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,6 +11,7 @@
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/**/compiler.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
|
@ -1,6 +1,13 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'com.github.johnrengelman.shadow' version '2.0.4'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
ap {
|
||||
compileClasspath += main.compileClasspath + main.output
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -8,6 +15,10 @@ dependencies {
|
||||
compile "com.google.guava:guava:${guavaVersion}"
|
||||
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,6 +31,15 @@ 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 {
|
||||
@ -27,3 +47,22 @@ artifacts {
|
||||
archives shadowJar
|
||||
archives sourcesJar
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Set up a Maven repository on Velocity's infrastructure, preferably something lightweight.
|
||||
/*repositories {
|
||||
maven {
|
||||
name = 'myRepo'
|
||||
url = "file://${buildDir}/repo"
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
@ -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,125 @@
|
||||
package com.velocitypowered.api.plugin.ap;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SerializedPluginDescription {
|
||||
private final String id;
|
||||
private final String author;
|
||||
private final String main;
|
||||
private final String version;
|
||||
private final List<Dependency> dependencies;
|
||||
|
||||
public SerializedPluginDescription(String id, String author, String main, String version) {
|
||||
this(id, author, main, version, ImmutableList.of());
|
||||
}
|
||||
|
||||
public SerializedPluginDescription(String id, String author, String main, String version, List<Dependency> dependencies) {
|
||||
this.id = Preconditions.checkNotNull(id, "id");
|
||||
this.author = Preconditions.checkNotNull(author, "author");
|
||||
this.main = Preconditions.checkNotNull(main, "main");
|
||||
this.version = Preconditions.checkNotNull(version, "version");
|
||||
this.dependencies = ImmutableList.copyOf(dependencies);
|
||||
}
|
||||
|
||||
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.author(), qualifiedName, plugin.version(), dependencies);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<Dependency> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@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(author, that.author) &&
|
||||
Objects.equals(main, that.main) &&
|
||||
Objects.equals(version, that.version) &&
|
||||
Objects.equals(dependencies, that.dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, author, main, version, dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SerializedPluginDescription{" +
|
||||
"id='" + id + '\'' +
|
||||
", author='" + author + '\'' +
|
||||
", main='" + main + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", dependencies=" + dependencies +
|
||||
'}';
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package com.velocitypowered.api.command;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a command that can be executed by a {@link CommandInvoker}, such as a {@link com.velocitypowered.api.proxy.Player}
|
||||
* or the console.
|
||||
*/
|
||||
public interface CommandExecutor {
|
||||
/**
|
||||
* Executes the command for the specified {@link CommandInvoker}.
|
||||
* @param invoker the invoker of this command
|
||||
* @param args the arguments for this command
|
||||
*/
|
||||
void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args);
|
||||
|
||||
/**
|
||||
* Provides tab complete suggestions for a command for a specified {@link CommandInvoker}.
|
||||
* @param invoker the invoker to run the command for
|
||||
* @param currentArgs the current, partial arguments for this command
|
||||
* @return tab complete suggestions
|
||||
*/
|
||||
default List<String> suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package com.velocitypowered.api.command;
|
||||
|
||||
import net.kyori.text.Component;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Represents something that can be used to run a {@link CommandExecutor}.
|
||||
*/
|
||||
public interface CommandInvoker {
|
||||
/**
|
||||
* Sends the specified {@code component} to the invoker.
|
||||
* @param component the text component to send
|
||||
*/
|
||||
void sendMessage(@Nonnull Component component);
|
||||
|
||||
/**
|
||||
* Determines whether or not the invoker has a particular permission.
|
||||
* @param permission the permission to check for
|
||||
* @return whether or not the invoker has permission to run this command
|
||||
*/
|
||||
boolean hasPermission(@Nonnull String permission);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
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 {
|
||||
void register(@NonNull Command command, String... aliases);
|
||||
|
||||
void unregister(@NonNull String alias);
|
||||
|
||||
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);
|
||||
}
|
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;
|
||||
|
||||
}
|
109
api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java
Normale Datei
109
api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java
Normale Datei
@ -0,0 +1,109 @@
|
||||
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.
|
||||
* @param result the new result
|
||||
*/
|
||||
void setResult(@NonNull R result);
|
||||
|
||||
/**
|
||||
* Represents a result for an event.
|
||||
*/
|
||||
interface Result {
|
||||
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;
|
||||
|
||||
private 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,49 @@
|
||||
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 org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* 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<ResultedEvent.ComponentResult> {
|
||||
private final InboundConnection connection;
|
||||
private final String username;
|
||||
private ComponentResult result;
|
||||
|
||||
public PreLoginEvent(InboundConnection connection, String username) {
|
||||
this.connection = Preconditions.checkNotNull(connection, "connection");
|
||||
this.username = Preconditions.checkNotNull(username, "username");
|
||||
this.result = ComponentResult.allowed();
|
||||
}
|
||||
|
||||
public InboundConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(@NonNull ComponentResult result) {
|
||||
this.result = Preconditions.checkNotNull(result, "result");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PreLoginEvent{" +
|
||||
"connection=" + connection +
|
||||
", username='" + username + '\'' +
|
||||
", result=" + result +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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,35 @@
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.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.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,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,37 @@
|
||||
package com.velocitypowered.api.event.proxy;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.api.server.ServerPing;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
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(@Nonnull 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,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;
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
44
api/src/main/java/com/velocitypowered/api/plugin/Plugin.java
Normale Datei
44
api/src/main/java/com/velocitypowered/api/plugin/Plugin.java
Normale Datei
@ -0,0 +1,44 @@
|
||||
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 version of the plugin.
|
||||
*
|
||||
* @return the version of the plugin, or an empty string if unknown
|
||||
*/
|
||||
String version() default "";
|
||||
|
||||
/**
|
||||
* The author of the plugin.
|
||||
*
|
||||
* @return the plugin's author, or empty if unknown
|
||||
*/
|
||||
String author() 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
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.Optional;
|
||||
import java.util.Set;
|
||||
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 version of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return the plugin version
|
||||
* @see Plugin#version()
|
||||
*/
|
||||
String getVersion();
|
||||
|
||||
/**
|
||||
* Gets the author of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return the plugin author
|
||||
* @see Plugin#author()
|
||||
*/
|
||||
String getAuthor();
|
||||
|
||||
/**
|
||||
* 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,79 @@
|
||||
package com.velocitypowered.api.plugin.meta;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
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 the plugin version, or {@code null} if unspecified
|
||||
*/
|
||||
@Nullable
|
||||
public String getVersion() {
|
||||
return 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import net.kyori.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@ -15,14 +16,14 @@ 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
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a connection to the proxy. There is no guarantee that the connection has been fully initialized.
|
||||
@ -12,6 +13,12 @@ public interface InboundConnection {
|
||||
*/
|
||||
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
|
||||
|
@ -1,18 +1,18 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.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.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a player who is connected to the proxy.
|
||||
*/
|
||||
public interface Player extends CommandInvoker, InboundConnection {
|
||||
public interface Player extends CommandSource, InboundConnection {
|
||||
/**
|
||||
* Returns the player's current username.
|
||||
* @return the username
|
||||
@ -35,7 +35,7 @@ public interface Player extends CommandInvoker, InboundConnection {
|
||||
* 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);
|
||||
}
|
||||
|
||||
@ -44,12 +44,12 @@ public interface Player extends CommandInvoker, InboundConnection {
|
||||
* @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);
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
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.scheduler.Scheduler;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
@ -9,7 +13,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a Minecraft proxy server that follows the Velocity API.
|
||||
* Represents a Minecraft proxy server that is compatible with the Velocity API.
|
||||
*/
|
||||
public interface ProxyServer {
|
||||
/**
|
||||
@ -65,10 +69,36 @@ public interface ProxyServer {
|
||||
void unregisterServer(@Nonnull ServerInfo server);
|
||||
|
||||
/**
|
||||
* Returns an instance of {@link CommandInvoker} that can be used to determine if the command is being invoked by
|
||||
* 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 CommandInvoker} instead of using the console invoker.
|
||||
* {@link CommandSource} instead of using the console invoker.
|
||||
* @return the console command invoker
|
||||
*/
|
||||
CommandInvoker getConsoleCommandInvoker();
|
||||
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();
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
package com.velocitypowered.api.scheduler;
|
||||
|
||||
/**
|
||||
* Represents a task that is scheduled to run on the proxy.
|
||||
*/
|
||||
public interface ScheduledTask {
|
||||
Object plugin();
|
||||
|
||||
TaskStatus status();
|
||||
|
||||
void cancel();
|
||||
}
|
22
api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java
Normale Datei
22
api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java
Normale Datei
@ -0,0 +1,22 @@
|
||||
package com.velocitypowered.api.scheduler;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Represents a scheduler to execute tasks on the proxy.
|
||||
*/
|
||||
public interface Scheduler {
|
||||
TaskBuilder buildTask(Object plugin, Runnable runnable);
|
||||
|
||||
interface TaskBuilder {
|
||||
TaskBuilder delay(int time, TimeUnit unit);
|
||||
|
||||
TaskBuilder repeat(int time, TimeUnit unit);
|
||||
|
||||
TaskBuilder clearDelay();
|
||||
|
||||
TaskBuilder clearRepeat();
|
||||
|
||||
ScheduledTask schedule();
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.api.scheduler;
|
||||
|
||||
public enum TaskStatus {
|
||||
SCHEDULED,
|
||||
CANCELLED,
|
||||
FINISHED
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package com.velocitypowered.api.server;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@ -25,7 +25,7 @@ public final class Favicon {
|
||||
* of functions.
|
||||
* @param base64Url the url for use with this favicon
|
||||
*/
|
||||
public Favicon(@Nonnull String base64Url) {
|
||||
public Favicon(@NonNull String base64Url) {
|
||||
this.base64Url = Preconditions.checkNotNull(base64Url, "base64Url");
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ public final class Favicon {
|
||||
* @param image the image to use for the favicon
|
||||
* @return the created {@link Favicon} instance
|
||||
*/
|
||||
public static Favicon create(@Nonnull BufferedImage image) {
|
||||
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());
|
||||
@ -79,8 +79,9 @@ public final class Favicon {
|
||||
* 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 {
|
||||
public static Favicon create(@NonNull Path path) throws IOException {
|
||||
try (InputStream stream = Files.newInputStream(path)) {
|
||||
return create(ImageIO.read(stream));
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.velocitypowered.api.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;
|
||||
}
|
||||
|
||||
|
240
api/src/main/java/com/velocitypowered/api/server/ServerPing.java
Normale Datei
240
api/src/main/java/com/velocitypowered/api/server/ServerPing.java
Normale Datei
@ -0,0 +1,240 @@
|
||||
package com.velocitypowered.api.server;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
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();
|
||||
}
|
||||
|
||||
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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
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;
|
||||
@ -12,7 +12,7 @@ public class GameProfile {
|
||||
private final String name;
|
||||
private final List<Property> properties;
|
||||
|
||||
public GameProfile(String id, String name, List<Property> properties) {
|
||||
public GameProfile(@NonNull String id, @NonNull String name, @NonNull List<Property> properties) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.properties = ImmutableList.copyOf(properties);
|
||||
@ -34,7 +34,7 @@ public class GameProfile {
|
||||
return ImmutableList.copyOf(properties);
|
||||
}
|
||||
|
||||
public static GameProfile forOfflinePlayer(String username) {
|
||||
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());
|
||||
@ -54,7 +54,7 @@ public class GameProfile {
|
||||
private final String value;
|
||||
private final String signature;
|
||||
|
||||
public Property(String name, String value, String signature) {
|
||||
public Property(@NonNull String name, @NonNull String value, @NonNull String signature) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.signature = signature;
|
@ -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("");
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
package com.velocitypowered.proxy.util;
|
||||
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;
|
||||
|
||||
public enum UuidUtils {
|
||||
;
|
||||
public class UuidUtils {
|
||||
private UuidUtils() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static UUID fromUndashed(final String string) {
|
||||
public static @NonNull UUID fromUndashed(final @NonNull String string) {
|
||||
Objects.requireNonNull(string, "string");
|
||||
Preconditions.checkArgument(string.length() == 32, "Length is incorrect");
|
||||
return new UUID(
|
||||
@ -18,12 +21,12 @@ public enum UuidUtils {
|
||||
);
|
||||
}
|
||||
|
||||
public static String toUndashed(final UUID 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);
|
||||
}
|
||||
|
||||
public static UUID generateOfflinePlayerUuid(String username) {
|
||||
public static @NonNull UUID generateOfflinePlayerUuid(@NonNull String username) {
|
||||
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ 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'
|
||||
@ -30,4 +31,4 @@ allprojects {
|
||||
junitXml.enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +32,14 @@ dependencies {
|
||||
|
||||
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}"
|
||||
|
@ -4,11 +4,15 @@ 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.api.command.CommandInvoker;
|
||||
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.server.Favicon;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.command.ServerCommand;
|
||||
import com.velocitypowered.proxy.command.ShutdownCommand;
|
||||
@ -16,9 +20,12 @@ 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.CommandManager;
|
||||
import com.velocitypowered.proxy.command.VelocityCommandManager;
|
||||
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;
|
||||
@ -40,6 +47,7 @@ import java.nio.file.Paths;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class VelocityServer implements ProxyServer {
|
||||
@ -55,13 +63,14 @@ public class VelocityServer implements ProxyServer {
|
||||
private NettyHttpClient httpClient;
|
||||
private KeyPair serverKeyPair;
|
||||
private final ServerMap servers = new ServerMap();
|
||||
private final CommandManager commandManager = new CommandManager();
|
||||
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 CommandInvoker consoleCommandInvoker = new CommandInvoker() {
|
||||
private final CommandSource consoleCommandSource = new CommandSource() {
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Component component) {
|
||||
logger.info(ComponentSerializers.LEGACY.serialize(component));
|
||||
@ -73,11 +82,13 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
};
|
||||
private Ratelimiter ipAttemptLimiter;
|
||||
private VelocityEventManager eventManager;
|
||||
private VelocityScheduler scheduler;
|
||||
|
||||
private VelocityServer() {
|
||||
commandManager.registerCommand("velocity", new VelocityCommand());
|
||||
commandManager.registerCommand("server", new ServerCommand());
|
||||
commandManager.registerCommand("shutdown", new ShutdownCommand());
|
||||
commandManager.register(new VelocityCommand(), "velocity");
|
||||
commandManager.register(new ServerCommand(), "server");
|
||||
commandManager.register(new ShutdownCommand(), "shutdown");
|
||||
}
|
||||
|
||||
public static VelocityServer getServer() {
|
||||
@ -92,7 +103,8 @@ public class VelocityServer implements ProxyServer {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public CommandManager getCommandManager() {
|
||||
@Override
|
||||
public VelocityCommandManager getCommandManager() {
|
||||
return commandManager;
|
||||
}
|
||||
|
||||
@ -121,10 +133,21 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
|
||||
|
||||
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
|
||||
|
||||
httpClient = new NettyHttpClient(this);
|
||||
eventManager = new VelocityEventManager(pluginManager);
|
||||
scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM);
|
||||
loadPlugins();
|
||||
|
||||
// Post the first event
|
||||
pluginManager.getPlugins().forEach(container -> {
|
||||
container.getInstance().ifPresent(plugin -> eventManager.register(plugin, plugin));
|
||||
});
|
||||
try {
|
||||
eventManager.fire(new ProxyInitializeEvent()).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
// Ignore, we don't care.
|
||||
}
|
||||
|
||||
this.cm.bind(configuration.getBind());
|
||||
|
||||
@ -133,6 +156,29 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
||||
}
|
||||
|
||||
public ServerMap getServers() {
|
||||
return servers;
|
||||
}
|
||||
@ -154,6 +200,14 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
this.cm.shutdown();
|
||||
|
||||
eventManager.fire(new ProxyShutdownEvent());
|
||||
try {
|
||||
eventManager.shutdown();
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Your plugins took over 10 seconds to shut down.");
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@ -226,7 +280,22 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandInvoker getConsoleCommandInvoker() {
|
||||
return consoleCommandInvoker;
|
||||
public CommandSource getConsoleCommandSource() {
|
||||
return consoleCommandSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginManager getPluginManager() {
|
||||
return pluginManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VelocityScheduler getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CommandManager {
|
||||
private final Map<String, CommandExecutor> executors = new HashMap<>();
|
||||
|
||||
public void registerCommand(String name, CommandExecutor executor) {
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
Preconditions.checkNotNull(executor, "executor");
|
||||
this.executors.put(name, executor);
|
||||
}
|
||||
|
||||
public void unregisterCommand(String name) {
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
this.executors.remove(name);
|
||||
}
|
||||
|
||||
public boolean execute(CommandInvoker invoker, String cmdLine) {
|
||||
Preconditions.checkNotNull(invoker, "invoker");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String command = split[0];
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
CommandExecutor executor = executors.get(command);
|
||||
if (executor == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
executor.execute(invoker, actualArgs);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + invoker, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<List<String>> offerSuggestions(CommandInvoker invoker, String cmdLine) {
|
||||
Preconditions.checkNotNull(invoker, "invoker");
|
||||
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(executors.keySet().stream()
|
||||
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
CommandExecutor executor = executors.get(command);
|
||||
if (executor == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(executor.suggest(invoker, actualArgs));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + invoker, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
@ -14,15 +14,15 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ServerCommand implements CommandExecutor {
|
||||
public class ServerCommand implements Command {
|
||||
@Override
|
||||
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
|
||||
if (!(invoker instanceof Player)) {
|
||||
invoker.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
|
||||
public void execute(@Nonnull CommandSource source, @Nonnull String[] args) {
|
||||
if (!(source instanceof Player)) {
|
||||
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = (Player) invoker;
|
||||
Player player = (Player) source;
|
||||
if (args.length == 1) {
|
||||
// Trying to connect to a server.
|
||||
String serverName = args[0];
|
||||
@ -42,7 +42,7 @@ public class ServerCommand implements CommandExecutor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) {
|
||||
public List<String> suggest(@Nonnull CommandSource source, @Nonnull String[] currentArgs) {
|
||||
if (currentArgs.length == 0) {
|
||||
return VelocityServer.getServer().getAllServers().stream()
|
||||
.map(ServerInfo::getName)
|
||||
|
@ -1,18 +1,18 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
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;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class ShutdownCommand implements CommandExecutor {
|
||||
public class ShutdownCommand implements Command {
|
||||
@Override
|
||||
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
|
||||
if (invoker != VelocityServer.getServer().getConsoleCommandInvoker()) {
|
||||
invoker.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
|
||||
public void execute(@Nonnull CommandSource source, @Nonnull 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();
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
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;
|
||||
@ -9,9 +9,9 @@ import net.kyori.text.format.TextColor;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class VelocityCommand implements CommandExecutor {
|
||||
public class VelocityCommand implements Command {
|
||||
@Override
|
||||
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
|
||||
public void execute(@Nonnull CommandSource source, @Nonnull String[] args) {
|
||||
String implVersion = VelocityServer.class.getPackage().getImplementationVersion();
|
||||
TextComponent thisIsVelocity = TextComponent.builder()
|
||||
.content("This is ")
|
||||
@ -35,8 +35,8 @@ public class VelocityCommand implements CommandExecutor {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
invoker.sendMessage(thisIsVelocity);
|
||||
invoker.sendMessage(velocityInfo);
|
||||
invoker.sendMessage(velocityWebsite);
|
||||
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,5 +1,7 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
@ -16,7 +18,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
public void activated() {
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new ServerConnectedEvent(connection.getProxyPlayer(),
|
||||
connection.getServerInfo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
//Not handleable packets: Chat, TabCompleteResponse, Respawn, Scoreboard*
|
||||
if (!connection.getProxyPlayer().isActive()) {
|
||||
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
|
||||
|
@ -7,7 +7,7 @@ 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;
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
@ -82,19 +83,19 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
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);
|
||||
return;
|
||||
} 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);
|
||||
TabCompleteResponse response = new TabCompleteResponse();
|
||||
response.setTransactionId(req.getTransactionId());
|
||||
player.getConnection().write(response);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +117,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public void disconnected() {
|
||||
player.teardown();
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new DisconnectEvent(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2,6 +2,9 @@ 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.util.MessagePosition;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
@ -9,7 +12,7 @@ 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;
|
||||
@ -25,6 +28,7 @@ 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;
|
||||
@ -35,19 +39,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 PermissionFunction permissionFunction = null;
|
||||
private int tryIndex = 0;
|
||||
private ServerConnection connectedServer;
|
||||
private ClientSettings clientSettings;
|
||||
private ServerConnection 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
|
||||
@ -78,6 +86,15 @@ 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();
|
||||
@ -89,7 +106,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
|
||||
public void sendMessage(@NonNull Component component, @NonNull MessagePosition position) {
|
||||
Preconditions.checkNotNull(component, "component");
|
||||
Preconditions.checkNotNull(position, "position");
|
||||
|
||||
@ -112,7 +129,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
|
||||
public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) {
|
||||
return new ConnectionRequestBuilderImpl(info);
|
||||
}
|
||||
|
||||
@ -191,8 +208,17 @@ 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 ServerConnection(newEvent.getResult().getInfo().get(), this, VelocityServer.getServer()).connect();
|
||||
});
|
||||
}
|
||||
|
||||
public void setConnectedServer(ServerConnection serverConnection) {
|
||||
@ -223,13 +249,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@Nonnull String permission) {
|
||||
return false; // TODO: Implement permissions.
|
||||
return permissionFunction.getPermissionSetting(permission).asBoolean();
|
||||
}
|
||||
|
||||
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
||||
private final ServerInfo info;
|
||||
|
||||
public ConnectionRequestBuilderImpl(ServerInfo info) {
|
||||
ConnectionRequestBuilderImpl(ServerInfo info) {
|
||||
this.info = Preconditions.checkNotNull(info, "info");
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
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.proxy.data.ServerPing;
|
||||
import com.velocitypowered.api.server.ServerPing;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@ -17,6 +20,7 @@ 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;
|
||||
@ -37,12 +41,14 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
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);
|
||||
@ -56,7 +62,8 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
|
||||
return;
|
||||
}
|
||||
connection.setSessionHandler(new LoginSessionHandler(connection));
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
|
||||
connection.setSessionHandler(new LoginSessionHandler(connection, ic));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@ -69,21 +76,25 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
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()),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
|
||||
configuration.getMotdComponent(),
|
||||
null
|
||||
);
|
||||
// The disconnect packet is the same as the server response one.
|
||||
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(ping)));
|
||||
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 InitialInboundConnection implements InboundConnection {
|
||||
private static class LegacyInboundConnection implements InboundConnection {
|
||||
private final MinecraftConnection connection;
|
||||
|
||||
private InitialInboundConnection(MinecraftConnection connection) {
|
||||
private LegacyInboundConnection(MinecraftConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@ -92,14 +103,19 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return connection.getChannel().isActive();
|
||||
return !connection.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProtocolVersion() {
|
||||
return connection.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,13 @@
|
||||
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.permission.PermissionsSetupEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.api.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,7 +15,6 @@ 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.Unpooled;
|
||||
import net.kyori.text.TextComponent;
|
||||
@ -33,12 +37,14 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
"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
|
||||
@ -53,7 +59,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
));
|
||||
} else {
|
||||
// Proceed with the regular login process.
|
||||
initiateLogin();
|
||||
beginPreLogin();
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ServerLogin) {
|
||||
@ -67,7 +73,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
message.setData(Unpooled.EMPTY_BUFFER);
|
||||
inbound.write(message);
|
||||
} else {
|
||||
initiateLogin();
|
||||
beginPreLogin();
|
||||
}
|
||||
} else if (packet instanceof EncryptionResponse) {
|
||||
try {
|
||||
@ -97,7 +103,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
|
||||
handleSuccessfulLogin(profile);
|
||||
initializePlayer(profile);
|
||||
}, inbound.getChannel().eventLoop())
|
||||
.exceptionally(exception -> {
|
||||
logger.error("Unable to enable encryption", exception);
|
||||
@ -113,16 +119,26 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private void initiateLogin() {
|
||||
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
|
||||
// Request encryption.
|
||||
EncryptionRequest request = generateRequest();
|
||||
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
|
||||
inbound.write(request);
|
||||
} else {
|
||||
// Offline-mode, don't try to request encryption.
|
||||
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
|
||||
}
|
||||
private void beginPreLogin() {
|
||||
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
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;
|
||||
}
|
||||
|
||||
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
|
||||
// Request encryption.
|
||||
EncryptionRequest request = generateRequest();
|
||||
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
|
||||
inbound.write(request);
|
||||
} else {
|
||||
// Offline-mode, don't try to request encryption.
|
||||
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()));
|
||||
}
|
||||
}, inbound.getChannel().eventLoop());
|
||||
}
|
||||
|
||||
private EncryptionRequest generateRequest() {
|
||||
@ -135,9 +151,31 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
return request;
|
||||
}
|
||||
|
||||
private void handleSuccessfulLogin(GameProfile profile) {
|
||||
private void initializePlayer(GameProfile profile) {
|
||||
// Initiate a regular connection and move over to it.
|
||||
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
|
||||
ConnectedPlayer player = new ConnectedPlayer(profile, inbound, apiInbound.getVirtualHost().orElse(null));
|
||||
|
||||
// load permissions first
|
||||
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 (!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));
|
||||
@ -151,8 +189,8 @@ 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);
|
||||
|
||||
inbound.setAssociation(player);
|
||||
|
@ -1,6 +1,9 @@
|
||||
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;
|
||||
@ -9,16 +12,18 @@ 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.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
|
||||
@ -37,15 +42,20 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
// Status request
|
||||
int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection.getProtocolVersion() :
|
||||
ProtocolConstants.MAXIMUM_GENERIC_VERSION;
|
||||
ServerPing ping = new ServerPing(
|
||||
ServerPing initialPing = new ServerPing(
|
||||
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
|
||||
configuration.getMotdComponent(),
|
||||
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
|
||||
|
@ -22,13 +22,12 @@ public final class VelocityConsole extends SimpleTerminalConsole {
|
||||
return super.buildReader(builder
|
||||
.appName("Velocity")
|
||||
.completer((reader, parsedLine, list) -> {
|
||||
Optional<List<String>> offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line());
|
||||
if (offers.isPresent()) {
|
||||
for (String offer : offers.get()) {
|
||||
if (offer.isEmpty()) continue;
|
||||
Optional<List<String>> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line());
|
||||
o.ifPresent(offers -> {
|
||||
for (String offer : offers) {
|
||||
list.add(new Candidate(offer));
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -40,8 +39,8 @@ public final class VelocityConsole extends SimpleTerminalConsole {
|
||||
|
||||
@Override
|
||||
protected void runCommand(String command) {
|
||||
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandInvoker(), command)) {
|
||||
server.getConsoleCommandInvoker().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
|
||||
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) {
|
||||
server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
package com.velocitypowered.proxy.data;
|
||||
|
||||
import com.velocitypowered.api.server.Favicon;
|
||||
import net.kyori.text.Component;
|
||||
|
||||
public class ServerPing {
|
||||
private final Version version;
|
||||
private final Players players;
|
||||
private final Component description;
|
||||
private final Favicon favicon;
|
||||
|
||||
public ServerPing(Version version, Players players, Component description, Favicon favicon) {
|
||||
this.version = version;
|
||||
this.players = players;
|
||||
this.description = description;
|
||||
this.favicon = favicon;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Players getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
||||
public Component getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Favicon getFavicon() {
|
||||
return favicon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerPing{" +
|
||||
"version=" + version +
|
||||
", players=" + players +
|
||||
", 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;
|
||||
|
||||
public Players(int online, int max) {
|
||||
this.online = online;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public int getOnline() {
|
||||
return online;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Players{" +
|
||||
"online=" + online +
|
||||
", max=" + max +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
public class PluginClassLoader extends URLClassLoader {
|
||||
private static final Set<PluginClassLoader> loaders = new CopyOnWriteArraySet<>();
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
public PluginClassLoader(URL[] urls) {
|
||||
super(urls);
|
||||
loaders.add(this);
|
||||
}
|
||||
|
||||
public void addPath(Path path) {
|
||||
try {
|
||||
addURL(path.toUri().toURL());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
return loadClass0(name, resolve, true);
|
||||
}
|
||||
|
||||
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException {
|
||||
try {
|
||||
return super.loadClass(name, resolve);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// Ignored: we'll try others
|
||||
}
|
||||
|
||||
if (checkOther) {
|
||||
for (PluginClassLoader loader : loaders) {
|
||||
if (loader != this) {
|
||||
try {
|
||||
return loader.loadClass0(name, resolve, false);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// We're trying others, safe to ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.event.EventHandler;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.proxy.util.concurrency.ThreadRecorderThreadFactory;
|
||||
import net.kyori.event.EventSubscriber;
|
||||
import net.kyori.event.PostResult;
|
||||
import net.kyori.event.SimpleEventBus;
|
||||
import net.kyori.event.method.*;
|
||||
import net.kyori.event.method.asm.ASMEventExecutorFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class VelocityEventManager implements EventManager {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityEventManager.class);
|
||||
|
||||
private final ListMultimap<Object, Object> registeredListenersByPlugin = Multimaps
|
||||
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
|
||||
private final ListMultimap<Object, EventHandler<?>> registeredHandlersByPlugin = Multimaps
|
||||
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
|
||||
private final VelocityEventBus bus = new VelocityEventBus(
|
||||
new ASMEventExecutorFactory<>(new PluginClassLoader(new URL[0])),
|
||||
new VelocityMethodScanner());
|
||||
private final ExecutorService service;
|
||||
private final ThreadRecorderThreadFactory recordingThreadFactory;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
public VelocityEventManager(PluginManager pluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.recordingThreadFactory = new ThreadRecorderThreadFactory(new ThreadFactoryBuilder()
|
||||
.setNameFormat("Velocity Event Executor - #%d").setDaemon(true).build());
|
||||
this.service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), recordingThreadFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Object plugin, @NonNull Object listener) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(listener, "listener");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
|
||||
registeredListenersByPlugin.put(plugin, listener);
|
||||
bus.register(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(eventClass, "eventClass");
|
||||
Preconditions.checkNotNull(postOrder, "postOrder");
|
||||
Preconditions.checkNotNull(handler, "listener");
|
||||
bus.register(eventClass, new KyoriToVelocityHandler<>(handler, postOrder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> @NonNull CompletableFuture<E> fire(@NonNull E event) {
|
||||
Preconditions.checkNotNull(event, "event");
|
||||
if (!bus.hasSubscribers(event.getClass())) {
|
||||
// Optimization: nobody's listening.
|
||||
return CompletableFuture.completedFuture(event);
|
||||
}
|
||||
|
||||
Runnable runEvent = () -> {
|
||||
PostResult result = bus.post(event);
|
||||
if (!result.exceptions().isEmpty()) {
|
||||
logger.error("Some errors occurred whilst posting event {}.", event);
|
||||
int i = 0;
|
||||
for (Throwable exception : result.exceptions().values()) {
|
||||
logger.error("#{}: \n", i++, exception);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (recordingThreadFactory.currentlyInFactory()) {
|
||||
// Optimization: fire the event immediately, we are on the event handling thread.
|
||||
runEvent.run();
|
||||
return CompletableFuture.completedFuture(event);
|
||||
}
|
||||
|
||||
CompletableFuture<E> eventFuture = new CompletableFuture<>();
|
||||
service.execute(() -> {
|
||||
runEvent.run();
|
||||
eventFuture.complete(event);
|
||||
});
|
||||
return eventFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListeners(@NonNull Object plugin) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
|
||||
Collection<Object> listeners = registeredListenersByPlugin.removeAll(plugin);
|
||||
listeners.forEach(bus::unregister);
|
||||
Collection<EventHandler<?>> handlers = registeredHandlersByPlugin.removeAll(plugin);
|
||||
handlers.forEach(bus::unregister);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListener(@NonNull Object plugin, @NonNull Object listener) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(listener, "listener");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
|
||||
registeredListenersByPlugin.remove(plugin, listener);
|
||||
bus.unregister(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(handler, "listener");
|
||||
registeredHandlersByPlugin.remove(plugin, handler);
|
||||
bus.unregister(handler);
|
||||
}
|
||||
|
||||
public void shutdown() throws InterruptedException {
|
||||
service.shutdown();
|
||||
service.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static class VelocityEventBus extends SimpleEventBus<Object> {
|
||||
private final MethodSubscriptionAdapter<Object> methodAdapter;
|
||||
|
||||
VelocityEventBus(EventExecutor.@NonNull Factory<Object, Object> factory, @NonNull MethodScanner<Object> methodScanner) {
|
||||
super(Object.class);
|
||||
this.methodAdapter = new SimpleMethodSubscriptionAdapter<>(this, factory, methodScanner);
|
||||
}
|
||||
|
||||
void register(Object listener) {
|
||||
this.methodAdapter.register(listener);
|
||||
}
|
||||
|
||||
void unregister(Object listener) {
|
||||
this.methodAdapter.unregister(listener);
|
||||
}
|
||||
|
||||
void unregister(EventHandler<?> handler) {
|
||||
this.unregister(s -> s instanceof KyoriToVelocityHandler && ((KyoriToVelocityHandler<?>) s).getHandler().equals(handler));
|
||||
}
|
||||
}
|
||||
|
||||
private static class VelocityMethodScanner implements MethodScanner<Object> {
|
||||
@Override
|
||||
public boolean shouldRegister(@NonNull Object listener, @NonNull Method method) {
|
||||
return method.isAnnotationPresent(Subscribe.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int postOrder(@NonNull Object listener, @NonNull Method method) {
|
||||
return method.getAnnotation(Subscribe.class).order().ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeCancelledEvents(@NonNull Object listener, @NonNull Method method) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class KyoriToVelocityHandler<E> implements EventSubscriber<E> {
|
||||
private final EventHandler<E> handler;
|
||||
private final int postOrder;
|
||||
|
||||
private KyoriToVelocityHandler(EventHandler<E> handler, PostOrder postOrder) {
|
||||
this.handler = handler;
|
||||
this.postOrder = postOrder.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(@NonNull E event) {
|
||||
handler.execute(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int postOrder() {
|
||||
return postOrder;
|
||||
}
|
||||
|
||||
public EventHandler<E> getHandler() {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.plugin.loader.JavaPluginLoader;
|
||||
import com.velocitypowered.proxy.plugin.util.PluginDependencyUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class VelocityPluginManager implements PluginManager {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class);
|
||||
|
||||
private final Map<String, PluginContainer> plugins = new HashMap<>();
|
||||
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
||||
private final VelocityServer server;
|
||||
|
||||
public VelocityPluginManager(VelocityServer server) {
|
||||
this.server = checkNotNull(server, "server");
|
||||
}
|
||||
|
||||
private void registerPlugin(@NonNull PluginContainer plugin) {
|
||||
plugins.put(plugin.getDescription().getId(), plugin);
|
||||
plugin.getInstance().ifPresent(instance -> pluginInstances.put(instance, plugin));
|
||||
}
|
||||
|
||||
public void loadPlugins(@NonNull Path directory) throws IOException {
|
||||
checkNotNull(directory, "directory");
|
||||
checkArgument(Files.isDirectory(directory), "provided path isn't a directory");
|
||||
|
||||
List<PluginDescription> found = new ArrayList<>();
|
||||
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, p -> Files.isRegularFile(p) && p.toString().endsWith(".jar"))) {
|
||||
for (Path path : stream) {
|
||||
try {
|
||||
found.add(loader.loadPlugin(path));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to load plugin {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found.isEmpty()) {
|
||||
// No plugins found
|
||||
return;
|
||||
}
|
||||
|
||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
|
||||
|
||||
// Now load the plugins
|
||||
pluginLoad:
|
||||
for (PluginDescription plugin : sortedPlugins) {
|
||||
// Verify dependencies
|
||||
for (PluginDependency dependency : plugin.getDependencies()) {
|
||||
if (!dependency.isOptional() && !isLoaded(dependency.getId())) {
|
||||
logger.error("Can't load plugin {} due to missing dependency {}", plugin.getId(), dependency.getId());
|
||||
continue pluginLoad;
|
||||
}
|
||||
}
|
||||
|
||||
// Actually create the plugin
|
||||
PluginContainer pluginObject;
|
||||
|
||||
try {
|
||||
pluginObject = loader.createPlugin(plugin);
|
||||
} catch (Exception e) {
|
||||
logger.error("Can't create plugin {}", plugin.getId(), e);
|
||||
continue;
|
||||
}
|
||||
|
||||
registerPlugin(pluginObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
|
||||
checkNotNull(instance, "instance");
|
||||
|
||||
if (instance instanceof PluginContainer) {
|
||||
return Optional.of((PluginContainer) instance);
|
||||
}
|
||||
|
||||
return Optional.ofNullable(pluginInstances.get(instance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
|
||||
checkNotNull(id, "id");
|
||||
return Optional.ofNullable(plugins.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<PluginContainer> getPlugins() {
|
||||
return Collections.unmodifiableCollection(plugins.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded(@NonNull String id) {
|
||||
return plugins.containsKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
|
||||
checkNotNull(plugin, "instance");
|
||||
checkNotNull(path, "path");
|
||||
checkArgument(pluginInstances.containsKey(plugin), "plugin is not loaded");
|
||||
|
||||
ClassLoader pluginClassloader = plugin.getClass().getClassLoader();
|
||||
if (pluginClassloader instanceof PluginClassLoader) {
|
||||
((PluginClassLoader) pluginClassloader).addPath(path);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Operation is not supported on non-Java Velocity plugins.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.velocitypowered.api.plugin.*;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.plugin.PluginClassLoader;
|
||||
import com.velocitypowered.proxy.plugin.loader.java.JavaVelocityPluginDescription;
|
||||
import com.velocitypowered.proxy.plugin.loader.java.SerializedPluginDescription;
|
||||
import com.velocitypowered.proxy.plugin.loader.java.VelocityPluginModule;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class JavaPluginLoader implements PluginLoader {
|
||||
private final ProxyServer server;
|
||||
private final Path baseDirectory;
|
||||
|
||||
public JavaPluginLoader(ProxyServer server, Path baseDirectory) {
|
||||
this.server = server;
|
||||
this.baseDirectory = baseDirectory;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PluginDescription loadPlugin(Path source) throws Exception {
|
||||
Optional<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
|
||||
|
||||
if (!serialized.isPresent()) {
|
||||
throw new InvalidPluginException("Did not find a valid velocity-info.json.");
|
||||
}
|
||||
|
||||
PluginClassLoader loader = new PluginClassLoader(
|
||||
new URL[] {source.toUri().toURL() }
|
||||
);
|
||||
|
||||
Class mainClass = loader.loadClass(serialized.get().getMain());
|
||||
VelocityPluginDescription description = createDescription(serialized.get(), source, mainClass);
|
||||
|
||||
String pluginId = description.getId();
|
||||
Pattern pattern = PluginDescription.ID_PATTERN;
|
||||
|
||||
if (!pattern.matcher(pluginId).matches()) {
|
||||
throw new InvalidPluginException("Plugin ID '" + pluginId + "' must match pattern " + pattern.pattern());
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PluginContainer createPlugin(PluginDescription description) throws Exception {
|
||||
if (!(description instanceof JavaVelocityPluginDescription)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
|
||||
}
|
||||
|
||||
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
|
||||
Optional<Path> source = javaDescription.getSource();
|
||||
|
||||
if (!source.isPresent()) {
|
||||
throw new IllegalArgumentException("No path in plugin description");
|
||||
}
|
||||
|
||||
Injector injector = Guice.createInjector(new VelocityPluginModule(server, javaDescription, baseDirectory));
|
||||
Object instance = injector.getInstance(javaDescription.getMainClass());
|
||||
|
||||
return new VelocityPluginContainer(
|
||||
description.getId(),
|
||||
description.getVersion(),
|
||||
description.getAuthor(),
|
||||
description.getDependencies(),
|
||||
source.get(),
|
||||
instance
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source) throws Exception {
|
||||
try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(source)))) {
|
||||
JarEntry entry;
|
||||
while ((entry = in.getNextJarEntry()) != null) {
|
||||
if (entry.getName().equals("velocity-plugin.json")) {
|
||||
try (Reader pluginInfoReader = new InputStreamReader(in)) {
|
||||
return Optional.of(VelocityServer.GSON.fromJson(pluginInfoReader, SerializedPluginDescription.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private VelocityPluginDescription createDescription(SerializedPluginDescription description, Path source, Class mainClass) {
|
||||
Set<PluginDependency> dependencies = new HashSet<>();
|
||||
|
||||
for (SerializedPluginDescription.Dependency dependency : description.getDependencies()) {
|
||||
dependencies.add(toDependencyMeta(dependency));
|
||||
}
|
||||
|
||||
return new JavaVelocityPluginDescription(
|
||||
description.getId(),
|
||||
description.getVersion(),
|
||||
description.getAuthor(),
|
||||
dependencies,
|
||||
source,
|
||||
mainClass
|
||||
);
|
||||
}
|
||||
|
||||
private static PluginDependency toDependencyMeta(SerializedPluginDescription.Dependency dependency) {
|
||||
return new PluginDependency(
|
||||
dependency.getId(),
|
||||
null, // TODO Implement version matching in dependency annotation
|
||||
dependency.isOptional()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* This interface is used for loading plugins.
|
||||
*/
|
||||
public interface PluginLoader {
|
||||
@Nonnull
|
||||
PluginDescription loadPlugin(Path source) throws Exception;
|
||||
|
||||
@Nonnull
|
||||
PluginContainer createPlugin(PluginDescription plugin) throws Exception;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public class VelocityPluginContainer extends VelocityPluginDescription implements PluginContainer {
|
||||
private final Object instance;
|
||||
|
||||
public VelocityPluginContainer(String id, String version, String author, Collection<PluginDependency> dependencies, Path source, Object instance) {
|
||||
super(id, version, author, dependencies, source);
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginDescription getDescription() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<?> getInstance() {
|
||||
return Optional.ofNullable(instance);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class VelocityPluginDescription implements PluginDescription {
|
||||
private final String id;
|
||||
private final String version;
|
||||
private final String author;
|
||||
private final Map<String, PluginDependency> dependencies;
|
||||
private final Path source;
|
||||
|
||||
public VelocityPluginDescription(String id, String version, String author, Collection<PluginDependency> dependencies, Path source) {
|
||||
this.id = checkNotNull(id, "id");
|
||||
this.version = checkNotNull(version, "version");
|
||||
this.author = checkNotNull(author, "author");
|
||||
this.dependencies = Maps.uniqueIndex(dependencies, PluginDependency::getId);
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PluginDependency> getDependencies() {
|
||||
return dependencies.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PluginDependency> getDependency(String id) {
|
||||
return Optional.ofNullable(dependencies.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getSource() {
|
||||
return Optional.ofNullable(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VelocityPluginDescription{" +
|
||||
"id='" + id + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", author='" + author + '\'' +
|
||||
", dependencies=" + dependencies +
|
||||
", source=" + source +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class JavaVelocityPluginDescription extends VelocityPluginDescription {
|
||||
private final Class mainClass;
|
||||
|
||||
public JavaVelocityPluginDescription(String id, String version, String author, Collection<PluginDependency> dependencies, Path source, Class mainClass) {
|
||||
super(id, version, author, dependencies, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
public Class getMainClass() {
|
||||
return mainClass;
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SerializedPluginDescription {
|
||||
private final String id;
|
||||
private final String author;
|
||||
private final String main;
|
||||
private final String version;
|
||||
private final List<Dependency> dependencies;
|
||||
|
||||
public SerializedPluginDescription(String id, String author, String main, String version) {
|
||||
this(id, author, main, version, ImmutableList.of());
|
||||
}
|
||||
|
||||
public SerializedPluginDescription(String id, String author, String main, String version, List<Dependency> dependencies) {
|
||||
this.id = Preconditions.checkNotNull(id, "id");
|
||||
this.author = Preconditions.checkNotNull(author, "author");
|
||||
this.main = Preconditions.checkNotNull(main, "main");
|
||||
this.version = Preconditions.checkNotNull(version, "version");
|
||||
this.dependencies = ImmutableList.copyOf(dependencies);
|
||||
}
|
||||
|
||||
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.author(), qualifiedName, plugin.version(), dependencies);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<Dependency> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@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(author, that.author) &&
|
||||
Objects.equals(main, that.main) &&
|
||||
Objects.equals(version, that.version) &&
|
||||
Objects.equals(dependencies, that.dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, author, main, version, dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SerializedPluginDescription{" +
|
||||
"id='" + id + '\'' +
|
||||
", author='" + author + '\'' +
|
||||
", main='" + main + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", dependencies=" + dependencies +
|
||||
'}';
|
||||
}
|
||||
|
||||
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,38 @@
|
||||
package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.google.inject.Binder;
|
||||
import com.google.inject.Module;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class VelocityPluginModule implements Module {
|
||||
private final ProxyServer server;
|
||||
private final JavaVelocityPluginDescription description;
|
||||
private final Path basePluginPath;
|
||||
|
||||
public VelocityPluginModule(ProxyServer server, JavaVelocityPluginDescription description, Path basePluginPath) {
|
||||
this.server = server;
|
||||
this.description = description;
|
||||
this.basePluginPath = basePluginPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Binder binder) {
|
||||
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
|
||||
binder.bind(ProxyServer.class).toInstance(server);
|
||||
binder.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(basePluginPath.resolve(description.getId()));
|
||||
binder.bind(PluginDescription.class).toInstance(description);
|
||||
binder.bind(PluginManager.class).toInstance(server.getPluginManager());
|
||||
binder.bind(EventManager.class).toInstance(server.getEventManager());
|
||||
binder.bind(CommandManager.class).toInstance(server.getCommandManager());
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.graph.Graph;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class PluginDependencyUtils {
|
||||
public static List<PluginDescription> sortCandidates(List<PluginDescription> candidates) {
|
||||
// Create our graph, we're going to be using this for Kahn's algorithm.
|
||||
MutableGraph<PluginDescription> graph = GraphBuilder.directed().allowsSelfLoops(false).build();
|
||||
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(candidates, PluginDescription::getId);
|
||||
|
||||
// Add edges
|
||||
for (PluginDescription description : candidates) {
|
||||
graph.addNode(description);
|
||||
|
||||
for (PluginDependency dependency : description.getDependencies()) {
|
||||
PluginDescription in = candidateMap.get(dependency.getId());
|
||||
|
||||
if (in != null) {
|
||||
graph.putEdge(description, in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find nodes that have no edges
|
||||
Queue<PluginDescription> noEdges = getNoDependencyCandidates(graph);
|
||||
|
||||
// Actually run Kahn's algorithm
|
||||
List<PluginDescription> sorted = new ArrayList<>();
|
||||
while (!noEdges.isEmpty()) {
|
||||
PluginDescription candidate = noEdges.poll();
|
||||
sorted.add(candidate);
|
||||
|
||||
for (PluginDescription node : graph.successors(candidate)) {
|
||||
graph.removeEdge(node, candidate);
|
||||
|
||||
if (graph.adjacentNodes(node).isEmpty()) {
|
||||
if (!noEdges.contains(node)) {
|
||||
noEdges.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!graph.edges().isEmpty()) {
|
||||
throw new IllegalStateException("Plugin circular dependency found: " + graph.toString());
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
public static Queue<PluginDescription> getNoDependencyCandidates(Graph<PluginDescription> graph) {
|
||||
Queue<PluginDescription> found = new ArrayDeque<>();
|
||||
|
||||
for (PluginDescription node : graph.nodes()) {
|
||||
if (graph.outDegree(node) == 0) {
|
||||
found.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.velocitypowered.proxy.data.ServerPing;
|
||||
import com.velocitypowered.api.server.ServerPing;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
|
||||
public class LegacyPingResponse {
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
public interface Sleeper {
|
||||
void sleep(long ms) throws InterruptedException;
|
||||
|
||||
Sleeper SYSTEM = Thread::sleep;
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.Scheduler;
|
||||
import com.velocitypowered.api.scheduler.TaskStatus;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class VelocityScheduler implements Scheduler {
|
||||
private final PluginManager pluginManager;
|
||||
private final ExecutorService taskService;
|
||||
private final Sleeper sleeper;
|
||||
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedListMultimap(
|
||||
Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
|
||||
|
||||
public VelocityScheduler(PluginManager pluginManager, Sleeper sleeper) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.sleeper = sleeper;
|
||||
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
|
||||
.setNameFormat("Velocity Task Scheduler - #%d").build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder buildTask(Object plugin, Runnable runnable) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(runnable, "runnable");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "plugin is not registered");
|
||||
return new TaskBuilderImpl(plugin, runnable);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
for (ScheduledTask task : ImmutableList.copyOf(tasksByPlugin.values())) {
|
||||
task.cancel();
|
||||
}
|
||||
taskService.shutdown();
|
||||
}
|
||||
|
||||
private class TaskBuilderImpl implements TaskBuilder {
|
||||
private final Object plugin;
|
||||
private final Runnable runnable;
|
||||
private long delay; // ms
|
||||
private long repeat; // ms
|
||||
|
||||
private TaskBuilderImpl(Object plugin, Runnable runnable) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder delay(int time, TimeUnit unit) {
|
||||
this.delay = unit.toMillis(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder repeat(int time, TimeUnit unit) {
|
||||
this.repeat = unit.toMillis(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder clearDelay() {
|
||||
this.delay = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder clearRepeat() {
|
||||
this.repeat = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledTask schedule() {
|
||||
VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat);
|
||||
taskService.execute(task);
|
||||
tasksByPlugin.put(plugin, task);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private class VelocityTask implements Runnable, ScheduledTask {
|
||||
private final Object plugin;
|
||||
private final Runnable runnable;
|
||||
private final long delay;
|
||||
private final long repeat;
|
||||
private volatile TaskStatus status;
|
||||
private Thread taskThread;
|
||||
|
||||
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
this.delay = delay;
|
||||
this.repeat = repeat;
|
||||
this.status = TaskStatus.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskStatus status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (status == TaskStatus.SCHEDULED) {
|
||||
status = TaskStatus.CANCELLED;
|
||||
if (taskThread != null) {
|
||||
taskThread.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
taskThread = Thread.currentThread();
|
||||
if (delay > 0) {
|
||||
try {
|
||||
sleeper.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
if (status == TaskStatus.CANCELLED) {
|
||||
onFinish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (status != TaskStatus.CANCELLED) {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin);
|
||||
}
|
||||
|
||||
if (repeat > 0) {
|
||||
try {
|
||||
sleeper.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
if (status == TaskStatus.CANCELLED) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status = TaskStatus.FINISHED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onFinish();
|
||||
}
|
||||
|
||||
private void onFinish() {
|
||||
tasksByPlugin.remove(plugin, this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.velocitypowered.proxy.util.concurrency;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
/**
|
||||
* Represents a {@link ThreadFactory} that records the threads it has spawned.
|
||||
*/
|
||||
public class ThreadRecorderThreadFactory implements ThreadFactory {
|
||||
private final ThreadFactory backing;
|
||||
private final Set<Thread> threads = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public ThreadRecorderThreadFactory(ThreadFactory backing) {
|
||||
this.backing = Preconditions.checkNotNull(backing, "backing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Preconditions.checkNotNull(runnable, "runnable");
|
||||
return backing.newThread(() -> {
|
||||
threads.add(Thread.currentThread());
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
threads.remove(Thread.currentThread());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean currentlyInFactory() {
|
||||
return threads.contains(Thread.currentThread());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int size() {
|
||||
return threads.size();
|
||||
}
|
||||
}
|
@ -2,7 +2,13 @@
|
||||
<Configuration status="warn">
|
||||
<Appenders>
|
||||
<TerminalConsole name="TerminalConsole">
|
||||
<PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
|
||||
<PatternLayout>
|
||||
<LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level] [%logger]: %minecraftFormatting{%msg}%n%xEx}">
|
||||
<!-- Velocity doesn't need a prefix -->
|
||||
<PatternMatch key="com.velocitypowered."
|
||||
pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
|
||||
</LoggerNamePatternSelector>
|
||||
</PatternLayout>
|
||||
</TerminalConsole>
|
||||
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"
|
||||
immediateFlush="false">
|
||||
|
@ -0,0 +1,51 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.TaskStatus;
|
||||
import com.velocitypowered.proxy.testutil.FakePluginManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class VelocitySchedulerTest {
|
||||
// TODO: The timings here will be inaccurate on slow systems. Need to find a testing-friendly replacement for Thread.sleep()
|
||||
|
||||
@Test
|
||||
void buildTask() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown).schedule();
|
||||
latch.await();
|
||||
assertEquals(TaskStatus.FINISHED, task.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelWorks() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
|
||||
AtomicInteger i = new AtomicInteger(3);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, i::decrementAndGet)
|
||||
.delay(100, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
task.cancel();
|
||||
Thread.sleep(200);
|
||||
assertEquals(3, i.get());
|
||||
assertEquals(TaskStatus.CANCELLED, task.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatTaskWorks() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown)
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.repeat(100, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
latch.await();
|
||||
task.cancel();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package com.velocitypowered.proxy.testutil;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FakePluginManager implements PluginManager {
|
||||
public static final Object PLUGIN_A = new Object();
|
||||
public static final Object PLUGIN_B = new Object();
|
||||
|
||||
public static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A);
|
||||
public static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B);
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
|
||||
if (instance == PLUGIN_A) {
|
||||
return Optional.of(PC_A);
|
||||
} else if (instance == PLUGIN_B) {
|
||||
return Optional.of(PC_B);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
|
||||
switch (id) {
|
||||
case "a":
|
||||
return Optional.of(PC_A);
|
||||
case "b":
|
||||
return Optional.of(PC_B);
|
||||
default:
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<PluginContainer> getPlugins() {
|
||||
return ImmutableList.of(PC_A, PC_B);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded(@NonNull String id) {
|
||||
return id.equals("a") || id.equals("b");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private static class FakePluginContainer implements PluginContainer {
|
||||
private final String id;
|
||||
private final Object instance;
|
||||
|
||||
private FakePluginContainer(String id, Object instance) {
|
||||
this.id = id;
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull PluginDescription getDescription() {
|
||||
return new PluginDescription() {
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<?> getInstance() {
|
||||
return Optional.of(instance);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
import com.velocitypowered.api.util.UuidUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
@ -0,0 +1,36 @@
|
||||
package com.velocitypowered.proxy.util.concurrency;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ThreadRecorderThreadFactoryTest {
|
||||
|
||||
@Test
|
||||
void newThread() throws Exception {
|
||||
ThreadRecorderThreadFactory factory = new ThreadRecorderThreadFactory(Executors.defaultThreadFactory());
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch endThread = new CountDownLatch(1);
|
||||
factory.newThread(() -> {
|
||||
started.countDown();
|
||||
assertTrue(factory.currentlyInFactory());
|
||||
assertEquals(1, factory.size());
|
||||
try {
|
||||
endThread.await();
|
||||
} catch (InterruptedException e) {
|
||||
fail(e);
|
||||
}
|
||||
}).start();
|
||||
started.await();
|
||||
assertFalse(factory.currentlyInFactory());
|
||||
assertEquals(1, factory.size());
|
||||
endThread.countDown();
|
||||
|
||||
// Wait a little bit to ensure the thread got shut down
|
||||
Thread.sleep(10);
|
||||
assertEquals(0, factory.size());
|
||||
}
|
||||
}
|
@ -6,4 +6,6 @@ include (
|
||||
)
|
||||
findProject(':api')?.name = 'velocity-api'
|
||||
findProject(':proxy')?.name = 'velocity-proxy'
|
||||
findProject(':native')?.name = 'velocity-native'
|
||||
findProject(':native')?.name = 'velocity-native'
|
||||
|
||||
enableFeaturePreview('STABLE_PUBLISHING')
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren