diff --git a/src/main/java/com/loki/lomines/LoMinesPlugin.java b/src/main/java/com/loki/lomines/LoMinesPlugin.java index 233cd39..1a0f46a 100644 --- a/src/main/java/com/loki/lomines/LoMinesPlugin.java +++ b/src/main/java/com/loki/lomines/LoMinesPlugin.java @@ -148,8 +148,11 @@ public final class LoMinesPlugin extends LoPlugin { * Registers all plugin commands. */ private void registerCommands() { - commandManager.register(new com.loki.lomines.command.MineCommands(this)); - + commandManager.register(new com.loki.lomines.command.AdminCommands(this)); + commandManager.register(new com.loki.lomines.command.PlayerCommands(this)); + commandManager.register(new com.loki.lomines.command.StatsCommands(this)); + commandManager.register(new com.loki.lomines.command.MaskCommands(this)); + loLogger().info("Commands registered"); } diff --git a/src/main/java/com/loki/lomines/command/AdminCommands.java b/src/main/java/com/loki/lomines/command/AdminCommands.java new file mode 100644 index 0000000..4245570 --- /dev/null +++ b/src/main/java/com/loki/lomines/command/AdminCommands.java @@ -0,0 +1,138 @@ +package com.loki.lomines.command; + +import com.loki.lomines.LoMinesPlugin; +import com.loki.lomines.core.Mine; +import com.loki.lomines.core.Mines; +import dev.lolib.commands.annotation.Arg; +import dev.lolib.commands.annotation.Command; +import dev.lolib.commands.annotation.Subcommand; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; + +import java.io.IOException; +import java.util.Collection; + +/** + * Admin commands for mine management. + * Provides commands for creating, deleting, resetting, reloading, and listing mines. + */ +@Command(value = "lm", permission = "lomines.use") +public class AdminCommands { + + private final LoMinesPlugin plugin; + private final Mines mines; + + public AdminCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.mines = plugin.getMines(); + } + + /** + * Creates a new mine with the given name. + * Usage: /lm create + */ + @Subcommand(value = "create", permission = "lomines.admin.create") + public void create(CommandSender sender, @Arg("name") String name) { + try { + mines.create(name); + sender.sendMessage(Component.text("Successfully created mine: " + name)); + sender.sendMessage(Component.text( + "Edit selection & contents in plugins/LoMines/mines/" + name + ".yml — " + + "for shape-based ore (not full cuboid), paint mask.marker blocks, then: /lm maskscan " + name)); + } catch (IllegalArgumentException e) { + sender.sendMessage(Component.text("Mine already exists: " + name)); + } catch (IOException e) { + sender.sendMessage(Component.text("Failed to create mine: " + e.getMessage())); + plugin.loLogger().error("Failed to create mine " + name, e); + } + } + + /** + * Deletes an existing mine. + * Usage: /lm delete + */ + @Subcommand(value = "delete", permission = "lomines.admin.delete") + public void delete(CommandSender sender, @Arg("name") String name) { + try { + mines.delete(name); + sender.sendMessage(Component.text("Successfully deleted mine: " + name)); + } catch (IllegalArgumentException e) { + sender.sendMessage(Component.text("Mine not found: " + name)); + } catch (IOException e) { + sender.sendMessage(Component.text("Failed to delete mine: " + e.getMessage())); + plugin.loLogger().error("Failed to delete mine " + name, e); + } + } + + /** + * Resets a mine by refilling all its regions. + * Usage: /lm reset [silent] + */ + @Subcommand(value = "reset", permission = "lomines.admin.reset") + public void reset( + CommandSender sender, + @Arg("mine") String mineName, + @Arg(value = "silent", optional = true, defaultValue = "false") boolean silent + ) { + try { + Mine mine = mines.get(mineName); + mine.reset(silent); + sender.sendMessage(Component.text("Resetting mine: " + mineName)); + } catch (IllegalArgumentException e) { + sender.sendMessage(Component.text("Mine not found: " + mineName)); + } + } + + /** + * Reloads all mine configurations from disk. + * Usage: /lm reload + */ + @Subcommand(value = "reload", permission = "lomines.admin.reload") + public void reload(CommandSender sender) { + try { + // Stop all existing mines + mines.getAll().forEach(Mine::stop); + + // Reload all mines + mines.loadAll(); + + int count = mines.getAll().size(); + sender.sendMessage(Component.text("Successfully reloaded " + count + " mine(s)")); + } catch (IOException e) { + sender.sendMessage(Component.text("Failed to reload mines: " + e.getMessage())); + plugin.loLogger().error("Failed to reload mines", e); + } + } + + /** + * Lists all active mines. + * Usage: /lm list + */ + @Subcommand(value = "list", permission = "lomines.admin.list") + public void list(CommandSender sender) { + Collection allMines = mines.getAll(); + + if (allMines.isEmpty()) { + sender.sendMessage(Component.text("No mines found")); + return; + } + + sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ===")); + + for (Mine mine : allMines) { + String status = formatMineStatus(mine); + sender.sendMessage(Component.text("- " + mine.getName() + " " + status)); + } + } + + /** + * Formats mine status information for display. + */ + private String formatMineStatus(Mine mine) { + int blocks = mine.getBlocks(); + int total = mine.getTotalVolume(); + double percent = mine.getPercentFilled(); + + return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent); + } +} diff --git a/src/main/java/com/loki/lomines/command/MaskCommands.java b/src/main/java/com/loki/lomines/command/MaskCommands.java new file mode 100644 index 0000000..fb4aafe --- /dev/null +++ b/src/main/java/com/loki/lomines/command/MaskCommands.java @@ -0,0 +1,50 @@ +package com.loki.lomines.command; + +import com.loki.lomines.LoMinesPlugin; +import com.loki.lomines.core.Mines; +import com.loki.lomines.data.ConfigParseException; +import dev.lolib.commands.annotation.Arg; +import dev.lolib.commands.annotation.Command; +import dev.lolib.commands.annotation.Subcommand; +import dev.lolib.scheduler.Scheduler; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; + +import java.io.IOException; + +/** + * Mask commands for mine shape management. + * Provides commands for scanning and saving mask positions. + */ +@Command(value = "lm", permission = "lomines.use") +public class MaskCommands { + + private final LoMinesPlugin plugin; + private final Mines mines; + + public MaskCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.mines = plugin.getMines(); + } + + /** + * Scans selection for marker blocks ({@code mask.marker}, default pink concrete), saves mask positions and switches mine to mask fill mode. + * Run from the server main thread (scheduled automatically). + */ + @Subcommand(value = "maskscan", permission = "lomines.admin.maskscan") + public void maskScan(CommandSender sender, @Arg("mine") String name) { + Scheduler.get(plugin).run(() -> { + try { + int n = mines.scanAndSaveMask(name); + sender.sendMessage(Component.text( + "Mask mode: saved " + n + " cell(s) for mine '" + name + + "'. Reset will spawn ore only at those positions.")); + } catch (IllegalArgumentException e) { + sender.sendMessage(Component.text("Mine not found: " + name)); + } catch (IOException | ConfigParseException e) { + sender.sendMessage(Component.text("Mask scan failed: " + e.getMessage())); + plugin.loLogger().error("maskscan failed for " + name, e); + } + }); + } +} diff --git a/src/main/java/com/loki/lomines/command/MineCommands.java b/src/main/java/com/loki/lomines/command/MineCommands.java deleted file mode 100644 index 92d2b67..0000000 --- a/src/main/java/com/loki/lomines/command/MineCommands.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.loki.lomines.command; - -import com.loki.lomines.LoMinesPlugin; -import com.loki.lomines.core.Mine; -import com.loki.lomines.core.Mines; -import com.loki.lomines.data.ConfigParseException; -import com.loki.lomines.data.Leaderboard; -import com.loki.lomines.data.LeaderboardEntry; -import com.loki.lomines.data.PlayerStats; -import com.loki.lomines.data.StatsManager; -import com.loki.lomines.wand.GroupWandItem; -import dev.lolib.commands.annotation.Arg; -import dev.lolib.scheduler.Scheduler; -import dev.lolib.commands.annotation.Command; -import dev.lolib.commands.annotation.Subcommand; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.io.IOException; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * Main command class for mine management. - * Provides commands for creating, deleting, resetting, reloading, and listing mines. - */ -@Command(value = "lm", permission = "lomines.use") -public class MineCommands { - - private final LoMinesPlugin plugin; - private final Mines mines; - - public MineCommands(LoMinesPlugin plugin) { - this.plugin = plugin; - this.mines = plugin.getMines(); - } - - @Subcommand(value = "wand", permission = "lomines.admin.wand") - public void wand(CommandSender sender) { - if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только для игрока.")); - return; - } - player.getInventory().addItem(GroupWandItem.create(plugin)); - player.sendMessage(Component.text( - "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", - NamedTextColor.GREEN)); - } - - @Subcommand(value = "group", permission = "lomines.admin.wand") - public void group(CommandSender sender, @Arg("prefix") String prefix) { - if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Префикс задаётся в игре.")); - return; - } - plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix); - String p = prefix.trim(); - player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); - } - - /** - * Creates a new mine with the given name. - * Usage: /lm create - */ - @Subcommand(value = "create", permission = "lomines.admin.create") - public void create(CommandSender sender, @Arg("name") String name) { - try { - mines.create(name); - sender.sendMessage(Component.text("Successfully created mine: " + name)); - sender.sendMessage(Component.text( - "Edit selection & contents in plugins/LoMines/mines/" + name + ".yml — " - + "for shape-based ore (not full cuboid), paint mask.marker blocks, then: /lm maskscan " + name)); - } catch (IllegalArgumentException e) { - sender.sendMessage(Component.text("Mine already exists: " + name)); - } catch (IOException e) { - sender.sendMessage(Component.text("Failed to create mine: " + e.getMessage())); - plugin.loLogger().error("Failed to create mine " + name, e); - } - } - - /** - * Deletes an existing mine. - * Usage: /lm delete - */ - @Subcommand(value = "delete", permission = "lomines.admin.delete") - public void delete(CommandSender sender, @Arg("name") String name) { - try { - mines.delete(name); - sender.sendMessage(Component.text("Successfully deleted mine: " + name)); - } catch (IllegalArgumentException e) { - sender.sendMessage(Component.text("Mine not found: " + name)); - } catch (IOException e) { - sender.sendMessage(Component.text("Failed to delete mine: " + e.getMessage())); - plugin.loLogger().error("Failed to delete mine " + name, e); - } - } - - /** - * Resets a mine by refilling all its regions. - * Usage: /lm reset [silent] - */ - @Subcommand(value = "reset", permission = "lomines.admin.reset") - public void reset( - CommandSender sender, - @Arg("mine") String mineName, - @Arg(value = "silent", optional = true, defaultValue = "false") boolean silent - ) { - try { - Mine mine = mines.get(mineName); - mine.reset(silent); - sender.sendMessage(Component.text("Resetting mine: " + mineName)); - } catch (IllegalArgumentException e) { - sender.sendMessage(Component.text("Mine not found: " + mineName)); - } - } - - /** - * Reloads all mine configurations from disk. - * Usage: /lm reload - */ - @Subcommand(value = "reload", permission = "lomines.admin.reload") - public void reload(CommandSender sender) { - try { - // Stop all existing mines - mines.getAll().forEach(Mine::stop); - - // Reload all mines - mines.loadAll(); - - int count = mines.getAll().size(); - sender.sendMessage(Component.text("Successfully reloaded " + count + " mine(s)")); - } catch (IOException e) { - sender.sendMessage(Component.text("Failed to reload mines: " + e.getMessage())); - plugin.loLogger().error("Failed to reload mines", e); - } - } - - /** - * Lists all active mines. - * Usage: /lm list - */ - @Subcommand(value = "list", permission = "lomines.admin.list") - public void list(CommandSender sender) { - Collection allMines = mines.getAll(); - - if (allMines.isEmpty()) { - sender.sendMessage(Component.text("No mines found")); - return; - } - - sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ===")); - - for (Mine mine : allMines) { - String status = formatMineStatus(mine); - sender.sendMessage(Component.text("- " + mine.getName() + " " + status)); - } - } - - /** - * Formats mine status information for display. - */ - private String formatMineStatus(Mine mine) { - int blocks = mine.getBlocks(); - int total = mine.getTotalVolume(); - double percent = mine.getPercentFilled(); - - return String.format("(%d/%d blocks, %.1f%%)", blocks, total, percent); - } - - /** - * Shows mining stats for self or another player. - * Usage: /lm stats [player] - */ - @Subcommand(value = "stats", permission = "lomines.stats") - public void stats(CommandSender sender, @Arg(value = "player", optional = true, defaultValue = "") String targetName) { - if (isStatsDisabled(sender)) { - return; - } - String name = targetName == null ? "" : targetName.trim(); - UUID uuid; - if (name.isEmpty()) { - if (!(sender instanceof Player self)) { - sender.sendMessage(Component.text("Укажите игрока: /lm stats <ник>", NamedTextColor.RED)); - return; - } - uuid = self.getUniqueId(); - } else { - if (!sender.hasPermission("lomines.stats.others")) { - sender.sendMessage(Component.text("Нет прав смотреть чужую статистику.", NamedTextColor.RED)); - return; - } - Player online = Bukkit.getPlayerExact(name); - if (online != null) { - uuid = online.getUniqueId(); - } else { - OfflinePlayer off = Bukkit.getOfflinePlayer(name); - uuid = off.getUniqueId(); - } - } - StatsManager sm = plugin.getStatsManager(); - PlayerStats ps = sm.getOrCreate(uuid); - String displayName = formatPlayerName(uuid); - sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD)); - sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE)); - int pos = sm.getLeaderboard().getPosition(uuid); - if (pos > 0) { - sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY)); - } - Map byMine = ps.getMineStatsSnapshot(); - if (byMine.isEmpty()) { - return; - } - sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); - byMine.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .limit(12) - .forEach(e -> sender.sendMessage(Component.text( - " " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA))); - } - - /** - * Leaderboard: global or per-mine. - * Usage: /lm top — глобально 10; /lm top 25 — глобально 25; /lm top <шахта> — по шахте; - * /lm top <шахта> <лимит> - */ - @Subcommand(value = "top", permission = "lomines.stats") - public void top( - CommandSender sender, - @Arg(value = "arg1", optional = true, defaultValue = "") String arg1, - @Arg(value = "arg2", optional = true, defaultValue = "") String arg2 - ) { - if (isStatsDisabled(sender)) { - return; - } - String a = arg1 == null ? "" : arg1.trim(); - String b = arg2 == null ? "" : arg2.trim(); - int limit; - String mineName; - if (a.isEmpty()) { - limit = clampLimit(10); - mineName = null; - } else if (b.isEmpty()) { - Integer asLimit = tryParsePositiveInt(a); - if (asLimit != null) { - limit = clampLimit(asLimit); - mineName = null; - } else { - limit = clampLimit(10); - mineName = a; - } - } else { - mineName = a; - Integer limParsed = tryParsePositiveInt(b); - limit = clampLimit(limParsed != null ? limParsed : 10); - } - Leaderboard lb = plugin.getStatsManager().getLeaderboard(); - if (mineName != null) { - var found = mines.find(mineName); - if (found.isEmpty()) { - sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); - return; - } - Mine m = found.get(); - String key = m.getName(); - List rows = lb.getTopByMine(key, limit); - sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); - return; - } - List rows = lb.getTopTotal(limit); - sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD)); - sendTopRows(sender, rows); - } - - private static void sendTopRows(CommandSender sender, List rows) { - if (rows.isEmpty()) { - sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY)); - return; - } - for (int i = 0; i < rows.size(); i++) { - LeaderboardEntry e = rows.get(i); - String name = formatPlayerName(e.getPlayerId()); - sender.sendMessage(Component.text( - (i + 1) + ". " + name + " — " + e.getCount(), - NamedTextColor.WHITE)); - } - } - - private static String formatPlayerName(UUID id) { - String name = Bukkit.getOfflinePlayer(id).getName(); - return name != null ? name : id.toString(); - } - - private boolean isStatsDisabled(CommandSender sender) { - if (!plugin.getConfig().getBoolean("statistics-enabled", true)) { - sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED)); - return true; - } - return false; - } - - private static int clampLimit(int limit) { - return Math.max(1, Math.min(50, limit)); - } - - private static Integer tryParsePositiveInt(String s) { - try { - int v = Integer.parseInt(s); - return v > 0 ? v : null; - } catch (NumberFormatException e) { - return null; - } - } - - /** - * Scans selection for marker blocks ({@code mask.marker}, default pink concrete), saves mask positions and switches mine to mask fill mode. - * Run from the server main thread (scheduled automatically). - */ - @Subcommand(value = "maskscan", permission = "lomines.admin.maskscan") - public void maskScan(CommandSender sender, @Arg("mine") String name) { - Scheduler.get(plugin).run(() -> { - try { - int n = mines.scanAndSaveMask(name); - sender.sendMessage(Component.text( - "Mask mode: saved " + n + " cell(s) for mine '" + name - + "'. Reset will spawn ore only at those positions.")); - } catch (IllegalArgumentException e) { - sender.sendMessage(Component.text("Mine not found: " + name)); - } catch (IOException | ConfigParseException e) { - sender.sendMessage(Component.text("Mask scan failed: " + e.getMessage())); - plugin.loLogger().error("maskscan failed for " + name, e); - } - }); - } -} diff --git a/src/main/java/com/loki/lomines/command/PlayerCommands.java b/src/main/java/com/loki/lomines/command/PlayerCommands.java new file mode 100644 index 0000000..64d3039 --- /dev/null +++ b/src/main/java/com/loki/lomines/command/PlayerCommands.java @@ -0,0 +1,48 @@ +package com.loki.lomines.command; + +import com.loki.lomines.LoMinesPlugin; +import com.loki.lomines.wand.GroupWandItem; +import dev.lolib.commands.annotation.Arg; +import dev.lolib.commands.annotation.Command; +import dev.lolib.commands.annotation.Subcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +/** + * Player commands for mine interaction. + * Provides commands for wand and group management. + */ +@Command(value = "lm", permission = "lomines.use") +public class PlayerCommands { + + private final LoMinesPlugin plugin; + + public PlayerCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + } + + @Subcommand(value = "wand", permission = "lomines.admin.wand") + public void wand(CommandSender sender) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Только для игрока.")); + return; + } + player.getInventory().addItem(GroupWandItem.create(plugin)); + player.sendMessage(Component.text( + "Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>", + NamedTextColor.GREEN)); + } + + @Subcommand(value = "group", permission = "lomines.admin.wand") + public void group(CommandSender sender, @Arg("prefix") String prefix) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Component.text("Префикс задаётся в игре.")); + return; + } + plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix); + String p = prefix.trim(); + player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA)); + } +} diff --git a/src/main/java/com/loki/lomines/command/StatsCommands.java b/src/main/java/com/loki/lomines/command/StatsCommands.java new file mode 100644 index 0000000..45f53dd --- /dev/null +++ b/src/main/java/com/loki/lomines/command/StatsCommands.java @@ -0,0 +1,183 @@ +package com.loki.lomines.command; + +import com.loki.lomines.LoMinesPlugin; +import com.loki.lomines.core.Mine; +import com.loki.lomines.core.Mines; +import com.loki.lomines.data.Leaderboard; +import com.loki.lomines.data.LeaderboardEntry; +import com.loki.lomines.data.PlayerStats; +import com.loki.lomines.data.StatsManager; +import dev.lolib.commands.annotation.Arg; +import dev.lolib.commands.annotation.Command; +import dev.lolib.commands.annotation.Subcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Statistics commands for player mining stats and leaderboards. + * Provides commands for viewing stats and top players. + */ +@Command(value = "lm", permission = "lomines.use") +public class StatsCommands { + + private final LoMinesPlugin plugin; + private final Mines mines; + + public StatsCommands(LoMinesPlugin plugin) { + this.plugin = plugin; + this.mines = plugin.getMines(); + } + + /** + * Shows mining stats for self or another player. + * Usage: /lm stats [player] + */ + @Subcommand(value = "stats", permission = "lomines.stats") + public void stats(CommandSender sender, @Arg(value = "player", optional = true, defaultValue = "") String targetName) { + if (isStatsDisabled(sender)) { + return; + } + String name = targetName == null ? "" : targetName.trim(); + UUID uuid; + if (name.isEmpty()) { + if (!(sender instanceof Player self)) { + sender.sendMessage(Component.text("Укажите игрока: /lm stats <ник>", NamedTextColor.RED)); + return; + } + uuid = self.getUniqueId(); + } else { + if (!sender.hasPermission("lomines.stats.others")) { + sender.sendMessage(Component.text("Нет прав смотреть чужую статистику.", NamedTextColor.RED)); + return; + } + Player online = Bukkit.getPlayerExact(name); + if (online != null) { + uuid = online.getUniqueId(); + } else { + OfflinePlayer off = Bukkit.getOfflinePlayer(name); + uuid = off.getUniqueId(); + } + } + StatsManager sm = plugin.getStatsManager(); + PlayerStats ps = sm.getOrCreate(uuid); + String displayName = formatPlayerName(uuid); + sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE)); + int pos = sm.getLeaderboard().getPosition(uuid); + if (pos > 0) { + sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY)); + } + Map byMine = ps.getMineStatsSnapshot(); + if (byMine.isEmpty()) { + return; + } + sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY)); + byMine.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(12) + .forEach(e -> sender.sendMessage(Component.text( + " " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA))); + } + + /** + * Leaderboard: global or per-mine. + * Usage: /lm top — глобально 10; /lm top 25 — глобально 25; /lm top <шахта> — по шахте; + * /lm top <шахта> <лимит> + */ + @Subcommand(value = "top", permission = "lomines.stats") + public void top( + CommandSender sender, + @Arg(value = "arg1", optional = true, defaultValue = "") String arg1, + @Arg(value = "arg2", optional = true, defaultValue = "") String arg2 + ) { + if (isStatsDisabled(sender)) { + return; + } + String a = arg1 == null ? "" : arg1.trim(); + String b = arg2 == null ? "" : arg2.trim(); + int limit; + String mineName; + if (a.isEmpty()) { + limit = clampLimit(10); + mineName = null; + } else if (b.isEmpty()) { + Integer asLimit = tryParsePositiveInt(a); + if (asLimit != null) { + limit = clampLimit(asLimit); + mineName = null; + } else { + limit = clampLimit(10); + mineName = a; + } + } else { + mineName = a; + Integer limParsed = tryParsePositiveInt(b); + limit = clampLimit(limParsed != null ? limParsed : 10); + } + Leaderboard lb = plugin.getStatsManager().getLeaderboard(); + if (mineName != null) { + var found = mines.find(mineName); + if (found.isEmpty()) { + sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED)); + return; + } + Mine m = found.get(); + String key = m.getName(); + List rows = lb.getTopByMine(key, limit); + sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD)); + sendTopRows(sender, rows); + return; + } + List rows = lb.getTopTotal(limit); + sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD)); + sendTopRows(sender, rows); + } + + private static void sendTopRows(CommandSender sender, List rows) { + if (rows.isEmpty()) { + sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY)); + return; + } + for (int i = 0; i < rows.size(); i++) { + LeaderboardEntry e = rows.get(i); + String name = formatPlayerName(e.getPlayerId()); + sender.sendMessage(Component.text( + (i + 1) + ". " + name + " — " + e.getCount(), + NamedTextColor.WHITE)); + } + } + + private static String formatPlayerName(UUID id) { + String name = Bukkit.getOfflinePlayer(id).getName(); + return name != null ? name : id.toString(); + } + + private boolean isStatsDisabled(CommandSender sender) { + if (!plugin.getConfig().getBoolean("statistics-enabled", true)) { + sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED)); + return true; + } + return false; + } + + private static int clampLimit(int limit) { + return Math.max(1, Math.min(50, limit)); + } + + private static Integer tryParsePositiveInt(String s) { + try { + int v = Integer.parseInt(s); + return v > 0 ? v : null; + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/src/test/java/com/loki/lomines/command/MineCommandsTest.java b/src/test/java/com/loki/lomines/command/AdminCommandsTest.java similarity index 90% rename from src/test/java/com/loki/lomines/command/MineCommandsTest.java rename to src/test/java/com/loki/lomines/command/AdminCommandsTest.java index cf061f6..ebfeeb4 100644 --- a/src/test/java/com/loki/lomines/command/MineCommandsTest.java +++ b/src/test/java/com/loki/lomines/command/AdminCommandsTest.java @@ -17,124 +17,124 @@ import java.util.Collection; import static org.mockito.Mockito.*; /** - * Unit tests for MineCommands class. + * Unit tests for AdminCommands class. */ -class MineCommandsTest { - +class AdminCommandsTest { + @Mock private LoMinesPlugin plugin; - + @Mock private Mines mines; - + @Mock private CommandSender sender; - + @Mock private Mine mine; - - private MineCommands commands; - + + private AdminCommands commands; + @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); when(plugin.getMines()).thenReturn(mines); - commands = new MineCommands(plugin); + commands = new AdminCommands(plugin); } - + @Test void testCreateMineSuccess() throws IOException { String mineName = "testmine"; - + commands.create(sender, mineName); - + verify(mines).create(mineName); verify(sender, times(2)).sendMessage(any(Component.class)); } - + @Test void testCreateMineAlreadyExists() throws IOException { String mineName = "existing"; doThrow(new IllegalArgumentException("Mine already exists")).when(mines).create(mineName); - + commands.create(sender, mineName); - + verify(sender).sendMessage(any(Component.class)); } - + @Test void testDeleteMineSuccess() throws IOException { String mineName = "testmine"; - + commands.delete(sender, mineName); - + verify(mines).delete(mineName); verify(sender).sendMessage(any(Component.class)); } - + @Test void testDeleteMineNotFound() throws IOException { String mineName = "nonexistent"; doThrow(new IllegalArgumentException("Mine not found")).when(mines).delete(mineName); - + commands.delete(sender, mineName); - + verify(sender).sendMessage(any(Component.class)); } - + @Test void testResetMineSuccess() { String mineName = "testmine"; when(mines.get(mineName)).thenReturn(mine); - + commands.reset(sender, mineName, false); - + verify(mine).reset(false); verify(sender).sendMessage(any(Component.class)); } - + @Test void testResetMineSilent() { String mineName = "testmine"; when(mines.get(mineName)).thenReturn(mine); - + commands.reset(sender, mineName, true); - + verify(mine).reset(true); verify(sender).sendMessage(any(Component.class)); } - + @Test void testResetMineNotFound() { String mineName = "nonexistent"; when(mines.get(mineName)).thenThrow(new IllegalArgumentException("Mine not found")); - + commands.reset(sender, mineName, false); - + verify(sender).sendMessage(any(Component.class)); } - + @Test void testReloadSuccess() throws IOException { Collection mineList = Arrays.asList(mine, mine); when(mines.getAll()).thenReturn(mineList); - + commands.reload(sender); - + verify(mine, times(2)).stop(); verify(mines).loadAll(); verify(sender).sendMessage(any(Component.class)); } - + @Test void testListMinesEmpty() { when(mines.getAll()).thenReturn(Arrays.asList()); - + commands.list(sender); - + verify(sender).sendMessage(any(Component.class)); } - + @Test void testListMinesWithData() { when(mines.getAll()).thenReturn(Arrays.asList(mine, mine)); @@ -142,9 +142,9 @@ class MineCommandsTest { when(mine.getBlocks()).thenReturn(100); when(mine.getTotalVolume()).thenReturn(200); when(mine.getPercentFilled()).thenReturn(50.0); - + commands.list(sender); - + verify(sender, atLeast(3)).sendMessage(any(Component.class)); } }