13
0
geforkt von Mirrors/Velocity

Add a basic command handling framework inspired by Bukkit/BungeeCord.

This doesn't yet support tab complete, that will come later. Additionally,
a /server command (using your configuration) and /velocity (shows basic
copyright information about the proxy) have been added.
Dieser Commit ist enthalten in:
Andrew Steinborn 2018-08-07 09:34:31 -04:00
Ursprung 09eff5a2fb
Commit 05693425bf
13 geänderte Dateien mit 309 neuen und 22 gelöschten Zeilen

Datei anzeigen

@ -23,6 +23,15 @@ wrapper script (`./gradlew`) as our CI builds using it.
It is sufficient to run `./gradlew build` to run the full build cycle.
## Running
Once you've built Velocity, you can copy and run the `-all` JAR from
`proxy/build/libs`. Velocity will generate a default configuration file
and you can configure it from there.
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
page.
## Status
Velocity is far from finished, but most of the essential pieces are in place:

Datei anzeigen

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

Datei anzeigen

@ -0,0 +1,23 @@
package com.velocitypowered.api.command;
import net.kyori.text.Component;
import javax.annotation.Nonnull;
/**
* Represents something that can be used to run a {@link CommandExecutor}.
*/
public interface CommandInvoker {
/**
* Sends the specified {@code component} to the invoker.
* @param component the text component to send
*/
void sendMessage(@Nonnull Component component);
/**
* Determines whether or not the invoker has a particular permission.
* @param permission the permission to check for
* @return whether or not the invoker has permission to run this command
*/
boolean hasPermission(@Nonnull String permission);
}

Datei anzeigen

@ -0,0 +1,26 @@
package com.velocitypowered.api.proxy;
import java.net.InetSocketAddress;
/**
* Represents a connection to the proxy. There is no guarantee that the connection has been fully initialized.
*/
public interface InboundConnection {
/**
* Returns the player's IP address.
* @return the player's IP
*/
InetSocketAddress getRemoteAddress();
/**
* Determine whether or not the player remains online.
* @return whether or not the player active
*/
boolean isActive();
/**
* Returns the current protocol version this connection uses.
* @return the protocol version the connection uses
*/
int getProtocolVersion();
}

Datei anzeigen

@ -1,18 +1,18 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.command.CommandInvoker;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.util.MessagePosition;
import net.kyori.text.Component;
import javax.annotation.Nonnull;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.UUID;
/**
* Represents a player who is connected to the proxy.
*/
public interface Player {
public interface Player extends CommandInvoker, InboundConnection {
/**
* Returns the player's current username.
* @return the username
@ -31,18 +31,6 @@ public interface Player {
*/
Optional<ServerInfo> getCurrentServer();
/**
* Returns the player's IP address.
* @return the player's IP
*/
InetSocketAddress getRemoteAddress();
/**
* Determine whether or not the player remains online.
* @return whether or not the player active
*/
boolean isActive();
/**
* Sends a chat message to the player's client.
* @param component the chat message to send

Datei anzeigen

@ -1,5 +1,6 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.command.CommandInvoker;
import com.velocitypowered.api.server.ServerInfo;
import javax.annotation.Nonnull;
@ -62,4 +63,12 @@ public interface ProxyServer {
* @param server the server to unregister
*/
void unregisterServer(@Nonnull ServerInfo server);
/**
* Returns an instance of {@link CommandInvoker} that can be used to determine if the command is being invoked by
* the console or a console-like executor. Plugins that execute commands are strongly urged to implement their own
* {@link CommandInvoker} instead of using the console invoker.
* @return the console command invoker
*/
CommandInvoker getConsoleCommandInvoker();
}

Datei anzeigen

@ -4,19 +4,24 @@ 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.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.network.ConnectionManager;
import com.velocitypowered.proxy.command.ServerCommand;
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.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.ServerMap;
import io.netty.bootstrap.Bootstrap;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -43,11 +48,26 @@ 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 Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final CommandInvoker consoleCommandInvoker = new CommandInvoker() {
@Override
public void sendMessage(@Nonnull Component component) {
// TODO: TerminalConsoleAppender
logger.info(ComponentSerializers.PLAIN.serialize(component));
}
@Override
public boolean hasPermission(@Nonnull String permission) {
return true;
}
};
private VelocityServer() {
commandManager.registerCommand("velocity", new VelocityCommand());
commandManager.registerCommand("server", new ServerCommand());
}
public static VelocityServer getServer() {
@ -62,6 +82,10 @@ public class VelocityServer implements ProxyServer {
return configuration;
}
public CommandManager getCommandManager() {
return commandManager;
}
public void start() {
logger.info("Using {}", Natives.compressor.getLoadedVariant());
logger.info("Using {}", Natives.cipher.getLoadedVariant());
@ -177,4 +201,9 @@ public class VelocityServer implements ProxyServer {
public void unregisterServer(@Nonnull ServerInfo server) {
servers.unregister(server);
}
@Override
public CommandInvoker getConsoleCommandInvoker() {
return consoleCommandInvoker;
}
}

Datei anzeigen

@ -0,0 +1,48 @@
package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.command.CommandExecutor;
import com.velocitypowered.api.command.CommandInvoker;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
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);
}
}
}

Datei anzeigen

@ -0,0 +1,41 @@
package com.velocitypowered.proxy.command;
import com.velocitypowered.api.command.CommandExecutor;
import com.velocitypowered.api.command.CommandInvoker;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import javax.annotation.Nonnull;
import java.util.Optional;
import java.util.stream.Collectors;
public class ServerCommand implements CommandExecutor {
@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));
return;
}
Player player = (Player) invoker;
if (args.length == 1) {
// Trying to connect to a server.
String serverName = args[0];
Optional<ServerInfo> server = VelocityServer.getServer().getServerInfo(serverName);
if (!server.isPresent()) {
player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED));
return;
}
player.createConnectionRequest(server.get()).fireAndForget();
} else {
String serverList = VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.collect(Collectors.joining(", "));
player.sendMessage(TextComponent.of("Available servers: " + serverList, TextColor.YELLOW));
}
}
}

Datei anzeigen

@ -0,0 +1,42 @@
package com.velocitypowered.proxy.command;
import com.velocitypowered.api.command.CommandExecutor;
import com.velocitypowered.api.command.CommandInvoker;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.format.TextColor;
import javax.annotation.Nonnull;
public class VelocityCommand implements CommandExecutor {
@Override
public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) {
String implVersion = VelocityServer.class.getPackage().getImplementationVersion();
TextComponent thisIsVelocity = TextComponent.builder()
.content("This is ")
.append(TextComponent.of("Velocity " + implVersion, TextColor.DARK_AQUA))
.append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.", TextColor.WHITE))
.build();
TextComponent velocityInfo = TextComponent.builder()
.content("Copyright 2018 Velocity Contributors. Velocity is freely licensed under the terms of the " +
"MIT License.")
.build();
TextComponent velocityWebsite = TextComponent.builder()
.content("Visit the ")
.append(TextComponent.builder("Velocity website")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.velocitypowered.com"))
.build())
.append(TextComponent.of(" or the ", TextColor.WHITE))
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity"))
.build())
.build();
invoker.sendMessage(thisIsVelocity);
invoker.sendMessage(velocityInfo);
invoker.sendMessage(velocityWebsite);
}
}

Datei anzeigen

@ -1,7 +1,7 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.proxy.command.VelocityCommand;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.data.scoreboard.Objective;
import com.velocitypowered.proxy.data.scoreboard.Score;
@ -9,16 +9,13 @@ import com.velocitypowered.proxy.data.scoreboard.Scoreboard;
import com.velocitypowered.proxy.data.scoreboard.Team;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.remap.EntityIdRemapper;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.ThrowableUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.EventLoop;
import io.netty.util.ReferenceCountUtil;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
@ -81,11 +78,21 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (packet instanceof Chat) {
Chat chat = (Chat) packet;
if (chat.getMessage().equals("/connect")) {
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566));
player.createConnectionRequest(info).fireAndForget();
return;
String msg = ((Chat) packet).getMessage();
if (msg.startsWith("/")) {
try {
if (!VelocityServer.getServer().getCommandManager().execute(player, msg.substring(1))) {
player.getConnectedServer().getMinecraftConnection().write(msg);
}
} catch (Exception e) {
logger.info("Exception occurred while running command for {}", player.getProfile().getName(), e);
player.sendMessage(TextComponent.of("An error occurred while running this command.", TextColor.RED));
return;
}
} else {
player.getConnectedServer().getMinecraftConnection().write(chat);
}
return;
}
if (packet instanceof PluginMessage) {

Datei anzeigen

@ -83,6 +83,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return connection.getChannel().isActive();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
@Override
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
Preconditions.checkNotNull(component, "component");
@ -217,6 +222,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")";
}
@Override
public boolean hasPermission(@Nonnull String permission) {
return false; // TODO: Implement permissions.
}
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final ServerInfo info;

Datei anzeigen

@ -1,6 +1,7 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
@ -14,6 +15,8 @@ import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.format.TextColor;
import java.net.InetSocketAddress;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
@ -70,4 +73,27 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
}
}
private static class InitialInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private InitialInboundConnection(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
}
}