chore: rewrite ARCHITECTURE, enable tests via MockBukkit, checkstyle for tests, cleanup bin/libs

This commit is contained in:
loki5512344 2026-08-12 13:59:33 +02:00
parent a0c1ab8ca1
commit e8d516020c
Signed by: boba
GPG key ID: 253067914055423B
53 changed files with 1093 additions and 1410 deletions

View file

@ -3,6 +3,7 @@ package dev.loki.lomines.command.admin.info;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@ -40,8 +41,16 @@ public class InfoCommand {
sender.sendMessage(Component.text("§7Блоков: §f" + mine.getBlocks() + "§7/§f" + mine.getTotalVolume() +
" §7(§f" + String.format("%.1f", mine.getPercentFilled()) + "%§7)"));
printRegions(sender, mine.getRegions());
printBlocks(sender, config.blocks().weights());
printResetAndTeleport(sender, config);
sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей"));
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
private void printRegions(CommandSender sender, List<Cuboid> regions) {
sender.sendMessage(Component.text("§6§lРегионы:"));
List<Cuboid> regions = mine.getRegions();
for (int i = 0; i < regions.size(); i++) {
Cuboid r = regions.get(i);
sender.sendMessage(Component.text("§7 #" + (i + 1) + ": §f" +
@ -50,8 +59,10 @@ public class InfoCommand {
" §7(§f" + r.getVolume() + " §7блоков)"));
}
sender.sendMessage(Component.text(""));
}
private void printBlocks(CommandSender sender, Map<BlockKey, Double> weights) {
sender.sendMessage(Component.text("§6§lБлоки:"));
Map<BlockKey, Double> weights = config.blocks().weights();
weights.entrySet().stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(10)
@ -60,7 +71,9 @@ public class InfoCommand {
if (weights.size() > 10) {
sender.sendMessage(Component.text("§7 ... и ещё " + (weights.size() - 10) + " блоков"));
}
}
private void printResetAndTeleport(CommandSender sender, MineConfig config) {
sender.sendMessage(Component.text("§6§lНастройки сброса:"));
sender.sendMessage(Component.text("§7 Интервал: §f" + config.reset().intervalDisplay()));
sender.sendMessage(Component.text("§7 Триггер по %: §f" +
@ -71,9 +84,6 @@ public class InfoCommand {
(config.teleport().enabled() ? "§aустановлена" : "§7не задана")));
sender.sendMessage(Component.text("§7 Точка спавна: §f" +
(config.playerSpawn().enabled() ? "§aустановлена" : "§7используется точка телепорта")));
sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей"));
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
private String formatBlockKey(BlockKey key) {

View file

@ -33,6 +33,20 @@ public final class RegionActionHandler {
return;
}
Cuboid newRegion = resolveNewRegion(player, mine);
if (newRegion == null) {
return;
}
List<Cuboid> currentRegions = new ArrayList<>(mine.getRegions());
currentRegions.add(newRegion);
updateMineRegions(mine, new RegionConfig(currentRegions));
sendRegionAdded(player, newRegion, currentRegions.size());
}
private Cuboid resolveNewRegion(Player player, Mine mine) {
var wandManager = plugin.getGroupWandManager();
var session = wandManager.getSession(player.getUniqueId());
int slot = session.getActiveSlot();
@ -40,7 +54,7 @@ public final class RegionActionHandler {
var pos2 = session.getPos2(slot);
if (pos1 == null || pos2 == null) {
player.sendMessage(Component.text("§cВыделите регион палочкой! ЛКМ - 1-я точка, ПКМ - 2-я точка"));
return;
return null;
}
Cuboid newRegion = new Cuboid(pos1, pos2);
String mineWorld = mine.getConfig().region().worldName();
@ -48,21 +62,17 @@ public final class RegionActionHandler {
if (!mineWorld.equals(regionWorld)) {
player.sendMessage(Component.text("§cРегион должен быть в мире §f" + mineWorld + "§c, а не §f" + regionWorld));
return;
return null;
}
session.clearCorners(slot);
return newRegion;
}
List<Cuboid> currentRegions = new ArrayList<>(mine.getRegions());
currentRegions.add(newRegion);
var newRegionConfig = new RegionConfig(currentRegions);
updateMineRegions(mine, newRegionConfig);
player.sendMessage(Component.text("§aДобавлен регион #" + currentRegions.size()));
private void sendRegionAdded(Player player, Cuboid newRegion, int count) {
player.sendMessage(Component.text("§aДобавлен регион #" + count));
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.clearCorners(slot);
}
public void removeRegion(CommandSender sender, String mineName, int index) {

View file

@ -21,53 +21,59 @@ public class RegionCommands {
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "regions" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm regions <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
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━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
case "addregion" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm addregion <mine>", NamedTextColor.RED));
return;
}
actionHandler.addRegion(sender, args[0]);
}
case "removeregion" -> {
if (args.length < 2) {
sender.sendMessage(Component.text("Usage: /lm removeregion <mine> <index>", NamedTextColor.RED));
return;
}
try {
int index = Integer.parseInt(args[1]);
actionHandler.removeRegion(sender, args[0], index);
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("Индекс должен быть числом: " + args[1], NamedTextColor.RED));
}
}
case "regions" -> handleRegions(sender, args);
case "addregion" -> handleAddRegion(sender, args);
case "removeregion" -> handleRemoveRegion(sender, args);
default -> {}
}
}
private void handleRegions(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm regions <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
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━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
private void handleAddRegion(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm addregion <mine>", NamedTextColor.RED));
return;
}
actionHandler.addRegion(sender, args[0]);
}
private void handleRemoveRegion(CommandSender sender, String[] args) {
if (args.length < 2) {
sender.sendMessage(Component.text("Usage: /lm removeregion <mine> <index>", NamedTextColor.RED));
return;
}
try {
int index = Integer.parseInt(args[1]);
actionHandler.removeRegion(sender, args[0], index);
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("Индекс должен быть числом: " + args[1], NamedTextColor.RED));
}
}
}

View file

@ -25,52 +25,62 @@ public class AdminCommands {
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "create" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm create <name>"));
return;
}
actionHandler.create(sender, args[0]);
}
case "delete" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm delete <name>"));
return;
}
actionHandler.delete(sender, args[0]);
}
case "edit" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm edit <mine>"));
return;
}
actionHandler.edit(sender, args[0]);
}
case "reset" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm reset <mine> [silent]"));
return;
}
boolean silent = args.length > 1 && (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("silent"));
actionHandler.reset(sender, args[0], silent);
}
case "create" -> handleCreate(sender, args);
case "delete" -> handleDelete(sender, args);
case "edit" -> handleEdit(sender, args);
case "reset" -> handleReset(sender, args);
case "reload" -> actionHandler.reload(sender);
case "list" -> {
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));
}
}
case "list" -> handleList(sender);
default -> {}
}
}
private void handleCreate(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm create <name>"));
return;
}
actionHandler.create(sender, args[0]);
}
private void handleDelete(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm delete <name>"));
return;
}
actionHandler.delete(sender, args[0]);
}
private void handleEdit(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm edit <mine>"));
return;
}
actionHandler.edit(sender, args[0]);
}
private void handleReset(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm reset <mine> [silent]"));
return;
}
boolean silent = args.length > 1 && (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("silent"));
actionHandler.reset(sender, args[0], silent);
}
private void handleList(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));
}
}
private String formatMineStatus(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();

View file

@ -23,27 +23,51 @@ public class CopyCommand {
String fromMineName = args[0];
String toMineName = args[1];
MinePair pair = resolveMines(sender, fromMineName, toMineName);
if (pair == null) {
return;
}
var sourceConfig = pair.fromMine().getConfig();
MineConfig newTargetConfig = buildCopiedConfig(pair.toMine(), sourceConfig);
try {
plugin.getMines().updateMineConfig(pair.toMine().getName(), newTargetConfig);
} catch (Exception e) {
sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED));
return;
}
sendCopySummary(sender, pair.fromMine(), pair.toMine(), sourceConfig);
}
private record MinePair(Mine fromMine, Mine toMine) {
}
private MinePair resolveMines(CommandSender sender, String fromMineName, String toMineName) {
Mine fromMine = plugin.getMines().find(fromMineName).orElse(null);
if (fromMine == null) {
sender.sendMessage(Component.text("Исходная шахта не найдена: " + fromMineName, NamedTextColor.RED));
return;
return null;
}
Mine toMine = plugin.getMines().find(toMineName).orElse(null);
if (toMine == null) {
sender.sendMessage(Component.text("Целевая шахта не найдена: " + toMineName, NamedTextColor.RED));
return;
return null;
}
if (fromMine.getName().equalsIgnoreCase(toMine.getName())) {
sender.sendMessage(Component.text("Нельзя копировать шахту саму в себя!", NamedTextColor.RED));
return;
return null;
}
return new MinePair(fromMine, toMine);
}
var sourceConfig = fromMine.getConfig();
private MineConfig buildCopiedConfig(Mine toMine, MineConfig sourceConfig) {
var targetConfig = toMine.getConfig();
MineConfig newTargetConfig = MineConfig.builder(toMine.getName())
return MineConfig.builder(toMine.getName())
.region(targetConfig.region())
.blocks(sourceConfig.blocks())
.reset(sourceConfig.reset())
@ -53,14 +77,9 @@ public class CopyCommand {
.worldGuard(sourceConfig.worldGuard())
.playerSpawn(sourceConfig.playerSpawn())
.build();
}
try {
plugin.getMines().updateMineConfig(toMine.getName(), newTargetConfig);
} catch (Exception e) {
sender.sendMessage(Component.text("Ошибка сохранения: " + e.getMessage(), NamedTextColor.RED));
return;
}
private void sendCopySummary(CommandSender sender, Mine fromMine, Mine toMine, MineConfig sourceConfig) {
sender.sendMessage(Component.text("§aКонфигурация скопирована из §f" + fromMine.getName() +
" §aв §f" + toMine.getName()));
sender.sendMessage(Component.text("§7Скопировано:"));

View file

@ -40,18 +40,7 @@ public final class TeleportActionHandler {
}
var config = mine.getConfig();
var newTeleport = TeleportConfig.at(player.getLocation());
var updatedConfig = MineConfig.builder(mineName)
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(newTeleport)
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
var updatedConfig = withTeleport(config, TeleportConfig.at(player.getLocation()));
plugin.getMines().getFileManager().saveConfig(updatedConfig);
sender.sendMessage(MessageFormatter.success("Teleport location for mine '" + mineName + "' set to your current position"));
@ -76,18 +65,7 @@ public final class TeleportActionHandler {
}
var config = mine.getConfig();
var newSpawn = PlayerSpawnConfig.at(player.getLocation());
var updatedConfig = MineConfig.builder(mineName)
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(newSpawn)
.build();
var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.at(player.getLocation()));
plugin.getMines().getFileManager().saveConfig(updatedConfig);
sender.sendMessage(MessageFormatter.success("Safe spawn location for mine '" + mineName + "' set to your current position"));
@ -108,16 +86,7 @@ public final class TeleportActionHandler {
}
var config = mine.getConfig();
var updatedConfig = MineConfig.builder(mineName)
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(PlayerSpawnConfig.disabled())
.build();
var updatedConfig = withPlayerSpawn(config, PlayerSpawnConfig.disabled());
plugin.getMines().getFileManager().saveConfig(updatedConfig);
sender.sendMessage(MessageFormatter.success("Spawn location for mine '" + mineName + "' cleared"));
@ -128,6 +97,32 @@ public final class TeleportActionHandler {
}
}
private MineConfig withTeleport(MineConfig config, TeleportConfig teleport) {
return MineConfig.builder(config.name())
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(teleport)
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
}
private MineConfig withPlayerSpawn(MineConfig config, PlayerSpawnConfig spawn) {
return MineConfig.builder(config.name())
.region(config.region())
.blocks(config.blocks())
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(spawn)
.build();
}
public String formatLocation(Location loc) {
return String.format("%.1f, %.1f, %.1f in %s",
loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName());

View file

@ -3,7 +3,6 @@ package dev.loki.lomines.command.admin.stats;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.stats.model.Leaderboard;
import dev.loki.lomines.data.stats.model.LeaderboardEntry;
import dev.loki.lomines.util.ErrorHandler;
import net.kyori.adventure.text.Component;
@ -77,43 +76,46 @@ public final class LeaderboardRenderer {
}
public static void renderTopCommand(CommandSender sender, String arg1, String arg2, Mines mines, LoMinesPlugin plugin) {
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("=== Топ по шахте \u00ab" + key + "\u00bb ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
TopRequest req = parseTopRequest(arg1, arg2);
if (req.mineName() != null) {
renderMineTop(sender, req.mineName(), req.limit(), mines, plugin);
return;
}
List<LeaderboardEntry> rows = lb.getTopTotal(limit);
List<LeaderboardEntry> rows = plugin.getStatsManager().getLeaderboard().getTopTotal(req.limit());
sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
}
private record TopRequest(int limit, String mineName) {
}
private static TopRequest parseTopRequest(String arg1, String arg2) {
String a = arg1 == null ? "" : arg1.trim();
String b = arg2 == null ? "" : arg2.trim();
if (a.isEmpty()) {
return new TopRequest(clampLimit(10), null);
}
if (b.isEmpty()) {
Integer asLimit = tryParsePositiveInt(a);
if (asLimit != null) {
return new TopRequest(clampLimit(asLimit), null);
}
return new TopRequest(clampLimit(10), a);
}
Integer limParsed = tryParsePositiveInt(b);
return new TopRequest(clampLimit(limParsed != null ? limParsed : 10), a);
}
private static void renderMineTop(CommandSender sender, String mineName, int limit, Mines mines, LoMinesPlugin plugin) {
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 = plugin.getStatsManager().getLeaderboard().getTopByMine(key, limit);
sender.sendMessage(Component.text("=== Топ по шахте \u00ab" + key + "\u00bb ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
}
}

View file

@ -26,36 +26,7 @@ public class StatsCommands {
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "stats" -> {
if (isStatsDisabled(sender)) {
return;
}
String targetName = args.length > 0 ? args[0] : "";
UUID uuid = LeaderboardRenderer.resolvePlayerUuid(sender, targetName, errorHandler);
if (uuid == null) {
return;
}
StatsManager sm = plugin.getStatsManager();
PlayerStats ps = sm.getOrCreate(uuid);
String displayName = LeaderboardRenderer.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)));
}
case "stats" -> handleStats(sender, args);
case "top" -> {
if (isStatsDisabled(sender)) {
return;
@ -68,6 +39,37 @@ public class StatsCommands {
}
}
private void handleStats(CommandSender sender, String[] args) {
if (isStatsDisabled(sender)) {
return;
}
String targetName = args.length > 0 ? args[0] : "";
UUID uuid = LeaderboardRenderer.resolvePlayerUuid(sender, targetName, errorHandler);
if (uuid == null) {
return;
}
StatsManager sm = plugin.getStatsManager();
PlayerStats ps = sm.getOrCreate(uuid);
String displayName = LeaderboardRenderer.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)));
}
private boolean isStatsDisabled(CommandSender sender) {
if (!plugin.getConfig().getBoolean("statistics-enabled", true)) {
sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED));

View file

@ -35,19 +35,8 @@ public class TeleportCommand {
return;
}
var config = mine.getConfig();
Location target;
if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) {
target = config.teleport().getLocation().get();
} else if (!mine.getRegions().isEmpty()) {
Cuboid region = mine.getRegions().get(0);
double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5;
double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5;
double centerY = region.getMaxY() + 1;
target = new Location(region.getWorld(), centerX, centerY, centerZ);
} else {
player.sendMessage(Component.text("У шахты нет настроенной точки телепорта!", NamedTextColor.RED));
Location target = resolveTarget(player, mine);
if (target == null) {
return;
}
@ -55,4 +44,20 @@ public class TeleportCommand {
player.sendMessage(Component.text("§aТелепортировано к шахте §f" + mine.getName() +
" §a(§f" + String.format("%.0f, %.0f, %.0f", target.getX(), target.getY(), target.getZ()) + "§a)"));
}
private Location resolveTarget(Player player, Mine mine) {
var config = mine.getConfig();
if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) {
return config.teleport().getLocation().get();
}
if (!mine.getRegions().isEmpty()) {
Cuboid region = mine.getRegions().get(0);
double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5;
double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5;
double centerY = region.getMaxY() + 1;
return new Location(region.getWorld(), centerX, centerY, centerZ);
}
player.sendMessage(Component.text("У шахты нет настроенной точки телепорта!", NamedTextColor.RED));
return null;
}
}

View file

@ -1,11 +0,0 @@
/**
* Core components of the LoMines plugin.
* <p>
* This package contains the main plugin class and core system classes:
* <ul>
* <li>{@code LoMinesPlugin} - Main plugin class extending LoPlugin</li>
* <li>{@code Mines} - Registry for managing all active mines</li>
* <li>{@code Mine} - Representation of a single mine with its configuration and state</li>
* </ul>
*/
package dev.loki.lomines.core.common;

View file

@ -44,6 +44,7 @@ public final class Mines {
}
public void create(String name, Location corner1, Location corner2) throws IOException {
name = name.toLowerCase().trim();
if (repository.exists(name)) {
throw new IllegalArgumentException("Mine already exists: " + name);
}

View file

@ -42,22 +42,23 @@ public final class BlockConfigLoader {
yaml.getString("blocks.fill-mode", "CUBOID").toUpperCase()
);
// Parse mask config if applicable
BlockConfig.MaskConfig mask = null;
if (fillMode == FillMode.MASK) {
String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete");
BlockKey marker = BlockKey.deserialize(markerStr);
return new BlockConfig(weights, fillMode, parseMaskConfig(yaml, fillMode));
}
Map<String, Boolean> positions = new HashMap<>();
List<String> posList = yaml.getStringList("blocks.mask.positions");
for (String pos : posList) {
positions.put(pos, true);
}
private BlockConfig.MaskConfig parseMaskConfig(YamlConfiguration yaml, FillMode fillMode) {
if (fillMode != FillMode.MASK) {
return null;
}
String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete");
BlockKey marker = BlockKey.deserialize(markerStr);
mask = new BlockConfig.MaskConfig(marker, positions);
Map<String, Boolean> positions = new HashMap<>();
List<String> posList = yaml.getStringList("blocks.mask.positions");
for (String pos : posList) {
positions.put(pos, true);
}
return new BlockConfig(weights, fillMode, mask);
return new BlockConfig.MaskConfig(marker, positions);
}
/**

View file

@ -22,44 +22,14 @@ public final class WorldGuardConfigLoader {
* Supports any WorldGuard flags. Default: block-break=allow.
*/
public WorldGuardConfig parse(YamlConfiguration yaml) {
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
// Check defaults
boolean defaultEnabled = defaults.getBoolean("worldguard.enabled", false);
if (!defaultEnabled) {
return WorldGuardConfig.disabled();
}
}
boolean enabled = yaml.getBoolean("worldguard.enabled",
defaults.getBoolean("worldguard.enabled", false));
if (!enabled) {
if (!isEnabled(yaml)) {
return WorldGuardConfig.disabled();
}
List<String> owners = yaml.getStringList("worldguard.owners");
if (owners.isEmpty()) {
owners = defaults.getStringList("worldguard.owners");
}
List<String> members = yaml.getStringList("worldguard.members");
if (members.isEmpty()) {
members = defaults.getStringList("worldguard.members");
}
// Support any WorldGuard flags. Default: only block-break=allow
List<String> flags = yaml.getStringList("worldguard.flags");
if (flags.isEmpty()) {
flags = defaults.getStringList("worldguard.flags");
}
if (flags.isEmpty()) {
flags = List.of("block-break=allow");
}
boolean protect = yaml.getBoolean("worldguard.protect-on-create",
defaults.getBoolean("worldguard.protect-on-create", true));
List<String> owners = resolveOwners(yaml);
List<String> members = resolveMembers(yaml);
List<String> flags = resolveFlags(yaml);
boolean protect = resolveProtect(yaml);
String tmpl = yaml.getString("worldguard.region-template",
defaults.getString("worldguard.region-template", "{mine_name}_{random_4}"));
@ -73,6 +43,50 @@ public final class WorldGuardConfigLoader {
.build();
}
private boolean isEnabled(YamlConfiguration yaml) {
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
boolean defaultEnabled = defaults.getBoolean("worldguard.enabled", false);
if (!defaultEnabled) {
return false;
}
}
return yaml.getBoolean("worldguard.enabled", defaults.getBoolean("worldguard.enabled", false));
}
private List<String> resolveOwners(YamlConfiguration yaml) {
List<String> owners = yaml.getStringList("worldguard.owners");
if (owners.isEmpty()) {
owners = defaults.getStringList("worldguard.owners");
}
return owners;
}
private List<String> resolveMembers(YamlConfiguration yaml) {
List<String> members = yaml.getStringList("worldguard.members");
if (members.isEmpty()) {
members = defaults.getStringList("worldguard.members");
}
return members;
}
private List<String> resolveFlags(YamlConfiguration yaml) {
// Support any WorldGuard flags. Default: only block-break=allow
List<String> flags = yaml.getStringList("worldguard.flags");
if (flags.isEmpty()) {
flags = defaults.getStringList("worldguard.flags");
}
if (flags.isEmpty()) {
flags = List.of("block-break=allow");
}
return flags;
}
private boolean resolveProtect(YamlConfiguration yaml) {
return yaml.getBoolean("worldguard.protect-on-create",
defaults.getBoolean("worldguard.protect-on-create", true));
}
/**
* Saves WorldGuard configuration to YAML.
*/

View file

@ -36,16 +36,31 @@ public final class RewardConfigLoader {
}
private RewardEntry parseRewardEntry(Map<?, ?> map) {
List<BlockKey> blocks = parseBlocks(map.get("blocks"));
List<ItemReward> items = parseItems(map.get("items"));
List<String> commands = parseCommands(map.get("commands"));
Object preventDropsObj = map.get("prevent-drops");
boolean preventDrops = preventDropsObj instanceof Boolean b && b;
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
return new RewardEntry(blocks, chance, items, commands, preventDrops);
}
private List<BlockKey> parseBlocks(Object rawBlocks) {
List<BlockKey> blocks = new ArrayList<>();
List<?> blockList = (List<?>) map.get("blocks");
List<?> blockList = (List<?>) rawBlocks;
if (blockList != null) {
for (Object b : blockList) {
blocks.add(BlockKey.deserialize(b.toString()));
}
}
return blocks;
}
private List<ItemReward> parseItems(Object rawItems) {
List<ItemReward> items = new ArrayList<>();
List<?> itemList = (List<?>) map.get("items");
List<?> itemList = (List<?>) rawItems;
if (itemList != null) {
for (Object i : itemList) {
@SuppressWarnings("unchecked")
@ -53,9 +68,11 @@ public final class RewardConfigLoader {
items.add(parseItemReward(itemMap));
}
}
return items;
}
private List<String> parseCommands(Object commandsObj) {
List<String> commands = new ArrayList<>();
Object commandsObj = map.get("commands");
if (commandsObj instanceof List<?> list) {
for (Object obj : list) {
if (obj != null) {
@ -63,12 +80,7 @@ public final class RewardConfigLoader {
}
}
}
Object preventDropsObj = map.get("prevent-drops");
boolean preventDrops = preventDropsObj instanceof Boolean b && b;
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
return new RewardEntry(blocks, chance, items, commands, preventDrops);
return commands;
}
private ItemReward parseItemReward(Map<String, Object> map) {

View file

@ -29,29 +29,30 @@ public final class RewardParser {
}
for (int i = 0; i < rewardsList.size(); i++) {
Object rewardObj = rewardsList.get(i);
if (!(rewardObj instanceof Map)) {
throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
Map<String, Object> rewardMap = (Map<String, Object>) rewardObj;
try {
Reward reward = entryParser.parseReward(rewardMap);
rewards.add(reward);
} catch (ConfigParseException e) {
throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(),
e
);
}
rewards.add(parseRewardAt(rewardsList.get(i), i));
}
return rewards;
}
private Reward parseRewardAt(Object rewardObj, int i) throws ConfigParseException {
if (!(rewardObj instanceof Map)) {
throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
Map<String, Object> rewardMap = (Map<String, Object>) rewardObj;
try {
return entryParser.parseReward(rewardMap);
} catch (ConfigParseException e) {
throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(),
e
);
}
}
}

View file

@ -43,34 +43,38 @@ public final class StatsPersistence {
}
for (String uuidString : playersSection.getKeys(false)) {
try {
UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) {
continue;
}
PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new);
long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks);
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) {
for (String mineName : minesSection.getKeys(false)) {
long mineBlocks = minesSection.getLong(mineName, 0);
playerStats.setMineBlocks(mineName, mineBlocks);
}
}
} catch (IllegalArgumentException e) {
plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString);
}
loadPlayer(stats, uuidString, playersSection);
}
plugin.getLogger().info("Loaded statistics for " + stats.size() + " player(s)");
}
private void loadPlayer(Map<UUID, PlayerStats> stats, String uuidString, ConfigurationSection playersSection) {
try {
UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) {
return;
}
PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new);
long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks);
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) {
for (String mineName : minesSection.getKeys(false)) {
long mineBlocks = minesSection.getLong(mineName, 0);
playerStats.setMineBlocks(mineName, mineBlocks);
}
}
} catch (IllegalArgumentException e) {
plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString);
}
}
public void save(Map<UUID, PlayerStats> stats) throws IOException {
YamlConfiguration yaml = new YamlConfiguration();

View file

@ -94,6 +94,20 @@ public final class GroupCreateGui {
return;
}
CreationResult result = createSlots(plugin, player, session, base);
if (result.created() == 0) {
player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text(
"Создано шахт: " + result.created() + (result.skipped() > 0 ? " (пропущено: " + result.skipped() + ")" : ""),
NamedTextColor.GREEN));
}
private record CreationResult(int created, int skipped) {
}
private static CreationResult createSlots(LoMinesPlugin plugin, Player player, GroupWandSession session, String base) {
Mines mines = plugin.getMines();
int created = 0;
int skipped = 0;
@ -115,13 +129,6 @@ public final class GroupCreateGui {
e.printStackTrace();
}
}
if (created == 0) {
player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text(
"Создано шахт: " + created + (skipped > 0 ? " (пропущено: " + skipped + ")" : ""),
NamedTextColor.GREEN));
return new CreationResult(created, skipped);
}
}

View file

@ -30,19 +30,32 @@ public final class BlockWeightEditor {
Map<BlockKey, Double> weights = new java.util.HashMap<>(config.weights());
double currentWeight = weights.getOrDefault(blockKey, 0.0) * 100.0;
double delta = 0;
double newWeight = Math.max(0, Math.min(100,
currentWeight + computeDelta(leftClick, shiftClick, rightClick)));
applyWeightChange(player, blockKey, weights, newWeight, rightClick, shiftClick);
BlocksGui.refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
private static double computeDelta(boolean leftClick, boolean shiftClick, boolean rightClick) {
if (leftClick && shiftClick) {
delta = 1.0;
} else if (rightClick && shiftClick) {
delta = -1.0;
} else if (leftClick) {
delta = 5.0;
} else if (rightClick) {
delta = -5.0;
return 1.0;
}
if (rightClick && shiftClick) {
return -1.0;
}
if (leftClick) {
return 5.0;
}
if (rightClick) {
return -5.0;
}
return 0;
}
double newWeight = Math.max(0, Math.min(100, currentWeight + delta));
private static void applyWeightChange(Player player, BlockKey blockKey, Map<BlockKey, Double> weights,
double newWeight, boolean rightClick, boolean shiftClick) {
if (rightClick && shiftClick && newWeight <= 0.1) {
weights.remove(blockKey);
player.sendMessage(Component.text(
@ -53,8 +66,6 @@ public final class BlockWeightEditor {
"\u00a7a\u0412\u0435\u0441 \u00a7f" + blockKey.serialize() + " \u00a7a\u0438\u0437\u043c\u0435\u043d\u0435\u043d: \u00a7f"
+ String.format("%.1f%%", newWeight)));
}
BlocksGui.refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
public static BlockKey getBlockAtSlot(Inventory inv, int slot, LoMinesPlugin plugin, String mineName) {

View file

@ -87,14 +87,7 @@ public final class ResetGui {
}
if (rawSlot == SLOT_MESSAGE) {
if (leftClick) {
player.sendMessage(Component.text("§eВведите сообщение в чат (или 'cancel' для отмены):"));
player.closeInventory();
} else if (rightClick) {
player.sendMessage(Component.text("§cСообщение очищено."));
refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
return true;
return handleMessageSlot(plugin, player, mineName, leftClick, rightClick);
}
if (rawSlot == SLOT_COMMANDS && leftClick) {
@ -105,6 +98,18 @@ public final class ResetGui {
return true;
}
private static boolean handleMessageSlot(LoMinesPlugin plugin, Player player, String mineName,
boolean leftClick, boolean rightClick) {
if (leftClick) {
player.sendMessage(Component.text("§eВведите сообщение в чат (или 'cancel' для отмены):"));
player.closeInventory();
} else if (rightClick) {
player.sendMessage(Component.text("§cСообщение очищено."));
refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
return true;
}
private static void adjustInterval(LoMinesPlugin plugin, Player player, String mineName,
boolean leftClick, boolean rightClick, boolean shiftClick) {
int delta = 0;

View file

@ -81,11 +81,11 @@ public final class MineEditGui {
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
return handleNavigationSlot(plugin, player, mineName, rawSlot);
}
private static boolean handleNavigationSlot(LoMinesPlugin plugin, Player player, String mineName, int rawSlot) {
switch (rawSlot) {
case SLOT_BACK -> {
player.closeInventory();
return true;
}
case SLOT_BLOCKS -> {
BlocksGui.open(plugin, player, mineName);
return true;
@ -98,6 +98,22 @@ public final class MineEditGui {
RewardsGui.open(plugin, player, mineName);
return true;
}
case SLOT_DELETE -> {
ConfirmDeleteGui.open(plugin, player, mineName);
return true;
}
default -> {
return handleInfoSlot(player, mineName, rawSlot);
}
}
}
private static boolean handleInfoSlot(Player player, String mineName, int rawSlot) {
switch (rawSlot) {
case SLOT_BACK -> {
player.closeInventory();
return true;
}
case SLOT_TELEPORT -> {
player.sendMessage(Component.text("Используйте команды /lm setteleport и /lm setspawn", NamedTextColor.YELLOW));
return true;
@ -111,10 +127,6 @@ public final class MineEditGui {
player.closeInventory();
return true;
}
case SLOT_DELETE -> {
ConfirmDeleteGui.open(plugin, player, mineName);
return true;
}
case SLOT_INFO, SLOT_REGIONS -> {
return true;
}

View file

@ -45,30 +45,34 @@ public final class WorldGuardRegionService {
}
String regionName = wgConfig.generateRegionName(mineName);
try {
RegionManager regionManager = getRegionManager(config.worldName());
if (regionManager == null) {
return null;
}
if (regionManager.hasRegion(regionName)) {
plugin.getLogger().info("WorldGuard region already exists: " + regionName);
return regionName;
}
ProtectedCuboidRegion region = templateRenderer.createRegionFromCuboids(regionName, config);
if (region == null) {
plugin.getLogger().warning("Failed to create region for mine " + mineName + ": no valid regions");
return null;
}
memberHandler.applyOwnersAndMembers(region, wgConfig);
templateRenderer.applyFlags(region, wgConfig.flags());
regionManager.addRegion(region);
plugin.getLogger().info("Created WorldGuard region '" + regionName + "' for mine " + mineName);
return regionName;
return createRegionInManager(mineName, config, regionName, wgConfig);
} catch (Exception e) {
plugin.getLogger().severe("Failed to create WorldGuard region for mine " + mineName + ": " + e.getMessage());
return null;
}
}
private String createRegionInManager(String mineName, MineConfig config, String regionName, WorldGuardConfig wgConfig) {
RegionManager regionManager = getRegionManager(config.worldName());
if (regionManager == null) {
return null;
}
if (regionManager.hasRegion(regionName)) {
plugin.getLogger().info("WorldGuard region already exists: " + regionName);
return regionName;
}
ProtectedCuboidRegion region = templateRenderer.createRegionFromCuboids(regionName, config);
if (region == null) {
plugin.getLogger().warning("Failed to create region for mine " + mineName + ": no valid regions");
return null;
}
memberHandler.applyOwnersAndMembers(region, wgConfig);
templateRenderer.applyFlags(region, wgConfig.flags());
regionManager.addRegion(region);
plugin.getLogger().info("Created WorldGuard region '" + regionName + "' for mine " + mineName);
return regionName;
}
public boolean updateRegion(String mineName, MineConfig config) {
if (!worldGuardEnabled) {
return false;

View file

@ -48,38 +48,44 @@ public final class PlayerInteractListener implements Listener {
if (action == Action.LEFT_CLICK_BLOCK && event.hasBlock()) {
event.setCancelled(true);
Block block = event.getClickedBlock();
if (block == null) {
return;
}
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
int slot = session.getActiveSlot();
session.setPos1(slot, block.getLocation());
player.sendMessage(Component.text(
"Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
// Start showing particles when selection is made
plugin.getWandParticleService().startShowingParticles(player);
handleLeftClick(player, event.getClickedBlock());
return;
}
if (action == Action.RIGHT_CLICK_BLOCK && !player.isSneaking()) {
event.setCancelled(true);
Block block = event.getClickedBlock();
if (block == null) {
return;
}
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
int slot = session.getActiveSlot();
session.setPos2(slot, block.getLocation());
player.sendMessage(Component.text(
"Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
// Start showing particles when selection is made
plugin.getWandParticleService().startShowingParticles(player);
handleRightClick(player, event.getClickedBlock());
}
}
private void handleLeftClick(Player player, Block block) {
if (block == null) {
return;
}
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
int slot = session.getActiveSlot();
session.setPos1(slot, block.getLocation());
player.sendMessage(Component.text(
"Угол 1 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
// Start showing particles when selection is made
plugin.getWandParticleService().startShowingParticles(player);
}
private void handleRightClick(Player player, Block block) {
if (block == null) {
return;
}
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
int slot = session.getActiveSlot();
session.setPos2(slot, block.getLocation());
player.sendMessage(Component.text(
"Угол 2 для шахты " + (slot + 1) + " установлен.", NamedTextColor.GREEN));
// Start showing particles when selection is made
plugin.getWandParticleService().startShowingParticles(player);
}
/**
* Starts/stops particle visualization when player switches items.
*/

View file

@ -42,6 +42,10 @@ public final class LocationParser {
);
}
return buildLocation(matcher, locationString);
}
private static Location buildLocation(Matcher matcher, String locationString) throws ConfigParseException {
try {
String worldName = matcher.group(1);
double x = Double.parseDouble(matcher.group(2));

View file

@ -40,6 +40,10 @@ public final class SafeTeleportUtil {
return destination;
}
return findFallbackSafe(world, x, y, z, yaw, pitch, maxUpOffset);
}
private static Location findFallbackSafe(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) {
Location result = findNearbySafe(world, x, y, z, yaw, pitch);
if (result != null) {
return result;