Plugin API (#34)
The Velocity API has had a lot of community input (special thanks to @hugmanrique who started the work, @lucko who contributed permissions support, and @Minecrell for providing initial feedback and an initial version of ServerListPlus). While the API is far from complete, there is enough available for people to start doing useful stuff with Velocity.
This commit is contained in:
@@ -32,12 +32,14 @@ dependencies {
|
||||
|
||||
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||
compile "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
|
||||
|
||||
compile 'net.minecrell:terminalconsoleappender:1.1.1'
|
||||
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
|
||||
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
|
||||
|
||||
compile 'it.unimi.dsi:fastutil:8.2.1'
|
||||
compile 'net.kyori:event-method-asm:3.0.0'
|
||||
|
||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
|
@@ -4,11 +4,15 @@ import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.api.server.Favicon;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.command.ServerCommand;
|
||||
import com.velocitypowered.proxy.command.ShutdownCommand;
|
||||
@@ -16,9 +20,12 @@ import com.velocitypowered.proxy.command.VelocityCommand;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.command.CommandManager;
|
||||
import com.velocitypowered.proxy.command.VelocityCommandManager;
|
||||
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
||||
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
|
||||
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
||||
import com.velocitypowered.proxy.scheduler.Sleeper;
|
||||
import com.velocitypowered.proxy.scheduler.VelocityScheduler;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.util.Ratelimiter;
|
||||
@@ -40,6 +47,7 @@ import java.nio.file.Paths;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class VelocityServer implements ProxyServer {
|
||||
@@ -55,13 +63,14 @@ public class VelocityServer implements ProxyServer {
|
||||
private NettyHttpClient httpClient;
|
||||
private KeyPair serverKeyPair;
|
||||
private final ServerMap servers = new ServerMap();
|
||||
private final CommandManager commandManager = new CommandManager();
|
||||
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
||||
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
|
||||
private boolean shutdown = false;
|
||||
private final VelocityPluginManager pluginManager = new VelocityPluginManager(this);
|
||||
|
||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||
private final CommandInvoker consoleCommandInvoker = new CommandInvoker() {
|
||||
private final CommandSource consoleCommandSource = new CommandSource() {
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Component component) {
|
||||
logger.info(ComponentSerializers.LEGACY.serialize(component));
|
||||
@@ -73,11 +82,13 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
};
|
||||
private Ratelimiter ipAttemptLimiter;
|
||||
private VelocityEventManager eventManager;
|
||||
private VelocityScheduler scheduler;
|
||||
|
||||
private VelocityServer() {
|
||||
commandManager.registerCommand("velocity", new VelocityCommand());
|
||||
commandManager.registerCommand("server", new ServerCommand());
|
||||
commandManager.registerCommand("shutdown", new ShutdownCommand());
|
||||
commandManager.register(new VelocityCommand(), "velocity");
|
||||
commandManager.register(new ServerCommand(), "server");
|
||||
commandManager.register(new ShutdownCommand(), "shutdown");
|
||||
}
|
||||
|
||||
public static VelocityServer getServer() {
|
||||
@@ -92,7 +103,8 @@ public class VelocityServer implements ProxyServer {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public CommandManager getCommandManager() {
|
||||
@Override
|
||||
public VelocityCommandManager getCommandManager() {
|
||||
return commandManager;
|
||||
}
|
||||
|
||||
@@ -121,10 +133,21 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
|
||||
|
||||
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
|
||||
|
||||
httpClient = new NettyHttpClient(this);
|
||||
eventManager = new VelocityEventManager(pluginManager);
|
||||
scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM);
|
||||
loadPlugins();
|
||||
|
||||
// Post the first event
|
||||
pluginManager.getPlugins().forEach(container -> {
|
||||
container.getInstance().ifPresent(plugin -> eventManager.register(plugin, plugin));
|
||||
});
|
||||
try {
|
||||
eventManager.fire(new ProxyInitializeEvent()).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
// Ignore, we don't care.
|
||||
}
|
||||
|
||||
this.cm.bind(configuration.getBind());
|
||||
|
||||
@@ -133,6 +156,29 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPlugins() {
|
||||
logger.info("Loading plugins...");
|
||||
|
||||
try {
|
||||
Path pluginPath = Paths.get("plugins");
|
||||
|
||||
if (Files.notExists(pluginPath)) {
|
||||
Files.createDirectory(pluginPath);
|
||||
} else {
|
||||
if (!Files.isDirectory(pluginPath)) {
|
||||
logger.warn("Plugin location {} is not a directory, continuing without loading plugins", pluginPath);
|
||||
return;
|
||||
}
|
||||
|
||||
pluginManager.loadPlugins(pluginPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Couldn't load plugins", e);
|
||||
}
|
||||
|
||||
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
||||
}
|
||||
|
||||
public ServerMap getServers() {
|
||||
return servers;
|
||||
}
|
||||
@@ -154,6 +200,14 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
this.cm.shutdown();
|
||||
|
||||
eventManager.fire(new ProxyShutdownEvent());
|
||||
try {
|
||||
eventManager.shutdown();
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Your plugins took over 10 seconds to shut down.");
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@@ -226,7 +280,22 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandInvoker getConsoleCommandInvoker() {
|
||||
return consoleCommandInvoker;
|
||||
public CommandSource getConsoleCommandSource() {
|
||||
return consoleCommandSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginManager getPluginManager() {
|
||||
return pluginManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VelocityScheduler getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
|
@@ -1,77 +0,0 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CommandManager {
|
||||
private final Map<String, CommandExecutor> executors = new HashMap<>();
|
||||
|
||||
public void registerCommand(String name, CommandExecutor executor) {
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
Preconditions.checkNotNull(executor, "executor");
|
||||
this.executors.put(name, executor);
|
||||
}
|
||||
|
||||
public void unregisterCommand(String name) {
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
this.executors.remove(name);
|
||||
}
|
||||
|
||||
public boolean execute(CommandInvoker invoker, String cmdLine) {
|
||||
Preconditions.checkNotNull(invoker, "invoker");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String command = split[0];
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
CommandExecutor executor = executors.get(command);
|
||||
if (executor == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
executor.execute(invoker, actualArgs);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + invoker, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<List<String>> offerSuggestions(CommandInvoker invoker, String cmdLine) {
|
||||
Preconditions.checkNotNull(invoker, "invoker");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String command = split[0];
|
||||
if (split.length == 1) {
|
||||
return Optional.of(executors.keySet().stream()
|
||||
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
CommandExecutor executor = executors.get(command);
|
||||
if (executor == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(executor.suggest(invoker, actualArgs));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + invoker, e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
@@ -14,15 +14,15 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ServerCommand implements CommandExecutor {
|
||||
public class ServerCommand implements Command {
|
||||
@Override
|
||||
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
|
||||
if (!(invoker instanceof Player)) {
|
||||
invoker.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
|
||||
public void execute(@Nonnull CommandSource source, @Nonnull String[] args) {
|
||||
if (!(source instanceof Player)) {
|
||||
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = (Player) invoker;
|
||||
Player player = (Player) source;
|
||||
if (args.length == 1) {
|
||||
// Trying to connect to a server.
|
||||
String serverName = args[0];
|
||||
@@ -42,7 +42,7 @@ public class ServerCommand implements CommandExecutor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) {
|
||||
public List<String> suggest(@Nonnull CommandSource source, @Nonnull String[] currentArgs) {
|
||||
if (currentArgs.length == 0) {
|
||||
return VelocityServer.getServer().getAllServers().stream()
|
||||
.map(ServerInfo::getName)
|
||||
|
@@ -1,18 +1,18 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class ShutdownCommand implements CommandExecutor {
|
||||
public class ShutdownCommand implements Command {
|
||||
@Override
|
||||
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
|
||||
if (invoker != VelocityServer.getServer().getConsoleCommandInvoker()) {
|
||||
invoker.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
|
||||
public void execute(@Nonnull CommandSource source, @Nonnull String[] args) {
|
||||
if (source != VelocityServer.getServer().getConsoleCommandSource()) {
|
||||
source.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
|
||||
return;
|
||||
}
|
||||
VelocityServer.getServer().shutdown();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandExecutor;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.event.ClickEvent;
|
||||
@@ -9,9 +9,9 @@ import net.kyori.text.format.TextColor;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class VelocityCommand implements CommandExecutor {
|
||||
public class VelocityCommand implements Command {
|
||||
@Override
|
||||
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
|
||||
public void execute(@Nonnull CommandSource source, @Nonnull String[] args) {
|
||||
String implVersion = VelocityServer.class.getPackage().getImplementationVersion();
|
||||
TextComponent thisIsVelocity = TextComponent.builder()
|
||||
.content("This is ")
|
||||
@@ -35,8 +35,8 @@ public class VelocityCommand implements CommandExecutor {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
invoker.sendMessage(thisIsVelocity);
|
||||
invoker.sendMessage(velocityInfo);
|
||||
invoker.sendMessage(velocityWebsite);
|
||||
source.sendMessage(thisIsVelocity);
|
||||
source.sendMessage(velocityInfo);
|
||||
source.sendMessage(velocityWebsite);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,85 @@
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class VelocityCommandManager implements CommandManager {
|
||||
private final Map<String, Command> commands = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void register(final Command command, final String... aliases) {
|
||||
Preconditions.checkNotNull(aliases, "aliases");
|
||||
Preconditions.checkNotNull(command, "executor");
|
||||
for (int i = 0, length = aliases.length; i < length; i++) {
|
||||
final String alias = aliases[i];
|
||||
Preconditions.checkNotNull(aliases, "alias at index %s", i);
|
||||
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(final String alias) {
|
||||
Preconditions.checkNotNull(alias, "name");
|
||||
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(CommandSource source, String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "invoker");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String alias = split[0];
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
if (command == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
command.execute(source, actualArgs);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<List<String>> offerSuggestions(CommandSource source, String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String command = split[0];
|
||||
if (split.length == 1) {
|
||||
return Optional.of(commands.keySet().stream()
|
||||
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
Command executor = commands.get(command);
|
||||
if (executor == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(executor.suggest(source, actualArgs));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + source, e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
@@ -16,7 +18,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
public void activated() {
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new ServerConnectedEvent(connection.getProxyPlayer(),
|
||||
connection.getServerInfo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(MinecraftPacket packet) {
|
||||
//Not handleable packets: Chat, TabCompleteResponse, Respawn, Scoreboard*
|
||||
if (!connection.getProxyPlayer().isActive()) {
|
||||
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
|
||||
|
@@ -7,7 +7,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.VelocityConstants;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.data.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
@@ -82,19 +83,19 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
response.setTransactionId(req.getTransactionId());
|
||||
response.setStart(lastSpace);
|
||||
response.setLength(req.getCommand().length() - lastSpace);
|
||||
|
||||
for (String s : offers.get()) {
|
||||
response.getOffers().add(new TabCompleteResponse.Offer(s, null));
|
||||
}
|
||||
|
||||
player.getConnection().write(response);
|
||||
return;
|
||||
} else {
|
||||
player.getConnectedServer().getMinecraftConnection().write(packet);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e);
|
||||
TabCompleteResponse response = new TabCompleteResponse();
|
||||
response.setTransactionId(req.getTransactionId());
|
||||
player.getConnection().write(response);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public void disconnected() {
|
||||
player.teardown();
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new DisconnectEvent(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -2,6 +2,9 @@ package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||
import com.velocitypowered.api.permission.PermissionFunction;
|
||||
import com.velocitypowered.api.permission.PermissionProvider;
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.api.util.MessagePosition;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
@@ -9,7 +12,7 @@ import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.data.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.backend.ServerConnection;
|
||||
@@ -25,6 +28,7 @@ import net.kyori.text.serializer.ComponentSerializers;
|
||||
import net.kyori.text.serializer.PlainComponentSerializer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.net.InetSocketAddress;
|
||||
@@ -35,19 +39,23 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
|
||||
public static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
|
||||
|
||||
private final GameProfile profile;
|
||||
private final MinecraftConnection connection;
|
||||
private final InetSocketAddress virtualHost;
|
||||
private PermissionFunction permissionFunction = null;
|
||||
private int tryIndex = 0;
|
||||
private ServerConnection connectedServer;
|
||||
private ClientSettings clientSettings;
|
||||
private ServerConnection connectionInFlight;
|
||||
|
||||
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
|
||||
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection, InetSocketAddress virtualHost) {
|
||||
this.profile = profile;
|
||||
this.connection = connection;
|
||||
this.virtualHost = virtualHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,6 +86,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.ofNullable(virtualHost);
|
||||
}
|
||||
|
||||
public void setPermissionFunction(PermissionFunction permissionFunction) {
|
||||
this.permissionFunction = permissionFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return connection.getChannel().isActive();
|
||||
@@ -89,7 +106,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
|
||||
public void sendMessage(@NonNull Component component, @NonNull MessagePosition position) {
|
||||
Preconditions.checkNotNull(component, "component");
|
||||
Preconditions.checkNotNull(position, "position");
|
||||
|
||||
@@ -112,7 +129,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
|
||||
public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) {
|
||||
return new ConnectionRequestBuilderImpl(info);
|
||||
}
|
||||
|
||||
@@ -191,8 +208,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
// Otherwise, initiate the connection.
|
||||
ServerConnection connection = new ServerConnection(request.getServer(), this, VelocityServer.getServer());
|
||||
return connection.connect();
|
||||
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
|
||||
return VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenCompose((newEvent) -> {
|
||||
if (!newEvent.getResult().isAllowed()) {
|
||||
return CompletableFuture.completedFuture(
|
||||
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED)
|
||||
);
|
||||
}
|
||||
|
||||
return new ServerConnection(newEvent.getResult().getInfo().get(), this, VelocityServer.getServer()).connect();
|
||||
});
|
||||
}
|
||||
|
||||
public void setConnectedServer(ServerConnection serverConnection) {
|
||||
@@ -223,13 +249,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@Nonnull String permission) {
|
||||
return false; // TODO: Implement permissions.
|
||||
return permissionFunction.getPermissionSetting(permission).asBoolean();
|
||||
}
|
||||
|
||||
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
||||
private final ServerInfo info;
|
||||
|
||||
public ConnectionRequestBuilderImpl(ServerInfo info) {
|
||||
ConnectionRequestBuilderImpl(ServerInfo info) {
|
||||
this.info = Preconditions.checkNotNull(info, "info");
|
||||
}
|
||||
|
||||
|
@@ -1,12 +1,15 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.data.ServerPing;
|
||||
import com.velocitypowered.api.server.ServerPing;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@@ -17,6 +20,7 @@ import net.kyori.text.format.TextColor;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
private final MinecraftConnection connection;
|
||||
@@ -37,12 +41,14 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName());
|
||||
}
|
||||
|
||||
InitialInboundConnection ic = new InitialInboundConnection(connection, (Handshake) packet);
|
||||
|
||||
Handshake handshake = (Handshake) packet;
|
||||
switch (handshake.getNextStatus()) {
|
||||
case StateRegistry.STATUS_ID:
|
||||
connection.setState(StateRegistry.STATUS);
|
||||
connection.setProtocolVersion(handshake.getProtocolVersion());
|
||||
connection.setSessionHandler(new StatusSessionHandler(connection));
|
||||
connection.setSessionHandler(new StatusSessionHandler(connection, ic));
|
||||
break;
|
||||
case StateRegistry.LOGIN_ID:
|
||||
connection.setState(StateRegistry.LOGIN);
|
||||
@@ -56,7 +62,8 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
|
||||
return;
|
||||
}
|
||||
connection.setSessionHandler(new LoginSessionHandler(connection));
|
||||
VelocityServer.getServer().getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
|
||||
connection.setSessionHandler(new LoginSessionHandler(connection, ic));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -69,21 +76,25 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
|
||||
ServerPing ping = new ServerPing(
|
||||
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
|
||||
configuration.getMotdComponent(),
|
||||
null
|
||||
);
|
||||
// The disconnect packet is the same as the server response one.
|
||||
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(ping)));
|
||||
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
// The disconnect packet is the same as the server response one.
|
||||
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(event.getPing())));
|
||||
}, connection.getChannel().eventLoop());
|
||||
} else if (packet instanceof LegacyHandshake) {
|
||||
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
|
||||
}
|
||||
}
|
||||
|
||||
private static class InitialInboundConnection implements InboundConnection {
|
||||
private static class LegacyInboundConnection implements InboundConnection {
|
||||
private final MinecraftConnection connection;
|
||||
|
||||
private InitialInboundConnection(MinecraftConnection connection) {
|
||||
private LegacyInboundConnection(MinecraftConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@@ -92,14 +103,19 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return connection.getChannel().isActive();
|
||||
return !connection.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProtocolVersion() {
|
||||
return connection.getProtocolVersion();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,38 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.protocol.packet.Handshake;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
class InitialInboundConnection implements InboundConnection {
|
||||
private final MinecraftConnection connection;
|
||||
private final Handshake handshake;
|
||||
|
||||
InitialInboundConnection(MinecraftConnection connection, Handshake handshake) {
|
||||
this.connection = connection;
|
||||
this.handshake = handshake;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return (InetSocketAddress) connection.getChannel().remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InetSocketAddress> getVirtualHost() {
|
||||
return Optional.of(InetSocketAddress.createUnresolved(handshake.getServerAddress(), handshake.getPort()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return connection.getChannel().isActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProtocolVersion() {
|
||||
return connection.getProtocolVersion();
|
||||
}
|
||||
}
|
@@ -1,8 +1,13 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.event.connection.LoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.connection.VelocityConstants;
|
||||
import com.velocitypowered.proxy.data.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@@ -10,7 +15,6 @@ import com.velocitypowered.proxy.protocol.packet.*;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.kyori.text.TextComponent;
|
||||
@@ -33,12 +37,14 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
|
||||
|
||||
private final MinecraftConnection inbound;
|
||||
private final InboundConnection apiInbound;
|
||||
private ServerLogin login;
|
||||
private byte[] verify;
|
||||
private int playerInfoId;
|
||||
|
||||
public LoginSessionHandler(MinecraftConnection inbound) {
|
||||
public LoginSessionHandler(MinecraftConnection inbound, InboundConnection apiInbound) {
|
||||
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
|
||||
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,7 +59,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
));
|
||||
} else {
|
||||
// Proceed with the regular login process.
|
||||
initiateLogin();
|
||||
beginPreLogin();
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ServerLogin) {
|
||||
@@ -67,7 +73,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
message.setData(Unpooled.EMPTY_BUFFER);
|
||||
inbound.write(message);
|
||||
} else {
|
||||
initiateLogin();
|
||||
beginPreLogin();
|
||||
}
|
||||
} else if (packet instanceof EncryptionResponse) {
|
||||
try {
|
||||
@@ -97,7 +103,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
|
||||
handleSuccessfulLogin(profile);
|
||||
initializePlayer(profile);
|
||||
}, inbound.getChannel().eventLoop())
|
||||
.exceptionally(exception -> {
|
||||
logger.error("Unable to enable encryption", exception);
|
||||
@@ -113,16 +119,26 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private void initiateLogin() {
|
||||
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
|
||||
// Request encryption.
|
||||
EncryptionRequest request = generateRequest();
|
||||
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
|
||||
inbound.write(request);
|
||||
} else {
|
||||
// Offline-mode, don't try to request encryption.
|
||||
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
|
||||
}
|
||||
private void beginPreLogin() {
|
||||
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
if (!event.getResult().isAllowed()) {
|
||||
// The component is guaranteed to be provided if the connection was denied.
|
||||
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
|
||||
// Request encryption.
|
||||
EncryptionRequest request = generateRequest();
|
||||
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
|
||||
inbound.write(request);
|
||||
} else {
|
||||
// Offline-mode, don't try to request encryption.
|
||||
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()));
|
||||
}
|
||||
}, inbound.getChannel().eventLoop());
|
||||
}
|
||||
|
||||
private EncryptionRequest generateRequest() {
|
||||
@@ -135,9 +151,31 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
return request;
|
||||
}
|
||||
|
||||
private void handleSuccessfulLogin(GameProfile profile) {
|
||||
private void initializePlayer(GameProfile profile) {
|
||||
// Initiate a regular connection and move over to it.
|
||||
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
|
||||
ConnectedPlayer player = new ConnectedPlayer(profile, inbound, apiInbound.getVirtualHost().orElse(null));
|
||||
|
||||
// load permissions first
|
||||
VelocityServer.getServer().getEventManager().fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
|
||||
.thenCompose(event -> {
|
||||
// wait for permissions to load, then set the players permission function
|
||||
player.setPermissionFunction(event.createFunction(player));
|
||||
// then call & wait for the login event
|
||||
return VelocityServer.getServer().getEventManager().fire(new LoginEvent(player));
|
||||
})
|
||||
// then complete the connection
|
||||
.thenAcceptAsync(event -> {
|
||||
if (!event.getResult().isAllowed()) {
|
||||
// The component is guaranteed to be provided if the connection was denied.
|
||||
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
|
||||
return;
|
||||
}
|
||||
|
||||
handleProxyLogin(player);
|
||||
}, inbound.getChannel().eventLoop());
|
||||
}
|
||||
|
||||
private void handleProxyLogin(ConnectedPlayer player) {
|
||||
Optional<ServerInfo> toTry = player.getNextServerToTry();
|
||||
if (!toTry.isPresent()) {
|
||||
player.close(TextComponent.of("No available servers", TextColor.RED));
|
||||
@@ -151,8 +189,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
ServerLoginSuccess success = new ServerLoginSuccess();
|
||||
success.setUsername(profile.getName());
|
||||
success.setUuid(profile.idAsUuid());
|
||||
success.setUsername(player.getUsername());
|
||||
success.setUuid(player.getUniqueId());
|
||||
inbound.write(success);
|
||||
|
||||
inbound.setAssociation(player);
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
@@ -9,16 +12,18 @@ import com.velocitypowered.proxy.protocol.packet.StatusPing;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.data.ServerPing;
|
||||
import com.velocitypowered.api.server.ServerPing;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
|
||||
public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
private final MinecraftConnection connection;
|
||||
private final InboundConnection inboundWrapper;
|
||||
|
||||
public StatusSessionHandler(MinecraftConnection connection) {
|
||||
public StatusSessionHandler(MinecraftConnection connection, InboundConnection inboundWrapper) {
|
||||
this.connection = connection;
|
||||
this.inboundWrapper = inboundWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,15 +42,20 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
// Status request
|
||||
int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection.getProtocolVersion() :
|
||||
ProtocolConstants.MAXIMUM_GENERIC_VERSION;
|
||||
ServerPing ping = new ServerPing(
|
||||
ServerPing initialPing = new ServerPing(
|
||||
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
|
||||
configuration.getMotdComponent(),
|
||||
configuration.getFavicon()
|
||||
);
|
||||
StatusResponse response = new StatusResponse();
|
||||
response.setStatus(VelocityServer.GSON.toJson(ping));
|
||||
connection.write(response);
|
||||
|
||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
||||
VelocityServer.getServer().getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
StatusResponse response = new StatusResponse();
|
||||
response.setStatus(VelocityServer.GSON.toJson(event.getPing()));
|
||||
connection.write(response);
|
||||
}, connection.getChannel().eventLoop());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -22,13 +22,12 @@ public final class VelocityConsole extends SimpleTerminalConsole {
|
||||
return super.buildReader(builder
|
||||
.appName("Velocity")
|
||||
.completer((reader, parsedLine, list) -> {
|
||||
Optional<List<String>> offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line());
|
||||
if (offers.isPresent()) {
|
||||
for (String offer : offers.get()) {
|
||||
if (offer.isEmpty()) continue;
|
||||
Optional<List<String>> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line());
|
||||
o.ifPresent(offers -> {
|
||||
for (String offer : offers) {
|
||||
list.add(new Candidate(offer));
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -40,8 +39,8 @@ public final class VelocityConsole extends SimpleTerminalConsole {
|
||||
|
||||
@Override
|
||||
protected void runCommand(String command) {
|
||||
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandInvoker(), command)) {
|
||||
server.getConsoleCommandInvoker().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
|
||||
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) {
|
||||
server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,84 +0,0 @@
|
||||
package com.velocitypowered.proxy.data;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.proxy.util.UuidUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GameProfile {
|
||||
private final String id;
|
||||
private final String name;
|
||||
private final List<Property> properties;
|
||||
|
||||
public GameProfile(String id, String name, List<Property> properties) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.properties = ImmutableList.copyOf(properties);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UUID idAsUuid() {
|
||||
return UuidUtils.fromUndashed(id);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<Property> getProperties() {
|
||||
return ImmutableList.copyOf(properties);
|
||||
}
|
||||
|
||||
public static GameProfile forOfflinePlayer(String username) {
|
||||
Preconditions.checkNotNull(username, "username");
|
||||
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
|
||||
return new GameProfile(id, username, ImmutableList.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GameProfile{" +
|
||||
"id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", properties=" + properties +
|
||||
'}';
|
||||
}
|
||||
|
||||
public class Property {
|
||||
private final String name;
|
||||
private final String value;
|
||||
private final String signature;
|
||||
|
||||
public Property(String name, String value, String signature) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Property{" +
|
||||
"name='" + name + '\'' +
|
||||
", value='" + value + '\'' +
|
||||
", signature='" + signature + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,96 +0,0 @@
|
||||
package com.velocitypowered.proxy.data;
|
||||
|
||||
import com.velocitypowered.api.server.Favicon;
|
||||
import net.kyori.text.Component;
|
||||
|
||||
public class ServerPing {
|
||||
private final Version version;
|
||||
private final Players players;
|
||||
private final Component description;
|
||||
private final Favicon favicon;
|
||||
|
||||
public ServerPing(Version version, Players players, Component description, Favicon favicon) {
|
||||
this.version = version;
|
||||
this.players = players;
|
||||
this.description = description;
|
||||
this.favicon = favicon;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Players getPlayers() {
|
||||
return players;
|
||||
}
|
||||
|
||||
public Component getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Favicon getFavicon() {
|
||||
return favicon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerPing{" +
|
||||
"version=" + version +
|
||||
", players=" + players +
|
||||
", description=" + description +
|
||||
", favicon='" + favicon + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class Version {
|
||||
private final int protocol;
|
||||
private final String name;
|
||||
|
||||
public Version(int protocol, String name) {
|
||||
this.protocol = protocol;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Version{" +
|
||||
"protocol=" + protocol +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class Players {
|
||||
private final int online;
|
||||
private final int max;
|
||||
|
||||
public Players(int online, int max) {
|
||||
this.online = online;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public int getOnline() {
|
||||
return online;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Players{" +
|
||||
"online=" + online +
|
||||
", max=" + max +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
public class PluginClassLoader extends URLClassLoader {
|
||||
private static final Set<PluginClassLoader> loaders = new CopyOnWriteArraySet<>();
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
public PluginClassLoader(URL[] urls) {
|
||||
super(urls);
|
||||
loaders.add(this);
|
||||
}
|
||||
|
||||
public void addPath(Path path) {
|
||||
try {
|
||||
addURL(path.toUri().toURL());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
return loadClass0(name, resolve, true);
|
||||
}
|
||||
|
||||
private Class<?> loadClass0(String name, boolean resolve, boolean checkOther) throws ClassNotFoundException {
|
||||
try {
|
||||
return super.loadClass(name, resolve);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// Ignored: we'll try others
|
||||
}
|
||||
|
||||
if (checkOther) {
|
||||
for (PluginClassLoader loader : loaders) {
|
||||
if (loader != this) {
|
||||
try {
|
||||
return loader.loadClass0(name, resolve, false);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// We're trying others, safe to ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
}
|
@@ -0,0 +1,192 @@
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.event.EventHandler;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.proxy.util.concurrency.ThreadRecorderThreadFactory;
|
||||
import net.kyori.event.EventSubscriber;
|
||||
import net.kyori.event.PostResult;
|
||||
import net.kyori.event.SimpleEventBus;
|
||||
import net.kyori.event.method.*;
|
||||
import net.kyori.event.method.asm.ASMEventExecutorFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class VelocityEventManager implements EventManager {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityEventManager.class);
|
||||
|
||||
private final ListMultimap<Object, Object> registeredListenersByPlugin = Multimaps
|
||||
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
|
||||
private final ListMultimap<Object, EventHandler<?>> registeredHandlersByPlugin = Multimaps
|
||||
.synchronizedListMultimap(Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
|
||||
private final VelocityEventBus bus = new VelocityEventBus(
|
||||
new ASMEventExecutorFactory<>(new PluginClassLoader(new URL[0])),
|
||||
new VelocityMethodScanner());
|
||||
private final ExecutorService service;
|
||||
private final ThreadRecorderThreadFactory recordingThreadFactory;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
public VelocityEventManager(PluginManager pluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.recordingThreadFactory = new ThreadRecorderThreadFactory(new ThreadFactoryBuilder()
|
||||
.setNameFormat("Velocity Event Executor - #%d").setDaemon(true).build());
|
||||
this.service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), recordingThreadFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Object plugin, @NonNull Object listener) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(listener, "listener");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
|
||||
registeredListenersByPlugin.put(plugin, listener);
|
||||
bus.register(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(eventClass, "eventClass");
|
||||
Preconditions.checkNotNull(postOrder, "postOrder");
|
||||
Preconditions.checkNotNull(handler, "listener");
|
||||
bus.register(eventClass, new KyoriToVelocityHandler<>(handler, postOrder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> @NonNull CompletableFuture<E> fire(@NonNull E event) {
|
||||
Preconditions.checkNotNull(event, "event");
|
||||
if (!bus.hasSubscribers(event.getClass())) {
|
||||
// Optimization: nobody's listening.
|
||||
return CompletableFuture.completedFuture(event);
|
||||
}
|
||||
|
||||
Runnable runEvent = () -> {
|
||||
PostResult result = bus.post(event);
|
||||
if (!result.exceptions().isEmpty()) {
|
||||
logger.error("Some errors occurred whilst posting event {}.", event);
|
||||
int i = 0;
|
||||
for (Throwable exception : result.exceptions().values()) {
|
||||
logger.error("#{}: \n", i++, exception);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (recordingThreadFactory.currentlyInFactory()) {
|
||||
// Optimization: fire the event immediately, we are on the event handling thread.
|
||||
runEvent.run();
|
||||
return CompletableFuture.completedFuture(event);
|
||||
}
|
||||
|
||||
CompletableFuture<E> eventFuture = new CompletableFuture<>();
|
||||
service.execute(() -> {
|
||||
runEvent.run();
|
||||
eventFuture.complete(event);
|
||||
});
|
||||
return eventFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListeners(@NonNull Object plugin) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
|
||||
Collection<Object> listeners = registeredListenersByPlugin.removeAll(plugin);
|
||||
listeners.forEach(bus::unregister);
|
||||
Collection<EventHandler<?>> handlers = registeredHandlersByPlugin.removeAll(plugin);
|
||||
handlers.forEach(bus::unregister);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListener(@NonNull Object plugin, @NonNull Object listener) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(listener, "listener");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "Specified plugin is not loaded");
|
||||
registeredListenersByPlugin.remove(plugin, listener);
|
||||
bus.unregister(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(handler, "listener");
|
||||
registeredHandlersByPlugin.remove(plugin, handler);
|
||||
bus.unregister(handler);
|
||||
}
|
||||
|
||||
public void shutdown() throws InterruptedException {
|
||||
service.shutdown();
|
||||
service.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static class VelocityEventBus extends SimpleEventBus<Object> {
|
||||
private final MethodSubscriptionAdapter<Object> methodAdapter;
|
||||
|
||||
VelocityEventBus(EventExecutor.@NonNull Factory<Object, Object> factory, @NonNull MethodScanner<Object> methodScanner) {
|
||||
super(Object.class);
|
||||
this.methodAdapter = new SimpleMethodSubscriptionAdapter<>(this, factory, methodScanner);
|
||||
}
|
||||
|
||||
void register(Object listener) {
|
||||
this.methodAdapter.register(listener);
|
||||
}
|
||||
|
||||
void unregister(Object listener) {
|
||||
this.methodAdapter.unregister(listener);
|
||||
}
|
||||
|
||||
void unregister(EventHandler<?> handler) {
|
||||
this.unregister(s -> s instanceof KyoriToVelocityHandler && ((KyoriToVelocityHandler<?>) s).getHandler().equals(handler));
|
||||
}
|
||||
}
|
||||
|
||||
private static class VelocityMethodScanner implements MethodScanner<Object> {
|
||||
@Override
|
||||
public boolean shouldRegister(@NonNull Object listener, @NonNull Method method) {
|
||||
return method.isAnnotationPresent(Subscribe.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int postOrder(@NonNull Object listener, @NonNull Method method) {
|
||||
return method.getAnnotation(Subscribe.class).order().ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumeCancelledEvents(@NonNull Object listener, @NonNull Method method) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class KyoriToVelocityHandler<E> implements EventSubscriber<E> {
|
||||
private final EventHandler<E> handler;
|
||||
private final int postOrder;
|
||||
|
||||
private KyoriToVelocityHandler(EventHandler<E> handler, PostOrder postOrder) {
|
||||
this.handler = handler;
|
||||
this.postOrder = postOrder.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(@NonNull E event) {
|
||||
handler.execute(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int postOrder() {
|
||||
return postOrder;
|
||||
}
|
||||
|
||||
public EventHandler<E> getHandler() {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.plugin.loader.JavaPluginLoader;
|
||||
import com.velocitypowered.proxy.plugin.util.PluginDependencyUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class VelocityPluginManager implements PluginManager {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class);
|
||||
|
||||
private final Map<String, PluginContainer> plugins = new HashMap<>();
|
||||
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
||||
private final VelocityServer server;
|
||||
|
||||
public VelocityPluginManager(VelocityServer server) {
|
||||
this.server = checkNotNull(server, "server");
|
||||
}
|
||||
|
||||
private void registerPlugin(@NonNull PluginContainer plugin) {
|
||||
plugins.put(plugin.getDescription().getId(), plugin);
|
||||
plugin.getInstance().ifPresent(instance -> pluginInstances.put(instance, plugin));
|
||||
}
|
||||
|
||||
public void loadPlugins(@NonNull Path directory) throws IOException {
|
||||
checkNotNull(directory, "directory");
|
||||
checkArgument(Files.isDirectory(directory), "provided path isn't a directory");
|
||||
|
||||
List<PluginDescription> found = new ArrayList<>();
|
||||
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, p -> Files.isRegularFile(p) && p.toString().endsWith(".jar"))) {
|
||||
for (Path path : stream) {
|
||||
try {
|
||||
found.add(loader.loadPlugin(path));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to load plugin {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found.isEmpty()) {
|
||||
// No plugins found
|
||||
return;
|
||||
}
|
||||
|
||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
|
||||
|
||||
// Now load the plugins
|
||||
pluginLoad:
|
||||
for (PluginDescription plugin : sortedPlugins) {
|
||||
// Verify dependencies
|
||||
for (PluginDependency dependency : plugin.getDependencies()) {
|
||||
if (!dependency.isOptional() && !isLoaded(dependency.getId())) {
|
||||
logger.error("Can't load plugin {} due to missing dependency {}", plugin.getId(), dependency.getId());
|
||||
continue pluginLoad;
|
||||
}
|
||||
}
|
||||
|
||||
// Actually create the plugin
|
||||
PluginContainer pluginObject;
|
||||
|
||||
try {
|
||||
pluginObject = loader.createPlugin(plugin);
|
||||
} catch (Exception e) {
|
||||
logger.error("Can't create plugin {}", plugin.getId(), e);
|
||||
continue;
|
||||
}
|
||||
|
||||
registerPlugin(pluginObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
|
||||
checkNotNull(instance, "instance");
|
||||
|
||||
if (instance instanceof PluginContainer) {
|
||||
return Optional.of((PluginContainer) instance);
|
||||
}
|
||||
|
||||
return Optional.ofNullable(pluginInstances.get(instance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
|
||||
checkNotNull(id, "id");
|
||||
return Optional.ofNullable(plugins.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<PluginContainer> getPlugins() {
|
||||
return Collections.unmodifiableCollection(plugins.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded(@NonNull String id) {
|
||||
return plugins.containsKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
|
||||
checkNotNull(plugin, "instance");
|
||||
checkNotNull(path, "path");
|
||||
checkArgument(pluginInstances.containsKey(plugin), "plugin is not loaded");
|
||||
|
||||
ClassLoader pluginClassloader = plugin.getClass().getClassLoader();
|
||||
if (pluginClassloader instanceof PluginClassLoader) {
|
||||
((PluginClassLoader) pluginClassloader).addPath(path);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Operation is not supported on non-Java Velocity plugins.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.velocitypowered.api.plugin.*;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.plugin.PluginClassLoader;
|
||||
import com.velocitypowered.proxy.plugin.loader.java.JavaVelocityPluginDescription;
|
||||
import com.velocitypowered.proxy.plugin.loader.java.SerializedPluginDescription;
|
||||
import com.velocitypowered.proxy.plugin.loader.java.VelocityPluginModule;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class JavaPluginLoader implements PluginLoader {
|
||||
private final ProxyServer server;
|
||||
private final Path baseDirectory;
|
||||
|
||||
public JavaPluginLoader(ProxyServer server, Path baseDirectory) {
|
||||
this.server = server;
|
||||
this.baseDirectory = baseDirectory;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PluginDescription loadPlugin(Path source) throws Exception {
|
||||
Optional<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
|
||||
|
||||
if (!serialized.isPresent()) {
|
||||
throw new InvalidPluginException("Did not find a valid velocity-info.json.");
|
||||
}
|
||||
|
||||
PluginClassLoader loader = new PluginClassLoader(
|
||||
new URL[] {source.toUri().toURL() }
|
||||
);
|
||||
|
||||
Class mainClass = loader.loadClass(serialized.get().getMain());
|
||||
VelocityPluginDescription description = createDescription(serialized.get(), source, mainClass);
|
||||
|
||||
String pluginId = description.getId();
|
||||
Pattern pattern = PluginDescription.ID_PATTERN;
|
||||
|
||||
if (!pattern.matcher(pluginId).matches()) {
|
||||
throw new InvalidPluginException("Plugin ID '" + pluginId + "' must match pattern " + pattern.pattern());
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PluginContainer createPlugin(PluginDescription description) throws Exception {
|
||||
if (!(description instanceof JavaVelocityPluginDescription)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
|
||||
}
|
||||
|
||||
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
|
||||
Optional<Path> source = javaDescription.getSource();
|
||||
|
||||
if (!source.isPresent()) {
|
||||
throw new IllegalArgumentException("No path in plugin description");
|
||||
}
|
||||
|
||||
Injector injector = Guice.createInjector(new VelocityPluginModule(server, javaDescription, baseDirectory));
|
||||
Object instance = injector.getInstance(javaDescription.getMainClass());
|
||||
|
||||
return new VelocityPluginContainer(
|
||||
description.getId(),
|
||||
description.getVersion(),
|
||||
description.getAuthor(),
|
||||
description.getDependencies(),
|
||||
source.get(),
|
||||
instance
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source) throws Exception {
|
||||
try (JarInputStream in = new JarInputStream(new BufferedInputStream(Files.newInputStream(source)))) {
|
||||
JarEntry entry;
|
||||
while ((entry = in.getNextJarEntry()) != null) {
|
||||
if (entry.getName().equals("velocity-plugin.json")) {
|
||||
try (Reader pluginInfoReader = new InputStreamReader(in)) {
|
||||
return Optional.of(VelocityServer.GSON.fromJson(pluginInfoReader, SerializedPluginDescription.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private VelocityPluginDescription createDescription(SerializedPluginDescription description, Path source, Class mainClass) {
|
||||
Set<PluginDependency> dependencies = new HashSet<>();
|
||||
|
||||
for (SerializedPluginDescription.Dependency dependency : description.getDependencies()) {
|
||||
dependencies.add(toDependencyMeta(dependency));
|
||||
}
|
||||
|
||||
return new JavaVelocityPluginDescription(
|
||||
description.getId(),
|
||||
description.getVersion(),
|
||||
description.getAuthor(),
|
||||
dependencies,
|
||||
source,
|
||||
mainClass
|
||||
);
|
||||
}
|
||||
|
||||
private static PluginDependency toDependencyMeta(SerializedPluginDescription.Dependency dependency) {
|
||||
return new PluginDependency(
|
||||
dependency.getId(),
|
||||
null, // TODO Implement version matching in dependency annotation
|
||||
dependency.isOptional()
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* This interface is used for loading plugins.
|
||||
*/
|
||||
public interface PluginLoader {
|
||||
@Nonnull
|
||||
PluginDescription loadPlugin(Path source) throws Exception;
|
||||
|
||||
@Nonnull
|
||||
PluginContainer createPlugin(PluginDescription plugin) throws Exception;
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public class VelocityPluginContainer extends VelocityPluginDescription implements PluginContainer {
|
||||
private final Object instance;
|
||||
|
||||
public VelocityPluginContainer(String id, String version, String author, Collection<PluginDependency> dependencies, Path source, Object instance) {
|
||||
super(id, version, author, dependencies, source);
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginDescription getDescription() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<?> getInstance() {
|
||||
return Optional.ofNullable(instance);
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class VelocityPluginDescription implements PluginDescription {
|
||||
private final String id;
|
||||
private final String version;
|
||||
private final String author;
|
||||
private final Map<String, PluginDependency> dependencies;
|
||||
private final Path source;
|
||||
|
||||
public VelocityPluginDescription(String id, String version, String author, Collection<PluginDependency> dependencies, Path source) {
|
||||
this.id = checkNotNull(id, "id");
|
||||
this.version = checkNotNull(version, "version");
|
||||
this.author = checkNotNull(author, "author");
|
||||
this.dependencies = Maps.uniqueIndex(dependencies, PluginDependency::getId);
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PluginDependency> getDependencies() {
|
||||
return dependencies.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PluginDependency> getDependency(String id) {
|
||||
return Optional.ofNullable(dependencies.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getSource() {
|
||||
return Optional.ofNullable(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VelocityPluginDescription{" +
|
||||
"id='" + id + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", author='" + author + '\'' +
|
||||
", dependencies=" + dependencies +
|
||||
", source=" + source +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class JavaVelocityPluginDescription extends VelocityPluginDescription {
|
||||
private final Class mainClass;
|
||||
|
||||
public JavaVelocityPluginDescription(String id, String version, String author, Collection<PluginDependency> dependencies, Path source, Class mainClass) {
|
||||
super(id, version, author, dependencies, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
public Class getMainClass() {
|
||||
return mainClass;
|
||||
}
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SerializedPluginDescription {
|
||||
private final String id;
|
||||
private final String author;
|
||||
private final String main;
|
||||
private final String version;
|
||||
private final List<Dependency> dependencies;
|
||||
|
||||
public SerializedPluginDescription(String id, String author, String main, String version) {
|
||||
this(id, author, main, version, ImmutableList.of());
|
||||
}
|
||||
|
||||
public SerializedPluginDescription(String id, String author, String main, String version, List<Dependency> dependencies) {
|
||||
this.id = Preconditions.checkNotNull(id, "id");
|
||||
this.author = Preconditions.checkNotNull(author, "author");
|
||||
this.main = Preconditions.checkNotNull(main, "main");
|
||||
this.version = Preconditions.checkNotNull(version, "version");
|
||||
this.dependencies = ImmutableList.copyOf(dependencies);
|
||||
}
|
||||
|
||||
public static SerializedPluginDescription from(Plugin plugin, String qualifiedName) {
|
||||
List<Dependency> dependencies = new ArrayList<>();
|
||||
for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) {
|
||||
dependencies.add(new Dependency(dependency.id(), dependency.optional()));
|
||||
}
|
||||
return new SerializedPluginDescription(plugin.id(), plugin.author(), qualifiedName, plugin.version(), dependencies);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<Dependency> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SerializedPluginDescription that = (SerializedPluginDescription) o;
|
||||
return Objects.equals(id, that.id) &&
|
||||
Objects.equals(author, that.author) &&
|
||||
Objects.equals(main, that.main) &&
|
||||
Objects.equals(version, that.version) &&
|
||||
Objects.equals(dependencies, that.dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, author, main, version, dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SerializedPluginDescription{" +
|
||||
"id='" + id + '\'' +
|
||||
", author='" + author + '\'' +
|
||||
", main='" + main + '\'' +
|
||||
", version='" + version + '\'' +
|
||||
", dependencies=" + dependencies +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class Dependency {
|
||||
private final String id;
|
||||
private final boolean optional;
|
||||
|
||||
public Dependency(String id, boolean optional) {
|
||||
this.id = id;
|
||||
this.optional = optional;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isOptional() {
|
||||
return optional;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Dependency that = (Dependency) o;
|
||||
return optional == that.optional &&
|
||||
Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, optional);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Dependency{" +
|
||||
"id='" + id + '\'' +
|
||||
", optional=" + optional +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.google.inject.Binder;
|
||||
import com.google.inject.Module;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class VelocityPluginModule implements Module {
|
||||
private final ProxyServer server;
|
||||
private final JavaVelocityPluginDescription description;
|
||||
private final Path basePluginPath;
|
||||
|
||||
public VelocityPluginModule(ProxyServer server, JavaVelocityPluginDescription description, Path basePluginPath) {
|
||||
this.server = server;
|
||||
this.description = description;
|
||||
this.basePluginPath = basePluginPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Binder binder) {
|
||||
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
|
||||
binder.bind(ProxyServer.class).toInstance(server);
|
||||
binder.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(basePluginPath.resolve(description.getId()));
|
||||
binder.bind(PluginDescription.class).toInstance(description);
|
||||
binder.bind(PluginManager.class).toInstance(server.getPluginManager());
|
||||
binder.bind(EventManager.class).toInstance(server.getEventManager());
|
||||
binder.bind(CommandManager.class).toInstance(server.getCommandManager());
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.graph.Graph;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class PluginDependencyUtils {
|
||||
public static List<PluginDescription> sortCandidates(List<PluginDescription> candidates) {
|
||||
// Create our graph, we're going to be using this for Kahn's algorithm.
|
||||
MutableGraph<PluginDescription> graph = GraphBuilder.directed().allowsSelfLoops(false).build();
|
||||
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(candidates, PluginDescription::getId);
|
||||
|
||||
// Add edges
|
||||
for (PluginDescription description : candidates) {
|
||||
graph.addNode(description);
|
||||
|
||||
for (PluginDependency dependency : description.getDependencies()) {
|
||||
PluginDescription in = candidateMap.get(dependency.getId());
|
||||
|
||||
if (in != null) {
|
||||
graph.putEdge(description, in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find nodes that have no edges
|
||||
Queue<PluginDescription> noEdges = getNoDependencyCandidates(graph);
|
||||
|
||||
// Actually run Kahn's algorithm
|
||||
List<PluginDescription> sorted = new ArrayList<>();
|
||||
while (!noEdges.isEmpty()) {
|
||||
PluginDescription candidate = noEdges.poll();
|
||||
sorted.add(candidate);
|
||||
|
||||
for (PluginDescription node : graph.successors(candidate)) {
|
||||
graph.removeEdge(node, candidate);
|
||||
|
||||
if (graph.adjacentNodes(node).isEmpty()) {
|
||||
if (!noEdges.contains(node)) {
|
||||
noEdges.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!graph.edges().isEmpty()) {
|
||||
throw new IllegalStateException("Plugin circular dependency found: " + graph.toString());
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
public static Queue<PluginDescription> getNoDependencyCandidates(Graph<PluginDescription> graph) {
|
||||
Queue<PluginDescription> found = new ArrayDeque<>();
|
||||
|
||||
for (PluginDescription node : graph.nodes()) {
|
||||
if (graph.outDegree(node) == 0) {
|
||||
found.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.velocitypowered.proxy.data.ServerPing;
|
||||
import com.velocitypowered.api.server.ServerPing;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
|
||||
public class LegacyPingResponse {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
public interface Sleeper {
|
||||
void sleep(long ms) throws InterruptedException;
|
||||
|
||||
Sleeper SYSTEM = Thread::sleep;
|
||||
}
|
@@ -0,0 +1,176 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.Scheduler;
|
||||
import com.velocitypowered.api.scheduler.TaskStatus;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class VelocityScheduler implements Scheduler {
|
||||
private final PluginManager pluginManager;
|
||||
private final ExecutorService taskService;
|
||||
private final Sleeper sleeper;
|
||||
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedListMultimap(
|
||||
Multimaps.newListMultimap(new IdentityHashMap<>(), ArrayList::new));
|
||||
|
||||
public VelocityScheduler(PluginManager pluginManager, Sleeper sleeper) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.sleeper = sleeper;
|
||||
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
|
||||
.setNameFormat("Velocity Task Scheduler - #%d").build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder buildTask(Object plugin, Runnable runnable) {
|
||||
Preconditions.checkNotNull(plugin, "plugin");
|
||||
Preconditions.checkNotNull(runnable, "runnable");
|
||||
Preconditions.checkArgument(pluginManager.fromInstance(plugin).isPresent(), "plugin is not registered");
|
||||
return new TaskBuilderImpl(plugin, runnable);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
for (ScheduledTask task : ImmutableList.copyOf(tasksByPlugin.values())) {
|
||||
task.cancel();
|
||||
}
|
||||
taskService.shutdown();
|
||||
}
|
||||
|
||||
private class TaskBuilderImpl implements TaskBuilder {
|
||||
private final Object plugin;
|
||||
private final Runnable runnable;
|
||||
private long delay; // ms
|
||||
private long repeat; // ms
|
||||
|
||||
private TaskBuilderImpl(Object plugin, Runnable runnable) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder delay(int time, TimeUnit unit) {
|
||||
this.delay = unit.toMillis(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder repeat(int time, TimeUnit unit) {
|
||||
this.repeat = unit.toMillis(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder clearDelay() {
|
||||
this.delay = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder clearRepeat() {
|
||||
this.repeat = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledTask schedule() {
|
||||
VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat);
|
||||
taskService.execute(task);
|
||||
tasksByPlugin.put(plugin, task);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private class VelocityTask implements Runnable, ScheduledTask {
|
||||
private final Object plugin;
|
||||
private final Runnable runnable;
|
||||
private final long delay;
|
||||
private final long repeat;
|
||||
private volatile TaskStatus status;
|
||||
private Thread taskThread;
|
||||
|
||||
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
this.delay = delay;
|
||||
this.repeat = repeat;
|
||||
this.status = TaskStatus.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskStatus status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (status == TaskStatus.SCHEDULED) {
|
||||
status = TaskStatus.CANCELLED;
|
||||
if (taskThread != null) {
|
||||
taskThread.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
taskThread = Thread.currentThread();
|
||||
if (delay > 0) {
|
||||
try {
|
||||
sleeper.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
if (status == TaskStatus.CANCELLED) {
|
||||
onFinish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (status != TaskStatus.CANCELLED) {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
Log.logger.error("Exception in task {} by plugin {}", runnable, plugin);
|
||||
}
|
||||
|
||||
if (repeat > 0) {
|
||||
try {
|
||||
sleeper.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
if (status == TaskStatus.CANCELLED) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status = TaskStatus.FINISHED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onFinish();
|
||||
}
|
||||
|
||||
private void onFinish() {
|
||||
tasksByPlugin.remove(plugin, this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log {
|
||||
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public enum UuidUtils {
|
||||
;
|
||||
|
||||
public static UUID fromUndashed(final String string) {
|
||||
Objects.requireNonNull(string, "string");
|
||||
Preconditions.checkArgument(string.length() == 32, "Length is incorrect");
|
||||
return new UUID(
|
||||
Long.parseUnsignedLong(string.substring(0, 16), 16),
|
||||
Long.parseUnsignedLong(string.substring(16), 16)
|
||||
);
|
||||
}
|
||||
|
||||
public static String toUndashed(final UUID uuid) {
|
||||
Preconditions.checkNotNull(uuid, "uuid");
|
||||
return Long.toUnsignedString(uuid.getMostSignificantBits(), 16) + Long.toUnsignedString(uuid.getLeastSignificantBits(), 16);
|
||||
}
|
||||
|
||||
public static UUID generateOfflinePlayerUuid(String username) {
|
||||
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package com.velocitypowered.proxy.util.concurrency;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
/**
|
||||
* Represents a {@link ThreadFactory} that records the threads it has spawned.
|
||||
*/
|
||||
public class ThreadRecorderThreadFactory implements ThreadFactory {
|
||||
private final ThreadFactory backing;
|
||||
private final Set<Thread> threads = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public ThreadRecorderThreadFactory(ThreadFactory backing) {
|
||||
this.backing = Preconditions.checkNotNull(backing, "backing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Preconditions.checkNotNull(runnable, "runnable");
|
||||
return backing.newThread(() -> {
|
||||
threads.add(Thread.currentThread());
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
threads.remove(Thread.currentThread());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean currentlyInFactory() {
|
||||
return threads.contains(Thread.currentThread());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int size() {
|
||||
return threads.size();
|
||||
}
|
||||
}
|
@@ -2,7 +2,13 @@
|
||||
<Configuration status="warn">
|
||||
<Appenders>
|
||||
<TerminalConsole name="TerminalConsole">
|
||||
<PatternLayout pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
|
||||
<PatternLayout>
|
||||
<LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss} %level] [%logger]: %minecraftFormatting{%msg}%n%xEx}">
|
||||
<!-- Velocity doesn't need a prefix -->
|
||||
<PatternMatch key="com.velocitypowered."
|
||||
pattern="%highlightError{[%d{HH:mm:ss} %level]: %minecraftFormatting{%msg}%n%xEx}"/>
|
||||
</LoggerNamePatternSelector>
|
||||
</PatternLayout>
|
||||
</TerminalConsole>
|
||||
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"
|
||||
immediateFlush="false">
|
||||
|
@@ -0,0 +1,51 @@
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.TaskStatus;
|
||||
import com.velocitypowered.proxy.testutil.FakePluginManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class VelocitySchedulerTest {
|
||||
// TODO: The timings here will be inaccurate on slow systems. Need to find a testing-friendly replacement for Thread.sleep()
|
||||
|
||||
@Test
|
||||
void buildTask() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown).schedule();
|
||||
latch.await();
|
||||
assertEquals(TaskStatus.FINISHED, task.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelWorks() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
|
||||
AtomicInteger i = new AtomicInteger(3);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, i::decrementAndGet)
|
||||
.delay(100, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
task.cancel();
|
||||
Thread.sleep(200);
|
||||
assertEquals(3, i.get());
|
||||
assertEquals(TaskStatus.CANCELLED, task.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatTaskWorks() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager(), Sleeper.SYSTEM);
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown)
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.repeat(100, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
latch.await();
|
||||
task.cancel();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
package com.velocitypowered.proxy.testutil;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FakePluginManager implements PluginManager {
|
||||
public static final Object PLUGIN_A = new Object();
|
||||
public static final Object PLUGIN_B = new Object();
|
||||
|
||||
public static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A);
|
||||
public static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B);
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance) {
|
||||
if (instance == PLUGIN_A) {
|
||||
return Optional.of(PC_A);
|
||||
} else if (instance == PLUGIN_B) {
|
||||
return Optional.of(PC_B);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<PluginContainer> getPlugin(@NonNull String id) {
|
||||
switch (id) {
|
||||
case "a":
|
||||
return Optional.of(PC_A);
|
||||
case "b":
|
||||
return Optional.of(PC_B);
|
||||
default:
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<PluginContainer> getPlugins() {
|
||||
return ImmutableList.of(PC_A, PC_B);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded(@NonNull String id) {
|
||||
return id.equals("a") || id.equals("b");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private static class FakePluginContainer implements PluginContainer {
|
||||
private final String id;
|
||||
private final Object instance;
|
||||
|
||||
private FakePluginContainer(String id, Object instance) {
|
||||
this.id = id;
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull PluginDescription getDescription() {
|
||||
return new PluginDescription() {
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<?> getInstance() {
|
||||
return Optional.of(instance);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
import com.velocitypowered.api.util.UuidUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
@@ -0,0 +1,36 @@
|
||||
package com.velocitypowered.proxy.util.concurrency;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ThreadRecorderThreadFactoryTest {
|
||||
|
||||
@Test
|
||||
void newThread() throws Exception {
|
||||
ThreadRecorderThreadFactory factory = new ThreadRecorderThreadFactory(Executors.defaultThreadFactory());
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch endThread = new CountDownLatch(1);
|
||||
factory.newThread(() -> {
|
||||
started.countDown();
|
||||
assertTrue(factory.currentlyInFactory());
|
||||
assertEquals(1, factory.size());
|
||||
try {
|
||||
endThread.await();
|
||||
} catch (InterruptedException e) {
|
||||
fail(e);
|
||||
}
|
||||
}).start();
|
||||
started.await();
|
||||
assertFalse(factory.currentlyInFactory());
|
||||
assertEquals(1, factory.size());
|
||||
endThread.countDown();
|
||||
|
||||
// Wait a little bit to ensure the thread got shut down
|
||||
Thread.sleep(10);
|
||||
assertEquals(0, factory.size());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user