refactor: split MineCommands into separate command classes
This commit is contained in:
parent
d317d0b1b0
commit
2dc25432fd
7 changed files with 464 additions and 383 deletions
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
138
src/main/java/com/loki/lomines/command/AdminCommands.java
Normal file
138
src/main/java/com/loki/lomines/command/AdminCommands.java
Normal file
|
|
@ -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 <name>
|
||||
*/
|
||||
@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 <name>
|
||||
*/
|
||||
@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 <mine> [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<Mine> 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);
|
||||
}
|
||||
}
|
||||
50
src/main/java/com/loki/lomines/command/MaskCommands.java
Normal file
50
src/main/java/com/loki/lomines/command/MaskCommands.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <name>
|
||||
*/
|
||||
@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 <name>
|
||||
*/
|
||||
@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 <mine> [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<Mine> 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<String, Long> byMine = ps.getMineStatsSnapshot();
|
||||
if (byMine.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY));
|
||||
byMine.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Long>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<LeaderboardEntry> rows = lb.getTopByMine(key, limit);
|
||||
sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD));
|
||||
sendTopRows(sender, rows);
|
||||
return;
|
||||
}
|
||||
List<LeaderboardEntry> rows = lb.getTopTotal(limit);
|
||||
sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD));
|
||||
sendTopRows(sender, rows);
|
||||
}
|
||||
|
||||
private static void sendTopRows(CommandSender sender, List<LeaderboardEntry> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/loki/lomines/command/PlayerCommands.java
Normal file
48
src/main/java/com/loki/lomines/command/PlayerCommands.java
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
183
src/main/java/com/loki/lomines/command/StatsCommands.java
Normal file
183
src/main/java/com/loki/lomines/command/StatsCommands.java
Normal file
|
|
@ -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<String, Long> byMine = ps.getMineStatsSnapshot();
|
||||
if (byMine.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY));
|
||||
byMine.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Long>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<LeaderboardEntry> rows = lb.getTopByMine(key, limit);
|
||||
sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD));
|
||||
sendTopRows(sender, rows);
|
||||
return;
|
||||
}
|
||||
List<LeaderboardEntry> rows = lb.getTopTotal(limit);
|
||||
sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD));
|
||||
sendTopRows(sender, rows);
|
||||
}
|
||||
|
||||
private static void sendTopRows(CommandSender sender, List<LeaderboardEntry> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Mine> 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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue