diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..af3101c --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'java' + id 'net.ltgt.apt' version '0.10' +} + +group 'sh.okx' +version '3.0-alpha' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + jcenter() + maven { + url 'http://nexus.hc.to/content/repositories/pub_releases' + } + maven { + url 'https://hub.spigotmc.org/nexus/content/groups/public/' + } + maven { + url 'http://repo.extendedclip.com/content/repositories/placeholderapi/' + } +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile 'org.mockito:mockito-core:2.+' + + compileOnly 'org.projectlombok:lombok:1.18.2' + apt "org.projectlombok:lombok:1.18.2" + + compile 'org.spigotmc:spigot-api:1.13-R0.1-SNAPSHOT' + compile('net.milkbowl.vault:VaultAPI:1.6') { + exclude group: 'org.bukkit' + } + compile 'me.clip:placeholderapi:2.9.+' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..01b8bf6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7eb5e88 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Aug 21 18:52:26 BST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..423c743 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'Rankup' + diff --git a/src/main/java/sh/okx/rankup/Metrics.java b/src/main/java/sh/okx/rankup/Metrics.java new file mode 100644 index 0000000..fba3376 --- /dev/null +++ b/src/main/java/sh/okx/rankup/Metrics.java @@ -0,0 +1,926 @@ +package sh.okx.rankup; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.ServicePriority; +import org.bukkit.plugin.java.JavaPlugin; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import javax.net.ssl.HttpsURLConnection; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +/** + * bStats collects some data for plugin authors. + *

+ * Check out https://bStats.org/ to learn more about bStats! + */ +public class Metrics { + + // The version of this bStats class + public static final int B_STATS_VERSION = 1; + + // The url to which the data is sent + private static final String URL = "https://bStats.org/submitData"; + + // Should failed requests be logged? + private static boolean logFailedRequests; + + // The uuid of the server + private static String serverUUID; + + // The plugin + private final JavaPlugin plugin; + + // A list with all custom charts + private final List charts = new ArrayList<>(); + + /** + * Class constructor. + * + * @param plugin The plugin which stats should be submitted. + */ + public Metrics(JavaPlugin plugin) { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null!"); + } + this.plugin = plugin; + + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Check if the config file exists + if (!config.isSet("serverUuid")) { + + // Add default values + config.addDefault("enabled", true); + // Every server gets it's unique random id. + config.addDefault("serverUuid", UUID.randomUUID().toString()); + // Should failed request be logged? + config.addDefault("logFailedRequests", false); + + // Inform the server owners about bStats + config.options().header( + "bStats collects some data for plugin authors like how many servers are using their plugins.\n" + + "To honor their work, you should not disable it.\n" + + "This has nearly no effect on the server performance!\n" + + "Check out https://bStats.org/ to learn more :)" + ).copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { + } + } + + // Load the data + serverUUID = config.getString("serverUuid"); + logFailedRequests = config.getBoolean("logFailedRequests", false); + if (config.getBoolean("enabled", true)) { + boolean found = false; + // Search for all other bStats Metrics classes to see if we are the first one + for (Class service : Bukkit.getServicesManager().getKnownServices()) { + try { + service.getField("B_STATS_VERSION"); // Our identifier :) + found = true; // We aren't the first + break; + } catch (NoSuchFieldException ignored) { + } + } + // Register our service + Bukkit.getServicesManager().register(Metrics.class, this, plugin, ServicePriority.Normal); + if (!found) { + // We are the first! + startSubmitting(); + } + } + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + if (chart == null) { + throw new IllegalArgumentException("Chart cannot be null!"); + } + charts.add(chart); + } + + /** + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { + final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!plugin.isEnabled()) { // Plugin was disabled + timer.cancel(); + return; + } + // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler + // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) + Bukkit.getScheduler().runTask(plugin, new Runnable() { + @Override + public void run() { + submitData(); + } + }); + } + }, 1000 * 60 * 5, 1000 * 60 * 30); + // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start + // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! + // WARNING: Just don't do it! + } + + /** + * Gets the plugin specific data. + * This method is called using Reflection. + * + * @return The plugin specific data. + */ + public JSONObject getPluginData() { + JSONObject data = new JSONObject(); + + String pluginName = plugin.getDescription().getName(); + String pluginVersion = plugin.getDescription().getVersion(); + + data.put("pluginName", pluginName); // Append the name of the plugin + data.put("pluginVersion", pluginVersion); // Append the version of the plugin + JSONArray customCharts = new JSONArray(); + for (CustomChart customChart : charts) { + // Add the data of the custom charts + JSONObject chart = customChart.getRequestJsonObject(); + if (chart == null) { // If the chart is null, we skip it + continue; + } + customCharts.add(chart); + } + data.put("customCharts", customCharts); + + return data; + } + + /** + * Gets the server specific data. + * + * @return The server specific data. + */ + private JSONObject getServerData() { + // Minecraft specific data + int playerAmount = Bukkit.getOnlinePlayers().size(); + int onlineMode = Bukkit.getOnlineMode() ? 1 : 0; + String bukkitVersion = org.bukkit.Bukkit.getVersion(); + bukkitVersion = bukkitVersion.substring(bukkitVersion.indexOf("MC: ") + 4, bukkitVersion.length() - 1); + + // OS/Java specific data + String javaVersion = System.getProperty("java.version"); + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + String osVersion = System.getProperty("os.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + JSONObject data = new JSONObject(); + + data.put("serverUUID", serverUUID); + + data.put("playerAmount", playerAmount); + data.put("onlineMode", onlineMode); + data.put("bukkitVersion", bukkitVersion); + + data.put("javaVersion", javaVersion); + data.put("osName", osName); + data.put("osArch", osArch); + data.put("osVersion", osVersion); + data.put("coreCount", coreCount); + + return data; + } + + /** + * Collects the data and sends it afterwards. + */ + private void submitData() { + final JSONObject data = getServerData(); + + JSONArray pluginData = new JSONArray(); + // Search for all other bStats Metrics classes to get their plugin data + for (Class service : Bukkit.getServicesManager().getKnownServices()) { + try { + service.getField("B_STATS_VERSION"); // Our identifier :) + } catch (NoSuchFieldException ignored) { + continue; // Continue "searching" + } + // Found one! + try { + pluginData.add(service.getMethod("getPluginData").invoke(Bukkit.getServicesManager().load(service))); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { + } + } + + data.put("plugins", pluginData); + + // Create a new thread for the connection to the bStats server + new Thread(new Runnable() { + @Override + public void run() { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (logFailedRequests) { + plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats of " + plugin.getName(), e); + } + } + } + }).start(); + + } + + /** + * Sends the data to the bStats server. + * + * @param data The data to send. + * @throws Exception If the request failed. + */ + private static void sendData(JSONObject data) throws Exception { + if (data == null) { + throw new IllegalArgumentException("Data cannot be null!"); + } + if (Bukkit.isPrimaryThread()) { + throw new IllegalAccessException("This method must not be called from the main thread!"); + } + HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); + + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + + // Add headers + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format + connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); + + // Send data + connection.setDoOutput(true); + DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); + outputStream.write(compressedData); + outputStream.flush(); + outputStream.close(); + + connection.getInputStream().close(); // We don't care about the response - Just send our data :) + } + + /** + * Gzips the given String. + * + * @param str The string to gzip. + * @return The gzipped String. + * @throws IOException If the compression failed. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(outputStream); + gzip.write(str.getBytes("UTF-8")); + gzip.close(); + return outputStream.toByteArray(); + } + + /** + * Represents a custom chart. + */ + public static abstract class CustomChart { + + // The id of the chart + protected final String chartId; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public CustomChart(String chartId) { + if (chartId == null || chartId.isEmpty()) { + throw new IllegalArgumentException("ChartId cannot be null or empty!"); + } + this.chartId = chartId; + } + + protected JSONObject getRequestJsonObject() { + JSONObject chart = new JSONObject(); + chart.put("chartId", chartId); + try { + JSONObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + chart.put("data", data); + } catch (Throwable t) { + if (logFailedRequests) { + Bukkit.getLogger().log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return chart; + } + + protected abstract JSONObject getChartData(); + + } + + /** + * Represents a custom simple pie. + */ + public static abstract class SimplePie extends CustomChart { + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public SimplePie(String chartId) { + super(chartId); + } + + /** + * Gets the value of the pie. + * + * @return The value of the pie. + */ + public abstract String getValue(); + + @Override + protected JSONObject getChartData() { + JSONObject data = new JSONObject(); + String value = getValue(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + data.put("value", value); + return data; + } + } + + /** + * Represents a custom advanced pie. + */ + public static abstract class AdvancedPie extends CustomChart { + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public AdvancedPie(String chartId) { + super(chartId); + } + + /** + * Gets the values of the pie. + * + * @param valueMap Just an empty map. The only reason it exists is to make your life easier. + * You don't have to create a map yourself! + * @return The values of the pie. + */ + public abstract HashMap getValues(HashMap valueMap); + + @Override + protected JSONObject getChartData() { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + HashMap map = getValues(new HashMap()); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.put(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + } + + /** + * Represents a custom single line chart. + */ + public static abstract class SingleLineChart extends CustomChart { + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public SingleLineChart(String chartId) { + super(chartId); + } + + /** + * Gets the value of the chart. + * + * @return The value of the chart. + */ + public abstract int getValue(); + + @Override + protected JSONObject getChartData() { + JSONObject data = new JSONObject(); + int value = getValue(); + if (value == 0) { + // Null = skip the chart + return null; + } + data.put("value", value); + return data; + } + + } + + /** + * Represents a custom multi line chart. + */ + public static abstract class MultiLineChart extends CustomChart { + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public MultiLineChart(String chartId) { + super(chartId); + } + + /** + * Gets the values of the chart. + * + * @param valueMap Just an empty map. The only reason it exists is to make your life easier. + * You don't have to create a map yourself! + * @return The values of the chart. + */ + public abstract HashMap getValues(HashMap valueMap); + + @Override + protected JSONObject getChartData() { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + HashMap map = getValues(new HashMap()); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.put(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + + } + + /** + * Represents a custom simple map chart. + */ + public static abstract class SimpleMapChart extends CustomChart { + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public SimpleMapChart(String chartId) { + super(chartId); + } + + /** + * Gets the value of the chart. + * + * @return The value of the chart. + */ + public abstract Country getValue(); + + @Override + protected JSONObject getChartData() { + JSONObject data = new JSONObject(); + Country value = getValue(); + + if (value == null) { + // Null = skip the chart + return null; + } + data.put("value", value.getCountryIsoTag()); + return data; + } + + } + + /** + * Represents a custom advanced map chart. + */ + public static abstract class AdvancedMapChart extends CustomChart { + + /** + * Class constructor. + * + * @param chartId The id of the chart. + */ + public AdvancedMapChart(String chartId) { + super(chartId); + } + + /** + * Gets the value of the chart. + * + * @param valueMap Just an empty map. The only reason it exists is to make your life easier. + * You don't have to create a map yourself! + * @return The value of the chart. + */ + public abstract HashMap getValues(HashMap valueMap); + + @Override + protected JSONObject getChartData() { + JSONObject data = new JSONObject(); + JSONObject values = new JSONObject(); + HashMap map = getValues(new HashMap()); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + continue; // Skip this invalid + } + allSkipped = false; + values.put(entry.getKey().getCountryIsoTag(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + data.put("values", values); + return data; + } + + } + + /** + * A enum which is used for custom maps. + */ + public enum Country { + + /** + * bStats will use the country of the server. + */ + AUTO_DETECT("AUTO", "Auto Detected"), + + ANDORRA("AD", "Andorra"), + UNITED_ARAB_EMIRATES("AE", "United Arab Emirates"), + AFGHANISTAN("AF", "Afghanistan"), + ANTIGUA_AND_BARBUDA("AG", "Antigua and Barbuda"), + ANGUILLA("AI", "Anguilla"), + ALBANIA("AL", "Albania"), + ARMENIA("AM", "Armenia"), + NETHERLANDS_ANTILLES("AN", "Netherlands Antilles"), + ANGOLA("AO", "Angola"), + ANTARCTICA("AQ", "Antarctica"), + ARGENTINA("AR", "Argentina"), + AMERICAN_SAMOA("AS", "American Samoa"), + AUSTRIA("AT", "Austria"), + AUSTRALIA("AU", "Australia"), + ARUBA("AW", "Aruba"), + ÅLAND_ISLANDS("AX", "Åland Islands"), + AZERBAIJAN("AZ", "Azerbaijan"), + BOSNIA_AND_HERZEGOVINA("BA", "Bosnia and Herzegovina"), + BARBADOS("BB", "Barbados"), + BANGLADESH("BD", "Bangladesh"), + BELGIUM("BE", "Belgium"), + BURKINA_FASO("BF", "Burkina Faso"), + BULGARIA("BG", "Bulgaria"), + BAHRAIN("BH", "Bahrain"), + BURUNDI("BI", "Burundi"), + BENIN("BJ", "Benin"), + SAINT_BARTHÉLEMY("BL", "Saint Barthélemy"), + BERMUDA("BM", "Bermuda"), + BRUNEI("BN", "Brunei"), + BOLIVIA("BO", "Bolivia"), + BONAIRE_SINT_EUSTATIUS_AND_SABA("BQ", "Bonaire, Sint Eustatius and Saba"), + BRAZIL("BR", "Brazil"), + BAHAMAS("BS", "Bahamas"), + BHUTAN("BT", "Bhutan"), + BOUVET_ISLAND("BV", "Bouvet Island"), + BOTSWANA("BW", "Botswana"), + BELARUS("BY", "Belarus"), + BELIZE("BZ", "Belize"), + CANADA("CA", "Canada"), + COCOS_ISLANDS("CC", "Cocos Islands"), + THE_DEMOCRATIC_REPUBLIC_OF_CONGO("CD", "The Democratic Republic Of Congo"), + CENTRAL_AFRICAN_REPUBLIC("CF", "Central African Republic"), + CONGO("CG", "Congo"), + SWITZERLAND("CH", "Switzerland"), + CÔTE_D_IVOIRE("CI", "Côte d'Ivoire"), + COOK_ISLANDS("CK", "Cook Islands"), + CHILE("CL", "Chile"), + CAMEROON("CM", "Cameroon"), + CHINA("CN", "China"), + COLOMBIA("CO", "Colombia"), + COSTA_RICA("CR", "Costa Rica"), + CUBA("CU", "Cuba"), + CAPE_VERDE("CV", "Cape Verde"), + CURAÇAO("CW", "Curaçao"), + CHRISTMAS_ISLAND("CX", "Christmas Island"), + CYPRUS("CY", "Cyprus"), + CZECH_REPUBLIC("CZ", "Czech Republic"), + GERMANY("DE", "Germany"), + DJIBOUTI("DJ", "Djibouti"), + DENMARK("DK", "Denmark"), + DOMINICA("DM", "Dominica"), + DOMINICAN_REPUBLIC("DO", "Dominican Republic"), + ALGERIA("DZ", "Algeria"), + ECUADOR("EC", "Ecuador"), + ESTONIA("EE", "Estonia"), + EGYPT("EG", "Egypt"), + WESTERN_SAHARA("EH", "Western Sahara"), + ERITREA("ER", "Eritrea"), + SPAIN("ES", "Spain"), + ETHIOPIA("ET", "Ethiopia"), + FINLAND("FI", "Finland"), + FIJI("FJ", "Fiji"), + FALKLAND_ISLANDS("FK", "Falkland Islands"), + MICRONESIA("FM", "Micronesia"), + FAROE_ISLANDS("FO", "Faroe Islands"), + FRANCE("FR", "France"), + GABON("GA", "Gabon"), + UNITED_KINGDOM("GB", "United Kingdom"), + GRENADA("GD", "Grenada"), + GEORGIA("GE", "Georgia"), + FRENCH_GUIANA("GF", "French Guiana"), + GUERNSEY("GG", "Guernsey"), + GHANA("GH", "Ghana"), + GIBRALTAR("GI", "Gibraltar"), + GREENLAND("GL", "Greenland"), + GAMBIA("GM", "Gambia"), + GUINEA("GN", "Guinea"), + GUADELOUPE("GP", "Guadeloupe"), + EQUATORIAL_GUINEA("GQ", "Equatorial Guinea"), + GREECE("GR", "Greece"), + SOUTH_GEORGIA_AND_THE_SOUTH_SANDWICH_ISLANDS("GS", "South Georgia And The South Sandwich Islands"), + GUATEMALA("GT", "Guatemala"), + GUAM("GU", "Guam"), + GUINEA_BISSAU("GW", "Guinea-Bissau"), + GUYANA("GY", "Guyana"), + HONG_KONG("HK", "Hong Kong"), + HEARD_ISLAND_AND_MCDONALD_ISLANDS("HM", "Heard Island And McDonald Islands"), + HONDURAS("HN", "Honduras"), + CROATIA("HR", "Croatia"), + HAITI("HT", "Haiti"), + HUNGARY("HU", "Hungary"), + INDONESIA("ID", "Indonesia"), + IRELAND("IE", "Ireland"), + ISRAEL("IL", "Israel"), + ISLE_OF_MAN("IM", "Isle Of Man"), + INDIA("IN", "India"), + BRITISH_INDIAN_OCEAN_TERRITORY("IO", "British Indian Ocean Territory"), + IRAQ("IQ", "Iraq"), + IRAN("IR", "Iran"), + ICELAND("IS", "Iceland"), + ITALY("IT", "Italy"), + JERSEY("JE", "Jersey"), + JAMAICA("JM", "Jamaica"), + JORDAN("JO", "Jordan"), + JAPAN("JP", "Japan"), + KENYA("KE", "Kenya"), + KYRGYZSTAN("KG", "Kyrgyzstan"), + CAMBODIA("KH", "Cambodia"), + KIRIBATI("KI", "Kiribati"), + COMOROS("KM", "Comoros"), + SAINT_KITTS_AND_NEVIS("KN", "Saint Kitts And Nevis"), + NORTH_KOREA("KP", "North Korea"), + SOUTH_KOREA("KR", "South Korea"), + KUWAIT("KW", "Kuwait"), + CAYMAN_ISLANDS("KY", "Cayman Islands"), + KAZAKHSTAN("KZ", "Kazakhstan"), + LAOS("LA", "Laos"), + LEBANON("LB", "Lebanon"), + SAINT_LUCIA("LC", "Saint Lucia"), + LIECHTENSTEIN("LI", "Liechtenstein"), + SRI_LANKA("LK", "Sri Lanka"), + LIBERIA("LR", "Liberia"), + LESOTHO("LS", "Lesotho"), + LITHUANIA("LT", "Lithuania"), + LUXEMBOURG("LU", "Luxembourg"), + LATVIA("LV", "Latvia"), + LIBYA("LY", "Libya"), + MOROCCO("MA", "Morocco"), + MONACO("MC", "Monaco"), + MOLDOVA("MD", "Moldova"), + MONTENEGRO("ME", "Montenegro"), + SAINT_MARTIN("MF", "Saint Martin"), + MADAGASCAR("MG", "Madagascar"), + MARSHALL_ISLANDS("MH", "Marshall Islands"), + MACEDONIA("MK", "Macedonia"), + MALI("ML", "Mali"), + MYANMAR("MM", "Myanmar"), + MONGOLIA("MN", "Mongolia"), + MACAO("MO", "Macao"), + NORTHERN_MARIANA_ISLANDS("MP", "Northern Mariana Islands"), + MARTINIQUE("MQ", "Martinique"), + MAURITANIA("MR", "Mauritania"), + MONTSERRAT("MS", "Montserrat"), + MALTA("MT", "Malta"), + MAURITIUS("MU", "Mauritius"), + MALDIVES("MV", "Maldives"), + MALAWI("MW", "Malawi"), + MEXICO("MX", "Mexico"), + MALAYSIA("MY", "Malaysia"), + MOZAMBIQUE("MZ", "Mozambique"), + NAMIBIA("NA", "Namibia"), + NEW_CALEDONIA("NC", "New Caledonia"), + NIGER("NE", "Niger"), + NORFOLK_ISLAND("NF", "Norfolk Island"), + NIGERIA("NG", "Nigeria"), + NICARAGUA("NI", "Nicaragua"), + NETHERLANDS("NL", "Netherlands"), + NORWAY("NO", "Norway"), + NEPAL("NP", "Nepal"), + NAURU("NR", "Nauru"), + NIUE("NU", "Niue"), + NEW_ZEALAND("NZ", "New Zealand"), + OMAN("OM", "Oman"), + PANAMA("PA", "Panama"), + PERU("PE", "Peru"), + FRENCH_POLYNESIA("PF", "French Polynesia"), + PAPUA_NEW_GUINEA("PG", "Papua New Guinea"), + PHILIPPINES("PH", "Philippines"), + PAKISTAN("PK", "Pakistan"), + POLAND("PL", "Poland"), + SAINT_PIERRE_AND_MIQUELON("PM", "Saint Pierre And Miquelon"), + PITCAIRN("PN", "Pitcairn"), + PUERTO_RICO("PR", "Puerto Rico"), + PALESTINE("PS", "Palestine"), + PORTUGAL("PT", "Portugal"), + PALAU("PW", "Palau"), + PARAGUAY("PY", "Paraguay"), + QATAR("QA", "Qatar"), + REUNION("RE", "Reunion"), + ROMANIA("RO", "Romania"), + SERBIA("RS", "Serbia"), + RUSSIA("RU", "Russia"), + RWANDA("RW", "Rwanda"), + SAUDI_ARABIA("SA", "Saudi Arabia"), + SOLOMON_ISLANDS("SB", "Solomon Islands"), + SEYCHELLES("SC", "Seychelles"), + SUDAN("SD", "Sudan"), + SWEDEN("SE", "Sweden"), + SINGAPORE("SG", "Singapore"), + SAINT_HELENA("SH", "Saint Helena"), + SLOVENIA("SI", "Slovenia"), + SVALBARD_AND_JAN_MAYEN("SJ", "Svalbard And Jan Mayen"), + SLOVAKIA("SK", "Slovakia"), + SIERRA_LEONE("SL", "Sierra Leone"), + SAN_MARINO("SM", "San Marino"), + SENEGAL("SN", "Senegal"), + SOMALIA("SO", "Somalia"), + SURINAME("SR", "Suriname"), + SOUTH_SUDAN("SS", "South Sudan"), + SAO_TOME_AND_PRINCIPE("ST", "Sao Tome And Principe"), + EL_SALVADOR("SV", "El Salvador"), + SINT_MAARTEN_DUTCH_PART("SX", "Sint Maarten (Dutch part)"), + SYRIA("SY", "Syria"), + SWAZILAND("SZ", "Swaziland"), + TURKS_AND_CAICOS_ISLANDS("TC", "Turks And Caicos Islands"), + CHAD("TD", "Chad"), + FRENCH_SOUTHERN_TERRITORIES("TF", "French Southern Territories"), + TOGO("TG", "Togo"), + THAILAND("TH", "Thailand"), + TAJIKISTAN("TJ", "Tajikistan"), + TOKELAU("TK", "Tokelau"), + TIMOR_LESTE("TL", "Timor-Leste"), + TURKMENISTAN("TM", "Turkmenistan"), + TUNISIA("TN", "Tunisia"), + TONGA("TO", "Tonga"), + TURKEY("TR", "Turkey"), + TRINIDAD_AND_TOBAGO("TT", "Trinidad and Tobago"), + TUVALU("TV", "Tuvalu"), + TAIWAN("TW", "Taiwan"), + TANZANIA("TZ", "Tanzania"), + UKRAINE("UA", "Ukraine"), + UGANDA("UG", "Uganda"), + UNITED_STATES_MINOR_OUTLYING_ISLANDS("UM", "United States Minor Outlying Islands"), + UNITED_STATES("US", "United States"), + URUGUAY("UY", "Uruguay"), + UZBEKISTAN("UZ", "Uzbekistan"), + VATICAN("VA", "Vatican"), + SAINT_VINCENT_AND_THE_GRENADINES("VC", "Saint Vincent And The Grenadines"), + VENEZUELA("VE", "Venezuela"), + BRITISH_VIRGIN_ISLANDS("VG", "British Virgin Islands"), + U_S__VIRGIN_ISLANDS("VI", "U.S. Virgin Islands"), + VIETNAM("VN", "Vietnam"), + VANUATU("VU", "Vanuatu"), + WALLIS_AND_FUTUNA("WF", "Wallis And Futuna"), + SAMOA("WS", "Samoa"), + YEMEN("YE", "Yemen"), + MAYOTTE("YT", "Mayotte"), + SOUTH_AFRICA("ZA", "South Africa"), + ZAMBIA("ZM", "Zambia"), + ZIMBABWE("ZW", "Zimbabwe"); + + private String isoTag; + private String name; + + Country(String isoTag, String name) { + this.isoTag = isoTag; + this.name = name; + } + + /** + * Gets the name of the country. + * + * @return The name of the country. + */ + public String getCountryName() { + return name; + } + + /** + * Gets the iso tag of the country. + * + * @return The iso tag of the country. + */ + public String getCountryIsoTag() { + return isoTag; + } + + /** + * Gets a country by it's iso tag. + * + * @param isoTag The iso tag of the county. + * @return The country with the given iso tag or null if unknown. + */ + public static Country byIsoTag(String isoTag) { + for (Country country : Country.values()) { + if (country.getCountryIsoTag().equals(isoTag)) { + return country; + } + } + return null; + } + + /** + * Gets a country by a locale. + * + * @param locale The locale. + * @return The country from the giben locale or null if unknown country or + * if the locale does not contain a country. + */ + public static Country byLocale(Locale locale) { + return byIsoTag(locale.getCountry()); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/sh/okx/rankup/Rankup.java b/src/main/java/sh/okx/rankup/Rankup.java new file mode 100644 index 0000000..555c56c --- /dev/null +++ b/src/main/java/sh/okx/rankup/Rankup.java @@ -0,0 +1,220 @@ +package sh.okx.rankup; + +import lombok.Getter; +import me.clip.placeholderapi.PlaceholderAPI; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.permission.Permission; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.InventoryView; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import sh.okx.rankup.commands.InfoCommand; +import sh.okx.rankup.commands.RankListCommand; +import sh.okx.rankup.commands.RankupCommand; +import sh.okx.rankup.gui.Gui; +import sh.okx.rankup.gui.GuiListener; +import sh.okx.rankup.messages.Message; +import sh.okx.rankup.messages.MessageBuilder; +import sh.okx.rankup.messages.Variable; +import sh.okx.rankup.placeholders.Placeholders; +import sh.okx.rankup.ranks.Rank; +import sh.okx.rankup.ranks.Rankups; +import sh.okx.rankup.ranks.requirements.PlaytimeHoursRequirement; +import sh.okx.rankup.ranks.requirements.XpLevelRequirement; +import sh.okx.rankup.ranks.requirements.MoneyRequirement; +import sh.okx.rankup.ranks.requirements.RequirementRegistry; + +import java.io.File; + +public class Rankup extends JavaPlugin { + @Getter + private Permission permissions; + @Getter + private Economy economy; + /** + * The registry for listing the requirements to /rankup. + */ + @Getter + private RequirementRegistry requirementRegistry = new RequirementRegistry(); + @Getter + private FileConfiguration messages; + @Getter + private FileConfiguration config; + @Getter + private Rankups rankups; + @Getter + private Placeholders placeholders; + + @Override + public void onEnable() { + registerRequirements(); + setupPermissions(); + setupEconomy(); + reload(); + + Metrics metrics = new Metrics(this); + metrics.addCustomChart(new Metrics.SimplePie("confirmation") { + @Override + public String getValue() { + return getConfig().getString("confirmation.type"); + } + }); + + getCommand("rankup").setExecutor(new RankupCommand(this)); + getCommand("rankup3").setExecutor(new InfoCommand(this)); + getCommand("ranks").setExecutor(new RankListCommand(this)); + getServer().getPluginManager().registerEvents(new GuiListener(this), this); + } + + + @Override + public void onDisable() { + closeInventories(); + PlaceholderAPI.unregisterExpansion(placeholders); + } + + public void reload() { + closeInventories(); + loadConfigs(); + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + placeholders = new Placeholders(this); + placeholders.register(); + } + + if(config.getInt("version") != YamlConfiguration.loadConfiguration(getTextResource("config.yml")).getInt("version")) { + getLogger().severe("You are using an outdated config!"); + getLogger().severe("This means that some things might not work!"); + getLogger().severe("To update, please rename your config files (or the folder they are in),"); + getLogger().severe("and run /rankup3 reload to generate a new config file."); + getLogger().severe("If that does not work, restart your server."); + getLogger().severe("You may then copy in your config values from the old config."); + getLogger().severe("Check the changelog on the Rankup spigot page to see the changes."); + } + } + + /** + * Closes all rankup inventories on disable + * so players cannot grab items from the inventory + * on a plugin reload. + */ + private void closeInventories() { + for(Player player : Bukkit.getOnlinePlayers()) { + InventoryView view = player.getOpenInventory(); + if(view.getType() == InventoryType.CHEST + && view.getTopInventory().getHolder() instanceof Gui) { + player.closeInventory(); + } + } + } + + private void loadConfigs() { + messages = loadConfig("messages.yml"); + config = loadConfig("config.yml"); + rankups = new Rankups(this, loadConfig("rankups.yml")); + } + + private FileConfiguration loadConfig(String name) { + File file = new File(getDataFolder(), name); + if (!file.exists()) { + saveResource(name, false); + } + return YamlConfiguration.loadConfiguration(file); + } + + private void registerRequirements() { + requirementRegistry.addRequirement(new MoneyRequirement(this, "money")); + requirementRegistry.addRequirement(new XpLevelRequirement(this, "xp-level")); + requirementRegistry.addRequirement(new PlaytimeHoursRequirement(this, "playtime-hours")); + } + + private void setupPermissions() { + RegisteredServiceProvider rsp = getServer().getServicesManager().getRegistration(Permission.class); + permissions = rsp.getProvider(); + } + + private void setupEconomy() { + RegisteredServiceProvider rsp = getServer().getServicesManager().getRegistration(Economy.class); + if (rsp != null) { + economy = rsp.getProvider(); + } else { + getLogger().warning("No economy found."); + } + } + + public MessageBuilder getMessage(Rank rank, Message message) { + ConfigurationSection messages = rankups.getConfig() + .getConfigurationSection(rank.getName()); + if(messages == null || !messages.isSet(message.getName())) { + messages = this.messages; + } + return MessageBuilder.of(messages, message); + } + + public MessageBuilder getMessage(Message message) { + return MessageBuilder.of(messages, message); + } + + public void rankup(Player player) { + if(!checkRankup(player)) { + return; + } + + Rank oldRank = rankups.getRank(player); + Rank rank = rankups.nextRank(oldRank); + + oldRank.applyRequirements(player); + + permissions.playerRemoveGroup(null, player, oldRank.getRank()); + permissions.playerAddGroup(null, player, rank.getRank()); + + getMessage(oldRank, Message.SUCCESS_PUBLIC) + .failIfEmpty() + .replaceAll(player, oldRank, rank) + .broadcast(); + getMessage(oldRank, Message.SUCCESS_PRIVATE) + .failIfEmpty() + .replaceAll(player, oldRank, rank) + .send(player); + + oldRank.runCommands(player, rank); + } + + /** + * Checks if a player can rankup, + * and if they can't, sends the player a message and returns false + * @param player the player to check if they can rankup + * @return true if the player can rankup, false otherwise + */ + public boolean checkRankup(Player player) { + Rank rank = rankups.getRank(player); + if (rank == null) { // check if in ladder + getMessage(Message.NOT_IN_LADDER) + .replace(Variable.PLAYER, player.getName()) + .send(player); + return false; + } else if (rank.isLastRank()) { // check if they are at the highest rank + getMessage(rank, Message.NO_RANKUP) + .replaceAll(player, rank) + .send(player); + return false; + } else if (!rank.checkRequirements(player)) { // check if they can afford it + MessageBuilder builder = + getMessage(rank, Message.REQUIREMENTS_NOT_MET) + .replaceAll(player, rank); + if (economy != null) { + double balance = economy.getBalance(player); + builder = builder + .replace(Variable.MONEY, balance) + .replace(Variable.MONEY_NEEDED, rank.getRequirement("money").getAmount() - balance); + } + builder.send(player); + return false; + } + return true; + } +} diff --git a/src/main/java/sh/okx/rankup/commands/InfoCommand.java b/src/main/java/sh/okx/rankup/commands/InfoCommand.java new file mode 100644 index 0000000..4edc09e --- /dev/null +++ b/src/main/java/sh/okx/rankup/commands/InfoCommand.java @@ -0,0 +1,72 @@ +package sh.okx.rankup.commands; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.PluginDescriptionFile; +import sh.okx.rankup.Rankup; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; + +@RequiredArgsConstructor +public class InfoCommand implements CommandExecutor { + private String versionMessage; + private final Rankup plugin; + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if(args.length > 0) { + if(args[0].equalsIgnoreCase("reload") && sender.hasPermission("rankup.reload")) { + plugin.reload(); + sender.sendMessage(ChatColor.GREEN + "" + ChatColor.BOLD + "Rankup " + ChatColor.YELLOW + "Reloaded configuration files."); + return true; + } + } + + PluginDescriptionFile description = plugin.getDescription(); + sender.sendMessage( + ChatColor.GREEN + "" + ChatColor.BOLD + description.getName() + " " + description.getVersion() + + ChatColor.YELLOW + " by " + ChatColor.BLUE + ChatColor.BOLD + String.join(", ", description.getAuthors())); + if(sender.hasPermission("rankup.reload")) { + sender.sendMessage(ChatColor.GREEN + "/" + label + " reload " + ChatColor.YELLOW + "Reloads configuration files."); + } + if(sender.hasPermission("rankup.checkversion")) { + if(versionMessage == null) { + sender.sendMessage(ChatColor.YELLOW + "Checking version..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String message; + try { + String latest = getLatestVersion(); + if (description.getVersion().equals(latest)) { + message = ChatColor.GREEN + "You are on the latest version."; + } else { + message = ChatColor.YELLOW + "A new version is available: " + ChatColor.GOLD + latest + + "\nhttps://www.spigotmc.org/resources/rankup.17933/"; + } + } catch (IOException e) { + message = ChatColor.RED + "Error while checking version."; + } + versionMessage = message; + Bukkit.getScheduler().runTask(plugin, () -> sender.sendMessage(versionMessage)); + }); + } else { + sender.sendMessage(versionMessage); + } + } + + return true; + } + + public String getLatestVersion() throws IOException { + URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=17933"); + String result = CharStreams.toString(new InputStreamReader(url.openStream(), Charsets.UTF_8)); + return result; + } +} diff --git a/src/main/java/sh/okx/rankup/commands/RankListCommand.java b/src/main/java/sh/okx/rankup/commands/RankListCommand.java new file mode 100644 index 0000000..3c92ba3 --- /dev/null +++ b/src/main/java/sh/okx/rankup/commands/RankListCommand.java @@ -0,0 +1,95 @@ +package sh.okx.rankup.commands; + +import lombok.RequiredArgsConstructor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; +import sh.okx.rankup.messages.Message; +import sh.okx.rankup.messages.MessageBuilder; +import sh.okx.rankup.messages.Variable; +import sh.okx.rankup.ranks.Rank; +import sh.okx.rankup.ranks.Rankups; +import sh.okx.rankup.ranks.requirements.Requirement; + +import java.text.DecimalFormat; + +@RequiredArgsConstructor +public class RankListCommand implements CommandExecutor { + private final Rankup plugin; + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + Rankups rankups = plugin.getRankups(); + Rank playerRank = null; + if(sender instanceof Player) { + playerRank = rankups.getRank((Player) sender); + } + + sendHeaderFooter(sender, playerRank, Message.RANKS_HEADER); + + int state = playerRank == null ? 2 : 0; + Rank rank = rankups.getFirstRank(); + do { + Rank next = rankups.nextRank(rank); + if(rank.equals(playerRank)) { + sendMessage(sender, 1, rank, next); + state = 2; + } else { + sendMessage(sender, state, rank, next); + } + rank = next; + } while(!rank.isLastRank()); + + sendHeaderFooter(sender, playerRank, Message.RANKS_FOOTER); + return true; + } + + private void sendHeaderFooter(CommandSender sender, Rank rank, Message type) { + MessageBuilder builder = plugin.getMessage(type) + .failIfEmpty(); + if(rank == null) { + builder.replace(Variable.PLAYER, sender.getName()); + } else { + builder.replaceAll(sender, rank); + } + builder.send(sender); + } + + private void sendMessage(CommandSender player, int state, Rank oldRank, Rank rank) { + if(state == 0) { + replaceCost(plugin.getMessage(oldRank, Message.RANKS_COMPLETE) + .replaceAll(player, oldRank, rank), player, oldRank) + .send(player); + } else if(state == 1) { + replaceCost(plugin.getMessage(oldRank, Message.RANKS_CURRENT) + .replaceAll(player, oldRank, rank), player, oldRank) + .send(player); + } else if(state == 2) { + replaceCost(plugin.getMessage(oldRank, Message.RANKS_INCOMPLETE) + .replaceAll(player, oldRank, rank), player, oldRank) + .send(player); + } + } + + private MessageBuilder replaceCost(MessageBuilder builder, CommandSender sender, Rank rank) { + Requirement money = rank.getRequirement("money"); + if(money == null || plugin.getEconomy() == null) { + return builder; + } + double amount; + if(sender instanceof Player && rank.isInRank((Player) sender)) { + amount = money.getRemaining((Player) sender); + } else { + amount = money.getAmount(); + } + DecimalFormat moneyFormat = plugin.getPlaceholders().getMoneyFormat(); + DecimalFormat percentFormat = plugin.getPlaceholders().getPercentFormat(); + return builder + .replace(Variable.MONEY_NEEDED, moneyFormat.format(amount)) + .replace(Variable.PERCENT_LEFT, percentFormat.format((amount / money.getAmount()) * 100)) + .replace(Variable.PERCENT_DONE, percentFormat.format((1-(amount / money.getAmount())) * 100)) + .replace(Variable.MONEY, moneyFormat.format(money.getAmount())); + } +} diff --git a/src/main/java/sh/okx/rankup/commands/RankupCommand.java b/src/main/java/sh/okx/rankup/commands/RankupCommand.java new file mode 100644 index 0000000..6467d95 --- /dev/null +++ b/src/main/java/sh/okx/rankup/commands/RankupCommand.java @@ -0,0 +1,72 @@ +package sh.okx.rankup.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; +import sh.okx.rankup.gui.Gui; +import sh.okx.rankup.messages.Message; +import sh.okx.rankup.messages.Variable; +import sh.okx.rankup.ranks.Rank; +import sh.okx.rankup.ranks.Rankups; + +import java.util.Map; +import java.util.WeakHashMap; + +public class RankupCommand implements CommandExecutor { + private final Map confirming = new WeakHashMap<>(); + private final Rankup plugin; + + public RankupCommand(Rankup plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // check if player + if (!(sender instanceof Player)) { + return false; + } + Player player = (Player) sender; + + Rankups rankups = plugin.getRankups(); + Rank rank = rankups.getRank(player); + if (!plugin.checkRankup(player)) { + return true; + } + + FileConfiguration config = plugin.getConfig(); + String confirmationType = config.getString("confirmation-type").toLowerCase(); + + // if they are on text confirming, rank them up + if (confirmationType.equals("text") && confirming.containsKey(player)) { + long time = System.currentTimeMillis() - confirming.remove(player); + if (time < config.getInt("text.timeout") * 1000) { + plugin.rankup(player); + return true; + } + } + + switch (confirmationType) { + case "text": + confirming.put(player, System.currentTimeMillis()); + plugin.getMessage(rank, Message.CONFIRMATION) + .replace(Variable.PLAYER, player.getName()) + .replace(Variable.RANK, rank.getRank()) + .replace(Variable.RANK_NAME, rank.getName()) + .send(player); + break; + case "gui": + Gui.of(player, rank, rankups.nextRank(rank), plugin).open(player); + break; + case "none": + plugin.rankup(player); + break; + default: + throw new IllegalArgumentException("Invalid confirmation type " + confirmationType); + } + return true; + } +} diff --git a/src/main/java/sh/okx/rankup/gui/Gui.java b/src/main/java/sh/okx/rankup/gui/Gui.java new file mode 100644 index 0000000..555765b --- /dev/null +++ b/src/main/java/sh/okx/rankup/gui/Gui.java @@ -0,0 +1,121 @@ +package sh.okx.rankup.gui; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import sh.okx.rankup.Rankup; +import sh.okx.rankup.messages.Message; +import sh.okx.rankup.messages.MessageBuilder; +import sh.okx.rankup.messages.Variable; +import sh.okx.rankup.ranks.Rank; + +import java.util.Arrays; +import java.util.stream.Collectors; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Gui implements InventoryHolder { + @Getter + private Inventory inventory; + @Getter + private ItemStack rankup; + @Getter + private ItemStack cancel; + + public void open(Player player) { + player.openInventory(inventory); + } + + public static Gui of(Player player, Rank oldRank, Rank rank, Rankup plugin) { + ConfigurationSection config = plugin.getConfig().getConfigurationSection("gui"); + ItemStack[] items = new ItemStack[config.getInt("rows") * 9]; + addItem(items, config.getConfigurationSection("rankup"), player, oldRank, rank); + addItem(items, config.getConfigurationSection("cancel"), player, oldRank, rank); + addItem(items, config.getConfigurationSection("fill"), player, oldRank, rank); + + Gui gui = new Gui(); + gui.rankup = getItem(config.getConfigurationSection("rankup"), player, oldRank, rank); + gui.cancel = getItem(config.getConfigurationSection("cancel"), player, oldRank, rank); + Inventory inventory = Bukkit.createInventory(gui, + items.length, + plugin.getMessage(rank, Message.TITLE) + .replaceAll(player, oldRank, rank) + .toString()); + inventory.setContents(items); + gui.inventory = inventory; + return gui; + } + + private static ItemStack getItem(ConfigurationSection section, Player player, Rank oldRank, Rank rank) { + boolean legacy = !Bukkit.getVersion().contains("1.13"); + + String materialName = section.getString("material").toUpperCase(); + // handle default material correctly on older versions + if (legacy && materialName.equals("BLACK_STAINED_GLASS_PANE")) { + materialName = "STAINED_GLASS_PANE:15"; + } + + ItemStack item; + if (legacy) { + String[] parts = materialName.split(":"); + Material material = Material.valueOf(parts[0]); + + short type = parts.length > 1 ? Short.parseShort(parts[1]) : 0; + item = new ItemStack(material, 1, type); + } else { + Material material = Material.valueOf(materialName); + item = new ItemStack(material); + } + + ItemMeta meta = item.getItemMeta(); + if (section.contains("lore")) { + meta.setLore(Arrays.stream(format(section.getString("lore"), player, oldRank, rank).split("\n")) + .map(string -> ChatColor.RESET + string) + .collect(Collectors.toList())); + } + if (section.contains("name")) { + meta.setDisplayName(ChatColor.RESET + format(section.getString("name"), player, oldRank, rank)); + } + item.setItemMeta(meta); + + return item; + } + + private static String format(String message, Player player, Rank oldRank, Rank rank) { + return new MessageBuilder(ChatColor.translateAlternateColorCodes('&', message)) + .replaceAll(player, oldRank, rank) + .toString(); + } + + private static void addItem(ItemStack[] items, ConfigurationSection section, Player player, Rank oldRank, Rank rank) { + ItemStack item = getItem(section, player, oldRank, rank); + if (section.getName().equalsIgnoreCase("fill")) { + for (int i = 0; i < items.length; i++) { + if (items[i] == null) { + items[i] = item; + } + } + return; + } + + String[] locations = section.getString("index").split(" "); + for (String location : locations) { + String[] parts = location.split("-"); + if (parts.length == 1) { + items[Integer.parseInt(parts[0])] = item; + } else { + for (int i = Integer.parseInt(parts[0]); i <= Integer.parseInt(parts[1]); i++) { + items[i] = item; + } + } + } + } +} diff --git a/src/main/java/sh/okx/rankup/gui/GuiListener.java b/src/main/java/sh/okx/rankup/gui/GuiListener.java new file mode 100644 index 0000000..e4897b3 --- /dev/null +++ b/src/main/java/sh/okx/rankup/gui/GuiListener.java @@ -0,0 +1,36 @@ +package sh.okx.rankup.gui; + +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import sh.okx.rankup.Rankup; + +@RequiredArgsConstructor +public class GuiListener implements Listener { + private final Rankup plugin; + + @EventHandler + public void on(InventoryClickEvent e) { + Inventory inventory = e.getInventory(); + if (inventory == null + || !(inventory.getHolder() instanceof Gui) + || !e.getInventory().equals(e.getClickedInventory())) { + return; + } + e.setCancelled(true); + + Player player = (Player) e.getWhoClicked(); + Gui gui = (Gui) inventory.getHolder(); + + if (gui.getRankup().isSimilar(e.getCurrentItem())) { + Bukkit.getScheduler().runTask(plugin, player::closeInventory); + plugin.rankup(player); + } else if (gui.getCancel().isSimilar(e.getCurrentItem())) { + Bukkit.getScheduler().runTask(plugin, player::closeInventory); + } + } +} diff --git a/src/main/java/sh/okx/rankup/messages/EmptyMessageBuilder.java b/src/main/java/sh/okx/rankup/messages/EmptyMessageBuilder.java new file mode 100644 index 0000000..9039317 --- /dev/null +++ b/src/main/java/sh/okx/rankup/messages/EmptyMessageBuilder.java @@ -0,0 +1,32 @@ +package sh.okx.rankup.messages; + +import org.bukkit.command.CommandSender; + +class EmptyMessageBuilder extends MessageBuilder { + EmptyMessageBuilder() { + super(null); + } + + @Override + public MessageBuilder failIfEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public MessageBuilder replace(Variable variable, Object value) { + return this; + } + + @Override + public void send(CommandSender sender) { + } + + @Override + public void broadcast() { + } + + @Override + public String toString() { + return null; + } +} diff --git a/src/main/java/sh/okx/rankup/messages/Message.java b/src/main/java/sh/okx/rankup/messages/Message.java new file mode 100644 index 0000000..f20813f --- /dev/null +++ b/src/main/java/sh/okx/rankup/messages/Message.java @@ -0,0 +1,25 @@ +package sh.okx.rankup.messages; + +import lombok.Getter; + +public enum Message { + NOT_IN_LADDER("not-in-ladder"), + REQUIREMENTS_NOT_MET("rankup.requirements-not-met"), + NO_RANKUP("rankup.no-rankup"), + SUCCESS_PUBLIC("rankup.success-public"), + SUCCESS_PRIVATE("rankup.success-private"), + CONFIRMATION("rankup.confirmation"), + TITLE("rankup.title"), + RANKS_HEADER("ranks.header"), + RANKS_FOOTER("ranks.footer"), + RANKS_COMPLETE("rankup.ranks.complete"), + RANKS_CURRENT("rankup.ranks.current"), + RANKS_INCOMPLETE("rankup.ranks.incomplete"); + + @Getter + private final String name; + + Message(String name) { + this.name = name; + } +} diff --git a/src/main/java/sh/okx/rankup/messages/MessageBuilder.java b/src/main/java/sh/okx/rankup/messages/MessageBuilder.java new file mode 100644 index 0000000..e522e30 --- /dev/null +++ b/src/main/java/sh/okx/rankup/messages/MessageBuilder.java @@ -0,0 +1,103 @@ +package sh.okx.rankup.messages; + +import lombok.AllArgsConstructor; +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import sh.okx.rankup.ranks.Rank; +import sh.okx.rankup.ranks.requirements.Requirement; + +import java.text.DecimalFormat; + +public class MessageBuilder { + private String message; + + public MessageBuilder(String message) { + this.message = message; + } + + public static MessageBuilder of(ConfigurationSection config, Message message) { + return new MessageBuilder(ChatColor.translateAlternateColorCodes('&', config.getString(message.getName()))); + } + + public MessageBuilder replace(Variable variable, Object value) { + this.message = variable.replace(message, String.valueOf(value)); + return this; + } + + public MessageBuilder replaceAll(CommandSender player, Rank rank) { + replace(Variable.PLAYER, player.getName()); + replaceAll(rank); + return this; + } + + public MessageBuilder replaceAll(CommandSender player, Rank oldRank, Rank rank) { + replace(Variable.PLAYER, player.getName()); + replaceAll(oldRank, rank); + return this; + } + + public MessageBuilder replaceAll(Rank rank) { + replace(Variable.RANK, rank.getRank()); + replace(Variable.RANK_NAME, rank.getName()); + return this; + } + + public MessageBuilder replaceAll(Rank oldRank, Rank rank) { + replace(Variable.RANK, rank.getRank()); + replace(Variable.RANK_NAME, rank.getName()); + replace(Variable.OLD_RANK, oldRank.getRank()); + replace(Variable.OLD_RANK_NAME, oldRank.getName()); + return this; + } + + public MessageBuilder replaceCost(CommandSender sender, Economy economy, Rank rank) { + Requirement money = rank.getRequirement("money"); + if(money == null || economy == null) { + return this; + } + replace(Variable.MONEY, money.getAmount()); + if(sender instanceof Player && rank.isInRank((Player) sender)) { + replace(Variable.MONEY_NEEDED, money.getRemaining((Player) sender)); + } else { + replace(Variable.MONEY_NEEDED, money.getAmount()); + } + return this; + } + + /** + * Fails the MessageBuilder if the message is empty. + * if this fails, all subsequent calls to that MessageBuilder will do nothing + * @return an EmptyMessageBuilder if the message is empty, itself otherwise + */ + public MessageBuilder failIfEmpty() { + if(message.isEmpty()) { + return new EmptyMessageBuilder(); + } else { + return this; + } + } + + public void send(CommandSender sender) { + sender.sendMessage(message); + } + + /** + * Sends the message to all players + * ie, calls MessageBuilder#send(Player) for all players online, and sends the message in the console. + */ + public void broadcast() { + for(Player player : Bukkit.getOnlinePlayers()) { + send(player); + } + send(Bukkit.getConsoleSender()); + } + + @Override + public String toString() { + return message; + } +} diff --git a/src/main/java/sh/okx/rankup/messages/Variable.java b/src/main/java/sh/okx/rankup/messages/Variable.java new file mode 100644 index 0000000..d3f18bf --- /dev/null +++ b/src/main/java/sh/okx/rankup/messages/Variable.java @@ -0,0 +1,31 @@ +package sh.okx.rankup.messages; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public enum Variable { + PLAYER, + OLD_RANK, + OLD_RANK_NAME, + RANK, + RANK_NAME, + MONEY, + MONEY_NEEDED, + PERCENT_DONE, + PERCENT_LEFT; + + public static Variable getVariable(String name) { + for(Variable variable : values()) { + if(variable.toString().equalsIgnoreCase(name)) { + return variable; + } + } + return null; + } + + public String replace(String message, String value) { + Pattern pattern = Pattern.compile("\\{" + this + "}", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(message); + return matcher.replaceAll(value); + } +} diff --git a/src/main/java/sh/okx/rankup/placeholders/Placeholders.java b/src/main/java/sh/okx/rankup/placeholders/Placeholders.java new file mode 100644 index 0000000..d2f56f7 --- /dev/null +++ b/src/main/java/sh/okx/rankup/placeholders/Placeholders.java @@ -0,0 +1,167 @@ +package sh.okx.rankup.placeholders; + +import lombok.Getter; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; +import sh.okx.rankup.ranks.Rank; +import sh.okx.rankup.ranks.Rankups; +import sh.okx.rankup.ranks.requirements.Requirement; + +import java.text.DecimalFormat; +import java.util.function.Function; + +public class Placeholders extends PlaceholderExpansion { + private final Rankup plugin; + @Getter + private final DecimalFormat moneyFormat; + @Getter + private final DecimalFormat percentFormat; + @Getter + private final DecimalFormat simpleFormat; + + public Placeholders(Rankup plugin) { + this.plugin = plugin; + this.moneyFormat = new DecimalFormat(plugin.getConfig().getString("placeholders.money-format")); + this.percentFormat = new DecimalFormat(plugin.getConfig().getString("placeholders.percent-format")); + this.simpleFormat = new DecimalFormat(plugin.getConfig().getString("placeholders.simple-format")); + } + + @Override + public String onPlaceholderRequest(Player player, String params) { + if (player == null) { + return ""; + } + params = params.toLowerCase(); + + Rankups rankups = plugin.getRankups(); + Rank rank = rankups.getRank(player); + Rank next = null; + if (rank != null) { + next = rankups.nextRank(rank); + } + + if(params.startsWith("requirement_")) { + String[] parts = params.split("_", 3); + return getPlaceholderRequirement(player, rank, + parts[1], parts.length > 2 ? parts[2] : ""); + } + + switch (params) { + case "current_rank": + return orElsePlaceholder(rank, Rank::getRank, "not-in-ladder"); + case "current_rank_name": + return orElsePlaceholder(rank, Rank::getRank, "not-in-ladder"); + case "current_rank_money": + return orElsePlaceholder(rank, r -> simplify(r.getRequirement("money").getAmount()), 0); + case "current_rank_money_formatted": + return moneyFormat.format(orElsePlaceholder(rank, r -> r.getRequirement("money").getAmount(), 0)); + case "next_rank": + if (rank == null) { + return getPlaceholder("not-in-ladder"); + } else if (next == null) { + return getPlaceholder("highest-rank"); + } else { + return next.getRank(); + } + case "next_rank_name": + if (rank == null) { + return getPlaceholder("not-in-ladder"); + } else if (next == null) { + return getPlaceholder("highest-rank"); + } else { + return next.getName(); + } + case "next_rank_money": + return orElsePlaceholder(next, r -> simplify(r.getRequirement("money").getAmount()), 0); + case "next_rank_money_formatted": + return moneyFormat.format(orElsePlaceholder(next, r -> r.getRequirement("money").getAmount(), 0)); + case "next_rank_money_left": + return orElsePlaceholder(next, r -> simplify(plugin.getEconomy().getBalance(player) - r.getRequirement("money").getAmount()), 0); + case "next_rank_money_left_formatted": + return moneyFormat.format(orElsePlaceholder(next, r -> plugin.getEconomy().getBalance(player) - r.getRequirement("money").getAmount(), 0)); + case "next_rank_percent_left": + return orElsePlaceholder(next, r -> (1-(plugin.getEconomy().getBalance(player) / r.getRequirement("money").getAmount())) * 100, 0); + case "next_rank_percent_left_formatted": + return percentFormat.format(orElsePlaceholder(next, r -> (1-(plugin.getEconomy().getBalance(player) / r.getRequirement("money").getAmount())) * 100, 0)); + case "next_rank_percent_done": + return orElsePlaceholder(next, r -> (plugin.getEconomy().getBalance(player) / r.getRequirement("money").getAmount()) * 100, 0); + case "next_rank_percent_done_formatted": + return percentFormat.format(orElsePlaceholder(next, r -> (plugin.getEconomy().getBalance(player) / r.getRequirement("money").getAmount()) * 100, 0)); + default: + return null; + } + } + + private String getPlaceholderRequirement(Player player, Rank rank, String requirementName, String params) { + if(rank == null) { + return ""; + } + Requirement requirement = rank.getRequirement(requirementName); + switch(params) { + case "": + return simpleFormat.format(orElse(requirement, Requirement::getAmount, 0)); + case "left": + return simpleFormat.format(orElse(requirement, r -> r.getRemaining(player), 0)); + case "percent_left": + return percentFormat.format(orElse(requirement, r -> (r.getRemaining(player) / r.getAmount()) * 100, 0)); + case "percent_done": + return percentFormat.format(orElse(requirement, r -> (1-(r.getRemaining(player) / r.getAmount())) * 100, 100)); + default: + return null; + } + } + + private Number simplify(Number number) { + if (number instanceof Float) { + return (float) number % 1 == 0 ? (int) number : number; + } else if (number instanceof Double) { + return (double) number % 1 == 0 ? (long) number : number; + } else { + return number; + } + } + + private String orElsePlaceholder(T t, Function value, Object fallback) { + if (t == null) { + return getPlaceholder(String.valueOf(fallback)); + } + + try { + return String.valueOf(value.apply(t)); + } catch (NullPointerException ex) { + return getPlaceholder(String.valueOf(fallback)); + } + } + + private R orElse(T t, Function value, R fallback) { + if (t == null) { + return fallback; + } + + try { + return value.apply(t); + } catch (NullPointerException ex) { + return fallback; + } + } + + private String getPlaceholder(String name) { + return plugin.getConfig().getString("placeholders." + name); + } + + @Override + public String getIdentifier() { + return "rankup"; + } + + @Override + public String getAuthor() { + return String.join(", ", plugin.getDescription().getAuthors()); + } + + @Override + public String getVersion() { + return plugin.getDescription().getVersion(); + } +} diff --git a/src/main/java/sh/okx/rankup/ranks/Prestige.java b/src/main/java/sh/okx/rankup/ranks/Prestige.java new file mode 100644 index 0000000..fe006cb --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/Prestige.java @@ -0,0 +1,4 @@ +package sh.okx.rankup.ranks; + +public class Prestige { +} diff --git a/src/main/java/sh/okx/rankup/ranks/Rank.java b/src/main/java/sh/okx/rankup/ranks/Rank.java new file mode 100644 index 0000000..9c5106b --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/Rank.java @@ -0,0 +1,138 @@ +package sh.okx.rankup.ranks; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; +import sh.okx.rankup.messages.MessageBuilder; +import sh.okx.rankup.messages.Variable; +import sh.okx.rankup.ranks.requirements.Requirement; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BinaryOperator; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class Rank { + private final Rankup plugin; + @Getter + private final String name; + @Getter + private final String next; + @Getter + private final String rank; + private final Set requirements; + private final BinaryOperator reducer; + private final List commands; + + public static Rank deserialize(Rankup plugin, ConfigurationSection section) { + String rank = section.getString("rank"); + + Set requirements = new HashSet<>(); + BinaryOperator reducer = null; + ConfigurationSection requirementsSection = section.getConfigurationSection("requirements"); + if(requirementsSection != null) { + for (Map.Entry entry : requirementsSection.getValues(false).entrySet()) { + String name = entry.getKey(); + double amount = Double.parseDouble(String.valueOf(entry.getValue())); + + Requirement requirement = plugin.getRequirementRegistry().newRequirement(name, amount); + if (requirement == null) { + plugin.getLogger().warning("Unknown requirement " + name); + } else { + requirements.add(requirement); + } + } + + String operation = section.getString("operation"); + if (operation == null) { + operation = "and"; + } + switch (operation) { + case "and": + reducer = (a, b) -> a && b; + break; + case "or": + reducer = (a, b) -> a || b; + break; + case "xor": + reducer = (a, b) -> (a && !b) || (b && !a); + break; + case "none": + reducer = (a, b) -> !a && !b; + break; + default: + throw new IllegalArgumentException("Invalid operation type for rank " + rank); + } + } + + return new Rank(plugin, + section.getName(), + section.getString("next"), + rank, + requirements, + reducer, + section.getStringList("commands")); + } + + public boolean checkRequirements(Player player) { + return requirements.stream() + .map(requirement -> requirement.check(player)) + .reduce(reducer) + .orElse(true); + } + + public boolean isInRank(Player player) { + String[] groups = plugin.getPermissions().getPlayerGroups(player); + for (String group : groups) { + if(group.equalsIgnoreCase(rank)) { + return true; + } + } + return false; + } + + public boolean isLastRank() { + return next == null; + } + + public Requirement getRequirement(String name) { + for(Requirement requirement : requirements) { + if(requirement.getName().equalsIgnoreCase(name)) { + return requirement; + } + } + return null; + } + + public void applyRequirements(Player player) { + for(Requirement requirement : requirements) { + requirement.apply(player); + } + } + + public void runCommands(Player player, Rank nextRank) { + for (String command : commands) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), new MessageBuilder(command) + .replace(Variable.PLAYER, player.getName()) + .replace(Variable.OLD_RANK, rank) + .replace(Variable.OLD_RANK_NAME, name) + .replace(Variable.RANK, nextRank.rank) + .replace(Variable.RANK_NAME, nextRank.name) + .toString()); + } + } + + @Override + public boolean equals(Object o) { + if(!(o instanceof Rank)) { + return false; + } + return ((Rank) o).name.equals(name); + } +} diff --git a/src/main/java/sh/okx/rankup/ranks/Rankups.java b/src/main/java/sh/okx/rankup/ranks/Rankups.java new file mode 100644 index 0000000..e36803e --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/Rankups.java @@ -0,0 +1,80 @@ +package sh.okx.rankup.ranks; + +import lombok.Getter; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class Rankups { + @Getter + private final FileConfiguration config; + private final Set ranks = new HashSet<>(); + + public Rankups(Rankup plugin, FileConfiguration config) { + this.config = config; + for (Map.Entry entry : config.getValues(false).entrySet()) { + ConfigurationSection rankSection = (ConfigurationSection) entry.getValue(); + ranks.add(Rank.deserialize(plugin, rankSection)); + } + } + + public Rank getFirstRank() { + OUTER: + for(Rank rank : ranks) { + // see if anything ranks up to this + for(Rank rank0 : ranks) { + if(!rank0.isLastRank() && rank0.getNext().equals(rank.getName())) { + continue OUTER; + } + } + // nothing ranks up to this + return rank; + } + return null; + } + + public Rank getRank(String name) { + for(Rank rank : ranks) { + if(rank.getName().equals(name)) { + return rank; + } + } + return null; + } + + public Rank getRank(Player player) { + return ranks.stream() + .filter(rank -> rank.isInRank(player)) + .findFirst() + .orElse(null); + } + + public Rank nextRank(Rank rank) { + if(rank.isLastRank()) { + return null; + } + + for(Rank nextRank : ranks) { + if (rank.getNext().equalsIgnoreCase(nextRank.getName())) { + return nextRank; + } + } + // this shouldn't happen but whatever + return null; + } + +// public boolean hasNext(Rank start, Rank rank) { +// while(!start.isLastRank()) { +// start = nextRank(rank); +// if(start.equals(rank)) { +// return true; +// } +// } +// return false; +// } +} diff --git a/src/main/java/sh/okx/rankup/ranks/requirements/MoneyRequirement.java b/src/main/java/sh/okx/rankup/ranks/requirements/MoneyRequirement.java new file mode 100644 index 0000000..74bf324 --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/requirements/MoneyRequirement.java @@ -0,0 +1,38 @@ +package sh.okx.rankup.ranks.requirements; + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; + +public class MoneyRequirement extends Requirement { + public MoneyRequirement(Rankup plugin, String name) { + super(plugin, name); + } + + protected MoneyRequirement(Requirement clone) { + super(clone); + } + + @Override + public boolean check(Player player) { + Economy economy = plugin.getEconomy(); + double balance = economy.getBalance(player); + return balance >= amount; + } + + @Override + public void apply(Player player) { + Economy economy = plugin.getEconomy(); + economy.withdrawPlayer(player, amount); + } + + @Override + public double getRemaining(Player player) { + return Math.max(0, amount - plugin.getEconomy().getBalance(player)); + } + + @Override + public Requirement clone() { + return new MoneyRequirement(this); + } +} \ No newline at end of file diff --git a/src/main/java/sh/okx/rankup/ranks/requirements/PlaytimeHoursRequirement.java b/src/main/java/sh/okx/rankup/ranks/requirements/PlaytimeHoursRequirement.java new file mode 100644 index 0000000..1406f30 --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/requirements/PlaytimeHoursRequirement.java @@ -0,0 +1,37 @@ +package sh.okx.rankup.ranks.requirements; + +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; + +public class PlaytimeHoursRequirement extends Requirement { + private static final int TICKS_PER_HOUR = 20 * 60 * 60; + + public PlaytimeHoursRequirement(Rankup plugin, String name) { + super(plugin, name); + } + + protected PlaytimeHoursRequirement(Requirement clone) { + super(clone); + } + + @Override + public boolean check(Player player) { + return player.getStatistic(Statistic.PLAY_ONE_MINUTE) * TICKS_PER_HOUR >= amount; + } + + @Override + public void apply(Player player) { + // well, we can't really take hours of playtime away, can we? + } + + @Override + public double getRemaining(Player player) { + return amount - (player.getStatistic(Statistic.PLAY_ONE_MINUTE) * TICKS_PER_HOUR); + } + + @Override + public Requirement clone() { + return new PlaytimeHoursRequirement(this); + } +} diff --git a/src/main/java/sh/okx/rankup/ranks/requirements/Requirement.java b/src/main/java/sh/okx/rankup/ranks/requirements/Requirement.java new file mode 100644 index 0000000..8d26188 --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/requirements/Requirement.java @@ -0,0 +1,55 @@ +package sh.okx.rankup.ranks.requirements; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; + +public abstract class Requirement implements Cloneable { + protected Rankup plugin; + @Getter + protected String name; + @Getter + @Setter + protected double amount; + + public Requirement(Rankup plugin, String name) { + this.plugin = plugin; + this.name = name; + } + + protected Requirement(Requirement clone) { + if(clone != null) { + this.plugin = clone.plugin; + this.name = clone.name; + this.amount = clone.amount; + } + } + + /** + * Check if a player meets this requirement + * @param player the player to check + * @return true if they meet the requirement, false otherwise + */ + public abstract boolean check(Player player); + + /** + * Apply the effect of this requirement to the player. + * For money, this could be taking money away from the player. + * You can assume that Requirement#check(Player) has been called, + * and has returned true immediately prior to this. + * @param player the player to take from + */ + public abstract void apply(Player player); + + /** + * Get the remaining amount needed for Requirement#check(Player) to yield true. + * This is not required and is only used in placeholders. + * @param player the player to find the remaining amount of + * @return the remaining amount needed. Should be non-negative. + */ + public double getRemaining(Player player) { + return amount; + } + public abstract Requirement clone(); +} diff --git a/src/main/java/sh/okx/rankup/ranks/requirements/RequirementRegistry.java b/src/main/java/sh/okx/rankup/ranks/requirements/RequirementRegistry.java new file mode 100644 index 0000000..69ef0ff --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/requirements/RequirementRegistry.java @@ -0,0 +1,23 @@ +package sh.okx.rankup.ranks.requirements; + +import java.util.HashSet; +import java.util.Set; + +public class RequirementRegistry { + private Set requirements = new HashSet<>(); + + public void addRequirement(Requirement requirement) { + requirements.add(requirement); + } + + public Requirement newRequirement(String name, double amount) { + for(Requirement requirement : requirements) { + if(requirement.getName().equalsIgnoreCase(name)) { + Requirement newRequirement = requirement.clone(); + newRequirement.setAmount(amount); + return newRequirement; + } + } + return null; + } +} diff --git a/src/main/java/sh/okx/rankup/ranks/requirements/XpLevelRequirement.java b/src/main/java/sh/okx/rankup/ranks/requirements/XpLevelRequirement.java new file mode 100644 index 0000000..f4bed0d --- /dev/null +++ b/src/main/java/sh/okx/rankup/ranks/requirements/XpLevelRequirement.java @@ -0,0 +1,40 @@ +package sh.okx.rankup.ranks.requirements; + +import org.bukkit.entity.Player; +import sh.okx.rankup.Rankup; + +public class XpLevelRequirement extends Requirement { + public XpLevelRequirement(Rankup plugin, String name) { + super(plugin, name); + } + + protected XpLevelRequirement(Requirement clone) { + super(clone); + } + + @Override + public void setAmount(double amount) { + // experience level should be a whole number + super.setAmount(Math.round(amount)); + } + + @Override + public boolean check(Player player) { + return player.getLevel() >= amount; + } + + @Override + public void apply(Player player) { + player.setLevel(player.getLevel() - (int) amount); + } + + @Override + public double getRemaining(Player player) { + return Math.max(0, amount - player.getLevel()); + } + + @Override + public Requirement clone() { + return new XpLevelRequirement(this); + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..b15deda --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,45 @@ +# this is used for letting you know that you need to update/change your config file +version: 0 + +# how people should confirm ranking up +# options are: gui, text or none +confirmation-type: 'gui' + +gui: + rows: 1 + rankup: + material: EMERALD_BLOCK + # index can be separated by spaces to show in multiple ways + # ie, 0-3 9-12 18-21 + # you can also just use a single number instead of a range. + index: 0-3 + name: '&a&lConfirm' + # lore is optional + lore: '&6Rankup to &b{RANK}' + cancel: + material: REDSTONE_BLOCK + index: 5-8 + name: '&c&lCancel' + fill: + name: ' ' + # if you are using a 1.8-1.12 and you want to change this + # you can use MATERIAL:data, for example STAINED_GLASS_PANE:8 + # this works for both the rankup and cancel blocks as well + material: BLACK_STAINED_GLASS_PANE + +text: + # the time in seconds for a player to + # confirm ranking up by typing the command again + timeout: 10 + +placeholders: + # format for money. for more information, see + # https://docs.oracle.com/javase/8/docs/api/java/text/DecimalFormat.html + money-format: "#,##0.##" + percent-format: "0.##" + # the format used for requirements + simple-format: "#.##" + # used for current_rank and next_rank placeholders when a player not in anything in rankups.yml + not-in-ladder: "None" + # used in next_rank placeholders when there is no rankup + highest-rank: "None" \ No newline at end of file diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..b94341e --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,35 @@ +# the messages in this section can be customised for each rankup in rankups.yml. +rankup: + # NOTE: if you are using requirements for your ranks that are NOT money, + # you will want to change this for each rank! + requirements-not-met: "&cYou need {MONEY} money to rankup." + no-rankup: "&eYou are at the highest rank." + # set to an empty string, ie: success-public: "" + # to hide that message. + success-public: "&a{PLAYER} &ehas ranked up to: &d{RANK}" + success-private: "&aYou have ranked up to: &d{RANK}" + # used for the text confirmation + confirmation: |- + &eAre you sure you want to rankup to &a{RANK}&e? + &eType &c/rankup &eagain to confirm. + # used for the GUI confirmation + title: "Rankup to {RANK}" + + # It is HIGHLY RECOMMENDED you override these in rankups.yml + # to show the specific requirements for each rank. + # however if you are just using money, you can use {MONEY} or {MONEY_NEEDED} or {PERCENT_DONE} or {PERCENT_LEFT} + # for example: + #ranks: + # complete: "&7{OLD_RANK} &8\xbb &7{RANK} &efor &7${MONEY}" + # current: "&c{OLD_RANK} &e\xbb &c{RANK} &efor &a${MONEY} &e{PERCENT_DONE}%" + # incomplete: "&r{OLD_RANK} &e\xbb &r{RANK} &efor &a${MONEY}" + ranks: + complete: "&7{OLD_RANK} &8\xbb &7{RANK}" + current: "&c{OLD_RANK} &e\xbb &c{RANK}" + incomplete: "&r{OLD_RANK} &e\xbb &r{RANK}" +ranks: + # an empty string disables the header/footer + header: '' + footer: '' + +not-in-ladder: "&cSorry, but we could not find any rankups for the group(s) you are in." \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..9d46051 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,41 @@ +name: Rankup +version: 3.0-alpha +main: sh.okx.rankup.Rankup +author: Okx +depend: [Vault] +softdepend: [PlaceholderAPI] +api-version: 1.13 + +commands: + rankup: + permission: rankup.rankup + description: Rankup. + rankup3: + permission: rankup3.info + description: View Rankup version and perform some administrative commands. + # support the old command + aliases: [pru] + ranks: + permission: rankup.ranks + description: List all the ranks. +permissions: + rankup.*: + children: + rankup.info: true + rankup.rankup: true + rankup.checkversion: true + rankup.ranks: true + rankup.reload: true + rankup.ranks: true + rankup.info: + default: true + rankup.rankup: + default: true + rankup.checkversion: + default: op + rankup.ranks: + default: true + rankup.reload: + default: op + rankup.ranks: + default: true \ No newline at end of file diff --git a/src/main/resources/rankups.yml b/src/main/resources/rankups.yml new file mode 100644 index 0000000..42be2cc --- /dev/null +++ b/src/main/resources/rankups.yml @@ -0,0 +1,49 @@ +Aexample: + # the name of the rank in your permissions plugin + rank: 'A' + # the next rank a player can rank up to. + # this must be the name of the configuration section. + # for example, the name of this configuration section is "Aexample". + # this is not required. + next: 'Bexample' + # List of requirements to go to the next rank + # (ie, this example will charge 1000 money to rankup from A to B) + # money: money from the server economy + # xp-level: amount of experience levels + # playtime-hours: hours a player has played + # custom requirements can also be added by other plugins. + requirements: + money: 1000 + # What requirements players need to match to /rankup. + # this is optional - if you don't use it, it defaults to "and" + # n.b. if there are no requirements players will always be able to /rankup. + # and: all requirements + # or: at least one requirement + # xor: only one requirement + # none: no requirements + operation: and + # the console will run these commands when a player ranks up + #commands: + # this will run when a player ranks up from A to B. + #- 'say {PLAYER} well done for ranking up from {OLD_RANK} to {RANK}!' +Bexample: + rank: 'B' + next: 'Cexample' + requirements: + money: 2500 +Cexample: + rank: 'C' + next: 'Dexample' + requirements: + money: 5000 + xp-level: 2 + # you can have a custom messages too. + # you can use this to list the requirements needed. + rankup: + requirements-not-met: '&cYou need 5000 money and 2 levels of XP to rankup to D.' + ranks: + complete: "&7{OLD_RANK} &8\xbb &7{RANK} &e(5000 money, 2 XP levels)" + current: "&c{OLD_RANK} &e\xbb &c{RANK} &e(5000 money, 2 XP levels)" + incomplete: "&r{OLD_RANK} &e\xbb &r{RANK} &e(5000 money, 2 XP levels)" +Dexample: + rank: 'D' \ No newline at end of file diff --git a/src/test/java/sh/okx/rankup/RankupTest.java b/src/test/java/sh/okx/rankup/RankupTest.java new file mode 100644 index 0000000..8d88f18 --- /dev/null +++ b/src/test/java/sh/okx/rankup/RankupTest.java @@ -0,0 +1,9 @@ +package sh.okx.rankup; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class RankupTest { +} \ No newline at end of file