feat: add multiple region support for mines

This commit is contained in:
loki5512344 2026-06-01 11:26:54 +02:00
parent 957468b6b6
commit 0f2735f171
8 changed files with 403 additions and 33 deletions

View file

@ -5,6 +5,7 @@ import dev.loki.lomines.command.admin.CopyCommand;
import dev.loki.lomines.command.admin.HologramCommands; import dev.loki.lomines.command.admin.HologramCommands;
import dev.loki.lomines.command.admin.InfoCommand; import dev.loki.lomines.command.admin.InfoCommand;
import dev.loki.lomines.command.admin.MaskCommands; 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.admin.StatsCommands;
import dev.loki.lomines.command.common.LoMinesTabCompleter; import dev.loki.lomines.command.common.LoMinesTabCompleter;
import dev.loki.lomines.command.player.PlayerCommands; import dev.loki.lomines.command.player.PlayerCommands;
@ -37,6 +38,7 @@ final class RegistrationManager {
commandManager.register(new InfoCommand(plugin)); commandManager.register(new InfoCommand(plugin));
commandManager.register(new TeleportCommand(plugin)); commandManager.register(new TeleportCommand(plugin));
commandManager.register(new CopyCommand(plugin)); commandManager.register(new CopyCommand(plugin));
commandManager.register(new RegionCommands(plugin));
plugin.loLogger().info("Commands registered"); plugin.loLogger().info("Commands registered");
} }

View file

@ -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 <mine>
*/
@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<Cuboid> 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 <mine>
*/
@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<Cuboid> 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 <mine> <index>
*/
@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<Cuboid> 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();
}
}

View file

@ -21,7 +21,7 @@ public class LoMinesTabCompleter implements TabCompleter {
private static final List<String> ALL_COMMANDS = List.of( private static final List<String> ALL_COMMANDS = List.of(
"create", "delete", "reset", "reload", "list", "wand", "group", "create", "delete", "reset", "reload", "list", "wand", "group",
"stats", "top", "maskscan", "edit", "setteleport", "setspawn", "stats", "top", "maskscan", "edit", "setteleport", "setspawn",
"clearspawn", "info", "tp", "copy", "help" "clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help"
); );
private static final List<String> BOOLEAN_VALUES = List.of("true", "false"); private static final List<String> BOOLEAN_VALUES = List.of("true", "false");
@ -67,13 +67,15 @@ public class LoMinesTabCompleter implements TabCompleter {
private List<String> completeSecondArg(CommandSender sender, String subcommand, String partial) { private List<String> completeSecondArg(CommandSender sender, String subcommand, String partial) {
return switch (subcommand) { return switch (subcommand) {
case "create" -> completeCreate(partial); 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); filterStartsWith(getMineNames(), partial);
case "reset" -> completeReset(partial); case "reset" -> completeReset(partial);
case "stats" -> completeStats(sender, partial); case "stats" -> completeStats(sender, partial);
case "top" -> completeTopFirstArg(partial); case "top" -> completeTopFirstArg(partial);
case "tp" -> completeTeleport(sender, partial); case "tp" -> completeTeleport(sender, partial);
case "copy" -> completeCopyFirstArg(partial); case "copy" -> completeCopyFirstArg(partial);
case "removeregion" -> completeRemoveRegion(sender, partial);
default -> new ArrayList<>(); default -> new ArrayList<>();
}; };
} }
@ -83,10 +85,24 @@ public class LoMinesTabCompleter implements TabCompleter {
case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial); case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial);
case "top" -> completeTopSecondArg(arg2, partial); case "top" -> completeTopSecondArg(arg2, partial);
case "copy" -> filterStartsWith(getMineNames(), partial); case "copy" -> filterStartsWith(getMineNames(), partial);
case "removeregion" -> completeRegionIndex(arg2, partial);
default -> new ArrayList<>(); default -> new ArrayList<>();
}; };
} }
private List<String> completeRegionIndex(String mineName, String partial) {
var mine = plugin.getMines().find(mineName);
if (mine.isEmpty()) {
return new ArrayList<>();
}
int regionCount = mine.get().getRegions().size();
List<String> indices = new ArrayList<>();
for (int i = 1; i <= regionCount; i++) {
indices.add(String.valueOf(i));
}
return filterStartsWith(indices, partial);
}
private List<String> completeCreate(String partial) { private List<String> completeCreate(String partial) {
if (partial.isEmpty()) { if (partial.isEmpty()) {
return new ArrayList<>(List.of("<name>")); return new ArrayList<>(List.of("<name>"));
@ -146,6 +162,13 @@ public class LoMinesTabCompleter implements TabCompleter {
return filterStartsWith(getMineNames(), partial); return filterStartsWith(getMineNames(), partial);
} }
private List<String> completeRemoveRegion(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.admin.regions")) {
return new ArrayList<>();
}
return filterStartsWith(getMineNames(), partial);
}
private boolean isNumericPartial(String partial) { private boolean isNumericPartial(String partial) {
return partial.isEmpty() || partial.matches("\\d*"); return partial.isEmpty() || partial.matches("\\d*");
} }
@ -179,6 +202,7 @@ public class LoMinesTabCompleter implements TabCompleter {
case "stats", "top" -> sender.hasPermission("lomines.stats"); case "stats", "top" -> sender.hasPermission("lomines.stats");
case "tp" -> sender.hasPermission("lomines.teleport"); case "tp" -> sender.hasPermission("lomines.teleport");
case "copy" -> sender.hasPermission("lomines.admin.copy"); case "copy" -> sender.hasPermission("lomines.admin.copy");
case "regions", "addregion", "removeregion" -> sender.hasPermission("lomines.admin.regions");
case "help" -> sender.hasPermission("lomines.use"); case "help" -> sender.hasPermission("lomines.use");
default -> true; default -> true;
}; };

View file

@ -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() { public WorldGuardRegionService getWorldGuardService() {
return worldGuardService; return worldGuardService;
} }

View file

@ -144,6 +144,22 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
configLoader.save(config); 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. * Saves mask positions for mask fill mode.
*/ */

View file

@ -2,33 +2,65 @@ package dev.loki.lomines.data.config.spawn;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; 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. * Used to teleport players out when they are suffocating after mine reset.
*/ */
public record PlayerSpawnConfig( public final class PlayerSpawnConfig {
boolean enabled,
Location location
) {
public PlayerSpawnConfig { private final boolean enabled;
if (!enabled) { private final List<Location> locations;
location = null;
} else if (location != null) { public PlayerSpawnConfig(boolean enabled, List<Location> locations) {
// Defensive copy this.enabled = enabled;
location = location.clone(); if (!enabled || locations == null) {
Objects.requireNonNull(location.getWorld(), "Spawn world cannot be null"); this.locations = List.of();
} else {
// Defensive copy with validation
List<Location> 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<Location> getLocation() { public Optional<Location> getLocation() {
return Optional.ofNullable(enabled ? location : null); return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0));
}
/**
* Returns all spawn locations.
*/
public List<Location> 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) { 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<Location> locations) {
return new PlayerSpawnConfig(true, locations);
}
/**
* Creates a new config with an additional location.
*/
public PlayerSpawnConfig addLocation(Location location) {
List<Location> 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<Location> newList = new ArrayList<>(locations);
newList.remove(index);
return new PlayerSpawnConfig(!newList.isEmpty(), newList);
}
/**
* Clears all locations (disables spawn).
*/
public PlayerSpawnConfig clear() {
return disabled();
} }
} }

View file

@ -2,32 +2,65 @@ package dev.loki.lomines.data.config.teleport;
import org.bukkit.Location; import org.bukkit.Location;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
/** /**
* Configuration for mine teleportation on reset. * Configuration for mine teleportation on reset.
* Supports multiple teleport locations for a single mine.
*/ */
public record TeleportConfig( public final class TeleportConfig {
boolean enabled,
Location location
) {
public TeleportConfig { private final boolean enabled;
if (!enabled) { private final List<Location> locations;
location = null;
} else if (location != null) { public TeleportConfig(boolean enabled, List<Location> locations) {
// Defensive copy this.enabled = enabled;
location = location.clone(); if (!enabled || locations == null) {
Objects.requireNonNull(location.getWorld(), "Teleport world cannot be null"); this.locations = List.of();
} else {
// Defensive copy with validation
List<Location> 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<Location> getLocation() { public Optional<Location> getLocation() {
return Optional.ofNullable(enabled ? location : null); return locations.isEmpty() ? Optional.empty() : Optional.of(locations.get(0));
}
/**
* Returns all teleport locations.
*/
public List<Location> 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) { 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<Location> locations) {
return new TeleportConfig(true, locations);
}
/**
* Creates a new config with an additional location.
*/
public TeleportConfig addLocation(Location location) {
List<Location> 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<Location> newList = new ArrayList<>(locations);
newList.remove(index);
return new TeleportConfig(!newList.isEmpty(), newList);
}
/**
* Clears all locations (disables teleport).
*/
public TeleportConfig clear() {
return disabled();
} }
} }

View file

@ -450,6 +450,10 @@ dev.loki.lomines/
- [x] Добавить `/lm tp <mine>` — телепортация ✅ - [x] Добавить `/lm tp <mine>` — телепортация ✅
- [x] Добавить `/lm copy <from> <to>` — копирование ✅ - [x] Добавить `/lm copy <from> <to>` — копирование ✅
- [x] Разбить большие GUI файлы (>200 строк) ✅ - [x] Разбить большие GUI файлы (>200 строк) ✅
- [x] Множественные регионы в одной шахте ✅
- `/lm regions <mine>` — список регионов
- `/lm addregion <mine>` — добавить регион
- `/lm removeregion <mine> <index>` — удалить регион
--- ---