diff --git a/.gitignore b/.gitignore index dc127090a..3fc46f0ef 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf +.idea/**/compiler.xml # Sensitive or high-churn files .idea/**/dataSources/ diff --git a/api/build.gradle b/api/build.gradle index 4f14afb33..18ec5eb48 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -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" + } + }*/ +} diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java new file mode 100644 index 000000000..a00f9c7d3 --- /dev/null +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java @@ -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 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; + } +} diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java new file mode 100644 index 000000000..d22aeeb16 --- /dev/null +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java @@ -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 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 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 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 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 + + '}'; + } + } +} diff --git a/api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor b/api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..a96abf5d2 --- /dev/null +++ b/api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor \ No newline at end of file diff --git a/api/src/main/java/com/velocitypowered/api/command/Command.java b/api/src/main/java/com/velocitypowered/api/command/Command.java new file mode 100644 index 000000000..d1cfd9f49 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/Command.java @@ -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 suggest(@NonNull CommandSource source, @NonNull String[] currentArgs) { + return ImmutableList.of(); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java b/api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java deleted file mode 100644 index 22c6eb765..000000000 --- a/api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java +++ /dev/null @@ -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 suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) { - return ImmutableList.of(); - } -} diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java b/api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java deleted file mode 100644 index 22230dcb9..000000000 --- a/api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java +++ /dev/null @@ -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); -} diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java new file mode 100644 index 000000000..38f031716 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -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); +} diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java new file mode 100644 index 000000000..70c11318e --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java @@ -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); +} diff --git a/api/src/main/java/com/velocitypowered/api/event/EventHandler.java b/api/src/main/java/com/velocitypowered/api/event/EventHandler.java new file mode 100644 index 000000000..146239cf7 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/EventHandler.java @@ -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 { + void execute(@NonNull E event); +} diff --git a/api/src/main/java/com/velocitypowered/api/event/EventManager.java b/api/src/main/java/com/velocitypowered/api/event/EventManager.java new file mode 100644 index 000000000..28b197e55 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/EventManager.java @@ -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 the event type to handle + */ + default void register(@NonNull Object plugin, @NonNull Class eventClass, @NonNull EventHandler 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 the event type to handle + */ + void register(@NonNull Object plugin, @NonNull Class eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler 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 CompletableFuture 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 the event type to handle + */ + void unregister(@NonNull Object plugin, @NonNull EventHandler handler); +} diff --git a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java new file mode 100644 index 000000000..a9bffe331 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java @@ -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; + +} diff --git a/api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java b/api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java new file mode 100644 index 000000000..04f28f390 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java @@ -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 { + /** + * 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 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); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java new file mode 100644 index 000000000..adb45b1ca --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java @@ -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; + +} diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/ConnectionHandshakeEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/ConnectionHandshakeEvent.java new file mode 100644 index 000000000..f2ecc0547 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/connection/ConnectionHandshakeEvent.java @@ -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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/DisconnectEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/DisconnectEvent.java new file mode 100644 index 000000000..0ef43909b --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/connection/DisconnectEvent.java @@ -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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/LoginEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/LoginEvent.java new file mode 100644 index 000000000..0ac7f9374 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/connection/LoginEvent.java @@ -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 { + 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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java new file mode 100644 index 000000000..ad0c9c4a3 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java @@ -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 { + 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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/permission/PermissionsSetupEvent.java b/api/src/main/java/com/velocitypowered/api/event/permission/PermissionsSetupEvent.java new file mode 100644 index 000000000..de3976025 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/permission/PermissionsSetupEvent.java @@ -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. + * + *

This event is only called once per subject, on initialisation.

+ */ +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. + * + *

Specifying null will reset the provider to the default + * instance given when the event was posted.

+ * + * @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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java new file mode 100644 index 000000000..1c7c3f416 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java @@ -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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java new file mode 100644 index 000000000..a80ce70e3 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java @@ -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 { + 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 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); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyInitializeEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyInitializeEvent.java new file mode 100644 index 000000000..14bee8b1e --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyInitializeEvent.java @@ -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"; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java new file mode 100644 index 000000000..53a7531cc --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java @@ -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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyShutdownEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyShutdownEvent.java new file mode 100644 index 000000000..0ae15f192 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyShutdownEvent.java @@ -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"; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/permission/PermissionFunction.java b/api/src/main/java/com/velocitypowered/api/permission/PermissionFunction.java new file mode 100644 index 000000000..102e36eba --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/permission/PermissionFunction.java @@ -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); +} diff --git a/api/src/main/java/com/velocitypowered/api/permission/PermissionProvider.java b/api/src/main/java/com/velocitypowered/api/permission/PermissionProvider.java new file mode 100644 index 000000000..fc040330c --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/permission/PermissionProvider.java @@ -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); +} diff --git a/api/src/main/java/com/velocitypowered/api/permission/PermissionSubject.java b/api/src/main/java/com/velocitypowered/api/permission/PermissionSubject.java new file mode 100644 index 000000000..8377f0729 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/permission/PermissionSubject.java @@ -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); +} diff --git a/api/src/main/java/com/velocitypowered/api/permission/Tristate.java b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java new file mode 100644 index 000000000..698a5f95b --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java @@ -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. + * + *

Possible values:

+ *

+ *
    + *
  • {@link #TRUE} - a positive setting
  • + *
  • {@link #FALSE} - a negative (negated) setting
  • + *
  • {@link #UNDEFINED} - a non-existent setting
  • + *
+ */ +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 true or false, respectively. + */ + public static @NonNull Tristate fromBoolean(boolean val) { + return val ? TRUE : FALSE; + } + + /** + * Returns a {@link Tristate} from a nullable boolean. + * + *

Unlike {@link #fromBoolean(boolean)}, this method returns {@link #UNDEFINED} + * if the value is null.

+ * + * @param val the boolean value + * @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value + * is null, true or false, 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. + * + *

A value of {@link #UNDEFINED} converts to false.

+ * + * @return a boolean representation of the Tristate. + */ + public boolean asBoolean() { + return this.booleanValue; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/Dependency.java b/api/src/main/java/com/velocitypowered/api/plugin/Dependency.java new file mode 100644 index 000000000..6af5d11e7 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/Dependency.java @@ -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; +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/InvalidPluginException.java b/api/src/main/java/com/velocitypowered/api/plugin/InvalidPluginException.java new file mode 100644 index 000000000..bd9bdad24 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/InvalidPluginException.java @@ -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); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java b/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java new file mode 100644 index 000000000..4f7ab93ed --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java @@ -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 {}; +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/PluginContainer.java b/api/src/main/java/com/velocitypowered/api/plugin/PluginContainer.java new file mode 100644 index 000000000..6ff5a6fee --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/PluginContainer.java @@ -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(); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java b/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java new file mode 100644 index 000000000..a61274e8d --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java @@ -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 getDependencies() { + return ImmutableSet.of(); + } + + default Optional 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 getSource() { + return Optional.empty(); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/PluginManager.java b/api/src/main/java/com/velocitypowered/api/plugin/PluginManager.java new file mode 100644 index 000000000..ba015d0a6 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/PluginManager.java @@ -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 fromInstance(@NonNull Object instance); + + /** + * Retrieves a {@link PluginContainer} based on its ID. + * + * @param id the plugin ID + * @return the plugin, if available + */ + @NonNull Optional getPlugin(@NonNull String id); + + /** + * Gets a {@link Collection} of all {@link PluginContainer}s. + * + * @return the plugins + */ + @NonNull Collection 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); +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/annotation/DataDirectory.java b/api/src/main/java/com/velocitypowered/api/plugin/annotation/DataDirectory.java new file mode 100644 index 000000000..aa120d94e --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/annotation/DataDirectory.java @@ -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 { +} diff --git a/api/src/main/java/com/velocitypowered/api/plugin/meta/PluginDependency.java b/api/src/main/java/com/velocitypowered/api/plugin/meta/PluginDependency.java new file mode 100644 index 000000000..eb792b9a8 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/plugin/meta/PluginDependency.java @@ -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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ConnectionRequestBuilder.java b/api/src/main/java/com/velocitypowered/api/proxy/ConnectionRequestBuilder.java index 5283ef422..e4507d752 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ConnectionRequestBuilder.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ConnectionRequestBuilder.java @@ -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 connect(); + @NonNull CompletableFuture connect(); /** * Initiates the connection to the remote server without waiting for a result. Velocity will use generic error diff --git a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java index 1385b594b..18bd0b421 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java @@ -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 getVirtualHost(); + /** * Determine whether or not the player remains online. * @return whether or not the player active diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 1e41576a6..86978ab57 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -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); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index 1dbb4b580..b35fe127c 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -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(); } diff --git a/api/src/main/java/com/velocitypowered/api/scheduler/ScheduledTask.java b/api/src/main/java/com/velocitypowered/api/scheduler/ScheduledTask.java new file mode 100644 index 000000000..58596c684 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/scheduler/ScheduledTask.java @@ -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(); +} diff --git a/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java b/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java new file mode 100644 index 000000000..5565a9e79 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java @@ -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(); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/scheduler/TaskStatus.java b/api/src/main/java/com/velocitypowered/api/scheduler/TaskStatus.java new file mode 100644 index 000000000..b5830fa51 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/scheduler/TaskStatus.java @@ -0,0 +1,7 @@ +package com.velocitypowered.api.scheduler; + +public enum TaskStatus { + SCHEDULED, + CANCELLED, + FINISHED +} diff --git a/api/src/main/java/com/velocitypowered/api/server/Favicon.java b/api/src/main/java/com/velocitypowered/api/server/Favicon.java index ceeaf517c..a25e5bea8 100644 --- a/api/src/main/java/com/velocitypowered/api/server/Favicon.java +++ b/api/src/main/java/com/velocitypowered/api/server/Favicon.java @@ -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)); } diff --git a/api/src/main/java/com/velocitypowered/api/server/ServerInfo.java b/api/src/main/java/com/velocitypowered/api/server/ServerInfo.java index 8bab632d0..ef83511f1 100644 --- a/api/src/main/java/com/velocitypowered/api/server/ServerInfo.java +++ b/api/src/main/java/com/velocitypowered/api/server/ServerInfo.java @@ -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; } diff --git a/api/src/main/java/com/velocitypowered/api/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/server/ServerPing.java new file mode 100644 index 000000000..f7842b412 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/server/ServerPing.java @@ -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 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 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 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 sample; + + public Players(int online, int max, List sample) { + this.online = online; + this.max = max; + this.sample = ImmutableList.copyOf(sample); + } + + public int getOnline() { + return online; + } + + public int getMax() { + return max; + } + + public List 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 + + '}'; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/data/GameProfile.java b/api/src/main/java/com/velocitypowered/api/util/GameProfile.java similarity index 83% rename from proxy/src/main/java/com/velocitypowered/proxy/data/GameProfile.java rename to api/src/main/java/com/velocitypowered/api/util/GameProfile.java index 6c6620c8e..f77b991f3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/data/GameProfile.java +++ b/api/src/main/java/com/velocitypowered/api/util/GameProfile.java @@ -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 properties; - public GameProfile(String id, String name, List properties) { + public GameProfile(@NonNull String id, @NonNull String name, @NonNull List 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; diff --git a/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java b/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java index 31703fe81..a21b97ffd 100644 --- a/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java +++ b/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java @@ -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(""); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/UuidUtils.java b/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java similarity index 63% rename from proxy/src/main/java/com/velocitypowered/proxy/util/UuidUtils.java rename to api/src/main/java/com/velocitypowered/api/util/UuidUtils.java index d42810ab2..3cff823cd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/UuidUtils.java +++ b/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java @@ -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)); } } diff --git a/build.gradle b/build.gradle index 42320b400..0b46cc4c2 100644 --- a/build.gradle +++ b/build.gradle @@ -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 } } -} \ No newline at end of file +} diff --git a/proxy/build.gradle b/proxy/build.gradle index af559e9ec..547faed22 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -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}" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 9102ce3fe..73a15ae9d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -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 connectionsByUuid = new ConcurrentHashMap<>(); private final Map 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; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java deleted file mode 100644 index a9871a08d..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java +++ /dev/null @@ -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 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> 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); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java index 16a05d021..ee68dbb1d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java @@ -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 suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) { + public List suggest(@Nonnull CommandSource source, @Nonnull String[] currentArgs) { if (currentArgs.length == 0) { return VelocityServer.getServer().getAllServers().stream() .map(ServerInfo::getName) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java index 302a49248..563ecd1fb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java @@ -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(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java index 272fe4777..fd1ae6b13 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java @@ -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); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java new file mode 100644 index 000000000..38c3759ea --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -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 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> 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); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index fadabf151..5d3797fec 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -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" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 24145a4f2..4cd1cd9c2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -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; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 549c8fb46..32344337d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -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 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index bf4f05916..ac4220624 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -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 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"); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index a603e7c8a..8b089a6ee 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -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 getVirtualHost() { + return Optional.empty(); + } + @Override public boolean isActive() { - return connection.getChannel().isActive(); + return !connection.isClosed(); } @Override public int getProtocolVersion() { - return connection.getProtocolVersion(); + return 0; } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java new file mode 100644 index 000000000..6c4ba9069 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java @@ -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 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(); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index 594b8ce26..c912ba7f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -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 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); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index 7d5f886bd..bef061769 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -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 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 4c46764fa..fcb1d34f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -22,13 +22,12 @@ public final class VelocityConsole extends SimpleTerminalConsole { return super.buildReader(builder .appName("Velocity") .completer((reader, parsedLine, list) -> { - Optional> offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line()); - if (offers.isPresent()) { - for (String offer : offers.get()) { - if (offer.isEmpty()) continue; + Optional> 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)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java b/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java deleted file mode 100644 index a6cb2a746..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java +++ /dev/null @@ -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 + - '}'; - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/PluginClassLoader.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/PluginClassLoader.java new file mode 100644 index 000000000..db2d2ad92 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/PluginClassLoader.java @@ -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 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); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java new file mode 100644 index 000000000..fa828883e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java @@ -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 registeredListenersByPlugin = Multimaps + .synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new)); + private final ListMultimap> 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 void register(@NonNull Object plugin, @NonNull Class eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler 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 @NonNull CompletableFuture 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 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 listeners = registeredListenersByPlugin.removeAll(plugin); + listeners.forEach(bus::unregister); + Collection> 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 void unregister(@NonNull Object plugin, @NonNull EventHandler 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 { + private final MethodSubscriptionAdapter methodAdapter; + + VelocityEventBus(EventExecutor.@NonNull Factory factory, @NonNull MethodScanner 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 { + @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 implements EventSubscriber { + private final EventHandler handler; + private final int postOrder; + + private KyoriToVelocityHandler(EventHandler 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 getHandler() { + return handler; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java new file mode 100644 index 000000000..85ae7fbf4 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -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 plugins = new HashMap<>(); + private final Map 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 found = new ArrayList<>(); + JavaPluginLoader loader = new JavaPluginLoader(server, directory); + + try (DirectoryStream 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 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 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 getPlugin(@NonNull String id) { + checkNotNull(id, "id"); + return Optional.ofNullable(plugins.get(id)); + } + + @Override + public @NonNull Collection 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."); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java new file mode 100644 index 000000000..bcbd0d36a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java @@ -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 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 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 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 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() + ); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/PluginLoader.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/PluginLoader.java new file mode 100644 index 000000000..db2af2d1a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/PluginLoader.java @@ -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; +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java new file mode 100644 index 000000000..b3cb0454a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java @@ -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 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); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java new file mode 100644 index 000000000..4aa490180 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java @@ -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 dependencies; + private final Path source; + + public VelocityPluginDescription(String id, String version, String author, Collection 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 getDependencies() { + return dependencies.values(); + } + + @Override + public Optional getDependency(String id) { + return Optional.ofNullable(dependencies.get(id)); + } + + @Override + public Optional getSource() { + return Optional.ofNullable(source); + } + + @Override + public String toString() { + return "VelocityPluginDescription{" + + "id='" + id + '\'' + + ", version='" + version + '\'' + + ", author='" + author + '\'' + + ", dependencies=" + dependencies + + ", source=" + source + + '}'; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java new file mode 100644 index 000000000..1b0ce229b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java @@ -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 dependencies, Path source, Class mainClass) { + super(id, version, author, dependencies, source); + this.mainClass = checkNotNull(mainClass); + } + + public Class getMainClass() { + return mainClass; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/SerializedPluginDescription.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/SerializedPluginDescription.java new file mode 100644 index 000000000..aeaffc867 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/SerializedPluginDescription.java @@ -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 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 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 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 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 + + '}'; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java new file mode 100644 index 000000000..cc3ddadc2 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java @@ -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()); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java new file mode 100644 index 000000000..2dcfc79f9 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java @@ -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 sortCandidates(List candidates) { + // Create our graph, we're going to be using this for Kahn's algorithm. + MutableGraph graph = GraphBuilder.directed().allowsSelfLoops(false).build(); + Map 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 noEdges = getNoDependencyCandidates(graph); + + // Actually run Kahn's algorithm + List 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 getNoDependencyCandidates(Graph graph) { + Queue found = new ArrayDeque<>(); + + for (PluginDescription node : graph.nodes()) { + if (graph.outDegree(node) == 0) { + found.add(node); + } + } + + return found; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPingResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPingResponse.java index 7e0491b2a..f980ccdb7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPingResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPingResponse.java @@ -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 { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/Sleeper.java b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/Sleeper.java new file mode 100644 index 000000000..b090ad110 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/Sleeper.java @@ -0,0 +1,7 @@ +package com.velocitypowered.proxy.scheduler; + +public interface Sleeper { + void sleep(long ms) throws InterruptedException; + + Sleeper SYSTEM = Thread::sleep; +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java new file mode 100644 index 000000000..5c40a0817 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java @@ -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 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); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactory.java new file mode 100644 index 000000000..f8a454e9a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactory.java @@ -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 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(); + } +} diff --git a/proxy/src/main/resources/log4j2.xml b/proxy/src/main/resources/log4j2.xml index ddaefacb2..0dc941c11 100644 --- a/proxy/src/main/resources/log4j2.xml +++ b/proxy/src/main/resources/log4j2.xml @@ -2,7 +2,13 @@ - + + + + + + diff --git a/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java b/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java new file mode 100644 index 000000000..d4d7a951a --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java @@ -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(); + } + +} \ No newline at end of file diff --git a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java new file mode 100644 index 000000000..5e5a30e40 --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java @@ -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 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 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 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); + } + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/UuidUtilsTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/UuidUtilsTest.java index 94f0b90cb..36cef2a1e 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/UuidUtilsTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/UuidUtilsTest.java @@ -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; diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactoryTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactoryTest.java new file mode 100644 index 000000000..72d026cf4 --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactoryTest.java @@ -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()); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e3e315775..044dc9fb2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,4 +6,6 @@ include ( ) findProject(':api')?.name = 'velocity-api' findProject(':proxy')?.name = 'velocity-proxy' -findProject(':native')?.name = 'velocity-native' \ No newline at end of file +findProject(':native')?.name = 'velocity-native' + +enableFeaturePreview('STABLE_PUBLISHING') \ No newline at end of file