Refactor: Restructure packages to follow KISS/SOLID principles - gui reorganized into mine/group/confirm/common, core into mine/service/common, command into admin/player/common, data/config/loader into block/entity/region/reward/system. All packages now follow max 6 files per folder rule. Extracted utility classes to reduce file sizes.

This commit is contained in:
loki5512344 2026-06-01 00:04:56 +02:00
parent 9692965a3e
commit 1b85bc5329
58 changed files with 1356 additions and 1364 deletions

View file

@ -1,7 +1,7 @@
package dev.loki.lomines;
import dev.loki.lomines.core.MineTicker;
import dev.loki.lomines.core.Mines;
import dev.loki.lomines.core.mine.MineTicker;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.data.stats.StatsManager;
import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.wand.GroupWandManager;

View file

@ -1,7 +1,7 @@
package dev.loki.lomines;
import dev.loki.lomines.core.MineTicker;
import dev.loki.lomines.core.Mines;
import dev.loki.lomines.core.mine.MineTicker;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.data.stats.StatsManager;
import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.wand.GroupWandManager;

View file

@ -1,10 +1,10 @@
package dev.loki.lomines;
import dev.loki.lomines.command.AdminCommands;
import dev.loki.lomines.command.LoMinesTabCompleter;
import dev.loki.lomines.command.MaskCommands;
import dev.loki.lomines.command.PlayerCommands;
import dev.loki.lomines.command.StatsCommands;
import dev.loki.lomines.command.admin.AdminCommands;
import dev.loki.lomines.command.admin.MaskCommands;
import dev.loki.lomines.command.admin.StatsCommands;
import dev.loki.lomines.command.common.LoMinesTabCompleter;
import dev.loki.lomines.command.player.PlayerCommands;
import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.listener.BlockBreakListener;
import dev.loki.lomines.listener.GroupGuiListener;

View file

@ -0,0 +1,161 @@
package dev.loki.lomines.command.admin;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.gui.mine.MineEditGui;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.util.MessageFormatter;
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;
import java.io.IOException;
import java.util.Collection;
/**
* Admin commands for mine management.
* Provides commands for creating, deleting, resetting, reloading, listing, and editing mines.
*/
@Command(value = "lm", permission = "lomines.use")
public class AdminCommands {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
public AdminCommands(LoMinesPlugin plugin) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
}
/**
* 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(MessageFormatter.success("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));
if (sender instanceof Player player) {
sender.sendMessage(Component.text(
"Or use: /lm edit " + name + " to open GUI editor", NamedTextColor.YELLOW));
}
} catch (IllegalArgumentException e) {
errorHandler.handleNotFound(sender, "Mine already exists", name);
} catch (IOException e) {
errorHandler.handleError(sender, "Failed to create mine: " + e.getMessage(),
"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(MessageFormatter.success("Successfully deleted mine: " + name));
} catch (IllegalArgumentException e) {
errorHandler.handleNotFound(sender, "Mine", name);
} catch (IOException e) {
errorHandler.handleError(sender, "Failed to delete mine: " + e.getMessage(),
"Failed to delete mine " + name, e);
}
}
/**
* Opens the GUI editor for a mine.
* Usage: /lm edit <mine>
*/
@Subcommand(value = "edit", permission = "lomines.admin.edit")
public void edit(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
}
Mine mine = mines.find(mineName).orElse(null);
if (mine == null) {
errorHandler.handleNotFound(sender, "Mine", mineName);
return;
}
MineEditGui.open(plugin, player, mineName);
}
/**
* 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(MessageFormatter.success("Resetting mine: " + mineName));
} catch (IllegalArgumentException e) {
errorHandler.handleNotFound(sender, "Mine", mineName);
}
}
/**
* Reloads all mine configurations from disk.
* Usage: /lm reload
*/
@Subcommand(value = "reload", permission = "lomines.admin.reload")
public void reload(CommandSender sender) {
try {
mines.getAll().forEach(Mine::stop);
mines.loadAll();
int count = mines.getAll().size();
sender.sendMessage(MessageFormatter.success("Successfully reloaded " + count + " mine(s)"));
} catch (IOException e) {
errorHandler.handleError(sender, "Failed to reload mines: " + e.getMessage(),
"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));
}
}
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);
}
}

View file

@ -1,8 +1,8 @@
package dev.loki.lomines.command;
package dev.loki.lomines.command.admin;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mines;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.data.config.loader.common.ConfigLoader;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.util.MessageFormatter;
import dev.lolib.commands.annotation.Arg;

View file

@ -1,8 +1,8 @@
package dev.loki.lomines.command;
package dev.loki.lomines.command.admin;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.Mines;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.data.stats.Leaderboard;
import dev.loki.lomines.data.stats.LeaderboardEntry;
import dev.loki.lomines.data.stats.PlayerStats;

View file

@ -1,16 +1,14 @@
package dev.loki.lomines.command;
package dev.loki.lomines.command.admin;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.Mines;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.gui.MineEditGui;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.util.MessageFormatter;
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;
@ -18,142 +16,19 @@ import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.util.Collection;
/**
* Admin commands for mine management.
* Provides commands for creating, deleting, resetting, reloading, listing, and editing mines.
* Commands for managing teleport and spawn locations for mines.
*/
@Command(value = "lm", permission = "lomines.use")
public class AdminCommands {
class TeleportCommands {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
public AdminCommands(LoMinesPlugin plugin) {
TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
}
/**
* 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(MessageFormatter.success("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));
if (sender instanceof Player player) {
sender.sendMessage(Component.text(
"Or use: /lm edit " + name + " to open GUI editor", NamedTextColor.YELLOW));
}
} catch (IllegalArgumentException e) {
errorHandler.handleNotFound(sender, "Mine already exists", name);
} catch (IOException e) {
errorHandler.handleError(sender, "Failed to create mine: " + e.getMessage(),
"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(MessageFormatter.success("Successfully deleted mine: " + name));
} catch (IllegalArgumentException e) {
errorHandler.handleNotFound(sender, "Mine", name);
} catch (IOException e) {
errorHandler.handleError(sender, "Failed to delete mine: " + e.getMessage(),
"Failed to delete mine " + name, e);
}
}
/**
* Opens the GUI editor for a mine.
* Usage: /lm edit <mine>
*/
@Subcommand(value = "edit", permission = "lomines.admin.edit")
public void edit(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
}
Mine mine = mines.find(mineName).orElse(null);
if (mine == null) {
errorHandler.handleNotFound(sender, "Mine", mineName);
return;
}
MineEditGui.open(plugin, player, mineName);
}
/**
* 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(MessageFormatter.success("Resetting mine: " + mineName));
} catch (IllegalArgumentException e) {
errorHandler.handleNotFound(sender, "Mine", mineName);
}
}
/**
* Reloads all mine configurations from disk.
* Usage: /lm reload
*/
@Subcommand(value = "reload", permission = "lomines.admin.reload")
public void reload(CommandSender sender) {
try {
mines.getAll().forEach(Mine::stop);
mines.loadAll();
int count = mines.getAll().size();
sender.sendMessage(MessageFormatter.success("Successfully reloaded " + count + " mine(s)"));
} catch (IOException e) {
errorHandler.handleError(sender, "Failed to reload mines: " + e.getMessage(),
"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));
}
this.mines = mines;
this.errorHandler = errorHandler;
}
/**
@ -161,7 +36,7 @@ public class AdminCommands {
* Usage: /lm setteleport <mine>
*/
@Subcommand(value = "setteleport", permission = "lomines.admin.setteleport")
public void setTeleport(CommandSender sender, @Arg("mine") String mineName) {
void setTeleport(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
@ -174,7 +49,6 @@ public class AdminCommands {
return;
}
// Get current player location and update config
var config = mine.getConfig();
var newTeleport = TeleportConfig.at(player.getLocation());
@ -203,7 +77,7 @@ public class AdminCommands {
* Usage: /lm setspawn <mine>
*/
@Subcommand(value = "setspawn", permission = "lomines.admin.setspawn")
public void setSpawn(CommandSender sender, @Arg("mine") String mineName) {
void setSpawn(CommandSender sender, @Arg("mine") String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
@ -216,7 +90,6 @@ public class AdminCommands {
return;
}
// Get current player location and update config
var config = mine.getConfig();
var newSpawn = PlayerSpawnConfig.at(player.getLocation());
@ -246,7 +119,7 @@ public class AdminCommands {
* Usage: /lm clearspawn <mine>
*/
@Subcommand(value = "clearspawn", permission = "lomines.admin.setspawn")
public void clearSpawn(CommandSender sender, @Arg("mine") String mineName) {
void clearSpawn(CommandSender sender, @Arg("mine") String mineName) {
try {
Mine mine = mines.find(mineName).orElse(null);
if (mine == null) {
@ -275,22 +148,8 @@ public class AdminCommands {
}
}
/**
* Formats location for display.
*/
private String formatLocation(Location loc) {
return String.format("%.1f, %.1f, %.1f in %s",
loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName());
}
/**
* 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);
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.command;
package dev.loki.lomines.command.common;
import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.command.Command;

View file

@ -10,4 +10,4 @@
* <li>{@code WorldGuardCommands} - WorldGuard integration commands</li>
* </ul>
*/
package dev.loki.lomines.command;
package dev.loki.lomines.command.common;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.command;
package dev.loki.lomines.command.player;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.util.ErrorHandler;

View file

@ -8,4 +8,4 @@
* <li>{@code Mine} - Representation of a single mine with its configuration and state</li>
* </ul>
*/
package dev.loki.lomines.core;
package dev.loki.lomines.core.common;

View file

@ -1,6 +1,7 @@
package dev.loki.lomines.core;
package dev.loki.lomines.core.mine;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.block.BlockSetter;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.block.BlockKey;
@ -10,6 +11,7 @@ import dev.loki.lomines.handler.MineBlockHandler;
import dev.loki.lomines.handler.MineResetHandler;
import dev.loki.lomines.util.location.BlockKeys;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.Location;
@ -26,73 +28,57 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public final class Mine {
// Immutable configuration
private final String name;
private final List<Cuboid> regions;
private final MineConfig config;
private final BlockSetter blockSetter;
private final LoMinesPlugin plugin;
private final int totalVolume;
/**
* Block keys for {@link FillMode#MASK}; empty when not mask mode.
*/
private final Set<String> maskBlockKeys;
// Mutable state (thread-safe)
private final AtomicInteger blocks;
private final AtomicInteger ticks;
// Handlers
private final MineResetHandler resetHandler;
private final MineBlockHandler blockHandler;
private final ActionBarHandler actionBarHandler;
// Tasks
private ScheduledTask actionBarTask;
/**
* Creates a new mine instance with new configuration system.
*/
public Mine(String name, MineConfig config, LoMinesPlugin plugin) {
this.name = name;
this.config = config;
this.plugin = plugin;
this.regions = config.region().regions();
this.blockSetter = createBlockSetter(config, plugin);
if (config.blocks().fillMode() == FillMode.MASK) {
this.maskBlockKeys = new HashSet<>();
if (config.blocks().mask() != null) {
for (String pos : config.blocks().mask().positions().keySet()) {
Location loc = parseLocation(pos);
if (loc != null) {
maskBlockKeys.add(BlockKeys.key(loc));
}
}
}
this.totalVolume = maskBlockKeys.size();
} else {
this.maskBlockKeys = Set.of();
this.totalVolume = config.region().totalVolume();
}
this.maskBlockKeys = initMaskBlockKeys(config);
this.totalVolume = config.blocks().fillMode() == FillMode.MASK
? maskBlockKeys.size()
: config.region().totalVolume();
this.blocks = new AtomicInteger(Math.max(0, totalVolume));
this.ticks = new AtomicInteger(0);
this.resetHandler = new MineResetHandler(this, plugin);
this.blockHandler = new MineBlockHandler(this, plugin);
this.actionBarHandler = new ActionBarHandler(this);
}
/**
* Starts periodic tasks for this mine.
*/
private Set<String> initMaskBlockKeys(MineConfig config) {
if (config.blocks().fillMode() != FillMode.MASK) {
return Set.of();
}
Set<String> keys = new HashSet<>();
if (config.blocks().mask() != null) {
for (String pos : config.blocks().mask().positions().keySet()) {
Location loc = LocationParser.parseSimple(pos);
if (loc != null) {
keys.add(BlockKeys.key(loc));
}
}
}
return keys;
}
public void start() {
startActionBarTask();
}
/**
* Stops all tasks for this mine.
*/
public void stop() {
if (actionBarTask != null) {
actionBarTask.cancel();
@ -100,116 +86,53 @@ public final class Mine {
}
}
/**
* Resets the mine by filling all regions with blocks.
*/
public void reset(boolean silent) {
resetHandler.reset(silent);
}
/**
* Handles a block break event in this mine.
*/
public void onBlockBreak(Player player, Block block) {
blockHandler.handle(player, block);
}
/**
* Checks if the given location is within any region of this mine.
*/
public boolean contains(Location location) {
return config.region().contains(location);
}
/**
* Whether breaking this block counts toward mine progress ({@link FillMode#MASK} ignores blocks outside painted cells).
*/
public boolean appliesToBlock(Location location) {
if (!contains(location)) {
return false;
}
if (config.blocks().fillMode() != FillMode.MASK) {
return true;
}
if (!contains(location)) return false;
if (config.blocks().fillMode() != FillMode.MASK) return true;
return maskBlockKeys.contains(BlockKeys.key(location));
}
/**
* Calculates the total volume of all regions in this mine.
*/
public int getTotalVolume() {
return totalVolume;
}
/**
* Calculates the percentage of blocks remaining in the mine.
*/
public double getPercentFilled() {
if (totalVolume == 0) {
return 0.0;
}
if (totalVolume == 0) return 0.0;
return (double) blocks.get() / totalVolume * 100.0;
}
/**
* Creates the appropriate BlockSetter based on block configuration.
*/
private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) {
var weights = config.blocks().weights();
if (weights.isEmpty()) {
throw new IllegalArgumentException("Mine must have at least one block type");
}
// Get the first block key to determine the setter type
BlockKey firstKey = weights.keySet().iterator().next();
return switch (firstKey) {
case BlockKey.Oraxen oraxen -> {
// TODO: Enable when OraxenBlockSetter is ready
throw new IllegalArgumentException("Oraxen integration is currently disabled");
}
case BlockKey.ItemsAdder itemsAdder -> {
// TODO: Enable when ItemsAdderBlockSetter is ready
throw new IllegalArgumentException("ItemsAdder integration is currently disabled");
}
case BlockKey.Vanilla vanilla ->
new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin);
case BlockKey.Oraxen oraxen -> throw new IllegalArgumentException("Oraxen integration is currently disabled");
case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("ItemsAdder integration is currently disabled");
case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin);
};
}
/**
* Starts the action bar update task.
*/
private void startActionBarTask() {
if (!config.ui().actionBarEnabled()) {
return;
}
if (!config.ui().actionBarEnabled()) return;
actionBarTask = Scheduler.get(plugin).runTimer(() -> {
actionBarHandler.sendToNearbyPlayers();
}, 10L, 10L);
}
private Location parseLocation(String str) {
String[] parts = str.split(";");
if (parts.length < 4) return null;
var world = org.bukkit.Bukkit.getWorld(parts[0]);
if (world == null) return null;
try {
double x = Double.parseDouble(parts[1]);
double y = Double.parseDouble(parts[2]);
double z = Double.parseDouble(parts[3]);
return new Location(world, x, y, z);
} catch (NumberFormatException e) {
return null;
}
}
// Getters
public String getName() {
return name;
}

View file

@ -0,0 +1,57 @@
package dev.loki.lomines.core.common;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.data.config.loader.common.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
/**
* Loads mine configurations from disk.
*/
final class MineLoader {
private final LoMinesPlugin plugin;
private final MineFileManager fileManager;
private final MineRepository repository;
MineLoader(LoMinesPlugin plugin, MineFileManager fileManager, MineRepository repository) {
this.plugin = plugin;
this.fileManager = fileManager;
this.repository = repository;
}
void loadAll() throws IOException {
repository.clear();
Path minesFolder = fileManager.getMinesFolder();
if (!Files.exists(minesFolder)) {
return;
}
try (Stream<Path> paths = Files.list(minesFolder)) {
paths.filter(path -> path.toString().endsWith(".yml"))
.filter(path -> !path.getFileName().toString().startsWith("_"))
.forEach(this::loadMineFromFile);
}
}
private void loadMineFromFile(Path configFile) {
try {
String fileName = configFile.getFileName().toString();
String mineName = fileName.substring(0, fileName.length() - 4);
MineConfig config = fileManager.loadConfig(mineName);
repository.createAndStart(mineName, config);
plugin.loLogger().info("Loaded mine: " + mineName);
} catch (IOException | ConfigLoader.ConfigLoadException e) {
plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage());
}
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.core;
package dev.loki.lomines.core.mine;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.ScheduledTask;

View file

@ -1,87 +1,49 @@
package dev.loki.lomines.core;
package dev.loki.lomines.core.mine;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.common.MineLoader;
import dev.loki.lomines.core.service.MaskScanService;
import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.loader.common.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardRegionService;
import org.bukkit.Location;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Registry for all active mines.
* Manages the lifecycle of mines including loading, creating, and deleting.
* Updated for new configuration system (v2).
*/
public final class Mines {
private final LoMinesPlugin plugin;
private final MineFileManager fileManager;
private final MineRepository repository;
private final MineLoader loader;
private final MaskScanService maskScanService;
private final WorldGuardRegionService worldGuardService;
public Mines(LoMinesPlugin plugin) {
this.plugin = plugin;
Path minesFolder = plugin.getDataFolder().toPath().resolve("mines");
this.fileManager = new MineFileManager(minesFolder);
this.fileManager = new MineFileManager(plugin.getDataFolder().toPath().resolve("mines"));
this.repository = new MineRepository(plugin, fileManager);
this.loader = new MineLoader(plugin, fileManager, repository);
this.maskScanService = new MaskScanService(repository, fileManager);
this.worldGuardService = new WorldGuardRegionService(plugin);
}
/**
* Loads all mines from the mines/ folder.
* Clears and replaces any previously loaded mines (used for full reload).
*/
public void loadAll() throws IOException {
repository.clear();
Path minesFolder = fileManager.getMinesFolder();
if (!Files.exists(minesFolder)) {
return;
loader.loadAll();
}
try (Stream<Path> paths = Files.list(minesFolder)) {
paths.filter(path -> path.toString().endsWith(".yml"))
.filter(path -> !path.getFileName().toString().startsWith("_")) // Skip defaults
.forEach(configFile -> loadMineFromFile(configFile));
}
}
private void loadMineFromFile(Path configFile) {
try {
String fileName = configFile.getFileName().toString();
String mineName = fileName.substring(0, fileName.length() - 4);
MineConfig config = fileManager.loadConfig(mineName);
repository.createAndStart(mineName, config);
plugin.loLogger().info("Loaded mine: " + mineName);
} catch (IOException | ConfigLoader.ConfigLoadException e) {
plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage());
}
}
/**
* Creates a new mine with the given name and default cuboid corners.
*/
public void create(String name) throws IOException {
create(name, null, null);
}
/**
* Creates a new mine; optional corners define {@code selection.1} and {@code selection.2} (same world required).
* If WorldGuard integration is enabled, creates a region for the mine.
*/
public void create(String name, Location corner1, Location corner2) throws IOException {
if (repository.exists(name)) {
throw new IllegalArgumentException("Mine already exists: " + name);
@ -100,90 +62,48 @@ public final class Mines {
throw new IOException("Failed to parse created mine configuration: " + e.getMessage(), e);
}
// Create WorldGuard region if enabled
String regionName = worldGuardService.createRegion(name, config);
if (regionName != null) {
plugin.loLogger().info("Created WorldGuard region '" + regionName + "' for mine: " + name);
}
worldGuardService.createRegion(name, config);
repository.createAndStart(name, config);
plugin.loLogger().info("Created mine: " + name);
}
/**
* Deletes a mine by name.
* Also deletes the associated WorldGuard region if it exists.
*/
public void delete(String name) throws IOException {
Mine mine = repository.remove(name);
if (mine == null) {
throw new IllegalArgumentException("Mine not found: " + name);
}
mine.stop();
// Delete WorldGuard region if exists
MineConfig config = mine.getConfig();
boolean wgDeleted = worldGuardService.deleteRegion(name, config);
if (wgDeleted) {
plugin.loLogger().info("Deleted WorldGuard region for mine: " + name);
}
worldGuardService.deleteRegion(name, mine.getConfig());
fileManager.deleteConfig(name);
}
/**
* Gets a mine by name.
*
* @throws IllegalArgumentException if mine not found
*/
public Mine get(String name) {
return repository.get(name);
}
/**
* Finds a mine by name, returning Optional.
*/
public Optional<Mine> find(String name) {
return repository.find(name);
}
/**
* Returns all mines.
*/
public Collection<Mine> getAll() {
return repository.getAll();
}
/**
* Finds a mine that contains the given location.
*/
public Optional<Mine> findByLocation(Location location) {
return repository.findByLocation(location);
}
/**
* Scans cuboid regions for {@code mask.marker} blocks, writes {@code fill-mode: mask} and positions to disk, then reloads the mine.
* Must run on the main thread (world access).
*/
public int scanAndSaveMask(String mineName) throws IOException, ConfigLoader.ConfigLoadException {
int count = maskScanService.scanAndSave(mineName);
plugin.loLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)");
return count;
}
/**
* Reloads one mine from disk (stop, replace instance, start tasks).
* Updates WorldGuard region if mine regions changed.
*/
public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException {
// Get old config before reload
Mine oldMine = repository.find(name).orElse(null);
repository.reload(name);
// Update WorldGuard region if mine exists
if (oldMine != null) {
Mine newMine = repository.find(name).orElse(null);
if (newMine != null) {
@ -192,16 +112,10 @@ public final class Mines {
}
}
/**
* Returns the WorldGuard region service for direct access.
*/
public WorldGuardRegionService getWorldGuardService() {
return worldGuardService;
}
/**
* Returns the file manager for saving mine configurations.
*/
public MineFileManager getFileManager() {
return fileManager;
}

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.core.service;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.loader.common.ConfigLoader;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.util.ValidationUtils;
import dev.loki.lomines.util.location.Cuboid;

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.core.service;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.loader.common.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey;

View file

@ -1,8 +1,8 @@
package dev.loki.lomines.core.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.loader.common.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import org.bukkit.Location;

View file

@ -1,6 +1,13 @@
package dev.loki.lomines.data.config;
import dev.loki.lomines.data.config.loader.*;
import dev.loki.lomines.data.config.loader.block.BlockConfigLoader;
import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader;
import dev.loki.lomines.data.config.loader.entity.TeleportConfigLoader;
import dev.loki.lomines.data.config.loader.region.RegionConfigLoader;
import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.reset.ResetConfigLoader;
import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader;
import dev.loki.lomines.data.config.loader.system.UIConfigLoader;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.block;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.entity;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.util.location.LocationParser;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.entity;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.util.location.LocationParser;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.region;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.util.location.LocationParser;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.region;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.reward;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.reward.RewardConfig;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.system;
import dev.loki.lomines.data.config.reset.ResetConfig;
import org.bukkit.configuration.file.YamlConfiguration;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config.loader;
package dev.loki.lomines.data.config.loader.system;
import dev.loki.lomines.data.config.ui.UIConfig;
import org.bukkit.configuration.file.YamlConfiguration;

View file

@ -0,0 +1,36 @@
package dev.loki.lomines.data.reward;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Parses reward commands from configuration.
*/
final class RewardCommandParser {
@SuppressWarnings("unchecked")
List<String> parseCommands(Map<String, Object> rewardMap) throws ConfigParseException {
List<String> commands = new ArrayList<>();
if (!rewardMap.containsKey("commands")) {
return commands;
}
Object commandsObj = rewardMap.get("commands");
if (!(commandsObj instanceof List)) {
throw new ConfigParseException(
"Invalid 'commands' type: expected list, got " +
(commandsObj != null ? commandsObj.getClass().getSimpleName() : "null")
);
}
List<String> commandsList = (List<String>) commandsObj;
for (String command : commandsList) {
if (command == null || command.trim().isEmpty()) {
throw new ConfigParseException("Command cannot be null or empty");
}
commands.add(command);
}
return commands;
}
}

View file

@ -0,0 +1,46 @@
package dev.loki.lomines.data.reward;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
import org.bukkit.Material;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Parses reward materials (blocks) from configuration.
*/
final class RewardMaterialParser {
@SuppressWarnings("unchecked")
List<Material> parseMaterials(Map<String, Object> rewardMap) throws ConfigParseException {
if (!rewardMap.containsKey("blocks")) {
throw new ConfigParseException("Missing required 'blocks' field in reward");
}
Object blocksObj = rewardMap.get("blocks");
if (!(blocksObj instanceof List)) {
throw new ConfigParseException(
"Invalid 'blocks' type: expected list, got " +
(blocksObj != null ? blocksObj.getClass().getSimpleName() : "null")
);
}
List<String> blocksList = (List<String>) blocksObj;
List<Material> materials = new ArrayList<>();
for (String blockName : blocksList) {
if (blockName == null || blockName.trim().isEmpty()) {
throw new ConfigParseException("Block name cannot be null or empty");
}
try {
Material material = ValidationUtils.validateMaterial(blockName);
materials.add(material);
} catch (IllegalArgumentException e) {
throw new ConfigParseException(
"Unknown material in reward blocks: '" + blockName + "'",
e
);
}
}
return materials;
}
}

View file

@ -1,9 +1,6 @@
package dev.loki.lomines.data.reward;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
@ -18,18 +15,15 @@ import java.util.Map;
public final class RewardParser {
private final RewardItemParser itemParser;
private final RewardMaterialParser materialParser;
private final RewardCommandParser commandParser;
public RewardParser() {
this.itemParser = new RewardItemParser();
this.materialParser = new RewardMaterialParser();
this.commandParser = new RewardCommandParser();
}
/**
* Parses all rewards from the YAML configuration.
*
* @param yaml the YAML configuration
* @return list of parsed Reward objects
* @throws ConfigParseException if reward configuration is invalid
*/
public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
List<Reward> rewards = new ArrayList<>();
@ -69,20 +63,13 @@ public final class RewardParser {
return rewards;
}
/**
* Parses a single reward from a map configuration.
*
* @param rewardMap the reward configuration map
* @return the parsed Reward object
* @throws ConfigParseException if the reward configuration is invalid
*/
private Reward parseReward(Map<String, Object> rewardMap) throws ConfigParseException {
double chance = parseRewardField(rewardMap, "chance", Double.class);
boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false);
List<Material> materials = parseRewardMaterials(rewardMap);
var materials = materialParser.parseMaterials(rewardMap);
List<ItemStack> items = parseRewardItems(rewardMap);
List<String> commands = parseRewardCommands(rewardMap);
List<String> commands = commandParser.parseCommands(rewardMap);
return new Reward(materials, chance, items, commands, preventDrops);
}
@ -137,38 +124,6 @@ public final class RewardParser {
}
}
private List<Material> parseRewardMaterials(Map<String, Object> rewardMap)
throws ConfigParseException {
if (!rewardMap.containsKey("blocks")) {
throw new ConfigParseException("Missing required 'blocks' field in reward");
}
Object blocksObj = rewardMap.get("blocks");
if (!(blocksObj instanceof List)) {
throw new ConfigParseException(
"Invalid 'blocks' type: expected list, got " +
(blocksObj != null ? blocksObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
List<String> blocksList = (List<String>) blocksObj;
List<Material> materials = new ArrayList<>();
for (String blockName : blocksList) {
if (blockName == null || blockName.trim().isEmpty()) {
throw new ConfigParseException("Block name cannot be null or empty");
}
try {
Material material = ValidationUtils.validateMaterial(blockName);
materials.add(material);
} catch (IllegalArgumentException e) {
throw new ConfigParseException(
"Unknown material in reward blocks: '" + blockName + "'",
e
);
}
}
return materials;
}
private List<ItemStack> parseRewardItems(Map<String, Object> rewardMap)
throws ConfigParseException {
List<ItemStack> items = new ArrayList<>();
@ -197,28 +152,4 @@ public final class RewardParser {
}
return items;
}
private List<String> parseRewardCommands(Map<String, Object> rewardMap)
throws ConfigParseException {
List<String> commands = new ArrayList<>();
if (!rewardMap.containsKey("commands")) {
return commands;
}
Object commandsObj = rewardMap.get("commands");
if (!(commandsObj instanceof List)) {
throw new ConfigParseException(
"Invalid 'commands' type: expected list, got " +
(commandsObj != null ? commandsObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
List<String> commandsList = (List<String>) commandsObj;
for (String command : commandsList) {
if (command == null || command.trim().isEmpty()) {
throw new ConfigParseException("Command cannot be null or empty");
}
commands.add(command);
}
return commands;
}
}

View file

@ -1,216 +0,0 @@
package dev.loki.lomines.gui;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mines;
import dev.loki.lomines.wand.GroupWandManager;
import dev.loki.lomines.wand.GroupWandSession;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
/**
* 6-row chest: left column = create; center 3×3 = nine sub-mines with shared settings.
*/
public final class GroupCreateGui {
/**
* Create group (same YAML defaults for every created mine).
*/
public static final int SLOT_CREATE = 0;
/**
* Shows /lm group prefix.
*/
public static final int SLOT_INFO = 8;
private static final int SIZE = 54;
/**
* 3×3 mine slots mapping index 0..8
*/
private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30};
private GroupCreateGui() {
}
public static void open(LoMinesPlugin plugin, Player player) {
GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId());
Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text("LoMines · группа (9 шахт)", NamedTextColor.DARK_GREEN));
holder.setInventory(inv);
fill(plugin, player, inv);
player.openInventory(inv);
}
public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv) {
fill(plugin, player, inv);
}
private static void fill(LoMinesPlugin plugin, Player player, Inventory inv) {
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
for (int i = 0; i < SIZE; i++) {
inv.setItem(i, filler());
}
inv.setItem(SLOT_CREATE, createButton());
inv.setItem(SLOT_INFO, infoPaper(session));
for (int i = 0; i < MINE_SLOTS.length; i++) {
inv.setItem(MINE_SLOTS[i], mineSlotItem(session, i));
}
}
private static ItemStack filler() {
ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = pane.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(" "));
pane.setItemMeta(meta);
}
return pane;
}
private static ItemStack createButton() {
ItemStack stack = new ItemStack(Material.LIME_CONCRETE);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.displayName(Component.text("Создать группу", NamedTextColor.GREEN));
meta.lore(List.of(
Component.text("Создаёт шахты с префиксом_1 … _9", NamedTextColor.GRAY),
Component.text("для слотов, где заданы оба угла", NamedTextColor.GRAY)
));
stack.setItemMeta(meta);
}
return stack;
}
private static ItemStack infoPaper(GroupWandSession session) {
ItemStack stack = new ItemStack(Material.PAPER);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
if (session.hasBaseName()) {
meta.displayName(Component.text("Префикс: " + session.getBaseName(), NamedTextColor.AQUA));
meta.lore(List.of(
Component.text("Файлы: " + session.getBaseName() + "_1.yml … _9.yml", NamedTextColor.GRAY),
Component.text("Настройки одинаковые; правьте один и копируйте при нужде", NamedTextColor.DARK_GRAY)
));
} else {
meta.displayName(Component.text("Задайте префикс", NamedTextColor.RED));
meta.lore(List.of(Component.text("/lm group <префикс>", NamedTextColor.GRAY)));
}
stack.setItemMeta(meta);
}
return stack;
}
private static ItemStack mineSlotItem(GroupWandSession session, int index) {
boolean active = session.getActiveSlot() == index;
boolean ready = session.isSlotReady(index);
Material mat;
NamedTextColor nameColor;
if (ready) {
mat = Material.LIME_STAINED_GLASS_PANE;
nameColor = NamedTextColor.GREEN;
} else if (session.getPos1(index) != null) {
mat = Material.YELLOW_STAINED_GLASS_PANE;
nameColor = NamedTextColor.YELLOW;
} else {
mat = Material.RED_STAINED_GLASS_PANE;
nameColor = NamedTextColor.RED;
}
if (active) {
mat = Material.LIGHT_BLUE_STAINED_GLASS_PANE;
}
ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
String label = "Шахта " + (index + 1);
meta.displayName(Component.text(label, nameColor));
List<Component> lore = new ArrayList<>();
lore.add(Component.text(active ? "Активный слот палочки" : "ПКМ — выбрать активным", NamedTextColor.GRAY));
lore.add(Component.text(
ready ? "Готово (2 точки)" : (session.getPos1(index) != null ? "Нужна 2-я точка" : "Нужны 2 точки"),
NamedTextColor.DARK_GRAY));
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
}
public static int mineSlotIndex(int rawSlot) {
for (int i = 0; i < MINE_SLOTS.length; i++) {
if (MINE_SLOTS[i] == rawSlot) {
return i;
}
}
return -1;
}
/**
* @return true if handled (caller should cancel event)
*/
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot) {
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
GroupWandManager mgr = plugin.getGroupWandManager();
GroupWandSession session = mgr.getSession(player.getUniqueId());
if (rawSlot == SLOT_CREATE) {
runCreate(plugin, player, session);
player.closeInventory();
return true;
}
int mineIdx = mineSlotIndex(rawSlot);
if (mineIdx >= 0) {
session.setActiveSlot(mineIdx);
Inventory top = player.getOpenInventory().getTopInventory();
if (top.getHolder() instanceof GroupCreateGuiHolder) {
refresh(plugin, player, top);
}
player.sendMessage(Component.text("Активный слот палочки: " + (mineIdx + 1), NamedTextColor.YELLOW));
return true;
}
return true;
}
private static void runCreate(LoMinesPlugin plugin, Player player, GroupWandSession session) {
if (!session.hasBaseName()) {
player.sendMessage(Component.text("Сначала: /lm group <префикс>", NamedTextColor.RED));
return;
}
String base = session.getBaseName();
if (!base.matches("^[a-zA-Z0-9_-]+$")) {
player.sendMessage(Component.text("Префикс: только латиница, цифры, _ -", NamedTextColor.RED));
return;
}
Mines mines = plugin.getMines();
int created = 0;
int skipped = 0;
for (int i = 0; i < 9; i++) {
if (!session.isSlotReady(i)) {
continue;
}
String mineName = base + "_" + (i + 1);
try {
mines.create(mineName, session.getPos1(i), session.getPos2(i));
created++;
} catch (IllegalArgumentException e) {
player.sendMessage(Component.text("Пропуск (уже есть): " + mineName, NamedTextColor.GOLD));
skipped++;
} catch (Exception e) {
player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED));
plugin.loLogger().error("Group create failed for " + mineName, e);
}
}
if (created == 0) {
player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом). Пустые слоты пропущены.", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text(
"Создано шахт: " + created + (skipped > 0 ? " (пропущено существующих: " + skipped + ")" : ""),
NamedTextColor.GREEN));
}
}

View file

@ -1,326 +0,0 @@
package dev.loki.lomines.gui;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.RewardConfig;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
/**
* Main GUI for editing a mine.
* Provides navigation to sub-editors for different configuration sections.
* Improved UX with better materials, enhanced lore, and clearer information display.
*/
public final class MineEditGui {
private static final int SIZE = 54;
private static final String TITLE_PREFIX = "Редактор шахты: ";
// Slot constants
public static final int SLOT_BACK = 49;
public static final int SLOT_BLOCKS = 10;
public static final int SLOT_REGIONS = 12;
public static final int SLOT_RESET = 14;
public static final int SLOT_REWARDS = 16;
public static final int SLOT_TELEPORT = 28;
public static final int SLOT_UI = 30;
public static final int SLOT_SAVE = 32;
public static final int SLOT_DELETE = 34;
public static final int SLOT_INFO = 40;
// UI Constants
private static final String SEP = "§8───────────────";
private static final String EDIT = "§e▸ Нажмите для редактирования";
private static final String CLOSE = "§c▸ Нажмите чтобы закрыть";
private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String DEL = "§c▸ Нажмите для удаления";
private MineEditGui() {}
public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName);
Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN));
holder.setInventory(inv);
fill(plugin, mine, inv);
player.openInventory(inv);
}
public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
player.closeInventory();
return;
}
fill(plugin, mine, inv);
}
private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) {
MineConfig config = mine.getConfig();
for (int i = 0; i < SIZE; i++) inv.setItem(i, filler());
inv.setItem(SLOT_BLOCKS, blocksItem(config));
inv.setItem(SLOT_REGIONS, regionsItem(config));
inv.setItem(SLOT_RESET, resetItem(config));
inv.setItem(SLOT_REWARDS, rewardsItem(config));
inv.setItem(SLOT_TELEPORT, teleportItem(config));
inv.setItem(SLOT_UI, uiItem(config));
inv.setItem(SLOT_SAVE, saveItem());
inv.setItem(SLOT_DELETE, deleteItem(mine.getName()));
inv.setItem(SLOT_INFO, infoItem(mine));
inv.setItem(SLOT_BACK, backItem());
}
private static ItemStack filler() {
ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = pane.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(" "));
pane.setItemMeta(meta);
}
return pane;
}
private static String loc(Location l) {
if (l == null || l.getWorld() == null) return "§7не задана";
return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ());
}
private static String trunc(String s, int max) {
if (s == null || s.isEmpty()) return "§7нет";
return s.length() > max ? s.substring(0, max - 3) + "..." : s;
}
private static ItemStack create(Material mat, String name, String... loreLines) {
ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(name, NamedTextColor.WHITE));
List<Component> lore = new ArrayList<>();
for (String line : loreLines) {
lore.add(Component.text(line, NamedTextColor.GRAY));
}
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
}
private static ItemStack blocksItem(MineConfig config) {
var w = config.blocks().weights();
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fТипов: §7" + config.blocks().blockCount());
lore.add(" §fРежим: §7" + config.blocks().fillMode().name());
lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет"));
if (!w.isEmpty()) {
lore.add("");
lore.add("§7Топ блоков:");
w.entrySet().stream().sorted((a, b) -> Double.compare(b.getValue(), a.getValue())).limit(3)
.forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100)));
}
lore.add("");
lore.add(EDIT);
return create(Material.DIAMOND_PICKAXE, "§a§lБлоки", lore.toArray(new String[0]));
}
private static ItemStack regionsItem(MineConfig config) {
return create(Material.COMPASS, "§a§lРегионы",
SEP,
"§7Настройки:",
" §fКоличество: §7" + config.region().regionCount(),
" §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков",
"",
" §fМир: §7" + config.worldName(),
"",
"§8Изменяется через палочку"
);
}
private static ItemStack resetItem(MineConfig config) {
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fИнтервал: §7" + config.reset().intervalDisplay());
lore.add(" §fТриггер по %: " + (config.reset().isPercentTriggerEnabled() ? "§a" + config.reset().percentTrigger() + "%" : "§7выкл"));
lore.add(" §fКоманд: §7" + config.reset().commands().size());
lore.add("");
lore.add(" §fСообщение: " + (config.reset().broadcastMessage().isEmpty() ? "§7нет" : "§aесть"));
if (!config.reset().broadcastMessage().isEmpty()) {
lore.add(" §8" + trunc(config.reset().broadcastMessage(), 25));
}
lore.add("");
lore.add(EDIT);
return create(Material.CLOCK, "§a§lСброс", lore.toArray(new String[0]));
}
private static ItemStack rewardsItem(MineConfig config) {
var entries = config.rewards().entries();
long items = entries.stream().flatMap(e -> e.items().stream()).count();
long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count();
long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count();
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fЗаписей: §7" + entries.size());
if (!entries.isEmpty()) {
lore.add("");
lore.add("§7Типы наград:");
lore.add(" §8• §fПредметов: §7" + items);
lore.add(" §8• §fКоманд: §7" + cmds);
if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop);
}
lore.add("");
lore.add(EDIT);
return create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0]));
}
private static ItemStack teleportItem(MineConfig config) {
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл"));
if (config.teleport().enabled()) {
config.teleport().getLocation().ifPresent(loc -> lore.add(" " + loc(loc)));
}
lore.add("");
lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл"));
if (config.playerSpawn().enabled()) {
config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + loc(loc)));
}
lore.add("");
lore.add("§8Команды:");
lore.add(" §8• §7/lm setteleport §f<mine>");
lore.add(" §8• §7/lm setspawn §f<mine>");
lore.add(" §8• §7/lm clearspawn §f<mine>");
lore.add("");
lore.add(EDIT);
return create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0]));
}
private static ItemStack uiItem(MineConfig config) {
return create(Material.PAINTING, "§a§lИнтерфейс",
SEP,
"§7Настройки:",
" §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"),
" §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков",
"",
"§7Формат ActionBar:",
" §8" + trunc(config.ui().actionBarFormat(), 28),
"",
"§7Формат таймера: §8" + config.ui().timerFormat(),
"",
EDIT
);
}
private static ItemStack saveItem() {
return create(Material.LIME_DYE, "§a§lСохранить изменения",
SEP,
"§7Сохраняет конфигурацию шахты",
"§7в файл на диске",
"",
SAVE
);
}
private static ItemStack deleteItem(String mineName) {
return create(Material.TNT, "§c§lУдалить шахту",
SEP,
"§c§lВнимание!",
"§cЭто действие нельзя отменить!",
"",
"§7Шахта: §f" + mineName,
"",
DEL
);
}
private static ItemStack infoItem(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();
double percent = mine.getPercentFilled();
String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c";
int filled = (int) Math.round(percent / 100.0 * 10);
String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled);
return create(Material.BOOK, "§b§lИнформация",
SEP,
"§7Базовые данные:",
" §fНазвание: §7" + mine.getName(),
" §fМир: §7" + mine.getConfig().worldName(),
"",
"§7Состояние заполнения:",
" §fБлоков: " + c + blocks + "§8/§7" + total,
" §fПроцент: " + c + String.format("%.1f%%", percent),
" " + bar
);
}
private static ItemStack backItem() {
return create(Material.ARROW, "§c§lЗакрыть",
SEP,
"§7Закрыть редактор",
"",
CLOSE
);
}
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
switch (rawSlot) {
case SLOT_BACK -> {
player.closeInventory();
return true;
}
case SLOT_BLOCKS -> {
player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_RESET -> {
player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_REWARDS -> {
player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_TELEPORT -> {
player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_UI -> {
player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_SAVE -> {
player.sendMessage(Component.text("Конфигурация сохранена для шахты: " + mineName, NamedTextColor.GREEN));
player.closeInventory();
return true;
}
case SLOT_DELETE -> {
ConfirmDeleteGui.open(plugin, player, mineName);
return true;
}
case SLOT_INFO, SLOT_REGIONS -> {
return true;
}
}
return true;
}
}

View file

@ -0,0 +1,54 @@
package dev.loki.lomines.gui.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
/**
* Factory for creating ItemStacks with name and lore.
*/
final class ItemStackFactory {
private ItemStackFactory() {}
static ItemStack create(Material mat, String name, String... loreLines) {
ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(name, NamedTextColor.WHITE));
List<Component> lore = new ArrayList<>();
for (String line : loreLines) {
lore.add(Component.text(line, NamedTextColor.GRAY));
}
meta.lore(lore);
stack.setItemMeta(meta);
}
return stack;
}
static String loc(Location l) {
if (l == null || l.getWorld() == null) return "§7не задана";
return String.format("§7%.0f§8/§7%.0f§8/§7%.0f", l.getX(), l.getY(), l.getZ());
}
static String trunc(String s, int max) {
if (s == null || s.isEmpty()) return "§7нет";
return s.length() > max ? s.substring(0, max - 3) + "..." : s;
}
static ItemStack filler() {
ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = pane.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(" "));
pane.setItemMeta(meta);
}
return pane;
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.gui;
package dev.loki.lomines.gui.confirm;
import dev.loki.lomines.LoMinesPlugin;
import net.kyori.adventure.text.Component;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.gui;
package dev.loki.lomines.gui.confirm;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;

View file

@ -0,0 +1,123 @@
package dev.loki.lomines.gui.group;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.wand.GroupWandManager;
import dev.loki.lomines.wand.GroupWandSession;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
/**
* 6-row chest: left column = create; center 3×3 = nine sub-mines with shared settings.
*/
public final class GroupCreateGui {
public static final int SLOT_CREATE = 0;
public static final int SLOT_INFO = 8;
private static final int SIZE = 54;
private static final int[] MINE_SLOTS = {10, 11, 12, 19, 20, 21, 28, 29, 30};
private GroupCreateGui() {}
public static void open(LoMinesPlugin plugin, Player player) {
GroupCreateGuiHolder holder = new GroupCreateGuiHolder(player.getUniqueId());
Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text("LoMines · группа (9 шахт)", NamedTextColor.DARK_GREEN));
holder.setInventory(inv);
fill(plugin, player, inv);
player.openInventory(inv);
}
public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv) {
fill(plugin, player, inv);
}
private static void fill(LoMinesPlugin plugin, Player player, Inventory inv) {
GroupWandSession session = plugin.getGroupWandManager().getSession(player.getUniqueId());
for (int i = 0; i < SIZE; i++) {
inv.setItem(i, GroupCreateItems.filler());
}
inv.setItem(SLOT_CREATE, GroupCreateItems.createButton());
inv.setItem(SLOT_INFO, GroupCreateItems.infoPaper(session));
for (int i = 0; i < MINE_SLOTS.length; i++) {
inv.setItem(MINE_SLOTS[i], GroupCreateItems.mineSlotItem(session, i));
}
}
public static int mineSlotIndex(int rawSlot) {
for (int i = 0; i < MINE_SLOTS.length; i++) {
if (MINE_SLOTS[i] == rawSlot) {
return i;
}
}
return -1;
}
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot) {
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
GroupWandManager mgr = plugin.getGroupWandManager();
GroupWandSession session = mgr.getSession(player.getUniqueId());
if (rawSlot == SLOT_CREATE) {
runCreate(plugin, player, session);
player.closeInventory();
return true;
}
int mineIdx = mineSlotIndex(rawSlot);
if (mineIdx >= 0) {
session.setActiveSlot(mineIdx);
Inventory top = player.getOpenInventory().getTopInventory();
if (top.getHolder() instanceof GroupCreateGuiHolder) {
refresh(plugin, player, top);
}
player.sendMessage(Component.text("Активный слот палочки: " + (mineIdx + 1), NamedTextColor.YELLOW));
return true;
}
return true;
}
private static void runCreate(LoMinesPlugin plugin, Player player, GroupWandSession session) {
if (!session.hasBaseName()) {
player.sendMessage(Component.text("Сначала: /lm group <префикс>", NamedTextColor.RED));
return;
}
String base = session.getBaseName();
if (!base.matches("^[a-zA-Z0-9_-]+$")) {
player.sendMessage(Component.text("Префикс: только латиница, цифры, _ -", NamedTextColor.RED));
return;
}
Mines mines = plugin.getMines();
int created = 0;
int skipped = 0;
for (int i = 0; i < 9; i++) {
if (!session.isSlotReady(i)) continue;
String mineName = base + "_" + (i + 1);
try {
mines.create(mineName, session.getPos1(i), session.getPos2(i));
created++;
} catch (IllegalArgumentException e) {
player.sendMessage(Component.text("Пропуск (уже есть): " + mineName, NamedTextColor.GOLD));
skipped++;
} catch (Exception e) {
player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED));
plugin.loLogger().error("Group create failed for " + mineName, e);
}
}
if (created == 0) {
player.sendMessage(Component.text("Нет готовых слотов (2 точки в каждом).", NamedTextColor.RED));
return;
}
player.sendMessage(Component.text(
"Создано шахт: " + created + (skipped > 0 ? " (пропущено: " + skipped + ")" : ""),
NamedTextColor.GREEN));
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.gui;
package dev.loki.lomines.gui.group;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;

View file

@ -0,0 +1,103 @@
package dev.loki.lomines.gui.group;
import dev.loki.lomines.wand.GroupWandSession;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
/**
* Item factory for GroupCreateGui.
*/
final class GroupCreateItems {
private GroupCreateItems() {}
static ItemStack filler() {
ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = pane.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(" "));
pane.setItemMeta(meta);
}
return pane;
}
static ItemStack createButton() {
ItemStack stack = new ItemStack(Material.LIME_CONCRETE);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
meta.displayName(Component.text("Создать группу", NamedTextColor.GREEN));
meta.lore(List.of(
Component.text("Создаёт шахты с префиксом_1 … _9", NamedTextColor.GRAY),
Component.text("для слотов, где заданы оба угла", NamedTextColor.GRAY)
));
stack.setItemMeta(meta);
}
return stack;
}
static ItemStack infoPaper(GroupWandSession session) {
ItemStack stack = new ItemStack(Material.PAPER);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
if (session.hasBaseName()) {
meta.displayName(Component.text("Префикс: " + session.getBaseName(), NamedTextColor.AQUA));
meta.lore(List.of(
Component.text("Файлы: " + session.getBaseName() + "_1.yml … _9.yml", NamedTextColor.GRAY),
Component.text("Настройки одинаковые; правьте один и копируйте при нужде", NamedTextColor.DARK_GRAY)
));
} else {
meta.displayName(Component.text("Задайте префикс", NamedTextColor.RED));
meta.lore(List.of(Component.text("/lm group <префикс>", NamedTextColor.GRAY)));
}
stack.setItemMeta(meta);
}
return stack;
}
static ItemStack mineSlotItem(GroupWandSession session, int index) {
boolean active = session.getActiveSlot() == index;
boolean ready = session.isSlotReady(index);
Material mat = getMaterialForSlot(session, index, ready, active);
NamedTextColor nameColor = ready ? NamedTextColor.GREEN :
(session.getPos1(index) != null ? NamedTextColor.YELLOW : NamedTextColor.RED);
ItemStack stack = new ItemStack(mat);
ItemMeta meta = stack.getItemMeta();
if (meta != null) {
String label = "Шахта " + (index + 1);
meta.displayName(Component.text(label, nameColor));
meta.lore(buildLore(session, index, active, ready));
stack.setItemMeta(meta);
}
return stack;
}
private static Material getMaterialForSlot(GroupWandSession session, int index, boolean ready, boolean active) {
if (active) {
return Material.LIGHT_BLUE_STAINED_GLASS_PANE;
}
if (ready) {
return Material.LIME_STAINED_GLASS_PANE;
}
if (session.getPos1(index) != null) {
return Material.YELLOW_STAINED_GLASS_PANE;
}
return Material.RED_STAINED_GLASS_PANE;
}
private static List<Component> buildLore(GroupWandSession session, int index, boolean active, boolean ready) {
List<Component> lore = new ArrayList<>();
lore.add(Component.text(active ? "Активный слот палочки" : "ПКМ — выбрать активным", NamedTextColor.GRAY));
lore.add(Component.text(
ready ? "Готово (2 точки)" : (session.getPos1(index) != null ? "Нужна 2-я точка" : "Нужны 2 точки"),
NamedTextColor.DARK_GRAY));
return lore;
}
}

View file

@ -0,0 +1,118 @@
package dev.loki.lomines.gui.mine;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.confirm.ConfirmDeleteGui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
/**
* Main GUI for editing a mine.
* Provides navigation to sub-editors for different configuration sections.
* Improved UX with better materials, enhanced lore, and clearer information display.
*/
public final class MineEditGui {
private static final int SIZE = 54;
private static final String TITLE_PREFIX = "Редактор шахты: ";
// Slot constants
public static final int SLOT_BACK = 49;
public static final int SLOT_BLOCKS = 10;
public static final int SLOT_REGIONS = 12;
public static final int SLOT_RESET = 14;
public static final int SLOT_REWARDS = 16;
public static final int SLOT_TELEPORT = 28;
public static final int SLOT_UI = 30;
public static final int SLOT_SAVE = 32;
public static final int SLOT_DELETE = 34;
public static final int SLOT_INFO = 40;
private MineEditGui() {}
public static void open(LoMinesPlugin plugin, Player player, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
player.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
MineEditGuiHolder holder = new MineEditGuiHolder(player.getUniqueId(), mineName);
Inventory inv = Bukkit.createInventory(holder, SIZE,
Component.text(TITLE_PREFIX + mineName, NamedTextColor.DARK_GREEN));
holder.setInventory(inv);
fill(plugin, mine, inv);
player.openInventory(inv);
}
public static void refresh(LoMinesPlugin plugin, Player player, Inventory inv, String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
player.closeInventory();
return;
}
fill(plugin, mine, inv);
}
private static void fill(LoMinesPlugin plugin, Mine mine, Inventory inv) {
MineConfig config = mine.getConfig();
for (int i = 0; i < SIZE; i++) inv.setItem(i, MineEditItems.filler());
inv.setItem(SLOT_BLOCKS, MineEditItems.blocksItem(config));
inv.setItem(SLOT_REGIONS, MineEditItems.regionsItem(config));
inv.setItem(SLOT_RESET, MineEditItems.resetItem(config));
inv.setItem(SLOT_REWARDS, MineEditItems.rewardsItem(config));
inv.setItem(SLOT_TELEPORT, MineEditItems.teleportItem(config));
inv.setItem(SLOT_UI, MineEditItems.uiItem(config));
inv.setItem(SLOT_SAVE, MineEditItems.saveItem());
inv.setItem(SLOT_DELETE, MineEditItems.deleteItem(mine.getName()));
inv.setItem(SLOT_INFO, MineEditItems.infoItem(mine));
inv.setItem(SLOT_BACK, MineEditItems.backItem());
}
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
switch (rawSlot) {
case SLOT_BACK -> {
player.closeInventory();
return true;
}
case SLOT_BLOCKS -> {
player.sendMessage(Component.text("Открытие редактора блоков... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_RESET -> {
player.sendMessage(Component.text("Открытие редактора сброса... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_REWARDS -> {
player.sendMessage(Component.text("Открытие редактора наград... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_TELEPORT -> {
player.sendMessage(Component.text("Открытие редактора телепорта... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_UI -> {
player.sendMessage(Component.text("Открытие редактора интерфейса... (в разработке)", NamedTextColor.YELLOW));
return true;
}
case SLOT_SAVE -> {
player.sendMessage(Component.text("Конфигурация сохранена для шахты: " + mineName, NamedTextColor.GREEN));
player.closeInventory();
return true;
}
case SLOT_DELETE -> {
ConfirmDeleteGui.open(plugin, player, mineName);
return true;
}
case SLOT_INFO, SLOT_REGIONS -> {
return true;
}
}
return true;
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.gui;
package dev.loki.lomines.gui.mine;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;

View file

@ -0,0 +1,189 @@
package dev.loki.lomines.gui.mine;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
/**
* Item factory for MineEditGui. Creates all inventory items.
*/
final class MineEditItems {
private static final String SEP = "§8───────────────";
private static final String EDIT = "§e▸ Нажмите для редактирования";
private static final String CLOSE = "§c▸ Нажмите чтобы закрыть";
private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String DEL = "§c▸ Нажмите для удаления";
private MineEditItems() {}
static ItemStack filler() {
return ItemStackFactory.filler();
}
static ItemStack blocksItem(MineConfig config) {
var w = config.blocks().weights();
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fТипов: §7" + config.blocks().blockCount());
lore.add(" §fРежим: §7" + config.blocks().fillMode().name());
lore.add(" §fКастомные: " + (config.blocks().hasCustomBlocks() ? "§aДа" : "§7Нет"));
if (!w.isEmpty()) {
lore.add("");
lore.add("§7Топ блоков:");
w.entrySet().stream().sorted((a, b) -> Double.compare(b.getValue(), a.getValue())).limit(3)
.forEach(e -> lore.add(String.format(" §8• §7%s §8(§7%.0f%%§8)", e.getKey().serialize(), e.getValue() * 100)));
}
lore.add("");
lore.add(EDIT);
return ItemStackFactory.create(Material.DIAMOND_PICKAXE, "§a§lБлоки", lore.toArray(new String[0]));
}
static ItemStack regionsItem(MineConfig config) {
return ItemStackFactory.create(Material.COMPASS, "§a§lРегионы",
SEP,
"§7Настройки:",
" §fКоличество: §7" + config.region().regionCount(),
" §fОбщий объём: §7" + config.region().totalVolume() + " §8блоков",
"",
" §fМир: §7" + config.worldName(),
"",
"§8Изменяется через палочку"
);
}
static ItemStack resetItem(MineConfig config) {
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fИнтервал: §7" + config.reset().intervalDisplay());
lore.add(" §fТриггер по %: " + (config.reset().isPercentTriggerEnabled() ? "§a" + config.reset().percentTrigger() + "%" : "§7выкл"));
lore.add(" §fКоманд: §7" + config.reset().commands().size());
lore.add("");
lore.add(" §fСообщение: " + (config.reset().broadcastMessage().isEmpty() ? "§7нет" : "§aесть"));
if (!config.reset().broadcastMessage().isEmpty()) {
lore.add(" §8" + ItemStackFactory.trunc(config.reset().broadcastMessage(), 25));
}
lore.add("");
lore.add(EDIT);
return ItemStackFactory.create(Material.CLOCK, "§a§lСброс", lore.toArray(new String[0]));
}
static ItemStack rewardsItem(MineConfig config) {
var entries = config.rewards().entries();
long items = entries.stream().flatMap(e -> e.items().stream()).count();
long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count();
long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count();
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fЗаписей: §7" + entries.size());
if (!entries.isEmpty()) {
lore.add("");
lore.add("§7Типы наград:");
lore.add(" §8• §fПредметов: §7" + items);
lore.add(" §8• §fКоманд: §7" + cmds);
if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop);
}
lore.add("");
lore.add(EDIT);
return ItemStackFactory.create(Material.GOLD_INGOT, "§a§lНаграды", lore.toArray(new String[0]));
}
static ItemStack teleportItem(MineConfig config) {
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
lore.add(" §fТелепорт: " + (config.teleport().enabled() ? "§aвкл" : "§7выкл"));
if (config.teleport().enabled()) {
config.teleport().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc)));
}
lore.add("");
lore.add(" §fСпавн (застрявших): " + (config.playerSpawn().enabled() ? "§aвкл" : "§7выкл"));
if (config.playerSpawn().enabled()) {
config.playerSpawn().getLocation().ifPresent(loc -> lore.add(" " + ItemStackFactory.loc(loc)));
}
lore.add("");
lore.add("§8Команды:");
lore.add(" §8• §7/lm setteleport §f<mine>");
lore.add(" §8• §7/lm setspawn §f<mine>");
lore.add(" §8• §7/lm clearspawn §f<mine>");
lore.add("");
lore.add(EDIT);
return ItemStackFactory.create(Material.ENDER_PEARL, "§a§lТелепорт", lore.toArray(new String[0]));
}
static ItemStack uiItem(MineConfig config) {
return ItemStackFactory.create(Material.PAINTING, "§a§lИнтерфейс",
SEP,
"§7Настройки:",
" §fAction Bar: " + (config.ui().actionBarEnabled() ? "§aвкл" : "§7выкл"),
" §fРадиус: §7" + (int) config.ui().actionBarRange() + " §8блоков",
"",
"§7Формат ActionBar:",
" §8" + ItemStackFactory.trunc(config.ui().actionBarFormat(), 28),
"",
"§7Формат таймера: §8" + config.ui().timerFormat(),
"",
EDIT
);
}
static ItemStack saveItem() {
return ItemStackFactory.create(Material.LIME_DYE, "§a§lСохранить изменения",
SEP,
"§7Сохраняет конфигурацию шахты",
"§7в файл на диске",
"",
SAVE
);
}
static ItemStack deleteItem(String mineName) {
return ItemStackFactory.create(Material.TNT, "§c§lУдалить шахту",
SEP,
"§c§lВнимание!",
"§cЭто действие нельзя отменить!",
"",
"§7Шахта: §f" + mineName,
"",
DEL
);
}
static ItemStack infoItem(Mine mine) {
int blocks = mine.getBlocks();
int total = mine.getTotalVolume();
double percent = mine.getPercentFilled();
String c = percent > 50 ? "§a" : percent > 25 ? "§e" : "§c";
int filled = (int) Math.round(percent / 100.0 * 10);
String bar = "§a" + "█".repeat(filled) + "§8" + "░".repeat(10 - filled);
return ItemStackFactory.create(Material.BOOK, "§b§lИнформация",
SEP,
"§7Базовые данные:",
" §fНазвание: §7" + mine.getName(),
" §fМир: §7" + mine.getConfig().worldName(),
"",
"§7Состояние заполнения:",
" §fБлоков: " + c + blocks + "§8/§7" + total,
" §fПроцент: " + c + String.format("%.1f%%", percent),
" " + bar
);
}
static ItemStack backItem() {
return ItemStackFactory.create(Material.ARROW, "§c§lЗакрыть",
SEP,
"§7Закрыть редактор",
"",
CLOSE
);
}
}

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.handler;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.util.format.TimeFormatter;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.utils.ActionBar;

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.handler;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.handler;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.util.block.BlockUpdateUtil;
import dev.loki.lomines.util.location.Cuboid;

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.integration.placeholder;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.mine.Mine;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

View file

@ -0,0 +1,53 @@
package dev.loki.lomines.integration.worldguard;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import dev.loki.lomines.LoMinesPlugin;
import java.util.UUID;
/**
* Handles adding owners and members to WorldGuard regions.
*/
final class WorldGuardMemberHandler {
private final LoMinesPlugin plugin;
WorldGuardMemberHandler(LoMinesPlugin plugin) {
this.plugin = plugin;
}
void applyOwnersAndMembers(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) {
applyOwners(region, wgConfig);
applyMembers(region, wgConfig);
}
private void applyOwners(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) {
for (String owner : wgConfig.owners()) {
if (owner.startsWith("uuid:")) {
try {
UUID uuid = UUID.fromString(owner.substring(5));
region.getOwners().addPlayer(uuid);
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid owner UUID: " + owner);
}
} else {
region.getOwners().addPlayer(owner);
}
}
}
private void applyMembers(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) {
for (String member : wgConfig.members()) {
if (member.startsWith("uuid:")) {
try {
UUID uuid = UUID.fromString(member.substring(5));
region.getMembers().addPlayer(uuid);
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid member UUID: " + member);
}
} else {
region.getMembers().addPlayer(member);
}
}
}
}

View file

@ -11,21 +11,21 @@ import org.bukkit.Bukkit;
import org.bukkit.World;
import java.util.List;
import java.util.UUID;
/**
* Service for managing WorldGuard regions for mines.
* Creates, updates, and deletes WorldGuard regions when mines are managed.
*/
public final class WorldGuardRegionService {
private final LoMinesPlugin plugin;
private final WorldGuardFlagParser flagParser;
private final WorldGuardMemberHandler memberHandler;
private boolean worldGuardEnabled;
public WorldGuardRegionService(LoMinesPlugin plugin) {
this.plugin = plugin;
this.flagParser = new WorldGuardFlagParser(plugin);
this.memberHandler = new WorldGuardMemberHandler(plugin);
checkWorldGuard();
}
@ -34,30 +34,21 @@ public final class WorldGuardRegionService {
&& Bukkit.getPluginManager().isPluginEnabled("WorldGuard");
if (worldGuardEnabled) {
plugin.loLogger().info("WorldGuard integration enabled - auto-region creation active");
plugin.loLogger().info("WorldGuard integration enabled");
}
}
/**
* Creates a WorldGuard region for a mine if enabled in config.
*/
public String createRegion(String mineName, MineConfig config) {
if (!worldGuardEnabled) {
return null;
}
if (!worldGuardEnabled) return null;
WorldGuardConfig wgConfig = config.worldGuard();
if (wgConfig == null || !wgConfig.enabled()) {
return null;
}
if (wgConfig == null || !wgConfig.enabled()) return null;
String regionName = wgConfig.generateRegionName(mineName);
try {
RegionManager regionManager = getRegionManager(config.worldName());
if (regionManager == null) {
return null;
}
if (regionManager == null) return null;
if (regionManager.hasRegion(regionName)) {
plugin.loLogger().info("WorldGuard region already exists: " + regionName);
@ -70,7 +61,7 @@ public final class WorldGuardRegionService {
return null;
}
applyOwnersAndMembers(region, wgConfig);
memberHandler.applyOwnersAndMembers(region, wgConfig);
applyFlags(region, wgConfig.flags());
regionManager.addRegion(region);
@ -79,31 +70,21 @@ public final class WorldGuardRegionService {
} catch (Exception e) {
plugin.loLogger().error("Failed to create WorldGuard region for mine " + mineName + ": " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* Updates an existing WorldGuard region when mine regions change.
*/
public boolean updateRegion(String mineName, MineConfig config) {
if (!worldGuardEnabled) {
return false;
}
if (!worldGuardEnabled) return false;
WorldGuardConfig wgConfig = config.worldGuard();
if (wgConfig == null || !wgConfig.enabled()) {
return false;
}
if (wgConfig == null || !wgConfig.enabled()) return false;
String regionName = wgConfig.generateRegionName(mineName);
try {
RegionManager regionManager = getRegionManager(config.worldName());
if (regionManager == null) {
return false;
}
if (regionManager == null) return false;
if (regionManager.hasRegion(regionName)) {
regionManager.removeRegion(regionName);
@ -117,26 +98,17 @@ public final class WorldGuardRegionService {
}
}
/**
* Deletes a WorldGuard region for a mine.
*/
public boolean deleteRegion(String mineName, MineConfig config) {
if (!worldGuardEnabled) {
return false;
}
if (!worldGuardEnabled) return false;
WorldGuardConfig wgConfig = config.worldGuard();
if (wgConfig == null) {
return false;
}
if (wgConfig == null) return false;
String regionName = wgConfig.generateRegionName(mineName);
try {
RegionManager regionManager = getRegionManager(config.worldName());
if (regionManager == null) {
return false;
}
if (regionManager == null) return false;
if (regionManager.hasRegion(regionName)) {
regionManager.removeRegion(regionName);
@ -153,9 +125,7 @@ public final class WorldGuardRegionService {
private RegionManager getRegionManager(String worldName) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return null;
}
if (world == null) return null;
return WorldGuard.getInstance()
.getPlatform()
@ -165,9 +135,7 @@ public final class WorldGuardRegionService {
private ProtectedCuboidRegion createRegionFromCuboids(String regionName, MineConfig config) {
List<Cuboid> regions = config.region().regions();
if (regions.isEmpty()) {
return null;
}
if (regions.isEmpty()) return null;
Cuboid first = regions.get(0);
BlockVector3 min = BlockVector3.at(first.getMinX(), first.getMinY(), first.getMinZ());
@ -183,36 +151,6 @@ public final class WorldGuardRegionService {
return mainRegion;
}
private void applyOwnersAndMembers(ProtectedCuboidRegion region, WorldGuardConfig wgConfig) {
// Set owners
for (String owner : wgConfig.owners()) {
if (owner.startsWith("uuid:")) {
try {
UUID uuid = UUID.fromString(owner.substring(5));
region.getOwners().addPlayer(uuid);
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid owner UUID: " + owner);
}
} else {
region.getOwners().addPlayer(owner);
}
}
// Set members
for (String member : wgConfig.members()) {
if (member.startsWith("uuid:")) {
try {
UUID uuid = UUID.fromString(member.substring(5));
region.getMembers().addPlayer(uuid);
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid member UUID: " + member);
}
} else {
region.getMembers().addPlayer(member);
}
}
}
private void applyFlags(ProtectedCuboidRegion region, List<String> flags) {
for (String flagEntry : flags) {
flagParser.parseAndSetFlag(region, flagEntry);

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.listener;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.mine.Mine;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

View file

@ -1,8 +1,8 @@
package dev.loki.lomines.listener;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.gui.GroupCreateGui;
import dev.loki.lomines.gui.GroupCreateGuiHolder;
import dev.loki.lomines.gui.group.GroupCreateGui;
import dev.loki.lomines.gui.group.GroupCreateGuiHolder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

View file

@ -1,10 +1,10 @@
package dev.loki.lomines.listener;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.gui.ConfirmDeleteGui;
import dev.loki.lomines.gui.ConfirmDeleteGuiHolder;
import dev.loki.lomines.gui.MineEditGui;
import dev.loki.lomines.gui.MineEditGuiHolder;
import dev.loki.lomines.gui.confirm.ConfirmDeleteGui;
import dev.loki.lomines.gui.confirm.ConfirmDeleteGuiHolder;
import dev.loki.lomines.gui.mine.MineEditGui;
import dev.loki.lomines.gui.mine.MineEditGuiHolder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.listener;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.gui.GroupCreateGui;
import dev.loki.lomines.gui.group.GroupCreateGui;
import dev.loki.lomines.wand.GroupWandItem;
import dev.loki.lomines.wand.GroupWandSession;
import net.kyori.adventure.text.Component;

View file

@ -17,8 +17,7 @@ import java.util.Collection;
*/
public final class BlockUpdateUtil {
private BlockUpdateUtil() {
}
private BlockUpdateUtil() {}
/**
* Sends a block update to all players who can see the specified chunk.
@ -27,15 +26,11 @@ public final class BlockUpdateUtil {
* @param block the block to update
*/
public static void sendBlockUpdate(Block block) {
if (block == null) {
return;
}
if (block == null) return;
// Get chunk and players in it
Chunk chunk = block.getChunk();
World world = block.getWorld();
// Send block change to all players in the world who can see this chunk
for (Player player : world.getPlayers()) {
if (isChunkVisibleToPlayer(player, chunk)) {
player.sendBlockChange(block.getLocation(), block.getBlockData());
@ -63,7 +58,6 @@ public final class BlockUpdateUtil {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
Block block = world.getBlockAt(x, y, z);
for (Player player : players) {
if (isLocationVisibleToPlayer(player, block.getLocation())) {
player.sendBlockChange(block.getLocation(), block.getBlockData());
@ -81,16 +75,11 @@ public final class BlockUpdateUtil {
* @param locations the locations to update
*/
public static void sendLocationsUpdate(java.util.List<Location> locations) {
if (locations == null || locations.isEmpty()) {
return;
}
if (locations == null || locations.isEmpty()) return;
for (Location loc : locations) {
if (loc.getWorld() == null) {
continue;
}
if (loc.getWorld() == null) continue;
Block block = loc.getBlock();
for (Player player : loc.getWorld().getPlayers()) {
if (isLocationVisibleToPlayer(player, loc)) {
player.sendBlockChange(loc, block.getBlockData());
@ -127,109 +116,8 @@ public final class BlockUpdateUtil {
}
}
/**
* Finds a safe teleport location near the given destination.
* Checks for suffocation hazards (blocks at head/body level).
* <p>Will not teleport too high - limited to maxUpOffset blocks above original.</p>
*
* @param destination the desired destination
* @param maxUpOffset maximum blocks to search upward (to avoid teleporting too high)
* @return a safe location (may be the same as destination if safe)
*/
public static Location findSafeTeleportLocation(Location destination, int maxUpOffset) {
if (destination == null || destination.getWorld() == null) {
return destination;
}
World world = destination.getWorld();
int x = destination.getBlockX();
int y = destination.getBlockY();
int z = destination.getBlockZ();
float yaw = destination.getYaw();
float pitch = destination.getPitch();
// Clamp maxUpOffset to reasonable range (2-10)
maxUpOffset = Math.max(2, Math.min(maxUpOffset, 10));
// Check if original location is safe
if (isSafeLocation(world, x, y, z)) {
return destination;
}
// Search nearby blocks first (same Y level, closer distance)
int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] offset : nearby) {
if (isSafeLocation(world, x + offset[0], y, z + offset[1])) {
return new Location(world, x + offset[0] + 0.5, y, z + offset[1] + 0.5, yaw, pitch);
}
}
// Search upward (limited by maxUpOffset to avoid teleporting too high)
for (int offset = 1; offset <= maxUpOffset; offset++) {
if (isSafeLocation(world, x, y + offset, z)) {
return new Location(world, x + 0.5, y + offset, z + 0.5, yaw, pitch);
}
}
// Search downward
for (int offset = 1; offset <= 5 && y - offset >= world.getMinHeight(); offset++) {
if (isSafeLocation(world, x, y - offset, z)) {
return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch);
}
}
// Search nearby with small Y offset
int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}};
for (int[] offset : nearbyDiagonal) {
for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) {
int newY = y + yOffset;
if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) {
continue;
}
if (isSafeLocation(world, x + offset[0], newY, z + offset[1])) {
return new Location(world, x + offset[0] + 0.5, newY, z + offset[1] + 0.5, yaw, pitch);
}
}
}
// If nothing else works, return original but at least center it
return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch);
}
/**
* Finds a safe teleport location with default max up offset of 3 blocks.
*/
public static Location findSafeTeleportLocation(Location destination) {
return findSafeTeleportLocation(destination, 3);
}
/**
* Checks if a location is safe for teleport (no suffocation).
*/
private static boolean isSafeLocation(World world, int x, int y, int z) {
// Check feet position (can be passable)
Block feetBlock = world.getBlockAt(x, y, z);
// Check head position (must be passable)
Block headBlock = world.getBlockAt(x, y + 1, z);
// Safe if feet is passable and head is passable
return isPassable(feetBlock) && isPassable(headBlock);
}
/**
* Checks if a block is passable (not solid).
*/
private static boolean isPassable(Block block) {
return !block.getType().isSolid() || block.getType().isAir();
}
/**
* Checks if a player can see a chunk (is within render distance).
*/
private static boolean isChunkVisibleToPlayer(Player player, Chunk chunk) {
if (!player.getWorld().equals(chunk.getWorld())) {
return false;
}
if (!player.getWorld().equals(chunk.getWorld())) return false;
int renderDistance = player.getClientViewDistance();
int playerChunkX = player.getLocation().getBlockX() >> 4;
@ -238,19 +126,13 @@ public final class BlockUpdateUtil {
int dx = Math.abs(playerChunkX - chunk.getX());
int dz = Math.abs(playerChunkZ - chunk.getZ());
// Check if chunk is within player's view distance
return dx <= renderDistance && dz <= renderDistance;
}
/**
* Checks if a location is visible to a player (within render distance).
*/
private static boolean isLocationVisibleToPlayer(Player player, Location loc) {
if (!player.getWorld().equals(loc.getWorld())) {
return false;
}
if (!player.getWorld().equals(loc.getWorld())) return false;
double renderDistance = player.getClientViewDistance() * 16; // blocks
double renderDistance = player.getClientViewDistance() * 16;
return player.getLocation().distanceSquared(loc) <= renderDistance * renderDistance;
}
}

View file

@ -67,6 +67,33 @@ public final class LocationParser {
}
}
/**
* Parses a simple location string in the format "world;x;y;z" without yaw/pitch.
* Returns null if parsing fails.
*
* @param locationString the location string to parse
* @return the parsed Location object, or null if parsing fails
*/
public static Location parseSimple(String locationString) {
if (locationString == null || locationString.isEmpty()) {
return null;
}
String[] parts = locationString.split(";");
if (parts.length < 4) return null;
World world = Bukkit.getWorld(parts[0]);
if (world == null) return null;
try {
double x = Double.parseDouble(parts[1]);
double y = Double.parseDouble(parts[2]);
double z = Double.parseDouble(parts[3]);
return new Location(world, x, y, z);
} catch (NumberFormatException e) {
return null;
}
}
/**
* Formats a Location object to a string in the format "world;x;y;z;yaw;pitch".
*

View file

@ -0,0 +1,117 @@
package dev.loki.lomines.util.location;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
/**
* Utility for finding safe teleport locations.
* Checks for suffocation hazards and finds alternative safe positions.
*/
public final class SafeTeleportUtil {
private SafeTeleportUtil() {}
/**
* Finds a safe teleport location near the given destination.
* Checks for suffocation hazards (blocks at head/body level).
* <p>Will not teleport too high - limited to maxUpOffset blocks above original.</p>
*
* @param destination the desired destination
* @param maxUpOffset maximum blocks to search upward (to avoid teleporting too high)
* @return a safe location (may be the same as destination if safe)
*/
public static Location findSafeTeleportLocation(Location destination, int maxUpOffset) {
if (destination == null || destination.getWorld() == null) {
return destination;
}
World world = destination.getWorld();
int x = destination.getBlockX();
int y = destination.getBlockY();
int z = destination.getBlockZ();
float yaw = destination.getYaw();
float pitch = destination.getPitch();
maxUpOffset = Math.max(2, Math.min(maxUpOffset, 10));
if (isSafeLocation(world, x, y, z)) {
return destination;
}
Location result = findNearbySafe(world, x, y, z, yaw, pitch);
if (result != null) return result;
result = searchUpward(world, x, y, z, yaw, pitch, maxUpOffset);
if (result != null) return result;
result = searchDownward(world, x, y, z, yaw, pitch);
if (result != null) return result;
result = searchDiagonal(world, x, y, z, yaw, pitch, maxUpOffset);
if (result != null) return result;
return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch);
}
/**
* Finds a safe teleport location with default max up offset of 3 blocks.
*/
public static Location findSafeTeleportLocation(Location destination) {
return findSafeTeleportLocation(destination, 3);
}
private static Location findNearbySafe(World world, int x, int y, int z, float yaw, float pitch) {
int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] offset : nearby) {
if (isSafeLocation(world, x + offset[0], y, z + offset[1])) {
return new Location(world, x + offset[0] + 0.5, y, z + offset[1] + 0.5, yaw, pitch);
}
}
return null;
}
private static Location searchUpward(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) {
for (int offset = 1; offset <= maxUpOffset; offset++) {
if (isSafeLocation(world, x, y + offset, z)) {
return new Location(world, x + 0.5, y + offset, z + 0.5, yaw, pitch);
}
}
return null;
}
private static Location searchDownward(World world, int x, int y, int z, float yaw, float pitch) {
for (int offset = 1; offset <= 5 && y - offset >= world.getMinHeight(); offset++) {
if (isSafeLocation(world, x, y - offset, z)) {
return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch);
}
}
return null;
}
private static Location searchDiagonal(World world, int x, int y, int z, float yaw, float pitch, int maxUpOffset) {
int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}};
for (int[] offset : nearbyDiagonal) {
for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) {
int newY = y + yOffset;
if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) {
continue;
}
if (isSafeLocation(world, x + offset[0], newY, z + offset[1])) {
return new Location(world, x + offset[0] + 0.5, newY, z + offset[1] + 0.5, yaw, pitch);
}
}
}
return null;
}
private static boolean isSafeLocation(World world, int x, int y, int z) {
Block feetBlock = world.getBlockAt(x, y, z);
Block headBlock = world.getBlockAt(x, y + 1, z);
return isPassable(feetBlock) && isPassable(headBlock);
}
private static boolean isPassable(Block block) {
return !block.getType().isSolid() || block.getType().isAir();
}
}

View file

@ -0,0 +1,120 @@
package dev.loki.lomines.wand;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Player;
/**
* Utility class for spawning particles to visualize wand selections.
*/
final class ParticleUtil {
private ParticleUtil() {}
static void showCornerParticles(Player player, Location loc, Color color) {
World world = loc.getWorld();
if (world == null) return;
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 1.0f);
for (int i = 0; i < 8; i++) {
double angle = i * Math.PI / 4;
double x = loc.getX() + 0.5 + Math.cos(angle) * 0.5;
double z = loc.getZ() + 0.5 + Math.sin(angle) * 0.5;
double y = loc.getY() + 0.5;
Location particleLoc = new Location(world, x, y, z);
player.spawnParticle(Particle.DUST, particleLoc, 1, dustOptions);
}
}
static void showEdgeParticles(Player player, Location pos1, Location pos2, Color color) {
World world = pos1.getWorld();
if (world == null) return;
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.8f);
int minX = Math.min(pos1.getBlockX(), pos2.getBlockX());
int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX());
int minY = Math.min(pos1.getBlockY(), pos2.getBlockY());
int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY());
int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ());
int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ());
double step = 1.0;
// Bottom face edges
spawnEdgeParticles(player, world, minX, minY, minZ, maxX, minY, minZ, step, dustOptions);
spawnEdgeParticles(player, world, minX, minY, maxZ, maxX, minY, maxZ, step, dustOptions);
spawnEdgeParticles(player, world, minX, minY, minZ, minX, minY, maxZ, step, dustOptions);
spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, minY, maxZ, step, dustOptions);
// Top face edges
spawnEdgeParticles(player, world, minX, maxY, minZ, maxX, maxY, minZ, step, dustOptions);
spawnEdgeParticles(player, world, minX, maxY, maxZ, maxX, maxY, maxZ, step, dustOptions);
spawnEdgeParticles(player, world, minX, maxY, minZ, minX, maxY, maxZ, step, dustOptions);
spawnEdgeParticles(player, world, maxX, maxY, minZ, maxX, maxY, maxZ, step, dustOptions);
// Vertical edges
spawnEdgeParticles(player, world, minX, minY, minZ, minX, maxY, minZ, step, dustOptions);
spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, maxY, minZ, step, dustOptions);
spawnEdgeParticles(player, world, minX, minY, maxZ, minX, maxY, maxZ, step, dustOptions);
spawnEdgeParticles(player, world, maxX, minY, maxZ, maxX, maxY, maxZ, step, dustOptions);
}
private static void spawnEdgeParticles(Player player, World world,
double x1, double y1, double z1,
double x2, double y2, double z2,
double step, Particle.DustOptions dustOptions) {
double distance = Math.sqrt(
Math.pow(x2 - x1, 2) +
Math.pow(y2 - y1, 2) +
Math.pow(z2 - z1, 2)
);
int count = (int) (distance / step) + 1;
for (int i = 0; i <= count; i++) {
double t = (double) i / count;
double x = x1 + (x2 - x1) * t + 0.5;
double y = y1 + (y2 - y1) * t + 0.5;
double z = z1 + (z2 - z1) * t + 0.5;
Location loc = new Location(world, x, y, z);
if (loc.getWorld() != null && loc.getWorld().equals(player.getWorld())) {
player.spawnParticle(Particle.DUST, loc, 1, dustOptions);
}
}
}
static void showOutlineParticles(Player player, Location pos1, Location pos2, Color color, int density) {
World world = pos1.getWorld();
if (world == null || !world.equals(player.getWorld())) return;
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.5f);
int minX = Math.min(pos1.getBlockX(), pos2.getBlockX());
int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX());
int minY = Math.min(pos1.getBlockY(), pos2.getBlockY());
int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY());
int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ());
int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ());
Location[] corners = {
new Location(world, minX + 0.5, minY + 0.5, minZ + 0.5),
new Location(world, maxX + 0.5, minY + 0.5, minZ + 0.5),
new Location(world, minX + 0.5, minY + 0.5, maxZ + 0.5),
new Location(world, maxX + 0.5, minY + 0.5, maxZ + 0.5),
new Location(world, minX + 0.5, maxY + 0.5, minZ + 0.5),
new Location(world, maxX + 0.5, maxY + 0.5, minZ + 0.5),
new Location(world, minX + 0.5, maxY + 0.5, maxZ + 0.5),
new Location(world, maxX + 0.5, maxY + 0.5, maxZ + 0.5)
};
for (Location corner : corners) {
player.spawnParticle(Particle.DUST, corner, 1, dustOptions);
}
}
}

View file

@ -3,8 +3,6 @@ package dev.loki.lomines.wand;
import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
@ -21,23 +19,17 @@ public final class WandParticleService {
private final LoMinesPlugin plugin;
private final Map<UUID, BukkitRunnable> activeTasks = new ConcurrentHashMap<>();
// Particle colors for different states
private static final Color COLOR_POS1 = Color.LIME; // Green for corner 1
private static final Color COLOR_POS2 = Color.AQUA; // Cyan for corner 2
private static final Color COLOR_EDGE = Color.YELLOW; // Yellow for edges
private static final Color COLOR_READY = Color.GREEN; // Green when both corners set
private static final Color COLOR_POS1 = Color.LIME;
private static final Color COLOR_POS2 = Color.AQUA;
private static final Color COLOR_EDGE = Color.YELLOW;
private static final Color COLOR_READY = Color.GREEN;
public WandParticleService(LoMinesPlugin plugin) {
this.plugin = plugin;
}
/**
* Starts showing particles for a player holding the wand.
*/
public void startShowingParticles(Player player) {
UUID playerId = player.getUniqueId();
// Stop existing task if any
stopShowingParticles(playerId);
BukkitRunnable task = new BukkitRunnable() {
@ -48,28 +40,19 @@ public final class WandParticleService {
activeTasks.remove(playerId);
return;
}
showParticles(player);
}
};
task.runTaskTimer(plugin, 0L, 10L); // Every 10 ticks (0.5 seconds)
task.runTaskTimer(plugin, 0L, 10L);
activeTasks.put(playerId, task);
}
/**
* Stops showing particles for a player.
*/
public void stopShowingParticles(UUID playerId) {
BukkitRunnable task = activeTasks.remove(playerId);
if (task != null) {
task.cancel();
}
if (task != null) task.cancel();
}
/**
* Stops all particle tasks.
*/
public void stopAll() {
for (BukkitRunnable task : activeTasks.values()) {
task.cancel();
@ -88,141 +71,34 @@ public final class WandParticleService {
Location pos1 = session.getPos1(activeSlot);
Location pos2 = session.getPos2(activeSlot);
// Show particles for corner 1 if set
if (pos1 != null && pos1.getWorld() != null) {
showCornerParticles(player, pos1, COLOR_POS1);
ParticleUtil.showCornerParticles(player, pos1, COLOR_POS1);
}
// Show particles for corner 2 if set
if (pos2 != null && pos2.getWorld() != null) {
showCornerParticles(player, pos2, COLOR_POS2);
ParticleUtil.showCornerParticles(player, pos2, COLOR_POS2);
}
// Show edge particles if both corners are set
if (pos1 != null && pos2 != null && pos1.getWorld() != null && pos2.getWorld() != null) {
if (pos1.getWorld().equals(pos2.getWorld())) {
showEdgeParticles(player, pos1, pos2, session.isSlotReady(activeSlot) ? COLOR_READY : COLOR_EDGE);
Color edgeColor = session.isSlotReady(activeSlot) ? COLOR_READY : COLOR_EDGE;
ParticleUtil.showEdgeParticles(player, pos1, pos2, edgeColor);
}
}
// Also show other slots that are ready (with lower intensity)
showOtherSlots(player, session, activeSlot);
}
private void showOtherSlots(Player player, GroupWandSession session, int activeSlot) {
for (int i = 0; i < 9; i++) {
if (i == activeSlot) continue;
if (session.isSlotReady(i)) {
Location p1 = session.getPos1(i);
Location p2 = session.getPos2(i);
if (p1 != null && p2 != null && p1.getWorld() != null && p1.getWorld().equals(player.getWorld())) {
showOutlineParticles(player, p1, p2, Color.GRAY, 5);
ParticleUtil.showOutlineParticles(player, p1, p2, Color.GRAY, 5);
}
}
}
}
private void showCornerParticles(Player player, Location loc, Color color) {
World world = loc.getWorld();
if (world == null) return;
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 1.0f);
// Show particles in a small sphere around the corner
for (int i = 0; i < 8; i++) {
double angle = i * Math.PI / 4;
double x = loc.getX() + 0.5 + Math.cos(angle) * 0.5;
double z = loc.getZ() + 0.5 + Math.sin(angle) * 0.5;
double y = loc.getY() + 0.5;
Location particleLoc = new Location(world, x, y, z);
player.spawnParticle(Particle.DUST, particleLoc, 1, dustOptions);
}
}
private void showEdgeParticles(Player player, Location pos1, Location pos2, Color color) {
World world = pos1.getWorld();
if (world == null) return;
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.8f);
int minX = Math.min(pos1.getBlockX(), pos2.getBlockX());
int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX());
int minY = Math.min(pos1.getBlockY(), pos2.getBlockY());
int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY());
int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ());
int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ());
// Show particles along the edges of the cuboid
double step = 1.0; // Every block
// Bottom face edges
spawnEdgeParticles(player, world, minX, minY, minZ, maxX, minY, minZ, step, dustOptions); // Bottom front
spawnEdgeParticles(player, world, minX, minY, maxZ, maxX, minY, maxZ, step, dustOptions); // Bottom back
spawnEdgeParticles(player, world, minX, minY, minZ, minX, minY, maxZ, step, dustOptions); // Bottom left
spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, minY, maxZ, step, dustOptions); // Bottom right
// Top face edges
spawnEdgeParticles(player, world, minX, maxY, minZ, maxX, maxY, minZ, step, dustOptions); // Top front
spawnEdgeParticles(player, world, minX, maxY, maxZ, maxX, maxY, maxZ, step, dustOptions); // Top back
spawnEdgeParticles(player, world, minX, maxY, minZ, minX, maxY, maxZ, step, dustOptions); // Top left
spawnEdgeParticles(player, world, maxX, maxY, minZ, maxX, maxY, maxZ, step, dustOptions); // Top right
// Vertical edges
spawnEdgeParticles(player, world, minX, minY, minZ, minX, maxY, minZ, step, dustOptions); // Front left
spawnEdgeParticles(player, world, maxX, minY, minZ, maxX, maxY, minZ, step, dustOptions); // Front right
spawnEdgeParticles(player, world, minX, minY, maxZ, minX, maxY, maxZ, step, dustOptions); // Back left
spawnEdgeParticles(player, world, maxX, minY, maxZ, maxX, maxY, maxZ, step, dustOptions); // Back right
}
private void spawnEdgeParticles(Player player, World world,
double x1, double y1, double z1,
double x2, double y2, double z2,
double step, Particle.DustOptions dustOptions) {
double distance = Math.sqrt(
Math.pow(x2 - x1, 2) +
Math.pow(y2 - y1, 2) +
Math.pow(z2 - z1, 2)
);
int count = (int) (distance / step) + 1;
for (int i = 0; i <= count; i++) {
double t = (double) i / count;
double x = x1 + (x2 - x1) * t + 0.5;
double y = y1 + (y2 - y1) * t + 0.5;
double z = z1 + (z2 - z1) * t + 0.5;
Location loc = new Location(world, x, y, z);
if (loc.getWorld() != null && loc.getWorld().equals(player.getWorld())) {
player.spawnParticle(Particle.DUST, loc, 1, dustOptions);
}
}
}
private void showOutlineParticles(Player player, Location pos1, Location pos2, Color color, int density) {
World world = pos1.getWorld();
if (world == null || !world.equals(player.getWorld())) return;
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 0.5f);
int minX = Math.min(pos1.getBlockX(), pos2.getBlockX());
int maxX = Math.max(pos1.getBlockX(), pos2.getBlockX());
int minY = Math.min(pos1.getBlockY(), pos2.getBlockY());
int maxY = Math.max(pos1.getBlockY(), pos2.getBlockY());
int minZ = Math.min(pos1.getBlockZ(), pos2.getBlockZ());
int maxZ = Math.max(pos1.getBlockZ(), pos2.getBlockZ());
// Just show corners for non-active slots (lighter)
Location[] corners = {
new Location(world, minX + 0.5, minY + 0.5, minZ + 0.5),
new Location(world, maxX + 0.5, minY + 0.5, minZ + 0.5),
new Location(world, minX + 0.5, minY + 0.5, maxZ + 0.5),
new Location(world, maxX + 0.5, minY + 0.5, maxZ + 0.5),
new Location(world, minX + 0.5, maxY + 0.5, minZ + 0.5),
new Location(world, maxX + 0.5, maxY + 0.5, minZ + 0.5),
new Location(world, minX + 0.5, maxY + 0.5, maxZ + 0.5),
new Location(world, maxX + 0.5, maxY + 0.5, maxZ + 0.5)
};
for (Location corner : corners) {
player.spawnParticle(Particle.DUST, corner, 1, dustOptions);
}
}
}

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.command;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.Mines;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;