diff --git a/src/main/java/com/ceraia/Ceraia.kt b/src/main/java/com/ceraia/Ceraia.kt index 9ac9586..afe1c8d 100644 --- a/src/main/java/com/ceraia/Ceraia.kt +++ b/src/main/java/com/ceraia/Ceraia.kt @@ -1,27 +1,32 @@ package com.ceraia -import com.ceraia.managers.PlayerManager +import com.ceraia.modules.ceraia.managers.PlayerManager +import com.ceraia.metrics.Metrics import com.ceraia.modules.* import com.ceraia.modules.arenas.ArenaModule -import com.ceraia.modules.races.ModuleRaces -import com.ceraia.modules.system.ModuleSystem +import com.ceraia.modules.RaceModule +import com.ceraia.modules.SystemModule import com.ceraia.util.ConfigHelper import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import java.io.File +import java.util.UUID class Ceraia : JavaPlugin() { private val plugin: Ceraia = this + + var metrics: Metrics? = null + lateinit var playerManager: PlayerManager private set - lateinit var moduleSeating: ModuleSeating + lateinit var seatingModule: SeatingModule private set - lateinit var moduleMarriage: ModuleMarriage + lateinit var marriageModule: MarriageModule private set - lateinit var moduleSystem: ModuleSystem + lateinit var systemModule: SystemModule private set - lateinit var moduleRaces: ModuleRaces + lateinit var raceModule: RaceModule private set lateinit var configHelper: ConfigHelper private set @@ -29,10 +34,11 @@ class Ceraia : JavaPlugin() { private set override fun onEnable() { + metrics = Metrics(this, 20303) + saveDefaultConfig() + File(dataFolder, "data").mkdirs() - File(dataFolder, "data/arenas").mkdirs() - File(dataFolder, "data/items").mkdirs() File(dataFolder, "data/users").mkdirs() /*---------------------------------*/ @@ -43,10 +49,10 @@ class Ceraia : JavaPlugin() { /*---------------------------------*/ /* Modules */ /*---------------------------------*/ - moduleSeating = ModuleSeating(plugin) - moduleMarriage = ModuleMarriage(plugin) - moduleSystem = ModuleSystem(plugin) - moduleRaces = ModuleRaces(plugin) + seatingModule = SeatingModule(plugin) + marriageModule = MarriageModule(plugin) + systemModule = SystemModule(plugin) + raceModule = RaceModule(plugin) arenaModule = ArenaModule(plugin) /*---------------------------------*/ @@ -57,6 +63,7 @@ class Ceraia : JavaPlugin() { override fun onDisable() { playerManager.savePlayers() + metrics?.shutdown() } fun noPermission(player: Player) { diff --git a/src/main/java/com/ceraia/metrics/Metrics.java b/src/main/java/com/ceraia/metrics/Metrics.java new file mode 100644 index 0000000..1629719 --- /dev/null +++ b/src/main/java/com/ceraia/metrics/Metrics.java @@ -0,0 +1,871 @@ +/* + * This Metrics class was auto-generated and can be copied into your project if you are + * not using a build tool like Gradle or Maven for dependency management. + * + * IMPORTANT: You are not allowed to modify this class, except changing the package. + * + * Disallowed modifications include but are not limited to: + * - Remove the option for users to opt-out + * - Change the frequency for data submission + * - Obfuscate the code (every obfuscator should allow you to make an exception for specific files) + * - Reformat the code (if you use a linter, add an exception) + * + * Violations will result in a ban of your plugin and account from bStats. + */ +package com.ceraia.metrics; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import javax.net.ssl.HttpsURLConnection; +import java.io.*; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; + +public class Metrics { + + private final Plugin plugin; + + private final MetricsBase metricsBase; + + /** + * Creates a new Metrics instance. + * + * @param plugin Your plugin instance. + * @param serviceId The id of the service. It can be found at What is my plugin id? + */ + public Metrics(JavaPlugin plugin, int serviceId) { + 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); + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true); + config.addDefault("serverUuid", UUID.randomUUID().toString()); + config.addDefault("logFailedRequests", false); + config.addDefault("logSentData", false); + config.addDefault("logResponseStatusText", false); + // Inform the server owners about bStats + config + .options() + .header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous.") + .copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { + } + } + // Load the data + boolean enabled = config.getBoolean("enabled", true); + String serverUUID = config.getString("serverUuid"); + boolean logErrors = config.getBoolean("logFailedRequests", false); + boolean logSentData = config.getBoolean("logSentData", false); + boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); + metricsBase = + new MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + this::appendPlatformData, + this::appendServiceData, + submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), + plugin::isEnabled, + (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), + (message) -> this.plugin.getLogger().log(Level.INFO, message), + logErrors, + logSentData, + logResponseStatusText); + } + + /** Shuts down the underlying scheduler service. */ + public void shutdown() { + metricsBase.shutdown(); + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + metricsBase.addCustomChart(chart); + } + + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("playerAmount", getPlayerAmount()); + builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); + builder.appendField("bukkitVersion", Bukkit.getVersion()); + builder.appendField("bukkitName", Bukkit.getName()); + builder.appendField("javaVersion", System.getProperty("java.version")); + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + + private void appendServiceData(JsonObjectBuilder builder) { + builder.appendField("pluginVersion", plugin.getDescription().getVersion()); + } + + private int getPlayerAmount() { + try { + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + return onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + // Just use the new method if the reflection failed + return Bukkit.getOnlinePlayers().size(); + } + } + + public static class MetricsBase { + + /** The version of the Metrics class. */ + public static final String METRICS_VERSION = "3.0.2"; + + private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; + + private final ScheduledExecutorService scheduler; + + private final String platform; + + private final String serverUuid; + + private final int serviceId; + + private final Consumer appendPlatformDataConsumer; + + private final Consumer appendServiceDataConsumer; + + private final Consumer submitTaskConsumer; + + private final Supplier checkServiceEnabledSupplier; + + private final BiConsumer errorLogger; + + private final Consumer infoLogger; + + private final boolean logErrors; + + private final boolean logSentData; + + private final boolean logResponseStatusText; + + private final Set customCharts = new HashSet<>(); + + private final boolean enabled; + + /** + * Creates a new MetricsBase class instance. + * + * @param platform The platform of the service. + * @param serviceId The id of the service. + * @param serverUuid The server uuid. + * @param enabled Whether or not data sending is enabled. + * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all platform-specific data. + * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all service-specific data. + * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be + * used to delegate the data collection to a another thread to prevent errors caused by + * concurrency. Can be {@code null}. + * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. + * @param errorLogger A consumer that accepts log message and an error. + * @param infoLogger A consumer that accepts info log messages. + * @param logErrors Whether or not errors should be logged. + * @param logSentData Whether or not the sent data should be logged. + * @param logResponseStatusText Whether or not the response status text should be logged. + */ + public MetricsBase( + String platform, + String serverUuid, + int serviceId, + boolean enabled, + Consumer appendPlatformDataConsumer, + Consumer appendServiceDataConsumer, + Consumer submitTaskConsumer, + Supplier checkServiceEnabledSupplier, + BiConsumer errorLogger, + Consumer infoLogger, + boolean logErrors, + boolean logSentData, + boolean logResponseStatusText) { + ScheduledThreadPoolExecutor scheduler = + new ScheduledThreadPoolExecutor(1, task -> new Thread(task, "bStats-Metrics")); + // We want delayed tasks (non-periodic) that will execute in the future to be + // cancelled when the scheduler is shutdown. + // Otherwise, we risk preventing the server from shutting down even when + // MetricsBase#shutdown() is called + scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.scheduler = scheduler; + this.platform = platform; + this.serverUuid = serverUuid; + this.serviceId = serviceId; + this.enabled = enabled; + this.appendPlatformDataConsumer = appendPlatformDataConsumer; + this.appendServiceDataConsumer = appendServiceDataConsumer; + this.submitTaskConsumer = submitTaskConsumer; + this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; + this.errorLogger = errorLogger; + this.infoLogger = infoLogger; + this.logErrors = logErrors; + this.logSentData = logSentData; + this.logResponseStatusText = logResponseStatusText; + checkRelocation(); + if (enabled) { + // WARNING: Removing the option to opt-out will get your plugin banned from + // bStats + startSubmitting(); + } + } + + public void addCustomChart(CustomChart chart) { + this.customCharts.add(chart); + } + + public void shutdown() { + scheduler.shutdown(); + } + + private void startSubmitting() { + final Runnable submitTask = + () -> { + if (!enabled || !checkServiceEnabledSupplier.get()) { + // Submitting data or service is disabled + scheduler.shutdown(); + return; + } + if (submitTaskConsumer != null) { + submitTaskConsumer.accept(this::submitData); + } else { + this.submitData(); + } + }; + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven + // distribution of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into + // the initial and second delay. + // WARNING: You must not modify and part of this Metrics class, including the + // submit delay or frequency! + // WARNING: Modifying this code will get your plugin banned on bStats. Just + // don't do it! + long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); + long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); + scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate( + submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); + } + + private void submitData() { + final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); + appendPlatformDataConsumer.accept(baseJsonBuilder); + final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); + appendServiceDataConsumer.accept(serviceJsonBuilder); + JsonObjectBuilder.JsonObject[] chartData = + customCharts.stream() + .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) + .filter(Objects::nonNull) + .toArray(JsonObjectBuilder.JsonObject[]::new); + serviceJsonBuilder.appendField("id", serviceId); + serviceJsonBuilder.appendField("customCharts", chartData); + baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); + baseJsonBuilder.appendField("serverUUID", serverUuid); + baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); + JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); + scheduler.execute( + () -> { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (logErrors) { + errorLogger.accept("Could not submit bStats metrics data", e); + } + } + }); + } + + private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { + if (logSentData) { + infoLogger.accept("Sent bStats metrics data: " + data.toString()); + } + String url = String.format(REPORT_URL, platform); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", "Metrics-Service/1"); + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(compressedData); + } + StringBuilder builder = new StringBuilder(); + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + } + if (logResponseStatusText) { + infoLogger.accept("Sent data to bStats and received response: " + builder); + } + } + + /** Checks that the class was properly relocated. */ + private void checkRelocation() { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null + || !System.getProperty("bstats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this + // little "trick" ... :D + final String defaultPackage = + new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); + final String examplePackage = + new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure no one just copy & pastes the example and uses the wrong + // package names + if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) + || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + /** + * Gzips the given string. + * + * @param str The string to gzip. + * @return The gzipped string. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + } + return outputStream.toByteArray(); + } + } + + public static class SimplePie extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimplePie(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + public static class MultiLineChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public MultiLineChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedPie extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedPie(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class SimpleBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimpleBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().length == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class DrilldownPie extends CustomChart { + + private final Callable>> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public DrilldownPie(String chartId, Callable>> callable) { + super(chartId); + this.callable = callable; + } + + @Override + public JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public abstract static class CustomChart { + + private final String chartId; + + protected CustomChart(String chartId) { + if (chartId == null) { + throw new IllegalArgumentException("chartId must not be null"); + } + this.chartId = chartId; + } + + public JsonObjectBuilder.JsonObject getRequestJsonObject( + BiConsumer errorLogger, boolean logErrors) { + JsonObjectBuilder builder = new JsonObjectBuilder(); + builder.appendField("chartId", chartId); + try { + JsonObjectBuilder.JsonObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + builder.appendField("data", data); + } catch (Throwable t) { + if (logErrors) { + errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return builder.build(); + } + + protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; + } + + public static class SingleLineChart extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SingleLineChart(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + /** + * An extremely simple JSON builder. + * + *

While this class is neither feature-rich nor the most performant one, it's sufficient enough + * for its use-case. + */ + public static class JsonObjectBuilder { + + private StringBuilder builder = new StringBuilder(); + + private boolean hasAtLeastOneField = false; + + public JsonObjectBuilder() { + builder.append("{"); + } + + /** + * Appends a null field to the JSON. + * + * @param key The key of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendNull(String key) { + appendFieldUnescaped(key, "null"); + return this; + } + + /** + * Appends a string field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String value) { + if (value == null) { + throw new IllegalArgumentException("JSON value must not be null"); + } + appendFieldUnescaped(key, "\"" + escape(value) + "\""); + return this; + } + + /** + * Appends an integer field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int value) { + appendFieldUnescaped(key, String.valueOf(value)); + return this; + } + + /** + * Appends an object to the JSON. + * + * @param key The key of the field. + * @param object The object. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject object) { + if (object == null) { + throw new IllegalArgumentException("JSON object must not be null"); + } + appendFieldUnescaped(key, object.toString()); + return this; + } + + /** + * Appends a string array to the JSON. + * + * @param key The key of the field. + * @param values The string array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values) + .map(value -> "\"" + escape(value) + "\"") + .collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an integer array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an object array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends a field to the object. + * + * @param key The key of the field. + * @param escapedValue The escaped value of the field. + */ + private void appendFieldUnescaped(String key, String escapedValue) { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + if (key == null) { + throw new IllegalArgumentException("JSON key must not be null"); + } + if (hasAtLeastOneField) { + builder.append(","); + } + builder.append("\"").append(escape(key)).append("\":").append(escapedValue); + hasAtLeastOneField = true; + } + + /** + * Builds the JSON string and invalidates this builder. + * + * @return The built JSON string. + */ + public JsonObject build() { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + JsonObject object = new JsonObject(builder.append("}").toString()); + builder = null; + return object; + } + + /** + * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. + * + *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. + * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). + * + * @param value The value to escape. + * @return The escaped value. + */ + private static String escape(String value) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"') { + builder.append("\\\""); + } else if (c == '\\') { + builder.append("\\\\"); + } else if (c <= '\u000F') { + builder.append("\\u000").append(Integer.toHexString(c)); + } else if (c <= '\u001F') { + builder.append("\\u00").append(Integer.toHexString(c)); + } else { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * A super simple representation of a JSON object. + * + *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not + * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, + * JsonObject)}. + */ + public static class JsonObject { + + private final String value; + + private JsonObject(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + } +} diff --git a/src/main/java/com/ceraia/modules/ModuleMarriage.kt b/src/main/java/com/ceraia/modules/MarriageModule.kt similarity index 61% rename from src/main/java/com/ceraia/modules/ModuleMarriage.kt rename to src/main/java/com/ceraia/modules/MarriageModule.kt index 6121c03..70126f7 100644 --- a/src/main/java/com/ceraia/modules/ModuleMarriage.kt +++ b/src/main/java/com/ceraia/modules/MarriageModule.kt @@ -15,24 +15,27 @@ import org.bukkit.event.Listener import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.util.StringUtil +import kotlin.math.cos +import kotlin.math.sin -class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter, Listener { +class MarriageModule(private val plugin: Ceraia) : CommandExecutor, TabCompleter, Listener { - private val invites: MutableMap = mutableMapOf() + private val proposals: MutableMap = mutableMapOf() + // First is parent of the request, second is the child + private val adoptionRequests: MutableMap = mutableMapOf() init { plugin.getCommand("marry")?.setExecutor(this) plugin.getCommand("divorce")?.setExecutor(this) plugin.getCommand("marry")?.tabCompleter = this - Bukkit.getPluginManager().registerEvents(this, plugin) } override fun onCommand(sender: CommandSender, cmd: Command, label: String, args: Array): Boolean { if (sender !is Player) return true - if (!sender.hasPermission("double.marry")) { + if (!sender.hasPermission("ceraia.marry")) { plugin.noPermission(sender) return true } @@ -51,13 +54,63 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter return true } - invite(sender, target) + propose(sender, target) + } + "adopt" -> { + if (args.isEmpty() || args.size < 2 || (args[0] != "parent" && args[0] != "kid")) { + sender.sendMessage(MiniMessage.miniMessage().deserialize("Usage: /adopt ")) + return true + } + + val target = plugin.server.getPlayer(args[1]) + + if (target == null) { + sender.sendMessage(MiniMessage.miniMessage().deserialize("Player not found")) + return true + } + + if(args[0] == "parent") adopt(sender, target) else adopt(target, sender) } } return true } + private fun adopt(parent: Player, child: Player) { + if (adoptionRequests.containsKey(parent)) { + if (adoptionRequests[parent] == child) { + acceptAdoption(parent, child) + return + } + } + + adoptionRequests[child] = parent + plugin.server.sendMessage(MiniMessage.miniMessage().deserialize( + "${parent.name} has invited ${child.name} to adopt them!" + )) + child.sendMessage(MiniMessage.miniMessage().deserialize( + "${parent.name} has invited you to adopt them! Click [here] to accept." + )) + } + + private fun acceptAdoption(parent: Player, child: Player) { + val parentCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(parent.uniqueId) + val childCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(child.uniqueId) + + if (adoptionRequests[child] != parent) { + return + } + + plugin.server.sendMessage(MiniMessage.miniMessage().deserialize( + "${child.name} has accepted ${parent.name}'s adoption proposal!" + )) + + childCeraiaPlayer.addParent(parent.name) + parentCeraiaPlayer.addChild(child.name) + + adoptionRequests.remove(child) + } + override fun onTabComplete(sender: CommandSender, cmd: Command, label: String, args: Array): List { // Return all online players except the sender val tabOptions = Bukkit.getServer().onlinePlayers @@ -71,7 +124,7 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter return returnedOptions } - fun invite(sender: Player, target: Player) { + private fun propose(sender: Player, target: Player) { val senderCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(sender.uniqueId) val targetCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(target.uniqueId) @@ -84,14 +137,14 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter return } - if (invites.containsKey(sender)) { - if (invites[sender] == target) { - accept(sender, target) + if (proposals.containsKey(sender)) { + if (proposals[sender] == target) { + acceptProposal(sender, target) return } } - invites[target] = sender + proposals[target] = sender plugin.server.sendMessage(MiniMessage.miniMessage().deserialize( "${sender.name} has invited ${target.name} to marry them!" )) @@ -100,7 +153,7 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter )) } - fun accept(target: Player, sender: Player) { + private fun acceptProposal(target: Player, sender: Player) { val senderCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(sender.uniqueId) val targetCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(target.uniqueId) @@ -114,7 +167,7 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter target.sendMessage(MiniMessage.miniMessage().deserialize("You are already married!")) return } - if (invites[target] != sender) { + if (proposals[target] != sender) { return } @@ -124,45 +177,24 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter targetCeraiaPlayer.marry(sender.name) senderCeraiaPlayer.marry(target.name) - invites.remove(target) - } - - fun decline(target: Player, sender: Player): Int { - val senderCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(sender.uniqueId) - val targetCeraiaPlayer = plugin.playerManager.getCeraiaPlayer(target.uniqueId) - - if (senderCeraiaPlayer.isMarried()) { - return 1 - } - if (targetCeraiaPlayer.isMarried()) { - return 2 - } - if (invites[target] != sender) { - return 3 - } - - plugin.server.sendMessage(MiniMessage.miniMessage().deserialize( - "${target.name} has declined ${sender.name}'s marriage proposal!" - )) - invites.remove(target) - return 4 + proposals.remove(target) } - fun divorce(player: Player) { - val doublePlayer = plugin.playerManager.getCeraiaPlayer(player.uniqueId) - val doublePartner = plugin.playerManager.getCeraiaPlayer(doublePlayer.getPartner() ?: return) + private fun divorce(player: Player) { + val ceraiaPlayer = plugin.playerManager.getCeraiaPlayer(player.uniqueId) + val ceraiaPartner = plugin.playerManager.getCeraiaPlayer(ceraiaPlayer.getPartner() ?: return) - doublePlayer.divorce() - doublePartner.divorce() + ceraiaPlayer.divorce() + ceraiaPartner.divorce() plugin.server.sendMessage(MiniMessage.miniMessage().deserialize( - "${player.name} has divorced ${doublePartner.name}." + "${player.name} has divorced ${ceraiaPartner.name}." )) } @EventHandler fun onPlayerQuit(event: PlayerQuitEvent) { - invites.remove(event.player) + proposals.remove(event.player) } @EventHandler @@ -170,8 +202,8 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter val rightClicked = event.rightClicked as? Player ?: return if (event.player.uniqueId == rightClicked.uniqueId || !event.player.isSneaking) return - val doublePlayer = plugin.playerManager.getCeraiaPlayer(event.player.uniqueId) - if (doublePlayer.isMarried() && rightClicked.name == doublePlayer.getMarriedName()) { + val ceraiaPlayer = plugin.playerManager.getCeraiaPlayer(event.player.uniqueId) + if (ceraiaPlayer.isMarried() && rightClicked.name == ceraiaPlayer.getMarriedName()) { // Spawn a bunch of hearts spawnHeartsAroundPlayer(rightClicked) spawnHeartsAroundPlayer(event.player) @@ -186,16 +218,12 @@ class ModuleMarriage(private val plugin: Ceraia) : CommandExecutor, TabCompleter repeat(heartsToSpawn) { val angle = Math.random() * Math.PI * 2 val radius = 0.5 - val x = playerLocation.x + Math.cos(angle) * radius + val x = playerLocation.x + cos(angle) * radius val y = playerLocation.y + (Math.random() * 0.3) + 1.5 - val z = playerLocation.z + Math.sin(angle) * radius + val z = playerLocation.z + sin(angle) * radius val particleLocation = Location(world, x, y, z) world.spawnParticle(Particle.HEART, particleLocation, 1) } } - - fun getRequests(player: Player): Collection { - return invites.filter { it.value == player }.keys.map { it.name } - } } diff --git a/src/main/java/com/ceraia/modules/races/ModuleRaces.kt b/src/main/java/com/ceraia/modules/RaceModule.kt similarity index 98% rename from src/main/java/com/ceraia/modules/races/ModuleRaces.kt rename to src/main/java/com/ceraia/modules/RaceModule.kt index d216883..5f3c013 100644 --- a/src/main/java/com/ceraia/modules/races/ModuleRaces.kt +++ b/src/main/java/com/ceraia/modules/RaceModule.kt @@ -1,4 +1,4 @@ -package com.ceraia.modules.races +package com.ceraia.modules import dev.triumphteam.gui.builder.item.ItemBuilder import dev.triumphteam.gui.guis.Gui @@ -21,7 +21,7 @@ import org.bukkit.util.StringUtil import java.io.File import java.util.* -class ModuleRaces(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabCompleter, Listener { +class RaceModule(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabCompleter, Listener { private var races: MutableList = ArrayList() private var raceFactions: MutableList = ArrayList() @@ -52,7 +52,7 @@ class ModuleRaces(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabC when (args[0]) { "reload" -> { - if (!sender.hasPermission("double.races.reload")) { + if (!sender.hasPermission("ceraia.races.reload")) { plugin.noPermission(sender) return true } @@ -62,7 +62,7 @@ class ModuleRaces(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabC } "become" -> { - if (!sender.hasPermission("double.races.become")) { + if (!sender.hasPermission("ceraia.races.become")) { plugin.noPermission(sender) return true } @@ -89,8 +89,8 @@ class ModuleRaces(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabC sender.sendMessage(MiniMessage.miniMessage().deserialize("Race not found!")) return true } else { - if (!sender.hasPermission("double.races.become." + args[1]) && - !sender.hasPermission("double.races.become.*") + if (!sender.hasPermission("ceraia.races.become." + args[1]) && + !sender.hasPermission("ceraia.races.become.*") ) { sender.sendMessage( MiniMessage.miniMessage().deserialize("You do not have permission to become this race") @@ -109,7 +109,7 @@ class ModuleRaces(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabC "gui" -> openFactionGui(sender) "restore" -> { - if (!sender.hasPermission("double.races.restore")) { + if (!sender.hasPermission("ceraia.races.restore")) { plugin.noPermission(sender) return true } @@ -543,8 +543,8 @@ class ModuleRaces(private val plugin: com.ceraia.Ceraia) : CommandExecutor, TabC val selectable: MutableList = ArrayList() for (race in races) { - if ((player.hasPermission("double.races.become." + race.name) || - player.hasPermission("double.races.become.*")) && + if ((player.hasPermission("ceraia.races.become." + race.name) || + player.hasPermission("ceraia.races.become.*")) && (faction.getRaceInhabitants().contains(race.name) || faction.getRaceInhabitants().contains("*")) ) selectable.add(race) diff --git a/src/main/java/com/ceraia/modules/ModuleSeating.kt b/src/main/java/com/ceraia/modules/SeatingModule.kt similarity index 97% rename from src/main/java/com/ceraia/modules/ModuleSeating.kt rename to src/main/java/com/ceraia/modules/SeatingModule.kt index 1149116..fac3582 100644 --- a/src/main/java/com/ceraia/modules/ModuleSeating.kt +++ b/src/main/java/com/ceraia/modules/SeatingModule.kt @@ -18,7 +18,7 @@ import org.bukkit.event.Listener import org.bukkit.event.player.PlayerInteractEvent import org.jetbrains.annotations.NotNull -class ModuleSeating(private val plugin: Ceraia) : CommandExecutor, TabCompleter, Listener { +class SeatingModule(private val plugin: Ceraia) : CommandExecutor, TabCompleter, Listener { val chairs: MutableList = mutableListOf() @@ -76,7 +76,7 @@ class ModuleSeating(private val plugin: Ceraia) : CommandExecutor, TabCompleter, if (sender !is Player) { return true } - if (!sender.hasPermission("double.sit")) { + if (!sender.hasPermission("ceraia.sit")) { plugin.noPermission(sender) return true } diff --git a/src/main/java/com/ceraia/modules/system/ModuleSystem.kt b/src/main/java/com/ceraia/modules/SystemModule.kt similarity index 81% rename from src/main/java/com/ceraia/modules/system/ModuleSystem.kt rename to src/main/java/com/ceraia/modules/SystemModule.kt index df0e936..b051ad2 100644 --- a/src/main/java/com/ceraia/modules/system/ModuleSystem.kt +++ b/src/main/java/com/ceraia/modules/SystemModule.kt @@ -1,4 +1,4 @@ -package com.ceraia.modules.system +package com.ceraia.modules import com.ceraia.Ceraia import net.kyori.adventure.text.minimessage.MiniMessage @@ -11,7 +11,7 @@ import org.bukkit.entity.Player import org.bukkit.event.Listener import org.bukkit.util.StringUtil -class ModuleSystem(private val plugin: Ceraia) : CommandExecutor, TabCompleter, Listener { +class SystemModule(private val plugin: Ceraia) : CommandExecutor, TabCompleter, Listener { init { // Register the commands @@ -39,14 +39,14 @@ class ModuleSystem(private val plugin: Ceraia) : CommandExecutor, TabCompleter, "version" -> { sender.sendMessage( MiniMessage.miniMessage().deserialize( - "Running Ceraia v${plugin.pluginMeta.version} by Axodouble" + "Running Ceraia v${plugin.pluginMeta.version} by Axoceraia" ) ) return true } "day", "noon" -> { - if (!sender.hasPermission("double.time.day") && - !sender.hasPermission("double.time.*") + if (!sender.hasPermission("ceraia.time.day") && + !sender.hasPermission("ceraia.time.*") ) { plugin.noPermission(sender as Player) return true @@ -60,8 +60,8 @@ class ModuleSystem(private val plugin: Ceraia) : CommandExecutor, TabCompleter, return true } "night" -> { - if (!sender.hasPermission("double.time.night") && - !sender.hasPermission("double.time.*") + if (!sender.hasPermission("ceraia.time.night") && + !sender.hasPermission("ceraia.time.*") ) { plugin.noPermission(sender as Player) return true @@ -72,7 +72,7 @@ class ModuleSystem(private val plugin: Ceraia) : CommandExecutor, TabCompleter, return true } "discord" -> { - if (!sender.hasPermission("double.discord")) { + if (!sender.hasPermission("ceraia.discord")) { plugin.noPermission(sender as Player) return true } @@ -104,7 +104,7 @@ class ModuleSystem(private val plugin: Ceraia) : CommandExecutor, TabCompleter, } private fun jump(player: Player) { - if (!player.hasPermission("double.jump")) { + if (!player.hasPermission("ceraia.jump")) { plugin.noPermission(player) return } @@ -126,18 +126,4 @@ class ModuleSystem(private val plugin: Ceraia) : CommandExecutor, TabCompleter, } } } - - private fun modHelp(sender: CommandSender) { - sender.sendMessage( - MiniMessage.miniMessage().deserialize( - """ - Mod Help - /mod ban pvp - /mod ban arenas - /mod remove pvp - /mod remove arenas - """.trimIndent() - ) - ) - } } \ No newline at end of file diff --git a/src/main/java/com/ceraia/modules/arenas/Arena.kt b/src/main/java/com/ceraia/modules/arenas/Arena.kt deleted file mode 100644 index 6fb9b73..0000000 --- a/src/main/java/com/ceraia/modules/arenas/Arena.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.ceraia.modules.arenas - -import com.ceraia.Ceraia -import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.World -import org.bukkit.entity.Player -import java.util.UUID - -class Arena private constructor( - private val plugin: Ceraia, - private val name: String, - private val owner: UUID, - private val breaking: Boolean, - private val spawnpoints: List, -) { - private val players: MutableMap = mutableMapOf() - private val placedBlocks: MutableList = mutableListOf() - private val brokenBlocks: MutableList = mutableListOf() - private val state: ArenaState = ArenaState.READY - private val world: World = spawnpoints[0].world - - fun getPlugin(): Ceraia { return this.plugin } - - fun getName(): String { return this.name } - - fun getOwner(): UUID { return this.owner } - - fun getSpawnpoints(): List { return this.spawnpoints } - - fun breakingAllowed(): Boolean { return this.breaking } - - fun getPlayers(): MutableMap { return this.players } - - fun getPlacedBlocks(): MutableList { return this.placedBlocks } - - fun getBrokenBlocks(): MutableList { return this.brokenBlocks } - - fun getState(): ArenaState { return this.state } - - fun addPlayer(player: Player, location: Location) { - this.players[player] = location - } - - fun removePlayer(player: Player) { - this.players.remove(player) - } - - fun addPlacedBlock(location: Location) { - this.placedBlocks.add(location) - } - - fun removePlacedBlocks() { - this.placedBlocks.forEach { - block -> block.block.type = Material.AIR - } - this.placedBlocks.clear() - } - - class Builder { - private lateinit var plugin: Ceraia - private lateinit var name: String - private lateinit var owner: UUID - private var breaking: Boolean = false // Default to false - private lateinit var spawnpoints: List - - fun plugin(plugin: Ceraia) = apply { this.plugin = plugin } - fun name(name: String) = apply { this.name = name } - fun owner(owner: UUID) = apply { this.owner = owner } - fun breaking(breaking: Boolean) = apply { this.breaking = breaking } - fun spawnpoints(spawnpoints: List) = apply { this.spawnpoints = spawnpoints } - - fun loadBuilder(): Arena { - return Arena(plugin, name, owner, breaking, spawnpoints) - } - } - - companion object { - fun builder() = Builder() - } - - enum class ArenaState { - READY, - STARTING, - INGAME, - ENDING, - ENDLESS - } -} \ No newline at end of file diff --git a/src/main/java/com/ceraia/modules/arenas/ArenaCommands.kt b/src/main/java/com/ceraia/modules/arenas/ArenaCommands.kt deleted file mode 100644 index d7286d6..0000000 --- a/src/main/java/com/ceraia/modules/arenas/ArenaCommands.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.ceraia.modules.arenas - -import com.ceraia.Ceraia -import com.mojang.brigadier.CommandDispatcher -import com.mojang.brigadier.arguments.StringArgumentType -import com.mojang.brigadier.builder.LiteralArgumentBuilder -import com.mojang.brigadier.builder.RequiredArgumentBuilder -import com.mojang.brigadier.context.CommandContext -import com.mojang.brigadier.exceptions.CommandSyntaxException -import org.bukkit.command.Command -import org.bukkit.command.CommandSender -import org.bukkit.event.Listener - -class ArenaCommands(plugin: Ceraia) : Listener { - private val plugin: Ceraia = plugin - private val dispatcher: CommandDispatcher = CommandDispatcher() - - init { - registerCommands() - } - - private fun registerCommands() { - dispatcher.register( - LiteralArgumentBuilder.literal("arena") - .then( - LiteralArgumentBuilder.literal("create") - .then( - RequiredArgumentBuilder.argument("name", StringArgumentType.string()) - .executes { context: CommandContext -> createArena(context) } - ) - ) - .then( - LiteralArgumentBuilder.literal("delete") - .then( - RequiredArgumentBuilder.argument("name", StringArgumentType.string()) - .executes { context: CommandContext -> deleteArena(context) } - ) - ) - ) - } - - private fun createArena(context: CommandContext): Int { - val sender = context.source - val name = StringArgumentType.getString(context, "name") - - sender.sendMessage("Arena $name created.") - return 1 - } - - private fun deleteArena(context: CommandContext): Int { - val sender = context.source - val name = StringArgumentType.getString(context, "name") - - sender.sendMessage("Arena $name deleted.") - return 1 - } - - fun onCommand(sender: CommandSender, command: Command, label: String, args: Array?): Boolean { - try { - dispatcher.execute(command.name + " " + args?.joinToString(" "), sender) - } catch (e: CommandSyntaxException) { - sender.sendMessage("Invalid command syntax.") - } - return true - } - - fun onTabComplete(sender: CommandSender, command: Command, alias: String, args: Array?): MutableList? { - - return null - } -} \ No newline at end of file diff --git a/src/main/java/com/ceraia/modules/arenas/ArenaModule.kt b/src/main/java/com/ceraia/modules/arenas/ArenaModule.kt index b76e157..67896a5 100644 --- a/src/main/java/com/ceraia/modules/arenas/ArenaModule.kt +++ b/src/main/java/com/ceraia/modules/arenas/ArenaModule.kt @@ -1,11 +1,79 @@ package com.ceraia.modules.arenas import com.ceraia.Ceraia +import com.ceraia.metrics.Metrics +import com.ceraia.modules.arenas.commands.arena.* +import com.ceraia.modules.arenas.commands.system.CommandMod +import com.ceraia.modules.arenas.commands.system.CommandVersion +import com.ceraia.modules.arenas.listeners.* +import com.ceraia.modules.arenas.managers.ArenaManager +import com.ceraia.modules.arenas.managers.EloScoreboardManager +import com.ceraia.modules.arenas.managers.InviteManager +import com.ceraia.modules.arenas.types.ArenaSelectGUI +import com.ceraia.modules.ceraia.managers.PlayerManager +import java.io.File +import java.util.* class ArenaModule(private val plugin: Ceraia) { + var arenaManager: ArenaManager? = null + private set + var inviteManager: InviteManager? = null + private set + var arenaSelectGUI: ArenaSelectGUI? = null + private set + var groupManager: CommandGVG? = null + private set + var playerManager: PlayerManager? = null + private set + var metrics: Metrics? = null + private var eloScoreBoardManager: EloScoreboardManager? = null init { - val arenaCommands = ArenaCommands(plugin) - plugin.getCommand("arena")?.setExecutor(arenaCommands::onCommand) - plugin.getCommand("arena")?.setTabCompleter(arenaCommands::onTabComplete) + File(plugin.dataFolder, "data/arena").mkdirs() + File(plugin.dataFolder, "data/arena/arenas").mkdirs() + File(plugin.dataFolder, "data/arena/items").mkdirs() + + // Managers + this.arenaManager = ArenaManager(plugin) + this.playerManager = PlayerManager(plugin) + this.eloScoreBoardManager = EloScoreboardManager(plugin) + this.inviteManager = InviteManager() + + this.arenaSelectGUI = ArenaSelectGUI(plugin) + this.groupManager = CommandGVG(plugin) + + // Command + val commandPVP = CommandPVP(plugin) + val commandArena = CommandArena(plugin) + val commandMod = CommandMod(plugin) + val commandTop = CommandTop(plugin) + val commandProfile = CommandProfile(plugin) + val commandVersion = CommandVersion(plugin) + + // Listeners + PlayerEloChangeListener(plugin) + ArenaFightListener(plugin) + PlayerInventoryListener(plugin) + ArenaBlockListener(plugin) + ArenaExplodeListener(plugin) + + // PvP Commands + Objects.requireNonNull(plugin.getCommand("pvp"))?.setExecutor(commandPVP) + Objects.requireNonNull(plugin.getCommand("arena"))?.setExecutor(commandArena) + Objects.requireNonNull(plugin.getCommand("gvg"))?.setExecutor(groupManager) + Objects.requireNonNull(plugin.getCommand("top"))?.setExecutor(commandTop) + Objects.requireNonNull(plugin.getCommand("leaderboard"))?.setExecutor(commandTop) + Objects.requireNonNull(plugin.getCommand("profile"))?.setExecutor(commandProfile) + Objects.requireNonNull(plugin.getCommand("stats"))?.setExecutor(commandProfile) + } + + fun calculateWinChance(player1: UUID, player2: UUID): Double { + val elo1 = playerManager?.getPlayer(player1)?.elo + val elo2 = playerManager?.getPlayer(player2)?.elo + + if ((elo1 == 0 || elo1 == null) || (elo2 == 0 || elo2 == null)) { + return 0.5 + } + + return 1 / (1 + Math.pow(10.0, (elo2 - elo1) / 400.0)) } } \ No newline at end of file diff --git a/src/main/java/com/ceraia/modules/arenas/Utils.kt b/src/main/java/com/ceraia/modules/arenas/Utils.kt new file mode 100644 index 0000000..0a254ce --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/Utils.kt @@ -0,0 +1,66 @@ +package com.ceraia.modules.arenas + +import com.ceraia.Ceraia +import com.ceraia.modules.arenas.types.Arena +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.configuration.file.FileConfiguration +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import java.io.File +import java.util.* + +object Utils { + @JvmStatic + fun revertInventory(plugin: Ceraia, pl: Player, arena: Arena) { + try { + val file = File(plugin.dataFolder, "data/pinventory_" + arena.name + "_" + pl.name + ".yml") + + val config: FileConfiguration = YamlConfiguration.loadConfiguration(file) + + val content = arrayOfNulls(pl.inventory.contents.size) + try { + for (s in Objects.requireNonNull(config.getConfigurationSection("items"))?.getKeys(false)!!) { + val i = s.toInt() + content[i] = config.getItemStack("items.$s") + } + } catch (e: Exception) { + println("Problem loading player inventories.") + } + + pl.inventory.contents = content + + file.delete() + } catch (e: Exception) { + e.printStackTrace() + println("Problem loading player inventory") + } + } + + @JvmStatic + fun teleportPlayerToSpawn(plugin: Ceraia, player: Player, arena: Arena) { + val useLocation = checkNotNull(plugin.config.getString("spawn_teleport.use")) + if (useLocation.equals("command", ignoreCase = true)) { + Objects.requireNonNull(plugin.config.getString("spawn_teleport.command"))?.let { + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + it + .replace("%player%", player.name) + ) + } + } else if (useLocation.equals("prior", ignoreCase = true)) { + val l = arena.getPlayerPriorLocation(player) + player.teleport(l) + } else { + val l = Location( + Objects.requireNonNull(plugin.config.getString("spawn_teleport.location.world")) + ?.let { Bukkit.getWorld(it) }, + plugin.config.getDouble("spawn_teleport.location.x"), + plugin.config.getDouble("spawn_teleport.location.y"), + plugin.config.getDouble("spawn_teleport.location.z") + ) + player.teleport(l) + } + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandArena.java b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandArena.java new file mode 100644 index 0000000..707ddc1 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandArena.java @@ -0,0 +1,329 @@ +package com.ceraia.modules.arenas.commands.arena; + +import com.ceraia.Ceraia; +import com.ceraia.modules.arenas.types.Arena; +import com.ceraia.modules.ceraia.types.CeraiaPlayer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public class CommandArena implements CommandExecutor, TabCompleter { + + private final Ceraia plugin; + + public CommandArena(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if(!sender.hasPermission("xdbl.arena")){ + sender.sendMessage(MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.no_permission"))); + return true; + } + + if (args.length == 0) { + arenaHelp(sender); + return true; + } + + if (args[0].equalsIgnoreCase("list")) { + arenaList(sender); + return true; + } + + if (args[0].equalsIgnoreCase("scoreboard") || args[0].equalsIgnoreCase("top")) { + Player p = (Player) sender; + + // Create and show a string list of the top 10 players with the highest elo + List top = new ArrayList<>(); + AtomicInteger i = new AtomicInteger(); + i.set(1); + + top.add(MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.scoreboard.top"))); + + plugin.getPlayerManager().getCeraiaPlayers().stream().sorted(Comparator.comparingInt(CeraiaPlayer::getElo).reversed()).limit(10).forEach(ap -> { + String playerName = Bukkit.getOfflinePlayer(ap.getUUID()).getName(); + int elo = ap.getElo(); + + String medal; // Default medal color for players outside the top 3 + + // Check for 1st, 2nd, and 3rd place + if (i.get() == 1) { + medal = ""; // Gold for 1st place + } else if (i.get() == 2) { + medal = "<#C0C0C0>"; // Silver for 2nd place + } else if (i.get() == 3) { + medal = "<#cd7f32>"; // Bronze for 3rd place + } else { + medal = ""; // Default medal color for players outside the top 3 + } + + top.add(MiniMessage.miniMessage().deserialize(medal + i + " " + playerName + " - " + elo + " ELO (" + (ap.getWins() + ap.getLosses()) + " games)")); + i.getAndIncrement(); + }); + + // Send the top 10 players with the highest elo to the player + top.forEach(p::sendMessage); + + return true; + } + + if (args[0].equalsIgnoreCase("delete")) { + arenaDelete(sender, args); + return true; + } + if (args[0].equalsIgnoreCase("create")) { + arenaCreate(sender, args); + return true; + } + if (args[0].equalsIgnoreCase("sp1")) { + arenaSP1(sender, args); + return true; + } + if (args[0].equalsIgnoreCase("sp2")) { + arenaSP2(sender, args); + return true; + } + if (args[0].equalsIgnoreCase("public")) { + arenaPublic(sender, args); + return true; + } + else { + badUsage(sender); + arenaHelp(sender); + return true; + } + } + + @Override + public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) { + List arenas = new ArrayList<>(); + + if (args.length == 1) { + return Arrays.asList("list", "delete", "public", "create", "sp1", "sp2", "top", "scoreboard"); + } else if (args.length == 2 && ( + args[0].equalsIgnoreCase("delete") || + args[0].equalsIgnoreCase("public") || + args[0].equalsIgnoreCase("sp1") || + args[0].equalsIgnoreCase("sp2"))) { + plugin.getArenaModule().getArenaManager().getArenas().forEach(a ->{ + if (a.getOwner().equals(sender.getName())) { + arenas.add(a.getName()); + } + }); + return arenas; + } else if (args.length == 2 && args[0].equalsIgnoreCase("create")) { + return Arrays.asList(""); + } else if ((args.length == 3 && args[0].equalsIgnoreCase("public"))) { + return Arrays.asList("true", "false"); + } + return new ArrayList<>(); + } + + private void arenaSP1(CommandSender sender, String[] args) { + if (args.length == 1) { + badUsage(sender); + return; + } + + String name = args[1]; + + Arena arena = plugin.getArenaModule().getArenaManager().getArena(name); + if (arena == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_found")) + ); + return; + } + if (!Objects.equals(arena.getOwner(), sender.getName())) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_yours")) + ); + return; + } + arena.setSpawnPoint1(((Player) sender).getLocation()); + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.sp1.success")) + ); + } + + private void arenaSP2(CommandSender sender, String[] args) { + if (args.length == 1) { + badUsage(sender); + return; + } + + String name = args[1]; + + Arena arena = plugin.getArenaModule().getArenaManager().getArena(name); + if (arena == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_found")) + ); + return; + } + + if (!Objects.equals(arena.getOwner(), sender.getName())) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_yours")) + ); + return; + } + arena.setSpawnPoint2(((Player) sender).getLocation()); + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.sp2.success")) + ); + } + + private void arenaCreate(CommandSender sender, String[] args) { + if (args.length == 1) { + badUsage(sender); + return; + } + + String name = args[1]; + + // Check if the string is the same as , if so state the user should put a name + if (name.equalsIgnoreCase("")) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.create.no_name")) + ); + return; + } + // Check if the string is alphanumeric + if (!name.matches("[a-zA-Z0-9]*")) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.create.alphanumeric")) + ); + return; + } + + // Check if the arena already exists + if (plugin.getArenaModule().getArenaManager().getArenas().stream().filter(a -> a.getName().equalsIgnoreCase(name)).count() > 0) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.create.exists")) + ); + return; + } + + File file = new File(plugin.getDataFolder(), "data/arenas/" + name + ".yml"); + + Arena arena = new Arena(plugin, name, sender.getName(), ((Player) sender).getLocation(), ((Player) sender).getLocation(), false, false, file); + + arena.setSpawnPoint1(((Player) sender).getLocation()); + arena.setSpawnPoint2(((Player) sender).getLocation()); + + plugin.getArenaModule().getArenaManager().addArena(arena); + + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.create.success")) + ); + } + + private void arenaDelete(CommandSender sender, String[] args) { + if (args.length == 1) { + badUsage(sender); + return; + } + + String name = args[1]; + Arena arena = plugin.getArenaModule().getArenaManager().getArena(name); + if (arena == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_found")) + ); + return; + } + if (!arena.getOwner().equals(sender.getName())) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_yours")) + ); + return; + } + if (arena.getState() != Arena.ArenaState.WAITING) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.delete.running")) + ); + return; + } + + arena.delete(); + plugin.getArenaModule().getArenaManager().removeArena(arena); + + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.delete.delete")) + ); + } + + private void arenaPublic(CommandSender sender, String[] args) { + if (args.length == 1) { + badUsage(sender); + return; + } + + String name = args[1]; + Arena arena = plugin.getArenaModule().getArenaManager().getArena(name); + if (arena == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_found")) + ); + return; + } + if (!arena.getOwner().equals(sender.getName())) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.not_yours")) + ); + return; + } + + boolean isPublic = arena.isPublic(); + + if (isPublic) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.public_command.success_private")) + ); + } else { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.arena.public_command.success_public")) + ); + } + + arena.setPublic(!isPublic); + } + + private void badUsage(CommandSender sender) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.bad_usage"))); + } + + private void arenaList(CommandSender sender) { + List arenas = plugin.getArenaModule().getArenaManager().getArenas().stream().filter(arena -> arena.getOwner().equals(sender.getName())).collect(Collectors.toList()); + plugin.getConfig().getStringList("messages.arena.list").forEach(s -> { + if (s.contains("%arenas%")) { + arenas.forEach(a -> { + sender.sendMessage(MiniMessage.miniMessage().deserialize(s.replace("%arenas%", a.getName()) + + (a.getSpawnPoint1() != null ? " (" + a.getSpawnPoint1().getBlockX() + ", " + a.getSpawnPoint1().getBlockY() + ", " + a.getSpawnPoint1().getBlockZ() + ")" : ""))); + }); + } else { + sender.sendMessage(MiniMessage.miniMessage().deserialize(s)); + } + }); + } + + private void arenaHelp(CommandSender sender) { + plugin.getConfig().getStringList("messages.arena.help").forEach(s -> { + sender.sendMessage(MiniMessage.miniMessage().deserialize(s)); + }); + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandGVG.java b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandGVG.java new file mode 100644 index 0000000..736d929 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandGVG.java @@ -0,0 +1,278 @@ +package com.ceraia.modules.arenas.commands.arena; + +import com.ceraia.Ceraia; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +public class CommandGVG implements CommandExecutor, TabCompleter, Listener { + + private final Ceraia plugin; + private final Map> groups = new HashMap<>(); + private final Map playersByGroup = new HashMap<>(); + + private final Map invites = new HashMap<>(); + + public CommandGVG(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if(!sender.hasPermission("xdbl.gvg")){ + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.no_permission")))); + return true; + } + + if (args.length == 0) { + plugin.getConfig().getStringList("messages.gvg.help").forEach(s -> sender.sendMessage(MiniMessage.miniMessage().deserialize(s))); + return true; + } + + Player player = (Player) sender; + + if (args[0].equalsIgnoreCase("invite")) { + if (playersByGroup.containsKey(player) && !groups.containsKey(player)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.dont_have_group")))); + return true; + } + + if (args.length == 1) { + badUsage(sender); + return true; + } + + List targets = new ArrayList<>(); + List notOnline = new ArrayList<>(); + + for (int i = 1; i < args.length; i++) { + Player target = Bukkit.getPlayer(args[i]); + if (target == null) { + notOnline.add(args[i]); + } else { + targets.add(target); + } + + if (playersByGroup.containsKey(target)) { + assert target != null; + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.invite.already_in_group")) + .replace("%player%", target.getName()))); + return true; + } + } + + if (targets.contains(player)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.invite.cant_invite_yourself")))); + return true; + } + + if (notOnline.size() > 0) { + System.out.println(notOnline); + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.player_offline")) + .replace("%player%", String.join(", ", notOnline)))); + return true; + } + + for (Player target : targets) { + Component message = Objects.requireNonNull( + MiniMessage.miniMessage().deserialize( + Objects.requireNonNull( + plugin.getConfig().getString("messages.gvg.invite.invite_message") + ).replace("%inviter%", player.getName() + ) + ) + ); + target.sendMessage(message); + + invites.put(target, player); + } + + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.invite.invite_sent")) + .replace("%player%", targets.stream().map(Player::getName).collect(Collectors.joining(", "))))); + return true; + } + else if (args[0].equalsIgnoreCase("accept")) { + Player inviter = invites.get(player); + + if (inviter == null || !inviter.isOnline()) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.accept.invite_not_found")))); + return true; + } + + if (playersByGroup.containsKey(inviter) && !groups.containsKey(inviter)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.accept.invite_not_found")))); + return true; + } + + if (playersByGroup.containsKey(player)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.accept.you_already_in_group")))); + return true; + } + + List group = groups.get(inviter); + if (group == null) { + group = new ArrayList<>(); + group.add(inviter); + playersByGroup.put(inviter, inviter); + } + group.add(player); + groups.put(inviter, group); + + playersByGroup.put(player, inviter); + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.accept.accepted")))); + for (Player pl : group) { + pl.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.accept.invite_accepted")) + .replace("%player%", player.getName()))); + } + return true; + } + else if (args[0].equalsIgnoreCase("leave")) { + leaveGang(player); + } + else if (args[0].equalsIgnoreCase("kick")) { + if (!groups.containsKey(player)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.kick.not_in_group")))); + return true; + } + + if (args.length == 1) { + badUsage(sender); + return true; + } + + Player target = plugin.getServer().getPlayer(args[1]); + if (target == null) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.player_offline")) + .replace("%player%", args[1]))); + return true; + } + + List group = groups.get(player); + + if (!group.contains(target)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.kick.player_not_in_group")))); + return true; + } + + group.remove(target); + playersByGroup.remove(target); + + target.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.kick.you_kicked")))); + + if (group.size() <= 1) { + + groups.get(player).forEach(p -> { + player.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.you_left")))); + playersByGroup.remove(p); + groups.remove(player); + }); + + groups.remove(player); + } else { + group.forEach(p -> p.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.kicked")) + .replace("%player%", target.getName())))); + } + return true; + } + else if (args[0].equalsIgnoreCase("fight")) { + String playerName = args[1]; + Player invited = Bukkit.getPlayer(playerName); + + if (invited == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.player_offline")) + .replace("%player%", playerName))); + return true; + } + + if (invited == player) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.invite.invite_yourself")))); + return true; + } + + if (!groups.containsKey(player)) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.not_in_group")))); + return true; + } + + if (!groups.containsKey(invited) || (groups.get(player).contains(invited) || groups.get(invited).contains(player))) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.player_not_in_group")))); + return true; + } + + plugin.getArenaModule().getArenaSelectGUI().openArenaList(player, invited); + } + + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args){ + if (args.length == 1) { + return Arrays.asList("invite", "accept", "leave", "kick", "fight"); + } + else if (args.length == 2 && args[0].equalsIgnoreCase("kick")) { + return getPlayersByGroup((Player) sender).stream().map(Player::getName).collect(Collectors.toList()); + } + else if (args.length == 2 && (args[0].equalsIgnoreCase("invite") ||args[0].equalsIgnoreCase("fight"))) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList()); + } + + return new ArrayList<>(); + } + + private void leaveGang(Player player) { + if (!playersByGroup.containsKey(player)) { + player.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.not_in_group")))); + return; + } + + Player owner = playersByGroup.get(player); + List group = groups.get(owner); + + if (group != null || group.size() <= 2) { + group.forEach(pl -> { + pl.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.you_left")))); + playersByGroup.remove(pl); + }); + + groups.remove(owner); + } else { + group.forEach(p -> player.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.gvg.leave.player_left_group")) + .replace("%player%", player.getName())))); + + group.remove(player); + groups.put(owner, group); + + playersByGroup.remove(player); + } + } + + private void badUsage(CommandSender sender) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.bad_usage")))); + } + + public List getPlayersByGroup(Player player) { + return groups.get(player); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + Player player = e.getPlayer(); + if (playersByGroup.containsKey(player)) { + leaveGang(player); + } + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandPVP.java b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandPVP.java new file mode 100644 index 0000000..a06ae8b --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandPVP.java @@ -0,0 +1,208 @@ +package com.ceraia.modules.arenas.commands.arena; + +import com.ceraia.Ceraia; +import com.ceraia.modules.arenas.managers.InviteManager; +import com.ceraia.modules.arenas.types.Arena; +import com.ceraia.modules.ceraia.types.CeraiaPlayer; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class CommandPVP implements CommandExecutor, TabCompleter { + + private final Ceraia plugin; + + public CommandPVP(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if(!sender.hasPermission("xdbl.pvp")){ + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.no_permission")))); + return true; + } + + if (args.length == 0) { + pvpHelp(sender); + return true; + } + + // Check if the sender is pvpbanned + CeraiaPlayer ceraiaPlayer = plugin.getPlayerManager().getCeraiaPlayer(((Player) sender).getUniqueId()); + + if (args[0].equalsIgnoreCase("accept")) { + Player p = (Player) sender; + + InviteManager.Invite invite = Objects.requireNonNull(plugin.getArenaModule().getInviteManager()).invites.get(p); + + if (invite == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.invite_accept.invite_not_found"))) + ); + plugin.getArenaModule().getInviteManager().invites.remove(p); + return true; + } + + if (!invite.invited.isOnline() || !invite.inviter.isOnline()) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.invite_accept.other_player_offline"))) + ); + return true; + } + + if(invite.accepted){ + sender.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.invite_accept.invite_already_accepted"))) + ); + return true; + } + + if (invite.arena.getState() != Arena.ArenaState.WAITING || plugin.getArenaModule().getArenaManager().getArenas().stream().noneMatch( + a -> a.getName().equalsIgnoreCase(invite.arena.getName()) + )) { + for (Player pl : Arrays.asList(invite.invited, invite.inviter)) { + pl.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.invite_accept.arena_not_ready"))) + ); + } + plugin.getArenaModule().getInviteManager().invites.remove(p); + return true; + } + + List playersToFight = new ArrayList<>(); + + if (invite.group) { + List group1 = plugin.getArenaModule().getGroupManager().getPlayersByGroup(invite.inviter); + List group2 = plugin.getArenaModule().getGroupManager().getPlayersByGroup(invite.invited); + + if (group1 == null || group2 == null) { + for (Player pl : Arrays.asList(invite.invited, invite.inviter)) { + pl.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.invite_accept.group.group_not_found"))) + ); + } + plugin.getArenaModule().getInviteManager().invites.remove(p); + return true; + } + + if (group1.size() < 2 || group2.size() < 2) { + for (Player pl : Arrays.asList(invite.invited, invite.inviter)) { + pl.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.invite_accept.group.group_too_small"))) + ); + } + plugin.getArenaModule().getInviteManager().invites.remove(p); + return true; + } + + boolean allPlayersAreReady = true; + + for (Player pl : group1) { + if (plugin.getArenaModule().getArenaManager().getArena(pl) != null) { + return true; + } + } + + for (Player pl : group2) { + if (plugin.getArenaModule().getArenaManager().getArena(pl) != null) { + return true; + } + } + + if (!allPlayersAreReady) { + for (Player pl : Arrays.asList(invite.invited, invite.inviter)) { + pl.sendMessage( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.invite_accept.group.someone_already_in_fight")) + ); + } + plugin.getArenaModule().getInviteManager().invites.remove(p); + return true; + } + + playersToFight.addAll(group1); + playersToFight.addAll(group2); + + group1.forEach(pl -> invite.arena.addPlayer(pl, 1)); + group2.forEach(pl -> invite.arena.addPlayer(pl, 2)); + } else { + playersToFight.add(invite.invited); + playersToFight.add(invite.inviter); + invite.arena.addPlayer(invite.invited, 1); + invite.arena.addPlayer(invite.inviter, 2); + } + + // Starting arena + invite.accepted = true; + invite.arena.start(invite, playersToFight); + + return true; + } + + // open gui for invite player + + String playerName = args[0]; + + // reload for op + if (playerName.equalsIgnoreCase("reload") && sender.isOp()) { + plugin.reloadConfig(); + sender.sendMessage(MiniMessage.miniMessage().deserialize( + "Reloaded!") + ); + return true; + } // If the player is reload and the sender is op, reload the config + + Player invited = Bukkit.getPlayer(playerName); + + if (invited == null) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.pvp.invite.player_offline"))) + ); + return true; + } // If the player is offline, return + + Player inviter = (Player) sender; + + if (inviter == invited) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.pvp.invite.invite_self"))) + ); + return true; + } // If the inviter is the same as the invited, return + + plugin.getArenaModule().getArenaSelectGUI().openArenaList(inviter, invited); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if (args.length == 1) { + List tabOptions = new ArrayList<>(); + // If there is an argument, suggest online player names + for (Player player : Bukkit.getOnlinePlayers()) { + // Exclude the sender's name from the suggestions + if (!player.getName().equals(sender.getName())) { + tabOptions.add(player.getName()); + } + } + + return tabOptions; + } + // If there is more than one argument, return an empty list + return new ArrayList<>(); + } + + private void pvpHelp(CommandSender sender) { + plugin.getConfig().getStringList("messages.pvp.help").forEach(s -> sender.sendMessage(MiniMessage.miniMessage().deserialize(s))); + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandProfile.java b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandProfile.java new file mode 100644 index 0000000..baed6ba --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandProfile.java @@ -0,0 +1,70 @@ +package com.ceraia.modules.arenas.commands.arena; + +import com.ceraia.Ceraia; +import com.ceraia.modules.ceraia.types.CeraiaPlayer; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class CommandProfile implements CommandExecutor, TabCompleter { + + private final Ceraia plugin; + + public CommandProfile(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if (!sender.hasPermission("xdbl.pvp")) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.no_permission")))); + return true; + } + + Player player; + if (args.length == 1) { + player = Bukkit.getPlayer(args[0]); + if (player == null) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.bad_usage")))); + return true; + } + } else { + player = (Player) sender; + } + + // Return the player's profile + CeraiaPlayer ceraiaPlayer = plugin.getPlayerManager().getCeraiaPlayer(player.getUniqueId()); + plugin.getConfig().getStringList("messages.profile").forEach(s -> sender.sendMessage(MiniMessage.miniMessage().deserialize(s + .replace("%player%", player.getName()) + .replace("%elo%", String.valueOf(ceraiaPlayer.getElo())) + .replace("%wins%", String.valueOf(ceraiaPlayer.getWins())) + .replace("%losses%", String.valueOf(ceraiaPlayer.getLosses())) + .replace("%games%", String.valueOf(ceraiaPlayer.getWins() + ceraiaPlayer.getLosses()))))); + + + + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + // Return a list of all players + if (args.length == 1) { + List players = new ArrayList<>(); + Bukkit.getOnlinePlayers().forEach(p -> players.add(p.getName())); + return players; + } + return new ArrayList<>(); + } + + +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandTop.java b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandTop.java new file mode 100644 index 0000000..c64fe25 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/arena/CommandTop.java @@ -0,0 +1,77 @@ +package com.ceraia.modules.arenas.commands.arena; + +import com.ceraia.Ceraia; +import com.ceraia.modules.ceraia.types.CeraiaPlayer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +public class CommandTop implements CommandExecutor, TabCompleter { + + private final Ceraia plugin; + + public CommandTop(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if (!sender.hasPermission("xdbl.pvp")) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.no_permission")))); + return true; + } + + Player p = (Player) sender; + + // Create and show a string list of the top 10 players with the highest elo + List top = new ArrayList<>(); + AtomicInteger i = new AtomicInteger(); + i.set(1); + + top.add(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.scoreboard.top")))); + + plugin.getPlayerManager().getCeraiaPlayers().stream().sorted(Comparator.comparingInt(CeraiaPlayer::getElo).reversed()).limit(10).forEach(ap -> { + String playerName = Bukkit.getOfflinePlayer(ap.getUUID()).getName(); + int elo = ap.getElo(); + + String medal; // Default medal color for players outside the top 3 + + // Check for 1st, 2nd, and 3rd place + if (i.get() == 1) { + medal = ""; // Gold for 1st place + } else if (i.get() == 2) { + medal = "<#C0C0C0>"; // Silver for 2nd place + } else if (i.get() == 3) { + medal = "<#cd7f32>"; // Bronze for 3rd place + } else { + medal = ""; // Default medal color for players outside the top 3 + } + + top.add(MiniMessage.miniMessage().deserialize(medal + i + " " + playerName + " - " + elo + " ELO (" + (ap.getWins() + ap.getLosses()) + " games)")); + i.getAndIncrement(); + }); + + // Send the top 10 players with the highest elo to the player + top.forEach(p::sendMessage); + + + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/system/CommandMod.java b/src/main/java/com/ceraia/modules/arenas/commands/system/CommandMod.java new file mode 100644 index 0000000..a0adb32 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/system/CommandMod.java @@ -0,0 +1,97 @@ +package com.ceraia.modules.arenas.commands.system; + +import com.ceraia.Ceraia; +import com.ceraia.modules.ceraia.types.CeraiaPlayer; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class CommandMod implements CommandExecutor, TabCompleter { + + private final Ceraia plugin; + + public CommandMod(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if (!sender.hasPermission("xdbl.mod")) { + sender.sendMessage(MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.no_permission")))); + return true; + } + + if (args.length == 0) { + ModHelp(sender); + return true; + } + + if (args.length == 1) { + if (args[0].equalsIgnoreCase("ban")) { + ModHelp(sender); + return true; + } + } + + if (args.length == 2) { + if (args[0].equalsIgnoreCase("ban")) { + if (args[1].equalsIgnoreCase("pvp")) { + ModHelp(sender); + return true; + } + if (args[1].equalsIgnoreCase("arena")) { + ModHelp(sender); + return true; + } + } + } + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if (args.length == 1) { + List tabOptions = new ArrayList<>(); + tabOptions.add("ban"); + tabOptions.add("remove"); + return tabOptions; + } + if (args.length == 2) { + List tabOptions = new ArrayList<>(); + if (args[1].equalsIgnoreCase("ban")) tabOptions.add("pvp"); + tabOptions.add("arena"); + return tabOptions; + } + if (args.length == 3) { + if (args[2].equalsIgnoreCase("pvp")) { + List tabOptions = new ArrayList<>(); + // If there is an argument, suggest online player names + for (Player player : Bukkit.getOnlinePlayers()) { + tabOptions.add(player.getName()); + } + return tabOptions; + } + if (args[2].equalsIgnoreCase("arena")) { + List tabOptions = new ArrayList<>(); + // If there is an argument, suggest all arena names + Objects.requireNonNull(plugin.getArenaModule().getArenaManager()).getArenas().forEach(arena -> tabOptions.add(arena.getName())); + return tabOptions; + } + } + // If there is more than one argument, return an empty list + return new ArrayList<>(); + } + + private void ModHelp(CommandSender sender) { + plugin.getConfig().getStringList("messages.mod.help").forEach(s -> sender.sendMessage(MiniMessage.miniMessage().deserialize(s))); + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/commands/system/CommandVersion.java b/src/main/java/com/ceraia/modules/arenas/commands/system/CommandVersion.java new file mode 100644 index 0000000..759d2aa --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/commands/system/CommandVersion.java @@ -0,0 +1,34 @@ +package com.ceraia.modules.arenas.commands.system; + +import com.ceraia.Ceraia; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class CommandVersion implements CommandExecutor, TabCompleter { + + private final Ceraia plugin; + + public CommandVersion(Ceraia plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + sender.sendMessage( + MiniMessage.miniMessage().deserialize("Running Ceraia v" + plugin.getPluginMeta().getVersion() + " by Axodouble") + ); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/listeners/ArenaBlockListener.kt b/src/main/java/com/ceraia/modules/arenas/listeners/ArenaBlockListener.kt new file mode 100644 index 0000000..e2a51c3 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/listeners/ArenaBlockListener.kt @@ -0,0 +1,33 @@ +package com.ceraia.modules.arenas.listeners + +import com.ceraia.Ceraia +import org.bukkit.Bukkit +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockPlaceEvent + +class ArenaBlockListener(private val plugin: Ceraia) : Listener { + init { + Bukkit.getPluginManager().registerEvents(this, plugin) + } + + @EventHandler + fun onBlockPlace(e: BlockPlaceEvent) { + val arena = plugin.arenaModule.arenaManager!!.getArena(e.player) ?: return + + arena.placeBlock(e.blockPlaced.location) + } + + @EventHandler + fun onBlockBreak(e: BlockBreakEvent) { + val arena = plugin.arenaModule.arenaManager!!.getArena(e.player) ?: return + + if (arena.placedBlocks.contains(e.block.location)) { + arena.removeBlock(e.block.location) + return + } + + e.isCancelled = true + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/listeners/ArenaExplodeListener.kt b/src/main/java/com/ceraia/modules/arenas/listeners/ArenaExplodeListener.kt new file mode 100644 index 0000000..804e4b1 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/listeners/ArenaExplodeListener.kt @@ -0,0 +1,93 @@ +package com.ceraia.modules.arenas.listeners + +import com.ceraia.Ceraia +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.block.data.type.RespawnAnchor +import org.bukkit.entity.* +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityExplodeEvent +import org.bukkit.event.player.PlayerInteractEvent + +class ArenaExplodeListener(private val plugin: Ceraia) : Listener { + init { + Bukkit.getPluginManager().registerEvents(this, plugin) + } + + private fun isInArena(player: Player): Boolean { + return plugin.arenaModule.arenaManager!!.getArena(player) != null + } + + // TNT + @EventHandler + fun onEntityExplode(e: EntityExplodeEvent) { + var source: Player? = null + if (e.entityType == EntityType.TNT) { + val tnt = e.entity as TNTPrimed + val entity = tnt.source as? Player ?: return + source = entity + } + + if (source == null || isInArena(source)) { + return + } + e.blockList().clear() + } + + // End crystal + @EventHandler + fun onHitCrystal(e: EntityDamageByEntityEvent) { + val entity = e.entity + val damager = e.damager + + val source: Player + + if (entity !is EnderCrystal) return + + if (damager is Player) { + source = damager + } else if (damager is Arrow) { + val entity2 = damager.shooter as Entity? as? Player ?: return + source = entity2 + } else { + return + } + + if (isInArena(source)) { + return + } + + e.isCancelled = true + if (e.entity.isValid) e.entity.remove() + e.entity.world.createExplosion( + e.entity.location, + 6f, + false, + false + ) + } + + // Respawn Anchor + @EventHandler + fun onFillAnchor(e: PlayerInteractEvent) { + if (e.clickedBlock == null) return + if (e.clickedBlock!!.type != Material.RESPAWN_ANCHOR) return + + val block = e.clickedBlock + val data = block!!.blockData as RespawnAnchor + if (data.charges < data.maximumCharges) return + + if (isInArena(e.player)) return + + e.isCancelled = true + block.type = Material.AIR + block.world.createExplosion( + block.location, + 5f, + false, + false + ) + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/listeners/ArenaFightListener.kt b/src/main/java/com/ceraia/modules/arenas/listeners/ArenaFightListener.kt new file mode 100644 index 0000000..90f333e --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/listeners/ArenaFightListener.kt @@ -0,0 +1,184 @@ +package com.ceraia.modules.arenas.listeners + +import com.ceraia.Ceraia +import com.ceraia.modules.arenas.types.Arena +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.* +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.scheduler.BukkitRunnable +import java.util.* + +class ArenaFightListener(private val plugin: Ceraia) : Listener { + init { + Bukkit.getPluginManager().registerEvents(this, plugin) + } + + private fun isInArena(player: Player): Boolean { + return plugin.arenaModule.arenaManager?.getArena(player) != null + } + + @EventHandler + fun onHit(e: EntityDamageByEntityEvent) { + val damager = e.damager as? Player ?: return + val player = e.entity as? Player ?: return + + if (!isInArena(damager) && !isInArena(player)) { + return + } + + if (((isInArena(damager) && !isInArena(player)) || (!isInArena(damager) && isInArena(player))) || plugin.arenaModule.arenaManager?.getArena( + damager + )?.name != plugin.arenaModule.arenaManager?.getArena(player)?.name + ) { + e.isCancelled = true + return + } + + val arena: Arena = plugin.arenaModule.arenaManager?.getArena(damager) ?: return + + if (arena.state != Arena.ArenaState.RUNNING) { + e.isCancelled = true + return + } + + if (arena.team1.contains(damager) && arena.team1.contains(player)) { + e.isCancelled = true + return + } + + if (arena.team2.contains(damager) && arena.team2.contains(player)) { + e.isCancelled = true + } + } + + @EventHandler + fun onDamage(e: EntityDamageEvent) { + val player = e.entity as? Player ?: return + + if (!isInArena(player)) { + return + } + + // Get player that hurt the player + var killer: Player? = null + if (e is EntityDamageByEntityEvent) { + // If the damage is caused by a player + if (e.damager is Player) { + plugin.logger.info("Player") + killer = e.damager as Player + } else if (e.damager is Projectile) { + plugin.logger.info("Arrow") + val projectile = e.damager as Projectile + if (projectile.shooter is Player) { + killer = projectile.shooter as Player + } + } else if (e.damager.type == EntityType.TNT) { + plugin.logger.info("TNT") + if (e.damager.customName() != null) { + killer = Bukkit.getPlayer(Objects.requireNonNull(e.damager.customName()).toString()) + } + } + } + + val arena: Arena = plugin.arenaModule.arenaManager?.getArena(player) ?: return + + val healthAfter: Double = player.health - e.finalDamage + if (healthAfter <= 0) { + // Check if during the fight totems are allowed + + var invite = plugin.arenaModule.inviteManager!!.invites[player] + + if (invite == null) invite = plugin.arenaModule.inviteManager!!.selectingInvites[killer] + + if (invite != null) { + if (arena.totems) { + if (player.inventory.itemInMainHand.type == Material.TOTEM_OF_UNDYING || player.inventory.itemInOffHand.type == Material.TOTEM_OF_UNDYING) { + return + } + } + } + + e.isCancelled = true + player.health = player.healthScale + + arena.end(player, false) + } + } + + @EventHandler + fun onDeath(e: PlayerDeathEvent) { + if (!isInArena(e.entity)) { + return + } + val arena = plugin.arenaModule.arenaManager?.getArena(e.entity) ?: return + + val loc = e.entity.location + + var killer = e.entity.killer + + if (killer == null) { + killer = Bukkit.getPlayer( + Objects.requireNonNull(Objects.requireNonNull(e.entity.lastDamageCause)?.entity?.customName()).toString() + ) + } + if (killer != null) { + matchEnd(e.entity, killer) + } + + e.entity.spigot().respawn() + object : BukkitRunnable() { + override fun run() { + e.entity.teleport(loc) + arena.end(e.entity, false) + } + }.runTaskLater(plugin, 5L) + } + + @EventHandler + fun onQuit(e: PlayerQuitEvent) { + if (!isInArena(e.player)) { + return + } + + val arena = plugin.arenaModule.arenaManager?.getArena(e.player) ?: return + + // Check in which team the player is + if (arena.team1.contains(e.player)) { + matchEnd(e.player, arena.team2[0]) + } else { + matchEnd(e.player, arena.team1[0]) + } + + arena.end(e.player, true) + } + + private fun matchEnd(loser: Player, winner: Player) { + val winnerUUID = winner.uniqueId + val loserUUID = loser.uniqueId + + // Get the win chance + val winChance = plugin.arenaModule.calculateWinChance(winnerUUID, loserUUID).toInt() + + // Announce the winner and the win chance in chat + Bukkit.broadcast( + MiniMessage.miniMessage().deserialize( + plugin.config.getString("messages.fight.end_global")!! + .replace("%winner%", winner.name).replace("%loser%", loser.name) + .replace("%elo%", plugin.playerManager.getCeraiaPlayer(loserUUID).elo.toString()) + .replace("%winchance%", winChance.toString()) + .replace("%arena%", plugin.arenaModule.arenaManager?.getArena(loser)?.name ?: "") + ) + ) + + // Handle ELO calculations + // #TODO: Re-implement ELO calculations + //plugin.getPlayerManager().PlayerKill(winnerUUID, loserUUID); + } +} \ No newline at end of file diff --git a/src/main/java/com/ceraia/modules/arenas/listeners/PlayerEloChangeListener.kt b/src/main/java/com/ceraia/modules/arenas/listeners/PlayerEloChangeListener.kt new file mode 100644 index 0000000..166b3dd --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/listeners/PlayerEloChangeListener.kt @@ -0,0 +1,25 @@ +package com.ceraia.modules.arenas.listeners + +import com.ceraia.Ceraia +import com.ceraia.modules.arenas.managers.EloScoreboardManager +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent + +class PlayerEloChangeListener(private val plugin: Ceraia) : Listener { + init { + plugin.server.pluginManager.registerEvents(this, plugin) + } + + @EventHandler // Custom on player Elo change event to update the scoreboard + fun onPlayerEloChange(event: PlayerEventListener?) { + val eloScoreboardManager = EloScoreboardManager(plugin) + eloScoreboardManager.updateScoreboard() + } + + @EventHandler // On player join event to update the scoreboard + fun onPlayerJoin(event: PlayerJoinEvent?) { + val eloScoreboardManager = EloScoreboardManager(plugin) + eloScoreboardManager.updateScoreboard() + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/listeners/PlayerEventListener.kt b/src/main/java/com/ceraia/modules/arenas/listeners/PlayerEventListener.kt new file mode 100644 index 0000000..842c6ea --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/listeners/PlayerEventListener.kt @@ -0,0 +1,19 @@ +package com.ceraia.modules.arenas.listeners + +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class PlayerEventListener : Event() { + companion object { + private val handlers = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return handlers + } + } + + override fun getHandlers(): HandlerList { + return handlers + } +} \ No newline at end of file diff --git a/src/main/java/com/ceraia/modules/arenas/listeners/PlayerInventoryListener.kt b/src/main/java/com/ceraia/modules/arenas/listeners/PlayerInventoryListener.kt new file mode 100644 index 0000000..bef7c6e --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/listeners/PlayerInventoryListener.kt @@ -0,0 +1,45 @@ +package com.ceraia.modules.arenas.listeners + +import com.ceraia.Ceraia +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.event.inventory.InventoryType +import org.bukkit.event.player.PlayerDropItemEvent + +class PlayerInventoryListener(private val plugin: Ceraia) : Listener { + init { + Bukkit.getPluginManager().registerEvents(this, plugin) + } + + private fun isInArena(player: Player): Boolean { + return plugin.arenaModule.arenaManager!!.getArena(player) != null + } + + @EventHandler + fun onDropItem(e: PlayerDropItemEvent) { + if (isInArena(e.player)) { + e.isCancelled = true + } + } + + @EventHandler + fun onClickInventory(e: InventoryClickEvent) { + val p = e.whoClicked as Player + + if (!isInArena(p)) { + return + } + + if (e.view.topInventory.type == InventoryType.PLAYER + || e.view.topInventory.type == + InventoryType.CRAFTING + ) { + return + } + + e.isCancelled = true + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/managers/ArenaManager.java b/src/main/java/com/ceraia/modules/arenas/managers/ArenaManager.java new file mode 100644 index 0000000..d3133b6 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/managers/ArenaManager.java @@ -0,0 +1,84 @@ +package com.ceraia.modules.arenas.managers; + +import com.ceraia.Ceraia; +import com.ceraia.modules.arenas.types.Arena; +import org.bukkit.Location; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ArenaManager { + + private final Ceraia plugin; + + private final List arenas = new ArrayList<>(); + private final Map playersInArena = new HashMap<>(); + + public ArenaManager(Ceraia plugin) { + this.plugin = plugin; + + // Load arenas + File f = new File(plugin.getDataFolder(), "data/arenas"); + if (!f.exists()) { + f.mkdirs(); + return; + } + + File[] files = f.listFiles(); + for (File file : files) { + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + + String name = config.getString("name"); + String owner = config.getString("owner"); + Location spawnPoint1 = config.getLocation("spawnPoint1"); + Location spawnPoint2 = config.getLocation("spawnPoint2"); + boolean isPublic = config.getBoolean("public", false); + boolean isFFA = config.getBoolean("ffa", false); + + Arena arena = new Arena(plugin, name, owner, spawnPoint1, spawnPoint2, isPublic, isFFA, file); + arenas.add(arena); + } + + } + + public List getArenas() { + return arenas; + } + + public Arena getArena(Player player) { + return playersInArena.get(player); + } + + public Arena getArena(String name) { + for (Arena arena : arenas) { + if (arena.getName().equalsIgnoreCase(name)) { + return arena; + } + } + return null; + } + + + public void addArena(Arena arena) { + arenas.add(arena); + arena.saveArena(); + } + + public void removeArena(Arena arena) { + arenas.remove(arena); + } + + public void addPlayerToArena(Player player, Arena arena) { + playersInArena.put(player, arena); + } + + public void removePlayerFromArena(Player player) { + playersInArena.remove(player); + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/managers/EloScoreboardManager.java b/src/main/java/com/ceraia/modules/arenas/managers/EloScoreboardManager.java new file mode 100644 index 0000000..8deeedf --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/managers/EloScoreboardManager.java @@ -0,0 +1,39 @@ +package com.ceraia.modules.arenas.managers; + +import com.ceraia.Ceraia; +import com.ceraia.modules.ceraia.types.CeraiaPlayer; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.DisplaySlot; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.ScoreboardManager; + +public class EloScoreboardManager { + + private final Ceraia plugin; + + public EloScoreboardManager(Ceraia plugin) { + this.plugin = plugin; + } + + public void updateScoreboard() { + ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager(); + Scoreboard scoreboardDefault = scoreboardManager.getNewScoreboard(); + Objective objectivePlayerList = scoreboardDefault.registerNewObjective("eloObjectivePlayerList", "dummy", MiniMessage.miniMessage().deserialize("Top Arena Players")); + Objective objectiveBelowName = scoreboardDefault.registerNewObjective("eloObjectiveBelowName", "dummy", MiniMessage.miniMessage().deserialize("ELO")); + + // Get all online players and set their score to their Elo rating + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { + CeraiaPlayer ceraiaPlayer = plugin.getPlayerManager().getCeraiaPlayer(onlinePlayer.getUniqueId()); + + objectivePlayerList.getScore(onlinePlayer.getName()).setScore(ceraiaPlayer.getElo()); + objectiveBelowName.getScore(onlinePlayer.getName()).setScore(ceraiaPlayer.getElo()); + objectivePlayerList.setDisplaySlot(DisplaySlot.PLAYER_LIST); + objectiveBelowName.setDisplaySlot(DisplaySlot.BELOW_NAME); + + onlinePlayer.setScoreboard(scoreboardDefault); + } + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/managers/InviteManager.java b/src/main/java/com/ceraia/modules/arenas/managers/InviteManager.java new file mode 100644 index 0000000..6737aed --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/managers/InviteManager.java @@ -0,0 +1,25 @@ +package com.ceraia.modules.arenas.managers; + +import com.ceraia.modules.arenas.types.Arena; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class InviteManager { + + public Map invites = new HashMap<>(); + public Map selectingInvites = new HashMap<>(); + + public static class Invite { + public Player inviter, invited; + public Arena arena; + public boolean accepted = false; + public boolean group; + + public Invite(Player inviter, Player invited) { + this.inviter = inviter; + this.invited = invited; + } + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/types/Arena.java b/src/main/java/com/ceraia/modules/arenas/types/Arena.java new file mode 100644 index 0000000..ec8e416 --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/types/Arena.java @@ -0,0 +1,436 @@ +package com.ceraia.modules.arenas.types; + +import com.ceraia.Ceraia; +import com.ceraia.modules.arenas.Utils; +import com.ceraia.modules.arenas.managers.InviteManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.title.Title; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public class Arena { + private final Ceraia plugin; + + // before ready + private final String name; + private final String owner; + private final List startPlayers = new ArrayList<>(); + private final List placedBlocks = new ArrayList<>(); + private final Map priorLocations = new HashMap<>(); + private boolean isPublic; + private File configFile; + // after ready + private Location spawnPoint1, spawnPoint2; + // after start + private List team1 = new ArrayList<>(); + private List team2 = new ArrayList<>(); + private ArenaState state = ArenaState.WAITING; + private int timer; + public boolean totems = false; + private boolean isFFA = false; + + public Arena(Ceraia plugin, String name, String owner, Location spawnPoint1, Location spawnPoint2, boolean isPublic, boolean isFFA, File configFile) { + this.plugin = plugin; + + this.name = name; + this.owner = owner; + this.spawnPoint1 = spawnPoint1; + this.spawnPoint2 = spawnPoint2; + this.isPublic = isPublic; + this.isFFA = isFFA; + + this.configFile = configFile; + } + + public boolean saveArena() { + try { + configFile = new File(plugin.getDataFolder(), "data/arenas/" + name + ".yml"); + + // Check if the file already exists + if (!configFile.exists()) { + configFile.createNewFile(); + } + + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Update or set the values + config.set("name", name); + config.set("owner", owner); + + config.set("spawnPoint1", spawnPoint1); + config.set("spawnPoint2", spawnPoint2); + config.set("public", isPublic); + + config.save(configFile); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + public boolean totems() { + return totems; + } + + public void setTotems(boolean totems) { + this.totems = totems; + } + + public void setTotems(boolean totems, boolean save) { + this.totems = totems; + if (save) { + saveArena(); + } + } + public int getTimer() { + return timer; + } + + public void setTimer(int timer) { + this.timer = timer; + } + + public ArenaState getState() { + return state; + } + + public void setState(ArenaState state) { + this.state = state; + } + + public void delete() { + configFile.delete(); + } + + public String getName() { + return name; + } + + public String getOwner() { + return owner; + } + + public Location getSpawnPoint1() { + return spawnPoint1; + } + + public void setSpawnPoint1(Location loc) { + spawnPoint1 = loc; + saveArena(); + } + + public Location getSpawnPoint2() { + return spawnPoint2; + } + + public void setSpawnPoint2(Location loc) { + spawnPoint2 = loc; + saveArena(); + } + + public List getTeam1() { + return team1; + } + + public void setTeam1(List team1) { + this.team1 = team1; + } + + public List getTeam2() { + return team2; + } + + public void setTeam2(List team2) { + this.team2 = team2; + } + + public void addPlayer(Player player, int team) { + plugin.getArenaModule().getArenaManager().addPlayerToArena(player, this); + if (team == 1) { + team1.add(player); + } else { + team2.add(player); + } + + startPlayers.add(player); + } + + public List getStartPlayers() { + return startPlayers; + } + + public List getOnlinePlayers() { + List onlinePlayers = new ArrayList<>(); + onlinePlayers.addAll(team1); + onlinePlayers.addAll(team2); + + return onlinePlayers; + } + + public void reset() { + team1.clear(); + team2.clear(); + startPlayers.clear(); + placedBlocks.clear(); + priorLocations.clear(); + } + + public void end(Player player, boolean quit) { + boolean end = false; + + + + List winners = new ArrayList<>(); + List losers = new ArrayList<>(); + + if (this.getTeam1().contains(player)) { + if (this.getTeam1().size() <= 1) { + end = true; + + winners.addAll(this.getTeam2()); + losers.addAll(this.getTeam1()); + } else { + List team = this.getTeam1(); + team.remove(player); + this.setTeam1(team); + } + } else { + if (this.getTeam2().size() <= 1) { + end = true; + + winners.addAll(this.getTeam1()); + losers.addAll(this.getTeam2()); + } else { + List team = this.getTeam2(); + team.remove(player); + this.setTeam2(team); + } + } + + if (!end || quit) { + Utils.teleportPlayerToSpawn(plugin, player, this); + plugin.getArenaModule().getArenaManager().removePlayerFromArena(player); + + player.getInventory().clear(); + Utils.revertInventory(plugin, player, this); + if (!end) { + return; + } + } + + for (Player pl : this.getOnlinePlayers()) { + if (pl == player && quit) { + continue; + } + + pl.sendMessage( + MiniMessage.miniMessage().deserialize( + plugin.getConfig().getString("messages.fight.end") + .replace("%winner%", winners.stream().map(Player::getName).collect(Collectors.joining(", "))) + .replace("%time%", String.valueOf(plugin.getConfig().getInt("cooldown.after"))) + ) + ); + + pl.getInventory().clear(); + + pl.setHealth(20); + pl.setFireTicks(0); + pl.setFoodLevel(20); + pl.setSaturation(20); + } + + this.setState(ArenaState.ENDING); + + Arena thisArena = this; + + new BukkitRunnable() { + public void run() { + for (Location loc : placedBlocks) { + loc.getBlock().setType(Material.AIR); + } + + for (Player pl : getOnlinePlayers()) { + if (pl == player && quit) { + continue; + } + Utils.teleportPlayerToSpawn(plugin, pl, thisArena); + + plugin.getArenaModule().getArenaManager().removePlayerFromArena(pl); + + Utils.revertInventory(plugin, pl, thisArena); + } + + // Reward + for (Player pl : winners) { + plugin.getPlayerManager().getCeraiaPlayer(pl.getUniqueId()).addWin(); + for (String command : plugin.getConfig().getStringList("rewards")) { + pl.performCommand(command.replace("%player%", pl.getName())); + } + } + + // Reward losers + for (Player pl : losers) { + plugin.getPlayerManager().getCeraiaPlayer(pl.getUniqueId()).addLoss(); + for (String command : plugin.getConfig().getStringList("rewards_lose")) { + pl.performCommand(command.replace("%player%", pl.getName())); + } + } + + thisArena.setState(ArenaState.WAITING); + reset(); + } + }.runTaskLater(plugin, plugin.getConfig().getInt("cooldown.after") * 20L); + } + + public void start(InviteManager.Invite invite, List players) { + this.setState(ArenaState.STARTING); + + try { + for (Player pl : players) { + priorLocations.put(pl, pl.getLocation()); + + ItemStack[] content = pl.getInventory().getContents(); + + File file = new File(plugin.getDataFolder(), "data/pinventory_" + this.getName() + "_" + pl.getName() + ".yml"); + file.createNewFile(); + + FileConfiguration yaml = YamlConfiguration.loadConfiguration(file); + + // list of itemstack from array + for (int i = 0; i < content.length; i++) { + if (content[i] == null) { + yaml.set("items." + i, "null"); + } else { + yaml.set("items." + i, content[i]); + } + } + + yaml.save(file); + } + } catch (IOException e) { + e.printStackTrace(); + System.out.println("Problem saving inventories, nothing was deleted!"); + for (Player pl : Arrays.asList(invite.invited, invite.inviter)) { + pl.sendMessage( + MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(plugin.getConfig().getString("messages.fight.problem_saving_inventories")) + ) + ); + } + + return; + } + + for (Player pl : this.getTeam1()) { + pl.teleport( + this.getSpawnPoint1() + ); + } + for (Player pl : this.getTeam2()) { + pl.teleport( + this.getSpawnPoint2() + ); + } + + for (Player pl : players) { + pl.setHealth(20); + pl.setFoodLevel(20); + pl.setSaturation(20); + pl.setGameMode(GameMode.SURVIVAL); + } + + this.setState(ArenaState.STARTING); + + AtomicInteger i = new AtomicInteger( + plugin.getConfig().getInt("cooldown.before") + 1 + ); + + Arena thisArena = this; + new BukkitRunnable() { + public void run() { + for (Player pl : players) { + if (i.get() == 0) { + pl.showTitle(Title.title(Component.empty(), Component.empty())); + } else if (i.get() == 1) { + Title title = Title.title( + MiniMessage.miniMessage().deserialize(plugin.getConfig().getString("messages.fight.started") + .replace("%time%", String.valueOf(i.get() - 1))), + Component.empty() + ); + + pl.showTitle(title); + } else { + pl.showTitle(Title.title(MiniMessage.miniMessage().deserialize( + plugin.getConfig().getString("messages.fight.starting") + .replace("%time%", String.valueOf(i.get() - 1)) + ), MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(plugin.getArenaModule().getArenaManager().getArena(pl).totems ? + plugin.getConfig().getString("messages.fight.totems_enabled") + : plugin.getConfig().getString("messages.fight.totems_disabled")) + ) + )); + } + } + + if (i.get() == 0) { + thisArena.setState(ArenaState.RUNNING); + cancel(); + return; + } + + i.decrementAndGet(); + + } + }.runTaskTimer(plugin, 0, 20); + } + + public void placeBlock(Location loc) { + placedBlocks.add(loc); + } + + public void removeBlock(Location loc) { + placedBlocks.remove(loc); + } + + public List getPlacedBlocks() { + return placedBlocks; + } + + public boolean isPublic() { + return isPublic; + } + + public void setPublic(boolean isPublic) { + this.isPublic = isPublic; + + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + config.set("public", isPublic); + config.save(configFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public Location getPlayerPriorLocation(Player pl) { + return priorLocations.get(pl); + } + + public enum ArenaState { + WAITING, STARTING, RUNNING, ENDING + } +} diff --git a/src/main/java/com/ceraia/modules/arenas/types/ArenaSelectGUI.java b/src/main/java/com/ceraia/modules/arenas/types/ArenaSelectGUI.java new file mode 100644 index 0000000..f619f3f --- /dev/null +++ b/src/main/java/com/ceraia/modules/arenas/types/ArenaSelectGUI.java @@ -0,0 +1,235 @@ +package com.ceraia.modules.arenas.types; + +import com.ceraia.Ceraia; +import com.ceraia.modules.arenas.managers.InviteManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.*; + +public class ArenaSelectGUI implements Listener { + private static Component INVENTORY_NAME_ARENAS; + private static Component INVENTORY_NAME_TOTEMS; + private final Ceraia plugin; + private final Map> selectingArenaCache = new HashMap<>(); + + public ArenaSelectGUI(Ceraia plugin) { + this.plugin = plugin; + + INVENTORY_NAME_ARENAS = MiniMessage.miniMessage().deserialize("Quick Text Arena Title"); + INVENTORY_NAME_TOTEMS = MiniMessage.miniMessage().deserialize("Quick Text Totem Title"); + + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + public void openArenaList(Player inviter, Player invited) { // Let the player select what arena to fight in + // Add the player invitee and inviter + InviteManager.Invite invite = new InviteManager.Invite(inviter, invited); + + plugin.getArenaModule().getInviteManager().selectingInvites.put(inviter, invite); + + // Get a list of all arenas accessible to the player + List arenas = plugin.getArenaModule().getArenaManager().getArenas() + .stream().filter(a -> a.isPublic() || a.getOwner().equals(inviter.getName())).toList(); + + // Size is from arenas.size() and must be devidable by 9 + int size = Math.max(9, (arenas.size() + 8) / 9 * 9); + + // Create the inventory + Inventory inv = Bukkit.createInventory(null, size, MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.inventory_name")))); + + // Create a map of the slot and the arena + Map arenasSelectSlots = new HashMap<>(); + + int i = 0; // Slot + for (Arena a : arenas.stream().filter(a -> a.getState() == Arena.ArenaState.WAITING).toList()) { // Filter out arenas that are not ready + ItemStack itemStack = new ItemStack(Objects.requireNonNull(Material.getMaterial( + Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.arena_item.item")) + ))); // Create the itemstack + ItemMeta meta = itemStack.getItemMeta(); + meta.displayName( + MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.arena_item.name")) + .replace("%arena_name%", a.getName()) + .replace("%arena_owner%", a.getOwner()) + ) + ); + + List lore = new ArrayList<>(); + for (String s : plugin.getConfig().getStringList("messages.arena_select_gui.arena_item.lore")) { + lore.add(MiniMessage.miniMessage().deserialize(s.replace("%arena_name%", a.getName()) + .replace("%arena_owner%", a.getOwner()) + .replace("%totems%", a.totems ? "enabled" : "disabled") + )); + } + + meta.lore(lore); + + itemStack.setItemMeta(meta); + + inv.setItem(i, itemStack); + arenasSelectSlots.put(i, a); + + i++; + } + + // Put the map in the cache + selectingArenaCache.put(inviter, arenasSelectSlots); + + // Open the inventory + inviter.openInventory(inv); + } + + public void openTotemEnabled(Player inviter, Arena arena){ // Let the player select whether to enable or disable totems in the fight + int size = 9; + + Inventory inv = Bukkit.createInventory(null, size, MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.totem_select_gui.inventory_name")))); + + ItemStack itemStackEnable = new ItemStack(Objects.requireNonNull(Material.getMaterial( + Objects.requireNonNull(plugin.getConfig().getString("messages.totem_select_gui.items.enable.item")) + ))); // Create the itemstack + ItemMeta metaEnable = itemStackEnable.getItemMeta(); + + metaEnable.displayName( + MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(plugin.getConfig().getString("messages.totem_select_gui.items.enable.name")) + ) + ); + + List loreEnable = new ArrayList<>(); + for (String s : plugin.getConfig().getStringList("messages.totem_select_gui.items.enable.lore")) { + loreEnable.add(MiniMessage.miniMessage().deserialize(s)); + } + + metaEnable.lore(loreEnable); + + itemStackEnable.setItemMeta(metaEnable); + + inv.setItem(1, itemStackEnable); + + ItemStack itemStackDisable = new ItemStack(Objects.requireNonNull(Material.getMaterial( + Objects.requireNonNull(plugin.getConfig().getString("messages.totem_select_gui.items.disable.item")) + ))); // Create the itemstack + ItemMeta metaDisable = itemStackDisable.getItemMeta(); + metaDisable.displayName( + MiniMessage.miniMessage().deserialize( + Objects.requireNonNull(plugin.getConfig().getString("messages.totem_select_gui.items.disable.name")) + ) + ); + + List loreDisable = new ArrayList<>(); + for (String s : plugin.getConfig().getStringList("messages.totem_select_gui.items.disable.lore")) { + loreDisable.add(MiniMessage.miniMessage().deserialize(s)); + } + + metaDisable.lore(loreDisable); + + itemStackDisable.setItemMeta(metaDisable); + + inv.setItem(7, itemStackDisable); + + // Set the center slot to the arena that was selected + ItemStack itemStackArena = new ItemStack(Objects.requireNonNull(Material.getMaterial( + Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.arena_item.item")) + ))); // Create the itemstack + + ItemMeta metaArena = itemStackArena.getItemMeta(); + metaArena.displayName( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.arena_item.name")) + .replace("%arena_name%", arena.getName()) + .replace("%arena_owner%", arena.getOwner())) + + ); + + List loreArena = new ArrayList<>(); + for (String s : plugin.getConfig().getStringList("messages.arena_select_gui.arena_item.lore")) { + loreArena.add(MiniMessage.miniMessage().deserialize(s.replace("%arena_name%", arena.getName()) + .replace("%arena_owner%", arena.getOwner()) + )); + } + + metaArena.lore(loreArena); + + inviter.openInventory(inv); + } + + @EventHandler + public void onClick(InventoryClickEvent e) { + if (e.getCurrentItem() == null || e.getCurrentItem().getType() == Material.AIR) { + return; + } // If the item doesn't exist or is air, return + + if (Objects.requireNonNull(e.getInventory()).getType() == InventoryType.PLAYER) { + return; + } // If the inventory is the player's inventory, return + + Player inviter = (Player) e.getWhoClicked(); // Get the player who clicked + + if (Objects.equals(e.getView().title().toString(), INVENTORY_NAME_ARENAS.toString())) { + e.setCancelled(true); // The inventory name is from the plugin, so cancel the event + + int slot = e.getSlot(); + Arena arena = selectingArenaCache.get(inviter).get(slot); // Get the specific player and then the arena from the cache + + InviteManager.Invite invite = plugin.getArenaModule().getInviteManager().selectingInvites.get(inviter); // Get the invite from the selectingInvites map + invite.arena = arena; // Set the arena in the invite + + + if (arena == null || arena.getState() != Arena.ArenaState.WAITING) { // Somehow arena doesn't work, purely debug + inviter.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.arena_not_ready")))) + ; + return; + } + + + openTotemEnabled(inviter, arena); + } + if (Objects.equals(e.getView().title().toString(), INVENTORY_NAME_TOTEMS.toString())) { + e.setCancelled(true); // The inventory name is from the plugin, so cancel the event + + int slot = e.getSlot(); + + // Get the invite from the selectingInvites map + InviteManager.Invite invite = plugin.getArenaModule().getInviteManager().selectingInvites.get(inviter); + + // Get the arena from the invite + Arena arena = invite.arena; + + // Set the totems in the invite + arena.totems = slot == 1; + + inviter.sendMessage( + MiniMessage.miniMessage().deserialize(Objects.requireNonNull(plugin.getConfig().getString("messages.pvp.invite.invite_sent")) + .replace("%player%", invite.invited.getName()))) + ; // Send the invite confirmation message + + String invite_message = Objects.requireNonNull(plugin.getConfig().getString("messages.arena_select_gui.invite_message")) + .replace("%inviter%", inviter.getName()) + .replace("%arena_name%", arena.getName()) + .replace("%winchance%", plugin.getArenaModule().calculateWinChance(inviter.getUniqueId(), invite.invited.getUniqueId()) + "%") + .replace("%totems%", arena.totems ? "enabled" : "disabled"); + // Get the invite message from the config and replace the placeholders + + Objects.requireNonNull(plugin.getServer().getPlayer(invite.invited.getUniqueId())).sendMessage(MiniMessage.miniMessage().deserialize(invite_message));// Send the actual message + + + invite.arena = arena; + + plugin.getArenaModule().getInviteManager().selectingInvites.remove(inviter); // Remove the invite from the selectingInvites map + plugin.getArenaModule().getInviteManager().invites.put(invite.invited, invite); // Put the invite in the invites map + + inviter.closeInventory(); // Close the inventory + } + } +} diff --git a/src/main/java/com/ceraia/modules/ceraia/CeraiaModule.kt b/src/main/java/com/ceraia/modules/ceraia/CeraiaModule.kt new file mode 100644 index 0000000..4ea682e --- /dev/null +++ b/src/main/java/com/ceraia/modules/ceraia/CeraiaModule.kt @@ -0,0 +1,18 @@ +package com.ceraia.modules.ceraia + +import com.ceraia.Ceraia +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.command.TabCompleter +import org.bukkit.entity.Player +import org.bukkit.event.Listener +import org.bukkit.util.StringUtil + +class CeraiaModule(private val plugin: Ceraia) : Listener { + init { + + } +} diff --git a/src/main/java/com/ceraia/managers/PlayerManager.kt b/src/main/java/com/ceraia/modules/ceraia/managers/PlayerManager.kt similarity index 77% rename from src/main/java/com/ceraia/managers/PlayerManager.kt rename to src/main/java/com/ceraia/modules/ceraia/managers/PlayerManager.kt index 2fd7ebd..715b30e 100644 --- a/src/main/java/com/ceraia/managers/PlayerManager.kt +++ b/src/main/java/com/ceraia/modules/ceraia/managers/PlayerManager.kt @@ -1,7 +1,7 @@ -package com.ceraia.managers +package com.ceraia.modules.ceraia.managers import com.ceraia.Ceraia -import com.ceraia.types.CeraiaPlayer +import com.ceraia.modules.ceraia.types.CeraiaPlayer import org.bukkit.Bukkit import org.bukkit.configuration.file.FileConfiguration import org.bukkit.configuration.file.YamlConfiguration @@ -11,7 +11,7 @@ import java.io.IOException import java.util.* class PlayerManager(private val plugin: Ceraia) { - val doublePlayers: MutableList = mutableListOf() + val ceraiaPlayers: MutableList = mutableListOf() init { // Load arenaPlayers @@ -27,7 +27,7 @@ class PlayerManager(private val plugin: Ceraia) { } - val doublePlayer = CeraiaPlayer( + val ceraiaPlayer = CeraiaPlayer( plugin, config.getString("name") ?: throw IllegalArgumentException("Name cannot be null"), config.getString("race", "human").toString(), @@ -39,27 +39,29 @@ class PlayerManager(private val plugin: Ceraia) { config.getBoolean("pvpbanned", false), config.getInt("wins", 0), config.getInt("losses", 0), + config.getStringList("parents"), + config.getStringList("children"), file ) - doublePlayers.add(doublePlayer) + ceraiaPlayers.add(ceraiaPlayer) } } fun getCeraiaPlayer(playerUUID: UUID): CeraiaPlayer { - return doublePlayers.find { it.uuid == playerUUID } ?: createNewCeraiaPlayer(playerUUID).also { - doublePlayers.add(it) + return ceraiaPlayers.find { it.uuid == playerUUID } ?: createNewCeraiaPlayer(playerUUID).also { + ceraiaPlayers.add(it) } } fun getCeraiaPlayer(playerName: String): CeraiaPlayer { - return doublePlayers.find { it.name == playerName } + return ceraiaPlayers.find { it.name == playerName } ?: createNewCeraiaPlayer(Bukkit.getPlayer(playerName)?.uniqueId ?: throw IllegalArgumentException("Player not found")) - .also { doublePlayers.add(it) } + .also { ceraiaPlayers.add(it) } } fun getCeraiaPlayer(player: Player): CeraiaPlayer { - return doublePlayers.find { it.uuid == player.uniqueId } ?: createNewCeraiaPlayer(player.uniqueId).also { - doublePlayers.add(it) + return ceraiaPlayers.find { it.uuid == player.uniqueId } ?: createNewCeraiaPlayer(player.uniqueId).also { + ceraiaPlayers.add(it) } } @@ -84,6 +86,8 @@ class PlayerManager(private val plugin: Ceraia) { set("wins", 0) set("losses", 0) set("logs", mutableListOf()) + set("parents", mutableListOf()) + set("children", mutableListOf()) } config.save(configFile) @@ -99,6 +103,8 @@ class PlayerManager(private val plugin: Ceraia) { false, 0, 0, + mutableListOf(), + mutableListOf(), configFile ) } catch (e: IOException) { @@ -115,21 +121,23 @@ class PlayerManager(private val plugin: Ceraia) { false, 0, 0, + mutableListOf(), + mutableListOf(), configFile ) } } fun getPlayer(uniqueId: UUID): CeraiaPlayer? { - return doublePlayers.find { it.uuid == uniqueId } + return ceraiaPlayers.find { it.uuid == uniqueId } } fun getPlayer(name: String): CeraiaPlayer? { - return doublePlayers.find { it.name.equals(name, ignoreCase = true) } + return ceraiaPlayers.find { it.name.equals(name, ignoreCase = true) } } fun savePlayers() { plugin.logger.info("Saving players...") - doublePlayers.forEach { it.savePlayer() } + ceraiaPlayers.forEach { it.savePlayer() } } } diff --git a/src/main/java/com/ceraia/types/DoublePlayer.kt b/src/main/java/com/ceraia/modules/ceraia/types/DoublePlayer.kt similarity index 73% rename from src/main/java/com/ceraia/types/DoublePlayer.kt rename to src/main/java/com/ceraia/modules/ceraia/types/DoublePlayer.kt index f10a488..64b8d23 100644 --- a/src/main/java/com/ceraia/types/DoublePlayer.kt +++ b/src/main/java/com/ceraia/modules/ceraia/types/DoublePlayer.kt @@ -1,4 +1,4 @@ -package com.ceraia.types +package com.ceraia.modules.ceraia.types import com.ceraia.Ceraia import org.bukkit.configuration.file.FileConfiguration @@ -19,9 +19,10 @@ class CeraiaPlayer( private var pvpBanned: Boolean, var wins: Int, var losses: Int, - private val configFile: File + private val parents: MutableList, + private var children: MutableList, + private val configFile: File, ) { - fun getUUID(): UUID = uuid fun togglePvpBan(): Boolean { @@ -47,6 +48,8 @@ class CeraiaPlayer( config.set("pvpbanned", pvpBanned) config.set("wins", wins) config.set("losses", losses) + config.set("parents", parents) + config.set("children", children) try { config.save(configFile) @@ -60,6 +63,22 @@ class CeraiaPlayer( savePlayer() } + fun disown(name: String){ + children.remove(name) + parents.remove(name) + savePlayer() + } + + fun addChild(name: String){ + children.add(name) + savePlayer() + } + + fun addParent(name: String){ + parents.add(name) + savePlayer() + } + fun marry(name: String) { marriedName = name savePlayer() @@ -80,4 +99,14 @@ class CeraiaPlayer( this.race = race savePlayer() } -} + + fun addWin() { + wins++ + savePlayer() + } + + fun addLoss() { + losses++ + savePlayer() + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4564b1f..819894f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,6 +6,26 @@ description: Ceraia, the utility plugin. author: Axodouble website: https://github.com/Ceraia/Minecraft commands: + pvp: + description: All pvp related commands. + usage: /pvp + arena: + description: All arena related commands. + usage: /arena + gvg: + description: All gvg related commands. + usage: /gvg + leaderboard: + description: All leaderboard related commands. + usage: /leaderboard + aliases: + - top + profile: + description: All profile related commands. + usage: /profile + stats: + description: All stats related commands. + usage: /stats version: description: Show the version of the plugin. usage: /version @@ -38,44 +58,44 @@ commands: description: A command to change your race usage: /race permissions : - double.discord: + ceraia.discord: description: "Access to the /discord command that is set in the config.yml" default: true - double.time: + ceraia.time: description: "Access to the /day and /night commands" default: op children: - double.time.day: + ceraia.time.day: description: "Access to the /day command" default: op - double.time.night: + ceraia.time.night: description: "Access to the /night command" default: op - double.time.*: + ceraia.time.*: description: "Access to all time commands" default: op - double.sit: + ceraia.sit: description: "Access to the /sit command and to sit on blocks" default: true - double.marry: + ceraia.marry: description: "Access to the /marry command" default: true - double.jump: + ceraia.jump: description: "Access to the /jump command" default: op - double.races: + ceraia.races: description: "Access to the /race command" default: true children: - double.races.become: + ceraia.races.become: description: "Access to become a race with /race become " default: true - double.races.become.*: + ceraia.races.become.*: description: "Access to specific races" default: true - double.races.reload: + ceraia.races.reload: description: "Access to reload the races" default: op - double.races.restore: + ceraia.races.restore: description: "Access to restore all races" default: op \ No newline at end of file