3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-11-16 21:10:30 +01:00
The Velocity API has had a lot of community input (special thanks to @hugmanrique who started the work, @lucko who contributed permissions support, and @Minecrell for providing initial feedback and an initial version of ServerListPlus).

While the API is far from complete, there is enough available for people to start doing useful stuff with Velocity.
Dieser Commit ist enthalten in:
Andrew Steinborn 2018-08-20 19:30:32 -04:00 committet von GitHub
Ursprung 8e836a5066
Commit a028467e66
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
89 geänderte Dateien mit 3453 neuen und 358 gelöschten Zeilen

1
.gitignore vendored
Datei anzeigen

@ -11,6 +11,7 @@
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/compiler.xml
# Sensitive or high-churn files
.idea/**/dataSources/

Datei anzeigen

@ -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"
}
}*/
}

Datei anzeigen

@ -0,0 +1,87 @@
package com.velocitypowered.api.plugin.ap;
import com.google.gson.Gson;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginDescription;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
@SupportedAnnotationTypes({"com.velocitypowered.api.plugin.Plugin"})
public class PluginAnnotationProcessor extends AbstractProcessor {
private ProcessingEnvironment environment;
private String pluginClassFound;
private boolean warnedAboutMultiplePlugins;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
this.environment = processingEnv;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
return false;
}
for (Element element : roundEnv.getElementsAnnotatedWith(Plugin.class)) {
if (element.getKind() != ElementKind.CLASS) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated with "
+ Plugin.class.getCanonicalName());
return false;
}
Name qualifiedName = ((TypeElement) element).getQualifiedName();
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
if (!warnedAboutMultiplePlugins) {
environment.getMessager().printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support " +
"multiple plugins. We are using " + pluginClassFound + " for your plugin's main class.");
warnedAboutMultiplePlugins = true;
}
return false;
}
Plugin plugin = element.getAnnotation(Plugin.class);
if (!PluginDescription.ID_PATTERN.matcher(plugin.id()).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Invalid ID for plugin "
+ qualifiedName + ". IDs must start alphabetically, have alphanumeric characters, and can " +
"contain dashes or underscores.");
return false;
}
// All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription.from(plugin, qualifiedName.toString());
try {
FileObject object = environment.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "velocity-plugin.json");
try (Writer writer = new BufferedWriter(object.openWriter())) {
new Gson().toJson(description, writer);
}
pluginClassFound = qualifiedName.toString();
} catch (IOException e) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to generate plugin file");
}
}
return false;
}
}

Datei anzeigen

@ -0,0 +1,125 @@
package com.velocitypowered.api.plugin.ap;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.plugin.Plugin;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class SerializedPluginDescription {
private final String id;
private final String author;
private final String main;
private final String version;
private final List<Dependency> dependencies;
public SerializedPluginDescription(String id, String author, String main, String version) {
this(id, author, main, version, ImmutableList.of());
}
public SerializedPluginDescription(String id, String author, String main, String version, List<Dependency> dependencies) {
this.id = Preconditions.checkNotNull(id, "id");
this.author = Preconditions.checkNotNull(author, "author");
this.main = Preconditions.checkNotNull(main, "main");
this.version = Preconditions.checkNotNull(version, "version");
this.dependencies = ImmutableList.copyOf(dependencies);
}
public static SerializedPluginDescription from(Plugin plugin, String qualifiedName) {
List<Dependency> dependencies = new ArrayList<>();
for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) {
dependencies.add(new Dependency(dependency.id(), dependency.optional()));
}
return new SerializedPluginDescription(plugin.id(), plugin.author(), qualifiedName, plugin.version(), dependencies);
}
public String getId() {
return id;
}
public String getAuthor() {
return author;
}
public String getMain() {
return main;
}
public String getVersion() {
return version;
}
public List<Dependency> getDependencies() {
return dependencies;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SerializedPluginDescription that = (SerializedPluginDescription) o;
return Objects.equals(id, that.id) &&
Objects.equals(author, that.author) &&
Objects.equals(main, that.main) &&
Objects.equals(version, that.version) &&
Objects.equals(dependencies, that.dependencies);
}
@Override
public int hashCode() {
return Objects.hash(id, author, main, version, dependencies);
}
@Override
public String toString() {
return "SerializedPluginDescription{" +
"id='" + id + '\'' +
", author='" + author + '\'' +
", main='" + main + '\'' +
", version='" + version + '\'' +
", dependencies=" + dependencies +
'}';
}
public static class Dependency {
private final String id;
private final boolean optional;
public Dependency(String id, boolean optional) {
this.id = id;
this.optional = optional;
}
public String getId() {
return id;
}
public boolean isOptional() {
return optional;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dependency that = (Dependency) o;
return optional == that.optional &&
Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id, optional);
}
@Override
public String toString() {
return "Dependency{" +
"id='" + id + '\'' +
", optional=" + optional +
'}';
}
}
}

Datei anzeigen

@ -0,0 +1 @@
com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor

Datei anzeigen

@ -0,0 +1,29 @@
package com.velocitypowered.api.command;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
/**
* Represents a command that can be executed by a {@link CommandSource}, such as a {@link com.velocitypowered.api.proxy.Player}
* or the console.
*/
public interface Command {
/**
* Executes the command for the specified {@link CommandSource}.
* @param source the source of this command
* @param args the arguments for this command
*/
void execute(@NonNull CommandSource source, @NonNull String[] args);
/**
* Provides tab complete suggestions for a command for a specified {@link CommandSource}.
* @param source the source to run the command for
* @param currentArgs the current, partial arguments for this command
* @return tab complete suggestions
*/
default List<String> suggest(@NonNull CommandSource source, @NonNull String[] currentArgs) {
return ImmutableList.of();
}
}

Datei anzeigen

@ -1,29 +0,0 @@
package com.velocitypowered.api.command;
import com.google.common.collect.ImmutableList;
import javax.annotation.Nonnull;
import java.util.List;
/**
* Represents a command that can be executed by a {@link CommandInvoker}, such as a {@link com.velocitypowered.api.proxy.Player}
* or the console.
*/
public interface CommandExecutor {
/**
* Executes the command for the specified {@link CommandInvoker}.
* @param invoker the invoker of this command
* @param args the arguments for this command
*/
void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args);
/**
* Provides tab complete suggestions for a command for a specified {@link CommandInvoker}.
* @param invoker the invoker to run the command for
* @param currentArgs the current, partial arguments for this command
* @return tab complete suggestions
*/
default List<String> suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) {
return ImmutableList.of();
}
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -0,0 +1,12 @@
package com.velocitypowered.api.event;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents an interface to perform direct dispatch of an event. This makes integration easier to achieve with platforms
* such as RxJava.
*/
@FunctionalInterface
public interface EventHandler<E> {
void execute(@NonNull E event);
}

Datei anzeigen

@ -0,0 +1,75 @@
package com.velocitypowered.api.event;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.concurrent.CompletableFuture;
/**
* Allows plugins to register and deregister listeners for event handlers.
*/
public interface EventManager {
/**
* Requests that the specified {@code listener} listen for events and associate it with the {@code plugin}.
* @param plugin the plugin to associate with the listener
* @param listener the listener to register
*/
void register(@NonNull Object plugin, @NonNull Object listener);
/**
* Requests that the specified {@code handler} listen for events and associate it with the {@code plugin}.
* @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register
* @param handler the handler to register
* @param <E> the event type to handle
*/
default <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull EventHandler<E> handler) {
register(plugin, eventClass, PostOrder.NORMAL, handler);
}
/**
* Requests that the specified {@code handler} listen for events and associate it with the {@code plugin}.
* @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register
* @param postOrder the order in which events should be posted to the handler
* @param handler the handler to register
* @param <E> the event type to handle
*/
<E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler);
/**
* Fires the specified event to the event bus asynchronously. This allows Velocity to continue servicing connections
* while a plugin handles a potentially long-running operation such as a database query.
* @param event the event to fire
* @return a {@link CompletableFuture} representing the posted event
*/
@NonNull <E> CompletableFuture<E> fire(@NonNull E event);
/**
* Posts the specified event to the event bus and discards the result.
* @param event the event to fire
*/
default void fireAndForget(@NonNull Object event) {
fire(event);
}
/**
* Unregisters all listeners for the specified {@code plugin}.
* @param plugin the plugin to deregister listeners for
*/
void unregisterListeners(@NonNull Object plugin);
/**
* Unregisters a specific listener for a specific plugin.
* @param plugin the plugin associated with the listener
* @param listener the listener to deregister
*/
void unregisterListener(@NonNull Object plugin, @NonNull Object listener);
/**
* Unregisters a specific event handler for a specific plugin.
* @param plugin the plugin to associate with the handler
* @param handler the handler to register
* @param <E> the event type to handle
*/
<E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler);
}

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -0,0 +1,109 @@
package com.velocitypowered.api.event;
import com.google.common.base.Preconditions;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Optional;
/**
* Indicates an event that has a result attached to it.
*/
public interface ResultedEvent<R extends ResultedEvent.Result> {
/**
* Returns the result associated with this event.
* @return the result of this event
*/
R getResult();
/**
* Sets the result of this event.
* @param result the new result
*/
void setResult(@NonNull R result);
/**
* Represents a result for an event.
*/
interface Result {
boolean isAllowed();
}
/**
* A generic "allowed/denied" result.
*/
class GenericResult implements Result {
private static final GenericResult ALLOWED = new GenericResult(true);
private static final GenericResult DENIED = new GenericResult(true);
private final boolean allowed;
private GenericResult(boolean b) {
this.allowed = b;
}
@Override
public boolean isAllowed() {
return allowed;
}
@Override
public String toString() {
return allowed ? "allowed" : "denied";
}
public static GenericResult allowed() {
return ALLOWED;
}
public static GenericResult denied() {
return DENIED;
}
}
/**
* Represents an "allowed/denied" result with a reason allowed for denial.
*/
class ComponentResult implements Result {
private static final ComponentResult ALLOWED = new ComponentResult(true, null);
private final boolean allowed;
private final @Nullable Component reason;
private ComponentResult(boolean allowed, @Nullable Component reason) {
this.allowed = allowed;
this.reason = reason;
}
@Override
public boolean isAllowed() {
return allowed;
}
public Optional<Component> getReason() {
return Optional.ofNullable(reason);
}
@Override
public String toString() {
if (allowed) {
return "allowed";
}
if (reason != null) {
return "denied: " + ComponentSerializers.PLAIN.serialize(reason);
}
return "denied";
}
public static ComponentResult allowed() {
return ALLOWED;
}
public static ComponentResult denied(@NonNull Component reason) {
Preconditions.checkNotNull(reason, "reason");
return new ComponentResult(false, reason);
}
}
}

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -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 +
'}';
}
}

Datei anzeigen

@ -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 +
'}';
}
}

Datei anzeigen

@ -0,0 +1,41 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This event is fired once the player has been authenticated but before they connect to a server on the proxy.
*/
public class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
private final Player player;
private ComponentResult result;
public LoginEvent(@NonNull Player player) {
this.player = Preconditions.checkNotNull(player, "player");
this.result = ComponentResult.allowed();
}
public Player getPlayer() {
return player;
}
@Override
public ComponentResult getResult() {
return result;
}
@Override
public void setResult(@NonNull ComponentResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
@Override
public String toString() {
return "LoginEvent{" +
"player=" + player +
", result=" + result +
'}';
}
}

Datei anzeigen

@ -0,0 +1,49 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This event is fired when a player has initiated a connection with the proxy but before the proxy authenticates the
* player with Mojang or before the player's proxy connection is fully established (for offline mode).
*/
public class PreLoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
private final InboundConnection connection;
private final String username;
private ComponentResult result;
public PreLoginEvent(InboundConnection connection, String username) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.username = Preconditions.checkNotNull(username, "username");
this.result = ComponentResult.allowed();
}
public InboundConnection getConnection() {
return connection;
}
public String getUsername() {
return username;
}
@Override
public ComponentResult getResult() {
return result;
}
@Override
public void setResult(@NonNull ComponentResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
@Override
public String toString() {
return "PreLoginEvent{" +
"connection=" + connection +
", username='" + username + '\'' +
", result=" + result +
'}';
}
}

Datei anzeigen

@ -0,0 +1,64 @@
package com.velocitypowered.api.event.permission;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.permission.PermissionFunction;
import com.velocitypowered.api.permission.PermissionProvider;
import com.velocitypowered.api.permission.PermissionSubject;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Called when a {@link PermissionSubject}'s permissions are being setup.
*
* <p>This event is only called once per subject, on initialisation.</p>
*/
public class PermissionsSetupEvent {
private final PermissionSubject subject;
private final PermissionProvider defaultProvider;
private PermissionProvider provider;
public PermissionsSetupEvent(PermissionSubject subject, PermissionProvider provider) {
this.subject = Preconditions.checkNotNull(subject, "subject");
this.provider = this.defaultProvider = Preconditions.checkNotNull(provider, "provider");
}
public @NonNull PermissionSubject getSubject() {
return this.subject;
}
/**
* Uses the provider function to obtain a {@link PermissionFunction} for
* the subject.
*
* @param subject the subject
* @return the obtained permission function
*/
public @NonNull PermissionFunction createFunction(PermissionSubject subject) {
return this.provider.createFunction(subject);
}
public @NonNull PermissionProvider getProvider() {
return this.provider;
}
/**
* Sets the {@link PermissionFunction} that should be used for the subject.
*
* <p>Specifying <code>null</code> will reset the provider to the default
* instance given when the event was posted.</p>
*
* @param provider the provider
*/
public void setProvider(@Nullable PermissionProvider provider) {
this.provider = provider == null ? this.defaultProvider : provider;
}
@Override
public String toString() {
return "PermissionsSetupEvent{" +
"subject=" + subject +
", defaultProvider=" + defaultProvider +
", provider=" + provider +
'}';
}
}

Datei anzeigen

@ -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 +
'}';
}
}

Datei anzeigen

@ -0,0 +1,86 @@
package com.velocitypowered.api.event.player;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.server.ServerInfo;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Optional;
/**
* This event is fired before the player connects to a server.
*/
public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEvent.ServerResult> {
private final Player player;
private ServerResult result;
public ServerPreConnectEvent(Player player, ServerResult result) {
this.player = Preconditions.checkNotNull(player, "player");
this.result = Preconditions.checkNotNull(result, "result");
}
public Player getPlayer() {
return player;
}
@Override
public ServerResult getResult() {
return result;
}
@Override
public void setResult(@NonNull ServerResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
@Override
public String toString() {
return "ServerPreConnectEvent{" +
"player=" + player +
", result=" + result +
'}';
}
/**
* Represents the result of the {@link ServerPreConnectEvent}.
*/
public static class ServerResult implements ResultedEvent.Result {
private static final ServerResult DENIED = new ServerResult(false, null);
private final boolean allowed;
private final ServerInfo info;
private ServerResult(boolean allowed, @Nullable ServerInfo info) {
this.allowed = allowed;
this.info = info;
}
@Override
public boolean isAllowed() {
return allowed;
}
public Optional<ServerInfo> getInfo() {
return Optional.ofNullable(info);
}
@Override
public String toString() {
if (!allowed) {
return "denied";
}
return "allowed: connect to " + info.getName();
}
public static ServerResult denied() {
return DENIED;
}
public static ServerResult allowed(ServerInfo server) {
Preconditions.checkNotNull(server, "server");
return new ServerResult(true, server);
}
}
}

Datei anzeigen

@ -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";
}
}

Datei anzeigen

@ -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 +
'}';
}
}

Datei anzeigen

@ -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";
}
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -0,0 +1,74 @@
package com.velocitypowered.api.permission;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Represents three different states of a setting.
*
* <p>Possible values:</p>
* <p></p>
* <ul>
* <li>{@link #TRUE} - a positive setting</li>
* <li>{@link #FALSE} - a negative (negated) setting</li>
* <li>{@link #UNDEFINED} - a non-existent setting</li>
* </ul>
*/
public enum Tristate {
/**
* A value indicating a positive setting
*/
TRUE(true),
/**
* A value indicating a negative (negated) setting
*/
FALSE(false),
/**
* A value indicating a non-existent setting
*/
UNDEFINED(false);
/**
* Returns a {@link Tristate} from a boolean
*
* @param val the boolean value
* @return {@link #TRUE} or {@link #FALSE}, if the value is <code>true</code> or <code>false</code>, respectively.
*/
public static @NonNull Tristate fromBoolean(boolean val) {
return val ? TRUE : FALSE;
}
/**
* Returns a {@link Tristate} from a nullable boolean.
*
* <p>Unlike {@link #fromBoolean(boolean)}, this method returns {@link #UNDEFINED}
* if the value is null.</p>
*
* @param val the boolean value
* @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value
* is <code>null</code>, <code>true</code> or <code>false</code>, respectively.
*/
public static @NonNull Tristate fromNullableBoolean(@Nullable Boolean val) {
return val == null ? UNDEFINED : val ? TRUE : FALSE;
}
private final boolean booleanValue;
Tristate(boolean booleanValue) {
this.booleanValue = booleanValue;
}
/**
* Returns the value of the Tristate as a boolean.
*
* <p>A value of {@link #UNDEFINED} converts to false.</p>
*
* @return a boolean representation of the Tristate.
*/
public boolean asBoolean() {
return this.booleanValue;
}
}

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -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);
}
}

Datei anzeigen

@ -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 {};
}

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -0,0 +1,71 @@
package com.velocitypowered.api.plugin;
import com.google.common.collect.ImmutableSet;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Represents metadata for a specific version of a plugin.
*/
public interface PluginDescription {
/**
* The pattern plugin IDs must match. Plugin IDs may only contain
* alphanumeric characters, dashes or underscores, must start with
* an alphabetic character and cannot be longer than 64 characters.
*/
Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");
/**
* Gets the qualified ID of the {@link Plugin} within this container.
*
* @return the plugin ID
* @see Plugin#id()
*/
String getId();
/**
* Gets the version of the {@link Plugin} within this container.
*
* @return the plugin version
* @see Plugin#version()
*/
String getVersion();
/**
* Gets the author of the {@link Plugin} within this container.
*
* @return the plugin author
* @see Plugin#author()
*/
String getAuthor();
/**
* Gets a {@link Collection} of all dependencies of the {@link Plugin} within
* this container.
*
* @return the plugin dependencies, can be empty
* @see Plugin#dependencies()
*/
default Collection<PluginDependency> getDependencies() {
return ImmutableSet.of();
}
default Optional<PluginDependency> getDependency(String id) {
return Optional.empty();
}
/**
* Returns the source the plugin was loaded from.
*
* @return the source the plugin was loaded from or {@link Optional#empty()}
* if unknown
*/
default Optional<Path> getSource() {
return Optional.empty();
}
}

Datei anzeigen

@ -0,0 +1,55 @@
package com.velocitypowered.api.plugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.logging.Logger;
/**
* The class that manages plugins. This manager can retrieve
* {@link PluginContainer}s from {@link Plugin} instances, getting
* {@link Logger}s, etc.
*/
public interface PluginManager {
/**
* Gets the plugin container from an instance.
*
* @param instance the instance
* @return the container
*/
@NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance);
/**
* Retrieves a {@link PluginContainer} based on its ID.
*
* @param id the plugin ID
* @return the plugin, if available
*/
@NonNull Optional<PluginContainer> getPlugin(@NonNull String id);
/**
* Gets a {@link Collection} of all {@link PluginContainer}s.
*
* @return the plugins
*/
@NonNull Collection<PluginContainer> getPlugins();
/**
* Checks if a plugin is loaded based on its ID.
*
* @param id the id of the {@link Plugin}
* @return {@code true} if loaded
*/
boolean isLoaded(@NonNull String id);
/**
* Adds the specified {@code path} to the plugin classpath.
*
* @param plugin the plugin
* @param path the path to the JAR you want to inject into the classpath
* @throws UnsupportedOperationException if the operation is not applicable to this plugin
*/
void addToClasspath(@NonNull Object plugin, @NonNull Path path);
}

Datei anzeigen

@ -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 {
}

Datei anzeigen

@ -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 +
'}';
}
}

Datei anzeigen

@ -2,6 +2,7 @@ package com.velocitypowered.api.proxy;
import com.velocitypowered.api.server.ServerInfo;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@ -15,14 +16,14 @@ public interface ConnectionRequestBuilder {
* Returns the server that this connection request represents.
* @return the server this request will connect to
*/
ServerInfo getServer();
@NonNull ServerInfo getServer();
/**
* Initiates the connection to the remote server and emits a result on the {@link CompletableFuture} after the user
* has logged on. No messages will be communicated to the client: the user is responsible for all error handling.
* @return a {@link CompletableFuture} representing the status of this connection
*/
CompletableFuture<Result> connect();
@NonNull CompletableFuture<Result> connect();
/**
* Initiates the connection to the remote server without waiting for a result. Velocity will use generic error

Datei anzeigen

@ -1,6 +1,7 @@
package com.velocitypowered.api.proxy;
import java.net.InetSocketAddress;
import java.util.Optional;
/**
* Represents a connection to the proxy. There is no guarantee that the connection has been fully initialized.
@ -12,6 +13,12 @@ public interface InboundConnection {
*/
InetSocketAddress getRemoteAddress();
/**
* Returns the hostname that the user entered into the client, if applicable.
* @return the hostname from the client
*/
Optional<InetSocketAddress> getVirtualHost();
/**
* Determine whether or not the player remains online.
* @return whether or not the player active

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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();
}

Datei anzeigen

@ -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();
}

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -0,0 +1,7 @@
package com.velocitypowered.api.scheduler;
public enum TaskStatus {
SCHEDULED,
CANCELLED,
FINISHED
}

Datei anzeigen

@ -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));
}

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -0,0 +1,240 @@
package com.velocitypowered.api.server;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.*;
/**
* Represents a 1.7 and above server list ping response. This class is immutable.
*/
public class ServerPing {
private final Version version;
private final Players players;
private final Component description;
private final @Nullable Favicon favicon;
public ServerPing(@NonNull Version version, @NonNull Players players, @NonNull Component description, @Nullable Favicon favicon) {
this.version = Preconditions.checkNotNull(version, "version");
this.players = Preconditions.checkNotNull(players, "players");
this.description = Preconditions.checkNotNull(description, "description");
this.favicon = favicon;
}
public Version getVersion() {
return version;
}
public Players getPlayers() {
return players;
}
public Component getDescription() {
return description;
}
public Optional<Favicon> getFavicon() {
return Optional.ofNullable(favicon);
}
@Override
public String toString() {
return "ServerPing{" +
"version=" + version +
", players=" + players +
", description=" + description +
", favicon='" + favicon + '\'' +
'}';
}
public Builder asBuilder() {
Builder builder = new Builder();
builder.version = version;
builder.onlinePlayers = players.online;
builder.maximumPlayers = players.max;
builder.samplePlayers.addAll(players.sample);
builder.description = description;
builder.favicon = favicon;
return builder;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Version version;
private int onlinePlayers;
private int maximumPlayers;
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
private Component description;
private Favicon favicon;
private Builder() {
}
public Builder version(Version version) {
this.version = Preconditions.checkNotNull(version, "version");
return this;
}
public Builder onlinePlayers(int onlinePlayers) {
this.onlinePlayers = onlinePlayers;
return this;
}
public Builder maximumPlayers(int maximumPlayers) {
this.maximumPlayers = maximumPlayers;
return this;
}
public Builder samplePlayers(SamplePlayer... players) {
this.samplePlayers.addAll(Arrays.asList(players));
return this;
}
public Builder clearSamplePlayers() {
this.samplePlayers.clear();
return this;
}
public Builder description(Component description) {
this.description = Preconditions.checkNotNull(description, "description");
return this;
}
public Builder favicon(Favicon favicon) {
this.favicon = Preconditions.checkNotNull(favicon, "favicon");
return this;
}
public ServerPing build() {
return new ServerPing(version, new Players(onlinePlayers, maximumPlayers, samplePlayers), description, favicon);
}
public Version getVersion() {
return version;
}
public int getOnlinePlayers() {
return onlinePlayers;
}
public int getMaximumPlayers() {
return maximumPlayers;
}
public List<SamplePlayer> getSamplePlayers() {
return samplePlayers;
}
public Component getDescription() {
return description;
}
public Favicon getFavicon() {
return favicon;
}
@Override
public String toString() {
return "Builder{" +
"version=" + version +
", onlinePlayers=" + onlinePlayers +
", maximumPlayers=" + maximumPlayers +
", samplePlayers=" + samplePlayers +
", description=" + description +
", favicon=" + favicon +
'}';
}
}
public static class Version {
private final int protocol;
private final String name;
public Version(int protocol, String name) {
this.protocol = protocol;
this.name = name;
}
public int getProtocol() {
return protocol;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Version{" +
"protocol=" + protocol +
", name='" + name + '\'' +
'}';
}
}
public static class Players {
private final int online;
private final int max;
private final List<SamplePlayer> sample;
public Players(int online, int max, List<SamplePlayer> sample) {
this.online = online;
this.max = max;
this.sample = ImmutableList.copyOf(sample);
}
public int getOnline() {
return online;
}
public int getMax() {
return max;
}
public List<SamplePlayer> getSample() {
return sample;
}
@Override
public String toString() {
return "Players{" +
"online=" + online +
", max=" + max +
", sample=" + sample +
'}';
}
}
public static class SamplePlayer {
private final String name;
private final UUID id;
public SamplePlayer(String name, UUID id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public UUID getId() {
return id;
}
@Override
public String toString() {
return "SamplePlayer{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
}

Datei anzeigen

@ -1,8 +1,8 @@
package com.velocitypowered.proxy.data;
package com.velocitypowered.api.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.util.UuidUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
import java.util.UUID;
@ -12,7 +12,7 @@ public class GameProfile {
private final String name;
private final List<Property> properties;
public GameProfile(String id, String name, List<Property> properties) {
public GameProfile(@NonNull String id, @NonNull String name, @NonNull List<Property> properties) {
this.id = id;
this.name = name;
this.properties = ImmutableList.copyOf(properties);
@ -34,7 +34,7 @@ public class GameProfile {
return ImmutableList.copyOf(properties);
}
public static GameProfile forOfflinePlayer(String username) {
public static GameProfile forOfflinePlayer(@NonNull String username) {
Preconditions.checkNotNull(username, "username");
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
return new GameProfile(id, username, ImmutableList.of());
@ -54,7 +54,7 @@ public class GameProfile {
private final String value;
private final String signature;
public Property(String name, String value, String signature) {
public Property(@NonNull String name, @NonNull String value, @NonNull String signature) {
this.name = name;
this.value = value;
this.signature = signature;

Datei anzeigen

@ -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("");
}

Datei anzeigen

@ -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));
}
}

Datei anzeigen

@ -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'

Datei anzeigen

@ -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}"

Datei anzeigen

@ -4,11 +4,15 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.api.command.CommandInvoker;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.server.Favicon;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.network.ConnectionManager;
import com.velocitypowered.proxy.command.ServerCommand;
import com.velocitypowered.proxy.command.ShutdownCommand;
@ -16,9 +20,12 @@ import com.velocitypowered.proxy.command.VelocityCommand;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.command.CommandManager;
import com.velocitypowered.proxy.command.VelocityCommandManager;
import com.velocitypowered.proxy.plugin.VelocityEventManager;
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.scheduler.Sleeper;
import com.velocitypowered.proxy.scheduler.VelocityScheduler;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.Ratelimiter;
@ -40,6 +47,7 @@ import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class VelocityServer implements ProxyServer {
@ -55,13 +63,14 @@ public class VelocityServer implements ProxyServer {
private NettyHttpClient httpClient;
private KeyPair serverKeyPair;
private final ServerMap servers = new ServerMap();
private final CommandManager commandManager = new CommandManager();
private final VelocityCommandManager commandManager = new VelocityCommandManager();
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
private boolean shutdown = false;
private final VelocityPluginManager pluginManager = new VelocityPluginManager(this);
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final CommandInvoker consoleCommandInvoker = new CommandInvoker() {
private final CommandSource consoleCommandSource = new CommandSource() {
@Override
public void sendMessage(@Nonnull Component component) {
logger.info(ComponentSerializers.LEGACY.serialize(component));
@ -73,11 +82,13 @@ public class VelocityServer implements ProxyServer {
}
};
private Ratelimiter ipAttemptLimiter;
private VelocityEventManager eventManager;
private VelocityScheduler scheduler;
private VelocityServer() {
commandManager.registerCommand("velocity", new VelocityCommand());
commandManager.registerCommand("server", new ServerCommand());
commandManager.registerCommand("shutdown", new ShutdownCommand());
commandManager.register(new VelocityCommand(), "velocity");
commandManager.register(new ServerCommand(), "server");
commandManager.register(new ShutdownCommand(), "shutdown");
}
public static VelocityServer getServer() {
@ -92,7 +103,8 @@ public class VelocityServer implements ProxyServer {
return configuration;
}
public CommandManager getCommandManager() {
@Override
public VelocityCommandManager getCommandManager() {
return commandManager;
}
@ -121,10 +133,21 @@ public class VelocityServer implements ProxyServer {
}
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
eventManager = new VelocityEventManager(pluginManager);
scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM);
loadPlugins();
// Post the first event
pluginManager.getPlugins().forEach(container -> {
container.getInstance().ifPresent(plugin -> eventManager.register(plugin, plugin));
});
try {
eventManager.fire(new ProxyInitializeEvent()).get();
} catch (InterruptedException | ExecutionException e) {
// Ignore, we don't care.
}
this.cm.bind(configuration.getBind());
@ -133,6 +156,29 @@ public class VelocityServer implements ProxyServer {
}
}
private void loadPlugins() {
logger.info("Loading plugins...");
try {
Path pluginPath = Paths.get("plugins");
if (Files.notExists(pluginPath)) {
Files.createDirectory(pluginPath);
} else {
if (!Files.isDirectory(pluginPath)) {
logger.warn("Plugin location {} is not a directory, continuing without loading plugins", pluginPath);
return;
}
pluginManager.loadPlugins(pluginPath);
}
} catch (Exception e) {
logger.error("Couldn't load plugins", e);
}
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
}
public ServerMap getServers() {
return servers;
}
@ -154,6 +200,14 @@ public class VelocityServer implements ProxyServer {
}
this.cm.shutdown();
eventManager.fire(new ProxyShutdownEvent());
try {
eventManager.shutdown();
} catch (InterruptedException e) {
logger.error("Your plugins took over 10 seconds to shut down.");
}
shutdown = true;
}
@ -226,7 +280,22 @@ public class VelocityServer implements ProxyServer {
}
@Override
public CommandInvoker getConsoleCommandInvoker() {
return consoleCommandInvoker;
public CommandSource getConsoleCommandSource() {
return consoleCommandSource;
}
@Override
public PluginManager getPluginManager() {
return pluginManager;
}
@Override
public EventManager getEventManager() {
return eventManager;
}
@Override
public VelocityScheduler getScheduler() {
return scheduler;
}
}

Datei anzeigen

@ -1,77 +0,0 @@
package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.CommandExecutor;
import com.velocitypowered.api.command.CommandInvoker;
import java.util.*;
import java.util.stream.Collectors;
public class CommandManager {
private final Map<String, CommandExecutor> executors = new HashMap<>();
public void registerCommand(String name, CommandExecutor executor) {
Preconditions.checkNotNull(name, "name");
Preconditions.checkNotNull(executor, "executor");
this.executors.put(name, executor);
}
public void unregisterCommand(String name) {
Preconditions.checkNotNull(name, "name");
this.executors.remove(name);
}
public boolean execute(CommandInvoker invoker, String cmdLine) {
Preconditions.checkNotNull(invoker, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
}
String command = split[0];
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
CommandExecutor executor = executors.get(command);
if (executor == null) {
return false;
}
try {
executor.execute(invoker, actualArgs);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + invoker, e);
}
}
public Optional<List<String>> offerSuggestions(CommandInvoker invoker, String cmdLine) {
Preconditions.checkNotNull(invoker, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return Optional.empty();
}
String command = split[0];
if (split.length == 1) {
return Optional.of(executors.keySet().stream()
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
.collect(Collectors.toList()));
}
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
CommandExecutor executor = executors.get(command);
if (executor == null) {
return Optional.empty();
}
try {
return Optional.of(executor.suggest(invoker, actualArgs));
} catch (Exception e) {
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + invoker, e);
}
}
}

Datei anzeigen

@ -1,8 +1,8 @@
package com.velocitypowered.proxy.command;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.CommandExecutor;
import com.velocitypowered.api.command.CommandInvoker;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
@ -14,15 +14,15 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ServerCommand implements CommandExecutor {
public class ServerCommand implements Command {
@Override
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
if (!(invoker instanceof Player)) {
invoker.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
public void execute(@Nonnull CommandSource source, @Nonnull String[] args) {
if (!(source instanceof Player)) {
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
return;
}
Player player = (Player) invoker;
Player player = (Player) source;
if (args.length == 1) {
// Trying to connect to a server.
String serverName = args[0];
@ -42,7 +42,7 @@ public class ServerCommand implements CommandExecutor {
}
@Override
public List<String> suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) {
public List<String> suggest(@Nonnull CommandSource source, @Nonnull String[] currentArgs) {
if (currentArgs.length == 0) {
return VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)

Datei anzeigen

@ -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();

Datei anzeigen

@ -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);
}
}

Datei anzeigen

@ -0,0 +1,85 @@
package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.CommandManager;
import java.util.*;
import java.util.stream.Collectors;
public class VelocityCommandManager implements CommandManager {
private final Map<String, Command> commands = new HashMap<>();
@Override
public void register(final Command command, final String... aliases) {
Preconditions.checkNotNull(aliases, "aliases");
Preconditions.checkNotNull(command, "executor");
for (int i = 0, length = aliases.length; i < length; i++) {
final String alias = aliases[i];
Preconditions.checkNotNull(aliases, "alias at index %s", i);
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
}
}
@Override
public void unregister(final String alias) {
Preconditions.checkNotNull(alias, "name");
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
}
@Override
public boolean execute(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
}
String alias = split[0];
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return false;
}
try {
command.execute(source, actualArgs);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
}
}
public Optional<List<String>> offerSuggestions(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return Optional.empty();
}
String command = split[0];
if (split.length == 1) {
return Optional.of(commands.keySet().stream()
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
.collect(Collectors.toList()));
}
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command executor = commands.get(command);
if (executor == null) {
return Optional.empty();
}
try {
return Optional.of(executor.suggest(source, actualArgs));
} catch (Exception e) {
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + source, e);
}
}
}

Datei anzeigen

@ -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;
@ -15,6 +17,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
this.connection = connection;
}
@Override
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*

Datei anzeigen

@ -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;

Datei anzeigen

@ -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

Datei anzeigen

@ -2,6 +2,9 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.permission.PermissionFunction;
import com.velocitypowered.api.permission.PermissionProvider;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.util.MessagePosition;
import com.velocitypowered.api.proxy.Player;
@ -9,7 +12,7 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
@ -25,6 +28,7 @@ import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.PlainComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import javax.annotation.Nonnull;
import java.net.InetSocketAddress;
@ -35,19 +39,23 @@ import java.util.concurrent.CompletableFuture;
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
public static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
private final GameProfile profile;
private final MinecraftConnection connection;
private final InetSocketAddress virtualHost;
private PermissionFunction permissionFunction = null;
private int tryIndex = 0;
private ServerConnection connectedServer;
private ClientSettings clientSettings;
private ServerConnection connectionInFlight;
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection, InetSocketAddress virtualHost) {
this.profile = profile;
this.connection = connection;
this.virtualHost = virtualHost;
}
@Override
@ -78,6 +86,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.ofNullable(virtualHost);
}
public void setPermissionFunction(PermissionFunction permissionFunction) {
this.permissionFunction = permissionFunction;
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
@ -89,7 +106,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
@Override
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
public void sendMessage(@NonNull Component component, @NonNull MessagePosition position) {
Preconditions.checkNotNull(component, "component");
Preconditions.checkNotNull(position, "position");
@ -112,7 +129,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
@Override
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) {
return new ConnectionRequestBuilderImpl(info);
}
@ -191,8 +208,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
// Otherwise, initiate the connection.
ServerConnection connection = new ServerConnection(request.getServer(), this, VelocityServer.getServer());
return connection.connect();
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
return VelocityServer.getServer().getEventManager().fire(event)
.thenCompose((newEvent) -> {
if (!newEvent.getResult().isAllowed()) {
return CompletableFuture.completedFuture(
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED)
);
}
return new ServerConnection(newEvent.getResult().getInfo().get(), this, VelocityServer.getServer()).connect();
});
}
public void setConnectedServer(ServerConnection serverConnection) {
@ -223,13 +249,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public boolean hasPermission(@Nonnull String permission) {
return false; // TODO: Implement permissions.
return permissionFunction.getPermissionSetting(permission).asBoolean();
}
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final ServerInfo info;
public ConnectionRequestBuilderImpl(ServerInfo info) {
ConnectionRequestBuilderImpl(ServerInfo info) {
this.info = Preconditions.checkNotNull(info, "info");
}

Datei anzeigen

@ -1,12 +1,15 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.data.ServerPing;
import com.velocitypowered.api.server.ServerPing;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
@ -17,6 +20,7 @@ import net.kyori.text.format.TextColor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Optional;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
@ -37,12 +41,14 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName());
}
InitialInboundConnection ic = new InitialInboundConnection(connection, (Handshake) packet);
Handshake handshake = (Handshake) packet;
switch (handshake.getNextStatus()) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(connection));
connection.setSessionHandler(new StatusSessionHandler(connection, ic));
break;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
@ -56,7 +62,8 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return;
}
connection.setSessionHandler(new LoginSessionHandler(connection));
VelocityServer.getServer().getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(connection, ic));
}
break;
default:
@ -69,21 +76,25 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
ServerPing ping = new ServerPing(
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null
);
// The disconnect packet is the same as the server response one.
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(ping)));
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
// The disconnect packet is the same as the server response one.
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(event.getPing())));
}, connection.getChannel().eventLoop());
} else if (packet instanceof LegacyHandshake) {
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
}
}
private static class InitialInboundConnection implements InboundConnection {
private static class LegacyInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private InitialInboundConnection(MinecraftConnection connection) {
private LegacyInboundConnection(MinecraftConnection connection) {
this.connection = connection;
}
@ -92,14 +103,19 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.empty();
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
return !connection.isClosed();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
return 0;
}
}
}

Datei anzeigen

@ -0,0 +1,38 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import java.net.InetSocketAddress;
import java.util.Optional;
class InitialInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private final Handshake handshake;
InitialInboundConnection(MinecraftConnection connection, Handshake handshake) {
this.connection = connection;
this.handshake = handshake;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.of(InetSocketAddress.createUnresolved(handshake.getServerAddress(), handshake.getPort()));
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
}

Datei anzeigen

@ -1,8 +1,13 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
@ -10,7 +15,6 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.util.EncryptionUtils;
import io.netty.buffer.Unpooled;
import net.kyori.text.TextComponent;
@ -33,12 +37,14 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
private final MinecraftConnection inbound;
private final InboundConnection apiInbound;
private ServerLogin login;
private byte[] verify;
private int playerInfoId;
public LoginSessionHandler(MinecraftConnection inbound) {
public LoginSessionHandler(MinecraftConnection inbound, InboundConnection apiInbound) {
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
}
@Override
@ -53,7 +59,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
));
} else {
// Proceed with the regular login process.
initiateLogin();
beginPreLogin();
}
}
} else if (packet instanceof ServerLogin) {
@ -67,7 +73,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
message.setData(Unpooled.EMPTY_BUFFER);
inbound.write(message);
} else {
initiateLogin();
beginPreLogin();
}
} else if (packet instanceof EncryptionResponse) {
try {
@ -97,7 +103,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
handleSuccessfulLogin(profile);
initializePlayer(profile);
}, inbound.getChannel().eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
@ -113,16 +119,26 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
}
private void initiateLogin() {
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
} else {
// Offline-mode, don't try to request encryption.
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
}
private void beginPreLogin() {
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
if (!event.getResult().isAllowed()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
return;
}
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
} else {
// Offline-mode, don't try to request encryption.
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()));
}
}, inbound.getChannel().eventLoop());
}
private EncryptionRequest generateRequest() {
@ -135,9 +151,31 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return request;
}
private void handleSuccessfulLogin(GameProfile profile) {
private void initializePlayer(GameProfile profile) {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
ConnectedPlayer player = new ConnectedPlayer(profile, inbound, apiInbound.getVirtualHost().orElse(null));
// load permissions first
VelocityServer.getServer().getEventManager().fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
.thenCompose(event -> {
// wait for permissions to load, then set the players permission function
player.setPermissionFunction(event.createFunction(player));
// then call & wait for the login event
return VelocityServer.getServer().getEventManager().fire(new LoginEvent(player));
})
// then complete the connection
.thenAcceptAsync(event -> {
if (!event.getResult().isAllowed()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
return;
}
handleProxyLogin(player);
}, inbound.getChannel().eventLoop());
}
private void handleProxyLogin(ConnectedPlayer player) {
Optional<ServerInfo> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.close(TextComponent.of("No available servers", TextColor.RED));
@ -151,8 +189,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(profile.getName());
success.setUuid(profile.idAsUuid());
success.setUsername(player.getUsername());
success.setUuid(player.getUniqueId());
inbound.write(success);
inbound.setAssociation(player);

Datei anzeigen

@ -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

Datei anzeigen

@ -22,13 +22,12 @@ public final class VelocityConsole extends SimpleTerminalConsole {
return super.buildReader(builder
.appName("Velocity")
.completer((reader, parsedLine, list) -> {
Optional<List<String>> offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line());
if (offers.isPresent()) {
for (String offer : offers.get()) {
if (offer.isEmpty()) continue;
Optional<List<String>> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line());
o.ifPresent(offers -> {
for (String offer : offers) {
list.add(new Candidate(offer));
}
}
});
})
);
}
@ -40,8 +39,8 @@ public final class VelocityConsole extends SimpleTerminalConsole {
@Override
protected void runCommand(String command) {
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandInvoker(), command)) {
server.getConsoleCommandInvoker().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) {
server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
}
}

Datei anzeigen

@ -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 +
'}';
}
}
}

Datei anzeigen

@ -0,0 +1,56 @@
package com.velocitypowered.proxy.plugin;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class PluginClassLoader extends URLClassLoader {
private static final Set<PluginClassLoader> loaders = new CopyOnWriteArraySet<>();
static {
ClassLoader.registerAsParallelCapable();
}
public PluginClassLoader(URL[] urls) {
super(urls);
loaders.add(this);
}
public void addPath(Path path) {
try {
addURL(path.toUri().toURL());
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return loadClass0(name, resolve, true);
}
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException {
try {
return super.loadClass(name, resolve);
} catch (ClassNotFoundException ignored) {
// Ignored: we'll try others
}
if (checkOther) {
for (PluginClassLoader loader : loaders) {
if (loader != this) {
try {
return loader.loadClass0(name, resolve, false);
} catch (ClassNotFoundException ignored) {
// We're trying others, safe to ignore
}
}
}
}
throw new ClassNotFoundException(name);
}
}

Datei anzeigen

@ -0,0 +1,192 @@
package com.velocitypowered.proxy.plugin;
import com.google.common.base.Preconditions;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.api.event.EventHandler;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.proxy.util.concurrency.ThreadRecorderThreadFactory;
import net.kyori.event.EventSubscriber;
import net.kyori.event.PostResult;
import net.kyori.event.SimpleEventBus;
import net.kyori.event.method.*;
import net.kyori.event.method.asm.ASMEventExecutorFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;
public class VelocityEventManager implements EventManager {
private static final Logger logger = LogManager.getLogger(VelocityEventManager.class);
private final ListMultimap<Object, Object> registeredListenersByPlugin = Multimaps
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
private final ListMultimap<Object, EventHandler<?>> registeredHandlersByPlugin = Multimaps
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
private final VelocityEventBus bus = new VelocityEventBus(
new ASMEventExecutorFactory<>(new PluginClassLoader(new URL[0])),
new VelocityMethodScanner());
private final ExecutorService service;
private final ThreadRecorderThreadFactory recordingThreadFactory;
private final PluginManager pluginManager;
public VelocityEventManager(PluginManager pluginManager) {
this.pluginManager = pluginManager;
this.recordingThreadFactory = new ThreadRecorderThreadFactory(new ThreadFactoryBuilder()
.setNameFormat("Velocity Event Executor - #%d").setDaemon(true).build());
this.service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), recordingThreadFactory);
}
@Override
public void register(@NonNull Object plugin, @NonNull Object listener) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(listener, "listener");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
registeredListenersByPlugin.put(plugin, listener);
bus.register(listener);
}
@Override
public <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(eventClass, "eventClass");
Preconditions.checkNotNull(postOrder, "postOrder");
Preconditions.checkNotNull(handler, "listener");
bus.register(eventClass, new KyoriToVelocityHandler<>(handler, postOrder));
}
@Override
public <E> @NonNull CompletableFuture<E> fire(@NonNull E event) {
Preconditions.checkNotNull(event, "event");
if (!bus.hasSubscribers(event.getClass())) {
// Optimization: nobody's listening.
return CompletableFuture.completedFuture(event);
}
Runnable runEvent = () -> {
PostResult result = bus.post(event);
if (!result.exceptions().isEmpty()) {
logger.error("Some errors occurred whilst posting event {}.", event);
int i = 0;
for (Throwable exception : result.exceptions().values()) {
logger.error("#{}: \n", i++, exception);
}
}
};
if (recordingThreadFactory.currentlyInFactory()) {
// Optimization: fire the event immediately, we are on the event handling thread.
runEvent.run();
return CompletableFuture.completedFuture(event);
}
CompletableFuture<E> eventFuture = new CompletableFuture<>();
service.execute(() -> {
runEvent.run();
eventFuture.complete(event);
});
return eventFuture;
}
@Override
public void unregisterListeners(@NonNull Object plugin) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
Collection<Object> listeners = registeredListenersByPlugin.removeAll(plugin);
listeners.forEach(bus::unregister);
Collection<EventHandler<?>> handlers = registeredHandlersByPlugin.removeAll(plugin);
handlers.forEach(bus::unregister);
}
@Override
public void unregisterListener(@NonNull Object plugin, @NonNull Object listener) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(listener, "listener");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
registeredListenersByPlugin.remove(plugin, listener);
bus.unregister(listener);
}
@Override
public <E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(handler, "listener");
registeredHandlersByPlugin.remove(plugin, handler);
bus.unregister(handler);
}
public void shutdown() throws InterruptedException {
service.shutdown();
service.awaitTermination(10, TimeUnit.SECONDS);
}
private static class VelocityEventBus extends SimpleEventBus<Object> {
private final MethodSubscriptionAdapter<Object> methodAdapter;
VelocityEventBus(EventExecutor.@NonNull Factory<Object, Object> factory, @NonNull MethodScanner<Object> methodScanner) {
super(Object.class);
this.methodAdapter = new SimpleMethodSubscriptionAdapter<>(this, factory, methodScanner);
}
void register(Object listener) {
this.methodAdapter.register(listener);
}
void unregister(Object listener) {
this.methodAdapter.unregister(listener);
}
void unregister(EventHandler<?> handler) {
this.unregister(s -> s instanceof KyoriToVelocityHandler && ((KyoriToVelocityHandler<?>) s).getHandler().equals(handler));
}
}
private static class VelocityMethodScanner implements MethodScanner<Object> {
@Override
public boolean shouldRegister(@NonNull Object listener, @NonNull Method method) {
return method.isAnnotationPresent(Subscribe.class);
}
@Override
public int postOrder(@NonNull Object listener, @NonNull Method method) {
return method.getAnnotation(Subscribe.class).order().ordinal();
}
@Override
public boolean consumeCancelledEvents(@NonNull Object listener, @NonNull Method method) {
return true;
}
}
private static class KyoriToVelocityHandler<E> implements EventSubscriber<E> {
private final EventHandler<E> handler;
private final int postOrder;
private KyoriToVelocityHandler(EventHandler<E> handler, PostOrder postOrder) {
this.handler = handler;
this.postOrder = postOrder.ordinal();
}
@Override
public void invoke(@NonNull E event) {
handler.execute(event);
}
@Override
public int postOrder() {
return postOrder;
}
public EventHandler<E> getHandler() {
return handler;
}
}
}

Datei anzeigen

@ -0,0 +1,129 @@
package com.velocitypowered.proxy.plugin;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.plugin.loader.JavaPluginLoader;
import com.velocitypowered.proxy.plugin.util.PluginDependencyUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public class VelocityPluginManager implements PluginManager {
private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class);
private final Map<String, PluginContainer> plugins = new HashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final VelocityServer server;
public VelocityPluginManager(VelocityServer server) {
this.server = checkNotNull(server, "server");
}
private void registerPlugin(@NonNull PluginContainer plugin) {
plugins.put(plugin.getDescription().getId(), plugin);
plugin.getInstance().ifPresent(instance -> pluginInstances.put(instance, plugin));
}
public void loadPlugins(@NonNull Path directory) throws IOException {
checkNotNull(directory, "directory");
checkArgument(Files.isDirectory(directory), "provided path isn't a directory");
List<PluginDescription> found = new ArrayList<>();
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, p -> Files.isRegularFile(p) && p.toString().endsWith(".jar"))) {
for (Path path : stream) {
try {
found.add(loader.loadPlugin(path));
} catch (Exception e) {
logger.error("Unable to load plugin {}", path, e);
}
}
}
if (found.isEmpty()) {
// No plugins found
return;
}
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
// Now load the plugins
pluginLoad:
for (PluginDescription plugin : sortedPlugins) {
// Verify dependencies
for (PluginDependency dependency : plugin.getDependencies()) {
if (!dependency.isOptional() && !isLoaded(dependency.getId())) {
logger.error("Can't load plugin {} due to missing dependency {}", plugin.getId(), dependency.getId());
continue pluginLoad;
}
}
// Actually create the plugin
PluginContainer pluginObject;
try {
pluginObject = loader.createPlugin(plugin);
} catch (Exception e) {
logger.error("Can't create plugin {}", plugin.getId(), e);
continue;
}
registerPlugin(pluginObject);
}
}
@Override
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
checkNotNull(instance, "instance");
if (instance instanceof PluginContainer) {
return Optional.of((PluginContainer) instance);
}
return Optional.ofNullable(pluginInstances.get(instance));
}
@Override
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
checkNotNull(id, "id");
return Optional.ofNullable(plugins.get(id));
}
@Override
public @NonNull Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(plugins.values());
}
@Override
public boolean isLoaded(@NonNull String id) {
return plugins.containsKey(id);
}
@Override
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
checkNotNull(plugin, "instance");
checkNotNull(path, "path");
checkArgument(pluginInstances.containsKey(plugin), "plugin is not loaded");
ClassLoader pluginClassloader = plugin.getClass().getClassLoader();
if (pluginClassloader instanceof PluginClassLoader) {
((PluginClassLoader) pluginClassloader).addPath(path);
} else {
throw new UnsupportedOperationException("Operation is not supported on non-Java Velocity plugins.");
}
}
}

Datei anzeigen

@ -0,0 +1,129 @@
package com.velocitypowered.proxy.plugin.loader;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.velocitypowered.api.plugin.*;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.plugin.PluginClassLoader;
import com.velocitypowered.proxy.plugin.loader.java.JavaVelocityPluginDescription;
import com.velocitypowered.proxy.plugin.loader.java.SerializedPluginDescription;
import com.velocitypowered.proxy.plugin.loader.java.VelocityPluginModule;
import javax.annotation.Nonnull;
import java.io.BufferedInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Pattern;
public class JavaPluginLoader implements PluginLoader {
private final ProxyServer server;
private final Path baseDirectory;
public JavaPluginLoader(ProxyServer server, Path baseDirectory) {
this.server = server;
this.baseDirectory = baseDirectory;
}
@Nonnull
@Override
public PluginDescription loadPlugin(Path source) throws Exception {
Optional<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
if (!serialized.isPresent()) {
throw new InvalidPluginException("Did not find a valid velocity-info.json.");
}
PluginClassLoader loader = new PluginClassLoader(
new URL[] {source.toUri().toURL() }
);
Class mainClass = loader.loadClass(serialized.get().getMain());
VelocityPluginDescription description = createDescription(serialized.get(), source, mainClass);
String pluginId = description.getId();
Pattern pattern = PluginDescription.ID_PATTERN;
if (!pattern.matcher(pluginId).matches()) {
throw new InvalidPluginException("Plugin ID '" + pluginId + "' must match pattern " + pattern.pattern());
}
return description;
}
@Nonnull
@Override
public PluginContainer createPlugin(PluginDescription description) throws Exception {
if (!(description instanceof JavaVelocityPluginDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
Optional<Path> source = javaDescription.getSource();
if (!source.isPresent()) {
throw new IllegalArgumentException("No path in plugin description");
}
Injector injector = Guice.createInjector(new VelocityPluginModule(server, javaDescription, baseDirectory));
Object instance = injector.getInstance(javaDescription.getMainClass());
return new VelocityPluginContainer(
description.getId(),
description.getVersion(),
description.getAuthor(),
description.getDependencies(),
source.get(),
instance
);
}
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source) throws Exception {
try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(source)))) {
JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) {
if (entry.getName().equals("velocity-plugin.json")) {
try (Reader pluginInfoReader = new InputStreamReader(in)) {
return Optional.of(VelocityServer.GSON.fromJson(pluginInfoReader, SerializedPluginDescription.class));
}
}
}
return Optional.empty();
}
}
private VelocityPluginDescription createDescription(SerializedPluginDescription description, Path source, Class mainClass) {
Set<PluginDependency> dependencies = new HashSet<>();
for (SerializedPluginDescription.Dependency dependency : description.getDependencies()) {
dependencies.add(toDependencyMeta(dependency));
}
return new JavaVelocityPluginDescription(
description.getId(),
description.getVersion(),
description.getAuthor(),
dependencies,
source,
mainClass
);
}
private static PluginDependency toDependencyMeta(SerializedPluginDescription.Dependency dependency) {
return new PluginDependency(
dependency.getId(),
null, // TODO Implement version matching in dependency annotation
dependency.isOptional()
);
}
}

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -0,0 +1,28 @@
package com.velocitypowered.proxy.plugin.loader;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
public class VelocityPluginContainer extends VelocityPluginDescription implements PluginContainer {
private final Object instance;
public VelocityPluginContainer(String id, String version, String author, Collection<PluginDependency> dependencies, Path source, Object instance) {
super(id, version, author, dependencies, source);
this.instance = instance;
}
@Override
public PluginDescription getDescription() {
return this;
}
@Override
public Optional<?> getInstance() {
return Optional.ofNullable(instance);
}
}

Datei anzeigen

@ -0,0 +1,69 @@
package com.velocitypowered.proxy.plugin.loader;
import com.google.common.collect.Maps;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
public class VelocityPluginDescription implements PluginDescription {
private final String id;
private final String version;
private final String author;
private final Map<String, PluginDependency> dependencies;
private final Path source;
public VelocityPluginDescription(String id, String version, String author, Collection<PluginDependency> dependencies, Path source) {
this.id = checkNotNull(id, "id");
this.version = checkNotNull(version, "version");
this.author = checkNotNull(author, "author");
this.dependencies = Maps.uniqueIndex(dependencies, PluginDependency::getId);
this.source = source;
}
@Override
public String getId() {
return id;
}
@Override
public String getVersion() {
return version;
}
@Override
public String getAuthor() {
return author;
}
@Override
public Collection<PluginDependency> getDependencies() {
return dependencies.values();
}
@Override
public Optional<PluginDependency> getDependency(String id) {
return Optional.ofNullable(dependencies.get(id));
}
@Override
public Optional<Path> getSource() {
return Optional.ofNullable(source);
}
@Override
public String toString() {
return "VelocityPluginDescription{" +
"id='" + id + '\'' +
", version='" + version + '\'' +
", author='" + author + '\'' +
", dependencies=" + dependencies +
", source=" + source +
'}';
}
}

Datei anzeigen

@ -0,0 +1,22 @@
package com.velocitypowered.proxy.plugin.loader.java;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
import java.nio.file.Path;
import java.util.Collection;
import static com.google.common.base.Preconditions.checkNotNull;
public class JavaVelocityPluginDescription extends VelocityPluginDescription {
private final Class mainClass;
public JavaVelocityPluginDescription(String id, String version, String author, Collection<PluginDependency> dependencies, Path source, Class mainClass) {
super(id, version, author, dependencies, source);
this.mainClass = checkNotNull(mainClass);
}
public Class getMainClass() {
return mainClass;
}
}

Datei anzeigen

@ -0,0 +1,125 @@
package com.velocitypowered.proxy.plugin.loader.java;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.plugin.Plugin;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class SerializedPluginDescription {
private final String id;
private final String author;
private final String main;
private final String version;
private final List<Dependency> dependencies;
public SerializedPluginDescription(String id, String author, String main, String version) {
this(id, author, main, version, ImmutableList.of());
}
public SerializedPluginDescription(String id, String author, String main, String version, List<Dependency> dependencies) {
this.id = Preconditions.checkNotNull(id, "id");
this.author = Preconditions.checkNotNull(author, "author");
this.main = Preconditions.checkNotNull(main, "main");
this.version = Preconditions.checkNotNull(version, "version");
this.dependencies = ImmutableList.copyOf(dependencies);
}
public static SerializedPluginDescription from(Plugin plugin, String qualifiedName) {
List<Dependency> dependencies = new ArrayList<>();
for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) {
dependencies.add(new Dependency(dependency.id(), dependency.optional()));
}
return new SerializedPluginDescription(plugin.id(), plugin.author(), qualifiedName, plugin.version(), dependencies);
}
public String getId() {
return id;
}
public String getAuthor() {
return author;
}
public String getMain() {
return main;
}
public String getVersion() {
return version;
}
public List<Dependency> getDependencies() {
return dependencies;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SerializedPluginDescription that = (SerializedPluginDescription) o;
return Objects.equals(id, that.id) &&
Objects.equals(author, that.author) &&
Objects.equals(main, that.main) &&
Objects.equals(version, that.version) &&
Objects.equals(dependencies, that.dependencies);
}
@Override
public int hashCode() {
return Objects.hash(id, author, main, version, dependencies);
}
@Override
public String toString() {
return "SerializedPluginDescription{" +
"id='" + id + '\'' +
", author='" + author + '\'' +
", main='" + main + '\'' +
", version='" + version + '\'' +
", dependencies=" + dependencies +
'}';
}
public static class Dependency {
private final String id;
private final boolean optional;
public Dependency(String id, boolean optional) {
this.id = id;
this.optional = optional;
}
public String getId() {
return id;
}
public boolean isOptional() {
return optional;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dependency that = (Dependency) o;
return optional == that.optional &&
Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id, optional);
}
@Override
public String toString() {
return "Dependency{" +
"id='" + id + '\'' +
", optional=" + optional +
'}';
}
}
}

Datei anzeigen

@ -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());
}
}

Datei anzeigen

@ -0,0 +1,69 @@
package com.velocitypowered.proxy.plugin.util;
import com.google.common.collect.Maps;
import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import java.util.*;
public class PluginDependencyUtils {
public static List<PluginDescription> sortCandidates(List<PluginDescription> candidates) {
// Create our graph, we're going to be using this for Kahn's algorithm.
MutableGraph<PluginDescription> graph = GraphBuilder.directed().allowsSelfLoops(false).build();
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(candidates, PluginDescription::getId);
// Add edges
for (PluginDescription description : candidates) {
graph.addNode(description);
for (PluginDependency dependency : description.getDependencies()) {
PluginDescription in = candidateMap.get(dependency.getId());
if (in != null) {
graph.putEdge(description, in);
}
}
}
// Find nodes that have no edges
Queue<PluginDescription> noEdges = getNoDependencyCandidates(graph);
// Actually run Kahn's algorithm
List<PluginDescription> sorted = new ArrayList<>();
while (!noEdges.isEmpty()) {
PluginDescription candidate = noEdges.poll();
sorted.add(candidate);
for (PluginDescription node : graph.successors(candidate)) {
graph.removeEdge(node, candidate);
if (graph.adjacentNodes(node).isEmpty()) {
if (!noEdges.contains(node)) {
noEdges.add(node);
}
}
}
}
if (!graph.edges().isEmpty()) {
throw new IllegalStateException("Plugin circular dependency found: " + graph.toString());
}
return sorted;
}
public static Queue<PluginDescription> getNoDependencyCandidates(Graph<PluginDescription> graph) {
Queue<PluginDescription> found = new ArrayDeque<>();
for (PluginDescription node : graph.nodes()) {
if (graph.outDegree(node) == 0) {
found.add(node);
}
}
return found;
}
}

Datei anzeigen

@ -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 {

Datei anzeigen

@ -0,0 +1,7 @@
package com.velocitypowered.proxy.scheduler;
public interface Sleeper {
void sleep(long ms) throws InterruptedException;
Sleeper SYSTEM = Thread::sleep;
}

Datei anzeigen

@ -0,0 +1,176 @@
package com.velocitypowered.proxy.scheduler;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.scheduler.ScheduledTask;
import com.velocitypowered.api.scheduler.Scheduler;
import com.velocitypowered.api.scheduler.TaskStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class VelocityScheduler implements Scheduler {
private final PluginManager pluginManager;
private final ExecutorService taskService;
private final Sleeper sleeper;
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedListMultimap(
Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
public VelocityScheduler(PluginManager pluginManager, Sleeper sleeper) {
this.pluginManager = pluginManager;
this.sleeper = sleeper;
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler - #%d").build());
}
@Override
public TaskBuilder buildTask(Object plugin, Runnable runnable) {
Preconditions.checkNotNull(plugin, "plugin");
Preconditions.checkNotNull(runnable, "runnable");
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "plugin is not registered");
return new TaskBuilderImpl(plugin, runnable);
}
public void shutdown() {
for (ScheduledTask task : ImmutableList.copyOf(tasksByPlugin.values())) {
task.cancel();
}
taskService.shutdown();
}
private class TaskBuilderImpl implements TaskBuilder {
private final Object plugin;
private final Runnable runnable;
private long delay; // ms
private long repeat; // ms
private TaskBuilderImpl(Object plugin, Runnable runnable) {
this.plugin = plugin;
this.runnable = runnable;
}
@Override
public TaskBuilder delay(int time, TimeUnit unit) {
this.delay = unit.toMillis(time);
return this;
}
@Override
public TaskBuilder repeat(int time, TimeUnit unit) {
this.repeat = unit.toMillis(time);
return this;
}
@Override
public TaskBuilder clearDelay() {
this.delay = 0;
return this;
}
@Override
public TaskBuilder clearRepeat() {
this.repeat = 0;
return this;
}
@Override
public ScheduledTask schedule() {
VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat);
taskService.execute(task);
tasksByPlugin.put(plugin, task);
return task;
}
}
private class VelocityTask implements Runnable, ScheduledTask {
private final Object plugin;
private final Runnable runnable;
private final long delay;
private final long repeat;
private volatile TaskStatus status;
private Thread taskThread;
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
this.plugin = plugin;
this.runnable = runnable;
this.delay = delay;
this.repeat = repeat;
this.status = TaskStatus.SCHEDULED;
}
@Override
public Object plugin() {
return plugin;
}
@Override
public TaskStatus status() {
return status;
}
@Override
public void cancel() {
if (status == TaskStatus.SCHEDULED) {
status = TaskStatus.CANCELLED;
if (taskThread != null) {
taskThread.interrupt();
}
}
}
@Override
public void run() {
taskThread = Thread.currentThread();
if (delay > 0) {
try {
sleeper.sleep(delay);
} catch (InterruptedException e) {
if (status == TaskStatus.CANCELLED) {
onFinish();
return;
}
}
}
while (status != TaskStatus.CANCELLED) {
try {
runnable.run();
} catch (Exception e) {
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin);
}
if (repeat > 0) {
try {
sleeper.sleep(delay);
} catch (InterruptedException e) {
if (status == TaskStatus.CANCELLED) {
break;
}
}
} else {
status = TaskStatus.FINISHED;
break;
}
}
onFinish();
}
private void onFinish() {
tasksByPlugin.remove(plugin, this);
}
}
private static class Log {
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
}
}

Datei anzeigen

@ -0,0 +1,42 @@
package com.velocitypowered.proxy.util.concurrency;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
/**
* Represents a {@link ThreadFactory} that records the threads it has spawned.
*/
public class ThreadRecorderThreadFactory implements ThreadFactory {
private final ThreadFactory backing;
private final Set<Thread> threads = ConcurrentHashMap.newKeySet();
public ThreadRecorderThreadFactory(ThreadFactory backing) {
this.backing = Preconditions.checkNotNull(backing, "backing");
}
@Override
public Thread newThread(Runnable runnable) {
Preconditions.checkNotNull(runnable, "runnable");
return backing.newThread(() -> {
threads.add(Thread.currentThread());
try {
runnable.run();
} finally {
threads.remove(Thread.currentThread());
}
});
}
public boolean currentlyInFactory() {
return threads.contains(Thread.currentThread());
}
@VisibleForTesting
int size() {
return threads.size();
}
}

Datei anzeigen

@ -2,7 +2,13 @@
<Configuration status="warn">
<Appenders>
<TerminalConsole name="TerminalConsole">
<PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
<PatternLayout>
<LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level] [%logger]: %minecraftFormatting{%msg}%n%xEx}">
<!-- Velocity doesn't need a prefix -->
<PatternMatch key="com.velocitypowered."
pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
</LoggerNamePatternSelector>
</PatternLayout>
</TerminalConsole>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"
immediateFlush="false">

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -0,0 +1,92 @@
package com.velocitypowered.proxy.testutil;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
public class FakePluginManager implements PluginManager {
public static final Object PLUGIN_A = new Object();
public static final Object PLUGIN_B = new Object();
public static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A);
public static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B);
@Override
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
if (instance == PLUGIN_A) {
return Optional.of(PC_A);
} else if (instance == PLUGIN_B) {
return Optional.of(PC_B);
} else {
return Optional.empty();
}
}
@Override
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
switch (id) {
case "a":
return Optional.of(PC_A);
case "b":
return Optional.of(PC_B);
default:
return Optional.empty();
}
}
@Override
public @NonNull Collection<PluginContainer> getPlugins() {
return ImmutableList.of(PC_A, PC_B);
}
@Override
public boolean isLoaded(@NonNull String id) {
return id.equals("a") || id.equals("b");
}
@Override
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
throw new UnsupportedOperationException();
}
private static class FakePluginContainer implements PluginContainer {
private final String id;
private final Object instance;
private FakePluginContainer(String id, Object instance) {
this.id = id;
this.instance = instance;
}
@Override
public @NonNull PluginDescription getDescription() {
return new PluginDescription() {
@Override
public String getId() {
return id;
}
@Override
public String getVersion() {
return "";
}
@Override
public String getAuthor() {
return "";
}
};
}
@Override
public Optional<?> getInstance() {
return Optional.of(instance);
}
}
}

Datei anzeigen

@ -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;

Datei anzeigen

@ -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());
}
}

Datei anzeigen

@ -7,3 +7,5 @@ include (
findProject(':api')?.name = 'velocity-api'
findProject(':proxy')?.name = 'velocity-proxy'
findProject(':native')?.name = 'velocity-native'
enableFeaturePreview('STABLE_PUBLISHING')