Initial commit

master
Crypnotic 2019-01-20 20:39:48 -06:00
commit 76040ff950
30 changed files with 1471 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Eclipse #
.classpath
.project
.settings
# IntelliJ #
*.iml
*.ipr
*.iws
# NetBeans #
nbproject
# Maven #
build.xml
target
# Mac #
.DS_Store
# Vim #
.*.sw[a-p]
# Misc #
build
bin
dist
manifest.mf
# Deploy #
pom.xml.releaseBackup
release.properties
/.gradle/

26
build.gradle Normal file
View File

@ -0,0 +1,26 @@
plugins {
id 'net.kyori.blossom' version '1.1.0'
id 'java'
}
group 'me.crypnotic'
version '1.0.0-SNAPSHOT'
repositories {
mavenCentral()
maven {
url 'https://repo.velocitypowered.com/snapshots/'
}
}
dependencies {
compile 'com.velocitypowered:velocity-api:1.0-SNAPSHOT'
compile 'org.projectlombok:lombok:1.18.4'
}
blossom {
replaceToken '@ID@', name.toLowerCase()
replaceToken '@NAME@', name
replaceToken '@VERSION@', version
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip

172
gradlew vendored Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = 'Neutron'

View File

@ -0,0 +1,68 @@
package me.crypnotic.neutron;
import java.nio.file.Path;
import org.slf4j.Logger;
import com.google.inject.Inject;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.Getter;
import me.crypnotic.neutron.api.Neutron;
import me.crypnotic.neutron.command.AlertCommand;
import me.crypnotic.neutron.command.FindCommand;
import me.crypnotic.neutron.command.InfoCommand;
import me.crypnotic.neutron.command.ListCommand;
import me.crypnotic.neutron.command.MessageCommand;
import me.crypnotic.neutron.command.SendCommand;
import me.crypnotic.neutron.command.ServerCommand;
import me.crypnotic.neutron.manager.ModuleManager;
@Plugin(id = "@ID@", name = "@NAME@", version = "@VERSION@")
public class NeutronPlugin {
@Inject
@Getter
private ProxyServer proxy;
@Inject
@Getter
private Logger logger;
@Inject
@DataDirectory
@Getter
private Path dataFolderPath;
@Getter
private ModuleManager moduleManager;
@Subscribe
public void onProxyInitialize(ProxyInitializeEvent event) {
Neutron.setNeutron(this);
this.moduleManager = new ModuleManager();
if (!moduleManager.init()) {
logger.warn("Failed to initialize ModuleManager");
return;
}
registerCommands();
}
private void registerCommands() {
CommandManager commandManager = proxy.getCommandManager();
commandManager.register(new AlertCommand(), "alert");
commandManager.register(new FindCommand(), "find");
commandManager.register(new InfoCommand(), "info");
commandManager.register(new ListCommand(), "glist");
commandManager.register(new MessageCommand(), "message", "msg", "tell", "whisper");
commandManager.register(new SendCommand(), "send");
commandManager.register(new ServerCommand(), "server");
}
}

View File

@ -0,0 +1,33 @@
package me.crypnotic.neutron.api;
import java.nio.file.Path;
import org.slf4j.Logger;
import com.velocitypowered.api.proxy.ProxyServer;
import me.crypnotic.neutron.NeutronPlugin;
import me.crypnotic.neutron.manager.ModuleManager;
public interface INeutronAccessor {
default NeutronPlugin getPlugin() {
return Neutron.getNeutron();
}
default ProxyServer getProxy() {
return getPlugin().getProxy();
}
default Logger getLogger() {
return getPlugin().getLogger();
}
default Path getDataFolderPath() {
return getPlugin().getDataFolderPath();
}
default ModuleManager getModuleManager() {
return getPlugin().getModuleManager();
}
}

View File

@ -0,0 +1,31 @@
package me.crypnotic.neutron.api;
import me.crypnotic.neutron.NeutronPlugin;
public class Neutron {
private static Object LOCK = new Object();
private static NeutronPlugin instance;
public static final NeutronPlugin getNeutron() {
return instance;
}
public static final void setNeutron(NeutronPlugin plugin) {
if (plugin == null) {
throw new IllegalStateException("NeutronPlugin instance cannot be null");
}
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = plugin;
return;
}
}
}
throw new IllegalStateException("NeutronPlugin instance cannot be redefined");
}
}

View File

@ -0,0 +1,47 @@
package me.crypnotic.neutron.api.command;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class CommandContext {
private final String[] arguments;
public String get(int index) {
if (index >= size()) {
throw new IllegalArgumentException("Index: " + index + " > Length: " + size());
}
return arguments[index];
}
public Integer getInteger(int index) {
try {
return Integer.valueOf(get(index));
} catch (NumberFormatException exception) {
return -1;
}
}
public String join(String delimeter) {
return String.join(" ", arguments);
}
public String join(String delimeter, int start) {
return join(delimeter, start, size());
}
public String join(String delimeter, int start, int end) {
StringBuilder builder = new StringBuilder();
for (int i = start; i < end; i++) {
builder.append(get(i));
if (i < end) {
builder.append(delimeter);
}
}
return builder.toString();
}
public int size() {
return arguments.length;
}
}

View File

@ -0,0 +1,67 @@
package me.crypnotic.neutron.api.command;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import lombok.SneakyThrows;
import me.crypnotic.neutron.api.INeutronAccessor;
import me.crypnotic.neutron.util.Strings;
public interface CommandWrapper extends Command, INeutronAccessor {
@Override
default void execute(CommandSource source, String[] args) {
try {
handle(source, new CommandContext(args));
} catch (CommandExitException exception) {
/* Catch silently */
}
}
@SneakyThrows
default void assertUsage(CommandSource source, boolean assertion) {
assertCustom(source, assertion, "&cUsage: {0}", getUsage());
}
@SneakyThrows
default void assertPlayer(CommandSource source, String message, Object... values) {
assertCustom(source, source instanceof Player, message, values);
}
@SneakyThrows
default void assertNull(CommandSource source, Object value, String message, Object... values) {
assertCustom(source, value == null, message, values);
}
@SneakyThrows
default void assertNotNull(CommandSource source, Object value, String message, Object... values) {
assertCustom(source, value != null, message, values);
}
@SneakyThrows
default void assertPermission(CommandSource source, String permission) {
assertCustom(source, source.hasPermission(permission), "&cYou don't have permission to execute this command.");
}
@SneakyThrows
default void assertCustom(CommandSource source, boolean assertion, String message, Object... values) {
if (!assertion) {
message(source, message, values);
throw new CommandExitException();
}
}
default void message(CommandSource source, String message, Object... values) {
source.sendMessage(Strings.formatAndColor(message, values));
}
void handle(CommandSource source, CommandContext context) throws CommandExitException;
String getUsage();
public class CommandExitException extends Exception {
private static final long serialVersionUID = -1299193476106186693L;
}
}

View File

@ -0,0 +1,31 @@
package me.crypnotic.neutron.command;
import com.velocitypowered.api.command.CommandSource;
import lombok.RequiredArgsConstructor;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
import net.kyori.text.TextComponent;
@RequiredArgsConstructor
public class AlertCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPermission(source, "neutron.command.alert");
assertUsage(source, context.size() > 0);
TextComponent message = Strings.formatAndColor("&7&l[&c&lALERT&7&l] &e" + context.join(" "));
getProxy().broadcast(message);
/* Log to console since ProxyServer#broadcast doesn't do so */
getProxy().getConsoleCommandSource().sendMessage(message);
}
@Override
public String getUsage() {
return "/alert (message)";
}
}

View File

@ -0,0 +1,44 @@
package me.crypnotic.neutron.command;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
public class FindCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPermission(source, "neutron.command.find");
assertUsage(source, context.size() > 0);
Player target = getProxy().getPlayer(context.get(0)).orElse(null);
assertNotNull(source, target, "&cUnknown player: {0}", context.get(0));
ServerConnection server = target.getCurrentServer().get();
/* We'll consider this offline as the Player is in a limbo state */
assertNotNull(source, server, "&c{0} is currently offline.", context.get(0));
source.sendMessage(Strings.formatAndColor("&b{0} &7is connected to &b{1}", target.getUsername(), server.getServerInfo().getName()));
}
@Override
public List<String> suggest(CommandSource source, String[] args) {
if (args.length == 0) {
return Arrays.asList();
}
return Strings.matchPlayer(getProxy(), args[0]).stream().map(Player::getUsername).collect(Collectors.toList());
}
@Override
public String getUsage() {
return "/find (player)";
}
}

View File

@ -0,0 +1,47 @@
package me.crypnotic.neutron.command;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
public class InfoCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPermission(source, "neutron.command.info");
assertUsage(source, context.size() > 0);
Player target = getProxy().getPlayer(context.get(0)).orElse(null);
assertNotNull(source, target, "&cUnknown player: {0}", context.get(0));
ServerConnection server = target.getCurrentServer().orElse(null);
message(source, "&l&7==> Information for player: &b{0}", target.getUsername());
message(source, "&7Unique Id: &b{0}", target.getUniqueId().toString());
message(source, "&7Minecraft Version: &b{0}", target.getProtocolVersion());
message(source, "&7Locale: &b{0}", target.getPlayerSettings().getLocale());
message(source, "&7Current Server: &b{0}", server != null ? server.getServerInfo().getName() : "N/A");
message(source, "&7Ping: &b{0}", target.getPing());
}
@Override
public String getUsage() {
return "/info (player)";
}
@Override
public List<String> suggest(CommandSource source, String[] args) {
if (args.length == 0) {
return Arrays.asList();
}
return Strings.matchPlayer(getProxy(), args[0]).stream().map(Player::getUsername).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,44 @@
package me.crypnotic.neutron.command;
import java.util.Collection;
import java.util.stream.Collectors;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.event.HoverEvent;
public class ListCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPermission(source, "neutron.command.list");
for (RegisteredServer server : getProxy().getAllServers()) {
ServerInfo info = server.getServerInfo();
Collection<Player> players = server.getPlayersConnected();
String playerString = players.stream().map(Player::getUsername).sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.joining(", "));
TextComponent message = Strings.formatAndColor("&a[{0}] &e{1} player{2} online", info.getName(), players.size(),
players.size() == 1 ? "" : "s");
message = message.hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.of(playerString)));
message = message.clickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/server " + info.getName()));
source.sendMessage(message);
}
}
@Override
public String getUsage() {
return "/glist";
}
}

View File

@ -0,0 +1,47 @@
package me.crypnotic.neutron.command;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
import net.kyori.text.TextComponent;
public class MessageCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPermission(source, "neutron.command.message");
assertUsage(source, context.size() > 1);
Player target = getProxy().getPlayer(context.get(0)).orElse(null);
assertNotNull(source, target, "&cUnknown player: {0}", context.get(0));
String sourceName = source instanceof Player ? ((Player) source).getUsername() : "Console";
TextComponent content = TextComponent.of(context.join(" ", 1));
TextComponent sourceMessage = Strings.formatAndColor("&b&lme » {0} &7> &o", target.getUsername()).append(content);
TextComponent targetMessage = Strings.formatAndColor("&b&l{0} » me &7> &o", sourceName).append(content);
source.sendMessage(sourceMessage);
target.sendMessage(targetMessage);
}
@Override
public String getUsage() {
return "/message (player) (message)";
}
@Override
public List<String> suggest(CommandSource source, String[] args) {
if (args.length == 0) {
return Arrays.asList();
}
return Strings.matchPlayer(getProxy(), args[0]).stream().map(Player::getUsername).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,80 @@
package me.crypnotic.neutron.command;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
public class SendCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPermission(source, "neutron.command.send");
assertUsage(source, context.size() > 1);
RegisteredServer targetServer = getProxy().getServer(context.get(1).toLowerCase()).orElse(null);
assertNotNull(source, targetServer, "&cUnknown server: {0}", context.get(1));
switch (context.get(0).toLowerCase()) {
case "current":
assertPlayer(source, "&cOnly players can use this subcommand.");
Player player = (Player) source;
ServerConnection currentServer = player.getCurrentServer().orElse(null);
assertNotNull(player, currentServer, "&cYou must be connected to a server to use this subcommand.");
currentServer.getServer().getPlayersConnected().forEach(targetPlayer -> {
targetPlayer.createConnectionRequest(targetServer).fireAndForget();
message(targetPlayer, "&aYou have been sent to &b{0}", targetServer.getServerInfo().getName());
});
message(player, "&aAll players from your current server have been sent to &b{0}", targetServer.getServerInfo().getName());
break;
case "all":
getProxy().getAllPlayers().forEach(targetPlayer -> {
targetPlayer.createConnectionRequest(targetServer).fireAndForget();
message(targetPlayer, "&aYou have been sent to &b{0}", targetServer.getServerInfo().getName());
});
message(source, "&aAll players have been sent to &b{0}", targetServer.getServerInfo().getName());
break;
default:
Player targetPlayer = getProxy().getPlayer(context.get(0)).orElse(null);
assertNotNull(source, targetPlayer, "&cUnknown player: {0}", context.get(0));
targetPlayer.createConnectionRequest(targetServer).fireAndForget();
message(targetPlayer, "&aYou have been sent to &b{0}", targetServer.getServerInfo().getName());
message(source, "&b{0} &ahas been sent to &b{1}", targetPlayer.getUsername(), targetServer.getServerInfo().getName());
break;
}
}
@Override
public List<String> suggest(CommandSource source, String[] args) {
if (args.length == 1) {
List<String> result = Strings.matchPlayer(getProxy(), args[0]).stream().map(Player::getUsername).collect(Collectors.toList());
/* Inject `current`/`all` subcommands */
result.add("current");
result.add("all");
return result;
} else if (args.length == 2) {
return Strings.matchServer(getProxy(), args[1]).stream().map(server -> server.getServerInfo().getName()).collect(Collectors.toList());
}
return Arrays.asList();
}
@Override
public String getUsage() {
return "/send (player / current / all) (server)";
}
}

View File

@ -0,0 +1,43 @@
package me.crypnotic.neutron.command;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import me.crypnotic.neutron.api.command.CommandContext;
import me.crypnotic.neutron.api.command.CommandWrapper;
import me.crypnotic.neutron.util.Strings;
public class ServerCommand implements CommandWrapper {
@Override
public void handle(CommandSource source, CommandContext context) throws CommandExitException {
assertPlayer(source, "&cOnly players can use this command.");
assertPermission(source, "neutron.command.server");
assertUsage(source, context.size() > 0);
Player player = (Player) source;
RegisteredServer server = getProxy().getServer(context.get(0)).orElse(null);
assertNotNull(source, server, "&cUnknown server: {0}", context.get(0));
player.createConnectionRequest(server).fireAndForget();
}
@Override
public String getUsage() {
return "/server (name)";
}
@Override
public List<String> suggest(CommandSource source, String[] args) {
if (args.length == 1) {
return Strings.matchServer(getProxy(), args[0]).stream().map(server -> server.getServerInfo().getName()).collect(Collectors.toList());
}
return Arrays.asList();
}
}

View File

@ -0,0 +1,109 @@
package me.crypnotic.neutron.manager;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import lombok.Getter;
import me.crypnotic.neutron.api.INeutronAccessor;
import me.crypnotic.neutron.module.AbstractModule;
import me.crypnotic.neutron.module.announcement.AnnouncementsModule;
import me.crypnotic.neutron.module.serverlist.ServerListModule;
import me.crypnotic.neutron.util.FileIO;
public class ModuleManager implements INeutronAccessor {
@Getter
private File mainConfigFile;
@Getter
private Toml mainConfig;
private Map<Class<? extends AbstractModule>, AbstractModule> modules;
public boolean init() {
this.modules = new HashMap<Class<? extends AbstractModule>, AbstractModule>();
try {
this.mainConfigFile = FileIO.getOrCreate(getDataFolderPath(), "config.toml");
this.mainConfig = new Toml().read(mainConfigFile);
} catch (Exception exception) {
exception.printStackTrace();
return false;
}
modules.put(AnnouncementsModule.class, new AnnouncementsModule());
modules.put(ServerListModule.class, new ServerListModule());
for (AbstractModule module : modules.values()) {
Toml table = mainConfig.getTable(module.getName());
if (table == null) {
getLogger().warn("Unknown module attempted to load: " + module.getName());
continue;
}
module.setEnabled(table.getBoolean("enabled"));
if (module.isEnabled()) {
if (module.init()) {
continue;
} else {
getLogger().warn("Module failed to initialize: " + module.getName());
module.setEnabled(false);
continue;
}
}
}
getProxy().getEventManager().register(getPlugin(), this);
getLogger().info("Modules loaded: " + modules.size());
return true;
}
@Subscribe
public void onProxyReload(ProxyReloadEvent event) {
try {
this.mainConfigFile = FileIO.getOrCreate(getDataFolderPath(), "config.toml");
this.mainConfig = new Toml().read(mainConfigFile);
} catch (Exception exception) {
exception.printStackTrace();
}
for (AbstractModule module : modules.values()) {
Toml table = mainConfig.getTable(module.getName());
if (table == null) {
getLogger().warn("Unknown module attempted to reload: " + module.getName());
continue;
}
module.setEnabled(table.getBoolean("enabled"));
if (module.isEnabled()) {
if (module.reload()) {
continue;
} else {
getLogger().warn("Module failed to reload: " + module.getName());
module.setEnabled(false);
continue;
}
} else {
}
}
getLogger().info(String.format("Modules reloaded: %d", modules.size()));
}
@SuppressWarnings("unchecked")
public <T extends AbstractModule> Optional<T> get(Class<T> clazz) {
return (Optional<T>) Optional.ofNullable(modules.get(clazz));
}
}

View File

@ -0,0 +1,20 @@
package me.crypnotic.neutron.module;
import lombok.Getter;
import lombok.Setter;
import me.crypnotic.neutron.api.INeutronAccessor;
public abstract class AbstractModule implements INeutronAccessor {
@Getter
@Setter
private boolean enabled;
public abstract boolean init();
public abstract boolean reload();
public abstract boolean shutdown();
public abstract String getName();
}

View File

@ -0,0 +1,38 @@
package me.crypnotic.neutron.module.announcement;
import java.util.List;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.scheduler.ScheduledTask;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@RequiredArgsConstructor
public class Announcements {
@Getter
private final String id;
@Getter
private final long interval;
@Getter
private final boolean maintainOrder;
@Getter
private final List<String> messages;
@Getter
private final String prefix;
@Getter
@Setter
private ScheduledTask task;
public static Announcements load(String id, Toml toml) {
long interval = toml.getLong("interval");
boolean maintainOrder = toml.getBoolean("maintain-order");
String prefix = toml.contains("prefix") ? toml.getString("prefix") : "";
List<String> messages = toml.getList("messages");
return new Announcements(id, interval, maintainOrder, messages, prefix);
}
}

View File

@ -0,0 +1,69 @@
package me.crypnotic.neutron.module.announcement;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.scheduler.ScheduledTask;
import me.crypnotic.neutron.module.AbstractModule;
import me.crypnotic.neutron.util.FileIO;
public class AnnouncementsModule extends AbstractModule {
private File file;
private Toml toml;
private Map<String, Announcements> announcements;
public boolean init() {
this.file = FileIO.getOrCreate(getDataFolderPath(), "announcements.toml");
this.toml = new Toml().read(file);
this.announcements = new HashMap<String, Announcements>();
for (Entry<String, Object> entry : toml.entrySet()) {
String id = entry.getKey();
if (announcements.containsKey(id)) {
getLogger().warn("An announcement list has already been defined with the id: " + id);
continue;
}
Announcements announcement = Announcements.load(id, toml.getTable(id));
if (announcement != null) {
announcements.put(id, announcement);
} else {
getLogger().warn("Failed to load announcement list: " + id);
}
ScheduledTask task = getProxy().getScheduler().buildTask(getPlugin(), new AnnouncementsTask(getPlugin(), announcement))
.repeat(announcement.getInterval(), TimeUnit.SECONDS).schedule();
announcement.setTask(task);
}
getLogger().info("Announcements loaded: " + announcements.size());
return true;
}
@Override
public boolean reload() {
return shutdown() && init();
}
@Override
public boolean shutdown() {
announcements.values().stream().map(Announcements::getTask).forEach(ScheduledTask::cancel);
announcements.clear();
return true;
}
@Override
public String getName() {
return "announcements";
}
}

View File

@ -0,0 +1,44 @@
package me.crypnotic.neutron.module.announcement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import me.crypnotic.neutron.NeutronPlugin;
import me.crypnotic.neutron.util.Strings;
import net.kyori.text.TextComponent;
@RequiredArgsConstructor
public class AnnouncementsTask implements Runnable {
private final NeutronPlugin plugin;
private final Announcements announcements;
private List<String> localMessages;
private volatile int index = 0;
@Override
public void run() {
if (index == 0) {
if (localMessages == null) {
/* Create a local copy to avoid reading or shuffling the master copy */
this.localMessages = new ArrayList<String>(announcements.getMessages());
}
if (!announcements.isMaintainOrder()) {
Collections.shuffle(localMessages);
}
}
TextComponent message = Strings.formatAndColor("{0}{1}", announcements.getPrefix(), localMessages.get(index));
plugin.getProxy().broadcast(message);
index += 1;
if (index == localMessages.size()) {
index = 0;
}
}
}

View File

@ -0,0 +1,60 @@
package me.crypnotic.neutron.module.serverlist;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.proxy.server.ServerPing.Builder;
import com.velocitypowered.api.proxy.server.ServerPing.SamplePlayer;
import lombok.RequiredArgsConstructor;
import me.crypnotic.neutron.api.INeutronAccessor;
@RequiredArgsConstructor
public class ServerListHandler implements INeutronAccessor {
private final ServerListModule module;
@Subscribe
public void onServerListPing(ProxyPingEvent event) {
if (!module.isEnabled()) {
return;
}
ServerPing original = event.getPing();
int playerCount = getProxy().getPlayerCount();
Builder builder = ServerPing.builder();
builder.version(original.getVersion());
builder.onlinePlayers(playerCount);
builder.description(module.getMotd());
switch (module.getPlayerCountType()) {
case CURRENT:
builder.maximumPlayers(playerCount);
break;
case ONEMORE:
builder.maximumPlayers(playerCount + 1);
break;
case STATIC:
builder.maximumPlayers(module.getMaxPlayerCount());
break;
}
switch (module.getServerPreviewType()) {
case MESSAGE:
builder.samplePlayers(module.getPreviewMessages());
break;
case PLAYERS:
builder.samplePlayers(getProxy().getAllPlayers().stream().map(player -> new SamplePlayer(player.getUsername(), player.getUniqueId()))
.toArray(SamplePlayer[]::new));
break;
case EMPTY:
break;
}
event.setPing(builder.build());
}
}

View File

@ -0,0 +1,68 @@
package me.crypnotic.neutron.module.serverlist;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.proxy.server.ServerPing.SamplePlayer;
import lombok.Getter;
import me.crypnotic.neutron.module.AbstractModule;
import me.crypnotic.neutron.util.Strings;
import net.kyori.text.TextComponent;
public class ServerListModule extends AbstractModule {
private Toml toml;
@Getter
private TextComponent motd;
@Getter
private PlayerCountType playerCountType;
@Getter
private int maxPlayerCount;
@Getter
private ServerPreviewType serverPreviewType;
@Getter
private SamplePlayer[] previewMessages;
@Override
public boolean init() {
this.toml = getModuleManager().getMainConfig().getTable(getName());
this.motd = Strings.color(toml.getString("motd"));
this.playerCountType = PlayerCountType.valueOf(toml.getString("player-count-type"));
this.maxPlayerCount = toml.getLong("max-player-count").intValue();
this.serverPreviewType = ServerPreviewType.valueOf(toml.getString("server-preview-type"));
this.previewMessages = toml.getList("preview-messages").stream().map(Object::toString).map(Strings::toSamplePlayer)
.toArray(SamplePlayer[]::new);
getProxy().getEventManager().register(getPlugin(), new ServerListHandler(this));
return true;
}
@Override
public boolean reload() {
return init();
}
@Override
public boolean shutdown() {
return true;
}
@Override
public String getName() {
return "serverlist";
}
public enum PlayerCountType {
CURRENT,
ONEMORE,
STATIC;
}
public enum ServerPreviewType {
EMPTY,
MESSAGE,
PLAYERS;
}
}

View File

@ -0,0 +1,30 @@
package me.crypnotic.neutron.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import me.crypnotic.neutron.NeutronPlugin;
public class FileIO {
public static File getOrCreate(Path path, String name) {
File file = new File(path.toFile(), name);
if (!file.exists()) {
try {
try (InputStream input = NeutronPlugin.class.getResourceAsStream("/" + name)) {
if (input != null) {
Files.copy(input, file.toPath());
} else {
file.createNewFile();
}
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
return file;
}
}

View File

@ -0,0 +1,71 @@
package me.crypnotic.neutron.util;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing.SamplePlayer;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
public class Strings {
@SuppressWarnings("deprecation")
public static TextComponent color(String text) {
return ComponentSerializers.LEGACY.deserialize(text, '&');
}
public static String format(String text, Object... params) {
for (int i = 0; i < params.length; i++) {
Object param = params[i];
text = text.replace("{" + i + "}", param == null ? "null" : param.toString());
}
return text;
}
public static TextComponent formatAndColor(String text, Object... params) {
return color(format(text, params));
}
public static Collection<Player> matchPlayer(ProxyServer proxy, String partialName) {
// A better error message might be nice. This just mimics the previous output
if (partialName == null) {
throw new NullPointerException("partialName");
}
Optional<Player> exactMatch = proxy.getPlayer(partialName);
if (exactMatch.isPresent()) {
return Collections.singleton(exactMatch.get());
}
return proxy.getAllPlayers().stream().filter(player -> player.getUsername().regionMatches(true, 0, partialName, 0, partialName.length()))
.collect(Collectors.toList());
}
public static Collection<RegisteredServer> matchServer(ProxyServer proxy, String partialName) {
// A better error message might be nice. This just mimics the previous output
if (partialName == null) {
throw new NullPointerException("partialName");
}
Optional<RegisteredServer> exactMatch = proxy.getServer(partialName);
if (exactMatch.isPresent()) {
return Collections.singleton(exactMatch.get());
}
return proxy.getAllServers().stream()
.filter(server -> server.getServerInfo().getName().regionMatches(true, 0, partialName, 0, partialName.length()))
.collect(Collectors.toList());
}
@SuppressWarnings("deprecation")
public static SamplePlayer toSamplePlayer(String text) {
return new SamplePlayer(ComponentSerializers.LEGACY.serialize(color(text)), UUID.randomUUID());
}
}

View File

@ -0,0 +1,28 @@
# Add [name] to create a new annoucement list
# Every list runs independently of one another
#
# A list must always contain three variables:
#
# - interval (how long to wait between messages - in seconds)
# - maintain-order (whether the list should be annouced in order or randomly
# - messages (the messages to be announced)
#
# The order in which these appear doesn't matter, but an announcement list will not load without them
#
# A list can also contain optional values:
#
# - prefix (the prefix to add before all messages in the list
#
[example]
# 5 minutes in seconds
interval = 300
maintain-order = true
# Optional
prefix = "&7[&bNeutron&7] &7"
messages = [
"Welcome to &bNeutron!",
"Neutron is an open-source project willing to welcome contributors with open arms"
]

View File

@ -0,0 +1,31 @@
[announcements]
enabled = true
[serverlist]
enabled = true
motd = "&7This velocity proxy is proudly powered by &bNeutron"
# The server list player count has three different types:
#
# STATIC - player count will always be the number defined under `max-player-count`
# CURRENT - player count matches the number of players online
# ONEMORE - player count shows the number of players online plus 1
#
# `max-player-count` is only used with the STATIC player count type
player-count-type = "STATIC"
max-player-count = 500
# The server list preview has three different types:
#
# MESSAGE - preview will show the messages defined under `preview-messages`
# PLAYERS - preview matches the vanilla server preview of showing online players
# EMPTY - preview is empty
#
# `preview-messages` is only used with the MESSAGE preview type
server-preview-type = "MESSAGE"
preview-messages = [
"&7Powered by a &bNeutron"
]