chore: unify checkstyle, fix violations, cleanup bin/

This commit is contained in:
loki5512344 2026-07-12 00:59:11 +02:00
parent 1d8c0ca483
commit a0c1ab8ca1
Signed by: boba
GPG key ID: 253067914055423B
156 changed files with 5222 additions and 4348 deletions

25
.classpath Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

34
.project Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>LoMines</name>
<comment>Project LoMines created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1783342209787</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -0,0 +1,13 @@
arguments=--init-script /home/loki/.cache/opencode/bin/jdtls/config_linux/org.eclipse.osgi/57/0/.cp/gradle/init/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/usr/lib/jvm/java-21-openjdk
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

View file

@ -1,6 +1,7 @@
plugins {
java
id("com.github.johnrengelman.shadow") version "8.1.1"
checkstyle
}
group = "com.loki"
@ -19,75 +20,47 @@ tasks.withType<JavaCompile> {
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
// PlaceholderAPI
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
// WorldGuard / WorldEdit
maven("https://maven.enginehub.org/repo/")
// CodeMC (DecentHolograms)
maven("https://repo.codemc.io/repository/maven-public/")
maven("https://jitpack.io")
}
val lolibTree = fileTree("libs") {
include("lolib*.jar")
}
if (lolibTree.files.isEmpty()) {
throw GradleException(
"Missing libs/lolib*.jar — add LoAPI fat jar to ${rootProject.file("libs").absolutePath}"
)
}
dependencies {
// Paper
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
// Local API
implementation(lolibTree)
// Commons
compileOnly("org.apache.commons:commons-math3:3.6.1")
compileOnly("commons-io:commons-io:2.18.0")
// Adventure
compileOnly("net.kyori:adventure-api:4.17.0")
compileOnly("net.kyori:adventure-text-minimessage:4.17.0")
// PlaceholderAPI
implementation("org.apache.commons:commons-math3:3.6.1")
implementation("commons-io:commons-io:2.18.0")
compileOnly("me.clip:placeholderapi:2.11.6")
// WorldGuard + WorldEdit
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.13")
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10")
// Optional
// compileOnly("com.github.oraxen:oraxen:1.180.0")
// compileOnly("com.github.LoneDev6:api-itemsadder:3.6.4")
// Tests
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
testImplementation("io.kotest:kotest-assertions-core:5.9.1")
testImplementation("io.kotest:kotest-property:5.9.1")
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-inline:5.2.0")
// Bukkit/Paper API for tests
testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
// Adventure for tests
testImplementation("net.kyori:adventure-api:4.17.0")
testImplementation("net.kyori:adventure-text-minimessage:4.17.0")
}
checkstyle {
toolVersion = "10.21.4"
configFile = file("config/checkstyle/checkstyle.xml")
isIgnoreFailures = false
maxErrors = 0
maxWarnings = 0
}
tasks.withType<Checkstyle>().configureEach {
reports {
xml.required.set(true)
html.required.set(true)
}
}
tasks {
shadowJar {

View file

@ -0,0 +1,89 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="error"/>
<property name="fileExtensions" value="java"/>
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value=".*[\\/]src[\\/]test[\\/].*"/>
</module>
<module name="SuppressWarningsFilter"/>
<module name="LineLength">
<property name="max" value="150"/>
<property name="ignorePattern"
value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="SuppressWarningsHolder"/>
<module name="MethodLength">
<property name="max" value="80"/>
<property name="countEmpty" value="false"/>
</module>
<module name="CyclomaticComplexity">
<property name="max" value="20"/>
</module>
<module name="ParameterNumber">
<property name="max" value="7"/>
</module>
<module name="AvoidStarImport"/>
<module name="UnusedImports"/>
<module name="RedundantImport"/>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-zA-Z][a-zA-Z0-9]*)*$"/>
</module>
<module name="TypeName"/>
<module name="MethodName"/>
<module name="MemberName"/>
<module name="ParameterName"/>
<module name="LocalVariableName"/>
<module name="ConstantName">
<property name="format" value="^log$|^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly"/>
<module name="RightCurly"/>
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<module name="EmptyStatement"/>
<module name="EqualsHashCode"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<module name="StringLiteralEquality"/>
<module name="DefaultComesLast"/>
<module name="MultipleVariableDeclarations"/>
<module name="OneStatementPerLine"/>
<module name="FallThrough"/>
<module name="HideUtilityClassConstructor"/>
<module name="OneTopLevelClass"/>
<module name="UpperEll"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="OuterTypeFilename"/>
<module name="EmptyBlock">
<property name="option" value="text"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected|ignore"/>
</module>
<module name="EqualsAvoidNull"/>
<module name="NoFinalizer"/>
<module name="NoClone"/>
<module name="PackageDeclaration"/>
</module>
</module>

0
gradlew vendored Normal file → Executable file
View file

View file

@ -1,11 +1,11 @@
package dev.loki.lomines;
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.core.mine.service.MineTicker;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.stats.service.StatsManager;
import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.wand.group.GroupWandManager;
import dev.lolib.commands.CommandManager;
import java.io.IOException;
import java.nio.file.Files;
@ -28,7 +28,7 @@ final class ComponentInitializer {
if (!Files.exists(minesFolder)) {
Files.createDirectories(minesFolder);
plugin.loLogger().info("Created mines directory");
plugin.getLogger().info("Created mines directory");
}
}
@ -36,20 +36,19 @@ final class ComponentInitializer {
Mines mines = new Mines(plugin);
GroupWandManager groupWandManager = new GroupWandManager();
StatsManager statsManager = new StatsManager(plugin);
CommandManager commandManager = new CommandManager(plugin);
IntegrationManager integrationManager = new IntegrationManager(plugin);
plugin.loLogger().info("Core components initialized");
plugin.getLogger().info("Core components initialized");
return new Components(mines, groupWandManager, statsManager, commandManager, integrationManager);
return new Components(mines, groupWandManager, statsManager, integrationManager);
}
void loadMines(Mines mines) {
try {
mines.loadAll();
plugin.loLogger().info("Loaded " + mines.getAll().size() + " mine(s)");
plugin.getLogger().info("Loaded " + mines.getAll().size() + " mine(s)");
} catch (IOException e) {
plugin.loLogger().error("Failed to load mines: " + e.getMessage());
plugin.getLogger().severe("Failed to load mines: " + e.getMessage());
e.printStackTrace();
}
}
@ -65,9 +64,9 @@ final class ComponentInitializer {
try {
statsManager.load();
statsManager.startAutoSave();
plugin.loLogger().info("Statistics system enabled");
plugin.getLogger().info("Statistics system enabled");
} catch (IOException e) {
plugin.loLogger().warn("Failed to load statistics: " + e.getMessage());
plugin.getLogger().warning("Failed to load statistics: " + e.getMessage());
}
}
}
@ -76,7 +75,6 @@ final class ComponentInitializer {
Mines mines,
GroupWandManager groupWandManager,
StatsManager statsManager,
CommandManager commandManager,
IntegrationManager integrationManager
) {
}

View file

@ -1,33 +1,26 @@
package dev.loki.lomines;
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.core.mine.service.MineTicker;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.stats.service.StatsManager;
import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.integration.hologram.HologramManager;
import dev.loki.lomines.wand.WandParticleService;
import dev.loki.lomines.wand.group.GroupWandManager;
import dev.lolib.commands.CommandManager;
import dev.lolib.core.LoPlugin;
import dev.lolib.core.dependency.DependencyManager;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Main plugin class for LoMines.
* Extends LoPlugin from LoAPI for Paper/Folia compatibility.
*/
public final class LoMinesPlugin extends LoPlugin {
public final class LoMinesPlugin extends JavaPlugin {
private Mines mines;
private MineTicker mineTicker;
private GroupWandManager groupWandManager;
private StatsManager statsManager;
private CommandManager commandManager;
private IntegrationManager integrationManager;
private WandParticleService wandParticleService;
private HologramManager hologramManager;
@Override
protected void enable() {
public void onEnable() {
try {
saveDefaultConfig();
@ -38,7 +31,6 @@ public final class LoMinesPlugin extends LoPlugin {
this.mines = components.mines();
this.groupWandManager = components.groupWandManager();
this.statsManager = components.statsManager();
this.commandManager = components.commandManager();
this.integrationManager = components.integrationManager();
this.wandParticleService = new WandParticleService(this);
this.hologramManager = new HologramManager(this);
@ -47,65 +39,51 @@ public final class LoMinesPlugin extends LoPlugin {
this.mineTicker = initializer.startTicker(mines);
RegistrationManager registrationManager = new RegistrationManager(this);
registrationManager.registerCommands(commandManager);
registrationManager.registerTabCompleter();
registrationManager.registerCommands();
registrationManager.registerListeners();
registrationManager.initializeIntegrations(integrationManager);
initializer.startStatistics(statsManager);
loLogger().info("LoMines has been enabled!");
getLogger().info("LoMines has been enabled!");
} catch (Exception e) {
loLogger().error("Failed to enable LoMines: " + e.getMessage());
getLogger().severe("Failed to enable LoMines: " + e.getMessage());
e.printStackTrace();
getServer().getPluginManager().disablePlugin(this);
}
}
@Override
protected void disable() {
public void onDisable() {
try {
if (mineTicker != null) {
mineTicker.stop();
mineTicker = null;
}
if (mines != null) {
mines.getAll().forEach(mine -> mine.stop());
}
if (statsManager != null) {
statsManager.stopAutoSave();
statsManager.save();
loLogger().info("Statistics saved successfully");
getLogger().info("Statistics saved successfully");
}
if (integrationManager != null) {
integrationManager.shutdown();
}
if (wandParticleService != null) {
wandParticleService.stopAll();
}
if (hologramManager != null) {
hologramManager.shutdown();
}
loLogger().info("LoMines has been disabled!");
getLogger().info("LoMines has been disabled!");
} catch (Exception e) {
loLogger().error("Error during plugin shutdown: " + e.getMessage());
getLogger().severe("Error during plugin shutdown: " + e.getMessage());
e.printStackTrace();
}
}
@Override
@SuppressWarnings("removal")
protected void dependencies(DependencyManager dependencyManager) {
dependencyManager.add("org.apache.commons", "commons-math3", "3.6.1");
dependencyManager.add("commons-io", "commons-io", "2.18.0");
}
public Mines getMines() {
return mines;
}
@ -118,10 +96,6 @@ public final class LoMinesPlugin extends LoPlugin {
return statsManager;
}
public CommandManager getCommandManager() {
return commandManager;
}
public IntegrationManager getIntegrationManager() {
return integrationManager;
}

View file

@ -1,16 +1,13 @@
package dev.loki.lomines;
import dev.loki.lomines.command.admin.*;
import dev.loki.lomines.command.common.LoMinesTabCompleter;
import dev.loki.lomines.command.player.PlayerCommands;
import dev.loki.lomines.command.player.TeleportCommand;
import dev.loki.lomines.command.LmCommand;
import dev.loki.lomines.integration.IntegrationManager;
import dev.loki.lomines.listener.*;
import dev.lolib.commands.CommandManager;
import dev.loki.lomines.listener.block.BlockBreakListener;
import dev.loki.lomines.listener.gui.GroupGuiListener;
import dev.loki.lomines.listener.gui.MineEditGuiListener;
import dev.loki.lomines.listener.player.PlayerInteractListener;
import dev.loki.lomines.listener.player.PlayerJoinListener;
/**
* Handles registration of commands, listeners, and integrations.
*/
final class RegistrationManager {
private final LoMinesPlugin plugin;
@ -19,28 +16,16 @@ final class RegistrationManager {
this.plugin = plugin;
}
void registerCommands(CommandManager commandManager) {
commandManager.register(new AdminCommands(plugin));
commandManager.register(new PlayerCommands(plugin));
commandManager.register(new StatsCommands(plugin));
commandManager.register(new MaskCommands(plugin));
commandManager.register(new HologramCommands(plugin));
commandManager.register(new InfoCommand(plugin));
commandManager.register(new TeleportCommand(plugin));
commandManager.register(new CopyCommand(plugin));
commandManager.register(new RegionCommands(plugin));
plugin.loLogger().info("Commands registered");
}
void registerTabCompleter() {
LoMinesTabCompleter tabCompleter = new LoMinesTabCompleter(plugin);
plugin.getCommand("lm").setTabCompleter(tabCompleter);
plugin.getCommand("lomines").setTabCompleter(tabCompleter);
plugin.getCommand("mine").setTabCompleter(tabCompleter);
plugin.getCommand("mines").setTabCompleter(tabCompleter);
plugin.loLogger().info("Tab completer registered");
void registerCommands() {
LmCommand executor = new LmCommand(plugin);
for (String alias : new String[]{"lm", "lomines", "mine", "mines"}) {
var cmd = plugin.getCommand(alias);
if (cmd != null) {
cmd.setExecutor(executor);
cmd.setTabCompleter(executor);
}
}
plugin.getLogger().info("Commands registered");
}
void registerListeners() {
@ -52,12 +37,11 @@ final class RegistrationManager {
if (plugin.getConfig().getBoolean("statistics-enabled", true)) {
plugin.getServer().getPluginManager().registerEvents(new PlayerJoinListener(plugin), plugin);
}
plugin.loLogger().info("Event listeners registered");
plugin.getLogger().info("Event listeners registered");
}
void initializeIntegrations(IntegrationManager integrationManager) {
integrationManager.initAll();
plugin.loLogger().info("Integrations initialized");
plugin.getLogger().info("Integrations initialized");
}
}

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.block;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.geo.Cuboid;
import org.bukkit.Location;
import java.util.List;

View file

@ -3,8 +3,8 @@ package dev.loki.lomines.block;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.util.block.BlockUpdateUtil;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.scheduler.Scheduler;
import dev.loki.lomines.util.location.geo.Cuboid;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
@ -35,11 +35,11 @@ public final class BukkitBlockSetter extends BlockSetter {
@Override
public void fill(Cuboid region, IntConsumer callback) {
Scheduler.get(plugin).runAsync(() -> {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = fillSync(region);
// Send block updates on main thread to prevent ghost blocks
Scheduler.get(plugin).run(() -> {
Bukkit.getScheduler().runTask(plugin, () -> {
BlockUpdateUtil.sendRegionUpdate(
region.getWorld(),
region.getMinX(), region.getMinY(), region.getMinZ(),
@ -53,14 +53,14 @@ public final class BukkitBlockSetter extends BlockSetter {
@Override
public void fillAtLocations(List<Location> locations, IntConsumer callback) {
if (locations == null || locations.isEmpty()) {
Scheduler.get(plugin).run(() -> callback.accept(0));
Bukkit.getScheduler().runTask(plugin, () -> callback.accept(0));
return;
}
Scheduler.get(plugin).runAsync(() -> {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
int count = fillAtLocationsSync(locations);
// Send block updates on main thread to prevent ghost blocks
Scheduler.get(plugin).run(() -> {
Bukkit.getScheduler().runTask(plugin, () -> {
BlockUpdateUtil.sendLocationsUpdate(locations);
callback.accept(count);
});

View file

@ -0,0 +1,85 @@
package dev.loki.lomines.command;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.command.admin.info.InfoCommand;
import dev.loki.lomines.command.admin.info.RegionCommands;
import dev.loki.lomines.command.admin.manage.AdminCommands;
import dev.loki.lomines.command.admin.manage.CopyCommand;
import dev.loki.lomines.command.admin.misc.MaskCommands;
import dev.loki.lomines.command.admin.misc.TeleportCommands;
import dev.loki.lomines.command.admin.stats.HologramCommands;
import dev.loki.lomines.command.admin.stats.StatsCommands;
import dev.loki.lomines.command.player.PlayerCommands;
import dev.loki.lomines.command.player.TeleportCommand;
import dev.loki.lomines.command.common.LoMinesTabCompleter;
import dev.loki.lomines.util.ErrorHandler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class LmCommand implements CommandExecutor, TabCompleter {
private final LoMinesPlugin plugin;
private final AdminCommands adminCommands;
private final InfoCommand infoCommand;
private final RegionCommands regionCommands;
private final CopyCommand copyCommand;
private final MaskCommands maskCommands;
private final TeleportCommands teleportCommands;
private final HologramCommands hologramCommands;
private final StatsCommands statsCommands;
private final PlayerCommands playerCommands;
private final TeleportCommand tpCommand;
private final LoMinesTabCompleter tabCompleter;
public LmCommand(LoMinesPlugin plugin) {
this.plugin = plugin;
this.adminCommands = new AdminCommands(plugin);
this.infoCommand = new InfoCommand(plugin);
this.regionCommands = new RegionCommands(plugin);
this.copyCommand = new CopyCommand(plugin);
this.maskCommands = new MaskCommands(plugin);
this.teleportCommands = new TeleportCommands(plugin, plugin.getMines(), new ErrorHandler(plugin.getLogger(), "[LoMines] "));
this.hologramCommands = new HologramCommands(plugin);
this.statsCommands = new StatsCommands(plugin);
this.playerCommands = new PlayerCommands(plugin);
this.tpCommand = new TeleportCommand(plugin);
this.tabCompleter = new LoMinesTabCompleter(plugin);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
sender.sendMessage("§6/lm help §f- Show help");
return true;
}
String sub = args[0].toLowerCase();
String[] rest = new String[args.length - 1];
System.arraycopy(args, 1, rest, 0, rest.length);
switch (sub) {
case "create", "delete", "edit", "reset", "reload", "list" -> adminCommands.handle(sender, sub, rest);
case "info" -> infoCommand.handle(sender, rest);
case "regions", "addregion", "removeregion" -> regionCommands.handle(sender, sub, rest);
case "copy" -> copyCommand.handle(sender, rest);
case "maskscan" -> maskCommands.handle(sender, rest);
case "setteleport", "setspawn", "clearspawn" -> teleportCommands.handle(sender, sub, rest);
case "hologram" -> hologramCommands.handle(sender, rest);
case "stats", "top" -> statsCommands.handle(sender, sub, rest);
case "tp" -> tpCommand.handle(sender, rest);
case "wand", "group", "help" -> playerCommands.handle(sender, sub, rest);
default -> sender.sendMessage("§cUnknown command: " + sub);
}
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
return tabCompleter.onTabComplete(sender, command, alias, args);
}
}

View file

@ -1,186 +0,0 @@
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.data.stats.Leaderboard;
import dev.loki.lomines.data.stats.LeaderboardEntry;
import dev.loki.lomines.data.stats.PlayerStats;
import dev.loki.lomines.data.stats.StatsManager;
import dev.loki.lomines.util.ErrorHandler;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Command;
import dev.lolib.commands.annotation.Subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Statistics commands for player mining stats and leaderboards.
* Provides commands for viewing stats and top players.
*/
@Command(value = "lm", permission = "lomines.use")
public class StatsCommands {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
public StatsCommands(LoMinesPlugin plugin) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
}
private static void sendTopRows(CommandSender sender, List<LeaderboardEntry> rows) {
if (rows.isEmpty()) {
sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY));
return;
}
for (int i = 0; i < rows.size(); i++) {
LeaderboardEntry e = rows.get(i);
String name = formatPlayerName(e.playerId());
sender.sendMessage(Component.text(
(i + 1) + ". " + name + " — " + e.count(),
NamedTextColor.WHITE));
}
}
private static String formatPlayerName(UUID id) {
String name = Bukkit.getOfflinePlayer(id).getName();
return name != null ? name : id.toString();
}
private static int clampLimit(int limit) {
return Math.max(1, Math.min(50, limit));
}
private static Integer tryParsePositiveInt(String s) {
try {
int v = Integer.parseInt(s);
return v > 0 ? v : null;
} catch (NumberFormatException e) {
return null;
}
}
/**
* Shows mining stats for self or another player.
* Usage: /lm stats [player]
*/
@Subcommand(value = "stats", permission = "lomines.stats")
public void stats(CommandSender sender, @Arg(value = "player", optional = true, defaultValue = "") String targetName) {
if (isStatsDisabled(sender)) {
return;
}
String name = targetName == null ? "" : targetName.trim();
UUID uuid;
if (name.isEmpty()) {
if (!(sender instanceof Player self)) {
errorHandler.sendError(sender, "Укажите игрока: /lm stats <ник>");
return;
}
uuid = self.getUniqueId();
} else {
if (!sender.hasPermission("lomines.stats.others")) {
errorHandler.handlePermissionDenied(sender, "смотреть чужую статистику");
return;
}
Player online = Bukkit.getPlayerExact(name);
if (online != null) {
uuid = online.getUniqueId();
} else {
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
uuid = off.getUniqueId();
}
}
StatsManager sm = plugin.getStatsManager();
PlayerStats ps = sm.getOrCreate(uuid);
String displayName = formatPlayerName(uuid);
sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD));
sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE));
int pos = sm.getLeaderboard().getPosition(uuid);
if (pos > 0) {
sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY));
}
Map<String, Long> byMine = ps.getMineStatsSnapshot();
if (byMine.isEmpty()) {
return;
}
sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY));
byMine.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(12)
.forEach(e -> sender.sendMessage(Component.text(
" " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA)));
}
/**
* Leaderboard: global or per-mine.
* Usage: /lm top — глобально 10; /lm top 25 — глобально 25; /lm top <шахта> — по шахте;
* /lm top <шахта> <лимит>
*/
@Subcommand(value = "top", permission = "lomines.stats")
public void top(
CommandSender sender,
@Arg(value = "arg1", optional = true, defaultValue = "") String arg1,
@Arg(value = "arg2", optional = true, defaultValue = "") String arg2
) {
if (isStatsDisabled(sender)) {
return;
}
String a = arg1 == null ? "" : arg1.trim();
String b = arg2 == null ? "" : arg2.trim();
int limit;
String mineName;
if (a.isEmpty()) {
limit = clampLimit(10);
mineName = null;
} else if (b.isEmpty()) {
Integer asLimit = tryParsePositiveInt(a);
if (asLimit != null) {
limit = clampLimit(asLimit);
mineName = null;
} else {
limit = clampLimit(10);
mineName = a;
}
} else {
mineName = a;
Integer limParsed = tryParsePositiveInt(b);
limit = clampLimit(limParsed != null ? limParsed : 10);
}
Leaderboard lb = plugin.getStatsManager().getLeaderboard();
if (mineName != null) {
var found = mines.find(mineName);
if (found.isEmpty()) {
errorHandler.handleNotFound(sender, "Шахта", mineName);
return;
}
Mine m = found.get();
String key = m.getName();
List<LeaderboardEntry> rows = lb.getTopByMine(key, limit);
sender.sendMessage(Component.text("=== Топ по шахте «" + key + "» ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
return;
}
List<LeaderboardEntry> rows = lb.getTopTotal(limit);
sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
}
private boolean isStatsDisabled(CommandSender sender) {
if (!plugin.getConfig().getBoolean("statistics-enabled", true)) {
sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED));
return true;
}
return false;
}
}

View file

@ -1,11 +1,9 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.info;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Subcommand;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
@ -13,9 +11,6 @@ import org.bukkit.command.CommandSender;
import java.util.List;
import java.util.Map;
/**
* Command to display detailed information about a mine.
*/
public class InfoCommand {
private final LoMinesPlugin plugin;
@ -24,12 +19,13 @@ public class InfoCommand {
this.plugin = plugin;
}
/**
* Shows detailed information about a mine.
* Usage: /lm info <mine>
*/
@Subcommand(value = "info", permission = "lomines.admin.info")
void info(CommandSender sender, @Arg("mine") String mineName) {
public void handle(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm info <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
@ -37,18 +33,13 @@ public class InfoCommand {
}
var config = mine.getConfig();
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
sender.sendMessage(Component.text("§6§lИнформация о шахте: §f" + mine.getName()));
sender.sendMessage(Component.text(""));
// Basic info
sender.sendMessage(Component.text("§7Мир: §f" + config.worldName()));
sender.sendMessage(Component.text("§7Блоков: §f" + mine.getBlocks() + "§7/§f" + mine.getTotalVolume() +
" §7(§f" + String.format("%.1f", mine.getPercentFilled()) + "%§7)"));
sender.sendMessage(Component.text(""));
// Regions
sender.sendMessage(Component.text("§6§lРегионы:"));
List<Cuboid> regions = mine.getRegions();
for (int i = 0; i < regions.size(); i++) {
@ -59,8 +50,6 @@ public class InfoCommand {
" §7(§f" + r.getVolume() + " §7блоков)"));
}
sender.sendMessage(Component.text(""));
// Blocks
sender.sendMessage(Component.text("§6§lБлоки:"));
Map<BlockKey, Double> weights = config.blocks().weights();
weights.entrySet().stream()
@ -71,26 +60,19 @@ public class InfoCommand {
if (weights.size() > 10) {
sender.sendMessage(Component.text("§7 ... и ещё " + (weights.size() - 10) + " блоков"));
}
sender.sendMessage(Component.text(""));
// Reset settings
sender.sendMessage(Component.text("§6§lНастройки сброса:"));
sender.sendMessage(Component.text("§7 Интервал: §f" + config.reset().intervalDisplay()));
sender.sendMessage(Component.text("§7 Триггер по %: §f" +
(config.reset().isPercentTriggerEnabled() ? config.reset().percentTrigger() + "%" : "выкл")));
sender.sendMessage(Component.text(""));
// Teleport/Spawn
sender.sendMessage(Component.text("§6§lТелепортация:"));
sender.sendMessage(Component.text("§7 Точка телепорта: §f" +
(config.teleport().enabled() ? "§aустановлена" : "§7не задана")));
sender.sendMessage(Component.text("§7 Точка спавна: §f" +
(config.playerSpawn().enabled() ? "§aустановлена" : "§7используется точка телепорта")));
sender.sendMessage(Component.text(""));
// Rewards
sender.sendMessage(Component.text("§6§lНаграды: §f" + config.rewards().entries().size() + " записей"));
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}

View file

@ -1,12 +1,10 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.info;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Subcommand;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
@ -15,51 +13,15 @@ import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/**
* Commands for managing mine regions (multiple spawn areas).
*/
public class RegionCommands {
public final class RegionActionHandler {
private final LoMinesPlugin plugin;
public RegionCommands(LoMinesPlugin plugin) {
public RegionActionHandler(LoMinesPlugin plugin) {
this.plugin = plugin;
}
/**
* Lists all regions in a mine.
* Usage: /lm regions <mine>
*/
@Subcommand(value = "regions", permission = "lomines.admin.regions")
void listRegions(CommandSender sender, @Arg("mine") String mineName) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
List<Cuboid> regions = mine.getRegions();
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
sender.sendMessage(Component.text("§6§lРегионы шахты §f" + mine.getName()));
sender.sendMessage(Component.text("§7Всего регионов: §f" + regions.size()));
sender.sendMessage(Component.text(""));
for (int i = 0; i < regions.size(); i++) {
Cuboid r = regions.get(i);
sender.sendMessage(Component.text("§e#" + (i + 1) + " §7(§f" + r.getVolume() + " §7блоков)"));
sender.sendMessage(Component.text(" §7Мир: §f" + r.getWorld().getName()));
sender.sendMessage(Component.text(" §7От: §f" + r.getMinX() + ", " + r.getMinY() + ", " + r.getMinZ()));
sender.sendMessage(Component.text(" §7До: §f" + r.getMaxX() + ", " + r.getMaxY() + ", " + r.getMaxZ()));
}
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
/**
* Adds a new region to a mine using current wand selection.
* Usage: /lm addregion <mine>
*/
@Subcommand(value = "addregion", permission = "lomines.admin.regions")
void addRegion(CommandSender sender, @Arg("mine") String mineName) {
public void addRegion(CommandSender sender, String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED));
return;
@ -89,11 +51,9 @@ public class RegionCommands {
return;
}
// Add new region to existing regions
List<Cuboid> currentRegions = new ArrayList<>(mine.getRegions());
currentRegions.add(newRegion);
// Update config
var newRegionConfig = new RegionConfig(currentRegions);
updateMineRegions(mine, newRegionConfig);
@ -105,14 +65,7 @@ public class RegionCommands {
session.clearCorners(slot);
}
/**
* Removes a region by index.
* Usage: /lm removeregion <mine> <index>
*/
@Subcommand(value = "removeregion", permission = "lomines.admin.regions")
void removeRegion(CommandSender sender,
@Arg("mine") String mineName,
@Arg("index") int index) {
public void removeRegion(CommandSender sender, String mineName, int index) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
@ -155,7 +108,6 @@ public class RegionCommands {
oldConfig.playerSpawn()
);
// Update and save
try {
plugin.getMines().updateMineConfig(mine.getName(), newMineConfig);
} catch (Exception e) {

View file

@ -0,0 +1,73 @@
package dev.loki.lomines.command.admin.info;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import java.util.List;
public class RegionCommands {
private final LoMinesPlugin plugin;
private final RegionActionHandler actionHandler;
public RegionCommands(LoMinesPlugin plugin) {
this.plugin = plugin;
this.actionHandler = new RegionActionHandler(plugin);
}
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "regions" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm regions <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
List<Cuboid> regions = mine.getRegions();
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
sender.sendMessage(Component.text("§6§lРегионы шахты §f" + mine.getName()));
sender.sendMessage(Component.text("§7Всего регионов: §f" + regions.size()));
sender.sendMessage(Component.text(""));
for (int i = 0; i < regions.size(); i++) {
Cuboid r = regions.get(i);
sender.sendMessage(Component.text("§e#" + (i + 1) + " §7(§f" + r.getVolume() + " §7блоков)"));
sender.sendMessage(Component.text(" §7Мир: §f" + r.getWorld().getName()));
sender.sendMessage(Component.text(" §7От: §f" + r.getMinX() + ", " + r.getMinY() + ", " + r.getMinZ()));
sender.sendMessage(Component.text(" §7До: §f" + r.getMaxX() + ", " + r.getMaxY() + ", " + r.getMaxZ()));
}
sender.sendMessage(Component.text("§8§m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
}
case "addregion" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm addregion <mine>", NamedTextColor.RED));
return;
}
actionHandler.addRegion(sender, args[0]);
}
case "removeregion" -> {
if (args.length < 2) {
sender.sendMessage(Component.text("Usage: /lm removeregion <mine> <index>", NamedTextColor.RED));
return;
}
try {
int index = Integer.parseInt(args[1]);
actionHandler.removeRegion(sender, args[0], index);
} catch (NumberFormatException e) {
sender.sendMessage(Component.text("Индекс должен быть числом: " + args[1], NamedTextColor.RED));
}
}
default -> {}
}
}
}

View file

@ -0,0 +1,80 @@
package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.util.ErrorHandler;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
import java.util.Collection;
public class AdminCommands {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
private final MineActionHandler actionHandler;
public AdminCommands(LoMinesPlugin plugin) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
this.actionHandler = new MineActionHandler(plugin, mines, errorHandler);
}
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "create" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm create <name>"));
return;
}
actionHandler.create(sender, args[0]);
}
case "delete" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm delete <name>"));
return;
}
actionHandler.delete(sender, args[0]);
}
case "edit" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm edit <mine>"));
return;
}
actionHandler.edit(sender, args[0]);
}
case "reset" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm reset <mine> [silent]"));
return;
}
boolean silent = args.length > 1 && (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("silent"));
actionHandler.reset(sender, args[0], silent);
}
case "reload" -> actionHandler.reload(sender);
case "list" -> {
Collection<Mine> allMines = mines.getAll();
if (allMines.isEmpty()) {
sender.sendMessage(Component.text("No mines found"));
return;
}
sender.sendMessage(Component.text("=== Mines (" + allMines.size() + ") ==="));
for (Mine mine : allMines) {
String status = formatMineStatus(mine);
sender.sendMessage(Component.text("- " + mine.getName() + " " + status));
}
}
default -> {}
}
}
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,17 +1,12 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Subcommand;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
/**
* Command to copy mine configuration from one mine to another.
*/
public class CopyCommand {
private final LoMinesPlugin plugin;
@ -20,15 +15,13 @@ public class CopyCommand {
this.plugin = plugin;
}
/**
* Copies mine configuration (blocks, reset settings, rewards, etc.)
* from source mine to target mine. Regions and positions are NOT copied.
* Usage: /lm copy <from> <to>
*/
@Subcommand(value = "copy", permission = "lomines.admin.copy")
void copy(CommandSender sender,
@Arg("from-mine") String fromMineName,
@Arg("to-mine") String toMineName) {
public void handle(CommandSender sender, String[] args) {
if (args.length < 2) {
sender.sendMessage(Component.text("Usage: /lm copy <from> <to>", NamedTextColor.RED));
return;
}
String fromMineName = args[0];
String toMineName = args[1];
Mine fromMine = plugin.getMines().find(fromMineName).orElse(null);
if (fromMine == null) {
@ -50,7 +43,6 @@ public class CopyCommand {
var sourceConfig = fromMine.getConfig();
var targetConfig = toMine.getConfig();
// Keep target region/name, copy all other sections from source.
MineConfig newTargetConfig = MineConfig.builder(toMine.getName())
.region(targetConfig.region())
.blocks(sourceConfig.blocks())

View file

@ -1,45 +1,31 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.manage;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.gui.mine.main.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 {
public final class MineActionHandler {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
public AdminCommands(LoMinesPlugin plugin) {
public MineActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
this.mines = mines;
this.errorHandler = errorHandler;
}
/**
* 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) {
public void create(CommandSender sender, String name) {
try {
mines.create(name);
sender.sendMessage(MessageFormatter.success("Successfully created mine: " + name));
@ -58,12 +44,7 @@ public class AdminCommands {
}
}
/**
* Deletes an existing mine.
* Usage: /lm delete <name>
*/
@Subcommand(value = "delete", permission = "lomines.admin.delete")
public void delete(CommandSender sender, @Arg("name") String name) {
public void delete(CommandSender sender, String name) {
try {
mines.delete(name);
sender.sendMessage(MessageFormatter.success("Successfully deleted mine: " + name));
@ -75,12 +56,7 @@ public class AdminCommands {
}
}
/**
* 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) {
public void edit(CommandSender sender, String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
@ -95,16 +71,7 @@ public class AdminCommands {
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
) {
public void reset(CommandSender sender, String mineName, boolean silent) {
try {
Mine mine = mines.get(mineName);
mine.reset(silent);
@ -114,11 +81,6 @@ public class AdminCommands {
}
}
/**
* 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);
@ -130,32 +92,4 @@ public class AdminCommands {
"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,23 +1,16 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.misc;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.config.ConfigLoader;
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 dev.lolib.scheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.io.IOException;
/**
* Mask commands for mine shape management.
* Provides commands for scanning and saving mask positions.
*/
@Command(value = "lm", permission = "lomines.use")
public class MaskCommands {
private final LoMinesPlugin plugin;
@ -30,9 +23,13 @@ public class MaskCommands {
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
}
@Subcommand(value = "maskscan", permission = "lomines.admin.maskscan")
public void maskScan(CommandSender sender, @Arg("mine") String name) {
Scheduler.get(plugin).run(() -> {
public void handle(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(MessageFormatter.error("Usage: /lm maskscan <mine>"));
return;
}
String name = args[0];
Bukkit.getScheduler().runTask(plugin, () -> {
try {
int n = mines.scanAndSaveMask(name);
sender.sendMessage(MessageFormatter.success(

View file

@ -1,42 +1,32 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.misc;
import dev.loki.lomines.LoMinesPlugin;
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.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.util.MessageFormatter;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Commands for managing teleport and spawn locations for mines.
*/
class TeleportCommands {
public final class TeleportActionHandler {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
public TeleportActionHandler(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
this.plugin = plugin;
this.mines = mines;
this.errorHandler = errorHandler;
}
/**
* Sets the teleport location for players when mine resets.
* Usage: /lm setteleport <mine>
*/
@Subcommand(value = "setteleport", permission = "lomines.admin.setteleport")
void setTeleport(CommandSender sender, @Arg("mine") String mineName) {
public void setTeleport(CommandSender sender, String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
@ -72,12 +62,7 @@ class TeleportCommands {
}
}
/**
* Sets the safe spawn location for stuck players after mine reset.
* Usage: /lm setspawn <mine>
*/
@Subcommand(value = "setspawn", permission = "lomines.admin.setspawn")
void setSpawn(CommandSender sender, @Arg("mine") String mineName) {
public void setSpawn(CommandSender sender, String mineName) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("This command can only be used by players!", NamedTextColor.RED));
return;
@ -114,12 +99,7 @@ class TeleportCommands {
}
}
/**
* Clears the spawn location for stuck players.
* Usage: /lm clearspawn <mine>
*/
@Subcommand(value = "clearspawn", permission = "lomines.admin.setspawn")
void clearSpawn(CommandSender sender, @Arg("mine") String mineName) {
public void clearSpawn(CommandSender sender, String mineName) {
try {
Mine mine = mines.find(mineName).orElse(null);
if (mine == null) {
@ -148,7 +128,7 @@ class TeleportCommands {
}
}
private String formatLocation(Location loc) {
public String formatLocation(Location loc) {
return String.format("%.1f, %.1f, %.1f in %s",
loc.getX(), loc.getY(), loc.getZ(), loc.getWorld().getName());
}

View file

@ -0,0 +1,30 @@
package dev.loki.lomines.command.admin.misc;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.util.ErrorHandler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public class TeleportCommands {
private final TeleportActionHandler actionHandler;
public TeleportCommands(LoMinesPlugin plugin, Mines mines, ErrorHandler errorHandler) {
this.actionHandler = new TeleportActionHandler(plugin, mines, errorHandler);
}
public void handle(CommandSender sender, String subcommand, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm " + subcommand + " <mine>", NamedTextColor.RED));
return;
}
switch (subcommand.toLowerCase()) {
case "setteleport" -> actionHandler.setTeleport(sender, args[0]);
case "setspawn" -> actionHandler.setSpawn(sender, args[0]);
case "clearspawn" -> actionHandler.clearSpawn(sender, args[0]);
default -> {}
}
}
}

View file

@ -1,17 +1,12 @@
package dev.loki.lomines.command.admin;
package dev.loki.lomines.command.admin.stats;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.integration.hologram.HologramManager;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
/**
* Commands for managing holograms above mines.
*/
public class HologramCommands {
private final LoMinesPlugin plugin;
@ -22,13 +17,14 @@ public class HologramCommands {
this.hologramManager = plugin.getHologramManager();
}
/**
* Toggles hologram for a mine.
* Usage: /lm hologram <mine> [on|off]
*/
@Subcommand(value = "hologram", permission = "lomines.admin.hologram")
void hologram(CommandSender sender, @Arg("mine") String mineName,
@Arg(value = "state", optional = true) String state) {
public void handle(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm hologram <mine> [on|off]", NamedTextColor.RED));
return;
}
String mineName = args[0];
String state = args.length > 1 ? args[1] : null;
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
@ -42,10 +38,9 @@ public class HologramCommands {
boolean enable;
if (state == null) {
// Toggle
enable = hologramManager.toggleHologram(mine);
} else {
enable = state.equalsIgnoreCase("on") || state.equalsIgnoreCase("true") || state.equalsIgnoreCase("enable");
enable = "on".equalsIgnoreCase(state) || "true".equalsIgnoreCase(state) || "enable".equalsIgnoreCase(state);
if (enable) {
hologramManager.createMineHologram(mine);
} else {

View file

@ -0,0 +1,119 @@
package dev.loki.lomines.command.admin.stats;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.stats.model.Leaderboard;
import dev.loki.lomines.data.stats.model.LeaderboardEntry;
import dev.loki.lomines.util.ErrorHandler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.UUID;
public final class LeaderboardRenderer {
private LeaderboardRenderer() {
}
public static void sendTopRows(CommandSender sender, List<LeaderboardEntry> rows) {
if (rows.isEmpty()) {
sender.sendMessage(Component.text("Пока нет данных.", NamedTextColor.GRAY));
return;
}
for (int i = 0; i < rows.size(); i++) {
LeaderboardEntry e = rows.get(i);
String name = formatPlayerName(e.playerId());
sender.sendMessage(Component.text(
(i + 1) + ". " + name + " \u2014 " + e.count(),
NamedTextColor.WHITE));
}
}
public static String formatPlayerName(UUID id) {
String name = Bukkit.getOfflinePlayer(id).getName();
return name != null ? name : id.toString();
}
public static int clampLimit(int limit) {
return Math.max(1, Math.min(50, limit));
}
public static Integer tryParsePositiveInt(String s) {
try {
int v = Integer.parseInt(s);
return v > 0 ? v : null;
} catch (NumberFormatException e) {
return null;
}
}
public static UUID resolvePlayerUuid(CommandSender sender, String targetName, ErrorHandler errorHandler) {
String name = targetName == null ? "" : targetName.trim();
if (name.isEmpty()) {
if (!(sender instanceof Player self)) {
errorHandler.sendError(sender, "Укажите игрока: /lm stats <ник>");
return null;
}
return self.getUniqueId();
} else {
if (!sender.hasPermission("lomines.stats.others")) {
errorHandler.handlePermissionDenied(sender, "смотреть чужую статистику");
return null;
}
Player online = Bukkit.getPlayerExact(name);
if (online != null) {
return online.getUniqueId();
} else {
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
return off.getUniqueId();
}
}
}
public static void renderTopCommand(CommandSender sender, String arg1, String arg2, Mines mines, LoMinesPlugin plugin) {
String a = arg1 == null ? "" : arg1.trim();
String b = arg2 == null ? "" : arg2.trim();
int limit;
String mineName;
if (a.isEmpty()) {
limit = clampLimit(10);
mineName = null;
} else if (b.isEmpty()) {
Integer asLimit = tryParsePositiveInt(a);
if (asLimit != null) {
limit = clampLimit(asLimit);
mineName = null;
} else {
limit = clampLimit(10);
mineName = a;
}
} else {
mineName = a;
Integer limParsed = tryParsePositiveInt(b);
limit = clampLimit(limParsed != null ? limParsed : 10);
}
Leaderboard lb = plugin.getStatsManager().getLeaderboard();
if (mineName != null) {
var found = mines.find(mineName);
if (found.isEmpty()) {
sender.sendMessage(Component.text("Шахта не найдена: " + mineName, NamedTextColor.RED));
return;
}
Mine m = found.get();
String key = m.getName();
List<LeaderboardEntry> rows = lb.getTopByMine(key, limit);
sender.sendMessage(Component.text("=== Топ по шахте \u00ab" + key + "\u00bb ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
return;
}
List<LeaderboardEntry> rows = lb.getTopTotal(limit);
sender.sendMessage(Component.text("=== Общий топ ===", NamedTextColor.GOLD));
sendTopRows(sender, rows);
}
}

View file

@ -0,0 +1,78 @@
package dev.loki.lomines.command.admin.stats;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.data.stats.model.PlayerStats;
import dev.loki.lomines.data.stats.service.StatsManager;
import dev.loki.lomines.util.ErrorHandler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import java.util.Map;
import java.util.UUID;
public class StatsCommands {
private final LoMinesPlugin plugin;
private final Mines mines;
private final ErrorHandler errorHandler;
public StatsCommands(LoMinesPlugin plugin) {
this.plugin = plugin;
this.mines = plugin.getMines();
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
}
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "stats" -> {
if (isStatsDisabled(sender)) {
return;
}
String targetName = args.length > 0 ? args[0] : "";
UUID uuid = LeaderboardRenderer.resolvePlayerUuid(sender, targetName, errorHandler);
if (uuid == null) {
return;
}
StatsManager sm = plugin.getStatsManager();
PlayerStats ps = sm.getOrCreate(uuid);
String displayName = LeaderboardRenderer.formatPlayerName(uuid);
sender.sendMessage(Component.text("=== Статистика: " + displayName + " ===", NamedTextColor.GOLD));
sender.sendMessage(Component.text("Всего блоков: " + ps.getTotalBlocks(), NamedTextColor.WHITE));
int pos = sm.getLeaderboard().getPosition(uuid);
if (pos > 0) {
sender.sendMessage(Component.text("Место в общем топе: #" + pos, NamedTextColor.GRAY));
}
Map<String, Long> byMine = ps.getMineStatsSnapshot();
if (byMine.isEmpty()) {
return;
}
sender.sendMessage(Component.text("По шахтам:", NamedTextColor.GRAY));
byMine.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(12)
.forEach(e -> sender.sendMessage(Component.text(
" " + e.getKey() + ": " + e.getValue(), NamedTextColor.DARK_AQUA)));
}
case "top" -> {
if (isStatsDisabled(sender)) {
return;
}
String arg1 = args.length > 0 ? args[0] : "";
String arg2 = args.length > 1 ? args[1] : "";
LeaderboardRenderer.renderTopCommand(sender, arg1, arg2, mines, plugin);
}
default -> {}
}
}
private boolean isStatsDisabled(CommandSender sender) {
if (!plugin.getConfig().getBoolean("statistics-enabled", true)) {
sender.sendMessage(Component.text("Статистика отключена в config.yml.", NamedTextColor.RED));
return true;
}
return false;
}
}

View file

@ -4,16 +4,10 @@ import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Tab completer for LoMines commands.
* Provides auto-completion suggestions for all subcommands and arguments.
*/
public class LoMinesTabCompleter implements TabCompleter {
private static final List<String> ALL_COMMANDS = List.of(
@ -21,12 +15,15 @@ public class LoMinesTabCompleter implements TabCompleter {
"stats", "top", "maskscan", "edit", "setteleport", "setspawn",
"clearspawn", "info", "tp", "copy", "regions", "addregion", "removeregion", "help"
);
private static final List<String> BOOLEAN_VALUES = List.of("true", "false");
private static final List<String> TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50");
private final LoMinesPlugin plugin;
private final SubcommandCompleter completer;
private final PermissionPredicate permission;
public LoMinesTabCompleter(LoMinesPlugin plugin) {
this.plugin = plugin;
this.completer = new SubcommandCompleter(plugin);
this.permission = new PermissionPredicate();
}
@Override
@ -39,169 +36,27 @@ public class LoMinesTabCompleter implements TabCompleter {
return switch (args.length) {
case 1 -> completeSubcommands(sender, partial);
case 2 -> completeSecondArg(sender, args[0].toLowerCase(), partial);
case 3 -> completeThirdArg(args[0].toLowerCase(), args[1], partial);
case 2 -> completer.completeSecondArg(sender, args[0].toLowerCase(), partial);
case 3 -> completer.completeThirdArg(args[0].toLowerCase(), args[1], partial);
default -> new ArrayList<>();
};
}
private boolean isLoMinesCommand(String name) {
return name.equalsIgnoreCase("lm") ||
name.equalsIgnoreCase("lomines") ||
name.equalsIgnoreCase("mine") ||
name.equalsIgnoreCase("mines");
return "lm".equalsIgnoreCase(name) ||
"lomines".equalsIgnoreCase(name) ||
"mine".equalsIgnoreCase(name) ||
"mines".equalsIgnoreCase(name);
}
private List<String> completeSubcommands(CommandSender sender, String partial) {
List<String> completions = new ArrayList<>();
for (String cmd : ALL_COMMANDS) {
if (cmd.startsWith(partial) && hasPermission(sender, cmd)) {
if (cmd.startsWith(partial) && permission.hasPermission(sender, cmd)) {
completions.add(cmd);
}
}
return completions;
}
private List<String> completeSecondArg(CommandSender sender, String subcommand, String partial) {
return switch (subcommand) {
case "create" -> completeCreate(partial);
case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info",
"regions", "addregion" -> filterStartsWith(getMineNames(), partial);
case "reset" -> completeReset(partial);
case "stats" -> completeStats(sender, partial);
case "top" -> completeTopFirstArg(partial);
case "tp" -> completeTeleport(sender, partial);
case "copy" -> completeCopyFirstArg(partial);
case "removeregion" -> completeRemoveRegion(sender, partial);
default -> new ArrayList<>();
};
}
private List<String> completeThirdArg(String subcommand, String arg2, String partial) {
return switch (subcommand) {
case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial);
case "top" -> completeTopSecondArg(arg2, partial);
case "copy" -> filterStartsWith(getMineNames(), partial);
case "removeregion" -> completeRegionIndex(arg2, partial);
default -> new ArrayList<>();
};
}
private List<String> completeRegionIndex(String mineName, String partial) {
var mine = plugin.getMines().find(mineName);
if (mine.isEmpty()) {
return new ArrayList<>();
}
int regionCount = mine.get().getRegions().size();
List<String> indices = new ArrayList<>();
for (int i = 1; i <= regionCount; i++) {
indices.add(String.valueOf(i));
}
return filterStartsWith(indices, partial);
}
private List<String> completeCreate(String partial) {
if (partial.isEmpty()) {
return new ArrayList<>(List.of("<name>"));
}
return new ArrayList<>();
}
private List<String> completeReset(String partial) {
List<String> mines = filterStartsWith(getMineNames(), partial);
if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) {
mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial));
}
return mines;
}
private List<String> completeStats(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.stats.others")) {
return new ArrayList<>();
}
return filterStartsWith(getOnlinePlayerNames(), partial);
}
private List<String> completeTopFirstArg(String partial) {
List<String> completions = new ArrayList<>();
completions.addAll(filterStartsWith(getMineNames(), partial));
if (isNumericPartial(partial)) {
completions.addAll(filterStartsWith(TOP_LIMITS, partial));
}
return completions;
}
private List<String> completeTopSecondArg(String firstArg, String partial) {
boolean firstArgWasMine = plugin.getMines().find(firstArg).isPresent();
if (firstArgWasMine && isNumericPartial(partial)) {
return filterStartsWith(TOP_LIMITS, partial);
}
if (!firstArgWasMine && isNumericPartial(firstArg)) {
return filterStartsWith(getMineNames(), partial);
}
return new ArrayList<>();
}
private List<String> completeTeleport(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.teleport")) {
return new ArrayList<>();
}
return filterStartsWith(getMineNames(), partial);
}
private List<String> completeCopyFirstArg(String partial) {
return filterStartsWith(getMineNames(), partial);
}
private List<String> completeRemoveRegion(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.admin.regions")) {
return new ArrayList<>();
}
return filterStartsWith(getMineNames(), partial);
}
private boolean isNumericPartial(String partial) {
return partial.isEmpty() || partial.matches("\\d*");
}
private List<String> filterStartsWith(List<String> options, String partial) {
return options.stream()
.filter(s -> s.toLowerCase().startsWith(partial))
.collect(Collectors.toList());
}
private List<String> getMineNames() {
return plugin.getMines().getAll().stream()
.map(mine -> mine.getName())
.collect(Collectors.toList());
}
private List<String> getOnlinePlayerNames() {
return plugin.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList());
}
private boolean hasPermission(CommandSender sender, String cmd) {
return switch (cmd) {
case "create", "delete", "reload", "list", "maskscan", "edit", "info" ->
sender.hasPermission("lomines.admin");
case "reset" -> sender.hasPermission("lomines.admin.reset");
case "setteleport" -> sender.hasPermission("lomines.admin.setteleport");
case "setspawn", "clearspawn" -> sender.hasPermission("lomines.admin.setspawn");
case "wand", "group" -> sender.hasPermission("lomines.admin.wand");
case "stats", "top" -> sender.hasPermission("lomines.stats");
case "tp" -> sender.hasPermission("lomines.teleport");
case "copy" -> sender.hasPermission("lomines.admin.copy");
case "regions", "addregion", "removeregion" -> sender.hasPermission("lomines.admin.regions");
case "help" -> sender.hasPermission("lomines.use");
default -> true;
};
}
}

View file

@ -0,0 +1,23 @@
package dev.loki.lomines.command.common;
import org.bukkit.command.CommandSender;
public final class PermissionPredicate {
public boolean hasPermission(CommandSender sender, String cmd) {
return switch (cmd) {
case "create", "delete", "reload", "list", "maskscan", "edit", "info" ->
sender.hasPermission("lomines.admin");
case "reset" -> sender.hasPermission("lomines.admin.reset");
case "setteleport" -> sender.hasPermission("lomines.admin.setteleport");
case "setspawn", "clearspawn" -> sender.hasPermission("lomines.admin.setspawn");
case "wand", "group" -> sender.hasPermission("lomines.admin.wand");
case "stats", "top" -> sender.hasPermission("lomines.stats");
case "tp" -> sender.hasPermission("lomines.teleport");
case "copy" -> sender.hasPermission("lomines.admin.copy");
case "regions", "addregion", "removeregion" -> sender.hasPermission("lomines.admin.regions");
case "help" -> sender.hasPermission("lomines.use");
default -> true;
};
}
}

View file

@ -0,0 +1,130 @@
package dev.loki.lomines.command.common;
import dev.loki.lomines.LoMinesPlugin;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public final class SubcommandCompleter {
private static final List<String> BOOLEAN_VALUES = List.of("true", "false");
private static final List<String> TOP_LIMITS = List.of("5", "10", "15", "20", "25", "50");
private final LoMinesPlugin plugin;
public SubcommandCompleter(LoMinesPlugin plugin) {
this.plugin = plugin;
}
public List<String> completeSecondArg(CommandSender sender, String subcommand, String partial) {
return switch (subcommand) {
case "create" -> completeCreate(partial);
case "delete", "maskscan", "edit", "setteleport", "setspawn", "clearspawn", "info",
"regions", "addregion" -> filterStartsWith(getMineNames(), partial);
case "reset" -> completeReset(partial);
case "stats" -> completeStats(sender, partial);
case "top" -> completeTopFirstArg(partial);
case "tp" -> completeTeleport(sender, partial);
case "copy" -> filterStartsWith(getMineNames(), partial);
case "removeregion" -> filterStartsWith(getMineNames(), partial);
default -> new ArrayList<>();
};
}
public List<String> completeThirdArg(String subcommand, String arg2, String partial) {
return switch (subcommand) {
case "reset" -> filterStartsWith(BOOLEAN_VALUES, partial);
case "top" -> completeTopSecondArg(arg2, partial);
case "copy" -> filterStartsWith(getMineNames(), partial);
case "removeregion" -> completeRegionIndex(arg2, partial);
default -> new ArrayList<>();
};
}
private List<String> completeRegionIndex(String mineName, String partial) {
var mine = plugin.getMines().find(mineName);
if (mine.isEmpty()) {
return new ArrayList<>();
}
int regionCount = mine.get().getRegions().size();
List<String> indices = new ArrayList<>();
for (int i = 1; i <= regionCount; i++) {
indices.add(String.valueOf(i));
}
return filterStartsWith(indices, partial);
}
private List<String> completeCreate(String partial) {
if (partial.isEmpty()) {
return new ArrayList<>(List.of("<name>"));
}
return new ArrayList<>();
}
private List<String> completeReset(String partial) {
List<String> mines = filterStartsWith(getMineNames(), partial);
if (partial.isEmpty() || "true".startsWith(partial) || "false".startsWith(partial)) {
mines.addAll(filterStartsWith(BOOLEAN_VALUES, partial));
}
return mines;
}
private List<String> completeStats(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.stats.others")) {
return new ArrayList<>();
}
return filterStartsWith(getOnlinePlayerNames(), partial);
}
private List<String> completeTopFirstArg(String partial) {
List<String> completions = new ArrayList<>();
completions.addAll(filterStartsWith(getMineNames(), partial));
if (isNumericPartial(partial)) {
completions.addAll(filterStartsWith(TOP_LIMITS, partial));
}
return completions;
}
private List<String> completeTopSecondArg(String firstArg, String partial) {
boolean firstArgWasMine = plugin.getMines().find(firstArg).isPresent();
if (firstArgWasMine && isNumericPartial(partial)) {
return filterStartsWith(TOP_LIMITS, partial);
}
if (!firstArgWasMine && isNumericPartial(firstArg)) {
return filterStartsWith(getMineNames(), partial);
}
return new ArrayList<>();
}
private List<String> completeTeleport(CommandSender sender, String partial) {
if (!sender.hasPermission("lomines.teleport")) {
return new ArrayList<>();
}
return filterStartsWith(getMineNames(), partial);
}
private boolean isNumericPartial(String partial) {
return partial.isEmpty() || partial.matches("\\d*");
}
public List<String> filterStartsWith(List<String> options, String partial) {
return options.stream()
.filter(s -> s.toLowerCase().startsWith(partial))
.collect(Collectors.toList());
}
public List<String> getMineNames() {
return plugin.getMines().getAll().stream()
.map(mine -> mine.getName())
.collect(Collectors.toList());
}
private List<String> getOnlinePlayerNames() {
return plugin.getServer().getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList());
}
}

View file

@ -3,19 +3,11 @@ package dev.loki.lomines.command.player;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.util.ErrorHandler;
import dev.loki.lomines.wand.group.GroupWandItem;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Command;
import dev.lolib.commands.annotation.Subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Player commands for mine interaction.
* Provides commands for wand and group management.
*/
@Command(value = "lm", permission = "lomines.use")
public class PlayerCommands {
private final LoMinesPlugin plugin;
@ -26,26 +18,32 @@ public class PlayerCommands {
this.errorHandler = new ErrorHandler(plugin.getLogger(), "[LoMines] ");
}
@Subcommand(value = "wand", permission = "lomines.admin.wand")
public void wand(CommandSender sender) {
if (!(sender instanceof Player player)) {
errorHandler.sendError(sender, "Только для игрока.");
return;
public void handle(CommandSender sender, String subcommand, String[] args) {
switch (subcommand.toLowerCase()) {
case "wand" -> {
if (!(sender instanceof Player player)) {
errorHandler.sendError(sender, "Только для игрока.");
return;
}
player.getInventory().addItem(GroupWandItem.create(plugin));
player.sendMessage(Component.text(
"Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>",
NamedTextColor.GREEN));
}
case "group" -> {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm group <prefix>", NamedTextColor.RED));
return;
}
if (!(sender instanceof Player player)) {
errorHandler.sendError(sender, "Префикс задаётся в игре.");
return;
}
plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(args[0]);
String p = args[0].trim();
player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA));
}
default -> {}
}
player.getInventory().addItem(GroupWandItem.create(plugin));
player.sendMessage(Component.text(
"Палочка группы: ЛКМ/ПКМ по блоку — углы, Shift+ПКМ — меню (9 шахт). Задайте /lm group <префикс>",
NamedTextColor.GREEN));
}
@Subcommand(value = "group", permission = "lomines.admin.wand")
public void group(CommandSender sender, @Arg("prefix") String prefix) {
if (!(sender instanceof Player player)) {
errorHandler.sendError(sender, "Префикс задаётся в игре.");
return;
}
plugin.getGroupWandManager().getSession(player.getUniqueId()).setBaseName(prefix);
String p = prefix.trim();
player.sendMessage(Component.text("Префикс: " + p + " → файлы " + p + "_1.yml … " + p + "_9.yml", NamedTextColor.AQUA));
}
}

View file

@ -1,19 +1,14 @@
package dev.loki.lomines.command.player;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.commands.annotation.Arg;
import dev.lolib.commands.annotation.Subcommand;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.util.location.geo.Cuboid;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Command to teleport to a mine.
*/
public class TeleportCommand {
private final LoMinesPlugin plugin;
@ -22,12 +17,13 @@ public class TeleportCommand {
this.plugin = plugin;
}
/**
* Teleports player to the mine's location.
* Usage: /lm tp <mine>
*/
@Subcommand(value = "tp", permission = "lomines.teleport")
void teleport(CommandSender sender, @Arg("mine") String mineName) {
public void handle(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /lm tp <mine>", NamedTextColor.RED));
return;
}
String mineName = args[0];
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED));
return;
@ -42,11 +38,9 @@ public class TeleportCommand {
var config = mine.getConfig();
Location target;
// Try teleport location first
if (config.teleport().enabled() && config.teleport().getLocation().isPresent()) {
target = config.teleport().getLocation().get();
} else if (!mine.getRegions().isEmpty()) {
// Use center of first region
Cuboid region = mine.getRegions().get(0);
double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5;
double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5;

View file

@ -1,32 +1,28 @@
package dev.loki.lomines.core.mine;
package dev.loki.lomines.core.mine.model;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.block.BlockSetter;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.handler.ActionBarHandler;
import dev.loki.lomines.handler.MineBlockHandler;
import dev.loki.lomines.handler.MineResetHandler;
import dev.loki.lomines.handler.ui.ActionBarHandler;
import dev.loki.lomines.handler.block.MineBlockHandler;
import dev.loki.lomines.handler.reset.MineResetHandler;
import dev.loki.lomines.util.location.BlockKeys;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.geo.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* Represents a single mine with its configuration and state.
* Thread-safe implementation using AtomicInteger for mutable state.
* Updated for new section-based configuration (v2).
*/
public final class Mine {
private final String name;
@ -34,14 +30,12 @@ public final class Mine {
private final MineConfig config;
private final BlockSetter blockSetter;
private final LoMinesPlugin plugin;
private final int totalVolume;
private final Set<String> maskBlockKeys;
private final AtomicInteger blocks;
private final AtomicInteger ticks;
private final MineState state;
private final MineResetHandler resetHandler;
private final MineBlockHandler blockHandler;
private final ActionBarHandler actionBarHandler;
private ScheduledTask actionBarTask;
private BukkitTask actionBarTask;
private final Set<String> maskBlockKeys;
public Mine(String name, MineConfig config, LoMinesPlugin plugin) {
this.name = name;
@ -50,30 +44,21 @@ public final class Mine {
this.regions = config.region().regions();
this.blockSetter = createBlockSetter(config, plugin);
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.state = new MineState(config);
this.resetHandler = new MineResetHandler(this, plugin);
this.blockHandler = new MineBlockHandler(this, plugin);
this.actionBarHandler = new ActionBarHandler(this);
}
private Set<String> initMaskBlockKeys(MineConfig config) {
if (config.blocks().fillMode() != FillMode.MASK) {
if (config.blocks().fillMode() != FillMode.MASK || config.blocks().mask() == null) {
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;
return config.blocks().mask().positions().keySet().stream()
.map(LocationParser::parseSimple)
.filter(Objects::nonNull)
.map(BlockKeys::key)
.collect(Collectors.toSet());
}
public void start() {
@ -100,18 +85,15 @@ public final class Mine {
}
public boolean appliesToBlock(Location location) {
if (!contains(location)) return false;
if (config.blocks().fillMode() != FillMode.MASK) return true;
return maskBlockKeys.contains(BlockKeys.key(location));
return contains(location) && (config.blocks().fillMode() != FillMode.MASK || maskBlockKeys.contains(BlockKeys.key(location)));
}
public int getTotalVolume() {
return totalVolume;
return state.getTotalVolume();
}
public double getPercentFilled() {
if (totalVolume == 0) return 0.0;
return (double) blocks.get() / totalVolume * 100.0;
return state.getPercentFilled();
}
private BlockSetter createBlockSetter(MineConfig config, LoMinesPlugin plugin) {
@ -121,19 +103,16 @@ public final class Mine {
}
BlockKey firstKey = weights.keySet().iterator().next();
return switch (firstKey) {
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.Oraxen oraxen -> throw new IllegalArgumentException("External plugins disabled");
case BlockKey.ItemsAdder itemsAdder -> throw new IllegalArgumentException("External plugins disabled");
case BlockKey.Vanilla vanilla -> new dev.loki.lomines.block.BukkitBlockSetter(weights, plugin);
};
}
private void startActionBarTask() {
if (!config.ui().actionBarEnabled()) return;
actionBarTask = Scheduler.get(plugin).runTimer(() -> {
actionBarHandler.sendToNearbyPlayers();
}, 10L, 10L);
if (config.ui().actionBarEnabled()) {
actionBarTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> actionBarHandler.sendToNearbyPlayers(), 10L, 10L);
}
}
public String getName() {
@ -152,19 +131,19 @@ public final class Mine {
return regions;
}
public AtomicInteger getBlocksAtomic() {
return blocks;
}
public int getBlocks() {
return blocks.get();
return state.getBlocks();
}
public int getTicks() {
return ticks.get();
return state.getTicks();
}
public AtomicInteger getBlocksAtomic() {
return state.getBlocksAtomic();
}
public AtomicInteger getTicksAtomic() {
return ticks;
return state.getTicksAtomic();
}
}

View file

@ -0,0 +1,82 @@
package dev.loki.lomines.core.mine.model;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.util.location.BlockKeys;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public final class MineState {
private final AtomicInteger blocks;
private final AtomicInteger ticks;
private final int totalVolume;
private final Set<String> maskBlockKeys;
public MineState(MineConfig config) {
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);
}
private static 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 int getTotalVolume() {
return totalVolume;
}
public double getPercentFilled() {
if (totalVolume == 0) {
return 0.0;
}
return (double) blocks.get() / totalVolume * 100.0;
}
public AtomicInteger getBlocksAtomic() {
return blocks;
}
public int getBlocks() {
return blocks.get();
}
public int getTicks() {
return ticks.get();
}
public AtomicInteger getTicksAtomic() {
return ticks;
}
public Set<String> getMaskBlockKeys() {
return maskBlockKeys;
}
public boolean appliesToBlock(Location location) {
if (maskBlockKeys.isEmpty()) {
return true;
}
return maskBlockKeys.contains(BlockKeys.key(location));
}
}

View file

@ -0,0 +1,33 @@
package dev.loki.lomines.core.mine.registry;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.service.MineRepository;
import org.bukkit.Location;
import java.util.Collection;
import java.util.Optional;
public final class MineFinder {
private final MineRepository repository;
public MineFinder(MineRepository repository) {
this.repository = repository;
}
public Mine get(String name) {
return repository.get(name);
}
public Optional<Mine> find(String name) {
return repository.find(name);
}
public Collection<Mine> getAll() {
return repository.getAll();
}
public Optional<Mine> findByLocation(Location location) {
return repository.findByLocation(location);
}
}

View file

@ -1,22 +1,20 @@
package dev.loki.lomines.core.mine;
package dev.loki.lomines.core.mine.registry;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.service.MaskScanService;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.service.MineLoader;
import dev.loki.lomines.core.service.mask.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.MineConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardRegionService;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.integration.worldguard.region.WorldGuardRegionService;
import org.bukkit.Location;
import java.io.IOException;
import java.util.Collection;
import java.util.Optional;
/**
* Registry for all active mines.
* Manages the lifecycle of mines including loading, creating, and deleting.
*/
public final class Mines {
private final LoMinesPlugin plugin;
@ -25,6 +23,7 @@ public final class Mines {
private final MineLoader loader;
private final MaskScanService maskScanService;
private final WorldGuardRegionService worldGuardService;
private final MineFinder finder;
public Mines(LoMinesPlugin plugin) {
this.plugin = plugin;
@ -33,6 +32,7 @@ public final class Mines {
this.loader = new MineLoader(plugin, fileManager, repository);
this.maskScanService = new MaskScanService(repository, fileManager);
this.worldGuardService = new WorldGuardRegionService(plugin);
this.finder = new MineFinder(repository);
}
public void loadAll() throws IOException {
@ -63,7 +63,7 @@ public final class Mines {
worldGuardService.createRegion(name, config);
repository.createAndStart(name, config);
plugin.loLogger().info("Created mine: " + name);
plugin.getLogger().info("Created mine: " + name);
}
public void delete(String name) throws IOException {
@ -78,62 +78,56 @@ public final class Mines {
}
public Mine get(String name) {
return repository.get(name);
return finder.get(name);
}
public Optional<Mine> find(String name) {
return repository.find(name);
return finder.find(name);
}
public Collection<Mine> getAll() {
return repository.getAll();
return finder.getAll();
}
public Optional<Mine> findByLocation(Location location) {
return repository.findByLocation(location);
return finder.findByLocation(location);
}
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)");
plugin.getLogger().info("Mask scan for '" + mineName + "': " + count + " marker block(s)");
return count;
}
public void reloadMine(String name) throws IOException, ConfigLoader.ConfigLoadException {
Mine oldMine = repository.find(name).orElse(null);
Mine oldMine = finder.find(name).orElse(null);
repository.reload(name);
if (oldMine != null) {
Mine newMine = repository.find(name).orElse(null);
Mine newMine = finder.find(name).orElse(null);
if (newMine != null) {
worldGuardService.updateRegion(name, newMine.getConfig());
}
}
}
/**
* Updates mine configuration and saves to disk.
* Used for modifying regions, blocks, etc.
*/
public void updateMineConfig(String name, MineConfig newConfig) throws IOException {
if (!repository.exists(name)) {
throw new IllegalArgumentException("Mine not found: " + name);
}
// Save new config to file
try {
fileManager.saveConfig(name, newConfig);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save mine config: " + e.getMessage(), e);
}
// Reload to apply changes
try {
Mine oldMine = repository.find(name).orElse(null);
Mine oldMine = finder.find(name).orElse(null);
repository.reload(name);
if (oldMine != null) {
Mine newMine = repository.find(name).orElse(null);
Mine newMine = finder.find(name).orElse(null);
if (newMine != null) {
worldGuardService.updateRegion(name, newMine.getConfig());
}

View file

@ -1,10 +1,10 @@
package dev.loki.lomines.core.mine;
package dev.loki.lomines.core.mine.service;
import dev.loki.lomines.LoMinesPlugin;
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.MineConfig;
import dev.loki.lomines.data.config.model.MineConfig;
import java.io.IOException;
import java.nio.file.Files;
@ -49,9 +49,9 @@ public final class MineLoader {
MineConfig config = fileManager.loadConfig(mineName);
repository.createAndStart(mineName, config);
plugin.loLogger().info("Loaded mine: " + mineName);
plugin.getLogger().info("Loaded mine: " + mineName);
} catch (IOException | ConfigLoader.ConfigLoadException e) {
plugin.loLogger().error("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage());
plugin.getLogger().severe("Failed to load mine from " + configFile.getFileName() + ": " + e.getMessage());
}
}
}

View file

@ -1,8 +1,10 @@
package dev.loki.lomines.core.mine;
package dev.loki.lomines.core.mine.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.core.mine.registry.Mines;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
/**
* Centralized ticker for all mines.
@ -13,7 +15,7 @@ public final class MineTicker {
private final Mines mines;
private final LoMinesPlugin plugin;
private ScheduledTask tickerTask;
private BukkitTask tickerTask;
public MineTicker(Mines mines, LoMinesPlugin plugin) {
this.mines = mines;
@ -29,7 +31,7 @@ public final class MineTicker {
return; // Already running
}
tickerTask = Scheduler.get(plugin).runTimer(() -> {
tickerTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
for (Mine mine : mines.getAll()) {
tickMine(mine);
}

View file

@ -1,38 +1,25 @@
package dev.loki.lomines.core.service;
import dev.loki.lomines.core.service.write.MineConfigWriter;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
/**
* Handles file operations for mine configurations.
* Updated for new section-based configuration (v2).
*/
public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
public final class MineFileManager {
private final Path minesFolder;
private final ConfigLoader configLoader;
private final MineConfigWriter configWriter;
public MineFileManager(Path minesFolder) {
this(minesFolder, new ConfigLoader(minesFolder.getParent()));
this.minesFolder = minesFolder;
this.configLoader = new ConfigLoader(minesFolder.getParent());
this.configWriter = new MineConfigWriter(minesFolder, configLoader);
}
public Path getMinesFolder() {
@ -45,86 +32,9 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
}
}
/**
* Creates a default mine configuration with new format.
* WorldGuard region settings are loaded from defaults.yml.
*/
public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException {
ensureFolderExists();
// Create default region
Location loc1 = corner1 != null ? corner1 : new Location(
org.bukkit.Bukkit.getWorlds().get(0), 0, 64, 0);
Location loc2 = corner2 != null ? corner2 : new Location(
org.bukkit.Bukkit.getWorlds().get(0), 10, 74, 10);
Cuboid cuboid = new Cuboid(loc1, loc2);
RegionConfig region = RegionConfig.single(cuboid);
// Create default blocks (stone only)
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 1.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
// Load WorldGuard defaults from _defaults.yml
WorldGuardConfig wgConfig = loadWorldGuardDefaults();
// Build mine config with defaults
MineConfig config = MineConfig.builder(name)
.region(region)
.blocks(blocks)
.reset(ResetConfig.defaults())
.rewards(RewardConfig.empty())
.teleport(TeleportConfig.disabled())
.ui(UIConfig.defaults())
.worldGuard(wgConfig)
.playerSpawn(PlayerSpawnConfig.disabled())
.build();
// Save using new loader
try {
configLoader.save(config);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save default mine config: " + e.getMessage(), e);
}
}
/**
* Loads WorldGuard configuration from defaults.yml.
*/
private WorldGuardConfig loadWorldGuardDefaults() {
Path defaultsPath = minesFolder.resolve("_defaults.yml");
if (!Files.exists(defaultsPath)) {
return WorldGuardConfig.disabled();
}
try {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
return WorldGuardConfig.disabled();
}
boolean enabled = wgSection.getBoolean("enabled", false);
if (!enabled) {
return WorldGuardConfig.disabled();
}
String template = wgSection.getString("region-template", "{mine_name}_{random_4}");
return WorldGuardConfig.builder()
.enabled(true)
.template(template)
.owners(wgSection.getStringList("owners"))
.members(wgSection.getStringList("members"))
.flags(wgSection.getStringList("flags"))
.protectOnCreate(wgSection.getBoolean("protect-on-create", true))
.build();
} catch (Exception e) {
return WorldGuardConfig.disabled();
}
configWriter.createDefaultConfig(name, corner1, corner2);
}
public MineConfig loadConfig(String name) throws IOException, ConfigLoader.ConfigLoadException {
@ -138,64 +48,16 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
}
}
/**
* Saves a mine configuration to file.
*
* @param config the configuration to save
* @throws ConfigLoader.ConfigLoadException if saving fails
*/
public void saveConfig(MineConfig config) throws ConfigLoader.ConfigLoadException {
configLoader.save(config);
}
/**
* Saves a mine configuration to file by name.
*
* @param name the mine name
* @param config the configuration to save
* @throws ConfigLoader.ConfigLoadException if saving fails
*/
public void saveConfig(String name, MineConfig config) throws ConfigLoader.ConfigLoadException {
// Ensure the config has the correct name
MineConfig configToSave = config;
if (!name.equals(config.region().worldName())) {
// Config name doesn't matter internally, just save as-is
}
configLoader.save(config);
}
/**
* Saves mask positions for mask fill mode.
*/
public void saveMaskPositions(String name, BlockKey markerMaterial, java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
MineConfig config = loadConfig(name);
// Build new mask config
Map<String, Boolean> posMap = new HashMap<>();
for (Location loc : positions) {
posMap.put(LocationParser.format(loc), true);
}
BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(markerMaterial, posMap);
// Create new block config with mask
BlockConfig newBlocks = new BlockConfig(
config.blocks().weights(),
FillMode.MASK,
mask
);
// Build updated config
MineConfig updated = MineConfig.builder(name)
.region(config.region())
.blocks(newBlocks)
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
configLoader.save(updated);
public void saveMaskPositions(String name, BlockKey markerMaterial,
java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
configWriter.saveMaskPositions(name, markerMaterial, positions);
}
}

View file

@ -1,13 +1,17 @@
package dev.loki.lomines.core.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.model.MineConfig;
import org.bukkit.Location;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
@ -84,6 +88,6 @@ public final class MineRepository {
stopAndRemove(name);
MineConfig config = fileManager.loadConfig(name);
Mine mine = createAndStart(name, config);
plugin.loLogger().info("Reloaded mine: " + name);
plugin.getLogger().info("Reloaded mine: " + name);
}
}

View file

@ -1,10 +1,12 @@
package dev.loki.lomines.core.service;
package dev.loki.lomines.core.service.mask;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.core.service.MineFileManager;
import dev.loki.lomines.core.service.MineRepository;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.util.ValidationUtils;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.geo.Cuboid;
import dev.loki.lomines.util.selection.MaskScanner;
import org.bukkit.Location;
import org.bukkit.Material;

View file

@ -0,0 +1,129 @@
package dev.loki.lomines.core.service.write;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import dev.loki.lomines.util.location.geo.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public final class MineConfigWriter {
private final Path minesFolder;
private final ConfigLoader configLoader;
public MineConfigWriter(Path minesFolder, ConfigLoader configLoader) {
this.minesFolder = minesFolder;
this.configLoader = configLoader;
}
public void createDefaultConfig(String name, Location corner1, Location corner2) throws IOException {
Location loc1 = corner1 != null ? corner1 : new Location(
org.bukkit.Bukkit.getWorlds().get(0), 0, 64, 0);
Location loc2 = corner2 != null ? corner2 : new Location(
org.bukkit.Bukkit.getWorlds().get(0), 10, 74, 10);
Cuboid cuboid = new Cuboid(loc1, loc2);
RegionConfig region = RegionConfig.single(cuboid);
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 1.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
WorldGuardConfig wgConfig = loadWorldGuardDefaults();
MineConfig config = MineConfig.builder(name)
.region(region)
.blocks(blocks)
.reset(ResetConfig.defaults())
.rewards(RewardConfig.empty())
.teleport(TeleportConfig.disabled())
.ui(UIConfig.defaults())
.worldGuard(wgConfig)
.playerSpawn(PlayerSpawnConfig.disabled())
.build();
try {
configLoader.save(config);
} catch (ConfigLoader.ConfigLoadException e) {
throw new IOException("Failed to save default mine config: " + e.getMessage(), e);
}
}
public void saveMaskPositions(String name, BlockKey markerMaterial,
java.util.List<Location> positions) throws IOException, ConfigLoader.ConfigLoadException {
MineConfig config = configLoader.load(name);
Map<String, Boolean> posMap = new HashMap<>();
for (Location loc : positions) {
posMap.put(LocationParser.format(loc), true);
}
BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(markerMaterial, posMap);
BlockConfig newBlocks = new BlockConfig(
config.blocks().weights(),
FillMode.MASK,
mask
);
MineConfig updated = MineConfig.builder(name)
.region(config.region())
.blocks(newBlocks)
.reset(config.reset())
.rewards(config.rewards())
.teleport(config.teleport())
.ui(config.ui())
.worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build();
configLoader.save(updated);
}
private WorldGuardConfig loadWorldGuardDefaults() {
Path defaultsPath = minesFolder.resolve("_defaults.yml");
if (!Files.exists(defaultsPath)) {
return WorldGuardConfig.disabled();
}
try {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
ConfigurationSection wgSection = yaml.getConfigurationSection("worldguard");
if (wgSection == null) {
return WorldGuardConfig.disabled();
}
boolean enabled = wgSection.getBoolean("enabled", false);
if (!enabled) {
return WorldGuardConfig.disabled();
}
String template = wgSection.getString("region-template", "{mine_name}_{random_4}");
return WorldGuardConfig.builder()
.enabled(true)
.template(template)
.owners(wgSection.getStringList("owners"))
.members(wgSection.getStringList("members"))
.flags(wgSection.getStringList("flags"))
.protectOnCreate(wgSection.getBoolean("protect-on-create", true))
.build();
} catch (Exception e) {
return WorldGuardConfig.disabled();
}
}
}

View file

@ -8,24 +8,18 @@ import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader;
import dev.loki.lomines.data.config.loader.system.ResetConfigLoader;
import dev.loki.lomines.data.config.loader.system.UIConfigLoader;
import dev.loki.lomines.data.config.model.MineConfig;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Clean config loader for section-based configuration.
* Supports YAML inheritance from defaults.
* Delegates to specialized loaders for each section.
*/
public final class ConfigLoader {
private final Path dataFolder;
private YamlConfiguration defaults;
private final DefaultsMerger defaultsMerger;
// Section loaders
private RegionConfigLoader regionLoader;
private BlockConfigLoader blockLoader;
private ResetConfigLoader resetLoader;
@ -37,69 +31,29 @@ public final class ConfigLoader {
public ConfigLoader(Path dataFolder) {
this.dataFolder = dataFolder;
loadDefaults();
this.defaultsMerger = new DefaultsMerger(dataFolder);
initLoaders();
}
private void loadDefaults() {
Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml");
defaults = new YamlConfiguration();
if (Files.exists(defaultsPath)) {
defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
} else {
setDefaults(defaults);
try {
Files.createDirectories(defaultsPath.getParent());
defaults.save(defaultsPath.toFile());
} catch (IOException e) {
// Ignore, use in-memory defaults
}
}
}
private void setDefaults(YamlConfiguration yaml) {
yaml.set("reset.interval", "5m");
yaml.set("reset.percent-trigger", 10.0);
yaml.set("reset.percent-enabled", false);
yaml.set("reset.commands", List.of());
yaml.set("reset.broadcast", "");
yaml.set("ui.actionbar.enabled", true);
yaml.set("ui.actionbar.format", "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})");
yaml.set("ui.actionbar.range", 50.0);
yaml.set("ui.timer-format", "mm:ss");
yaml.set("teleport.enabled", false);
yaml.set("rewards", List.of());
worldGuardLoader.setDefaults(yaml);
playerSpawnLoader.setDefaults(yaml);
}
private void initLoaders() {
regionLoader = new RegionConfigLoader();
blockLoader = new BlockConfigLoader();
resetLoader = new ResetConfigLoader(defaults);
resetLoader = new ResetConfigLoader(defaultsMerger.getDefaults());
rewardLoader = new RewardConfigLoader();
teleportLoader = new TeleportConfigLoader();
uiLoader = new UIConfigLoader(defaults);
worldGuardLoader = new WorldGuardConfigLoader(defaults);
uiLoader = new UIConfigLoader(defaultsMerger.getDefaults());
worldGuardLoader = new WorldGuardConfigLoader(defaultsMerger.getDefaults());
playerSpawnLoader = new PlayerSpawnConfigLoader();
}
/**
* Load a mine configuration from file.
*/
public MineConfig load(String mineName) throws ConfigLoadException {
Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml");
if (!Files.exists(configPath)) {
if (!Files.exists(dataFolder.resolve("mines").resolve(mineName + ".yml"))) {
throw new ConfigLoadException("Mine not found: " + mineName);
}
Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml");
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configPath.toFile());
mergeDefaults(yaml);
defaultsMerger.mergeDefaults(yaml);
try {
return MineConfig.builder(mineName)
@ -117,11 +71,7 @@ public final class ConfigLoader {
}
}
/**
* Save a mine configuration to file.
*/
public void save(MineConfig config) throws ConfigLoadException {
Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml");
YamlConfiguration yaml = new YamlConfiguration();
regionLoader.save(yaml, config.region());
@ -134,6 +84,7 @@ public final class ConfigLoader {
playerSpawnLoader.save(yaml, config.playerSpawn());
try {
Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml");
Files.createDirectories(configPath.getParent());
yaml.save(configPath.toFile());
} catch (IOException e) {
@ -141,17 +92,6 @@ public final class ConfigLoader {
}
}
private void mergeDefaults(YamlConfiguration yaml) {
for (String key : defaults.getKeys(true)) {
if (!yaml.contains(key)) {
yaml.set(key, defaults.get(key));
}
}
}
/**
* Exception for config loading errors.
*/
public static class ConfigLoadException extends Exception {
public ConfigLoadException(String message) {
super(message);

View file

@ -0,0 +1,71 @@
package dev.loki.lomines.data.config;
import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class DefaultsMerger {
private final Path dataFolder;
private YamlConfiguration defaults;
public DefaultsMerger(Path dataFolder) {
this.dataFolder = dataFolder;
loadDefaults();
}
public YamlConfiguration getDefaults() {
return defaults;
}
private void loadDefaults() {
Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml");
defaults = new YamlConfiguration();
if (Files.exists(defaultsPath)) {
defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
} else {
setDefaults(defaults);
try {
Files.createDirectories(defaultsPath.getParent());
defaults.save(defaultsPath.toFile());
} catch (IOException e) {
// Ignore, use in-memory defaults
}
}
}
private void setDefaults(YamlConfiguration yaml) {
yaml.set("reset.interval", "5m");
yaml.set("reset.percent-trigger", 10.0);
yaml.set("reset.percent-enabled", false);
yaml.set("reset.commands", List.of());
yaml.set("reset.broadcast", "");
yaml.set("ui.actionbar.enabled", true);
yaml.set("ui.actionbar.format", "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})");
yaml.set("ui.actionbar.range", 50.0);
yaml.set("ui.timer-format", "mm:ss");
yaml.set("teleport.enabled", false);
yaml.set("rewards", List.of());
WorldGuardConfigLoader wgLoader = new WorldGuardConfigLoader(new YamlConfiguration());
wgLoader.setDefaults(yaml);
PlayerSpawnConfigLoader psLoader = new PlayerSpawnConfigLoader();
psLoader.setDefaults(yaml);
}
public void mergeDefaults(YamlConfiguration yaml) {
for (String key : defaults.getKeys(true)) {
if (!yaml.contains(key)) {
yaml.set(key, defaults.get(key));
}
}
}
}

View file

@ -32,7 +32,9 @@ public final class BlockConfigLoader {
Map<BlockKey, Double> weights = new HashMap<>();
for (String key : contents.getKeys(false)) {
double weight = contents.getDouble(key);
if (weight <= 0) continue;
if (weight <= 0) {
continue;
}
weights.put(BlockKey.deserialize(key), weight);
}

View file

@ -66,7 +66,9 @@ public final class RegionConfigLoader {
break;
}
}
if (!hasMore) break;
if (!hasMore) {
break;
}
}
}
}

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.data.config.loader.region;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
@ -38,9 +38,6 @@ public final class WorldGuardConfigLoader {
return WorldGuardConfig.disabled();
}
String template = yaml.getString("worldguard.region-template",
defaults.getString("worldguard.region-template", "{mine_name}_{random_4}"));
List<String> owners = yaml.getStringList("worldguard.owners");
if (owners.isEmpty()) {
owners = defaults.getStringList("worldguard.owners");
@ -63,9 +60,12 @@ public final class WorldGuardConfigLoader {
boolean protect = yaml.getBoolean("worldguard.protect-on-create",
defaults.getBoolean("worldguard.protect-on-create", true));
String tmpl = yaml.getString("worldguard.region-template",
defaults.getString("worldguard.region-template", "{mine_name}_{random_4}"));
return WorldGuardConfig.builder()
.enabled(true)
.template(template)
.template(tmpl)
.owners(owners)
.members(members)
.flags(flags)

View file

@ -36,9 +36,6 @@ public final class RewardConfigLoader {
}
private RewardEntry parseRewardEntry(Map<?, ?> map) {
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
List<BlockKey> blocks = new ArrayList<>();
List<?> blockList = (List<?>) map.get("blocks");
if (blockList != null) {
@ -69,6 +66,8 @@ public final class RewardConfigLoader {
Object preventDropsObj = map.get("prevent-drops");
boolean preventDrops = preventDropsObj instanceof Boolean b && b;
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
return new RewardEntry(blocks, chance, items, commands, preventDrops);
}

View file

@ -0,0 +1,64 @@
package dev.loki.lomines.data.config.model;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import org.bukkit.Location;
import java.util.Objects;
import java.util.Optional;
public record MineConfig(
String name,
RegionConfig region,
BlockConfig blocks,
ResetConfig reset,
RewardConfig rewards,
TeleportConfig teleport,
UIConfig ui,
WorldGuardConfig worldGuard,
PlayerSpawnConfig playerSpawn
) {
public MineConfig {
Objects.requireNonNull(name, "Mine name cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Mine name cannot be blank");
}
name = name.toLowerCase().trim();
Objects.requireNonNull(region, "Region config cannot be null");
Objects.requireNonNull(blocks, "Block config cannot be null");
Objects.requireNonNull(reset, "Reset config cannot be null");
Objects.requireNonNull(rewards, "Reward config cannot be null");
Objects.requireNonNull(teleport, "Teleport config cannot be null");
Objects.requireNonNull(ui, "UI config cannot be null");
if (playerSpawn == null) {
playerSpawn = PlayerSpawnConfig.disabled();
}
}
public static MineConfigDefaults.Builder builder(String name) {
return new MineConfigDefaults.Builder(name);
}
public int volume() {
return region.totalVolume();
}
public String worldName() {
return region.worldName();
}
public Optional<Location> getSpawnForStuckPlayer() {
return playerSpawn.enabled()
? playerSpawn.getLocation()
: teleport.getLocation();
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config;
package dev.loki.lomines.data.config.model;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.region.RegionConfig;
@ -7,58 +7,13 @@ import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import org.bukkit.Location;
public final class MineConfigDefaults {
import java.util.Objects;
import java.util.Optional;
/**
* Clean, type-safe, section-based mine configuration.
* Proper separation of concerns: each section handles one aspect.
*/
public record MineConfig(
String name,
RegionConfig region,
BlockConfig blocks,
ResetConfig reset,
RewardConfig rewards,
TeleportConfig teleport,
UIConfig ui,
WorldGuardConfig worldGuard,
PlayerSpawnConfig playerSpawn
) {
public MineConfig {
Objects.requireNonNull(name, "Mine name cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Mine name cannot be blank");
}
name = name.toLowerCase().trim();
Objects.requireNonNull(region, "Region config cannot be null");
Objects.requireNonNull(blocks, "Block config cannot be null");
Objects.requireNonNull(reset, "Reset config cannot be null");
Objects.requireNonNull(rewards, "Reward config cannot be null");
Objects.requireNonNull(teleport, "Teleport config cannot be null");
Objects.requireNonNull(ui, "UI config cannot be null");
// worldGuard and playerSpawn can be null (disabled by default)
if (playerSpawn == null) {
playerSpawn = PlayerSpawnConfig.disabled();
}
private MineConfigDefaults() {
}
/**
* Creates builder for fluent construction.
*/
public static Builder builder(String name) {
return new Builder(name);
}
/**
* Creates config with sensible defaults.
*/
public static MineConfig defaults(String name, RegionConfig region, BlockConfig blocks) {
return new MineConfig(
name,
@ -73,32 +28,6 @@ public record MineConfig(
);
}
/**
* Total volume of the mine (for progress calculation).
*/
public int volume() {
return region.totalVolume();
}
/**
* World name where the mine is located.
*/
public String worldName() {
return region.worldName();
}
/**
* Returns the spawn location for stuck players.
* If playerSpawn is not set, falls back to teleport location.
*/
public Optional<Location> getSpawnForStuckPlayer() {
return playerSpawn.enabled()
? playerSpawn.getLocation()
: teleport.getLocation();
}
// --- Builder ---
public static class Builder {
private final String name;
private RegionConfig region;
@ -110,7 +39,7 @@ public record MineConfig(
private WorldGuardConfig worldGuard = WorldGuardConfig.disabled();
private PlayerSpawnConfig playerSpawn = PlayerSpawnConfig.disabled();
private Builder(String name) {
public Builder(String name) {
this.name = name;
}

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.data.config.region;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.geo.Cuboid;
import org.bukkit.Location;
import java.util.Collections;

View file

@ -3,9 +3,6 @@ package dev.loki.lomines.data.config.reset;
import java.time.Duration;
import java.util.List;
/**
* Configuration for mine reset behavior.
*/
public record ResetConfig(
Duration interval,
double percentTrigger,
@ -14,34 +11,16 @@ public record ResetConfig(
String broadcastMessage
) {
public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5);
public static final double DEFAULT_PERCENT_TRIGGER = 10.0;
public ResetConfig {
// Normalize and validate interval
if (interval == null || interval.isZero() || interval.isNegative()) {
interval = DEFAULT_INTERVAL;
}
// Ensure interval is reasonable (1 second to 24 hours)
if (interval.getSeconds() < 1) {
interval = Duration.ofSeconds(1);
} else if (interval.getSeconds() > 86400) {
interval = Duration.ofHours(24);
}
// Validate percent trigger
if (percentTrigger < 0 || percentTrigger > 100) {
percentTrigger = DEFAULT_PERCENT_TRIGGER;
}
interval = ResetConfigValidator.validateInterval(interval);
percentTrigger = ResetConfigValidator.validatePercentTrigger(percentTrigger);
commands = commands != null ? List.copyOf(commands) : List.of();
broadcastMessage = broadcastMessage != null ? broadcastMessage : "";
}
/**
* Default config: 5 minutes, 10% trigger disabled.
*/
public static final Duration DEFAULT_INTERVAL = ResetConfigValidator.DEFAULT_INTERVAL;
public static final double DEFAULT_PERCENT_TRIGGER = ResetConfigValidator.DEFAULT_PERCENT_TRIGGER;
public static ResetConfig defaults() {
return new ResetConfig(
DEFAULT_INTERVAL,
@ -52,23 +31,14 @@ public record ResetConfig(
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
/**
* Interval in ticks (20 ticks = 1 second).
*/
public long intervalTicks() {
return interval.getSeconds() * 20;
}
/**
* Human-readable interval string.
*/
public String intervalDisplay() {
long seconds = interval.getSeconds();
if (seconds < 60) {
@ -80,9 +50,6 @@ public record ResetConfig(
}
}
/**
* Whether reset should trigger based on percent mined.
*/
public boolean isPercentTriggerEnabled() {
return percentEnabled && percentTrigger > 0;
}
@ -94,38 +61,18 @@ public record ResetConfig(
private List<String> commands = List.of();
private String broadcastMessage = "";
private static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
// Assume seconds if no suffix
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
public Builder interval(Duration interval) {
this.interval = interval;
return this;
}
public Builder intervalTicks(long ticks) {
this.interval = Duration.ofSeconds(ticks / 20);
public Builder interval(String humanReadable) {
this.interval = ResetConfigValidator.parseDuration(humanReadable);
return this;
}
public Builder interval(String humanReadable) {
this.interval = parseDuration(humanReadable);
public Builder intervalTicks(long ticks) {
this.interval = Duration.ofSeconds(ticks / 20);
return this;
}

View file

@ -0,0 +1,50 @@
package dev.loki.lomines.data.config.reset;
import java.time.Duration;
public final class ResetConfigValidator {
public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5);
public static final double DEFAULT_PERCENT_TRIGGER = 10.0;
private ResetConfigValidator() {
}
public static Duration validateInterval(Duration interval) {
if (interval == null || interval.isZero() || interval.isNegative()) {
interval = DEFAULT_INTERVAL;
}
if (interval.getSeconds() < 1) {
interval = Duration.ofSeconds(1);
} else if (interval.getSeconds() > 86400) {
interval = Duration.ofHours(24);
}
return interval;
}
public static double validatePercentTrigger(double percentTrigger) {
if (percentTrigger < 0 || percentTrigger > 100) {
return DEFAULT_PERCENT_TRIGGER;
}
return percentTrigger;
}
public static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
}

View file

@ -1,8 +1,10 @@
package dev.loki.lomines.data.config.ui;
import dev.loki.lomines.util.format.ColorUtils;
import dev.loki.lomines.util.format.color.ColorUtils;
import net.kyori.adventure.text.Component;
import java.util.Locale;
/**
* Configuration for UI elements (action bar, messages).
*/
@ -16,6 +18,7 @@ public record UIConfig(
public static final String DEFAULT_ACTIONBAR_FORMAT = "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})";
public static final String DEFAULT_TIMER_FORMAT = "mm:ss";
public UIConfig {
hologram = hologram != null ? hologram : HologramConfig.disabled();
actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank()
@ -49,7 +52,7 @@ public record UIConfig(
public Component formatActionBar(String mineName, double percent, String timeStr, int blocks, int total) {
String parsed = actionBarFormat
.replace("{mine}", mineName)
.replace("{percent}", String.format("%.1f", percent))
.replace("{percent}", String.format(Locale.US, "%.1f", percent))
.replace("{time}", timeStr)
.replace("{blocks}", String.valueOf(blocks))
.replace("{total}", String.valueOf(total));

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.entity;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
@ -49,8 +49,12 @@ public record Reward(List<Material> materials, double chance, List<ItemStack> it
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Reward reward = (Reward) o;
return Double.compare(reward.chance, chance) == 0 &&
preventDrops == reward.preventDrops &&
@ -59,4 +63,8 @@ public record Reward(List<Material> materials, double chance, List<ItemStack> it
Objects.equals(commands, reward.commands);
}
@Override
public int hashCode() {
return Objects.hash(materials, chance, items, commands, preventDrops);
}
}

View file

@ -1,69 +1,29 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.configuration.file.YamlConfiguration;
import dev.loki.lomines.data.reward.entity.Reward;
import dev.loki.lomines.data.reward.parse.command.RewardCommandParser;
import dev.loki.lomines.data.reward.parse.item.RewardItemParser;
import dev.loki.lomines.data.reward.parse.item.RewardMaterialParser;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Parses reward configurations from YAML.
* Handles reward chance, materials, items, commands, and prevent-drops settings.
*/
public final class RewardParser {
public final class RewardEntryParser {
private final RewardItemParser itemParser;
private final RewardMaterialParser materialParser;
private final RewardCommandParser commandParser;
public RewardParser() {
public RewardEntryParser() {
this.itemParser = new RewardItemParser();
this.materialParser = new RewardMaterialParser();
this.commandParser = new RewardCommandParser();
}
public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
List<Reward> rewards = new ArrayList<>();
if (!yaml.contains("random-rewards")) {
return rewards;
}
List<?> rewardsList = yaml.getList("random-rewards");
if (rewardsList == null) {
return rewards;
}
for (int i = 0; i < rewardsList.size(); i++) {
Object rewardObj = rewardsList.get(i);
if (!(rewardObj instanceof Map)) {
throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
Map<String, Object> rewardMap = (Map<String, Object>) rewardObj;
try {
Reward reward = parseReward(rewardMap);
rewards.add(reward);
} catch (ConfigParseException e) {
throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(),
e
);
}
}
return rewards;
}
private Reward parseReward(Map<String, Object> rewardMap) throws ConfigParseException {
public Reward parseReward(Map<String, Object> rewardMap) throws ConfigParseException {
double chance = parseRewardField(rewardMap, "chance", Double.class);
boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false);
@ -75,7 +35,7 @@ public final class RewardParser {
}
@SuppressWarnings("unchecked")
private <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type)
public <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type)
throws ConfigParseException {
if (!map.containsKey(key)) {
throw new ConfigParseException("Missing required '" + key + "' field in reward");
@ -96,7 +56,7 @@ public final class RewardParser {
}
@SuppressWarnings("unchecked")
private <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type, T defaultValue) {
public <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type, T defaultValue) {
if (!map.containsKey(key)) {
return defaultValue;
}
@ -107,7 +67,7 @@ public final class RewardParser {
return defaultValue;
}
private Double parseDoubleValue(String key, Object value) throws ConfigParseException {
public Double parseDoubleValue(String key, Object value) throws ConfigParseException {
try {
if (value instanceof Number) {
return ((Number) value).doubleValue();
@ -124,7 +84,7 @@ public final class RewardParser {
}
}
private List<ItemStack> parseRewardItems(Map<String, Object> rewardMap)
public List<ItemStack> parseRewardItems(Map<String, Object> rewardMap)
throws ConfigParseException {
List<ItemStack> items = new ArrayList<>();
if (!rewardMap.containsKey("items")) {

View file

@ -0,0 +1,57 @@
package dev.loki.lomines.data.reward.parse;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.data.reward.entity.Reward;
import org.bukkit.configuration.file.YamlConfiguration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public final class RewardParser {
private final RewardEntryParser entryParser;
public RewardParser() {
this.entryParser = new RewardEntryParser();
}
public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
List<Reward> rewards = new ArrayList<>();
if (!yaml.contains("random-rewards")) {
return rewards;
}
List<?> rewardsList = yaml.getList("random-rewards");
if (rewardsList == null) {
return rewards;
}
for (int i = 0; i < rewardsList.size(); i++) {
Object rewardObj = rewardsList.get(i);
if (!(rewardObj instanceof Map)) {
throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
Map<String, Object> rewardMap = (Map<String, Object>) rewardObj;
try {
Reward reward = entryParser.parseReward(rewardMap);
rewards.add(reward);
} catch (ConfigParseException e) {
throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(),
e
);
}
}
return rewards;
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse.command;
import dev.loki.lomines.data.config.parser.ConfigParseException;
@ -9,10 +9,10 @@ import java.util.Map;
/**
* Parses reward commands from configuration.
*/
final class RewardCommandParser {
public final class RewardCommandParser {
@SuppressWarnings("unchecked")
List<String> parseCommands(Map<String, Object> rewardMap) throws ConfigParseException {
public List<String> parseCommands(Map<String, Object> rewardMap) throws ConfigParseException {
List<String> commands = new ArrayList<>();
if (!rewardMap.containsKey("commands")) {
return commands;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse.item;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
@ -12,7 +12,7 @@ import java.util.Map;
* Parses ItemStack objects from YAML reward item configurations.
* Handles material type, amount, and basic item properties.
*/
final class RewardItemParser {
public final class RewardItemParser {
/**
* Parses a single reward item from a map configuration.
@ -22,7 +22,7 @@ final class RewardItemParser {
* @return the parsed ItemStack
* @throws ConfigParseException if the item configuration is invalid
*/
ItemStack parseItem(Map<String, Object> itemMap, int index) throws ConfigParseException {
public ItemStack parseItem(Map<String, Object> itemMap, int index) throws ConfigParseException {
if (!itemMap.containsKey("type")) {
throw new ConfigParseException("Missing required 'type' field in item at index " + index);
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse.item;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
@ -11,10 +11,10 @@ import java.util.Map;
/**
* Parses reward materials (blocks) from configuration.
*/
final class RewardMaterialParser {
public final class RewardMaterialParser {
@SuppressWarnings("unchecked")
List<Material> parseMaterials(Map<String, Object> rewardMap) throws ConfigParseException {
public List<Material> parseMaterials(Map<String, Object> rewardMap) throws ConfigParseException {
if (!rewardMap.containsKey("blocks")) {
throw new ConfigParseException("Missing required 'blocks' field in reward");
}

View file

@ -1,181 +0,0 @@
package dev.loki.lomines.data.stats;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages player statistics for all mines.
* Handles loading, saving, and auto-saving of player statistics.
*/
public final class StatsManager {
private final Map<UUID, PlayerStats> stats = new ConcurrentHashMap<>();
private final LoMinesPlugin plugin;
private final Path statsFile;
private final Leaderboard leaderboard;
private ScheduledTask saveTask;
public StatsManager(LoMinesPlugin plugin) {
this.plugin = plugin;
this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml");
this.leaderboard = new Leaderboard(this);
}
/**
* Loads statistics from file.
*/
public void load() throws IOException {
if (!Files.exists(statsFile)) {
plugin.loLogger().info("Stats file does not exist, starting with empty statistics");
return;
}
File file = statsFile.toFile();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
ConfigurationSection playersSection = yaml.getConfigurationSection("players");
if (playersSection == null) {
plugin.loLogger().info("No players section found in stats.yml");
return;
}
for (String uuidString : playersSection.getKeys(false)) {
try {
UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) {
continue;
}
PlayerStats playerStats = getOrCreate(playerId);
// Load total blocks
long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks);
// Load mine-specific stats
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) {
for (String mineName : minesSection.getKeys(false)) {
long mineBlocks = minesSection.getLong(mineName, 0);
playerStats.setMineBlocks(mineName, mineBlocks);
}
}
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid UUID in stats.yml: " + uuidString);
}
}
plugin.loLogger().info("Loaded statistics for " + stats.size() + " player(s)");
}
/**
* Saves statistics to file.
*/
public void save() throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerStats> entry : stats.entrySet()) {
UUID playerId = entry.getKey();
PlayerStats playerStats = entry.getValue();
String path = "players." + playerId.toString();
// Save total blocks
yaml.set(path + ".total", playerStats.getTotalBlocks());
// Save mine-specific stats
Map<String, Long> mineStats = playerStats.getMineStatsSnapshot();
if (!mineStats.isEmpty()) {
for (Map.Entry<String, Long> mineEntry : mineStats.entrySet()) {
yaml.set(path + ".mines." + mineEntry.getKey(), mineEntry.getValue());
}
}
}
// Ensure parent directory exists
if (!Files.exists(statsFile.getParent())) {
Files.createDirectories(statsFile.getParent());
}
yaml.save(statsFile.toFile());
}
/**
* Starts auto-save task.
*/
public void startAutoSave() {
// Auto-save every 5 minutes (20 ticks/second * 60 seconds * 5 minutes)
long interval = 20L * 60L * 5L;
saveTask = Scheduler.get(plugin).runTimerAsync(
() -> {
try {
save();
plugin.loLogger().info("Statistics auto-saved");
} catch (IOException e) {
plugin.loLogger().error("Failed to auto-save statistics: " + e.getMessage());
}
},
interval,
interval
);
}
/**
* Stops auto-save task.
*/
public void stopAutoSave() {
if (saveTask != null) {
saveTask.cancel();
saveTask = null;
}
}
/**
* Gets or creates player statistics.
*/
public PlayerStats getOrCreate(UUID playerId) {
return stats.computeIfAbsent(playerId, PlayerStats::new);
}
/**
* Increments block count for a player in a specific mine.
*/
public void incrementBlocks(UUID playerId, String mineName) {
PlayerStats playerStats = getOrCreate(playerId);
playerStats.incrementTotal();
playerStats.incrementMine(mineName);
// Invalidate leaderboard cache
leaderboard.invalidateCache();
}
/**
* Returns all player statistics.
*/
public Collection<PlayerStats> getAllStats() {
return Collections.unmodifiableCollection(stats.values());
}
/**
* Returns the leaderboard instance.
*/
public Leaderboard getLeaderboard() {
return leaderboard;
}
}

View file

@ -1,4 +1,6 @@
package dev.loki.lomines.data.stats;
package dev.loki.lomines.data.stats.model;
import dev.loki.lomines.data.stats.service.StatsManager;
import java.util.Comparator;
import java.util.List;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.stats;
package dev.loki.lomines.data.stats.model;
import java.util.Objects;
import java.util.UUID;
@ -16,12 +16,21 @@ public record LeaderboardEntry(UUID playerId, long count) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LeaderboardEntry that = (LeaderboardEntry) o;
return count == that.count && playerId.equals(that.playerId);
}
@Override
public int hashCode() {
return Objects.hash(playerId, count);
}
@Override
public String toString() {
return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}";

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.stats;
package dev.loki.lomines.data.stats.model;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

View file

@ -0,0 +1,72 @@
package dev.loki.lomines.data.stats.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.model.Leaderboard;
import dev.loki.lomines.data.stats.model.PlayerStats;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class StatsManager {
private final Map<UUID, PlayerStats> stats = new ConcurrentHashMap<>();
private final LoMinesPlugin plugin;
private final Leaderboard leaderboard;
private final StatsPersistence persistence;
public StatsManager(LoMinesPlugin plugin) {
this.plugin = plugin;
this.leaderboard = new Leaderboard(this);
this.persistence = new StatsPersistence(plugin, this);
}
public StatsPersistence getPersistence() {
return persistence;
}
public void load() throws IOException {
persistence.load(stats);
}
public void save() throws IOException {
persistence.save(stats);
}
public void startAutoSave() {
persistence.startAutoSave(() -> {
try {
save();
plugin.getLogger().info("Statistics auto-saved");
} catch (IOException e) {
plugin.getLogger().severe("Failed to auto-save statistics: " + e.getMessage());
}
});
}
public void stopAutoSave() {
persistence.stopAutoSave();
}
public PlayerStats getOrCreate(UUID playerId) {
return stats.computeIfAbsent(playerId, PlayerStats::new);
}
public void incrementBlocks(UUID playerId, String mineName) {
PlayerStats playerStats = getOrCreate(playerId);
playerStats.incrementTotal();
playerStats.incrementMine(mineName);
leaderboard.invalidateCache();
}
public Collection<PlayerStats> getAllStats() {
return Collections.unmodifiableCollection(stats.values());
}
public Leaderboard getLeaderboard() {
return leaderboard;
}
}

View file

@ -0,0 +1,110 @@
package dev.loki.lomines.data.stats.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.model.PlayerStats;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
public final class StatsPersistence {
private final LoMinesPlugin plugin;
private final Path statsFile;
private final StatsManager statsManager;
private BukkitTask saveTask;
public StatsPersistence(LoMinesPlugin plugin, StatsManager statsManager) {
this.plugin = plugin;
this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml");
this.statsManager = statsManager;
}
public void load(Map<UUID, PlayerStats> stats) throws IOException {
if (!Files.exists(statsFile)) {
plugin.getLogger().info("Stats file does not exist, starting with empty statistics");
return;
}
File file = statsFile.toFile();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
ConfigurationSection playersSection = yaml.getConfigurationSection("players");
if (playersSection == null) {
plugin.getLogger().info("No players section found in stats.yml");
return;
}
for (String uuidString : playersSection.getKeys(false)) {
try {
UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) {
continue;
}
PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new);
long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks);
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) {
for (String mineName : minesSection.getKeys(false)) {
long mineBlocks = minesSection.getLong(mineName, 0);
playerStats.setMineBlocks(mineName, mineBlocks);
}
}
} catch (IllegalArgumentException e) {
plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString);
}
}
plugin.getLogger().info("Loaded statistics for " + stats.size() + " player(s)");
}
public void save(Map<UUID, PlayerStats> stats) throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerStats> entry : stats.entrySet()) {
UUID playerId = entry.getKey();
PlayerStats playerStats = entry.getValue();
String path = "players." + playerId.toString();
yaml.set(path + ".total", playerStats.getTotalBlocks());
Map<String, Long> mineStats = playerStats.getMineStatsSnapshot();
if (!mineStats.isEmpty()) {
for (Map.Entry<String, Long> mineEntry : mineStats.entrySet()) {
yaml.set(path + ".mines." + mineEntry.getKey(), mineEntry.getValue());
}
}
}
if (!Files.exists(statsFile.getParent())) {
Files.createDirectories(statsFile.getParent());
}
yaml.save(statsFile.toFile());
}
public void startAutoSave(Runnable saveAction) {
long interval = 20L * 60L * 5L;
saveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, saveAction, interval, interval);
}
public void stopAutoSave() {
if (saveTask != null) {
saveTask.cancel();
saveTask = null;
}
}
}

View file

@ -34,12 +34,16 @@ public final class ItemStackFactory {
}
public static String loc(Location l) {
if (l == null || l.getWorld() == null) return "§7не задана";
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());
}
public static String trunc(String s, int max) {
if (s == null || s.isEmpty()) return "§7нет";
if (s == null || s.isEmpty()) {
return "§7нет";
}
return s.length() > max ? s.substring(0, max - 3) + "..." : s;
}

View file

@ -1,13 +0,0 @@
/**
* GUI editors for mine configuration.
* <p>
* This package contains graphical user interface implementations using LoAPI's InventoryGUI:
* <ul>
* <li>{@code MinesEditor} - List view of all mines</li>
* <li>{@code MineEditor} - Main menu for a specific mine</li>
* <li>{@code MineContentsEditor} - Editor for mine block contents</li>
* <li>{@code MineRewardsEditor} - List view of mine rewards</li>
* <li>{@code MineRewardEditor} - Editor for a specific reward</li>
* </ul>
*/
package dev.loki.lomines.gui;

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.gui.group;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mines;
import dev.loki.lomines.core.mine.registry.Mines;
import dev.loki.lomines.wand.group.GroupWandManager;
import dev.loki.lomines.wand.group.GroupWandSession;
import net.kyori.adventure.text.Component;
@ -99,7 +99,9 @@ public final class GroupCreateGui {
int skipped = 0;
for (int i = 0; i < 9; i++) {
if (!session.isSlotReady(i)) continue;
if (!session.isSlotReady(i)) {
continue;
}
String mineName = base + "_" + (i + 1);
try {
mines.create(mineName, session.getPos1(i), session.getPos2(i));
@ -109,7 +111,8 @@ public final class GroupCreateGui {
skipped++;
} catch (Exception e) {
player.sendMessage(Component.text("Ошибка " + mineName + ": " + e.getMessage(), NamedTextColor.RED));
plugin.loLogger().error("Group create failed for " + mineName, e);
plugin.getLogger().severe("Group create failed for " + mineName);
e.printStackTrace();
}
}

View file

@ -1,180 +0,0 @@
package dev.loki.lomines.gui.mine.edit.blocks;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.gui.common.ItemStackFactory;
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 dev.loki.lomines.gui.mine.holder.BlockMaterialSelectionGuiHolder;
import java.util.ArrayList;
import java.util.List;
/**
* GUI for selecting a material to add to mine blocks.
* Shows paginated list of available materials.
*/
public final class BlockMaterialSelectionGui {
private static final int SIZE = 54;
private static final String TITLE = "Выберите материал";
private static final int SLOT_BACK = 49;
private static final int SLOT_PREV = 45;
private static final int SLOT_NEXT = 53;
private static final int ITEMS_PER_PAGE = 45;
private static final List<Material> AVAILABLE_MATERIALS = new ArrayList<>();
static {
// Initialize with common mine materials
for (Material mat : Material.values()) {
if (mat.isBlock() && !mat.isAir() && mat.isSolid()) {
AVAILABLE_MATERIALS.add(mat);
}
}
AVAILABLE_MATERIALS.sort((a, b) -> a.name().compareTo(b.name()));
}
private BlockMaterialSelectionGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) {
open(plugin, player, mineName, 0);
}
public static void open(LoMinesPlugin plugin, Player player, String mineName, int page) {
BlockMaterialSelectionGuiHolder holder = new BlockMaterialSelectionGuiHolder(
player.getUniqueId(), mineName, page);
Inventory inv = Bukkit.createInventory(holder, SIZE,
TITLE + " (стр. " + (page + 1) + ")");
holder.setInventory(inv);
fill(inv, page);
player.openInventory(inv);
}
private static void fill(Inventory inv, int page) {
// Fill background
for (int i = 0; i < SIZE; i++) {
inv.setItem(i, ItemStackFactory.filler());
}
// Calculate range for this page
int start = page * ITEMS_PER_PAGE;
int end = Math.min(start + ITEMS_PER_PAGE, AVAILABLE_MATERIALS.size());
// Fill materials
for (int i = 0; i < ITEMS_PER_PAGE && (start + i) < end; i++) {
Material mat = AVAILABLE_MATERIALS.get(start + i);
inv.setItem(i, materialItem(mat));
}
// Navigation buttons
if (page > 0) {
inv.setItem(SLOT_PREV, prevPageItem());
}
if (end < AVAILABLE_MATERIALS.size()) {
inv.setItem(SLOT_NEXT, nextPageItem());
}
// Back button
inv.setItem(SLOT_BACK, backItem());
}
private static ItemStack materialItem(Material material) {
String name = formatMaterialName(material);
return ItemStackFactory.create(material, "§a§l" + name,
"§8───────────────",
"§7Нажмите для добавления",
"§7в конфигурацию шахты",
"",
"§8ID: §7" + material.name().toLowerCase()
);
}
private static String formatMaterialName(Material material) {
String name = material.name().toLowerCase().replace("_", " ");
StringBuilder result = new StringBuilder();
for (String word : name.split(" ")) {
if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1))
.append(" ");
}
}
return result.toString().trim();
}
private static ItemStack prevPageItem() {
return ItemStackFactory.create(Material.ARROW, "§e§l← Предыдущая",
"§8───────────────",
"§7Нажмите для перехода",
"§7на предыдущую страницу"
);
}
private static ItemStack nextPageItem() {
return ItemStackFactory.create(Material.ARROW, "§e§lСледующая →",
"§8───────────────",
"§7Нажмите для перехода",
"§7на следующую страницу"
);
}
private static ItemStack backItem() {
return ItemStackFactory.create(Material.BARRIER, "§c§lОтмена",
"§8───────────────",
"§7Вернуться без добавления"
);
}
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, int page, boolean leftClick) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
if (rawSlot == SLOT_BACK) {
BlocksGui.open(plugin, player, mineName);
return true;
}
if (rawSlot == SLOT_PREV && page > 0) {
open(plugin, player, mineName, page - 1);
return true;
}
if (rawSlot == SLOT_NEXT && (page + 1) * ITEMS_PER_PAGE < AVAILABLE_MATERIALS.size()) {
open(plugin, player, mineName, page + 1);
return true;
}
// Material selected
if (rawSlot < ITEMS_PER_PAGE && leftClick) {
int index = page * ITEMS_PER_PAGE + rawSlot;
if (index < AVAILABLE_MATERIALS.size()) {
Material selected = AVAILABLE_MATERIALS.get(index);
addBlockToMine(plugin, player, mineName, selected);
}
return true;
}
return true;
}
private static void addBlockToMine(LoMinesPlugin plugin, Player player,
String mineName, Material material) {
player.sendMessage(Component.text("§aДобавлен блок: §f" + material.name().toLowerCase()));
player.sendMessage(Component.text("§7Вес установлен: §f10% (настройте в редакторе)"));
BlocksGui.open(plugin, player, mineName);
}
public static Material getMaterialAtSlot(int slot, int page) {
int index = page * ITEMS_PER_PAGE + slot;
if (index >= 0 && index < AVAILABLE_MATERIALS.size()) {
return AVAILABLE_MATERIALS.get(index);
}
return null;
}
}

View file

@ -0,0 +1,79 @@
package dev.loki.lomines.gui.mine.edit.blocks.edit;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.gui.mine.edit.blocks.view.BlocksGui;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public final class BlockWeightEditor {
private BlockWeightEditor() {
}
public static void adjustBlockWeight(LoMinesPlugin plugin, Player player, String mineName,
BlockKey blockKey, boolean leftClick, boolean shiftClick,
boolean rightClick) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
return;
}
BlockConfig config = mine.getConfig().blocks();
Map<BlockKey, Double> weights = new java.util.HashMap<>(config.weights());
double currentWeight = weights.getOrDefault(blockKey, 0.0) * 100.0;
double delta = 0;
if (leftClick && shiftClick) {
delta = 1.0;
} else if (rightClick && shiftClick) {
delta = -1.0;
} else if (leftClick) {
delta = 5.0;
} else if (rightClick) {
delta = -5.0;
}
double newWeight = Math.max(0, Math.min(100, currentWeight + delta));
if (rightClick && shiftClick && newWeight <= 0.1) {
weights.remove(blockKey);
player.sendMessage(Component.text(
"\u00a7c\u0411\u043b\u043e\u043a \u00a7f" + blockKey.serialize() + " \u00a7c\u0443\u0434\u0430\u043b\u0435\u043d!"));
} else {
weights.put(blockKey, newWeight / 100.0);
player.sendMessage(Component.text(
"\u00a7a\u0412\u0435\u0441 \u00a7f" + blockKey.serialize() + " \u00a7a\u0438\u0437\u043c\u0435\u043d\u0435\u043d: \u00a7f"
+ String.format("%.1f%%", newWeight)));
}
BlocksGui.refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
public static BlockKey getBlockAtSlot(Inventory inv, int slot, LoMinesPlugin plugin, String mineName) {
if (slot < 0 || slot >= 36) {
return null;
}
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) {
return null;
}
BlockConfig config = mine.getConfig().blocks();
List<Map.Entry<BlockKey, Double>> sortedEntries = new ArrayList<>(config.weights().entrySet());
sortedEntries.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
if (slot < sortedEntries.size()) {
return sortedEntries.get(slot).getKey();
}
return null;
}
}

View file

@ -0,0 +1,106 @@
package dev.loki.lomines.gui.mine.edit.blocks.select;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.edit.blocks.view.BlocksGui;
import dev.loki.lomines.gui.mine.holder.edit.BlockMaterialSelectionGuiHolder;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.List;
public final class BlockMaterialSelectionGui {
private static final int SIZE = 54;
private static final String TITLE = "Выберите материал";
private static final int SLOT_BACK = 49;
private static final int SLOT_PREV = 45;
private static final int SLOT_NEXT = 53;
private static final int ITEMS_PER_PAGE = 45;
private BlockMaterialSelectionGui() {
}
public static void open(LoMinesPlugin plugin, Player player, String mineName) {
open(plugin, player, mineName, 0);
}
public static void open(LoMinesPlugin plugin, Player player, String mineName, int page) {
BlockMaterialSelectionGuiHolder holder = new BlockMaterialSelectionGuiHolder(
player.getUniqueId(), mineName, page);
Inventory inv = Bukkit.createInventory(holder, SIZE,
TITLE + " (стр. " + (page + 1) + ")");
holder.setInventory(inv);
fill(inv, page);
player.openInventory(inv);
}
private static void fill(Inventory inv, int page) {
List<Material> materials = BlockMaterialSelector.getAvailableMaterials();
for (int i = 0; i < SIZE; i++) {
inv.setItem(i, ItemStackFactory.filler());
}
int start = page * ITEMS_PER_PAGE;
int end = Math.min(start + ITEMS_PER_PAGE, materials.size());
for (int i = 0; i < ITEMS_PER_PAGE && (start + i) < end; i++) {
Material mat = materials.get(start + i);
inv.setItem(i, BlockMaterialSelector.materialItem(mat));
}
if (page > 0) {
inv.setItem(SLOT_PREV, BlockMaterialSelector.prevPageItem());
}
if (end < materials.size()) {
inv.setItem(SLOT_NEXT, BlockMaterialSelector.nextPageItem());
}
inv.setItem(SLOT_BACK, BlockMaterialSelector.backItem());
}
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, int page, boolean leftClick) {
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
List<Material> materials = BlockMaterialSelector.getAvailableMaterials();
if (rawSlot == SLOT_BACK) {
BlocksGui.open(plugin, player, mineName);
return true;
}
if (rawSlot == SLOT_PREV && page > 0) {
open(plugin, player, mineName, page - 1);
return true;
}
if (rawSlot == SLOT_NEXT && (page + 1) * ITEMS_PER_PAGE < materials.size()) {
open(plugin, player, mineName, page + 1);
return true;
}
if (rawSlot < ITEMS_PER_PAGE && leftClick) {
int index = page * ITEMS_PER_PAGE + rawSlot;
if (index < materials.size()) {
Material selected = materials.get(index);
player.sendMessage(Component.text("§aДобавлен блок: §f" + selected.name().toLowerCase()));
player.sendMessage(Component.text("§7Вес установлен: §f10% (настройте в редакторе)"));
BlocksGui.open(plugin, player, mineName);
}
return true;
}
return true;
}
public static Material getMaterialAtSlot(int slot, int page) {
return BlockMaterialSelector.getMaterialAtSlot(slot, page, ITEMS_PER_PAGE);
}
}

View file

@ -0,0 +1,83 @@
package dev.loki.lomines.gui.mine.edit.blocks.select;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
public final class BlockMaterialSelector {
private static final List<Material> AVAILABLE_MATERIALS = new ArrayList<>();
static {
for (Material mat : Material.values()) {
if (mat.isBlock() && !mat.isAir() && mat.isSolid()) {
AVAILABLE_MATERIALS.add(mat);
}
}
AVAILABLE_MATERIALS.sort((a, b) -> a.name().compareTo(b.name()));
}
private BlockMaterialSelector() {
}
public static List<Material> getAvailableMaterials() {
return AVAILABLE_MATERIALS;
}
public static Material getMaterialAtSlot(int slot, int page, int itemsPerPage) {
int index = page * itemsPerPage + slot;
if (index >= 0 && index < AVAILABLE_MATERIALS.size()) {
return AVAILABLE_MATERIALS.get(index);
}
return null;
}
public static String formatMaterialName(Material material) {
String name = material.name().toLowerCase().replace("_", " ");
StringBuilder result = new StringBuilder();
for (String word : name.split(" ")) {
if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1))
.append(" ");
}
}
return result.toString().trim();
}
public static ItemStack materialItem(Material material) {
String name = formatMaterialName(material);
return ItemStackFactory.create(material, "\u00a7a\u00a7l" + name,
"\u00a78\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
"\u00a77\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0434\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
"\u00a77\u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0448\u0430\u0445\u0442\u044b",
"",
"\u00a78ID: \u00a77" + material.name().toLowerCase());
}
public static ItemStack prevPageItem() {
return ItemStackFactory.create(Material.ARROW, "\u00a7e\u00a7l\u2190 \u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f",
"\u00a78\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
"\u00a77\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430",
"\u00a77\u043d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443"
);
}
public static ItemStack nextPageItem() {
return ItemStackFactory.create(Material.ARROW, "\u00a7e\u00a7l\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u2192",
"\u00a78\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
"\u00a77\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430",
"\u00a77\u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443"
);
}
public static ItemStack backItem() {
return ItemStackFactory.create(Material.BARRIER, "\u00a7c\u00a7l\u041e\u0442\u043c\u0435\u043d\u0430",
"\u00a78\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
"\u00a77\u0412\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f"
+ " \u0431\u0435\u0437 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f");
}
}

View file

@ -1,11 +1,13 @@
package dev.loki.lomines.gui.mine.edit.blocks;
package dev.loki.lomines.gui.mine.edit.blocks.view;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.BlocksGuiHolder;
import dev.loki.lomines.gui.mine.edit.blocks.edit.BlockWeightEditor;
import dev.loki.lomines.gui.mine.edit.blocks.select.BlockMaterialSelectionGui;
import dev.loki.lomines.gui.mine.holder.edit.BlocksGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@ -17,9 +19,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* GUI for editing block weights in a mine.
*/
public final class BlocksGui {
private static final int SIZE = 54;
@ -66,7 +65,9 @@ public final class BlocksGui {
int slot = 0;
for (Map.Entry<BlockKey, Double> entry : sortedEntries) {
if (slot >= 36) break;
if (slot >= 36) {
break;
}
inv.setItem(slot, BlocksGuiItems.blockItem(entry.getKey(), entry.getValue()));
slot++;
}
@ -75,10 +76,13 @@ public final class BlocksGui {
inv.setItem(SLOT_BACK, BlocksGuiItems.backItem());
}
@SuppressWarnings("checkstyle:ParameterNumber")
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, boolean leftClick, boolean shiftClick,
boolean rightClick, BlockKey clickedBlock) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
String mineName, boolean leftClick, boolean shiftClick,
boolean rightClick, BlockKey clickedBlock) {
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
if (rawSlot == SLOT_BACK) {
MineEditGui.open(plugin, player, mineName);
@ -91,61 +95,14 @@ public final class BlocksGui {
}
if (rawSlot < 36 && clickedBlock != null) {
adjustBlockWeight(plugin, player, mineName, clickedBlock, leftClick, shiftClick, rightClick);
BlockWeightEditor.adjustBlockWeight(plugin, player, mineName, clickedBlock, leftClick, shiftClick, rightClick);
return true;
}
return true;
}
private static void adjustBlockWeight(LoMinesPlugin plugin, Player player, String mineName,
BlockKey blockKey, boolean leftClick, boolean shiftClick,
boolean rightClick) {
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) return;
BlockConfig config = mine.getConfig().blocks();
Map<BlockKey, Double> weights = new java.util.HashMap<>(config.weights());
double currentWeight = weights.getOrDefault(blockKey, 0.0) * 100.0;
double delta = 0;
if (leftClick && shiftClick) {
delta = 1.0;
} else if (rightClick && shiftClick) {
delta = -1.0;
} else if (leftClick) {
delta = 5.0;
} else if (rightClick) {
delta = -5.0;
}
double newWeight = Math.max(0, Math.min(100, currentWeight + delta));
if (rightClick && shiftClick && newWeight <= 0.1) {
weights.remove(blockKey);
player.sendMessage(Component.text("§cБлок §f" + blockKey.serialize() + " §cудален!"));
} else {
weights.put(blockKey, newWeight / 100.0);
player.sendMessage(Component.text("§aВес §f" + blockKey.serialize() + " §aизменен: §f" +
String.format("%.1f%%", newWeight)));
}
refresh(player, player.getOpenInventory().getTopInventory(), mineName, plugin);
}
public static BlockKey getBlockAtSlot(Inventory inv, int slot, LoMinesPlugin plugin, String mineName) {
if (slot < 0 || slot >= 36) return null;
Mine mine = plugin.getMines().find(mineName).orElse(null);
if (mine == null) return null;
BlockConfig config = mine.getConfig().blocks();
List<Map.Entry<BlockKey, Double>> sortedEntries = new ArrayList<>(config.weights().entrySet());
sortedEntries.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
if (slot < sortedEntries.size()) {
return sortedEntries.get(slot).getKey();
}
return null;
return BlockWeightEditor.getBlockAtSlot(inv, slot, plugin, mineName);
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.gui.mine.edit.blocks;
package dev.loki.lomines.gui.mine.edit.blocks.view;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.gui.common.ItemStackFactory;

View file

@ -1,9 +1,9 @@
package dev.loki.lomines.gui.mine.edit.reset;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.ResetGuiHolder;
import dev.loki.lomines.gui.mine.holder.edit.reset.ResetGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@ -67,7 +67,9 @@ public final class ResetGui {
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, boolean leftClick, boolean rightClick,
boolean shiftClick) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
if (rawSlot == SLOT_BACK) {
MineEditGui.open(plugin, player, mineName);

View file

@ -1,10 +1,10 @@
package dev.loki.lomines.gui.mine.edit.rewards;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import dev.loki.lomines.gui.mine.holder.RewardsGuiHolder;
import dev.loki.lomines.gui.mine.holder.edit.rewards.RewardsGuiHolder;
import dev.loki.lomines.gui.mine.main.MineEditGui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@ -74,9 +74,9 @@ public final class RewardsGui {
int itemsCount = entry.items().size();
int commandsCount = entry.commands().size();
String blocksList = String.join(", ", entry.blocks().stream()
.limit(3)
.map(b -> b.serialize().toLowerCase())
.toList());
.limit(3)
.map(b -> b.serialize().toLowerCase())
.toList());
if (entry.blocks().size() > 3) {
blocksList += "...";
}
@ -116,7 +116,9 @@ public final class RewardsGui {
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot,
String mineName, boolean leftClick, boolean rightClick,
int rewardIndex) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
if (rawSlot == SLOT_BACK) {
MineEditGui.open(plugin, player, mineName);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,111 @@
package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
final class MineEditExtraItems {
private static final String SEP = "§8───────────────";
private static final String EDIT = "§e▸ Нажмите для редактирования";
private static final String SAVE = "§a▸ Нажмите для сохранения";
private static final String DEL = "§c▸ Нажмите для удаления";
private MineEditExtraItems() {
}
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Закрыть редактор",
"",
"§c▸ Нажмите чтобы закрыть"
);
}
}

View file

@ -1,13 +1,13 @@
package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.gui.confirm.ConfirmDeleteGui;
import dev.loki.lomines.gui.mine.edit.blocks.BlocksGui;
import dev.loki.lomines.gui.mine.edit.blocks.view.BlocksGui;
import dev.loki.lomines.gui.mine.edit.reset.ResetGui;
import dev.loki.lomines.gui.mine.edit.rewards.RewardsGui;
import dev.loki.lomines.gui.mine.holder.MineEditGuiHolder;
import dev.loki.lomines.gui.mine.holder.main.MineEditGuiHolder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
@ -62,21 +62,25 @@ public final class MineEditGui {
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());
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());
inv.setItem(SLOT_TELEPORT, MineEditExtraItems.teleportItem(config));
inv.setItem(SLOT_UI, MineEditExtraItems.uiItem(config));
inv.setItem(SLOT_SAVE, MineEditExtraItems.saveItem());
inv.setItem(SLOT_DELETE, MineEditExtraItems.deleteItem(mine.getName()));
inv.setItem(SLOT_INFO, MineEditExtraItems.infoItem(mine));
inv.setItem(SLOT_BACK, MineEditExtraItems.backItem());
}
public static boolean handleClick(LoMinesPlugin plugin, Player player, int rawSlot, String mineName) {
if (rawSlot < 0 || rawSlot >= SIZE) return false;
if (rawSlot < 0 || rawSlot >= SIZE) {
return false;
}
switch (rawSlot) {
case SLOT_BACK -> {
player.closeInventory();
@ -114,6 +118,7 @@ public final class MineEditGui {
case SLOT_INFO, SLOT_REGIONS -> {
return true;
}
default -> {}
}
return true;
}

View file

@ -1,7 +1,6 @@
package dev.loki.lomines.gui.mine.main;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.gui.common.ItemStackFactory;
import org.bukkit.Material;
@ -17,25 +16,24 @@ 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Нет"));
var w = config.blocks().weights();
if (!w.isEmpty()) {
lore.add("");
lore.add("§7Топ блоков:");
@ -78,10 +76,10 @@ final class MineEditItems {
}
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();
final var entries = config.rewards().entries();
final long items = entries.stream().flatMap(e -> e.items().stream()).count();
final long cmds = entries.stream().filter(e -> !e.commands().isEmpty()).count();
final long blockDrop = entries.stream().filter(RewardConfig.RewardEntry::preventVanillaDrops).count();
List<String> lore = new ArrayList<>();
lore.add(SEP);
lore.add("§7Настройки:");
@ -91,100 +89,13 @@ final class MineEditItems {
lore.add("§7Типы наград:");
lore.add(" §8• §fПредметов: §7" + items);
lore.add(" §8• §fКоманд: §7" + cmds);
if (blockDrop > 0) lore.add(" §8• §cБлокируют дроп: §7" + blockDrop);
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,209 +0,0 @@
package dev.loki.lomines.handler;
import dev.loki.lomines.LoMinesPlugin;
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;
import dev.loki.lomines.util.location.LocationParser;
import dev.lolib.scheduler.Scheduler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Handles mine reset operations.
* Ensures thread-safe reset execution and prevents concurrent resets.
*
* <p>Reset process:</p>
* <ol>
* <li>Check if reset is already running (atomic check)</li>
* <li>Fill all regions asynchronously via BlockSetter</li>
* <li>Wait for all regions to complete</li>
* <li>Execute post-reset actions in main thread</li>
* </ol>
*
* <p>Updated for new configuration system (v2).</p>
*/
public final class MineResetHandler {
private final Mine mine;
private final LoMinesPlugin plugin;
private final AtomicBoolean running = new AtomicBoolean(false);
public MineResetHandler(Mine mine, LoMinesPlugin plugin) {
this.mine = mine;
this.plugin = plugin;
}
/**
* Resets the mine by filling all regions with blocks.
* If a reset is already in progress, this call is ignored.
*
* @param silent If true, suppresses broadcast messages
*/
public void reset(boolean silent) {
if (!running.compareAndSet(false, true)) {
return;
}
try {
resetAllRegions(silent);
} catch (Exception e) {
running.set(false);
plugin.loLogger().error("Failed to reset mine " + mine.getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
/**
* Fills all regions asynchronously and coordinates completion.
*/
private void resetAllRegions(boolean silent) {
List<Cuboid> regions = mine.getRegions();
AtomicInteger completed = new AtomicInteger(0);
AtomicInteger totalBlocks = new AtomicInteger(0);
if (mine.getBlockSetter() == null) {
plugin.loLogger().warn("BlockSetter not initialized for mine " + mine.getName());
running.set(false);
return;
}
// Handle MASK fill mode
if (mine.getConfig().blocks().fillMode() == FillMode.MASK) {
List<Location> maskPositions = getMaskPositions();
if (maskPositions.isEmpty()) {
plugin.loLogger().warn("No mask positions found for mine " + mine.getName());
running.set(false);
return;
}
mine.getBlockSetter().fillAtLocations(maskPositions, placed ->
Scheduler.get(plugin).run(() -> onResetComplete(placed, silent)));
return;
}
// Handle CUBOID fill mode
for (Cuboid region : regions) {
mine.getBlockSetter().fill(region, blocksSet -> {
totalBlocks.addAndGet(blocksSet);
if (completed.incrementAndGet() == regions.size()) {
Scheduler.get(plugin).run(() -> {
onResetComplete(totalBlocks.get(), silent);
});
}
});
}
}
/**
* Gets mask positions from config.
*/
private List<Location> getMaskPositions() {
List<Location> positions = new ArrayList<>();
var mask = mine.getConfig().blocks().mask();
if (mask == null) {
return positions;
}
for (String posStr : mask.positions().keySet()) {
try {
Location loc = LocationParser.parse(posStr);
if (loc != null) {
positions.add(loc);
}
} catch (Exception e) {
plugin.loLogger().warn("Invalid mask position in mine " + mine.getName() + ": " + posStr);
}
}
return positions;
}
/**
* Called when all regions have been filled.
* Executes post-reset actions in the main thread.
*/
private void onResetComplete(int totalBlocks, boolean silent) {
mine.getBlocksAtomic().set(totalBlocks);
mine.getTicksAtomic().set(0);
executeResetCommands();
if (!silent) {
broadcastReset();
}
if (mine.getConfig().teleport().enabled()) {
teleportPlayers();
}
running.set(false);
}
/**
* Executes commands configured for reset.
*/
private void executeResetCommands() {
List<String> commands = mine.getConfig().reset().commands();
for (String command : commands) {
String parsed = command.replace("%mine%", mine.getName());
plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed);
}
}
/**
* Broadcasts reset message to players.
*/
private void broadcastReset() {
String message = mine.getConfig().reset().broadcastMessage();
if (message != null && !message.isEmpty()) {
String formatted = message.replace("%mine%", mine.getName());
Component component = MiniMessage.miniMessage().deserialize(formatted);
plugin.getServer().broadcast(component);
}
}
/**
* Teleports ONLY stuck players (those inside solid blocks) to a safe location.
* Uses player-spawn location if configured, otherwise uses teleport location.
* Only teleports if player is suffocating in a block, not all players in mine!
*/
private void teleportPlayers() {
// Get spawn location for stuck players (player-spawn config) or fall back to teleport location
var destOpt = mine.getConfig().getSpawnForStuckPlayer();
if (destOpt.isEmpty()) {
return;
}
Location dest = destOpt.get();
if (dest.getWorld() == null) {
return;
}
// Find a safe teleport location (max 3 blocks up to avoid teleporting too high)
Location safeDest = BlockUpdateUtil.findSafeTeleportLocation(dest, 3);
for (Player p : dest.getWorld().getPlayers()) {
// Only teleport if player is INSIDE the mine AND is suffocating in a solid block
if (mine.contains(p.getLocation()) && isPlayerStuckInBlock(p)) {
p.teleport(safeDest);
}
}
}
/**
* Checks if player is stuck inside a solid block (suffocating).
*/
private boolean isPlayerStuckInBlock(Player player) {
Location loc = player.getLocation();
var block = loc.getBlock();
// Check if block at player position is solid (not air/liquid)
return block.getType().isSolid() && !block.isLiquid();
}
}

View file

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

View file

@ -0,0 +1,125 @@
package dev.loki.lomines.handler.reset;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.util.location.geo.Cuboid;
import dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Bukkit;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/** Thread-safe mine reset with async fill and post-reset actions. */
public final class MineResetHandler {
private final Mine mine;
private final LoMinesPlugin plugin;
private final PlayerTeleportHandler teleportHandler;
private final AtomicBoolean running = new AtomicBoolean(false);
public MineResetHandler(Mine mine, LoMinesPlugin plugin) {
this.mine = mine;
this.plugin = plugin;
this.teleportHandler = new PlayerTeleportHandler(mine);
}
public void reset(boolean silent) {
if (!running.compareAndSet(false, true)) {
return;
}
try {
resetAllRegions(silent);
} catch (Exception e) {
running.set(false);
plugin.getLogger().severe("Failed to reset mine " + mine.getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
private void resetAllRegions(boolean silent) {
List<Cuboid> regions = mine.getRegions();
AtomicInteger completed = new AtomicInteger(0);
AtomicInteger totalBlocks = new AtomicInteger(0);
if (mine.getBlockSetter() == null) {
plugin.getLogger().warning("BlockSetter not initialized for mine " + mine.getName());
running.set(false);
return;
}
if (mine.getConfig().blocks().fillMode() == FillMode.MASK) {
List<Location> maskPositions = getMaskPositions();
if (maskPositions.isEmpty()) {
plugin.getLogger().warning("No mask positions found for mine " + mine.getName());
running.set(false);
return;
}
mine.getBlockSetter().fillAtLocations(maskPositions, placed ->
Bukkit.getScheduler().runTask(plugin, () -> onResetComplete(placed, silent)));
return;
}
for (Cuboid region : regions) {
mine.getBlockSetter().fill(region, blocksSet -> {
totalBlocks.addAndGet(blocksSet);
if (completed.incrementAndGet() == regions.size()) {
Bukkit.getScheduler().runTask(plugin, () -> onResetComplete(totalBlocks.get(), silent));
}
});
}
}
private List<Location> getMaskPositions() {
List<Location> positions = new ArrayList<>();
var mask = mine.getConfig().blocks().mask();
if (mask == null) {
return positions;
}
for (String posStr : mask.positions().keySet()) {
try {
Location loc = LocationParser.parse(posStr);
if (loc != null) {
positions.add(loc);
}
} catch (Exception e) {
plugin.getLogger().warning("Invalid mask position in mine " + mine.getName() + ": " + posStr);
}
}
return positions;
}
private void onResetComplete(int totalBlocks, boolean silent) {
mine.getBlocksAtomic().set(totalBlocks);
mine.getTicksAtomic().set(0);
executeResetCommands();
if (!silent) {
broadcastReset();
}
if (mine.getConfig().teleport().enabled()) {
teleportHandler.teleportStuckPlayers();
}
running.set(false);
}
private void executeResetCommands() {
List<String> commands = mine.getConfig().reset().commands();
for (String command : commands) {
String parsed = command.replace("%mine%", mine.getName());
plugin.getServer().dispatchCommand(plugin.getServer().getConsoleSender(), parsed);
}
}
private void broadcastReset() {
String message = mine.getConfig().reset().broadcastMessage();
if (message == null || message.isEmpty()) {
return;
}
String formatted = message.replace("%mine%", mine.getName());
Component component = MiniMessage.miniMessage().deserialize(formatted);
plugin.getServer().broadcast(component);
}
}

View file

@ -0,0 +1,40 @@
package dev.loki.lomines.handler.reset;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.util.block.SafeTeleportFinder;
import org.bukkit.Location;
import org.bukkit.entity.Player;
public final class PlayerTeleportHandler {
private final Mine mine;
public PlayerTeleportHandler(Mine mine) {
this.mine = mine;
}
public void teleportStuckPlayers() {
var destOpt = mine.getConfig().getSpawnForStuckPlayer();
if (destOpt.isEmpty()) {
return;
}
Location dest = destOpt.get();
if (dest.getWorld() == null) {
return;
}
Location safeDest = SafeTeleportFinder.findSafeTeleportLocation(dest, 3);
for (Player p : dest.getWorld().getPlayers()) {
if (mine.contains(p.getLocation()) && isPlayerStuckInBlock(p)) {
p.teleport(safeDest);
}
}
}
private boolean isPlayerStuckInBlock(Player player) {
Location loc = player.getLocation();
var block = loc.getBlock();
return block.getType().isSolid() && !block.isLiquid();
}
}

View file

@ -1,7 +1,7 @@
package dev.loki.lomines.handler;
package dev.loki.lomines.handler.reward;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.reward.RewardConfig;
import org.bukkit.Material;

View file

@ -1,8 +1,8 @@
package dev.loki.lomines.handler;
package dev.loki.lomines.handler.ui;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.util.location.geo.Cuboid;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.util.location.Cuboid;
import dev.lolib.utils.ActionBar;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@ -44,7 +44,7 @@ public final class ActionBarHandler {
center.getWorld().getNearbyEntities(center, range, range, range).stream()
.filter(entity -> entity instanceof Player)
.map(entity -> (Player) entity)
.forEach(player -> ActionBar.send(player, message));
.forEach(player -> player.sendActionBar(message));
}
/**
@ -73,7 +73,9 @@ public final class ActionBarHandler {
*/
private String formatTime() {
int remainingTicks = (int) (mine.getConfig().reset().intervalTicks() - mine.getTicks());
if (remainingTicks < 0) remainingTicks = 0;
if (remainingTicks < 0) {
remainingTicks = 0;
}
return mine.getConfig().ui().formatTimer(remainingTicks / 20);
}
}

View file

@ -33,13 +33,13 @@ public final class IntegrationManager {
try {
placeholderExpansion = new LoMinesPlaceholderExpansion(plugin);
if (placeholderExpansion.register()) {
plugin.loLogger().info("PlaceholderAPI integration enabled");
plugin.loLogger().info("Available placeholders: %lomines_mine_<name>_percent%, %lomines_player_blocksmined%, etc.");
plugin.getLogger().info("PlaceholderAPI integration enabled");
plugin.getLogger().info("Available placeholders: %lomines_mine_<name>_percent%, %lomines_player_blocksmined%, etc.");
} else {
plugin.loLogger().warn("Failed to register PlaceholderAPI expansion");
plugin.getLogger().warning("Failed to register PlaceholderAPI expansion");
}
} catch (Exception e) {
plugin.loLogger().warn("Failed to enable PlaceholderAPI integration: " + e.getMessage());
plugin.getLogger().warning("Failed to enable PlaceholderAPI integration: " + e.getMessage());
e.printStackTrace();
}
}
@ -47,7 +47,7 @@ public final class IntegrationManager {
private void checkWorldGuard() {
if (isPluginEnabled("WorldGuard")) {
plugin.loLogger().info("WorldGuard detected - auto-region creation enabled");
plugin.getLogger().info("WorldGuard detected - auto-region creation enabled");
}
}
@ -67,7 +67,7 @@ public final class IntegrationManager {
try {
placeholderExpansion.unregister();
} catch (Exception e) {
plugin.loLogger().warn("Failed to unregister PlaceholderAPI expansion: " + e.getMessage());
plugin.getLogger().warning("Failed to unregister PlaceholderAPI expansion: " + e.getMessage());
}
}
}

View file

@ -1,11 +1,10 @@
package dev.loki.lomines.integration.hologram;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.mine.Mine;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.data.config.ui.HologramConfig;
import dev.loki.lomines.integration.hologram.provider.DecentHologramsProvider;
import dev.loki.lomines.integration.hologram.provider.HolographicDisplaysProvider;
import dev.loki.lomines.util.format.ColorUtils;
import org.bukkit.Location;
import java.util.ArrayList;
@ -13,18 +12,16 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages holograms for all mines.
* Displays mine information above each mine region.
*/
public final class HologramManager {
private final LoMinesPlugin plugin;
private final HologramRenderer renderer;
private final Map<String, Integer> activeTasks = new ConcurrentHashMap<>();
private HologramProvider provider;
public HologramManager(LoMinesPlugin plugin) {
this.plugin = plugin;
this.renderer = new HologramRenderer();
initializeProvider();
}
@ -33,48 +30,49 @@ public final class HologramManager {
HolographicDisplaysProvider hdProvider = new HolographicDisplaysProvider();
if (hdProvider.isAvailable()) {
this.provider = hdProvider;
plugin.loLogger().info("Using HolographicDisplays for holograms");
plugin.getLogger().info("Using HolographicDisplays for holograms");
return;
}
DecentHologramsProvider dhProvider = new DecentHologramsProvider();
if (dhProvider.isAvailable()) {
this.provider = dhProvider;
plugin.loLogger().info("Using DecentHolograms for holograms");
plugin.getLogger().info("Using DecentHolograms for holograms");
return;
}
plugin.loLogger().warn("No hologram provider found. Install HolographicDisplays or DecentHolograms.");
plugin.getLogger().warning("No hologram provider found. Install HolographicDisplays or DecentHolograms.");
}
/**
* Creates a hologram for a mine if enabled in config.
*/
public void createMineHologram(Mine mine) {
if (provider == null || !provider.isAvailable()) return;
if (provider == null || !provider.isAvailable()) {
return;
}
HologramConfig config = mine.getConfig().ui().hologram();
if (!config.enabled()) return;
if (!config.enabled()) {
return;
}
String id = "lomines_" + mine.getName();
Location loc = calculateHologramLocation(mine, config.height());
List<String> lines = formatLines(config.format(), mine);
Location loc = renderer.calculateLocation(mine, config.height());
List<String> lines = renderer.formatLines(config.format(), mine);
provider.createHologram(id, loc, lines);
startUpdateTask(mine);
}
/**
* Updates a mine's hologram with current data.
*/
public void updateMineHologram(Mine mine) {
if (provider == null || !provider.isAvailable()) return;
if (provider == null || !provider.isAvailable()) {
return;
}
String id = "lomines_" + mine.getName();
if (!provider.exists(id)) return;
if (!provider.exists(id)) {
return;
}
HologramConfig config = mine.getConfig().ui().hologram();
List<String> lines = formatLines(config.format(), mine);
List<String> lines = renderer.formatLines(config.format(), mine);
provider.updateHologram(id, lines);
}
@ -82,8 +80,9 @@ public final class HologramManager {
* Deletes a mine's hologram.
*/
public void deleteMineHologram(String mineName) {
if (provider == null) return;
if (provider == null) {
return;
}
String id = "lomines_" + mineName;
provider.deleteHologram(id);
@ -97,7 +96,9 @@ public final class HologramManager {
* Toggles hologram for a mine.
*/
public boolean toggleHologram(Mine mine) {
if (provider == null || !provider.isAvailable()) return false;
if (provider == null || !provider.isAvailable()) {
return false;
}
String id = "lomines_" + mine.getName();
boolean currentlyEnabled = provider.exists(id);
@ -131,62 +132,6 @@ public final class HologramManager {
activeTasks.put(mineName, taskId);
}
private Location calculateHologramLocation(Mine mine, double height) {
if (mine.getRegions().isEmpty()) {
return null;
}
var region = mine.getRegions().get(0);
double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5;
double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5;
double centerY = region.getMaxY() + height;
return new Location(region.getWorld(), centerX, centerY, centerZ);
}
private List<String> formatLines(List<String> format, Mine mine) {
List<String> result = new ArrayList<>();
for (String line : format) {
String formatted = line
.replace("{mine}", mine.getName())
.replace("{percent}", String.format("%.1f", mine.getPercentFilled()))
.replace("{bar}", createProgressBar(mine.getPercentFilled()))
.replace("{time}", formatResetTime(mine));
// Convert &#RRGGBB and &codes to legacy for hologram plugins
result.add(ColorUtils.toLegacy(formatted));
}
return result;
}
private String createProgressBar(double percent) {
int filled = (int) Math.round(percent / 10.0);
int empty = 10 - filled;
// Use &#RRGGBB format which works with most hologram plugins
String bar = "&#00FF00" + // Green for filled
"█".repeat(Math.max(0, filled)) +
"&#808080" + // Gray for empty
"░".repeat(Math.max(0, empty));
return bar;
}
private String formatResetTime(Mine mine) {
int ticks = mine.getTicks();
int seconds = (int) ((6000 - ticks) / 20.0); // Assuming 5min = 6000 ticks
if (seconds < 0) seconds = 0;
int mins = seconds / 60;
int secs = seconds % 60;
return String.format("%02d:%02d", mins, secs);
}
/**
* Cleans up all holograms on shutdown.
*/
public void shutdown() {
for (String mineName : new ArrayList<>(activeTasks.keySet())) {
deleteMineHologram(mineName);

View file

@ -0,0 +1,53 @@
package dev.loki.lomines.integration.hologram;
import dev.loki.lomines.core.mine.model.Mine;
import dev.loki.lomines.util.format.color.ColorUtils;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public final class HologramRenderer {
public Location calculateLocation(Mine mine, double height) {
if (mine.getRegions().isEmpty()) {
return null;
}
var region = mine.getRegions().get(0);
double centerX = (region.getMinX() + region.getMaxX()) / 2.0 + 0.5;
double centerZ = (region.getMinZ() + region.getMaxZ()) / 2.0 + 0.5;
double centerY = region.getMaxY() + height;
return new Location(region.getWorld(), centerX, centerY, centerZ);
}
public List<String> formatLines(List<String> format, Mine mine) {
List<String> result = new ArrayList<>();
for (String line : format) {
String formatted = line
.replace("{mine}", mine.getName())
.replace("{percent}", String.format("%.1f", mine.getPercentFilled()))
.replace("{bar}", createProgressBar(mine.getPercentFilled()))
.replace("{time}", formatResetTime(mine));
result.add(ColorUtils.toLegacy(formatted));
}
return result;
}
private String createProgressBar(double percent) {
int filled = (int) Math.round(percent / 10.0);
int empty = 10 - filled;
return "&#00FF00" + "█".repeat(Math.max(0, filled))
+ "&#808080" + "░".repeat(Math.max(0, empty));
}
private String formatResetTime(Mine mine) {
int ticks = mine.getTicks();
int seconds = (int) ((6000 - ticks) / 20.0);
if (seconds < 0) {
seconds = 0;
}
int mins = seconds / 60;
int secs = seconds % 60;
return String.format("%02d:%02d", mins, secs);
}
}

View file

@ -28,8 +28,12 @@ public final class DecentHologramsProvider implements HologramProvider {
@Override
public boolean createHologram(String id, Location location, List<String> lines) {
if (!available) return false;
if (holograms.containsKey(id)) return false;
if (!available) {
return false;
}
if (holograms.containsKey(id)) {
return false;
}
holograms.put(id, location);
return true;
@ -37,30 +41,40 @@ public final class DecentHologramsProvider implements HologramProvider {
@Override
public boolean updateHologram(String id, List<String> lines) {
if (!available) return false;
if (!available) {
return false;
}
return holograms.containsKey(id);
}
@Override
public boolean moveHologram(String id, Location newLocation) {
if (!available) return false;
if (!available) {
return false;
}
if (!holograms.containsKey(id)) return false;
if (!holograms.containsKey(id)) {
return false;
}
holograms.put(id, newLocation);
return true;
}
@Override
public boolean deleteHologram(String id) {
if (!available) return false;
if (!available) {
return false;
}
return holograms.remove(id) != null;
}
@Override
public boolean exists(String id) {
if (!available) return false;
if (!available) {
return false;
}
return holograms.containsKey(id);
}

View file

@ -28,8 +28,12 @@ public final class HolographicDisplaysProvider implements HologramProvider {
@Override
public boolean createHologram(String id, Location location, List<String> lines) {
if (!available) return false;
if (holograms.containsKey(id)) return false;
if (!available) {
return false;
}
if (holograms.containsKey(id)) {
return false;
}
// Runtime bridge disabled when API jar is absent at compile time.
// We keep state so manager logic stays consistent.
holograms.put(id, location);
@ -38,30 +42,40 @@ public final class HolographicDisplaysProvider implements HologramProvider {
@Override
public boolean updateHologram(String id, List<String> lines) {
if (!available) return false;
if (!available) {
return false;
}
return holograms.containsKey(id);
}
@Override
public boolean moveHologram(String id, Location newLocation) {
if (!available) return false;
if (!available) {
return false;
}
if (!holograms.containsKey(id)) return false;
if (!holograms.containsKey(id)) {
return false;
}
holograms.put(id, newLocation);
return true;
}
@Override
public boolean deleteHologram(String id) {
if (!available) return false;
if (!available) {
return false;
}
return holograms.remove(id) != null;
}
@Override
public boolean exists(String id) {
if (!available) return false;
if (!available) {
return false;
}
return holograms.containsKey(id);
}

View file

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

View file

@ -0,0 +1,50 @@
package dev.loki.lomines.integration.worldguard.config;
import java.util.Random;
public final class RegionTemplateConfig {
private static final Random RANDOM = new Random();
public static final String DEFAULT_TEMPLATE = "{mine_name}_{random_4}";
private RegionTemplateConfig() {
}
public static String generateRegionName(String regionNameTemplate, String mineName) {
String result = regionNameTemplate;
result = result.replace("{mine_name}", mineName);
if (result.contains("{random_4}")) {
int randomNum = RANDOM.nextInt(10000);
result = result.replace("{random_4}", String.format("%04d", randomNum));
}
if (result.contains("{random_3}")) {
int randomNum = RANDOM.nextInt(1000);
result = result.replace("{random_3}", String.format("%03d", randomNum));
}
if (result.contains("{random_6}")) {
result = result.replace("{random_6}", generateRandomString(6));
}
return result.replaceAll("[^a-zA-Z0-9_-]", "_").toLowerCase();
}
private static String generateRandomString(int length) {
String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String resolveTemplate(String template, String mineName) {
if (template == null || template.isBlank()) {
return generateRegionName(DEFAULT_TEMPLATE, mineName);
}
return generateRegionName(template, mineName);
}
}

View file

@ -1,12 +1,7 @@
package dev.loki.lomines.integration.worldguard;
package dev.loki.lomines.integration.worldguard.config;
import java.util.List;
import java.util.Random;
/**
* Configuration for WorldGuard auto-region creation.
* Supports any WorldGuard flags, default is only block-break=allow.
*/
public record WorldGuardConfig(
boolean enabled,
String regionNameTemplate,
@ -16,30 +11,21 @@ public record WorldGuardConfig(
boolean protectOnCreate
) {
private static final Random RANDOM = new Random();
private static final String DEFAULT_TEMPLATE = "{mine_name}_{random_4}";
public WorldGuardConfig {
enabled = enabled;
regionNameTemplate = regionNameTemplate != null && !regionNameTemplate.isBlank()
? regionNameTemplate
: DEFAULT_TEMPLATE;
: RegionTemplateConfig.DEFAULT_TEMPLATE;
owners = owners != null ? List.copyOf(owners) : List.of();
members = members != null ? List.copyOf(members) : List.of();
// Default: only block-break=allow. User can add any flags they want.
flags = flags != null && !flags.isEmpty() ? List.copyOf(flags) : List.of("block-break=allow");
protectOnCreate = protectOnCreate;
}
/**
* Default configuration with auto-region enabled.
* Default flag: block-break=allow (everyone can mine).
* User can add any other flags.
*/
public static WorldGuardConfig defaults() {
return new WorldGuardConfig(
true,
DEFAULT_TEMPLATE,
RegionTemplateConfig.DEFAULT_TEMPLATE,
List.of(),
List.of(),
List.of("block-break=allow"),
@ -47,13 +33,10 @@ public record WorldGuardConfig(
);
}
/**
* Disabled configuration.
*/
public static WorldGuardConfig disabled() {
return new WorldGuardConfig(
false,
DEFAULT_TEMPLATE,
RegionTemplateConfig.DEFAULT_TEMPLATE,
List.of(),
List.of(),
List.of(),
@ -61,58 +44,17 @@ public record WorldGuardConfig(
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
/**
* Generates a region name from the template.
*
* @param mineName the name of the mine
* @return the generated region name
*/
public String generateRegionName(String mineName) {
String result = regionNameTemplate;
// Replace {mine_name}
result = result.replace("{mine_name}", mineName);
// Replace {random_4} with 4 random digits
if (result.contains("{random_4}")) {
int randomNum = RANDOM.nextInt(10000);
result = result.replace("{random_4}", String.format("%04d", randomNum));
}
// Replace {random_3} with 3 random digits
if (result.contains("{random_3}")) {
int randomNum = RANDOM.nextInt(1000);
result = result.replace("{random_3}", String.format("%03d", randomNum));
}
// Replace {random_6} with 6 random alphanumeric
if (result.contains("{random_6}")) {
result = result.replace("{random_6}", generateRandomString(6));
}
// Sanitize for WorldGuard (only alphanumeric, underscore, hyphen)
return result.replaceAll("[^a-zA-Z0-9_-]", "_").toLowerCase();
}
private String generateRandomString(int length) {
String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(RANDOM.nextInt(chars.length())));
}
return sb.toString();
return RegionTemplateConfig.resolveTemplate(regionNameTemplate, mineName);
}
public static class Builder {
private boolean enabled = true;
private String template = DEFAULT_TEMPLATE;
private String template = RegionTemplateConfig.DEFAULT_TEMPLATE;
private List<String> owners = List.of();
private List<String> members = List.of();
private List<String> flags = List.of("block-break=allow");
@ -152,4 +94,4 @@ public record WorldGuardConfig(
return new WorldGuardConfig(enabled, template, owners, members, flags, protectOnCreate);
}
}
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.integration.worldguard;
package dev.loki.lomines.integration.worldguard.flag;
import com.sk89q.worldguard.protection.flags.Flags;
import com.sk89q.worldguard.protection.flags.StateFlag;
@ -40,10 +40,10 @@ public final class WorldGuardFlagParser {
case "creeper-explosion" -> setStateFlag(region, Flags.CREEPER_EXPLOSION, value);
case "greeting" -> region.setFlag(Flags.GREET_MESSAGE, value);
case "farewell" -> region.setFlag(Flags.FAREWELL_MESSAGE, value);
default -> plugin.loLogger().warn("Unknown WorldGuard flag: " + flagName);
default -> plugin.getLogger().warning("Unknown WorldGuard flag: " + flagName);
}
} catch (Exception e) {
plugin.loLogger().warn("Failed to set flag " + flagName + ": " + e.getMessage());
plugin.getLogger().warning("Failed to set flag " + flagName + ": " + e.getMessage());
}
}

View file

@ -0,0 +1,43 @@
package dev.loki.lomines.integration.worldguard.region;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import dev.loki.lomines.data.config.model.MineConfig;
import dev.loki.lomines.integration.worldguard.flag.WorldGuardFlagParser;
import dev.loki.lomines.util.location.geo.Cuboid;
import java.util.List;
public final class RegionTemplateRenderer {
private final WorldGuardFlagParser flagParser;
public RegionTemplateRenderer(WorldGuardFlagParser flagParser) {
this.flagParser = flagParser;
}
public ProtectedCuboidRegion createRegionFromCuboids(String regionName, MineConfig config) {
List<Cuboid> regions = config.region().regions();
if (regions.isEmpty()) {
return null;
}
Cuboid first = regions.get(0);
BlockVector3 min = BlockVector3.at(first.getMinX(), first.getMinY(), first.getMinZ());
BlockVector3 max = BlockVector3.at(first.getMaxX(), first.getMaxY(), first.getMaxZ());
ProtectedCuboidRegion mainRegion = new ProtectedCuboidRegion(regionName, min, max);
if (regions.size() > 1) {
// Log warning about multiple regions - only first is fully protected
}
return mainRegion;
}
public void applyFlags(ProtectedCuboidRegion region, List<String> flags) {
for (String flagEntry : flags) {
flagParser.parseAndSetFlag(region, flagEntry);
}
}
}

View file

@ -1,7 +1,8 @@
package dev.loki.lomines.integration.worldguard;
package dev.loki.lomines.integration.worldguard.region;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import java.util.UUID;
@ -28,7 +29,7 @@ final class WorldGuardMemberHandler {
UUID uuid = UUID.fromString(owner.substring(5));
region.getOwners().addPlayer(uuid);
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid owner UUID: " + owner);
plugin.getLogger().warning("Invalid owner UUID: " + owner);
}
} else {
region.getOwners().addPlayer(owner);
@ -43,7 +44,7 @@ final class WorldGuardMemberHandler {
UUID uuid = UUID.fromString(member.substring(5));
region.getMembers().addPlayer(uuid);
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid member UUID: " + member);
plugin.getLogger().warning("Invalid member UUID: " + member);
}
} else {
region.getMembers().addPlayer(member);

Some files were not shown because too many files have changed in this diff Show more