diff --git a/build.gradle b/sponge/build.gradle
similarity index 100%
rename from build.gradle
rename to sponge/build.gradle
diff --git a/gradle.properties b/sponge/gradle.properties
similarity index 100%
rename from gradle.properties
rename to sponge/gradle.properties
diff --git a/gradle/wrapper/gradle-wrapper.jar b/sponge/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from gradle/wrapper/gradle-wrapper.jar
rename to sponge/gradle/wrapper/gradle-wrapper.jar
diff --git a/gradle/wrapper/gradle-wrapper.properties b/sponge/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from gradle/wrapper/gradle-wrapper.properties
rename to sponge/gradle/wrapper/gradle-wrapper.properties
diff --git a/gradlew b/sponge/gradlew
old mode 100755
new mode 100644
similarity index 100%
rename from gradlew
rename to sponge/gradlew
diff --git a/gradlew.bat b/sponge/gradlew.bat
similarity index 96%
rename from gradlew.bat
rename to sponge/gradlew.bat
index 24467a1..9618d8d 100644
--- a/gradlew.bat
+++ b/sponge/gradlew.bat
@@ -1,100 +1,100 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@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="-Xmx64m" "-Xms64m"
-
-@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
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@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="-Xmx64m" "-Xms64m"
+
+@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
diff --git a/settings.gradle b/sponge/settings.gradle
similarity index 100%
rename from settings.gradle
rename to sponge/settings.gradle
diff --git a/src/main/java/tv/voidstar/altimeter/Altimeter.java b/sponge/src/main/java/tv/voidstar/altimeter/Altimeter.java
similarity index 100%
rename from src/main/java/tv/voidstar/altimeter/Altimeter.java
rename to sponge/src/main/java/tv/voidstar/altimeter/Altimeter.java
diff --git a/src/main/java/tv/voidstar/altimeter/AltimeterConfig.java b/sponge/src/main/java/tv/voidstar/altimeter/AltimeterConfig.java
similarity index 100%
rename from src/main/java/tv/voidstar/altimeter/AltimeterConfig.java
rename to sponge/src/main/java/tv/voidstar/altimeter/AltimeterConfig.java
diff --git a/src/main/java/tv/voidstar/altimeter/AltimeterData.java b/sponge/src/main/java/tv/voidstar/altimeter/AltimeterData.java
similarity index 100%
rename from src/main/java/tv/voidstar/altimeter/AltimeterData.java
rename to sponge/src/main/java/tv/voidstar/altimeter/AltimeterData.java
diff --git a/src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java b/sponge/src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java
similarity index 100%
rename from src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java
rename to sponge/src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java
diff --git a/src/main/java/tv/voidstar/altimeter/command/MainExecutor.java b/sponge/src/main/java/tv/voidstar/altimeter/command/MainExecutor.java
similarity index 100%
rename from src/main/java/tv/voidstar/altimeter/command/MainExecutor.java
rename to sponge/src/main/java/tv/voidstar/altimeter/command/MainExecutor.java
diff --git a/src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java b/sponge/src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java
similarity index 100%
rename from src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java
rename to sponge/src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java
diff --git a/velocity/.run/Debug Velocity.run.xml b/velocity/.run/Debug Velocity.run.xml
new file mode 100644
index 0000000..5a15d87
--- /dev/null
+++ b/velocity/.run/Debug Velocity.run.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/velocity/.run/Velocity Server.run.xml b/velocity/.run/Velocity Server.run.xml
new file mode 100644
index 0000000..96badc9
--- /dev/null
+++ b/velocity/.run/Velocity Server.run.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/velocity/build.gradle b/velocity/build.gradle
new file mode 100644
index 0000000..0e134c6
--- /dev/null
+++ b/velocity/build.gradle
@@ -0,0 +1,45 @@
+plugins {
+ id 'com.github.johnrengelman.shadow' version '4.0.4'
+}
+
+apply plugin: 'java'
+
+group = 'tv.voidstar'
+version = '1.0-SNAPSHOT'
+
+sourceCompatibility = '1.8'
+targetCompatibility = '1.8'
+
+dependencies {
+ compileOnly 'com.velocitypowered:velocity-api:3.0.0'
+ annotationProcessor 'com.velocitypowered:velocity-api:3.0.0'
+ shadow 'com.github.Eufranio:StorageUtils:2.4'
+ //shadow 'org.xerial:sqlite-jdbc:3.36.0.2'
+ //shadow 'mysql:mysql-connector-java:8.0.26'
+}
+
+configurations {
+ compile.extendsFrom shadow
+}
+
+shadowJar {
+ configurations = [project.configurations.shadow]
+
+ archiveClassifier = null
+ from sourceSets.main.output
+
+ dependencies {
+ exclude(dependency('org.spongepowered:configurate-core'))
+ exclude(dependency('org.spongepowered:configurate-hocon'))
+ }
+}
+
+repositories {
+ maven {
+ name 'velocity'
+ url 'https://nexus.velocitypowered.com/repository/maven-public/'
+ }
+ maven { url = 'https://jitpack.io' }
+}
+
+build.dependsOn shadowJar
diff --git a/velocity/gradle.properties b/velocity/gradle.properties
new file mode 100644
index 0000000..e69de29
diff --git a/velocity/gradle/wrapper/gradle-wrapper.jar b/velocity/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..5c2d1cf
Binary files /dev/null and b/velocity/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/velocity/gradle/wrapper/gradle-wrapper.properties b/velocity/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..59b5f89
--- /dev/null
+++ b/velocity/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/velocity/gradlew b/velocity/gradlew
new file mode 100644
index 0000000..83f2acf
--- /dev/null
+++ b/velocity/gradlew
@@ -0,0 +1,188 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## 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='"-Xmx64m" "-Xms64m"'
+
+# 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 or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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" "$@"
diff --git a/velocity/gradlew.bat b/velocity/gradlew.bat
new file mode 100644
index 0000000..9618d8d
--- /dev/null
+++ b/velocity/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@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="-Xmx64m" "-Xms64m"
+
+@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
diff --git a/velocity/settings.gradle b/velocity/settings.gradle
new file mode 100644
index 0000000..3338661
--- /dev/null
+++ b/velocity/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'altimeter'
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/Altimeter.java b/velocity/src/main/java/tv/voidstar/altimeter/Altimeter.java
new file mode 100644
index 0000000..1f1189e
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/Altimeter.java
@@ -0,0 +1,157 @@
+package tv.voidstar.altimeter;
+
+import com.google.inject.Inject;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.velocitypowered.api.command.BrigadierCommand;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.event.PostOrder;
+import com.velocitypowered.api.event.ResultedEvent;
+import com.velocitypowered.api.event.Subscribe;
+import com.velocitypowered.api.event.connection.LoginEvent;
+import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
+import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
+import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
+import com.velocitypowered.api.plugin.Plugin;
+import com.velocitypowered.api.plugin.annotation.DataDirectory;
+import com.velocitypowered.api.proxy.ProxyServer;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.slf4j.Logger;
+import tv.voidstar.altimeter.command.ClearExecutor;
+import tv.voidstar.altimeter.command.MainExecutor;
+import tv.voidstar.altimeter.command.OverrideExecutor;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+@Plugin(
+ id = "altimeter",
+ name = "Altimeter",
+ version = "1.0",
+ description = "Limit number of accounts joining per IP address",
+ authors = {"ZeHeisenberg"}
+)
+public class Altimeter {
+
+ private static Altimeter plugin;
+
+ @Inject
+ @DataDirectory
+ private Path defaultConfigDir;
+
+ @Inject
+ private Logger logger;
+
+ @Inject
+ ProxyServer server;
+
+ private static Altimeter getInstance() {
+ return plugin;
+ }
+
+ public static Logger getLogger() {
+ return plugin.logger;
+ }
+
+ @Subscribe
+ public void onInit(ProxyInitializeEvent event) throws IOException {
+ plugin = this;
+
+ File rootDir = defaultConfigDir.toFile(); //new File(defaultConfigDir.toFile(), "altimeter");
+ if (!rootDir.exists()) {
+ if (!rootDir.mkdirs()) {
+ Altimeter.getLogger().error("Unable to create root config dir");
+ }
+ }
+
+ logger.info("Altimeter is starting");
+
+ this.loadLibraries();
+
+ AltimeterConfig.init(rootDir);
+ AltimeterData.init(rootDir);
+
+ //AltimeterConfig.load();
+ AltimeterData.load();
+
+ registerCommands();
+
+ this.server.getScheduler()
+ .buildTask(this, () -> AltimeterData.checkAndClearAccounts(Instant.now()))
+ .repeat(AltimeterConfig.getCheckIntervalValue(), AltimeterConfig.getCheckIntervalUnit())
+ .schedule();
+ }
+
+ @Subscribe
+ public void onReload(ProxyReloadEvent event) {
+ AltimeterConfig.load();
+ AltimeterData.reload();
+ getLogger().info("Altimeter reloaded");
+ }
+
+ @Subscribe(order = PostOrder.FIRST)
+ public void onLogin(LoginEvent event) {
+ UUID player = event.getPlayer().getUniqueId();
+ String ip = event.getPlayer().getRemoteAddress().getAddress().getHostAddress();
+ Altimeter.getLogger().info("{} logging in from {}", player, ip);
+ if (!AltimeterData.canLogIn(player, ip)) {
+ event.setResult(ResultedEvent.ComponentResult.denied(Component.text(
+ AltimeterConfig.getDisconnectMessage(),
+ NamedTextColor.RED
+ )));
+ }
+ }
+
+ @Subscribe
+ public void onStop(ProxyShutdownEvent event) {
+ AltimeterData.save();
+ AltimeterConfig.save();
+ }
+
+ private void loadLibraries() {
+ try {
+ logger.info("Loading libraries");
+ Path libraries = this.defaultConfigDir.resolve("libraries");
+ Files.createDirectories(libraries);
+
+ try (Stream stream = Files.walk(libraries)) {
+ stream.forEach(filePath -> {
+ logger.info("Adding library file " + filePath.getFileName() + " to the classpath");
+ server.getPluginManager().addToClasspath(this, filePath);
+ });
+ }
+ } catch (Exception e) {
+ logger.error("Could not load libraries.");
+ }
+ }
+
+ private void registerCommands() {
+ LiteralCommandNode altimeterCommand = LiteralArgumentBuilder
+ .literal("altimeter")
+ .executes(new MainExecutor())
+ .then(LiteralArgumentBuilder.literal("clear")
+ .then(RequiredArgumentBuilder.argument("target", StringArgumentType.word())
+ .executes(new ClearExecutor())
+ )
+ ).then(LiteralArgumentBuilder.literal("override")
+ .then(RequiredArgumentBuilder.argument("ip", StringArgumentType.word())
+ .then(RequiredArgumentBuilder.argument("limit", IntegerArgumentType.integer())
+ .executes(new OverrideExecutor())
+ )
+ )
+ )
+ .build();
+
+ BrigadierCommand altimeter = new BrigadierCommand(altimeterCommand);
+ server.getCommandManager().register(altimeter);
+ }
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/AltimeterConfig.java b/velocity/src/main/java/tv/voidstar/altimeter/AltimeterConfig.java
new file mode 100644
index 0000000..f0a83fe
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/AltimeterConfig.java
@@ -0,0 +1,215 @@
+package tv.voidstar.altimeter;
+
+import com.google.common.net.InetAddresses;
+import ninja.leaping.configurate.ConfigurationNode;
+import ninja.leaping.configurate.ConfigurationOptions;
+import ninja.leaping.configurate.commented.CommentedConfigurationNode;
+import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class AltimeterConfig {
+
+ private static final HashMap accountLimitOverrides = new HashMap<>();
+ private static File configFile;
+ private static @NonNull HoconConfigurationLoader loader;
+ private static CommentedConfigurationNode configs;
+ private static Duration accountTTL;
+ private static int accountLimit;
+ private static long checkIntervalValue;
+ private static TimeUnit checkIntervalUnit;
+ private static String disconnectMessage;
+ private static String databaseUrl;
+ private static boolean enableDatabase;
+
+ public static void init(File rootDir) {
+ configFile = new File(rootDir, "altimeter.conf");
+ loader = HoconConfigurationLoader.builder()
+ .setFile(configFile)
+ .setDefaultOptions(ConfigurationOptions.defaults().withShouldCopyDefaults(true))
+ .build();
+ load();
+ }
+
+ public static void load() {
+ try {
+ if (!configFile.exists()) {
+ configFile.createNewFile();
+ }
+ configs = loader.load();
+ } catch (IOException e) {
+ Altimeter.getLogger().error("Unable to load config file.", e);
+ }
+
+ // defaults
+ configs.getNode("altimeter").setComment("General Altimeter configurations.");
+
+ disconnectMessage = configs.getNode("altimeter", "disconnectMessage")
+ .getString("Too many accounts have logged in from this address. Contact a server admin.");
+ enableDatabase = configs.getNode("altimeter", "enableDatabase").getBoolean(true);
+ databaseUrl = configs.getNode("altimeter", "databaseUrl")
+ .getString("jdbc:sqlite:Altimeter.db");
+
+ CommentedConfigurationNode checkInterval = configs.getNode("altimeter", "checkInterval")
+ .setComment("How often accounts should be checked and cleared from IP lists.");
+ getValOrSetDefault(checkInterval.getNode("value"), 5);
+ getValOrSetDefault(checkInterval.getNode("unit").setComment("DAYS, HOURS, MINUTES, or SECONDS"), "MINUTES");
+
+ checkIntervalValue = checkInterval.getNode("value").getLong();
+
+ String checkIU = checkInterval.getNode("unit").getString();
+ try {
+ checkIntervalUnit = TimeUnit.valueOf(checkIU.toUpperCase());
+ } catch (IllegalArgumentException | NullPointerException e) {
+ checkIntervalValue = 5;
+ checkIntervalUnit = TimeUnit.MINUTES;
+ Altimeter.getLogger().error("Invalid checkInterval unit in config: {}. Setting checkInterval to default 5 MINUTES", checkIU, e);
+ Altimeter.getLogger().error("Read https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/TimeUnit.html#enum_constant_detail for allowed values.");
+ }
+
+ CommentedConfigurationNode accountTTLNode = configs.getNode("altimeter", "ttl")
+ .setComment("How long after an account is added to the queue is it removed.");
+ getValOrSetDefault(accountTTLNode.getNode("value"), 30);
+ getValOrSetDefault(accountTTLNode.getNode("unit").setComment("DAYS, HOURS, MINUTES, or SECONDS"), "DAYS");
+
+ long accountTTLValue = accountTTLNode.getNode("value").getLong();
+ ChronoUnit accountTTLUnit;
+
+ try {
+ accountTTLUnit = ChronoUnit.valueOf(accountTTLNode.getNode("unit").getString().toUpperCase());
+ } catch (IllegalArgumentException | NullPointerException e) {
+ accountTTLValue = 30;
+ accountTTLUnit = ChronoUnit.DAYS;
+ Altimeter.getLogger().error("Invalid TTL unit in config: {}. Setting ttl to default 30 DAYS", checkIU, e);
+ Altimeter.getLogger().error("Read https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#enum.constant.summary for allowed values.");
+ }
+
+ accountTTL = Duration.of(accountTTLValue, accountTTLUnit);
+
+ ConfigurationNode accountLimitNode = configs.getNode("altimeter", "accountLimit")
+ .setComment("How many accounts from one IP can log in.");
+ getValOrSetDefault(accountLimitNode, 5);
+ accountLimit = accountLimitNode.getInt();
+
+ Altimeter.getLogger().info("load limit overrides data");
+ ConfigurationNode limitOverridesNode = configs.getNode("altimeter", "limitOverrides")
+ .setComment("Override account limit for specific IPs");
+ if (!limitOverridesNode.isList()) {
+ ConfigurationNode override = limitOverridesNode.appendListNode();
+ override.getNode("ip").setValue("127.0.0.1");
+ override.getNode("limit").setValue(50);
+ } else {
+ for (ConfigurationNode overrideNode : limitOverridesNode.getChildrenList()) {
+ String ip = overrideNode.getNode("ip").getString("in.va.li.d");
+ int limit = overrideNode.getNode("limit").getInt(accountLimit);
+ if (InetAddresses.isInetAddress(ip)) {
+ accountLimitOverrides.put(ip, limit);
+ Altimeter.getLogger().info("{} has {} account limit", ip, limit);
+ } else {
+ Altimeter.getLogger().error("Invalid IP in limitOverrides configuration {}", ip);
+ }
+ }
+ }
+
+ save();
+ }
+
+ public static void save() {
+ configs.getNode("altimeter").removeChild("limitOverrides");
+ ConfigurationNode limitOverridesNode = configs.getNode("altimeter", "limitOverrides");
+ for (Map.Entry entry : accountLimitOverrides.entrySet()) {
+ ConfigurationNode limitOverrideElement = limitOverridesNode.appendListNode();
+ limitOverrideElement.getNode("ip").setValue(entry.getKey());
+ limitOverrideElement.getNode("limit").setValue(entry.getValue());
+ }
+ try {
+ loader.save(configs);
+ } catch (IOException e) {
+ Altimeter.getLogger().error("Unable to save config file.", e);
+ }
+ }
+
+ public static int getAccountLimit(String ip) {
+ int limit = accountLimitOverrides.getOrDefault(ip, accountLimit);
+ Altimeter.getLogger().info("account limit for {} is {}", ip, limit);
+ return limit;
+ }
+
+ public static void setAccountLimit(int accountLimit) {
+ AltimeterConfig.accountLimit = accountLimit;
+ }
+
+ public static Duration getAccountTTL() {
+ return accountTTL;
+ }
+
+ public static void setAccountTTL(String accountTTL) {
+ try {
+ AltimeterConfig.accountTTL = Duration.parse(accountTTL);
+ } catch (DateTimeParseException e) {
+ Altimeter.getLogger().error("Invalid Duration.parse() string: {}", accountTTL, e);
+ Altimeter.getLogger().error("Consider reading https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-");
+ }
+ }
+
+ private static void getValOrSetDefault(ConfigurationNode node, boolean def) {
+ if (!(node.getValue() instanceof Boolean))
+ node.setValue(def);
+ }
+
+ private static void getValOrSetDefault(ConfigurationNode node, String def) {
+ if (node.getString() == null)
+ node.setValue(def);
+ }
+
+ private static void getValOrSetDefault(ConfigurationNode node, Number def) {
+ if (!(node.getValue() instanceof Number))
+ node.setValue(def);
+ }
+
+ public static TimeUnit getCheckIntervalUnit() {
+ return checkIntervalUnit;
+ }
+
+ public static void setCheckIntervalUnit(TimeUnit checkIntervalUnit) {
+ AltimeterConfig.checkIntervalUnit = checkIntervalUnit;
+ }
+
+ public static long getCheckIntervalValue() {
+ return checkIntervalValue;
+ }
+
+ public static void setCheckIntervalValue(long checkIntervalValue) {
+ AltimeterConfig.checkIntervalValue = checkIntervalValue;
+ }
+
+ public static String getDisconnectMessage() {
+ return disconnectMessage;
+ }
+
+ public static boolean isDatabaseEnabled() {
+ return enableDatabase;
+ }
+
+ public static String getDatabaseUrl() {
+ return databaseUrl;
+ }
+
+ public static boolean setOverride(String ip, int limit) {
+ boolean replace = accountLimitOverrides.putIfAbsent(ip, limit) != null;
+ if (replace) {
+ accountLimitOverrides.replace(ip, limit);
+ }
+ AltimeterData.trimAccountList(ip);
+ save();
+ return replace;
+ }
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/AltimeterData.java b/velocity/src/main/java/tv/voidstar/altimeter/AltimeterData.java
new file mode 100644
index 0000000..0245277
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/AltimeterData.java
@@ -0,0 +1,243 @@
+package tv.voidstar.altimeter;
+
+import com.j256.ormlite.table.TableUtils;
+import io.github.eufranio.storage.Persistable;
+import ninja.leaping.configurate.ConfigurationNode;
+import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
+import tv.voidstar.altimeter.data.AccountData;
+import tv.voidstar.altimeter.data.IPData;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.*;
+
+public class AltimeterData {
+ private static final Map> ipAccountMap = Collections.synchronizedMap(new HashMap<>());
+
+ private static File ipAccountListsFile;
+ private static HoconConfigurationLoader loader;
+
+ private static ConfigurationNode ipAccountsMap;
+
+ static Persistable ips;
+ static Persistable accounts;
+
+ public static void init(File rootDir) throws IOException {
+ if (AltimeterConfig.isDatabaseEnabled()) {
+ Altimeter.getLogger().info("Initializing database storage");
+ ips = Persistable.create(IPData.class, AltimeterConfig.getDatabaseUrl());
+ ips.idFieldName = "ip";
+
+ accounts = Persistable.create(AccountData.class, AltimeterConfig.getDatabaseUrl());
+ } else {
+ Altimeter.getLogger().info("Initializing flat file storage");
+ ipAccountListsFile = new File(rootDir, "accounts.conf");
+ if (!ipAccountListsFile.exists())
+ ipAccountListsFile.createNewFile();
+
+ loader = HoconConfigurationLoader.builder().setFile(ipAccountListsFile).build();
+ ipAccountsMap = loader.load();
+ }
+ }
+
+ public static void load() {
+ if (AltimeterConfig.isDatabaseEnabled())
+ return;
+
+ ipAccountMap.clear();
+ for (ConfigurationNode ipNode : ipAccountsMap.getNode("ips").getChildrenList()) {
+ String ip = ipNode.getNode("ip").getString();
+ HashMap accountsForIP = new HashMap<>();
+ for (ConfigurationNode accountEntry : ipNode.getNode("accounts").getChildrenList()) {
+ UUID uuid = UUID.fromString(accountEntry.getNode("uuid").getString());
+ Instant ttl = Instant.ofEpochMilli(accountEntry.getNode("ttl").getLong());
+ accountsForIP.put(uuid, ttl);
+ }
+ ipAccountMap.put(ip, accountsForIP);
+ }
+ }
+
+ public static void save() {
+ if (AltimeterConfig.isDatabaseEnabled())
+ return;
+
+ ipAccountsMap.removeChild("ips");
+ ConfigurationNode list = ipAccountsMap.getNode("ips");
+ for (String ip : ipAccountMap.keySet()) {
+ ConfigurationNode ipNode = list.appendListNode();
+ ipNode.getNode("ip").setValue(ip);
+ ConfigurationNode accountList = ipNode.getNode("accounts");
+ HashMap accountsForIP = ipAccountMap.get(ip);
+ for (UUID uuid : accountsForIP.keySet()) {
+ ConfigurationNode accountForIP = accountList.appendListNode();
+ accountForIP.getNode("uuid").setValue(uuid.toString());
+ accountForIP.getNode("ttl").setValue(accountsForIP.get(uuid).toEpochMilli());
+ }
+ }
+ try {
+ loader.save(ipAccountsMap);
+ } catch (IOException e) {
+ Altimeter.getLogger().error("Could not save Account/IP lists to disk", e);
+ }
+ }
+
+ public static void reload() {
+ if (AltimeterConfig.isDatabaseEnabled())
+ return;
+
+ ipAccountMap.clear();
+ load();
+ }
+
+ public static boolean canLogIn(UUID player, String ip) {
+ if (AltimeterConfig.isDatabaseEnabled()) {
+ IPData ipData = ips.getOrCreate(ip);
+ try { ipData.refresh(); } catch (Exception e) { e.printStackTrace(); }
+
+ if (ipData.accounts.stream().anyMatch(acc -> acc.uuid.equals(player)))
+ return true;
+
+ if (ipData.accounts.size() < AltimeterConfig.getAccountLimit(ip)) {
+ Altimeter.getLogger().info("\tAllowed login and logged account for IP");
+ AccountData data = new AccountData() {{
+ ip = ipData;
+ uuid = player;
+ ttl = Instant.now().plus(AltimeterConfig.getAccountTTL());
+ }};
+ accounts.save(data);
+ return true;
+ }
+
+ Altimeter.getLogger().info("\tDenied login. too many accounts from this ip");
+ return false;
+ } else {
+ HashMap accounts = ipAccountMap.computeIfAbsent(ip, k -> new HashMap<>());
+ if (accounts.containsKey(player)) {
+ return true;
+ } else if (accounts.size() < AltimeterConfig.getAccountLimit(ip)) {
+ Altimeter.getLogger().info("\tAllowed login and logged account for IP");
+ accounts.put(player, Instant.now().plus(AltimeterConfig.getAccountTTL()));
+ save();
+ return true;
+ } else {
+ Altimeter.getLogger().info("\tDenied login. too many accounts from this ip");
+ return false;
+ }
+ }
+ }
+
+ public static void checkAndClearAccounts(Instant now) {
+ if (AltimeterConfig.isDatabaseEnabled()) {
+ try {
+ for (AccountData data : accounts.objDao) {
+ if (now.isAfter(data.ttl))
+ accounts.delete(data);
+ }
+ } catch (Exception e) {
+ Altimeter.getLogger().error("Error clearing IPs");
+ e.printStackTrace();
+ }
+ } else {
+ Iterator>> ipIterator = ipAccountMap.entrySet().iterator();
+ boolean dataChanged = false;
+ while (ipIterator.hasNext()) {
+ Map.Entry> ipEntry = ipIterator.next();
+ HashMap accounts = ipEntry.getValue();
+ accounts.forEach((key, value) -> {
+ if (now.isAfter(value)) {
+ accounts.remove(key);
+ }
+ });
+ dataChanged = true;
+ if (accounts.isEmpty())
+ ipIterator.remove();
+ }
+ boolean trimmed = trimAllAccountLists();
+ if (dataChanged || trimmed)
+ save();
+ }
+ }
+
+ static boolean trimAllAccountLists() {
+ boolean trimmed = false;
+ Iterator>> ipIterator = ipAccountMap.entrySet().iterator();
+ while (ipIterator.hasNext()) {
+ Map.Entry> ipEntry = ipIterator.next();
+ if (trimAccountList(ipEntry.getKey())) {
+ trimmed = true;
+ }
+ if (ipEntry.getValue().isEmpty())
+ ipIterator.remove();
+ }
+ return trimmed;
+ }
+
+ public static boolean trimAccountList(String ip) {
+ if (AltimeterConfig.isDatabaseEnabled()) {
+ IPData ipData = ips.get(ip);
+ if (ipData == null)
+ return false;
+
+ try { ipData.refresh(); } catch (Exception e) { e.printStackTrace(); }
+
+ if (ipData.accounts.isEmpty() || AltimeterConfig.getAccountLimit(ip) >= ipData.accounts.size())
+ return false;
+
+ int elementsToRemove = ipData.accounts.size() - AltimeterConfig.getAccountLimit(ip);
+ Comparator comparator = Comparator.comparingLong(acc -> acc.ttl.toEpochMilli());
+ ipData.accounts.stream()
+ .sorted(comparator.reversed())
+ .limit(elementsToRemove)
+ .forEach(acc -> accounts.delete(acc));
+ } else {
+ HashMap accounts = ipAccountMap.get(ip);
+ if (accounts == null || AltimeterConfig.getAccountLimit(ip) > accounts.size())
+ return false;
+
+ int elementsToRemove = accounts.size() - AltimeterConfig.getAccountLimit(ip);
+ accounts.entrySet().stream()
+ .sorted(Comparator.comparingLong(e -> ((Map.Entry) e).getValue().toEpochMilli()).reversed())
+ .limit(elementsToRemove)
+ .forEach(e -> accounts.remove(e.getKey()));
+ }
+ return true;
+ }
+
+ public static boolean clear(String target) {
+ if (AltimeterConfig.isDatabaseEnabled()) {
+ if (target.equals("all")) {
+ try {
+ TableUtils.clearTable(ips.objDao.getConnectionSource(), IPData.class);
+ return true;
+ } catch (Exception e) {
+ Altimeter.getLogger().error("Error clearing the database:");
+ e.printStackTrace();
+ return false;
+ }
+ } else {
+ IPData ipData = ips.get(target);
+ if (ipData == null)
+ return false;
+
+ try { ipData.refresh(); } catch (Exception e) { e.printStackTrace(); }
+ ipData.accounts.clear();
+ ips.delete(ipData);
+ return true;
+ }
+ } else {
+ boolean success;
+ if (target.equals("all")) {
+ ipAccountMap.clear();
+ success = true;
+ } else {
+ success = ipAccountMap.remove(target) != null;
+ if (success) {
+ trimAccountList(target);
+ }
+ }
+ save();
+ return success;
+ }
+ }
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java b/velocity/src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java
new file mode 100644
index 0000000..ce01c5e
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/command/ClearExecutor.java
@@ -0,0 +1,41 @@
+package tv.voidstar.altimeter.command;
+
+import com.google.common.net.InetAddresses;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.velocitypowered.api.command.CommandSource;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import tv.voidstar.altimeter.AltimeterData;
+
+public class ClearExecutor implements Command {
+
+ @Override
+ public int run(CommandContext context) throws CommandSyntaxException {
+ CommandSource src = context.getSource();
+ String target = StringArgumentType.getString(context, "target");
+ if (target.equals("all")) {
+ if (!src.hasPermission("altimeter.clear.all")) {
+ src.sendMessage(Component.text("[Altimeter] You don't have permission to do that.", NamedTextColor.RED));
+ return 1;
+ }
+ } else if (InetAddresses.isInetAddress(target)) {
+ if (!src.hasPermission("altimeter.clear.ip")) {
+ src.sendMessage(Component.text("[Altimeter] You don't have permission to do that.", NamedTextColor.RED));
+ return 1;
+ }
+ } else {
+ src.sendMessage(Component.text("[Altimeter] You must supply 'all' or an IP address.", NamedTextColor.RED));
+ return 1;
+ }
+
+ if (AltimeterData.clear(target)) {
+ src.sendMessage(Component.text("[Altimeter] Successfully cleared account entries for " + target, NamedTextColor.GREEN));
+ } else {
+ src.sendMessage(Component.text("[Altimeter] Unable to clear account entries for " + target, NamedTextColor.RED));
+ }
+ return 1;
+ }
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/command/MainExecutor.java b/velocity/src/main/java/tv/voidstar/altimeter/command/MainExecutor.java
new file mode 100644
index 0000000..72dbdda
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/command/MainExecutor.java
@@ -0,0 +1,22 @@
+package tv.voidstar.altimeter.command;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.velocitypowered.api.command.CommandSource;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+
+public class MainExecutor implements Command {
+
+ @Override
+ public int run(CommandContext context) throws CommandSyntaxException {
+ CommandSource src = context.getSource();
+ src.sendMessage(Component.text("------ Altimeter command list ------", NamedTextColor.AQUA));
+ src.sendMessage(Component.text("/altimeter clear [all|x.x.x.x] - clear accounts for all IPs, or for a specific IP.", NamedTextColor.GOLD));
+ src.sendMessage(Component.text("/altimeter override - sets a limit for a given IP. (ignores the global limit)", NamedTextColor.GOLD));
+ src.sendMessage(Component.text("-------------------------------------", NamedTextColor.AQUA));
+ return 1;
+ }
+
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java b/velocity/src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java
new file mode 100644
index 0000000..91d264c
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/command/OverrideExecutor.java
@@ -0,0 +1,45 @@
+package tv.voidstar.altimeter.command;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.velocitypowered.api.command.CommandSource;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import tv.voidstar.altimeter.Altimeter;
+import tv.voidstar.altimeter.AltimeterConfig;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+public class OverrideExecutor implements Command {
+
+ @Override
+ public int run(CommandContext context) throws CommandSyntaxException {
+ CommandSource src = context.getSource();
+ if (!src.hasPermission("altimeter.override")) {
+ src.sendMessage(Component.text("You don't have permission to do this.", NamedTextColor.RED));
+ return 1;
+ }
+
+ InetAddress address;
+ try {
+ address = InetAddress.getByName(StringArgumentType.getString(context, "ip"));
+ } catch (UnknownHostException var8) {
+ src.sendMessage(Component.text("Invalid IP address!", NamedTextColor.RED));
+ return 1;
+ }
+
+ String ip = address.getHostAddress();
+ int limit = IntegerArgumentType.getInteger(context, "limit");
+
+ Altimeter.getLogger().info("ip: " + ip + ", limit: " + limit);
+ String verb = AltimeterConfig.setOverride(ip, limit) ? "updated" : "set";
+
+ src.sendMessage(Component.text("[Altimeter] Successfully " + verb + " override limit to " + limit + " for " + ip, NamedTextColor.GREEN));
+ return 1;
+ }
+
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/data/AccountData.java b/velocity/src/main/java/tv/voidstar/altimeter/data/AccountData.java
new file mode 100644
index 0000000..a705155
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/data/AccountData.java
@@ -0,0 +1,26 @@
+package tv.voidstar.altimeter.data;
+
+import com.j256.ormlite.field.DataType;
+import com.j256.ormlite.field.DatabaseField;
+import com.j256.ormlite.misc.BaseDaoEnabled;
+import com.j256.ormlite.table.DatabaseTable;
+
+import java.time.Instant;
+import java.util.UUID;
+
+@DatabaseTable(tableName = "accounts")
+public class AccountData extends BaseDaoEnabled {
+
+ @DatabaseField(generatedId = true)
+ public int id;
+
+ @DatabaseField(foreign = true)
+ public IPData ip;
+
+ @DatabaseField
+ public UUID uuid;
+
+ @DatabaseField(dataType = DataType.SERIALIZABLE)
+ public Instant ttl;
+
+}
diff --git a/velocity/src/main/java/tv/voidstar/altimeter/data/IPData.java b/velocity/src/main/java/tv/voidstar/altimeter/data/IPData.java
new file mode 100644
index 0000000..1586f99
--- /dev/null
+++ b/velocity/src/main/java/tv/voidstar/altimeter/data/IPData.java
@@ -0,0 +1,18 @@
+package tv.voidstar.altimeter.data;
+
+import com.j256.ormlite.dao.ForeignCollection;
+import com.j256.ormlite.field.DatabaseField;
+import com.j256.ormlite.field.ForeignCollectionField;
+import com.j256.ormlite.misc.BaseDaoEnabled;
+import com.j256.ormlite.table.DatabaseTable;
+
+@DatabaseTable(tableName = "ips")
+public class IPData extends BaseDaoEnabled {
+
+ @DatabaseField(id = true)
+ public String ip;
+
+ @ForeignCollectionField(eager = true)
+ public ForeignCollection accounts;
+
+}