From a028467e66616f1f7eecf4757344a41d1c2c7750 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 20 Aug 2018 19:30:32 -0400 Subject: [PATCH] Plugin API (#34) The Velocity API has had a lot of community input (special thanks to @hugmanrique who started the work, @lucko who contributed permissions support, and @Minecrell for providing initial feedback and an initial version of ServerListPlus). While the API is far from complete, there is enough available for people to start doing useful stuff with Velocity. --- .gitignore | 1 + api/build.gradle | 39 +++ .../plugin/ap/PluginAnnotationProcessor.java | 87 +++++++ .../ap/SerializedPluginDescription.java | 125 +++++++++ .../javax.annotation.processing.Processor | 1 + .../velocitypowered/api/command/Command.java | 29 +++ .../api/command/CommandExecutor.java | 29 --- .../api/command/CommandInvoker.java | 23 -- .../api/command/CommandManager.java | 14 + .../api/command/CommandSource.java | 16 ++ .../api/event/EventHandler.java | 12 + .../api/event/EventManager.java | 75 ++++++ .../velocitypowered/api/event/PostOrder.java | 11 + .../api/event/ResultedEvent.java | 109 ++++++++ .../velocitypowered/api/event/Subscribe.java | 22 ++ .../connection/ConnectionHandshakeEvent.java | 27 ++ .../api/event/connection/DisconnectEvent.java | 28 ++ .../api/event/connection/LoginEvent.java | 41 +++ .../api/event/connection/PreLoginEvent.java | 49 ++++ .../permission/PermissionsSetupEvent.java | 64 +++++ .../event/player/ServerConnectedEvent.java | 35 +++ .../event/player/ServerPreConnectEvent.java | 86 +++++++ .../api/event/proxy/ProxyInitializeEvent.java | 11 + .../api/event/proxy/ProxyPingEvent.java | 37 +++ .../api/event/proxy/ProxyShutdownEvent.java | 12 + .../api/permission/PermissionFunction.java | 33 +++ .../api/permission/PermissionProvider.java | 17 ++ .../api/permission/PermissionSubject.java | 16 ++ .../api/permission/Tristate.java | 74 ++++++ .../api/plugin/Dependency.java | 30 +++ .../api/plugin/InvalidPluginException.java | 19 ++ .../velocitypowered/api/plugin/Plugin.java | 44 ++++ .../api/plugin/PluginContainer.java | 26 ++ .../api/plugin/PluginDescription.java | 71 ++++++ .../api/plugin/PluginManager.java | 55 ++++ .../api/plugin/annotation/DataDirectory.java | 18 ++ .../api/plugin/meta/PluginDependency.java | 79 ++++++ .../api/proxy/ConnectionRequestBuilder.java | 5 +- .../api/proxy/InboundConnection.java | 7 + .../com/velocitypowered/api/proxy/Player.java | 12 +- .../api/proxy/ProxyServer.java | 40 ++- .../api/scheduler/ScheduledTask.java | 12 + .../api/scheduler/Scheduler.java | 22 ++ .../api/scheduler/TaskStatus.java | 7 + .../velocitypowered/api/server/Favicon.java | 9 +- .../api/server/ServerInfo.java | 11 +- .../api/server/ServerPing.java | 240 ++++++++++++++++++ .../api/util}/GameProfile.java | 10 +- .../api/util/LegacyChatColorUtils.java | 5 +- .../velocitypowered/api}/util/UuidUtils.java | 15 +- build.gradle | 3 +- proxy/build.gradle | 2 + .../velocitypowered/proxy/VelocityServer.java | 97 ++++++- .../proxy/command/CommandManager.java | 77 ------ .../proxy/command/ServerCommand.java | 16 +- .../proxy/command/ShutdownCommand.java | 12 +- .../proxy/command/VelocityCommand.java | 14 +- .../proxy/command/VelocityCommandManager.java | 85 +++++++ .../backend/BackendPlaySessionHandler.java | 10 +- .../backend/LoginSessionHandler.java | 2 +- .../client/ClientPlaySessionHandler.java | 12 +- .../connection/client/ConnectedPlayer.java | 42 ++- .../client/HandshakeSessionHandler.java | 36 ++- .../client/InitialInboundConnection.java | 38 +++ .../client/LoginSessionHandler.java | 78 ++++-- .../client/StatusSessionHandler.java | 24 +- .../proxy/console/VelocityConsole.java | 13 +- .../proxy/data/ServerPing.java | 96 ------- .../proxy/plugin/PluginClassLoader.java | 56 ++++ .../proxy/plugin/VelocityEventManager.java | 192 ++++++++++++++ .../proxy/plugin/VelocityPluginManager.java | 129 ++++++++++ .../proxy/plugin/loader/JavaPluginLoader.java | 129 ++++++++++ .../proxy/plugin/loader/PluginLoader.java | 18 ++ .../loader/VelocityPluginContainer.java | 28 ++ .../loader/VelocityPluginDescription.java | 69 +++++ .../java/JavaVelocityPluginDescription.java | 22 ++ .../java/SerializedPluginDescription.java | 125 +++++++++ .../loader/java/VelocityPluginModule.java | 38 +++ .../plugin/util/PluginDependencyUtils.java | 69 +++++ .../protocol/packet/LegacyPingResponse.java | 2 +- .../proxy/scheduler/Sleeper.java | 7 + .../proxy/scheduler/VelocityScheduler.java | 176 +++++++++++++ .../ThreadRecorderThreadFactory.java | 42 +++ proxy/src/main/resources/log4j2.xml | 8 +- .../scheduler/VelocitySchedulerTest.java | 51 ++++ .../proxy/testutil/FakePluginManager.java | 92 +++++++ .../proxy/util/UuidUtilsTest.java | 1 + .../ThreadRecorderThreadFactoryTest.java | 36 +++ settings.gradle | 4 +- 89 files changed, 3453 insertions(+), 358 deletions(-) create mode 100644 api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java create mode 100644 api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java create mode 100644 api/src/ap/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 api/src/main/java/com/velocitypowered/api/command/Command.java delete mode 100644 api/src/main/java/com/velocitypowered/api/command/CommandExecutor.java delete mode 100644 api/src/main/java/com/velocitypowered/api/command/CommandInvoker.java create mode 100644 api/src/main/java/com/velocitypowered/api/command/CommandManager.java create mode 100644 api/src/main/java/com/velocitypowered/api/command/CommandSource.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/EventHandler.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/EventManager.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/PostOrder.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/ResultedEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/Subscribe.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/connection/ConnectionHandshakeEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/connection/DisconnectEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/connection/LoginEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/permission/PermissionsSetupEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/ServerConnectedEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/proxy/ProxyInitializeEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/proxy/ProxyShutdownEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/permission/PermissionFunction.java create mode 100644 api/src/main/java/com/velocitypowered/api/permission/PermissionProvider.java create mode 100644 api/src/main/java/com/velocitypowered/api/permission/PermissionSubject.java create mode 100644 api/src/main/java/com/velocitypowered/api/permission/Tristate.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/Dependency.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/InvalidPluginException.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/Plugin.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/PluginContainer.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/PluginManager.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/annotation/DataDirectory.java create mode 100644 api/src/main/java/com/velocitypowered/api/plugin/meta/PluginDependency.java create mode 100644 api/src/main/java/com/velocitypowered/api/scheduler/ScheduledTask.java create mode 100644 api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java create mode 100644 api/src/main/java/com/velocitypowered/api/scheduler/TaskStatus.java create mode 100644 api/src/main/java/com/velocitypowered/api/server/ServerPing.java rename {proxy/src/main/java/com/velocitypowered/proxy/data => api/src/main/java/com/velocitypowered/api/util}/GameProfile.java (83%) rename {proxy/src/main/java/com/velocitypowered/proxy => api/src/main/java/com/velocitypowered/api}/util/UuidUtils.java (63%) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/PluginClassLoader.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/PluginLoader.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/SerializedPluginDescription.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/scheduler/Sleeper.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactory.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/concurrency/ThreadRecorderThreadFactoryTest.java 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