From 38ae967f636195adb41edfc837a934eaa86b11f7 Mon Sep 17 00:00:00 2001 From: RednedEpic Date: Sat, 29 Jun 2024 18:45:07 -0500 Subject: [PATCH] Further cleanups and add /ba reload along with memory leak fixes --- .../org/battleplugins/arena/BattleArena.java | 363 ++++++------------ .../arena/command/BACommandExecutor.java | 19 + .../arena/competition/CompetitionManager.java | 221 +++++++++++ .../arena/competition/LiveCompetition.java | 16 +- .../arena/competition/StatListener.java | 16 +- .../competition/event/EventScheduler.java | 3 +- .../arena/competition/phase/PhaseManager.java | 13 +- .../competition/victory/VictoryManager.java | 9 + .../arena/event/ArenaEventManager.java | 12 + .../arena/event/ArenaEventType.java | 2 + .../arena/event/BattleArenaReloadEvent.java | 38 ++ .../arena/event/BattleArenaReloadedEvent.java | 38 ++ .../event/player/ArenaLifeDepleteEvent.java | 4 + .../event/player/ArenaLivesExhaustEvent.java | 30 ++ .../arena/messages/Messages.java | 7 + .../arena/util/LoggerHolder.java | 40 ++ .../battleplugins/arena/util/UnitUtil.java | 14 + 17 files changed, 587 insertions(+), 258 deletions(-) create mode 100644 plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java create mode 100644 plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadEvent.java create mode 100644 plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadedEvent.java create mode 100644 plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLivesExhaustEvent.java create mode 100644 plugin/src/main/java/org/battleplugins/arena/util/LoggerHolder.java diff --git a/plugin/src/main/java/org/battleplugins/arena/BattleArena.java b/plugin/src/main/java/org/battleplugins/arena/BattleArena.java index c2590a01..3610de2c 100644 --- a/plugin/src/main/java/org/battleplugins/arena/BattleArena.java +++ b/plugin/src/main/java/org/battleplugins/arena/BattleArena.java @@ -3,31 +3,29 @@ import org.battleplugins.arena.command.BACommandExecutor; import org.battleplugins.arena.command.BaseCommandExecutor; import org.battleplugins.arena.competition.Competition; +import org.battleplugins.arena.competition.CompetitionManager; import org.battleplugins.arena.competition.CompetitionResult; import org.battleplugins.arena.competition.CompetitionType; -import org.battleplugins.arena.competition.JoinResult; -import org.battleplugins.arena.competition.LiveCompetition; import org.battleplugins.arena.competition.PlayerRole; import org.battleplugins.arena.competition.event.EventOptions; import org.battleplugins.arena.competition.event.EventScheduler; import org.battleplugins.arena.competition.event.EventType; import org.battleplugins.arena.competition.map.LiveCompetitionMap; import org.battleplugins.arena.competition.map.MapType; -import org.battleplugins.arena.competition.phase.CompetitionPhaseType; -import org.battleplugins.arena.competition.phase.phases.VictoryPhase; import org.battleplugins.arena.config.ArenaConfigParser; import org.battleplugins.arena.config.ParseException; import org.battleplugins.arena.event.BattleArenaPostInitializeEvent; import org.battleplugins.arena.event.BattleArenaPreInitializeEvent; +import org.battleplugins.arena.event.BattleArenaReloadEvent; +import org.battleplugins.arena.event.BattleArenaReloadedEvent; import org.battleplugins.arena.event.BattleArenaShutdownEvent; -import org.battleplugins.arena.event.arena.ArenaCreateCompetitionEvent; -import org.battleplugins.arena.event.player.ArenaLeaveEvent; import org.battleplugins.arena.messages.MessageLoader; import org.battleplugins.arena.module.ArenaModuleContainer; import org.battleplugins.arena.module.ArenaModuleLoader; import org.battleplugins.arena.module.ModuleLoadException; import org.battleplugins.arena.team.ArenaTeams; import org.battleplugins.arena.util.CommandInjector; +import org.battleplugins.arena.util.LoggerHolder; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; import org.bukkit.configuration.Configuration; @@ -37,7 +35,9 @@ import org.bukkit.event.Listener; import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; import java.io.File; import java.io.IOException; @@ -45,7 +45,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -60,16 +59,16 @@ /** * The main class for BattleArena. */ -public class BattleArena extends JavaPlugin implements Listener { +public class BattleArena extends JavaPlugin implements Listener, LoggerHolder { private static BattleArena instance; final Map> arenaTypes = new HashMap<>(); final Map arenas = new HashMap<>(); private final Map>> arenaMaps = new HashMap<>(); - private final Map>> competitions = new HashMap<>(); - private final Map arenaLoaders = new HashMap<>(); + + private final CompetitionManager competitionManager = new CompetitionManager(this); private final EventScheduler eventScheduler = new EventScheduler(); private BattleArenaConfig config; @@ -77,7 +76,6 @@ public class BattleArena extends JavaPlugin implements Listener { private ArenaTeams teams; private Path arenasPath; - private Path modulesPath; private boolean debugMode; @@ -87,9 +85,9 @@ public void onLoad() { Path dataFolder = this.getDataFolder().toPath(); this.arenasPath = dataFolder.resolve("arenas"); - this.modulesPath = dataFolder.resolve("modules"); + Path modulesPath = dataFolder.resolve("modules"); - this.moduleLoader = new ArenaModuleLoader(this, this.getClassLoader(), this.modulesPath); + this.moduleLoader = new ArenaModuleLoader(this, this.getClassLoader(), modulesPath); try { this.moduleLoader.loadModules(); } catch (IOException e) { @@ -103,6 +101,10 @@ public void onLoad() { public void onEnable() { Bukkit.getPluginManager().registerEvents(this, this); + this.enable(); + } + + private void enable() { // Copy our default configs this.saveDefaultConfig(); @@ -209,14 +211,30 @@ public void onEnable() { public void onDisable() { new BattleArenaShutdownEvent(this).callEvent(); + this.disable(); + } + + private void disable() { // Close all active competitions - this.completeAllActiveCompetitions(); + this.competitionManager.completeAllActiveCompetitions(); // Stop all scheduled events - this.eventScheduler.stopAllScheduledEvents(); + this.eventScheduler.stopAllEvents(); // Clear dynamic maps this.clearDynamicMaps(); + + this.arenaTypes.clear(); + for (Arena arena : this.arenas.values()) { + arena.getEventManager().unregisterAll(); + } + + this.arenas.clear(); + this.arenaMaps.clear(); + this.arenaLoaders.clear(); + + this.config = null; + this.teams = null; } @EventHandler @@ -228,6 +246,10 @@ public void onServerLoad(ServerLoadEvent event) { // arena config files will be valid. new BattleArenaPostInitializeEvent(this).callEvent(); + this.postInitialize(); + } + + private void postInitialize() { // Load all arenas this.loadArenas(); @@ -267,6 +289,29 @@ public void onServerLoad(ServerLoadEvent event) { } } + public void reload() { + new BattleArenaReloadEvent(this).callEvent(); + + this.disable(); + this.enable(); + this.postInitialize(); + + new BattleArenaReloadedEvent(this).callEvent(); + } + + public boolean isInArena(Player player) { + return ArenaPlayer.getArenaPlayer(player) != null; + } + + public Optional arena(String name) { + return Optional.ofNullable(this.arenas.get(name)); + } + + @Nullable + public Arena getArena(String name) { + return this.arenas.get(name); + } + public void registerArena(String name, Class arena) { this.registerArena(name, arena, () -> { try { @@ -283,6 +328,32 @@ public void registerArena(String name, Class arenaClass, Su this.arenaTypes.put(name, arenaClass); } + public List> getMaps(Arena arena) { + List> maps = this.arenaMaps.get(arena); + if (maps == null) { + return List.of(); + } + + return List.copyOf(maps); + } + + public Optional> map(Arena arena, String name) { + return Optional.ofNullable(this.getMap(arena, name)); + } + + @Nullable + public LiveCompetitionMap getMap(Arena arena, String name) { + List> maps = this.arenaMaps.get(arena); + if (maps == null) { + return null; + } + + return maps.stream() + .filter(map -> map.getName().equals(name)) + .findFirst() + .orElse(null); + } + public void addArenaMap(Arena arena, LiveCompetitionMap map) { this.arenaMaps.computeIfAbsent(arena, k -> new ArrayList<>()).add(map); } @@ -291,7 +362,11 @@ public void removeArenaMap(Arena arena, LiveCompetitionMap map) { this.arenaMaps.computeIfAbsent(arena, k -> new ArrayList<>()).remove(map); // If the map is removed, also remove the competition if applicable - this.competitions.computeIfAbsent(arena, k -> new ArrayList<>()).removeIf(competition -> competition.getMap() == map); + for (Competition competition : this.competitionManager.getCompetitions(arena)) { + if (competition.getMap() == map) { + this.competitionManager.removeCompetition(arena, competition); + } + } // Now remove the map from the file system Path mapPath = arena.getMapsPath().resolve(map.getName().toLowerCase(Locale.ROOT) + ".yml"); @@ -302,66 +377,34 @@ public void removeArenaMap(Arena arena, LiveCompetitionMap map) { } } - public void addCompetition(Arena arena, Competition competition) { - this.competitions.computeIfAbsent(arena, k -> new ArrayList<>()).add(competition); - - this.getServer().getPluginManager().callEvent(new ArenaCreateCompetitionEvent(arena, competition)); + public List> getCompetitions(Arena arena) { + return this.competitionManager.getCompetitions(arena); } - @SuppressWarnings("unchecked") - public void removeCompetition(Arena arena, Competition competition) { - List> competitions = this.competitions.get(arena); - if (competitions == null) { - return; - } - - Set> phases = arena.getPhases(); - - // Check if we have a victory phase - CompetitionPhaseType> victoryPhase = null; - for (CompetitionPhaseType phase : phases) { - if (VictoryPhase.class.isAssignableFrom(phase.getPhaseType())) { - victoryPhase = (CompetitionPhaseType>) phase; - break; - } - } + public List> getCompetitions(Arena arena, String name) { + return this.competitionManager.getCompetitions(arena, name); + } - boolean removed = competitions.remove(competition); - if (removed && competition instanceof LiveCompetition liveCompetition) { - // De-reference any remaining resources - liveCompetition.getVictoryManager().end(true); + public CompletableFuture getOrCreateCompetition(Arena arena, Player player, PlayerRole role, @Nullable String name) { + return this.competitionManager.getOrCreateCompetition(arena, player, role, name); + } - if (victoryPhase != null && !(VictoryPhase.class.isAssignableFrom(liveCompetition.getPhase().getPhaseType()))) { - liveCompetition.getPhaseManager().setPhase(victoryPhase); + public CompletableFuture findJoinableCompetition(List> competitions, Player player, PlayerRole role) { + return this.competitionManager.findJoinableCompetition(competitions, player, role); + } - VictoryPhase phase = (VictoryPhase) liveCompetition.getPhaseManager().getCurrentPhase(); - phase.onDraw(); // Mark as a draw - } else { - // No victory phase - just forcefully kick every player - for (ArenaPlayer player : liveCompetition.getPlayers()) { - liveCompetition.leave(player, ArenaLeaveEvent.Cause.SHUTDOWN); - } - } - } + public void addCompetition(Arena arena, Competition competition) { + this.competitionManager.addCompetition(arena, competition); + } - competitions.remove(competition); - if (competition.getMap().getType() == MapType.DYNAMIC && competition.getMap() instanceof LiveCompetitionMap map) { - this.clearDynamicMap(map); - } + public void removeCompetition(Arena arena, Competition competition) { + this.competitionManager.removeCompetition(arena, competition); } public Path getMapsPath() { return this.getDataFolder().toPath().resolve("maps"); } - private void completeAllActiveCompetitions() { - for (Map.Entry>> entry : Map.copyOf(this.competitions).entrySet()) { - for (Competition competition : List.copyOf(entry.getValue())) { - this.removeCompetition(entry.getKey(), competition); - } - } - } - private void loadArenas() { // Register our arenas once ALL the plugins have loaded. This ensures that // all custom plugins adding their own arena types have been loaded. @@ -421,139 +464,6 @@ private void loadArenaMaps() { } } - public boolean isInArena(Player player) { - return ArenaPlayer.getArenaPlayer(player) != null; - } - - @Nullable - public Arena getArena(String name) { - return this.arenas.get(name); - } - - public List> getMaps(Arena arena) { - List> maps = this.arenaMaps.get(arena); - if (maps == null) { - return List.of(); - } - - return List.copyOf(maps); - } - - @Nullable - public LiveCompetitionMap getMap(Arena arena, String name) { - List> maps = this.arenaMaps.get(arena); - if (maps == null) { - return null; - } - - return maps.stream() - .filter(map -> map.getName().equals(name)) - .findFirst() - .orElse(null); - } - - public List> getCompetitions(Arena arena) { - return List.copyOf(this.competitions.getOrDefault(arena, List.of())); - } - - public List> getCompetitions(Arena arena, String name) { - List> competitions = BattleArena.getInstance().getCompetitions(arena); - return competitions.stream() - .filter(competition -> competition.getMap().getName().equals(name)) - .toList(); - } - - public CompletableFuture getOrCreateCompetition(Arena arena, Player player, PlayerRole role, @Nullable String name) { - // See if we can join any already open competitions - List> openCompetitions = this.getCompetitions(arena, name); - CompletableFuture joinableCompetition = this.findJoinableCompetition(openCompetitions, player, role); - return joinableCompetition.thenApplyAsync(result -> { - if (result.competition() != null) { - return result; - } - - CompetitionResult invalidResult = new CompetitionResult(null, !result.result().canJoin() ? result.result() : JoinResult.NOT_JOINABLE); - if (arena.getType() == CompetitionType.EVENT) { - // Cannot create non-requested dynamic competitions for events - return invalidResult; - } - - List> maps = this.arenaMaps.get(arena); - if (maps == null) { - // No maps, return - return invalidResult; - } - - // Ensure we have WorldEdit installed - if (this.getServer().getPluginManager().getPlugin("WorldEdit") == null) { - this.error("WorldEdit is required to create dynamic competitions! Not proceeding with creating a new dynamic competition."); - return invalidResult; - } - - // Check if we have exceeded the maximum number of dynamic maps - List> allCompetitions = this.getCompetitions(arena); - long dynamicMaps = allCompetitions.stream() - .map(Competition::getMap) - .filter(map -> map.getType() == MapType.DYNAMIC) - .count(); - - if (dynamicMaps >= this.config.getMaxDynamicMaps() && this.config.getMaxDynamicMaps() != -1) { - this.warn("Exceeded maximum number of dynamic maps for arena {}! Not proceeding with creating a new dynamic competition.", arena.getName()); - return invalidResult; - } - - // Create a new competition if possible - - if (name == null) { - // Shuffle results if map name is not requested - maps = new ArrayList<>(maps); - Collections.shuffle(maps); - } - - for (LiveCompetitionMap map : maps) { - if (map.getType() != MapType.DYNAMIC) { - continue; - } - - if ((name == null || map.getName().equals(name))) { - Competition competition = map.createDynamicCompetition(arena); - if (competition == null) { - this.warn("Failed to create dynamic competition for map {} in arena {}!", map.getName(), arena.getName()); - continue; - } - - this.addCompetition(arena, competition); - return new CompetitionResult(competition, JoinResult.SUCCESS); - } - } - - // No open competitions found or unable to create a new one - return invalidResult; - }, Bukkit.getScheduler().getMainThreadExecutor(this)); - } - - public CompletableFuture findJoinableCompetition(List> competitions, Player player, PlayerRole role) { - return this.findJoinableCompetition(competitions, player, role, null); - } - - 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); - CompletableFuture result = competition.canJoin(player, role); - JoinResult joinResult = result.join(); - if (joinResult == JoinResult.SUCCESS) { - return CompletableFuture.completedFuture(new CompetitionResult(competition, JoinResult.SUCCESS)); - } else { - List> remainingCompetitions = new ArrayList<>(competitions); - remainingCompetitions.remove(competition); - - return this.findJoinableCompetition(remainingCompetitions, player, role, joinResult); - } - } - public EventScheduler getEventScheduler() { return this.eventScheduler; } @@ -566,15 +476,15 @@ public ArenaTeams getTeams() { return this.teams; } + public Optional> module(String id) { + return Optional.ofNullable(this.getModule(id)); + } + @Nullable public ArenaModuleContainer getModule(String id) { return this.moduleLoader.getModule(id); } - public Optional> module(String id) { - return Optional.ofNullable(this.getModule(id)); - } - public List> getModules() { return this.moduleLoader.getModules(); } @@ -608,60 +518,19 @@ private void clearDynamicMaps() { } } - private void clearDynamicMap(LiveCompetitionMap map) { - if (map.getType() != MapType.DYNAMIC) { - return; - } - - Bukkit.unloadWorld(map.getWorld(), false); - - try { - try (Stream pathsToDelete = Files.walk(map.getWorld().getWorldFolder().toPath())) { - for (Path path : pathsToDelete.sorted(Comparator.reverseOrder()).toList()) { - Files.deleteIfExists(path); - } - } - } catch (IOException e) { - this.error("Failed to delete dynamic map {}", map.getName(), e); - } - } - + @Override public boolean isDebugMode() { return this.debugMode; } + @Override public void setDebugMode(boolean debugMode) { this.debugMode = debugMode; } - public void info(String message) { - this.getSLF4JLogger().info(message); - } - - public void info(String message, Object... args) { - this.getSLF4JLogger().info(message, args); - } - - public void error(String message) { - this.getSLF4JLogger().error(message); - } - - public void error(String message, Object... args) { - this.getSLF4JLogger().error(message, args); - } - - public void warn(String message) { - this.getSLF4JLogger().warn(message); - } - - public void warn(String message, Object... args) { - this.getSLF4JLogger().warn(message, args); - } - - public void debug(String message, Object... args) { - if (this.debugMode) { - this.getSLF4JLogger().info("[DEBUG] " + message, args); - } + @Override + public @NotNull Logger getSLF4JLogger() { + return super.getSLF4JLogger(); } public static BattleArena getInstance() { diff --git a/plugin/src/main/java/org/battleplugins/arena/command/BACommandExecutor.java b/plugin/src/main/java/org/battleplugins/arena/command/BACommandExecutor.java index 4bf009f7..3535061f 100644 --- a/plugin/src/main/java/org/battleplugins/arena/command/BACommandExecutor.java +++ b/plugin/src/main/java/org/battleplugins/arena/command/BACommandExecutor.java @@ -11,6 +11,7 @@ import org.battleplugins.arena.messages.Messages; import org.battleplugins.arena.util.InventoryBackup; import org.battleplugins.arena.util.OptionSelector; +import org.battleplugins.arena.util.UnitUtil; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -20,6 +21,7 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; public class BACommandExecutor extends BaseCommandExecutor { @@ -162,4 +164,21 @@ public void debug(Player player) { BattleArena.getInstance().setDebugMode(!BattleArena.getInstance().isDebugMode()); Messages.DEBUG_MODE_SET_TO.send(player, Boolean.toString(BattleArena.getInstance().isDebugMode())); } + + @ArenaCommand(commands = "reload", description = "Reloads the plugin.", permissionNode = "reload") + public void reload(Player player) { + Messages.STARTING_RELOAD.send(player); + long start = System.currentTimeMillis(); + + try { + BattleArena.getInstance().reload(); + } catch (Exception e) { + Messages.RELOAD_FAILED.send(player); + BattleArena.getInstance().error("Failed to reload plugin", e); + return; + } + + long end = System.currentTimeMillis(); + Messages.RELOAD_COMPLETE.send(player, UnitUtil.toUnitString(player, end - start, TimeUnit.MILLISECONDS)); + } } diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java b/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java new file mode 100644 index 00000000..324c2cde --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/competition/CompetitionManager.java @@ -0,0 +1,221 @@ +package org.battleplugins.arena.competition; + +import org.battleplugins.arena.Arena; +import org.battleplugins.arena.ArenaPlayer; +import org.battleplugins.arena.BattleArena; +import org.battleplugins.arena.competition.map.LiveCompetitionMap; +import org.battleplugins.arena.competition.map.MapType; +import org.battleplugins.arena.competition.phase.CompetitionPhaseType; +import org.battleplugins.arena.competition.phase.phases.VictoryPhase; +import org.battleplugins.arena.event.arena.ArenaCreateCompetitionEvent; +import org.battleplugins.arena.event.player.ArenaLeaveEvent; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +public class CompetitionManager { + private final Map>> competitions = new HashMap<>(); + + private final BattleArena plugin; + + public CompetitionManager(BattleArena plugin) { + this.plugin = plugin; + } + + public List> getCompetitions(Arena arena) { + List> competitions = this.competitions.get(arena); + return competitions == null ? List.of() : List.copyOf(competitions); + } + + public List> getCompetitions(Arena arena, String name) { + List> competitions = this.getCompetitions(arena); + return competitions.stream() + .filter(competition -> competition.getMap().getName().equals(name)) + .toList(); + } + + public CompletableFuture getOrCreateCompetition(Arena arena, Player player, PlayerRole role, @Nullable String name) { + // See if we can join any already open competitions + List> openCompetitions = this.getCompetitions(arena, name); + CompletableFuture joinableCompetition = this.findJoinableCompetition(openCompetitions, player, role); + return joinableCompetition.thenApplyAsync(result -> { + if (result.competition() != null) { + return result; + } + + CompetitionResult invalidResult = new CompetitionResult(null, !result.result().canJoin() ? result.result() : JoinResult.NOT_JOINABLE); + if (arena.getType() == CompetitionType.EVENT) { + // Cannot create non-requested dynamic competitions for events + return invalidResult; + } + + List> maps = this.plugin.getMaps(arena); + if (maps == null) { + // No maps, return + return invalidResult; + } + + // Ensure we have WorldEdit installed + if (this.plugin.getServer().getPluginManager().getPlugin("WorldEdit") == null) { + this.plugin.error("WorldEdit is required to create dynamic competitions! Not proceeding with creating a new dynamic competition."); + return invalidResult; + } + + // Check if we have exceeded the maximum number of dynamic maps + List> allCompetitions = this.getCompetitions(arena); + long dynamicMaps = allCompetitions.stream() + .map(Competition::getMap) + .filter(map -> map.getType() == MapType.DYNAMIC) + .count(); + + if (dynamicMaps >= this.plugin.getMainConfig().getMaxDynamicMaps() && this.plugin.getMainConfig().getMaxDynamicMaps() != -1) { + this.plugin.warn("Exceeded maximum number of dynamic maps for arena {}! Not proceeding with creating a new dynamic competition.", arena.getName()); + return invalidResult; + } + + // Create a new competition if possible + + if (name == null) { + // Shuffle results if map name is not requested + maps = new ArrayList<>(maps); + Collections.shuffle(maps); + } + + for (LiveCompetitionMap map : maps) { + if (map.getType() != MapType.DYNAMIC) { + continue; + } + + if ((name == null || map.getName().equals(name))) { + Competition competition = map.createDynamicCompetition(arena); + if (competition == null) { + this.plugin.warn("Failed to create dynamic competition for map {} in arena {}!", map.getName(), arena.getName()); + continue; + } + + this.addCompetition(arena, competition); + return new CompetitionResult(competition, JoinResult.SUCCESS); + } + } + + // No open competitions found or unable to create a new one + return invalidResult; + }, Bukkit.getScheduler().getMainThreadExecutor(this.plugin)); + } + + public CompletableFuture findJoinableCompetition(List> competitions, Player player, PlayerRole role) { + return this.findJoinableCompetition(competitions, player, role, null); + } + + public 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); + CompletableFuture result = competition.canJoin(player, role); + JoinResult joinResult = result.join(); + if (joinResult == JoinResult.SUCCESS) { + return CompletableFuture.completedFuture(new CompetitionResult(competition, JoinResult.SUCCESS)); + } else { + List> remainingCompetitions = new ArrayList<>(competitions); + remainingCompetitions.remove(competition); + + return this.findJoinableCompetition(remainingCompetitions, player, role, joinResult); + } + } + + public void addCompetition(Arena arena, Competition competition) { + this.competitions.computeIfAbsent(arena, k -> new ArrayList<>()).add(competition); + this.plugin.getServer().getPluginManager().callEvent(new ArenaCreateCompetitionEvent(arena, competition)); + } + + @SuppressWarnings("unchecked") + public void removeCompetition(Arena arena, Competition competition) { + List> competitions = this.competitions.get(arena); + if (competitions == null) { + return; + } + + Set> phases = arena.getPhases(); + + // Check if we have a victory phase + CompetitionPhaseType> victoryPhase = null; + for (CompetitionPhaseType phase : phases) { + if (VictoryPhase.class.isAssignableFrom(phase.getPhaseType())) { + victoryPhase = (CompetitionPhaseType>) phase; + break; + } + } + + boolean removed = competitions.remove(competition); + if (removed && competition instanceof LiveCompetition liveCompetition) { + // De-reference any remaining resources + liveCompetition.getVictoryManager().end(true); + + if (victoryPhase != null && !(VictoryPhase.class.isAssignableFrom(liveCompetition.getPhase().getPhaseType()))) { + liveCompetition.getPhaseManager().setPhase(victoryPhase); + + VictoryPhase phase = (VictoryPhase) liveCompetition.getPhaseManager().getCurrentPhase(); + phase.onDraw(); // Mark as a draw + + // End the victory phase + liveCompetition.getPhaseManager().end(true); + } else { + // No victory phase - just forcefully kick every player + for (ArenaPlayer player : liveCompetition.getPlayers()) { + liveCompetition.leave(player, ArenaLeaveEvent.Cause.SHUTDOWN); + } + } + + liveCompetition.onDestroy(); + } + + competitions.remove(competition); + if (competition.getMap().getType() == MapType.DYNAMIC && competition.getMap() instanceof LiveCompetitionMap map) { + this.clearDynamicMap(map); + } + } + + public void completeAllActiveCompetitions() { + for (Map.Entry>> entry : Map.copyOf(this.competitions).entrySet()) { + for (Competition competition : List.copyOf(entry.getValue())) { + this.removeCompetition(entry.getKey(), competition); + } + } + } + + private void clearDynamicMap(LiveCompetitionMap map) { + if (map.getType() != MapType.DYNAMIC) { + return; + } + + Bukkit.unloadWorld(map.getWorld(), false); + if (!map.getWorld().getWorldFolder().exists()) { + return; + } + + try { + try (Stream pathsToDelete = Files.walk(map.getWorld().getWorldFolder().toPath())) { + for (Path path : pathsToDelete.sorted(Comparator.reverseOrder()).toList()) { + Files.deleteIfExists(path); + } + } + } catch (IOException e) { + this.plugin.error("Failed to delete dynamic map {}", map.getName(), e); + } + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/LiveCompetition.java b/plugin/src/main/java/org/battleplugins/arena/competition/LiveCompetition.java index 28d215f3..8731b0ea 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/LiveCompetition.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/LiveCompetition.java @@ -47,6 +47,10 @@ public abstract class LiveCompetition> implements Arena private final TeamManager teamManager; private final VictoryManager victoryManager; + private final CompetitionListener competitionListener; + private final OptionsListener optionsListener; + private final StatListener statListener; + public LiveCompetition(Arena arena, LiveCompetitionMap map) { this.arena = arena; this.map = map; @@ -55,15 +59,21 @@ public LiveCompetition(Arena arena, LiveCompetitionMap map) { this.teamManager = new TeamManager(this); this.victoryManager = new VictoryManager<>(arena, (T) this); - arena.getEventManager().registerEvents(new CompetitionListener<>(this)); - arena.getEventManager().registerEvents(new OptionsListener<>(this)); - arena.getEventManager().registerEvents(new StatListener<>(this)); + arena.getEventManager().registerEvents(this.competitionListener = new CompetitionListener<>(this)); + arena.getEventManager().registerEvents(this.optionsListener = new OptionsListener<>(this)); + arena.getEventManager().registerEvents(this.statListener = new StatListener<>(this)); // Set the initial phase CompetitionPhaseType initialPhase = arena.getInitialPhase(); this.phaseManager.setPhase(initialPhase); } + protected void onDestroy() { + this.arena.getEventManager().unregisterEvents(this.competitionListener); + this.arena.getEventManager().unregisterEvents(this.optionsListener); + this.arena.getEventManager().unregisterEvents(this.statListener); + } + private ArenaPlayer createPlayer(Player player) { return new ArenaPlayer(player, this.arena, this); } diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/StatListener.java b/plugin/src/main/java/org/battleplugins/arena/competition/StatListener.java index 0bb2f38e..0567b2ed 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/StatListener.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/StatListener.java @@ -6,6 +6,7 @@ import org.battleplugins.arena.event.player.ArenaDeathEvent; import org.battleplugins.arena.event.player.ArenaKillEvent; import org.battleplugins.arena.event.player.ArenaLifeDepleteEvent; +import org.battleplugins.arena.event.player.ArenaLivesExhaustEvent; import org.battleplugins.arena.event.player.ArenaStatChangeEvent; import org.battleplugins.arena.stat.ArenaStats; import org.bukkit.event.EventPriority; @@ -31,10 +32,19 @@ public void onKill(ArenaKillEvent event) { } @ArenaEventHandler(priority = EventPriority.LOWEST) - public void onLifeDeplete(ArenaStatChangeEvent event) { + public void onStatChange(ArenaStatChangeEvent event) { if (event.getStat() == ArenaStats.LIVES && event.getStatHolder() instanceof ArenaPlayer player) { - ArenaLifeDepleteEvent lifeDepleteEvent = new ArenaLifeDepleteEvent(this.competition.getArena(), player, (int) event.getNewValue()); - this.competition.getArena().getEventManager().callEvent(lifeDepleteEvent); + int newValue = (int) event.getNewValue(); + if (event.getOldValue() != null && (int) event.getOldValue() < newValue) { + return; + } + + if (newValue == 0) { + this.competition.getArena().getEventManager().callEvent(new ArenaLivesExhaustEvent(this.competition.getArena(), player)); + return; + } + + this.competition.getArena().getEventManager().callEvent(new ArenaLifeDepleteEvent(this.competition.getArena(), player, newValue)); } } diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/event/EventScheduler.java b/plugin/src/main/java/org/battleplugins/arena/competition/event/EventScheduler.java index 3f279e08..d7475e39 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/event/EventScheduler.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/event/EventScheduler.java @@ -101,12 +101,13 @@ public void eventEnded(Arena arena, Competition competition) { arena.getPlugin().info("Event in arena {} has ended. Rescheduling event at interval.", arena.getName()); } - public void stopAllScheduledEvents() { + public void stopAllEvents() { for (ScheduledEvent task : this.scheduledEvents.values()) { task.task().cancel(); } this.scheduledEvents.clear(); + this.activeEvents.clear(); } public Set getScheduledEvents() { diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/phase/PhaseManager.java b/plugin/src/main/java/org/battleplugins/arena/competition/phase/PhaseManager.java index f3bcb917..91c61b68 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/phase/PhaseManager.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/phase/PhaseManager.java @@ -2,6 +2,7 @@ import org.battleplugins.arena.Arena; import org.battleplugins.arena.competition.Competition; +import org.battleplugins.arena.competition.victory.VictoryCondition; public class PhaseManager> { private final Arena arena; @@ -19,16 +20,20 @@ public void setPhase(CompetitionPhaseType phaseType) { } public void setPhase(CompetitionPhaseType phaseType, boolean complete) { + this.end(complete); + + this.currentPhase = this.arena.createPhase(phaseType, this.competition); + this.arena.getEventManager().registerEvents(this.currentPhase); + this.currentPhase.start(); + } + + public void end(boolean complete) { if (this.currentPhase != null) { if (complete) { this.currentPhase.complete(); } this.arena.getEventManager().unregisterEvents(this.currentPhase); } - - this.currentPhase = this.arena.createPhase(phaseType, this.competition); - this.arena.getEventManager().registerEvents(this.currentPhase); - this.currentPhase.start(); } public CompetitionPhase getCurrentPhase() { diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/victory/VictoryManager.java b/plugin/src/main/java/org/battleplugins/arena/competition/victory/VictoryManager.java index 757bd2d4..440cfcf1 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/victory/VictoryManager.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/victory/VictoryManager.java @@ -18,12 +18,14 @@ public class VictoryManager> implements ArenaListener, CompetitionLike { private final Map, VictoryCondition> victoryConditions = new HashMap<>(); + private final Arena arena; private final T competition; private boolean closed = false; @SuppressWarnings({ "rawtypes", "unchecked" }) public VictoryManager(Arena arena, T competition) { + this.arena = arena; this.competition = competition; for (Map.Entry, VictoryConditionType.Provider> entry : arena.getVictoryConditions().entrySet()) { @@ -71,6 +73,13 @@ public void end(boolean closed) { for (VictoryCondition condition : this.victoryConditions.values()) { condition.end(); } + + if (closed) { + this.arena.getEventManager().unregisterEvents(this); + for (VictoryCondition condition : this.victoryConditions.values()) { + this.arena.getEventManager().unregisterEvents(condition); + } + } } @ArenaEventHandler diff --git a/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventManager.java b/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventManager.java index 138baaee..4b9828a0 100644 --- a/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventManager.java +++ b/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventManager.java @@ -69,6 +69,7 @@ public class ArenaEventManager { } }; + private final List trackedListeners = new ArrayList<>(); private final Arena arena; public ArenaEventManager(Arena arena) { @@ -161,6 +162,8 @@ private void pollActions(Competition competition, Iterator itera } public void registerEvents(ArenaListener listener) { + this.trackedListeners.add(listener); + for (Method method : listener.getClass().getDeclaredMethods()) { method.setAccessible(true); @@ -262,6 +265,15 @@ public void registerEvents(ArenaListener listener) { public void unregisterEvents(ArenaListener listener) { HandlerList.unregisterAll(listener); + this.trackedListeners.remove(listener); + } + + public void unregisterAll() { + for (ArenaListener listener : this.trackedListeners) { + HandlerList.unregisterAll(listener); + } + + this.trackedListeners.clear(); } @Nullable diff --git a/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventType.java b/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventType.java index d2a03bfa..fecf9836 100644 --- a/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventType.java +++ b/plugin/src/main/java/org/battleplugins/arena/event/ArenaEventType.java @@ -11,6 +11,7 @@ import org.battleplugins.arena.event.player.ArenaKillEvent; import org.battleplugins.arena.event.player.ArenaLeaveEvent; import org.battleplugins.arena.event.player.ArenaLifeDepleteEvent; +import org.battleplugins.arena.event.player.ArenaLivesExhaustEvent; import org.battleplugins.arena.event.player.ArenaRespawnEvent; import org.battleplugins.arena.event.player.ArenaSpectateEvent; import org.battleplugins.arena.event.player.ArenaStatChangeEvent; @@ -31,6 +32,7 @@ public final class ArenaEventType { public static final ArenaEventType ON_JOIN = new ArenaEventType<>("on-join", ArenaJoinEvent.class); public static final ArenaEventType ON_LEAVE = new ArenaEventType<>("on-leave", ArenaLeaveEvent.class); public static final ArenaEventType ON_LIFE_DEPLETE = new ArenaEventType<>("on-life-deplete", ArenaLifeDepleteEvent.class); + public static final ArenaEventType ON_LIVES_EXHAUST = new ArenaEventType<>("on-lives-exhaust", ArenaLivesExhaustEvent.class); public static final ArenaEventType ON_LOSE = new ArenaEventType<>("on-lose", ArenaLoseEvent.class); public static final ArenaEventType ON_RESPAWN = new ArenaEventType<>("on-respawn", ArenaRespawnEvent.class); public static final ArenaEventType ON_SPECTATE = new ArenaEventType<>("on-spectate", ArenaSpectateEvent.class); diff --git a/plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadEvent.java b/plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadEvent.java new file mode 100644 index 00000000..c98f69f0 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadEvent.java @@ -0,0 +1,38 @@ +package org.battleplugins.arena.event; + +import org.battleplugins.arena.BattleArena; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when BattleArena is reloaded. + */ +public class BattleArenaReloadEvent extends Event { + private final static HandlerList HANDLERS = new HandlerList(); + + private final BattleArena battleArena; + + public BattleArenaReloadEvent(BattleArena battleArena) { + this.battleArena = battleArena; + } + + /** + * Gets the {@link BattleArena} instance. + * + * @return the BattleArena instance + */ + public BattleArena getBattleArena() { + return battleArena; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadedEvent.java b/plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadedEvent.java new file mode 100644 index 00000000..3e178228 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/event/BattleArenaReloadedEvent.java @@ -0,0 +1,38 @@ +package org.battleplugins.arena.event; + +import org.battleplugins.arena.BattleArena; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when BattleArena is finished reloading. + */ +public class BattleArenaReloadedEvent extends Event { + private final static HandlerList HANDLERS = new HandlerList(); + + private final BattleArena battleArena; + + public BattleArenaReloadedEvent(BattleArena battleArena) { + this.battleArena = battleArena; + } + + /** + * Gets the {@link BattleArena} instance. + * + * @return the BattleArena instance + */ + public BattleArena getBattleArena() { + return battleArena; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLifeDepleteEvent.java b/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLifeDepleteEvent.java index f0ad3452..3d20576e 100644 --- a/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLifeDepleteEvent.java +++ b/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLifeDepleteEvent.java @@ -9,6 +9,10 @@ /** * Called for {@link ArenaPlayer}s who lose a life * in an {@link Arena}. + *

+ * This event will be called any time a player has a + * life left to live. In the event the player has no + * lives left, {@link ArenaLivesExhaustEvent} will be called. */ @EventTrigger("on-life-deplete") public class ArenaLifeDepleteEvent extends BukkitArenaPlayerEvent { diff --git a/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLivesExhaustEvent.java b/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLivesExhaustEvent.java new file mode 100644 index 00000000..958e7364 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/event/player/ArenaLivesExhaustEvent.java @@ -0,0 +1,30 @@ +package org.battleplugins.arena.event.player; + +import org.battleplugins.arena.Arena; +import org.battleplugins.arena.ArenaPlayer; +import org.battleplugins.arena.event.EventTrigger; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called for {@link ArenaPlayer}s who exhaust all of their lives + * in an {@link Arena}. + */ +@EventTrigger("on-lives-exhaust") +public class ArenaLivesExhaustEvent extends BukkitArenaPlayerEvent { + private final static HandlerList HANDLERS = new HandlerList(); + + public ArenaLivesExhaustEvent(@NotNull Arena arena, @NotNull ArenaPlayer player) { + super(arena, player); + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/messages/Messages.java b/plugin/src/main/java/org/battleplugins/arena/messages/Messages.java index a13b85fa..e3343766 100644 --- a/plugin/src/main/java/org/battleplugins/arena/messages/Messages.java +++ b/plugin/src/main/java/org/battleplugins/arena/messages/Messages.java @@ -105,12 +105,16 @@ public final class Messages { public static final Message MAP_FAILED_TO_SAVE = error("editor-map-failed-to-save", "An error occurred saving map {}! Please see the console for errors."); // Util strings + public static final Message MILLISECONDS = message("util-milliseconds", "milliseconds"); + public static final Message MILLISECOND = message("util-millisecond", "millisecond"); public static final Message SECONDS = message("util-seconds", "seconds"); public static final Message SECOND = message("util-second", "second"); public static final Message MINUTES = message("util-minutes", "minutes"); public static final Message MINUTE = message("util-minute", "minute"); public static final Message HOURS = message("util-hours", "hours"); public static final Message HOUR = message("util-hour", "hour"); + public static final Message DAYS = message("util-days", "days"); + public static final Message DAY = message("util-day", "day"); public static final Message ENABLED = message("util-enabled", "enabled", NamedTextColor.GREEN); public static final Message DISABLED = message("util-disabled", "disabled", NamedTextColor.RED); @@ -128,6 +132,9 @@ public final class Messages { public static final Message BACKUP_NUMBER = message("util-backup-number", "Backup #{}"); public static final Message MODULES = message("util-modules", "Modules"); public static final Message MODULE = message("util-module", "- {}: {}"); + public static final Message STARTING_RELOAD = info("util-starting-reload", "Reloading BattleArena..."); + public static final Message RELOAD_COMPLETE = success("util-reload-complete", "Reload complete in {}!"); + public static final Message RELOAD_FAILED = error("util-reload-failed", "Reload failed! Please see the console for more information."); static void init() { // no-op diff --git a/plugin/src/main/java/org/battleplugins/arena/util/LoggerHolder.java b/plugin/src/main/java/org/battleplugins/arena/util/LoggerHolder.java new file mode 100644 index 00000000..217b5016 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/util/LoggerHolder.java @@ -0,0 +1,40 @@ +package org.battleplugins.arena.util; + +public interface LoggerHolder { + + default void info(String message) { + this.getSLF4JLogger().info(message); + } + + default void info(String message, Object... args) { + this.getSLF4JLogger().info(message, args); + } + + default void error(String message) { + this.getSLF4JLogger().error(message); + } + + default void error(String message, Object... args) { + this.getSLF4JLogger().error(message, args); + } + + default void warn(String message) { + this.getSLF4JLogger().warn(message); + } + + default void warn(String message, Object... args) { + this.getSLF4JLogger().warn(message, args); + } + + default void debug(String message, Object... args) { + if (this.isDebugMode()) { + this.getSLF4JLogger().info("[DEBUG] " + message, args); + } + } + + org.slf4j.Logger getSLF4JLogger(); + + boolean isDebugMode(); + + void setDebugMode(boolean debugMode); +} diff --git a/plugin/src/main/java/org/battleplugins/arena/util/UnitUtil.java b/plugin/src/main/java/org/battleplugins/arena/util/UnitUtil.java index 5689c48a..3608faf0 100644 --- a/plugin/src/main/java/org/battleplugins/arena/util/UnitUtil.java +++ b/plugin/src/main/java/org/battleplugins/arena/util/UnitUtil.java @@ -10,6 +10,13 @@ public final class UnitUtil { public static String toUnitString(CommandSender viewer, long amount, TimeUnit unit) { switch (unit) { + case MILLISECONDS -> { + if (amount == 1) { + return amount + " " + Messages.MILLISECOND.asPlainText(); + } else { + return amount + " " + Messages.MILLISECONDS.asPlainText(); + } + } case SECONDS -> { if (amount == 1) { return amount + " " + Messages.SECOND.asPlainText(); @@ -32,6 +39,13 @@ public static String toUnitString(CommandSender viewer, long amount, TimeUnit un return amount + " " + Messages.HOURS.asPlainText(); } } + case DAYS -> { + if (amount == 1) { + return amount + " " + Messages.DAY.asPlainText(); + } else { + return amount + " " + Messages.DAYS.asPlainText(); + } + } } // Realistically, we will only ever be using the values above