From 0f2735f17159aee29c75dcdaa315424322dafee2 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 1 Jun 2026 11:26:54 +0200 Subject: [PATCH] feat: add multiple region support for mines --- .../dev/loki/lomines/RegistrationManager.java | 2 + .../lomines/command/admin/RegionCommands.java | 161 ++++++++++++++++++ .../command/common/LoMinesTabCompleter.java | 28 ++- .../dev/loki/lomines/core/mine/Mines.java | 28 +++ .../lomines/core/service/MineFileManager.java | 16 ++ .../data/config/spawn/PlayerSpawnConfig.java | 99 +++++++++-- .../data/config/teleport/TeleportConfig.java | 98 +++++++++-- todo.md | 4 + 8 files changed, 403 insertions(+), 33 deletions(-) create mode 100644 src/main/java/dev/loki/lomines/command/admin/RegionCommands.java diff --git a/src/main/java/dev/loki/lomines/RegistrationManager.java b/src/main/java/dev/loki/lomines/RegistrationManager.java index 5d0af9f..094043d 100644 --- a/src/main/java/dev/loki/lomines/RegistrationManager.java +++ b/src/main/java/dev/loki/lomines/RegistrationManager.java @@ -5,6 +5,7 @@ import dev.loki.lomines.command.admin.CopyCommand; import dev.loki.lomines.command.admin.HologramCommands; import dev.loki.lomines.command.admin.InfoCommand; import dev.loki.lomines.command.admin.MaskCommands; +import dev.loki.lomines.command.admin.RegionCommands; import dev.loki.lomines.command.admin.StatsCommands; import dev.loki.lomines.command.common.LoMinesTabCompleter; import dev.loki.lomines.command.player.PlayerCommands; @@ -37,6 +38,7 @@ final class RegistrationManager { commandManager.register(new InfoCommand(plugin)); commandManager.register(new TeleportCommand(plugin)); commandManager.register(new CopyCommand(plugin)); + commandManager.register(new RegionCommands(plugin)); plugin.loLogger().info("Commands registered"); } diff --git a/src/main/java/dev/loki/lomines/command/admin/RegionCommands.java b/src/main/java/dev/loki/lomines/command/admin/RegionCommands.java new file mode 100644 index 0000000..5cc8257 --- /dev/null +++ b/src/main/java/dev/loki/lomines/command/admin/RegionCommands.java @@ -0,0 +1,161 @@ +package dev.loki.lomines.command.admin; + +import dev.loki.lomines.LoMinesPlugin; +import dev.loki.lomines.core.mine.Mine; +import dev.loki.lomines.data.config.MineConfig; +import dev.loki.lomines.data.config.region.RegionConfig; +import dev.loki.lomines.util.location.Cuboid; +import dev.lolilb.commands.annotation.Arg; +import dev.lolilb.commands.annotation.Subcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +/** + * Commands for managing mine regions (multiple spawn areas). + */ +public class RegionCommands { + + private final LoMinesPlugin plugin; + + public RegionCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + /** + * Lists all regions in a mine. + * Usage: /lm regions + */ + @Subcommand(value = "regions", permission = "lomines.admin.regions") + void listRegions(CommandSender sender, @Arg("mine") String mineName) { + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + List regions = mine.getRegions(); + sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); + sender.sendMessage(Component.text("§6§lРегионы шахты §f" + mine.getName())); + sender.sendMessage(Component.text("§7Всего регионов: §f" + regions.size())); + sender.sendMessage(Component.text("")); + + for (int i = 0; i < regions.size(); i++) { + Cuboid r = regions.get(i); + sender.sendMessage(Component.text("§e#" + (i + 1) + " §7(§f" + r.getVolume() + " §7блоков)")); + sender.sendMessage(Component.text(" §7Мир: §f" + r.getWorld().getName())); + sender.sendMessage(Component.text(" §7От: §f" + r.getMinX() + ", " + r.getMinY() + ", " + r.getMinZ())); + sender.sendMessage(Component.text(" §7До: §f" + r.getMaxX() + ", " + r.getMaxY() + ", " + r.getMaxZ())); + } + sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); + } + + /** + * Adds a new region to a mine using current wand selection. + * Usage: /lm addregion + */ + @Subcommand(value = "addregion", permission = "lomines.admin.regions") + void addRegion(CommandSender sender, @Arg("mine") String mineName) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED)); + return; + } + + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + var wandManager = plugin.getGroupWandManager(); + var session = wandManager.getSession(player); + + if (!session.isComplete()) { + player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка")); + return; + } + + Cuboid newRegion = session.toCuboid(); + String mineWorld = mine.getConfig().region().worldName(); + String regionWorld = newRegion.getWorld().getName(); + + if (!mineWorld.equals(regionWorld)) { + player.sendMessage(Component.text("§cРегион должен быть в мире §f" + mineWorld + "§c, а не §f" + regionWorld)); + return; + } + + // Add new region to existing regions + List currentRegions = new ArrayList<>(mine.getRegions()); + currentRegions.add(newRegion); + + // Update config + var newRegionConfig = new RegionConfig(currentRegions); + updateMineRegions(mine, newRegionConfig); + + player.sendMessage(Component.text("§aДобавлен регион #" + currentRegions.size())); + player.sendMessage(Component.text("§7От: §f" + newRegion.getMinX() + ", " + newRegion.getMinY() + ", " + newRegion.getMinZ())); + player.sendMessage(Component.text("§7До: §f" + newRegion.getMaxX() + ", " + newRegion.getMaxY() + ", " + newRegion.getMaxZ())); + player.sendMessage(Component.text("§7Объём: §f" + newRegion.getVolume() + " §7блоков")); + + session.clear(); + } + + /** + * Removes a region by index. + * Usage: /lm removeregion + */ + @Subcommand(value = "removeregion", permission = "lomines.admin.regions") + void removeRegion(CommandSender sender, + @Arg("mine") String mineName, + @Arg("index") int index) { + Mine mine = plugin.getMines().find(mineName).orElse(null); + if (mine == null) { + sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + + List currentRegions = new ArrayList<>(mine.getRegions()); + + if (currentRegions.size() <= 1) { + sender.sendMessage(Component.text("§cНельзя удалить последний регион! Удалите всю шахту.")); + return; + } + + if (index < 1 || index > currentRegions.size()) { + sender.sendMessage(Component.text("§cНеверный номер региона. Доступны: 1-" + currentRegions.size())); + return; + } + + Cuboid removed = currentRegions.remove(index - 1); + var newRegionConfig = new RegionConfig(currentRegions); + updateMineRegions(mine, newRegionConfig); + + sender.sendMessage(Component.text("§aУдалён регион #" + index)); + sender.sendMessage(Component.text("§7Был: §f" + removed.getMinX() + ", " + removed.getMinY() + ", " + removed.getMinZ() + + " §7-> §f" + removed.getMaxX() + ", " + removed.getMaxY() + ", " + removed.getMaxZ())); + sender.sendMessage(Component.text("§7Осталось регионов: §f" + currentRegions.size())); + } + + private void updateMineRegions(Mine mine, RegionConfig newConfig) { + var oldConfig = mine.getConfig(); + var newMineConfig = new MineConfig( + newConfig, + oldConfig.blocks(), + oldConfig.reset(), + oldConfig.teleport(), + oldConfig.playerSpawn(), + oldConfig.rewards(), + oldConfig.ui(), + oldConfig.worldGuard() + ); + + // Update and save + plugin.getMines().updateMineConfig(mine.getName(), newMineConfig); + mine.save(); + } +} diff --git a/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java index b90ef64..f74f7df 100644 --- a/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java +++ b/src/main/java/dev/loki/lomines/command/common/LoMinesTabCompleter.java @@ -21,7 +21,7 @@ public class LoMinesTabCompleter implements TabCompleter { private static final List ALL_COMMANDS = List.of( "create", "delete", "reset", "reload", "list", "wand", "group", "stats", "top", "maskscan", "edit", "setteleport", "setspawn", - "clearspawn", "info", "tp", "copy", "help" + "clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help" ); private static final List BOOLEAN_VALUES = List.of("true", "false"); @@ -67,13 +67,15 @@ public class LoMinesTabCompleter implements TabCompleter { private List completeSecondArg(CommandSender sender, String subcommand, String partial) { return switch (subcommand) { case "create" -> completeCreate(partial); - case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info" -> + case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info", + "regions", "addregion" -> filterStartsWith(getMineNames(), partial); case "reset" -> completeReset(partial); case "stats" -> completeStats(sender, partial); case "top" -> completeTopFirstArg(partial); case "tp" -> completeTeleport(sender, partial); case "copy" -> completeCopyFirstArg(partial); + case "removeregion" -> completeRemoveRegion(sender, partial); default -> new ArrayList<>(); }; } @@ -83,10 +85,24 @@ public class LoMinesTabCompleter implements TabCompleter { case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial); case "top" -> completeTopSecondArg(arg2, partial); case "copy" -> filterStartsWith(getMineNames(), partial); + case "removeregion" -> completeRegionIndex(arg2, partial); default -> new ArrayList<>(); }; } + private List completeRegionIndex(String mineName, String partial) { + var mine = plugin.getMines().find(mineName); + if (mine.isEmpty()) { + return new ArrayList<>(); + } + int regionCount = mine.get().getRegions().size(); + List indices = new ArrayList<>(); + for (int i = 1; i <= regionCount; i++) { + indices.add(String.valueOf(i)); + } + return filterStartsWith(indices, partial); + } + private List completeCreate(String partial) { if (partial.isEmpty()) { return new ArrayList<>(List.of("")); @@ -146,6 +162,13 @@ public class LoMinesTabCompleter implements TabCompleter { return filterStartsWith(getMineNames(), partial); } + private List completeRemoveRegion(CommandSender sender, String partial) { + if (!sender.hasPermission("lomines.admin.regions")) { + return new ArrayList<>(); + } + return filterStartsWith(getMineNames(), partial); + } + private boolean isNumericPartial(String partial) { return partial.isEmpty() || partial.matches("\\d*"); } @@ -179,6 +202,7 @@ public class LoMinesTabCompleter implements TabCompleter { case "stats", "top" -> sender.hasPermission("lomines.stats"); case "tp" -> sender.hasPermission("lomines.teleport"); case "copy" -> sender.hasPermission("lomines.admin.copy"); + case "regions", "addregion", "removeregion" -> sender.hasPermission("lomines.admin.regions"); case "help" -> sender.hasPermission("lomines.use"); default -> true; }; diff --git a/src/main/java/dev/loki/lomines/core/mine/Mines.java b/src/main/java/dev/loki/lomines/core/mine/Mines.java index 6c08932..fc3674d 100644 --- a/src/main/java/dev/loki/lomines/core/mine/Mines.java +++ b/src/main/java/dev/loki/lomines/core/mine/Mines.java @@ -112,6 +112,34 @@ public final class Mines { } } + /** + * Updates mine configuration and saves to disk. + * Used for modifying regions, blocks, etc. + */ + public void updateMineConfig(String name, MineConfig newConfig) throws IOException { + if (!repository.exists(name)) { + throw new IllegalArgumentException("Mine not found: " + name); + } + + // Save new config to file + fileManager.saveConfig(name, newConfig); + + // Reload to apply changes + try { + Mine oldMine = repository.find(name).orElse(null); + repository.reload(name); + + if (oldMine != null) { + Mine newMine = repository.find(name).orElse(null); + if (newMine != null) { + worldGuardService.updateRegion(name, newMine.getConfig()); + } + } + } catch (ConfigLoader.ConfigLoadException e) { + throw new IOException("Failed to reload mine after config update: " + e.getMessage(), e); + } + } + public WorldGuardRegionService getWorldGuardService() { return worldGuardService; } diff --git a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java index 3b77c63..70a1327 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java +++ b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java @@ -144,6 +144,22 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { configLoader.save(config); } + /** + * Saves a mine configuration to file by name. + * + * @param name the mine name + * @param config the configuration to save + * @throws ConfigLoader.ConfigLoadException if saving fails + */ + public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException { + // Ensure the config has the correct name + MineConfig configToSave = config; + if (!name.equals(config.region().worldName())) { + // Config name doesn't matter internally, just save as-is + } + configLoader.save(config); + } + /** * Saves mask positions for mask fill mode. */ diff --git a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java index 8cecd00..75f62de 100644 --- a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java @@ -2,33 +2,65 @@ package dev.loki.lomines.data.config.spawn; import org.bukkit.Location; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Optional; /** - * Configuration for player spawn location when stuck in blocks. + * Configuration for player spawn locations when stuck in blocks. + * Supports multiple spawn points for a single mine. * Used to teleport players out when they are suffocating after mine reset. */ -public record PlayerSpawnConfig( - boolean enabled, - Location location -) { +public final class PlayerSpawnConfig { - public PlayerSpawnConfig { - if (!enabled) { - location = null; - } else if (location != null) { - // Defensive copy - location = location.clone(); - Objects.requireNonNull(location.getWorld(), "Spawn world cannot be null"); + private final boolean enabled; + private final List locations; + + public PlayerSpawnConfig(boolean enabled, List locations) { + this.enabled = enabled; + if (!enabled || locations == null) { + this.locations = List.of(); + } else { + // Defensive copy with validation + List copy = new ArrayList<>(); + for (Location loc : locations) { + if (loc != null) { + Location cloned = loc.clone(); + Objects.requireNonNull(cloned.getWorld(), "Spawn world cannot be null"); + copy.add(cloned); + } + } + this.locations = List.copyOf(copy); } } /** - * Returns the spawn location if enabled. + * Returns the primary spawn location (first in list) if enabled. */ public Optional getLocation() { - return Optional.ofNullable(enabled ? location : null); + return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0)); + } + + /** + * Returns all spawn locations. + */ + public List getLocations() { + return locations; + } + + /** + * Returns true if spawn is enabled and has at least one location. + */ + public boolean enabled() { + return enabled && !locations.isEmpty(); + } + + /** + * Returns the number of configured spawn locations. + */ + public int count() { + return locations.size(); } /** @@ -39,9 +71,44 @@ public record PlayerSpawnConfig( } /** - * Creates enabled config with location. + * Creates enabled config with single location. */ public static PlayerSpawnConfig at(Location location) { - return new PlayerSpawnConfig(true, location); + return new PlayerSpawnConfig(true, List.of(location)); + } + + /** + * Creates enabled config with multiple locations. + */ + public static PlayerSpawnConfig at(List locations) { + return new PlayerSpawnConfig(true, locations); + } + + /** + * Creates a new config with an additional location. + */ + public PlayerSpawnConfig addLocation(Location location) { + List newList = new ArrayList<>(locations); + newList.add(location); + return new PlayerSpawnConfig(true, newList); + } + + /** + * Creates a new config without the location at given index. + */ + public PlayerSpawnConfig removeLocation(int index) { + if (index < 0 || index >= locations.size()) { + return this; + } + List newList = new ArrayList<>(locations); + newList.remove(index); + return new PlayerSpawnConfig(!newList.isEmpty(), newList); + } + + /** + * Clears all locations (disables spawn). + */ + public PlayerSpawnConfig clear() { + return disabled(); } } diff --git a/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java b/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java index c468b63..51faeea 100644 --- a/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/teleport/TeleportConfig.java @@ -2,32 +2,65 @@ package dev.loki.lomines.data.config.teleport; import org.bukkit.Location; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Optional; /** * Configuration for mine teleportation on reset. + * Supports multiple teleport locations for a single mine. */ -public record TeleportConfig( - boolean enabled, - Location location -) { +public final class TeleportConfig { - public TeleportConfig { - if (!enabled) { - location = null; - } else if (location != null) { - // Defensive copy - location = location.clone(); - Objects.requireNonNull(location.getWorld(), "Teleport world cannot be null"); + private final boolean enabled; + private final List locations; + + public TeleportConfig(boolean enabled, List locations) { + this.enabled = enabled; + if (!enabled || locations == null) { + this.locations = List.of(); + } else { + // Defensive copy with validation + List copy = new ArrayList<>(); + for (Location loc : locations) { + if (loc != null) { + Location cloned = loc.clone(); + Objects.requireNonNull(cloned.getWorld(), "Teleport world cannot be null"); + copy.add(cloned); + } + } + this.locations = List.copyOf(copy); } } /** - * Returns the teleport location if enabled. + * Returns the primary teleport location (first in list) if enabled. */ public Optional getLocation() { - return Optional.ofNullable(enabled ? location : null); + return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0)); + } + + /** + * Returns all teleport locations. + */ + public List getLocations() { + return locations; + } + + /** + * Returns true if teleport is enabled and has at least one location. + */ + public boolean enabled() { + return enabled && !locations.isEmpty(); + } + + /** + * Returns the number of configured teleport locations. + */ + public int count() { + return locations.size(); } /** @@ -38,9 +71,44 @@ public record TeleportConfig( } /** - * Creates enabled config with location. + * Creates enabled config with single location. */ public static TeleportConfig at(Location location) { - return new TeleportConfig(true, location); + return new TeleportConfig(true, List.of(location)); + } + + /** + * Creates enabled config with multiple locations. + */ + public static TeleportConfig at(List locations) { + return new TeleportConfig(true, locations); + } + + /** + * Creates a new config with an additional location. + */ + public TeleportConfig addLocation(Location location) { + List newList = new ArrayList<>(locations); + newList.add(location); + return new TeleportConfig(true, newList); + } + + /** + * Creates a new config without the location at given index. + */ + public TeleportConfig removeLocation(int index) { + if (index < 0 || index >= locations.size()) { + return this; + } + List newList = new ArrayList<>(locations); + newList.remove(index); + return new TeleportConfig(!newList.isEmpty(), newList); + } + + /** + * Clears all locations (disables teleport). + */ + public TeleportConfig clear() { + return disabled(); } } diff --git a/todo.md b/todo.md index 267df22..07c9fd7 100644 --- a/todo.md +++ b/todo.md @@ -450,6 +450,10 @@ dev.loki.lomines/ - [x] Добавить `/lm tp ` — телепортация ✅ - [x] Добавить `/lm copy ` — копирование ✅ - [x] Разбить большие GUI файлы (>200 строк) ✅ +- [x] Множественные регионы в одной шахте ✅ + - `/lm regions ` — список регионов + - `/lm addregion ` — добавить регион + - `/lm removeregion ` — удалить регион ---