From def95245de6b27667f17df066664d49f0f6051bb Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Thu, 5 Dec 2024 13:20:46 +0000 Subject: [PATCH] Improvements to arena joining - Competitions with players are prioritized now when running / join without specifying a map - Added option to config to allow for / join without a map to randomly select a map - Added a config updater to automatically include new config options to old configs whenever they are added --- .../org/battleplugins/arena/BattleArena.java | 4 +- .../arena/BattleArenaConfig.java | 28 ++++++++ .../arena/command/ArenaCommandExecutor.java | 30 +++++--- .../arena/competition/CompetitionManager.java | 13 +++- .../arena/config/ArenaConfigParser.java | 72 +++++++++++++++++++ .../battleplugins/arena/config/Updater.java | 25 +++++++ .../arena/config/updater/ConfigUpdater.java | 20 ++++++ .../arena/config/updater/UpdaterStep.java | 17 +++++ .../org/battleplugins/arena/util/Version.java | 4 +- plugin/src/main/resources/config.yml | 8 ++- 10 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 plugin/src/main/java/org/battleplugins/arena/config/Updater.java create mode 100644 plugin/src/main/java/org/battleplugins/arena/config/updater/ConfigUpdater.java create mode 100644 plugin/src/main/java/org/battleplugins/arena/config/updater/UpdaterStep.java diff --git a/plugin/src/main/java/org/battleplugins/arena/BattleArena.java b/plugin/src/main/java/org/battleplugins/arena/BattleArena.java index d474b857..7102dfb7 100644 --- a/plugin/src/main/java/org/battleplugins/arena/BattleArena.java +++ b/plugin/src/main/java/org/battleplugins/arena/BattleArena.java @@ -79,7 +79,9 @@ public class BattleArena extends JavaPlugin implements LoggerHolder { private Path arenasPath; - private boolean debugMode; + // Set to true before config is loaded in the event that the config + // fails to load and the additional debug information is required + private boolean debugMode = true; @Override public void onLoad() { diff --git a/plugin/src/main/java/org/battleplugins/arena/BattleArenaConfig.java b/plugin/src/main/java/org/battleplugins/arena/BattleArenaConfig.java index fd0901c8..8077418e 100644 --- a/plugin/src/main/java/org/battleplugins/arena/BattleArenaConfig.java +++ b/plugin/src/main/java/org/battleplugins/arena/BattleArenaConfig.java @@ -2,6 +2,9 @@ import org.battleplugins.arena.competition.event.EventOptions; import org.battleplugins.arena.config.ArenaOption; +import org.battleplugins.arena.config.Updater; +import org.battleplugins.arena.config.updater.ConfigUpdater; +import org.battleplugins.arena.config.updater.UpdaterStep; import java.util.List; import java.util.Map; @@ -9,6 +12,7 @@ /** * Represents the BattleArena configuration. */ +@Updater(BattleArenaConfig.Updater.class) public class BattleArenaConfig { @ArenaOption(name = "config-version", description = "The version of the config.", required = true) @@ -23,6 +27,9 @@ public class BattleArenaConfig { @ArenaOption(name = "max-dynamic-maps", description = "The maximum number of dynamic maps an Arena can have allocated at once.", required = true) private int maxDynamicMaps; + @ArenaOption(name = "randomized-arena-join", description = "Whether players should be randomly placed in an Arena when joining without specifying a map.", required = true) + private boolean randomizedArenaJoin; + @ArenaOption(name = "disabled-modules", description = "Modules that are disabled by default.") private List disabledModules; @@ -48,6 +55,10 @@ public int getMaxDynamicMaps() { return this.maxDynamicMaps; } + public boolean isRandomizedArenaJoin() { + return this.randomizedArenaJoin; + } + public List getDisabledModules() { return this.disabledModules == null ? List.of() : List.copyOf(this.disabledModules); } @@ -59,4 +70,21 @@ public Map> getEvents() { public boolean isDebugMode() { return this.debugMode; } + + public static class Updater implements ConfigUpdater { + + @Override + public Map> buildUpdaters() { + return Map.of( + "3.1", (config, instance) -> { + config.set("randomized-arena-join", false); + config.setComments("randomized-arena-join", List.of( + "Whether joining an arena using / join without specifying a map should", + "randomly pick an arena, rather than joining the most convenient one. Competitions", + "with players waiting will always be prioritized though, even with this setting", + "enabled." + )); + }); + } + } } diff --git a/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java b/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java index caed5af0..e11b5d1a 100644 --- a/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java +++ b/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java @@ -10,6 +10,7 @@ import org.battleplugins.arena.competition.PlayerRole; import org.battleplugins.arena.competition.map.CompetitionMap; import org.battleplugins.arena.competition.map.LiveCompetitionMap; +import org.battleplugins.arena.competition.map.MapType; import org.battleplugins.arena.competition.phase.CompetitionPhase; import org.battleplugins.arena.competition.phase.CompetitionPhaseType; import org.battleplugins.arena.competition.phase.PhaseManager; @@ -26,6 +27,7 @@ import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.List; @@ -34,6 +36,19 @@ import java.util.Set; public class ArenaCommandExecutor extends BaseCommandExecutor { + private static final CompetitionMap RANDOM_MAP_MARKER = new CompetitionMap() { + + @Override + public String getName() { + return "marker"; + } + + @Override + public MapType getType() { + return MapType.STATIC; + } + }; + protected final Arena arena; public ArenaCommandExecutor(Arena arena) { @@ -48,13 +63,7 @@ public ArenaCommandExecutor(String parentCommand, Arena arena) { @ArenaCommand(commands = { "join", "j" }, description = "Join an arena.", permissionNode = "join") public void join(Player player) { - List maps = this.arena.getPlugin().getMaps(this.arena); - if (maps.isEmpty()) { - Messages.NO_OPEN_ARENAS.send(player); - return; - } - - this.join(player, maps.iterator().next()); + this.join(player, RANDOM_MAP_MARKER); } @ArenaCommand(commands = { "join", "j" }, description = "Join an arena.", permissionNode = "join.map") @@ -69,7 +78,7 @@ public void join(Player player, @Argument(name = "map") CompetitionMap map) { return; } - List> competitions = this.arena.getPlugin().getCompetitions(this.arena, map.getName()); + List> competitions = map == RANDOM_MAP_MARKER ? this.arena.getPlugin().getCompetitions(this.arena) : this.arena.getPlugin().getCompetitions(this.arena, map.getName()); this.arena.getPlugin().findJoinableCompetition(competitions, player, PlayerRole.PLAYING).whenCompleteAsync((result, e) -> { if (e != null) { Messages.ARENA_ERROR.send(player, e.getMessage()); @@ -83,6 +92,11 @@ public void join(Player player, @Argument(name = "map") CompetitionMap map) { Messages.ARENA_JOINED.send(player, competition.getMap().getName()); } else { + if (map == RANDOM_MAP_MARKER) { + Messages.NO_OPEN_ARENAS.send(player); + return; + } + // Try and create a dynamic competition if possible this.arena.getPlugin() .getOrCreateCompetition(this.arena, player, PlayerRole.PLAYING, map.getName()) diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java b/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java index 6cdd8026..d113e669 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java @@ -120,12 +120,21 @@ public CompletableFuture findJoinableCompetition(List findJoinableCompetition(List> competitions, Player player, PlayerRole role, @Nullable JoinResult lastResult) { + private CompletableFuture findJoinableCompetition(List> competitions, Player player, PlayerRole role, @Nullable JoinResult lastResult) { if (competitions.isEmpty()) { return CompletableFuture.completedFuture(new CompetitionResult(null, lastResult == null ? JoinResult.NOT_JOINABLE : lastResult)); } - Competition competition = competitions.get(0); + if (this.plugin.getMainConfig().isRandomizedArenaJoin()) { + competitions = new ArrayList<>(competitions); + Collections.shuffle(competitions); + } + + // Select the competition with the most number of players + Competition competition = competitions.stream() + .max(Comparator.comparingInt(Competition::getAlivePlayerCount)) + .orElse(null); + CompletableFuture result = competition.canJoin(player, role); JoinResult joinResult = result.join(); if (joinResult == JoinResult.SUCCESS) { diff --git a/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java b/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java index accda881..ba282dda 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java @@ -1,11 +1,17 @@ package org.battleplugins.arena.config; import org.apache.commons.lang3.reflect.FieldUtils; +import org.battleplugins.arena.BattleArena; import org.battleplugins.arena.config.context.ContextProvider; +import org.battleplugins.arena.config.updater.ConfigUpdater; +import org.battleplugins.arena.config.updater.UpdaterStep; +import org.battleplugins.arena.util.Version; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.MemoryConfiguration; +import org.bukkit.configuration.file.FileConfiguration; import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; @@ -13,6 +19,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -44,6 +51,8 @@ public static T newInstance(Class type, ConfigurationSection configuratio public static T newInstance(@Nullable Path sourceFile, Class type, ConfigurationSection configuration, @Nullable Object scope, @Nullable Object id) throws ParseException { T instance = newClassInstance(sourceFile, type); + updateConfig(type, configuration, instance, sourceFile); + try { populateFields(sourceFile, instance, configuration, scope, id); if (instance instanceof PostProcessable postProcessable) { @@ -530,6 +539,67 @@ private static List toMemorySections(List list) { return sections; } + @SuppressWarnings("unchecked") + private static void updateConfig(Class type, ConfigurationSection configuration, T instance, @Nullable Path sourceFile) throws ParseException { + // Check if the config has an updater + if (!type.isAnnotationPresent(Updater.class)) { + return; + } + + Updater updater = type.getDeclaredAnnotation(Updater.class); + try { + // Build the updaters and update the config if applicable + ConfigUpdater configUpdater = (ConfigUpdater) updater.value().getConstructor().newInstance(); + Map> updaters = configUpdater.buildUpdaters(); + List> entries = updaters.entrySet().stream() + .map(entry -> new UpdaterEntry<>(Version.of(entry.getKey()), entry.getValue())) + .sorted(Comparator.comparing(UpdaterEntry::version)) + .toList(); + + // No entries - no need to update + if (entries.isEmpty()) { + return; + } + + Version configVersion = Version.of(configuration.getString("config-version")); + Version latestVersion = entries.get(entries.size() - 1).version(); + + // If the config version is equal to (or greater?) than the latest version, we don't need to update + if (!configVersion.isGreaterThanOrEqualTo(latestVersion)) { + // Otherwise, we need to update + boolean updatersRan = false; + for (UpdaterEntry entry : entries) { + if (configVersion.isLessThan(entry.version())) { + entry.step().update(configuration, instance); + + // Set the new config version + configuration.set("config-version", entry.version().toString()); + + BattleArena.getInstance().info("Updated config {} to version {}", type.getSimpleName(), entry.version()); + updatersRan = true; + } + } + + if (updatersRan) { + // Save config + if (configuration instanceof FileConfiguration fileConfig) { + try { + fileConfig.save(sourceFile.toFile()); + } catch (IOException e) { + throw new ParseException("Failed to save configuration file " + sourceFile + " after processing updates", e) + .cause(ParseException.Cause.INTERNAL_ERROR) + .sourceFile(sourceFile); + } + } + } + } + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new ParseException("Failed to instantiate updater for class " + type.getName(), e) + .cause(ParseException.Cause.INTERNAL_ERROR) + .sourceFile(sourceFile); + } + } + private static ConfigurationSection toMemorySection(Map map) { MemoryConfiguration memoryConfig = new MemoryConfiguration(); memoryConfig.addDefaults(map); @@ -539,4 +609,6 @@ private static ConfigurationSection toMemorySection(Map map) { public interface Parser { T parse(Object object) throws ParseException; } + + private record UpdaterEntry(Version version, UpdaterStep step) {} } diff --git a/plugin/src/main/java/org/battleplugins/arena/config/Updater.java b/plugin/src/main/java/org/battleplugins/arena/config/Updater.java new file mode 100644 index 00000000..bb06bc06 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/config/Updater.java @@ -0,0 +1,25 @@ +package org.battleplugins.arena.config; + +import org.battleplugins.arena.config.updater.ConfigUpdater; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used over a configurable class to specify + * which updater should be used to update the configuration + * across versions. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Updater { + + /** + * The class of the updater. + * + * @return the class of the updater + */ + Class> value(); +} diff --git a/plugin/src/main/java/org/battleplugins/arena/config/updater/ConfigUpdater.java b/plugin/src/main/java/org/battleplugins/arena/config/updater/ConfigUpdater.java new file mode 100644 index 00000000..672798c5 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/config/updater/ConfigUpdater.java @@ -0,0 +1,20 @@ +package org.battleplugins.arena.config.updater; + +import java.util.Map; + +/** + * An updater for updating a config file across versions. + */ +public interface ConfigUpdater { + + /** + * Builds the updater for the config file. + *

+ * The key of the map will be the version to update + * to, and the value will be the updater step to update + * the config file. + * + * @return the updater for the config file + */ + Map> buildUpdaters(); +} diff --git a/plugin/src/main/java/org/battleplugins/arena/config/updater/UpdaterStep.java b/plugin/src/main/java/org/battleplugins/arena/config/updater/UpdaterStep.java new file mode 100644 index 00000000..1ff913b7 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/config/updater/UpdaterStep.java @@ -0,0 +1,17 @@ +package org.battleplugins.arena.config.updater; + +import org.bukkit.configuration.ConfigurationSection; + +/** + * An updater step for updating a config file across versions. + */ +public interface UpdaterStep { + + /** + * Updates the config file to the specified version. + * + * @param config the config + * @param instance the config instance + */ + void update(ConfigurationSection config, T instance); +} diff --git a/plugin/src/main/java/org/battleplugins/arena/util/Version.java b/plugin/src/main/java/org/battleplugins/arena/util/Version.java index bbf939a7..b2f47047 100644 --- a/plugin/src/main/java/org/battleplugins/arena/util/Version.java +++ b/plugin/src/main/java/org/battleplugins/arena/util/Version.java @@ -28,7 +28,7 @@ public class Version implements Comparable { private Version(String version) { this.version = version; - this.tester = () -> Bukkit.getPluginManager().isPluginEnabled("BattleArena"); + this.tester = () -> true; } private Version(Plugin plugin) { @@ -319,4 +319,4 @@ public static Version of(Plugin plugin) { public static Version getServerVersion() { return SERVER_VERSION; } -} \ No newline at end of file +} diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml index 84061b28..77e2c68e 100644 --- a/plugin/src/main/resources/config.yml +++ b/plugin/src/main/resources/config.yml @@ -5,7 +5,7 @@ # Support: https://discord.gg/tMVPVJf # GitHub: https://github.com/BattlePlugins/BattleArena # ----------------- -config-version: 3.0 # The config version, do not change! +config-version: 3.1 # The config version, do not change! # Whether player inventories should be backed up when joining competitions. backup-inventories: true @@ -17,6 +17,12 @@ max-backups: 5 # Set to -1 to disable this limit. max-dynamic-maps: 5 +# Whether joining an arena using / join without specifying a map should +# randomly pick an arena, rather than joining the most convenient one. Competitions +# with players waiting will always be prioritized though, even with this setting +# enabled. +randomized-arena-join: false + # Modules that are disabled by default. BattleArena comes pre-installed with # multiple modules that can be disabled below if their behavior is not desired disabled-modules: []